自然语言处理

完整示例训练过程用到的API

 1 import tensorflow as tf
 2 
 3 dataset = tf.data.TextLineDataset(file_path)
 4 # tf.string_split
 5 # tf.string_to_number
 6 # tf.size
 7 # tf.data.Dataset.zip
 8 # tf.concat
 9 
10 # tf.logical_and
11 # tf.logical_not
12 # tf.logical_or
13 #
14 # tf.greater
15 # tf.greater_equal
16 # tf.less
17 # tf.less_equal
18 # tf.not_equal
19 
20 # dataset.map
21 # dataset.filter
22 # dataset.shuffle
23 # dataset.padded_batch
24 # dataset.make_one_shot_iterator
25 # dataset.make_initializable_iterator
26 
27 # tf.TensorShape
28 # tf.nn.rnn_cell.MultiRNNCell
29 # tf.nn.rnn_cell.BasicLSTMCell
30 # tf.transpose
31 # tf.random_uniform_initializer
32 # tf.nn.embedding_lookup
33 # tf.nn.dropout
34 # tf.nn.dynamic_rnn
35 # tf.nn.sparse_softmax_cross_entropy_with_logits
36 # tf.nn.softmax_cross_entropy_with_logits
37 # tf.sequence_mask
38 
39 # tf.reduce_sum
40 # tf.reduce_mean
41 # tf.reduce_max
42 # tf.reduce_min
43 # tf.reduce_any
44 # tf.reduce_all
45 # tf.reduce_prod
46 
47 # tf.to_float
48 # tf.to_double
49 # tf.to_int32
50 # tf.to_int64
51 # tf.to_bfloat16
52 
53 # optimizer = tf.train.GradientDescentOptimizer(learning_rate=1.0)
54 # optimizer.apply_gradients
55 
56 # tf.clip_by_global_norm
57 # tf.clip_by_value
58 # tf.clip_by_norm
59 # tf.clip_by_average_norm

tf.reduce_all和tf.reduce_any

 1 a = [[False, True], [True, True], [True, True]]
 2 l = tf.convert_to_tensor(a)
 3 s1 = tf.reduce_all(l)  # Computes the "logical and" of elements across dimensions of a tensor.
 4 s2 = tf.reduce_any(l)  # Computes the "logical or" of elements across dimensions of a tensor.
 5 
 6 print(l, s1, s2)
 7 with tf.Session() as sess:
 8     print(sess.run([l, s1, s2]))
 9 
10 # [array([[False,  True],
11 #        [ True,  True],
12 #        [ True,  True]]), False, True]

编码过程用到的API

1 # tf.convert_to_tensor
2 # init_array = tf.TensorArray
3 # init_array.read  返回一个元素
4 # init_array.write  返回整个数组
5 # init_array.stack  一次性取出全部
6 #
7 # tf.reduce_all
8 # self.dec_cell.call
9 # tf.while_loop

这一章将介绍如何使用深度学习方法解决自然语言处理问题。

9.1 语言模型的背景知识

语言模型是自然语言处理问题中一类最基本的问题,它有着非常广泛的应用,也是理解更加复杂的自然语言处理问题的基础。

9.1.1 语言模型简介

假设一门语言中所有可能的句子服从某一个概率分布,每个句子出现的概率加起来为1,那么“语言模型”的任务就是预测每个句子在语言中出现的概率。把句子看成单词的序列,语言模型可以表示为一个计算p(w1, w2, w3,…, wm)的模型。语言模型仅仅对句子出现的概率进行建模,并不尝试去“理解”句子的内容含义。

语言模型有很多应用。很多生成自然语言文本的应用都依赖语言模型来优化输出文本的流畅性。生成的句子在语言模型中的概率越高,说明其越有可能是一个流程、自然的句子。例如在输入法中,假设输入的拼音串为“xianzaiquna”,输出可能是”西安在去哪“,也可能是”现在去哪“,这时输入法就利用语言模型比较两个输出的概率,得出”现在去哪“更有可能是用户所需要的输出。在统计机器翻译的噪声信道模型(Noise Channel Model)中,每个候选翻译的概率由一个翻译模型和一个语言模型共同决定,其中的语言模型就起到了在目标语言中挑选较为合理的句子的作用。在9.3小节将看到,神经网络机器翻译的Seq2Seq模型可以看作是一个条件语言模型(Conditional Language Model),它相当于是在给定输入的情况下对目标语言的所有句子估算概率,并选择其中概率最大的句子作为输出。

计算一个句子的概率:

首先一个句子可以被看成是一个单词序列:

  S = (w1, w2, w3,…, wm),

其中m为句子的长度。那么,它的概率可以表示为

  p(S) = p(w1, w2, w3,…, wm) = 

     p(w1)p(w2|w1)p(w3|w1, w2,) … p(wm|w1, w2, w3,…, wm-1),

p(wm|w1, w2, w3,…, wm-1)表示,已知前m-1个单词时,第m个单词为wm的条件概率。如果能对这一项建模,那么只要把每个位置的条件概率相乘,就能计算一个句子出现的概率。然而一般来说,任何一门语言的词汇量都很大,词汇的组合更是不计其数。假设一门语言的词汇量为V,如果要将p(wm|w1, w2, w3,…, wm-1)的所有参数保存一个模型里,将需要vm个参数,一般的句子长度远远超出了实际可行的范围。为了评估这些参数的取值,常见的方法由n-gram模型、决策树、最大熵模型、条件随机场、神经网络模型等。这里先以其中最简单的n-gram模型来介绍语言模型问题。

为了控制参数数量,n-gram模型做了一个有限历史假设:当前单词的出现概率仅仅与前面的n-1个单词相关,因此以上公式可以近似为

  p(S) = p(w1, w2, w3,…, wm) = Πimp(wi| w1, w2, w3,…, wi-1)

n-gram模型里的n指的是当前单词依赖它前面的单词的个数。通常n可以取1、2、3、4。n-gram模型中需要预估的参数为条件概率p(wi|wi-n+1,…, wi-1)。假设某种语言的单词表大小为V,那么n-gram模型需要估计的不同参数数量为O(vn)量级。当n越大时,模型在理论上越准确,但也越复杂,需要的计算量和训练语料数据量也就越大,因为n取>=4的情况非常少。

n-gram模型的参数一般采用最大似然估计(Maximum Likelihood Estimation, MLE)方法计算:

  p(wi|wi-n+1,…, wi-1) = C(wi-n+1,…, wi-1, wi) / C(wi-n+1,…, wi-1),

其中C(X)表示单词序列X在训练语料中出现的次数。训练语料的规模越大,参数估计的结果越可靠。但即使训练数据的规模非常大时,还是有很多单词序列在训练语料中不会出现,这就会导致很多参数为0。为了避免因为乘以0而导致整个句子概率为0,使用最大似然估计方法时需要加入平滑避免参数取值为0。

9.1.2 语言模型的评价方法

语言模型效果好坏的常用评价标准是复杂度(perplexity)。在测试集上得到的复杂度越低,说明建模效果越好。计算perplexity值的公式如下:

  

简单来说,perplexity值刻画的是语言模型预测一个语言样本的能力。

从上面的定义中可以看出,perplexity实际是计算每个单词得到的概率倒数的几何平均,因此perplexity可以理解为平均分支系数(average branching factor),即模型预测下一个词时的平均可选择数量。例如,考虑一个由0~9这10个数字随机组成的长度为m的序列,由于这10个数字出现的概率是随机的,所以每个数字出现的概率是1//10,因此,在任何时刻,模型都有10个等概率的候选答案可以选择,于是perplexity就是10。计算过程如下:

  

在语言模型的训练中,通常采用perplexity的对数表达形式:

  

相比先乘积再求平方根的方式,采用对数形式(因为可以转为加法运算)可以加速计算,同时避免概率乘积数值过小导致浮点数向下溢出的问题。

在数学上,log perplexity可以看成真实分布与预测分布之间的交叉熵。假设x是一个离散变量,μ(x)和v(x)是两个与x相关的概率分布,那么μ和v之间交叉熵的定义是在分布μ下-log(v(x))的期望值:

  

把x看作单词,μ(x)为每个位置上单词的真实分布,v(x)为模型的预测分布p(wi|wi-n+1,…, wi-1),就可以看出log perplexity和交叉熵是等价的。唯一的区别在于,由于语言的真实分布是未知的,因此在log perplexity的定义中,真实分布用测试语料中的取样代替,即认为在给定上文w1, w2, w3,…, wi-1的条件下,语料中出现单词 w的概率为1,出现其他单词的概率均为0。

