基于注意力的机器翻译

目录

一、什么是机器翻译

二、机器翻译完成相关知识介绍

2.1 编码器——解码器(seq2seq)

2.1.1 编码器

2.1.2 解码器

2.1.3 训练模型

2.2 束搜索

2.2.1 贪婪搜索

2.2.2 穷举搜索

2.2.3 束搜索

2.3 注意力机制

2.3.1 计算背景变量

3.1.1 矢量化计算

2.3.2 更新隐藏状态

2.3.3 发展

三、 上述知识应用(机器翻译)

3.1 数据读取和预处理

3.2 编码器

3.3 注意力机制

 3.4 含注意力机制的解码器

3.5 训练模型

3.6 预测不定长的序列

3.7 评价翻译结果

四、利用注意力机制进行机器翻译(日译中)

4.1 任务目的

4.2 开发环境

4.3数据集介绍

五、具体代码实现

5.1 Get the parallel dataset

5.2 Prepare the tokenizers

5.3 Build the TorchText Vocab objects and convert the sentences into Torch tensors

5.4 Create the DataLoader object to be iterated during training 

5.5 Sequence-to-sequence Transformer

5.6 Start training 

5.7 Try translating a Japanese sentence using the trained model 

5.8Save the Vocab objects and trained model


一、什么是机器翻译

机器翻译是指将一段文本从一种语言自动翻译到另一种语言。因为一段文本序列在不同语言中的长度不一定相同,所以我们使用机器翻译为例来介绍编码器—解码器和注意力机制的应用。

二、机器翻译完成相关知识介绍

2.1 编码器——解码器(seq2seq)

在许多自然语言处理应用中,输入和输出均可为不固定长度的序列。以机器翻译为例,输入可能是一段变长的英语文本序列,输出则可能是一段变长的法语文本序列,例如:

英语输入:“They”、“are”、“watching”、“.”

法语输出:“Ils”、“regardent”、“.”

当处理不定长序列输入输出时,我们可以采用编码器—解码器或seq2seq模型。这两种模型都涉及两个循环神经网络,分别为编码器和解码器。编码器用于分析输入序列,而解码器则用于生成输出序列。

在下图中展示了使用编码器—解码器将上述英语句子翻译为法语句子的一种方式。在训练数据集中,我们可以在每个句子后添加特殊符号“<eos>”(序列结束)以表示序列的终止。编码器的每个时间步的输入依次是英语句子中的单词、标点和特殊符号“<eos>”。在图中,编码器的最终时间步隐藏状态被用作输入句子的表征或编码信息。解码器在每个时间步中使用输入句子的编码信息、上个时间步的输出以及隐藏状态作为输入。我们期望解码器能够逐步正确输出翻译后的法语单词、标点和特殊符号"<eos>"。值得注意的是,在解码器的初始时间步,会使用表示序列开头的特殊符号"<bos>"(序列开始)。

图1 使用编码器—解码器将句子由英语翻译成法语。编码器和解码器分别为循环神经网络

2.1.1 编码器

编码器的作用是把一个不定长的输入序列变换成一个定长的背景变量𝑐𝑐,并在该背景变量中编码输入序列信息。常用的编码器是循环神经网络。

让我们考虑批量大小为1的时序数据样本。假设输入序列是𝑥1,…,𝑥𝑇𝑥1,…,𝑥𝑇,例如𝑥𝑖是输入句子中的第𝑖个词。在时间步𝑡𝑡,循环神经网络将输入𝑥𝑡的特征向量𝑥𝑡和上个时间步的隐藏状态ℎ𝑡−1变换为当前时间步的隐藏状态ℎ𝑡。我们可以用函数𝑓表达循环神经网络隐藏层的变换:

ℎ𝑡=𝑓(𝑥𝑡,ℎ𝑡−1).

接下来,编码器通过自定义函数𝑞将各个时间步的隐藏状态变换为背景变量

𝑐=𝑞(ℎ1,…,ℎ𝑇).

例如,当选择𝑞(ℎ1,…,ℎ𝑇)=ℎ𝑇时,背景变量是输入序列最终时间步的隐藏状态ℎ𝑇。

以上描述的编码器是一个单向的循环神经网络,每个时间步的隐藏状态只取决于该时间步及之前的输入子序列。我们也可以使用双向循环神经网络构造编码器。在这种情况下,编码器每个时间步的隐藏状态同时取决于该时间步之前和之后的子序列(包括当前时间步的输入),并编码了整个序列的信息。

2.1.2 解码器

刚刚已经介绍,编码器输出的背景变量𝑐𝑐编码了整个输入序列𝑥1,…,𝑥𝑇的信息。给定训练样本中的输出序列𝑦1,𝑦2,…,𝑦𝑇′,对每个时间步𝑡′(符号与输入序列或编码器的时间步𝑡𝑡有区别),解码器输出𝑦𝑡‘的条件概率将基于之前的输出序列𝑦1,…,𝑦𝑡′−1和背景变量,即𝑃(𝑦𝑡′∣𝑦1,…,𝑦𝑡′−1,𝑐)。

为此,我们可以使用另一个循环神经网络作为解码器。在输出序列的时间步𝑡′,解码器将上一时间步的输出𝑦𝑡′−1以及背景变量𝑐作为输入,并将它们与上一时间步的隐藏状态𝑠𝑡′−1变换为当前时间步的隐藏状态𝑠𝑡′。因此,我们可以用函数𝑔𝑔表达解码器隐藏层的变换:

𝑠𝑡′=𝑔(𝑦𝑡′−1,𝑐,𝑠𝑡′−1).

有了解码器的隐藏状态后,我们可以使用自定义的输出层和softmax运算来计算𝑃(𝑦𝑡′∣𝑦1,…,𝑦𝑡′−1,𝑐)𝑃(𝑦𝑡′∣𝑦1,…,𝑦𝑡′−1,𝑐),例如,基于当前时间步的解码器隐藏状态 𝑠𝑡′、上一时间步的输出𝑦𝑡′−1以及背景变量𝑐来计算当前时间步输出𝑦𝑡′的概率分布。

2.1.3 训练模型

根据最大似然估计,我们可以最大化输出序列基于输入序列的条件概率

𝑃(𝑦1,…,𝑦𝑇′∣𝑥1,…,𝑥𝑇)=∏𝑡′=1𝑇′𝑃(𝑦𝑡′∣𝑦1,…,𝑦𝑡′−1,𝑥1,…,𝑥𝑇)=∏𝑡′=1𝑇′𝑃(𝑦𝑡′∣𝑦1,…,𝑦𝑡′−1,𝑐),

并得到该输出序列的损失

−log𝑃(𝑦1,…,𝑦𝑇′∣𝑥1,…,𝑥𝑇)=−∑𝑡′=1𝑇′log𝑃(𝑦𝑡′∣𝑦1,…,𝑦𝑡′−1,𝑐),

在模型训练中,所有输出序列损失的均值通常作为需要最小化的损失函数。在图1所描述的模型预测中,我们需要将解码器在上一个时间步的输出作为当前时间步的输入。与此不同,在训练中我们也可以将标签序列(训练集的真实输出序列)在上一个时间步的标签作为解码器在当前时间步的输入。这叫作强制教学(teacher forcing)。

2.2 束搜索

上一节介绍了如何训练输入和输出均为不定长序列的编码器—解码器。本节我们介绍如何使用编码器—解码器来预测不定长的序列。

