AI大模型入门到实战系列(四)深入理解 Transformer 大语言模型

深入理解 Transformer 大语言模型

在机器学习中,自回归模型 特指一类使用自身早期预测结果来进行后续预测的模型。例如,文本生成式大语言模型在预测下一个词元时,会依赖之前已生成的词元序列,因此它们也被归为自回归模型。这一名称通常用于将其与BERT等非自回归的文本表示模型区分开来。

这类模型在一次前向传播中,除了其循环生成的特性,还包含两个关键内部组件:分词器语言建模头

  1. 分词器 :其核心是一个固定的词表 。分词器首先将输入文本分解为词元序列,并转换为对应的词元ID。在模型内部,每个词元ID通过词元嵌入层被映射为一个向量表示,作为神经网络的实际输入。
  2. 神经网络 :主体由一系列堆叠的Transformer块构成,负责所有复杂的计算和处理。
  3. 语言建模头 :位于Transformer堆栈的末端。它的作用是将神经网络输出的高维向量,转换为一个针对词表中所有词元的概率分数分布,从而预测下一个最可能的词元。

安装前置包

python 复制代码
# %%capture
# !pip install transformers>=4.41.2 accelerate>=0.31.0

加载大语言模型

这里我们还是以前面的phi3模型为例子

python 复制代码
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline

# 加载模型和分词器
tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-3-mini-4k-instruct")

model = AutoModelForCausalLM.from_pretrained(
    "microsoft/Phi-3-mini-4k-instruct",
    device_map="cuda",          # 自动将模型分配到GPU
    torch_dtype=torch.bfloat16  # 选择数据类型为torch.bfloat16 
    #torch_dtype="auto",         # 自动选择数据类型(float16/fp32)
    trust_remote_code=False,    # 不信任远程代码(安全考虑)
)

# 创建文本生成管道
generator = pipeline(
    "text-generation",          # 任务类型:文本生成
    model=model,
    tokenizer=tokenizer,
    return_full_text=False,     # 只返回新生成的文本,不包括输入
    max_new_tokens=50,          # 最大生成长度:50个新token
    do_sample=False,            # 使用贪婪解码而非采样
)

代码解释:

  • AutoTokenizer.from_pretrained():自动加载与预训练模型匹配的分词器
  • AutoModelForCausalLM.from_pretrained():加载因果语言模型(自回归模型)
  • device_map="cuda":将模型加载到GPU显存中
  • torch_dtype="auto":根据GPU能力自动选择精度(如bfloat16)
  • pipeline():Hugging Face提供的高级API,简化推理流程

训练好的Transformer LLM的输入与输出

python 复制代码
prompt = "Write an email apologizing to Sarah for the tragic gardening mishap. Explain how it happened."

output = generator(prompt)

print(output[0]['generated_text'])

代码解释:

  • prompt:用户提供的输入提示文本
  • generator(prompt):使用管道生成文本
  • output[0]['generated_text']:提取生成的结果

输出

Mention the steps you're taking to prevent it in the future.

Dear Sarah,

I hope this message finds you well. I am writing to express my sincerest apologies for the unfortunate incident that occurred

查看模型架构

python 复制代码
print(model)

输出分析:

Phi3ForCausalLM(

(model): Phi3Model(

(embed_tokens): Embedding(32064, 3072, padding_idx=32000) # 嵌入层:词表大小32064,维度3072

(embed_dropout): Dropout(p=0.0, inplace=False) # 嵌入层dropout

(layers): ModuleList(

(0-31): 32 x Phi3DecoderLayer( # 32个解码器层

(self_attn): Phi3Attention( # 自注意力机制

(o_proj): Linear(in_features=3072, out_features=3072, bias=False)

(qkv_proj): Linear(in_features=3072, out_features=9216, bias=False)

(rotary_emb): Phi3RotaryEmbedding() # 旋转位置编码

)

(mlp): Phi3MLP( # 前馈神经网络

(gate_up_proj): Linear(in_features=3072, out_features=16384, bias=False)

(down_proj): Linear(in_features=8192, out_features=3072, bias=False)

(activation_fn): SiLU() # 激活函数

)

(input_layernorm): Phi3RMSNorm() # 层归一化

(resid_attn_dropout): Dropout(p=0.0, inplace=False)

(resid_mlp_dropout): Dropout(p=0.0, inplace=False)

(post_attention_layernorm): Phi3RMSNorm()

)

)

(norm): Phi3RMSNorm() # 最终层归一化

)

(lm_head): Linear(in_features=3072, out_features=32064, bias=False) # 语言模型头

)