在神经网络模型中,p(wi| w1, w2, w3,…, wi-1)分布通常是由一个softmax层产生的。这时tensorflow提供了两个方便计算交叉熵的函数:tf.nn.softmax_cross_entropy_with_logits和tf.nn.sparse_softmax_cross_entropy_with_logits。

 1 import tensorflow as tf
 2 
 3 # tf.nn.softmax_cross_entropy_with_logits与tf.nn.sparse_softmax_cross_entropy_with_logits的区别在于
 4 # 前者需要预测目标(即label)以概率分布的形式给出
 5 
 6 # 假设词汇表大小为3,语料包含有2个单词,分别为2和0
 7 word_labels = tf.constant([2, 0])
 8 
 9 # 假设模型对两个单词预测时,产生的logit分别为[2.0, -1.0, 3.0]和[1.0, 0.0, -0.5]
10 predict_logits = tf.constant([[2.0, -1.0, 3.0], [1.0, 0.0, -0.5]])
11 
12 # 交叉熵
13 loss = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=word_labels, logits=predict_logits)
14 
15 # softmax_cross_entropy_with_logits需要将预测目标(即label)以概率分布的形式给出
16 word_prob_distribution = tf.constant([[0.0, 0.0, 1.0], [1.0, 0.0, 0.0]])
17 loss2 = tf.nn.softmax_cross_entropy_with_logits(labels=word_prob_distribution, logits=predict_logits)
18 
19 with tf.Session() as sess:
20     print(sess.run([loss, loss2]))
21 
22 # 结果: 对应两个预测的perplexity损失
23 # [array([0.32656264, 0.4643688 ], dtype=float32), array([0.32656264, 0.4643688 ], dtype=float32)]
24 
25 # 由于softmax_cross_entropy_with_logits允许提供一个概率分布,因此在使用时有更大的自由度。
26 # 例如,一种叫label smoothing的技巧是将正确数据的概率设为一个比1.0略小的值,将错误数据的概率设为比0.0略大的值,这样可以避免模型与数据过拟合,在某些时候可以提高训练效果。
27 word_prob_smooth = tf.constant([[0.01, 0.01, 0.98], [0.98, 0.01, 0.01]])
28 loss3 = tf.nn.softmax_cross_entropy_with_logits(labels=word_prob_smooth, logits=predict_logits)
29 
30 with tf.Session() as sess:
31     print(sess.run(loss3))
32 # [0.37656265 0.48936883]

9.2 神经语言模型

上节提到,n-gram模型为了控制参数数量,需要将上下文信息控制在几个单词以内。也就是说,在预测下一个单词时,n-gram模型只能考虑前n个单词的信息,这对语言模型的能力造成了很大的限制。与之相比,循环神经网络可以将任意长度的上文信息存储在隐藏状态中。因此使用循环神经网络作为语言模型有着天然的优势。

由于隐藏状态的维度有限,它并不能真的存储’所有‘的上文信息,通常来说,距离越远的上文对下一个单词的影响越小,因此存储’所有‘信息也并非必要。研究表面,在神经语言模型中保留13个单词的上文信息大致可以取得与保留所有上文信息相同的效果。

与图像数据不同,自然语言文本数据无法直接被当做数据值提供给神经网络,需要进行预处理。

9.2.1 预处理

9.2.2 batching

9.2.3 完整示例

9.2.1 PTB数据集的预处理

 PTB(Penn Treebank Dataset)文本数据集是目前语言模型学习中使用最广泛的数据集。

下载地址:http://www.fit.vutbr.cz/~imikolov/rnnlm/simple-examples.tgz

本节只需关心simple-example/data下的ptb.train.txt、ptb.valid.txt、ptb.test.txt三个文件。这三个数据文件已经过预处理,相邻单词之间用空格隔开。数据集中共包含9998个不同的单词词汇,加上稀有词语的特殊符号<unk>和语句结束符在内,一共有10000个词汇。在使用perplexity比较不同的语言模型时,文本的预处理和词汇表必须保持一致。例如,如果一个语言模型将“don't”视为一个单词,而另一个语言模型在预处理时将其拆为“don”和“t”两个单词,那么这两个语言模型得到的perplexity值就是不可比较的。近年来关于语言模型方面的论文大多采用了Mikolov提供的这一预处理后的数据版本,由此保证了论文之间具有可比性。
为了将文本转化为模型可以读入的单词序列,需要将不同的词汇分别映射为不同的整数编号。
1). 首先按照词频顺序为每个词汇分配一个编号,然后将词汇表保存到一个独立的vocab文件中。
生成词汇表:一行一个单词,按词频从大到小排序,
 1 import collections
 2 from operator import itemgetter
 3 
 4 RAW_DATA = '/home/yangxl/files/ptb/ptb.train.txt'
 5 VOCAB_OUTPUT = '/home/yangxl/files/ptb/ptb.vocab'
 6 
 7 counter = collections.Counter()
 8 with open(RAW_DATA, 'r') as f:
 9     for line in f.readlines():
10         for word in line.strip().split():
11             counter[word] += 1
12 
13 # 按词频顺序对单词进行排序
14 sorted_word_to_cnt = sorted(counter.items(), key=itemgetter(1), reverse=True)
15 sorted_words = [x[0] for x in sorted_word_to_cnt]
16 
17 # 稍后我们需要在文本换行处加入句子结束符'<eos>',这里预先将其加入词汇表。
# 这个句子结束符应该就相当于翻译模型中的'_'吧。
18 sorted_words = ['<eos>'] + sorted_words 19 20 # 在9.3.2小节处理机器翻译数据时,除了'<eos>',还需要将'<unk>'和句子起始符'<sos>'加入词汇表,并从词汇表中删除低频慈湖。
# 在PTB数据中,因为输入数据已经将低频词汇替换为'unk',因此不需要这一步骤。
# 恰好10000个词汇。
21 # sorted_words = ['<unk>', '<sos>', '<eos>'] + sorted_words 22 # if len(sorted_words) > 10000: 23 # sorted_words = sorted_words[:10000] 24 25 with open(VOCAB_OUTPUT, 'w') as f_out: 26 for word in sorted_words: 27 f_out.write(word + ' ')

在确定了词汇表(就一个词汇表)之后,再将训练文件、测试文件等根据词汇表文件转化为单词编号。每个单词的编号就是它在词汇文件中的行号。(说是替换,其实是重新创建了一个新文件)

 1 RAW_DATA = '/home/error/simple-examples/data/ptb.test.txt'
 2 VACAB = '/home/error/ptb/test.vocab'
 3 OUTPUT_DATA = '/home/error/ptb/ptb.test'
 4 
 5 with codecs.open(VACAB, 'r', 'utf-8') as f:
 6     vocab = [word.strip() for word in f.readlines()]
 7 word_to_id = {v: k for k, v in enumerate(vocab)}
 8 print(word_to_id)
 9 print(word_to_id['<eos>'])
10 
11 def get_id(word):
12     return word_to_id[word] if word in word_to_id else word_to_id['<unk>']
13 
14 fin = codecs.open(RAW_DATA, 'r', 'utf-8')
15 fout = codecs.open(OUTPUT_DATA, 'w', 'utf-8')
16 
17 for line in fin:
18     words = line.strip().split() + ['<eos>']  # 读取出来的行,每行都要加一个换行符(为了替换为序号),在读取并strip时去掉了,所以得加上
19     out_line = ' '.join([str(get_id(w)) for w in words]) + '
'  # 这个换行符只是为了换行(上面那个是为了替换为序号)
20     fout.write(out_line)
21 
22 fin.close()
23 fout.close()

上面的实例使用了文本文件来保存经过处理的数据。在实际工程中,通常使用TFRecord格式来提高读写效率。

虽然预处理原则上可以放在TF的Dataset框架中与读取文本同时进行,但在工程实践上,保存处理好的数据有几个重要的优点:第一,在调试模型的过程中,可以保证不同模型采取的预处理步骤相同;第二,减小文件体积,节省磁盘读取的时间;第三,方便对预处理步骤本身进行debug,例如,在模型训练效果不理想时,只需检查最终的数据文件就可以知道是不是预处理过程出了问题。

9.2.2 PTB数据的batching方法

在文本数据中,由于每个句子的长度不同,又无法像图像那样调整到固定维度,因为在对文本数据进行batching时需要采取一些特殊操作。最常见的方法是使用填充(padding),将同一batch内的句子长度补齐。

但是,在PTB数据集中,每个句子并非随机抽取的文本,而是在上下文之间有关联的内容。语言模型为了利用上下文信息,必须将前面句子的信息传递到后面的句子。为了解决这个问题,通常采用的是另一种batching方法。

如果模型大小没有限制,那么最理想的设计是将整个文档前后连接起来,当做一个句子来训练。但现实中这是无法实现的。例如PTB数据总共约有19万词,若将整个文档放入一个计算图,循环神经网络将展开成一个19万层的前馈网络。这样会导致计算图过大,另外序列过长可能造成训练中梯度爆炸的问题。