上一节里已经提到,在准备训练数据集时,我们通常会在样本的输入序列和输出序列后面分别附上一个特殊符号"<eos>"表示序列的终止。我们在接下来的讨论中也将沿用上一节的全部数学符号。为了便于讨论,假设解码器的输出是一段文本序列。设输出文本词典\gamma(包含特殊符号"<eos>")的大小为|\gamma |,输出序列的最大长度为T'。所有可能的输出序列一共有\mathcal{O}(\left|\mathcal{Y}\right|^{T'})种。这些输出序列中所有特殊符号"<eos>"后面的子序列将被舍弃。

2.2.1 贪婪搜索

让我们先来看一个简单的解决方案:贪婪搜索(greedy search)。对于输出序列任一时间步𝑡′𝑡′,我们从\left | \gamma \right |个词中搜索出条件概率最大的词

y_{t'}=\underset{y\epsilon \gamma }{argmax}P(y|y_{1},......y_{t'-1},c)

作为输出。一旦搜索出"<eos>"符号,或者输出序列长度已经达到了最大长度𝑇′𝑇′,便完成输出。

我们在描述解码器时提到,基于输入序列生成输出序列的条件概率是∏𝑇′𝑡′=1𝑃(𝑦𝑡′∣𝑦1,…,𝑦𝑡′−1,𝑐)。我们将该条件概率最大的输出序列称为最优输出序列。而贪婪搜索的主要问题是不能保证得到最优输出序列。

下面来看一个例子。假设输出词典里面有“A”“B”“C”和“<eos>”这4个词。图2中每个时间步下的4个数字分别代表了该时间步生成“A”“B”“C”和“<eos>”这4个词的条件概率。在每个时间步,贪婪搜索选取条件概率最大的词。因此,图10.9中将生成输出序列“A”“B”“C”“<eos>”。该输出序列的条件概率是0.5×0.4×0.4×0.6=0.0480.5×0.4×0.4×0.6=0.048。

图2 在每个时间步,贪婪搜索选取条件概率最大的词

接下来,观察图3演示的例子。与图2中不同,图3在时间步2中选取了条件概率第二大的词“C”。由于时间步3所基于的时间步1和2的输出子序列由图2中的“A”“B”变为了图3中的“A”“C”,图3中时间步3生成各个词的条件概率发生了变化。我们选取条件概率最大的词“B”。此时时间步4所基于的前3个时间步的输出子序列为“A”“C”“B”,与图2中的“A”“B”“C”不同。因此,图3中时间步4生成各个词的条件概率也与图2中的不同。我们发现,此时的输出序列“A”“C”“B”“<eos>”的条件概率是0.5×0.3×0.6×0.6=0.0540.5×0.3×0.6×0.6=0.054,大于贪婪搜索得到的输出序列的条件概率。因此,贪婪搜索得到的输出序列“A”“B”“C”“<eos>”并非最优输出序列。

图3 在时间步2选取条件概率第二大的词“C”

2.2.2 穷举搜索

如果目标是得到最优输出序列,我们可以考虑穷举搜索(exhaustive search):穷举所有可能的输出序列,输出条件概率最大的序列。