从概率分布中选择单个token(采样/解码)

python 复制代码
prompt = "中国的首都是"

# 对输入提示进行分词
input_ids = tokenizer(prompt, return_tensors="pt").input_ids

# 将输入移到GPU
input_ids = input_ids.to("cuda")

# 获取模型在lm_head之前的输出(隐藏状态)
model_output = model.model(input_ids)

# 获取lm_head的输出(logits)
lm_head_output = model.lm_head(model_output[0])

# 选择最后一个位置概率最高的token
token_id = lm_head_output[0,-1].argmax(-1)

# 解码回文本
tokenizer.decode(token_id)

代码详细解释:

  1. 分词过程

    python 复制代码
    # 输入:prompt = "中国的首都是"
    # 分词后:input_ids = [token_id1, token_id2, ...]
  2. 前向传播流程

    复制代码
    输入序列 → 嵌入层 → Transformer层 → 层归一化 → lm_head
  3. 形状分析

    python 复制代码
    # model_output[0].shape = torch.Size([1, 6, 3072])
    # 含义:[批次大小=1, 序列长度=6个token, 隐藏维度=3072]
    
    # lm_head_output.shape = torch.Size([1, 6, 32064])
    # 含义:[批次大小=1, 序列长度=6, 词表大小=32064]
  4. 选择token

    • lm_head_output[0, -1]:取最后一个位置的logits(形状:32064)
    • .argmax(-1):找到概率最高的token索引
    • 结果应为:'北'

通过缓存键值来加速生成

python 复制代码
prompt = "Write an long email apologizing to Sarah for the tragic gardening mishap. Explain how it happened."

# 对输入提示进行分词
input_ids = tokenizer(prompt, return_tensors="pt").input_ids
input_ids = input_ids.to("cuda")

使用KV缓存的生成速度测试

python 复制代码
%%timeit -n 1
# 生成文本(启用KV缓存)
generation_output = model.generate(
  input_ids=input_ids,
  max_new_tokens=100,      # 生成100个新token
  use_cache=True           # 启用KV缓存(默认)
)
# 执行时间:约6.66秒

不使用KV缓存的生成速度测试

python 复制代码
%%timeit -n 1
# 生成文本(禁用KV缓存)
generation_output = model.generate(
  input_ids=input_ids,
  max_new_tokens=100,
  use_cache=False          # 禁用KV缓存
)
# 执行时间:约21.9秒(慢3倍多!)

KV缓存机制详细解释

为什么需要KV缓存?

在自回归生成中(一次生成一个token),每次生成新token时,模型需要重新处理整个历史序列。如果没有缓存:

  1. 第一次生成:处理 N 个token
  2. 第二次生成:重新处理 N+1 个token
  3. 第三次生成:重新处理 N+2 个token
  4. ... 如此类推

时间复杂度:O(n²),其中n是总序列长度

KV缓存如何工作?

  1. 自注意力机制的关键值(KV)

    • 每个注意力头计算:Q(查询)、K(键)、V(值)
    • K和V只依赖于输入的嵌入和位置编码,与要生成的内容无关
  2. 缓存机制

    python 复制代码
    # 第一次生成(处理整个提示)
    输入: "法国的首都是"
    计算: K1, V1, K2, V2, K3, V3, K4, V4, K5, V5, K6, V6
    生成: "巴黎"
    
    # 第二次生成(只计算新token)
    输入: "巴黎"
    使用缓存: K1-V6(已经计算过)
    只计算: K7, V7(新token的键值)
    生成: "。"
  3. 内存与计算权衡

    • 内存开销:需要存储所有历史token的K和V
    • 计算节省:避免重复计算历史token的注意力

技术实现细节

