背景介绍【了解】
Transformer是一种基于自注意力机制(Self-Attention)的深度学习架构,它摒弃了传统的循环或卷积结构,能够高效并行处理序列数据,是现代大语言模型和人工智能发展的核心基石。
2017年提出,2018年google发表了BERT模型,使得Transformer架构流行起来,BERT在许多NLP任务上,取得了Soat的成就。
论文地址:https://arxiv.org/pdf/1706.03762
模型优势
- 1、能够实现并行计算,提高模型训练效率
- 2、更好的特征提取能力
展开查看更多
思想间的关系:
Seq2Seq
Seq2Seq+注意力机制
Transformer
2017 《attention is all you need》
2018 Bert
2022 ChatGPT、T5
绝大多数大模型底层的原理都是Transformer
Transformer模型架构【掌握】
整体架构
组成部分:
- 1、输入部分
- 2、编码器部分
- 3、解码器部分
- 4、输出部分
Transformer 架构各模块详解
此章节是重要面试部分,重要面试题,要背诵的内容 ,尤其多头自注意力层、多头注意力层、掩码多头自注意力层
-
什么是注意力机制
展开查看答案
注意力机制的核心思想是:让模型在处理当前词时,动态关注和它最相关的其他词,而不是对所有信息一视同仁。这样做的好处是,模型能把更多计算资源放在关键信息上,更容易捕获长距离依赖关系。 -
解释下注意力机制的计算公式
展开查看答案
注意力的计算可以分成三步:先用Q和K计算相关性分数,再用Softmax把分数变成权重,最后对V做加权求和。公式本质上是在计算一个问题:当前词应该重点参考哪些词,以及参考多少。 -
注意力计算公式中为什么要除以根号dk
展开查看答案
因为Q·K^T的结果会随着维度d_k变大而变大,数值过大时,Softmax会变得特别陡,导致梯度很小,不利于训练。除以sqrt(d_k)的作用就是把分数缩放到更稳定的范围,让训练更平稳。防止梯度消失 -
什么是多头/多头的好处是什么
展开查看答案
多头注意力就是把注意力机制拆成多个“头”并行计算,让模型同时从多个角度关注句子中每个词的含义。 -
什么是交叉注意力机制
展开查看答案
交叉注意力是指Q来自当前序列,K和V来自另一个序列。Transformer 里常见的场景是:解码器在生成当前词时,去关注编码器输出的信息,从而把源序列内容融合进来。 -
fasttext模型特点
展开查看答案
fastText是一个轻量、高效的文本分类和词向量模型。它的特点是训练速度快、效果稳定,还会把词拆成n-gram子词,因此对生僻词、拼写变化词比普通词袋方法更友好。
公式
$Attention(Q,K,V)=Softmax\left(\frac{Q\cdot K^T}{\sqrt{d_k}}\right)\cdot V$ 注意力函数 Q、K、V,等于 Softmax 作用于「Q 点乘 K 转置,再除以根号 d_k」的结果,最后点乘 V。
编码器(Encoder)侧模块
Nx:重复堆叠
- 含义:编码器层的层数,论文中取值为6
- 作用:将「多头自注意力层 + 残差连接和层归一化 + 前馈网络」重复N次,构成完整编码器
Feed Forward:前馈网络/前馈全连接层
- 作用:对每个位置的词向量单独进行特征增强。经过注意力层处理后的数据,再通过前馈网络加工,为每个词补充更多额外特征信息
- 示例:在“猫抓老鼠”这句话中,前馈网络会增强“猫”的动物名词属性、“抓”的动词属性
Add & Norm:残差连接 和 层归一化/规范化层
- 作用:
- Add 残差连接:将注意力层的输出与位置编码后的输入相加,防止数据信息丢失
- Norm 层归一化:避免模型训练过程偏离,缓解梯度爆炸或梯度消失问题
Multi-Head Attention:多头自注意力层
- 作用:让模型同时从多个角度关注句子中每个词的含义
- 示例:就业辅导场景中,1位老师负责50名学生时,每个学生获得的关注度有限,面试复盘只能逐个进行;如果有3位老师,学生的受关注度提升,且复盘工作可以并行开展
Positional Encoding:位置编码
- 作用:为词向量补充位置信息。Transformer 本身无法感知词与词的顺序,需要额外为每个词添加位置编码
- 示例:“小龙女想过过过过过过的生活”中,三组“过过”的实际含义各不相同,依赖位置信息区分
Input Embedding:编码器的词嵌入层
- 作用:将编码器端输入的词索引转换为词向量
解码器与输出侧模块
Nx:重复堆叠
- 含义:解码器层的层数
- 作用:对应解码器侧的层级重复堆叠
Linear 和 Softmax
- 作用:将隐藏层数据经过线性变换后,通过 softmax 激活函数,最终输出为词汇表中所有词的概率,概率总和为1
Add & Norm:残差连接 和 层归一化/规范化层
- 作用:
- Add 残差连接:防止数据信息丢失
- Norm 层归一化:保障模型训练过程稳定
Feed Forward:前馈网络/前馈全连接层
- 作用:对每个位置的词向量单独进行特征增强。经过注意力层处理后的数据,再通过前馈网络加工,为每个词补充更多额外信息
Multi-Head Attention:多头注意力层
- 作用:让解码器关注编码器端输出的数据
- 示例:翻译任务中,解码器得到法语词汇后,需要与编码器端所有的 key 分别计算相似度,再转换为注意力权重
Masked Multi-Head Attention:掩码多头自注意力层
- 作用:通过掩码机制,防止模型“偷看”当前位置之后的词语
- 示例:翻译生成 “welcome you to canton” 时,生成到 “to” 的时刻,只能参考前面的 “welcome” 和 “you”,不能看到后面的 “canton”
Positional Encoding:位置编码
- 作用:为词向量补充位置信息。Transformer 本身无法感知词与词的顺序,需要额外为每个词添加位置编码信息
Output Embedding:目标序列的词嵌入层
- 作用:将解码器端输入的词索引转换为词向量
架构概述:
总结流程:
输入文字 -> 嵌入 -> 位置编码 -> 编码器多层加工(注意力+前馈) -> 解码器多层加工(掩码注意力 + 关联编码器 + 前馈) -> 线性层 -> softmax -> 输出概率选词
整个架构的核心:
注意力机制抓关联,多层堆叠挖深度,残差归一化稳训练
一眼看懂
输入端 = Input Embedding + Positional Encoding
编码器 = Nx × [Multi-Head Self-Attention + Add & Norm + Feed Forward]
解码器 = Nx × [Masked Multi-Head Self-Attention + Multi-Head Attention + Add & Norm + Feed Forward]
输出端 = Linear + Softmax
Transformer 模型的代码实现
展开查看详情
代码开发思路
重点组件
只需要搞清楚如下几个组件的功能、作用和代码开发
具体开发步骤
- 编码器