虽然穷举搜索可以得到最优输出序列,但它的计算开销\mathcal{O}(\left|\mathcal{Y}\right|^{T'})很容易过大。例如,当|\mathcal{Y}|=10000T'=10时,我们将评估10000^{10} = 10^{40}个序列:这几乎不可能完成。而贪婪搜索的计算开销是\mathcal{O}(\left|\mathcal{Y}\right|T'),通常显著小于穷举搜索的计算开销。例如,当|\mathcal{Y}|=10000T'=10时,我们只需评估10000\times10=10^5个序列。

2.2.3 束搜索

束搜索(beam search)是对贪婪搜索的一个改进算法。它有一个束宽(beam size)超参数。我们将它设为𝑘。在时间步1时,选取当前时间步条件概率最大的𝑘个词,分别组成𝑘个候选输出序列的首词。在之后的每个时间步,基于上个时间步的𝑘𝑘个候选输出序列,从k\left|\mathcal{Y}\right|个可能的输出序列中选取条件概率最大的𝑘个,作为该时间步的候选输出序列。最终,我们从各个时间步的候选输出序列中筛选出包含特殊符号“<eos>”的序列,并将它们中所有特殊符号“<eos>”后面的子序列舍弃,得到最终候选输出序列的集合。

图4 束搜索的过程。束宽为2,输出序列最大长度为3。候选输出序列有A、C、AB、CE、ABD和CED

图4通过一个例子演示了束搜索的过程。假设输出序列的词典中只包含5个元素,即\mathcal{Y} = \{A, B, C, D, E\},且其中一个为特殊符号“<eos>”。设束搜索的束宽等于2,输出序列最大长度为3。在输出序列的时间步1时,假设条件概率𝑃(𝑦1∣𝑐)𝑃(𝑦1∣𝑐)最大的2个词为𝐴𝐴和𝐶𝐶。我们在时间步2时将对所有的y_2 \in \mathcal{Y}都分别计算𝑃(𝑦2∣𝐴,𝑐)𝑃(𝑦2∣𝐴,𝑐)和𝑃(𝑦2∣𝐶,𝑐)𝑃(𝑦2∣𝐶,𝑐),并从计算出的10个条件概率中取最大的2个,假设为𝑃(𝐵∣𝐴,𝑐)𝑃(𝐵∣𝐴,𝑐)和𝑃(𝐸∣𝐶,𝑐)𝑃(𝐸∣𝐶,𝑐)。那么,我们在时间步3时将对所有的y_3 \in \mathcal{Y}都分别计算𝑃(𝑦3∣𝐴,𝐵,𝑐)𝑃(𝑦3∣𝐴,𝐵,𝑐)和𝑃(𝑦3∣𝐶,𝐸,𝑐)𝑃(𝑦3∣𝐶,𝐸,𝑐),并从计算出的10个条件概率中取最大的2个,假设为𝑃(𝐷∣𝐴,𝐵,𝑐)𝑃(𝐷∣𝐴,𝐵,𝑐)和𝑃(𝐷∣𝐶,𝐸,𝑐)𝑃(𝐷∣𝐶,𝐸,𝑐)。如此一来,我们得到6个候选输出序列:(1)𝐴;(2)𝐶;(3)𝐴、𝐵;(4)𝐶、𝐸;(5)𝐴、𝐵、𝐷和(6)𝐶、𝐸、𝐷。接下来,我们将根据这6个序列得出最终候选输出序列的集合。

在最终候选输出序列的集合中,我们取以下分数最高的序列作为输出序列:

1𝐿𝛼log𝑃(𝑦1,…,𝑦𝐿)=1𝐿𝛼∑𝑡′=1𝐿log𝑃(𝑦𝑡′∣𝑦1,…,𝑦𝑡′−1,𝑐),

其中𝐿为最终候选序列长度,𝛼一般可选为0.75。分母上的𝐿𝛼是为了惩罚较长序列在以上分数中较多的对数相加项。分析可知,束搜索的计算开销为\mathcal{O}(k\left|\mathcal{Y}\right|T')。这介于贪婪搜索和穷举搜索的计算开销之间。此外,贪婪搜索可看作是束宽为1的束搜索。束搜索通过灵活的束宽𝑘𝑘来权衡计算开销和搜索质量。

2.3 注意力机制

在上述过程中,解码器在各个时间步依赖相同的背景变量来获取输入序列信息。当编码器为循环神经网络时,背景变量来自它最终时间步的隐藏状态。

现在,让我们再次思考刚刚提到的翻译例子:输入为英语序列“They”“are”“watching”“.”,输出为法语序列“Ils”“regardent”“.”。不难想到,解码器在生成输出序列中的每一个词时可能只需利用输入序列某一部分的信息。例如,在输出序列的时间步1,解码器可以主要依赖“They”“are”的信息来生成“Ils”,在时间步2则主要使用来自“watching”的编码信息生成“regardent”,最后在时间步3则直接映射句号“.”。这看上去就像是在解码器的每一时间步对输入序列中不同时间步的表征或编码信息分配不同的注意力一样。这也是注意力机制的由来 。

仍然以循环神经网络为例,注意力机制通过对编码器所有时间步的隐藏状态做加权平均来得到背景变量。解码器在每一时间步调整这些权重,即注意力权重,从而能够在不同时间步分别关注输入序列中的不同部分并编码进相应时间步的背景变量。本节我们将讨论注意力机制是怎么工作的。

在“编码器—解码器”里我们区分了输入序列或编码器的索引𝑡与输出序列或解码器的索引𝑡′。该节中,解码器在时间步𝑡′的隐藏状态𝑠𝑡′=𝑔(𝑦𝑡′−1,𝑐,𝑠𝑡′−1),其中𝑦𝑡′−1是上一时间步𝑡′−1的输出𝑦𝑡′−1的表征,且任一时间步𝑡′使用相同的背景变量𝑐。但在注意力机制中,解码器的每一时间步将使用可变的背景变量。记𝑐𝑡′是解码器在时间步𝑡′的背景变量,那么解码器在该时间步的隐藏状态可以改写为

𝑠𝑡′=𝑔(𝑦𝑡′−1,𝑐𝑡′,𝑠𝑡′−1).

这里的关键是如何计算背景变量𝑐𝑡′和如何利用它来更新隐藏状态𝑠𝑡′。下面将分别描述这两个关键点。

2.3.1 计算背景变量

我们先描述第一个关键点,即计算背景变量。图5描绘了注意力机制如何为解码器在时间步2计算背景变量。首先,函数𝑎根据解码器在时间步1的隐藏状态和编码器在各个时间步的隐藏状态计算softmax运算的输入。softmax运算输出概率分布并对编码器各个时间步的隐藏状态做加权平均,从而得到背景变量。

图5 编码器—解码器上的注意力机制

具体来说,令编码器在时间步𝑡𝑡的隐藏状态为ℎ𝑡,且总时间步数为𝑇。那么解码器在时间步𝑡′的背景变量为所有编码器隐藏状态的加权平均:

𝑐𝑡′=∑𝑡=1𝑇𝛼𝑡′𝑡ℎ𝑡,

其中给定𝑡′时,权重𝛼𝑡′𝑡在𝑡=1,…,𝑇的值是一个概率分布。为了得到概率分布,我们可以使用softmax运算:

𝛼𝑡′𝑡=exp(𝑒𝑡′𝑡)∑𝑇𝑘=1exp(𝑒𝑡′𝑘),𝑡=1,…,𝑇.

现在,我们需要定义如何计算上式中softmax运算的输入𝑒𝑡′𝑡。由于𝑒𝑡′𝑡同时取决于解码器的时间步𝑡′和编码器的时间步𝑡,我们不妨以解码器在时间步𝑡′−1的隐藏状态𝑠𝑡′−1与编码器在时间步𝑡的隐藏状态ℎ𝑡为输入,并通过函数𝑎计算𝑒𝑡′𝑡:

𝑒𝑡′𝑡=𝑎(𝑠𝑡′−1,ℎ𝑡).

这里函数𝑎有多种选择,如果两个输入向量长度相同,一个简单的选择是计算它们的内积𝑎(𝑠,ℎ)=𝑠⊤ℎ。而最早提出注意力机制的论文则将输入连结后通过含单隐藏层的多层感知机变换:

𝑎(𝑠,ℎ)=𝑣⊤tanh(𝑊𝑠+𝑊ℎ),

其中𝑣、𝑊𝑠、𝑊ℎ都是可以学习的模型参数。

3.1.1 矢量化计算

我们还可以对注意力机制采用更高效的矢量化计算。广义上,注意力机制的输入包括查询项以及一一对应的键项和值项,其中值项是需要加权平均的一组项。在加权平均中,值项的权重来自查询项以及与该值项对应的键项的计算。

在上面的例子中,查询项为解码器的隐藏状态,键项和值项均为编码器的隐藏状态。 让我们考虑一个常见的简单情形,即编码器和解码器的隐藏单元个数均为ℎℎ,且函数𝑎(𝑠,ℎ)=𝑠⊤ℎ。假设我们希望根据解码器单个隐藏状态𝑠𝑡′−1∈ℝℎ和编码器所有隐藏状态ℎ𝑡∈ℝℎ,𝑡=1,…,𝑇来计算背景向量𝑐𝑡′∈ℝℎ。 我们可以将查询项矩阵𝑄∈ℝ1×ℎ设为𝑠⊤𝑡′−1𝑠𝑡′−1⊤,并令键项矩阵𝐾∈ℝ𝑇×ℎ和值项矩阵𝑉∈𝑅𝑇×ℎ相同且第𝑡行均为ℎ𝑡⊤。此时,我们只需要通过矢量化计算

softmax(𝑄𝐾⊤)𝑉

即可算出转置后的背景向量𝑐𝑡′⊤。当查询项矩阵𝑄的行数为𝑛时,上式将得到𝑛行的输出矩阵。输出矩阵与查询项矩阵在相同行上一一对应。

2.3.2 更新隐藏状态

现在我们描述第二个关键点,即更新隐藏状态。以门控循环单元为例,在解码器中我们可以对门控循环单元(GRU)中门控循环单元的设计稍作修改,从而变换上一时间步𝑡′−1的输出𝑦𝑡′−1、隐藏状态𝑠𝑡′−1和当前时间步𝑡′的含注意力机制的背景变量𝑐𝑡′。解码器在时间步𝑡′的隐藏状态为

𝑠𝑡′=𝑧𝑡′⊙𝑠𝑡′−1+(1−𝑧𝑡′)⊙𝑠̃ 𝑡′,

其中的重置门、更新门和候选隐藏状态分别为

𝑟𝑡′=𝜎(𝑊𝑦𝑟𝑦𝑡′−1+𝑊𝑠𝑟𝑠𝑡′−1+𝑊𝑐𝑟𝑐𝑡′+𝑏𝑟),

𝑧𝑡′=𝜎(𝑊𝑦𝑧𝑦𝑡′−1+𝑊𝑠𝑧𝑠𝑡′−1+𝑊𝑐𝑧𝑐𝑡′+𝑏𝑧),

𝑠̃ 𝑡′=tanh(𝑊𝑦𝑠𝑦𝑡′−1+𝑊𝑠𝑠(𝑠𝑡′−1⊙𝑟𝑡′)+𝑊𝑐𝑠𝑐𝑡′+𝑏𝑠)

其中含下标的𝑊和𝑏分别为门控循环单元的权重参数和偏差参数。

2.3.3 发展

本质上,注意力机制能够为表征中较有价值的部分分配较多的计算资源。这个有趣的想法自提出后得到了快速发展,特别是启发了依靠注意力机制来编码输入序列并解码出输出序列的变换器(Transformer)模型的设计 。变换器抛弃了卷积神经网络和循环神经网络的架构。它在计算效率上比基于循环神经网络的编码器—解码器模型通常更具明显优势。含注意力机制的变换器的编码结构在后来的BERT预训练模型中得以应用并令后者大放异彩:微调后的模型在多达11项自然语言处理任务中取得了当时最先进的结果 。不久后,同样是基于变换器设计的GPT-2模型于新收集的语料数据集预训练后,在7个未参与训练的语言模型数据集上均取得了当时最先进的结果 。除了自然语言处理领域,注意力机制还被广泛用于图像分类、自动图像描述、唇语解读以及语音识别。

三、 上述知识应用(机器翻译)

3.1 数据读取和预处理

我们先定义一些特殊符号。其中“<pad>”(padding)符号用来添加在较短序列后,直到每个序列等长,而“<bos>”和“<eos>”符号分别表示序列的开始和结束。

接着定义两个辅助函数对后面读取的数据进行预处理。

为了演示方便,我们在这里使用一个很小的法语—英语数据集。在这个数据集里,每一行是一对法语句子和它对应的英语句子,中间使用'\t'隔开。在读取数据时,我们在句末附上“<eos>”符号,并可能通过添加“<pad>”符号使每个序列的长度均为max_seq_len。我们为法语词和英语词分别创建词典。法语词的索引和英语词的索引相互独立。

!tar -xf d2lzh_pytorch.tar
import collections
import os
import io
import math
import torch
from torch import nn
import torch.nn.functional as F
import torchtext.vocab as Vocab
import torch.utils.data as Data

import sys
# sys.path.append("..") 
import d2lzh_pytorch as d2l

PAD, BOS, EOS = '<pad>', '<bos>', '<eos>'
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

print(torch.__version__, device)



# 将一个序列中所有的词记录在all_tokens中以便之后构造词典,然后在该序列后面添加PAD直到序列
# 长度变为max_seq_len,然后将序列保存在all_seqs中
def process_one_seq(seq_tokens, all_tokens, all_seqs, max_seq_len):
    all_tokens.extend(seq_tokens)
    seq_tokens += [EOS] + [PAD] * (max_seq_len - len(seq_tokens) - 1)
    all_seqs.append(seq_tokens)

# 使用所有的词来构造词典。并将所有序列中的词变换为词索引后构造Tensor
def build_data(all_tokens, all_seqs):
    vocab = Vocab.Vocab(collections.Counter(all_tokens),
                        specials=[PAD, BOS, EOS])
    indices = [[vocab.stoi[w] for w in seq] for seq in all_seqs]
    return vocab, torch.tensor(indices)


def read_data(max_seq_len):
    # in和out分别是input和output的缩写
    in_tokens, out_tokens, in_seqs, out_seqs = [], [], [], []
    with io.open('fr-en-small.txt') as f:
        lines = f.readlines()
    for line in lines:
        in_seq, out_seq = line.rstrip().split('\t')
        in_seq_tokens, out_seq_tokens = in_seq.split(' '), out_seq.split(' ')
        if max(len(in_seq_tokens), len(out_seq_tokens)) > max_seq_len - 1:
            continue  # 如果加上EOS后长于max_seq_len,则忽略掉此样本
        process_one_seq(in_seq_tokens, in_tokens, in_seqs, max_seq_len)
        process_one_seq(out_seq_tokens, out_tokens, out_seqs, max_seq_len)
    in_vocab, in_data = build_data(in_tokens, in_seqs)
    out_vocab, out_data = build_data(out_tokens, out_seqs)
    return in_vocab, out_vocab, Data.TensorDataset(in_data, out_data)


#将序列的最大长度设成7,然后查看读取到的第一个样本。该样本分别包含法语词索引序列和英语词索引序列。
max_seq_len = 7
in_vocab, out_vocab, dataset = read_data(max_seq_len)
dataset[0]

该数据集文件为https://course.educg.net/e11c14880a9dd6557631593f4e95e330/files/fr-en-small.txt

运行结果如下:

3.2 编码器

在编码器中,我们将输入语言的词索引通过词嵌入层得到词的表征,然后输入到一个多层门控循环单元中。PyTorch的nn.GRU实例在前向计算后也会分别返回输出和最终时间步的多层隐藏状态。其中的输出指的是最后一层的隐藏层在各个时间步的隐藏状态,并不涉及输出层计算。注意力机制将这些输出作为键项和值项。

class Encoder(nn.Module):
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
                 drop_prob=0, **kwargs):
        super(Encoder, self).__init__(**kwargs)
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.rnn = nn.GRU(embed_size, num_hiddens, num_layers, dropout=drop_prob)

    def forward(self, inputs, state):
        # 输入形状是(批量大小, 时间步数)。将输出互换样本维和时间步维
        embedding = self.embedding(inputs.long()).permute(1, 0, 2) # (seq_len, batch, input_size)
        return self.rnn(embedding, state)

    def begin_state(self):
        return None



#示例

#下面我们来创建一个批量大小为4、时间步数为7的小批量序列输入。设门控循环单元的隐藏层个数为2,隐藏单元个数为16。编码器对该输入执行前向计算后返回的输出形状为(时间步数, 批量大小, 隐藏单元个数)。门控循环单元在最终时间步的多层隐藏状态的形状为(隐藏层个数, 批量大小, 隐藏单元个数)。对于门控循环单元来说,state就是一个元素,即隐藏状态;如果使用长短期记忆,state是一个元组,包含两个元素即隐藏状态和记忆细胞。
encoder = Encoder(vocab_size=10, embed_size=8, num_hiddens=16, num_layers=2)
output, state = encoder(torch.zeros((4, 7)), encoder.begin_state())
output.shape, state.shape # GRU的state是h, 而LSTM的是一个元组(h, c)

运行结果如下:

3.3 注意力机制

将输入连结后通过含单隐藏层的多层感知机变换。其中隐藏层的输入是解码器的隐藏状态与编码器在所有时间步上隐藏状态的一一连结,且使用tanh函数作为激活函数。输出层的输出个数为1。两个Linear实例均不使用偏差。其中函数𝑎𝑎定义里向量𝑣𝑣的长度是一个超参数,即attention_size

注意力机制的输入包括查询项、键项和值项。设编码器和解码器的隐藏单元个数相同。这里的查询项为解码器在上一时间步的隐藏状态,形状为(批量大小, 隐藏单元个数);键项和值项均为编码器在所有时间步的隐藏状态,形状为(时间步数, 批量大小, 隐藏单元个数)。注意力机制返回当前时间步的背景变量,形状为(批量大小, 隐藏单元个数)。

def attention_model(input_size, attention_size):
    model = nn.Sequential(nn.Linear(input_size, attention_size, bias=False),
                          nn.Tanh(),
                          nn.Linear(attention_size, 1, bias=False))
    return model



def attention_forward(model, enc_states, dec_state):
    """
    enc_states: (时间步数, 批量大小, 隐藏单元个数)
    dec_state: (批量大小, 隐藏单元个数)
    """
    # 将解码器隐藏状态广播到和编码器隐藏状态形状相同后进行连结
    dec_states = dec_state.unsqueeze(dim=0).expand_as(enc_states)
    enc_and_dec_states = torch.cat((enc_states, dec_states), dim=2)
    e = model(enc_and_dec_states)  # 形状为(时间步数, 批量大小, 1)
    alpha = F.softmax(e, dim=0)  # 在时间步维度做softmax运算
    return (alpha * enc_states).sum(dim=0)  # 返回背景变量





#示例
#在下面的例子中,编码器的时间步数为10,批量大小为4,编码器和解码器的隐藏单元个数均为8。注意力机#制返回一个小批量的背景向量,每个背景向量的长度等于编码器的隐藏单元个数。因此输出的形状为(4, #8)。
seq_len, batch_size, num_hiddens = 10, 4, 8
model = attention_model(2*num_hiddens, 10) 
enc_states = torch.zeros((seq_len, batch_size, num_hiddens))
dec_state = torch.zeros((batch_size, num_hiddens))
attention_forward(model, enc_states, dec_state).shape

运行结果如下: 

 3.4 含注意力机制的解码器

我们直接将编码器在最终时间步的隐藏状态作为解码器的初始隐藏状态。这要求编码器和解码器的循环神经网络使用相同的隐藏层个数和隐藏单元个数。

在解码器的前向计算中,我们先通过刚刚介绍的注意力机制计算得到当前时间步的背景向量。由于解码器的输入来自输出语言的词索引,我们将输入通过词嵌入层得到表征,然后和背景向量在特征维连结。我们将连结后的结果与上一时间步的隐藏状态通过门控循环单元计算出当前时间步的输出与隐藏状态。最后,我们将输出通过全连接层变换为有关各个输出词的预测,形状为(批量大小, 输出词典大小)。

class Decoder(nn.Module):
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
                 attention_size, drop_prob=0):
        super(Decoder, self).__init__()
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.attention = attention_model(2*num_hiddens, attention_size)
        # GRU的输入包含attention输出的c和实际输入, 所以尺寸是 num_hiddens+embed_size
        self.rnn = nn.GRU(num_hiddens + embed_size, num_hiddens, 
                          num_layers, dropout=drop_prob)
        self.out = nn.Linear(num_hiddens, vocab_size)

    def forward(self, cur_input, state, enc_states):
        """
        cur_input shape: (batch, )
        state shape: (num_layers, batch, num_hiddens)
        """
        # 使用注意力机制计算背景向量
        c = attention_forward(self.attention, enc_states, state[-1])
        # 将嵌入后的输入和背景向量在特征维连结, (批量大小, num_hiddens+embed_size)
        input_and_c = torch.cat((self.embedding(cur_input), c), dim=1) 
        # 为输入和背景向量的连结增加时间步维,时间步个数为1
        output, state = self.rnn(input_and_c.unsqueeze(0), state)
        # 移除时间步维,输出形状为(批量大小, 输出词典大小)
        output = self.out(output).squeeze(dim=0)
        return output, state

    def begin_state(self, enc_state):
        # 直接将编码器最终时间步的隐藏状态作为解码器的初始隐藏状态
        return enc_state

3.5 训练模型

def batch_loss(encoder, decoder, X, Y, loss):
    batch_size = X.shape[0]
    enc_state = encoder.begin_state()
    enc_outputs, enc_state = encoder(X, enc_state)
    # 初始化解码器的隐藏状态
    dec_state = decoder.begin_state(enc_state)
    # 解码器在最初时间步的输入是BOS
    dec_input = torch.tensor([out_vocab.stoi[BOS]] * batch_size)
    # 我们将使用掩码变量mask来忽略掉标签为填充项PAD的损失, 初始全1
    mask, num_not_pad_tokens = torch.ones(batch_size,), 0
    l = torch.tensor([0.0])
    for y in Y.permute(1,0): # Y shape: (batch, seq_len)
        dec_output, dec_state = decoder(dec_input, dec_state, enc_outputs)
        l = l + (mask * loss(dec_output, y)).sum()
        dec_input = y  # 使用强制教学
        num_not_pad_tokens += mask.sum().item()
        # EOS后面全是PAD. 下面一行保证一旦遇到EOS接下来的循环中mask就一直是0
        mask = mask * (y != out_vocab.stoi[EOS]).float()
    return l / num_not_pad_tokens




def train(encoder, decoder, dataset, lr, batch_size, num_epochs):
    enc_optimizer = torch.optim.Adam(encoder.parameters(), lr=lr)
    dec_optimizer = torch.optim.Adam(decoder.parameters(), lr=lr)

    loss = nn.CrossEntropyLoss(reduction='none')
    data_iter = Data.DataLoader(dataset, batch_size, shuffle=True)
    for epoch in range(num_epochs):
        l_sum = 0.0
        for X, Y in data_iter:
            enc_optimizer.zero_grad()
            dec_optimizer.zero_grad()
            l = batch_loss(encoder, decoder, X, Y, loss)
            l.backward()
            enc_optimizer.step()
            dec_optimizer.step()
            l_sum += l.item()
        if (epoch + 1) % 10 == 0:
            print("epoch %d, loss %.3f" % (epoch + 1, l_sum / len(data_iter)))



#创建实例并设置超参数
embed_size, num_hiddens, num_layers = 64, 64, 2
attention_size, drop_prob, lr, batch_size, num_epochs = 10, 0.5, 0.01, 2, 50
encoder = Encoder(len(in_vocab), embed_size, num_hiddens, num_layers,
                  drop_prob)
decoder = Decoder(len(out_vocab), embed_size, num_hiddens, num_layers,
                  attention_size, drop_prob)
train(encoder, decoder, dataset, lr, batch_size, num_epochs)

运行结果如下: 

3.6 预测不定长的序列

def translate(encoder, decoder, input_seq, max_seq_len):
    in_tokens = input_seq.split(' ')
    in_tokens += [EOS] + [PAD] * (max_seq_len - len(in_tokens) - 1)
    enc_input = torch.tensor([[in_vocab.stoi[tk] for tk in in_tokens]]) # batch=1
    enc_state = encoder.begin_state()
    enc_output, enc_state = encoder(enc_input, enc_state)
    dec_input = torch.tensor([out_vocab.stoi[BOS]])
    dec_state = decoder.begin_state(enc_state)
    output_tokens = []
    for _ in range(max_seq_len):
        dec_output, dec_state = decoder(dec_input, dec_state, enc_output)
        pred = dec_output.argmax(dim=1)
        pred_token = out_vocab.itos[int(pred.item())]
        if pred_token == EOS:  # 当任一时间步搜索出EOS时,输出序列即完成
            break
        else:
            output_tokens.append(pred_token)
            dec_input = pred
    return output_tokens



input_seq = 'ils regardent .'
translate(encoder, decoder, input_seq, max_seq_len)

运行结果如下:

3.7 评价翻译结果

评价机器翻译结果通常使用BLEU(Bilingual Evaluation Understudy)[1]。对于模型预测序列中任意的子序列,BLEU考察这个子序列是否出现在标签序列中。

具体来说,设词数为𝑛的子序列的精度为𝑝𝑛。它是预测序列与标签序列匹配词数为𝑛的子序列的数量与预测序列中词数为𝑛的子序列的数量之比。举个例子,假设标签序列为𝐴、𝐵、𝐶、𝐷、𝐸、𝐹,预测序列为𝐴、𝐵、𝐵、𝐶、𝐷,那么𝑝1=4/5,𝑝2=3/4,𝑝3=1/3,𝑝4=0𝑝1=4/5,𝑝2=3/4,𝑝3=1/3,𝑝4=0。设𝑙𝑒𝑛label𝑙𝑒𝑛label和𝑙𝑒𝑛pred𝑙𝑒𝑛pred分别为标签序列和预测序列的词数,那么,BLEU的定义为

其中𝑘𝑘是我们希望匹配的子序列的最大词数。可以看到当预测序列和标签序列完全一致时,BLEU为1。

因为匹配较长子序列比匹配较短子序列更难,BLEU对匹配较长子序列的精度赋予了更大权重。例如,当𝑝𝑛固定在0.5时,随着𝑛𝑛的增大,0.51/2≈0.7,0.51/4≈0.84,0.51/8≈0.92,0.51/16≈0.960.51/2≈0.7,0.51/4≈0.84,0.51/8≈0.92,0.51/16≈0.96。另外,模型预测较短序列往往会得到较高𝑝𝑛值。因此,上式中连乘项前面的系数是为了惩罚较短的输出而设的。举个例子,当𝑘=2时,假设标签序列为𝐴、𝐵、𝐶、𝐷、𝐸、𝐹,而预测序列为𝐴、𝐵。虽然𝑝1=𝑝2=1𝑝1=𝑝2=1,但惩罚系数exp(1−6/2)≈0.14exp⁡(1−6/2)≈0.14,因此BLEU也接近0.14。

def bleu(pred_tokens, label_tokens, k):
    len_pred, len_label = len(pred_tokens), len(label_tokens)
    score = math.exp(min(0, 1 - len_label / len_pred))
    for n in range(1, k + 1):
        num_matches, label_subs = 0, collections.defaultdict(int)
        for i in range(len_label - n + 1):
            label_subs[''.join(label_tokens[i: i + n])] += 1
        for i in range(len_pred - n + 1):
            if label_subs[''.join(pred_tokens[i: i + n])] > 0:
                num_matches += 1
                label_subs[''.join(pred_tokens[i: i + n])] -= 1
        score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))
    return score


