Seq2Seq源码解析(基于Theano框架)

来源:互联网 发布:zip暴力解压软件 编辑:程序博客网 时间:2024/06/06 03:09

1、初步认识 se2seq

1.1 这是一个通用型 端到端(end-to-end)的学习框架,包括2个部分,一个是Encoder(编码器,负责编码源句子的输入),另外一个是Decoder(解码器,负责解析目标句子的生成)。你既可以用它来做翻译模型,也可以用来做单轮或多轮对话系统、甚至古诗词生成任务等等等…… (简直是深度学习的万金油啊!)

1.2 关于seq2seq的两篇经典之作,希望大家都去拜读一下原文,真的写得很棒!

第一篇采用seq2seq翻译模型框架
Sutskever et al., 2014. Sequence to Sequence Learning with Neural Networks

第一篇采用seq2seq+attention机制
Neural Machine Translation by Jointly Learning to Align and Translate


2、简单了解 seq2seq

2.1 基于Theano框架的seq2seq源码下载地址
百度云盘链接:http://pan.baidu.com/s/1qXGiiTm 密码:td9g

2.2 下载完解压缩,得到如下文件:

这里写图片描述

其中,session1是不带attention机制的seq2seq,session2是带attention机制的seq2seq。提醒一下:本次代码剖析是不带attention机制的seq2seq,即session1文件。

2.3 如何运行这份代码

首先 ,假如你拥有了一份平行语料库(源数据集合,目标数据集合)。你应该修改data目录下的setup_local_env.sh,修改输入数据的路径,以及经过处理后数据保存的路径(生成.tok文件,生成的源/目标字典.pkl)。 (运行命令: ./setup_local_env.sh)

其次 ,训练模型的时候。修改session1目录下的train.sh,记得设置成gpu(否则cpu得跑N天啊,N=无穷大),对应代码 export THEANO_FLAGS=device= gpu ,floatX=float32 。因为train.sh本质上是运行python train_nmt.py,因此你得修改session1目录下的train_nmt.py,设置数据的路径,以及初始化一些参数。

最后 ,如果你想测试模型。运行session1目录下的test.sh,设置数据路径、源/目标字典路径、训练好的模型路径等等参数。本质上是运行session1目录下的translate.py


3、深入解析 se2seq

这里,我将分2部分进行源码的解析,分别包括训练部分train和测试部分test。


3.1 训练部分train :

3.1.1 首先阅读nmt.py脚本的def train()函数

最终训练调用路径如下:

train.sh –> train_nmt.py –> nmt.py

train_nmt.py中几个关键的参数

 'model': 存储训练模型的路径 'dim_word': 词向量的维度,也就是把输入x,数字单词转成dim_word维数的词向量word vertor。 'dim': GRU单元数的数目,也就是Encoder和Decoder隐藏层的数目 'n-words': 源字典和目标字典的大小,不在该字典的单词,为unk,对应数字1'optimizer': 更新参数选择'adadelta'算法,当然你也可以采用较为简单的'SGD'随机梯度下降算法。 'decay-c': 正则化惩罚项,这么暂时不考虑 'use-dropout': dropout机制防止过拟合,提高模型泛化能力,暂时不考虑 'learning-rate': 学习率,只有更新参数算法采用'SGD',才会用到学习率。 'reload':这个参数值得重视。reload建议设置为True,训练模型的过程中,即使机器崩溃了,下次训练可以接着上次的断点继续训练。 


3.1.2 nmt.py脚本中的train()函数

数据真正开始驱动的地方! 代码如下:

def train(dim_word=100,  # word vector dimensionality          dim=1000,  # the number of GRU units          encoder='gru',          decoder='gru_cond_simple',          patience=10,  # early stopping patience          max_epochs=5000,          finish_after=10000000,  # finish after this many updates          dispFreq=100,          decay_c=0.,  # L2 regularization penalty          alpha_c=0.,  # not used          lrate=0.01,  # learning rate          n_words_src=100000,  # source vocabulary size          n_words=100000,  # target vocabulary size          maxlen=100,  # maximum length of the description          optimizer='rmsprop',          batch_size=16,          valid_batch_size=16,          saveto='model.npz',          validFreq=1000,          saveFreq=1000,  # save the parameters after every saveFreq updates          sampleFreq=100,  # generate some samples after every sampleFreq          datasets=[              '/data/lisatmp3/chokyun/europarl/europarl-v7.fr-en.en.tok',              '/data/lisatmp3/chokyun/europarl/europarl-v7.fr-en.fr.tok'],          valid_datasets=['../data/dev/newstest2011.en.tok',                          '../data/dev/newstest2011.fr.tok'],          dictionaries=[              '/data/lisatmp3/chokyun/europarl/europarl-v7.fr-en.en.tok.pkl',              '/data/lisatmp3/chokyun/europarl/europarl-v7.fr-en.fr.tok.pkl'],          use_dropout=False,          reload_=False,          overwrite=False):    # Model options    model_options = locals().copy()    # load dictionaries and invert them    worddicts = [None] * len(dictionaries)    worddicts_r = [None] * len(dictionaries)    for ii, dd in enumerate(dictionaries):        with open(dd, 'rb') as f:            worddicts[ii] = pkl.load(f)        worddicts_r[ii] = dict()        for kk, vv in worddicts[ii].iteritems():            worddicts_r[ii][vv] = kk    # reload options    if reload_ and os.path.exists(saveto):        print 'Reloading model options'        with open('%s.pkl' % saveto, 'rb') as f:            model_options = pkl.load(f)    print 'Loading data'    train = TextIterator(datasets[0], datasets[1],                         dictionaries[0], dictionaries[1],                         n_words_source=n_words_src, n_words_target=n_words,                         batch_size=batch_size,                         maxlen=maxlen)    valid = TextIterator(valid_datasets[0], valid_datasets[1],                         dictionaries[0], dictionaries[1],                         n_words_source=n_words_src, n_words_target=n_words,                         batch_size=valid_batch_size,                         maxlen=maxlen)    print 'Building model'    params = init_params(model_options)    # reload parameters    if reload_ and os.path.exists(saveto):        print 'Reloading model parameters'        params = load_params(saveto, params)    tparams = init_tparams(params)    trng, use_noise, \        x, x_mask, y, y_mask, \        opt_ret, \        cost = \        build_model(tparams, model_options)    inps = [x, x_mask, y, y_mask]    print 'Building sampler'    f_init, f_next = build_sampler(tparams, model_options, trng, use_noise)    # before any regularizer    print 'Building f_log_probs...',    f_log_probs = theano.function(inps, cost, profile=profile)    print 'Done'    cost = cost.mean()    # apply L2 regularization on weights    if decay_c > 0.:        decay_c = theano.shared(numpy.float32(decay_c), name='decay_c')        weight_decay = 0.        for kk, vv in tparams.iteritems():            weight_decay += (vv ** 2).sum()        weight_decay *= decay_c        cost += weight_decay    # un used, attention weight regularization    if alpha_c > 0. and not model_options['decoder'].endswith('simple'):        alpha_c = theano.shared(numpy.float32(alpha_c), name='alpha_c')        alpha_reg = alpha_c * (            (tensor.cast(y_mask.sum(0)//x_mask.sum(0), 'float32')[:, None] -             opt_ret['dec_alphas'].sum(0))**2).sum(1).mean()        cost += alpha_reg    # after all regularizers - compile the computational graph for cost    print 'Building f_cost...',    f_cost = theano.function(inps, cost, profile=profile)    print 'Done'    print 'Computing gradient...',    grads = tensor.grad(cost, wrt=itemlist(tparams))    print 'Done'    # compile the optimizer, the actual computational graph is compiled here    lr = tensor.scalar(name='lr')    print 'Building optimizers...',    f_grad_shared, f_update = eval(optimizer)(lr, tparams, grads, inps, cost)    print 'Done'    print 'Optimization'    best_p = None    bad_counter = 0    uidx = 0    estop = False    history_errs = []    # reload history    if reload_ and os.path.exists(saveto):        rmodel = numpy.load(saveto)        history_errs = list(rmodel['history_errs'])        if 'uidx' in rmodel:            uidx = rmodel['uidx']    if validFreq == -1:        validFreq = len(train[0])/batch_size    if saveFreq == -1:        saveFreq = len(train[0])/batch_size    if sampleFreq == -1:        sampleFreq = len(train[0])/batch_size    for eidx in xrange(max_epochs):        n_samples = 0        for x, y in train:            n_samples += len(x)            uidx += 1            use_noise.set_value(1.)            print "---------------"            print x            x, x_mask, y, y_mask = prepare_data(x, y, maxlen=maxlen,                                                n_words_src=n_words_src,                                                n_words=n_words)            if x is None:                print 'Minibatch with zero sample under length ', maxlen                uidx -= 1                continue            ud_start = time.time()            # compute cost, grads and copy grads to shared variables            cost = f_grad_shared(x, x_mask, y, y_mask)            # do the update on parameters            f_update(lrate)            ud = time.time() - ud_start            # check for bad numbers, usually we remove non-finite elements            # and continue training - but not done here            if numpy.isnan(cost) or numpy.isinf(cost):                print 'NaN detected'                return 1., 1., 1.            # verbose            if numpy.mod(uidx, dispFreq) == 0:                print 'Epoch ', eidx, 'Update ', uidx, 'Cost ', cost, 'UD ', ud            # save the best model so far, in addition, save the latest model            # into a separate file with the iteration number for external eval            if numpy.mod(uidx, saveFreq) == 0:                print 'Saving the best model...',                if best_p is not None:                    params = best_p                else:                    params = unzip(tparams)                numpy.savez(saveto, history_errs=history_errs, uidx=uidx, **params)                pkl.dump(model_options, open('%s.pkl' % saveto, 'wb'))                print 'Done'                # save with uidx                if not overwrite:                    print 'Saving the model at iteration {}...'.format(uidx),                    saveto_uidx = '{}.iter{}.npz'.format(                        os.path.splitext(saveto)[0], uidx)                    numpy.savez(saveto_uidx, history_errs=history_errs,                                uidx=uidx, **unzip(tparams))                    print 'Done'            # generate some samples with the model and display them            if numpy.mod(uidx, sampleFreq) == 0:                # FIXME: random selection?                for jj in xrange(numpy.minimum(5, x.shape[1])):                    stochastic = True                    sample, score = gen_sample(tparams, f_init, f_next,                                               x[:, jj][:, None],                                               model_options, trng=trng, k=1,                                               maxlen=30,                                               stochastic=stochastic,                                               argmax=False)                    print 'Source ', jj, ': ',                    for vv in x[:, jj]:                        if vv == 0:                            break                        if vv in worddicts_r[0]:                            print worddicts_r[0][vv],                        else:                            print 'UNK',                    print                    print 'Truth ', jj, ' : ',                    for vv in y[:, jj]:                        if vv == 0:                            break                        if vv in worddicts_r[1]:                            print worddicts_r[1][vv],                        else:                            print 'UNK',                    print                    print 'Sample ', jj, ': ',                    if stochastic:                        ss = sample                    else:                        score = score / numpy.array([len(s) for s in sample])                        ss = sample[score.argmin()]                    for vv in ss:                        if vv == 0:                            break                        if vv in worddicts_r[1]:                            print worddicts_r[1][vv],                        else:                            print 'UNK',                    print            # validate model on validation set and early stop if necessary            if numpy.mod(uidx, validFreq) == 0:                use_noise.set_value(0.)                valid_errs = pred_probs(f_log_probs, prepare_data,                                        model_options, valid)                valid_err = valid_errs.mean()                history_errs.append(valid_err)                if uidx == 0 or valid_err <= numpy.array(history_errs).min():                    best_p = unzip(tparams)                    bad_counter = 0                if len(history_errs) > patience and valid_err >= \                        numpy.array(history_errs)[:-patience].min():                    bad_counter += 1                    if bad_counter > patience:                        print 'Early Stop!'                        estop = True                        break                if numpy.isnan(valid_err):                    ipdb.set_trace()                print 'Valid ', valid_err            # finish after this many updates            if uidx >= finish_after:                print 'Finishing after %d iterations!' % uidx                estop = True                break        print 'Seen %d samples' % n_samples        if estop:            break    if best_p is not None:        zipp(best_p, tparams)    use_noise.set_value(0.)    valid_err = pred_probs(f_log_probs, prepare_data,                           model_options, valid).mean()    print 'Valid ', valid_err    params = copy.copy(best_p)    numpy.savez(saveto, zipped_params=best_p,                history_errs=history_errs,                uidx=uidx,                **params)    return valid_err

代码解析:


第147行: 从这句代码开始,才真正有数据开始驱动! for x in train 首先调用了第54行TextIterator,即data_iterator.py脚本中的 next()函数,此时返回了x的实例化数据,for example(这里取batch=2):
x=
[[64 103]
[2 40 ]
[2 41 ]
[2 42 ]
[3 43 ]
[2 0]
[2 0]
[65 0 ]
[66 0 ]
[67 0]
[0 0]]


第153行: 调用了nmt.py脚本中的prepare_data()函数,返回了x和x_mask。for example:
x=
[[64 103]
[2 40 ]
[2 41 ]
[2 42 ]
[3 43 ]
[2 0]
[2 0]
[65 0 ]
[66 0 ]
[67 0]
[0 0]]

x_mask=
[[1 1]
[1 1]
[1 1]
[1 1]
[1 1]
[1 1]
[1 0]
[1 0]
[1 0]
[1 0]
[1 0]]

第165行: 计算cost,调用流畅如下:

Created with Raphaël 2.1.0第165行 cost = f_grad_shared(x, x_mask, y, y_mask)第120行 f_grad_shared, f_update = eval(optimizer)(lr, tparams, grads, inps, cost)nmt.py中adadelta()函数 第16行 f_grad_shared = theano.function(inp, cost, updates=zgup+rg2up,第74-79行 trng, use_noise, x, x_mask, y, y_mask, opt_ret, cost = build_model(tparams, model_options)


其中,第120行 optimizer参数更新优化器选用的是adadelta,当然你也可以选用adam、rmsprop或者比较耳熟能详的sgd。 所以最终cost计算是在build_model函数中定义的,so, 我们迫不及待地去看看nmt.py脚本中的build_model()函数吧!!!

第168行: f_update(lrate) ,其中lrate为学习率。因为这里选用更新优化参数为adadelta,该算法与学习率无关。若采用SGD,则更新参数与学习率有关。调用流程如下:

Created with Raphaël 2.1.0第168行 f_update(lrate)第120行 f_grad_shared, f_update = eval(optimizer)(lr, tparams, grads, inps, cost)nmt.py中adadelta()函数 第26行 f_update = theano.function([lr], [], updates=ru2up+param_up,on_unused_input='ignore', profile=profile)

到此,一次batch样本更新了一次权重参数。

第179行: 每迭代dispFreq次数batch样本,打印一次结果。

第184行:每迭代saveFreq次数batch样本,保存一次模型参数,模型放置的路径为saveto


第205行: 每迭代sampleFreq次数batch样本,显示5个源句子翻译的结果,这里调用了gen_sample()函数,也就是开始进行测试输出结果了,看看系统训练得好不好。 因此,我们赶紧去nmt.py脚本中的gen_sample()函数瞧个究竟,到底测试的时候是如何输出一个单词一个单纯蹦出来,最终组合成一个目标句子的。

第216-222行: 打印源句子x

第224-231: 打印 Ground True句子y

第234-246: 因为这里采用随机采样的方式生成目标单词,所以,sample变量便是生成的目标句子。 然后,打印生成的目标句子。

第249-270行: 输出验证集的cost。如果验证集的cost一直掉不下来,也就没必要继续训练下去了。因为验证集和测试集通常具有相同的数据分布。因此,可以通过验证集cost的最低点,找到最好的model参数。


3.1.3 data_iterator.py脚本中的 next()函数

实现字母单词到数字单词的转换!,并返回batch个源句子和目标句子

代码如下:

while True:    # read from source file and map to word index    try:        ss = self.source_buffer.pop()    except IndexError:        break    ss = [self.source_dict[w] if w in self.source_dict else 1 for w in ss]    if self.n_words_source > 0:        ss = [w if w < self.n_words_source else 1 for w in ss]    # read from source file and map to word index    tt = self.target_buffer.pop()    tt = [self.target_dict[w] if w in self.target_dict else 1          for w in tt]    if self.n_words_target > 0:        tt = [w if w < self.n_words_target else 1 for w in tt]    if len(ss) > self.maxlen and len(tt) > self.maxlen:        continue    source.append(ss)    target.append(tt)    if len(source) >= self.batch_size or \                    len(target) >= self.batch_size:        break

代码解析:

第4行: 每次弹出一个句子赋给ss变量;

第7行:将句子的单词变成其相对于的数字,即判断句子的每一个单词,若字典中存在对应的数字,则用该数字替代,反之则为1。其中,数字1对应‘UNK’,数字0对应‘EOS’。

该句子代码可以按如下理解:

for w in ss:    if w in self.source_dict:        self.source_dict[w]    else:         1

第9行:判断该数字值是否超过的源字典的大小,若小于说明该字母对应的数字在字典内;否则不在字典内,则修改为1,即为 ‘unk’。为什么要进行这一步操作呢,而不把源字典设置得足够大呢,避免unk的出现?

原因有以下2点:
1) 这是因为源字典设置的太大,W权重的维数增加,操作大维数的矩阵是非常耗时间的,通常字典大小设置在万数量级。
2)超过源字典大小的数字,一般在train集出现的频次较低,所以设置维1影响不会太大。因为字典是根据train集合单词出现频次从小到大排列的。

