首页 > 其他分享 >机器翻译及实践 进阶版:基于Transformer实现机器翻译(日译中)

机器翻译及实践 进阶版:基于Transformer实现机器翻译(日译中)

时间:2024-07-01 15:59:20浏览次数:22  
标签:vocab src Transformer 进阶 tgt mask 机器翻译 ja 注意力

机器翻译及实践 进阶版:基于Transformer实现机器翻译(日译中)


前言

本文为笔者nlp实验内容,主要介绍了transfomer原理和基于transformer的中日翻译代码实现。
如有错误,请多包涵。
另外,本文可能会包含很多自言自语,也请见谅。


一、所需要的前置知识——Transformer

1.自注意力机制

本部分我们还是按照传统,以what—why—how的逻辑进行叙述。

1.1 Query&Key&Value版注意力机制

上一节我们叙述了一下注意力机制,其实就是计算得到不同的权重,然后利用该权重去处理背景信息。下面介绍一下常见的实现方法:Query&Key&Value版注意力机制

1.1.1 什么是Query&Key&Value版注意力机制

首先我们来认识几个概念:

  • 查询(Query): 指的是查询的范围,自主提示,即主观意识的特征向量
  • 键(Key): 指的是被比对的项,非自主提示,即物体的突出特征信息向量
  • 值(Value) : 则是代表物体本身的特征向量,通常和Key成对出现

注意力机制是通过Query与Key的注意力汇聚(给定一个 Query,计算Query与 Key的相关性,然后根据Query与Key的相关性去找到最合适的 Value)实现对Value的注意力权重分配,生成最终的输出结果。可以看看下面这张陈年老图:在这里插入图片描述
还是有点抽象?那看看下面的例子:

  • 当你用上淘宝购物时,你会敲入一句关键词(比如:显瘦),这个就是Query。
  • 搜索系统会根据关键词这个去查找一系列相关的Key(商品名称、图片)。
  • 最后系统会将相应的 Value (具体的衣服)返回给你。

在这个栗子中,Query, Key 和 Value 的每个属性虽然在不同的空间,其实他们是有一定的潜在关系的,也就是说通过某种变换,可以使得三者的属性在一个相近的空间中。

1.1.2 为什么引入Query&Key&Value版注意力机制

首先,注意力机制的好处不用我说了吧。

那我主要说说为什么用Query&Key&Value版注意力机制,而不使用其他的

在讨论为什么使用Query、Key和Value(QKV)来计算注意力机制而不是其他方法时,我们需要理解注意力机制的核心目的:在处理序列数据时,能够动态地关注到对于当前任务最为重要的信息片段。QKV注意力机制之所以被广泛采用,主要基于其独特的计算方式和对复杂数据处理的高效性。

  1. 信息筛选的高效性: QKV注意力机制通过Query(查询)来识别信息需求,Key(键)来确定信息位置,Value(值)来提供实际信息。这种分工合作的方式直接指向了需要关注的信息,减少了无关信息的干扰,提高了信息筛选的效率。
  2. 并行处理与计算优化: QKV注意力机制支持并行处理,所有的注意力计算可以同时进行。这大大加快了训练和推理的速度,尤其是在处理大规模数据集时,显著提高了计算效率。
  3. 提升模型的解释性: QKV机制通过显式地展示哪些部分的信息被重点关注,帮助研究者和开发者更好地理解模型的决策过程。这种解释性对于调试模型、发现并修正潜在的偏差与错误极为重要。
1.1.3 如何实现Query&Key&Value版注意力机制(原理)

输入Query、Key、Value:

  • 阶段一:根据Query和Key计算两者之间的相关性或相似性(常见方法点积、余弦相似度,MLP网络),得到注意力得分;

在这里插入图片描述

  • 阶段二:对注意力得分进行缩放scale(除以维度的根号),再softmax函数,一方面可以进行归一化,将原始计算分值整理成所有元素权重之和为1的概率分布;另一方面也可以通过softmax的内在机制更加突出重要元素的权重。一般采用如下公式计算:
    在这里插入图片描述

  • 阶段三:根据权重系数对Value值进行加权求和,得到Attention Value(此时的V是具有一些注意力信息的,更重要的信息更关注,不重要的信息被忽视了);
    在这里插入图片描述

这三个阶段可以用下图表示:(还是陈年老图)
在这里插入图片描述

1.2 自注意力机制

1.2.1 什么是自注意力机制(注意其余注意力机制的区别)

自注意力机制实际上是注意力机制中的一种,也是一种网络的构型,它想要解决的问题是:神经网络接收的输入是很多大小不一的向量,并且不同向量向量之间有一定的关系,但是实际训练的时候无法充分发挥这些输入之间的关系而导致模型训练结果效果极差。 比如机器翻译(序列到序列的问题,机器自己决定多少个标签),词性标注(Pos tagging一个向量对应一个标签),语义分析(多个向量对应一个标签)等文字处理问题。