#定义一个辅助打印函数
def score(input_seq, label_seq, k):
    pred_tokens = translate(encoder, decoder, input_seq, max_seq_len)
    label_tokens = label_seq.split(' ')
    print('bleu %.3f, predict: %s' % (bleu(pred_tokens, label_tokens, k),
                                      ' '.join(pred_tokens)))



score('ils regardent .', 'they are watching .', k=2)


score('ils sont canadienne .', 'they are canadian .', k=2)

运行结果如下:

四、利用注意力机制进行机器翻译(日译中)

4.1 任务目的

使用编码器—解码器和注意力机制将日语翻译为中文。

4.2 开发环境

谷歌的kaggle平台

4.3数据集介绍

在本教程中,我们将使用从JParaCrawl下载的日语-英语平行数据集!该数据集被描述为“由NTT创建的最大的可公开获取的日语-英语平行语料库。主要通过网络爬取和自动对齐平行句子来创建”。您也可以在这里查看论文。

五、具体代码实现

5.1 Get the parallel dataset

import math
import torchtext
import torch
import torch.nn as nn
from torch import Tensor
from torch.nn.utils.rnn import pad_sequence
from torch.utils.data import DataLoader
from collections import Counter
from torchtext.vocab import Vocab
from torchtext.vocab import build_vocab_from_iterator
from collections import Counter
from torch.nn import TransformerEncoder, TransformerDecoder, TransformerEncoderLayer, TransformerDecoderLayer
import io
import time
import pandas as pd
import numpy as np
import pickle
import tqdm
import sentencepiece as spm
torch.manual_seed(0)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(torch.cuda.get_device_name(0)) #显示gpu设备名称