- 解码器
参考编码器的开发过程,大部分地方的功能直接复用。主要是开发【掩码多头自注意力层】
- 输出部分
非常简单,nn.Linear全连接层+Softmax激活函数
- 代码的作用和开发顺序
输入部分
词嵌入层
注意:为什么embedding之后要乘以根号下d_model
- 原因1:为了防止position encoding的信息覆盖我们的word embedding,也就是防止两者的值差距过大。所以进行一个数值增大
- 原因2:符合正态分布
- 代码实现
class Embedding(nn.Module):
def __init__(self,vocab_size,d_model):
# 1- 初始化父类
super().__init__()
# 2- 设置属性值
self.vocab_size = vocab_size # 词汇表大小
self.d_model = d_model # 词向量的维度。例如:512
# 3- 搭建网络结构:只有一个词嵌入层
self.embed = nn.Embedding(num_embeddings=self.vocab_size,embedding_dim=self.d_model)
def forward(self,input):
"""
前向传播。输入一条句子,得到词向量
:param input: 一条句子,里面的元素是词索引。张量形状[batch_size每个批次中句子的条数,seq_len每条句子中词的个数]
:return: 词向量
"""
"""
为什么要乘以math.sqrt(self.d_model),也就是根号dk?
答:为了对数据进行缩放,避免与位置编码的数据值之间的大小差异过大。为了让模型训练稳定,也就是缓解梯度消失或梯度爆炸
词向量维度越大,越容易出现极小值
"""
return self.embed(input) * math.sqrt(self.d_model)
- 测试代码
def use_embedding():
# 1- 创建词嵌入层类的实例对象
my_embed = Embedding(vocab_size=1000,d_model=5)
# my_embed = Embedding(vocab_size=1000,d_model=10240)
# 2- 准备数据
# 注意:目前情况下,词索引的取值区间[0,999]
x = torch.tensor([
# 单词索引
[100, 2, 666],
[500, 888, 421]
])
# 3- 输入句子,得到词向量
word_vector = my_embed(x)
print(f"词向量的形状{word_vector.shape}") # 2条句子,3句子中3个词,5词向量维度
print(f"词向量的数据{word_vector}")
print(f"词向量的数据{word_vector.abs().min()}") # 获得乘以根号dk以后数据中绝对值的最小值
位置编码
作用
Transformer编码器或解码器中缺乏位置信息,因此加入位置编码器,将词汇的位置可能代表的不同特征信息和word_embedding进行融合,以此来弥补位置信息的缺失。
位置编码器实现方式:三角函数来实现的,sin、cos函数
为什么使用三角函数来进行位置编码
1、保证同一词汇随着所在位置不同它对应位置嵌入向量会发生变化
2、正弦波和余弦波的值域范围都是1到-1这又很好的控制了嵌入数值的大小, 有助于梯度的快速计算