第21、22行:把单词替换成数字的句子添加到source和target列表对象中。

第24行:如果source或者target里面有self.batch_size个句子,则break,即每次return 回去的source和target都包含batch_size个由数字组成的句子。

假设batch_size的大小为2,则return回去的source的格式为
[ [64,2,2,2,3,2,2,65,66,67,],[103,40,41,42,43] ]

继续返回nmt.py脚本中的train()函数吧!


3.1.4 nmt.py脚本中的prepare_data()函数

给x和y分别准备x_mask和y_mask层,目的是防止padding 0带来的误差影响。为什么要进行这一步准备mask的操作?

为了提高运行速度同时防止梯度波动太大,梯度更新采用了batch_size的方式。此时,就必须保证batch_size句子的长度相等。因此,选取了最长句子的长度作为标准,其他句子补0与之对齐。

正因为其他句子进行了补0对齐操作,然而在实际GRU计算中,在每个timestep,这些填充的0仍然会喂给网络的隐藏层,引发错误操作。因此,需要加入mask层来剔除,过滤那些填充0所带来的影响。

这里不妨假设seqs_x=[ [64,2,2,2,3,2,2,65,66,67,],[103,40,41,42,43] ],即batch_size设置为2.

代码如下:

def prepare_data(seqs_x, seqs_y, maxlen=None,                 n_words_src=30000, n_words=30000):    # x: a list of sentences    lengths_x = [len(s) for s in seqs_x]    lengths_y = [len(s) for s in seqs_y]    # filter sequences according to maximum sequence length    if maxlen is not None:        new_seqs_x = []        new_seqs_y = []        new_lengths_x = []        new_lengths_y = []        for l_x, s_x, l_y, s_y in zip(lengths_x, seqs_x, lengths_y, seqs_y):            if l_x < maxlen and l_y < maxlen:                new_seqs_x.append(s_x)                new_lengths_x.append(l_x)                new_seqs_y.append(s_y)                new_lengths_y.append(l_y)        lengths_x = new_lengths_x        seqs_x = new_seqs_x        lengths_y = new_lengths_y        seqs_y = new_seqs_y        if len(lengths_x) < 1 or len(lengths_y) < 1:            return None, None, None, None    n_samples = len(seqs_x)    maxlen_x = numpy.max(lengths_x) + 1    maxlen_y = numpy.max(lengths_y) + 1    # pad batches and create masks    x = numpy.zeros((maxlen_x, n_samples)).astype('int64')    y = numpy.zeros((maxlen_y, n_samples)).astype('int64')    x_mask = numpy.zeros((maxlen_x, n_samples)).astype('float32')    y_mask = numpy.zeros((maxlen_y, n_samples)).astype('float32')    for idx, [s_x, s_y] in enumerate(zip(seqs_x, seqs_y)):        x[:lengths_x[idx], idx] = s_x        x_mask[:lengths_x[idx]+1, idx] = 1.        y[:lengths_y[idx], idx] = s_y        y_mask[:lengths_y[idx]+1, idx] = 1.    return x, x_mask, y, y_mask

代码解析:

第5行: lengths_x=[10,5]

第15-19行:过滤掉单词长度大于maxlen的句子,这里maxlen设置为50。因为不带attention机制的GRU或者LSTM,端对端翻译模型随着源句子单词数的增加,翻译效果急剧下降。因为,随着句子长度的增加,会丢失很多语义之间的信息。

第29行: maxlen_x=max(10,5)+1,即为11。为了提高运行速度同时防止梯度波动太大,梯度更新采用了batch_size的方式。此时,就必须保证batch_size句子的长度相等。因此,选取了最长句子的长度10作为标准,其他句子补0与之对齐。加1,是为了在每个句子末尾添加一个0,表示‘eos’,即句子结束标识符。

第33、35行: x和x_mask均初始化为11*2的二维数组,并全部赋值为0,之所以x_mask数据类型为float32,是因为其后面需要参与theano共享变量的运算,而theano共享变量需要设置为float32的类型,才能在GPU进行加速计算。

第37-42行: 最终x值如下,即句子最后末尾添加了一个句子结束标识符0,代表‘eos
x=
[[64 103]
[2 40 ]
[2 41 ]
[2 42 ]
[3 43 ]
[2 0]
[2 0]
[65 0 ]
[66 0 ]
[67 0]
[0 0]]

x_mask的值如下,即有数字的地方均为1(提醒:包括‘eos’所对应的数字0),其他padding 为0,后面两者相乘(即,x_mask * 隐藏层的状态)就可以把padding为0喂给系统的影响消除。

x_mask=
[[1 1]
[1 1]
[1 1]
[1 1]
[1 1]
[1 1]
[1 0]
[1 0]
[1 0]
[1 0]
[1 0]]

第43行:返回x和x_mask。其中,x添加了eos句子结束标识符,x_mask根据最长句子长度进行了padding 0的操作。

继续返回nmt.py脚本中的train()函数吧!


3.1.5 nmt.py脚本中的build_model()函数

Encoder-Decoder的核心部分,代码如下:

def build_model(tparams, options):    opt_ret = dict()    trng = RandomStreams(1234)    use_noise = theano.shared(numpy.float32(0.))    # description string: #words x #samples    x = tensor.matrix('x', dtype='int64')    x_mask = tensor.matrix('x_mask', dtype='float32')    y = tensor.matrix('y', dtype='int64')    y_mask = tensor.matrix('y_mask', dtype='float32')    n_timesteps = x.shape[0]    n_timesteps_trg = y.shape[0]    n_samples = x.shape[1]    # word embedding (source)    emb = tparams['Wemb'][x.flatten()]    emb = emb.reshape([n_timesteps, n_samples, options['dim_word']])    # pass through encoder gru, recurrence here    proj = get_layer(options['encoder'])[1](tparams, emb, options,                                            prefix='encoder',                                            mask=x_mask)    # last hidden state of encoder rnn will be used to initialize decoder rnn    ctx = proj[0][-1]    ctx_mean = ctx    # initial decoder state    init_state = get_layer('ff')[1](tparams, ctx_mean, options,                                    prefix='ff_state', activ='tanh')    # word embedding (target), we will shift the target sequence one time step    # to the right. This is done because of the bi-gram connections in the    # readout and decoder rnn. The first target will be all zeros and we will    # not condition on the last output.    emb = tparams['Wemb_dec'][y.flatten()]    emb = emb.reshape([n_timesteps_trg, n_samples, options['dim_word']])    emb_shifted = tensor.zeros_like(emb)    emb_shifted = tensor.set_subtensor(emb_shifted[1:], emb[:-1])    emb = emb_shifted    # decoder - pass through the decoder gru, recurrence here    proj = get_layer(options['decoder'])[1](tparams, emb, options,                                            prefix='decoder',                                            mask=y_mask, context=ctx,                                            one_step=False,                                            init_state=init_state)    # hidden states of the decoder gru    proj_h = proj    # we will condition on the last state of the encoder only    ctxs = ctx[None, :, :]    # compute word probabilities    logit_lstm = get_layer('ff')[1](tparams, proj_h, options,                                    prefix='ff_logit_lstm', activ='linear')    logit_prev = get_layer('ff')[1](tparams, emb, options,                                    prefix='ff_logit_prev', activ='linear')    logit_ctx = get_layer('ff')[1](tparams, ctxs, options,                                   prefix='ff_logit_ctx', activ='linear')    logit = tensor.tanh(logit_lstm+logit_prev+logit_ctx)    logit = get_layer('ff')[1](tparams, logit, options, prefix='ff_logit',                               activ='linear')    logit_shp = logit.shape    probs = tensor.nnet.softmax(        logit.reshape([logit_shp[0]*logit_shp[1], logit_shp[2]]))    # cost    y_flat = y.flatten()    y_flat_idx = tensor.arange(y_flat.shape[0]) * options['n_words'] + y_flat    cost = -tensor.log(probs.flatten()[y_flat_idx])    cost = cost.reshape([y.shape[0], y.shape[1]])    cost = (cost * y_mask).sum(0)    return trng, use_noise, x, x_mask, y, y_mask, opt_ret, cost

代码解析:

提醒,这里dim_word=300,即每个词向量的维度为1*300;dim=500,即GRU单元的数目;n-words=1000,即源字典和目标字典的大小为1000;batch_size=2,即n_sample2=2,每次并行处理2个句子;Wemb的维度为n_word_src*dim_word,即1000*300

第18行:首先,将x通过flatten换成1D的向量,接着通过对Wemb矩阵的索引,将x换成对应的词向量矩阵2D;

第19行:最后,reshape成[step,batch,dim_word]的3D矩阵;


第22行:这里‘encoder’=gru,因此,这里调用了nmt.py脚本的gru_layer()函数,这里返回了proj,是Encoder部分所有隐藏层的状态。不妨移步nmt.py脚本中的gru_layer()函数瞧个究竟。

第27行: ctx和ctx_mean均等于encoder最后一个隐藏层ht

ctx = proj[0][-1]ctx_mean = ctx

第31行: 初始化decoder的隐藏层状态,即初始化s0s0的维数为2D,s0=[batch,dim]
s0=tanh(ctx_meanff_state_W)+ff_state_b

第38行:首先,将y通过flatten换成1D的向量,接着通过对Wemb_dec矩阵的索引,将y换成对应的词向量矩阵2D;

第39行:最后,reshape成[step,batch,dim_word]的3D矩阵;

第41行: emb[:-1]:表示emb矩阵从第0行到倒数第2行。 emb_shifted[1:] :表示emb_shifted矩阵从第一行到最后一行 。即,emb_shifted矩阵第0行的元素全为0,从第一行到最后一行由emb[:-1]元素的值替换。

第42行: 即最终emb矩阵跟原来的emb矩阵发生了如下变化:每一行向下挪动了一行。如,第0行的元素,跑到了第1行…第n-1行的元素跑到了第n行。此外,第0行的元素全部为0。 为什么做如此操作?因为,decoder的时候,第一个timestep喂给隐藏层的输入是全0,然后产生了第一个单词,用第一个单词作为输入,产生第二个单词,直到最终输出‘eos’,整个过程才结束。


第45-49行: 这里‘decoder’=gru_cond_simple,因此,这里调用了gru_cond_simple_layer()函数,这里返回了proj,是decoder部分所有隐藏层的状态。不妨移步nmt.py脚本中gru_cond_simple_layer()函数瞧个究竟。

第51行: proj_h=proj 包括了decoder所有的隐藏层状态。proj_h的维数为3D,proj_h=[n_step,batch,dim]

第54行: 因为ctx是2D向量(batch,dim),需扩展为3D向量,方便后面并行计算。

第57行:

logit_lstm=projff_logit_lstm_W+ff_logit_lstm_b

其中,ff_logit_lstm_W=[dim,dim_word] ,是2D向量。
因此,logit_lstm=[n_step,batch,dim],是3D向量。

第59行:

logit_prev=embff_logit_prev_W+ff_logit_prev_b

其中,emb=[n_step,batch,dim_word] , ff_logit_prev_W=[dim_word,dim_word]
因此,logit_prev=[n_step,batch,dim_word],是3D向量。

第61行:

logit_ctx=ctxsff_logit_ctx_W+ff_logit_ctx_b

其中,txs=[None,batch,dim]ff_logit_ctx=[dim,dim_word]
因此,logit_ctx=[None,batch,dim_word],是3D向量。

第63行:

logit=tanh(logit_lstm+logit_prev+logit_ctx)

可见,最终产生的目标单词主要由三部分组成。
第一部分,logit_lstm是decoder的隐藏层的贡献;
第二部分,logit_prevyt1,也就是前一个单词的贡献;
第三部分,logit_ctx 是 encoder的贡献,此时encoder的最后一个隐藏层ht包含了encoder所以输入的信息。

第64行:

logit=logitff_logit_W+ff_logit_b

logitff_logit_W=[n_step,batch,dim_word][dim_word,n_words]=[n_step,batch,n_words]

第66行: logit_shp=[n_step,batch,n_word]

第67行:将目标单词的输出值进行softmax操作,转变为目标单词的输出概率。其中,probs=[n_step*batch,n_word],是2D向量。列向量n_words,表示输出目标字典单词的概率,即每一个tempstep,每一个batch输出一个维数为n_word的向量,对应目标字典每一个单词的概率。

再次啰嗦一下,一个值得思考的关键点: train每一个timestep,每一个batch都输出了一个维数为n_word的1D向量,我们训练的目标应该使得,目标单词在n_word中所对应的位置输出概率最大。如此,测试的时候我们才可以取输出n_word中概率最大的单词作为输出(贪心算法的思想,很容易想象,取每一个时刻输出概率最高的单词组成的句子,不一定得是整个句子输出概率最高的!),当然也可以采用beam search取整个句子输出概率最大的句子(语言模型更好,输出的句子更加流畅通顺)!

第71行: y_flat是大小为n_step*batch的1D向量,代表ground true的目标句子,此时每个单词只是一个数字,对应字典的单词。

第72行: y_flat_idx 每一个目标单词用n_word来表示,即one-hot,也就是每个目标单词占据n_word的大小。

第73行: probs.flatten()[y_flat_idx] 表示每一个目标单词在n_word输出中对应的概率。
取负对数似然作为损失函数。你可以这样简单理解。如果目标单词在n_word输出中对应的概率趋近于1,此时,损失趋近于0;反之,目标单词在n_word输出中对应的概率趋近于0,此时,损失趋近于无穷大。

第74行: cost=[n_step,batch],方便与y_mask进行相乘,此时维度与y_mask一致,y_mask=[n_step,batch]

第75行: cost * y_mask,去掉padding 0 带来的误差影响,sum(0)是把矩阵的所有行累加起来,即把所有的timestep的误差累加起来。此时,cost=[batch],代表每一个输出句子与目标句子的误差。

继续返回nmt.py脚本中的train函数吧!


3.1.6 nmt.py脚本中的gru_layer()函数。

build_model()调用gru_layer()代码如下:

proj = get_layer(options['encoder'])[1](tparams, emb,        options, prefix='encoder', mask=x_mask)

这里,state_below即为x的emd,维数为[step,batch,dim_word],mask=x_mask,还记得x_mask的目的吗?为了屏蔽掉padding 0带来的误差,因为在每一个step中,padding 的0仍然会喂给Encoder的隐藏层。

gru_layer()是具体实现Encoder的代码。

def gru_layer(tparams, state_below, options, prefix='gru', mask=None,              **kwargs):    nsteps = state_below.shape[0]    if state_below.ndim == 3:        n_samples = state_below.shape[1]    else:        n_samples = 1    dim = tparams[_p(prefix, 'Ux')].shape[1]    if mask is None:        mask = tensor.alloc(1., state_below.shape[0], 1)    # utility function to slice a tensor    def _slice(_x, n, dim):        if _x.ndim == 3:            return _x[:, :, n*dim:(n+1)*dim]        return _x[:, n*dim:(n+1)*dim]    # state_below is the input word embeddings    # input to the gates, concatenated    state_below_ = tensor.dot(state_below, tparams[_p(prefix, 'W')]) + \        tparams[_p(prefix, 'b')]    # input to compute the hidden state proposal    state_belowx = tensor.dot(state_below, tparams[_p(prefix, 'Wx')]) + \        tparams[_p(prefix, 'bx')]    # step function to be used by scan    # arguments    | sequences |outputs-info| non-seqs    def _step_slice(m_, x_, xx_,  h_,          U, Ux):        preact = tensor.dot(h_, U)        preact += x_        # reset and update gates        r = tensor.nnet.sigmoid(_slice(preact, 0, dim))        u = tensor.nnet.sigmoid(_slice(preact, 1, dim))        # compute the hidden state proposal        preactx = tensor.dot(h_, Ux)        preactx = preactx * r        preactx = preactx + xx_        # hidden state proposal        h = tensor.tanh(preactx)        # leaky integrate and obtain next hidden state        h = u * h_ + (1. - u) * h        h = m_[:, None] * h + (1. - m_)[:, None] * h_        return h    # prepare scan arguments    seqs = [mask, state_below_, state_belowx]    init_states = [tensor.alloc(0., n_samples, dim)]    _step = _step_slice    shared_vars = [tparams[_p(prefix, 'U')],                   tparams[_p(prefix, 'Ux')]]    rval, updates = theano.scan(_step,                                sequences=seqs,                                outputs_info=init_states,                                non_sequences=shared_vars,                                name=_p(prefix, '_layers'),                                n_steps=nsteps,                                profile=profile,                                strict=True)    rval = [rval]    return rval

代码解析:

第9行: dim为GRU的单元数目,这里为1000。

第22行: 这句话是输入x到gate门之间的计算。

state_below_: tensor. dot( x , encoder_W)+encoder_b,

这句话有点不太好理解,因此我会多费点口舌。 为什么要进行这一步操作呢???