对此问题的解决方法是,将长序列截断为固定长度的子序列。循环神经网络在处理完一个子序列后,它最终的隐藏状态将复制到下一个序列作为初始值,这样在前向计算时,效果等同于一次性顺序地读取了整个文档;而在反向传播时,梯度则只在每个子序列内部传播。

为了利用计算时的并行能力,我们希望每一次计算可以对多个句子进行并行处理,同时又要尽量保证batch之间的上下文连续。解决方案是,先将整个文档切分成若干连续段落(可以理解为几行),再让batch中的每个位置负责其中一段(可以理解为纵向,一个batch含有所有连续段落的一小部分)。例如,如果batch大小为4,为了让batch的每个位置负责一个子序列,需要将全文平均分为4个子序列,这样每个子文档内部的所有数据仍可以被顺序处理。

下面的代码从文本文件中读取数据,并按上面介绍的方案将数据整理成batch。由于PTB数据集比较小,因此可以直接将整个数据集一次性读入内存。

 1 import numpy as np
 2 
 3 TRAIN_DATA = '/home/yangxl/files/ptb/ptb.train'
 4 TRAIN_BATCH_SIZE = 20
 5 TRAIN_NUM_STEP = 35
 6 
 7 
 8 # 从文件中读取数据,并返回包含单词编号的数组
 9 def read_data(file_path):
10     with open(file_path, 'r') as fin:
11         # 将整个文档读进一个长字符串
12         id_string = ' '.join([line.strip() for line in fin])
13     # 将读取的单词编号转为整数
14     id_list = [int(w) for w in id_string.split()]
15     return id_list
16 
17 
18 def make_batches(id_list, batch_size, num_step):
19     # batch的数量,即每一个子序列在纵向上能切分为多少小段:1327段;num_step为每一小段的大小,即一个句子的长度;
20     # 每个batch包含的单词数量为batch_size * num_step
21     num_batches = (len(id_list) - 1) // (batch_size * num_step)  # 这里减1,是为了预留一个数据当作label使用
22 
23     # 如9-4图所示,将数据整理成一个维度为[batch_size, num_batches * num_step]的二维数组
24     data = np.array(id_list[: num_batches * batch_size * num_step])  # 后面689个数据未用到。
25     data = np.reshape(data, [batch_size, num_batches * num_step])  #
26     # print(data.shape)  # (20, 46445)
27 
28     # 沿着第二个维度将数据切分成num_batches个shape为(batch_size, num_step)的batch,存入一个列表
29     data_batched = np.split(data, num_batches, axis=1)  # (1327, 20, 35)
30 
31     # label的处理与data一样。
32     label = np.array(id_list[1: num_batches * batch_size * num_step + 1])
33     label = np.reshape(label, [batch_size, num_batches * num_step])
34     label_batches = np.split(label, num_batches, axis=1)
35 
36     # 返回一个长度为num_batches的数组,
37     return list(zip(data_batched, label_batches))  # (1327, 2, 20, 35)
38 
39 
40 def main():
41     train_batches = make_batches(read_data(TRAIN_DATA), TRAIN_BATCH_SIZE, TRAIN_NUM_STEP)
42 
43 
44 if __name__ == '__main__':
45     main()

9.2.3 基于循环神经网络的神经语言模型

如图9-1所示,与第8章介绍的循环神经网络相比,NLP应用中主要多了两个层:词向量层(embedding)和softmax层。

词向量层

在输入层,每个单词用一个实数向量表示,这个向量被称为“词向量”。词向量可以形象地理解为将词汇表嵌入到一个固定维度的实数空间里。将单词编号转化为词向量主要有两大作用。

1. 降低输入的维度。如果不使用词向量,而直接将单词以one-hot vector的形式输入循环神经网络,那么输入的维度大小将与词汇表大小相同,通常在10000以上。而词向量的维度通常在200~1000之间,这将大大减少循环神经网络的参数数量与计算量。

2. 增加语义信息。简单的单词编号是不包含任何语义信息的。两个单词之间编号相近,并不意味着它们的含义有任何关联。而词向量层将稀疏的编号转化为稠密的向量表示,这使得词向量有可能包含更为丰富的信息。在自然语言应用中学习得到的词向量通常会将含义相似的词赋予取值相近的词向量值,使得上层的网络可以更容易地抓住相似单词之间的共性。例如,因为猫和狗都需要吃东西,因此在预测下文中出现单词“吃”的概率时,上文中出现“猫”或“狗”带来的影响可能是相似的。这样的任务训练出来的词向量中,代表“猫”和“狗”的词向量取值很可能是相似的。

假设词向量的维度是EMB_SIZE,词汇表的大小为VOCAB_SIZE,那么所有单词的词向量可以放入一个大小为VOCAB_SIZE × EME_SIZE的矩阵内。在读取词向量时,可以调用tf.nn.embedding_lookup方法。

 1 import tensorflow as tf
 2 
 3 BATCH_SIZE = 20
 4 NUM_STEP = 35
 5 VOCAB_SIZE = 1000
 6 EMB_SIZE = 200
 7 
 8 input_data = tf.placeholder(dtype=tf.int64, shape=[BATCH_SIZE, NUM_STEP])  # int32, int64
 9 embedding = tf.get_variable('embedding', [VOCAB_SIZE, EMB_SIZE])
10 input_embedding = tf.nn.embedding_lookup(embedding, input_data)
11 # 输出矩阵比输入矩阵多一个维度,新增维度的大小为EMB_SIZE。
12 # 在语言模型中,一般input_data的维度为batch_size * num_steps,而输出的input_embedding的维度是batch_size * num_steps * EMB_SIZE.
13 print(input_embedding)  # Tensor("embedding_lookup/Identity:0", shape=(20, 35, 200), dtype=float32)

softmax层

softmax层的作用是将循环神经网络的输出转化为一个单词表中每个单词的输出概率。

为此需要两个步骤:第一,使用一个线性映射将循环神经网络的输出映射为一个维度与词汇表大小相同的向量,这一步的输出叫做logits.

 1 import tensorflow as tf
 2 
 3 BATCH_SIZE = 20
 4 NUM_STEP = 35
 5 HIDDEN_SIZE = 10
 6 VOCAB_SIZE = 1000
 7 
 8 # 定义线性映射用到的参数。
 9 weight = tf.get_variable('weight', shape=[HIDDEN_SIZE, VOCAB_SIZE])
10 bias = tf.get_variable('bias', [VOCAB_SIZE])
11 # 计算线性映射。output是RNN的输出,shape=(batch_size * num_steps, HIDDEN_SIZE)
12 output = tf.placeholder(tf.float32, shape=[BATCH_SIZE * NUM_STEP, HIDDEN_SIZE])
13 logits = tf.nn.bias_add(tf.matmul(output, weight), bias)
14 print(logits)  # Tensor("BiasAdd:0", shape=(700, 1000), dtype=float32)

第二,调用softmax将logits转化为和为1的概率分布。语言模型的每一步输出都可以看作一个分类问题:在VOCAB_SIZE个可能的类别中决定这一步最可能输出的单词

1 probs = tf.nn.softmax(logits)  # 概率;probs的维度与logits的维度相同

模型训练通常并不关心概率的具体取值,而更关心最终的log perplexity,因此可以调用tf.nn.sparse_softmax_cross_entropy_with_logits方法直接从logits计算log perplexity作为损失函数:

1 # labels是一个大小为[batch_size * num_step]的一维数组,包含每个位置正确的单词编号。
2 # logits的维度为[batch_size * num_step, HIDDEN_SIZE VOCAB_SIZE](书上是错的),loss的维度与labels相同,代表每个位置上的log perplexity。
3 labels = tf.placeholder(tf.int64, shape=[BATCH_SIZE * NUM_STEP])
4 loss = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=labels, logits=logits)
5 print(loss)  # shape=(700,)

通过共享参数减少参数数量

softmax层和词向量层的参数数量都与词汇表大小VOCAB_SIZE成正比。VOCAB_SIZE的数值通常较大,而HIDDEN_SIZE相对较小,导致softmax和embedding在整个网络的参数数量中占很大比例。例如,VOCAB_SIZE为10000,HIDDEN_SIZE和EMB_SIZE都为512,循环神经网络采用双层LSTM,那么词向量层和Softmax层的参数数量均为10000*512=5120000,而循环神经网络本身的参数数量仅为2*4*2*512*512=4194304(LSTM有4个参数矩阵,每个参数矩阵的维度是[2*HIDDEN_SIZE, HIDDEN_SIZE],忽略偏置项, 见图8-7),少于词向量层和Softmax层的参数数量,仅占总参数数量的29%。