注意力机制和自注意力机制的区别:

(1)注意力机制的Q和K是不同来源的,例如,在Encoder-Decoder模型中,K是Encoder中的元素,而Q是Decoder中的元素。在中译英模型中,Q是中文单词特征,而K则是英文单词特征。

(2)自注意力机制的Q和K则都是来自于同一组的元素,例如,在Encoder-Decoder模型中,Q和K都是Encoder中的元素,即Q和K都是中文特征,相互之间做注意力汇聚。也可以理解为同一句话中的词元或者同一张图像中不同的patch,这都是一组元素内部相互做注意力机制,因此,自注意力机制(self-attention)也被称为内部注意力机制(intra-attention)。

自注意力机制虽然考虑了所有的输入向量,但没有考虑到向量的位置信息。在实际的文字处理问题中,可能在不同位置词语具有不同的性质,比如动词往往较低频率出现在句首。但也有方法进行补救—— 位置编码

1.2.2 为什么引入自注意力机制

针对全连接神经网络对于多个相关的输入无法建立起相关性的这个问题,通过自注意力机制来解决,自注意力机制实际上是想让机器注意到整个输入中不同部分之间的相关性。

自注意力机制是注意力机制的变体,其减少了对外部信息的依赖,更擅长捕捉数据或特征的内部相关性。自注意力机制的关键点在于,Q、K、V是同一个东西,或者三者来源于同一个X,三者同源。通过X找到X里面的关键点,从而更关注X的关键信息,忽略X的不重要信息。
不是输入语句和输出语句之间的注意力机制,而是输入语句内部元素之间或者输出语句内部元素之间发生的注意力机制。

1.2.3 如何实现自注意力机制

其实步骤和注意力机制是一样的,以对Thinking Machines这句话进行自注意力为例:

  • 阶段一得到Q,K,V的值。对于每一个向量x,分别乘上三个系数 W q , W k , W v W^{q},W^{k},W^{v} Wq,Wk,Wv ,得到的Q,K和V分别表示query,key和value,这三个W就是我们需要学习的参数。
    在这里插入图片描述

  • 阶段二:利用得到的Q和K计算每两个输入向量之间的相关性,一般采用点积计算,为每个向量计算一个score:score =q · k
    在这里插入图片描述

  • 阶段三:将刚得到的相似度除以 d k \sqrt{d_{k}} dk​ ​ ,再进行Softmax。经过Softmax的归一化后,每个值是一个大于0且小于1的权重系数,且总和为1,这个结果可以被理解成一个权重矩阵。
    在这里插入图片描述

  • 阶段四:使用刚得到的权重矩阵,与V相乘,计算加权求和。

在这里插入图片描述
最终得到z1和z2两个新向量。

其中z1表示的是thinking这个词向量的新的向量表示(通过thinking这个词向量,去查询和thinking machine这句话里面每个单词和thinking之间的相似度)。

也就是说新的z1依然是 thinking 的词向量表示,只不过这个词向量的表示蕴含了 thinking machines 这句话对于 thinking 而言哪个更重要的信息。

1.3 多头自注意力机制

1.3.1 什么是多头自注意力机制

在实践中,当给定相同的查询、键和值的集合时, 我们希望模型可以基于相同的注意力机制学习到不同的行为, 然后将不同的行为作为知识组合起来, 捕获序列内各种范围的依赖关系 (例如,短距离依赖和长距离依赖关系)。 因此,允许注意力机制组合使用查询、键和值的不同 子空间表示(representation subspaces)可能是有益的。

为此,与其只使用单独一个注意力汇聚, 我们可以用独立学习得到的n组不同的线性投影(linear projections)来变换查询、键和值。 然后,这n组变换后的查询、键和值将并行地送到注意力汇聚中。 最后,将这n个注意力汇聚的输出拼接在一起, 并且通过另一个可以学习的线性投影进行变换, 以产生最终输出。 这种设计被称为多头注意力(multihead attention)
在这里插入图片描述

1.3.2 为什么引入多头自注意力机制

我认为最重要的就是:增强模型的表达能力

  • 不同表示子空间的依赖捕获: 多头自注意力机制允许模型在多个不同的子空间中同时注意输入序列的不同表示。每个“头”独立地计算注意力,然后将这些“头”的输出合并,从而能够捕捉更复杂的模式和关系。
  • 局部和全局特征的融合: 通过多头的设置,模型可以同时关注于局部和全局的特征,这对于理解文本中的复杂结构和语义非常重要。
  • 提高抽象层次: 多头自注意力通过在不同的表示子空间中操作,使得网络能够学习到更高级别的抽象信息,这是单头注意力难以实现的。