device



device(type='cuda')
df = pd.read_csv('/kaggle/input/testdatazh-ja/zh-ja.bicleaner05.txt', sep='\\t', engine='python', header=None)
# 提取第 2 列(索引为 2)的数据,并转换为 Python 列表,存储在 trainen 中
trainen = df[2].values.tolist()  # 句子的英文部分存储在 trainen 列表中

# 提取第 3 列(索引为 3)的数据,并转换为 Python 列表,存储在 trainja 中
trainja = df[3].values.tolist()  # 句子的日文部分存储在 trainja 列表中



trainen.pop(5972)
trainja.pop(5972)


print(trainen[500])
print(trainja[500])

数据集下载链接:[http://www.kecl.ntt.co.jp/icl/lirg/jparacrawl]

运行结果如下: 

5.2 Prepare the tokenizers

ja_tokenizer = spm.SentencePieceProcessor(model_file='/kaggle/input/nopretok/pytorch/ja.nopretok/1/spm.ja.nopretok.model')
en_tokenizer = spm.SentencePieceProcessor(model_file='/kaggle/input/nopretok/pytorch/en.nopretok/1/spm.en.nopretok.model')


en_tokenizer.encode("All residents aged 20 to 59 years who live in Japan must enroll in public pension system.")


ja_tokenizer.encode("年金 日本に住んでいる20歳~60歳の全ての人は、公的年金制度に加入しなければなりません。")

tokenizer下载链接为: https://course.educg.net/112692e6982c63da2fe29324a5411450/files/spm.en.nopretok.model

https://course.educg.net/112692e6982c63da2fe29324a5411450/files/spm.ja.nopretok.model

运行结果如下: 

5.3 Build the TorchText Vocab objects and convert the sentences into Torch tensors

使用分词器和原始句子,我们然后构建从TorchText导入的Vocab对象。这个过程可能需要几秒钟或几分钟,这取决于我们的数据集大小和计算能力。不同的分词器也会影响构建词汇表所需的时间。
 

from collections import Counter
import torch

def build_vocab(sentences, tokenizer):
    counter = Counter()
    for sentence in sentences:
        counter.update(tokenizer.encode(sentence, out_type=str))
    return build_vocab_from_iterator([counter.keys()], specials=['<unk>', '<pad>', '<bos>', '<eos>'])


ja_vocab = build_vocab(trainja, ja_tokenizer)
en_vocab = build_vocab(trainen, en_tokenizer)


#得到词汇表对象后,我们可以利用词汇表和分词器对象为我们的训练数据构建张量。
def data_process(ja, en):
    data = []
    for (raw_ja, raw_en) in zip(ja, en):
        ja_tensor_ = torch.tensor([ja_vocab[token] for token in ja_tokenizer.encode(raw_ja.rstrip("\n"), out_type=str)],
                                  dtype=torch.long)
        en_tensor_ = torch.tensor([en_vocab[token] for token in en_tokenizer.encode(raw_en.rstrip("\n"), out_type=str)],
                                  dtype=torch.long)
        data.append((ja_tensor_, en_tensor_))
    return data


train_data = data_process(trainja, trainen)

5.4 Create the DataLoader object to be iterated during training 

在这里,我将批量大小设置为16,以防止“cuda内存不足”的错误,但这取决于各种因素,比如机器内存容量、数据大小等,因此根据需要随时更改批量大小。

BATCH_SIZE = 32
PAD_IDX = ja_vocab['<pad>']
BOS_IDX = ja_vocab['<bos>']
EOS_IDX = ja_vocab['<eos>']
def generate_batch(data_batch):
    ja_batch, en_batch = [], []
    for (ja_item, en_item) in data_batch:
        ja_batch.append(torch.cat([torch.tensor([BOS_IDX]), ja_item, torch.tensor([EOS_IDX])], dim=0))
        en_batch.append(torch.cat([torch.tensor([BOS_IDX]), en_item, torch.tensor([EOS_IDX])], dim=0))
    ja_batch = pad_sequence(ja_batch, padding_value=PAD_IDX)
    en_batch = pad_sequence(en_batch, padding_value=PAD_IDX)
    return ja_batch, en_batch
train_iter = DataLoader(train_data, batch_size=BATCH_SIZE,
                        shuffle=True, collate_fn=generate_batch)

5.5 Sequence-to-sequence Transformer

Transformer是“Attention is all you need”论文中介绍的Seq2Seq模型,用于解决机器翻译任务。Transformer模型包括一个编码器和一个解码器块,每个块都包含固定数量的层。

编码器通过一系列的多头注意力和前馈网络层处理输入序列。编码器的输出被称为内存,将其与目标张量一起输入解码器。编码器和解码器通过使用教师强制技术进行端到端训练。

from torch.nn import (TransformerEncoder, TransformerDecoder,
                      TransformerEncoderLayer, TransformerDecoderLayer)

# 定义一个序列到序列的Transformer模型
class Seq2SeqTransformer(nn.Module):
    def __init__(self, num_encoder_layers: int, num_decoder_layers: int,
                 emb_size: int, src_vocab_size: int, tgt_vocab_size: int,
                 dim_feedforward: int = 512, dropout: float = 0.1):
        super(Seq2SeqTransformer, self).__init__()

        # 定义Transformer编码器层
        encoder_layer = TransformerEncoderLayer(d_model=emb_size, nhead=NHEAD,
                                                dim_feedforward=dim_feedforward)

        self.transformer_encoder = TransformerEncoder(encoder_layer, num_layers=num_encoder_layers)

        # 定义Transformer解码器层
        decoder_layer = TransformerDecoderLayer(d_model=emb_size, nhead=NHEAD,
                                                dim_feedforward=dim_feedforward)

        self.transformer_decoder = TransformerDecoder(decoder_layer, num_layers=num_decoder_layers)

        # 定义生成器,将解码器的输出映射到目标词汇表大小
        self.generator = nn.Linear(emb_size, tgt_vocab_size)


        # 定义源语言和目标语言的嵌入层
        self.src_tok_emb = TokenEmbedding(src_vocab_size, emb_size)
        self.tgt_tok_emb = TokenEmbedding(tgt_vocab_size, emb_size)

        # 定义位置编码,用于加入位置信息
        self.positional_encoding = PositionalEncoding(emb_size, dropout=dropout)

    # 前向传播函数
    def forward(self, src: Tensor, trg: Tensor, src_mask: Tensor,
                tgt_mask: Tensor, src_padding_mask: Tensor,
                tgt_padding_mask: Tensor, memory_key_padding_mask: Tensor):
 
        src_emb = self.positional_encoding(self.src_tok_emb(src))

        tgt_emb = self.positional_encoding(self.tgt_tok_emb(trg))
        

        memory = self.transformer_encoder(src_emb, src_mask, src_padding_mask)
        
        # 将目标输入和编码器的输出通过解码器
        outs = self.transformer_decoder(tgt_emb, memory, tgt_mask, None,
                                        tgt_padding_mask, memory_key_padding_mask)
        # 将解码器的输出通过生成器映射到目标词汇表大小
        return self.generator(outs)

    # 编码函数
    def encode(self, src: Tensor, src_mask: Tensor):
        return self.transformer_encoder(self.positional_encoding(
                            self.src_tok_emb(src)), src_mask)


    # 解码函数
    def decode(self, tgt: Tensor, memory: Tensor, tgt_mask: Tensor):
        return self.transformer_decoder(self.positional_encoding(
                          self.tgt_tok_emb(tgt)), memory,
                          tgt_mask)



class PositionalEncoding(nn.Module):
    def __init__(self, emb_size: int, dropout: float, maxlen: int = 5000):
        super(PositionalEncoding, self).__init__()
        
        den = torch.exp(- torch.arange(0, emb_size, 2) * math.log(10000) / emb_size)

        pos = torch.arange(0, maxlen).reshape(maxlen, 1)

        pos_embedding = torch.zeros((maxlen, emb_size))

        pos_embedding[:, 0::2] = torch.sin(pos * den)

        pos_embedding[:, 1::2] = torch.cos(pos * den)
        
        pos_embedding = pos_embedding.unsqueeze(-2)

        self.dropout = nn.Dropout(dropout)

        self.register_buffer('pos_embedding', pos_embedding)

    # 前向传播函数
    def forward(self, token_embedding: Tensor):
        return self.dropout(token_embedding +
                            self.pos_embedding[:token_embedding.size(0), :])

class TokenEmbedding(nn.Module):
    def __init__(self, vocab_size: int, emb_size: int):
        super(TokenEmbedding, self).__init__()
        
        self.embedding = nn.Embedding(vocab_size, emb_size)
        
        self.emb_size = emb_size

    # 前向传播函数
    def forward(self, tokens: Tensor):
        # 将词元索引映射到嵌入向量,并乘以嵌入向量维度的平方根
        return self.embedding(tokens.long()) * math.sqrt(self.emb_size)



def generate_square_subsequent_mask(sz):
    mask = (torch.triu(torch.ones((sz, sz), device=device)) == 1).transpose(0, 1)
    mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
    return mask

def create_mask(src, tgt):
    # 获取源输入和目标输入的序列长度
    src_seq_len = src.shape[0]
    tgt_seq_len = tgt.shape[0]

    tgt_mask = generate_square_subsequent_mask(tgt_seq_len)
    src_mask = torch.zeros((src_seq_len, src_seq_len), device=device).type(torch.bool)

    src_padding_mask = (src == PAD_IDX).transpose(0, 1)
    tgt_padding_mask = (tgt == PAD_IDX).transpose(0, 1)

    return src_mask, tgt_mask, src_padding_mask, tgt_padding_mask





# 超参数
SRC_VOCAB_SIZE = len(ja_vocab)      # 原语言词汇表大小
TGT_VOCAB_SIZE = len(en_vocab)      # 目标语言词汇表大小
EMB_SIZE = 512                      # 嵌入向量维度
NHEAD = 8                           # 多头注意力机制的头数
FFN_HID_DIM = 512                   # 前馈神经网络的维度
BATCH_SIZE = 16                     # 批次大小
NUM_ENCODER_LAYERS = 3              # 编码器层数
NUM_DECODER_LAYERS = 3              # 解码器层数
NUM_EPOCHS = 16                     # 训练轮数

# 初始化Seq2SeqTransformer模型
transformer = Seq2SeqTransformer(NUM_ENCODER_LAYERS, NUM_DECODER_LAYERS,
                                 EMB_SIZE, SRC_VOCAB_SIZE, TGT_VOCAB_SIZE,
                                 FFN_HID_DIM)

for p in transformer.parameters():
    if p.dim() > 1:
        nn.init.xavier_uniform_(p)

transformer = transformer.to(device)

loss_fn = nn.CrossEntropyLoss(ignore_index=PAD_IDX)

optimizer = optim.Adam(
    transformer.parameters(), lr=0.0001, betas=(0.9, 0.98), eps=1e-9
)

# 定义训练一个epoch的函数
def train_epoch(model, train_iter, optimizer):
    model.train()  
    losses = 0     
    for idx, (src, tgt) in enumerate(train_iter):
        src = src.to(device)    
        tgt = tgt.to(device)    

        tgt_input = tgt[:-1, :] 

        # 生成掩码
        src_mask, tgt_mask, src_padding_mask, tgt_padding_mask = create_mask(src, tgt_input)

        # 前向传播
        logits = model(src, tgt_input, src_mask, tgt_mask,
                       src_padding_mask, tgt_padding_mask, src_padding_mask)

        optimizer.zero_grad()  

        tgt_out = tgt[1:, :]  
        
        # 计算损失
        loss = loss_fn(logits.reshape(-1, logits.shape[-1]), tgt_out.reshape(-1))
        
        # 反向传播
        loss.backward()
        
        # 优化器更新参数
        optimizer.step()
        
        losses += loss.item()  # 累加损失
    return losses / len(train_iter)  # 返回平均损失

# 定义评估函数
def evaluate(model, val_iter):
    model.eval()  
    losses = 0    # 初始化损失值
    with torch.no_grad(): 
        for idx, (src, tgt) in enumerate(val_iter):
            src = src.to(device) 
            tgt = tgt.to(device)  

            tgt_input = tgt[:-1, :] 

            # 生成掩码
            src_mask, tgt_mask, src_padding_mask, tgt_padding_mask = create_mask(src, tgt_input)

            # 前向传播
            logits = model(src, tgt_input, src_mask, tgt_mask,
                           src_padding_mask, tgt_padding_mask, src_padding_mask)
            
            tgt_out = tgt[1:, :]  
            
            # 计算损失
            loss = loss_fn(logits.reshape(-1, logits.shape[-1]), tgt_out.reshape(-1))
            
            losses += loss.item()  # 累加损失
    return losses / len(val_iter)  # 返回平均损失

5.6 Start training 

for epoch in tqdm.tqdm(range(1, NUM_EPOCHS+1)):
    start_time = time.time()
    train_loss = train_epoch(transformer, train_iter, optimizer)
    end_time = time.time()
    print((f"Epoch: {epoch}, Train loss: {train_loss:.3f}, "
            f"Epoch time = {(end_time - start_time):.3f}s"))

运行结果如下(在kaggle运行大概需要两个半小时): 

5.7 Try translating a Japanese sentence using the trained model 

def greedy_decode(model, src, src_mask, max_len, start_symbol):
    src = src.to(device)
    src_mask = src_mask.to(device)
    memory = model.encode(src, src_mask)
    ys = torch.ones(1, 1).fill_(start_symbol).type(torch.long).to(device)
    for i in range(max_len-1):
        memory = memory.to(device)
        memory_mask = torch.zeros(ys.shape[0], memory.shape[0]).to(device).type(torch.bool)
        tgt_mask = (generate_square_subsequent_mask(ys.size(0)).type(torch.bool)).to(device)
        out = model.decode(ys, memory, tgt_mask)
        out = out.transpose(0, 1)
        prob = model.generator(out[:, -1])
        _, next_word = torch.max(prob, dim = 1)
        next_word = next_word.item()
        ys = torch.cat([ys,
                        torch.ones(1, 1).type_as(src.data).fill_(next_word)], dim=0)
        if next_word == EOS_IDX:
            break
    return ys
def translate(model, src, src_vocab, tgt_vocab, src_tokenizer):
    model.eval()
    tokens = [BOS_IDX] + [src_vocab.stoi[tok] for tok in src_tokenizer.encode(src, out_type=str)]+ [EOS_IDX]
    num_tokens = len(tokens)
    src = (torch.LongTensor(tokens).reshape(num_tokens, 1) )
    src_mask = (torch.zeros(num_tokens, num_tokens)).type(torch.bool)
    tgt_tokens = greedy_decode(model, src, src_mask, max_len=num_tokens + 5, start_symbol=BOS_IDX).flatten()

    return " ".join([tgt_vocab.itos[tok] for tok in tgt_tokens]).replace("<bos>", "").replace("<eos>", "")



from torchtext.vocab import build_vocab_from_iterator
# 定义特殊标记的索引
BOS_IDX = 0
EOS_IDX = 1
PAD_IDX = 2
UNK_IDX = 3

# 构建词汇表
def build_vocab(sentences, tokenizer):
    def yield_tokens(sentences):
        for sentence in sentences:
            yield tokenizer.encode(sentence, out_type=str)
    vocab = build_vocab_from_iterator(yield_tokens(sentences), specials=['<bos>', '<eos>', '<pad>', '<unk>'])
    vocab.set_default_index(UNK_IDX)
    return vocab

# 使⽤ build_vocab 函数分别为⽇语 (trainja) 和英语 (trainen) 句⼦构建词汇表
ja_vocab = build_vocab(trainja, ja_tokenizer)
en_vocab = build_vocab(trainen, en_tokenizer)

def translate(model, src, src_vocab, tgt_vocab, src_tokenizer):
    model.eval()
    tokens = [BOS_IDX] + [src_vocab[tok] for tok in src_tokenizer.encode(src, out_type=str)] + [EOS_IDX]
    num_tokens = len(tokens)


    src = (torch.LongTensor(tokens).reshape(num_tokens, 1)).to(device)
    src_mask = (torch.zeros(num_tokens, num_tokens)).type(torch.bool).to(device)
    tgt_tokens = greedy_decode(model, src, src_mask, max_len=num_tokens + 5, start_symbol=BOS_IDX).flatten()
    return " ".join([tgt_vocab.get_itos()[tok] for tok in tgt_tokens]).replace("<bos>", "").replace("<eos>", "")



translate(transformer, "HSコード 8515 はんだ付け用、ろう付け用又は溶接用の機器(電気式(電気加熱ガス式を含む。)", ja_vocab, en_vocab, ja_tokenizer)


translate(transformer, " HSコード 9207 電気的に音を発生し又は増幅する楽器(例えば、オルガン、ギター及びアコーディオン)", ja_vocab, en_vocab, ja_tokenizer)


translate(transformer, "HSコード 1504 魚又は海棲哺乳動物の油脂及びその分別物(化学的な変性加工をしてないものに限るものとし、精製してあるかないかを問わない。)", ja_vocab, en_vocab, ja_tokenizer)

运行结果如下:

 

 

trainen.pop(5)

trainja.pop(5)

运行结果如下: 

从第一个测试结果和正确翻译的对比可以看出,得到的模型翻译效果并不好,可能是训练次数少的原因,也可能是语料库不足以及语料库混乱的原因,在后续实验中可以着重注意这个问题。

5.8Save the Vocab objects and trained model

import pickle
file = open('en_vocab.pkl', 'wb')
pickle.dump(en_vocab, file)
file.close()
file = open('ja_vocab.pkl', 'wb')
pickle.dump(ja_vocab, file)
file.close()

torch.save(transformer.state_dict(), 'inference_model')


torch.save({
  'epoch': NUM_EPOCHS,
  'model_state_dict': transformer.state_dict(),
  'optimizer_state_dict': optimizer.state_dict(),
  'loss': train_loss,
  }, 'model_checkpoint.tar')

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值