序言:总结一下之前transformer的手撕代码demo。顺便附带一些简单的面试问题.
transformer网络结构:
transformer流程简述:
transformer流程:
①先进行数据前处理Tokenizer符号化,nn.Embedding词嵌入,对齐。
②然后Positional Encoding位置编码(生成正弦/余弦位置编码)
③进行Encoder编码
1,多头注意力: QKA同源,多头分割,计算注意力加权,注意力权重 * V值矩阵,合并输出
2,残差连接后做LN
3,前馈网络,等于做了高效的全连接
4,再残差连接做LN
5,输出K,V给Decoder用
④进行Decoder解码。
1,多头注意力: QKA同源
2,残差连接后做LN
3,交叉多头注意力:Decoder的自注意力输出,与来自于EncoderK,V。
4,残差连接后做LN
5,前馈网络
⑤FC全连接,softmax分类。
transformer模块代码(demo,实测可运行)
# Written on June 2, 2025, by sn
# just a mode for understand transformer
import torch
import numpy as np
import torch.nn as nn
class MultiHeadAttention(nn.Module):
"""多头注意力机制"""
def __init__(self, d_k: int, d_v: int, d_model: int, num_heads: int, p: float = 0.):
"""
Args:
d_k: 每个头的键/查询维度
d_v: 每个头的值维度
d_model: 模型维度
num_heads: 注意力头数
p: dropout概率
"""
super(MultiHeadAttention, self).__init__()
self.d_model = d_model
self.d_k = d_k
self.d_v = d_v
self.num_heads = num_heads
self.dropout = nn.Dropout(p)
# 线性投影层
self.W_Q = nn.Linear(d_model, d_k * num_heads)
self.W_K = nn.Linear(d_model, d_k * num_heads)
self.W_V = nn.Linear(d_model, d_v * num_heads)
self.W_out = nn.Linear(d_v * num_heads, d_model)
# 使用He初始化
nn.init.normal_(self.W_Q.weight, mean=0, std=np.sqrt(2.0 / (d_model + d_k)))
nn.init.normal_(self.W_K.weight, mean=0, std=np.sqrt(2.0 / (d_model + d_k)))
nn.init.normal_(self.W_V.weight, mean=0, std=np.sqrt(2.0 / (d_model + d_v)))
nn.init.normal_(self.W_out.weight, mean=0, std=np.sqrt(2.0 / (d_model + d_v)))
pass
def forward(self, Q: torch.Tensor, K: torch.Tensor, V: torch.Tensor,
attn_mask: torch.Tensor = None, **kwargs) -> torch.Tensor:
"""
Args:
Q: 查询矩阵 (N, q_len, d_model)
K: 键矩阵 (N, k_len, d_model)
V: 值矩阵 (N, k_len, d_model)
attn_mask: 注意力掩码 (N, q_len, k_len)
Returns:
多头注意力输出 (N, q_len, d_model)
"""
'''1. 获取输入形状和参数'''
N = Q.size(0) # batch_size
q_len, k_len = Q.size(1), K.size(1) # 查询和键的长度
d_k, d_v = self.d_k, self.d_v # 每个头的键/值维度
num_heads = self.num_heads # 头数
'''2. 线性投影 + 多头分割 '''
# Q.shape: (N, q_len, d_model)->
# W_Q(Q): (N, q_len, d_k * num_heads)->view: (N, q_len, num_heads, d_k)->transpose: (N, num_heads, q_len, d_k)
Q = self.W_Q(Q).view(N, -1, num_heads, d_k).transpose(1, 2)
K = self.W_K(K).view(N, -1, num_heads, d_k).transpose(1, 2)
V = self.W_V(V).view(N, -1, num_heads, d_v).transpose(1, 2)
'''3. 处理注意力掩码'''
if attn_mask is not None:
assert attn_mask.size() == (N, q_len, k_len), "掩码形状应为(batch, q_len, k_len)"
# 将掩码广播到多头维度:(N, q_len, k_len)
# -> unsqueeze(1): (N, 1, q_len, k_len) -> expand: (N, num_heads, q_len, k_len)
attn_mask = attn_mask.unsqueeze(1).expand(-1, num_heads, -1, -1)
'''4. 计算缩放点积注意力'''
# 计算注意力分数: Q * K^T / sqrt(d_k) scores形状: (N, num_heads, q_len, k_len)
# Q 和 K 相乘可以 衡量两个向量的相似度,
scores = torch.matmul(Q, K.transpose(-1, -2)) / np.sqrt(d_k)
# 应用掩码(将需要屏蔽的位置设为极小的负值,softmax后接近0)
if attn_mask is not None:
scores.masked_fill_(attn_mask, -1e4)
# Softmax归一化得到注意力权重
attns = torch.softmax(scores, dim=-1) # 形状: (N, num_heads, q_len, k_len)
''' 5. 计算加权和 '''
output = torch.matmul(attns, V) # 注意力权重 * 值矩阵
'''6. 合并多头输出'''
# output形状: (N, num_heads, q_len, d_v) tips:contiguous()内存连续
# -> transpose(1,2): (N, q_len, num_heads, d_v)-> view: (N, q_len, num_heads * d_v) [合并所有头的输出]
output = output.transpose(1, 2).contiguous().view(N, -1, d_v * num_heads)
'''7. 最终线性投影'''
# output形状: (N, q_len, num_heads * d_v) -> W_out: (N, q_len, d_model)
end_output = self.W_out(output)
return end_output
pass
class PoswiseFFN(nn.Module):
"""位置前馈网络"""
def __init__(self, d_model: int, d_ff: int, p: float = 0.):
"""
Args:
d_model: 模型维度
d_ff: 前馈网络隐藏层维度
p: dropout概率
"""
super(PoswiseFFN, self).__init__()
self.d_model = d_model
self.d_ff = d_ff
self.conv1 = nn.Conv1d(d_model, d_ff, 1) # 等价于(高效)全连接层 并升维
self.conv2 = nn.Conv1d(d_ff, d_model, 1) # 降维保持维度一致
self.relu = nn.ReLU(inplace=True)
self.dropout = nn.Dropout(p=p)
def forward(self, X: torch.Tensor) -> torch.Tensor:
"""
Args:
X: 输入张量 (N, seq_len, d_model)
Returns:
输出张量 (N, seq_len, d_model)
"""
out = self.conv1(X.transpose(1, 2)) # (N, d_ff, seq_len)
out = self.relu(out)
out = self.conv2(out).transpose(1, 2) # (N, d_model, seq_len)
out = self.dropout(out)
return out
class EncoderLayer(nn.Module):
"""编码器层"""
def __init__(self, dim: int, n: int, dff: int, dropout_posffn: float, dropout_attn: float):
"""
Args:
dim: 输入维度
n: 注意力头数
dff: 前馈网络隐藏层维度
dropout_posffn: 前馈网络dropout概率
dropout_attn: 注意力dropout概率
多头:并行计算多个注意力子空间,提升模型的表达能力、泛化能力和对复杂依赖关系的捕捉能力
"""
super(EncoderLayer, self).__init__()
assert dim % n == 0, "模型维度必须能被头数整除"
hdim = dim // n # 每个头的维度
'''1, Multi-HeadAttention多头注意力构建'''
self.multi_head_attn = MultiHeadAttention(hdim, hdim, dim, n, dropout_attn)
'''2, Add&Norm 链接和LN'''
self.norm1 = nn.LayerNorm(dim)
self.norm2 = nn.LayerNorm(dim)
'''3,Feed Forward 前馈网络 '''
self.poswise_ffn = PoswiseFFN(dim, dff, p=dropout_posffn)
pass
def forward(self, enc_in: torch.Tensor, attn_mask: torch.Tensor = None) -> torch.Tensor:
"""
Args:
enc_in: 编码器输入 (N, seq_len, dim)
attn_mask: 注意力掩码
Returns:
编码器输出 (N, seq_len, dim)
"""
'''part one'''
residual = enc_in # 保存residual用于做残差连接
x = self.multi_head_attn(enc_in, enc_in, enc_in, attn_mask) # Multi-HeadAttention 多头注意力处理
out = self.norm1(residual + x) # Add & Norm 残差连接后LN
'''part two'''
residual = out # 保存part one处理后的residual用于做后面的残差连接
x = self.poswise_ffn(out) # 前馈神经网络处理
end_out = self.norm2(residual + x) # Add & Norm 残差连接后LN
return end_out
pass
def pos_sinusoid_embedding(seq_len: int, d_model: int) -> torch.Tensor:
"""
生成正弦/余弦位置编码
Args:
seq_len: 序列长度
d_model: 模型维度
Returns:
形状为(seq_len, d_model)的位置编码矩阵
"""
embeddings = torch.zeros((seq_len, d_model))
for i in range(d_model):
# 偶数位置用sin,奇数位置用cos
f = torch.sin if i % 2 == 0 else torch.cos
# 计算位置编码值
embeddings[:, i] = f(torch.arange(0, seq_len) / np.power(1e4, 2 * (i // 2) / d_model))
return embeddings.float()
class Encoder(nn.Module):
'''编码器'''
def __init__(
self, dropout_emb: float, dropout_posffn: float, dropout_attn: float,
num_layers: int, enc_dim: int, num_heads: int, dff: int, tgt_len: int,):
"""
Args:
dropout_emb: 嵌入层dropout概率
dropout_posffn: 前馈网络dropout概率
dropout_attn: 注意力dropout概率
num_layers: 编码器层数
enc_dim: 编码器维度
num_heads: 注意力头数
dff: 前馈网络隐藏层维度
tgt_len: 最大序列长度
"""
super().__init__()
'''1,Positional Encoding位置编码 (使用预训练的正弦/余弦编码)'''
self.pos_emb = nn.Embedding.from_pretrained(pos_sinusoid_embedding(tgt_len, enc_dim), freeze=True)
self.emb_dropout = nn.Dropout(dropout_emb) # 嵌入层dropout(可选)
'''2, Encoder x N 正式构建6个编码器 '''
self.layers = nn.ModuleList([
EncoderLayer(enc_dim, num_heads, dff, dropout_posffn, dropout_attn) for _ in range(num_layers)
])
pass
def forward(self,X: torch.Tensor, X_lens: torch.Tensor, mask: torch.Tensor = None):
"""
Args:
X: 输入序列 (N, seq_len, d_model)
X_lens: 每个序列的实际长度
mask: 注意力掩码
Returns:
编码器输出 (N, seq_len, d_model)
"""
batch_size, seq_len, d_model = X.shape # X参数获取
out = X + self.pos_emb(torch.arange(seq_len, device=X.device)) # 添加位置编码
out = self.emb_dropout(out) # 应用dropout
for layer in self.layers: # 遍历编码器
out = layer(out, mask) # 6次编码
return out
pass
class DecoderLayer(nn.Module):
"""解码器层"""
def __init__(self, dim: int, n: int, dff: int, dropout_posffn: float, dropout_attn: float):
"""
Args:
dim: 输入维度
n: 注意力头数
dff: 前馈网络隐藏层维度
dropout_posffn: 前馈网络dropout概率
dropout_attn: 注意力dropout概率
"""
super(DecoderLayer, self).__init__()
assert dim % n == 0, "模型维度必须能被头数整除"
hdim = dim // n # 每个头的维度
'''1 多头注意力(自注意力和编码器-解码器注意力)'''
self.dec_attn = MultiHeadAttention(hdim, hdim, dim, n, dropout_attn) # 带掩码
self.enc_dec_attn = MultiHeadAttention(hdim, hdim, dim, n, dropout_attn) # 不带掩码
'''2 前馈网络'''
self.poswise_ffn = PoswiseFFN(dim, dff, p=dropout_posffn)
# 层归一化
self.norm1 = nn.LayerNorm(dim)
self.norm2 = nn.LayerNorm(dim)
self.norm3 = nn.LayerNorm(dim)
def forward(
self, dec_in: torch.Tensor, enc_out: torch.Tensor,
dec_mask: torch.Tensor = None, dec_enc_mask: torch.Tensor = None,
cache: torch.Tensor = None, freqs_cis: torch.Tensor = None) -> torch.Tensor:
"""
Args:
dec_in: 解码器输入 (N, seq_len, dim)
enc_out: 编码器输出 (N, seq_len, dim)
dec_mask: 解码器自注意力掩码
dec_enc_mask: 编码器-解码器注意力掩码
cache: 用于推理的缓存(未使用)
freqs_cis: RoPE旋转位置编码(未使用)
Returns:
解码器输出 (N, seq_len, dim)
"""
'''part one'''
residual = dec_in # 保存residual 用做残差连接
x = self.dec_attn(dec_in, dec_in, dec_in, dec_mask) # 带掩码多头注意力
dec_out = self.norm1(residual + x) # Add & Norm 残差连接后LN
'''part two'''
residual = dec_out # 保存上次residual 用做下次残差连接
x = self.enc_dec_attn(dec_out, enc_out, enc_out, dec_enc_mask) # 不带掩码多头注意力 两个值来自于编码器,一个来自于解码器
dec_out = self.norm2(residual + x) # Add & Norm 残差连接后LN
'''part three'''
residual = dec_out # 保存上次residual 用做下次残差连接
x = self.poswise_ffn(dec_out)
end_out = self.norm3(x + residual)
return end_out
class Decoder(nn.Module):
'''解码器'''
def __init__(
self, dropout_emb: float, dropout_posffn: float, dropout_attn: float,
num_layers: int, dec_dim: int, num_heads: int, dff: int, tgt_len: int, tgt_vocab_size: int,
):
"""
Args:
dropout_emb: 嵌入层dropout概率
dropout_posffn: 前馈网络dropout概率
dropout_attn: 注意力dropout概率
num_layers: 解码器层数
dec_dim: 解码器维度
num_heads: 注意力头数
dff: 前馈网络隐藏层维度
tgt_len: 目标序列最大长度
tgt_vocab_size: 目标词汇表大小
"""
super(Decoder, self).__init__()
'''1 目标词嵌入'''
self.tgt_emb = nn.Embedding(tgt_vocab_size, dec_dim)
self.dropout_emb = nn.Dropout(p=dropout_emb)
'''2 位置编码'''
self.pos_emb = nn.Embedding.from_pretrained(pos_sinusoid_embedding(tgt_len, dec_dim), freeze=True)
'''3, Decoder x N 正式构建6个解码器 '''
self.layers = nn.ModuleList([
DecoderLayer(dec_dim, num_heads, dff, dropout_posffn, dropout_attn) for _ in range(num_layers)
])
def forward(
self, labels: torch.Tensor, enc_out: torch.Tensor,
dec_mask: torch.Tensor, dec_enc_mask: torch.Tensor,) -> torch.Tensor:
"""
Args:
labels: 目标序列 (N, seq_len)
enc_out: 编码器输出 (N, seq_len, dim)
dec_mask: 解码器自注意力掩码
dec_enc_mask: 编码器-解码器注意力掩码
cache: 用于推理的缓存(未使用)
Returns:
解码器输出 (N, seq_len, dec_dim)
"""
tgt_emb = self.tgt_emb(labels) # 目标词嵌入
pos_emb = self.pos_emb(torch.arange(labels.size(1), device=labels.device)) # 加入位置编码
dec_out = self.dropout_emb(tgt_emb + pos_emb) # 应用dropout
for layer in self.layers: # 遍历解码器
dec_out = layer(dec_out, enc_out, dec_mask, dec_enc_mask) # 6解编码
return dec_out
pass
def get_len_mask(b: int, max_len: int, feat_lens: torch.Tensor, device: torch.device) -> torch.Tensor:
"""
生成长度掩码,用于屏蔽padding部分
Args:
b: batch size
max_len: 序列最大长度
feat_lens: 每个样本的实际长度
device: 设备类型
Returns:
形状为(b, max_len, max_len)的布尔掩码,True表示需要屏蔽的位置
"""
# 初始化为全1(需要屏蔽),然后将有效部分设为0
attn_mask = torch.ones((b, max_len, max_len), device=device)
for i in range(b):
attn_mask[i, :, :feat_lens[i]] = 0 # 有效部分设为0
return attn_mask.to(torch.bool)
def get_subsequent_mask(b: int, max_len: int, device: torch.device) -> torch.Tensor:
"""
生成解码器的自回归掩码(防止看到未来信息)
Args:
b: batch size
max_len: 序列最大长度
device: 设备类型
Returns:
上三角矩阵(b, max_len, max_len),对角线以上为True(需要屏蔽)
"""
return torch.triu(torch.ones((b, max_len, max_len), device=device), diagonal=1).to(torch.bool)
def get_enc_dec_mask(b: int, max_feat_len: int, feat_lens: torch.Tensor, max_label_len: int, device: torch.device):
"""
生成编码器-解码器注意力掩码
Args:
b: batch size
max_feat_len: 编码器输入最大长度
feat_lens: 每个编码器输入的实际长度
max_label_len: 解码器输入最大长度
device: 设备类型
Returns:
形状为(b, max_label_len, max_feat_len)的掩码
"""
attn_mask = torch.zeros((b, max_label_len, max_feat_len), device=device) # (b, seq_q, seq_k)
for i in range(b):
attn_mask[i, :, feat_lens[i]:] = 1 # 将padding部分设为1(需要屏蔽)
return attn_mask.to(torch.bool)
class Transformer(nn.Module):
"""Transformer模型"""
def __init__(self, frontend: nn.Module, encoder: nn.Module, decoder: nn.Module, dec_out_dim: int, vocab: int):
super().__init__()
self.frontend = frontend # 前端特征提取模型
self.encoder = encoder # 编码器模型
self.decoder = decoder # 解码器模型
self.linear = nn.Linear(dec_out_dim, vocab) # 全连接线性输出层
pass
def forward(self, X: torch.Tensor, X_lens: torch.Tensor, labels: torch.Tensor):
'''输入校验'''
X_lens = X_lens.long() # 确保长度是整数(用于掩码生成)
labels = labels.long() # 确保标签是整数(用于Embedding)
b = X.size(0) # batch size 获取
device = X.device # 运行设备处理
'''前端特征提取'''
out = self.frontend(X)
max_feat_len = out.size(1) # 前端可能进行子采样,需重新计算 输入序列的最大长度
max_label_len = labels.size(1) # 需重新计算 输出序列的最大长度
'''编码输出'''
enc_mask = get_len_mask(b, max_feat_len, X_lens, device) # 获取 生成长度掩码,用于屏蔽padding
enc_out = self.encoder(out, X_lens, enc_mask) # 编码输出
'''解码输出'''
dec_mask = get_subsequent_mask(b, max_label_len, device) # 获取 自回归掩码, 用于屏蔽"未来"信息
dec_enc_mask = get_enc_dec_mask(b, max_feat_len, X_lens, max_label_len, device) # 获取 注意力掩码,
dec_out = self.decoder(labels, enc_out, dec_mask, dec_enc_mask) # 解码输出
'''输出层'''
end_out = self.linear(dec_out) # 全连接输出层
return end_out
pass
if __name__ == '__main__':
# Q1 传入的前处理层,编码器,解码器,隐藏层维度,词汇表大小是否合理。
# Q2 前端特征提取时,重新计算max_feat_len和max_label_len原因
# Q3 长度掩码 ,自回归掩码 , 注意力掩码 原文只有 多头注意力掩码
# Q4 多头注意力中多头的具体实现
'''测试配置'''
batch_size = 16 # 每次训练输入的样本数量
max_feat_len = 100 # 输入序列的最大长度(时间步数)
fbank_dim = 80 # 输入特征的维度(如FBank特征维度)
hidden_dim = 512 # Transformer隐藏层维度/嵌入维度(d_model)
vocab_size = 26 # 输出词汇表大小
max_label_len = 100 # 输出序列的最大长度
# 生成随机输入特征 (batch_size, 时间步, 特征维度)
# 形状说明:(16, 100, 80) - 16个样本,每个样本100帧,每帧80维FBank特征
fbank_feature = torch.randn(batch_size, max_feat_len, fbank_dim)
# 生成每个样本的实际长度 (batch_size,)
# 值范围:1到max_feat_len之间的随机整数,表示每个样本的有效长度
feat_lens = torch.randint(1, max_feat_len, (batch_size,))
# 生成随机标签序列 (batch_size, 输出序列长度)
# 值范围:0-25(对应26个字母),形状(16, 100)
labels = torch.randint(0, 26, (batch_size, max_label_len))
# 生成每个标签序列的实际长度 (batch_size,)
# 值范围:1-10之间的随机整数,表示每个输出序列的有效长度
label_lens = torch.randint(1, 10, (batch_size,))
'''1,Input Embedding前处理层构建: 输入嵌入层 特征提取并与模型维度对齐 '''
feature_extractor = nn.Linear(fbank_dim, hidden_dim)
'''2,Encoder 编码器构建'''
encoder = Encoder(
dropout_emb=0.1, dropout_posffn=0.1, dropout_attn=0.,
num_layers=6, enc_dim=hidden_dim, num_heads=8, dff=2048, tgt_len=2048
)
'''3,Decoder 解码器构建'''
decoder = Decoder(
dropout_emb=0.1, dropout_posffn=0.1, dropout_attn=0.,
num_layers=6, dec_dim=hidden_dim, num_heads=8, dff=2048, tgt_len=2048, tgt_vocab_size=vocab_size
)
'''4,transformer实例化
传入:前处理层,编码器,解码器,隐藏层维度,词汇表大小'''
transformer = Transformer(feature_extractor, encoder, decoder, hidden_dim, vocab_size)
'''transformer 前向传播测试'''
logits = transformer(fbank_feature, feat_lens, labels)
print("输出形状:([batch_size,max_label_len,vocab_size])", logits.shape)
transformer面试问题:
①为什么要进行位置编码?语言是带有时序关系的,所以需要我们加一个位置编码用来区分先后注意力机制结合词义+位置,正确捕捉语义。(举例:我 爱 你, 你 爱 我)
②核心模块(能力)?多头自注意力-->捕捉序列内部的长距离依赖关系,对于上下文的理解。
③怎么理解self-attion(普通描述):?" 今天我来面试 " ,进行分词(今天,我,来,面试)重复每个词和其他词之间的关系相关性打分。
⑧Q和K相乘softmax得到分数?点积运算 Q⋅K可以 衡量两个向量的相似度,点积值越大 → 向量方向越接近(相似度越高)
④为什么要(➗根号d)?1,首先进行缩放有利于softmax函数运算,避免梯度消失。独立同分布的随机变量点积的方差大致是根号d:起到了归一化的作用,更符合高斯分布。
⑤怎么理解多头?从多角度,词性,词义,等不同方法去捕捉多样性特征。多个独立的线性投影和注意力计算增强模型的非线性表达能力。并行执行,显著加快训练和推理速度。
⑥为什么要加Sequence Mask 和padding Mask?
Sequence Mask :整句输入确保Decoder在训练时只能基于已生成的部分预测下一个词。避免看到未来信息。
Padding Mask:处理批量训练中的因为需要对齐而变长序列,加入掩码避免无效计算。例子 1["I", "love", "NLP"] ,2["Hello", "world", ""]--> [1, 1, 1] , [1, 1, 0]
⑦CNN与transofmer提取特征的不同之处在哪?
CNN : 局部特征提取。参数量较少,速度快。权重共享。没有丰富长距离依赖信息。也没有时序。
transofmer: 全局特征提取,有丰富的长距离依赖,上下文信息。高质量的数据量大的情况,会效果很好。但参数量更大,这速度较慢。在CV领域,容易过拟合。
⑧为什么用LN而不是BN?在NLP任务中,句子长度不一致,通常需要对较短的句子进行填充,短句较多的时候BN填充太多0会降低长句的有效特征,LN对单个样本的所有特征进行归一化不会产生这种情况。而且BN会破坏位置编码信息。
⑨cnn与transofmer提取特征的不同之处在那儿?
CNN,局部特征提取。局全感知,权重共享。参数量较少。速度快。没有丰富长距离依赖信息。
Transofmer,全局特征提取。有丰富的长距离依赖,上下文信息。高质量的数据量大的情况,会效果很好。参数量更大,速度较慢。在CV领域,容易过拟合