主要是因为,RNN在每一个timestep中,都会计算更新隐藏层。其中,权重W与输入x是提前知道的,所以可以全部提前并行计算,这样大大提高了运行速度。

其中,x的维数是3D,x=[step,batch,dim_word] ; encoder_W的维数是2D.
encoder_W定义在nmt.py脚本param_init_gru()中,如下:

 W = numpy.concatenate([norm_weight(nin, dim),                           norm_weight(nin, dim)], axis=1)    params[_p(prefix, 'W')] = W    params[_p(prefix, 'b')] = numpy.zeros((2 * dim,)).astype('float32')

为了解释上面两句代码,首先解释下numpy.concatenate(),能够一次完成多个数组的拼接。

a=numpy.array([1,2,3],[4,5,6])b=numpy.array([11,22,33],[44,55,66])numpy.concatenate((a,b),axis=0)  //axis=0 沿着列方向的轴进行拼接//因此返回  [[ 1  2  3]//          [ 4  5  6]//          [11 22 33]//          [44 55 66 ]]np.concatenate((a,b),axis=1) //axis=1 沿着行方向的轴进行拼接//因此返回 [[1,2,3,11,22,33]//         [4,5,6,44,55,66]]

按上面的理解,这里encoder_W的维度为(nin,2*dim)=(dim_word,2*dim)。这里,dim_word是word vector的维数,dim是GRU的数目。你一定满心疑惑了,为什么要进行拼接的操作呢??? 先透露一下,拼接是把更新门ut的W权重与复位门rt的W权重按行方向的轴拼到一起了。其实,最大的目的都是为了速度,并行计算提高运算速度!!!

接着,你可能更疑惑了,3D的向量x,如何跟2D的向量encoder_W进行点乘呢??? 这是theano中广播(broadcast)的机制,对两个形状不同的阵列进行数学计算的处理机制。较小的阵列“广播”到较大阵列相同的形状尺度上,使它们对等以可以进行数学计算。具体广播细节,自行度娘或者Google。

tensor. dot( x , encoder_W)+encoder_b//(n_step,batch,dim_word)点乘(dim_word,2*dim)=(n_step,batch,2*dim)

第25行: 这句话是输入x到GRU的记忆单元h˜t

state_belowx: tensor. dot( x , encoder_Wx)+encoder_bx,

同理,state_belowx的维数为(n_step,batch,dim_word)点乘(dim_word,dim)=(n_step,batch,dim)

第59行: 返回值rval,包含了scan迭代的每一步的输出结果,也就是encoder的所有隐藏层的状态。

scan是theano中构建循环Graph的方法。函数原型如下:

theano.scan(fn, sequences=None, outputs_info=None, non_sequences=None, n_steps=None, truncate_gradient=-1, go_backwards=False, mode=None, name=None, profile=False, allow_gc=None, strict=False)

fn: 可以是一个lambda表达式或者def函数,每执行一步scan操作,调用一次fn。其中,fn函数的参数为:(sequences,outputs_info,non_sequences);

sequences: scan进行迭代的变量

outputs_info:它描述了输出的初始状态,和输出具有相同的维度,值得注意的是,没进行一步scan操作,outputs_info的数值会被上一次迭代的输出值更新替换掉;

non_sequences: scan进行迭代的常量,一般为theano的共享变量;

n_step:代表了scan操作的迭代次数,这里代表了该batch_size中,最长句子的单词个数+1(+1是因为每个句子添加了一个结束标志符‘eos’,即数字0);

strict:设置为True,用于检测fn函数用到的所有共享变量是否均包含在non_sequences中,若不满足则会Raise an error。

有一点特别需要强调注意: scan计算过程会将参数sequences中的矩阵按照axis=0(即纵向)进行切片,每次只取一个time_step的序列数据。这一点,从代码第54行和第61行也可以看出来,outputs_info的维度为2D(batch,dim)。

第30行: _step_slice函数,scan每扫一次(也就是每一个timestep,对应RNN喂了一个单词),_step_slice函数执行一次。
为了让大家有个更直观的理解,GRU encoder的公式如下:

zt=σ(W(z)xt+U(z)ht1)

rt=σ(W(r)xt+U(r)ht1)

h˜t=tanh(rtUxht1+Wxxt)

ht=(1zt)ht1+zth˜t

参数如下:
m_ : x_mask
x_:state_below_
xx_:state_belowx
h_: ht1 上一个时刻的隐藏层
U: encoder_U , 注意,这里U=[Uz:Ur]
Ux:encoder_Ux

第31、32行:实现了W(z)xt+U(z)ht1W(r)xt+U(r)ht1的并行操作。此时,preact=[W(r)xt+U(r)ht1 : W(z)xt+U(z)ht1]

第35、36行:将preact进行切割, , 注意这里 z就是代码里面的u,也就是更新门的意思。
_slice(preact, 0, dim) 为 [W(r)xt+U(r)ht1]

_slice(preact, 1, dim) 为 [W(z)xt+U(z)ht1]

 //对应GRU公式二 r = tensor.nnet.sigmoid(_slice(preact, 0, dim)) //对应GRU公式一 u = tensor.nnet.sigmoid(_slice(preact, 1, dim))

第39-44行: 对应GRU公式三

第47行:对应GRU公式四

第48行: 这一行特别有意思。目的就是用x_mask屏蔽掉padding 0带来的误差。(因为长度小于maxlen的句子,会进行padding 0的操作,但是,这些paddng 0 在每一个timestep都会喂给Encoder的隐藏层,显然这些误差得消除掉!)

因为,此时m_是1D的向量(batch),h是2D的向量(batch*dim),所以需要进行m_[:,None]扩展为2D的向量。

当m_[:,None]中为0的 时候,也就是timestep到了padding 0的时刻,这时候ht应该不能更新,继续保持上一个时刻ht1的状态)。此时式子第一项为0,第二项为ht1

当m_[:,None]中为1的时候,也就是timestep还没进行到padding 0 的时刻,此时喂给系统仍然是有效的单词(包括‘eos’),此时应该继续更新ht。因此,式子第一项为ht,第二项为0。

第67、68行: 当进行了n_step次迭代后,也就是所有单词一步步喂给了系统结束后。因为,rval变量包含了每次迭代的输出结果,也就是rval=[rval]即是encoder全部隐藏层的状态,最后将rval变量 return回去。

继续返回nmt.py脚本中的build_model()函数吧!

3.1.7 nmt.py脚本中的gru_cond_simple_layer()函数

build_model()调用gru_cond_simple_layer()代码如下:

 proj = get_layer(options['decoder'])[1](tparams, emb, options,                                            prefix='decoder',                                            mask=y_mask, context=ctx,                                            one_step=False,                                            init_state=init_state)

其中,context=ctx是指Encoder最后一个隐藏层ht的状态;
init_state=init_state,是指Decoder隐藏层的初始状态s0;

gru_cond_simple_layer()函数是具体实现Decoder的代码。

代码如下:

def gru_cond_simple_layer(tparams, state_below, options, prefix='gru',                          mask=None, context=None, one_step=False,                          init_state=None,                          **kwargs):    assert context, 'Context must be provided'    if one_step:        assert init_state, 'previous state must be provided'    nsteps = state_below.shape[0]    if state_below.ndim == 3:        n_samples = state_below.shape[1]    else:        n_samples = 1    # mask    if mask is None:        mask = tensor.alloc(1., state_below.shape[0], 1)    dim = tparams[_p(prefix, 'Ux')].shape[1]    # initial/previous state    if init_state is None:        init_state = tensor.alloc(0., n_samples, dim)    assert context.ndim == 2, 'Context must be 2-d: #sample x dim'    # projected context to GRU gates    pctx_ = tensor.dot(context, tparams[_p(prefix, 'Wc')])    # projected context to hidden state proposal    pctxx_ = tensor.dot(context, tparams[_p(prefix, 'Wcx')])    def _slice(_x, n, dim):        if _x.ndim == 3:            return _x[:, :, n*dim:(n+1)*dim]        return _x[:, n*dim:(n+1)*dim]    # projected x to gates    state_belowx = tensor.dot(state_below, tparams[_p(prefix, 'Wx')]) + \        tparams[_p(prefix, 'bx')]    # projected x to hidden state proposal    state_below_ = tensor.dot(state_below, tparams[_p(prefix, 'W')]) + \        tparams[_p(prefix, 'b')]    # step function to be used by scan    # arguments    | sequences |outputs-info|       non-seqs    def _step_slice(m_, x_, xx_,    h_,       pctx_, pctxx_, U, Ux):        preact = tensor.dot(h_, U)        preact += x_        preact += pctx_        preact = tensor.nnet.sigmoid(preact)        r = _slice(preact, 0, dim)        u = _slice(preact, 1, dim)        preactx = tensor.dot(h_, Ux)        preactx *= r        preactx += xx_        preactx += pctxx_        h = tensor.tanh(preactx)        h = u * h_ + (1. - u) * h        h = m_[:, None] * h + (1. - m_)[:, None] * h_        return h    seqs = [mask, state_below_, state_belowx]    _step = _step_slice    shared_vars = [tparams[_p(prefix, 'U')],                   tparams[_p(prefix, 'Ux')]]    if one_step:        rval = _step(*(seqs+[init_state, pctx_, pctxx_]+shared_vars))    else:        rval, updates = theano.scan(_step,                                    sequences=seqs,                                    outputs_info=[init_state],                                    non_sequences=[pctx_,                                                   pctxx_]+shared_vars,                                    name=_p(prefix, '_layers'),                                    n_steps=nsteps,                                    profile=profile,                                    strict=True)    return rval

