PyTorch生成式人工智能(24)——使用PyTorch构建Transformer模型
PyTorch生成式人工智能(24)——使用PyTorch构建Transformer模型
- 0. 前言
- 1. 注意力机制 和 Transformer
- 1.1 注意力机制
- 1.2 Transformer 架构
- 1.3 不同类型的 Transformer
- 2. 构建编码器
- 2.1 注意力机制
- 2.2 创建编码器
- 3. 构建编码器-解码器 Transformer
- 3.1 创建解码器层
- 3.2 创建编码器-解码器 Transformer
- 4. 基于 Transformer 构建机器翻译模型
- 4.1 定义生成器
- 4.2 创建翻译模型
- 小结
- 系列链接
0. 前言
相较于传统模型,如循环神经网络 (Recurrent Neural Network, RNN) 和卷积神经网络 (Convolutional Neural Network, CNN),Transformer
的优势在于能够有效地理解输入和输出序列中元素之间的关系,尤其是在长距离依赖的情况下,例如文本中相距较远的两个单词之间的关系。与 RNN
不同,Transformer
能够并行训练,显著减少训练时间,并且能够处理大规模数据集。这种创新性的架构在大语言模型 (Large Language Model
, LLM
) 如 ChatGPT
、BERT
和 DeepSeek
的发展中起到了关键作用,标志着人工智能领域发展的一个重要里程碑。
在 Transformer
模型之前,自然语言处理 (Natuarl Language Processing
, NLP
) 及类似任务主要依赖 RNN
,其中包括长短期记忆 (Long Short-Term Memory, LSTM) 网络。然而,RNN
按顺序处理信息,由于无法并行训练,限制了其速度,并且在保持序列早期部分信息方面存在困难,因此难以捕捉长期依赖关系。
Transformer
架构的创新在于其注意力机制。注意力机制通过分配权重来评估序列中单词之间的关系,基于训练数据决定单词之间的语义相关性。这使得像 ChatGPT
这样的模型能够理解单词之间的关系,从而更有效地理解语言数据。输入的非顺序处理使得并行训练成为可能,从而缩短了训练时间,并支持使用大规模数据集,从而推动了 LLM
的崛起,并促进了当前人工智能 (Artificial Intelligence
, AI
) 技术的飞速发展。
为了从零开始构建 Transformer
,我们首先探讨自注意力机制的工作原理,包括查询 (query
)、键 (key
) 和值 (value
) 向量的作用,以及缩放点积注意力 (Scaled Dot Product Attention
, SDPA
) 的计算。通过将层归一化和残差连接集成到多头注意力层中,并与前馈层结合,构建编码器层。然后,堆叠六个编码器层构建编码器,还将实现 Transformer
中的解码器。
1. 注意力机制 和 Transformer
要理解机器学习中的 Transformer
,首先必须理解注意力机制。该机制使 Transformer
能够识别序列元素之间的长程依赖性,这是它们与早期序列预测模型(如 RNN
)的关键区别所在。通过注意力机制,Transformer
能够同时关注序列中的每个元素,理解每个单词的上下文。
接下来,以单词 “bank
” 为例,说明注意力机制如何根据上下文解释词义。在句子 “I went fishing by the river yesterday, remaining near the bank the whole afternoon
” 中,单词 “bank
” 与 “fishing
” 相关联,因为它指的是河岸的区域。其中,Transformer
将 “bank
” 理解为河流地形的一部分。相比之下,在句子 “Kate went to the bank after work yesterday and deposited a check there
” 中,“bank
” 与 “check
” 相关联,使得 Transformer
将 “bank
” 识别为金融机构。
1.1 注意力机制
注意力机制是一种用于确定序列中元素之间相互关系的方法,通过计算得分来表示一个元素与序列中其他元素的关系,得分越高表示关系越强。在 NLP
中,这一机制有助于有意义地连接句子中的单词。本节将实现用于 Transformer
的注意力机制。
我们将构建一个由编码器和解码器组成的 Transformer
机器翻译模型,编码器将英语句子(如 “How are you?
” )转化为捕捉其含义的向量表示,解码器则使用这些向量表示生成法语翻译。
为了将短语 “How are you?
” 转换为向量表示,模型首先将其拆分为词元序列 [how, are, you, ?]
。每个词元由一个 256
维的向量表示,称为词嵌入 (word embeddings
),它捕捉了每个词元的含义。编码器还使用位置编码 (positional encoding
) 来确定词元在序列中的位置。将位置编码添加到词嵌入中,形成输入嵌入 (input embeddings
),用于计算自注意力 (self-attention
)。对于 “How are you?
”,输入嵌入形成一个维度为 (4, 256)
的张量,其中 4
表示词元数量,256
表示每个嵌入的维度。
计算注意力有多种方法,本节将介绍最常见的方法——缩放点积注意力 (Scaled Dot Product Attention
, SDPA
) 。该机制也称为自注意力 (self-attention
),因为算法计算一个单词如何关注序列中的所有单词,包括它自身。下图展示了计算 SDPA
的过程。
在计算注意力时,查询 (query
)、键 (key
) 和值 (value
) 的灵感来自于检索系统。假如,我们访问图书馆寻找一本书,在图书馆的搜索引擎中搜索“金融中的机器学习”,这个短语就是查询 (query
),图书馆中的书名和描述则充当键 (key
)。根据查询与这些键之间的相似度,图书馆的检索系统会推荐一份书单(值,value
),其中书名或描述中包含“机器学习”、“金融”或两者的书,可能会排名更高。相比之下,与这些查询无关的书籍匹配分数较低,因此不太可能被推荐。
为了计算 SDPA
,输入嵌入 XXX 会通过三层不同的神经网络进行处理。这些层的相应权重为 WQW_QWQ、WKW_KWK 和 WVW_VWV,每个权重的维度为 256×256256 \times 256256×256,这些权重在训练阶段从数据中学习。因此,可以通过以下公式计算查询 QQQ、键 KKK 和值 VVV:
Q=X×WQK=X×WKV=X×WVQ = X \times W_Q \\ K = X \times W_K \\ V = X \times W_V Q=X×WQK=X×WKV=X×WV
其中,QQQ、KKK 和 VVV 的维度与输入嵌入 XXX 相匹配,均为 4×2564 \times 2564×256。
类似于检索系统示例,在注意力机制中,使用 SDPA
方法来评估查询向量与键向量之间的相似度。SDPA
计算查询 QQQ 和键 KKK 向量的点积。较高的点积表示这两个向量之间的相似度较强,反之亦然。缩放后的注意力分数计算如下:
Attention Score(Q,K)=Q⋅KTdk\text{Attention Score} (Q,K)= \frac{Q \cdot K^T}{\sqrt{d_k}} Attention Score(Q,K)=dkQ⋅KT
其中,dkd_kdk 表示键向量 KKK 的维度,在本节中为 256
。通过除以 dkd_kdk 的平方根来缩放 QQQ 和 KKK 的点积,从而稳定训练过程,缩放操作是为了防止点积的值过大。当向量维度很高时,查询和键向量之间的点积可能会变得非常大,因为查询向量的每个元素都会与键向量的每个元素相乘,然后将这些乘积加起来。
接下来,对这些注意力分数应用 softmax
函数,将它们转化为注意力权重,确保一个单词对句子中所有单词的总注意力之和为 100%
。
上图展示注意力的计算过程。对于句子 “How are you?
”,注意力权重形成一个 4×4
的矩阵,表示句子中每个词元 (["How", "are", "you", "?"]
) 与其他所有词元(包括其自身)的关系。例如,在注意力权重矩阵的第一行,词元 “How
” 将 10%
的注意力分配给自己,分别将 40%
、40%
和 10%
的注意力分配给其他三个词元。
最终的注意力输出通过注意力权重与值向量 VVV 的点积计算得出:
Attention(Q,K,V)=softmax(Q⋅KTdk)⋅V\text{Attention} (Q,K,V)= \text{softmax}(\frac{Q \cdot K^T}{\sqrt{d_k}})\cdot V Attention(Q,K,V)=softmax(dkQ⋅KT)⋅V
输出维度保持为 4 × 256
,与输入维度一致。
总结来说,整个过程从句子 “How are you?
” 的输入嵌入 XXX 开始,XXX 的维度为 4×256
。该嵌入包含了四个词元的独立含义,但缺乏上下文理解。注意力机制的最终输出是 Attention(Q,K,V)\text{Attention}(Q, K, V)Attention(Q,K,V),其维度仍为 4 × 256
,该输出可以看作是对四个原始词元的上下文组合。原始词元的权重根据每个词元在上下文中的相关性变化,赋予在句子中更重要的词更高的权重。通过这一过程,注意力机制将表示孤立词元的向量转换为蕴含上下文意义的向量,从而提取出更丰富、更细致的句子理解。
此外,Transformer
模型并不只使用一组查询、键和值向量,而是采用了多头注意力 (multihead attention
) 机制。例如,256
维的查询、键和值向量可以被分割成 8
个注意力头,每个注意力头有一组维度为 32
(256/8=32
) 的查询、键和值向量。每个注意力头关注输入的不同部分或方面,使得模型能够捕捉更广泛的信息,并形成对输入数据更详细、更上下文相关的理解。当一个词在句子中有多种含义时(例如双关语),多头注意力尤其有用。在下一节的英法翻译任务中,将实现将 QQQ、KKK、VVV 分割成多个注意力头,在每个注意力头中计算注意力,然后再将它们拼接回一个单一的注意力向量。
1.2 Transformer 架构
注意力机制的概念于 2014
年提出,在《Attention Is All You Need
》中注意力机制得到了广泛应用,该论文专注于创建一种用于机器语言翻译的模型,称为 Transformer
,如下图所示,它具有一个依赖于注意力机制的编码器-解码器结构。
以英法翻译为例。Transformer
的编码器将一个英语句子,如 “I don’t speak French
”,转换成表示其含义的向量表示。然后,Transformer
的解码器处理这些向量,并生成法语翻译 “Je ne parle pas français
”。编码器的作用是捕捉原始英语句子的核心含义。例如,如果编码器有效,它应该能够将 “I don’t speak French
” 和 “I do not speak French
” 转换为相似的向量表示,解码器将解释这些向量并生成相似的翻译。
Transformer
中的编码器首先对英语和法语句子进行分词,采用子词分词 (subword tokenization
) 技术。子词分词是一种自然语言处理技术,用于将单词拆分成更小的组成部分(即子词),从而实现更高效和细致的处理。例如,英语短语 “I do not speak French
” 被拆分为六个词元:(i, do, not, speak, fr, ench)
;类似地,其法语翻译 “Je ne parle pas français
” 被同样拆分为六个词元:(je, ne, parle, pas, franc, ais)
。这种分词方法增强了 Transformer
处理语言变化和复杂性的能力。
深度学习模型(包括 Transformer
)无法直接处理文本,因此在输入模型之前,词元会被索引为整数。这些词元通常会首先使用独热编码 (one-hot encoding
) 表示,然后,通过词嵌入层将它们压缩成具有连续值的向量,其尺寸更小(例如长度为 256
)。因此,在应用词嵌入后,句子 “I do not speak French
” 被表示为一个 6 × 256
的矩阵。
与循环神经网络按顺序处理数据不同,Transformer
并行处理输入数据(例如句子)。这种并行性提高了效率,但并不会自动识别输入的顺序。为了解决这个问题,Transformer
在输入嵌入中添加了位置编码 (positional encoding
),位置编码是分配给输入序列中每个位置的独特向量,并且与输入嵌入的维度对齐。向量值由特定的位置函数确定,涉及不同频率的正弦和余弦函数,定义如下:
PositionalEncoding(pos,2i)=sin(posn2i/d)PositionalEncoding(pos,2i+1)=cos(posn2i/d)\text{PositionalEncoding}(\text{pos},2i)=sin(\frac{\text{pos}}{n^{2i/d}})\\ \text{PositionalEncoding}(\text{pos},2i+1)=cos(\frac{\text{pos}}{n^{2i/d}}) PositionalEncoding(pos,2i)=sin(n2i/dpos)PositionalEncoding(pos,2i+1)=cos(n2i/dpos)
在以上方程中,使用正弦函数计算偶数索引的向量,使用余弦函数计算奇数索引的向量。两个参数 pospospos 和 iii 分别表示词元在序列中的位置和向量中的索引。以短语 “I do not speak French
” 的位置编码为例,它被表示为一个 6 × 256
的矩阵,大小与该句子的词嵌入相同。其中,pospospos 的范围是从 0
到 5
,而索引 2i2i2i 和 2i+12i + 12i+1 共同覆盖 256
个不同的值(从 0
到 255
)。这种位置编码方法的一个优点是所有值都被限制在 -1
到 1
的范围内。
需要注意的是,每个词元的位置都由一个 256
维的向量唯一标识,这些向量值在训练过程中保持不变。在输入到注意力层之前,这些位置编码会被添加到序列的词嵌入中。以句子 “I do not speak French
” 为例,编码器生成词嵌入和位置编码,每个的维度均为 6 × 256
,然后将它们合并成一个 6 × 256
维表示。随后,编码器应用注意力机制,将这个嵌入向量精细化为更复杂的向量表示,这些表示捕捉了句子的整体含义,然后将它们传递给解码器。
Transformer
的编码器如下图所示,由六个相同的层 (N = 6
) 组成。每一层包含两个不同的子层,第一个子层是多头自注意力层,第二个子层是一个逐位置的全连接前馈网络,该网络独立地处理序列中的每个位置,而不是将它们视为顺序元素。在模型架构中,每个子层都包含层归一化和残差连接。层归一化将观测值标准化,使其均值为零,标准差为单位。这种归一化有助于稳定训练过程。在归一化层之后,进行残差连接,这意味着每个子层的输入会与其输出相加,从而增强信息在网络中的流动。
Transformer
模型的解码器如下图所示,由六个相同的解码器层 (N = 6
) 组成。每个解码器层包含三个子层:一个多头自注意力子层,一个在第一子层输出和编码器输出之间执行多头交叉注意力的子层,最后是一个前馈子层。需要注意的是,每个子层的输入是前一个子层的输出。此外,解码器层中的第二个子层也将编码器的输出作为输入。这对于整合编码器的信息至关重要:解码器正是通过这种方式基于编码器的输出生成翻译。
解码器的自注意力子层的一个关键特点是掩码机制。这种掩码机制防止模型访问序列中的未来位置,确保某个位置的预测只能依赖于之前已知的元素。这种顺序依赖性对于语言翻译或文本生成等任务至关重要。
解码过程从解码器接收一个法语输入短语开始。解码器将法语词元转化为词嵌入和位置编码,然后将它们合并为一个嵌入。此步骤确保模型不仅理解短语的语义内容,而且保持顺序上下文,这对于准确的翻译或生成任务至关重要。
解码器以自回归方式运行,逐个生成输出序列的词元。在第一个时间步,它从表示句子开始的 “BOS
” (句子开始词元)开始。使用这个开始词元作为初始输入,解码器检查英语短语 “I do not speak French
” 的向量表示,并尝试预测 “BOS
” 之后的第一个词元,假设解码器的第一个预测是 “Je
”。在下一个时间步,使用序列 “BOS Je
”作为新的输入来预测下一个词元。持续进行上述过程,解码器将每个新预测的词元添加到其输入序列中,用于后续的预测。
翻译过程在解码器预测出 “EOS
” (句子结束词元)时结束,标志着句子的结束。在准备训练数据时,我们会在每个短语的末尾添加 EOS
,这样模型就学会了 EOS
表示句子的结束。达到这个词元时,解码器识别到翻译任务已完成并停止操作。这种自回归方法确保解码过程中的每一步都依赖所有先前预测的词元,从而生成连贯且上下文合适的翻译。
1.3 不同类型的 Transformer
Transformer
有三种类型:仅编码器 Transformer
、仅解码器 Transformer
和编码器-解码器 Transformer
。
仅编码器 Transformer
由 N
个相同的编码器层组成,能够将序列转换为抽象的连续向量表示。例如,BERT
是一个仅编码器 Transformer
,包含 12
个编码器层。仅编码器 Transformer
可以用于文本分类。例如,如果两个句子具有相似的向量表示,我们可以将它们归为同一类别;而如果两个序列具有非常不同的向量表示,我们可以将它们归为不同类别。
仅解码器 Transformer
由 N
个相同的解码器层组成。例如,ChatGPT
是一个仅解码器 Transformer
,包含多个解码器层。仅解码器 Transformer
可以根据提示生成文本。例如,它会提取提示中单词的语义,并预测最可能的下一个词元。然后,它将该词元添加到提示的末尾,并重复该过程,直到文本达到一定长度。
编码器-解码器 Transformer
用于处理复杂任务,例如文本到图像生成或语音识别。编码器-解码器 Transformer
结合了编码器和解码器的优势。编码器在处理和理解输入数据时非常高效,而解码器在生成输出时表现优异。编码器与解码器的结合使得模型能够有效理解复杂的输入(如文本或语音),并生成复杂的输出(如图像或转录文本)。
2. 构建编码器
本节将讨论如何构建 Transformer
中的编码器。具体而言,我们将深入探讨如何构建每个编码器层内的各个子层,并实现多头自注意力机制。
2.1 注意力机制
虽然存在多种注意力机制,本节中我们使用缩放点积注意力 (Scaled Dot-Product Attention
, SDPA
)。SDPA
注意力机制通过查询 (query
)、键 (key
) 和值 (value
) 来计算序列中元素之间的关系。它为每个元素分配一个分数,表示该元素与序列中所有元素(包括它自身)的关联程度。
Transformer
模型并不是使用单一的查询、键和值向量,而是采用了多头注意力机制。将 256
维的查询、键和值向量分成 8
个头,每个头有一组 32
维的查询、键和值向量 (256 / 8 = 32
)。每个头关注输入的不同部分或方面,使得模型能够捕捉更广泛的信息,并对输入数据形成更详细和具有上下文的理解。例如,多头注意力可以帮助模型理解 “bank
” 一词的多种含义:“Why is the river so rich? Because it has two banks.
”
(1) 在模块 util.py
中定义 attention()
函数:
import torch
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"import numpy as np
from torch import nn
from copy import deepcopy
import mathdef attention(query, key, value, mask=None, dropout=None):d_k = query.size(-1)# 缩放后的注意力得分是查询和键的点积,并经过 d_k 的平方根缩放scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)if mask is not None:# 如果存在掩码,则隐藏序列中的未来元素scores = scores.masked_fill(mask == 0, -1e9)# 计算注意力权重p_attn = nn.functional.softmax(scores, dim=-1)if dropout is not None:p_attn = dropout(p_attn)# 返回注意力和注意力权重return torch.matmul(p_attn, value), p_attn
下图通过一个实例展示了多头注意力的工作原理。将位置编码添加到词嵌入之后,句子 “How are you?
” 的嵌入是一个大小为 (1, 6, 256)
的张量。其中,1
表示批次中只有一个句子,句子中有 6
个词元,因为我们在序列的开头和结尾分别添加了 BOS
和 EOS
词元。这些嵌入通过三个线性层获得查询 (Q
)、键 (K
) 和值 (V
) 向量,每个向量的大小都是 (1, 6, 256)
。然后将它们分成八个头,得到八组不同的 Q
、K
和 V
,每个大小为 (1, 6, 256/8 = 32)
。将 attention()
函数应用于每一组,从而产生八个注意力输出,每个输出的大小也是 (1, 6, 32)
。然后我们将这八个注意力输出拼接成一个单一的注意力输出,得到是一个大小为 (1, 6, 32 × 8 = 256)
的张量。最后,拼接后的注意力通过另一个大小为 256 × 256
的线性层,生成最终输出,即 MultiHeadAttention()
类的输出,尺寸保持为 (1, 6, 256)
,与原始输入相同。
(2) 使用 PyTorch
在 util.py
中实现多头注意力:
class MultiHeadedAttention(nn.Module):def __init__(self, h, d_model, dropout=0.1):super().__init__()assert d_model % h == 0self.d_k = d_model // hself.h = hself.linears = nn.ModuleList([deepcopy(nn.Linear(d_model, d_model)) for i in range(4)])self.attn = Noneself.dropout = nn.Dropout(p=dropout)def forward(self, query, key, value, mask=None):if mask is not None:mask = mask.unsqueeze(1)nbatches = query.size(0)# 输入通过三个线性层,得到 Q、K、V,并将它们拆分成多个注意力头query, key, value = [l(x).view(nbatches, -1, self.h,self.d_k).transpose(1, 2)for l, x in zip(self.linears, (query, key, value))]# 计算每个头的注意力和注意力权重x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)# 将多头注意力向量连接成一个单一的注意力向量x = x.transpose(1, 2).contiguous().view(nbatches, -1, self.h * self.d_k)# 输出通过一个线性层output = self.linears[-1](x)return output
(3) 每个编码器层和解码器层还包含一个前馈子层,这是一个两层的全连接神经网络,旨在增强模型捕捉和学习训练数据集中复杂特征的能力。此外,神经网络独立处理每个嵌入,而不是将嵌入序列视为单一的向量。因此,我们通常称其为逐位置前馈网络。在 util.py
模块中定义 PositionwiseFeedForward()
类:
class PositionwiseFeedForward(nn.Module):def __init__(self, d_model, d_ff, dropout=0.1):super().__init__()self.w_1 = nn.Linear(d_model, d_ff)self.w_2 = nn.Linear(d_ff, d_model)self.dropout = nn.Dropout(dropout)def forward(self, x):h1 = self.w_1(x)h2 = self.dropout(h1)return self.w_2(h2)
PositionwiseFeedForward()
类定义了两个关键参数:d_ff
(前馈层的维度)和 d_model
(模型维度的大小)。通常,d_ff
的值选择为 d_model
的四倍。在本节中,d_model
为 256
,因此将 d_ff
设置为 256 x 4 = 1024
。这种将隐藏层扩大为模型尺寸数倍的做法是 Transformer
架构中的标准方法,它增强了网络捕捉和学习训练数据集中复杂特征的能力。
2.2 创建编码器
(1) 为了创建编码器层,首先定义 EncoderLayer()
类和 SublayerConnection()
类:
class SublayerConnection(nn.Module):def __init__(self, size, dropout):super().__init__()self.norm = LayerNorm(size)self.dropout = nn.Dropout(dropout)def forward(self, x, sublayer):# 每个子层都经过残差连接和层归一化output = x + self.dropout(sublayer(self.norm(x)))return output class EncoderLayer(nn.Module):def __init__(self, size, self_attn, feed_forward, dropout):super().__init__()self.self_attn = self_attnself.feed_forward = feed_forwardself.sublayer = nn.ModuleList([deepcopy(SublayerConnection(size, dropout)) for i in range(2)])self.size = size def forward(self, x, mask):# 每个编码器层中的第一个子层是一个多头自注意力网络x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))# 每个编码器层中的第二个子层是一个前馈网络output = self.sublayer[1](x, self.feed_forward)return output
每个编码器层由两个不同的子层组成:一个是多头自注意力层,使用 MultiHeadAttention()
类;另一个是简单的逐位置全连接前馈网络,使用 PositionwiseFeedForward()
类。此外,这两个子层都包含了层归一化和残差连接,残差连接方法用于解决消失梯度问题。Transformer
中残差连接的另一个好处是,为位置编码(仅在第一层之前计算)传递到后续层提供了一条通道。
(2) 层归一化在一定程度上类似于批归一化,将层中的观测标准化,使其均值为零,标准差为 1
。在 util.py
中定义 LayerNorm()
类,用于执行层归一化:
class LayerNorm(nn.Module):def __init__(self, features, eps=1e-6):super().__init__()self.a_2 = nn.Parameter(torch.ones(features))self.b_2 = nn.Parameter(torch.zeros(features))self.eps = epsdef forward(self, x):mean = x.mean(-1, keepdim=True) std = x.std(-1, keepdim=True)x_zscore = (x - mean) / torch.sqrt(std ** 2 + self.eps)output = self.a_2*x_zscore+self.b_2return output
(3) 通过堆叠六个编码器层创建编码器,在 util.py
模块中定义 Encoder()
类:
class Encoder(nn.Module):def __init__(self, layer, N):super().__init__()self.layers = nn.ModuleList([deepcopy(layer) for i in range(N)])self.norm = LayerNorm(layer.size)def forward(self, x, mask):for layer in self.layers:x = layer(x, mask)output = self.norm(x)return output
其中,Encoder()
类定义了两个参数:layer
,即在 EncoderLayer()
类所指定的编码器层;N
,即编码器中的编码器层数量。Encoder()
类接受输入 x
(例如一批英文短语)和 mask
(用于掩码序列填充)来生成输出(捕捉英文短语含义的向量表示)。
3. 构建编码器-解码器 Transformer
接下来我们继续实现解码器。首先创建解码器层,然后,堆叠 N = 6
个相同的解码器层来形成一个解码器。
3.1 创建解码器层
每个解码器层由三个子层组成:
- 多头自注意力层
- 来自第一个子层的输出与编码器输出之间的交叉注意力层
- 前馈网络
每个子层都包含层归一化和残差连接,此外,解码器部分的多头自注意力子层被掩码化,以防止当前位置关注后续的位置。这个掩码强制模型使用序列中的之前元素来预测后续元素。
(1) 在 util.py
模块中定义 DecoderLayer()
类:
class DecoderLayer(nn.Module):def __init__(self, size, self_attn, src_attn,feed_forward, dropout):super().__init__()self.size = sizeself.self_attn = self_attnself.src_attn = src_attnself.feed_forward = feed_forwardself.sublayer = nn.ModuleList([deepcopy(SublayerConnection(size, dropout)) for i in range(3)])def forward(self, x, memory, src_mask, tgt_mask):# 第一个子层是一个掩码多头自注意力层x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, tgt_mask))# 第二个子层是目标语言和源语言之间的跨注意力层x = self.sublayer[1](x, lambda x:self.src_attn(x, memory, memory, src_mask))# 第三个子层是一个前馈网络output = self.sublayer[2](x, self.feed_forward)return output
为了说明解码器层的操作,我们继续使用上一小节的示例。解码器接受词元 ['BOS', 'comment', 'et', 'es-vous', '?']
,以及来自编码器的输出 (memory
) 来预测序列 ['comment', 'et', 'es-vous', '?', 'EOS']
。['BOS', 'comment', 'et', 'es-vous', '?']
的嵌入是一个大小为 (1, 5, 256)
的张量:1
是批次中的序列数量,5
是序列中的词元数量,256
表示每个词元由一个 256 维的向量表示。将这个嵌入通过第一个子层,即掩码多头自注意力层。这一过程与编码器层中的多头自注意力计算类似,不同之处在于,这个过程使用了一个掩码,在以上代码中为 tgt_mask
,在当前示例中是一个 5 × 5
的张量,其值如下:
可以看到,掩码的下半部分(张量中主对角线以下的值)被设置为 True
,而掩码的上半部分(主对角线以上的值)被设置为 False
。当这个掩码应用于注意力分数时,第一个词元在第一个时间步仅关注自身。在第二个时间步,注意力分数仅在前两个词元之间计算。随着过程的继续,例如在第三个时间步,解码器使用词元 ['BOS', 'comment', 'et']
来预测词元 ‘es-vous
’,注意力分数仅在这三个词元之间计算,从而有效地隐藏了之后的词元 ['es-vous', '?']
。
遵循这一过程,第一个子层生成的输出是一个大小为 (1, 5, 256)
的张量,与输入的大小匹配。这个输出 (x
) 随后输入到第二个子层。在第二个子层中,计算 x
与编码器部分输出(称为 memory
)之间的交叉注意力。memory的维度是 (1, 6, 256)
,因为英文短语 “How are you?
” 被转换为六个词元 ['BOS', 'how', 'are', 'you', '?', 'EOS']
。
下图展示了交叉注意力权重的计算过程。为了计算 x
和 memory
之间的交叉注意力,我们首先将 x
通过一个神经网络,得到查询 (query
),其维度是 (1, 5, 256
)。然后,我们将 memory
通过两个神经网络得到键 (key
) 和值 (value
),维度均为为 (1, 6, 256)
。计算缩放注意力分数,维度是 (1, 5, 6)
:查询 Q 的维度是 (1, 5, 256)
,转置后的键 K 的维度是 (1, 256, 6)
。因此,缩放注意力分数是两者的点积,经过 dk\sqrt{d_k}dk 缩放,得到的大小为 (1, 5, 6)
。在对缩放注意力分数应用 softmax
函数后,得到注意力权重,这是一个 5 × 6
的矩阵。这个矩阵告诉法语输入 ['BOS', 'comment', 'et', 'es-vous', '?']
中的五个词元如何关注英语短语 ['BOS', 'how', 'are', 'you', '?', 'EOS']
中的六个词元。这就是解码器在翻译过程中捕捉英文短语含义的方式。
第二个子层中的最终交叉注意力是通过注意力权重和值向量 VVV 的点积计算得到的。注意力权重的维度是 (1, 5, 6)
,值向量的维度是 (1, 6, 256)
,因此,最终的交叉注意力,即两者的点积,大小为 (1, 5, 256)
。因此,第二个子层的输入和输出具有相同的维度 (1, 5, 256)
。经过第二个子层处理后,输出将进入第三个子层,即前馈网络。
3.2 创建编码器-解码器 Transformer
(1) 解码器由 N = 6
个相同的解码器层组成。Decoder()
类在 util.py
模块中定义:
class Decoder(nn.Module):def __init__(self, layer, N):super().__init__()self.layers = nn.ModuleList([deepcopy(layer) for i in range(N)])self.norm = LayerNorm(layer.size)def forward(self, x, memory, src_mask, tgt_mask):for layer in self.layers:x = layer(x, memory, src_mask, tgt_mask)output = self.norm(x)return output
(2) 为了创建编码器-解码器 Transformer
,首先在 util.py
模块中定义 Transformer()
类:
class Transformer(nn.Module):def __init__(self, encoder, decoder,src_embed, tgt_embed, generator):super().__init__()# 定义编码器self.encoder = encoder# 定义解码器self.decoder = decoderself.src_embed = src_embedself.tgt_embed = tgt_embedself.generator = generatordef encode(self, src, src_mask):return self.encoder(self.src_embed(src), src_mask)def decode(self, memory, src_mask, tgt, tgt_mask):return self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)def forward(self, src, tgt, src_mask, tgt_mask):# 源语言编码为向量表示memory = self.encode(src, src_mask)# 解码器使用这些向量表示生成目标语言的翻译output = self.decode(memory, src_mask, tgt, tgt_mask)return output
Transformer()
类由五个关键组件构成:编码器 (encoder
)、解码器 (decoder
)、源语言嵌入 (src_embed
)、目标语言嵌入 (tgt_embed
) 和生成器 (generator
)。编码器和解码器分别由 Encoder()
和 Decoder()
类表示。在英法翻译示例中,生成源语言的嵌入需要使用词嵌入 (word embedding
) 和位置编码 (positional encoding
) 处理英语短语的数值表示,并将结果结合组合成 src_embed
组件;同样地,对于目标语言,我们以相同的方式处理法语短语的数值表示,并将结果结合组合成 tgt_embed
组件。生成器 Generator()
则为目标语言中的每个词元对应的索引生成预测概率。
4. 基于 Transformer 构建机器翻译模型
在本节中,我们将整合所有组件,创建一个可以在任意两种语言之间进行翻译的 Transformer
模型。
4.1 定义生成器
首先,我们在 util.py
模块中定义 Generator()
类,用于生成下一个词元的概率分布,如下图所示。其基本思路是为解码器附加一个输出头 (head
),用于下游任务。在本节示例中,下游任务是预测法语翻译中的下一个词元。
(1) 定义 Generator()
类,Generator()
类会为每个索引生成预测概率,这些索引对应于目标语言中的词元,这使得模型能够利用先前生成的词元和编码器的输出,以自回归的方式顺序地预测词元:
class Generator(nn.Module):def __init__(self, d_model, vocab):super().__init__()self.proj = nn.Linear(d_model, vocab)def forward(self, x):out = self.proj(x)probs = nn.functional.log_softmax(out, dim=-1)return probs
4.2 创建翻译模型
(1) 创建 Transformer
模型,用于在任意两种语言之间进行翻译(例如,英语到法语或中文到英语)。在 util.py
中定义 create_model()
函数实现此功能:
def create_model(src_vocab, tgt_vocab, N, d_model,d_ff, h, dropout=0.1):attn=MultiHeadedAttention(h, d_model).to(DEVICE)ff=PositionwiseFeedForward(d_model, d_ff, dropout).to(DEVICE)pos=PositionalEncoding(d_model, dropout).to(DEVICE)model = Transformer(# 通过实例化 Encoder() 类创建编码器Encoder(EncoderLayer(d_model,deepcopy(attn),deepcopy(ff),dropout).to(DEVICE),N).to(DEVICE),# 通过实例化 Decoder() 类创建解码器Decoder(DecoderLayer(d_model,deepcopy(attn),deepcopy(attn),deepcopy(ff), dropout).to(DEVICE),N).to(DEVICE),# 将源语言通过词嵌入和位置编码创建 src_embednn.Sequential(Embeddings(d_model, src_vocab).to(DEVICE),deepcopy(pos)),# 将目标语言通过词嵌入和位置编码创建 tgt_embednn.Sequential(Embeddings(d_model, tgt_vocab).to(DEVICE),deepcopy(pos)),# 通过实例化 Generator() 类创建生成器Generator(d_model, tgt_vocab)).to(DEVICE)for p in model.parameters():if p.dim() > 1:nn.init.xavier_uniform_(p)return model.to(DEVICE)
在 create_model()
函数中,使用 Encoder()
、Decoder()
和 Generator()
类顺序地构建 Transformer()
类的五个关键组件:编码器 (encoder
)、解码器 (decoder
)、源语言嵌入 (src_embed
)、目标语言嵌入 (tgt_embed
) 和生成器 (generator
)。
小结
Transformer
是一种先进的深度学习模型,擅长处理序列到序列的预测任务。其优势在于能够有效理解输入和输出序列中元素之间的长距离关系Transformer
架构的创新型在于其注意力机制。注意力机制通过分配权重来评估序列中单词之间的关系,基于训练数据确定单词之间的关联程度。这使得Transformer
模型能够理解单词之间的关系,从而更有效地理解人类语言- 为了计算缩放点积注意力 (
Scaled Dot-Product Attention
,SDPA
),输入嵌入 XXX 通过三个不同的神经网络层进行处理:查询 (QQQ)、键 (KKK) 和值 (VVV)。这些层的相应权重分别为 WQW_QWQ、WKW_KWK 和 WVW_VWV。我们可以按以下方式计算 QQQ、KKK 和 VVV:
Q=X⋅WQK=X⋅WKV=X⋅WVQ = X\cdot W_Q\\ K = X \cdot W_K\\ V = X \cdot W_V Q=X⋅WQK=X⋅WKV=X⋅WV
SDPA
计算如下:
Attention(Q,K,V)=softmax(Q⋅KTdk)⋅V\text{Attention} (Q,K,V)= \text{softmax}(\frac{Q \cdot K^T}{\sqrt{d_k}})\cdot V Attention(Q,K,V)=softmax(dkQ⋅KT)⋅V
其中 dkd_kdk 表示键向量 KKK 的维度。对注意力分数应用softmax
函数,将其转换为注意力权重,这确保了一个单词对句子中所有单词的总注意力之和为100%
。最终的注意力是注意力权重与值向量 VVV 的点积 Transformer
模型采用了多头注意力 (multihead attention
) 机制,查询、键和值向量被拆分成多个头,每个头关注输入的不同部分或方面,使得模型能够捕捉更广泛的信息,并形成对输入数据更详细和更具上下文的理解。当一个单词在句子中有多重含义时,多头注意力尤其有用
系列链接
PyTorch生成式人工智能实战:从零打造创意引擎
PyTorch生成式人工智能(1)——神经网络与模型训练过程详解
PyTorch生成式人工智能(2)——PyTorch基础
PyTorch生成式人工智能(3)——使用PyTorch构建神经网络
PyTorch生成式人工智能(4)——卷积神经网络详解
PyTorch生成式人工智能(5)——分类任务详解
PyTorch生成式人工智能(6)——生成模型(Generative Model)详解
PyTorch生成式人工智能(7)——生成对抗网络实践详解
PyTorch生成式人工智能(8)——深度卷积生成对抗网络
PyTorch生成式人工智能(9)——Pix2Pix详解与实现
PyTorch生成式人工智能(10)——CyclelGAN详解与实现
PyTorch生成式人工智能(11)——神经风格迁移
PyTorch生成式人工智能(12)——StyleGAN详解与实现
PyTorch生成式人工智能(13)——WGAN详解与实现
PyTorch生成式人工智能(14)——条件生成对抗网络(conditional GAN,cGAN)
PyTorch生成式人工智能(15)——自注意力生成对抗网络(Self-Attention GAN, SAGAN)
PyTorch生成式人工智能(16)——自编码器(AutoEncoder)详解
PyTorch生成式人工智能(17)——变分自编码器详解与实现
PyTorch生成式人工智能(18)——循环神经网络详解与实现
PyTorch生成式人工智能(19)——自回归模型详解与实现
PyTorch生成式人工智能(20)——像素卷积神经网络(PixelCNN)
PyTorch生成式人工智能(21)——归一化流模型(Normalizing Flow Model)
PyTorch生成式人工智能(22)——GLOW详解与实现
PyTorch生成式人工智能(23)——能量模型(Energy-Based Model)