1.3.3 如何实现多头自注意力机制

其实就是n个自注意力机制组合起来,具体可见下图:
在这里插入图片描述

2.Transformer具体流程

Transformer 的整体模型架构如下所示:
在这里插入图片描述
其实Transformer 本质上是一个 Encoder-Decoder 架构。因此中间部分的 Transformer 可以分为两个部分:编码组件和解码组件
在这里插入图片描述

2.1 编码组件和解码组件

编码组件由多层编码器(Encoder)组成。解码组件也是由相同层数的解码器(Decoder)组成。假设层数为6.
在这里插入图片描述
每个编码器由两个子层组成:Self-Attention 层(自注意力层)Position-wise Feed Forward Network(前馈网络,缩写为 FFN)
每个编码器的结构都是相同的,但是它们使用不同的权重参数。
在这里插入图片描述
编码器的输入会先流入 Self-Attention 层。它可以让编码器在对特定词进行编码时使用输入句子中的其他词的信息然后,Self-Attention 层的输出会流入前馈网络。

解码器也有编码器中这两层,但是它们之间还有一个注意力层(即 Encoder-Decoder Attention),其用来帮忙解码器关注输入句子的相关部分(类似于 seq2seq 模型中的注意力)。

在这里插入图片描述

2.2 位置前馈网络(Position-wise Feed-Forward Networks)

位置前馈网络就是一个全连接前馈网络,每个位置的词都单独经过这个完全相同的前馈神经网络。其由两个线性变换组成,即两个全连接层组成,第一个全连接层的激活函数为 ReLU 激活函数。可以表示为:

F F N ( x ) = m a x ( 0 , x W 1 + b 1 ) W 2 + b 2 FFN(x)=max(0, xW_1+b_1)W_2+b_2 FFN(x)=max(0,xW1​+b1​)W2​+b2​
在每个编码器和解码器中,虽然这个全连接前馈网络结构相同,但是不共享参数。整个前馈网络的输入和输出维度都是 d m o d e l = 512 d_{model}=512 dmodel​=512 ,第一个全连接层的输出和第二个全连接层的输入维度为 d d f f = 2048 d_{dff}=2048 ddff​=2048.

2.3 残差连接和层归一化

编码器结构中有一个需要注意的细节:每个编码器的每个子层(Self-Attention 层和 FFN 层)都有一个残差连接,再执行一个层标准化操作,整个计算过程可以表示为:
s u b − l a y e r − o u t p u t = L a y e r N o r m ( x + S u b L a y e r ( x ) ) sub-layer-output=LayerNorm(x+SubLayer(x)) sub−layer−output=LayerNorm(x+SubLayer(x))
在这里插入图片描述
将向量和自注意力层的层标准化操作可视化,如下图所示:
在这里插入图片描述
上面的操作也适用于解码器的子层。假设一个 Transformer 是由 2 层编码器和 2 层解码器组成,其如下图所示:
在这里插入图片描述为了方便进行残差连接,编码器和解码器中的所有子层和嵌入层的输出维度需要保持一致。

2.4 位置编码

Transformer 模型为每个输入的词嵌入向量添加一个向量。这些向量遵循模型学习的特定模式,有助于模型确定每个词的位置,或序列中不同词之间的距离。
在这里插入图片描述

如果我们假设词嵌入向量的维度是 4,那么实际的位置编码如下:
在这里插入图片描述
位置编码向量具体的数学公式如下:
P E p o s , 2 i = s i n ( p o s / 1000 0 2 i / d ) PE_{pos,2i}=sin(pos/10000^{2i/d}) PEpos,2i​=sin(pos/100002i/d)
P E p o s , 2 i + 1 = c o s ( p o s / 1000 0 2 i / d ) PE_{pos,2i+1}=cos(pos/10000^{2i/d}) PEpos,2i+1​=cos(pos/100002i/d)
其中, p o s pos pos 表示位置, i i i 表示维度。

2.5 MASK(掩码)

Mask 表示掩码,它对某些值进行掩盖,使其在参数更新时不产生效果。Transformer 模型里面涉及两种 mask,分别是 Padding Mask 和 Sequence Mask。其中,Padding Mask 在所有的 scaled dot-product attention 里面都需要用到,而 Sequence Mask 只有在 Decoder 的 Self-Attention 里面用到。

2.5.1 Padding Mask

什么是 Padding mask 呢?因为每个批次输入序列的长度是不一样的,所以我们要对输入序列进行对齐。具体来说,就是在较短的序列后面填充 0(但是如果输入的序列太长,则是截断,把多余的直接舍弃)。因为这些填充的位置,其实是没有什么意义的,所以我们的 Attention 机制不应该把注意力放在这些位置上,所以我们需要进行一些处理。