代码解析:

代码几乎与encoder的部分一模一样。这也难怪,因为seq2seq主要由encoder和decoder两部分组成。而,这份源代码中,encoder和decoder部分均采用GRU实现的。

先贴出公式吧。GRU decoder的公式如下:
这里,Context即为encoder最后一个隐藏层的状态

zt=σ(W(z)xt+U(z)st1+ContextWc)

rt=σ(W(r)xt+U(r)st1+ContextWc)

s˜t=tanh(rt(Uxst1)+Wxxt+ContextWcx)

st=(1zt)s˜t+ztst1

唯一谈谈跟encoder不同的部分,也就是one_step这个参数。
one_step=False,是指在train的时候,调用scan函数,直接进行n_step次迭代,输出隐藏层的全部状态。
one_step=True,是指在test的时候,不调用scan函数,而是一个timestep改变一次隐藏层,蹦出一个单词。

因此,这部分代码你可以参考上述4个公式和encoder相对应的代码阅读,这里就不再赘述了。

顺便一提,最终 return rval ,rval包含了decoder全部时刻的隐藏层状态。

继续返回nmt.py脚本中的build_model()函数吧!

3.1.8 nmt.py脚本中gen_sample()函数

值得注意的是,
当 stochastic=True的时候,采用的是随机采样选中下一个词;
当stochastic=False的时候,采用的是beam search获得目标句子,其中,k的值为Beam Size的大小(eg, k=5) 。

gen_sample()函数,主要完成测试输出目标句子的功能。

代码如下:

def gen_sample(tparams, f_init, f_next, x, options, trng=None, k=1, maxlen=30,               stochastic=True, argmax=False):    # k is the beam size we have    if k > 1:        assert not stochastic, \            'Beam search does not support stochastic sampling'    sample = []    sample_score = []    if stochastic:        sample_score = 0    live_k = 1    dead_k = 0    hyp_samples = [[]] * live_k    hyp_scores = numpy.zeros(live_k).astype('float32')    hyp_states = []    # get initial state of decoder rnn and encoder context    ret = f_init(x)    next_state, ctx0 = ret[0], ret[1]    next_w = [-1]  # indicator for the first target word (bos target)    for ii in xrange(maxlen):        ctx = numpy.tile(ctx0, [live_k, 1])        inps = [next_w, ctx, next_state]        ret = f_next(*inps)        next_p, next_w, next_state = ret[0], ret[1], ret[2]        if stochastic:            if argmax:                nw = next_p[0].argmax()            else:                nw = next_w[0]            sample.append(nw)            sample_score -= numpy.log(next_p[0, nw])            if nw == 0:                break        else:            cand_scores = hyp_scores[:, None] - numpy.log(next_p)            cand_flat = cand_scores.flatten()            ranks_flat = cand_flat.argsort()[:(k-dead_k)]            voc_size = next_p.shape[1]            trans_indices = ranks_flat / voc_size            word_indices = ranks_flat % voc_size            costs = cand_flat[ranks_flat]            new_hyp_samples = []            new_hyp_scores = numpy.zeros(k-dead_k).astype('float32')            new_hyp_states = []            for idx, [ti, wi] in enumerate(zip(trans_indices, word_indices)):                new_hyp_samples.append(hyp_samples[ti]+[wi])                new_hyp_scores[idx] = copy.copy(costs[idx])                new_hyp_states.append(copy.copy(next_state[ti]))            # check the finished samples            new_live_k = 0            hyp_samples = []            hyp_scores = []            hyp_states = []            for idx in xrange(len(new_hyp_samples)):                if new_hyp_samples[idx][-1] == 0:                    sample.append(new_hyp_samples[idx])                    sample_score.append(new_hyp_scores[idx])                    dead_k += 1                else:                    new_live_k += 1                    hyp_samples.append(new_hyp_samples[idx])                    hyp_scores.append(new_hyp_scores[idx])                    hyp_states.append(new_hyp_states[idx])            hyp_scores = numpy.array(hyp_scores)            live_k = new_live_k            if new_live_k < 1:                break            if dead_k >= k:                break            next_w = numpy.array([w[-1] for w in hyp_samples])            next_state = numpy.array(hyp_states)    if not stochastic:        # dump every remaining one        if live_k > 0:            for idx in xrange(live_k):                sample.append(hyp_samples[idx])                sample_score.append(hyp_scores[idx])    return sample, sample_score

代码解析:

这里,我先从 stochastic=True (即采用的是随机采样选中下一个词) 讲起,稍后再来分析Beam search的过程。
首先介绍一下gen_sample()各个参数的含义。


f_init 和f_next 这两个函数定义在nmt.py脚本的buile_sample()函数中,是具体实现目标单词一个接一个蹦出来的代码。buile_sample()函数特别重要,建议先阅读,有助于理解gen_sample()。移步至去nmt.py脚本中build_sample()函数瞧个究竟吧!

第22行: ret = f_init(x) 调用f_init(x),完成encoder阶段。此时,ret=[init_state, ctx]

第24行: next_w = [-1] ,代表将输出第一个目标单词,也就是emb第一个timestep初始化为全0.

第26行:进行maxlen次timestep迭代,或者知道遇到’eos’,目标句子的生成过程才会结束。

第27行: 这句话在Beam search起作用,当采用随机采样的时候,这里 ctx = numpy.tile(ctx0, [1, 1]),相当于ctx0复制了一次,所以ctx=ctx0。

第29行: ret = f_next(*inps) ,调用f_next(),完成decoder阶段。此时,ret=[next_p, next_w, next_state]

第32-40行: 表示采用随机采样方式生成目标句子的过程。
首先,判断argmax是否等于True,若等于,则根据输出的next_p中挑出概率最大的单词作为下一个单词(贪心算法的思想)。反之,下一个单词的输出则是根据随机采样方式获得,则nw=next_w[0]。把每次timestep输出的目标单词添加保存到sample变量中,同时,将相对应的误差添加到sample_socre变量中。

第41-91行: 采用beam search方式生成目标句子的过程,后面会详细介绍,这里暂时先跳过。

第94行: 将包含目标单词的变量sample,及其所对应的误差变量sample_socre 返回去。

继续返回nmt.py脚本中的train()函数吧!


3.1.9 nmt.py脚本中build_sample()函数

build_sampler函数,主要完成测试阶段,decoder部分,目标单词一个接一个输出的具体实现

代码如下:

def build_sampler(tparams, options, trng, use_noise):    x = tensor.matrix('x', dtype='int64')    n_timesteps = x.shape[0]    n_samples = x.shape[1]    # word embedding (source)    emb = tparams['Wemb'][x.flatten()]    emb = emb.reshape([n_timesteps, n_samples, options['dim_word']])    # encoder    proj = get_layer(options['encoder'])[1](tparams, emb, options,                                            prefix='encoder')    ctx = proj[0][-1]    ctx_mean = ctx    init_state = get_layer('ff')[1](tparams, ctx_mean, options,                                    prefix='ff_state', activ='tanh')    print 'Building f_init...',    outs = [init_state, ctx]    f_init = theano.function([x], outs, name='f_init', profile=profile)    print 'Done'    # y: 1 x 1    y = tensor.vector('y_sampler', dtype='int64')    init_state = tensor.matrix('init_state', dtype='float32')    # if it's the first word, emb should be all zero    emb = tensor.switch(y[:, None] < 0,                        tensor.alloc(0., 1, tparams['Wemb_dec'].shape[1]),                        tparams['Wemb_dec'][y])    # apply one step of gru layer    proj = get_layer(options['decoder'])[1](tparams, emb, options,                                            prefix='decoder',                                            mask=None, context=ctx,                                            one_step=True,                                            init_state=init_state)    next_state = proj    ctxs = ctx    # compute the output probability dist and sample    logit_lstm = get_layer('ff')[1](tparams, next_state, options,                                    prefix='ff_logit_lstm', activ='linear')    logit_prev = get_layer('ff')[1](tparams, emb, options,                                    prefix='ff_logit_prev', activ='linear')    logit_ctx = get_layer('ff')[1](tparams, ctxs, options,                                   prefix='ff_logit_ctx', activ='linear')    logit = tensor.tanh(logit_lstm+logit_prev+logit_ctx)    logit = get_layer('ff')[1](tparams, logit, options,                               prefix='ff_logit', activ='linear')    next_probs = tensor.nnet.softmax(logit)    next_sample = trng.multinomial(pvals=next_probs).argmax(1)    # next word probability    print 'Building f_next..',    inps = [y, ctx, init_state]    outs = [next_probs, next_sample, next_state]    f_next = theano.function(inps, outs, name='f_next', profile=profile)    print 'Done'    return f_init, f_next

