深度之眼Paper带读笔记NLP.15:多层LSTM.Baseline.07

前言

本课程来自深度之眼deepshare.net,部分截图来自课程视频。
文章标题:Sequence to Sequence Learning with Neural Networks
使用多层LSTM的Seq2Seq模型/使用神经网络来做序列到序列的学习
作者:llya Sutskever,Oriol Vinyals,Quoc V. Le
单位:Google
发表会议及时间:NIPS2014
在线LaTeX公式编辑器
本文是大量实验中得到的一系列TRICK,提炼处理的文章,与普通的理论模型文章不太一样。
在这里插入图片描述
在这里插入图片描述

第一课 论文导读

a. 神经机器翻译概率
神经机器翻译就是使用神经网络使得机器能够自动将一种语言的句子翻译成另外一种语言的句子,它可以解决不同母语的人之间的交流障碍。
b. 两种神经机器翻译模型
最开始的神经机器翻译模型只是使用一个多层的LSTM,多层LSTM的最底层为源语言的输入,多层LSTM的最高层为目标语言的输出。之后产生了Encoder-Decoder的模型,即Encoder将源语言的句子压缩成一个向量,Decoder利用压缩得到的向量生成目标语言的句子。
c. LSTM以及多层LSTM
LSTM是一种特殊的RNN,通过增加一个记忆细胞和几个门来加强序列处理的长距离依赖。而多层LSTM就是单层LSTM网上叠加,上一层LSTM每一个时间步的输出作为当前层LSTM的输入。

BLEU介绍

BLEU 用来评价翻译的结果。
BLEU: a Method for Automatic Evaluation of Machine Translation, 2002
人工评价:通过人主观对翻译进行打分
优点:准确
缺点:速度慢,价格昂贵

机器自动评价:通过设置指标对翻译结果自动评价
优点:较为准确,速度快,免费
缺点:可能和人工评价有一些出入

BLEU实例

BLEU是一种评价指标,它可以用来自动对机器翻译的结果进行评价。
下面看一个BLEU评价的例子,假如我们现在有一句中文:
猫在垫上。
Candidate(翻译结果): the the the the the the the.
Reference 1(参考答案1): the cat is on the mat.
Reference 2(参考答案2): there is a cat on the mat.
下面计算单词【the】出现在Candidate中的次数:
C o u n t ( t h e ) = 7 Count(the)=7 Count(the)=7
分别计算单词【the】出现在Reference 1和Reference 2中的次数:
C o u n t 1 c l i p ( t h e ) = m i n ( 7 , 2 ) = 2 C o u n t 2 c l i p ( t h e ) = m i n ( 7 , 1 ) = 1 Count_1^{clip}(the)=min(7,2)=2\\ Count_2^{clip}(the)=min(7,1)=1 Count1clip(the)=min(7,2)=2Count2clip(the)=min(7,1)=1
根据最大值得到单词【the】出现在参考答案中的次数:
C o u n t c l i p ( t h e ) = m a x ( 2 , 1 ) = 2 Count^{clip}(the)=max(2,1)=2 Countclip(the)=max(2,1)=2
最后计算出单词【the】BLEU:
p 1 = C o u n t c l i p ( t h e ) C o u n t ( t h e ) = 2 / 7 p_1=\cfrac{Count^{clip}(the)}{Count(the)}=2/7 p1=Count(the)Countclip(the)=2/7
下标1代表1-gram
用代码表示:

from nltk.translate.bleu_score import sentence_bleu 
reference=[[' the',' cat',' is',' on',' the',' mat'],
[' there',' is','a',' cat',' on',' the',' mat']]
candidate=[' the',' the',' the',' the',' the',' the',' the']
score=sentence_bleu(reference, candidate, weights=(1,0,0,0))
# 这里的weights对应最终公式中的w,也就是每种gram的权重
print(score)

只计算1-gram,所以会有warning
在这里插入图片描述

BLEU改进