具体的做法:把这些位置的值加上一个非常大的负数(负无穷),这样的话,经过 Softmax 后,这些位置的概率就会接近 0。

2.5.2 Sequence Mask

Sequence Mask 是为了使得 Decoder 不能看见未来的信息。也就是对于一个序列,在 t tt 时刻,我们的解码输出应该只能依赖于 t tt 时刻之前的输出,而不能依赖 t tt 之后的输出。因为我们需要想一个办法,把 t tt 之后的信息给隐藏起来。

具体的做法:产生一个上三角矩阵,上三角的值全为 0。把这个矩阵作用在每个序列上,就可以达到我们的目的。

二、编码实现

1.库版本号以及相关配置

numpy 1.23.5
torch 1.8.1+cu111(torch版本貌似影响不大2.1.2也行)
torchaudio 0.8.1
torchtext 0.6.0
torchvision 0.9.1+cu111
sentencepiece 0.2.0

此外,本模型需要用到GPU,给大家一个时间参考
RTX 4090(24GB) 54min

没GPU的建议去Kaggle白嫖CPU T100,大概跑1h40min,虽然时间长了一点,但白嫖,我觉得很香!!

2.数据预处理和加载

我一直认为这部分是所有模型最关键的,模型再炫酷,数据不行,也是白搭!!

2.1导入数据集

# 从'zh-ja.bicleaner05.txt'文件读取数据到一个DataFrame中,使用制表符作为分隔符,不使用C语言引擎,不指定列名
# df代表整个数据集
df = pd.read_csv('zh-ja.bicleaner05.txt', sep='\\t', engine='python', header=None)

# 将df中第3列的数据转换为一个列表trainen,用于存储英文数据
trainen = df[2].values.tolist()  # 将第3列转换为列表

# 将df中第4列的数据转换为一个列表trainja,用于存储日文数据
trainja = df[3].values.tolist()  # 将第4列转换为列表

# 取消下面两行的注释将会删除列表中索引为5972的数据
# trainen.pop(5972)
# trainja.pop(5972)

我们来观察一下该数据集,例如我们来查看第1个句子,和它对应的翻译:

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

运行结果:
在这里插入图片描述

2.2导入分词器

需要注意:日语不像英文那样用空格来分隔单词。故我们使用JParaCrawl提供的分词器(貌似可以进行日文和英文的分词)

我感觉原文里面这个命名有点问题,ja_tokenizer为日文分词器,en_tokenizer我更愿意将其认为解码分词器(而非英文分词器)。

思考了一下,他应该就是英文分词器,压根没用中文分词器!!!英文中文跟日文也一个样,没空格,所以中文用这个分词器,最后的结果就是按字分,压根不是按词分。

但老师对不起,我还没想到特别好的解决方法,用咱们的中文分词器进行处理后,但貌似文本格式不匹配,就放弃了。

代码:

# 使用英文分词器对指定的句子分词
en_tokenizer.encode("All residents aged 20 to 59 years who live in Japan must enroll in public pension system.",out_type='str')
# 使用日文分词器对指定的句子分词
ja_tokenizer.encode("年金 日本に住んでいる20歳~60歳の全ての人は、公的年金制度に加入しなければなりません。",out_type='str')

运行结果:
英文分词:
在这里插入图片描述
用英文分词器对中文分词的结果:
在这里插入图片描述
这种分词,最后翻译的效果能好就怪了!!!

日文分词:(其实我也不知道到底分的好不好,但老师说这块需要改进,那肯定是存在问题的)
在这里插入图片描述

2.3构建词汇表

# 根据给定的训练数据(trainja和trainen)和对应的分词器(ja_tokenizer和en_tokenizer),统计每个语言中单词的出现频率,并创建词汇表。
def build_vocab(sentences, tokenizer):
    counter = Counter()  # 创建一个计数器,用于统计单词频率

    # 遍历sentences中的每个句子
    for sentence in sentences:
        # 使用tokenizer对句子进行编码,将句子转换为字符串形式并更新counter
        # out_type=str确保返回的是字符串,因为Counter需要的是可哈希的元素
        counter.update(tokenizer.encode(sentence, out_type=str))

    # 返回一个Vocab对象,包含计数器和一些特殊标记(如unk, pad, bos, eos)
    # 这些特殊标记在自然语言处理中通常用于填充、未知单词标记等
    return Vocab(counter, specials=['<unk>', '<pad>', '<bos>', '<eos>'])

