目录
一、Transformer介绍
1.1 Transformer的诞生
2017年,Google发出一篇论文《Attention is All You Need》,提出了transformer模型。它彻底改变了自然语言处理 (NLP) 领域,并在机器翻译、文本生成、文本分类等任务中取得了显著的成果。
2018年10月,Google发出一篇论文《BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding》, BERT模型横空出世, 并横扫NLP领域11项任务的最佳成绩!
论文地址: https://arxiv.org/pdf/1810.04805.pdf
而在BERT中发挥重要作用的结构就是Transformer, 之后又相继出现XLNET,roBERT等模型击败了BERT,但是他们的核心没有变,仍然是Transformer。
1.2 什么是Transformer
Transformer 是一种基于自注意力机制(Self-Attention)的序列到序列 (sequence-to-sequence) 的深度学习模型,最早由 Vaswani 等人在 2017 年的论文《Attention is All You Need》中提出。旨在解决自然语言处理(NLP)中的序列到序列(Seq2Seq)问题,如机器翻译等任务。与传统的循环神经网络(RNN)和长短期记忆网络(LSTM)不同,Transformer 完全依赖于自注意力机制,并摒弃了序列化计算过程,允许模型并行处理整个输入序列,因此具有更高的效率和更强的性能。
核心思想:注意力机制是 Transformer 模型的核心。它可以让模型在处理序列中的每个位置时,关注序列中其他位置的信息。 这意味着模型可以根据当前任务动态地调整每个位置的重要性,从而更好地捕捉序列中的长距离依赖关系。
1.3 Transformer的优势
相比之前占领市场的LSTM和GRU模型,Transformer有两个显著的优势:
并行计算:与RNN和LSTM不同,Transformer在编码器端使用自注意力机制,而不是递归神经网络,不需要逐步处理序列中的每个元素,因此可以通过并行计算大幅度加速训练过程。Transformer能够利用分布式GPU进行并行训练,提升模型训练效率。
捕捉长距离依赖:自注意力机制能够直接计算输入序列中任意两个元素之间的关系,从而更好地捕捉长距离依赖。在分析预测更长的文本时, 捕捉间隔较长的语义关联效果更好。
下面是一张在测评比较图:
1.4 Transformer的市场
在著名的SOTA机器翻译榜单上, 几乎所有排名靠前的模型都使用Transformer。
Transformer基本上可以看作是工业界的风向标, 市场空间自然不必多说
二、Transformer架构
2.1 Transformer模型的作用
基于seq2seq架构的transformer模型可以完成NLP领域研究的典型任务, 如机器翻译, 文本生成等。同时又可以构建预训练语言模型,用于不同任务的迁移学习。
在接下来的架构分析中, 我们将假设使用Transformer模型架构处理从一种语言文本到另一种语言文本的翻译工作, 因此很多命名方式遵循NLP中的规则。比如: Embeddding层将称作文本嵌入层, Embedding层产生的张量称为词嵌入张量, 它的最后一维将称作词向量等。
捕捉长距离依赖关系
传统的RNN和CNN模型在处理长序列时,难以捕捉远距离元素之间的依赖关系。
Transformer通过自注意力机制,能够直接计算输入序列中任意两个元素之间的关系,从而更好地捕捉长距离依赖。
并行计算
RNN模型需要按时间步依次处理序列数据,无法并行计算。
Transformer模型完全基于注意力机制,可以并行处理整个序列,显著提高了计算效率。
灵活性和可扩展性
Transformer模型具有较高的扩展性,可以通过增加层数或头数来提高模型的表达能力,适用于各种复杂的任务。
Transformer的架构设计灵活,可以轻松适应不同的输入和输出形式(文本、图像、音频以及语音等)。
2.2 Transformer总体架构图
2.2.1 Transformer总体架构
输入部分
输入嵌入:将输入的词或子词转换为固定维度的向量表示。
位置编码:由于Transformer不使用循环或卷积结构,无法直接捕捉序列的顺序信息,因此通过位置编码为输入添加位置信息。位置编码通常使用正弦和余弦函数来生成,并与输入的嵌入向量相加。
输出部分
线性变换:将解码器的输出映射到词汇表大小的维度。
Softmax激活函数:生成每个词的概率分布。
编码器部分
由N个相同的编码器层堆叠而成
多头自注意力机制:
这是Transformer的核心组件。自注意力机制允许模型在处理一个token时,同时关注输入序列中的所有其他 token,并计算它们之间的相关性权重。
多头机制将输入映射到多个不同的子空间,并行计算注意力,从而增强模型的表达能力。
前馈神经网络:
每个注意力层的输出都会经过一个前馈神经网络 (通常是两层全连接层) 进行非线性变换,进一步提取特征。
残差连接:
为了缓解深层网络中的梯度消失问题,每个子层(注意力层和前馈层)的输入会与该层的输出相加,形成残差连接。
规范化层(层归一化):
在每个子层的输入和输出之间进行层归一化,可以加速训练并提高模型稳定性。
解码器部分
由N个相同的解码器层堆叠而成
掩码多头自注意力机制:
与编码器中的自注意力类似,但加入了掩码机制,确保模型在生成每个token时,只能关注到该token之前的 token。
这种掩码机制保证了解码过程的自回归性,即模型按照顺序逐步生成输出序列。
多头编码器-解码器注意力机制:
解码器还需要关注编码器的输出,从而将编码器的语义信息融入到解码过程。
该注意力机制的查询 (Query) 来自解码器的上一层输出,键 (Key) 和值 (Value) 来自编码器的输出。
前馈神经网络、残差连接和规范化层(层归一化): 与编码器类似。
2.2.2 输入部分
源文本嵌入层及其位置编码器
目标文本嵌入层及其位置编码器
2.2.3 输出部分
线性层
softmax层
2.2.4 编码器部分
由N个编码器层堆叠而成
每个编码器层由两个子层连接结构组成
第一个子层连接结构包括一个多头自注意力子层和一个残差连接以及规范化层(层归一化)
第二个子层连接结构包括一个前馈全连接子层和一个残差连接以及规范化层(层归一化)
2.2.5 解码器部分
由N个解码器层堆叠而成
每个解码器层由三个子层连接结构组成
第一个子层连接结构包括一个多头自注意力子层和一个残差连接以及规范化层(层归一化)
第二个子层连接结构包括一个多头注意力子层和一个残差连接以及规范化层(层归一化)
第三个子层连接结构包括一个前馈全连接子层和一个残差连接以及规范化层(层归一化)
三、架构输入部分实现
3.1 输入部分介绍
输入部分包含:
源文本嵌入层及其位置编码器
目标文本嵌入层及其位置编码器
3.2 文本嵌入层
文本嵌入层(Text Embedding Layer)是深度学习模型中将文本(通常是单词或句子)转换为固定大小的向量表示的一个关键层。它的目标是将每个文本单元(如单词或子词)映射到一个高维空间中,以便模型能够更好地捕捉到词汇的语义信息和语法信息。
无论是源文本嵌入还是目标文本嵌入,都是为了将文本中词汇的数字表示转变为向量表示, 希望在这样的高维空间捕捉词汇间的关系。常见的词嵌入方法包括:Word2Vec, GloVe, FastText, 以及可学习的embedding层。
nn.Embedding演示:
embedding = nn.Embedding(num_embeddings=10, embedding_dim=3) input = torch.LongTensor([[1, 2, 4, 5], [4, 3, 2, 9]]) print(embedding(input)) # padding_idx: 指定用于填充的索引。如果设置为0,则索引为0的输入将始终映射到一个全零向量,并且在反向传播时不会更新该嵌入。 # 全零向量:padding_idx 指定的索引(如0)会被映射到一个全零向量。 # 不更新梯度:在训练过程中,padding_idx 对应的嵌入向量不会被更新。 # 用途:常用于处理变长序列的填充部分,避免填充部分对模型训练产生影响。 embedding = nn.Embedding(num_embeddings=10, embedding_dim=3, padding_idx=0) input = torch.LongTensor([[0, 2, 0, 5]]) print(embedding(input))
输出结果:
tensor([[[-1.0378, 0.0594, 2.6601], [ 1.0423, -0.4094, 0.3436], [-1.8989, 1.3664, -0.3701], [ 0.3930, 0.9908, 1.5700]], [[-1.8989, 1.3664, -0.3701], [ 0.3479, -0.2118, -0.1244], [ 1.0423, -0.4094, 0.3436], [ 0.4161, 0.4799, -0.4094]]], grad_fn=<EmbeddingBackward0>) tensor([[[ 0.0000, 0.0000, 0.0000], [-0.3378, 1.1013, -1.7552], [ 0.0000, 0.0000, 0.0000], [ 0.9153, 0.3548, 2.1857]]], grad_fn=<EmbeddingBackward0>)
文本嵌入层的代码实现:
# 导入必备的工具包 import torch # 预定义的网络层torch.nn, 工具开发者已经帮助我们开发好的一些常用层, # 比如,卷积层, lstm层, embedding层等, 不需要我们再重新造轮子. import torch.nn as nn # 数学计算工具包 import math # Embeddings类 实现思路分析 # 1 init函数 (self, d_model, vocab) # 设置类属性 定义词嵌入层 self.lut层 # 2 forward(x)函数 # self.lut(x) * math.sqrt(self.d_model) class Embeddings(nn.Module): def __init__(self, vocab, d_model): # 参数vocab 词汇表大小 # 参数d_model 每个词汇的特征尺寸 词嵌入维度 super(Embeddings, self).__init__() self.vocab = vocab self.d_model = d_model # 定义词嵌入层 self.embed = nn.Embedding(self.vocab, self.d_model) def forward(self, x): # 将x传给self.embed并与根号下self.d_model相乘作为结果返回 # 词嵌入层的权重通常初始化较小(如均匀分布在[-0.1, 0.1]), 导致嵌入后的向量幅度较小。 # x经过词嵌入后乘以sqrt(d_model)来增大x的值, 与位置编码信息值量纲[-1,1]差不多, 确保两者相加时信息平衡。 return self.embed(x) * math.sqrt(self.d_model)
调用:
def dm_test_Embeddings(): vocab = 1000 # 词表大小是1000 d_model = 512 # 词嵌入维度是512维 # 实例化词嵌入层 my_embeddings = Embeddings(vocab, d_model) x = Variable(torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]])) embed = my_embeddings(x) print('embed.shape', embed.shape, '\nembed--->\n', embed) if __name__ == '__main__': dm_test_Embeddings()
输出结果:
embed.shape torch.Size([2, 4, 512]) embed---> tensor([[[ -0.9264, -38.2338, 18.5574, ..., 36.3375, -20.2969, 6.8429], [ 14.1119, -5.2766, 1.8418, ..., 20.2530, -21.8190, -0.3868], [-28.7141, 15.6517, 7.0343, ..., -24.7212, 11.7169, 9.1018], [ 25.7579, 23.5704, 6.5489, ..., 2.7449, -0.5301, 15.3012]], [[-18.3689, -23.4577, -10.3820, ..., 6.2848, -3.8342, -2.2106], [ 41.2917, 26.6871, 21.3547, ..., -21.6043, -14.3414, -5.8511], [-42.9315, -6.7251, -3.0975, ..., -8.0635, -37.7340, 4.6406], [ -1.2356, 22.7560, -28.8711, ..., 56.8816, -12.9638, 0.5119]]], grad_fn=<MulBackward0>)
3.3 位置编码器
位置编码器(Positional Encoding)是Transformer模型中的一个重要组成部分,用于在模型中引入序列中各个元素的位置或顺序信息。因为在Transformer的编码器结构中, 并没有RNN那样针对词汇位置信息的处理能力,它无法直接感知输入数据中的顺序关系。因此需要在Embedding层后加入位置编码器,将词汇位置不同可能会产生不同语义的信息加入到词嵌入张量中, 以弥补位置信息的缺失。
位置编码器将序列位置(例如,单词在句子中的位置)转换为一组向量,这些向量会与输入的词嵌入(word embeddings)相加,使模型能够在处理每个词时,理解其在序列中的位置。
位置编码通常使用正弦和余弦函数生成,使得模型可以区分不同位置的token。
周期性
正弦和余弦函数是周期性的,它们的周期是2π,可以自然地表示位置之间的相对关系。
对于两个位置pos和pos+k,它们的位置编码可以通过正弦和余弦函数的周期性关系直接关联起来。
例如“位置pos+k是位置pos的下一个位置”。它们的位置编码之间存在一种线性关系:
其中Mk是一个与位置偏移k相关的线性变换矩阵,这种关联性使得模型能够轻松地学习到位置之间的相对关系。
在句子“The cat sat on the mat”中,单词“sat”与“cat”之间的关系(相邻)比它们的绝对位置(第2个和第3个词)更重要。
平滑性
正弦和余弦函数是平滑的,能够捕捉到位置的连续变化。
在正弦和余弦函数中,斜率也是连续变化的,函数的导数(即变化的速率)在所有地方都存在并且是平滑的。
有界性
正弦和余弦函数的值域在
[-1, 1]
之间,避免了数值爆炸问题。输出范围可控,位置编码的值不会过大或过小,避免了数值不稳定性。
在Transformer中,位置编码的公式是:
其中:
$$pos$$ 是位置
$$i$$ 是维度索引
$$d_{model}$$ 是模型的维度
位置编码器的代码实现:
# 位置编码器类PositionalEncoding 实现思路分析 # 1 init函数 (self, d_model, dropout, max_len=5000) # super()函数 定义层self.dropout # 定义位置编码矩阵pe 定义位置列-矩阵position 定义变化矩阵div_term # 套公式div_term = torch.exp(-torch.arange(0, d_model, 2) / d_model * math.log(10000.0)) # 位置列-矩阵 * 变化矩阵 阿达码积my_matmulres # 给pe矩阵偶数列奇数列赋值 pe[:, 0::2] pe[:, 1::2] # pe矩阵注册到模型缓冲区 pe.unsqueeze(0)三维 self.register_buffer('pe', pe) # 2 forward(self, x) 返回self.dropout(x) # 给x数据添加位置特征信息 x = x + self.pe[:,:x.shape[1]] class PositionalEncoding(nn.Module): def __init__(self, d_model, dropout, max_len=5000): # 参数d_model 词嵌入维度 eg: 512个特征 # 参数max_len 单词token个数 eg: 60个单词 super(PositionalEncoding, self).__init__() # 定义dropout层 self.dropout = nn.Dropout(p=dropout) # 思路:位置编码矩阵 + 特征矩阵 相当于给特征增加了位置信息 # 定义位置编码矩阵PE eg pe[60, 512], 位置编码矩阵和特征矩阵形状是一样的 pe = torch.zeros(max_len, d_model) # 定义位置列-矩阵position 数据形状[max_len,1] eg: [0,1,2,3,4...60]^T position = torch.arange(0, max_len).unsqueeze(1) # print('position--->', position.shape, position) # 方式一计算 # _2i = torch.arange(0, d_model, step=2).float() # pe[:, 0::2] = torch.sin(position / 10000 ** (_2i / d_model)) # pe[:, 1::2] = torch.cos(position / 10000 ** (_2i / d_model)) # 方式二计算 # 定义变化矩阵div_term [1,256] # torch.arange(start=1, end=512, 2)结果并不包含end。在start和end之间做一个等差数组 [0, 2, 4, 6 ... 510] # math.log(10000.0)对常数10000取自然对数 # torch.exp()指数运算 div_term = torch.exp(-torch.arange(0, d_model, 2) / d_model * math.log(10000.0)) # 位置列-矩阵 @ 变化矩阵 做矩阵运算 [60*1]@ [1*256] ==> 60 *256 # 矩阵相乘也就是行列对应位置相乘再相加,其含义,给每一个列属性(列特征)增加位置编码信息 my_matmulres = position * div_term # print('my_matmulres--->', my_matmulres.shape, my_matmulres) # 给位置编码矩阵奇数列,赋值sin曲线特征 pe[:, 0::2] = torch.sin(my_matmulres) # 给位置编码矩阵偶数列,赋值cos曲线特征 pe[:, 1::2] = torch.cos(my_matmulres) # 形状变化 [60,512]-->[1,60,512] pe = pe.unsqueeze(0) # 把pe位置编码矩阵 注册成模型的持久缓冲区buffer; 模型保存再加载时,可以根模型参数一样,一同被加载 # 什么是buffer: 对模型效果有帮助的,但是却不是模型结构中超参数或者参数,不参与模型训练 self.register_buffer('pe', pe) def forward(self, x): # 注意:输入的x形状2*4*512 pe形状1*60*512 如何进行相加 # 只需按照x的单词个数 给特征增加位置信息 x = x + self.pe[:, :x.shape[1]] return self.dropout(x)
调用:
ef dm_test_PositionalEncoding(): vocab = 1000 # 词表大小是1000 d_model = 512 # 词嵌入维度是512维 # 1 实例化词嵌入层 my_embeddings = Embeddings(vocab, d_model) # 2 让数据经过词嵌入层 [2,4] --->[2,4,512] x = Variable(torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]])) embed = my_embeddings(x) # print('embed--->', embed.shape) # 3 创建pe位置矩阵 生成位置特征数据[1,60,512] my_pe = PositionalEncoding(d_model=d_model, dropout=0.1, max_len=60) # 4 给词嵌入数据embed 添加位置特征 [2,4,512] ---> [2,4,512] pe_result = my_pe(embed) print('pe_result.shape--->', pe_result.shape) print('pe_result--->', pe_result) if __name__ == '__main__': dm_test_PositionalEncoding()
输出结果:
pe_result.shape---> torch.Size([2, 4, 512]) pe_result---> tensor([[[-31.7233, 7.1880, -35.8665, ..., -0.0000, -8.7751, -39.0546], [ -8.1961, -5.8707, 6.6460, ..., 41.9253, -10.8338, -0.0000], [ 13.6579, 11.7870, 39.9218, ..., 0.0000, 10.1335, 11.4966], [-10.0956, -4.7150, -27.5220, ..., -7.3646, -40.9978, 13.9090]], [[-31.8694, 33.0375, 15.3635, ..., 14.1677, 13.7319, -5.1402], [ 14.0821, 26.6213, -6.8929, ..., -6.5874, 1.0178, 9.6971], [-27.0425, 25.3489, 27.7457, ..., 0.0000, 10.3564, 1.5655], [-21.3439, -12.2684, -56.1276, ..., 13.8567, 0.0000, -0.0000]]], grad_fn=<MulBackward0>)
绘制词汇向量中特征的分布曲线:
# 绘制PE位置特征sin-cos曲线 def dm_draw_PE_feature(): # 1 创建pe位置矩阵[1,5000,20],每一列数值信息:奇数列sin曲线 偶数列cos曲线 my_pe = PositionalEncoding(d_model=20, dropout=0) print('my_positionalencoding.shape--->', my_pe.pe.shape) # 2 创建数据x[1,100,20], 给数据x添加位置特征 [1,100,20] ---> [1,100,20] y = my_pe(Variable(torch.zeros(1, 100, 20))) print('y--->', y.shape) # 3 画图 绘制pe位置矩阵的第4-7列特征曲线 plt.figure(figsize=(20, 20)) # 第0个句子的,所有单词的,绘制4到8维度的特征 看看sin-cos曲线变化 plt.plot(np.arange(100), y[0, :, 4:8].numpy()) plt.legend(["dim %d" % p for p in [4, 5, 6, 7]]) plt.show() # print('直接查看pe数据形状--->', my_pe.pe.shape) # [1,5000,20] # 直接绘制pe数据也是ok # plt.figure(figsize=(20, 20)) # # 第0个句子的,所有单词的,绘制4到8维度的特征 看看sin-cos曲线变化 # plt.plot(np.arange(100), my_pe.pe[0,0:100, 4:8]) # plt.legend(["dim %d" %p for p in [4,5,6,7]]) # plt.show() if __name__ == '__main__': dm_draw_PE_feature()
输出结果:
效果分析:
每条颜色的曲线代表某一个词汇中的特征在不同位置的含义
保证同一词汇随着所在位置不同它对应位置嵌入向量会发生变化
正弦波和余弦波的值域范围都是1到-1这又很好的控制了嵌入数值的大小, 有助于梯度的快速计算
四、编码器部分实现
4.1 编码器介绍
由N个编码器层堆叠而成,每个编码器层由两个子层连接结构组成。
第一个子层连接结构包括一个多头自注意力子层和一个残差连接以及规范化层(层归一化)。
第二个子层连接结构包括一个前馈全连接子层和一个残差连接以及规范化层(层归一化)。
4.2 掩码张量
4.2.1 掩码张量介绍
掩码张量(Mask Tensor)是一种用于控制模型对输入数据的访问或处理方式的工具。它在深度学习中广泛应用,尤其是在处理变长序列(如自然语言处理中的句子)或需要忽略某些数据时。掩码张量通常是一个二进制张量(值为0或1),用于指示哪些位置是有效的,哪些位置是无效的(需要被忽略)。
掩代表遮掩,码就是我们张量中的数值,它的尺寸不定,里面一般只有1和0的元素,代表位置被遮掩或者不被遮掩,至于是0位置被遮掩还是1位置被遮掩可以自定义,因此它的作用就是让另外一个张量中的一些数值被遮掩,也可以说被替换, 它的表现形式是一个张量。
4.2.2 掩码张量作用
忽略填充部分:在处理变长序列时,通常会用填充(padding)将序列补齐到相同长度。掩码可以告诉模型忽略这些填充部分。
防止未来信息泄露:在解码器中,掩码可以防止模型在生成当前词时访问未来的信息。
选择性处理:在注意力机制中,掩码可以控制哪些位置可以参与计算。
在transformer中, 掩码张量主要应用在attention(将在下一小节讲解)时,有一些生成的attention张量中的值计算时有可能已知了未来信息而得到的,未来信息被看到是因为训练时会把整个输出结果都一次性进行Embedding,但是理论上解码器的输出却不是一次就能产生最终结果的,而是一次次通过上一次结果综合得出的,因此未来的信息可能被提前利用,所以我们会进行遮掩。关于解码器的有关知识将在后面的章节中讲解。
4.2.3 生成掩码张量代码实现
上三角掩码 (Causal Mask): 用于遮盖未来位置,确保自回归模型的正确性。
上三角矩阵和np.triu函数演示
上三角:全0三角方向朝上
# 上三角矩阵:下面矩阵中0组成的形状为上三角矩阵 ''' [[[0. 1. 1. 1. 1.] [0. 0. 1. 1. 1.] [0. 0. 0. 1. 1.] [0. 0. 0. 0. 1.] [0. 0. 0. 0. 0.]]] nn.triu()函数功能介绍 def triu(m, k) m:表示一个矩阵 K:表示对角线的起始位置(k取值默认为0) return: 返回函数的上三角矩阵 ''' import numpy as np import torch import matplotlib.pyplot as plt def dm_test_nptriu(): # 测试产生上三角矩阵 # k>0->对角线往上移 print(np.triu([[1, 1, 1, 1, 1], [2, 2, 2, 2, 2], [3, 3, 3, 3, 3], [4, 4, 4, 4, 4], [5, 5, 5, 5, 5]], k=1)) # 默认k=0 print(np.triu([[1, 1, 1, 1, 1], [2, 2, 2, 2, 2], [3, 3, 3, 3, 3], [4, 4, 4, 4, 4], [5, 5, 5, 5, 5]], k=0)) # k<0->对角线往下移 print(np.triu([[1, 1, 1, 1, 1], [2, 2, 2, 2, 2], [3, 3, 3, 3, 3], [4, 4, 4, 4, 4], [5, 5, 5, 5, 5]], k=-1))
输出结果:
[[0 1 1 1 1] [0 0 2 2 2] [0 0 0 3 3] [0 0 0 0 4] [0 0 0 0 0]] [[1 1 1 1 1] [0 2 2 2 2] [0 0 3 3 3] [0 0 0 4 4] [0 0 0 0 5]] [[1 1 1 1 1] [2 2 2 2 2] [0 3 3 3 3] [0 0 4 4 4] [0 0 0 5 5]]
生成掩码函数
# 下三角:全0三角朝下 # 下三角矩阵作用: 生成字符时,希望模型不要使用当前字符和后面的字符。 # 使用遮掩mask,防止未来的信息可能被提前利用 # 实现方法: 1-上三角矩阵 # 函数 subsequent_mask 实现分析 # 产生上三角矩阵 np.triu(m=np.ones((1, size, size)), k=1).astype('uint8') # 返回下三角矩阵 torch.from_numpy(1 - my_mask) def subsequent_mask(size): # 产生上三角矩阵 产生一个方阵 subsequent_mask = np.triu(np.ones((1, size, size)), k=1).astype('uint8') # 返回下三角矩阵 return torch.from_numpy(1 - subsequent_mask) if __name__ == '__main__': # 产生5*5的下三角矩阵 size = 5 sm = subsequent_mask(size) print('下三角矩阵--->\n', sm)
输出结果:
下三角矩阵---> tensor([[[1, 0, 0, 0, 0], [1, 1, 0, 0, 0], [1, 1, 1, 0, 0], [1, 1, 1, 1, 0], [1, 1, 1, 1, 1]]], dtype=torch.uint8)
4.2.4 掩码张量可视化
plt.figure(figsize=(5,5)) # subsequent_mask(20)->20*20 # subsequent_mask(20)[0]->获取第1个样本 plt.imshow(subsequent_mask(20)[0]) plt.show()
输出结果:
效果分析:
通过观察可视化方阵, 黄色是1的部分, 这里代表被遮掩, 紫色代表没有被遮掩的信息, 横坐标代表目标词汇的位置, 纵坐标代表可查看的位置。
我们看到,在0的位置我们一看望过去都是黄色的,都被遮住了,1的位置一眼望过去还是黄色,说明第一次词还没有产生,从第二个位置看过去,就能看到位置1的词,其他位置看不到,以此类推。
4.2.5 小结
什么是掩码张量:
掩代表遮掩,码就是我们张量中的数值,它的尺寸不定,里面一般只有1和0的元素,代表位置被遮掩或者不被遮掩,至于是0位置被遮掩还是1位置被遮掩可以自定义,因此它的作用就是让另外一个张量中的一些数值被遮掩, 也可以说被替换, 它的表现形式是一个张量.
掩码张量的作用:
在transformer中, 掩码张量的主要作用在应用attention(将在下一小节讲解)时,有一些生成的attetion张量中的值计算有可能已知量未来信息而得到的,未来信息被看到是因为训练时会把整个输出结果都一次性进行Embedding,但是理论上解码器的的输出却不是一次就能产生最终结果的,而是一次次通过上一次结果综合得出的,因此,未来的信息可能被提前利用. 所以,我们会进行遮掩. 关于解码器的有关知识将在后面的章节中讲解.
实现了生成向后遮掩的掩码张量函数: subsequent_mask
它的输入是size, 代表掩码张量的大小.
它的输出是一个最后两维形成1方阵的下三角阵.
最后对生成的掩码张量进行了可视化分析, 更深一步理解了它的用途.
4.3 注意力机制
我们这里使用的注意力计算规则:
4.3.1 注意力计算规则的代码实现
import torch import torch.nn as nn import math from torch.autograd import Variable # 自注意力机制函数attention 实现思路分析 # attention(query, key, value, mask=None, dropout=None) # 1 求查询张量特征尺寸大小 d_k # 2 求查询张量q的权重分布socres q@k^T /math.sqrt(d_k) # 形状[2,4,512] @ [2,512,4] --->[2,4,4] # 3 是否对权重分布scores进行 scores.masked_fill(mask == 0, -1e9) # 4 求查询张量q的权重分布 p_attn F.softmax() # 5 是否对p_attn进行dropout if dropout is not None: # 6 求查询张量q的注意力结果表示 [2,4,4]@[2,4,512] --->[2,4,512] # 7 返回q的注意力结果表示 q的权重分布 def attention(query, key, value, mask=None, dropout=None): # query, key, value:代表注意力的三个输入张量 # mask:代表掩码张量 # dropout:传入的dropout实例化对象 # 1 求查询张量特征尺寸大小 d_k = query.size()[-1] # 2 求查询张量q的权重分布socres q@k^T /math.sqrt(d_k) # [2,4,512] @ [2,512,4] --->[2,4,4] scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k) # 3 是否对权重分布scores 进行 masked_fill if mask is not None: # 根据mask矩阵0的位置 对sorces矩阵对应位置进行掩码 # mask:一个布尔张量(True 或 False) # value:在自然语言处理中,将填充部分的注意力分数设置为一个极小的值(如 -1e9),使其在 Softmax 后接近 0。 scores = scores.masked_fill(mask=(mask == 0), value=-1e9) # 4 求查询张量q的权重分布 softmax p_attn = torch.softmax(scores, dim=-1) # 5 是否对p_attn进行dropout if dropout is not None: p_attn = dropout(p_attn) # 返回 查询张量q的注意力结果表示 bmm-matmul运算, 注意力查询张量q的权重分布p_attn # [2,4,4]*[2,4,512] --->[2,4,512] return torch.matmul(p_attn, value), p_attn
tensor.masked_fill演示:
input = Variable(torch.randn(5, 5)) print('input--->', input) mask = Variable(torch.zeros(5, 5)) print('mask--->', mask) # value:在自然语言处理中,将填充部分的注意力分数设置为一个极小的值(如 -1e9),使其在 Softmax 后接近 0。 masked_result = input.masked_fill(mask=(mask == 0), value=-1e9) print('masked_result--->', masked_result)
输出结果:
input---> tensor([[-1.4726, -1.4558, -0.4536, 1.4769, -0.4171], [ 2.6026, -0.4664, 0.7003, -0.1138, 0.7594], [-2.3509, 0.7317, 0.3886, 2.0829, -0.3056], [-1.2629, -0.4168, 0.9880, -1.6269, 0.6392], [ 1.3450, 0.3062, -0.9918, -0.3646, -0.8079]]) mask---> tensor([[0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.]]) masked_result---> tensor([[-1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09], [-1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09], [-1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09], [-1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09], [-1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09]])
调用注意力函数
def dm_test_attention(): vocab = 1000 # 词表大小是1000 d_model = 512 # 词嵌入维度是512维 # 输入x 是一个使用Variable封装的长整型张量, 形状是2 x 4 x = Variable(torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]])) # 输入部分的Embeddings类 my_embeddings = Embeddings(vocab, d_model) x = my_embeddings(x) dropout = 0.1 # 置0比率为0.1 max_len = 60 # 句子最大长度 # 输入部分的PositionalEncoding类 my_pe = PositionalEncoding(d_model, dropout, max_len) pe_result = my_pe(x) query = key = value = pe_result # torch.Size([2, 4, 512]) print('编码阶段 对注意力权重分布 不做掩码') attn1, p_attn1 = attention(query, key, value) print('注意力权重 p_attn1--->', p_attn1.shape, '\n', p_attn1) # torch.Size([2, 4, 4]) print('注意力表示结果 attn1--->', attn1.shape, '\n', attn1) # torch.Size([2, 4, 512]) # print('*' * 50) # print('编码阶段 对注意力权重分布 做掩码') # mask = Variable(torch.zeros(2, 4, 4)) # attn2, p_attn2 = attention(query, key, value, mask=mask) # print("注意力权重 p_attn2--->", p_attn2.shape, '\n', p_attn2) # print("注意力表示结果 attn2--->", attn2.shape, '\n', attn2) if __name__ == '__main__': dm_test_attention()
对注意力权重分布不做掩码:
编码阶段 对注意力权重分布 不做掩码 scores---> tensor([[[12929.8975, -768.2662, -227.1127, -429.6189], [ -768.2662, 13351.5928, 1842.8860, -54.2066], [ -227.1127, 1842.8860, 13930.0576, -57.5323], [ -429.6189, -54.2066, -57.5323, 12392.1836]], [[13130.8389, 814.8187, 789.8242, -1144.2612], [ 814.8187, 13931.6807, -523.0581, 848.3032], [ 789.8242, -523.0581, 13404.6865, 41.4354], [-1144.2612, 848.3032, 41.4354, 12641.4355]]], grad_fn=<DivBackward0>) 注意力权重 p_attn1---> torch.Size([2, 4, 4]) tensor([[[1., 0., 0., 0.], [0., 1., 0., 0.], [0., 0., 1., 0.], [0., 0., 0., 1.]], [[1., 0., 0., 0.], [0., 1., 0., 0.], [0., 0., 1., 0.], [0., 0., 0., 1.]]], grad_fn=<SoftmaxBackward0>) 注意力表示结果 attn1---> torch.Size([2, 4, 512]) tensor([[[-34.1762, -31.8242, 0.0000, ..., 8.7288, 31.3358, 23.9411], [ 7.3822, -13.1088, 17.1596, ..., 14.3473, 0.0000, -34.8241], [-22.5616, -12.6677, -6.0994, ..., -1.1240, -14.5099, 4.0953], [-30.2166, -32.1527, 8.3162, ..., -10.3806, 26.4887, -4.8087]], [[-36.1426, 51.5065, 30.8935, ..., -21.3317, -2.9736, 29.0986], [ 39.5471, 17.5239, 16.6272, ..., -10.5076, -40.5844, 42.3104], [ -7.1572, 24.7332, -5.7353, ..., 19.7273, -7.6412, 29.2498], [ 0.2364, -14.2803, -7.0324, ..., -10.5994, 45.9028, -11.9923]]], grad_fn=<UnsafeViewBackward0>)
4.3.2 带有mask的输入参数
def dm_test_attention(): vocab = 1000 # 词表大小是1000 d_model = 512 # 词嵌入维度是512维 # 输入x 是一个使用Variable封装的长整型张量, 形状是2 x 4 x = Variable(torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]])) # 输入部分的Embeddings类 my_embeddings = Embeddings(vocab, d_model) x = my_embeddings(x) dropout = 0.1 # 置0比率为0.1 max_len = 60 # 句子最大长度 # 输入部分的PositionalEncoding类 my_pe = PositionalEncoding(d_model, dropout, max_len) pe_result = my_pe(x) query = key = value = pe_result # torch.Size([2, 4, 512]) print('编码阶段 对注意力权重分布 做掩码') mask = Variable(torch.zeros(2, 4, 4)) attn2, p_attn2 = attention(query, key, value, mask=mask) print("注意力权重 p_attn2--->", p_attn2.shape, '\n', p_attn2) print("注意力表示结果 attn2--->", attn2.shape, '\n', attn2) if __name__ == '__main__': dm_test_attention()
带有mask的输出效果:
编码阶段 对注意力权重分布 做掩码 scores---> tensor([[[-1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09], [-1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09], [-1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09], [-1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09]], [[-1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09], [-1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09], [-1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09], [-1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09]]], grad_fn=<MaskedFillBackward0>) 注意力权重 p_attn2---> torch.Size([2, 4, 4]) tensor([[[0.2500, 0.2500, 0.2500, 0.2500], [0.2500, 0.2500, 0.2500, 0.2500], [0.2500, 0.2500, 0.2500, 0.2500], [0.2500, 0.2500, 0.2500, 0.2500]], [[0.2500, 0.2500, 0.2500, 0.2500], [0.2500, 0.2500, 0.2500, 0.2500], [0.2500, 0.2500, 0.2500, 0.2500], [0.2500, 0.2500, 0.2500, 0.2500]]], grad_fn=<SoftmaxBackward0>) 注意力表示结果 attn2---> torch.Size([2, 4, 512]) tensor([[[-19.8931, -22.4384, 4.8441, ..., 2.8929, 10.8286, -2.8991], [-19.8931, -22.4384, 4.8441, ..., 2.8929, 10.8286, -2.8991], [-19.8931, -22.4384, 4.8441, ..., 2.8929, 10.8286, -2.8991], [-19.8931, -22.4384, 4.8441, ..., 2.8929, 10.8286, -2.8991]], [[ -0.8791, 19.8708, 8.6883, ..., -5.6779, -1.3241, 22.1666], [ -0.8791, 19.8708, 8.6883, ..., -5.6779, -1.3241, 22.1666], [ -0.8791, 19.8708, 8.6883, ..., -5.6779, -1.3241, 22.1666], [ -0.8791, 19.8708, 8.6883, ..., -5.6779, -1.3241, 22.1666]]], grad_fn=<UnsafeViewBackward0>)
4.3.3 小结
学习并实现了注意力计算规则的函数: attention
它的输入就是Q,K,V以及mask和dropout, mask用于掩码, dropout用于随机置0。
它的输出有两个, query的注意力表示以及注意力张量。
4.4 多头注意力机制
4.4.1 概念
多头注意力机制(Multi-Head Attention)是Transformer模型的核心组件之一,用于捕捉输入序列中不同位置之间的依赖关系。它通过并行计算多个注意力头(Attention Heads),从不同的子空间中提取信息,从而增强模型的表达能力。
多头注意力机制的关键在于同时使用多个注意力头,每个注意力头学习不同的子空间(不同的“视角”),从而可以在不同的子空间中获取输入序列之间的不同关系。在所有注意力头计算完成后,它们的结果会被合并,以此产生更丰富的表示。
在多头自注意力机制中,输入句子中的每个词的词嵌入向量会被分割成多个头 (head)。 假设有h个头,那么每个头获得的向量维度就是 d_model / h。这种分割只发生在词嵌入向量的最后一维 (即 d_model 维度) 上。词嵌入层张量形状->(batch_size, seq_len, d_model)
多头注意力机制通过并行计算多个注意力头,从不同的子空间中提取信息,然后将结果拼接起来。具体步骤如下:
步骤1:线性变换
对输入进行线性变换,生成多个头的查询、键和值:
$$Q_i=QW_i^Q,K_i=KW_i^K,V_i=VW_i^V$$
其中:
$$W_i^Q,W_i^K,W_i^V$$ 是第$$i$$个头的可学习权重矩阵。
$$Q,K,V$$是输入序列的查询、键和值。
步骤2:计算注意力
对每个头分别计算注意力:
$$head_i=Attention(Q_i,K_i,V_i)$$
步骤3:拼接多头结果
将所有头的输出拼接起来:
$$MultiHead(Q,K,V)=Concat(head_1,head_2,…,head_h)W^O$$
其中:
$$h$$是注意力头的数量。
$$W^O$$是输出的可学习权重矩阵。
4.4.2 结构图
从多头注意力的结构图中,貌似这个所谓的多个头就是指多组线性变换层。其实并不是,图中只使用了一组线性变化层,即三个变换张量对Q,K,V分别进行线性变换,这些变换不会改变原有张量的尺寸,因此每个变换矩阵都是方阵,得到输出结果后,多头的作用才开始显现,每个头开始从词义层面分割输出的张量,也就是每个头都想获得一组Q,K,V进行注意力机制的计算(句子中的每个词的表示的一部分),也就是只分割了最后一维的词嵌入向量(batch_size, seq_len, d_model)。这就是所谓的多头,将每个头的获得的输入送到注意力机制中, 就形成多头注意力机制。
4.4.3 作用
这种结构设计能让每个注意力机制去优化每个词汇的不同特征部分,从而均衡同一种注意力机制可能产生的偏差,让词义拥有来自更多元的表达,实验表明可以从而提升模型效果.
捕捉多种关系:每个头能够从不同的子空间捕捉信息,允许模型同时关注序列中不同的方面。不同的头可以学习序列中不同的相似性或关联性。
提高模型表达能力:多头注意力机制使得模型能够并行地从多个角度理解数据,从而提高模型的表达能力。
并行化计算:由于每个头的计算是独立的,可以并行处理,提升计算效率。
4.4.4 代码实现
import copy # 多头注意力机制类 MultiHeadedAttention 实现思路分析 # 1 init函数 (self, head, embedding_dim, dropout=0.1) # 每个头特征尺寸大小self.d_k 多少个头self.head 线性层列表self.linears # self.linears = clones(nn.Linear(embedding_dim, embedding_dim), 4) # 注意力权重分布self.attn=None dropout层self.dropout # 2 forward(self, query, key, value, mask=None) # 2-1 掩码增加一个维度[8,4,4] -->[1,8,4,4] 求多少批次batch_size # 2-2 数据经过线性层 切成8个头,view(batch_size, -1, self.head, self.d_k), transpose(1,2)数据形状变化 # 数据形状变化[2,4,512] ---> [2,4,8,64] ---> [2,8,4,64] # 2-3 24个头 一起送入到attention函数中求 x, self.attn # attention([2,8,4,64],[2,8,4,64],[2,8,4,64],[1,8,4,4]) ==> x[2,8,4,64], self.attn[2,8,4,4]] # 2-4 数据形状再变化回来 x.transpose(1,2).contiguous().view(,,) # 数据形状变化 [2,8,4,64] ---> [2,4,8,64] ---> [2,4,512] # 2-5 返回最后线性层结果 return self.linears[-1](x) # 深度copy模型 输入模型对象和copy的个数 存储到模型列表中 def clones(module, N): return nn.ModuleList([copy.deepcopy(module) for _ in range(N)]) class MultiHeadedAttention(nn.Module): def __init__(self, head, embedding_dim, dropout=0.1): super(MultiHeadedAttention, self).__init__() # 确认数据特征能否被被整除 eg 特征尺寸256 % 头数8 assert embedding_dim % head == 0 # 计算每个头特征尺寸 特征尺寸256 // 头数8 = 64 self.d_k = embedding_dim // head # 多少头数 self.head = head # 四个线性层 self.linears = clones(nn.Linear(embedding_dim, embedding_dim), 4) # 注意力权重分布 self.attn = None # dropout层 self.dropout = nn.Dropout(p = dropout) def forward(self, query, key, value, mask=None): # 若使用掩码,则掩码增加一个维度[8,4,4] -->[1,8,4,4] if mask is not None: mask = mask.unsqueeze(0) # 求数据多少行 eg:[2,4,512] 则batch_size=2 batch_size = query.size()[0] # 数据形状变化[2,4,512] ---> [2,4,8,64] ---> [2,8,4,64] # 4代表4个单词 8代表8个头 让句子长度4和句子特征64靠在一起 更有利捕捉句子特征 query, key, value = [model(x).view(batch_size, -1, self.head, self.d_k).transpose(1,2) for model, x in zip(self.linears, (query, key, value) ) ] # myoutptlist_data = [] # for model, x in zip(self.linears, (query, key, value)): # print('x--->', x.shape) # [2,4,512] # myoutput = model(x) # print('myoutput--->', myoutput.shape) # [2,4,512] # # [2,4,512] --> [2,4,8,64] --> [2,8,4,64] # tmpmyoutput = myoutput.view(batch_size, -1, self.head, self.d_k).transpose(1, 2) # myoutptlist_data.append( tmpmyoutput ) # mylen = len(myoutptlist_data) # mylen:3 # query = myoutptlist_data[0] # [2,8,4,64] # key = myoutptlist_data[1] # [2,8,4,64] # value = myoutptlist_data[2] # [2,8,4,64] # attention()->4.3章节注意力机制函数 # 注意力结果表示x形状 [2,8,4,64] 注意力权重attn形状:[2,8,4,4] # attention([2,8,4,64],[2,8,4,64],[2,8,4,64],[1,8,4,4]) ==> x[2,8,4,64], self.attn[2,8,4,4]] x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout) # 拼接多头 # 数据形状变化 [2,8,4,64] ---> [2,4,8,64] ---> [2,4,512] x = x.transpose(1,2).contiguous().view(batch_size, -1, self.head*self.d_k) # 返回最后变化后的结果 [2,4,512]---> [2,4,512] return self.linears[-1](x)
函数调用:
# 测试多头注意力机制 def dm_test_MultiHeadedAttention(): d_model = 512 # 词嵌入维度是512维 vocab = 1000 # 词表大小是1000 # 输入x 是一个使用Variable封装的长整型张量, 形状是2 x 4 x = Variable(torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]])) # 输入部分的Embeddings类 my_embeddings = Embeddings(d_model, vocab) x = my_embeddings(x) dropout = 0.1 # 置0比率为0.1 max_len = 60 # 句子最大长度 # 输入部分的PositionalEncoding类 my_pe = PositionalEncoding(d_model, dropout, max_len) pe_result = my_pe(x) head = 8 # 头数head query = key = value = pe_result # torch.Size([2, 4, 512]) # 输入的掩码张量mask mask = Variable(torch.zeros(8, 4, 4)) my_mha = MultiHeadedAttention(head, d_model, dropout) x = my_mha(query, key, value, mask) print('多头注意机制后的x', x.shape, '\n', x) print('多头注意力机制的注意力权重分布', my_mha.attn.shape) if __name__ == '__main__': dm_test_MultiHeadedAttention()
输出结果:
多头注意机制后的x torch.Size([2, 4, 512]) tensor([[[-1.5029, 2.2905, 3.5975, ..., 2.2099, -5.0509, -3.9762], [ 0.9588, 0.5078, 0.2165, ..., 6.0500, -0.1836, -1.5780], [-1.4579, 2.2605, 3.1544, ..., 5.5600, -4.3246, -3.8505], [-1.0983, 1.0788, 2.8548, ..., 4.6244, -3.7929, -3.1282]], [[-2.9288, -2.2267, 3.6888, ..., -0.5745, 5.1030, -6.2719], [-2.5670, 2.3732, 2.1863, ..., -5.6680, 3.0177, -1.9991], [-1.8909, -0.5001, 2.6733, ..., -2.8760, 4.8483, -5.3741], [-4.7026, -2.5351, 3.0038, ..., -0.5272, 3.3875, -5.8972]]], grad_fn=<ViewBackward0>) 多头注意力机制的注意力权重分布 torch.Size([2, 8, 4, 4])
4.4.5 小结
什么是多头注意力机制:
多头注意力机制(Multi-Head Attention)是 Transformer 模型的核心组件之一,用于捕捉输入序列中不同位置之间的依赖关系。它通过并行计算多个注意力头(Attention Heads),从不同的子空间中提取信息,从而增强模型的表达能力。
多头注意力机制的作用:
捕捉多种关系:每个头能够从不同的子空间捕捉信息,允许模型同时关注序列中不同的方面。不同的头可以学习序列中不同的相似性或关联性。
提高模型表达能力:多头注意力机制使得模型能够并行地从多个角度理解数据,从而提高模型的表达能力。
并行化计算:由于每个头的计算是独立的,可以并行处理,提升计算效率。
实现多头注意力机制的类: MultiHeadedAttention
因为多头注意力机制中需要使用多个相同的线性层, 首先实现了克隆函数clones。
clones函数的输入是module,N,分别代表克隆的目标层,和克隆个数。
clones函数的输出是装有N个克隆层的Module列表。
接着实现MultiHeadedAttention类, 它的初始化函数输入是h, d_model, dropout分别代表头数,词嵌入维度和置零比率。
它的实例化对象输入是Q, K, V以及掩码张量mask。
它的实例化对象输出是通过多头注意力机制处理的Q的注意力表示。
4.5 前馈全连接层
4.5.1 介绍
前馈全连接层(Feed-Forward Neural Network, FFN)是Transformer模型中用于进一步处理输入数据的一个核心组件,位于多头注意力机制(Multi-Head Attention)后面。它主要用于对每个位置的表示进行非线性转换,以增强模型的表达能力。
在Transformer模型中,前馈全连接层通常由两个线性变换层(全连接层)和一个非线性激活函数组成,其结构可以用以下公式表示:
$$FFN(x)=ReLU(xW_1+b_1)W_2+b_2$$
其中:
$$x$$:输入特征(形状为
[batch_size, seq_len, d_model]
)。$$W_1,b_1$$:第一层的权重矩阵和偏置。
$$W_2,b_2$$:第二层的权重矩阵和偏置。
$$ReLU$$:激活函数(也可以是其他非线性函数,如$$GELU$$)。
参数说明:
第一层将输入从
d_model
维度映射到d_ff
维度(通常d_ff > d_model
)。第二层将
d_ff
维度映射回d_model
维度。
作用:
非线性变换:通过激活函数(如ReLU或GELU)对数据进行非线性转换,使得模型能够表示复杂的函数映射,增强模型的表达能力。
提升维度:前馈层将输入的维度
d_model
扩展到更大的维度d_ff
,再缩回d_model
。这种升维-降维的操作,允许模型在更高维的空间中进行更丰富的特征学习。位置独立性:前馈层是对每个位置的表示独立处理的,意味着它不会考虑序列中元素之间的位置关系,而是通过注意力机制来捕捉这些关系。因此,前馈层更专注于处理每个位置的特征变换。
4.5.2 代码实现
# 前馈全连接层PositionwiseFeedForward实现思路分析 # 1 init函数 (self, d_model, d_ff, dropout=0.1): # 定义线性层self.w1 self.w2, self.dropout层 # 2 forward(self, x) # 数据经过self.w1(x) -> F.relu() ->self.dropout() ->self.w2 返回 class PositionwiseFeedForward(nn.Module): def __init__(self, d_model, d_ff, dropout=0.1): # d_model 第1个线性层输入维度 # d_ff 第2个线性层输出维度 super(PositionwiseFeedForward, self).__init__() # 定义线性层w1 w2 dropout self.w1 = nn.Linear(d_model, d_ff) self.w2 = nn.Linear(d_ff, d_model) self.dropout = nn.Dropout(p= dropout) def forward(self, x): # 数据依次经过第1个线性层 relu激活层 dropout层,然后是第2个线性层 return self.w2(self.dropout(torch.relu(self.w1(x))))
ReLU函数公式: ReLU(x)=max(0, x)
ReLU函数图像:
函数调用:
def dm_test_PositionwiseFeedForward(): vocab = 1000 # 词表大小是1000 d_model = 512 # 词嵌入维度是512维 # 输入x 是一个使用Variable封装的长整型张量, 形状是2 x 4 x = Variable(torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]])) # 输入部分的Embeddings类 my_embeddings = Embeddings(vocab, d_model) x = my_embeddings(x) dropout = 0.1 # 置0比率为0.1 max_len = 60 # 句子最大长度 # 输入部分的PositionalEncoding类 my_pe = PositionalEncoding(d_model, dropout, max_len) pe_result = my_pe(x) head = 8 # 头数head query = key = value = pe_result # torch.Size([2, 4, 512]) # 输入的掩码张量mask mask = Variable(torch.zeros(8, 4, 4)) my_mha = MultiHeadedAttention(head, d_model, dropout) x = my_mha(query, key, value, mask) # 测试前馈全链接层 my_PFF = PositionwiseFeedForward(d_model=512, d_ff=64, dropout=0.1) ff_result = my_PFF(x) print('x--->', ff_result.shape, ff_result) if __name__ == '__main__': dm_test_PositionwiseFeedForward()
输出结果:
x---> torch.Size([2, 4, 512]) tensor([[[ 0.2622, 0.4398, -0.4772, ..., -0.1021, 0.9268, 2.1575], [ 0.4111, 1.1654, -0.5249, ..., -0.2609, 2.1317, 2.6406], [ 0.9710, 0.0747, -1.6504, ..., -0.9052, 1.3052, 3.3491], [ 0.5767, 1.7927, -1.0501, ..., 0.0282, 1.4282, 3.1193]], [[ 0.4097, -0.1773, -0.8848, ..., -0.8248, 2.7958, 0.9619], [ 0.1791, -1.0730, -0.8525, ..., -1.5877, 2.3985, 1.4295], [ 0.6742, 0.0753, -1.0631, ..., -2.4176, 2.1048, 0.5939], [-0.2708, -0.4281, -0.1506, ..., -0.9803, 1.8283, 1.0055]]], grad_fn=<ViewBackward0>)
4.5.3 小结
什么是前馈全连接层:
在Transformer模型中,前馈全连接层通常由两个线性变换层(全连接层)和一个非线性激活函数组成。
前馈全连接层的作用:
非线性变换: 非线性激活函数为模型引入了非线性能力,使其可以学习非线性关系。
提升维度:前馈层将输入的维度
d_model
扩展到更大的维度d_ff
,再缩回d_model
。这种升维-降维的操作,允许模型在更高维的空间中进行更丰富的特征学习,提取更高级的特征。位置独立性:每个位置的输入会通过相同的前馈全连接层,但每个位置的表示都经过独立的变换,这有助于模型学习位置相关的特征。
实现前馈全连接层的类: PositionwiseFeedForward
它的实例化参数为d_model, d_ff, dropout, 分别代表词嵌入维度, 线性变换维度, 和置零比率。
它的输入参数x, 表示上层的输出。
它的输出是经过两层线性网络变换的特征表示。
4.6 规范化层(层归一化)
4.6.1 介绍
概念
层归一化(Layer Normalization) 是一种用于提高深度神经网络训练稳定性和加速收敛的技术,广泛应用于现代深度学习模型中,尤其是在Transformer等序列建模网络中。它通过对每一层的输出进行归一化处理,来缓解梯度消失或爆炸的问题,并有助于模型在训练过程中更加稳定。
核心思想
层归一化的核心思想是对每个输入样本在每一层内部进行标准化。具体来说,它会将输入的特征按层(即按样本维度的层面)进行归一化,而不是像批归一化(Batch Normalization)那样按批次(即样本的层面)进行归一化。
层归一化的目标是确保每个神经网络层的输入分布具有一致的均值和方差,这样可以防止激活值过大或过小,导致梯度在反向传播时出现不稳定的情况。
公式
假设某一层的输入为一个向量$$x=[x_1, x_2, ..., x_d]$$,其中$$d$$是该层的特征维度。层归一化对该层的输入进行标准化的过程如下:
计算均值和方差
对输入向量$$x$$中的每个元素,计算其均值和方差:
均值: $$u = \frac{1}{d} \sum_{i=1}^{d} x_i$$
方差: $$\sigma^2 = \frac{1}{d} \sum_{i=1}^{d} (x_i - \mu)^2$$
标准化
然后,通过将每个元素减去均值并除以标准差来对输入进行标准化,得到标准化后的值$$\hat{x}_i$$:
$$\hat{x}_i = \frac{x_i - \mu}{\sqrt{\sigma^2 + \epsilon}}$$
其中,$$\epsilon$$是一个小的常数(通常是 $$10^{-5}$$ 或 $$10^{-6}$$),用于避免除零错误。
缩放与平移
在标准化的基础上,层归一化通常会引入两个可学习的参数:缩放因子(gamma) 和 平移因子(beta),用于调整标准化后的输出,以便网络能够学习到合适的表示。
缩放因子:$$\gamma \in \mathbb{R}^d$$
平移因子:$$\beta \in \mathbb{R}^d$$
最终输出为
$$y_i = \gamma \hat{x}_i + \beta$$
这里,$$y_i$$就是经过层归一化处理后的输出。
作用
它是所有深层网络模型都需要的标准网络层,因为随着网络层数的增加,通过多层的计算后参数可能开始出现过大或过小的情况,这样可能会导致学习过程出现异常,模型可能收敛非常的慢. 因此都会在一定层数后接规范化层进行数值的规范化,使其特征数值在合理范围内.
稳定训练过程:层归一化可以确保每一层的输入分布更加稳定,避免激活值过大或过小,防止梯度消失或爆炸。
不依赖批次大小:层归一化与 批归一化(Batch Normalization) 不同,它不依赖于批次大小,而是针对单个样本的特征进行归一化。这意味着层归一化在RNN和Transformer等处理变长输入的模型中更加有效,因为这些模型的批次大小可能变化。
适用于时间序列模型:由于它对每个时间步进行归一化处理,而不依赖于整个批次,因此在像LSTM或Transformer等序列模型中,层归一化能够很好地适应时间序列的动态变化。
更快的收敛速度:通过归一化,每一层的激活值保持在相对统一的范围内,这有助于更快速的梯度更新,从而加速收敛。
Tips:层归一化 vs 批归一化
特性 层归一化(Layer Normalization) 批归一化(Batch Normalization) 计算方式 每个样本的特征维度进行归一化 对一个批次的所有样本进行归一化 归一化维度 按特征维度进行归一化(样本内部归一化) 按批次维度进行归一化(批次内归一化) 适用模型 适用于 RNN、Transformer 等序列模型 适用于CNN、全连接网络等大多数网络 依赖批次大小 不依赖批次大小 依赖批次大小,较小批次会影响统计量的准确性 训练速度 可以加速训练,但不如批归一化显著 通常能加速训练并稳定模型,但受限于批次大小 实现复杂度 实现简单,无需存储全局统计量 需要存储全局均值和方差
4.6.2 代码实现
# 规范化层 LayerNorm 实现思路分析 # 1 init函数 (self, features, eps=1e-6): # 定义线性层self.a2 self.b2, nn.Parameter(torch.ones(features)) # 2 forward(self, x) 返回标准化后的结果 # 对数据求均值 保持形状不变 x.mean(-1, keepdims=True) # 对数据求方差 保持形状不变 x.std(-1, keepdims=True) # 对数据进行标准化变换 反向传播可学习参数a2 b2 # eg self.a2 * (x-mean)/(std + self.eps) + self.b2 class LayerNorm(nn.Module): def __init__(self, features, eps=1e-6): # 参数features 待规范化的数据 # 参数 eps=1e-6 防止分母为零 super(LayerNorm, self).__init__() # 定义a2 γ规范化层的系数 y=kx+b中的k self.a2 = nn.Parameter(torch.ones(features)) # 定义b2 β规范化层的系数 y=kx+b中的b self.b2 = nn.Parameter(torch.zeros(features)) # 小常数 self.eps = eps def forward(self, x): # 对数据求均值 保持形状不变 # [2,4,512] -> [2,4,1] mean = x.mean(-1, keepdims=True) # 对数据求方差 保持形状不变 # [2,4,512] -> [2,4,1] std = x.std(-1, keepdims=True) # 对数据进行标准化变换 反向传播可学习参数a2 b2 # 注意 * 表示对应位置相乘 不是矩阵运算 x = self.a2 * (x-mean) / (std + self.eps) + self.b2 return x
函数调用:
# 规范化层测试 def dm_test_LayerNorm(): vocab = 1000 # 词表大小是1000 embedding_dim = 512 # 词嵌入维度是512维 # 输入x 是一个使用Variable封装的长整型张量, 形状是2 x 4 x = Variable(torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]])) emb = Embeddings(vocab, embedding_dim) embr = emb(x) dropout = 0.2 max_len = 60 # 句子最大长度 x = embr # [2, 4, 512] pe = PositionalEncoding(embedding_dim, dropout, max_len) pe_result = pe(x) query = key = value = pe_result # torch.Size([2, 4, 512]) # 调用验证 d_ff = 64 head = 8 # 多头注意力机制的输出 作为前馈全连接层的输入 mask = Variable(torch.zeros(8, 4, 4)) mha = MultiHeadedAttention(head, embedding_dim, dropout) mha_result = mha(query, key, value, mask) x = mha_result ff = PositionwiseFeedForward(embedding_dim, d_ff, dropout) ff_result = ff(x) features = d_model = 512 eps = 1e-6 x = ff_result ln = LayerNorm(features, eps) ln_result = ln(x) print('规范化层:', ln_result.shape, ln_result) if __name__ == '__main__': dm_test_LayerNorm()
输出结果:
规范化层: torch.Size([2, 4, 512]) tensor([[[-0.3092, -0.0910, -1.6996, ..., -0.9303, 0.4432, 0.2160], [ 0.6669, 1.0475, 0.3514, ..., 1.4554, 0.9730, -1.4658], [ 1.3659, 0.6126, -0.8531, ..., -0.8469, 1.0127, -1.0504], [ 1.3816, 0.3314, -0.3039, ..., 0.2701, 0.5732, -1.0152]], [[ 0.3848, -0.8395, 0.0173, ..., 0.6260, 0.3904, -1.2215], [-1.1391, -0.8122, 0.1111, ..., 0.2632, -0.0076, -1.2140], [ 0.3211, -0.4198, 0.1856, ..., -0.6273, 0.1961, 0.0278], [-0.2528, -1.5907, 0.3159, ..., -0.1957, 0.0760, -1.1218]]], grad_fn=<AddBackward0>)
4.6.3 小结
什么是规范化层:
一种用于提高深度神经网络训练稳定性和加速收敛的技术,广泛应用于现代深度学习模型中,尤其是在Transformer等序列建模网络中。
对每个输入样本在每一层内部进行标准化,确保每个神经网络层的输入分布具有一致的均值和方差。
规范化层的作用:
稳定训练:减少内部协变量偏移(Internal Covariate Shift),使训练过程更加稳定。
加速收敛:通过归一化输入分布,加快模型的收敛速度。
改善泛化:提高模型的泛化能力。
实现规范化层的类: LayerNorm
它的实例化参数有两个, features和eps,分别表示词嵌入特征大小,和一个足够小的数。
它的输入参数x代表来自上一层的输出。
它的输出就是经过规范化的特征表示。
4.7 子层连接
4.7.1 介绍
概念
子层连接(Sublayer Connection),也称为残差连接(Residual Connection),是Transformer模型中的一个关键设计,用于将多个子层(如自注意力层和前馈全连接层)组合在一起。它通过残差连接(Residual Connection)和层归一化(Layer Normalization)来增强模型的训练稳定性和性能。
如下图所示,输入到每个子层以及规范化层的过程中,还使用了残差连接(跳跃连接),因此我们把这一部分结构整体叫做子层连接(代表子层及其连接结构),在每个编码器层中都有两个子层,这两个子层加上周围的连接结构就形成了两个子层连接结构。
结构
残差连接:将子层的输入直接加到子层的输出上。
层归一化:对残差连接的结果进行归一化。
公式:
$$Output=LayerNorm(x+Sublayer(x))$$
$$x$$:子层的输入
$$Sublayer(x)$$:子层的输出(如自注意力层或前馈全连接层)
$$LayerNorm$$:层归一化
作用
避免梯度消失或爆炸:在深度神经网络中,梯度可能会在反向传播过程中逐渐消失或爆炸,导致训练不稳定。通过残差连接,输入能够直接传递到输出,从而有效地缓解了梯度消失问题。梯度可以通过残差路径传递,使得深层网络的训练变得更加容易。
加速收敛:由于残差连接使得信息更容易流动,因此它能够加速模型的训练过程。这种加速效果特别显著,在更深层的网络中,残差连接可以帮助网络更快地收敛到最佳解。
有效信息传递:层归一化的应用确保了每一层的输出具有合适的分布,从而避免了过大的激活值引起的数值不稳定问题。这保证了模型的训练过程中,信息能够有效地在不同层之间传递。
防止过拟合:通过残差连接,模型可以更好地捕捉和保留有用的特征,避免信息丢失,有助于减轻过拟合问题,尤其是在深层网络中。
4.7.2 代码实现
# 子层连接结构 子层(前馈全连接层 或者 注意力机制层)+ norm层 + 残差连接 # SublayerConnection实现思路分析 # 1 init函数 (self, size, dropout=0.1): # 定义self.norm层 self.dropout层, 其中LayerNorm(size) # 2 forward(self, x, sublayer) 返回+以后的结果 # 数据self.norm() -> sublayer()->self.dropout() + x class SublayerConnection(nn.Module): def __init__(self, size, dropout=0.1): # 参数size 词嵌入维度尺寸大小 # 参数dropout 置零比率 super(SublayerConnection, self).__init__() # 定义norm层 self.norm = nn.LayerNorm(size) # 定义dropout self.dropout = nn.Dropout(dropout) def forward(self, x, sublayer): # 参数x 代表数据 # sublayer 函数入口地址 子层函数(前馈全连接层 或者 注意力机制层函数的入口地址) # 方式1 # 数据self.norm() -> sublayer()->self.dropout() + x myres = x + self.dropout(sublayer(self.norm(x))) # 方式2 # 数据sublayer() -> self.norm() ->self.dropout() + x # myres = x + self.dropout(self.norm(x.subtype(x))) return myres
函数调用:
def dm_test_SublayerConnection(): size = 512 head = 8 vocab = 1000 # 词表大小是1000 d_model = 512 # 输入x 是一个使用Variable封装的长整型张量, 形状是2 x 4 x = Variable(torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]])) emb = Embeddings(vocab, d_model) embr = emb(x) dropout = 0.2 max_len = 60 # 句子最大长度 x = embr # [2, 4, 512] pe = PositionalEncoding(d_model, dropout, max_len) pe_result = pe(x) x = pe_result mask = Variable(torch.zeros(8, 4, 4)) # 多头自注意力子层 self_attn = MultiHeadedAttention(head, d_model) sublayer = lambda x: self_attn(x, x, x, mask) # 子层链接结构 sc = SublayerConnection(size, dropout) sc_result = sc(x, sublayer) print('sc_result.shape--->', sc_result.shape) print('sc_result--->', sc_result) if __name__ == '__main__': dm_test_SublayerConnection()
输出结果:
sc_result.shape---> torch.Size([2, 4, 512]) sc_result---> tensor([[[ 7.0114e+00, 3.5230e+01, -3.9275e+01, ..., 3.0409e+01, 4.3308e+01, -1.4148e+01], [ 6.0625e+01, -1.7394e+01, -1.6531e+01, ..., 1.7075e+01, 1.2843e+01, 1.7425e+01], [-3.2658e+00, -3.8847e+01, -2.7823e+00, ..., -3.3643e+01, 2.1593e+01, 2.6389e+01], [ 3.1625e+01, 4.7571e+00, -3.5086e+01, ..., 1.1333e-01, 4.0569e-02, 4.4964e-01]], [[-1.1280e+01, 8.7631e-01, -1.5429e+01, ..., -5.2952e+01, -2.4344e+01, -8.6224e+00], [ 2.5465e+01, -3.4799e+01, 2.4003e-01, ..., 4.8790e+01, -2.3115e-01, -4.4688e+01], [-5.0735e-02, 1.1536e+00, 2.1595e-01, ..., -2.3241e+01, 1.8083e+01, 1.7346e+01], [ 2.1725e+00, 9.8909e+00, -1.9748e+01, ..., 2.9949e+01, 2.8737e+01, 2.5619e-02]]], grad_fn=<AddBackward0>)
4.7.3 小结
什么是子层连接结构:
子层连接(Sublayer Connection),也称为残差连接(Residual Connection),是 Transformer 模型中的一个关键设计,用于将多个子层(如自注意力层和前馈全连接层)组合在一起。它通过残差连接(Residual Connection)和层归一化(Layer Normalization)来增强模型的训练稳定性和性能。
学习并实现了子层连接结构的类: SublayerConnection
类的初始化函数输入参数是size, dropout, 分别代表词嵌入大小和置零比率。
它的实例化对象输入参数是x, sublayer, 分别代表上一层输出以及子层的函数表示。
它的输出就是通过子层连接结构处理的输出。
4.8 编码器层
4.8.1 介绍
概念
编码器层(Encoder Layer)是Transformer编码器的基本构建单元,它重复堆叠形成整个编码器,负责逐步提取输入序列的特征。每个编码器层由两个核心子层组成:
多头自注意力机制(Multi-Head Self-Attention):用于捕捉输入序列中每个位置与其他位置的关系。
前馈全连接层(Feed-Forward Neural Network, FFN):用于对每个位置的表示进行非线性变换。
每个子层后都有残差连接(Residual Connection)和层归一化(Layer Normalization),以增强模型的训练稳定性和性能。
结构/工作流程
输入:
每个编码器层的输入是上一层编码器层的输出,或者对于第一层编码器层来说,是输入嵌入向量加上位置编码向量,形状为 (
[batch_size, seq_len, d_model]
)。
多头自注意力机制:
将输入 x 传递给多头自注意力层,得到输出 $$Attention(x)$$。
多头自注意力层会捕捉输入序列中各个 token 之间的依赖关系,每个注意力头关注不同的特征,然后将多个头的输出拼接。
Q, K, V 三个矩阵都来自于相同的输入 x,这是 “自” 注意力的含义。
残差连接与层归一化:
将多头自注意力的输入 x 与输出 $$Attention(x)$$ 相加,形成残差连接:
$$x + Attention(x)$$
对残差连接的结果进行层归一化,得到第一部分的输出:
$$LayerNorm(x + Attention(x))$$
前馈全连接层:
将经过残差连接和层归一化的输出传递给前馈全连接网络,得到输出$$FFN(x)$$。
前馈全连接网络会对每个token的表示进行非线性变换,并提取更高级的特征。
残差连接与层归一化:
将前馈全连接网络的输入 (即上一层的输出) 与输出$$FFN(x)$$相加,形成残差连接:
$$LayerNorm(x + Attention(x)) + FFN(x)$$
对残差连接的结果进行层归一化,得到第二部分的输出,也是编码器层的最终输出:
$$LayerNorm(LayerNorm(x + Attention(x)) + FFN(x))$$
输出:
经过处理的特征表示,该层的输出会被传递给下一层的输入(形状为
[batch_size, seq_len, d_model]
)。
作用
对输入的token表示进行处理,从而提取更高级的特征和上下文信息。每个编码器层都能够处理上一层的输出,并逐步将其转化为更丰富、更抽象的表示。
4.8.2 代码实现
# 编码器层类 EncoderLayer 实现思路分析 # init函数 (self, size, self_attn, feed_forward, dropout): # 实例化多头注意力层对象self_attn # 前馈全连接层对象feed_forward size词嵌入维度512 # clones两个子层连接结构 self.sublayer = clones(SublayerConnection(size,dropout),2) # forward函数 (self, x, mask) # 数据经过子层连接结构1 self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask)) # 数据经过子层连接结构2 self.sublayer[1](x, self.feed_forward) class EncoderLayer(nn.Module): def __init__(self, size, self_atten, feed_forward, dropout): super(EncoderLayer, self).__init__() # 实例化多头注意力层对象 self.self_attn = self_atten # 前馈全连接层对象feed_forward self.feed_forward = feed_forward # size词嵌入维度512 self.size = size # clones两个子层连接结构 self.sublayer = clones(SublayerConnection(size,dropout),2) self.sublayer = clones(SublayerConnection(size, dropout), 2) def forward(self, x, mask): # 数据经过第1个子层连接结构 # 参数x:传入的数据 参数lambda x... : 子函数入口地址 x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask)) # 数据经过第2个子层连接结构 # 参数x:传入的数据 self.feed_forward子函数入口地址 x = self.sublayer[1](x, self.feed_forward) return x
函数调用:
def dm_test_EncoderLayer(): vocab = 1000 # 词表大小是1000 d_model = 512 # 输入x 是一个使用Variable封装的长整型张量, 形状是2 x 4 x = Variable(torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]])) emb = Embeddings(vocab, d_model) embr = emb(x) dropout = 0.2 max_len = 60 # 句子最大长度 x = embr # [2, 4, 512] pe = PositionalEncoding(d_model, dropout, max_len) pe_result = pe(x) x = pe_result size = 512 head = 8 d_ff = 64 # 实例化多头注意力机制类对象 self_attn = MultiHeadedAttention(head, d_model) # 实例化前馈全连接层对象 ff = PositionwiseFeedForward(d_model, d_ff, dropout) # mask数据 mask = Variable(torch.zeros(8, 4, 4)) # 实例化编码器层对象 my_encoderlayer = EncoderLayer(size, self_attn, ff, dropout) # 数据通过编码层编码 el_result = my_encoderlayer(x, mask) print('el_result.shape', el_result.shape, el_result) if __name__ == '__main__': dm_test_EncoderLayer()
输出结果:
el_result.shape torch.Size([2, 4, 512]) tensor([[[ 42.3457, -17.2756, 0.1695, ..., 20.6587, -28.8568, 0.1003], [-13.3407, -18.3088, -21.3491, ..., -8.9874, -40.2917, 56.1672], [ 44.9739, 27.2132, -15.0390, ..., -7.1177, 5.7607, 13.7697], [ 0.1546, -20.3611, 15.9304, ..., 7.0122, 42.5421, -1.7627]], [[-15.8305, -5.5882, -27.7982, ..., -0.2449, 62.8648, -18.9937], [ -0.2576, -5.0591, -0.5264, ..., 35.8405, -14.7624, 59.5532], [ 2.9312, 40.0757, 42.7443, ..., 42.9479, 62.8699, 37.2467], [ 10.2777, -17.0255, -8.0018, ..., 34.2319, -35.8646, -0.3673]]], grad_fn=<AddBackward0>)
4.8.3 小结
什么是编码器层:
编码器层(Encoder Layer)是Transformer编码器的基本构建单元,它重复堆叠形成整个编码器,负责逐步提取输入序列的特征。
编码器层的作用:
对输入的token表示进行处理,从而提取更高级的特征和上下文信息。每个编码器层都能够处理上一层的输出,并逐步将其转化为更丰富、更抽象的表示。
学习并实现了编码器层的类: EncoderLayer
类的初始化函数共有4个, 别是size,其实就是我们词嵌入维度的大小. 第二个self_attn,之后我们将传入多头自注意力子层实例化对象, 并且是自注意力机制. 第三个是feed_froward, 之后我们将传入前馈全连接层实例化对象. 最后一个是置0比率dropout。
实例化对象的输入参数有2个,x代表来自上一层的输出, mask代表掩码张量。
它的输出代表经过整个编码层的特征表示。
4.9 编码器
4.9.1 介绍
概念
编码器(Encoder)是Transformer架构中的核心组成部分,它负责将输入的序列(通常是一个词汇序列或其他类型的输入数据)映射到一个高维空间中的表示(中间语义张量c),这个高维空间中的表示随后会被解码器用于生成输出序列。在 Transformer中,编码器是由多个相同结构的层叠加而成,每个编码器层都由多头自注意力机制和前馈全连接网络两个子层组成,并且每个子层都使用残差连接和层归一化来确保训练过程的稳定性和加速收敛。
结构
编码器由多个相同的编码器层(Encoder Layer)堆叠而成。每个编码器层包含两个核心子层:
多头自注意力机制(Multi-Head Self-Attention):
计算输入序列中每个位置与其他位置的相关性。
通过多个注意力头捕捉不同子空间的特征。
前馈全连接层(Feed-Forward Neural Network, FFN):
对每个位置的表示进行非线性变换。
通常由两个全连接层和激活函数(如 ReLU)组成。
每个子层后都有:
残差连接(Residual Connection):将输入直接加到子层输出上。
层归一化(Layer Normalization):对输出进行归一化。
工作流程
输入:
每个编码器层的输入是上一层的输出,或者对于第一层来说,是输入嵌入向量加上位置编码向量。
多头自注意力机制:
将输入传递给多头自注意力层,捕捉输入序列中各个token之间的依赖关系,并生成一个加权后的表示。
残差连接和层归一化:
将多头自注意力的输入与输出相加,形成残差连接,然后进行层归一化操作。
前馈全连接网络:
将经过残差连接和层归一化的输出传递给前馈全连接网络,进行非线性变换和特征提取。
残差连接和层归一化:
将前馈全连接网络的输入与输出相加,形成残差连接,然后进行层归一化操作。
输出:
每个编码器层的输出都会作为下一层编码器层的输入,最终最后一层的输出就是整个编码器的输出。
作用
编码器的主要功能是将输入序列映射到一个高维特征空间,具体包括:
特征提取:通过自注意力机制捕捉输入序列中每个位置与其他位置的关系。
特征增强:通过前馈全连接层对特征进行非线性变换。
层次化表示:通过多层堆叠,逐步提取更高层次的特征表示。
4.9.2 代码实现
# 编码器类 Encoder 实现思路分析 # init函数 (self, layer, N) # 实例化多个编码器层对象self.layers 通过方法clones(layer, N) # 实例化规范化层 self.norm = LayerNorm(layer.size) # forward函数 (self, x, mask) # 数据经过N个层 x = layer(x, mask) # 返回规范化后的数据 return self.norm(x) class Encoder(nn.Module): def __init__(self, layer, N): # 参数layer 1个编码器层 # 参数 编码器层的个数 super(Encoder, self).__init__() # 实例化多个编码器层对象 self.layers = clones(layer, N) # 实例化规范化层 self.norm = nn.LayerNorm(layer.size) def forward(self, x, mask): # 数据经过N个层 x = layer(x, mask) for layer in self.layers: x = layer(x, mask) # 返回规范化后的数据 return self.norm(x) return self.norm(x)
函数调用:
def dm_test_Encoder(): vocab = 1000 # 词表大小是1000 d_model = 512 # 输入x 是一个使用Variable封装的长整型张量, 形状是2 x 4 x = Variable(torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]])) # writeFile("dafdsafds") emb = Embeddings(vocab, d_model) embr = emb(x) dropout = 0.2 max_len = 60 # 句子最大长度 x = embr # [2, 4, 512] pe = PositionalEncoding(d_model, dropout, max_len) pe_result = pe(x) x = pe_result # 获取位置编码器层 编码以后的结果 size = 512 head = 8 d_model = 512 d_ff = 64 c = copy.deepcopy attn = MultiHeadedAttention(head, d_model) dropout = 0.2 ff = PositionwiseFeedForward(d_model, d_ff, dropout) layer = EncoderLayer(size, c(attn), c(ff), dropout) # 编码器中编码器层的个数N N = 6 mask = Variable(torch.zeros(8, 4, 4)) # 实例化编码器对象 en = Encoder(layer, N) en_result = en(x, mask) print('en_result.shape--->', en_result.shape) print('en_result--->', en_result) if __name__ == '__main__': dm_test_Encoder()
输出结果:
en_result.shape---> torch.Size([2, 4, 512]) en_result---> tensor([[[-1.0195, -2.3425, 1.2915, ..., -0.1201, -1.1911, 2.0452], [-1.1250, 0.8957, -1.4046, ..., -1.5621, -1.0622, -0.0025], [-0.9776, -0.1088, 0.0161, ..., 1.2835, 0.0135, -0.4939], [ 0.1376, -0.5369, 0.3099, ..., -0.4200, 2.2780, -1.8525]], [[ 2.2200, 1.8239, -1.6174, ..., -0.3400, -0.3632, -2.1963], [ 1.6247, 0.6643, 0.0277, ..., -0.0780, 0.0067, 0.8427], [-0.4811, 1.1233, 0.1594, ..., 1.1535, 0.8534, 0.2948], [-0.4268, -0.4649, 1.7288, ..., -2.4360, -0.0879, -0.9242]]], grad_fn=<AddBackward0>)
4.9.3 小结
什么是编码器:
编码器(Encoder) 是 Transformer 架构中的核心组成部分,它负责将输入的序列(通常是一个词汇序列或其他类型的输入数据)映射到一个高维空间中的表示(中间语义张量c),这个高维空间中的表示随后会被解码器用于生成输出序列。
编码器的作用:
将输入序列映射到一个高维特征空间,对输入进行指定的特征提取。
学习并实现了编码器的类: Encoder
类的初始化函数参数有两个,分别是layer和N,代表编码器层和编码器层的个数。
forward函数的输入参数也有两个, 和编码器层的forward相同, x代表上一层的输出, mask代码掩码张量。
编码器类的输出就是Transformer中编码器的特征提取表示, 它将成为解码器的输入的一部分。
五、 解码器部分实现
5.1 解码器层
5.1.1 介绍
概念
解码器层(Decoder Layer)是Transformer架构中的基本单元之一,每个解码器层负责处理目标序列并与编码器的输出交互,以生成目标序列中的每个词。
作用
根据编码器提供的上下文信息和已生成的输出序列,逐步预测下一个token的表示。每个解码器层都能够处理上一层的输出,并结合编码器的信息,逐步生成更符合目标的输出序列。
tips:
为什么是逐步预测下一个token?transformer的核心任务是生成一个输出序列(如翻译、文本生成等)。输出序列的长度通常是未知的,且每个时间步的输出依赖于前一个时间步的输出。因此,解码器必须逐步生成序列:
逐步生成:解码器每次生成一个字符(或词),并将其作为下一个时间步的输入。
自回归特性:这种逐步生成的方式称为自回归(Auto-regressive),是序列生成任务的典型特性。
训练阶段:解码器的输入是真实的目标序列(Ground Truth),通过Teacher Forcing的方式逐时间步训练。
推理阶段:解码器没有真实的目标序列,只能依赖自己生成的输出作为下一个时间步的输入。
结构
一个解码器层主要包含以下三个子层,每个子层都包含残差连接和层归一化:
掩码多头自注意力机制 (Masked Multi-Head Self-Attention):
与编码器中的自注意力类似,但加入了掩码机制,确保模型在生成每个token时,只能关注到该token之前的 token。
用于捕捉输出序列中已生成 token 之间的关系。
公式:
$$\text{Masked Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}} + \text{Mask}\right)V$$
$$Q$$、$$K$$、$$V$$分别是查询、键和值的矩阵。
$$\text{Mask}$$是遮挡矩阵
$$d_k$$是键的维度
多头编码器-解码器注意力机制 (Multi-Head Encoder-Decoder Attention):
解码器还需要关注编码器的输出,从而将编码器的语义信息融入到解码过程。
该注意力机制的Query(Q)来自解码器的上一层输出,Key(K)和Value(V) 来自编码器的输出。
公式:
$$\text{Cross Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V$$
前馈全连接网络 (Feed-Forward Network):
一个简单的全连接网络,包含两个线性变换层和一个非线性激活函数(通常是ReLU或GELU)。
用于对每个token的表示进行非线性变换,并提取更高级的特征。
这个网络独立地处理每个位置的表示。
公式:
$$\text{FFN}(x) = \text{ReLU}(xW_1 + b_1)W_2 + b_2$$
$$W_1$$、$$W_2$$ 是权重矩阵。
$$b_1$$、$$b_2$$ 是偏置向量。
工作流程
输入:
每个解码器层的输入是上一层解码器层的输出,或者对于第一层解码器层来说,是输出嵌入向量加上位置编码向量(形状为
[batch_size, seq_len, d_model]
)。
掩码多头自注意力:
将输入 x 传递给掩码多头自注意力层,得到输出:$$MaskedAttention(x)$$。
掩码多头自注意力层捕捉输出序列中已生成token之间的关系,并且通过掩码机制来保证了自回归性质。
Q, K, V 三个矩阵都来自于解码器的输入x。
残差连接和层归一化 (第一部分):
将掩码多头自注意力的输入x与输出$$MaskedAttention(x)$$ 相加,形成残差连接:
$$x + MaskedAttention(x)$$
对残差连接的结果进行层归一化,得到第一部分的输出:
$$LayerNorm(x + MaskedAttention(x))$$
多头编码器-解码器注意力:
将经过层归一化的输出作为Query(Q),将编码器的输出作为Key(K)和Value(V)传递给多头编码器-解码器注意力层,得到输出:
$$EncDecAttention(x, encoder_output)$$
Q来自解码器,K,V来自编码器。
该层允许解码器关注编码器提供的上下文信息,将输入序列的信息融合到输出序列的生成过程中。
残差连接和层归一化 (第二部分):
将多头编码器-解码器注意力的输入 (即上一层输出) 与输出 $$EncDecAttention(x, encoder_output)$$ 相加,形成残差连接:
$$LayerNorm(x + MaskedAttention(x)) + EncDecAttention(x, encoder_output)$$
对残差连接的结果进行层归一化,得到第二部分的输出:
$$LayerNorm(LayerNorm(x + MaskedAttention(x)) + EncDecAttention(x, encoder_output))$$
前馈全连接网络:
将经过残差连接和层归一化的输出传递给前馈全连接网络,得到输出:$$FFN(x)$$。
前馈全连接网络会对每个 token 的表示进行非线性变换,并提取更高级的特征。
残差连接和层归一化 (第三部分):
将前馈全连接网络的输入(即上一层输出)与输出$$FFN(x)$$相加,形成残差连接:
$$LayerNorm(LayerNorm(x + MaskedAttention(x)) + EncDecAttention(x, encoder_output)) + FFN(x)$$
对残差连接的结果进行层归一化,得到第三部分的输出,也是解码器层的最终输出:
$$LayerNorm(LayerNorm(LayerNorm(x + MaskedAttention(x)) + EncDecAttention(x, encoder_output)) + FFN(x))$$
输出:
该层的输出会被传递给下一层解码器的输入(形状为
[batch_size, seq_len, d_model]
)。
5.1.2 代码实现
# 解码器层类 DecoderLayer 实现思路分析 # init函数 (self, size, self_attn, src_attn, feed_forward, dropout) # 词嵌入维度尺寸大小size 自注意力机制层对象self_attn 一般注意力机制层对象src_attn 前馈全连接层对象feed_forward # clones3子层连接结构 self.sublayer = clones(SublayerConnection(size,dropout),3) # forward函数 (self, x, memory, source_mask, target_mask) # 数据经过子层连接结构1 self.sublayer[0](x, lambda x:self.self_attn(x, x, x, target_mask)) # 数据经过子层连接结构2 self.sublayer[1](x, lambda x:self.src_attn(x, m, m, source_mask)) # 数据经过子层连接结构3 self.sublayer[2](x, self.feed_forward) class DecoderLayer(nn.Module): def __init__(self, size, self_attn, src_attn, feed_forward, dropout): super(DecoderLayer, self).__init__() # 词嵌入维度尺寸大小 self.size = size # 自注意力机制层对象 q=k=v self.self_attn = self_attn # 一遍注意力机制对象 q!=k=v self.src_attn = src_attn # 前馈全连接层对象 self.feed_forward = feed_forward # clones3子层连接结构 self.sublayer = clones(SublayerConnection(size, dropout), 3) def forward(self, x, memory, source_mask, target_mask): m = memory # 数据经过子层连接结构1 x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, target_mask)) # 数据经过子层连接结构2 x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, source_mask)) # 数据经过子层连接结构3 x = self.sublayer[2](x, self.feed_forward) return x
函数调用:
def dm_test_DecoderLayer(): vocab = 1000 # 词表大小是1000 d_model = 512 # 输入x 是一个使用Variable封装的长整型张量, 形状是2 x 4 x = Variable(torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]])) emb = Embeddings(vocab, d_model) embr = emb(x) dropout = 0.2 max_len = 60 # 句子最大长度 x = embr # [2, 4, 512] pe = PositionalEncoding(d_model, dropout, max_len) pe_result = pe(x) x = pe_result # 获取位置编码器层 编码以后的结果 # 类的实例化参数与解码器层类似, 相比多出了src_attn, 但是和self_attn是同一个类. head = 8 d_ff = 64 size = 512 self_attn = src_attn = MultiHeadedAttention(head, d_model, dropout) # 前馈全连接层也和之前相同 ff = PositionwiseFeedForward(d_model, d_ff, dropout) # 产生编码器结果 # 注意此函数返回编码以后的结果 要有返回值, dm_test_Encoder函数后return en_result en_result = dm_test_Encoder() memory = en_result mask = Variable(torch.zeros(8, 4, 4)) source_mask = target_mask = mask # 实例化解码器层 对象 dl = DecoderLayer(size, self_attn, src_attn, ff, dropout) # 对象调用 dl_result = dl(x, memory, source_mask, target_mask) print(dl_result.shape) print(dl_result) if __name__ == '__main__': dm_test_DecoderLayer()
输出结果:
torch.Size([2, 4, 512]) tensor([[[ -4.0888, -0.3569, 19.7575, ..., -15.3458, -0.8096, 11.2497], [ -4.9898, 48.7993, -11.4645, ..., 8.6564, 10.2307, -31.0148], [ 26.7606, 47.9486, 33.0609, ..., -18.9295, 5.0897, 94.1305], [ -0.3094, 10.9734, -25.9352, ..., 26.9372, -50.0279, -2.6424]], [[-56.7436, 39.1334, 0.8712, ..., 0.8533, -14.4249, -19.0897], [-50.5535, 56.5821, 50.2424, ..., 19.4151, 5.6403, -42.5772], [ -0.3644, -45.2319, -5.3061, ..., 3.8075, 18.8357, 39.3184], [ 57.7590, -3.2241, 12.3510, ..., -34.8855, -0.7769, -0.7925]]], grad_fn=<AddBackward0>)
5.1.3 小结
什么是编码器层:
解码器层是Transformer架构中的基本单元之一,每个解码器层负责处理目标序列并与编码器的输出交互,以生成目标序列中的每个词。
解码器层的作用:
根据编码器提供的上下文信息和已生成的输出序列,逐步预测下一个token的表示。每个解码器层都能够处理上一层的输出,并结合编码器的信息,逐步生成更符合目标的输出序列。
实现解码器层的类: DecoderLayer
类的初始化函数的参数有5个, 分别是size,代表词嵌入的维度大小, 同时也代表解码器层的尺寸,第二个是self_attn,多头自注意力对象,也就是说这个注意力机制需要Q=K=V,第三个是src_attn,多头注意力对象,这里Q!=K=V,第四个是前馈全连接层对象,最后就是droupout置0比率。
forward函数的参数有4个,分别是来自上一层的输入x,来自编码器层的语义存储变量mermory,以及源数据掩码张量和目标数据掩码张量。
最终输出了由编码器输入和目标数据一同作用的特征提取结果。
5.2 解码器
5.2.1 介绍
概念
解码器(Decoder)是Transformer模型的另一个核心组件,负责将编码器生成的特征表示转换为目标序列(如翻译后的句子或生成的文本)。解码器的设计类似于编码器,但增加了一些关键机制(如掩码自注意力)来适应序列生成任务。
解码器部分:
由N个解码器层堆叠而成
每个解码器层由三个子层连接结构组成
第一个子层连接结构包括一个多头自注意力子层和规范化层以及一个残差连接
第二个子层连接结构包括一个多头注意力子层和规范化层以及一个残差连接
第三个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接
作用
特征解码:利用编码器生成的特征表示,逐步生成目标序列。
自回归生成:在生成目标序列时,解码器以自回归方式工作,即每次生成一个词,并将其作为下一步的输入。
捕捉上下文信息:通过自注意力机制和编码器-解码器注意力机制,捕捉目标序列和源序列之间的关系。
结构
解码器由多个相同的解码器层(Decoder Layer)堆叠而成。每个解码器层包含三个核心子层:
掩码多头自注意力机制(Masked Multi-Head Self-Attention):
计算目标序列中每个位置与之前位置的相关性。
通过掩码防止当前位置访问未来的信息。
编码器-解码器注意力机制(Encoder-Decoder Attention):
计算目标序列与源序列的相关性。
利用编码器的输出作为键和值。
前馈全连接层(Feed-Forward Neural Network, FFN):
对每个位置的表示进行非线性变换。
工作流程
输入:
每个解码器层的输入是上一层的输出,或者对于第一层来说,是输出嵌入向量加上位置编码向量。
在训练时,输入是真实的输出序列(或者说“黄金标签”),而在推理时,输入是模型之前生成的输出序列。
掩码多头自注意力:
将输入传递给掩码多头自注意力层。
掩码机制确保了模型在预测某个token时,只能关注到它之前的token,而不能关注到它之后的token。这是为了保证解码的自回归性质。
残差连接和层归一化 (第一部分):
将掩码多头自注意力的输入与输出相加,形成残差连接,然后进行层归一化操作。
多头编码器-解码器注意力:
将经过层归一化的输出作为Query(Q),将编码器的输出作为Key(K)和Value(V)传递给多头编码器-解码器注意力层。
这个子层允许解码器关注编码器提供的上下文信息,从而将输入序列的信息融入到输出序列的生成过程中。
残差连接和层归一化 (第二部分):
将多头编码器-解码器注意力的输入与输出相加,形成残差连接,然后进行层归一化操作。
前馈全连接网络:
将经过层归一化的输出传递给前馈全连接网络,进行非线性变换和特征提取。
残差连接和层归一化 (第三部分):
将前馈全连接网络的输入与输出相加,形成残差连接,然后进行层归一化操作。
输出:
每个解码器层的输出都会作为下一层解码器层的输入。
5.2.2 代码实现
# 解码器类 Decoder 实现思路分析 # init函数 (self, layer, N): # self.layers clones N个解码器层clones(layer, N) # self.norm 定义规范化层 LayerNorm(layer.size) # forward函数 (self, x, memory, source_mask, target_mask) # 数据以此经过各个子层 x = layer(x, memory, source_mask, target_mask) # 数据最后经过规范化层 return self.norm(x) # 返回处理好的数据 class Decoder(nn.Module): def __init__(self, layer, N): # 参数layer 解码器层对象 # 参数N 解码器层对象的个数 super(Decoder, self).__init__() # clones N个解码器层 self.layers = clones(layer, N) # 定义规范化层 self.norm = nn.LayerNorm(layer.size) def forward(self, x, memory, source_mask, target_mask): # 数据以此经过各个子层 for layer in self.layers: x = layer(x, memory, source_mask, target_mask) # 数据最后经过规范化层 return self.norm(x)
函数调用:
# 测试 解码器 def dm_test_Decoder(): vocab = 1000 # 词表大小是1000 d_model = 512 # 输入x 是一个使用Variable封装的长整型张量, 形状是2 x 4 x = Variable(torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]])) emb = Embeddings(vocab, d_model) embr = emb(x) dropout = 0.2 max_len = 60 # 句子最大长度 x = embr # [2, 4, 512] pe = PositionalEncoding(d_model, dropout, max_len) pe_result = pe(x) x = pe_result # 获取位置编码器层 编码以后的结果 # 分别是解码器层layer和解码器层的个数N size = 512 d_model = 512 head = 8 d_ff = 64 dropout = 0.2 # 创建深拷贝函数 c = copy.deepcopy # 多头注意力对象 attn = MultiHeadedAttention(head, d_model) # 前馈全连接层 ff = PositionwiseFeedForward(d_model, d_ff, dropout) # 解码器层 # c(attn):调用深拷贝函数对attn进行深拷贝 layer = DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout) N = 6 # 输入参数与解码器层的输入参数相同 x = pe_result # 产生编码器结果 en_result = dm_test_Encoder() memory = en_result # 掩码对象 mask = Variable(torch.zeros(8, 4, 4)) # sorce掩码 target掩码 source_mask = target_mask = mask # 创建 解码器 对象 de = Decoder(layer, N) # 解码器对象 解码 de_result = de(x, memory, source_mask, target_mask) print(de_result) print(de_result.shape) if __name__ == '__main__': dm_test_Decoder()
输出结果:
tensor([[[ 0.2676, -0.0462, -0.1605, ..., -1.9458, -0.0999, 0.1553], [-0.7347, -0.7527, -1.8725, ..., -0.8728, 2.1724, -0.0156], [ 0.8754, -0.0533, -0.0659, ..., 1.6729, -0.7587, 0.0440], [ 0.0066, -0.0581, 1.2448, ..., 0.3517, 0.5180, -0.8565]], [[-1.7937, 0.0469, 0.1267, ..., 0.7833, -0.2724, 0.0750], [ 0.1900, -1.8415, -2.1007, ..., -0.2223, -1.8143, 0.6919], [ 0.3878, -2.1848, -0.4604, ..., -0.3649, 0.6707, 0.1380], [ 0.9642, 0.9571, -0.2017, ..., 0.8043, -0.1301, 0.1208]]], grad_fn=<NativeLayerNormBackward0>) torch.Size([2, 4, 512])
5.2.3 小结
什么是解码器:
解码器(Decoder)是Transformer模型的另一个核心组件,负责将编码器生成的特征表示转换为目标序列(如翻译后的句子或生成的文本)。解码器的设计类似于编码器,但增加了一些关键机制(如掩码自注意力)来适应序列生成任务。
解码器的作用:
特征解码:利用编码器生成的特征表示,逐步生成目标序列。
自回归生成:在生成目标序列时,解码器以自回归方式工作,即每次生成一个词,并将其作为下一步的输入。
捕捉上下文信息:通过自注意力机制和编码器-解码器注意力机制,捕捉目标序列和源序列之间的关系。
实现解码器的类: Decoder
类的初始化函数的参数有两个,第一个就是解码器层layer,第二个是解码器层的个数N。
forward函数中的参数有4个,x代表目标数据的嵌入表示,memory是编码器层的输出,src_mask, tgt_mask代表源数据和目标数据的掩码张量。
输出解码过程的最终特征表示。
六、输出部分实现
6.1 输出层介绍
概念
输出层(Output layer)是模型最终生成目标序列的核心部分,它接收解码器的最后一层输出,并将其转换为目标词汇表中的具体词。
输出层通常由一个线性变换层 (Linear Layer)和一个Softmax层组成。
作用
特征映射:将解码器的输出(高维特征表示)映射到目标词汇表的大小。
概率分布:通过 Softmax 函数生成每个位置的目标词的概率分布。
序列生成:根据概率分布生成目标序列(如翻译后的句子或生成的文本)。
结构
线性变换层(Linear Layer):
也称为全连接层 (Fully Connected Layer)。
将解码器的输出从
d_model
维度映射到tgt_vocab_size
维度。公式:$$logits=W⋅x+b$$,其中 $$W$$ 是权重矩阵,$$b$$ 是偏置向量。
Softmax层:
将线性变换的输出转换为概率分布。
对于每个词汇表中的token,Softmax函数会计算出一个概率值,表示模型预测这个token作为下一个输出的概率。
公式:$$probs=Softmax(logits)$$。
工作流程
解码器输出: 解码器最后一层的输出向量,通常是一个形状为 (batch_size, seq_len, d_model)的张量。
线性变换层: 解码器的输出向量会通过线性变换层,映射到一个新的向量空间,维度为tgt_vocab_size,输出的形状变为 (batch_size, seq_len, tgt_vocab_size)。
Softmax函数: 线性变换层的输出会经过Softmax函数,将其转化为概率分布,输出的形状依然是 (batch_size, seq_len, tgt_vocab_size),其中每个值表示该位置预测为某个词汇表中token的概率。
选择预测结果:
训练: 在训练过程中,我们通常使用交叉熵损失函数来比较模型的预测概率分布与真实标签的分布,并更新模型的参数。
推理/预测: 在推理或预测时,我们会选择概率最高的token作为模型的预测输出。例如:可以使用argmax函数来选择概率最大的token的索引。
6.2 代码实现
# 解码器类 Generator 实现思路分析 # init函数 (self, d_model, vocab_size) # 定义线性层self.project # forward函数 (self, x) # 数据 torch.log_softmax(self.project(x), dim=-1) class Generator(nn.Module): def __init__(self, d_model, vocab_size): # 参数d_model 线性层输入特征尺寸大小 # 参数vocab_size 线性层输出尺寸大小 super(Generator, self).__init__() # 定义线性层 self.project = nn.Linear(d_model, vocab_size) def forward(self, x): # 数据经过线性层 最后一个维度归一化 log方式 x = torch.log_softmax(self.project(x), dim=-1) return x
函数调用:
if __name__ == '__main__': # 实例化output层对象 # 解码器预测词的输出维度 d_model = 512 # 词汇表中词数量 vocab_size = 1000 my_generator = Generator(d_model, vocab_size) # 准备模型数据 x = torch.randn(2, 4, 512) # 数据经过out层 gen_result = my_generator(x) print('gen_result--->', gen_result.shape, '\n', gen_result)
输出结果:
gen_result---> torch.Size([2, 4, 1000]) tensor([[[-7.4949, -7.8329, -7.2474, ..., -6.6283, -7.0262, -7.1283], [-7.4107, -6.8480, -6.7202, ..., -5.3701, -7.2082, -7.2225], [-7.3881, -7.7449, -5.8400, ..., -6.8243, -7.1951, -6.4599], [-7.3690, -6.8549, -7.4158, ..., -7.0073, -7.0981, -6.9379]], [[-7.9115, -7.0295, -7.2436, ..., -7.5143, -6.7885, -6.8484], [-6.9169, -7.1748, -6.4291, ..., -5.8896, -7.4494, -8.3578], [-6.7564, -7.3401, -7.1291, ..., -6.4325, -6.7251, -5.9878], [-6.5363, -7.7376, -7.5278, ..., -6.9457, -5.5249, -6.3223]]], grad_fn=<LogSoftmaxBackward0>)
6.3 小结
什么是输出层:
模型最终生成目标序列的核心部分,它接收解码器的最后一层输出,并将其转换为目标词汇表中的具体词。
输出层作用:
特征映射:将解码器的输出(高维特征表示)映射到目标词汇表的大小。
概率分布:通过Softmax函数生成每个位置的目标词的概率分布。
序列生成:根据概率分布生成目标序列(如翻译后的句子或生成的文本)。
线性层作用:
将解码器的输出从
d_model
维度映射到tgt_vocab_size
维度。
softmax层作用:
将线性变换的输出转换为概率分布。
实现线性层和softmax层的类: Generator
初始化函数的输入参数有两个, d_model代表词嵌入维度, tgt_vocab_size代表词表大小。
forward函数接受上一层的输出。
最终获得经过线性层和softmax层处理的结果。
七、模型构建
7.1 模型构建介绍
通过上面的小节, 我们已经完成了所有组成部分的实现, 接下来就来实现完整的编码器-解码器结构.
Transformer总体架构图:
Transformer模型由两部分组成:
编码器(Encoder):负责处理输入序列并提取上下文特征。
解码器(Decoder):根据编码器的输出和目标序列生成最终的目标序列。
其核心模块包括:
嵌入层:将输入/目标序列映射为高维向量。
位置编码:为序列引入位置信息。
多头注意力机制:捕获序列中不同位置间的依赖关系。
前馈全连接网络:进行非线性特征变换。
层归一化和残差连接:稳定训练过程。
输出层:生成目标序列中的每个词。
Transformer模型的构建流程:
1. 输入处理
输入数据准备:
将源语言和目标语言的句子转换为整数索引序列。
通过词汇表进行词嵌入处理。
嵌入层:
对输入序列中的每个词索引,查找对应的词向量。
结果是形状为 (batch_size, seq_len, d_model)的嵌入张量,
其中:
batch_size:批量大小。
seq_len:序列长度。
d_model:嵌入维度。
位置编码(Positional Encoding):
为输入序列中的每个位置添加位置信息。
通过固定正弦函数或可学习的位置嵌入实现。
结果是每个时间步都包含了位置信息的嵌入表示。
2. 编码器(Encoder)
编码器由多个堆叠的编码器层组成,每个编码器层包括以下组件:
自注意力机制(Self-Attention):
计算输入序列中每个位置对其他位置的相关性。
生成的上下文表示包含输入序列中的全局依赖关系。
残差连接和层归一化:
将自注意力的输出与输入相加,保持信息流。
通过层归一化稳定训练。
前馈全连接网络:
对每个时间步的上下文向量单独应用两层全连接网络(带激活函数)。
进一步非线性变换输入特征。
多层堆叠:
编码器由$$N_{\text{enc}}$$个编码器层组成,堆叠后的结果是最终的编码器输出,形状为 (batch_size, seq_len, d_model)。
3. 解码器(Decoder)
解码器也由多个堆叠的解码器层组成,每层包含以下组件:
掩码自注意力机制:
仅允许目标序列中每个时间步关注它自身及之前的时间步。
避免泄漏未来时间步的信息。
编码器-解码器注意力机制:
将目标序列的上下文表示与编码器的输出进行交互。
捕获目标序列与源序列之间的对齐关系。
残差连接和层归一化:
同样为解码器的每个子层添加残差连接和层归一化,确保梯度稳定。
前馈全连接网络:
通过两层全连接网络对解码器的上下文表示进一步变换。
多层堆叠:
解码器由$$N_{\text{dec}}$$个解码器层组成,堆叠后生成解码器的最终输出,形状为 (batch_size, seq_len, d_model)。
4. 输出层
线性层:
将解码器最后一层的输出投影到目标词汇表的大小。
输出 $$logits$$,形状为 (batch_size, seq_len, tgt_vocab_size)。
Softmax 层:
对 $$logits$$ 应用 $$softmax$$ 激活函数,将其转换为概率分布。
每个时间步的输出表示目标词汇表中每个词的概率。
生成目标序列:
在训练阶段,利用真实目标序列进行监督学习。
在推断阶段,通过贪心搜索或集束搜索生成完整的目标序列。
7.2 编码器-解码器结构的代码实现
EncoderDecoder函数完成编码解码的子任务,就是把编码和解码的流程进行封装实现。
# 编码解码内部函数类 EncoderDecoder 实现分析 # init函数 (self, encoder, decoder, source_embed, target_embed, generator) # 5个成员属性赋值 encoder 编码器对象 decoder 解码器对象 source_embed source端词嵌入层对象 # target_embed target端词嵌入层对象 generator 输出层对象 # forward函数 (self, source, target, source_mask, target_mask) # 1 编码 s.encoder(self.src_embed(source), source_mask) # 2 解码 s.decoder(self.tgt_embed(target), memory, source_mask, target_mask) # 3 输出 s.generator() # 使用EncoderDecoder类来实现编码器-解码器结构 class EncoderDecoder(nn.Module): def __init__(self, encoder, decoder, source_embed, target_embed, generator): # 初始化函数中有5个参数, 分别是编码器对象, 解码器对象, 源数据嵌入函数, 目标数据嵌入函数, 以及输出部分的类别生成器对象 super(EncoderDecoder, self).__init__() # 将参数传入到类中 self.encoder = encoder self.decoder = decoder self.src_embed = source_embed self.tgt_embed = target_embed self.generator = generator def forward(self, source, target, source_mask, target_mask): # 在forward函数中,有四个参数, source代表源数据, target代表目标数据, source_mask和target_mask代表对应的掩码张量 # 在函数中, 将source, source_mask传入编码函数, 得到结果后, # 与source_mask,target,和target_mask一同传给解码函数 return self.generator(self.decode(self.encode(source, source_mask), source_mask, target, target_mask)) def encode(self, source, source_mask): # 编码函数, 以source和source_mask为参数 # 使用src_embed对source做处理, 然后和source_mask一起传给self.encoder return self.encoder(self.src_embed(source), source_mask) def decode(self, memory, source_mask, target, target_mask): # 解码函数, 以memory即编码器的输出, source_mask, target, target_mask为参数 # 使用tgt_embed对target做处理, 然后和source_mask, target_mask, memory一起传给self.decoder return self.decoder(self.tgt_embed(target), memory, source_mask, target_mask)
7.3 Tansformer模型构建过程的代码实现
make_model函数初始化一个一个组件对象(轮子对象),调用EncoderDecoder()函数
# make_model函数实现思路分析 # 函数原型 (source_vocab, target_vocab, N=6, d_model=512, d_ff=2048, head=8, dropout=0.1) # 实例化多头注意力层对象 attn # 实例化前馈全连接对象ff # 实例化位置编码器对象position # 构建 EncoderDecoder对象(Encoder对象, Decoder对象, # source端输入部分nn.Sequential(), # target端输入部分nn.Sequential(), # 线性层输出Generator) # 对模型参数初始化 nn.init.xavier_uniform_(p) # 注意使用 c = copy.deepcopy # 返回model def make_model(source_vocab, target_vocab, N=6, d_model=512, d_ff=2048, head=8, dropout=0.1): c = copy.deepcopy # 实例化多头注意力层对象 attn = MultiHeadedAttention(head=head, embedding_dim=512, dropout=dropout) # 实例化前馈全连接对象ff ff = PositionwiseFeedForward(d_model=d_model, d_ff=d_ff, dropout=dropout) # 实例化 位置编码器对象position position = PositionalEncoding(d_model=d_model, dropout=dropout) # 构建 EncoderDecoder对象 model = EncoderDecoder( # 编码器对象 Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N), # 解码器对象 Decoder(DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout), N), # 词嵌入层 位置编码器层容器 nn.Sequential(Embeddings(d_model, source_vocab), c(position)), # 词嵌入层 位置编码器层容器 nn.Sequential(Embeddings(d_model, target_vocab), c(position)), # 输出层对象 Generator(d_model, target_vocab)) for p in model.parameters(): if p.dim() > 1: nn.init.xavier_uniform_(p) return model
函数调用:
def dm_test_make_model(): source_vocab = 512 target_vocab = 512 N = 6 my_transform_modelobj = make_model(source_vocab, target_vocab, N=N, d_model=512, d_ff=2048, head=8, dropout=0.1) print(my_transform_modelobj) # 假设源数据与目标数据相同, 实际中并不相同 source = target = Variable(torch.LongTensor([[1, 2, 3, 8], [3, 4, 1, 8]])) # 假设src_mask与tgt_mask相同,实际中并不相同 source_mask = target_mask = Variable(torch.zeros(8, 4, 4)) # mydata = my_transform_modelobj(source, target, source_mask, target_mask) print('mydata.shape--->', mydata.shape) print('mydata--->', mydata) if __name__ == '__main__': dm_test_make_model()
7.4 小结
实现编码器-解码器结构的类: EncoderDecoder
类的初始化函数传入5个参数, 分别是编码器对象, 解码器对象, 源数据嵌入函数, 目标数据嵌入函数, 以及输出部分的类别生成器对象
类中共实现三个函数, forward, encode, decode
forward是主要逻辑函数, 有四个参数, source代表源数据, target代表目标数据, source_mask和target_mask代表对应的掩码张量
encode是编码函数, 以source和source_mask为参数
decode是解码函数, 以memory即编码器的输出, source_mask, target, target_mask为参数
实现模型构建函数: make_model
有7个参数,分别是源数据特征(词汇)总数,目标数据特征(词汇)总数,编码器和解码器堆叠数,词向量映射维度,前馈全连接网络中变换矩阵的维度,多头注意力结构中的多头数,以及置零比率dropout
该函数最后返回一个构建好的模型对象