注意,在上面的例子中,词向量层和softmax层的参数数量是相等的,它们都为每个单词分配了长度为512的向量。如果共享词向量层和softmax层的参数,不仅能大幅减少参数数量,还能提供最终模型的效果。(能够共享参数的原因,可能是两层都是线性变换,可以使用任意参数系,那么使用相同的参数系也无妨,进一步可以说,它们是不可训练的)。下面的完整代码样例中实现了这一方法。

语言模型完整实例:

  1 import numpy as np
  2 import tensorflow as tf
  3 
  4 TRAIN_DATA = '/home/yangxl/files/ptb/ptb.train'
  5 EVAL_DATA = '/home/yangxl/files/ptb/ptb.valid'
  6 TEST_DATA = '/home/yangxl/files/ptb/ptb.test'
  7 HIDDEN_SIZE = 300  # 隐藏层大小
  8 NUM_LAYERS = 2  # 深层循环神经网络中LSTM结构的层数
  9 VOCAB_SIZE = 10000  # 词汇表大小
 10 TRAIN_BATCH_SIZE = 20  # 训练数据batch大小
 11 TRAIN_NUM_STEP = 35  # 训练数据截断长度
 12 
 13 EVAL_BATCH_SIZE = 1  # 测试数据batch大小
 14 EVAL_NUM_STEP = 1  # 测试数据截断长度
 15 NUM_EPOCH = 10  # 使用训练数据的轮数
 16 LSTM_KEEP_PROB = 0.9  # LSTM节点不被dropout的概率
 17 EMBEDDING_KEEP_PROB = 0.9  # 词向量不被dropout的概率
 18 MAX_GRAD_NORM = 5  # 用于控制梯度膨胀的梯度大小上限
 19 SHARE_EMB_AND_SOFTMAX = True  # 在softmax层和词向量层之间共享参数
 20 
 21 
 22 # 从文件中读取数据,并返回包含单词编号的数组
 23 def read_data(file_path):
 24     with open(file_path, 'r') as fin:
 25         # 将整个文档读进一个长字符串
 26         id_string = ' '.join([line.strip() for line in fin])
 27     # 将读取的单词编号转为整数
 28     id_list = [int(w) for w in id_string.split()]  # 长度为929589
 29     return id_list
 30 
 31 
 32 def make_batches(id_list, batch_size, num_step):
 33     # batch的数量,即每一个子序列在纵向上能切分为多少小段:1327段;num_step为每一小段的大小,即一个句子的长度;
 34     # 每个batch包含的单词数量为batch_size * num_step
 35     num_batches = (len(id_list) - 1) // (batch_size * num_step)  # 这里减1,是为了预留一个数据当作label使用
 36 
 37     # 如9-4图所示,将数据整理成一个维度为[batch_size, num_batches * num_step]的二维数组
 38     data = np.array(id_list[: num_batches * batch_size * num_step])  # 后面689个数据未用到。
 39     data = np.reshape(data, [batch_size, num_batches * num_step])  #
 40     # print(data.shape)  # (20, 46445)
 41 
 42     # 沿着第二个维度将数据切分成num_batches个shape为(batch_size, num_step)的batch,存入一个列表
 43     data_batched = np.split(data, num_batches, axis=1)  # (1327, 20, 35)
 44 
 45     # label的处理与data一样。
 46     label = np.array(id_list[1: num_batches * batch_size * num_step + 1])
 47     label = np.reshape(label, [batch_size, num_batches * num_step])
 48     label_batches = np.split(label, num_batches, axis=1)
 49 
 50     # 返回一个长度为num_batches的数组,
 51     return list(zip(data_batched, label_batches))  # (1327, 2, 20, 35)
 52 
 53 
 54 # 通过一个PTBModel类来描述模型,这样方便维护循环神经网络中的状态。
 55 class PTBModel(object):
 56     def __init__(self, is_training, batch_size, num_steps):
 57         self.batch_size = batch_size
 58         self.num_steps = num_steps  # 在run_epoch()中计算iters时会用到
 59 
 60         # 定义每一步的输入和预期输出,二者维度相同。
 61         self.input_data = tf.placeholder(dtype=tf.int32, shape=[batch_size, num_steps])
 62         self.targets = tf.placeholder(dtype=tf.int32, shape=[batch_size, num_steps])
 63 
 64         # 定义以LSTM为循环体结构且使用dropout的深层循环神经网络。
 65         dropout_keep_prob = LSTM_KEEP_PROB if is_training else 1.0
 66         lstm_cells = [
 67             tf.nn.rnn_cell.DropoutWrapper(
 68                 tf.nn.rnn_cell.LSTMCell(HIDDEN_SIZE),
 69                 output_keep_prob=dropout_keep_prob)
 70             for _ in range(NUM_LAYERS)]
 71         cell = tf.nn.rnn_cell.MultiRNNCell(lstm_cells)
 72 
 73         # 初始化最初的状态,即全零的向量。这个量只在每个epoch初始化第一个batch时使用。
 74         # 和其他神经网络类似,在优化循环神经网络时,每次也会使用一个batch的训练样本。 c和h的shape都为[batch_size, HIDDEN_SIZE]
 75         self.initial_state = cell.zero_state(batch_size, tf.float32)
 76 
 77         # 定义单词的词向量矩阵
 78         embedding = tf.get_variable('embedding', [VOCAB_SIZE, HIDDEN_SIZE])
 79 
 80         # 将输入单词转为词向量
 81         inputs = tf.nn.embedding_lookup(embedding, self.input_data)
 82 
 83         # 只在训练时使用dropout
 84         if is_training:
 85             # 在输入之前要dropout, 各个循环层也要dropout。不改变inputs.shape
 86             inputs = tf.nn.dropout(inputs, keep_prob=EMBEDDING_KEEP_PROB)
 87 
 88         # 定义输出列表。在这里先将不同时刻LSMT的输出收集起来,再一起提供给softmax层。
 89         # outputs = []
 90         # state = self.initial_state
 91         # with tf.variable_scope('RNN'):
 92         #     for time_step in range(num_steps):
 93         #         if time_step > 0:
 94         #             tf.get_variable_scope().reuse_variables()
 95         #         cell_output, state = cell(inputs[:, time_step, :], state)
 96         #         outputs.append(cell_output)  # 长度为35,元素的shape=(20, 300)
 97         # 把输出队列展开为[batch, num_steps*hidden_size]的形状,然后在reshape成[batch*num_steps, hidden_size]的形状
 98         # 比直接按行拼接区别在于能够让同一片段的数据放在一起, 也是为了在计算loss时方便使用reshape得到labels
 99         # output = tf.reshape(tf.concat(outputs, axis=1), [-1, HIDDEN_SIZE])  # shape=(700, 300)