# 分别使用ja_tokenizer和en_tokenizer构建日语和中文的词汇表
ja_vocab = build_vocab(trainja, ja_tokenizer)  # trainja是一个包含日语文本的列表
en_vocab = build_vocab(trainen, en_tokenizer)  # trainen是一个包含中文文本的列表

还是跟之前一样,我们来观察一下构建的词汇表:

运行以下代码:

print("日文词典(ja_vocab):")
for key, value in ja_vocab.stoi.items():
    print(f"{key}: {value}")

en_vocab = build_vocab(trainen, en_tokenizer)
print("中文词典(en_vocab):")
for key, value in en_vocab.stoi.items():
    print(f"{key}: {value}")

词汇表展示:
在这里插入图片描述
在这里插入图片描述

2.4构建词汇表

## 将训练数据(trainja和trainen)中的日语文本和中文文本转换为处理后的张量形式。每个元素都是一个元组,包含一个日语张量和一个英语张量。
## 通过这种方式,原始文本被编码为数值表示,便于神经网络模型进行训练。rstrip("\n")用于移除每个文本末尾的换行符。

def data_process(ja, en):
    # 初始化一个空列表data,用于存储处理后的数据对
    data = []

    # 使用zip函数,按元素对齐ja和en两个列表
    for (raw_ja, raw_en) in zip(ja, en):
        # 对每个原始日语文本raw_ja进行处理:
        # 1. 使用ja_tokenizer对文本进行编码,转换为字符串形式的token列表
        # 2. 将token列表中的每个元素映射到ja_vocab中对应的索引(如果token不在词汇表中,使用unk的索引)
        # 3. 将处理后的token列表转换为PyTorch张量,数据类型为long(整数)
        ja_tensor_ = torch.tensor([ja_vocab[token] for token in ja_tokenizer.encode(raw_ja.rstrip("\n"), out_type=str)], dtype=torch.long)

        # 对每个原始中文文本raw_en进行类似处理
        en_tensor_ = torch.tensor([en_vocab[token] for token in en_tokenizer.encode(raw_en.rstrip("\n"), out_type=str)], dtype=torch.long)

        # 将处理后的日语和中文张量对添加到data列表中
        data.append((ja_tensor_, en_tensor_))

    # 返回处理后的数据列表
    return data

# 使用data_process函数处理trainja和trainen(训练数据),并将结果存储在train_data变量中
train_data = data_process(trainja, trainen)

我们来看一下第2个训练数据:
在这里插入图片描述

2.5 数据加载器

其实对这部分,我感觉我不是很熟悉,每次编码都是直接用模板套;

代码:

## 本代码将训练数据(train_data)转换为批次数据,每个批次包含日语和中文的序列。
## generate_batch函数负责在每个序列的开始和结束添加BOS和EOS标记,并使用pad标记填充序列,确保所有序列具有相同的长度。
## DataLoader是一个PyTorch工具,用于从数据集中按批次加载数据,shuffle=True表示数据在每个epoch开始时会随机打乱,
## collate_fn=generate_batch指定了使用自定义的generate_batch函数来处理每个批次的数据。

BATCH_SIZE = 8  # 每个批次的样本数量
PAD_IDX = ja_vocab['<pad>']  # 使用pad标记的索引
BOS_IDX = ja_vocab['<bos>']  # 使用开始标记的索引
EOS_IDX = ja_vocab['<eos>']  # 使用结束标记的索引

# 定义一个函数generate_batch,它接受一个数据批次(data_batch)作为输入
def generate_batch(data_batch):
    # 初始化两个空列表ja_batch和en_batch,用于存储处理后的日语和中文批次数据
    ja_batch, en_batch = [], []

    # 遍历数据批次中的每个数据对(ja_item, en_item)
    for (ja_item, en_item) in data_batch:
        # 对每个数据对进行处理:
        # 1. 在日语序列的开始和结束添加BOS和EOS标记
        ja_batch.append(torch.cat([torch.tensor([BOS_IDX]), ja_item, torch.tensor([EOS_IDX])], dim=0))
        # 2. 对中文序列进行同样的处理
        en_batch.append(torch.cat([torch.tensor([BOS_IDX]), en_item, torch.tensor([EOS_IDX])], dim=0))

    # 使用pad_sequence函数,对日语和中文批次进行填充,确保所有序列的长度相同,pad值为PAD_IDX
    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

# 创建一个DataLoader,从train_data中加载数据,设置batch_size为BATCH_SIZE,进行随机洗牌,并使用generate_batch函数作为collate_fn
train_iter = DataLoader(train_data, batch_size=BATCH_SIZE,
                        shuffle=True, collate_fn=generate_batch)

3.定义模型

我认为大概的框架如下:
在这里插入图片描述

至于编码器解码器里面的自注意力机制,归一化,初始化,优化函数等,可见下方代码(因为我做了详细的注释,应该草履虫都能看懂!!)

