输入数据处理
每个batch包含source和target两个矩阵,source矩阵第二维代表输入句子长度,句子长度不同时用0填充到相同的长度,同时在最后加一个全0列,比如:
[
[1,2,0],
[1,0,0]
]
第一个句子的长度为2,第二个句子的长度为1. target矩阵和source一样的处理方法。
target矩阵在求出embeding后的shape为[batch,sentence_len, embeding_zise], 需要在每个句子的开头加上一个起始符号0,同时将句子最后面一个0的emebding删除掉得到的形状还是[batch,sentence_len, embeding_zise]
测试阶段解码时,每一个step的计算方式:
先求出词的概率分布:
p(j) = p[0:MAX_VOC] (生成概率) 串联上 p[source sentence len](copy概率)。
源句子中位置j处的词如果属于[0:MAX_VOC], 则这个词实际概率为:p[s[j]]+p[MAX_VOC+j], 由生成概率和copy概率两部分组成 其中s[j]代表词的ID;然后将p[MAX_VOC+j]设置为0. 其他词的概率不用变。
本文只包含decoder网络
decoder数据准备
识别target句子中的每个词和源句子中哪些位置的词相同,在返回的cc矩阵里标注
返回值 的shape为 [source.shape[0], target.shape[1], source.shape[1]]
- source.shape[0]:为一个batch里,源句子数;
- target.shape[1] :每个目标句子的长度
- source.shape[1]:每个源句子的长度
代码如下:
def cc_martix(source, target):
cc = np.zeros((source.shape[0], target.shape[1], source.shape[1]), dtype='float32')
for k in xrange(source.shape[0]):
for j in xrange(target.shape[1]):
for i in xrange(source.shape[1]):
if (source[k, i] == target[k, j]) and (source[k, i] > 0):
cc[k][j][i] = 1.
return cc
对source和target用下面这个函数处理一下。将data中大于config[‘voc_size’]的元素都设置为1,其他保持不变。词典按照频率从大到小的顺序排列,所以index越大,频率越小。
def unk_filter(data):
if config['voc_size'] == -1:
return copy.copy(data)
else:
mask = (np.less(data, config['voc_size'])).astype(dtype='int32')
data = copy.copy(data * mask + (1 - mask))
return data
下面函数的target代表解码时一个batch的输入,这个函数的作用是准备好数据用来解码
def prepare_xy(self, target, cc_matrix):
'''
target: (nb_samples, index_seq) nb_samples代表每个batch里有多少个句子
cc_matrix: (nb_samples, maxlen_t, maxlen_s)目标词和源句子中哪些位置的词相同
context: (nb_samples)
return:
Y_mask 其实是一个mask,标记target中哪些位置是真实的词(标记为1),哪些位置的值是填充的值(然后标记为0)
Y的shape (nb_samples, maxlen_t, embedding_dim)
X: 就是target句子的embeding结果,在每个句子的开头加上了起始符号,也就是全0
X_mask:在Y_mask的第二维的开始插入一个1,同时去掉第二维最后一个词,形状为[nb_samples, maxlen_t]
LL :就是CC矩阵,没变化
XL_mask:形状为[nb_samples, maxlen_t],标记目标句子中的词是否和源句子中的某个词相同
Count:记录了每个句子的长度,shape为[nb_samples,1]
'''
Y, Y_mask = self.Embed(target, True)
#Y[:, :-1, :] 这里面-1表示不包含数组的最后一个元素
#在Y的第二维的第一个位置插入一个全0,同时去掉第二维最后一个词,得到形状为(nb_samples, maxlen_t, embedding_dim)
X = T.concatenate([alloc_zeros_matrix(Y.shape[0], 1, Y.shape[2]), Y[:, :-1, :]], axis=1)
LL = cc_matrix
#T.gt(a,b)将a里值大于b的位置标记为true,其他的位置标记为false
#XL_mask的形状为[nb_samples, maxlen_t],得到目标句子中的每个词是否和源句子中的某个词相同
XL_mask = T.cast(T.gt(T.sum(LL, axis=2), 0), dtype='float32')
if not self.config['use_input']:
X *= 0
#在Y_mask的第二维的开始插入一个1,同时去掉第二维最后一个词,形状为[nb_samples, maxlen_t]
X_mask = T.concatenate([T.ones((Y.shape[0], 1)), Y_mask[:, :-1]], axis=1)
#Count里记录了每个句子的长度[nb_samples,1]
Count = T.cast(T.sum(X_mask, axis=1), dtype=theano.config.floatX)
return X, X_mask, LL, XL_mask, Y_mask, Count
decoder attention
计算一个step的attention权重,返回shape 为(nb_samples, maxlen_s), 其中nb_samples就是batch size。
这里用到了coverage机制 可以参考 Get To The Point: Summarization with Pointer-Generator Networks 一文中的Coverage mechanism。大体的意思是:在解码step t 时,对之前每个解码step的attention权重求和。
def __call__(self, X, S,
Smask=None,
return_log=False,
Cov=None):
assert X.ndim + 1 == S.ndim, 'source should be one more dimension than target.'
# X is the key: (nb_samples, x_dim) 解码时一个step的hidden state
# S (nb_samples, maxlen_s, ctx_dim) encoder里每一个step的hidden state
# Cov is the coverage vector (nb_samples, maxlen_s)
# (nb_samples, source_num, hidden_dims)
Eng = dot(X[:, None, :], self.Wa) + dot(S, self.Ua)
Eng = self.tanh(Eng)
# location aware:
if self.coverage:
# (nb_samples, source_num, hidden_dims)
Eng += dot(Cov[:, :, None], self.Ca)
#(nb_samples, source_num, hidden_dims) * (hidden_dims*1) 得到(nb_samples, source_num, 1)
Eng = dot(Eng, self.va)
Eng = Eng[:, :, 0] # 降维为 (nb_samples, source_num)
if Smask is not None:
# I want to use mask!
EngSum = logSumExp(Eng, axis=1, mask=Smask)
if return_log:
return (Eng - EngSum) * Smask
else:
return T.exp(Eng - EngSum) * Smask
else:
if return_log:
return T.log(self.softmax(Eng))
else:
return self.softmax(Eng)
计算decoder
def build_decoder(self,
target,
cc_matrix,
context,
c_mask,
return_count=False,
train=True):
"""
Build the Computational Graph ::> Context is essential
c_mask :二维数组[nb_samples, max_len_s]
context 保存encoder里每一步的hidden state
"""
# context: (nb_samples, max_len, contxt_dim),输入到一个全连接层,改变最后一维的长度,得到(h_j * W_c)
#后面用来计算update
context_A = self.Is(context) # (nb_samples, max_len, embed_dim)
X, X_mask, LL, XL_mask, Y_mask, Count = self.prepare_xy(target, cc_matrix)
# input drop-out if any.
if self.dropout > 0:
X = self.D(X, train=train)
# Initial state of RNN 第二维第一个位置代表句子最后一个词的隐藏状态?
Init_h = self.Initializer(context[:, 0, :]) # default order ->
Init_a = T.zeros((context.shape[0], context.shape[1]), dtype='float32')
coverage = T.zeros((context.shape[0], context.shape[1]), dtype='float32')
X = X.dimshuffle((1, 0, 2))
X_mask = X_mask.dimshuffle((1, 0))
LL = LL.dimshuffle((1, 0, 2)) # (maxlen_t, nb_samples, maxlen_s) maxlen_t锛歮ax target size
XL_mask = XL_mask.dimshuffle((1, 0)) # (maxlen_t, nb_samples)
def _recurrence(x, x_mask, ll, xl_mask, prev_h, prev_a, cov, cc, cm, ca):
"""
x: (nb_samples, embed_dims)
x_mask: (nb_samples, )
ll: (nb_samples, maxlen_s)
xl_mask:(nb_samples, )
-----------------------------------------
prev_h: (nb_samples, hidden_dims)
prev_a: (nb_samples, maxlen_s)
cov: (nb_samples, maxlen_s) *** coverage ***
-----------------------------------------
cc: (nb_samples, maxlen_s, cxt_dim)
cm: c_mask (nb_samples, maxlen_s)
ca: (nb_samples, maxlen_s, ebd_dim) context_A 用来计算copy时的评分
"""
#根据上一步的h,计算下一步的c_i
prob = self.attention_reader(prev_h, cc, Smask=cm, Cov=cov)
#更新coverage分布向量
ncov = cov + prob
#c_i 代表上一步的attention vector,会由于这一步的RNN输入
cxt = T.sum(cc * prob[:, :, None], axis=1)
# compute input word embedding (mixed). ca * prev_a[:, :, None]得到的是update
x_in = T.concatenate([x, T.sum(ca * prev_a[:, :, None], axis=1)], axis=-1)
# compute the current hidden states of the RNN.
x_out = self.RNN(x_in, mask=x_mask, C=cxt, init_h=prev_h, one_step=True)
# compute the current readout vector.
r_in = [x_out]
# copynet decoding (nb_samples, out_put_dim+context_dim)
#根据x_out,计算取vocabulary里每个词的概率
r_out = self.hidden_readout(x_out) # (nb_samples, voc_size)
#将r_in最后一维的长度变为和cc最后一维的长度相同,这样的话两者的最后一维上就可以做element-wise乘法了
key = self.Os(r_in) # (nb_samples, cxt_dim) :: key
#计算key和encoder里每个step的相关性,即得到每个位置的权重
Eng = T.sum(key[:, None, :] * cc, axis=-1)
#下面两步其实相当于求softmax,在后面会具体讲一下
EngSum = logSumExp(Eng, axis=-1, mask=cm, c=r_out)
next_p = T.concatenate([T.exp(r_out - EngSum), T.exp(Eng - EngSum) * cm], axis=-1)
#copy模式下的概率. 对于一个target词,只留下源句子中和其相同的位置的概率
next_c = next_p[:, self.config['dec_voc_size']:] * ll # (nb_samples, maxlen_s)
#生成模式下的概率
next_b = next_p[:, :self.config['dec_voc_size']]
#下面两项计算update值
sum_a = T.sum(next_c, axis=1, keepdims=True) # (nb_samples,1)
next_a = (next_c / (sum_a + err)) * xl_mask[:, None] # numerically consideration
return x_out, next_a, ncov, sum_a, next_b
#代入参数时,顺序为sequences, outputs_info, non_sequences
outputs, _ = theano.scan(
_recurrence,
sequences=[X, X_mask, LL, XL_mask],
outputs_info=[Init_h, Init_a, coverage, None, None],#None的值不传入函数
non_sequences=[context, c_mask, context_A]
)
X_out, source_prob, coverages, source_sum, prob_dist = [z.dimshuffle((1, 0, 2)) for z in outputs]
X = X.dimshuffle((1, 0, 2))
X_mask = X_mask.dimshuffle((1, 0))
XL_mask = XL_mask.dimshuffle((1, 0))
'''
当词是unk并且这个词在源句中时,在target里是用1代替的,1就代表UNK,同时在XL_mask中标记这个位置的词是和源句中的某个词相同。
所以下面两行的功能是:只留下target矩阵中非填充的词,且当target词大于了voc即UNK词,且没在原句中出现时,才标记为1。
得到的shape为[nb_samples, maxlen_t],
'''
U_mask = T.ones_like(target) * (1 - T.eq(target, 1))
U_mask += (1 - U_mask) * (1 - XL_mask)
'''
概率计算分四种:
1、当target词属于vocabulary且target词不在原句中时,用生成概率;
2、当target词属于vocabulary且target词在原句中时,用生成概率加上这个词在x中的copy概率和;
3、当target词为UNK,且不在源句子中时,用生成UNK的概率
4、当target词为UNK,且在源句中时,用原句中每个和UNK词相同的位置的概率和
self._grab_prob(prob_dist, target) * U_mask :计算1、2中的生成概率、3
source_sum.sum(axis=-1): source_sum的shape为[nb_samples, maxlen_t, 1]。计算2中的copy概率以及第四条
'''
#prob_dist :[nb_samples, maxlen_t, voc_size]
self._grab_prob(prob_dist, target) * U_mask
#log_prob : shape (nb_samples,)是一个矩阵
log_prob = T.sum(T.log(
self._grab_prob(prob_dist, target) * U_mask +
source_sum.sum(axis=-1) + err
) * X_mask, axis=1)
#(nb_samples,)每个句子对应的perplex,即每个词的平均概率
log_ppl = log_prob / (Count + err)
if return_count:
return log_prob, Count
else:
return log_prob, log_ppl
The log-sum-exp trick
参考The log-sum-exp trick in Machine Learning
Let’s say we have an n-dimensional vector and want to calculate:
if you try to calculate it naively, you quite quickly will encounter underflows or overflows, depending on the scale of xi . Even if you work in log-space, the limited precision of computers is not enough and the result will be INF or -INF. So what can we do?
We can show, that the following equation holds:
其中a取x中的最大值,如果用上式右边替代y来计算,就不会出现上面的问题. 这样计算softmax就可以按照下面的方法:
这张图就对应上面代码的
EngSum = logSumExp(Eng, axis=-1, mask=cm, c=r_out)
next_p = T.concatenate([T.exp(r_out - EngSum), T.exp(Eng - EngSum) * cm], axis=-1)