100         # print('output', output)
101         # print('state', state)
102 
103         # 下面这样自动计算的更牛
104         outputs, state = tf.nn.dynamic_rnn(cell, inputs, dtype=tf.float32)
105         output = tf.reshape(outputs, [-1, HIDDEN_SIZE])
106         print('output2', output)
107         print('state2', state)
108 
109         # Softmax层:将RNN在每个位置上的输出转化为各个单词的logits.
110         if SHARE_EMB_AND_SOFTMAX:  # 共享参数,不会创建新的变量
111             weight = tf.transpose(embedding)
112         else:
113             weight = tf.get_variable('weight', [HIDDEN_SIZE, VOCAB_SIZE])
114         bias = tf.get_variable('bias', [VOCAB_SIZE])
115         logits = tf.matmul(output, weight) + bias  # output、weight的先后问题??
116 
117         # 定义交叉熵损失函数和平均损失
118         # loss = tf.losses.mean_squared_error(labels=tf.reshape(self.targets, [-1]), predictions=logits)
119         loss = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=tf.reshape(self.targets, [-1]), logits=logits)
120         self.cost = tf.reduce_sum(loss) / batch_size
121         self.final_state = state  # 保存了每次batch的状态
122 
123         if not is_training:
124             return
125 
126         trainable_variables = tf.trainable_variables()
127         # print('train', trainable_variables)
128         # [<tf.Variable 'language_model/embedding:0' shape=(10000, 300) dtype=float32_ref>,
129         #  <tf.Variable 'language_model/RNN/multi_rnn_cell/cell_0/lstm_cell/kernel:0' shape=(600, 1200) dtype=float32_ref>,  每层有4个维度为[2n, n]的参数矩阵,所以kernel.shape=[600, 1200]
130         #  <tf.Variable 'language_model/RNN/multi_rnn_cell/cell_0/lstm_cell/bias:0' shape=(1200,) dtype=float32_ref>,
131         #  <tf.Variable 'language_model/RNN/multi_rnn_cell/cell_1/lstm_cell/kernel:0' shape=(600, 1200) dtype=float32_ref>,
132         #  <tf.Variable 'language_model/RNN/multi_rnn_cell/cell_1/lstm_cell/bias:0' shape=(1200,) dtype=float32_ref>,
133         #  <tf.Variable 'language_model/bias:0' shape=(10000,) dtype=float32_ref>]
134         # 控制梯度大小,定义优化方法和训练步骤
135         grads, _ = tf.clip_by_global_norm(tf.gradients(self.cost, trainable_variables), MAX_GRAD_NORM)
136         optimizer = tf.train.GradientDescentOptimizer(learning_rate=1.0)
137         self.train_op = optimizer.apply_gradients(zip(grads, trainable_variables))
138 
139 
140 def run_epoch(session, model, batches, train_op, output_log, step):
141     # train_op作为一个专门的参数而不是使用model.train_op,是为了区分训练过程和测试/验证过程
142 
143     # 计算平均perplexity的辅助变量
144     total_costs = 0.0
145     iters = 0
146     state = session.run(model.initial_state)
147     # 训练一个epoch
148     for x, y in batches:
149         # 在当前batch上运行train_op并计算损失值。交叉熵损失函数计算的就是下一个单词为给定单词的概率
150         cost, state, _ = session.run(
151             # 最需要注意的是state,每个epoch都要重新提供初始化状态。
152             [model.cost, model.final_state, train_op], feed_dict={model.input_data: x, model.targets: y, model.initial_state: state}  # feed_dict里面的值不只是placeholder!
153         )
154         total_costs += cost
155         iters += model.num_steps
156 
157         # 只有在训练时输出日志
158         if output_log and step % 100 == 0:
159             print('After %d steps, perplexity is %.3f' % (step, np.exp(total_costs / iters)))
160 
161         step += 1
162 
163     return step, np.exp(total_costs / iters)
164 
165 
166 def main():
167     # 定义初始化函数
168     initializer = tf.random_normal_initializer(-0.05, 0.05)
169 
170     # 定义训练用的循环神经网络模型
171     with tf.variable_scope('language_model', reuse=None, initializer=initializer):
172         train_model = PTBModel(True, TRAIN_BATCH_SIZE, TRAIN_NUM_STEP)
173 
174     # 定义测试用的循环神经网络模型,它与train_model共用参数,但没有dropout
175     with tf.variable_scope('language_model', reuse=True, initializer=initializer):
176         eval_model = PTBModel(False, EVAL_BATCH_SIZE, EVAL_NUM_STEP)
177 
178     # 训练模型
179     with tf.Session() as session:
180         tf.global_variables_initializer().run()
181         train_batches = make_batches(read_data(TRAIN_DATA), TRAIN_BATCH_SIZE, TRAIN_NUM_STEP)
182         eval_batches = make_batches(read_data(EVAL_DATA), EVAL_BATCH_SIZE, EVAL_NUM_STEP)
183         test_batches = make_batches(read_data(TEST_DATA), EVAL_BATCH_SIZE, EVAL_NUM_STEP)
184 
185         step = 0
186 
187         for i in range(NUM_EPOCH):
188             print('In iteration: %d' % (i+1))
189             step, train_pplx = run_epoch(session, train_model, train_batches, train_model.train_op, True, step)
190             print('Epoch: %d Train PerPlexity: %.3f' % (i+1, train_pplx))
191 
192             _, eval_pplx = run_epoch(session, eval_model, eval_batches, tf.no_op(), False, 0)
193             print('Epoch: %d Eval Perplexity: %.3f' % (i+1, eval_pplx))
194 
195             _, test_pplx = run_epoch(session, eval_model, test_batches, tf.no_op(), False, 0)
196             print('Test Perplexity: %.3f' % test_pplx)
197 
198 
199 if __name__ == '__main__':
200     main()

也就吃个饭的时间(一个多小时),TIME+都到478了,看来还真不是常规的时间。

top命令的TIME/TIME+是指的进程所使用的CPU时间,不是进程启动到现在的时间,因此,如果一个进程使用的cpu很少,那即使这个进程已经存在N长时间,TIME/TIME+也是很小的数值。此外,如果你的系统有多个CPU,或者是多核CPU的话,那么,进程占用多个cpu的时间是累加的,上边的示例,一个多小时对应478,就说明机器是8核的。

通过调整LSTM隐藏层的节点个数和大小以及训练迭代的轮数还可以将perplexity值降到最低。

非常多的自然语言处理应用的技术都是基于语言模型。

9.3 神经网络机器翻译

最基础的机器翻译算法——Seq2Seq模型。

9.3.1 机器翻译背景与Seq2Seq模型介绍

Seq2Seq模型的基本思想:使用一个循环神经网络读取输入句子,将整个句子的信息压缩到一个固定维度的编码中,再使用另一个循环神经网络读取这个编码,将其“解压”为目标语言的一个句子。这两个循环神经网络分别成为编码器(Encoder)和解码器(Decoder),这个模型也称为encoder-decoder模型。

解码器部分的结构与语言模型几乎完全相同:输入为单词的词向量,输出为softmax层产生的单词概率,损失函数为log perplexity。解码器可以理解为一个以输入编码为前提的语言模型。语言模型中使用的一些技巧,如共享softmax层和词向量的参数,都可以直接应用到Seq2Seq模型的解码器中。

编码器分布则更为简单。它与解码器一样拥有词向量层和循环神经网络,但是由于编码阶段并未输出,因此不需要softmax层。

在训练过程中,编码器顺序读入每个单词的词向量,然后将最终的隐藏状态复制到解码器作为初始状态解码器的第一个输入是一个特殊的<sos>字符(start-of-sentence),每一步预测的单词是训练数据的目标句子,预测序列的最后一个单词是与语言模型相同的'<eos>'字符(end-of-sentence)

在机器翻译应用中,真实应用场景中的测试步骤与语言模型的测试步骤有所不同。语言模型中测试的标准是给定目标句子上的perplexity。而机器翻译的测试方法是,让解码器在没有“正确答案”的情况下自主生成一个翻译句子,然后采用人工或自动的方法对翻译句子的质量进行评测

在解码过程中,每一步预测的单词中概率最大的单词被选为这一步的输出,并复制到下一步的输入中(图9-3中用虚线表示)。这里描述的是最简单的贪心算法,在真实应用中普遍采用集束搜索(Beam Search)方法来获得最好的翻译效果。

9.3.2 机器翻译文本数据的预处理

机器翻译领域最重要的公开数据集是WMT数据集(Workshop on Statistical Machine Translation),下载地址:http://data.statmt.org/wmt17/translation-task/,该会议从2016年起改为Conference on Machine Translation。每年,该会议就会组织一次机器翻译领域的竞赛,其提供的训练和测试数据也成为了机器翻译领域论文的标准数据集。然而由于WMT数据集较大,训练时间较长,因此本节采用一个较小的IWLST TED演讲数据集作为示例,下载地址:https://wit3.fbk.eu/mt.php?release=2015-01

点击'0.51'下载的就是了。

它的英文-中文训练数据包含21万个句子对,内容是TED演讲的中英字幕。

对于平行语料的预处理,其步骤和9.2.1小节中关于PTB数据的预处理基本一样。首先,需要统计语料中出现的单词,为每个单词分配一个ID,将词汇表存入一个vocab文件,然后将文本转化为用单词编号的形式来表示。

与前面不同的地方主要在于,下载的文本没有经过预处理,尤其没有经过切词。例如,由于每个英文单词和标点符号之间紧密相连,导致不能像处理PTB数据那样直接用空格对单词进行切割。为此需要用一些独立的工具来进行切词操作。

最常用的切词工具是moses:(书上的地址不对)

1 git clone https://github.com/moses-smt/mosesdecoder.git

切词操作:

 1 # 英文切词
 2 error@error-F14CU27:~/moses/mosesdecoder/scripts/tokenizer$ perl ./tokenizer.perl -no-escape -l en < /home/error/en-zh/train.tags.en-zh.en > /home/error/codes/train.tags.en-zh.en
 3 Tokenizer Version 1.1
 4 Language: en
 5 Number of threads: 1
 6 
 7 # 中文切词
 8 # 先把汉字和汉字切分开
 9 error@error-F14CU27:~/en-zh$ sed 's/ //g; s/B/ /g' ./train.tags.en-zh.zh > /home/error/codes/train.tags.en-zh.zh
10 
11 # 切分汉字和标点符号
12 error@error-F14CU27:~/moses/mosesdecoder/scripts/tokenizer$ perl ./tokenizer.perl -no-escape -l en < /home/error/codes/train.tags.en-zh.zh > /home/error/codes/train.tags.en-zh.zh2
13 Tokenizer Version 1.1
14 Language: en
15 Number of threads: 1

$ cat train.tags.en-zh.en | grep -v '<url>'| grep -v '<keywords>' | grep -v '<speaker>' | grep -v '<talkid>' | grep -v '<translator' | grep -v '<reviewer' > train.en

yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/ //g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/B/ /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/,/ , /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/。 / 。/g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/!/ !/g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/!/ !/g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/,/ , /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/./ . /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/?/ ? /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/?/ ? /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/《/ 《 /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/》/ 》 /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/"/ " /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/— —/ —— /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/•/ • /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/、/ 、 /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/·/ · /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/:/ : /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/…/ … /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/【/ 【 /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/】/ 】 /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/(/ ( /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/)/ ) /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/-/ - /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/- -/--/g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/- -/--/g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/— —/ —— /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/- -/ - - /g' train.zh.bk yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/- -/--/g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/-/ - /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/- -/--/g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/(/ ( /g' train.zh.bk
yangxl@yangxl-Lenovo-ideapad-330C-15IKB:~/files/trans$ sed -i 's/)/ ) /g' train.zh.bk

完成切词后,再使用之前处理PTB数据的方法,分别生成英文文本和中文文本词汇文件,并将文本转化为单词编号。生成词汇文件时,需要注意将<sos>、<eos>、<unk>这3个词手动加入到词汇表中,并且要限制词汇表大小,将词频过低的次替换为<unk>。假定英文词汇表大小为10000,中文词汇表大小为4000。

在PTB 数据中,由于句子之间有上下文关联,因此可以直接将连续的句子连接起来称为一个大的段落。而在机器翻译的训练样本中,每个句子对通常是作为独立的数据来训练的。由于每个句子的长短不一致,因此在将这些句子放入同一个batch 时,需要将较短的句子补齐到与同batch内最长句子相同的长度。用于填充长度而填入的位置叫作填充(padding)。在TensorFlow中,tf.data.Dataset.padded_ batch函数提供了这一功能。

循环神经网络在读取数据时会将填充位置的内容与其他内容一样纳入计算,因此为了不让填充影响训练,有两方面需要注意:
第一,循环神经网络在读取填充时,应当跳过这一位置的计算。以编码器为例,如果编码器在读取填充时,像正常输入一样处理填充输入,那么在读取“B1B200”之后产生的最后一位隐藏状态就和读取“B1B2”之后的隐藏状态不同,会产生错误的结果。
TensorFlow提供了tf.nn.dynamic_rnn方法来实现这一功能。dynamic_rnn对每一个batch的数据读取两个输入:输入数据的内容(维度为[batch_size, time])和输入数据的长度(维度为[time])。对于输入batch里的每一条数据,在读取了相应长度的内容后,dynamic_rnn就跳过后面的输入,直接把前一步的计算结果复制到后面的时刻。这样可以保证padding是否存在不影响模型效果。

另外值得注意的是,使用dyanmic_rnn时每个batch的最大序列长度不需要相同。例如,在上面的例子中,第一个batch的维度是2×4,而第二个batch的维度是2×7。在训练中dynamic_rnn会根据每个batch的最大长度动态展开到需要的层数,这就是它被称为“dynamic”的原因。

第二,在设计损失函数时需要特别将填充位置的损失的权重设置为0,这样在填充位置产生的预测不会影响梯度的计算。

下面的代码使用tf.data.Dataset.padded_batch来进行填充和batching,并记录每个句子的序列长度以用作dynamic_rnn的输入。与前面PTB的例子不同,这里没有将所有数据读入内存,而是使用Dataset从磁盘动态读取数据。

数据处理的示例见完整代码的前面部分。

sequence_mask方法:

 1 g = tf.sequence_mask([8, 9, 7, 5, 8], maxlen=10, dtype=tf.float32)
 2 
 3 with tf.Session() as sess:
 4     print(sess.run(g))
 5 
 6 # 结果:
 7 # [[1. 1. 1. 1. 1. 1. 1. 1. 0. 0.]
 8 #  [1. 1. 1. 1. 1. 1. 1. 1. 1. 0.]
 9 #  [1. 1. 1. 1. 1. 1. 1. 0. 0. 0.]
10 #  [1. 1. 1. 1. 1. 0. 0. 0. 0. 0.]
11 #  [1. 1. 1. 1. 1. 1. 1. 1. 0. 0.]]

seq2seq模型完整实例:

  1 #!coding:utf8
  2 
  3 import tensorflow as tf
  4 import time
  5 
  6 MAX_LEN = 50  # 限定句子的最大单词数量
  7 SOS_ID = 1  # 目标语言词汇表中<sos>的ID
  8 
  9 SRC_TRAIN_DATA = 'D:\files\tf\train.txt.en.num'
 10 TRG_TRAIN_DATA = 'D:\files\tf\train.txt.zh2.num'
 11 CHECKPOINT_PATH = 'D:\files\tf\seq222seq_ckpt'
 12 HIDDEN_SIZE = 1024  # 隐藏层大小
 13 NUM_LAYERS = 2  # 层数
 14 SRC_VOCAB_SIZE = 10000  # 源语言词汇表大小
 15 TRG_VOCAB_SIZE = 4000  # 目标语言词汇表大小
 16 BATCH_SIZE = 100
 17 NUM_EPOCH = 1
 18 KEEP_PROB = 0.8  # 节点不被dropout的概率
 19 MAX_GRAD_NORM = 5  # 用于控制梯度膨胀的梯度大小上限
 20 SHARE_EMB_AND_SOFTMAX = True
 21 
 22 
 23 # 使用Dataset从一个文件中读取一个语言的数据。
 24 # 数据格式为每行一句话,单词已经转化为单词编号
 25 # 行数据,当前行单词个数
 26 def MakeDataset(file_path):
 27     dataset = tf.data.TextLineDataset(file_path)  # 取出来的是一行,bytes类型
 28     # b'95 13 1590 0 4 11 90 4870 0 4 2'
 29 
 30     # 根据空格将单词编号切分开并放入一个一维向量
 31     dataset = dataset.map(lambda string: tf.string_split([string]).values)  # '[]'不能丢
 32     # [b'95' b'13' b'1590' b'0' b'4' b'11' b'90' b'4870' b'0' b'4' b'2']
 33 
 34     # 将字符串形式的单词编号转化为整数
 35     dataset = dataset.map(lambda string: tf.string_to_number(string, tf.int32))
 36     # [  95   13 1590    0    4   11   90 4870    0    4    2]
 37 
 38     # 统计每个句子的单词数量,并与句子内容一起放入Dataset中
 39     dataset = dataset.map(lambda x: (x, tf.size(x)))  # ([  95   13 1590    0    4   11   90 4870    0    4    2], 11)
 40     return dataset
 41 
 42 
 43 # 从源语言文件src_path和目标语言文件trg_path中分别读取数据,并进行填充和batching操作
 44 def MakeSrcTrgDataset(src_path, trg_path, batch_size):
 45     # 首先分别读取源语言数据和目标语言数据
 46     src_data = MakeDataset(src_path)  # ([  95   13 1590    0    4   11   90 4870    0    4    2], 11)
 47     trg_data = MakeDataset(trg_path)  # ([  40, 5545,  610,  118,   10,  261,    7,  171, 4827,  507,    4, 5,    7,   40, 5545,  610,    6,    2], 18)
 48 
 49     # 通过zip操作将两个Dataset合并为一个Dataset。
 50     dataset = tf.data.Dataset.zip((src_data, trg_data))
 51 
 52     # 删除内容为空的句子和长度过长的句子
 53     def FilterLength(src_tuple, trg_tuple):
 54         ((src_input, src_len), (trg_label, trg_len)) = (src_tuple, trg_tuple)
 55         src_len_ok = tf.logical_and(tf.greater(src_len, 1), tf.less_equal(src_len, MAX_LEN))  # 句子长度大于1且不大于50
 56         trg_len_ok = tf.logical_and(tf.greater(trg_len, 1), tf.less_equal(trg_len, MAX_LEN))
 57         return tf.logical_and(src_len_ok, trg_len_ok)
 58     dataset = dataset.filter(FilterLength)  # 作为过滤器的函数必须能够返回布尔值。
 59 
 60     # 解码器需要两种格式的目标句子。
 61         # 1. 解码器的输入(trg_input), 形式如同'<sos> X Y Z'
 62         # 2. 解码器的目标输出(trg_label), 形式如同'X Y Z <eos>'
 63         # 上面从目标文件中读到的目标句子是'X Y Z <eos>'的形式,需要从中生成'<sos> X Y Z'形式并加入到dataset中。
 64     def MakeTrgInput(src_tuple, trg_tuple):
 65         ((src_input, src_len), (trg_label, trg_len)) = (src_tuple, trg_tuple)  # [  40, 5545,  610,  118,   10,  261,    7,  171, 4827,  507,    4, 5,    7,   40, 5545,  610,    6,    2]
 66         trg_input = tf.concat([[SOS_ID], trg_label[:-1]], axis=0)
 67         return ((src_input, src_len), (trg_input, trg_label, trg_len))
 68     dataset = dataset.map(MakeTrgInput)
 69 
 70     # 随机打乱循环数据
 71     dataset = dataset.shuffle(10000)
 72 
 73     # 规定填充后输出的数据维度
 74     padded_shapes = (
 75         (tf.TensorShape([None]),  # 源句子是长度未知的向量
 76          tf.TensorShape([])),  # 源句子长度是单个数字
 77 
 78         (tf.TensorShape([None]),  # 目标句子(解码器输入)是长度未知的向量
 79          tf.TensorShape([None]),  # 目标句子(解码器目标输出)是长度未知的向量
 80          tf.TensorShape([]))  # 目标句子长度是单个数字
 81     )
 82 
 83     # batching操作,
 84     # Defaults are `0` for numeric types and the empty string for string types.
 85     batched_dataset = dataset.padded_batch(batch_size, padded_shapes)  # src.shape=(batch_size, max_length)
 86     return batched_dataset
 87 
 88 
 89 # 定义模型
 90 class NMTModel(object):
 91     def __init__(self):
 92         self.enc_cell = tf.nn.rnn_cell.MultiRNNCell(
 93             [tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE) for _ in range(NUM_LAYERS)]
 94         )
 95         self.dec_cell = tf.nn.rnn_cell.MultiRNNCell(
 96             [tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE) for _ in range(NUM_LAYERS)]
 97         )
 98 
 99         # 为源语言和目标语言分别定义词向量
