自然语言处理——05 Transformer架构和手写实现
1 Transformer背景介绍
1.1 Transformer 的诞生
- Transformer是一个在自然语言处理领域中引起了革命性变革的模型架构;
- 它首次被提出于2017年的论文《Attention is All You Need》中,由Google的研究团队提出;
- 论文地址:
https://arxiv.org/pdf/1706.03762.pdf
; - 这篇论文开创了一种全新的模型架构,成为了许多自然语言处理任务的基础,如机器翻译、文本摘要、对话生成等;
- 随后许多基于 Transformer 的变种模型也相继涌现,例如 BERT、GPT 等,进一步推动了自然语言处理领域的发展;
- 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 优势
-
相比之前占领市场的LSTM和GRU模型,Transformer有两个显著的优势:
- Transformer能够利用分布式GPU进行并行训练,提升模型训练效率;
- 在分析预测更长的文本时,捕捉间隔较长的语义关联效果更好;
-
RNN、LSTM、Transformer 对从长文本中提取事物特的征效果对比:
- RNN 和 LSTM 在文本长度20~30之后效果下降显著;
- Transformer在文本长度超过40以后也能保持较好效果。
2 Transformer架构
2.1 作用
- 基于 Seq2Seq 架构的 Transformer 模型可以完成NLP领域研究的典型任务,如机器翻译、文本生成等;
- 又可构建预训练语言模型,用于不同任务的迁移学习。
2.2 架构图
-
分为输入、输出、编码器、解码器四大模块:
-
输入部分。负责把原始文本转化为模型能理解的向量,分两步:
- 文本嵌入层(Embedding):把文本里的词/字,转换成固定维度的向量,让模型“看懂”文字
- 位置编码器(Positional Encoding):给向量加入“位置信息”,因为模型本身不理解语序,通过数学公式给不同位置的词打上时间戳,比如让模型区分“我打你”和“你打我”
-
输出部分。把模型计算的向量,转回人类能理解的文本概率,分两步:
- 线性层(Linear):把模型输出的高维向量,映射到“词表维度”(比如词表有 1 万个词,就输出 1 万维向量)
- Softmax 层:把向量转成概率分布(比如“猫”的概率 0.9,“狗”0.1),选出最可能的输出词
-
编码器。把输入文本,编码成包含“语义+位置”的向量,给解码器用
- 由N个编码器层堆叠而成(比如 BERT 用了 12 层),每个编码器层由两个子层连接结构组成
- 第一个子层连接结构包括一个多头自注意力子层和规范化层以及一个残差连接
- 多头注意力(Multi-Head Attention):让模型同时从“多个角度”理解文本(比如“苹果”既可以是水果,也可以是公司),捕捉词与词的复杂关系
- 注意:多头注意力 = 多头自注意力
- 第二个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接
- 前馈网络(Feed Forward):对注意力输出做进一步加工,增强模型表达能力
-
解码器。基于编码器的向量,一步步生成目标文本(比如翻译、对话回复)
- 由N个解码器层堆叠而成,每个解码器层由三个子层连接结构组成
- 第一个子层连接结构包括一个掩码多头注意力子层和规范化层以及一个残差连接
- 掩码多头注意力(Masked Multi-Head Attention):避免生成时“偷看”未来词 ,即强制模型只能看前面生成的词,不能偷看后面,确保生成逻辑合理;
- 第二个子层连接结构包括一个多头注意力子层和规范化层以及一个残差连接
- 第三个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接
2.3 模块之间的连接
-
编码器层之间的连接
-
每个编码器层结构一样(多头注意力→Add&Norm→前馈网络→Add&Norm),层与层首尾相连:上一层编码器的输出,直接作为下一层编码器的输入;
-
比如第 1 层编码器处理完输入文本,输出的向量传给第 2 层,第 2 层再基于它进一步提炼语义,以此类推;
-
每层内部的“Add&Norm”(残差连接 + 层归一化),让信息传递更稳定:
-
残差连接:把层的输入直接加到输出上,避免梯度消失,保证深层网络能有效训练;
-
层归一化:统一数据分布,加速训练,让每层输出适合下一层处理;
-
-
-
解码器层之间的连接
-
解码器层比编码器多了掩码多头注意力(避免生成时偷看未来词),且层间同样是上层输出→下层输入;
-
但解码器还多了和编码器的交互:所有解码器层,都会接收编码器输出的 K、V,让解码器在生成时,能参考输入文本的完整语义;
-
多层交互的意义:
- 下层解码器不仅要处理上层解码器的输出(生成的中间结果),还要结合编码器的全局语义,保证生成内容既符合已生成部分的逻辑,又贴合输入文本的意思;
- 比如翻译时,解码器每一层生成的词,都要参考:① 上一层生成的内容 ② 输入原文的语义,确保翻译准确。
-
3 输入模块
3.1 文本嵌入层
-
文本嵌入(Input Embedding):把文本里的词/字,转换成固定维度的向量,让模型“看懂”文字;
-
代码实现:
import torch import torch.nn as nn import math from torch.autograd import Variable
# 定义词嵌入类,继承自PyTorch的神经网络模块 class Embeddings(nn.Module):def __init__(self, d_model, vocab):# 调用父类构造函数super(Embeddings, self).__init__()# 保存模型维度(词向量维度)self.d_model = d_model# 保存词汇表大小self.vocab = vocab# 创建嵌入层:将词汇表中的每个词映射到d_model维度的向量# nn.Embedding(vocab, d_model)表示创建一个vocab×d_model的嵌入矩阵self.lut = nn.Embedding(vocab, d_model)def forward(self, x):# 前向传播过程:# 1. 通过嵌入层(lut)将输入的词索引转换为词向量# 2. 乘以sqrt(d_model)进行缩放,这是Transformer原论文中的标准化处理# 目的是使嵌入向量的方差更稳定,便于后续计算x = self.lut(x) * math.sqrt(self.d_model)return x
# 测试 # 1 准备输入数据: # 创建一个2×4的张量,表示2个句子,每个句子包含4个词的索引 # 例如[100, 2, 421, 508]表示第一个句子由索引为100、2、421、508的词组成 x = torch.tensor([[100, 2, 421, 508], [491, 998, 1, 221]])# 2 实例化文本词嵌入层: # d_model=512表示每个词将被映射为512维向量 # vocab=1000表示词汇表大小为1000(词索引范围0-999) myembeddings = Embeddings(512, 1000) # 打印嵌入层结构,可看到包含一个嵌入矩阵(lut) print('myembeddings-->', '\n', myembeddings)# 3 将输入数据传入嵌入层进行处理: # 输入形状为[2,4](2个句子,每个4个词) # 输出形状为[2,4,512](每个词被转换为512维向量) embed_res = myembeddings(x) # 打印输出结果的形状和内容 print('embed_res-->', '\n', embed_res.shape, embed_res)
myembeddings--> Embeddings((lut): Embedding(1000, 512) ) embed_res--> torch.Size([2, 4, 512]) tensor([[[-14.5152, -13.2002, -5.5680, ..., 29.4084, 2.3136, -6.9167],[ 14.0747, 13.0510, -8.1570, ..., -7.6437, -34.2573, -35.3612],[ 3.1101, 25.9544, 31.5883, ..., -21.1296, -16.1313, -7.9658],[-12.8753, 6.4940, -13.9052, ..., -27.2177, -10.7502, 12.4219]],[[-18.0274, -6.3029, 0.2113, ..., 24.8638, 23.5917, 4.1037],[ 8.5993, 28.4783, 4.2310, ..., -8.0540, -18.9501, 16.6723],[ 2.8034, -6.9236, 4.2790, ..., 25.7739, -27.4348, 33.3031],[ 49.3233, 2.0055, 35.1686, ..., -13.9049, 4.3302, -61.2292]]],grad_fn=<MulBackward0>)py
3.2 位置编码器
-
位置编码器(Position Encoding):给向量加入“位置信息”;
-
为什么需要加入位置信息?因为当模型直接处理词向量时,它是不懂语序的;
-
比如:
-
“在教室中间,唱我爱中国”这句话里的两个“中”字在不同的位置意思是不同的;
-
“你欠我 100 元,我欠你 100 元”,调换语序后逻辑完全变了;
-
-
但 Transformer 的注意力机制,本身不自带“位置感知”,所以得手动给词向量加“位置特征” ,让模型区分“相同词在不同位置的语义”;
-
-
位置编码(Position Encoding)是怎么实现的?
-
简单说,给每个位置生成一个“位置向量”,和词向量相加,让模型同时拿到“词的语义 + 位置信息”;
-
位置编码函数以当前元素在序列中的位置作为输入,然后使用一组预定义的参数(即位置参数)来生成对应位置的位置向量;
-
这个过程可以表示为:
-
偶数维度(
i
是偶数):PE(pos,2i)=sin(pos/10000(2i/dmodel))PE(pos, 2i) = sin(pos / 10000^{(2i/d_{model})})PE(pos,2i)=sin(pos/10000(2i/dmodel)) -
奇数维度(
i
是奇数):PE(pos,2i+1)=cos(pos/10000(2i/dmodel))PE(pos, 2i+1) = cos(pos / 10000^{(2i/d_{model})})PE(pos,2i+1)=cos(pos/10000(2i/dmodel))
-
-
其中:
- PEPEPE:位置编码函数
- pospospos:当前元素的位置
- iii:嵌入向量的第几个特征(奇数特征、偶数特征)
- dmodeld_{model}dmodel:代表嵌入向量的维度
-
-
人话版:用正弦、余弦函数,给不同位置生成不同的向量,并且让“位置越近的词,向量越像;位置越远的词,向量越不像”,帮模型学出语序关系;
-
-
通过这种方式,Transformer 可以在每个输入元素的嵌入向量中加入其位置信息,从而在训练过程中自动学习到元素间的相对位置关系;
-
代码实现:
# 定义位置编码类,继承自PyTorch的神经网络模块 # 作用:为词向量添加位置信息,解决Transformer模型无法感知词序的问题 class PositionalEncoding(nn.Module):def __init__(self, d_model, dropout=0.1, max_len=5000):super(PositionalEncoding, self).__init__()# 创建dropout层(正则化),用于防止过拟合self.dropout = nn.Dropout(p=dropout)# 初始化位置编码矩阵pe,形状为[max_len, d_model]# max_len表示最大序列长度,d_model表示词向量维度pe = torch.zeros(max_len, d_model)# 生成位置索引矩阵position,形状为[max_len, 1]# 例如max_len=60时,position为[[0], [1], [2], ..., [59]]position = torch.arange(0, max_len).unsqueeze(1)# 计算公式中的指数部分:div_term = 10000^(-2i/d_model)# 这里用指数函数实现,避免直接计算大数幂# 结果形状为[1, d_model/2],因为步长为2,只计算偶数索引div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model))# 计算位置编码的正弦和余弦值# 位置索引矩阵与div_term相乘,形状为[max_len, d_model/2]my_matmulres = position * div_term # [max_len,1] * [1,d_model/2] → [max_len, d_model/2]# 偶数维度(0,2,4...)使用正弦函数pe[:, 0::2] = torch.sin(my_matmulres)# 奇数维度(1,3,5...)使用余弦函数pe[:, 1::2] = torch.cos(my_matmulres)# 增加一个维度,使pe形状变为[1, max_len, d_model]# 便于后续与批次数据(batch)进行广播相加pe = pe.unsqueeze(0)# 将pe注册为模型的缓冲区,不会被视为模型参数,但会随模型一起保存self.register_buffer('pe', pe)def forward(self, x):# x是经过词嵌入后的张量,形状为[batch_size, seq_len, d_model]# 获取输入序列的长度seq_len = x.size()[1]# 将位置编码与词向量相加# 截取与输入序列长度匹配的位置编码部分(pe[:, :seq_len])# Variable(..., requires_grad=False)表示该张量不需要计算梯度x = x + Variable(self.pe[:, :seq_len], requires_grad=False)# 应用dropout后返回return self.dropout(x)
# 测试 # 1 准备输入数据:2个句子,每个句子包含4个词的索引 x = torch.tensor([[100, 2, 421, 508], [491, 998, 1, 221]]) # 2 实例化词嵌入层:每个词将被映射为512维向量,词汇表大小为1000 myembeddings = Embeddings(512, 1000) print('myembeddings-->', '\n', myembeddings) # 打印嵌入层结构 # 3 将输入数据传入嵌入层:[2,4] → [2,4,512] embed_res = myembeddings(x) print('embed_res-->', '\n', embed_res.shape, '\n', embed_res) # 打印嵌入结果的形状和内容 # 4 实例化位置编码层:512维向量,dropout率0.1,最大序列长度60 mypositionalencoding = PositionalEncoding(d_model=512, dropout=0.1, max_len=60) print('mypositionalencoding-->', '\n', mypositionalencoding) # 打印位置编码层结构 # 5 为词向量添加位置信息:[2,4,512] → [2,4,512](形状不变,但内容包含位置信息) pe_res = mypositionalencoding(embed_res) print('添加位置特征以后的x-->', '\n', pe_res.shape) # 打印添加位置信息后的形状
myembeddings--> Embeddings((lut): Embedding(1000, 512) ) embed_res--> torch.Size([2, 4, 512]) tensor([[[ -0.4726, -15.4523, -20.8254, ..., -22.9488, -19.3678, 42.7671],[ 6.3850, -33.8331, 39.0846, ..., -14.6052, 21.3754, -9.6324],[ 2.0856, -36.8561, 34.4633, ..., -11.0786, 0.4189, -3.4014],[ 7.1107, -14.0333, 11.1394, ..., 3.9053, -31.7206, -35.4916]],[[ -5.3564, 11.3095, 30.2699, ..., 14.8927, 29.2233, -25.7789],[ 31.4193, 22.8599, -22.7599, ..., 5.7198, -7.1939, 27.9142],[ 22.2970, -31.5149, 18.2011, ..., -2.7294, -17.8481, -30.6120],[ 0.9254, 17.7495, 2.0863, ..., 9.8266, -20.9246, 34.1658]]],grad_fn=<MulBackward0>) mypositionalencoding--> PositionalEncoding((dropout): Dropout(p=0.1, inplace=False) ) 添加位置特征以后的x--> torch.Size([2, 4, 512])
4 编码器模块
4.1 掩码张量
-
上下三角矩阵基础
-
上三角矩阵:0 元素集中在矩阵上方,形成三角形状(图里左边的矩阵示例,对角线下方全是 0);
-
下三角矩阵:0 元素集中在矩阵下方,形成三角形状(图里右侧的矩阵示例,对角线上方全是 0);
-
-
下三角矩阵的核心作用——解决模型“偷看未来”问题
-
在序列生成任务里(比如语言模型逐字生成文本),模型每一个时间步生成一个字符,但不能提前看到“还没生成的未来字符”;
-
下三角矩阵的掩码逻辑是:
-
使用掩码mark,比如:用 1 表示看不到,0 表示看得见(或反过来定义,核心是区分能看和不能看);
-
让模型在第 t 步生成时,只能看到前 t-1 步的结果 ,未来步骤(t+1、t+2…)的信息被 1 遮蔽,模拟人类“逐字生成、看不到后面内容”的过程;
-
-
-
举个栗子:假设要生成 4 个字符(比如“我、爱、中、国”),分 4 个时间步依次生成,下三角掩码的作用是:
-
第 1 个时间步:要生成第 1 个字符(比如“我”),但此时“爱、中、国” 还没生成,属于“未来信息”,所以用 1 全部遮蔽(模型看不到后面内容,只能基于初始信息预测“我”);
-
第 2 个时间步:要生成第 2 个字符(比如“爱”),此时只能参考“第 1 步生成的‘我’” ,“中、国” 仍被遮蔽(1 盖住);
-
第 3 个时间步:生成“中” ,能参考“我、爱” ,“国” 被遮蔽;
-
第 4 个时间步:生成“国” ,能参考前 3 步的“我、爱、中”;
-
-
代码实现:
-
np.triu(m, k)
函数:可以返回一个上三角矩阵,m
表示传入一个矩阵,k
表示对角线的起始位置(默认取零);
import numpy as np
print('k=1\n', 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)) print('k=0\n', 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)) print('k=-1\n', 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=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]] k=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]] k=-1[[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]]
# 得到下三角矩阵 def subsequent_mask(size):# 产生上三角矩阵my_mask = np.triu(m=np.ones((1, size, size)), k=1).astype('uint8')# 返回下三角矩阵my_mask = torch.from_numpy(1 - my_mask )return my_mask
# 测试 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 自注意力机制
-
自注意力机制,即Q=K=V,公式:
Attention(Q,K,V)=Softmax(QK⊤dk)⋅V \text{Attention}(\boldsymbol{Q}, \boldsymbol{K}, \boldsymbol{V}) = \text{Softmax}\bigg( \frac{\boldsymbol{Q}\boldsymbol{K}^\top}{\sqrt{d_k}} \bigg) \cdot \boldsymbol{V} Attention(Q,K,V)=Softmax(dkQK⊤)⋅V -
代码实现:
# 自注意力机制 def attention(query, key, value, mask=None, dropout=None):# 获取query的最后一个维度大小d_k = query.size()[-1]# 计算注意力分数:query与key的转置进行矩阵乘法,再除以d_k的平方根进行缩放# 这一步是为了防止计算出的分数过大,导致softmax后梯度消失scores = torch.matmul(query, key.transpose(-1, -2)) / math.sqrt(d_k)# 如果提供了mask矩阵,则将mask为0的位置的分数设为一个极小值(-1e9)# 这样在经过softmax后,这些位置的注意力权重会接近0,实现掩码效果if mask is not None:scores = scores.masked_fill(mask == 0, -1e9)# 对分数进行softmax操作,得到注意力权重分布# 这里在最后一个维度上进行softmax,确保每个位置对所有位置的注意力权重之和为1p_attn = F.softmax(scores, dim=-1)# 如果提供了dropout,则对注意力权重应用dropout操作,防止过拟合if dropout is not None:p_attn = dropout(p_attn)# 返回注意力加权后的value(即上下文向量)和注意力权重return torch.matmul(p_attn, value), p_attn
# 测试 d_model = 512 # 词嵌入维度是512维 vocab = 1000 # 词表大小是1000# 输入x是一个使用Variable封装的长整型张量, 形状是2 x 4 # 表示2个句子,每个句子包含4个词的索引(例如:第一个句子的词索引是100, 2, 421, 508) x = Variable(torch.LongTensor([[100, 2, 421, 508],[491, 998, 1, 221]]))# 将词索引转换为词嵌入向量 # Embeddings类会创建一个大小为[vocab, d_model]的嵌入矩阵 # 这里x的形状会从[2, 4]变为[2, 4, 512] my_embeddings = Embeddings(d_model, vocab) x = my_embeddings(x)# 位置编码参数 dropout = 0.1 # 置0比率为0.1 max_len = 60 # 句子最大长度# 为词嵌入添加位置编码 # 位置编码用于给模型提供词的位置信息,解决Transformer模型没有顺序感知能力的问题 my_pe = PositionalEncoding(d_model, dropout, max_len) pe_result = my_pe(x) # 形状仍为[2, 4, 512]# 在自注意力中,query、key、value来自同一输入 # 这里pe_result的形状是[2, 4, 512],分别代表:2个样本,每个样本4个词,每个词512维 query = key = value = pe_resultprint('没有使用mask矩阵对注意力分布进行处理') # 调用注意力函数,不使用mask和dropout attn1, p_attn1 = attention(query, key, value, mask=None, dropout=None) # 输出注意力结果:形状为[2, 4, 512],与输入形状保持一致 print('注意力结果表示attn1--->', attn1.shape, attn1) # 输出注意力权重分布:形状为[2, 4, 4],表示每个词对其他所有词的注意力权重 # 对于第一个样本的第一个词,p_attn1[0,0,:]是它对句子中4个词的注意力权重 print('注意力权重分布p_attn1--->', p_attn1.shape, '\n', p_attn1)print('使用mask对注意力分布进行处理,注意:这里的mask矩阵是一个全零的矩阵') # 创建一个全零的mask矩阵,形状为[2, 4, 4] mask_zero = torch.zeros(2, 4, 4) # 使用全零mask调用注意力函数 attn2, p_attn2 = attention(query, key, value, mask=mask_zero, dropout=None) # 由于mask全为0,所有位置都会被掩码,注意力结果会接近全零 print('注意力结果表示attn2--->', attn2.shape, attn2) # 全零mask会导致所有分数都被设为-1e9,softmax后所有位置权重几乎相等 # 这在实际应用中没有意义,通常mask用于掩盖填充位置或未来信息(如解码器中的掩码) print('注意力权重分布p_attn2--->', p_attn2.shape, '\n', p_attn2)
无
mask
矩阵(attn1
、p_attn1
)-
注意力结果
attn1
(形状torch.Size([2, 4, 512])
)- 维度含义:
[2, 4, 512]
对应 2 个样本、每个样本 4 个词、每个词 512 维的上下文表示; - 计算逻辑:通过
query
(查询)、key
(键)的相似度计算注意力权重,再用权重对value
(值)加权求和,得到融合上下文信息的新表示; - 数值意义:每个词的输出是句子中所有词的“加权混合”,权重由
p_attn1
决定,体现自注意力对不同位置的关注程度;
- 维度含义:
-
注意力权重
p_attn1
(形状torch.Size([2, 4, 4])
)- 维度含义:
[2, 4, 4]
对应 2 个样本、每个样本 4 个词、每个词对 4 个位置的注意力分配(行是“当前词”,列是“被关注词”); - 数值特征:
- 每行(如
[1., 0., 0., 0.]
)表示当前词对自身/其他词的注意力权重,第一行1.
集中在自身(对角线),体现自注意力初始对自身位置的强关注(但后续会随模型学习动态调整); - 本质是
softmax
输出,每行和为1
,反映“当前词该关注句子中哪些位置”,无mask
时所有位置都参与计算;
- 每行(如
- 维度含义:
全零
mask
矩阵(attn2
、p_attn2
)-
关键代码逻辑
if mask is not None:scores = scores.masked_fill(mask == 0, -1e9) # mask 为 0 的位置设为极小值 p_attn = F.softmax(scores, dim=-1) # 极小值经 softmax 后趋近 0
- 全零
mask
会把所有注意力分数(scores
)中mask == 0
的位置(即全部位置)设为-1e9
,导致softmax
后这些位置权重几乎为0
;
- 全零
-
注意力结果
attn2
(形状torch.Size([2, 4, 512])
):由于所有位置的注意力权重趋近0
,value
被“无效化”,输出结果整体数值小且样本内各词的表示趋于一致(如第一句 4 个词输出几乎相同); -
注意力权重
p_attn2
(形状torch.Size([2, 4, 4])
)- 因
scores
被强制置为极小值,softmax
后每行权重均匀分布(如[0.2500, 0.2500, 0.2500, 0.2500]
),每行和仍为1
,但失去了“针对性关注”的意义。 - 这体现
mask
的核心作用:干预注意力分配,强制模型“看不到”某些位置(如训练解码器时,不能提前关注未来词)。
- 因
-
4.3 多头注意力机制
-
多头注意力机制(Multi-Head Attention)核心是解决 “单头注意力提取特征不够全面” 的问题,让模型从多个视角捕捉信息;
- 把模型对文本的“注意力”拆成多个子注意力(比如 8 个头),每个子注意力独立计算 “关注重点”;
- 类比:1 个头 = 1 个人看文本,8 个头 = 8 个人同时看文本,每人关注不同细节,最后汇总所有人的观察;
- 把模型对文本的“注意力”拆成多个子注意力(比如 8 个头),每个子注意力独立计算 “关注重点”;
-
该机制是 Transformer 最核心的组件之一,不管是 Encoder(编码器)还是 Decoder(解码器),都靠它提取特征;
-
作用:
- 特征提取更全面:单头注意力只能从 1 个角度捕捉文本关系(比如“苹果”和“水果”的关联),多头能从多个角度同时捕捉(比如同时关注“苹果-水果”“苹果-价格”“苹果-颜色”等关系);
-
平衡偏差,提升鲁棒性:每个头关注不同细节,能避免“单头注意力的偏见”(比如单头可能过度关注局部,多头能兼顾局部+全局);
-
实现流程:
-
线性变换(Linear):
- 输入文本的特征(比如词嵌入后的向量,形状
[batch_size, seq_len, d_model]
),分别对 Q(查询)、K(键)、V(值) 做线性变换; - 作用:把原始特征投影到新的空间(为后续拆分多头做准备);
- 输入文本的特征(比如词嵌入后的向量,形状
-
view 切分(拆分多头):把 Q、K、V 的特征拆成多个“头”
- 比如:总特征维度
d_model=512
,拆成h=8
个头 → 每个头的维度是512/8=64
; - 数据形状变化:
[batch_size, seq_len, 512]
→[batch_size, seq_len, 8, 64]
,再通过transpose
(维度交换)调整维度顺序(方便后续计算);
transpose
(维度交换 ),是为了让“头维度”和“序列维度”对齐 ,方便注意力计算:- 拆分多头后,数据形状是
[batch_size, seq_len, h, d_k]
,但注意力计算需要[batch_size, h, seq_len, d_k]
(把 “头” 放到第二维); - 用
transpose(1, 2)
交换维度 1(seq_len)和维度 2(h),就能满足计算要求;
- 比如:总特征维度
-
attention 操作(每个头独立计算):
- 每个头单独执行缩放点积注意力(Scaled Dot-Product Attention),计算“当前位置该关注哪些内容”;
- 关键是 “缩放” :用
1/√d_k
(d_k
是每个头的维度,比如 64)缩放点积结果,防止数值过大导致 Softmax 饱和(梯度消失);
-
Concat(合并多头结果):
- 把 8 个头各自计算的注意力结果,拼接回一个大张量;
- 数据形状变化:
[batch_size, seq_len, 8, 64]
→[batch_size, seq_len, 512]
(8 个头×64 维度 = 512 维度);
-
线性层变换(Linear):对拼接后的特征,再做一次线性变换,输出最终的多头注意力结果(形状和输入一致,保证能接入后续网络);
-
-
数据形状变化全流程
-
假设输入是:
-
batch_size=2
(批量大小,一次算 2 条文本) -
seq_len=4
(每条文本有 4 个词) -
d_model=512
(每个词的特征维度) -
h=8
(多头数,拆成 8 个头)
-
-
-
按步骤看形状变化:
步骤 操作 输入形状 输出形状(关键变化) 原因 线性变换 对 Q/K/V 做线性变换 [2, 4, 512]
[2, 4, 512]
(形状不变,维度投影)线性变换不改变维度,只改变特征表示 view 切分多头 拆分 + transpose [2, 4, 512]
[2, 8, 4, 64]
(拆分后调整维度顺序)拆成 8 头,每头 64 维; transpose
把 “头维度” 放到第二维,方便计算注意力计算 Scaled Dot-Product [2, 8, 4, 64]
(Q/K/V)[2, 8, 4, 64]
(每个头的注意力结果)每个头独立计算,输出和输入维度一致的加权特征 Concat 合并 拼接多头结果 [2, 8, 4, 64]
[2, 4, 512]
(合并 8 头×64 维 = 512 维)把 8 个头的结果拼回原维度,保证和输入特征维度对齐 线性层变换 最终线性变换 [2, 4, 512]
[2, 4, 512]
(形状不变,调整特征)调整特征表示,让多头注意力的输出能直接接入后续网络(比如 Feed Forward) -
代码实现:
import copy # 克隆模块函数:创建N个相同的神经网络层(深拷贝) # 用于快速创建多个相同配置的线性层或注意力层 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__()# 确保嵌入维度能被头数整除(512 = 8×64)assert embedding_dim % head == 0, "嵌入维度必须是头数的整数倍"# 每个注意力头的特征维度(512/8=64)self.d_k = embedding_dim // head# 注意力头的数量(通常为8)self.head = head# 创建4个相同的线性层:前3个用于Q、K、V的线性变换,第4个用于最终输出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):# 处理掩码:增加一个维度以适配多头结构# 原始掩码形状可能为[batch_size, seq_len, seq_len]# 增加维度后变为[1, batch_size, seq_len, seq_len],便于广播到所有头if mask is not None:mask = mask.unsqueeze(0)# 获取批次大小(一次处理的样本数)batch_size = query.size(0)# 1. 线性变换并拆分多头# 对Q、K、V分别应用线性层,然后拆分为多个头# 形状变化:[batch_size, seq_len, embedding_dim] # → [batch_size, seq_len, head, d_k] # → [batch_size, head, seq_len, d_k](交换维度便于并行计算)query, key, value = [linear_layer(x).view(batch_size, -1, self.head, self.d_k).transpose(1, 2)for linear_layer, x in zip(self.linears[:3], (query, key, value))]# 2. 计算缩放点积注意力# 所有头并行计算注意力,得到每个头的输出和注意力权重# x形状:[batch_size, head, seq_len, d_k]# self.attn形状:[batch_size, head, seq_len, seq_len]x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)# 3. 合并多头结果# 先交换维度回到[batch_size, seq_len, head, d_k]# 再拼接所有头的结果,恢复为[batch_size, seq_len, embedding_dim]# contiguous()确保内存连续,避免view操作出错x = x.transpose(1, 2).contiguous()\.view(batch_size, -1, self.head * self.d_k)# 4. 最终线性变换# 通过第4个线性层输出,确保输出维度与输入一致return self.linears[-1](x)
# 模型配置参数 d_model = 512 # 词嵌入维度 vocab = 1000 # 词表大小(此处未实际使用,仅作示意)# 模拟经过词嵌入和位置编码后的输入数据 # 形状:[batch_size=2, seq_len=4, embedding_dim=512] # 表示2个样本,每个样本包含4个词,每个词用512维向量表示 pe_result = torch.randn(2, 4, 512)# 注意力配置 head = 8 # 8个注意力头 dropout = 0.1 # Dropout概率# 在自注意力中,Q、K、V来自同一输入 query = key = value = pe_result # 形状均为[2, 4, 512]# 模拟掩码张量(实际应用中可能是下三角矩阵或padding掩码) # 形状:[8, 4, 4],这里全为0表示无遮挡(具体含义取决于attention函数实现) mask = Variable(torch.zeros(8, 4, 4))# 创建多头注意力实例 mha_obj = MultiHeadedAttention(head=8, embedding_dim=512, dropout=0.1) print('多头注意力模块结构-->', '\n', mha_obj)# 执行前向传播 x = mha_obj(query, key, value, mask)# 输出结果信息 print('多头注意力输出形状:', x.shape) # 应保持为[2, 4, 512],与输入维度一致 print('多头注意力输出示例:\n', x) print('注意力权重分布形状:', mha_obj.attn.shape) # 应是[2, 8, 4, 4],表示每个头对序列的关注程度
多头注意力模块结构--> MultiHeadedAttention((linears): ModuleList((0-3): 4 x Linear(in_features=512, out_features=512, bias=True))(dropout): Dropout(p=0.1, inplace=False) ) 多头注意力输出形状: torch.Size([2, 4, 512]) 多头注意力输出示例:tensor([[[ 0.0749, -0.2319, -0.1561, ..., 0.1854, -0.4134, -0.2787],[ 0.1043, -0.3432, -0.2507, ..., 0.1662, -0.3766, -0.1641],[ 0.1895, -0.3838, -0.1551, ..., 0.1585, -0.3339, -0.2875],[ 0.1057, -0.2786, -0.3286, ..., 0.0901, -0.2358, -0.2145]],[[ 0.0437, 0.0394, -0.0415, ..., -0.2013, 0.2189, -0.3542],[-0.0181, 0.1286, 0.0449, ..., -0.2191, 0.2135, -0.3262],[ 0.0374, 0.1004, -0.0203, ..., -0.2034, 0.2199, -0.4733],[-0.0436, 0.1673, -0.0374, ..., -0.3164, 0.1513, -0.2991]]],grad_fn=<ViewBackward0>) 注意力权重分布形状: torch.Size([2, 8, 4, 4])
4.4 前馈全连接层
-
前馈全连接层(Feed - Forward Network)是 Transformer 编码器和解码器中,在多头注意力层之后的一个**两层线性变换 + 非线性激活(通常是 ReLU)**的全连接网络 ,公式一般可简单表示为:
FFN(x)=max(0,xW1+b1)W2+b2 \text{FFN}(x) = \max(0, xW_1 + b_1)W_2 + b_2 FFN(x)=max(0,xW1+b1)W2+b2- 其中 xxx 是多头注意力层的输出,W1、b1、W2、b2W_1、b_1、W_2、b_2W1、b1、W2、b2 是可学习的参数,先经过一次线性变换 + ReLU 激活,再经过一次线性变换得到结果;
-
作用
-
补充注意力机制的表达能力
- 注意力机制(多头注意力)主要擅长捕捉序列中的全局依赖关系(比如文本里词与词远距离的关联),但对局部特征的非线性变换和复杂组合能力有限;
- 前馈全连接层通过两层线性变换 + 非线性激活,能对注意力输出的特征做更灵活的非线性变换,挖掘特征里更细致的模式,增强模型对复杂过程的拟合能力;
-
增加模型容量,强化特征提取:
-
Transformer 整体架构里,多头注意力层负责“信息交互”,前馈全连接层负责“特征加工”;
-
两层网络的设计,让模型可以在不改变序列长度和维度的前提下,进一步升维→非线性变换→降维(比如从 dmodeld_{model}dmodel 升到 4dmodel4d_{model}4dmodel 再降回 dmodeld_{model}dmodel ),拓展模型的表示空间,提升对复杂任务的建模能力;
-
-
保持网络简洁与计算高效:虽然是两层全连接,但结构简单、计算清晰,在多头注意力捕捉到全局关联后,快速对局部特征做精细调整,和注意力层配合,让 Transformer 既能看全局又能抓细节,且整体计算复杂度可控;
-
-
代码实现:
# 前馈全连接层 # 作用:对多头注意力输出的特征进行非线性变换和维度调整 class PositionwiseFeedForward(nn.Module):def __init__(self, d_model, d_ff, dropout=0.1):super(PositionwiseFeedForward, self).__init__()# 第一层线性变换:将输入从模型维度(d_model)映射到更高维度(d_ff)# 通常d_ff是d_model的4倍(如512→2048),目的是增加模型表达能力self.w1 = nn.Linear(d_model, d_ff)# 第二层线性变换:将高维度特征映射回原始模型维度(d_model)self.w2 = nn.Linear(d_ff, d_model)# Dropout层:防止过拟合,训练时随机丢弃部分神经元self.dropout = nn.Dropout(p=dropout)def forward(self, x):# 前向传播过程:升维→非线性激活→dropout→降维# 1. 第一层线性变换:升维操作# 输入形状:[batch_size, seq_len, d_model]# 输出形状:[batch_size, seq_len, d_ff]x = self.w1(x)# 2. ReLU非线性激活:引入非线性特征转换能力# 作用:过滤掉负值,保留重要特征,增加模型非线性表达能力x = F.relu(x)# 3. Dropout:训练时随机丢弃部分特征(比例由dropout参数决定)# 防止模型过度依赖某些特定特征,增强泛化能力x = self.dropout(x)# 4. 第二层线性变换:降维操作,恢复到原始模型维度# 输出形状:[batch_size, seq_len, d_model](与输入维度保持一致)x = self.w2(x)return x
# 实例化并测试前馈全连接层 # 1. 创建前馈层对象:输入维度512,中间维度1024,dropout概率0.1 # 这里d_model=512(Transformer常用维度),d_ff=1024(中间维度) pff = PositionwiseFeedForward(512, 1024) print('前馈全连接层结构-->', '\n', pff)# 2. 模拟输入数据:多头注意力层的输出 # 形状:[2, 4, 512] 表示: # - batch_size=2(2个样本) # - seq_len=4(每个样本包含4个词) # - d_model=512(每个词的特征维度) x = torch.randn(2, 4, 512)# 3. 执行前向传播 x = pff(x)# 4. 输出结果信息 # 输出形状应保持为[2, 4, 512],与输入维度一致(保证Transformer各层维度兼容) print('前馈全连接层输出形状-->', x.shape) print('前馈全连接层输出示例-->\n', x)
前馈全连接层结构--> PositionwiseFeedForward((w1): Linear(in_features=512, out_features=1024, bias=True)(w2): Linear(in_features=1024, out_features=512, bias=True)(dropout): Dropout(p=0.1, inplace=False) ) 前馈全连接层输出形状--> torch.Size([2, 4, 512]) 前馈全连接层输出示例-->tensor([[[ 0.1485, 0.0773, 0.0564, ..., 0.0442, -0.0356, -0.3529],[ 0.1291, 0.3525, -0.3547, ..., -0.4741, 0.1650, -0.4423],[ 0.4230, 0.4983, -0.0499, ..., 0.0282, -0.2800, -0.0464],[-0.2525, 0.1127, -0.3050, ..., -0.0719, 0.1438, -0.2287]],[[ 0.2149, -0.0096, -0.1444, ..., -0.2483, -0.1202, -0.0784],[-0.0651, -0.1707, -0.1879, ..., -0.2209, -0.0517, 0.1598],[-0.0598, 0.0573, -0.3793, ..., -0.0255, 0.2486, 0.2039],[ 0.2349, 0.2064, 0.0353, ..., -0.0128, 0.2795, -0.1182]]],grad_fn=<ViewBackward0>)
4.5 规范化层
-
规范化层(Normalization Layer):对神经网络每层的输出做归一化 ,让输出的均值接近 0,标准差接近 1;
-
作用
-
防止梯度消失/爆炸
-
痛点:深度网络训练时,反向传播的梯度会因为层数深,要么“越来越小(梯度消失)”,要么“越来越大(梯度爆炸)”;
-
规范化的作用:通过归一化,让每层输出的分布更稳定,梯度传播更顺畅;
-
-
加速训练收敛
-
痛点:如果每层输入分布不稳定,模型需要反复适应不同尺度的输入,训练会很慢;
-
规范化的作用:让每层输入分布 “标准化”,模型不用再花大量时间适配分布,训练收敛速度会加快;
-
-
-
计算方式:归一化 + 可学习的缩放平移
-
基础归一化:对每个样本的特征维度,计算均值(mean)和方差(var),然后做归一化:
normed=x−mean(x)var(x)+ϵ(加ϵ是防止分母为0)\text{normed} = \frac{x - \text{mean}(x)}{\sqrt{\text{var}(x) + \epsilon}}(加 \epsilon 是防止分母为 0)normed=var(x)+ϵx−mean(x)(加ϵ是防止分母为0) -
可学习的缩放平移:归一化后的数据可能丢失一些“独特性”,所以引入可学习的参数(缩放因子 γ\gammaγ 、平移因子 β\betaβ ),对归一化结果做调整:
y=γ⋅normed+βy = \gamma \cdot \text{normed} + \betay=γ⋅normed+β- 这样既保证分布稳定,又保留模型学习特征的灵活性;
-
-
代码实现:
# 规范化层 # 作用:对神经网络层的输出进行归一化,稳定数据分布,加速模型训练 class LayerNorm(nn.Module):def __init__(self, features, eps=1e-6):super(LayerNorm, self).__init__()# 可学习的缩放因子(γ):初始化为1,用于调整归一化后的标准差self.a2 = nn.Parameter(torch.ones(features))# 可学习的偏移因子(β):初始化为1,用于调整归一化后的均值self.b2 = nn.Parameter(torch.ones(features))# 极小值(ε):防止计算标准差时出现除零错误self.eps = epsdef forward(self, x):# 1. 计算特征维度的均值# dim=-1:在最后一个维度(特征维度)上计算均值# keepdims=True:保持维度不变,便于后续广播运算mean = x.mean(dim=-1, keepdims=True)# 2. 计算特征维度的标准差std = x.std(dim=-1, keepdims=True)# 3. 执行归一化操作并应用可学习参数# (x-mean)/(std+self.eps):将数据标准化到均值为0、标准差为1# self.a2 * ... + self.b2:通过可学习参数调整分布,保留特征表达能力x = self.a2 * (x - mean) / (std + self.eps) + self.b2return x
# 1. 实例化层归一化对象 # features=512:表示输入的特征维度为512(与Transformer的d_model一致) mylayernorm = LayerNorm(512) print('层归一化模块结构--->', mylayernorm)# 2. 模拟输入数据:通常是多头注意力或前馈层的输出 # 形状:[2, 4, 512] 表示 # - batch_size=2(2个样本) # - seq_len=4(每个样本包含4个词) # - features=512(每个词的特征维度) pe_result = torch.randn(2, 4, 512)# 3. 执行层归一化操作 layernorm_result = mylayernorm(pe_result)# 4. 输出结果信息 # 输出形状保持不变:[2, 4, 512],与输入维度一致 print('归一化后结果--->', '\n', layernorm_result) print('归一化后形状--->', layernorm_result.shape)
层归一化模块结构---> LayerNorm() 归一化后结果---> tensor([[[ 1.2330, 1.4901, 2.3851, ..., -0.4475, 1.3985, 1.4327],[-1.1817, 0.1999, 1.6270, ..., 2.4579, 1.4098, 1.1370],[ 1.2047, 1.8635, 0.9725, ..., 1.4474, 0.0658, 2.0698],[-0.6319, 1.6903, -0.6223, ..., 1.5820, 0.7415, 2.0514]],[[ 1.4775, 1.6501, -0.1401, ..., 1.5139, 0.0597, 1.9987],[-0.3583, 0.5468, 0.5537, ..., 1.0000, 0.6599, 0.1459],[ 1.6366, 0.3432, 0.2239, ..., 1.5737, 1.7353, -0.1689],[ 1.2552, 2.7511, 1.0337, ..., 1.5086, -0.0759, -1.0661]]],grad_fn=<AddBackward0>) 归一化后形状---> torch.Size([2, 4, 512])
4.6 子层连接结构
-
子层连接结构(Sublayer Connection) = 子层(多头注意力层 / 前馈全连接层) + 规范化层(Layer Norm) + 残差连接(Residual Connection);
- 即Transformer 编码器/解码器里的“Add & Norm”结构;
-
作用:让网络在训练深层结构时更稳定 ,同时复用原始特征 ,避免信息丢失;
-
设计思想:
-
残差连接(Residual Connection):把子层的输入
x
和输出f(x)
相加,即x + f(x)
;-
网络学习的是 输入到输出的差异(残差),而不是全新的输出,这样梯度反向传播时更顺畅(解决深度网络梯度消失问题);
-
同时保留了原始输入信息,让网络在学习新特征时,不丢弃原始特征,从而缓解深度网络的退化问题(层数深但效果差);
-
-
和规范化层的配合:相加后接 规范化层(Layer Norm),进一步稳定数据分布,让训练更高效;
-
-
以 Transformer 编码器的一层为例:
-
输入:前一层的输出
x
(形状[batch, seq_len, d_model]
); -
子层:先过
多头注意力层
(输出f1(x)
),再过前馈全连接层
(输出f2(x)
); -
子层连接:
-
多头注意力层后:
x + f1(x)
→ 规范化 → 输出x1
-
前馈全连接层后:
x1 + f2(x1)
→ 规范化 → 输出x2
-
-
效果:原始特征
x
被不断 “残差复用”,网络能同时学习 “原始特征 + 新特征”,训练更稳定、效果更好。
-
-
代码实现:
# 子层连接结构 # 作用:实现残差连接(Residual Connection)和层归一化,解决深层网络训练问题 class SublayerConnection(nn.Module):def __init__(self, size, dropout=0.1):super(SublayerConnection, self).__init__()# 层归一化模块:用于稳定数据分布# size=512表示输入特征维度,与d_model保持一致self.norm = LayerNorm(features=size)# Dropout层:随机丢弃部分特征,防止过拟合self.dropout = nn.Dropout(p=dropout)def forward(self, x, sublayer):# 前向传播逻辑:残差连接 + 归一化 + 子层计算# 1. self.norm(x):先对输入做层归一化,稳定输入分布# 2. sublayer(...):将归一化结果传入子层(如多头注意力或前馈层)处理# 3. self.dropout(...):对子层输出做dropout# 4. x + ...:残差连接,将原始输入与子层输出相加,保留原始信息x = x + self.dropout(sublayer(self.norm(x)))return x
# 测试子层连接结构 # 1. 配置参数:特征维度512(与Transformer的d_model一致) size = 512# 2. 实例化子层连接对象 my_sublayerconnection = SublayerConnection(size) print('子层连接结构--->', '\n', my_sublayerconnection)# 3. 准备输入数据 # 形状:[2, 4, 512] 表示 # - batch_size=2(2个样本) # - seq_len=4(每个样本包含4个词) # - d_model=512(每个词的特征维度) x = torch.randn(2, 4, 512)# 4. 定义子层函数(此处使用多头注意力作为子层) # 4.1 创建掩码张量:控制注意力可见范围 mask = Variable(torch.zeros(8, 4, 4)) # 8个头,序列长度4×4的掩码# 4.2 实例化多头注意力对象 my_mha = MultiHeadedAttention(head=8, embedding_dim=512, dropout=0.1)# 4.3 构建子层函数:用lambda定义一个接收x并返回多头注意力结果的函数 # 这里是自注意力场景,所以Q=K=V=x,同时传入mask sublayer = lambda x: my_mha(x, x, x, mask)# 5. 执行子层连接的前向传播 x = my_sublayerconnection(x, sublayer)# 6. 输出结果信息 # 输出形状保持为[2, 4, 512],与输入维度一致,保证网络层可以堆叠 print('子层连接输出形状-->', x.shape) print('子层连接输出示例-->\n', x)
子层连接结构---> SublayerConnection((norm): LayerNorm()(dropout): Dropout(p=0.1, inplace=False) ) 子层连接输出形状--> torch.Size([2, 4, 512]) 子层连接输出示例-->tensor([[[ 0.7640, -1.4482, -0.7880, ..., -2.8398, -0.5970, -0.9483],[ 0.7543, 1.1868, -0.6065, ..., 0.3826, -1.9023, 2.3471],[ 0.2556, -0.3880, -0.6368, ..., -0.3626, -2.2249, -0.0231],[-0.1966, 1.5248, 0.5996, ..., -1.2028, -0.5463, 0.0258]],[[-1.9931, 1.7534, 1.2699, ..., 0.0704, -0.0749, 1.1665],[-1.1254, 0.2871, -0.4503, ..., -0.0401, -2.0071, 0.3882],[ 1.2936, -0.2862, 0.7176, ..., -0.4722, 0.5769, 1.0802],[-0.2146, -0.6041, -0.7175, ..., -0.7689, -1.1698, 0.3052]]],grad_fn=<AddBackward0>)
4.7 编码器层
-
见
2.2 架构图
中的解释; -
代码实现:
# 编码器层:构成编码器的基本单元 # 每个编码器层包含两个核心子层:多头自注意力层和前馈全连接层 class EncoderLayer(nn.Module):def __init__(self, size, self_attn, feed_forward, dropout):super(EncoderLayer, self).__init__()# 多头自注意力机制实例(处理序列内部依赖)self.self_attn = self_attn# 前馈全连接层实例(处理非线性特征变换)self.feed_forward = feed_forward# 特征维度(与d_model一致,通常为512)self.size = size# 克隆两个子层连接结构:分别用于连接注意力层和前馈层# 每个子层连接包含残差连接和层归一化self.sublayer = clones(SublayerConnection(size, dropout), 2)def forward(self, x, mask):# 第一个子层:多头自注意力层# 使用lambda函数包装自注意力调用(需传入Q=K=V=x和mask)# 输入x经过自注意力处理后,通过子层连接(残差+归一化)得到新特征x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))# 第二个子层:前馈全连接层# 前馈层只需传入x一个参数,可直接作为子层函数# 注意力输出经过前馈层和子层连接,得到最终输出x = self.sublayer[1](x, self.feed_forward)return x
# 测试编码器层功能 # 1. 准备输入数据:经过词嵌入和位置编码后的特征 # 形状:[2, 4, 512] 表示 # - batch_size=2(2个样本) # - seq_len=4(每个样本含4个词) # - d_model=512(每个词的特征维度) pe_result = torch.randn(2, 4, 512)# 2. 创建注意力掩码:控制注意力可见范围 # 此处为全0掩码(表示无遮挡,适用于编码器的自注意力) mask = Variable(torch.zeros(8, 4, 4)) # 8个头,4×4的序列掩码# 3. 实例化多头注意力机制(编码器的核心组件) my_mha = MultiHeadedAttention(head=8, embedding_dim=512, dropout=0.1)# 4. 实例化前馈全连接层(编码器的第二个核心组件) d_model, d_ff = 512, 1024 # d_ff通常为d_model的2-4倍 my_positionwisefeedforward = PositionwiseFeedForward(d_model, d_ff)# 5. 实例化编码器层:组合注意力层和前馈层 myencoderlayer = EncoderLayer(size=512, # 特征维度self_attn=my_mha, # 多头自注意力实例feed_forward=my_positionwisefeedforward, # 前馈层实例dropout=0.1 # dropout概率 ) print('编码器层结构-->', '\n', myencoderlayer)# 6. 执行编码器层的前向传播 x = myencoderlayer(pe_result, mask)# 7. 输出结果信息 # 输出形状保持为[2, 4, 512],与输入维度一致,支持多层堆叠 print('编码器层输出形状-->', x.shape) print('编码器层输出示例-->\n', x)
4.8 编码器
-
由N个编码器层堆叠而成;
-
代码实现:
class Encoder(nn.Module):def __init__(self, layer, N):"""初始化编码器:param layer: 单个编码器层(EncoderLayer)的实例,作为基础单元:param N: 编码器层的数量,即堆叠多少个相同的 EncoderLayer"""super(Encoder, self).__init__()# 使用 clones 函数克隆 N 个相同的编码器层,组成编码器的多层结构self.layers = clones(layer, N)# 编码器最终的归一化层,对所有编码器层的输出做最后的归一化self.norm = LayerNorm(layer.size)def forward(self, x, mask):"""编码器前向传播逻辑:param x: 输入的张量,形状通常为 [batch_size, seq_len, d_model],代表序列的特征表示:param mask: 掩码张量,用于遮挡不需要关注的位置(比如padding位置),形状需适配注意力机制:return: 经过多层编码器处理并归一化后的输出张量"""# 依次通过每个编码器层,逐层处理输入 xfor layer in self.layers:x = layer(x, mask)# 对多层编码器的最终输出做归一化,稳定数据分布,便于后续处理return self.norm(x)
# 1. 准备输入数据 # 模拟经过词嵌入和位置编码后的张量,形状为 [2, 4, 512] # 2 是 batch_size(批次大小),4 是序列长度,512 是特征维度(d_model) pe_result = torch.randn(2, 4, 512) # 2. 实例化多头注意力机制对象 # 定义掩码张量,这里模拟注意力机制中可能用到的掩码,形状需根据实际任务调整 # Variable 在现代 PyTorch 中可直接用张量,这里为了兼容示例写法 mask = torch.zeros(8, 4, 4) # 建议用 torch.zeros 替代 Variable,现代 PyTorch 已融合相关功能 # 创建多头注意力实例,8 个头,嵌入维度 512,dropout 概率 0.1 my_mha = MultiHeadedAttention(head=8, embedding_dim=512, dropout=0.1) # 3. 实例化前馈全连接层(PositionwiseFeedForward) d_model, d_ff = 512, 1024 # d_model 是特征维度,d_ff 是前馈层中间维度 my_positionwisefeedforward = PositionwiseFeedForward(d_model, d_ff) # 4. 实例化单个编码器层(EncoderLayer) # 使用 copy.deepcopy 深拷贝基础层的组件,确保各层参数独立 my_encoderlayer = EncoderLayer(size=512, # 特征维度,与 d_model 一致self_attn=copy.deepcopy(my_mha), # 多头注意力机制实例,深拷贝避免参数共享feed_forward=copy.deepcopy(my_positionwisefeedforward), # 前馈全连接层实例,深拷贝dropout=0.1 # dropout 概率 )# 5. 实例化编码器(Encoder) N = 6 # 编码器层的数量,即堆叠 6 个 EncoderLayer,模拟 Transformer 常见的层数 myencoder = Encoder(my_encoderlayer, N) print('myencoder--->', myencoder) # 打印编码器的结构,查看各层组件# 6. 给编码器喂数据,执行前向传播 encoder_result = myencoder(pe_result, mask) print('encoder_result--->', encoder_result.shape, encoder_result) # 输出结果形状应该与输入兼容,最终为 [2, 4, 512](经过多层处理和归一化后)
myencoder--->Encoder((layers): ModuleList((0-5): 6 x EncoderLayer((self_attn): MultiHeadedAttention((linears): ModuleList((0-3): 4 x Linear(in_features=512, out_features=512, bias=True))(dropout): Dropout(p=0.1, inplace=False))(feed_forward): PositionwiseFeedForward((w1): Linear(in_features=512, out_features=1024, bias=True)(w2): Linear(in_features=1024, out_features=512, bias=True)(dropout): Dropout(p=0.1, inplace=False))(sublayer): ModuleList((0-1): 2 x SublayerConnection((norm): LayerNorm()(dropout): Dropout(p=0.1, inplace=False)))))(norm): LayerNorm() ) encoder_result--->torch.Size([2, 4, 512]) tensor([[[ 0.7874, 0.0152, -0.1616, ..., 1.0391, 1.9653, 0.9493],[-0.0702, 0.3721, 0.5866, ..., 1.0290, 1.9238, 0.9333],[ 0.2720, 0.4404, 0.1152, ..., 0.8435, 2.5630, 0.7451],[-0.4498, 0.4163, -0.4528, ..., 0.9210, 2.3571, 1.0598]],[[-0.1373, -0.0045, -0.3837, ..., 0.6510, 2.6839, 1.0667],[ 0.3194, -0.1602, -0.0055, ..., 1.3445, 2.3394, 1.1189],[ 0.2313, 0.1300, 0.2165, ..., 0.9906, 2.7832, 0.7643],[ 1.0796, 0.1260, 0.7240, ..., 1.2957, 2.1473, 0.1576]]],grad_fn=<AddBackward0>)
5 解码器模块
5.1 解码器层
-
见
2.2 架构图
中的解释; -
代码实现:
# 解码器层 class DecoderLayer(nn.Module):def __init__(self, size, self_attn, src_attn, feed_forward, dropout):"""初始化解码器层:param size: 特征维度(通常等于 d_model,如 512 ),用于层归一化等操作:param self_attn: 多头自注意力机制实例,用于处理目标序列内部的依赖关系:param src_attn: 多头注意力机制实例,用于建立目标序列与编码器输出(memory)的关联(交叉注意力):param feed_forward: 前馈全连接层实例,用于对特征进行非线性变换:param dropout: dropout 概率,用于随机丢弃特征防止过拟合"""super(DecoderLayer, self).__init__()self.size = size # 记录特征维度,方便后续使用# 目标序列自注意力,处理目标序列内部的上下文关系,会配合 target_mask 防止偷看未来信息self.self_attn = self_attn# 编码器-解码器交叉注意力,让解码器关注编码器输出的语义信息(memory)self.src_attn = src_attn# 前馈全连接层,对注意力输出的特征进一步做非线性变换self.feed_forward = feed_forward# 克隆 3 个子层连接结构,分别用于衔接“目标序列自注意力”“编码器-解码器交叉注意力”“前馈全连接层”# 每个子层连接包含残差连接和层归一化,稳定训练过程、缓解梯度消失self.sublayer = clones(SublayerConnection(size, dropout), 3)def forward(self, x, memory, source_mask, target_mask):"""解码器层前向传播逻辑:param x: 目标序列的特征表示,形状一般为 [batch_size, target_seq_len, d_model]:param memory: 编码器的输出,即中间语义张量,形状为 [batch_size, source_seq_len, d_model]:param source_mask: 源序列掩码,用于遮挡源序列中无效的位置(如 padding 部分):param target_mask: 目标序列掩码,用于遮挡目标序列中无效位置和未来位置(防止解码器偷看未生成内容):return: 经过解码器层处理后的特征张量,形状与输入 x 一致"""# 简化表示编码器输出,方便后续使用m = memory # 第一个子层连接:处理目标序列自注意力# 用 lambda 函数封装自注意力的调用,传入目标序列掩码 target_mask,让自注意力仅关注目标序列的历史信息x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, target_mask))# 第二个子层连接:处理编码器-解码器交叉注意力# 用 lambda 函数封装交叉注意力的调用,传入源序列掩码 source_mask,让解码器关注源序列有效位置# 这里 query 是目标序列特征 x,key 和 value 是编码器输出 memory,建立二者的关联x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, source_mask))# 第三个子层连接:处理前馈全连接层,对特征做非线性变换# 前馈层不需要额外掩码等复杂参数,直接传入调用即可x = self.sublayer[2](x, self.feed_forward)return x
# 1. 准备目标序列输入数据 # 模拟目标序列经过词嵌入和位置编码后的特征,形状 [2, 4, 512] # 2 是 batch_size,4 是目标序列长度,512 是特征维度(d_model) pe_result = torch.randn(2, 4, 512)# 2. 实例化多头注意力机制对象,用于自注意力和交叉注意力 # 定义源序列掩码和目标序列掩码,实际任务中需根据场景设置有效遮挡 # 现代 PyTorch 中 Variable 可直接用张量,这里为兼容示例保留写法,建议替换为 torch.zeros source_mask = torch.zeros(8, 4, 4) target_mask = torch.zeros(8, 4, 4) # 实例化多头注意力,8 个头、特征维度 512、dropout 0.1,同时用于自注意力和交叉注意力(演示用,实际可按需配置不同实例) self_attn = src_attn = MultiHeadedAttention(head=8, embedding_dim=512, dropout=0.1)# 3. 实例化前馈全连接层 d_model, d_ff = 512, 1024 # d_model 是特征维度,d_ff 是前馈层中间维度 ff = PositionwiseFeedForward(d_model, d_ff)# 4. 实例化解码器层 my_decoderlayer = DecoderLayer(size=512, # 特征维度,与 d_model 一致self_attn=self_attn, # 目标序列自注意力实例src_attn=src_attn, # 编码器-解码器交叉注意力实例feed_forward=ff, # 前馈全连接层实例dropout=0.1 # dropout 概率 ) print('my_decoderlayer--->\n', my_decoderlayer) # 打印解码器层结构,查看内部组件# 5. 准备编码器输出(中间语义张量 memory) # 模拟编码器输出,形状 [2, 4, 512],与编码器的输出维度匹配 memory = torch.randn(2, 4, 512)# 6. 执行解码器层前向传播 dl_result = my_decoderlayer(pe_result, memory, source_mask, target_mask)# 7. 输出结果信息 # 输出形状应与输入 x 一致,为 [2, 4, 512],表示经过解码器层处理后的特征 print('dl_result--->\n', dl_result.shape, dl_result)
my_decoderlayer--->DecoderLayer((self_attn): MultiHeadedAttention((linears): ModuleList((0-3): 4 x Linear(in_features=512, out_features=512, bias=True))(dropout): Dropout(p=0.1, inplace=False))(src_attn): MultiHeadedAttention((linears): ModuleList((0-3): 4 x Linear(in_features=512, out_features=512, bias=True))(dropout): Dropout(p=0.1, inplace=False))(feed_forward): PositionwiseFeedForward((w1): Linear(in_features=512, out_features=1024, bias=True)(w2): Linear(in_features=1024, out_features=512, bias=True)(dropout): Dropout(p=0.1, inplace=False))(sublayer): ModuleList((0-2): 3 x SublayerConnection((norm): LayerNorm()(dropout): Dropout(p=0.1, inplace=False))) ) dl_result--->torch.Size([2, 4, 512]) tensor([[[-0.2673, -0.7484, -0.8648, ..., 1.1468, 1.0752, -0.3081],[ 0.0218, 0.3388, -2.2474, ..., 0.4264, -1.0780, -0.5149],[ 1.3638, 1.1331, -1.4108, ..., -1.7524, -1.3423, 2.2540],[ 2.5196, 1.1510, -1.8040, ..., 1.2455, 0.9219, -0.2100]],[[-0.2532, -0.6766, -1.8363, ..., 0.7334, 0.7155, 2.3199],[ 0.8584, 0.6730, 0.8297, ..., -1.3669, 0.9678, -1.0209],[ 2.8984, 0.4378, 0.9591, ..., 0.9279, 0.0485, -0.4845],[-0.0297, 0.4177, -0.3382, ..., 1.2077, 0.2602, 1.1394]]],grad_fn=<AddBackward0>)
5.2 解码器
-
由N个解码器层堆叠而成;
-
代码实现:
# 解码器 class Decoder(nn.Module):def __init__(self, layer, N):super(Decoder, self).__init__()# 克隆N个相同的解码器层# 每个解码器层结构相同但参数独立,用于逐层提取特征self.layers = clones(layer, N)# 解码器最终的归一化层:对所有解码器层的输出做最终归一化self.norm = LayerNorm(layer.size)def forward(self, x, memory, source_mask, target_mask):"""解码器前向传播参数说明:x: 目标序列的特征(如目标语言的词嵌入+位置编码)形状: [batch_size, target_seq_len, d_model]memory: 编码器的输出(中间语义张量)形状: [batch_size, source_seq_len, d_model]source_mask: 源序列掩码(遮挡源序列中的padding位置)target_mask: 目标序列掩码(遮挡目标序列中的padding和未来位置)返回:经过多层解码后的特征,形状与x一致"""# 依次通过每个解码器层,逐层处理输入for layer in self.layers:# 每个解码器层需要四个输入:# 目标序列特征x、编码器输出memory、源掩码、目标掩码x = layer(x, memory, source_mask, target_mask)# 对最终输出做归一化,稳定数据分布return self.norm(x)
# 深拷贝工具:确保各层参数独立(避免共享权重) c = copy.deepcopy# 1. 准备目标序列输入数据 # 模拟目标序列经过词嵌入和位置编码后的特征 # 形状: [2, 4, 512] 表示 # - batch_size=2(2个样本) # - target_seq_len=4(目标序列长度为4) # - d_model=512(特征维度) pe_result = torch.randn(2, 4, 512)# 2. 准备掩码和注意力组件 # 2.1 掩码定义: # source_mask: 源序列掩码(用于编码器-解码器注意力) # target_mask: 目标序列掩码(用于解码器自注意力,含未来位置遮挡) source_mask = Variable(torch.zeros(8, 4, 4)) # 8个头,源序列掩码 target_mask = Variable(torch.zeros(8, 4, 4)) # 8个头,目标序列掩码(实际应为下三角矩阵)# 2.2 实例化注意力组件: # self_attn: 解码器自注意力(处理目标序列内部依赖) # src_attn: 编码器-解码器交叉注意力(关联源序列和目标序列) self_attn = src_attn = MultiHeadedAttention(head=8, embedding_dim=512, dropout=0.1)# 3. 实例化前馈全连接层 d_model, d_ff = 512, 1024 # d_ff为中间维度(通常是d_model的4倍) ff = PositionwiseFeedForward(d_model, d_ff)# 4. 实例化单个解码器层(DecoderLayer) # 参数说明: # - size=512: 特征维度 # - self_attn: 解码器自注意力 # - src_attn: 交叉注意力 # - ff: 前馈层 # - dropout=0.1: 防过拟合 my_decoderlayer = DecoderLayer(size=512,self_attn=c(self_attn), # 深拷贝确保参数独立src_attn=c(src_attn), # 深拷贝确保参数独立feed_forward=c(ff), # 深拷贝确保参数独立dropout=0.1 )# 5. 准备编码器输出(中间语义张量memory) # 形状: [2, 4, 512],与编码器输出维度一致 memory = torch.randn(2, 4, 512)# 6. 实例化解码器:堆叠6个解码器层(与原论文一致) my_decoder = Decoder(my_decoderlayer, N=6) print('解码器结构--->\n', my_decoder)# 7. 执行解码器前向传播 decoder_result = my_decoder(pe_result, memory, source_mask, target_mask)# 8. 输出结果信息 # 输出形状保持为[2, 4, 512],与输入目标序列维度一致 print('解码器输出形状--->', decoder_result.shape) print('解码器输出示例-->\n', decoder_result)
解码器结构--->Decoder((layers): ModuleList((0-5): 6 x DecoderLayer((self_attn): MultiHeadedAttention((linears): ModuleList((0-3): 4 x Linear(in_features=512, out_features=512, bias=True))(dropout): Dropout(p=0.1, inplace=False))(src_attn): MultiHeadedAttention((linears): ModuleList((0-3): 4 x Linear(in_features=512, out_features=512, bias=True))(dropout): Dropout(p=0.1, inplace=False))(feed_forward): PositionwiseFeedForward((w1): Linear(in_features=512, out_features=1024, bias=True)(w2): Linear(in_features=1024, out_features=512, bias=True)(dropout): Dropout(p=0.1, inplace=False))(sublayer): ModuleList((0-2): 3 x SublayerConnection((norm): LayerNorm()(dropout): Dropout(p=0.1, inplace=False)))))(norm): LayerNorm() ) 解码器输出形状---> torch.Size([2, 4, 512]) 解码器输出示例-->tensor([[[-0.1577, -0.3706, 1.9802, ..., 1.2657, -0.1967, -0.4584],[-0.2006, -1.4549, 1.7445, ..., 1.3675, 0.2369, -0.8578],[ 0.5700, -0.6026, 1.6290, ..., 1.1199, 0.8338, -0.2420],[ 0.1437, -1.0446, 1.9349, ..., 1.1487, 0.8960, 0.1158]],[[ 1.1915, -0.6627, 2.5401, ..., 1.7567, 2.8790, 0.4833],[ 0.3432, -0.6784, 2.6090, ..., 2.0123, 2.0650, 0.2333],[ 1.2552, -0.6955, 1.7449, ..., 1.9456, 2.2976, 0.1523],[ 0.8503, -0.2008, 2.2772, ..., 1.4724, 2.5741, -0.2159]]],grad_fn=<AddBackward0>)
5.3 练习一
-
问:Transformer结构中,注意力机制有哪几个,分别说一说作用?
-
答:Transformer 有 3 种注意力机制
-
编码器:多头自注意力(Multi-Head Self-Attention)
-
作用:提取源文本的特征信息 ,让每个词关注源文本中其他词的关系;
-
举例:输入源文本“我爱中国”,多头自注意力会并行分析
- “我”的语义特征
- “爱”的语义特征
- 以此类推,每个词都能捕捉到和其他词的语义关联,提取丰富的特征;
-
-
解码器:Masked 多头自注意力(Masked Multi-Head Self-Attention)
-
作用:提取目标文本的特征信息,但要**“遮挡未来词”**,保证生成逻辑合理;
-
举例(训练阶段生成目标文本“I love china!”):
- 生成 “I” 时,要遮挡后面的 “love china!” ,只看历史生成的内容(这里可能只有起始符,所以主要关注自身 );
- 生成 “love” 时,要遮挡后面的 “china!” ,只能看 “I” 和自身;
- 生成 “china” 时,要遮挡后面的 “!” ,只能看 “I love” 和自身;
- 这样保证生成过程是串行、合理的,符合语言生成的逻辑(不能偷看未来词);
-
-
解码器:Encode-Decode 注意力(Encoder-Decoder Attention)
-
作用:让解码器融合编码器的语义信息(即编码中间语义张量
C
),生成和源文本对应的目标文本; -
通俗理解:解码器生成每个词时,不仅要看目标文本的历史(Masked 注意力做的事),还要回头看源文本的语义,保证生成的词和源文本对应;
-
举例(翻译”我爱中国“ → ”I love china!“):
- 生成 “love” 时,Encode-Decode 注意力会让解码器关注源文本的 “爱” ,确保 “love” 和 “爱” 语义对齐;
- 生成 “china” 时,关注源文本的 “中” ,确保和 “中国” 对应。
-
-
5.4 练习二
-
问:Transformer结构中,在编码时需要掩码mask,再解码时也需要掩码mask,二者有什么区别?
-
答:
-
编码时的 Mask:处理 Pad 填充,消除无效词影响
-
训练时,为了 batch 并行计算,会把不同长度的句子补全到相同长度(比如有的句子短,就用
[PAD]
填充)。但这些[PAD]
是“无效词”,注意力机制不能关注它们; -
作用:编码时的 Mask(一般叫 Padding Mask),会把
[PAD]
位置遮挡,让注意力机制只关注真实词,避免无效填充影响特征提取; -
举例:句子“我爱中国”(长度 4),补全后变成“我爱中国[PAD]”(假设 batch 最大长度是 5)。编码时,Mask 会遮挡最后一个
[PAD]
,让注意力计算时忽略它,只关注”我、爱、中、国”;
-
-
解码时的 Mask:防止偷看未来词,保证生成逻辑
-
解码是串行生成(一个词一个词生成),比如翻译时,生成第 3 个词不能提前看到第 4、5 个词;
-
作用:解码时的 Mask(一般叫 Sequence Mask),会 遮挡未来位置的词,让解码器生成每个词时,只能看到“历史生成的词”,看不到“未来要生成的词”。
-
-
6 输出部分
-
见
2.2 架构图
中的解释; -
代码实现:
class Generator(nn.Module):def __init__(self, d_model, vocab_size):super(Generator, self).__init__()# 定义线性层self.project = nn.Linear(d_model, vocab_size)def forward(self, x):# 数据经过线性层,最后一个维度归一化x = F.log_softmax(self.project(x), dim=-1)return x
7 模型构建
7.1 【编码器-解码器】结构
-
现在已经完成了所有组成部分的实现,接下来实现完整的【编码器-解码器】结构;
-
代码实现:
# 【编码器-解码器】结构 # 功能:连接编码器、解码器及相关组件,实现端到端的序列转换(如翻译、摘要等) class EncoderDecoder(nn.Module):def __init__(self, encoder, decoder, source_embed, target_embed, generator):super(EncoderDecoder, self).__init__()# 编码器实例:负责处理源序列(如输入文本)self.encoder = encoder# 解码器实例:负责生成目标序列(如输出文本)self.decoder = decoder# 源序列嵌入器:将源序列的词索引转换为特征向量(词嵌入+位置编码)self.source_embed = source_embed# 目标序列嵌入器:将目标序列的词索引转换为特征向量(词嵌入+位置编码)self.target_embed = target_embed# 生成器:将解码器输出转换为最终预测结果(如词汇表概率分布)self.generator = generatordef forward(self, source, target, source_mask, target_mask):"""前向传播:完成从源序列到目标序列的转换参数说明:source: 源序列输入(如源语言句子的词索引)形状: [batch_size, source_seq_len]target: 目标序列输入(如目标语言句子的词索引)形状: [batch_size, target_seq_len]source_mask: 源序列掩码(处理源序列padding)target_mask: 目标序列掩码(处理目标序列padding和未来位置)返回:目标序列的预测结果(如词汇表上的概率分布)"""# 1. 源序列处理流程:# - 先通过source_embed将词索引转换为特征向量(含位置信息)# - 再传入编码器得到中间语义张量memorymemory = self.encoder(self.source_embed(source), source_mask)# 2. 目标序列处理流程:# - 先通过target_embed将词索引转换为特征向量(含位置信息)# - 再传入解码器,结合memory和掩码得到解码特征x = self.decoder(self.target_embed(target), memory, source_mask, target_mask)# 3. 生成最终预测:将解码特征转换为词汇表概率分布x = self.generator(x)return x
7.2 Transformer模型构建
-
编写一个
make_model
函数,用于初始化一个一个组件对象(轮子对象),然后调用EncoderDecoder()
函数; -
代码实现:
# - EncoderDecoder: 编码器-解码器顶层结构 # - Encoder/Decoder: 编码器/解码器主体 # - EncoderLayer/DecoderLayer: 编码器/解码器层 # - MultiHeadedAttention: 多头注意力 # - PositionwiseFeedForward: 前馈全连接层 # - PositionalEncoding: 位置编码 # - Embeddings: 词嵌入层 # - Generator: 输出预测层def make_model(source_vocab, # 源语言词汇表大小target_vocab, # 目标语言词汇词汇表大小N=6, # 编码器/解码器层数(默认6层,与原论文一致)d_model=512, # 模型特征维度(默认512)d_ff=2048, # 前馈层中间维度(默认2048,为d_model的4倍)head=8, # 注意力头数(默认8头)dropout=0.1 # dropout概率(默认0.1) ):# 深拷贝工具:确保各层组件参数独立c = copy.deepcopy# 1. 创建基础组件# 多头注意力机制实例(将用于编码器、解码器的多个注意力层)attn = MultiHeadedAttention(head=head, embedding_dim=d_model, dropout=dropout)# 前馈全连接层实例(将用于编码器层和解码器层)ff = PositionwiseFeedForward(d_model=d_model, d_ff=d_ff, dropout=dropout)# 位置编码实例(为序列添加位置信息,源/目标序列共享位置编码逻辑)position = PositionalEncoding(d_model=d_model, dropout=dropout)# 2. 构建完整的EncoderDecoder模型model = EncoderDecoder(# 编码器:由N个EncoderLayer堆叠而成encoder=Encoder(layer=EncoderLayer(d_model, c(attn), c(ff), dropout), # 基础编码器层N=N # 堆叠N层),# 解码器:由N个DecoderLayer堆叠而成decoder=Decoder(layer=DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout), # 基础解码器层N=N # 堆叠N层),# 源序列嵌入器:词嵌入 + 位置编码(顺序执行)source_embed=nn.Sequential(Embeddings(d_model, source_vocab), # 源语言词嵌入(将词索引转为d_model维向量)c(position) # 位置编码(添加位置信息)),# 目标序列嵌入器:词嵌入 + 位置编码(顺序执行)target_embed=nn.Sequential(Embeddings(d_model, target_vocab), # 目标语言词嵌入c(position) # 位置编码(与源序列共享逻辑,参数独立)),# 生成器:将解码器输出转为目标词汇表概率分布generator=Generator(d_model, target_vocab))# 3. 参数初始化:对所有可学习参数做 Xavier 均匀初始化# 确保各层参数初始分布合理,加速训练收敛for p in model.parameters():if p.dim() > 1: # 仅对矩阵参数(如权重)初始化,偏置参数(1维)可保持默认nn.init.xavier_uniform_(p)return model
# 测试Transformer模型构建与运行 # 1. 配置模型参数 source_vocab = 500 # 源语言词汇表大小(假设有500个词) target_vocab = 1000 # 目标语言词汇表大小(假设有1000个词) N = 6 # 编码器/解码器层数# 2. 构建完整的Transformer模型 my_transform_modelobj = make_model(source_vocab=source_vocab,target_vocab=target_vocab,N=N,d_model=512,d_ff=2048,head=8,dropout=0.1 ) print('Transformer模型结构--->', my_transform_modelobj) # 打印模型整体结构# 3. 准备输入数据 # 3.1 源序列和目标序列(此处为演示让二者相同,实际中不同) # 形状:[2, 4] 表示 batch_size=2,序列长度=4(每个元素是词的索引) source = target = Variable(torch.LongTensor([[1, 2, 3, 8], [3, 4, 1, 8]]))# 3.2 源序列掩码和目标序列掩码(此处为演示让二者相同,实际中不同) # 形状:[8, 4, 4] 表示8个头,序列长度4×4的掩码矩阵 source_mask = target_mask = Variable(torch.zeros(8, 4, 4))# 4. 执行模型前向传播 mydata = my_transform_modelobj(source, target, source_mask, target_mask)# 5. 输出结果信息 # 输出形状应为:[batch_size, target_seq_len, target_vocab] # 即 [2, 4, 1000],表示每个位置对1000个目标词的预测概率分布 print('模型输出形状--->', mydata.shape) print('模型输出示例-->\n', mydata)
Transformer模型结构---> EncoderDecoder((encoder): Encoder((layers): ModuleList((0-5): 6 x EncoderLayer((self_attn): MultiHeadedAttention((linears): ModuleList((0-3): 4 x Linear(in_features=512, out_features=512, bias=True))(dropout): Dropout(p=0.1, inplace=False))(feed_forward): PositionwiseFeedForward((w1): Linear(in_features=512, out_features=2048, bias=True)(w2): Linear(in_features=2048, out_features=512, bias=True)(dropout): Dropout(p=0.1, inplace=False))(sublayer): ModuleList((0-1): 2 x SublayerConnection((norm): LayerNorm()(dropout): Dropout(p=0.1, inplace=False)))))(norm): LayerNorm())(decoder): Decoder((layers): ModuleList((0-5): 6 x DecoderLayer((self_attn): MultiHeadedAttention((linears): ModuleList((0-3): 4 x Linear(in_features=512, out_features=512, bias=True))(dropout): Dropout(p=0.1, inplace=False))(src_attn): MultiHeadedAttention((linears): ModuleList((0-3): 4 x Linear(in_features=512, out_features=512, bias=True))(dropout): Dropout(p=0.1, inplace=False))(feed_forward): PositionwiseFeedForward((w1): Linear(in_features=512, out_features=2048, bias=True)(w2): Linear(in_features=2048, out_features=512, bias=True)(dropout): Dropout(p=0.1, inplace=False))(sublayer): ModuleList((0-2): 3 x SublayerConnection((norm): LayerNorm()(dropout): Dropout(p=0.1, inplace=False)))))(norm): LayerNorm())(source_embed): Sequential((0): Embeddings((lut): Embedding(500, 512))(1): PositionalEncoding((dropout): Dropout(p=0.1, inplace=False)))(target_embed): Sequential((0): Embeddings((lut): Embedding(1000, 512))(1): PositionalEncoding((dropout): Dropout(p=0.1, inplace=False)))(generator): Generator((project): Linear(in_features=512, out_features=1000, bias=True)) ) 模型输出形状---> torch.Size([2, 4, 1000]) 模型输出示例-->tensor([[[-6.5468, -8.4262, -9.1137, ..., -9.0156, -6.8213, -7.7497],[-7.0823, -8.0404, -9.8192, ..., -8.7386, -7.1272, -7.1321],[-6.3075, -7.8114, -9.4348, ..., -8.1470, -6.5067, -7.8177],[-6.8289, -8.4570, -8.9954, ..., -8.8075, -6.4789, -7.6764]],[[-5.9231, -8.7100, -9.1708, ..., -8.7897, -6.8369, -8.3745],[-6.5998, -8.3359, -8.5046, ..., -8.0448, -6.7674, -7.5100],[-6.3656, -7.9740, -8.4566, ..., -8.4705, -6.4160, -7.9261],[-6.2937, -8.2231, -8.4524, ..., -8.0650, -6.5525, -8.5411]]],grad_fn=<LogSoftmaxBackward0>)