- 代码实现
class PositionalEncoding(nn.Module):
def __init__(self,d_model,dropout,max_len=60):
"""
位置编码初始化方法
:param d_model: 词向量维度。例如:512
:param dropout: 神经元随机失活概率
:param max_len: 能够处理的句子最大长度。也就是词的个数
"""
# 1- 初始化父类
super().__init__()
# 2- 创建随机失活层
self.dropout = nn.Dropout(p=dropout)
# 3- 定义pe(也就是位置编码张量)
pe = torch.zeros(size=(max_len,d_model)) # 目前的形状[60,d_model]
# 4- 定义列向量,用来存储输入句子中词索引信息
position = torch.arange(0,max_len).unsqueeze(1) # 形状[60,1]
# 5- 得到pos/分母公式中的,1/分母
div_term = 1/(10000**(torch.arange(0,d_model,2).float()/d_model))
# 6- 计算pos/分母的结果
position_value = position * div_term
# 7- 调用sin、cos分别计算位置编码值
pe[:,0::2] = torch.sin(position_value) # 偶数维度
pe[:,1::2] = torch.cos(position_value) # 奇数维度
# 8- 调整pe的形状变成3维,也就是[60,d_model]->[1,60,d_model]每个批次1条句子,每个句子最多60个词,词向量是d_model
pe = pe.unsqueeze(0)
# 9- 将pe的值注册到缓存中,通过它来不断的更新位置编码信息。后面可以直接通过self.pe调用
self.register_buffer("pe",pe)
def forward(self,embed):
"""
位置编码的前向传播:多条句子中所有词的词向量,和位置编码张量进行求和操作,返回结果
:param embed: 词向量
:return:
"""
"""
embed.shape[1]:embed的形状[batch_size,seq_len,d_model],得到多少个词
为什么self.pe[:, :embed.shape[1]]?
如果这条句子中词的个数超过了max_len,那么最多只对前max_len个词的位置编码进行相加
"""
result = embed + self.pe[:, :embed.shape[1]]
print(f"对应的位置编码值:{self.pe[:, :embed.shape[1]]}")
return self.dropout(result)- 测试代码
def use_positional_encoding():
d_model = 512
# 实例化词嵌入层类的对象
my_embed = Embedding(vocab_size=1000,d_model=d_model)
# 准备数据
x = torch.tensor([
# 单词索引
[100, 2, 421, 600],
[500, 888, 421, 615]
])
# 输入数据,得到对应的词向量
word_embed = my_embed(x)
print(f"词向量的形状:{word_embed.shape}")
print(f"词向量的值:{word_embed}")
# 创建位置编码
my_pe = PositionalEncoding(d_model=d_model,dropout=0.1,max_len=60)
# 调用位置编码,最终效果是往词向量中加上了位置编码的值
result = my_pe(word_embed)
print(f"最终的形状:{result.shape}")
print(f"最终的值:{result}")
return result
# 可视化位置编码
def plot_position():
# 1. 实例化位置编码器.
my_position = PositionalEncoding(20, dropout=0.1, max_len=100)
# 2. 生成全0的输入, 观察位置编码的模式.
# (1, 100, 20) -> 批次大小, 句子长度, 词嵌入维度
embed = torch.zeros(1, 100, 20)
y = my_position(embed)
# 3. 设置图表大小.
plt.figure(figsize=(20, 15))
# 绘制位置编码第4到第7列, 100个词的 [4, 5, 6, 7]
"""
图形的信息解释:
x轴:词的索引,目前总共有100个词
y轴:位置编码值
"""
plt.plot(np.arange(100), y[0, :, 4:8].detach().numpy())
plt.legend([f'dim {p}' for p in [4, 5, 6, 7]])
plt.show()
编码器部分
掩码张量
作用
掩码:掩就是遮掩、码就是张量。掩码本身需要一个掩码张量,掩码张量的作用是对另一个张量进行数据信息的掩盖。一般掩码张量是由0和1两种数字组成,至于是0对应位置或是1对应位置进行掩码,可以自己设定
- PyTorch版本
# 使用PyTorch实现掩码。【掌握】
"""
总结:
1- 上三角掩码:右上角的值为1,左下角的值为0
2- 下三角掩码:右上角的值为0,左下角的值为1
3- 相同的规律:对角线的时候diagonal为0;如果是正数,并且越来越大,那么往右上角移动;反之往左下角移动;diagonal的取值超过范围不会报错
4- 【掌握】掩码的作用:为了防止解码器端当前时间步偷看后面时间步的数据,你只能看到你前面时间步已经预测出来的数据。核心是防止模型训练效果不好
5- 【掌握】代码:torch.triu(t,diagonal=0)
"""
import torch
def torch_mask():
# 创建初始张量
t = torch.ones(size=(5,5))
print(f"原始张量:{t}")
# 进行掩码操作
# 上三角掩码
u_0_mask = torch.triu(t,diagonal=0)
print(f"上三角掩码0:{u_0_mask}")
u_1_mask = torch.triu(t, diagonal=1)
print(f"上三角掩码1:{u_1_mask}")
u__1_mask = torch.triu(t, diagonal=-1)
print(f"上三角掩码_1:{u__1_mask}")
u_n_mask = torch.triu(t, diagonal=1000)
print(f"上三角掩码n:{u_n_mask}")
print("-"*30)
# 下三角掩码
l_0_mask = torch.tril(t, diagonal=0)
print(f"下三角掩码0:{l_0_mask}")
l_1_mask = torch.tril(t, diagonal=1)
print(f"下三角掩码1:{l_1_mask}")
l__1_mask = torch.tril(t, diagonal=-1)
print(f"下三角掩码-1:{l__1_mask}")- numpy版本
import numpy as np
def np_mask():
# 1- 准备数据
arr = np.ones(shape=(5,5))
# 2- 上三角掩码
# k和PyTorch中diagonal参数的作用完全一样
u_result = np.triu(arr,k=0)
print(f"上三角掩码0:\n{u_result}")
u_result = np.triu(arr, k=1)
print(f"上三角掩码1:\n{u_result}")
u_result = np.triu(arr, k=-1)
print(f"上三角掩码-1:\n{u_result}")
# 3- 下三角掩码
l_result = 1 - u_result
print(f"下三角掩码:\n{l_result}")
l_result = np.tril(arr,k=0)
print(f"下三角掩码0:\n{l_result}")
l_result = np.tril(arr, k=1)
print(f"下三角掩码1:\n{l_result}")
l_result = np.tril(arr, k=-1)
print(f"下三角掩码-1:\n{l_result}")- 掩码张量的可视化
# 掩码张量的可视化.
def dm03_test_mask():
# 简单说(总结): 纵向选'当前位置', 横向看'能看哪些位置', 0(黄色): 遮挡, 1(紫色): 不遮挡
plt.figure(figsize=(5, 5))
plt.imshow(demo2(20)[0])
plt.show()
注意力机制【掌握】
计算规则
自注意力机制,规则:Q乘以K的转置,然后除以根号下d_k,然后再进行Softmax,最后和V进行张量矩阵相乘
如果是自注意力,那么Q、K、V的值完全相同
注意力计算
扩展知识:点积和点乘
点积:等同于矩阵乘法,可以使用matmul函数进行计算,也可以使用dot函数进行计算。在注意力机制场景下,一般使用matmul
点乘:两个张量的形状要完全一样。对应坐标位置元素相乘
- 代码实现
import math
import torch
import torch.nn as nn
from torch import Tensor
from demo01_input import *
def attention(query:Tensor,key:Tensor,value:Tensor,mask:Tensor=None,dropout=None):
"""
注意力计算方法。能够涵盖如下几种情况:
1- 编码器端的多头自注意力:query=key=value,mask为None
2- 解码器端的掩码多头自注意力:query=key=value,mask不为None
3- 解码器端的多头注意力(交叉注意力):query和key、value不等,但是key和value相同,mask为None
:param query: 查询张量,形状:[batch_size每个批次中有多少条句子,seq_len每条句子中有几个词,d_model词向量的维度/隐藏状态维度]
:param key: 键张量,形状:[batch_size,seq_len,d_model]
:param value: 值张量,形状:[batch_size,seq_len,d_model]
:param dropout: 随机失活Dropout层对象
:param mask: 掩码张量,形状:[batch_size,seq_len,seq_len]
:return: 专属信息包,权重张量。类型是元组
"""
# 1- 获得d_k:词向量的维度
d_k = query.shape[-1]
# 2- Q和K的转置相乘;再除以根号d_k。得到相似性得分scores
# K的转置:由 [batch_size,seq_len,d_model] 变成了 [batch_size,d_model,seq_len]
scores = torch.matmul(query,key.transpose(-2,-1)) / math.sqrt(d_k)
# 3- 可选:对相似性得分scores进行掩码处理
if mask is not None:
# 对需要进行掩码的地方,将值替换成-1e9。注意:不要直接设置为0。需要和softmax结合理解。e的-1e9的结果趋近于0,表示权重趋近于0
scores = scores.masked_fill(mask==0,value=-1e9)
# 4- 将相似性转成权重
weight = torch.softmax(scores,dim=-1)
# 5- 可选:随机时候,缓解过拟合
if dropout is not None:
weight = dropout(weight)
# 6- 权重和value进行矩阵乘法,得到专属信息包
C = torch.matmul(weight,value)
return C,weight
- 测试代码
def use_attention():
# 1- 输入的数据先经过 词嵌入层 和 位置编码
posi_embed = use_positional_encoding()
# 2- 调用注意力计算方法
# 2.1- 准备query、key、value参数
query=key=value=posi_embed
# 2.2- 准备掩码【可选】
# (2,4,4)值的来源于use_positional_encoding的x。每个批次2条句子,每条句子4个词
# 形状:[batch_size,seq_len,seq_len]
mask = torch.triu(torch.ones(size=(2,4,4)))
# 2.3- 随机失活网络层
dropout = nn.Dropout(p=0.1)
# 2.4- 调用
# 2.4.1- 编码器端:没有掩码的多头自注意力
encoder_attention_C, encoder_attention_weight = attention(query,key,value,dropout=dropout)
print(f"编码器C:{encoder_attention_C.shape}-->{encoder_attention_C}")
print(f"编码器C权重:{encoder_attention_weight.shape}-->{encoder_attention_weight}")
# 2.4.2- 解码器端:有掩码的多头自注意力
decoder_attention_C, decoder_attention_weight = attention(query,key,value,dropout=dropout,mask=mask)
print(f"解码器C:{decoder_attention_C.shape}-->{decoder_attention_C}")
print(f"解码器C权重:{decoder_attention_weight.shape}-->{decoder_attention_weight}")多头注意力机制【掌握】
概念
多头的好处:
1- 能够让模型进行并行计算,提高运行效率
2- 多个头来同时处理数据,那么模型对数据的理解、学习会更好,模型整体效果会更好
- 多头注意力机制原理图