代码解析:

代码几乎与build_model()相似,build_model()是给训练模型的时候用的,build_sample()是为测试模型服务的。

第11行: 返回encoder阶段隐藏层的状态,proj

第13、14行: 将encoder阶段最后一个隐藏层ht作为decoder阶段的Context

第15行: 初始化decoder的隐藏层状态s0
s0=tanh(ctxff_state_W+ff_state_b)

第20行:f_init函数的定义,theano的内置函数function,输入为[x],输出为outs,其中outs=[init_state, ctx]。其中,ctx为encoder的最后一个隐藏层的状态,作为Context;init_state为初始化decoder的隐藏层状态s0

第28行: switch函数原型为tensor.switch(Bool条件判断 ,True Operation,False Operation)。
即,若输出的目标单词是y1,则emb初始化为全0;若不是则emb=(yt1的word vector)。

第33行: 注意与build_model不一样的地方,在于这里的参数 one_step=True,emb=yt1。因此,返回的proj只是一个timestep对应的隐藏层状态。 proj=[dim],是1D的向量

第34-51行:
参考build_model()对应的代码,这里就不再赘述了。其中,next_probs=[n_word],是1D的向量。

第52行: 采用随机采样的方式挑选目标单词

next_sample = trng.multinomial(pvals=next_probs).argmax(1)

第58行: f_next函数的定义,theano的内置函数function,输入为[y,ctx,init_state],输出为[next_probs, next_sample,next_state]。

第59行: 最后将定义好计算公式的函数名 f_init,和f_next返回去供调用。 f_init对应encoder,f_next对应decoder。

继续返回nmt.py脚本中的gen_sample()函数吧!


3.2 测试部分test :

至此,train部分的代码已经解释完毕。下面继续分析test部分是如何根据训练好的模型,进行test测试的。还记得提前提醒过大家,test的时候采用的是Beam Search方式生成目标句子。

运行test.sh 脚本(./test.sh)即可以。test.sh代码如下:

export THEANO_FLAGS=device=cpu,floatX=float32#cd $PBS_O_WORKDIRpython ./translate.py -k 5 -n -p 1 \    $HOME/2016_seq2seq/dl4mt-tutorial-master/session1/models/model_session1.npz \    $HOME/2016_seq2seq/dl4mt-tutorial-master/dataset/train_question_10.tok.pkl \    $HOME/2016_seq2seq/dl4mt-tutorial-master/dataset/train_answer_10.tok.pkl \    $HOME/2016_seq2seq/dl4mt-tutorial-master/dataset/test_question_10.tok\    $HOME/2016_seq2seq/dl4mt-tutorial-master/dataset/predict_test_answer_10.tok

代码解析:

第一行: 配置代码运行是采用CPU还是GPU,训练的时候肯定要采用GPU,测试的时候选用CPU就足够了。这里,采用的是CPU。

运行translate.py 函数,同时包括7个输入参数。

1)-k 5:表示Beam search 的窗口大小为5。
2)-p 1:表示启动1个进程,这里运用了多进程的机制,你可以根据资源和测试集合的大小设置多个进程数目。
3)model存放的位置,也就是train阶段训练好的模型。
4)源句子字典的路径
5)目标句子的路径
6)待测试源句子的路径
7)系统根据模型生成目标句子的存放路径

接下来,马不停蹄继续去看translate.py脚本的代码吧!


3.2.1 translate.py 脚本

主要完成test集合的输出。7个初始化的参数分别如下:

1)-k 5:表示Beam search 的窗口大小为5。
2)-p 1:表示启动1个进程,这里运用了多进程的机制,你可以根据资源和测试集合的大小设置多个进程数目。
3)model存放的位置,也就是train阶段训练好的模型。
4)源句子字典的路径
5)目标句子的路径
6)待测试源句子的路径
7)系统根据模型生成目标句子的存放路径

代码如下:

'''Translates a source file using a translation model.'''import argparseimport numpyimport cPickle as pklfrom nmt import (build_sampler, gen_sample_test, load_params,                 init_params, init_tparams)from multiprocessing import Process, Queuedef translate_model(queue, rqueue, pid, model, options, k, normalize):    from theano.sandbox.rng_mrg import MRG_RandomStreams as RandomStreams    from theano import shared    trng = RandomStreams(1234)    use_noise = shared(numpy.float32(0.))    # allocate model parameters    params = init_params(options)    # load model parameters and set theano shared variables    params = load_params(model, params)    tparams = init_tparams(params)    # word index    f_init, f_next = build_sampler(tparams, options, trng, use_noise)    def _translate(seq):        # sample given an input sequence and obtain scores        sample, score = gen_sample(tparams, f_init, f_next,                                   numpy.array(seq).reshape([len(seq), 1]),                                   options, trng=trng, k=k, maxlen=200,                                   stochastic=False)        # normalize scores according to sequence lengths        if normalize:            lengths = numpy.array([len(s) for s in sample])            score = score / lengths        sidx = numpy.argmin(score)        return sample[sidx]    while True:        req = queue.get()        if req is None:            break        idx, x = req[0], req[1]        print pid, '-', idx        seq = _translate(x)        rqueue.put((idx, seq))    returndef main(model, dictionary, dictionary_target, source_file, saveto, k=5,         normalize=False, n_process=5, chr_level=False):    # load model model_options    with open('%s.pkl' % model, 'rb') as f:        options = pkl.load(f)    # load source dictionary and invert    with open(dictionary, 'rb') as f:        word_dict = pkl.load(f)    word_idict = dict()    for kk, vv in word_dict.iteritems():        word_idict[vv] = kk    word_idict[0] = '<eos>'    word_idict[1] = 'UNK'    # load target dictionary and invert    with open(dictionary_target, 'rb') as f:        word_dict_trg = pkl.load(f)    word_idict_trg = dict()    for kk, vv in word_dict_trg.iteritems():        word_idict_trg[vv] = kk    word_idict_trg[0] = '<eos>'    word_idict_trg[1] = 'UNK'    # create input and output queues for processes    queue = Queue()    rqueue = Queue()    processes = [None] * n_process    for midx in xrange(n_process):        processes[midx] = Process(            target=translate_model,            args=(queue, rqueue, midx, model, options, k, normalize,))        processes[midx].start()    # utility function    def _seqs2words(caps):        capsw = []        for cc in caps:            ww = []            for w in cc:                if w == 0:                    break                ww.append(word_idict_trg[w])            capsw.append(' '.join(ww))        return capsw    def _send_jobs(fname):        with open(fname, 'r') as f:            for idx, line in enumerate(f):                if chr_level:                    words = list(line.decode('utf-8').strip())                else:                    words = line.strip().split()                x = map(lambda w: word_dict[w] if w in word_dict else 1, words)                x = map(lambda ii: ii if ii < options['n_words'] else 1, x)                x += [0]                queue.put((idx, x))        return idx+1    def _finish_processes():        for midx in xrange(n_process):            queue.put(None)    def _retrieve_jobs(n_samples):        trans = [None] * n_samples        for idx in xrange(n_samples):            resp = rqueue.get()            trans[resp[0]] = resp[1]            if numpy.mod(idx, 10) == 0:                print 'Sample ', (idx+1), '/', n_samples, ' Done'        return trans    print 'Translating ', source_file, '...'    n_samples = _send_jobs(source_file)    trans = _seqs2words(_retrieve_jobs(n_samples))    _finish_processes()    with open(saveto, 'w') as f:        print >>f, '\n'.join(trans)    print 'Done'if __name__ == "__main__":    parser = argparse.ArgumentParser()    parser.add_argument('-k', type=int, default=5)    parser.add_argument('-p', type=int, default=5)    parser.add_argument('-n', action="store_true", default=False)    parser.add_argument('-c', action="store_true", default=False)    parser.add_argument('model', type=str)    parser.add_argument('dictionary', type=str)    parser.add_argument('dictionary_target', type=str)    parser.add_argument('source', type=str)    parser.add_argument('saveto', type=str)    args = parser.parse_args()    main(args.model, args.dictionary, args.dictionary_target, args.source,         args.saveto, k=args.k, normalize=args.n, n_process=args.p,         chr_level=args.c)