python 复制代码
# 在transformers库中的实现
class Phi3Attention(nn.Module):
    def forward(self, hidden_states, past_key_values=None, use_cache=False):
        # 如果使用缓存且past_key_values不为None
        if use_cache and past_key_values is not None:
            # 只计算当前新token的Q
            # 从缓存中获取历史的K、V
            # 拼接当前K、V到缓存中
            pass
        
        # 返回当前的注意力输出和更新后的缓存
        return attention_output, new_key_values

性能对比分析

方法 时间 速度提升 内存使用
无缓存 21.9秒 1x 较低
有缓存 6.66秒 3.3x 较高

实际应用建议

  1. 长文本生成必须使用缓存:对于对话、文章生成等场景
  2. 短文本可不使用:对于单次分类、情感分析等
  3. 注意内存限制:缓存会占用显存,需平衡序列长度和批次大小
  4. 滑动窗口注意力:现代模型(如Phi-3)可能使用滑动窗口注意力,只缓存最近的部分token

Transformer 的本质探讨

参数共享范围

不共用,但有层次化的共享关系

共享的层次结构

1. 单个注意力头内:位置间共享

python 复制代码
# 假设一个注意力头
W_Q = [d_model × d_k]  # 所有位置的词共享这个Q投影矩阵
W_K = [d_model × d_k]  # 所有位置的词共享这个K投影矩阵  
W_V = [d_model × d_v]  # 所有位置的词共享这个V投影矩阵

# 对于输入序列X[n × d_model],每个位置i:
Q_i = X[i] @ W_Q  # 所有位置用同一个W_Q
K_i = X[i] @ W_K  # 所有位置用同一个W_K
V_i = X[i] @ W_V  # 所有位置用同一个W_V

在同一个注意力头内,所有位置(所有词)共用同一组W_Q、W_K、W_V矩阵。

2. 不同注意力头间:不共享

python 复制代码
# 假设8个注意力头(h=8)
W_Q_1, W_K_1, W_V_1  # 头1的投影矩阵
W_Q_2, W_K_2, W_V_2  # 头2的投影矩阵
...
W_Q_8, W_K_8, W_V_8  # 头8的投影矩阵

每个注意力头有自己独立的W_Q、W_K、W_V矩阵。

3. 不同网络层间:不共享

python 复制代码
# 假设Transformer有6层
Layer1: W_Q_1, W_K_1, W_V_1  # 第1层的所有头的矩阵
Layer2: W_Q_2, W_K_2, W_V_2  # 第2层的所有头的矩阵
...
Layer6: W_Q_6, W_K_6, W_V_6  # 第6层的所有头的矩阵

每一层Transformer都有自己的全套投影矩阵。

共享范围 是否共享 比喻 目的
同一头内,不同位置 ✅ 共享 同一部门的所有员工用相同工作模板 参数效率、泛化
同一层内,不同头 ❌ 不共享 不同部门有不同专业视角 多角度理解
不同层之间 ❌ 不共享 不同管理层级有不同关注点 层次化抽象

整个模型不共用一套QKV矩阵,而是按照"层→头→位置"的层次结构进行合理分配,平衡了表达能力和参数效率!

每个词都会生成自己的Q、K、V

假设句子是:"我 爱 吃 苹果"(4个token)

python 复制代码
输入嵌入向量:
X = [x1, x2, x3, x4]  # 每个x_i都是d_model维向量
      我  爱  吃  苹果

# 通过线性变换得到Q、K、V
Q = X @ W_Q  # [4, d_k]  每个词都有自己的Q向量
K = X @ W_K  # [4, d_k]  每个词都有自己的K向量  
V = X @ W_V  # [4, d_v]  每个词都有自己的V向量

# 结果:
Q = [q1, q2, q3, q4]  # q1是"我"的查询向量,q2是"爱"的查询向量...
K = [k1, k2, k3, k4]  # 每个词的键向量
V = [v1, v2, v3, v4]  # 每个词的值向量

以"吃"这个词(第3个位置)为例:

计算"吃"的新表示时:

用q3(吃的Q)去匹配所有词的K

匹配度1 = q3·k1 # 吃与我相关吗?