100         # 注意VOCAB_SIZE是词汇表大小,不是文本大小。
101         # 初始化的变量取什么值都无所谓吧
102         self.src_embedding = tf.get_variable('src_emb', [SRC_VOCAB_SIZE, HIDDEN_SIZE])
103         self.trg_embedding = tf.get_variable('trg_emb', [TRG_VOCAB_SIZE, HIDDEN_SIZE])
104 
105         # softmax层的变量都是trg的,因为是输出层进行分类
106         if SHARE_EMB_AND_SOFTMAX:
107             self.softmax_weight = tf.transpose(self.trg_embedding)
108         else:
109             self.softmax_weight = tf.get_variable('weight', [HIDDEN_SIZE, TRG_VOCAB_SIZE])
110         self.softmax_bias = tf.get_variable('softmax_bias', [TRG_VOCAB_SIZE])
111 
112     def forward(self, src_input, src_size, trg_input, trg_label, trg_size):
113         # src_input.shape=(5, 17) src_size=[10  9 13 11 17]  # (5, 17)是因为没有去掉文件头部几行,看了不下一遍了。
114         # trg_label.shape=(5, 30) trg_size=[30 21 26 18 21]
115         batch_size = tf.shape(src_input)[0]
116 
117         src_emb = tf.nn.embedding_lookup(self.src_embedding, src_input)  # (5, 17, 1024)
118         trg_emb = tf.nn.embedding_lookup(self.trg_embedding, trg_input)  # (5, 30, 1024)  # 解码器的输入trg_input
119 
120         # 在词向量上进行dropout,dropout不会改变shape。
121         src_emb = tf.nn.dropout(src_emb, KEEP_PROB)  # (5, 17, 1024)
122         trg_emb = tf.nn.dropout(trg_emb, KEEP_PROB)  # (5, 30, 1024)
123 
124         # 使用dynamic_rnn构造编码器
125         # 不改变shape
126         with tf.variable_scope('encoder'):
127             # 因为编码器是一个双层LSTM结构,因此enc_state是一个包含两个LSTMStateTuple类的tuple,每个类对应编码器中一层的状态。
128                 # state_c和state_h的shape都为[batch_size, HIDDEN_SIZE],即(5, 1024)
129             # enc_outputs是顶层LSTM在每一步的输出,维度为[batch_size, max_time, HIDDEN_SIZE],同输入(5, 17, 1024)
130                 # 后两个参数的作用??
131             enc_outputs, enc_state = tf.nn.dynamic_rnn(
132                 self.enc_cell, src_emb, src_size, dtype=tf.float32
133             )
134 
135         # 构造解码器
136         with tf.variable_scope('decoder'):
137             # dec_outputs.shape=(5, 30, 1024), 同输入
138             # 输出隐藏状态,4个都是(5, 1024)
139                 # 解码器的初始状态为编码器的输出状态
140             dec_outputs, _ = tf.nn.dynamic_rnn(
141                 self.dec_cell, trg_emb, trg_size, initial_state=enc_state  # 编码器的最终状态,作为解码器的初始状态。
142             )
143 
144         # 计算解码器每一步的log perplexity
145         output = tf.reshape(dec_outputs, [-1, HIDDEN_SIZE])  # shape=(150, 1024)
146 
147         # softmax层输出
148         logits = tf.matmul(output, self.softmax_weight) + self.softmax_bias  # shape=(150, 4000)
149         loss = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=tf.reshape(trg_label, [-1]), logits=logits)  # shape=(150,)  一个batch(150个单词编号)的总损失。
150 
151         # 在计算平均损失时,需要将填充位置的权重设置为0,以避免无效位置的预测干扰模型的训练
152         label_weight = tf.sequence_mask(
153             trg_size, maxlen=tf.shape(trg_label)[1], dtype=tf.float32
154         )  # 假如trg_size=[30 21 26 18 21], maxlen=30, 那么结果的第二个元素为[21个1.0,(30-21)个0.0]
155 
156         label_weight = tf.reshape(label_weight, [-1])  # shape=(150,)
157 
158         cost = tf.reduce_sum(loss * label_weight)  # 一个batch的损失,1035.8269  # 把填充位置的损失过滤掉了
159         cost_per_token = cost / tf.reduce_sum(label_weight)  # 一个单词编号对应的损失,8.704428  # 也是把填充位置排除了
160 
161         # 定义反向传播
162         trainable_variables = tf.trainable_variables()
163 
164         # 控制梯度大小,定义优化方法和训练步骤
165         grads = tf.gradients(cost / tf.to_float(batch_size), trainable_variables)  # 习惯上,一个batch有batch_size个数据,除以batch_size表示一个`数据`(一个句子)的损失
166         grads, _ = tf.clip_by_global_norm(grads, MAX_GRAD_NORM)
167         optimizer = tf.train.GradientDescentOptimizer(learning_rate=1.0)
168         train_op = optimizer.apply_gradients(zip(grads, trainable_variables))
169         return cost_per_token, train_op
170         # return src_input, src_size, src_emb, dropout_src_emb, enc_outputs, trg_input, trg_label, trg_size, trg_emb, dropout_trg_emb, dec_outputs, output, logits, loss, cost_per_token, train_op
171 
172 
173 def run_epoch(sess, cost_op, train_op, saver, step):
174     # 重复训练步骤直至遍历完dataset中的所有数据
175     # 训练之前不知道需要训练多少轮
176     while True:
177         try:
178             cost, _ = sess.run([cost_op, train_op])
179             if step % 10 == 0:
180                 print('After %s steps, per token cost is %.3f' % (step, cost))
181             # 每200步保存一个checkpoint
182             if step % 200 == 0:
183                 saver.save(sess, CHECKPOINT_PATH, global_step=step)
184             step += 1
185         except tf.errors.OutOfRangeError:
186             break
187     return step
188 
189 
190 def main():
191     initializer = tf.random_uniform_initializer(-0.05, 0.05)
192 
193     # 定义训练用的循环神经网络模型
194     with tf.variable_scope('nmt_model', reuse=None, initializer=initializer):
195         train_model = NMTModel()
196 
197     # 定义输入数据
198     data = MakeSrcTrgDataset(SRC_TRAIN_DATA, TRG_TRAIN_DATA, BATCH_SIZE)
199     iter = data.make_initializable_iterator()
200     (src, src_size), (trg_input, trg_label, trg_size) = iter.get_next()  # trg_input的shape=[batch_size, max_length]
201 
202     cost_op, train_op = train_model.forward(src, src_size, trg_input, trg_label, trg_size)
203     # src_input, src_size, src_emb, dropout_src_emb, enc_outputs, trg_input, trg_label, trg_size, trg_emb, dropout_trg_emb, dec_outputs, output, logits, loss, cost_per_token, train_op = train_model.forward(None, src, src_size, trg_input, trg_label, trg_size)
204 
205     # 训练模型
206     saver = tf.train.Saver()
207     step = 0
208     with tf.Session() as sess:
209         tf.global_variables_initializer().run()
210 
211         # src_input, src_size, src_emb, dropout_src_emb, enc_outputs, trg_input, trg_label, trg_size, trg_emb, dropout_trg_emb, dec_outputs, output, logits, loss, cost_per_token, train_op = sess.run([src_input, src_size, src_emb, dropout_src_emb, enc_outputs, trg_input, trg_label, trg_size, trg_emb, dropout_trg_emb, dec_outputs, output, logits, loss, cost_per_token, train_op])
212         # print(src_input.shape, src_size, src_emb.shape, dropout_src_emb.shape, enc_outputs.shape, trg_input.shape, trg_label.shape, trg_size, trg_emb.shape, dropout_trg_emb.shape, dec_outputs.shape, output.shape, logits.shape, loss.shape, cost_per_token, train_op)
213         # return
214 
215         for i in range(NUM_EPOCH):
216             print('In iterator: %d' % (i+1))
217             # iterator为何放在循环中,遍历第二遍时还要重新初始化??
218             sess.run(iter.initializer)  # 取值之前一定不要忘记迭代器初始化
219             step = run_epoch(sess, cost_op, train_op, saver, step)
220             print('In interator %d is end, step is %d' % (i+1, step))
221 
222 if __name__ == '__main__':
223     print(int(time.time()))
224     main()
225     print(int(time.time()))

