Seq2Seq
1、简介
使用RNN来做机器翻译, 机器翻译模型有很多种, 这里介绍sequence-2-sequence模型, 机器翻译是many-2-many多对多的问题。输入的英语长度大于1, 输出的德语长度也大于1,而且输入和输出的长度不固定。
做任何机器学习的应用, 第一步都是处理输出。这里就取一个小规模数据集就行。可用该网站英语翻译语料 (manythings.org)](http://www.manythings.org/anki/))提供的小规模数据集来训练一个seq-2-seq模型。这个网站上有多种语言翻译的数据, 这个文件是德语和英语的翻译。文件中, 坐标是英语句子, 右边是德语句子, 给定一句英语, 如何翻译结果呢, match其中一个德语句子就算完全正确。
处理数据时, 把这些句子用矩阵表示, 首先做预处理, 比如把大小变成小写, 去掉标点符号。
处理之后就要做tokenization, 把一句话变成很多个单词或者字符。做tokenization时, 要用两个不同的tokenizer, 英语用一个, 德语用一个。tokenization之后要建立两个字典, 一个英语字典, 一个德语字典。
tokenization可以是character level也可以是word level, 二者都可以。character level tokenization会把一句话分割成很多个字符, word level tokenization会把一句话分割成很多单词。
为了方便起见, 这里使用character level tokenization, 一句话变成一个list, list中每个元素变成一个字符。但实际的机器翻译都是用word level tokenization, 因为它们数据集足够大。
刚才提到为什么要用两个不同的tokenizer, 这里解释一下。在字符层面, 不同的语言通常有不同的alphabet字母表。 英语有26个拉丁字母, 如果区分大小写就有53个字母。
德语也有26个拉丁字母, 但还有4个不常用字母。中文没有字母, 而是有几千个汉字。日语字符更复杂, 有46个片假名, 还有几百个汉字。两种语言的字符通常是不同的。所以应该用两种不同的tokenizer, 各有各的字母表。
如果是word level tokenization, 就更应该用两种不同的tokenizer和不同的字典。英语和德语的词汇完全不一样, 绝大多数德语单词在英语字典里面找不到。此外, 不同的语言有不同的分词方法, 汉语、日语和欧洲语言的分词方法就不一样。
keras提供的库叫作tokenization, 会自动生成字典。左边是英语字典, 一共有27个字符,包含26个字母和一个空格。27个字符分别对应27个数字。右边是德语字典, 删除了不常用的字符, 只保留了26个字母和一个空格。任务是把英语翻译成德语。英语是原语言, 德语是目标语言。要往目标语言-德语的字典里添加两个符号。一个起始符\t, 一个终止符\n。拿什么做起止符和终止符都可以, 只要不跟字典里面已有的字符冲突即可。
tokenization结束之后, 每句话就变成一个字符的列表, 并且生成一个英语字典和一个德语字典。用这个字典就可以把每个字符映射成一个整数。这样一句话就变成了一个sequence, 每个元素是一个整数。
可以进一步把每个数字用one-hot向量表示, 做完one-hot encoding,每个字符用一个向量表示, 每句话用一个矩阵表示,这个矩阵就是RNN的输入。
2、模型搭建
下面开始搭建一个seq2seq模型, 并且开始训练这个模型。 seq2seq模型有一个encoder编码器和一个decoder解码器。encoder是LSTM或其他的RNN模型。用来从输入的英语句子中提取特征。encoder最后一个状态就是从输入的句子中提取的特征, 包含这句话的信息。encoder其余的状态没有用, 都被丢弃, encoder的输出是LSTM最后的状态h以及最后的传输带C。
seq2seq还有一个decoder用来生成德语。这个decoder其实就是上次讲解的text generation。与上次的text generation文本生成器唯一的区别在于初始状态。上次用的文本生成器的初始状态是一个全零向量。这里的decoder的初始状态是encoder的最后一个状态。encoder最后一个状态h和c是从输入的英语句子中提取的特征向量。概括了输入的英语句子, decoder靠这个状态来知道这句英语是go away。
再强调一下, decoder的初始状态是encoder的最后一个状态。通过encoder最后一个状态, decoder得知输入的英语句子是go away。现在decoder开始生成德语句子, decoder是个LSTM模型, 它每次接受一个输入, 然后输出对下一个字符的预测。第一个输入必须是起始符, 用\t表示。这就是为什么要在德语字典中加入起始符。decoder会输出一个概率分布, 记作向量p。
起始符后面德语的第一个字母是m, 把m做one-hot encoder, 作为标签y, 用标签y和预测p的cross entropy作为损失函数。
希望预测p尽量接近y, 所以损失函数越小越好。有了损失函数就可以反向传播计算梯度。梯度会从损失函数传到decoder, 然后再从decoder一直传到encoder。然后用梯度下降来更新decoder和encoder的模型参数, 让损失函数减小。
然后输入是两个字符, 起始符与字母m。 decoder会输出对下一个字符的预测,记为向量p。输入的字符串的下一个字符是字母a。把a做one hot encoding, 作为标签y。损失函数是标签y与预测p的cross entropy, 然后用反向传播计算梯度, 然后更新decoder和encoder。
再下一个输入是3个字符, 起始符, 字母m和字母a。LSTM输出对下一个字符的预测记为向量p。真实的下一个字母是c, 所以标签y是字母c的one hot向量。同样的道理, 用反向传播计算梯度, 然后做梯度下降更新encoder和decoder。
不断重复这个过程, 直到德语的最后一个字符。
最后一轮, 把整句德语作为decoder输入, 所以用停止符的one-hot向量作为标签y。希望输出的预测尽可能接近标签, 也就是停止符。然后再做一次反向传播, 再更新一次模型参数,不断重复这个训练过程, 拿所有的英语德语二元组来训练decoder和encoder。
3、训练
如果用keras、pytorch或TensorFlow等来搭建一个seq2seq模型, 你需要这么做。encoder的输入是英文句子的one hot encoding,用一个矩阵表示。encoder网络有一层或多层LSTM用来从英文句子中提取特征, LSTM的输出是最后一个状态h与最终的传输带c。decoder网络的初始状态是h与c, 这样可以让encoder和decoder连起来。在做反向传播时, 梯度可以顺着这条线从decoder传播到encoder。decoder网络的输入是德语的上半句话,decoder输出当前状态h, 然后全连接层输出对下一个字符的预测。
训练好了seq2seq模型, 可以拿它把英语翻译成德语。把一句英语的每个字符输入encoder, encoder会在状态h和c中积累这句话的信息。encoder输出最后的状态记作h0和c0, 它们是从这句话里面提取的特征, h0和c0被送给了decoder。
4、推理
encoder的输出h0和c0被记为decoder的初始状态。
这样decoder就知道输入的英文句子是go away。现在decoder就跟文本生成器一样工作。 首先把起始符输入decoder, 有了新的输入, decoder就会更新状态h和传输带c并预测下一个字符, 并输出一个概率分布。
根据概率分布来做抽样, 得到字符词, 然后把c记录下来。
不断重复这个过程, 更新状态并且生成新的字符, 然后用新生成的字符作为下一轮的输入。
运行14轮之后的状态是h14和c14。上一轮生成的字符是字母e, 现在拿它作为输入, 根据decoder输出的概率分布做抽样, 可能碰巧抽到了终止符。一旦抽到终止符, 就终止文本生成并返回记录下来的字符串,这个字符串就是模型翻译得到的德语。
总结上述过程。
使用seq2seq模型做机器翻译模型, 模型有一个encoder网络和一个decoder网络。
encoder的输入是一句英语, 每输入一个词, RNN会更新状态。把输入的信息积累在encoder状态里。
encoder最后一个状态就是从英文句子里提取的特征, encoder只输出最后一个状态, 会扔掉之前的所有状态。
把最后一个状态传递给decoder网络。
把encoder的最后一个状态作为decoder的初始状态。初始化后, decoder网络就知道输入的英文句子了, 然后decoder作为一个文本生成器生成一句德语。首先把起始符作为decoder RNN的输入, decoder RNN会更新状态s1。
然后全连接层输出的预测概率记为p1,根据概率分布p1做抽样得到下一个字符记作z1。
decoder将z1作为输入更新状态s2, 并输出预测的概率为p2, 根据p2抽样得到新的字符z2。
同样的道理decoder网络拿z2作为输入更新状态s3。不断重复这个过程, 抽样得到新的字符记作z, 然后拿z作为下一轮输入更新状态s以及计算概率分布p。
如果抽到了停止符, 那么就终止文本生成, 返回生成的序列。
5、seq2seq模型改进
针对上述的seq2seq模型, 如何做模型改进。
seq2seq模型的原理是这样的。encoder处理输入的英语句子, 把信息压缩到状态向量里面, encoder最后一个状态是整句话的一个概要。理想情况下, encoder最后一个状态包含整句英语的完整信息, 当然那是理想情况。
假如英语句子太长, LSTM就会遗忘。假如英语里面有些信息被遗忘了。那么encoder就不可能有英语句子的完整信息, decoder生成的德语肯定会有遗漏。
一种很显然的改进就是用双向LSTM代替单向LSTM。双向LSTM有两条链, 一条从左到右, 一条从右到左。两条链独立运行,分别从输入中提取特征, 从左到右的最后一个状态ht可能会遗忘最左边的输入。从右到左的这条链的最后一个状态ht’会记住最左边的信息, 把ht和ht‘结合起来,就能更好的记住所有的输入。
综上, 单向LSTM换成双向LSTM可以更好的记住输入的句子,但decoder必须是单向LSTM。decoder就是一个文本生成器, 必须按照顺序生成文本, 所以decoder不能用双向LSTM。
上面用了character level tokenization, 这样比较方便, 不需要用embedding层。但最好还是用word level tokenization。 原因是这样的, 英文平均每个单词有4.5个字母, 如果用单词代替字符,那么输入的序列就能缩短4.5倍, 序列更短就更不能遗忘。
但是想要用word level tokenization, 需要有足够大的数据集。用word level tokenization得到的vocabulary大约是1万。因此one hot向量的维度大约是1万, 必须用word embedding 得到更低维的向量。embedding层的参数量太大了, 用小的数据集无法得到很好的训练。embedding层会有overfitting问题,得有足够大的训练数据集,或者对embedding层做预训练, 才能避免overfitting。
另外一种改进方法是multi task learning, 多任务学习。encoder读入一句英语, 把英语句子概括成最终的状态向量h和c。decoder通过h和c获取英语句子的信息, 然后生成一句德语。训练时比较decoder的预测与真实的德语单词, 从而获得损失函数和梯度,用来更新decoder和encoder。
把英语翻译成德语是一个任务, 还可以多加几个任务。比如把英语句子翻译成英语句子本身。添加一个decoder, 让它根据h和c来生成英语句子。这样一来, encoder只有一个而训练数据多了一倍,所以encoder可以被训练得很好。
还可以添加其他很多任务,英文翻译成很多其他语言的数据集。可以利用这些数据更好的训练encoder。
比如还可添加更多任务,把英语翻译成法语, 西班牙语等, 添加更多的decoder, 让这些decoder生成各种语言。但encoder只有一个。如果用10种语言做训练, 那么训练encoder的数据就多了10倍, encoder可以训练的更好。目标是把英语翻译成德语, 通过借助其他语言可以把encoder变得更好。 虽然德语decoder没有改进, 但是翻译的效果还是会变好。