copy net

来源:互联网 发布:puppy linux 安装软件 编辑:程序博客网 时间:2024/06/16 03:13

输入数据处理
每个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)