代码解析:

第89行-93行:
queue: 源句子的队列
rqueue: 翻译后的目标句子的队列

调用multiprocessing.Process函数(),支持多进程操作,可以大大体高运行速度。
target:调用的函数对象,即translate_model()函数;
args:为target调用的参数元祖

因此,translate_model()才是真正test的核心!!! 去瞧个究竟吧!!!

第46行: 只要queue(源句子的队列)有内容,便一直循环。

第47、51行: req = queue.get(), 每次弹出一个源句子,即x=req[1]。

第53行: seq = _translate(x),调用_translate(x)函数,最终返回seq,即翻译后的目标句子。

_translate()函数主要调用了nmt.py脚本中的gen_sample()函数。这里,stochastic=False,k=5,表示采用beam search方式生成目标句子。
最终gen_sample()函数返回2个变量sample, sample_score。其中,sample保存k个候选句子。sample_score保存k个句子对应的概率(经过log运算操作过的概率,概率越小,目标句子生成效果越好)。


_translate()根据返回来的sample,sample_score,选取平均概率最小的句子返回去(最终生成的目标句子)。
因此,请移步到nmt.py脚本中的gen_sample函数,瞧一瞧beam search的具体实现!

第134行:调用_send_jobs()函数,把源句子单词转换成数字,并存储在queue队列中。最终返回源句子的数目。 即n_samples=源句子的数目。

第135行: 首先调用_retrieve_jobs(),每进行10个Sample(源句子的样本)的翻译,则打印一次,最终返回数字目标句子的列表对象,最终翻译好的句子保存在rqueue队列中。。其次,调用_seqs2words(),把数字目标句子转化成单词目标句子,并返回单词目标句子的列表对象。

第138行: 存储单词目标句子到相对应的路径下。


3.2.2 nmt.py脚本中的gen_sample()函数

具体实现beam search操作的代码。

train和test都调用了gen_sample()函数,为什么呢?

train的时候采用了随机采样的方式。因为stochastic默认为True,即是用随机采样的方式选择Decoder的下一个单词,当然你也可以手动设置stochastic=False 和k的值,选用beam search)

test的时候采用了beam search的方式。

之所以Train的时候要调用gen_sample()函数,目的是为了让你看一下从Train集挑5个case,Decoder后的效果如何(主要是为了看看训的效果大体如何),这一步其实是多余的,因为从valid集看cost,才是最终确定best model参数的关键,因为valid集在train的时候是不可见的,valid集的cost低,才能保证test的效果好。

def gen_sample(tparams, f_init, f_next, x, options, trng=None, k=1, maxlen=30,               stochastic=True, argmax=False):    # k is the beam size we have    if k > 1:        assert not stochastic, \            'Beam search does not support stochastic sampling'    sample = []    sample_score = []    if stochastic:        sample_score = 0    live_k = 1    dead_k = 0    hyp_samples = [[]] * live_k    hyp_scores = numpy.zeros(live_k).astype('float32')    hyp_states = []    # get initial state of decoder rnn and encoder context    ret = f_init(x)    next_state, ctx0 = ret[0], ret[1]    next_w = [-1]  # indicator for the first target word (bos target)    for ii in xrange(maxlen):        ctx = numpy.tile(ctx0, [live_k, 1])        inps = [next_w, ctx, next_state]        ret = f_next(*inps)        next_p, next_w, next_state = ret[0], ret[1], ret[2]        if stochastic:            if argmax:                nw = next_p[0].argmax()            else:                nw = next_w[0]            sample.append(nw)            sample_score -= numpy.log(next_p[0, nw])            if nw == 0:                break        else:            cand_scores = hyp_scores[:, None] - numpy.log(next_p)            cand_flat = cand_scores.flatten()            ranks_flat = cand_flat.argsort()[:(k-dead_k)]            voc_size = next_p.shape[1]            trans_indices = ranks_flat / voc_size            word_indices = ranks_flat % voc_size            costs = cand_flat[ranks_flat]            new_hyp_samples = []            new_hyp_scores = numpy.zeros(k-dead_k).astype('float32')            new_hyp_states = []            for idx, [ti, wi] in enumerate(zip(trans_indices, word_indices)):                new_hyp_samples.append(hyp_samples[ti]+[wi])                new_hyp_scores[idx] = copy.copy(costs[idx])                new_hyp_states.append(copy.copy(next_state[ti]))            # check the finished samples            new_live_k = 0            hyp_samples = []            hyp_scores = []            hyp_states = []            for idx in xrange(len(new_hyp_samples)):                if new_hyp_samples[idx][-1] == 0:                    sample.append(new_hyp_samples[idx])                    sample_score.append(new_hyp_scores[idx])                    dead_k += 1                else:                    new_live_k += 1                    hyp_samples.append(new_hyp_samples[idx])                    hyp_scores.append(new_hyp_scores[idx])                    hyp_states.append(new_hyp_states[idx])            hyp_scores = numpy.array(hyp_scores)            live_k = new_live_k            if new_live_k < 1:                break            if dead_k >= k:                break            next_w = numpy.array([w[-1] for w in hyp_samples])            next_state = numpy.array(hyp_states)    if not stochastic:        # dump every remaining one        if live_k > 0:            for idx in xrange(live_k):                sample.append(hyp_samples[idx])                sample_score.append(hyp_scores[idx])    return sample, sample_score

代码解析:

其中,f_init()和f_next()的具体操作定义在build_sample()函数中。

第22行: 调用f_init(x),完成源句子encoder的过程,返回[decoder的s0,Context]

第24行: next_w=[-1],代表emb初始化为全0,表示即将输出目标句子的第一个单词。

第26行: 迭代maxlen次操作,或者直到句子翻译结束。

第41行: 具体beam search的过程,这里k=5

第42行: 调用f_next,返回 |V|个词的概率(为了简单测试,我取|v|=1000),然后取log运算,,作为得分。概率越接近1,得分越趋近于0;反之,概率越趋近于0,得分趋于无穷大。
其中,cand_scores的维度大小随输出next_w个数变化。当输入为1个单词的时候,cand_scores是1D的向量,为|V|;当输入为n时,也就是并行输入n个单词,cand_scores=[n , n_word],这里n_word就是|V|target

第44行: 每次是取Top (k-dead_k)概率值最低的索引(因为对概率值做了log运算,越接近1,值越小),argsort()返回的是索引,不是具体的概率值。有一点值得注意的是,并不是每次都取Top k个

第47行: trans_indices = ranks_flat / voc_size 表示上一个单词的索引。

第48行: word_indices = ranks_flat % voc_size 表示|V|target的索引。

第49行: 根据argsort()返回的是索引,找到对应的概率值(做了log运算处理)

第55-58行: new_hyp_samples保存所有的目标句子(包括翻译完成(句子结尾是0)和还未完成的句子)。

hyp_samples:保留那些还仍未完成的目标句子,即仍然是激活状态,需要继续驱动产生下一个单词的句子。

new_hyp_samples.append(hyp_samples[ti]+[wi])这句代码便是根据索引 trans_indices找到上一个单词,根据word_indices从|V个目标字典找到具体的单词,然后添加到 new_hyp_samples中。

第66-71行:

遍历new_hyp_samples:
如果new_hyp_samples中发现有结束的句子,也就是句末出现0,那么就把该句子添加到sample中。因此,sample变量是保存那些已经翻译完毕的目标句子。同时,dead_k=dead_k+1,当dead_k>=k,表示所有的句子均翻译完毕了。
反之,如果new_hyp_samples中句子没有结束,则进行如下操作:
1)new_live_k = new_live_k + 1,new_live_k表示处于激活状态的句子数目。如果new_like_k<1,表示翻译结束了。
2)将仍为翻译完毕的目标句子添加到hyp_samples中,继续操作。

第84行: 挑选所有未完成句子(hyp_samples)的最后一个词,最为下一次输入,即next_p。即第一次next_p是输入一个单词,第二次开始,next_p并行输入len(hyp_samples)个单词。

第94行: 最终迭代结束,sample里面的元素就是我们要的候选句子,sample_score便是对应候选句子经过log运算处理的概率。整个句子概率越低,表示翻译质量越好。

下图展示了迭代的过程。
这里写图片描述

继续返回translate.py脚本吧!

4 0
原创粉丝点击