编码器和解码器

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

# Seq2SeqTransformer类:
# 模型包含一个编码器(TransformerEncoder)和一个解码器(TransformerDecoder)。
# 在初始化时,根据输入参数创建了TransformerEncoderLayer和TransformerDecoderLayer,以及词嵌入层和位置编码层。
# forward方法负责处理整个序列的输入和输出,encode方法用于单独编码源序列,decode方法用于解码目标序列,给定编码后的记忆。
# 模型通过线性层(generator)将Transformer的输出映射到目标词汇表。
# 定义Seq2SeqTransformer类,继承自nn.Module
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__()

        # 创建TransformerEncoderLayer对象,参数d_model为嵌入维度,nhead为头的数量,dim_feedforward为前馈层维度
        encoder_layer = TransformerEncoderLayer(d_model=emb_size, nhead=NHEAD, dim_feedforward=dim_feedforward)

        # 初始化TransformerEncoder,传入encoder_layer和num_layers参数
        self.transformer_encoder = TransformerEncoder(encoder_layer, num_layers=num_encoder_layers)

        # 创建TransformerDecoderLayer对象,与encoder_layer参数相同
        decoder_layer = TransformerDecoderLayer(d_model=emb_size, nhead=NHEAD, dim_feedforward=dim_feedforward)

        # 初始化TransformerDecoder,传入decoder_layer和num_layers参数
        self.transformer_decoder = TransformerDecoder(decoder_layer, num_layers=num_decoder_layers)

        # 创建线性层(生成器),用于将Transformer的输出映射到目标词汇表
        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)

    # 定义forward方法,处理输入序列
    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))

        # 使用TransformerEncoder处理源序列
        memory = self.transformer_encoder(src_emb, src_mask, src_padding_mask)

        # 使用TransformerDecoder处理目标序列,生成输出
        outs = self.transformer_decoder(tgt_emb, memory, tgt_mask, None,
                                        tgt_padding_mask, memory_key_padding_mask)

        # 通过生成器层将Transformer的输出映射到目标词汇表
        return self.generator(outs)

    # 定义encode方法,仅用于编码源序列
    def encode(self, src: Tensor, src_mask: Tensor):
        return self.transformer_encoder(self.positional_encoding(
                            self.src_tok_emb(src)), src_mask)

    # 定义decode方法,仅用于解码目标序列,给定编码后的记忆
    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)

位置编码和词嵌入

# PositionalEncoding(位置编码)负责为词嵌入添加位置信息,通过计算余弦和正弦函数的衰减序列来实现。
# TokenEmbedding则负责将词汇表中的词ID映射到词嵌入,

# 位置编码
class PositionalEncoding(nn.Module):
    def __init__(self, emb_size: int, dropout, maxlen: int = 5000):  # 初始化函数,接收参数emb_size(词嵌入维度)、dropout概率和maxlen(最大序列长度,默认5000)
        super(PositionalEncoding, self).__init__()  # 调用父类(nn.Module)的初始化方法

        # 创建一个den变量,用于计算余弦和正弦函数的衰减系数
        den = torch.exp(- torch.arange(0, emb_size, 2) * math.log(10000) / emb_size)

        # pos是一个从0到maxlen的整数序列,用于表示位置信息
        pos = torch.arange(0, maxlen).reshape(maxlen, 1)

        # pos_embedding是一个全零矩阵,用于存储位置编码,其中奇数索引存储sin(pos * den),偶数索引存储cos(pos * den)
        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转换为形状(maxlen, emb_size, 1),以便在前向传播时可以与词嵌入相加
        pos_embedding = pos_embedding.unsqueeze(-2)

        # 初始化dropout层
        self.dropout = nn.Dropout(dropout)
        # 将pos_embedding注册为缓冲区,以便在前向传播时可以方便地访问
        self.register_buffer('pos_embedding', pos_embedding)

    # 前向传播函数,接收词嵌入并添加位置编码
    def forward(self, token_embedding: Tensor):
        # 返回dropout后的词嵌入加上对应位置编码
        return self.dropout(token_embedding + self.pos_embedding[:token_embedding.size(0), :])

# 定义一个名为TokenEmbedding的神经网络模块,用于生成词嵌入
class TokenEmbedding(nn.Module):
    def __init__(self, vocab_size: int, emb_size):  # 初始化函数,接收参数vocab_size(词汇表大小)和emb_size(词嵌入维度)
        super(TokenEmbedding, self).__init__()  # 调用父类(nn.Module)的初始化方法

        # 创建一个嵌入层,将词汇表中的每个词映射到emb_size维度的向量
        self.embedding = nn.Embedding(vocab_size, emb_size)
        # 设置emb_size变量,用于后续计算
        self.emb_size = emb_size

    # 前向传播函数,接收整数形式的词ID并返回对应的词嵌入,乘以emb_size的平方根以进行初始化缩放
    def forward(self, tokens: Tensor):
        return self.embedding(tokens.long()) * math.sqrt(self.emb_size)

