零基础入门AI:Transformer详解(自注意力机制、前馈神经网络等)
一、自注意力机制
Self-Attention Mechanism,自注意力机制(上图中的attention),用于捕捉序列数据内部依赖关系的关键技术。
人工智能领域有很多网络算法,Transformer独领风骚,它在NLP和CV中非常重要,Transformer中的核心组件就是自注意力机制
学习Transformer网络之前我们先重点学习这个网络中的自注意力机制
1. 产生背景
自注意力机制的产生与序列建模任务(如机器翻译、文本生成等)中的挑战密切相关,比如RNN、LSTM等在处理长序列时有梯度消失(或爆炸)、计算效率低、难以并行化等诸多局限性。
1.1 认识seq2seq
Seq2Seq(Sequence-to-Sequence)是NLP中的经典框架,广泛应用于机器翻译、文本摘要等。其结构包括:
- Encoder:编码器,将输入序列编码为一个固定长度的上下文向量。
- Decoder:解码器,基于上下文向量生成输出序列。
1.2 提出问题
在自注意力机制之前,序列建模任务主要依赖于以下模型:
- RNN:循环神经网络,能够处理序列数据,但存在梯度消失/爆炸问题,难以捕捉长距离依赖。
- LSTM/GRU:通过门控机制缓解了RNN的问题,但处理长序列时难以并行化,计算效率低。
共同问题是:
- 长距离依赖:随着序列长度增加,模型难以有效捕捉远距离元素之间的关系。
- 计算效率:需要逐步处理序列,无法并行计算,训练速度较慢。
- 信息瓶颈:编码器会将整个输入序列压缩为一个固定长度的向量,可能导致信息丢失,灵活度太低。
1.3 解决问题
一开始选择的是CNN的思想,但是CNN要堆叠很多层,于是就出现了自注意力机制。
1.3.1 解决并行化
使用CNN解决并行化问题。问题及解决对比:
存在的问题 | CNN |
---|---|
难以并行化 | CNN通过层的堆叠,提高感受野,使得上层输出可以捕获长时序关系 |
需要逐步处理序列,训练速度较慢 | 可并行化了,但堆叠的层过多 |
CNN 证明了并行化在序列建模中的可行性,并为后续模型(如 Transformer)提供了灵感。
1.3.2 引入自注意力机制
为了综合解决上述各种问题,提出了自注意力机制,并在Transformer中得到了广泛应用。其核心思想是:
- 长距离依赖捕获:通过计算序列中每个元素与其他元素的相关性,捕捉全局依赖关系。
- 并行计算:不依赖序列顺序,可并行处理整个序列,显著提高计算效率。
- 动态权重分配:模型可以动态地关注序列中不同位置的重要信息,不再依赖固定的上下文向量。
- 灵活性:可以处理不同长度的输入序列,不像卷积或RNN那样对输入的结构有严格要求。
1.4 使用场景
语言的含义是极度依赖上下文,同一个词或句子在不同的上下文中可能会有完全不同的含义。比如:货拉拉拉不拉拉布拉多要看拉布拉多在货拉拉上拉不拉baba~
比如下面这个机器人第二法则:
机器人第二法则
机器人必须遵守人类给它的命令,除非该命令违背了第一法则。
要想理解或处理句子中高亮的三个词语,那就必须和上下文联系起来。当模型处理这句话的时候,它必须知道:
- 「它」指代机器人
- 「命令」指代前半句话中人类给机器人下的命令,即「人类给它的命令」
- 「第一法则」指机器人第一法则的完整内容。
那么,此时我们就需要使用自注意力机制来理解上下文。
1.5 基本概念
自注意力机制,就是找到当前词和所在句子的所有的词之间的关联关系。
1.5.1 核心目标
自注意力的目标是让每个位置的表示能够根据整个序列中其他位置的信息进行加权融合,从而捕获实体之间的相互关系。
自注意力机制是要回答:“我(某个位置)应该关注谁(其他位置)?以及关注了之后该怎么融合信息?”
1.5.2 专业术语
为了提升表达能力和灵活度,我们不能直接使用一个词嵌入向量。
自注意力机制通过引入查询向量(Query)、键向量(Key)、**值向量(Value)**概念来实现序列中各元素之间的信息交互和依赖建模。
-
Q:Query
表示当前查询者的位置,用来发出问题:“我想知道对我来说谁重要”。
-
K:Key
表示被查询者的身份,是所有位置给出的“介绍信”或“标签”,告诉别人自己是谁。
-
V:Value
表示被查询者实际信息,也就是一旦你决定“关注我了”,我就把这份信息给你。
1.5.3 QKV的意义
序列中的每个 TokenTokenToken 都有 Q、K、VQ、K、VQ、K、V 三个角色:
- 所有位置之间需要【查询-响应】这样的互动,单一角色表达能力就太死板。
- “我该关注谁”是“我”和“他们”之间的交互过程,所以需要把“我”和“他们”分别建模(Q vs K)。
- 而最终融合的信息 VVV 可能和你打分 Q⋅KQ·KQ⋅K 的依据不完全相同,如:
- KKK :强调结构特征→【位置或语法角色】
- VVV: 强调语义内容→【单词的意义】
2. 实现过程
自注意力机制通过计算输入数据中每个位置与其他位置的相关性,来调整每个位置的信息表示。
2.1 输入序列
输入是一个序列,如词向量序列,假设:
X=(x1,x2,…,xn)∈Rn×dX = (x_1, x_2, \dots, x_n) \in \mathbb{R}^{n \times d} X=(x1,x2,…,xn)∈Rn×d
是 nnn 个输入,ddd 是输入维度,则自注意力的目的是捕获 nnn 个实体之间的关系。
2.2 词语关系
it代表的是animal还是street呢,对我们来说简单,但对机器来说是很难判断的。self-attention就能够让机器把it和animal联系起来。
2.3 线性变换
自注意力机制依赖于三个核心概念:查询向量Query、键向量Key、值向量Value。他们对输入 XXX 进行三次线性变换,得到三个矩阵。
x向量通过词嵌入获取
def test():embedding=nn.Embedding(10,512)#比如语料有10个单词,转换后每个单词的维度是512input=torch.randint(0,10,(512,),dtype=torch.int64)#随便给一个输入embedding_out=embedding(input)#每个数字被映射为5个数字的向量print(embedding_out,embedding_out.shape)
2.3.1 查询向量
Q = Query, 是自注意力机制中的“询问者”。每个输入都会生成一个查询向量,表示当前词的需求。
- 作用:用于与键向量计算相似度(通过点积方式),确定当前词与其他词的相关性。
- 生成方式:通过一个权重矩阵将输入数据(如词向量)映射到查询空间。
Q=XWqQ=X W_q Q=XWq
WqW_qWq 是可学习权重矩阵,维度为 d×dkd \times d_kd×dk,dkd_kdk是超参数,表示查询向量的维度。
2.3.2 键向量
**K = **Key,表示其他词的信息,供查询向量匹配。每个输入都会生成一个键向量,表示其能够提供的信息内容。
- 作用:与查询向量计算点积,生成注意力权重。点积越大,表示它们之间的相关性越强。
- 生成方式:通过一个权重矩阵将输入数据(如词向量)映射到键空间。
K=XWkK=X W_k K=XWk
WkW_kWk 是可学习权重矩阵,维度为 d×dkd \times d_kd×dk,dkd_kdk是超参数,表示键向量的维度。
2.3.3 值向量
V = Value, 值向量包含了每个输入实际的信息内容,相关性决定了信息被聚焦的程度。
- 作用:使用值向量基于注意力得分进行加权求和,生成最终的输出表示。
- 生成方式:通过一个权重矩阵将输入数据(如词向量)映射到值空间。
V=XWvV=X W_v V=XWv
WvW_vWv 是可学习权重矩阵,维度为 d×dvd \times d_vd×dv,dvd_vdv是超参数,表示值向量的维度。
注:dvd_vdv 和 dkd_kdk 通常是相等的。
2.3.4 以图示意
通过线性变换得到三个向量的变化如下图所示:
2.4 注意力得分
使用点积来计算查询向量和键向量之间的相似度(相关性),除以缩放因子 dk\sqrt{d_k}dk 来避免数值过大,使得梯度稳定更新。得到注意力得分矩阵:
Attention(Q,K)=QKTdk\text{Attention}(Q, K) = \frac{QK^T}{\sqrt{d_k}} Attention(Q,K)=dkQKT
注意力得分矩阵维度是 n×nn \times nn×n,其中 nnn 是序列的长度。每个元素 (i,j)(i, j)(i,j) 表示第 iii 个元素与第 jjj 个元素之间的相似度。
参考示意图如下:
公式: α1,i=q1⋅kidk\alpha_{1,i} = \frac{q^1 \cdot k^i}{\sqrt{d_k}}α1,i=dkq1⋅ki
2.5 归一化
为了将注意力得分转换为概率分布,需按行对得分矩阵进行 softmaxsoftmaxsoftmax 操作,确保每行的和为 1,得到的矩阵表示每个元素对其他元素的注意力权重。是的,包括自己。
Attention Weight=softmax(QKTdk)\text{Attention Weight} = \text{softmax} \left( \frac{QK^T}{\sqrt{d_k}} \right) Attention Weight=softmax(dkQKT)
具体到每行的公式如下:
α^1,i=exp(α1,i)∑jexp(α1,j)\hat{\alpha}_{1,i} = \frac{\exp(\alpha_{1,i})}{\sum_j \exp(\alpha_{1,j})} α^1,i=∑jexp(α1,j)exp(α1,i)
- α1,i\alpha_{1,i}α1,i :第 111 个词语和第 iii 个词语之间的原始注意力得分。
- α^1,i\hat{\alpha}_{1,i}α^1,i :经过归一化后的注意力得分。
2.6 加权求和
通过将注意力权重矩阵与值矩阵 VVV 相乘,得到加权的值表示。
Output=Attention Weight×V=softmax(QKTdk)×V\text{Output} =\text{Attention Weight} \times V = \text{softmax} \left( \frac{QK^T}{\sqrt{d_k}} \right) \times V Output=Attention Weight×V=softmax(dkQKT)×V
具体计算示意图如下:
More ActionsQ和K计算相似度后,经 softmaxsoftmaxsoftmax 得到注意力,再乘V,最后相加得到包含注意力的输出
注:通过上下文来描述每一个词
2.7 输出
最终得到的输出是一个维度为 n×dvn \times d_vn×dv 的新矩阵,其中每个元素的表示都被加权了。
本质:将Query和Key分别计算相似性,然后经过softmax得到相似性概率权重即注意力,再乘以Value,最后相加即可得到包含注意力的输出
至此,艺术已成,“我"就是"我”,“我"不再是"我”。
2.8 案例代码
from torch import nn
import torch
if __name__ == '__main__':# "我是人工智能的一名老师"vocab={"我":0,"是":1,"人工智能":2,"的":3,"一名":4,"老师":5}#词数 num_words=len(vocab)#创建一个嵌入层(词向量)embedding=nn.Embedding(num_words,256)#获取 "我" 的词向量embedding_vector=embedding(torch.tensor([vocab["我"]]))# print("我:",embedding_vector,embedding_vector.shape)#一个词映射一个向量(256维)#获取 "是" 的词向量embedding_vector_1=embedding(torch.tensor([vocab["是"]]))#获取 "人工智能" 的词向量embedding_vector_2=embedding(torch.tensor([vocab["人工智能"]]))#获取 "的" 的词向量embedding_vector_3=embedding(torch.tensor([vocab["的"]]))#获取 "一名" 的词向量embedding_vector_4=embedding(torch.tensor([vocab["一名"]]))#获取 "老师" 的词向量embedding_vector_5=embedding(torch.tensor([vocab["老师"]]))# embedding_vector=embedding(torch.tensor([0,1,2,3,4,5]))# print(embedding_vector.shape)#一个词映射一个向量(256维)# 进行qkv的映射:输入256维的词向量,输出4维的qkv向量q_layer=nn.Linear(256,4)k_layer=nn.Linear(256,4)v_layer=nn.Linear(256,4)# 映射出"我"的qkvq=q_layer(embedding_vector)k=k_layer(embedding_vector)v=v_layer(embedding_vector)# print(f"q:{q} k:{k} v:{v}")# 映射出"是"的qkvq_1=q_layer(embedding_vector_1)k_1=k_layer(embedding_vector_1)v_1=v_layer(embedding_vector_1)# print(f"q_1:{q_1} k_1:{k_1} v_1:{v_1}")# 映射出"人工智能"的qkvq_2=q_layer(embedding_vector_2)k_2=k_layer(embedding_vector_2)v_2=v_layer(embedding_vector_2)# print(f"q_2:{q_2} k_2:{k_2} v_2:{v_2}")# 映射出"的"的qkvq_3=q_layer(embedding_vector_3)k_3=k_layer(embedding_vector_3)v_3=v_layer(embedding_vector_3)# print(f"q_3:{q_3} k_3:{k_3} v_3:{v_3}")# 映射出"一名"的qkvq_4=q_layer(embedding_vector_4)k_4=k_layer(embedding_vector_4)v_4=v_layer(embedding_vector_4)# print(f"q_4:{q_4} k_4:{k_4} v_4:{v_4}")# 映射出"老师"的qkvq_5=q_layer(embedding_vector_5)k_5=k_layer(embedding_vector_5)v_5=v_layer(embedding_vector_5)# print(f"q_5:{q_5} k_5:{k_5} v_5:{v_5}")# 计算"我"和"我"的相似度(点积,余弦相似度)similarity=torch.matmul(q,k.T)/torch.sqrt(torch.tensor(4))print('"我"和"我"',similarity)# 计算"我"和"是"的相似度(点积,余弦相似度)similarity1=torch.matmul(q,k_1.T)/torch.sqrt(torch.tensor(4))print('"我"和"是"',similarity1)# 计算"我"和"人工智能"的相似度(点积,余弦相似度)similarity2=torch.matmul(q,k_2.T)/torch.sqrt(torch.tensor(4))print('"我"和"人工智能"',similarity2)# 计算"我"和"的"的相似度(点积,余弦相似度)similarity3=torch.matmul(q,k_3.T)/torch.sqrt(torch.tensor(4))print('"我"和"的"',similarity3)# 计算"我"和"一名"的相似度(点积,余弦相似度)similarity4=torch.matmul(q,k_4.T)/torch.sqrt(torch.tensor(4))print('"我"和"一名"',similarity4)# 计算"我"和"老师"的相似度(点积,余弦相似度)similarity5=torch.matmul(q,k_5.T)/torch.sqrt(torch.tensor(4))print('"我"和"老师"',similarity5)# 获取"我"的注意力权重similarity_all=torch.cat([similarity,similarity1,similarity2,similarity3,similarity4,similarity5],dim=1)similarity_all=similarity_all.view(1,-1)print("similarity_all",similarity_all)# 注意力得分attention_weights=torch.softmax(similarity_all,-1)print("attention_weights",attention_weights[0])# 获取"我"的加了注意力的向量(注意力向量)all_v=torch.cat([v,v_1,v_2,v_3,v_4,v_5])print("all_v",all_v)attention_vector=torch.matmul(attention_weights[0],all_v)print("attention_vector",attention_vector)#这个向量是"我"的向量,有4个维度 这个向量包含了全局所有词的信息("我"不再是"我")
思考? 怎么把每个词的嵌入向量都变成新的注意力向量
from torch import nn
import torchif __name__ == '__main__':# "我是人工智能的一名老师"vocab = {"我": 0,"是": 1,"人工智能": 2,"的": 3,"一名": 4,"老师": 5}# 词数 num_words = len(vocab)# 创建一个嵌入层(词向量)embedding = nn.Embedding(num_words, 256)# 获取所有词的词向量all_indices = torch.tensor([vocab[word] for word in ["我", "是", "人工智能", "的", "一名", "老师"]])all_embeddings = embedding(all_indices)# 进行qkv的映射:输入256维的词向量,输出4维(一般跟输入一样,这里为了打印效果好看全)的qkv向量q_layer = nn.Linear(256, 4)k_layer = nn.Linear(256, 4)v_layer = nn.Linear(256, 4)# 映射出所有词的qkvall_q = q_layer(all_embeddings) # 形状: [6, 4]all_k = k_layer(all_embeddings) # 形状: [6, 4]all_v = v_layer(all_embeddings) # 形状: [6, 4]# 计算所有查询与所有键的相似度矩阵# all_q: [6, 4], all_k.T: [4, 6] -> 结果: [6, 6]attention_scores = torch.matmul(all_q, all_k.T) / torch.sqrt(torch.tensor(4.0))# 对每一行应用softmax,得到注意力权重矩阵attention_weights = torch.softmax(attention_scores, dim=-1) # 形状: [6, 6]# 使用注意力权重对所有值向量进行加权求和# attention_weights: [6, 6], all_v: [6, 4] -> 结果: [6, 4]attention_output = torch.matmul(attention_weights, all_v)print(attention_output)
3. 多头注意力机制
Multi-Head Attention,多头注意力机制,是对自注意力机制的扩展。
3.1 基本概念
多头注意力机制的核心思想是,将注意力机制中的 Q、K、VQ、K、VQ、K、V 分成多个头(组),每个头计算出独立的注意力结果,然后将所有头的输出拼接起来,最后通过一个线性变换得到最终的输出**(分组的思想)**。
比如一个词有512维 那么可以把它分为n个组,然后每个组分别有三个w向量映射为qkv,那么qkv的结果就会得到有n组
下面以2个头为例:
公式:qi=Wqaiqi,1=Wq,1qiqi,2=Wq,2qiq^{i}=W^{q}a^{i} \quad \quad q^{i,1}=W^{q,1}q^{i}\quad\quad q^{i,2}=W^{q,2}q^{i}qi=Wqaiqi,1=Wq,1qiqi,2=Wq,2qi
表达能力
一个词本来是1维 就一个token id 一个数字 但是我们把它映射为了512维 也就是从512个方面(特征)来看待这个词:情感,词法,过去式,将来式,进行时,偏旁部首,名次,动词等等等我们都想不出来的一些方面
因为是n组注意力
比如第一组是512维中的64个 这64个可以表示从情感的特征
比如第二组是512维中的另外64个 这64个可以表示从词法上的特征
比如第x组…
通过多个并行的头在不同的子空间中学习上下文信息,让同一个句子在不同场景下表达不同的意思,增强模型的表达能力和灵活性。
3.2 多头机制实现
注:多头注意力机制通常先将词向量映射为Q、K、V,然后再分成多个头。
import torch
import torch.nn as nn# ======================
# 1. 词嵌入层 (Embedding)
# ======================
# 创建词嵌入层:假设10个单词,每个单词映射为512维向量
vocab_size = 10 # 词汇表大小
embed_dim = 512 # 嵌入维度
embedding = nn.Embedding(vocab_size, embed_dim)# 生成随机输入:序列长度为512的token索引
input_seq = torch.randint(0, vocab_size, (512,), dtype=torch.long)# 通过嵌入层:将索引转换为向量
# 输出形状: (序列长度, 嵌入维度) = (512, 512)
embedding_out = embedding(input_seq)
print("嵌入层输出形状:", embedding_out.shape) # torch.Size([512, 512]) 有512个单词,每个单词映射为512维向量# ==============================
# 2. 多头注意力权重矩阵初始化
# ==============================
num_heads = 8 # 注意力头数量
head_dim = embed_dim // num_heads # 每个头的维度 512/8=64# 初始化Q、K、V的投影矩阵 (标准Transformer实现方式)
# 注意:使用单个大矩阵而不是多个小矩阵
W_Q = torch.randn(embed_dim, embed_dim) # (512, 512)
W_K = torch.randn(embed_dim, embed_dim) # (512, 512)
W_V = torch.randn(embed_dim, embed_dim) # (512, 512)
3.2.1 全部映射
# =====================================
# 3. 计算Query, Key, Value (正确实现)
# =====================================
# 一次性计算所有头的投影
all_queries = torch.matmul(embedding_out, W_Q) # (512,512) × (512,512) = (512,512)
all_keys = torch.matmul(embedding_out, W_K) # (512,512)
all_values = torch.matmul(embedding_out, W_V) # (512,512)
3.2.2 分割多头
# 将投影结果分割成多个头
# 步骤:
# 1. 重塑形状: (seq_len, num_heads, head_dim)
# 2. 调整维度顺序: (num_heads, seq_len, head_dim)
queries = all_queries.view(512, num_heads, head_dim).permute(1, 0, 2)
keys = all_keys.view(512, num_heads, head_dim).permute(1, 0, 2)
values = all_values.view(512, num_heads, head_dim).permute(1, 0, 2)print("Queries形状:", queries.shape) # torch.Size([8, 512, 64])
print("Keys形状:", keys.shape) # torch.Size([8, 512, 64])
print("Values形状:", values.shape) # torch.Size([8, 512, 64])
3.2.3 计算注意力分数
分别计算每一个头的注意力分数
# ==============================
# 4. 计算注意力分数 (Scaled Dot-Product)
# ==============================
# 计算Q和K的点积 (每个头独立计算)
# 矩阵乘法: (8,512,64) × (8,64,512) = (8,512,512)
attention_scores = torch.matmul(queries, keys.permute(0, 2, 1))# 缩放因子 (sqrt(d_k))
scale_factor = torch.sqrt(torch.tensor(head_dim, dtype=torch.float))
attention_scores = attention_scores / scale_factor# 应用softmax得到注意力权重
attention_weights = torch.softmax(attention_scores, dim=-1)
print("注意力权重形状:", attention_weights.shape) # torch.Size([8, 512, 512])
3.2.4 计算注意力输出
v的每一个权重进行加权乘v
# ==============================
# 5. 计算注意力输出
# ==============================
# 注意力权重与Value相乘
# (8,512,512) × (8,512,64) = (8,512,64)
attention_output = torch.matmul(attention_weights, values)
3.2.5 合并多头
# ==============================
# 6. 合并多头输出
# ==============================
# 步骤:
# 1. 调整维度顺序: (num_heads, seq_len, head_dim) -> (seq_len, num_heads, head_dim)
# 2. 重塑形状: (seq_len, embed_dim)
attention_output = attention_output.permute(1, 0, 2).contiguous() # (512, 8, 64)
combined_output = attention_output.view(512, embed_dim) # (512, 512)
print("合并后的注意力输出形状:", combined_output.shape) # torch.Size([512, 512])
3.3 官方API用法
实际开发中, 官方已经帮我们实现好了就一行代码
# ===========================================
# 7. 使用PyTorch内置多头注意力层 (推荐方式)
# ===========================================
# 实际开发中推荐使用内置实现
multihead_attn = nn.MultiheadAttention(embed_dim, num_heads)
# 传入qkv
attn_output, attn_weights = multihead_attn(embedding_out, # (512,1,512)embedding_out, # (512,1,512)embedding_out # (512,1,512)
)
print("内置多头注意力输出形状:", attn_output.shape) # torch.Size([512,512])
print("内置多头注意力权重形状:", attn_weights.shape) # torch.Size([512,512])
4. 位置关联
从上述计算中可以看出,计算过程是并行的且与输入顺序无关,只与查询Query、键Key和值Value的值本身有关,这是不合理的。
4.1 位置编码
位置编码通过为每个位置的词(或元素)生成一个唯一的向量,来编码该词的位置信息。下面讲transformer网络的时候详细讲
二、层归一化
层归一化,Layer Normalization。
Layer Normalizatioh和Batch Normalization都是用来规范化中间特征分布,稳定和加速神经网络训练的,但它们在处理方式、应用场景和结构上有本质区别。
1. 核心区别
特征 | BatchNorm | LayerNorm |
---|---|---|
归一化维度 | 对每个特征维度在一个batch中归一化 | 对每个样本的所有特征维度归一化 |
应用场景 | CNN常用 | Transformer、RNN等序列建模场景常用 |
依赖 batch_size | 依赖 | 不依赖 |
推理阶段 | 需要记录训练时的均值/方差 | 否,直接使用当前样本计算 |
2. 计算差异
从计算的角度理解二者的差异。
2.1 Batch Normalization
以 2D2D2D 特征为例:
μc=1N⋅H⋅W∑n,h,wxnchw,σc2=1N⋅H⋅W∑n,h,w(xnchw−μc)2\mu_c = \frac{1}{N \cdot H \cdot W} \sum_{n,h,w} x_{nchw}, \quad \sigma_c^2 = \frac{1}{N \cdot H \cdot W} \sum_{n,h,w} (x_{nchw} - \mu_c)^2 μc=N⋅H⋅W1n,h,w∑xnchw,σc2=N⋅H⋅W1n,h,w∑(xnchw−μc)2
归一化每个通道维度:
x^nchw=xnchw−μcσc2+ϵ\hat{x}_{nchw} = \frac{x_{nchw} - \mu_c}{\sqrt{\sigma_c^2 + \epsilon}} x^nchw=σc2+ϵxnchw−μc
2.2 Layer Normalization
以每个样本为单位,对其所有特征维度归一化:
μ=1H∑i=1Hxi,σ2=1H∑i=1H(xi−μ)2\mu = \frac{1}{H} \sum_{i=1}^{H} x_i, \quad \sigma^2 = \frac{1}{H} \sum_{i=1}^{H} (x_i - \mu)^2 μ=H1i=1∑Hxi,σ2=H1i=1∑H(xi−μ)2
归一化:
x^i=xi−μσ2+ϵ\hat{x}_i = \frac{x_i - \mu}{\sqrt{\sigma^2 + \epsilon}} x^i=σ2+ϵxi−μ
2.3 案例助解
假设你有一句话经过嵌入层后的表示是一个矩阵,形状是:
[句子长度, 特征维度] = [5, 4]
一句 555 个词,每个词用 444 维向量表示:
["I", "am", "a", "good", "student"]
经过 embeddingembeddingembedding 之后得到:
I -> [1.2, -0.5, 0.3, 0.7]
am -> [1.0, 0.0, 0.1, 0.6]
a -> [0.9, 0.2, 0.4, 0.3]
good -> [1.3, -0.3, 0.5, 0.8]
student -> [1.1, -0.1, 0.2, 0.4]
它对每个词的 444 维向量分别归一化:
对 I=[1.2,−0.5,0.3,0.7]I = [1.2, -0.5, 0.3, 0.7]I=[1.2,−0.5,0.3,0.7] 这个词:
-
求均值 μ
μ=1.2+(−0.5)+0.3+0.74=0.425\mu = \frac{1.2 + (-0.5) + 0.3 + 0.7}{4} = 0.425 μ=41.2+(−0.5)+0.3+0.7=0.425 -
求方差 σ²
σ2=(1.2−0.425)2+(−0.5−0.425)2+...4≈0.4319\sigma^2 = \frac{(1.2 - 0.425)^2 + (-0.5 - 0.425)^2 + ...}{4} \approx 0.4319 σ2=4(1.2−0.425)2+(−0.5−0.425)2+...≈0.4319 -
归一化: 每一维都减去均值除以标准差
LayerNorm(xi)=xi−μσ2+ϵ\text{LayerNorm}(x_i) = \frac{x_i - \mu}{\sqrt{\sigma^2 + \epsilon}} LayerNorm(xi)=σ2+ϵxi−μ
最终你得到一个新的 [1.2,−0.5,0.3,0.7][1.2, -0.5, 0.3, 0.7][1.2,−0.5,0.3,0.7] 的“归一化版本”,然后对“am”、“a”、“good”、“student”每一个词都分别做这个过程。
三、Transformer
模型Transformer在2017年由Google发布:Attention Is All You Need,主要用于处理序列数据,是NLP中最常用架构之一。它替代了RNN及LSTM,并以此为基础衍生出诸如BERT、GPT、DeepSeek、LLaMA等几乎所有现代大语言模型。
Transformer不使用卷积和循环网络,是一个完全基于自注意力机制的模型。
1. 主要特点
Transformer 有三大主要特征。
1.1 Self-Attention
自注意力机制,Transformer的核心是自注意力机制,它允许模型在处理某个位置的输入时,能够直接与其他位置的输入交互,而不像CNN、RNN只能顺序处理数据。自注意力机制通过计算输入序列中各位置之间的相似度来决定各位置之间的影响力,从而提高了模型的表现力。
1.2 并行化能力
由于Transformer不依赖于序列的顺序处理,它的计算过程可以并行化,这就可以显著提高了训练效率。
1.3 Encoder-Decoder
Transformer 采用了典型的编码器-解码器架构。编码器负责处理输入序列,将其转换为上下文相关的表示;解码器则根据这些表示生成输出序列。
2. 模型结构
Transformer主要由编码器(Encoder)和解码器(Decoder)组成,广泛应用于自然语言处理任务,尤其是机器翻译。
3. Encoder-Decoder框架
6个结构完全相同的编码器:
6个结构完全相同的解码器:
3.1 编码器Encoder
Encoder由N个相同结构的编码模块堆积而成,原始的Transformer是6个。
模块组成部分:Multi-Head Attention、Feed Forward、Add&Norm。
3.1.1 Multi-Head Attention
就是多头注意力机制
3.1.2 Add&Norm
经过残差连接和层归一化处理,让训练过程更稳定。残差就是之前提到过的残差思想。
More ActionsLayer Normalization会将每一层神经元的输入都调整成相同均值方差,可以加快收敛。
3.1.3 Feed Forward
就是一个全连接层,用于对每个位置的输出进行非线性变换。
Feed Forward 层由两个全连接层组成
- $ FFN(x) = max(0,xW1 +b1)W2 +b2$
- 第一层激活函数为 ReLU,第二层不使用激活函数。
- xxx是输入,全连接层的输入和输出都是512512512维,中间隐层维度为 204820482048。
3.2 解码器Decoder
与编码器相似,由N个相同结构的解码模块堆积而成。它还引入了“掩蔽多头自注意力(Masked Multi-Head Attention)”层。
组成部分:Masked Multi-Head Attention、Add & Norm、Multi-Head Attention、Feed Forward
3.2.0 自回归特性
Autoregressive Property,是指模型在生成序列时,每生成一个新的元素,只能依赖前面已经生成的元素,不能依赖未来的信息,而不是一次性生成整个序列。
-
逐步生成:
- 模型一次生成一个元素,并将它作为下一次生成的输入。
-
因果性:
- 自回归模型是因果的(causal),即当前时刻的输出只依赖于过去时刻的输入,而不依赖于未来的输入。
- 这种因果性确保了模型在生成序列时不会“偷看”未来的信息。
-
数学表示:
-
假设序列 x=(x1,x2,…,xT)x = (x_1, x_2, \dots, x_T)x=(x1,x2,…,xT),自回归模型的目标是建模序列的联合概率分布 P(x)P(x)P(x)。
-
根据概率链式法则,联合概率可以分解为条件概率的乘积:
P(x)=P(x1)⋅P(x2∣x1)⋅P(x3∣x1,x2)⋅⋯⋅P(xT∣x1,x2,…,xT−1)P(x) = P(x_1) \cdot P(x_2 | x_1) \cdot P(x_3 | x_1, x_2) \cdot \dots \cdot P(x_T | x_1, x_2, \dots, x_{T-1}) P(x)=P(x1)⋅P(x2∣x1)⋅P(x3∣x1,x2)⋅⋯⋅P(xT∣x1,x2,…,xT−1) -
自回归模型通过逐步预测每个条件概率 $ P(x_t | x_1, x_2, \dots, x_{t-1})$ 来生成序列。
-
简单来说,自回归特性让模型在生成序列时能够遵循时间顺序和因果关系,避免了在训练过程中“泄漏”未来信息。
3.2.1 Masked Multi-Head
Masked Multi-Head Attention,用于解决序列任务的并行计算与自回归特性冲突问题。
3.2.1.1 产生背景
解码采用矩阵并行计算,一步就把所有目标单词预测出来。所以我们要做到:
- 当解码第1个字时,只能与第1个字的特征计算相关性;
- 当解码第2个字时,只能与第1、2个字的特征计算相关性,依此类推。
- 在翻译的过程中是顺序翻译的,即翻译完第 iii 个单词,才可以翻译第 i+1i+1i+1 个单词。
- 通过 Masked 操作可以防止第 iii 个单词知道 i+1i+1i+1 个单词及其之后的信息。
于是,引入了Masked设计。
3.2.1.2 内部结构
第一个Multi-Head Attention采用了Masked操作。
内部结构:
新增了Mask:先Mask,后通过 softmaxsoftmaxsoftmax 得到归一化的注意力权重。
Z=softmax(QKTdq+M)VM∈Rn×nZ = \text{softmax}\left( \frac{Q K^T}{\sqrt{d_q}} + M \right) V \\ M \in \mathbb{R}^{n \times n} Z=softmax(dqQKT+M)VM∈Rn×n
3.2.1.3 实现步骤
例子:以 0123450\:1\:2\:3\:4\:5012345 分别表示 <Begin> I have a cat <end>,实现步骤如下:
第一步:
Decoder输入矩阵XXX:包含 <Begin>l have a cat (0,1,2,3,4) 五个单词的表示向量。
Mask矩阵:是一个 5×55\times55×5 的上三角矩阵。
**第二步:**标准的自注意力计算中间特征,和masked无关
通过输入矩阵 XXX 计算得到 Q,K,VQ,K,VQ,K,V 矩阵,然后计算 QQQ 和 KTK^TKT的乘积 QKTQK^TQKT
第三步: 计算中间特征
计算注意力分数,在 SoftmaxSoftmaxSoftmax 之前需要使用Mask矩阵遮挡住每个单词之后的信息。
其中,黑色部分表示注意力值为 000。
**Mask $ QK^T$ **每一行的和为 111,词 000 在词 1,2,3,41,2,3,41,2,3,4 上注意力值为 000,词 111 在词 2,3,42,3,42,3,4 上注意力值为 000,以此类推。
Mask矩阵数学表示如下:
Mij={0,if i≥j−∞,if i<jM_{ij} = \begin{cases} 0, & \text{if } i \geq j \\ -\infty, & \text{if } i < j \end{cases} Mij={0,−∞,if i≥jif i<j
其中:
-
Mij=0当i≥jM_{ij} = 0 \quad 当 i \geq jMij=0当i≥j,即当前位置 iii 可以看到自己及之前的所有位置。
-
Mij=−∞当i<jM_{ij} = -\infty \quad 当i < jMij=−∞当i<j,即当前位置 iii 不允许看到未来的任何位置。
针对当前例子Masked矩阵如下:
M=(0−∞−∞−∞−∞00−∞−∞−∞000−∞−∞0000−∞00000)M = \begin{pmatrix} 0 & -\infty & -\infty & -\infty & -\infty \\ 0 & 0 & -\infty & -\infty & -\infty \\ 0 & 0 & 0 & -\infty & -\infty \\ 0 & 0 & 0 & 0 & -\infty \\ 0 & 0 & 0 & 0 & 0 \end{pmatrix} M=00000−∞0000−∞−∞000−∞−∞−∞00−∞−∞−∞−∞0
第四步: 计算输出特征
使用注意力矩阵**Mask $ QK^T$ **与矩阵 VVV 相乘,得到输出 ZZZ,单词 111 的输出向量 Z1Z_1Z1,只包含单词 111 信息,单词 222 的输出向量 Z2Z_2Z2,只包含单词 1、21、21、2 信息,以此类推。
3.2.2 Add&Norm
同上:代码实现的时候,先Norm后Add
3.2.3 Multi-Head Attention
这是解码模块第二个Multi-Head Attention
说明:
- 根据Encoder的输出 CCC 计算得到 K,VK,VK,V;
- 根据上一个 Decoder block 的输出 ZZZ 计算 QQQ;
- 如果是第一个Decoder block则使用输入矩阵 XXX 进行计算;
3.2.4 Feed Forward
同上
4. 输入模块
输入模块是将原始输入数据转换为适合网络处理的向量表示:单词嵌入向量 + 位置编码向量。
词嵌入向量:
可以采用 Word2Vec、Fasttext、Glove等算法预训练得到,也可以在 Transformer 中训练得到。
位置编码向量:
- Transformer模型没有像RNN或CNN那样的顺序信息;
- 需要显式地将位置信息加入到输入中,以让模型知道各个元素在序列中的位置;
- 位置编码通过添加一个与词嵌入同维度的向量来实现,它为每个位置提供一个唯一的表示;
- 位置编码可以是固定函数或可学习的。
4.1 单词输入
单词输入采用词嵌入就可以了,比较直接。
4.1.1 向量填充
不同训练句子单词个数通常是不一样,为了确保同一batch中的序列长度一致,便于批量处理,我们需要做向量填充操作。
- 可以简单统计所有训练句子的单词个数,取最大值,如 101010;
- 编码器输入是 10×51210\times51210×512,额外填充的512维向量可以采用固定的标志编码得到
- 如果遇到极端的特别长的,可以丢掉一部分特殊字符。
- 需要PaddingMask进行掩盖。
4.2 位置编码
PE(Position Embedding )
在自然语言中,词语的顺序非常重要。比如:“猫追狗”和“狗追猫”意思完全不同。
但是,Transformer 模型没有像 RNN 那样的“顺序处理”机制,也不是像 CNN 那样通过滑动窗口感知局部顺序。它使用的是自注意力机制,这种机制本身是“无序”的——它只关心哪些词和哪些词相关,不关心它们在第几个位置。
所以,我们必须手动告诉模型:“这个词在第1个位置”,“那个词在第5个位置”……
位置编码 是一个向量,用来表示一个词在句子中的位置。
- 每个位置(第1个词、第2个词……)都对应一个唯一的编码向量。
- 这个向量的长度和词向量(word embedding)相同(比如都是 512 维)。
- 然后把这个位置编码加到词向量上,形成最终输入。
最终输入 = 词向量 + 位置编码
这样,模型看到的就不再是“孤立的词”,而是“带有位置信息的词”。
位置编码可以通过正弦和余弦函数生成,也可以通过学习得到。
Transformer 论文使用正弦(sin)和余弦(cos)函数来生成位置编码,而不是直接学一个表。这是为什么呢?
优点一:唯一性 + 规律性
每个位置都会生成一个独特的向量,不会重复。
优点二:能表达“相对位置”
模型可以学会:
“位置
pos + k
的编码” 可以从 “位置pos
的编码” 推导出来。
这有助于模型理解“前后关系”,比如“动词通常出现在主语后3个位置”。
优点三:能处理更长的句子(外推能力强)
即使训练时最长只见过 100 个词的句子,测试时遇到 150 个词的句子,也能通过公式继续计算位置编码,而不会报错。
4.2.1 固定函数
对于位置 pos
(比如第2个词,pos=2
),在第 i
维上的编码值是:
其中:
pos
:词在句子中的位置(0, 1, 2, …, n-1)i
:向量的维度索引(0, 1, 2, …, d_model-1)d_model
:模型的维度(论文中是 512)- 100001000010000 是一个超参数,用于控制频率的衰减。
比如: sentence = “ 我 喜欢 自然语言 处理” 词数N = 5 维度d = 4,句子长度为 555,编码向量维数 D=4D=4D=4
- 计算得到的位置编码会直接加到词嵌入中。
- 所有句子共享这个位置编码矩阵。
4.2.1.1 函数特点
- 连续性:通过正弦和余弦的方式,位置编码具有平滑的特性,便于捕捉位置间的连续性。
- 可区分性:每个位置编码的值都是唯一的,可以帮助模型区分不同位置。
4.2.1.2 位置可控性
正弦余弦函数特征:
sin(α+β)=sinαcosβ+cosαsinβcos(α+β)=cosαcosβ−sinαsinβ\sin(\alpha + \beta) = \sin \alpha \cos \beta + \cos \alpha \sin \beta \\ \cos(\alpha + \beta) = \cos \alpha \cos \beta - \sin \alpha \sin \beta sin(α+β)=sinαcosβ+cosαsinβcos(α+β)=cosαcosβ−sinαsinβ
基于此特性,对于固定长度的间距 kkk,PE(pos+k)PE(pos+k)PE(pos+k) 可以用 PE(pos)PE(pos)PE(pos) 计算得到。
PE(pos+k,2i)=PE(pos,2i)×PE(k,2i+1)+PE(pos,2i+1)×PE(k,2i)PE(pos+k,2i+1)=PE(pos,2i+1)×PE(k,2i+1)−PE(pos,2i)×PE(k,2i)PE(pos+k,2i)=PE(pos,2i)\times PE(k,2i+1)+PE(pos,2i+1)\times PE(k,2i) \\ PE(pos+k,2i+1)=PE(pos,2i+1)\times PE(k,2i+1)-PE(pos,2i)\times PE(k,2i) PE(pos+k,2i)=PE(pos,2i)×PE(k,2i+1)+PE(pos,2i+1)×PE(k,2i)PE(pos+k,2i+1)=PE(pos,2i+1)×PE(k,2i+1)−PE(pos,2i)×PE(k,2i)
那这个有啥玩意用处呢?假设k=1,那么:
- 下一个词语的位置编码向量可由前面的编码向量线性表示;
- 使模型能适应比训练集里面最长的句子更长的句子。
假设训练集最长的句子是 202020 个单词,输入长度为 212121 个单词的句子,则可以计算出第 212121 位的位置向量
5. 解码输出
解码器的最终输出通过线性变换和 SoftmaxSoftmaxSoftmax 函数,生成每个时间步的概率分布。
5.1 基本流程
说明:
-
编码器输出:K 和 V;
- 通过线性变换映射出 K 和 V
- 输入到解码器的第二个注意力模块,这就是Cross-Attention
-
解码器输入:
- 第一个编码器:<BOS> + K 和 V
- 后续的解码器:已有解码器输出 + K 和 V
-
针对案例:
- 输入开始解码[<BOS>],预测 iii
- 输入已解码的[<BOS>, iii],预测 amamam
- 输入已解码的[<BOS>, i,ami, ami,am],预测 aaa,以此类推
- 输入已解码的[<BOS> , i,am,a,studenti ,am,a,studenti,am,a,student],预测 <EOS>
- 预测结束
5.2 预测输出
解码器经线性变换后,基于 SoftmaxSoftmaxSoftmax 输出概率值,它表示对词汇表中每个词的概率。
具体过程:
- 解码器输出:每个时间步输出一个隐藏状态。
- 线性层:解码器的输出经线性变换层映射到目标词汇表,得到一个原始分数。
- SoftmaxSoftmaxSoftmax: 经 SoftmaxSoftmaxSoftmax后,得到一个概率分布,最大概率值对应的词就是预测结果。
5.3 训练与推理
在训练与推理过程中,解码器的工作机制是有差异的。
5.3.1 训练阶段
- 在训练时,解码器的当前输入是真实目标序列,叫做teacher forcing。
- 直接使用真实标签输入,避免了错误传播,模型能更快收敛。
x | 输入给解码器x | y | 目标标签(预测目标)y |
---|---|---|---|
x1 | <BOS> | y1 | I |
x2 | <BOS>, I | y2 | am |
x3 | <BOS>, I, am | y3 | a |
x_n | … | y_n | … |
5.3.2 推理阶段
-
没有真实答案提供,模型只能自己一步步预测出来!
-
每一步的输出会被作为下一步的输入
当前输入给解码器 模型预测输出 <BOS>
I
(模型预测的)<BOS>, I
am
(模型预测的)<BOS>, I, am
a
(模型预测的)<BOS>, I, am, a
student
(模型预测的)<BOS>, I, am, a, student
<EOS>
(模型预测的) -
如前面预测错了,后面会受到影响,这就是所谓的 暴露偏差 问题。
6. 认知掩码
Padding Mask和Sequence Mask是两种常见的掩码机制。
6.1 PaddingMask
填充掩码。
输入的序列长度通常不同,为了进行批处理,需要对短序列进行填充,使得所有输入序列长度一致。填充符号(如 <PAD>
)并不代表实际信息,因此需要被掩盖,以确保不会影响模型计算。
[‘我’, ‘喜欢’, , ]
6.1.1 使用场景
- 填充掩码用于自注意力计算中,避免填充部分对模型产生影响。
- 填充掩码一般用于编码器部分,也会传递到解码器部分,确保解码器不会基于填充符号做决策。
6.1.2 具体实现
- 实现:填充掩码通常是一个与输入序列长度相同的向量,把填充位置标记为 111,其他标记为 000。
- 掩膜过程:在计算注意力权重时,把填充位置的注意力值设为 −∞-∞−∞,SoftmaxSoftmaxSoftmax 后为 000,从而忽略这些位置。
6.2 SequenceMask
序列掩码主要用于解码阶段,尤其在生成任务中,确保模型在生成每个词时只依赖已生成部分,而不会看到未来的词。
6.2.1 使用场景
- 在生成任务中,解码器的自注意力机制需要被限制,以确保只使用已生成的词进行生成。
- Sequence Mask主要用于解码器的自注意力层中,防止模型在生成当前词时,看到后续词汇。
6.2.2 具体实现
- 实现:序列掩码通常是一个上三角矩阵,掩码位置标记为 −∞-∞−∞,表示这些位置的词在当前步骤不可见。
- 掩膜过程:在计算解码器自注意力时,序列掩码将未来的词的注意力设为 −∞-∞−∞,确保模型只能“看到”已生成的词。
6.3 掩码对比
掩码类型 | 使用场景 | 描述 |
---|---|---|
Padding Mask | 输入序列存在填充符号时 | 避免填充符号对自注意力计算产生影响,确保模型只关注有效单词。 |
Sequence Mask | 解码阶段,生成任务中的自回归生成 | 防止模型看到当前时间步之后的词,确保生成顺序的正确性。 |
- Padding Mask主要用于编码器和解码器中处理输入填充符号的问题。
- Sequence Mask主要应用于解码器,确保生成过程中模型只依赖于已经生成的部分,而不能访问未来的词。
掩码机制确保了自注意力机制能够在不同任务中正确处理输入和生成序列,保障模型的有效性和稳定性。