基于DeepSeek、ChatGPT等生成式大语言模型的工作机制,本文以支持文本生成任务为目标,讨论如何构建一可运行的的大语言模型最小系统原型。使用PyTorch开发框架提供的深度学习基础服务,采用逐行Python源码解析的方式介绍以下内容:
- 如何设计一个适用于对话生成任务的文本训练数据集,包括编码器输入、解码器输入、解码器输出数据。
- 生成式大语言模型的系统架构:编码器、解码器、前馈神经网络、残差连接和层归一化等核心组件。
- 对于一输入的用户提示语,生成式大语言模型的工作流程是什么。如何进行词嵌入编码、如何增加一位置编码、如何计算注意力权重。
- 自注意力机制(Self-Attention)如何捕捉输入的文本序列中的依赖关系。
- 基于一深度学习模型,如何基于设计的数据集进行模型训练。
- 基于已训练的模型,对于一用户提示语,如何使用贪心解码(Greedy Decoding)算法,自回归地生成目标文本序列。
本文参考引用以下资料:
- DeepSeek R1 deepseek-ai/DeepSeek-R1
- Harvard NLP http://nlp.seas.harvard.edu/2018/04/03/attention.html
- 终于把 Transformer 算法搞懂了!!
- wenjtop/transformer: Transformer
一、设计训练数据
考虑一个支持对话生成任务的大语言模型数据训练场景,设计一个数据集,该数据集包括:编码器输入、解码器输入、解码器输出。 以源码中的训练数据集/train_data的第一条对话为例:
- 上一句:”你好 P P P”
- 当前输入:”S 你好! 今天 天气 真 不错”
- 期望回复:”你好! 今天 天气 真 不错 E”
对应的编码:
- enc_inputs[0] = [1, 0, 0, 0, 0]
- dec_inputs[0] = [1, 3, 4, 5, 6, 7, 0, 0, 0]
- dec_outputs[0] = [3, 4, 5, 6, 7, 2, 0, 0, 0]
在 Transformer 训练中,解码器输入(Decoder Input) 和 解码器输出(Decoder Output) 的设计是为了实现自回归(Autoregressive)训练,即给定编码器输入和解码器输入的前n个词,模型应该能预测解码器输出的第n+1个词,而不是一次性生成整个序列。两者的区别是:
- 解码器输入的内容是在训练时,解码器的输入是目标序列的前缀,通常以特殊标记 S(Start)开头,后面跟着真实的目标序列(但不包括最后一个词)。其作用是作为解码器的初始输入,用于预测下一个词,模拟⾃回归⽣成过程。
- 解码器输出的内容是解码器应该预测的目标序列,通常以真实的目标序列(去掉开头的 S)加上结束标记 E(End)结尾。其作用是作为训练时的标签(ground truth),用于计算损失,监督模型预测正确的序列。
举例训练时的对应关系如下:
- 编码器输入(上一句话):[1, 0, 0, 0, 0] ← “你好 P P P P”
- 解码器输入(目标序列的前缀):[1, 3, 4, 5, 6, 7, 0, 0, 0] ← “S 你好! 今天 天气 真 不错 P P P”
- 解码器输出(目标序列):[3, 4, 5, 6, 7, 2, 0, 0, 0] ← “你好! 今天 天气 真 不错 E P P P”
⾃回归⽣成过程如下:
- 输入 [1] (S) → 预测 3 (你好!)
- 输入 [1, 3] (S 你好!) → 预测 4 (今天)
- 以此类推,直到预测到结束标记 E。
源码:train_data.py
import torch
import torch.utils.data as Data
# 设计一个适用于对话生成任务的训练场景:
# 表格 [Encoder_input, Decoder_input, Decoder_output]
# 对话生成场景:[上一句, 当前对话输入, 期望回复]
# Decoder_input:训练时的"提示",包含开始标记和正确的前文
# Decoder_output:训练时的"答案",是模型需要预测的目标序列
# S: 开始符号 E: 结束符号 P: 占位符号,如果当前句子不足固定长度用P占位
train_data = [
['你好 P P P P', 'S 你好! 今天 天气 真 不错', '你好! 今天 天气 真 不错 E'],
['今天 天气 怎么样 P P', 'S 今天 阳光 明媚 , 很 适合 出门', '今天 阳光 明媚 , 很 适合 出门 E'],
['你 喜欢 什么 运动 P', 'S 我 喜欢 打篮球 和 游泳', '我 喜欢 打篮球 和 游泳 E'],
['你 会 做饭 吗 P', 'S 我 会 做 一些 简单的 菜', '我 会 做 一些 简单的 菜 E'],
['最近 在 看 什么 书', 'S 我 在 看 《三体》 , 非常 精彩', '我 在 看 《三体》 , 非常 精彩 E'],
['推荐 一部 电影 P P', 'S 《肖申克的救赎》 很 经典', '《肖申克的救赎》 很 经典 E'],
['怎么 学习 编程 P P', 'S 可以 从 Python 开始 , 多 写 代码', '可以 从 Python 开始 , 多 写 代码 E'], # 长度为 9
['周末 有 什么 计划 P', 'S 我 打算 去 爬山 , 放松 一下', '我 打算 去 爬山 , 放松 一下 E']
]
# Encoder 输入的最大长度. 长度补齐都为 5
# src_len = len(train_data[0][0].split(" "))
src_len = 5
# Decoder 输入输出最大长度 . 长度补齐都为最大输出长度。
# tgt_len = len(train_data[0][1].split(" "))
tgt_len = 9 # 最大输出长度
# Encoder 中文词汇表(对话场景专用) {中文词语:索引值}
# src_vocab = {'P': 0, '我': 1, '是': 2, '学': 3, '生': 4, '喜': 5, '欢': 6, '习': 7, '男': 8}
src_vocab = {
'P': 0, '你好': 1, '今天': 2, '天气': 3, '怎么样': 4, '喜欢': 5,
'什么': 6, '运动': 7, '会': 8, '做饭': 9, '最近': 10, '看': 11,
'书': 12, '推荐': 13, '一部': 14, '电影': 15, '怎么': 16, '学习': 17,
'编程': 18, '周末': 19, '有': 20, '计划': 21, '!': 22, '阳光': 23,
'明媚': 24, '很': 25, '适合': 26, '出门': 27, '我': 28, '打篮球': 29,
'和': 30, '游泳': 31, '做': 32, '一些': 33, '简单的': 34, '菜': 35,
'在': 36, '《三体》': 37, ',': 38, '非常': 39, '精彩': 40, '《肖申克的救赎》': 41,
'经典': 42, '可以': 43, '从': 44, 'Python': 45, '开始': 46, '多': 47,
'写': 48, '代码': 49, '打算': 50, '去': 51, '爬山': 52, '放松': 53,
'一下': 54, '你': 55 , '吗': 56
}
src_idx2word = {src_vocab[key]: key for key in src_vocab}
# 反向字典:通过遍历 src_vocab 的键(key),将原字典的 值(索引) 和 键(字) 互换,生成 {索引: 字} 的新字典。
src_vocab_size = len(src_vocab) # 字典字的个数
# Decoder 目标词汇表(生成回复) {中文词语:索引值}
tgt_vocab = {
'P': 0, 'S': 1, 'E': 2, '你好!': 3, '今天': 4, '天气': 5, '真': 6,
'不错': 7, '阳光': 8, '明媚': 9, ',': 10, '很': 11, '适合': 12,
'出门': 13, '我': 14, '喜欢': 15, '打篮球': 16, '和': 17, '游泳': 18,
'会': 19, '做': 20, '一些': 21, '简单的': 22, '菜': 23, '在': 24,
'看': 25, '《三体》': 26, '非常': 27, '精彩': 28, '《肖申克的救赎》': 29,
'经典': 30, '可以': 31, '从': 32, 'Python': 33, '开始': 34, '写': 35,
'代码': 36, '打算': 37, '去': 38, '爬山': 39, '放松': 40, '一下': 41,
'多': 42, '怎么样': 43, '你': 44, '运动': 45, '一部': 46, # 你喜欢 '': 42,
'做饭': 47, '最近': 48, '什么': 49, '推荐': 50, '电影': 51, # 最近在
'怎么': 52, '编程': 53, '周末': 54, '计划': 55 # 怎么学习
}
# 把目标字典转换成 索引:字的形式
idx2word = {tgt_vocab[key]: key for key in tgt_vocab}
# 目标字典尺寸
tgt_vocab_size = len(tgt_vocab)
# 生成训练数据集包括:编码器输入、解码器输入、解码器输出。
def gen_train_data():
enc_inputs, dec_inputs, dec_outputs = [], [], []
for i in range(len(train_data)):
# 处理编码器输入(上一句话)
enc_tokens = train_data[i][0].split()
enc_input = [src_vocab[token] for token in enc_tokens]
# 用P填充到固定长度 src_len
enc_input = enc_input + [0] * (src_len - len(enc_input))
# 处理解码器输入(当前对话输入,带S)
dec_input_tokens = train_data[i][1].split()
dec_input = [tgt_vocab[token] for token in dec_input_tokens]
# 用P填充到固定长度 tgt_len
dec_input = dec_input + [0] * (tgt_len - len(dec_input))
# 处理解码器输出(期望回复,带E)
dec_output_tokens = train_data[i][2].split()
dec_output = [tgt_vocab[token] for token in dec_output_tokens]
# 用P填充到固定长度 tgt_len
dec_output = dec_output + [0] * (tgt_len - len(dec_output))
enc_inputs.append(enc_input) # 输出是一二维数组
dec_inputs.append(dec_input)
dec_outputs.append(dec_output)
print(f'enc_inputs={enc_inputs}')
print(f'dec_inputs={dec_inputs}')
print(f'dec_outputs={dec_outputs}')
return torch.LongTensor(enc_inputs), torch.LongTensor(dec_inputs), torch.LongTensor(dec_outputs)
# 自定义数据集函数
class TrainDataSet(Data.Dataset):
def __init__(self, enc_inputs, dec_inputs, dec_outputs):
super(TrainDataSet, self).__init__()
self.enc_inputs = enc_inputs
self.dec_inputs = dec_inputs
self.dec_outputs = dec_outputs
def __len__(self):
return self.enc_inputs.shape[0]
def __getitem__(self, idx):
return self.enc_inputs[idx], self.dec_inputs[idx], self.dec_outputs[idx]
以下章节以该训练数据为例子,谈基于Transformer架构的大语言模型的系统架构和工作流程,及如何支持对话生成任务的。
二、大语言模型的组成
基于Transformer架构的大语言模型由两个主要部分组成
- 编码器(Encoder)
- 解码器(Decoder)
1、编码器
编码器负责将输入序列转换成一系列的上下文相关的表示。它由 N 个相同的编码器层堆叠而成。
a. 编码器中的单个层
EncoderLayer类
是 大语言模型中的编码器中的单个层的技术实现,参考该类的构造函数,每个编码器层包含两个子层:
- 多头自注意力机制(Multi-Head Attention):负责计算输入序列中每个词与其他词之间的关联程度。
- 前馈/信息向前单向传递神经网络(Feed-Forward Neural Network – FFNN):对自注意力机制的输出进行非线性变换。Feed-Forward(前馈) 是神经网络中的一种基本结构,其核心特点是:信息向前单向传递(从输入层→隐藏层→输出层),没有循环或反馈连接。
每个子层都使用了残差连接和层归一化。例如在函数 PoswiseFeedForwardNet() 中,通过调用Pytorch中的nn.LayerNorm模块功能来支持残差连接和层归一化。
# EncoderLayer 是 Transformer 编码器中的单个层,包含多头自注意力机制和前馈神经网络两个主要组件。
class EncoderLayer(nn.Module):
def __init__(self):
super(EncoderLayer, self).__init__()
# 多头注意力机制
self.enc_self_attn = MultiHeadAttention()
# 前馈神经网络
self.pos_ffn = PoswiseFeedForwardNet()
# 输入:
# enc_inputs: [batch_size, src_len, d_model] 经过词嵌入和位置编码的输入序列
# enc_self_attn_mask: [batch_size, src_len, src_len] 注意力掩码,用于屏蔽填充位置
# 输出:
# enc_outputs: [batch_size, src_len, d_model] 经过编码器层处理后的特征表示
# attn`: [batch_size, n_heads, src_len, src_len] 注意力权重矩阵,可用于可视化分析
def forward(self, enc_inputs, enc_self_attn_mask):
# 输入3个enc_inputs分别与W_q、W_k、W_v相乘得到Q、K、V
# enc_outputs: [batch_size, src_len, d_model],
# attn: [batch_size, n_heads, src_len, src_len]
enc_outputs, attn = self.enc_self_attn(enc_inputs, enc_inputs, enc_inputs,
enc_self_attn_mask)
# enc_outputs: [batch_size, src_len, d_model]
enc_outputs = self.pos_ffn(enc_outputs)
return enc_outputs, attn
b. 编码器
Encoder类
是 大语言模型中的编码器的技术实现,根据该类的构造函数,包含多个/n_layers=6个编码层。编码器负责将输入序列转换为富含上下文信息的特征表示。主要功能:
- 特征提取:将输入序列转换为高层次的特征表示
- 上下文编码:通过自注意力机制捕获序列内部的依赖关系
- 为解码器提供信息:输出的特征表示将作为解码器的键值对
假设输入:enc_inputs
: [[1, 2, 3, 4, 0]]
(batch_size=1, src_len=5) 处理过程:
- 词嵌入 →
[1, 5, 512]
- 位置编码 →
[1, 5, 512]
- 经过6层EncoderLayer →
[1, 5, 512]
- 输出富含上下文信息的特征表示
class Encoder(nn.Module):
def __init__(self):
super(Encoder, self).__init__()
# 把词/字转换字向量
self.src_emb = nn.Embedding(src_vocab_size, d_model)
# 加入位置信息
self.pos_emb = PositionalEncoding(d_model)
self.layers = nn.ModuleList([EncoderLayer() for _ in range(n_layers)])
# 输入 enc_inputs: [batch_size, src_len] 原始输入序列的词汇索引张量, 例如:`[[1, 2, 3, 4, 0], [1, 5, 6, 3, 7]]`
# 输出 enc_outputs: [batch_size, src_len, d_model] 经过多层编码器处理后的最终特征表示。每个位置都包含了整个序列的上下文信息。
def forward(self, enc_inputs):
# enc_outputs: [batch_size, src_len, d_model]
enc_outputs = self.src_emb(enc_inputs)
# enc_outputs: [batch_size, src_len, d_model]
enc_outputs = self.pos_emb(enc_outputs)
# enc_self_attn_mask: [batch_size, src_len, src_len]
enc_self_attn_mask = get_attn_pad_mask(enc_inputs, enc_inputs)
enc_self_attns = []
for layer in self.layers:
# enc_outputs : [batch_size, src_len, d_model]
# enc_self_attn : [batch_size, n_heads, src_len, src_len]
enc_outputs, enc_self_attn = layer(enc_outputs, enc_self_attn_mask)
enc_self_attns.append(enc_self_attn)
return enc_outputs, enc_self_attns
c. 输入序列的注意力掩码
get_attn_pad_mask
函数用于生成一个注意力掩码(attention mask),主要作用是屏蔽输入序列中的填充部分(padding tokens),防止模型在计算注意力时关注到这些无意义的填充位置。屏蔽掉句子中没有实际意义的占位符,例如’今天 天气 怎么样 P P’ ,填充符号P对对应句子没有实际意义,所以需要被屏蔽,Encoder_input 和Decoder_input占位符都需要被屏蔽。
seq_q
: 查询序列(Query),形状为[batch_size, len_q]
seq_k
: 键序列(Key),形状为[batch_size, len_k]
# 生成一个注意力掩码(attention mask),主要作用是屏蔽输入序列中的填充部分(padding tokens),
# 确保了模型只关注实际有意义的词元,而忽略填充部分,提高了训练的效率和准确性。
# 输入:
# seq_q: 查询序列(Query),torch.Tensor 形状为 [batch_size, len_q] PyTorch Tensor
# seq_k: 键序列(Key),torch.Tensor 形状为 [batch_size, len_k]
# 输出:
# 输出形状:[batch_size, len_q, len_k]
"""
假设一个批次中有以下序列(P=0):
seq_k = [[1, 2, 0, 0],
[3, 0, 0, 0]]
生成的掩码为(1表示需要屏蔽的位置):
pad_attn_mask = [
[[False, False, True, True]], # 第一个样本
[[False, True, True, True]] # 第二个样本
]
扩展后(假设 len_q=3):
[
[[False, False, True, True], # 第一个样本,每个查询位置都看到相同的键掩码
[False, False, True, True],
[False, False, True, True]],
[[False, True, True, True], # 第二个样本
[False, True, True, True],
[False, True, True, True]]
]
"""
def get_attn_pad_mask(seq_q, seq_k):
# batch_size 表示批次大小。如果有32个句子一起处理,batch_size = 32。
# len_q 表示查询序列长度 表示每个样本的序列长度(token数量)例如:如果每个句子有50个词, len_q = 50
batch_size, len_q = seq_q.size()
batch_size, len_k = seq_k.size()
# 找出 seq_k 中所有等于0(即填充符号P)的位置,返回一个布尔张量(True表示该位置是填充符)。
# seq_k.data.eq(0) 的形状是 [batch_size, len_k](二维)
# .unsqueeze(1) 作用是在维度1(即第二个维度)上增加一个维度, 结果形状变为 [batch_size, 1, len_k](三维)
pad_attn_mask = seq_k.data.eq(0).unsqueeze(1)
# 扩展成多维度
# 使用 .expand() 将掩码从 [batch_size, 1, len_k] 扩展为 [batch_size, len_q, len_k]。
# 这样每个查询位置都会复制相同的键掩码,形成最终的注意力掩码矩阵。
return pad_attn_mask.expand(batch_size, len_q, len_k)
2、解码器
解码器负责将编码器输出的上下文表示,结合已生成的输出序列,逐步生成新的输出序列。
a. 解码器中的单个层
DecoderLayer类
是 大语言模型中的解码器中的单个层的技术实现。参考该类的构造函数,每个解码器层包含三个子层:
- 掩码多头自注意力机制(Masked Multi-Head Attention):与编码器中的自注意力类似,但加入了掩码机制,确保在生成当前词时只能关注到已经生成的词,避免“作弊”。
- 编码器-解码器注意力(Encoder-Decoder Attention):它使得解码器在生成输出时能够关注编码器输出的上下文信息。
- 前馈/信息向前单向传递神经网络(Feed-Forward Neural Network):与编码器中的前馈网络类似。
同样,每个子层都使用了残差连接和层归一化。例如在函数 PoswiseFeedForwardNet() 中,通过调用Pytorch中的nn.LayerNorm模块功能来支持残差连接和层归一化。
# 解码器中的单个层,包含三个核心组件:掩码自注意力机制、编码器-解码器注意力机制和前馈神经网络。
"""
示例
编码器输入:['怎么', '学习', '编程', 'P', 'P'] → [16, 17, 18, 0, 0]
解码器输入:['S', '可以', '从', 'Python', '开始', ',', '多', '写', '代码'] → [1, 31, 32, 33, 34, 10, 42, 35, 36]
期望输出:['可以', '从', 'Python', '开始', ',', '多', '写', '代码', 'E'] → [31, 32, 33, 34, 10, 42, 35, 36, 2]
dec_inputs: [1, 9, 512] (解码器输入嵌入)
enc_outputs: [1, 5, 512] (编码器输出)
dec_self_attn_mask: [1, 9, 9] (防止看到未来信息的掩码) 掩码确保在预测位置i时只能看到位置1到i-1
dec_enc_attn_mask: [1, 9, 5] (编码器填充位置掩码)
注意力模式示例:
位置: 1(S) 2(可以) 3(从) 4(Python) 5(开始) 6(,) 7(多) 8(写) 9(代码)
1(S) 1 0 0 0 0 0 0 0 0
2(可以) 0.3 0.7 0 0 0 0 0 0 0
3(从) 0.2 0.4 0.4 0 0 0 0 0 0
4(Python)0.1 0.3 0.3 0.3 0 0 0 0 0
...
"""
class DecoderLayer(nn.Module):
def __init__(self):
super(DecoderLayer, self).__init__()
# 掩码多头自注意力机制
self.dec_self_attn = MultiHeadAttention()
# 编码器-解码器多头注意力机制
self.dec_enc_attn = MultiHeadAttention()
# 位置前馈神经网络
self.pos_ffn = PoswiseFeedForwardNet()
# dec_inputs: [batch_size, tgt_len, d_model]
# enc_outputs: [batch_size, src_len, d_model]
# dec_self_attn_mask: [batch_size, tgt_len, tgt_len]
# dec_enc_attn_mask: [batch_size, tgt_len, src_len]
def forward(self, dec_inputs, enc_outputs, dec_self_attn_mask,
dec_enc_attn_mask):
# 1. 掩码自注意力计算
# dec_outputs: [batch_size, tgt_len, d_model]
# dec_self_attn: [batch_size, n_heads, tgt_len, tgt_len]
dec_outputs, dec_self_attn = self.dec_self_attn(dec_inputs, dec_inputs,
dec_inputs,
dec_self_attn_mask)
# 2. 编码器-解码器注意力计算
# dec_outputs: [batch_size, tgt_len, d_model]
# dec_enc_attn: [batch_size, h_heads, tgt_len, src_len]
dec_outputs, dec_enc_attn = self.dec_enc_attn(dec_outputs, enc_outputs,
enc_outputs,
dec_enc_attn_mask)
# 3. 前馈神经网络处理
# dec_outputs: [batch_size, tgt_len, d_model]
dec_outputs = self.pos_ffn(dec_outputs)
return dec_outputs, dec_self_attn, dec_enc_attn
b. 解码器
Decoder类
是 大语言模型中的解码器的技术实现,根据该类的构造函数,包含多个/n_layers=6个解码层。模型中的解码器部分,主要用于序列生成任务。
# 模型中的解码器部分,主要用于序列生成任务。
class Decoder(nn.Module):
def __init__(self):
super(Decoder, self).__init__()
# 目标语言词嵌入
# 将目标序列(待生成的序列)的 token IDs 转换为 `d_model` 维的向量表示。
self.tgt_emb = nn.Embedding(tgt_vocab_size, d_model)
# 位置编码
self.pos_emb = PositionalEncoding(d_model)
# 堆叠多个DecoderLayer由 `n_layers` 个 `DecoderLayer` 组成的堆叠结构(典型值:6层)。
self.layers = nn.ModuleList([DecoderLayer() for _ in range(n_layers)])
# 输入:
# dec_inputs: 目标序列的 token IDs,形状 `[batch_size, tgt_len]`。
# enc_intpus: 源序列的 token IDs(用于计算交叉注意力掩码),形状 `[batch_size, src_len]`。
# enc_outputs: 编码器的输出,形状 `[batch_size, src_len, d_model]`。
# 输出:
# dec_outputs: 经过解码器层处理后的特征表示 [batch_size, tgt_len, d_model]
# dec_self_attn: 自注意力权重矩阵 [batch_size, n_heads, tgt_len, tgt_len]
# dec_enc_attn: 编码器-解码器注意力权重矩阵[batch_size, n_heads, tgt_len, src_len]
"""
举例:
问题(编码器输入):"怎么学习编程" → ['怎么', '学习', '编程', 'P', 'P']
回答(解码器输入):"S 可以 从 Python 开始 , 多 写 代码" → ['S', '可以', '从', 'Python', '开始', ',', '多', '写', '代码']
"""
def forward(self, dec_inputs, enc_inputs, enc_outputs):
# [batch_size, tgt_len, d_model]
dec_outputs = self.tgt_emb(dec_inputs)
# [batch_size, tgt_len, d_model]
dec_outputs = self.pos_emb(dec_outputs).cuda()
# [batch_size, tgt_len, tgt_len]
dec_self_attn_pad_mask = get_attn_pad_mask(dec_inputs, dec_inputs).cuda()
# [batch_size, tgt_len, tgt_len]
dec_self_attn_subsequence_mask = get_attn_subsequence_mask(dec_inputs).cuda()
# [batch_size, tgt_len, tgt_len]
dec_self_attn_mask = torch.gt((dec_self_attn_pad_mask +
dec_self_attn_subsequence_mask), 0).cuda()
# [batc_size, tgt_len, src_len]
dec_enc_attn_mask = get_attn_pad_mask(dec_inputs, enc_inputs)
dec_self_attns, dec_enc_attns = [], []
for layer in self.layers:
# dec_outputs: [batch_size, tgt_len, d_model]
# dec_self_attn: [batch_size, n_heads, tgt_len, tgt_len]
# dec_enc_attn: [batch_size, h_heads, tgt_len, src_len]
dec_outputs, dec_self_attn, dec_enc_attn = layer(dec_outputs, enc_outputs, dec_self_attn_mask,
dec_enc_attn_mask)
dec_self_attns.append(dec_self_attn)
dec_enc_attns.append(dec_enc_attn)
return dec_outputs, dec_self_attns, dec_enc_attns
c. 上三角掩码矩阵
创建一個上三角掩码矩阵,用于防止解码器在预测第i个词时看到第i+1及之后的词。
# 创建一個上三角掩码矩阵,用于防止解码器在预测第i个词时看到第i+1及之后的词。
# 输入:seq: [batch_size, tgt_len] 类型: torch.Tensor 内容: 解码器的输入序列,包含词汇索引值
# 输出:类型: torch.ByteTensor(8位整数张量), 形状: [batch_size, tgt_len, tgt_len] , 内容: 上三角掩码矩阵,用于屏蔽未来信息
"""
输入序列示例
seq = torch.tensor([
[1, 2, 3, 4], # 第一个样本:["S", "可以", "从", "Python"]
[1, 5, 6, 0] # 第二个样本:["S", "我", "打算", "P"]
])
第一个样本的掩码 [4×4]
[[0, 1, 1, 1],
[0, 0, 1, 1],
[0, 0, 0, 1],
[0, 0, 0, 0]]
第二个样本的掩码 [4×4]
[[0, 1, 1, 1],
[0, 0, 1, 1],
[0, 0, 0, 1],
[0, 0, 0, 0]]
"""
def get_attn_subsequence_mask(seq):
attn_shape = [seq.size(0), seq.size(1), seq.size(1)]
# 生成上三角矩阵,[batch_size, tgt_len, tgt_len]
subsequence_mask = np.triu(np.ones(attn_shape), k=1)
# [batch_size, tgt_len, tgt_len]
subsequence_mask = torch.from_numpy(subsequence_mask).byte()
return subsequence_mask
3、大语言模型
Transformer
类是完整的基于Transformer架构的大语言模型,参考类构造函数,集成了编码器、解码器和输出投影层,实现了序列到序列的转换功能。这种设计使 Transformer 成为强大的序列转换模型,在机器翻译、文本生成等任务中表现出色。
以训练数据中的对话为例:
- 问题(编码器输入):”怎么学习编程” →
['怎么', '学习', '编程', 'P', 'P']
- 回答(解码器输入):”S 可以 从 Python 开始 , 多 写 代码” →
['S', '可以', '从', 'Python', '开始', ',', '多', '写', '代码']
输入:
enc_inputs
: tensor([[16, 17, 18, 0, 0]]) [‘怎么’, ‘学习’, ‘编程’, ‘P’, ‘P’]dec_inputs
: tensor([[1, 31, 32, 33, 34, 10, 42, 35, 36]]) [‘S’, ‘可以’, ‘从’, ‘Python’, ‘开始’, ‘,’, ‘多’, ‘写’, ‘代码’]- dec_inputs: tensor([[1, 31, 32, 33, 34, 10, 42, 35, 36]]) [‘S’, ‘可以’, ‘从’, ‘Python’, ‘开始’, ‘,’, ‘多’, ‘写’, ‘代码’]
主输出: 词汇概率分布,形状: [batch_size * tgt_len, tgt_vocab_size] = [9, 56]。内容: 每个位置在56个词汇上的概率分布。
源码:main.py
# `Transformer` 类是完整的 Transformer 模型,集成了编码器、解码器和输出投影层,实现了序列到序列的转换功能。
class Transformer(nn.Module):
def __init__(self):
super(Transformer, self).__init__()
self.Encoder = Encoder().cuda()
self.Decoder = Decoder().cuda()
self.projection = nn.Linear(d_model, tgt_vocab_size, bias=False).cuda()
# 输入
# enc_inputs: [batch_size, src_len] 源序列的词汇索引(如中文句子)
# dec_inputs: [batch_size, tgt_len] 目标序列的词汇索引(如英文句子,训练时包含起始符)
# 输出
# 主输出: [batch_size * tgt_len, tgt_vocab_size] 每个位置的目标词汇概率分布,用于计算交叉熵损失。
# 注意力权重(用于分析和可视化):
# enc_self_attns: List of [batch_size, n_heads, src_len, src_len] 编码器各层的自注意力模式
# dec_self_attns: List of [batch_size, n_heads, tgt_len, tgt_len] 解码器各层的自注意力模式
# dec_enc_attns: List of [batch_size, n_heads, tgt_len, src_len] 解码器对编码器的注意力模式(源-目标对齐)
def forward(self, enc_inputs, dec_inputs):
# enc_outputs: [batch_size, src_len, d_model]
# # enc_self_attns: [n_layers, batch_size, n_heads, src_len, src_len]
enc_outputs, enc_self_attns = self.Encoder(enc_inputs)
# dec_outpus : [batch_size, tgt_len, d_model]
# dec_self_attns: [n_layers, batch_size, n_heads, tgt_len, tgt_len]
# dec_enc_attn : [n_layers, batch_size, tgt_len, src_len]
dec_outputs, dec_self_attns, dec_enc_attns = self.Decoder(
dec_inputs, enc_inputs, enc_outputs)
# dec_logits: [batch_size, tgt_len, tgt_vocab_size]
dec_logits = self.projection(dec_outputs)
return dec_logits.view(-1, dec_logits.size(-1)), enc_self_attns, dec_self_attns, dec_enc_attns
三、工作流程
假设我们有一个输入序列 (X),其形状为 (n, d),其中 (n) 是序列长度,(d) 是每个词的嵌入维度,基于Transformer架构的大语言模型的整体工作流程为:
A、输入表示:词嵌入编码和位置编码。
B、编码器工作流程 (6层),每层编码器执行:
- 多头自注意力:计算输入序列内部的关系
- 残差连接 + 层归一化
- 前馈神经网络:非线性变换
- 残差连接 + 层归一化
C、解码器工作流程 (6层),每层编码器执行:
- 掩码多头自注意力:只能看到当前位置及之前的信息
- 编码器-解码器注意力:关注编码器输出的相关信息
- 前馈神经网络:非线性变换
其中自注意力计算流程为:
- 生成查询、键和值:通过线性变换将输入 (X) 转换为查询(Query)、键(Key)和值(Value):其中,(W_Q)、(W_K) 和 (W_V) 是学习的权重矩阵。
- 计算注意力权重:计算查询与键的点积,以获取注意力分数,并进行缩放:其中 (d_k) 是键向量的维度,用于防止点积值过大。
- 应用 Softmax 函数:对注意力分数应用Softmax函数,以获取注意力权重。Softmax函数输入为任意实数向量(如 [2.3, -1.5, 0.7]),输出为归一化后的概率向量,向量中的概率和为1(如 [0.72, 0.15, 0.13])。
- 加权求和:使用注意力权重对值进行加权求和,生成最终的自注意力输出。
1. 输入文本的词嵌入
大语言模型处理的是序列数据,最初输入是离散的词(Token)。中文词向量是通过训练将离散的词语/Token映射到高维空间中的稠密向量(比如512维或768维,chatGPT达到了12,500维,维度越高能表达的信息量越多)。每个向量隐含了词语的 语义 和 语法 特征,例如:
- 语义:
"猫"
和"狗"
的向量方向接近(同属动物)。 - 语法:
"跑步"
和"游泳"
的向量方向接近(同属动词)。
词向量相似度:是将词语语义映射到数学空间的量化表达,核心在于通过向量计算捕捉词语间的语义关联。实际应用中需结合具体任务选择:模型、优化分词,并理解其局限性:如多义词、领域偏差等。例如Word2Vec 是 Google 于 2013 年提出的浅层神经网络词嵌入模型,旨在将文本中的单词映射为稠密向量(词向量),通过向量空间距离捕捉语义和语法关系。
词向量的意义
- 语义相似性:向量空间中距离相近的词具有相似含义(如
国王 - 男
≈女王 - 女
)。 - 上下文关联:同一上下文中出现的词向量方向相近(如
咖啡 - 因特网
≈茶 - 微信
)。 - 数学运算:向量可进行加减运算推断关系(如
巴黎 - 法国
+北京 - 中国
≈东京 - 日本
)。
以下代码为具体实现方法。nn.Embedding
是 PyTorch 中用于处理离散型数据(如单词、字符、类别等)的核心模块,它将离散的整数索引映射为连续的稠密向量(即词嵌入/word embeddings)。该函数将输入的单词/字符整数序列(如 [5, 3, 9, …])转换为连续的向量序列(形状为 (batch_size, seq_len, d_model))。
import torch.nn as nn
class Encoder(nn.Module):
def __init__(self):
super(Encoder, self).__init__()
# 把词/字转换字向量。src_vocab_size = len(src_vocab) 为输入文本中的字典中词的个数。
# d_model = 512 配置值: 词嵌入的维度
self.src_emb = nn.Embedding(src_vocab_size, d_model)
# ...
2. 位置编码
RNN(循环神经网络,Recurrent Neural Network)是AI领域中一种专门处理序列数据的神经网络结构。它的核心特点是具有循环连接,使网络能够保留之前步骤的“记忆”,从而对动态时间行为建模。这种结构适合处理时间序列、自然语言、语音等具有时序关系的数据。但是,由于基于Transformer架构的大语言模型完全依赖自注意力机制,没有像 RNN 那样的循环结构来感知顺序。因此需要通过位置编码来显式地注入位置信息,使模型能感知序列中单词的顺序。需解决了自注意力机制缺乏位置感知的问题:
- 首先,我们需要把输入的文字进行词嵌入,每一个词用一个向量表示,称为词向量,一个句子就可以用一个矩阵表示。
- 然后把词向量加上位置信息得到编码器的输入 矩阵。其实位置信息矩阵/Positional Encoding是固定公式计算出来的,值不会改变,每次有数据来了直接加上位置信息矩阵就行。也就是使用正弦和余弦函数为输入序列添加位置信息。
位置信息编码 / PE – Positional Encoding的计算公式如下:
- 第2i维度为偶数,
PE(pos,2i) = sin(pos/10000^(2i/d_model))
- 第2i+1维度为奇数,PE(pos,2i+1) = cos(pos/10000^(2i / d_model))
其中:
- pos:表示在一句话中词的位置。
- i:词向量的第i个维度。
- d-model: 表示词向量一共有多少维。代码中取512。
源码:
class PositionalEncoding(nn.Module):
def __init__(self, d_model, dropout=0.1, max_len=5000):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)
pos_table = np.array([
[pos / np.power(10000, 2 * i / d_model) for i in range(d_model)]
if pos != 0 else np.zeros(d_model) for pos in range(max_len)])
# 字嵌入维度为偶数时
pos_table[1:, 0::2] = np.sin(pos_table[1:, 0::2])
# 字嵌入维度为奇数时
pos_table[1:, 1::2] = np.cos(pos_table[1:, 1::2])
# enc_inputs: [seq_len, d_model]
self.pos_table = torch.FloatTensor(pos_table).cuda()
# enc_inputs: [batch_size, seq_len, d_model]
def forward(self, enc_inputs):
enc_inputs += self.pos_table[:enc_inputs.size(1), :]
return self.dropout(enc_inputs.cuda())
示例:词嵌入编码的维度 d_model = 4 的情况。对于位置 pos=1
的单词,其4维位置编码计算如下:
维度 i | 公式类型 | 具体计算 |
---|---|---|
i=0 (第0维) | PE(1,0) = sin(1/10000^(0/4)) | sin(1/10000^0) = sin(1) |
i=1 (第1维) | PE(1,1) = cos(1/10000^(0/4)) | cos(1/10000^0) = cos(1) |
i=2 (第2维) | PE(1,2) = sin(1/10000^(2/4)) | sin(1/10000^0.5) |
i=3 (第3维) | PE(1,3) = cos(1/10000^(2/4)) | cos(1/10000^0.5) |
为什么这样设计?考虑:PE(pos,2i) = sin(pos/10000 ^ (2i/d_model))
- 频率递减
- 对于 f(x) = sin( a * x) , 随着维度索引
i
增大,因 i在分母中,a 变小,2*pi 周期变化的 sin( a * x) 频率逐渐降低。 i
越小 → 频率越高 → 捕捉细粒度位置变化。感知相邻单词的精确位置。i
越大 → 频率越低 → 捕捉粗粒度位置变化。感知句子级别的相对位置。
- 相对位置编码
关键数学性质:对于固定偏移量 k
,PE(pos+k)
可以表示为 PE(pos)
的线性函数
PE(pos+k, 2i) = a × PE(pos, 2i) + b × PE(pos, 2i+1)
PE(pos+k, 2i+1) = c × PE(pos, 2i) + d × PE(pos, 2i+1)
这使得模型能够学习到相对位置信息。
- 奇偶维度交替
- 偶数维度 (2i):正弦函数
- 奇数维度 (2i+1):余弦函数
- 这样设计确保了每个位置都有唯一的编码
可以可视化理解为:想象一个 [max_len, d_model]
的矩阵: max_len为句子最大长度
- 行方向 (pos):不同单词的位置
- 列方向 (i):同一位置的不同频率分量
这样的编码方式为每个位置创建了一个独一无二的”指纹”,让大语言模型能够理解单词在序列中的顺序关系。
3. 自注意力机制
自注意力机制是 大语言模型 的核心。它允许模型在处理序列中的每个词时,能够 “关注” 到序列中的其他所有词,并计算它们对当前词的重要性,从而捕捉序列中的全局依赖关系。这种机制特别适用于处理序列数据,比如自然语言处理中的文本,因为它能够捕捉到序列内部的长距离依赖关系和上下文信息。
注意力机制就是用来帮助模型判断:每个词和其他词之间的关联有多强。具体来说,它会计算出每一个词的“注意力权重”,告诉模型:这个词在理解另一个词的时候有多重要。它的核心思想是让序列中的每个元素都有机会“看到”序列中的其他元素,从而捕捉到它们之间的相互关系。模型可以根据其他元素的表示来调整其自身的表示。它是一种“自身”的注意力机制,即它只关注序列内部的元素。在这种机制下,序列中的每个元素都会对序列中的其他所有元素进行“关注”,从而形成一个内部的、自关联的表示。
举例:在 大语言模型 中,输入是一句话或一段文本,比如:“我喜欢吃苹果”。这句话会被拆分成一个个词(token),每个词都会被转换成一个向量(也就是一组数字),表示它的含义。接下来,大语言模型 就要分析这些词之间有什么关系。例如,“我”和“吃”之间有动作关系,“吃”和“苹果”之间也有动作与对象的关系。具体来说,给定一个输入序列 ,自注意力机制的目标是通过计算每个单词与其他单词的相关性来加权更新每个单词的表示(向量值)。
a、线性变换
对于输入序列中的每个词,自注意力机制会生成三个向量。这些向量通过线性变换从输入向量 X 得到:
- Q = X * Wq
- K = X * WK
- V = X * WV
其中,Wq 、Wk、Wv为可学习的权重矩阵。场景举例:“小明昨天去了学校,他今天也去了学校。”。
查询(Query,Q):代表当前的“疑问”或“关注点”。它表达了模型当前需要什么信息。Query 可能是当前要回答的问题或解码器当前步需要生成的词。类比:我想知道“他”是谁?
键(Key, K):表示输入信息的“标识”或“索引”。它描述了输入序列中每个元素(词、像素、特征等)的“身份”或“内容概要”,用于判断该元素与当前 Query 的相关性。 Key 可能是句子中每个词的向量表示(Embedding)。类比:“小明”能回答这个问题。
值(Value, V):表示输入信息的“实际内容”。它包含了每个输入元素真正要提供的信息。在文本大语言模型中,通常 Value 和 Key 是相同的向量(Self-Attention 最常见),也可以是不同的向量(例如经过转换)。类比:“小明昨天去了学校”。Value(值):也就是真正想获取的信息。V其实通过公式计算出来,注意力机制最终目标就是为了获取到这个V,如果没有后续的前馈网络计算,这个V就是最终输出的结果。
b、计算注意力分数
通过查询 Q 和键 K 的点积来衡量它们之间的相似度。
Attention Score (Q, K, V) = (Q . K)T / sqrt (dk)
计算注意力分数核心的目的是做Q矩阵与K转置矩阵做点积相乘,从几何空间可以理解为,求两个矩阵向量间的距离,距离越近,说明关联度越高。以上计算过程,就是注意力机制,这个注意力的含义就是查找词语之间的关系,最终输出一个注意力得分,最高得分就是要输出的词汇。
c、计算注意力权重
查询 Q 和键 K 的点积后通常会进行缩放(Scaling),以防止点积结果过大导致 softmax 函数进入梯度饱和区。对注意力分数应用 Softmax 函数,将其转换为概率分布,得到注意力权重。这些权重表示了序列中每个词对当前词的重要性。具体的计算公式为:
Attention Weight (Q, K, V) = softmax( Attention Score ) = softmax( (Q . K)T / sqrt (dk) )
d、加权求和
最后,将注意力权重与值向量 V 进行加权求和,得到每个输入元素的输出表示:
Output (Q, K, V) = Attention Weight (Q, K, V) * V = softmax( (Q . K)T / sqrt (dk) ) * V
e、源码实现
计算注意力信息、残差和归一化源码实现:
这个注意力机制被用于:
- 编码器自注意力:处理输入序列内部关系
- 解码器自注意力:处理目标序列内部关系(带掩码)
- 编码器-解码器注意力:连接源语言和目标语言
这是大语言模型模型能够捕获长距离依赖关系的核心技术。
源码:tranformer.py
# 大语言模型中核心的缩放点积注意力机制实现
"""
输入:
参数 形状 描述
Q [batch_size, n_heads, len_q, d_k] 查询矩阵
K [batch_size, n_heads, len_k, d_k] 键矩阵
V [batch_size, n_heads, len_v, d_v] 值矩阵
attn_mask [batch_size, n_heads, seq_len, seq_len] 注意力掩码
注意: len_v = len_k(键和值的序列长度相同)
输出:
context [batch_size, n_heads, len_q, d_v] 注意力加权后的上下文向量
attn [batch_size, n_heads, len_q, len_k] 注意力权重矩阵(可用于可视化)
实际示例
假设:batch_size=2, n_heads=8, len_q=5, d_k=64, d_v=64
计算过程:
Q: [2, 8, 5, 64], K: [2, 8, 5, 64]
scores: [2, 8, 5, 5] (每个头每个查询位置对每个键位置的分数)
attn: [2, 8, 5, 5] (softmax后的注意力权重)
context: [2, 8, 5, 64] (加权求和后的输出)
"""
class ScaledDotProductAttention(nn.Module):
def __init__(self):
super(ScaledDotProductAttention, self).__init__()
# Q: [batch_size, n_heads, len_q, d_k]
# K: [batch_size, n_heads, len_k, d_k]
# V: [batch_size, n_heads, len_v(=len_k), d_v]
# attn_mask: [batch_size, n_heads, seq_len, seq_len]
def forward(self, Q, K, V, attn_mask):
# scores : [batch_size, n_heads, len_q, len_k]
scores = torch.matmul(Q, K.transpose(-1, -2)) / np.sqrt(d_k)
# 如果时停用词P就等于 0
scores.masked_fill_(attn_mask, -1e9)
attn = nn.Softmax(dim=-1)(scores)
# [batch_size, n_heads, len_q, d_v]
context = torch.matmul(attn, V)
return context, attn
4. 多头自注意力机制
为了捕捉不同的特征和上下文信息,让模型能够从不同的子空间中学习到不同的注意力模式,大语言模型 使用了多头自注意力机制。多头注意力为同时运行多个独立的注意力头(如:5个、8个甚至更多),每个头关注不同的特征模式,最终将结果拼接后统一处理。想象一群人(多个头)同时观察一幅画,有人关注颜色,有人关注形状,有人关注构图,最后综合所有视角得出完整理解。现实生活中,我们对一句话的理解往往是多角度的。比如:“我喜欢吃苹果”,包括
- 动作关系(“吃” 和 “我”、“苹果”)
- 情感关系(“喜欢” 表达情感)
- 实体识别(“苹果” 是水果)
多头注意力具体步骤如下:
- 多头分离:将输入分为多个头,每个头都有自己的查询、键和值的线性变换。
- 并行计算:在每个头上独立计算自注意力输出。
- 拼接和线性变换:将所有头的输出拼接在一起,并通过一个线性层进行变换,以生成最终的输出。
自注意力机制通过允许模型动态地关注输入序列的不同部分,显著提升了模型在处理自然语言等序列数据时的能力。这一机制的成功引领了多个基于大语言模型的模型的广泛应用,并推动了深度学习的研究和发展。
源码实现:
"""
这是一个标准的Transformer多头注意力机制的PyTorch实现。
通过调用 ScaledDotProductAttention, 支持多个注意力头同时计算,提高效率。
__init__函数参数说明:
d_model: 输入/输出的维度(通常为512)
n_heads: 注意力头的数量(通常为8)
d_k: 每个注意力头的键/查询维度(通常为64)
d_v: 每个注意力头的值维度(通常为64)
"""
class MultiHeadAttention(nn.Module):
def __init__(self):
super(MultiHeadAttention, self).__init__()
self.W_Q = nn.Linear(d_model, d_k * n_heads, bias=False)
self.W_K = nn.Linear(d_model, d_k * n_heads, bias=False)
self.W_V = nn.Linear(d_model, d_v * n_heads, bias=False)
self.fc = nn.Linear(n_heads * d_v, d_model, bias=False)
# input_Q: [batch_size, len_q, d_model]
# input_K: [batch_size, len_k, d_model]
# input_V: [batch_size, len_v(=len_k), d_model]
# attn_mask: [batch_size, seq_len, seq_len]
def forward(self, input_Q, input_K, input_V, attn_mask):
residual, batch_size = input_Q, input_Q.size(0)
# 线性变换、维度重塑和转置
# Q: [batch_size, n_heads, len_q, d_k]
Q = self.W_Q(input_Q).view(batch_size, -1, n_heads, d_k).transpose(1, 2)
# K: [batch_size, n_heads, len_k, d_k]
K = self.W_K(input_K).view(batch_size, -1, n_heads, d_k).transpose(1, 2)
# V: [batch_size, n_heads, len_v(=len_k), d_v]
V = self.W_V(input_V).view(batch_size, -1, n_heads, d_v).transpose(1, 2)
# attn_mask : [batch_size, n_heads, seq_len, seq_len]
attn_mask = attn_mask.unsqueeze(1).repeat(1, n_heads, 1, 1)
# context: [batch_size, n_heads, len_q, d_v]
# attn: [batch_size, n_heads, len_q, len_k]
context, attn = ScaledDotProductAttention()(Q, K, V, attn_mask)
# context: [batch_size, len_q, n_heads * d_v]
context = context.transpose(1, 2).reshape(batch_size, -1, n_heads * d_v)
# [batch_size, len_q, d_model]
output = self.fc(context)
return nn.LayerNorm(d_model).cuda()(output + residual), attn
5. 前馈神经网络
在每个自注意力层之后,大语言模型 添加了一个简单的、全连接的前馈神经网络(Feed-Forward Neural Network – FFNN)。它对自注意力机制的输出进行非线性变换,增加模型的表达能力。
前馈神经网络有几个显著特点:
- 没有记忆功能:每次输入都是独立的,不会记住之前的数据。
- 逐层处理:每一层都会对数据进行一次非线性变换。
- 可扩展性强:可以通过增加层数或每层的神经元数量来提升模型能力。
举个例子,假设你有一个神经网络用来判断一张图片是不是猫。前馈神经网络会先接收图片的像素数据,然后经过几个隐藏层的处理,最后输出一个“是猫”或“不是猫”的结果。
位置级前馈网络PoswiseFeedForwardNet
是大语言模型中重要的组成部分,它:
- 为每个位置提供独立的非线性变换
- 通过残差连接和层归一化稳定训练
- 扩展和收缩的维度设计增强模型表达能力
- 与注意力机制配合,共同构建强大的序列建模能力
"""
位置级前馈网络(Position-wise Feed Forward Network),每个位置独立应用相同的前馈网络。
网络结构: 输入 → Linear(d_model→d_ff) → ReLU → Linear(d_ff→d_model) → 输出
参数说明:
d_model: 模型维度(512)
d_ff: 前馈网络隐藏层维度(2048)
bias=False: 不使用偏置项(论文中的配置)
输入:形状为 [batch_size, seq_len, d_model] 的张量
输出:和输入的数据结构完全一样。
数学公式: FFN(x)=LayerNorm(x+Linear2(ReLU(Linear1(x))))
计算过程:
输入: [2, 5, 512]
↓
第一层线性变换 Linear1: 512 → 2048 → [2, 5, 2048]
↓
ReLU: [2, 5, 2048]
↓
第二层线性变换 Linear2: 2048 → 512 → [2, 5, 512]
↓
+ residual: [2, 5, 512] + [2, 5, 512] = [2, 5, 512]
↓
层归一化 LayerNorm: [2, 5, 512] 在最后一个维度(d_model)上进行归一化
"""
class PoswiseFeedForwardNet(nn.Module):
def __init__(self):
super(PoswiseFeedForwardNet, self).__init__()
self.fc = nn.Sequential(
# 第一层线性变换
nn.Linear(d_model, d_ff, bias=False),
# 激活函数
nn.ReLU(),
# 第二层线性变换
nn.Linear(d_ff, d_model, bias=False))
# inputs: 形状为 [batch_size, seq_len, d_model] 的张量
def forward(self, inputs):
residual = inputs
output = self.fc(inputs)
# 源码 output + residual: 残差连接(加入原始输入)
# 源码 nn.LayerNorm(d_model): 层归一化,在最后一个维度(d_model)上进行归一化
# 源码 .cuda(): 确保在GPU上计算
# [batch_size, seq_len, d_model]
return nn.LayerNorm(d_model).cuda()(output + residual)
6. 残差连接和层归一化
为缓解深层网络训练时梯度消失和收敛困难,大语言模型 在每个子层后都使用了残差连接和层归一化。
残差连接:
残差连接(Residual Connection)是深度神经网络中的一种关键结构,由何恺明团队在2015年提出(ResNet论文),用于解决深度网络的梯度消失/爆炸和网络退化(Degradation)问题。其核心思想是通过跨层跳跃连接(Skip Connection),让网络能够直接学习输入与输出之间的残差(即变化部分),而非直接拟合复杂映射。
残差连接的直觉:假设要拟合的函数为 H(x),传统网络直接学习 H(x)。残差网络改为学习 残差 F(x)=H(x)−x,即 H(x)=F(x)+x。如果恒等映射(H(x)=x)是最优解,网络只需将 F(x) 推向 0,比直接拟合恒等映射更容易。
残差连接的数学公式为:Output = Layer(X) + X
其中 X 是输入,Layer(X) 是该层的输出。
层归一化:
层归一化(Layer Normalization)对每个样本的特征维度做归一化,用于稳定深度神经网络的训练过程。技术上使用PyTorch 中的torch.nn.LayerNorm 模块实现层归一化(Layer Normalization)。
四、模型的训练和推理
1. 模型实例
model = Transformer().cuda()
criterion = nn.CrossEntropyLoss(ignore_index=0) #忽略 占位符 索引为0.
optimizer = optim.SGD(model.parameters(), lr=1e-3, momentum=0.99)
2. 模型训练
1、数据准备:
- 使用
gen_train_data()
从train_data
中生成编码器和解码器的输入输出张量。 - 使用
DataLoader
将数据封装成批次(batch_size=2),并打乱顺序(shuffle=True)。
2、初始化 Transformer 模型,并将其移动到 GPU(.cuda()
)。
3、损失函数与优化器:
- 使用
CrossEntropyLoss
,并忽略索引为 0 的 token(即填充符P
)。 - 使用 SGD 优化器,学习率为 0.001,动量为 0.99。
4、训练循环:
- 每个 epoch 遍历所有批次。
- 将数据移动到 GPU。
- 前向传播得到输出和注意力权重。
- 计算损失(忽略填充符)。
- 反向传播并更新模型参数。
5、训练完成后将模型保存为 generative_model.pth
。
import torch.nn as nn
import torch.optim as optim
from datasets import *
from transformer import Transformer
if __name__ == "__main__":
enc_inputs, dec_inputs, dec_outputs = gen_train_data()
loader = Data.DataLoader(TrainDataSet(enc_inputs, dec_inputs, dec_outputs), 2, True)
model = Transformer().cuda()
# 忽略 占位符 索引为0.
criterion = nn.CrossEntropyLoss(ignore_index=0)
optimizer = optim.SGD(model.parameters(), lr=1e-3, momentum=0.99)
for epoch in range(50):
# enc_inputs : [batch_size, src_len]
# dec_inputs : [batch_size, tgt_len]
# dec_outputs: [batch_size, tgt_len]
for enc_inputs, dec_inputs, dec_outputs in loader:
enc_inputs, dec_inputs, dec_outputs = enc_inputs.cuda(), dec_inputs.cuda(), dec_outputs.cuda()
# outputs: [batch_size * tgt_len, tgt_vocab_size]
outputs, enc_self_attns, dec_self_attns, dec_enc_attns = model(enc_inputs, dec_inputs)
loss = criterion(outputs, dec_outputs.view(-1))
print('Epoch:', '%04d' % (epoch + 1), 'loss =', '{:.6f}'.format(loss))
optimizer.zero_grad()
loss.backward()
optimizer.step()
torch.save(model, 'generative_model.pth')
print("保存模型")
训练过程:python main.py
Epoch: 0001 loss = 2.400406
Epoch: 0001 loss = 2.555903
Epoch: 0002 loss = 2.067433
Epoch: 0002 loss = 1.902348
..........................................
Epoch: 0049 loss = 0.003035
Epoch: 0050 loss = 0.002156
Epoch: 0050 loss = 0.001873
保存模型 generative_model.pth
输出PyTorch模型文件:generative_model.pth
3. 模型推理
这是一个贪心解码(Greedy Decoding)函数,用于自回归地生成目标序列。核心功能为根据输入的对话上文(encoder输入),自动生成回复(decoder输出)。工作方式为:逐个生成token,每次选择概率最高的词作为下一个词。
源码:infer.py
def infer(model, enc_input, start_symbol):
enc_outputs, enc_self_attns = model.Encoder(enc_input)
dec_input = torch.zeros(1, tgt_len).type_as(enc_input.data)
next_symbol = start_symbol
for i in range(0, tgt_len):
dec_input[0][i] = next_symbol
dec_outputs, _, _ = model.Decoder(dec_input, enc_input, enc_outputs)
# 映射到词汇表
projected = model.projection(dec_outputs)
# 取最大概率词
prob = projected.squeeze(0).max(dim=-1, keepdim=False)[1]
next_word = prob.data[i]
next_symbol = next_word.item()
return dec_input
# 加载数据集
enc_inputs, dec_inputs, dec_outputs = gen_train_data()
loader = Data.DataLoader(TrainDataSet(enc_inputs, dec_inputs, dec_outputs), 2, True)
enc_inputs, _, _ = next(iter(loader))
# 加载模型
model = torch.load('generative_model.pth')
# 执行 infer() 函数。从 字符 S 开始预测。使用贪心解码生成完整的解码器输入序列 Decoder_input。
predict_dec_input = infer(model, enc_inputs[0].view(1, -1).cuda(), start_symbol=tgt_vocab["S"])
# 重新进行完整的前向传播。这里使用完整生成的序列重新进行一次性前向传播,获得更准确的概率分布。
# 输入1:enc_inputs → [1, src_len] (编码器输入)
# 输入2:predict_dec_input → [1, tgt_len] (生成的解码器输入)
# 输出:predict → [1 * tgt_len, tgt_vocab_size]
predict, _, _, _ = model(enc_inputs[0].view(1, -1).cuda(), predict_dec_input)
# predict: [9, 56] (tgt_len=9个位置,每个位置56个词汇的概率)
# max(1, keepdim=True): 在维度1(词汇表维度)取最大值
# 返回:(max_values, max_indices)
# [1]: 取最大值的索引
# 结果:predict → [9, 1] (每个位置预测的词汇索引)
predict = predict.data.max(1, keepdim=True)[1]
# 转换为可读文本
# predict.squeeze() 移除张量中所有维度为1的维度,实现张量的"压缩"。
print([src_idx2word[int(i)] for i in enc_inputs[0]], '->',
[idx2word[n.item()] for n in predict.squeeze()])
生成过程示例:输入编码器:[‘怎么’, ‘学习’, ‘编程’, ‘P’, ‘P’]
步骤 | dec_input | 预测的下一个词 | 动作 |
---|---|---|---|
1 | [S, 0, 0, 0, 0, 0, 0, 0, 0] | ‘可以’ | 填入’可以’ |
2 | [S, 可以, 0, 0, 0, 0, 0, 0, 0] | ‘从’ | 填入’从’ |
3 | [S, 可以, 从, 0, 0, 0, 0, 0, 0] | ‘Python’ | 填入’Python’ |
… | … | … | … |
最终 | [S, 可以, 从, Python, 开始, ,, 多, 写, 代码] | – | 完成 |
执行 python infer.py 后,输出:
['怎么', '学习', '编程', 'P', 'P'] -> ['可以', '从', 'Python', '开始', ',', '多', '写', '代码', 'E']
五、优点和应用场景
1、优点
基于Transformer架构的大语言模型优点:
- 并行性:完全基于注意力机制,允许并行计算,大大加快了训练速度。
- 长距离依赖:自注意力机制能够直接建模序列中任意两个词之间的关系,无需像 RNN 那样通过长距离的传播来捕捉依赖,因此在处理长序列时效果更好。
- 可解释性:注意力权重可以直观地展示模型在生成某个词时关注了输入序列的哪些部分,提供了一定的可解释性。
- 可扩展性强:容易扩展到大规模模型,比如后来的 GPT、BERT 等都是基于 Transformer 架构。
- 通用性高:不仅限于 NLP,还拓展到图像处理、语音识别等领域。
2、应用场景
自注意力机制在多个领域都有广泛的应用:
- 自然语言处理:在BERT、GPT等预训练语言模型中,自注意力机制使得模型能够捕捉句子中的长距离依赖关系,极大地提高了语言理解的能力。
- 图像处理:在图像分类和目标检测任务中,自注意力机制可以帮助模型关注图像中的关键部分,提高识别的准确性。
- 时间序列分析:在金融市场预测、气象数据分析等领域,自注意力机制能够捕捉时间序列中的长期模式,提供更准确的预测。
自注意力机制通过允许模型动态地关注输入序列的不同部分,显著提升了模型在处理自然语言等序列数据时的能力。这一机制的成功引领了多个基于Transformer的模型的广泛应用,并推动了深度学习的研究和发展。