实验目的:
基于mindspore搭建seq2seq模型实现文本翻译
实验内容:
- 数据准备:
-
使用的数据集为Multi30K数据集,大规模的图像-文本数据集,包含30K+图片,每张图片对应两类不同的文本描述:
- 英语描述,及对应的德语翻译;
- 五个独立的、非翻译而来的英语和德语描述,描述中包含的细节并不相同;
-
数据下载模块:
-
使用使用download进行数据下载,并将tar.gz文件解压到指定文件夹。
下载好的数据集目录结构如下:
-
-
数据预处理
-
加载数据集,目前数据为句子形式的文本,需要进行分词,即将句子拆解为单独的词元(token,可以为字符或者单词);
定义一个名为Multi30K的类,用于加载和处理Multi30K数据集。该类通过读取下载的文件,对德语(de)和英语(en)文本进行分词处理,并以元组形式组织成(德语句子, 英语句子)对,便于后续处理。
创建Multi30K实例分别对应训练、验证和测试数据集。
打印出一个测试数据集中的德英句子对作为预览。 -
将每个词元映射到从0开始的数字索引中(为节约存储空间,可过滤掉词频低的词元),词元和数字索引所构成的集合叫做词典(vocabulary);
-
添加特殊占位符,标明序列的起始与结束,统一序列长度,并创建数据迭代器;
定义了一个Vocab类,用于根据词频构建词汇表,包括特殊标记如(未知词)、(填充)、(句首)和(句尾)
-
使用collections中的Counter和OrderedDict统计英/德语每个单词在整体文本中出现的频率。构建词频字典,然后再将词频字典转为词典。
通过build_vocab函数统计训练数据集中德语和英语的词频,并利用这些统计信息创建德语和英语的词汇表实例,同时设置最小词频阈值为2。
-
数据迭代器
进一步处理数据(包括批处理,添加起始和终止符号,统一序列长度)后,将数据以张量的形式返回。创建数据迭代器需要如下参数:
- dataset:分词后的数据集
- de_vocab:德语词典
- en_vocab:英语词典
- batch_size:批量大小,即一个batch中包含多少个序列
- max_len:序列最大长度,为最长有效文本长度 + 2(序列开始、序列结束占位符),如不满则补齐,如超过则丢弃
- drop_remainder:是否在最后一个batch未满时,丢弃该batch
定义了一个Iterator类,用于生成训练、验证和测试数据的批次。它实现了对数据集的遍历、序列长度排序、填充至固定长度、编码为数字索引以及转换为MindSpore的Tensor格式等功能,以适配神经网络模型的输入要求。
配置三个迭代器实例,分别对应训练、验证和测试数据集,设置了批大小为128,最大序列长度为32,且在训练迭代器中启用了丢弃末尾不足一个批次的数据的功能。
-
- 模型构建:
- 定义基础组件:
- Encoder:负责编码输入序列。它首先利用嵌入层(Embedding Layer)将输入的单词索引转换为向量表示,接着通过双向GRU(Gated Recurrent Unit)捕获序列的前后上下文信息,并通过全连接层(Dense Layer)整合双向GRU的输出,为解码器提供初始隐藏状态。
- Attention:注意力机制使解码器能够根据当前解码状态有选择地关注输入序列的不同部分。它计算解码器隐藏状态与编码器输出之间的匹配程度,生成注意力权重,并据此加权求和编码器的输出,形成上下文向量。
- Decoder:解码器在编码器提供的上下文信息的基础上,逐步生成目标序列。它同样使用GRU更新隐藏状态,并结合注意力机制的结果及当前输入(或上一时刻的预测),最终通过全连接层预测下一个单词。
- 实现Seq2Seq模型
- Seq2Seq类:整合了编码器和解码器,实现了整体的序列到序列翻译流程。它还包含了一个create_mask方法来标识源序列中的填充(pad)位置,以在计算注意力权重时排除这些位置的影响。
- 在construct方法中,模型首先运行编码器处理输入序列,然后以开始标记()启动解码器。解码过程通过循环进行,每一步根据前一时间步的输出(或目标序列的真实词元,取决于是否使用Teacher Forcing策略)以及编码器输出和当前隐藏状态,通过注意力机制更新隐藏状态并预测下一个单词,直到达到目标序列长度。
- 模型训练:
- 初始化参数
- 定义模型所需的各种超参数,包括词汇表大小(输入输出维度)、嵌入层维度、隐藏层维度、Dropout比例以及填充(pad)索引等。
- 设置计算与返回数据类型,以及是否在昇腾(Ascend)硬件上运行的标志。
- 实例化模型组件:Attention层、Encoder、Decoder以及Seq2Seq模型本身,还包括优化器(Adam)和损失函数(CrossEntropyLoss),其中损失函数会忽略填充项的损失计算。
- 自定义函数
- clip_by_norm函数实现了梯度裁剪,确保训练过程中的稳定性和效率
- forward_fn定义了前向传播过程,计算模型输出及损失。
- train_step完成单步训练,包括前向传播、梯度计算、梯度裁剪和参数更新。
- train和evaluate函数分别用于模型的训练和验证,通过迭代数据集进行模型的训练或评估,并监控损失。
- 训练与评估循环
- 模型进行指定轮数的训练(num_epochs),每轮结束后评估验证集上的性能。
- 使用tqdm库显示训练进度条,增强用户体验。
- 每轮训练后,根据验证集上的损失决定是否保存当前最佳模型。
- 模型推理:
- 翻译句子功能
- 实现了translate_sentence函数,将输入的德语文本转换成英语
- 文本预处理、编码、通过模型预测解码,最后将预测的索引转回英语单词。
- 模型加载与测试
- 加载训练好的模型参数,对测试数据集中的示例进行翻译,并展示源句、目标句及模型预测的结果。
- src = [‘ein’, ‘mann’, ‘mit’, ‘einem’, ‘orangefarbenen’, ‘hut’, ‘,’, ‘der’, ‘etwas’, ‘anstarrt’, ‘.’]
trg = [‘a’, ‘man’, ‘in’, ‘an’, ‘orange’, ‘hat’, ‘starring’, ‘at’, ‘something’, ‘.’] - predicted trg = [‘a’, ‘man’, ‘in’, ‘an’, ‘orange’, ‘hat’, ‘is’, ‘something’, ‘something’, ‘.’]
- src = [‘ein’, ‘mann’, ‘mit’, ‘einem’, ‘orangefarbenen’, ‘hut’, ‘,’, ‘der’, ‘etwas’, ‘anstarrt’, ‘.’]
- 应用nltk库中的BLEU评分来量化模型的翻译质量,整个测试数据集上的表现通过calculate_bleu函数计算得出。
- 加载训练好的模型参数,对测试数据集中的示例进行翻译,并展示源句、目标句及模型预测的结果。
实验原理:
-
编码器(Encoder):
在编码器中,我们输入一个序列 X = { x 1 , x 2 , . . . , x T } X=\{x_1, x_2, ..., x_T\} X={x1,x2,...,xT},在embedding层将其转化为向量,循环计算隐藏状态 H = { h 1 , h 2 , . . . , h T } H=\{h_1, h_2, ..., h_T\} H={h1,h2,...,hT},并在最后的隐藏状态中返回上下文向量 z = h T z=h_T z=hT。实现编码器的方式有很多种,在这里我们使用的是门控循环单元模型(Gated Rrecurrent Units, GRU)。它在原始RNN的基础上引入了门机制(gate mechanism),用以控制输入隐藏状态和从隐藏状态输出的信息。其中,更新门(update gate, 又称记忆门,一般用 z t z_t zt表示)用于控制前一时刻的状态信息 h t − 1 h_{t-1} ht−1被带入到当前状态 h t h_t ht中的程度。重置门(reset gate,一般用 r t r_t rt表示)控制前一状态 h t h_t ht有多少信息被写入到当前候选集 n t n_t nt上。
h t = RNN ( e ( x t ) , h t − 1 ) h_t = \text{RNN}(e(x_t), h_{t-1}) ht=RNN(e(xt),ht−1)
在进行文本翻译类任务时,我们一般使用双向GRU,即在训练中同时考虑当前词语之前及之后的文本内容。双向GRU的每层由两个RNN构成,前向RNN由左至右循环计算隐藏状态,反向RNN从右至左计算隐藏状态,公式表达如下:
h t → = EncoderGRU → ( e ( x t → ) , h t − 1 → ) h t ← = EncoderGRU ← ( e ( x t ← ) , h t − 1 ← ) \begin{align*} h_t^\rightarrow &= \text{EncoderGRU}^\rightarrow(e(x_t^\rightarrow),h_{t-1}^\rightarrow)\\ h_t^\leftarrow &= \text{EncoderGRU}^\leftarrow(e(x_t^\leftarrow),h_{t-1}^\leftarrow) \end{align*} ht→ht←=EncoderGRU→(e(xt→),ht−1→)=EncoderGRU←(e(xt←),ht−1←)
每个RNN网络在观察到句子中的最后一个词后,输出一个上下文向量,前向RNN的输出为 z → = h T → z^\rightarrow=h_T^\rightarrow z→=hT→,反向RNN的输出为 z ← = h T ← z^\leftarrow=h_T^\leftarrow z←=hT←。
编码器最终会返回两项:
outputs
和hidden
。-
outputs
为双向GRU最上层隐藏状态,形状为[max_len, batch_size, hid_dim * num_directions]。以 t = 1 t=1 t=1时刻为例,其对应的output为前向RNN中 t = 1 t=1 t=1时刻最上层隐藏状态和反向RNN中 t = T t=T t=T时刻的结合,即 h 1 = [ h 1 → ; h T ← ] h_1 = [h_1^\rightarrow; h_{T}^\leftarrow] h1=[h1→;hT←]; -
hidden
表示每层的最终隐藏状态,即上文提到的上下文向量。后续将作为编码器初始时刻的隐藏状态 s 0 s_0 s0,但由于编码器(decoder)的结构并不是双向的,仅仅需要一个上下文向量 z z z,为了与之对应,我们将编码器中的两个向量组合起来,放入全连接层 g g g中,并最后使用激活函数 t a n h tanh tanh;
z = tanh ( g ( h T → , h T ← ) ) = tanh ( g ( z → , z ← ) ) = s 0 z=\tanh(g(h_T^\rightarrow, h_T^\leftarrow)) = \tanh(g(z^\rightarrow, z^\leftarrow)) = s_0 z=tanh(g(hT→,hT←))=tanh(g(z→,z←))=s0
MindSpore提供了GRU的接口,可以在编码器搭建中直接调用,通过设置参数
bidirectional=True
使用双向GRU。 -
-
注意力层(Attention):
在机器翻译中,每个生成的词可能对应源句子中不同的词,而传统的无注意力机制的Seq2Seq模型更偏向于关注句子中的最后一个词。为了进一步优化模型,我们引入了注意力机制。
注意力机制便是赋予源句子和目标句子中对应的词以更高的权重,它整合了我们目前为止编码与解码的所有信息,并输出一个表示注意力权重的向量 a t a_t at,用来决定在下一步的预测 y ^ t + \hat{y}_{t+} y^t+中应该给予哪些词更高的关注度。首先,我们需要明确编码器中的每一个隐藏状态和解码器中上一个时刻隐藏状态之间的匹配程度 E t E_t Et。
截止到当前的时刻 t t t,编码器(encoder)中的所有信息为全部前向和后向RNN的隐藏状态的组合 H H H,是一个有 T T T个张量的序列;解码器(decoder)中的所有信息为上一时刻的隐藏状态 s t − 1 s_{t-1} st−1,是一个单独的张量。为了统一二者的维度,我们需要将解码器中上一时刻的隐藏状态 s t − 1 s_{t-1} st−1重复 T T T次,接着把处理好的解码器信息与编码器信息堆叠起来,并输入到线性层
att
和激活函数 tanh \text{tanh} tanh中,计算编码器与解码器隐藏状态之间的能量 E t E_t Et。E t = tanh ( attn ( s t − 1 , H ) ) E_t = \tanh(\text{attn}(s_{t-1}, H)) Et=tanh(attn(st−1,H))
当前 E t E_t Et的每个batch中tensor的形状为[dec hid dim, src len],但是注意最终的注意力权重是需要作用在源序列之上的,所以注意力权重的维度也应该与源句子的维度[src len]相对应。为此,我们引入了一个可学习的张量 v v v。
a ^ t = v E t \hat{a}_t = v E_t a^t=vEt
我们可以将 v v v看作是所有编码器隐藏状态的加权和的权重,简单来说便是对源序列中的每个词的关注程度。 v v v的参数是随机初始化的,它会在反向传播中与模型的其余部分一起学习。此外, v v v并不依赖于时间,所以在解码中每个时间步长使用的 v v v是一致的。
最终,我们使用 softmax \text{softmax} softmax函数,来保证注意力向量 a t a_t at中每一个元素的大小都在0-1之间,并且所有元素加和为1。
a t = softmax ( a t ^ ) a_t = \text{softmax}(\hat{a_t}) at=softmax(at^)
-
解码器(Decoder)
解码器中包含了上述的注意力层,在获得注意力权重向量 a t a_t at后,我们将其应用在编码器的隐藏状态 H H H上,得到一个表示编码器隐藏状态加权和的向量 w t w_t wt。w t = a t H w_t = a_t H wt=atH
我们将该向量 w t w_t wt,连同embedding后的输入 d ( y t ) d(y_t) d(yt),上一时刻的隐藏状态 s t − 1 s_{t-1} st−1,一起放入编码器的RNN网络中,并将输出送入线性层 f f f,得到关于目标句子中下一时刻出现的单词的预测。
s t = DecoderGRU ( d ( y t ) , w t , s t − 1 ) s_t = \text{DecoderGRU}(d(y_t), w_t, s_{t-1}) st=DecoderGRU(d(yt),wt,st−1)
y ^ t + 1 = f ( d ( y t ) , w t , s t ) \hat{y}_{t+1} = f(d(y_t), w_t, s_t) y^t+1=f(d(yt),wt,st)
-
Seq2Seq:
Seq2Seq封装器将我们之前创建的编码器与解码器合并起来。简单梳理一下整体过程:
- 初始化空数列
outputs
,用于储存每次的预测结果; - 源序列 X X X作为编码器的输入,输出 z z z和 H H H;
- 解码器初始时刻的隐藏状态为编码器中输出的上下文向量,即编码器最后时刻的隐藏状态, s 0 = z = h T s_0 = z = h_T s0=z=hT;
- 解码器最开始的输入 y 1 y_1 y1为表示序列开始的占位符<bos>;
- 重复以下步骤:
- 将此时刻 t t t的输入 y t y_t yt,上一时刻的隐藏状态 s t − 1 s_{t-1} st−1,编码器中的所有隐藏状态 H H H作为输入;
- 输出对下一时刻的预测 y ^ t + 1 \hat{y}_{t+1} y^t+1,以及新的隐藏状态 s t s_t st;
- 将预测结果存入
outputs
中 - 确定是否使用teacher forcing,如是, y t + 1 = y ^ t + 1 y_{t+1} = \hat{y}_{t+1} yt+1=y^t+1,如否,下一时刻的输入为目标序列中的词;
- 初始化空数列
-
BLEU得分:
双语替换评测得分(bilingual evaluation understudy,BLEU)为衡量文本翻译模型生成出来的语句好坏的一种算法,它的核心在于评估机器翻译的译文 pred \text{pred} pred 与人工翻译的参考译文 label \text{label} label 的相似度。通过对机器译文的片段与参考译文进行比较,计算出各个片段的的分数,并配以权重进行加和,基本规则为:- 惩罚过短的预测,即如果机器翻译出来的译文相对于人工翻译的参考译文过于短小,则命中率越高,需要施加更多的惩罚;
- 对长段落匹配更高的权重,即如果出现长段落的完全命中,说明机器翻译的译文更贴近人工翻译的参考译文;
BLEU的公式如下:
e x p ( m i n ( 0 , 1 − l e n ( label ) l e n ( pred ) ) Π n = 1 k p n 1 / 2 n ) exp(min(0, 1-\frac{len(\text{label})}{len(\text{pred})})\Pi^k_{n=1}p_n^{1/2^n}) exp(min(0,1−len(pred)len(label))Πn=1kpn1/2n)
len(label)
:人工翻译的译文长度len(pred)
:机器翻译的译文长度p_n
:n-gram的精度
实验结果:
心得体会:
这次实验不仅让我对深度学习中的自然语言处理(NLP)领域有了更深入的了解,还让我在实际操作中锻炼了自己的编程和问题解决能力。
我深入了解了Seq2Seq模型的基本原理和组成部分,包括编码器、解码器以及注意力机制等。通过亲手实现这些组件,我更加清晰地理解了它们是如何协同工作以完成翻译任务的。