上面的例子可以看到,只考虑了1-gram的问题,这样的话,对每个词进行翻译就能得到很高的分,完全没考虑到句子的流利性。
改进:使用多-gram融合,如:使用1,2,3,4-gram。公式如下:
p n = ∑ n − g r a m ∈ C C o u n t c l i p ( n − g r a m ) ∑ n − g r a m ∈ C C o u n t ( n − g r a m ) p_n=\cfrac{\sum_{n-gram\in C}Count^{clip}(n-gram)}{\sum_{n-gram\in C}Count(n-gram)} pn=ngramCCount(ngram)ngramCCountclip(ngram)
还是上面的例子:
假如我们现在有一句中文:
猫在垫上。
Candidate(翻译结果): the the the the the the the.
Reference 1(参考答案1): the cat is on the mat.
Reference 2(参考答案2): there is a cat on the mat.
在Candidate中的:
1-gram:the (在参考答案中出现的最大次数为2, p 1 = 2 / 7 p_1=2/7 p1=2/7
2-gram:the the (在参考答案中出现的最大次数为0, p 2 = 0 p_2=0 p2=0
3-gram:the the the (在参考答案中出现的最大次数为0, p 3 = 0 p_3=0 p3=0
4-gram:the the the the (在参考答案中出现的最大次数为0, p 4 = 0 p_4=0 p4=0
因此:
p n = 0.25 × p 1 + 0.25 × p 2 + 0.25 × p 3 + 0.25 × p 4 = 1 / 14 p_n=0.25\times p_1+0.25\times p_2+0.25\times p_3+0.25\times p_4=1/14 pn=0.25×p1+0.25×p2+0.25×p3+0.25×p4=1/14
当然这样还是有问题,就是这个算法对于短句有利,例如:
假如我们现在有一句中文:
猫在垫上。
Candidate(翻译结果): the.
Reference 1(参考答案1): the cat is on the mat.
Reference 2(参考答案2): there is a cat on the mat.
在Candidate中的:
1-gram:the (在参考答案中出现的最大次数为2, p 1 = 1 / 1 p_1=1/1 p1=1/1
2-gram:无 (在参考答案中出现的最大次数为0, p 2 = 0 p_2=0 p2=0
3-gram:无 (在参考答案中出现的最大次数为0, p 3 = 0 p_3=0 p3=0
4-gram:无 (在参考答案中出现的最大次数为0, p 4 = 0 p_4=0 p4=0
因此:
p n = 0.25 × p 1 + 0.25 × p 2 + 0.25 × p 3 + 0.25 × p 4 = 1 / 4 p_n=0.25\times p_1+0.25\times p_2+0.25\times p_3+0.25\times p_4=1/4 pn=0.25×p1+0.25×p2+0.25×p3+0.25×p4=1/4
明显看到翻译结果还是很烂,但是句子变短以后分数变大了。
再次改进:对长度加上惩罚因子BP(BLEU punishment)。
B P = { 1  if  c > r e ( 1 − r / c )  if  c < r BP=\begin{cases} &1 \quad \text{ if } c>r \\ &e^{(1-r/c)} \quad \text{ if } c<r \end{cases} BP={1 if c>re(1r/c) if c<r
公式中r是Reference参考答案的长度,c是Candidate翻译结果的长度,从公式中可以看到当翻译结果的长度大于参考答案的长度,BP为1,不惩罚;当翻译结果的长度小于参考答案的长度时, ( 1 − r / c ) < 0 , e ( 1 − r / c ) < 1 (1-r/c)<0,e^{(1-r/c)}<1 (1r/c)<0,e(1r/c)<1
平均指标BLEU公式变成:
B L E U = B P ⋅ exp ( ∑ n = 1 N w n log p n ) BLEU=BP\cdot \text{exp}\left(\sum_{n=1}^Nw_n\text{log}p_n\right) BLEU=BPexp(n=1Nwnlogpn)
这里用的是4-gram所以权重 w n w_n wn有四个,分别对应1-gram到4-gram,这里还对 p n p_n pn进行了log处理,log处理后大的会更大,接近0(越小)对应值越小。
https://www.shuxuele.com/algebra/exponents-logarithms.html

机器翻译简介

机器翻译:使用机器自动将某种语言的一句话翻译成另外一种语言。
意义:可以解决人类之间因为不同语言交流不畅的问题。
16年的图:
在这里插入图片描述
机器翻译领域还有很多问题函待解决,19年ACL的BEST paper就是关于机器翻译中曝光度的研究。

机器翻译相关方法

Generating Sequences With Recurrent Neural Networks
在这里插入图片描述
加解码器来自下面的文章:
Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation(好熟悉的文章名。。。)
Encoder:普通的LSTM,将一句话映射成一个向量C。
Decoder:对于隐藏层:
h t = f ( h t − 1 , y t − 1 , c ) h_t=f(h_{t-1},y_{t-1},c) ht=f(ht1,yt1,c)
对于输出层:
P y t = g ( h t , y t − 1 , c ) P_{y_t}=g(h_{t},y_{t-1},c) Pyt=g(ht,yt1,c)
在这里插入图片描述

前期知识储备

·了解LSTM以及多层LSTM
·LSTM是最常用的RNN模型之一,RNN介绍可以参考
点我去看
·多层LSTM:实质上是单层LSTM的叠加
·了解Seq2Seq模型
·了解Seq2Seq模型的相关概念以及其中Encoder和Decoder的含义。可以参考点我去看

第二课 论文精读

论文整体框架

摘要

  1. DNN在很多任务上取得了非常好的结果,但是它并不能解决Seq2Seq模型。Deep Neural Networks (DNNs) are powerful models that have achieved excellent performance on difficult learning tasks. Although DNNs work well whenever large labeled training sets are available, they cannot be used to map sequences to sequences.
  2. 我们使用多层LSTM作为Encoder和Decoder,并且在WMT14英语到法语上取得了34.8的BLEU 的结果。In this paper, we present a general end-to-end approach to sequence learning that makes minimal assumptions on the sequence structure. Our method uses a multilayered Long Short-Term Memory (LSTM) to map the input sequence to a vector of a fixed dimensionality, and then another deep LSTM to decode the target sequence from the vector. Our main result is that on an English to French translation task from the WMT-14 dataset, the translations produced by the LSTM achieve a BLEU score of 34.8 on the entire test set, where the LSTM’s BLEU score was penalized on out-of-vocabulary words.
  3. 此外,LSTM在长度上表现也很好,我们使用深度NMT模型来对统计机器翻译的结果进行重排序,能够使结果BLEU从33.3提高到36.5。Additionally, the LSTM did not have difficulty on long sentences. For comparison, a phrase-based SMT system achieves a BLEU score of 33.3 on the same dataset. When we used the LSTM to rerank the 1000 hypotheses produced by the aforementioned SMT system, its BLEU score increases to 36.5, which is close to the previous state of the art.
  4. LSTM能够很好地学习到局部和全局的特征,最后我们发现对源句子倒序输入能够大大提高翻译的效果,因为这样可以缩短一些词从源语言到目标语言的依赖长度。t. The
    LSTM also learned sensible phrase and sentence representations that are sensitive to word order and are relatively invariant to the active and the passive voice. Finally, we found that reversing the order of the words in all source sentences (but not target sentences) improved the LSTM’s performance markedly, because doing so introduced many short term dependencies between the source and the target sentence which made the optimization problem easier.

1.介绍

  1. 深度神经网络非常成功,但是却很难处理序列到序列的问题。
  2. 本文使用一种新的Seq2Seq模型结果来解决序列到序列的问题,其中Seq2Seq模型的Encoder
    和Decoder都使用的是LSTM。
  3. 前人研究者针对这个问题已经有了很多工作,包括Seq2Seq模型和注意力机制。
  4. 本文的深度Seq2Seq模型在机器翻译上取得了非常好的效果。

2.模型
3.实验
4相关工作
5&6.总结&致谢

  1. Introduction
  2. The Model
  3. Experiments
    3.1 Dataset details
    3.2 Decoding and Rescoring
    3.3 Reversing the Source Sentences
    3.4 Training details
    3.5 Parallelization
    3.6 Experimental Results
    3.7 Performance on long sentences
    3.8 Model Analysis
  4. Related Work
  5. Conclusion

传统/经典算法模型

1.Encoder-Decoder(见导读)

2.基于attention的机器翻译

来自之前读过的:Neural Machine Translation by Jointly Learning to Align and Translate
Encoder:单层双向LSTM。(就是下图中下面两层方框)
Decoder:
对于输出:
p ( y i ) = g ( y i − 1 , s i , c i ) p(y_i)=g(y_{i-1},s_i,c_i) p(yi)=g(yi1,si,ci)
对于 c i c_i ci
c i = ∑ j = 1 T x a i j h j , a i j = e x p ( e i j ) ∑ k = 1 T x e x p ( e i k ) c_i=\sum_{j=1}^{T_x}a_{ij}h_j,a_{ij}=\frac{exp(e_{ij})}{\sum_{k=1}^{T_x}exp(e_{ik})} ci=j=1Txaijhj,aij=k=1Txexp(eik)exp(eij)
w h e r e   e i j = a ( s j − 1 , h j ) where \space e_{ij}=a(s_{j-1},h_j) where eij=a(sj1,hj)
在这里插入图片描述

本文模型

深度神经机器翻译模型Deep Neural Machine Translation Model,Deep NMT模型
1.Encoder和Decoder是不同的LSTM。(下图中有白色Encoder和黄色Decoder)不同颜色的的两个LSTM之间参数当然不共享
2.使用4层的深度LSTM。(从下往上数)隐层参数以白色那个为例应该是:
h 1 4 h 2 4 h 3 4 h 1 3 h 2 3 h 3 3 h 1 2 h 2 2 h 3 2 h 1 1 h 2 1 h 3 1 \begin{matrix} h_1^4 &h_2^4 & h_3^4\\ h_1^3 &h_2^3 & h_3^3\\ h_1^2 &h_2^2 & h_3^2\\ h_1^1 &h_2^1 & h_3^1 \end{matrix} h14h13h12h11h24h23h22h21h34h33h32h31
白色Encoder和黄色Decoder的参数是同时训练的。
3.将输入逆序输入。(原输入是ABC)这样输入的开始部分和输出离得比较近,因此开始部分翻译效果比较好,但是会导致后面部分翻译效果一般。
在这里插入图片描述
4.最下面那里的黄色的输入是训练的时候才有的。
5.本文算法里面貌似还用到了维特比Beam Search,不展开写。

Tricks

  1. 对于Encoder和Deocder,使用不同的LSTM。
  2. 深层的LSTM比浅层的LSTM效果好。(当然也不能太深,太深不好训练,也容易梯度消失)
  3. 对源语言倒序输入(Reverse the order)会大幅度提高翻译效果。
  4. 对句子划分batch之前按长度进行排序,避免了大量的由于长句产生的pad,将速度提高了2倍

应用

  1. 是谷歌翻译的基础。就是后面的GNMT
  2. 多层的LSTM配合Attention是Transformer出来前最好的模型。

实验和结果

数据集

WMT’14English to French:包含36M英语到法语的双语语料,是机器翻译领域最常用的语料之一。
lwslt14English to German:包含170K英语到德语的双语语料,机器翻译领域最常用的语料之一,是一个相对较小的语料。(这个能跑)
在这里插入图片描述
数据集中语料是英文和德文个一个
各取前4句看看:

<url>http://www.ted.com/talks/stephen_palumbi_following_the_mercury_trail.html</url>
It can be a very complicated thing, the ocean.
And it can be a very complicated thing, what human health is.
And bringing those two together might seem a very daunting task, but what I'm going to try to say is that even in that complexity, there's some simple themes that I think, if we understand, we can really move forward.
And those simple themes aren't really themes about the complex science of what's going on, but things that we all pretty well know.
<url>http://www.ted.com/talks/lang/de/stephen_palumbi_following_the_mercury_trail.html</url>
Das Meer kann ziemlich kompliziert sein.
Und was menschliche Gesundheit ist, kann auch ziemlich kompliziert sein.
Und diese zwei zusammen zu bringen, erscheint vielleicht wie eine gewaltige Aufgabe. Aber was ich Ihnen zu sagen versuche ist, dass es trotz dieser Komplexität einige einfache Themen gibt, von denen ich denke, wenn wir diese verstehen, können wir uns wirklich weiter entwickeln.
Und diese einfachen Themen sind eigentlich keine komplexen wissenschaftlichen Zusammenhänge, sondern Tatsachen,die wir alle gut kennen.

实验结果

Deep NMT直接和baseline模型的对比。
本文提出的模型在单模型上比Bahdanau的基于attention的模型(第一个)要好得多,并且继承多个模型之后得到了非常好的结果。
single代表单个模型,不是单层LSTM。
这里用到了Beam Search,每个分支保留1个结果,到每个分支保留2个结果时,得分增加比较明显(从33.00到34.50),再增加到每个分支12个结果,效果没有这么明显(从34.50到34.81)。
在这里插入图片描述
使用本文提出的模型结合统计机器翻译模型(具体就是对机器翻译的结果进行重新打分,排序),最终得到了比state-of-the-art只差0.5的相当好的结果。
关于打分原文是:
We also used the LSTM to rescore the 1000-best lists produced by the baseline system [29]. To rescore an n-best list, we computed the log probability of every hypothesis with our LSTM and took an even average with their score and the LSTM’s score.
意思就是用统计机器翻译(SMT)的得分与本文模型得分进行平均,然后结果排序,选最好那个。
在这里插入图片描述
该模型对词序是敏感的(以下几个图是通过PCA对8000维的词表示进行降维得到的二维结果):
在这里插入图片描述
对于主动和被动语态也分得很清楚:
在这里插入图片描述
翻译结果和长度(左边)以及词频(右边)的关系。
在这里插入图片描述

讨论和总结

讨论

为什么要讲这篇论文?
这篇论文提出的多层LSTM的结构是transformer出来之前机器翻译领域的标准做法。
本文提出的模型有何缺点?
LSTM的缺点就是并行差,所以后来有很多使用CNN和transformer做机器翻译的文章。
后来的改进模型?
后来的改进主要集中在attention上。

总结(主要创新点)

A.对于Encoder和Decoder使用不同的LSTM。
B.在Encoder和Decoder中都是用了多层LSTM。
C.对输入进行逆序输入可以大大提高翻译效果。

对比模型:本文对比了基于attention的神经机器翻译模型和传统的统计机器翻译模型。
模型:本文提出了一种基于多层LSTM的神经机器翻译模型。
实验:本文提出的模型在单模型和结合统计机器翻译的模型上都取得了非常好的结果。

关键点
• 验证了Seq2Seq模型对于序列到序列任务的有效性。
• 从实验的角度发现了很多提高翻译效果的tricks
• Deep NMT模型
创新点
• 提出了一种新的神经机器翻译模型—Deep NMT模型
• 提出了一些提高神经机器翻译效果的tricks——多层LSTM和倒序输入等。
• 在WMT14英语到法语翻译上得到了非常好的结果。

启发点
• Seq2Seq模型就是使用一个LSTM提取输入序列的特征,每个时间步输入一个词,从而生成固定维度的句子向量表示,然后Deocder使用另外一个LSTM来从这个向量中生成输入序列。
The idea is to use one LSTM to read the input sequence, one timestep at a time, to obtain large fixed dimensional vector representation, and then to use another LSTM to extract the output sequence
from that vector(Introduction P3)


这里的Encoder和Decoder的思想非常重要,这里是用LSTM来作为Encoder和Decoder,后面还出现了CNN,RNN作为Encoder和Decoder的文章。


• 我们的实验也支持这个结论,我们的模型生成的句子表示能够明确词序信息,并且能够识别出来同一 种含义的主动和被动语态。
A qualitative evaluation supports this claim, showing that our model is aware of word order and is fairly invariant to the active and passive voice.(Introduction P8)

参考论文

D. Bahdanau,K. Cho, and Y. Bengio. Neural machine translation by jointly learning to align and translate. arXiv preprint arXiv:1409.0473,2014.

代码复现

代码结构

在这里插入图片描述

数据集

IWSLT14
19M左右
原始数据中有xml格式的,要预先处理。
弄成这样
在这里插入图片描述
在训练集里面还有一些网站url标签,读取的时候要过滤掉
最后的训练数据大概16w条
载入数据集后打印第一条的
print(iwslt_data.source_data[0])#反向之后pad之后的0在前面了
print(iwslt_data.target_data_input[0])#以"": 2开头
print(iwslt_data.target_data[0])#以"": 3结尾
在这里插入图片描述

数据处理

这里是要读取源语言和目标语言,所以处理两种语言代码有部分不一样,要分开写,具体看注释

# coding:utf-8
from torch.utils import data
import os
import nltk
import numpy as np
import pickle
from collections import Counter


# ·加载数据集
# ·读取双语语料,这个步骤源语言和目标语言要同时做,防止长度不一致
# ·创建word2id
# ·将数据转化成id,这个步骤源语言和目标语言要分开处理,源语言要逆序处理,目标语言要加结尾标志

class iwslt_Data(data.DataLoader):
    # 这里要指定初始化的文件名,可以是训练集、验证集、测试集
    def __init__(self, source_data_name="train.tags.de-en.de", target_data_name="train.tags.de-en.en",
                 source_vocab_size=30000, target_vocab_size=30000):
        self.path = os.path.abspath(".")
        if "data" not in self.path:
            self.path += "/data"
        self.source_data_name = source_data_name
        self.target_data_name = target_data_name
        self.source_vocab_size = source_vocab_size
        self.target_vocab_size = target_vocab_size
        self.source_data, self.target_data, self.target_data_input = self.load_data()

    # 加载数据集
    def load_data(self):

        raw_source_data = open(self.path + "/iwslt14/" + self.source_data_name, encoding="utf-8").readlines()
        raw_target_data = open(self.path + "/iwslt14/" + self.target_data_name, encoding="utf-8").readlines()
        # 每行都有一个/n的换行符,要去掉
        raw_source_data = [x[0:-1] for x in raw_source_data]
        raw_target_data = [x[0:-1] for x in raw_target_data]
        # 打印原语言语料和目标语言语料记录数量
        print(len(raw_target_data))
        print(len(raw_source_data))
        source_data = []
        target_data = []
        for i in range(len(raw_source_data)):
            # 对语料进行过滤,为空的不要,带标签的<URL>的不要
            # 这里要注意,源语言和目标语言要同时进行过滤,否则会造成两个语料长度不一样
            if raw_target_data[i] != "" and raw_source_data[i] != "" and raw_source_data[i][0] != "<" and \
                    raw_target_data[i][0] != "<":
                # 过滤后进行分词
                source_sentence = nltk.word_tokenize(raw_source_data[i], language="german")
                target_sentence = nltk.word_tokenize(raw_target_data[i], language="english")
                # 句子太长不好训练,因此对超过100的句子进行截断,这个长度可以改写为参数
                # 当然太短的句子也应该过滤掉,这里没有处理
                # 这里也没有对大小写进行转化
                if len(source_sentence) <= 100 and len(target_sentence) <= 100:
                    source_data.append(source_sentence)
                    target_data.append(target_sentence)
        if not os.path.exists(self.path + "/iwslt14/source_word2id"):
            source_word2id = self.get_word2id(source_data, self.source_vocab_size)
            target_word2id = self.get_word2id(target_data, self.target_vocab_size)
            self.source_word2id = source_word2id
            self.target_word2id = target_word2id
            pickle.dump(source_word2id, open(self.path + "/iwslt14/source_word2id", "wb"))
            pickle.dump(target_word2id, open(self.path + "/iwslt14/target_word2id", "wb"))
        else:
            self.source_word2id = pickle.load(open(self.path + "/iwslt14/source_word2id", "rb"))
            self.target_word2id = pickle.load(open(self.path + "/iwslt14/target_word2id", "rb"))
        source_data = self.get_id_datas(source_data, self.source_word2id)
        target_data = self.get_id_datas(target_data, self.target_word2id, is_source=False)

        # 根据原文的图1,当目标语言作为标签进行训练的时候,前面还有一个<EOS>,就是我们定义的"<start>": 2,这里加上这个起始标志2
        target_data_input = [[2] + sentence[0:-1] for sentence in target_data]
        source_data = np.array(source_data)
        target_data = np.array(target_data)
        target_data_input = np.array(target_data_input)
        return source_data, target_data, target_data_input

    # 创建word2id
    def get_word2id(self, data, word_num):
        words = []
        for sentence in data:
            for word in sentence:
                words.append(word)
        # 取词频前word_num个词安排进入词表,这里为4个特殊词留了坑
        word_freq = dict(Counter(words).most_common(word_num - 4))
        word2id = {"<pad>": 0, "<unk>": 1, "<start>": 2, "<end>": 3}
        # 根据词频分配id
        for word in word_freq:
            word2id[word] = len(word2id)
        return word2id

    # 将数据转化成id
    def get_id_datas(self, datas, word2id, is_source=True):
        for i, sentence in enumerate(datas):
            for j, word in enumerate(sentence):
                datas[i][j] = word2id.get(word, 1)
            # 源语言数据转ID不用加"<start>": 2和"<end>": 3,但是需要将输入逆序处理,长度不够100的补齐"<pad>": 0
            if is_source:
                datas[i] = datas[i][0:100] + [0] * (100 - len(datas[i]))
                datas[i].reverse()
            else:  # 目标语言数据转ID,要在前后加"<end>": 3,长度不够100的补齐"<pad>": 0
                datas[i] = datas[i][0:99] + [3] + [0] * (99 - len(datas[i]))
        return datas

    def __getitem__(self, idx):
        return self.source_data[idx], self.target_data_input[idx], self.target_data[idx]

    def __len__(self):
        return len(self.source_data)


if __name__ == "__main__":
    iwslt_data = iwslt_Data()
    print(iwslt_data.source_data.shape)
    print(iwslt_data.target_data_input.shape)
    print(iwslt_data.target_data.shape)
    print(iwslt_data.source_data[0])
    print(iwslt_data.target_data_input[0])
    print(iwslt_data.target_data[0])

模型构建

# -*- coding: utf-8 -*-
import torch
import torch.nn as nn
import numpy as np


class Deep_NMT(nn.Module):
    def __init__(self, source_vocab_size, target_vocab_size, embedding_size,
                 source_length, target_length, lstm_size):
        super(Deep_NMT, self).__init__()
        # 源语言和目标语言的词表示大小embedding_size、lstm_size可以不一样,但是一般设置为一样大小
        self.source_embedding = nn.Embedding(source_vocab_size, embedding_size)
        self.target_embedding = nn.Embedding(target_vocab_size, embedding_size)
        # 这里要batch_first设置为True,如果不设置这个,那么输入维度大小及顺序为:length,batch_size,embedding_size
        # 懒得做转置什么的处理,就用了batch_first=True
        self.encoder = nn.LSTM(input_size=embedding_size, hidden_size=lstm_size, num_layers=4,
                               batch_first=True)
        self.decoder = nn.LSTM(input_size=embedding_size, hidden_size=lstm_size, num_layers=4,
                               batch_first=True)
        self.fc = nn.Linear(lstm_size, target_vocab_size)

    def forward(self, source_data, target_data, mode="train"):
        source_data_embedding = self.source_embedding(source_data)  # batch_size*length*embedding_size
        # 有4个LSTM叠在一起,这里只返回最上面那个LSTM的所有hidden layer到enc_output
        # enc_output.shape:batch_size*length*lstm_size
        # Encoder最右边的隐藏层h和c(最后一个timestep的h和c),一共两个list
        # enc_hidden:[h1,h2,h3,h4],[c1,c2,c3,c4]
        enc_output, enc_hidden = self.encoder(source_data_embedding)
        # 训练和测试不一样,训练的Decoder有真实标签的输入,具体看原文图1
        # 测试的时候没有标签输入,需要一步一步的预测,因为每一次预测都要先拿上一步的结果作为输入
        if mode == "train":
            # 注意,这里的target_data是添加了起始标志的,相当于load中的target_data_input
            target_data_embedding = self.target_embedding(target_data)  # batch_size*length*embedding_size
            # 将Encoder最后时间步中带有两个list的enc_hidden作为Decoder的输入,得到dec_output, dec_hidden
            # 有4个LSTM叠在一起,这里只返回最上面那个LSTM的所有hidden layer到dec_output
            # dec_output.shape:batch_size*length*lstm_size
            # Decoder最右边的隐藏层h和c(最后一个timestep的h和c),一共两个list
            # dec_hidden:[h1,h2,h3,h4],[c1,c2,c3,c4]
            dec_output, dec_hidden = self.decoder(target_data_embedding, enc_hidden)
            # 接FC层,softmax留在loss中做
            outs = self.fc(dec_output)  # batch_size*length*vocab_size
        # 这里是测试
        else:
            # 测试时没有真实的标签,全靠预测,从开始标签开始,所以相当于length为1
            # #batch_size*1*embedding_size
            target_data_embedding = self.target_embedding(target_data)
            # Decoder最右边的隐藏层h和c:[h1,h2,h3,h4],[c1,c2,c3,c4],这个作为第一个时间步的输入
            dec_prev_hidden = enc_hidden
            outs = []
            # 按时间步来进行预测,这里每一步对应Decoder(4个LSTM叠在一起)的每一列
            for i in range(100):
                # dec_output.shape:batch_size*1*lstm_size
                # dec_hidden:[h1,h2,h3,h4],[c1,c2,c3,c4]
                dec_output, dec_hidden = self.decoder(target_data_embedding, dec_prev_hidden)
                pred = self.fc(dec_output)# batch_size*1*target_vacab_size
                pred = torch.argmax(pred, dim=-1)# batch_size*1
                outs.append(pred.squeeze().cpu().numpy())
                dec_prev_hidden = dec_hidden# [h1,h2,h3,h4],[c1,c2,c3,c4]
                target_data_embedding = self.target_embedding(pred)  # batch_size*1*embedding_size
        return outs# 100*batch_size


if __name__ == "__main__":
    deep_nmt = Deep_NMT(source_vocab_size=30000, target_vocab_size=30000, embedding_size=256,
                        source_length=100, target_length=100, lstm_size=256)
    source_data = torch.Tensor(np.zeros([64, 100])).long()
    target_data = torch.Tensor(np.zeros([64, 100])).long()
    preds = deep_nmt(source_data, target_data)
    print(preds.shape)
    target_data = torch.Tensor(np.zeros([64, 1])).long()
    preds = deep_nmt(source_data, target_data, mode="test")
    print(np.array(preds).shape)

训练和测试

# -*- coding: utf-8 -*-
import torch
import torch.autograd as autograd
import torch.nn as nn
import torch.optim as optim
from model import Deep_NMT
from data import iwslt_Data
import numpy as np
from tqdm import tqdm
import config as argumentparser

config = argumentparser.ArgumentParser()
torch.manual_seed(config.seed)
from nltk.translate.bleu_score import corpus_bleu

if config.cuda and torch.cuda.is_available():
    torch.cuda.set_device(config.gpu)


# 求验证集loss
def get_dev_loss(data_iter):
    model.eval()
    process_bar = tqdm(data_iter)
    loss = 0
    for source_data, target_data_input, target_data in process_bar:
        if config.cuda and torch.cuda.is_available():
            source_data = source_data.cuda()
            target_data_input = target_data_input.cuda()
            target_data = target_data.cuda()
        else:
            source_data = torch.autograd.Variable(source_data).long()
            target_data_input = torch.autograd.Variable(target_data_input).long()
        target_data = torch.autograd.Variable(target_data).squeeze()
        out = model(source_data, target_data_input)
        loss_now = criterion(out.view(-1, 30000), autograd.Variable(target_data.view(-1).long()))
        weights = target_data.view(-1) != 0  # 用target_data生成mask,为0的位置无需loss更新,只算为1的(也就是有值的)位置
        # loss_now * weights.float()只会计算mask为1的部分,下面的分母计算求和也只会计算mask为1的部分
        loss_now = torch.sum((loss_now * weights.float())) / torch.sum(weights.float())
        loss += loss_now.data.item()
    return loss


# 生成测试集的BLEU分数
def get_test_bleu(data_iter):
    model.eval()
    process_bar = tqdm(data_iter)
    refs = []
    preds = []
    for source_data, target_data_input, target_data in process_bar:
        # target_input本来全部都是0,然后都加2,变成了start
        # 原来是batch_size*1大小都是0,现在是batch_size*1大小都是2
        target_input = torch.Tensor(np.zeros([source_data.shape[0], 1]) + 2)
        if config.cuda and torch.cuda.is_available():
            source_data = source_data.cuda()
            target_input = target_input.cuda().long()
        else:
            source_data = torch.autograd.Variable(source_data).long()
            target_input = torch.autograd.Variable(target_input).long()
        # 这里计算BLEU可以直接用ID进行计算,反正是算词频,用词本身和用词ID都一样
        target_data = target_data.numpy()
        out = model(source_data, target_input, mode="test")
        out = np.array(out).T  # 64*100
        tmp_preds = []
        for i in range(out.shape[0]):
            tmp_preds.append([])
        # 开始预测
        for i in range(out.shape[0]):
            for j in range(out.shape[1]):
                # 遇到EOS停止
                if out[i][j] != 3:
                    tmp_preds[i].append(out[i][j])
                else:
                    break
        preds += tmp_preds
        tmp_refs = []
        # 找到真实标签
        for i in range(target_data.shape[0]):
            tmp_refs.append([])
        for i in range(target_data.shape[0]):
            for j in range(target_data.shape[1]):
                if target_data[i][j] != 3 and target_data[i][j] != 0:
                    tmp_refs[i].append(target_data[i][j])
        # 随便每一个目标语言只有一个标签,也就是只有个ref,但是在计算BLEU的时候是可以有多个ref的
        # 所以这里外面还要加一层[]
        tmp_refs = [[x] for x in tmp_refs]
        refs += tmp_refs
    # 计算BLEU分数,最后乘以100
    bleu = corpus_bleu(refs, preds) * 100

    # 把预测结果和标签分别写入文件
    with open("./data/result.txt", "w") as f:
        for i in range(len(preds)):
            tmp_ref = [target_id2word[id] for id in refs[i][0]]
            tmp_pred = [target_id2word[id] for id in preds[i]]
            f.write("ref: " + " ".join(tmp_ref) + "\n")
            f.write("pred: " + " ".join(tmp_pred) + "\n")
            f.write("\n\n")
    return bleu


# 导入训练集
training_set = iwslt_Data()
training_iter = torch.utils.data.DataLoader(dataset=training_set,
                                            batch_size=config.batch_size,
                                            shuffle=True,
                                            num_workers=0)
# 导入验证集
valid_set = iwslt_Data(source_data_name="IWSLT14.TED.dev2010.de-en.de", target_data_name="IWSLT14.TED.dev2010.de-en.en")
valid_iter = torch.utils.data.DataLoader(dataset=valid_set,
                                         batch_size=config.batch_size,
                                         shuffle=True,
                                         num_workers=0)
# 导入测试集
test_set = iwslt_Data(source_data_name="IWSLT14.TED.tst2012.de-en.de", target_data_name="IWSLT14.TED.tst2012.de-en.en")
test_iter = torch.utils.data.DataLoader(dataset=test_set,
                                        batch_size=config.batch_size,
                                        shuffle=True,
                                        num_workers=0)
model = Deep_NMT(source_vocab_size=30000, target_vocab_size=30000, embedding_size=256,
                 source_length=100, target_length=100, lstm_size=256)
if config.cuda and torch.cuda.is_available():
    model.cuda()
# reduce=False代表返回的是一个向量,否则返回一个值
# 由于测试输入句子很多都经过pad补齐了0,这些0我们不想计算,如果返回向量,我们就可以适用一个mask对为0的位置进行过滤,不进行计算,加快运行速度。
criterion = nn.CrossEntropyLoss(reduce=False)
optimizer = optim.Adam(model.parameters(), lr=config.learning_rate)
loss = -1
target_id2word = dict([[x[1], x[0]] for x in training_set.target_word2id.items()])
for epoch in range(config.epoch):
    model.train()
    process_bar = tqdm(training_iter)
    for source_data, target_data_input, target_data in process_bar:
        if config.cuda and torch.cuda.is_available():
            source_data = source_data.cuda()
            target_data_input = target_data_input.cuda()
            target_data = target_data.cuda()
        else:
            source_data = torch.autograd.Variable(source_data).long()
            target_data_input = torch.autograd.Variable(target_data_input).long()
        target_data = torch.autograd.Variable(target_data).squeeze()
        out = model(source_data, target_data_input)

        loss_now = criterion(out.view(-1, 30000), autograd.Variable(target_data.view(-1).long()))
        weights = target_data.view(-1) != 0  # 添加mask忽略pad项
        loss_now = torch.sum((loss_now * weights.float())) / torch.sum(weights.float())
        if loss == -1:
            loss = loss_now.data.item()
        else:
            loss = 0.95 * loss + 0.05 * loss_now.data.item()  # loss平滑
        process_bar.set_postfix(loss=loss_now.data.item())
        process_bar.update()
        optimizer.zero_grad()
        loss_now.backward()
        optimizer.step()
    test_bleu = get_test_bleu(test_iter)
    print("test bleu is:", test_bleu)
    valid_loss = get_dev_loss(valid_iter)
    print("valid loss is:", valid_loss)

作业

多层LSTM为什么比单层LSTM要好,对输入逆序为什么能够提高效果,试分析原因?
答:这里的多层LSTM是指垂直方向的堆叠,横向的是不同时间步(sequence or timestep)的堆叠。多层LSTM从模型的复杂度的角度上来看,肯定要比单层的LSTM模型要复杂,从理论上来说,越复杂的模型其涵盖的特征空间就越大,也就是可以用来获取或者表达越复杂的特征。一个简单的例子就是,一条直线(线性模型),只能把平面粗暴划分成两类,但是如果二次方程可以拟合出复杂些的曲线。关于为什么要进行深度或者说多层的叠加,可以参考李宏毅老师Why Deep(上)Why Deep(下)
但是从实作上面来看,不可能无限的加深,因为会有梯度消失和梯度爆炸等问题困扰,当然为了解决这些问题,有残差网络、梯度cliff等思想的提出,但是计算能力以及训练难度也是拦路虎,越复杂的模型计算能力要求越高,训练数据要求越多。
关于词的逆序输入问题,原文已经解释很清楚了,因此大量的句子进行输入的时候,后面是pad进行补齐到指定长度的,因此都是数据在前,屁股后面一堆0的情况,而最终Encoder得到的向量和距离它最近的层相关性会比较强,我们把输入数据反序后,0在前,数据靠后,就使得数据接近Encoder得到的向量,效果自然有变好。不知道Bi-LSTM是不是也是解决这个问题的方法之一。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

oldmao_2000

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值