在解码的程序中,解码器的实现与训练时有很大不同。因为训练时解码器可以从输入中读取完整的目标训练句子,因此可以用dynamic_rnn简单地展开成前馈网络。而在解码过程中,模型只能看到输入句子,却不能看到目标句子。解码器的第一步读取<sos>符,预测目标句子的第一个单词,然后需要将这个预测的单词复制到第二步作为输入,再预测第二个单词,知道预测的单词为<eos>为止。这个过程需要使用一个循环结构来实现。tf.while_loop。

翻译过程:

  1 #!coding:utf8
  2 
  3 import tensorflow as tf
  4 
  5 CHECKPOINT_PATH = '/home/yangxl/codes/seq2seq_ckpt-400'
  6 
  7 # 必须要与训练模型的参数一致
  8 HIDDEN_SIZE = 1024  # 隐藏层大小
  9 NUM_LAYERS = 2  # 层数
 10 SRC_VOCAB_SIZE = 10000  # 源语言词汇表大小
 11 TRG_VOCAB_SIZE = 10000  # 目标语言词汇表大小
 12 SHARE_EMB_AND_SOFTMAX = True
 13 
 14 SOS_ID = 1
 15 EOS_ID = 2
 16 
 17 class NMTModel(object):
 18     def __init__(self):
 19         # 与训练模型中的__init__函数相同。
 20         self.enc_cell = tf.nn.rnn_cell.MultiRNNCell(
 21             [tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE) for _ in range(NUM_LAYERS)]
 22         )
 23         self.dec_cell = tf.nn.rnn_cell.MultiRNNCell(
 24             [tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE) for _ in range(NUM_LAYERS)]
 25         )
 26 
 27         # 为源语言和目标语言分别定义词向量
 28         self.src_embedding = tf.get_variable('src_emb', [SRC_VOCAB_SIZE, HIDDEN_SIZE])
 29         self.trg_embedding = tf.get_variable('trg_emb', [TRG_VOCAB_SIZE, HIDDEN_SIZE])
 30 
 31         # softmax层的变量
 32         if SHARE_EMB_AND_SOFTMAX:
 33             self.softmax_weight = tf.transpose(self.trg_embedding)
 34         else:
 35             self.softmax_weight = tf.get_variable('weight', [HIDDEN_SIZE, TRG_VOCAB_SIZE])
 36         self.softmax_bias = tf.get_variable('softmax_bias', [TRG_VOCAB_SIZE])
 37 
 38     def inference(self, src_input):
 39         # dynamic_rnn要求输入是batch形式,因此这里需要把输入整理为大小为1的batch
 40         src_size = tf.convert_to_tensor([len(src_input)], dtype=tf.int32)  # 加上`[]`使其batch为1  # shape=(1,)
 41         src_input = tf.convert_to_tensor([src_input], dtype=tf.int32)      # 这也要加, 与上面对应,shape=(1, 6)
 42         src_emb = tf.nn.embedding_lookup(self.src_embedding, src_input)  # (1, 6, 1024)
 43 
 44         # 构造编码器,与训练时一致
 45         with tf.variable_scope('encoder'):
 46             # 输入数据的rank至少为3(batch_size, max_time, ...)
 47             # enc_output.shape=(1, 6, 1024)
 48             enc_output, enc_state = tf.nn.dynamic_rnn(
 49                 self.enc_cell, src_emb, src_size, dtype=tf.float32
 50             )
 51 
 52         # 设置解码的最大步数,以避免极端情况下出现无限循环问题。
 53         MAX_DEC_LEN = 100
 54 
 55         with tf.variable_scope('decoder/rnn/multi_rnn_cell'):  # 这样写是为了与加载文件中的一致
 56             # 使用一个变长的tensorarray来存储生成的句子
 57             init_array = tf.TensorArray(dtype=tf.int32, size=0, dynamic_size=True, clear_after_read=False)
 58             # 填入第一个单词<sos>作为解码器的输入
 59             # init_array.read()
 60             # init_array.write()
 61             # init_array.stack()
 62             init_array = init_array.write(0, SOS_ID)
 63             # 构建初始的循环状态,循环状态包含循环神经网络的隐藏状态,保存生成句子的tensorarry,以及记录解码步数step
 64             init_loop_var = (enc_state, init_array, 0)
 65 
 66             # tf.while_loop的循环条件: 直到解码器输出<sos>,或者达到最大步数
 67             def continue_loop_condition(state, trg_ids, step):
 68                 # trg_ids.read(step), 即init_array.read(step)
 69                 # tf.reduce_all
 70                 return tf.reduce_all(tf.logical_and(  # 去掉reduce_all也没问题
 71                     tf.not_equal(trg_ids.read(step), EOS_ID),
 72                     tf.less(step, MAX_DEC_LEN-1)
 73                 ))
 74 
 75             def loop_body(state, trg_ids, step):
 76                 # 读取最后一步输出的单词,并读取其词向量
 77                 trg_input = [trg_ids.read(step)]  # 加一个中括号  # (1,1)
 78                 trg_emb = tf.nn.embedding_lookup(self.trg_embedding, trg_input)  # (1, 1, 1024)
 79                 # 这里不使用dynamic_rnn,而是直接调用dec_cell向前计算一步
 80                 dec_outputs, next_state = self.dec_cell.call(state=state, inputs=trg_emb)  # (1, 1, 1024)
 81                 # 计算每个可能的输出单词对应的logits
 82                 output = tf.reshape(dec_outputs, [-1, HIDDEN_SIZE])  # (1, 1024)
 83                 logits = tf.matmul(output, self.softmax_weight) + self.softmax_bias  # (1, 4000)
 84                 # 选取logit值最大的单词作为这一步的输出
 85                 next_id = tf.argmax(logits, axis=1, output_type=tf.int32)
 86                 # 将这一步输出的单词写入循环状态的trg_ids中
 87                 trg_ids = trg_ids.write(step+1, next_id[0])  # 写入,然后下一步输出
 88                 return next_state, trg_ids, step+1
 89 
 90             # 执行循环tf.while_loop,返回最终状态
 91             state, trg_ids, step = tf.while_loop(continue_loop_condition, loop_body, init_loop_var)
 92             return trg_ids.stack()
 93 
 94 
 95 def main(x):
 96     with tf.variable_scope('nmt_model', reuse=None):
 97         model = NMTModel()
 98 
 99     test_sentence = [90, 13, 9, 689, 4, 2]
100     output_op = model.inference(test_sentence)
101 
102     saver = tf.train.Saver()
103     with tf.Session() as sess:
104 
105         saver.restore(sess, CHECKPOINT_PATH)
106 
107         # 读取翻译结果
108         output = sess.run(output_op)
109         print('result: ', output)
110 
111 if __name__ == '__main__':
112     tf.app.run()

API:

tf.string_split()、tf.string_join()

 1 strs = tf.string_split([b'95 13 1590 0 4 11 90 4870 0 4 2'])
 2 SparseTensor(indices=Tensor("StringSplit:0", shape=(?, 2), dtype=int64), values=Tensor("StringSplit:1", shape=(?,), dtype=string), dense_shape=Tensor("StringSplit:2", shape=(2,), dtype=int64))  # 有3个属性:indices、values、dense_shape
 3 
 4 with tf.Session() as sess:
 5     print(sess.run(strs))
 6 SparseTensorValue(indices=array([[ 0,  0],
 7        [ 0,  1],
 8        [ 0,  2],
 9        [ 0,  3],
10        [ 0,  4],
11        [ 0,  5],
12        [ 0,  6],
13        [ 0,  7],
14        [ 0,  8],
15        [ 0,  9],
16        [ 0, 10]], dtype=int64), values=array([b'95', b'13', b'1590', b'0', b'4', b'11', b'90', b'4870', b'0',
17        b'4', b'2'], dtype=object), dense_shape=array([ 1, 11], dtype=int64))

验证是否改变shape:embedding改变、dropout、dynastic_rnn不变。具体代码看训练过程。

原文地址:https://www.cnblogs.com/yangxiaoling/p/10015744.html