匹配度2 = q3·k2 # 吃与爱相关吗?

匹配度3 = q3·k3 # 吃与吃自身相关吗?(自注意力)

匹配度4 = q3·k4 # 吃与苹果相关吗?这个得分会很高!

将匹配度softmax得到权重

权重 = [0.05, 0.02, 0.08, 0.85] # 假设值

加权求和所有词的V

新_吃 = 0.05v1 + 0.02 v2 + 0.08v3 + 0.85 v4

"吃"现在包含了"苹果"的信息,理解了"吃苹果"这个搭配

并行计算

在实际实现中,不是循环处理每个词,而是用矩阵一次算完:

python 复制代码
# 1. 计算注意力分数矩阵
scores = Q @ K.T  # [4,4]矩阵,每个元素是Q_i和K_j的点积
# scores[i,j] = 词i查询与词j键的匹配度

# 2. 计算注意力权重
attn_weights = softmax(scores / sqrt(d_k))  # [4,4]
# 每一行是一个词对所有词的注意力权重

# 3. 计算输出
output = attn_weights @ V  # [4, d_v]
# output[i] = 词i的新表示,是所有词V的加权和

输出

output = [

新表示_我, # 基于[我,爱,吃,苹果]的信息

新表示_爱, # 基于[我,爱,吃,苹果]的信息

新表示_吃, # 基于[我,爱,吃,苹果]的信息

新表示_苹果 # 基于[我,爱,吃,苹果]的信息

]

三个容易混淆的概念

词嵌入矩阵(Embedding Matrix)、QKV投影矩阵(Projection Matrices)和QKV计算结果(QKV Vectors)这三个概念非常容易混淆,需要正确理解这三者的关系

词嵌入矩阵(Embedding Matrix)

python 复制代码
W_embed = [vocab_size, d_model]
# 例如:[30000, 512]   # 这是与词表大小相关的唯一矩阵
# 可训练参数:30000 × 512 = 15,360,000

QKV投影矩阵(Projection Matrices)

python 复制代码
# 单个注意力头:
W_Q = [d_model, d_k]  # [512, 64]
W_K = [d_model, d_k]  # [512, 64]
W_V = [d_model, d_v]  # [512, 64]
W_O = [d_v, d_model]  # [64, 512]  # 输出投影

# 总参数量:4 × 512 × 64 = 131,072(单个头)

模型隐藏维度d_model就是词嵌入向量的大小,即d_model = 嵌入维度.

d_model(模型隐藏维度):决定了输入的维度和模型的容量

num_heads(注意力头数):决定了多头注意力的并行度

设计选择d_k/d_v:通常等于d_model/num_heads

QKV计算结果(QKV Vectors)

python 复制代码
# 输入序列长度=4,批量大小=1
Q = [batch=1, seq_len=4, d_k=64]  # [1, 4, 64]
K = [batch=1, seq_len=4, d_k=64]  # [1, 4, 64]
V = [batch=1, seq_len=4, d_v=64]  # [1, 4, 64]

# 注意:这个形状与词表大小(30000)完全无关!
相关推荐
爱笑的眼睛112 小时前
从零构建与深度优化:PyTorch训练循环的工程化实践
java·人工智能·python·ai
c#上位机2 小时前
halcon刚性变换(平移+旋转)——vector_angle_to_rigid
人工智能·计算机视觉·c#·上位机·halcon·机器视觉
liliangcsdn2 小时前
如何使用pytorch模拟Pearson loss训练模型
人工智能·pytorch·python
做cv的小昊2 小时前
VLM相关论文阅读:【LoRA】Low-rank Adaptation of Large Language Models
论文阅读·人工智能·深度学习·计算机视觉·语言模型·自然语言处理·transformer
VertGrow AI销冠2 小时前
AI获客软件VertGrow AI销冠的自动化功能测评
人工智能
TextIn智能文档云平台2 小时前
抽取出的JSON结构混乱,如何设计后处理规则来标准化输出?
人工智能·json
百罹鸟2 小时前
在langchain Next 项目中使用 shadcn/ui 的记录
前端·css·人工智能
python_1362 小时前
2026年AI论文修改降重工具推荐喵喵降
人工智能