掩码

 这个代码的作用是为自注意力机制(如Transformer模型中的Multi-Head Attention)生成所需的掩码。
# generate_square_subsequent_mask函数生成了一个方形的后续掩码,用于限制注意力头只能关注前面的元素,避免了自环和未来信息的依赖。
# create_mask函数根据输入的源(src)和目标(tgt)序列,生成源掩码、目标掩码、源填充掩码和目标填充掩码,
# 从而确保模型只关注有效信息。

# 生成一个方形的后文掩码
def generate_square_subsequent_mask(sz: int):  # sz是矩阵的大小
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")  # 获取当前设备

    # 创建一个sz x sz的全1矩阵,然后使用torch.triu函数将其变成上三角矩阵
    mask = (torch.triu(torch.ones((sz, sz), device=device), diagonal=1) == 1).transpose(0, 1)

    # 将矩阵转换为浮点型,并将0填充为负无穷,1填充为0,这样可以确保后续的注意力机制只关注前面的元素
    mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))

    return mask

# 根据源和目标序列创建相应的掩码
def create_mask(src: torch.Tensor, tgt: torch.Tensor):  # src和tgt是输入的源和目标序列

    # 获取源序列和目标序列的长度
    src_seq_len = src.shape[0]
    tgt_seq_len = tgt.shape[0]

    # 生成目标序列的后续掩码,用于自注意力机制
    tgt_mask = generate_square_subsequent_mask(tgt_seq_len)

    # 初始化源序列的掩码,全0矩阵,类型为布尔型
    src_mask = torch.zeros((src_seq_len, src_seq_len), device=device).type(torch.bool)

    # 创建源和目标的填充掩码,用于忽略填充(如PAD)的元素
    src_padding_mask = (src == PAD_IDX).transpose(0, 1)  # PAD_IDX是填充值的索引
    tgt_padding_mask = (tgt == PAD_IDX).transpose(0, 1)

    # 返回源掩码、目标掩码、源填充掩码和目标填充掩码
    return src_mask, tgt_mask, src_padding_mask, tgt_padding_mask

超参数和优化函数等内容

# 定义训练和评估Seq2SeqTransformer模型的函数

# 定义超参数
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)  # 参数包括编码器和解码器的层数、词嵌入维度、源和目标词汇表大小以及前馈网络隐藏层维度

# 对模型参数进行初始化,如果参数维度大于1,则使用Xavier均匀分布初始化
for p in transformer.parameters():
    if p.dim() > 1:
        nn.init.xavier_uniform_(p)

# 将模型移动到指定的设备(CPU或GPU)
transformer = transformer.to(device)

# 定义损失函数,使用交叉熵损失,忽略填充值
loss_fn = torch.nn.CrossEntropyLoss(ignore_index=PAD_IDX)

# 创建优化器,使用Adam优化器,学习率0.0001,动量参数设置为(0.9, 0.98),epsilon设置为1e-9
optimizer = torch.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

    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)

4.训练

这部分主要是花时间,只需1h

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"))

训练过程截图(以证明真实我训的,哈哈):
在这里插入图片描述
在这里插入图片描述

5.模型性能展示

我们来看一看翻译的效果:我选了3句话

# ja_vocab: 日语词汇表,用于将文本编码为数字序列。
# en_vocab: 中文词汇表,用于将翻译后的文本解码为文字。
# ja_tokenizer: 日语分词器,用于对输入文本进行预处理,将其分割成单词或子词。

#translate(transformer, "HSコード 8515 はんだ付け用、ろう付け用又は溶接用の機器(電気式(電気加熱ガス式を含む。)", ja_vocab, en_vocab, ja_tokenizer)
#translate(transformer, "今日はいい天気です。散歩に行きましょう。", ja_vocab, en_vocab, ja_tokenizer)
translate(transformer, "自然言語処理が好きです。", ja_vocab, en_vocab, ja_tokenizer)

一句是自带的:
在这里插入图片描述

一句是:今天天气很好,我们去散步吧
在这里插入图片描述

一句是:我喜欢自然语言处理
在这里插入图片描述
可以看到,最后一句翻译的最好,果然自然语言处理最喜欢自然语言处理。(冷笑话)

最后保存模型:

## 保存en_vocab和ja_vocab两个词汇表
import pickle
# open a file, where you want to store the data
file = open('en_vocab.pkl', 'wb')
# dump information to that file
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')```

总结