- 代码实现
def clones(model_obj, nums):
"""
创建指定个数的相同网络结构对象
:param model_obj: 网络结构对象
:param nums: 个数
:return: 网络结构对象列表
"""
# 语法 nn.ModuleList([copy.deepcopy(模型对象) for _ in range(深拷贝复制的个数)])
return nn.ModuleList([copy.deepcopy(model_obj) for _ in range(nums)])
class MultiHeadAttention(nn.Module):
def __init__(self,d_model,head,dropout_p=0.1):
"""
初始化
:param d_model: 词向量维度/隐藏层隐藏状态向量维度。例如:512
:param head: 多头的头数。例如:8
:param dropout_p: 随机失活概率
"""
# 1- 确定d_model能够被head整除
assert d_model%head==0
# 2- 初始化父类
super().__init__()
# 3- 设置属性值
self.d_model = d_model
self.head = head
self.head_dim = self.d_model // self.head # 每个头分别处理的数据维度。例如:64
self.dropout = nn.Dropout(p=dropout_p)
# 4- 搭建网络结构
"""
4个线性层的作用如下:
第1个线性层:专门用来query进行线性处理,决定“当前词在找什么东西”
第2个线性层:专门用来key进行线性处理,决定“其他词能够提供什么信息”
第3个线性层:专门用来value进行线性处理,决定“其他词的信息的重要程度是什么样的”
第4个线性层:对多头并行处理,并且concat拼接后的张量做最终的线性处理,让数据变得更加平稳
"""
self.linear_list = clones(nn.Linear(in_features=self.d_model,out_features=self.d_model), 4)
# 5- 权重张量
self.weight = None
def forward(self,query:Tensor,key:Tensor,value:Tensor,mask:Tensor=None):
"""
前向传播:多头注意力计算
:param query: 查询张量,形状:[batch_size每个批次中有多少条句子,seq_len每条句子中有几个词,d_model词向量的维度/隐藏状态维度]
:param key: 键张量,形状:[batch_size,seq_len,d_model]
:param value: 值张量,形状:[batch_size,seq_len,d_model]
:param mask: 掩码张量,形状:[head,seq_len,seq_len]。注意第一个维度代表的是头数
:return:
"""
# 1- 掩码处理:进行升维,3维变4维
if mask is not None:
# 例如:[8,4,4] -> [1,8,4,4]
# 不管每个批次中有多少条句子,每条句子用的掩码是同一份
mask = mask.unsqueeze(0)
# 2- 获得batch_size,也就是批次中句子的条数
batch_size = query.shape[0]
# 3- 前3个线性层分别并行对QKV进行处理
# 方式一:分开版
# [(Linear,query), (Linear,key), (Linear,value)]
linear_output_list = []
model_and_data_list = list(zip(self.linear_list, (query,key,value)))
for model,data in model_and_data_list:
"""
1- model(data):线性变换
2- reshape:[2,4,512]->[2,4,8,64]
3- transpose:[2,4,8,64]->[2,8,4,64]
"""
model_output = model(data)
reshape_output = model_output.reshape(batch_size,-1,self.head,self.head_dim)
linear_output_list.append(reshape_output.transpose(1,2))
new_query, new_key, new_value = linear_output_list
# 方式二:合并版【理解】
# new_query, new_key, new_value = [
# model(data).reshape(batch_size,-1,self.head,self.head_dim).transpose(1,2)
# for model,data in list(zip(self.linear_list, (query,key,value)))
# ]
# 4- 多头并行计算注意力
C,weight = attention(new_query, new_key, new_value,mask=mask,dropout=self.dropout)
self.weight = weight
# 5- 对多头处理后的数据进行拼接
# [2,8,4,64] -> [2,4,8,64] -> [2,4,512]
result = C.transpose(1,2).reshape(batch_size,-1,self.head*self.head_dim)
# 6- 调用最后一个线性层对拼接后的数据进行处理,让模型更加稳定
return self.linear_list[-1](result)- 测试代码
def use_multi_head_attention():
# 1- 获取位置编码之后的词数据
position_data = use_positional_encoding()
# 2- query、key、value参数
query=key=value=position_data
# 3- 创建多头注意力实例对象
mask = torch.triu(torch.ones(size=(8, 4, 4)))
my_attention = MultiHeadAttention(d_model=512,head=8,dropout_p=0.1)
# 4- 调用前向传播
result = my_attention(query,key,value,mask=mask)
print(f"多头注意力计算结果:{result.shape}")
print(f"多头注意力计算结果:{result}")
return result
前馈网络
概念:前馈网络用来增强模型的拟合能力,内部包含两个全连接层
- 代码实现
# 前馈网络/前馈全连接层:通过调整张量大小,实现强化信息的过程
class FeedForward(nn.Module):
def __init__(self,d_model,output_dim,dropout_p=0.1):
# 1- 初始化父类
super().__init__()
# 2- 搭建网络结构
# 2.1- 第一个线性层:用来对输入的词向量维度进行放大,例如:512->1024
self.linear_1 = nn.Linear(in_features=d_model,out_features=output_dim)
# 2.2- 随机失活层
self.dropout = nn.Dropout(p=dropout_p)
# 2.3- 第二个线性层:用来对上一个线性层输出的结果进行浓缩,例如:1024->512
self.linear_2 = nn.Linear(in_features=output_dim, out_features=d_model)
def forward(self,data):
# 1- 调用第一个线性层
data = self.linear_1(data)
# 2- 调用随机失活
"""
注意力机制本质是线性计算,如果没有非线性因素,那么整个网络模型比较简单,只能处理线性问题(也就是回归)
引入relu激活也就是引入了非线性因素;Transformer论文中用的就是relu
"""
data = self.dropout(torch.relu(data))
# 3- 调用第二个线性层
data = self.linear_2(data)
return data层归一化
作用
随着网络深度的增加,模型参数会出现过大或过小的情况,进而可能影响模型的收敛,因此进行规范化,将参数规范致某个特征范围内,辅助模型快速收敛。
代码实现
# 层归一化/规范化层:随着网络模型训练,数据有可能出现极值,导致梯度爆炸或消失,为了让模型训练稳定,通过标准化处理,让数据变的正常
# 也就是要实现y=kx+b,其中x要经过标准化处理(要计算数据的均值mean和标准差std)
class LayerNorm(nn.Module):
def __init__(self,d_model):
# 1- 初始化父类
super().__init__()
# 2- 线性公式中参数的定义
"""
为什么k和b要通过nn.Parameter进行定义?
答:因为nn.Parameter会自动将k和b注册到神经网络模型中,作为可训练的参数。通过反向传播得到k和b。
如果不写nn.Parameter,那么k和b的值永远固定,不会发生变化。
对应前面神经网络模型代码中如下两行代码
optimizer = optim.Adam(params=model.parameters())
optimizer.step()
"""
# 2.1- 定义k
self.k = nn.Parameter(torch.ones(d_model))
# 2.2- 定义b
self.b = nn.Parameter(torch.zeros(d_model))
# 3- 定义小常数:防止分母为0
self.eps = 1e-6
def forward(self,data):
"""
前向传播:输入前面子层处理后的数据,经过层归一化进行【标准化处理】,让模型训练稳定
要实现y=kx+b,其中x要经过标准化处理(要计算数据的均值mean和标准差std)
:param data: 前面子层处理后的数据。形状[batch_size,seq_len,d_model]
:return:
"""
# 1- 计算均值
"""
参数解释:
dim=-1:对最后一个维度计算均值
keepdim=True:表示计算后,张量的形状保留。举例:[2,4,512]->[2,4,1]
"""
mean = data.mean(dim=-1,keepdim=True)
# 2- 计算标准差
std = data.std(dim=-1, keepdim=True)
# 3- 实现y=kx+b
return self.k * (data - mean)/(std+self.eps) + self.b子层连接结构
- 代码实现
# 子层连接类
class SubLayerConnection(nn.Module):
def __init__(self,d_model,dropout_p):
# 1- 初始化父类
super().__init__()
# 2- 创建层归一化
"""
为什么把LayerNorm创建在__init__中?
答:因为每个子层的后面都有该层
"""
self.layer_norm = LayerNorm(d_model)
# 3- 创建随机失活层
self.dropout = nn.Dropout(p=dropout_p)
def forward(self,data,sublayer_obj):
"""
前向传播:使用你指定的具体子层实例对象【sublayer_obj】对数据【data】进行指定的处理
:param data: 输入到层中的数据
:param sublayer_obj: 具体子层实例对象。例如:多头自注意力实例对象、前馈网络实例对象、掩码多头自注意力实例对象、多头注意力实例对象(交叉注意力)
:return:
"""
# 具体子层实例对象【sublayer_obj】对数据【data】进行指定的处理
# 写法一:原始Transformer的实现
# 子层处理(也就是调用forward) -> 随机失活层 -> 残差连接 -> 层归一化
# result = self.layer_norm(self.dropout(sublayer_obj(data)) + data)
# 写法二:目前主流大模型的实现
# 层归一化 -> 子层处理(也就是调用forward) -> 随机失活层 -> 残差连接
"""
第一步:层归一化 self.layer_norm(data)
第二步:子层处理 sublayer_obj(self.layer_norm(data))
第三步:随机失活层 self.dropout(sublayer_obj(self.layer_norm(data)))
第四步:残差连接 + data
"""
result = self.dropout(sublayer_obj(self.layer_norm(data))) + data
return result- 子层测试代码
# 子层连接测试
def use_SubLayerConnection():
# 1- 准备数据:词嵌入层+位置编码
data = use_positional_encoding()
d_model = 512
# 2- 子层连接类的实例对象
SubLayerConnection_obj = SubLayerConnection(d_model,dropout_p=0.1)
# 3- 演示子层的组合
# 3.1- 多头自注意力层 + 残差连接和层归一化
# 3.1.1- 创建 多头自注意力层 数据处理规则
# 定义方式一:函数的形式
def data_handler_fn(data):
# 创建 多头自注意力层 类的实例对象
attention_obj = MultiHeadAttention(d_model=d_model,head=8,dropout_p=0.1)
# 调用forward
return attention_obj(query=data,key=data,value=data)
# 3.1.2- 组装得到子层:也就是调用SubLayerConnection它的forward
multi_head_attention_output = SubLayerConnection_obj(data,data_handler_fn)
print(f"多头自注意力层输出结果形状:{multi_head_attention_output.shape}")
# 3.2- 前馈网络层 + 残差连接和层归一化
# 定义方式二:lambda的形式
feed_forward_output = SubLayerConnection_obj(
multi_head_attention_output,
lambda data:FeedForward(d_model=d_model,output_dim=1024,dropout_p=0.1)(data)
)
print(f"前馈网络层输出结果形状:{feed_forward_output.shape}")编码器层
作用
每个编码器层完成一次对输入的特征提取过程, 即编码过程.
代码实现
from demo02_component import *
# 编码器层
class EncoderLayer(nn.Module):
def __init__(self,d_model,multi_head_self_attn,feed_forward,dropout_p=0.1):
"""
定义编码器层的结构
:param d_model: 词向量维度
:param multi_head_self_attn: 多头自注意力类的实例对象
:param feed_forward: 前馈网络类的实例对象
:param dropout_p:
"""
# 1- 初始化父类
super().__init__()
# 2- 设置属性值
self.d_model = d_model
self.dropout_p = dropout_p
self.multi_head_self_attn = multi_head_self_attn
self.feed_forward = feed_forward
# 3- 创建子层
# 3.1- 第一个子层:多头自注意力子层
self.multi_layer = SubLayerConnection(self.d_model,self.dropout_p)
# 3.2- 第二个子层:前馈网络子层
self.feed_forward_layer = SubLayerConnection(self.d_model,self.dropout_p)
def forward(self,data):
# 1- 数据经过第一个层的处理:多头自注意力子层
multi_output = self.multi_layer(data, lambda x: self.multi_head_self_attn(query=x, key=x, value=x, mask=None))
# 2- 数据经过第二个层的处理:前馈网络子层
encoder_output = self.feed_forward_layer(multi_output, lambda x: self.feed_forward(x))
return encoder_output编码器
- 代码实现
# 编码器
class Encoder(nn.Module):
def __init__(self, encoder_layer:EncoderLayer, N):
"""
定义编码器结构。也就是将编码器层重复N层
:param encoder_layer: 编码器层
:param N:
"""
# 1- 初始化父类
super().__init__()
# 2- 将编码器层创建N对象
self.encoder_layer_list = clones(encoder_layer,N)
# 3- 层归一化
"""
这东西不是必须的,只是为了缓解经过N层处理后的数据,让数据变得更加平稳。这一层不是必须的
"""
self.layer_norm = LayerNorm(encoder_layer.d_model)
def forward(self,data):
"""
注意:for循环中两个data需要与传递给forward的参数名称完全一致,否则6层网络处理的数据就会断开
"""
for encoder_layer in self.encoder_layer_list:
data = encoder_layer(data)
return self.layer_norm(data)- 编码器完整测试代码
# 测试编码器
# 测试最终的编码器
def use_encoder():
# 1- 得到词向量数据:词向量 + 位置编码
position_data = use_positional_encoding()
d_model = 512
dropout = 0.1
# 2- 创建编码器的实例对象
# 2.1- 多头自注意力子层的实例对象
multi_head = MultiHeadAttention(d_model=d_model,head=8,dropout_p=dropout)
# 2.2- 前馈网络子层的实例对象
ff = FeedForward(d_model=d_model,output_dim=1024,dropout_p=dropout)
# 2.3- 编码器层的实例对象
encoder_layder = EncoderLayer(d_model=d_model,multi_head_self_attn=multi_head,feed_forward=ff,dropout_p=dropout)
# 2.4- 编码器层 堆叠N层得到编码器
encoder = Encoder(encoder_layder,6)
# 3- 数据进行前向传播
result = encoder(data=position_data)
print(f"编码器最终的输出结果是:{result.shape}")
return result
解码器部分
解码器层
作用
作为解码器的组成单元, 每个解码器层根据给定的输入向目标方向进行特征提取操作
代码实现
"""
解码器层由如下3层组成
1- 第一层:掩码多头自注意力层
掩码多头自注意力 + 层归一化 + 残差连接
2- 第二层:多头注意力层
多头注意力 + 层归一化 + 残差连接
3- 第三层:前馈网络层
前馈网络 + 层归一化 + 残差连接
"""
from demo03_encoder import *
from demo02_component import *
# 解码器层
class DecoderLayer(nn.Module):
def __init__(self,d_model,mask_multi_head_self_attn,multi_head_attn,feed_forward,dropout_p=0.1):
"""
定义解码器层的结构
:param d_model: 词向量维度
:param mask_multi_head_self_attn: 掩码多头自注意力类的实例对象
:param multi_head_attn: 多头注意力类的实例对象
:param feed_forward: 前馈网络类的实例对象
:param dropout_p:
"""
# 1- 初始化父类
super().__init__()
# 2- 设置属性值
self.d_model = d_model
self.dropout_p = dropout_p
self.mask_multi_head_self_attn = mask_multi_head_self_attn
self.multi_head_attn = multi_head_attn
self.feed_forward = feed_forward
# 3- 创建子层
# 3.1- 第一个子层:掩码多头自注意力子层
self.mask_multi_self_layer = SubLayerConnection(self.d_model,self.dropout_p)
# 3.2- 第二个子层:多头注意力子层
self.multi_layer = SubLayerConnection(self.d_model, self.dropout_p)
# 3.3- 第三个子层:前馈网络子层
self.feed_forward_layer = SubLayerConnection(self.d_model,self.dropout_p)
def forward(self,data,mask,encoder_output):
# 1- 数据经过第一个层的处理:掩码多头自注意力子层
mask_multi_self_output = self.mask_multi_self_layer(data, lambda x: self.mask_multi_head_self_attn(query=x, key=x, value=x, mask=mask))
# 2- 数据经过第二个层的处理:多头注意力子层
multi_output = self.multi_layer(mask_multi_self_output, lambda x: self.multi_head_attn(query=x, key=encoder_output, value=encoder_output, mask=None))
# 3- 数据经过第三个层的处理:前馈网络子层
decoder_output = self.feed_forward_layer(multi_output, lambda x: self.feed_forward(x))
return decoder_output解码器
作用
根据编码器的结果以及上一次预测的结果, 对下一次可能出现的'值'进行特征表示
- 代码实现
## 解码器
class Decoder(nn.Module):
def __init__(self, decoder_layer:DecoderLayer, N):
"""
定义解码器结构。也就是将解码器层重复N层
:param decoder_layer: 解码器层
:param N:
"""
# 1- 初始化父类
super().__init__()
# 2- 将解码器层创建N对象
self.decoder_layer_list = clones(decoder_layer,N)
# 3- 层归一化
"""
这东西不是必须的,只是为了缓解经过N层处理后的数据,让数据变得更加平稳。这一层不是必须的
"""
self.layer_norm = LayerNorm(decoder_layer.d_model)
def forward(self,data, mask, encoder_output):
"""
注意:for循环中两个data需要与传递给forward的参数名称完全一致,否则6层网络处理的数据就会断开
"""
for decoder_layer in self.decoder_layer_list:
data = decoder_layer(data, mask, encoder_output)
return self.layer_norm(data)- 解码器完整测试代码
# 测试解码器
# 测试最终的解码器
def use_decoder():
# 常用参数定义
d_model = 512
dropout = 0.1
# 1- 获得编码器最终结果数据。作为key、value传递给到解码器
encoder_output = use_encoder()
# 2- 准备解码器端原始目标值数据
y = torch.tensor([
# 例如法语单词索引
[1, 2, 3, 4],
[5, 6, 7, 8]
])
# 3- 构建解码器
# 3.1- 基础组件实例对象创建
# 解码器端的词嵌入层
embed = Embedding(vocab_size=1200,d_model=d_model)
# 解码器端的位置编码
position_encoding = PositionalEncoding(d_model=d_model,dropout=dropout)
# 前馈网络子层
feed_forward = FeedForward(d_model=d_model,output_dim=1024,dropout_p=dropout)
# 多头注意力子层
src_attn = MultiHeadAttention(d_model=d_model,head=8,dropout_p=dropout)
# 掩码多头自注意力子层
self_attn = MultiHeadAttention(d_model=d_model, head=8, dropout_p=dropout)
# 3.2- 拼接得到解码器层
decoder_layer = DecoderLayer(
d_model=d_model,
mask_multi_head_self_attn=self_attn,
multi_head_attn=src_attn,
feed_forward=feed_forward,
dropout_p=dropout
)
# 3.3- 解码器层堆叠N层,得到解码器
decoder = Decoder(decoder_layer,6)
# 4- 处理数据
# 4.1- 准备掩码
mask = torch.zeros(size=(8,4,4)) # 掩码多头自注意力子层 的 掩码。# 注意,第一个位置是多头的数量
# 4.2- 得到词向量+位置编码
data = embed(y)
data = position_encoding(data)
# 4.3- 调用解码器
decoder_result = decoder(data, mask, encoder_output)
print(f"解码器最终的结果:{decoder_result.shape}")输出部分
作用:通过线性变化得到指定维度的输出
- 代码实现
"""
Transformer的输出部分:
Linear线性层和Softmax激活函数
"""
import torch
import torch.nn as nn
class Output(nn.Module):
def __init__(self,d_model,vocab_size):
# 1- 初始化父类
super().__init__()
# 2- 搭建神经网络结构
self.linear = nn.Linear(in_features=d_model,out_features=vocab_size)
def forward(self,data):
"""
:param data: 解码器最终的输出结果,形状[batch_size,seq_len,d_model]
:return:
"""
return torch.softmax(self.linear(data),dim=-1)
模型搭建
- 自定义的Transformer模型搭建代码
"""
Transformer框架的结构:
编码器端_输入部分
词嵌入层
位置编码
编码器
多头自注意力子层:多头自注意力 + 层归一化 + 残差连接
前馈网络子层:前馈网络 + 层归一化 + 残差连接
解码器端_输入部分
词嵌入层
位置编码
解码器
掩码多头自注意力子层:掩码多头自注意力 + 层归一化 + 残差连接
多头注意力子层(交叉注意力):多头注意力 + 层归一化 + 残差连接
前馈网络子层:前馈网络 + 层归一化 + 残差连接
输出部分
线性层
Softmax激活函数
结果
"""
from demo01_input import *
from demo02_component import *
from demo03_encoder import *
from demo04_decoder import *
from demo05_output import *
# 创建Transformer框架类
class MyTransformer(nn.Module):
def __init__(self, en_embde_pos, encoder, de_embde_pos, decoder, output):
"""
将前面开发的各个部分组织得到完整的Transformer框架
:param en_embde_pos: 编码器端_输入部分 实例对象
:param encoder: 编码器 实例对象
:param de_embde_pos: 解码器端_输入部分 实例对象
:param decoder: 解码器 实例对象
:param output: 输出部分 实例对象
"""
# 1- 初始化父类
super().__init__()
# 2- 设置属性值
self.en_embde_pos = en_embde_pos
self.encoder = encoder
self.de_embde_pos = de_embde_pos
self.decoder = decoder
self.output = output
def forward(self,en_input,de_input,mask):
"""
Transformer前向传播
:param en_input: 输入到编码器端的词索引数据
:param de_input: 输入到解码器端的词索引数据
:param mask: 解码器使用的掩码
:return:
"""
# 1- 编码器端_输入部分:词嵌入层、位置编码
encoder_data = self.en_embde_pos(en_input)
# 2- 编码器
encoder_data = self.encoder(encoder_data)
# 3- 解码器端_输入部分:词嵌入层、位置编码
decoder_data = self.de_embde_pos(de_input)
# 4- 解码器
decoder_data = self.decoder(
data=decoder_data,
mask=mask,
encoder_output=encoder_data
)
# 5- 输出部分
return self.output(decoder_data)- 调用Transformer模型框架
# 调用Transformer框架
def get_mytransformer():
# 常用变量
d_model = 512
dropout = 0.1
de_vocab_size = 4345
# ---------------- 编码器部分 ----------------
# 词嵌入层
en_ebd = Embedding(vocab_size=1000, d_model=d_model)
# 位置编码
en_pos = PositionalEncoding(d_model=d_model, dropout=dropout, max_len=60)
# 多头自注意力
en_multi_self_attn = MultiHeadAttention(d_model=d_model, head=8, dropout_p=dropout)
# 前馈网络
en_ff = FeedForward(d_model=d_model, output_dim=1024, dropout_p=dropout)
# 组装得到编码器层
encoder_layer = EncoderLayer(
d_model=d_model,
multi_head_self_attn=en_multi_self_attn,
feed_forward=en_ff,
dropout_p=dropout
)
# 得到编码器
encoder = Encoder(encoder_layer=encoder_layer, N=6)
# ---------------- 解码器部分 ----------------
# 词嵌入层
de_ebd = Embedding(vocab_size=de_vocab_size, d_model=d_model)
# 位置编码
de_pos = PositionalEncoding(d_model=d_model, dropout=dropout, max_len=60)
# 掩码多头自注意力
de_multi_self_attn = MultiHeadAttention(d_model=d_model, head=8, dropout_p=dropout)
# 多头注意力
de_multi_attn = MultiHeadAttention(d_model=d_model, head=8, dropout_p=dropout)
# 前馈网络
de_ff = FeedForward(d_model=d_model, output_dim=1024, dropout_p=dropout)
# 组装得到编码器层
decoder_layer = DecoderLayer(
d_model=d_model,
mask_multi_head_self_attn=de_multi_self_attn,
multi_head_attn=de_multi_attn,
feed_forward=de_ff,
dropout_p=dropout
)
# 得到编码器
decoder = Decoder(decoder_layer=decoder_layer, N=6)
# ---------------- 输出部分 ----------------
output = Output(d_model=d_model, vocab_size=de_vocab_size)
# ---------------- 组装得到Transformer框架 ----------------
my_transformer = MyTransformer(
en_embde_pos=nn.Sequential(en_ebd, en_pos), # 表示 词先经过词嵌入层的处理,然后再经过位置编码的处理。注意:位置不能乱
encoder=encoder,
de_embde_pos=nn.Sequential(de_ebd, de_pos),
decoder=decoder,
output=output
)
print(f"框架结构信息:{my_transformer}")
return my_transformer- 测试Transformer模型架构
# 测试编码器
# 测试最终的编码器
def use_encoder():
# 1- 得到词向量数据:词向量 + 位置编码
position_data = use_positional_encoding()
d_model = 512
dropout = 0.1
# 2- 创建编码器的实例对象
# 2.1- 多头自注意力子层的实例对象
multi_head = MultiHeadAttention(d_model=d_model,head=8,dropout_p=dropout)
# 2.2- 前馈网络子层的实例对象
ff = FeedForward(d_model=d_model,output_dim=1024,dropout_p=dropout)
# 2.3- 编码器层的实例对象
encoder_layder = EncoderLayer(d_model=d_model,multi_head_self_attn=multi_head,feed_forward=ff,dropout_p=dropout)
# 2.4- 编码器层 堆叠N层得到编码器
encoder = Encoder(encoder_layder,6)
# 3- 数据进行前向传播
result = encoder(data=position_data)
print(f"编码器最终的输出结果是:{result.shape}")
return result