本文我们回顾了transformer模型,同时用其进行了中日翻译。
但效果并不是很好,我觉得主要原因在于:
1.中文分词效果不好:我们直接用的是英文的分词器,分出来的词都是一个字作为一个词,明显有错误,需要改正。但我使用了现有的中文分词工具后,后面加载数据部分却显示规格不正确,这个我一定会利用期末周后的时间把他改好的。
2.还需要训练更久。

标签:vocab,src,Transformer,进阶,tgt,mask,机器翻译,ja,注意力
From: https://blog.csdn.net/wwyhhh/article/details/139981606

相关文章

  • 机器翻译及实践 初级版:含注意力机制的编码器—解码器模型
    机器翻译及实践初级版:含注意力机制的编码器—解码器模型前言一、什么是机器翻译?二、所需要的前置知识(一).Seq2Seq1.什么是Seq2Seq2.机器翻译为什么要用Seq2Seq3.如何使用Seq2Seq3.1编码器的实现3.2解码器的实现3.3训练模型(二).注意力机制1.什么是注意力机制2.机器翻译为......
  • 一键进阶ComfyUI!懂AI的设计师现在都在用的节点式Stable Diffusion!内附安装包
    大家好,我是设计师阿威目前使用StableDiffusion进行创作的工具主要有两个:WebUI和ComfyUI。而更晚出现的ComfyUI凭借超高的可定制性和复现性迅速火遍全球。有设计师表示SD发布了XL1.0后,ComfyUI用它优秀的底层逻辑率先打击了臃肿不稳定的WebUI1.6,成为更适合“体验”XL的......
  • 写个时钟(进阶篇)
    在“写个时钟(行为篇)”中,我们通过 JavaScript 动态创建的时针、分针和秒针,并直接在 JavaScript 中通过控制行内样式的 transform属性,设置 rotate 的值,实现指针的旋转。这样的方式,对于 DOM 的控制消耗较大,若放到大项目中,对项目性能具有一定的影响,也让 JavaScript 代......
  • 【动画进阶】类 ChatGpt 多行文本打字效果
    今天我们来学习一个有意思的多行文本输入打字效果,像是这样:这个效果其实本身并非特别困难,实现的方式也很多,在本文中,我们更多的会聚焦于整个多行打字效果最后的动态光标的实现。也就是如何在文本不断变长,在不确定行数的情况下,让文字的最末行右侧处,一直有一个不断闪烁的光标效果:......
  • 独家原创 | Matlab实现CNN-Transformer多变量回归预测
    独家原创|Matlab实现CNN-Transformer多变量回归预测目录独家原创|Matlab实现CNN-Transformer多变量回归预测效果一览基本介绍程序设计参考资料效果一览基本介绍1.Matlab实现CNN-Transformer多变量回归预测;2.运行环境为Matlab2023b及以上;3.data为数......
  • 信我!这里有普通人也能理解的 Transformer
    引言如今爆火的大模型,GPT-3,BERT等,通过大量的参数和数据,为我们提供了前所未有的自然语言处理能力,使得机器能够更好地理解和生成人类的语言。而注意力机制无疑是重要的基石之一,作为一种新的神经网络结构,使得模型能够更好地捕捉序列中的长距离依赖关系,从而大大提高了模型的性......
  • 51单片机项目:进阶版密码锁(附代码详解)
    一、基本功能简介1.四位密码锁        默认密码为1201(小彩蛋*1),后续可自由修改密码。2.输入密码        按下不同按键,输入相应的数字(最多输入四位,输入少于四位使用0补全)按键与数字对应表按键数字S11S22S33S44S55S66S77S88S99S100......
  • Transformer详解encoder
    目录1.InputEmbedding2.PositionalEncoding3.Multi-HeadAttention4.Add&Norm5.Feedforward+Add&Norm6.代码展示(1)layer_norm(2)encoder_layer=1最近刚好梳理了下transformer,今天就来讲讲它~        Transformer是谷歌大脑2017年在论文attention......
  • JAVA高级进阶14设计模板
    第十四天、设计模板什么是设计模板(Designpattern)?一个问题通常有n种解法,其中肯定有一种解法是最优的,这个最优的解法被人总结出来了,称之为设计模式设计模式有20多种,对应20多种软件开发中会遇到的问题单例设计模式单例设计模式作用:确保一个类只有一个对象场景:计算......
  • JAVA高级进阶13单元测试、反射、注解
    第十三天、单元测试、反射、注解单元测试介绍单元测试就是针对最小的功能单元(方法),编写测试代码对其进行正确性测试咱们之前是如何进行单元测试的?有啥问题?只能在main方法编写测试代码,去调用其他方法进行测试。无法实现自动化测试,一个方法测试失败,可能影响其他方......