深度学习的数学原理(三十八)—— Transformer 完整训练代码实战

衔接前序 :第 36-37 篇分别以代码实战实现了编码器和解码器的前向传播,并通过手动验证确认了每步计算的正确性。但前两篇使用的都是随机初始化的模型 ,输出没有语义意义。本文是"代码实战三部曲"的终章,将训练一个完整的 Transformer 模型(中英翻译),并通过逐阶段的检查点(checkpoint)分析,观察参数如何从随机走向有序。

建议运行配套 notebook 38_transformer_training.ipynb 边看边读,效果最佳。


一、概述

本文的核心问题是:随机初始化的一组参数,经过梯度下降训练后,内部发生了什么变化?

为了回答这个问题,我们训练一个小型 Transformer(仅 13 万参数),并在训练过程中设置 8 个检查点(0、50、100、200、500、1000、2000、3000 步),在每个检查点保存:

  • Embedding 矩阵(用于 t-SNE 可视化和余弦相似度分析)
  • 编码器自注意力权重(观察注意力模式如何聚焦)
  • QKV 和 FFN 的 Frobenius 范数(观察参数增长轨迹)
  • 训练/验证损失(确认模型确实在学习)

通过纵向对比这些检查点,我们可以清晰地看到"学习"的本质------从随机的混沌状态,逐渐涌现出有结构、有规律的表示

模型配置

与第 36-37 篇完全一致:

参数
d_model 32
注意力头数 h 4
每头维度 d_k 8
FFN 隐藏层 d_ff 128
编码器/解码器层数 N 3
总参数量 133,302
优化器 Adam (lr=1e-3)
损失函数 CrossEntropy (ignore pad)
Batch size 32
训练数据 800 句对
训练步数 3000 步(120 个 epoch)

导入依赖和模型初始化:

python 复制代码
import torch
import torch.nn as nn
import numpy as np
from sklearn.manifold import TSNE
from data_utils import build_vocab, load_parallel_data, encode_line, decode_line, collate_batch
from transformer_modules import Transformer

model = Transformer(
    src_vocab_size=zh_spec['vocab_size'],
    tgt_vocab_size=en_spec['vocab_size'],
    d_model=32, h=4, d_ff=128, N=3
).to(device)

二、参数规模分析

在开始训练之前,我们先审视模型的参数构成。总参数量为 133,302,分布如下:

组件 参数量 占比 说明
Encoder Embedding 41,856 31.4% 1308 字 × 32 维
Decoder Embedding 1,728 1.3% 54 字符 × 32 维
Encoder 层 (×3) 37,056 27.8% 每层 4×1024(Q/K/V/O) + 2×4096(FFN) + 4×32(LN)
Decoder 层 (×3) 49,920 37.4% 每层多一组交叉注意力 4×1024
Decoder Output 1,782 1.3% 54 × 32 + 54 bias
总计 133,302 100%

几个关键观察:

  1. Embedding 占了约 1/3 的参数。中文词表 1308 字,每个字 32 维,这 41,856 个参数是训练中需要"学习"的核心语义空间。
  2. 解码器层比编码器层多 35% 的参数(49,920 vs 37,056)。原因很直观------解码器每层多了一组交叉注意力的 Q/K/V/O(四个 32×32 矩阵)。
  3. 总参数量 13 万,这是一个能在 CPU 上秒级完成训练的微型模型,但麻雀虽小五脏俱全------包含完整 Transformer 的所有组件。
python 复制代码
# 逐层参数统计
for name, param in model.named_parameters():
    print(f'  {name}: {param.numel()} params')

# 输出示例:
#   encoder.embedding.weight: 41856 params
#   encoder.layers.0.self_attn.W_Q.weight: 1024 params
#   ...
#   decoder.layers.0.cross_attn.W_Q.weight: 1024 params  # 编码器没有这组
#   ...

三、训练过程与损失变化

3.1 训练循环

训练采用 Teacher Forcing 模式------每一步向解码器输入完整的目标序列(已右移),而不是上一步的预测。损失函数是带 padding mask 的交叉熵:

python 复制代码
def masked_cross_entropy(logits, target, ignore_idx=0):
    """Cross entropy ignoring padding positions."""
    vocab_size = logits.size(-1)
    logits_flat = logits.contiguous().view(-1, vocab_size)
    target_flat = target.contiguous().view(-1)
    return nn.functional.cross_entropy(logits_flat, target_flat, ignore_index=ignore_idx)

关键细节:

  • decoder_input = tgt_batch[:, :-1]:去掉目标序列的最后一个 token(解码器不需要预测 EOS 之后的 token)
  • targets = tgt_batch[:, 1:]:去掉 SOS,让解码器学习预测下一个 token
  • ignore_index=0:忽略 <pad> 位置(索引 0)的损失

3.2 损失曲线

左图(训练损失)

  • 初始损失 = 4.09,最终损失 = 0.41,下降 90.0%
  • 红色虚线标记检查点位置(steps 50, 100, 200, 500, 1000, 2000, 3000)
  • 训练损失持续下降,说明模型在训练集上不断进步

右图(验证损失)

  • 验证损失从 3.15 起步,在前 500 步(约 20 个 epoch)与训练损失同步下降
  • 500 步之后,验证损失开始持续上升,而训练损失继续下降------这是典型的**过拟合(overfitting)**信号

3.3 过拟合分析

这个结果非常有教育意义。由于我们的训练数据只有 800 句对,而模型有 13 万参数(参数远多于数据量),过拟合是必然发生的。

关键转折点:

阶段 步数范围 训练损失 验证损失 状态
初期学习 0 → 500 4.09 → 2.07 3.15 → 2.80 有效学习,泛化良好
开始过拟合 500 → 1000 2.07 → 1.41 2.80 → 3.70 开始记忆训练数据
严重过拟合 1000 → 2000 1.41 → 0.71 3.70 → 5.68 大量记忆,泛化恶化
极度过拟合 2000 → 3000 0.71 → 0.41 5.68 → 7.25 几乎"背下"训练集

这是一个刻意设计的"小数据 + 小模型"实验------用最小的计算成本,清晰地展示了从"学到知识"到"过度记忆"的完整过程。

3.4 逐 Epoch 进度(部分展示)

复制代码
Epoch   1: train=3.43 val=3.15 (steps:   25)
Epoch   5: train=2.62 val=2.68 (steps:  125)
Epoch  10: train=2.44 val=2.62 (steps:  250)
Epoch  20: train=2.07 val=2.80 (steps:  500)   ← 最佳验证损失
Epoch  30: train=1.70 val=3.17 (steps:  750)
Epoch  40: train=1.41 val=3.70 (steps: 1000)
Epoch  60: train=1.00 val=4.66 (steps: 1500)
Epoch  80: train=0.71 val=5.68 (steps: 2000)
Epoch 100: train=0.58 val=6.41 (steps: 2500)
Epoch 120: train=0.46 val=7.25 (steps: 3000)

尽管训练后期过拟合严重,但参数层面的变化正是我们想要的观察素材------参数从随机到有序的轨迹,即使在过拟合阶段也同样有意义。


四、Embedding 矩阵的演化 ⭐

Embedding 层是整个 Transformer 的入口,它将离散的 token ID 映射到连续的向量空间。初始化的 Embedding 是随机的------所有向量杂乱无章;经过训练后,语义相近的字会在向量空间中彼此靠近。

4.1 t-SNE 可视化

上图用 t-SNE 将 32 维的 embedding 降维到 2 维平面,展示了 4 个关键阶段(step 0、100、1000、3000)的 embedding 分布。

Step 0(随机初始化)

  • 所有字符的向量散落在平面上,没有明显结构
  • 红箭头标注的"我"、"爱"等字与其他字没有区分

Step 100(训练早期)

  • 散点开始出现初步聚集趋势,但结构仍然模糊

Step 1000(过拟合中期)

  • 聚类明显增强,部分语义相关的字已经形成了可分辨的小簇
  • 向量空间的结构变得更加清晰

Step 3000(训练结束)

  • 尽管模型已严重过拟合,但 embedding 空间的结构反而更加清晰------过拟合使模型对训练数据的表示更加精细
  • 语义相关的字(如"学"和"习"、"深"和"度")在空间中形成了更紧凑的簇

这个观察很重要:过拟合虽然对泛化有害,但它确实让模型在训练数据上学到了更精细的表示。Embedding 从完全随机到高度结构化,正是"随机到有序"的最佳可视化证据。

4.2 余弦相似度矩阵

用另一个视角看 embedding 的变化------计算 10 个常见中文字符间的余弦相似度矩阵。对角线的自相似度始终为 1(深红色),而非对角线元素表示两个字符的语义相似度。

但事实上,本次训练得到的结果并不理想,可能是由于词嵌入矩阵太小了,没有办法得到有效更新

具体看几对字符的相似度变化(全部 8 个检查点):

复制代码
Pair       Step 0    Step 50   Step 100  Step 200  Step 500  Step 1000 Step 2000 Step 3000
我-你        0.1987    0.1951    0.1921    0.1948    0.1893    0.1852    0.1805    0.1731   # 正相关,缓慢下降
深-度        0.0399    0.0397    0.0430    0.0513    0.0615    0.0520    0.0492    0.0418   # 微弱正相关,先升后降
学-习       -0.0912   -0.0944   -0.0978   -0.0992   -0.1145   -0.1504   -0.1486   -0.1462   # 负相关且持续增长(区分度加大)
好-不       -0.0602   -0.0582   -0.0561   -0.0549   -0.0456   -0.0280   -0.0273   -0.0226   # 负相关,趋于零(开始区分)

相对于之前 125 步训练时微小的变化(±0.01-0.02),3000 步训练的变化幅度显著增大(最高达 ±0.06):

  • "我-你":从 0.20 降至 0.17。两者都是人称代词,但"我"是第一人称、"你"是第二人称,模型逐渐学会了区分它们(正相关度下降)
  • "学-习":从 -0.09 降至 -0.15。有趣的是这对语义相关的字变成了负相关------因为过拟合的模型开始过度区分它们
  • "好-不":从 -0.06 升至 -0.02,趋近于零。模型开始将这两个字视为无关(而非负相关)

五、注意力模式的演化 ⭐

5.1 注意力热力图对比

上图展示编码器第 0 层 Head 0 和 Head 2 在 4 个训练阶段(step 0、100、1000、3000)的注意力权重。测试句子为"我爱深度学习"。

Step 0(随机初始化)

  • 权重分布相对均匀,没有明显的"聚焦"------每个 token 大致均匀地关注所有其他 token

Step 100(训练早期)

  • 注意力分布开始出现不均匀,某些位置开始有更高的权重

Step 1000-3000(过拟合阶段)

  • 注意力的结构越来越清晰,每一列的颜色分布差异增大
  • Head 0 和 Head 2 的注意力模式出现了明显的分化

5.2 注意力聚焦度(KL 散度)

为了量化注意力的"聚焦"程度,我们计算注意力分布与均匀分布之间的 KL 散度

DKL(P∥U)=∑iP(i)log⁡P(i)U(i)D_{\text{KL}}(P \parallel U) = \sum_i P(i) \log\frac{P(i)}{U(i)}DKL(P∥U)=i∑P(i)logU(i)P(i)

其中 PPP 是注意力分布,UUU 是均匀分布(对于 8 个 token 的序列,U(i)=1/8=0.125U(i) = 1/8 = 0.125U(i)=1/8=0.125)。KL 散度越大,说明注意力分布越不均匀、越"聚焦"。

复制代码
=== Attention Focusedness (KL divergence from uniform) ===
Step    0       100     1000    3000
Head 0  1.7390  1.6958  1.9583  1.9316    # 先降后升,最终聚焦
Head 1  1.8584  1.8865  1.7129  1.8314    # 先升后降再升,波动较大
Head 2  1.9623  1.9133  2.0493  2.0262    # 整体上升,聚焦度增加
Head 3  2.0626  2.0191  2.0072  1.9765    # 持续下降,趋于均匀

这里再次验证了"多头注意力"的功能分化现象:

  • Head 2:聚焦度明显增加(1.96 → 2.03),变得更加"专注"
  • Head 3:聚焦度持续下降(2.06 → 1.98),变得更加"均匀"
  • Head 0 和 Head 1:在训练过程中聚焦度存在波动,体现了注意力头的动态调整

与之前 125 步训练相比,3000 步后的分化更加明显------更大的训练量放大了各头之间的差异


六、参数范数的变化

训练过程中,各层参数的 Frobenius 范数会以不同的速率增长。范数增长的速度可以反映该参数对梯度更新的敏感程度。

上图展示了编码器 3 个层中 6 组参数(WQ、WK、WV、WO、W1、W2)的 Frobenius 范数随训练步数的变化:

编码器第 0 层

  • 所有参数的范数在 500 步后加速增长,正好对应过拟合开始的时间点
  • W1(FFN 第一层)的范数最大(从 6.60 增长到 8.24),因为其维度最高(32×128=4096 个参数)
  • W2(FFN 第二层)的增长幅度最大 (3.25 → 5.20,增长 60%),说明过拟合中 FFN 输出投影的调整最为剧烈

编码器第 1 层和第 2 层

  • 范数的变化模式与第 0 层类似
  • 第 2 层的各参数增长幅度略大于第 0 层

完整范数数据(编码器第 0 层):

复制代码
Param     Step 0    Step 50   Step 100  Step 200  Step 500  Step 1000 Step 2000 Step 3000  增长幅度
_WQ       3.2230    3.2301    3.2425    3.3102    3.5790    3.7996    3.9892    4.0959    +27.1%
_WK       3.2556    3.2619    3.2707    3.3416    3.6097    3.8293    4.0140    4.1242    +26.7%
_WV       3.3168    3.3182    3.3479    3.5199    3.9906    4.1844    4.2404    4.2457    +28.0%
_WO       3.3367    3.3359    3.3659    3.5473    4.0597    4.2581    4.3030    4.2999    +28.9%
_W1       6.6009    6.6433    6.6296    6.6361    7.1726    7.7157    8.1009    8.2443    +24.9%
_W2       3.2507    3.3243    3.3239    3.3554    3.8296    4.4282    4.9532    5.1982    +60.0% ← 增长最快!

核心观察

  1. W2(FFN 输出投影)增长最快(+60%)------这是本次训练与之前 125 步训练最大的不同。在过拟合阶段,FFN 的输出投影被大幅调整,因为模型在"记忆"训练样本的特定模式
  2. WQ 和 WK 增长适中(+27%)------注意力机制始终保持活跃,但增长幅度不如 W2
  3. WO(输出投影)增长略高于 WQ/WK(+29%)------多头输出的合并也在持续调整
  4. W1(FFN 输入投影)增长最小(+25%)------高维空间(128 维)的初始投影相对稳定

一个重要发现 :在训练不足时(125 步),WQ/WK 增长最快;在充分训练后(3000 步),W2 增长最快。这说明不同训练阶段,模型的"学习重点"会转移------初期以调整注意力机制为主,后期以调整 FFN 记忆模式为主。


七、推理验证

7.1 贪心解码

训练完成后,我们使用贪心解码在验证集上进行翻译测试:

复制代码
#     Source                         Prediction                     Reference
-------------------------------------------------------------------------------
1     至于 布朗 所 作 的 预言 , 本来 就 失 之 偏颇   weongsnorodedthisthi   as to brown 's prediction , ...
2     他们 不 相信 我 并 把 我 关进 一 间 囚室 .    theireisissisfonthel   they did not believe me and ...
3     李鹏 首先 对 赫普图拉 访华 表示 欢迎 .        itiliglifebeliofelic   li peng first extended his ...
4     粮食 储备 也 很 充裕 .                 astheinrofpesforpenc   grain reserves are ample too ...
5     东 条 英 机 出生 在 日本 一个 大 军阀 家庭 .   thireverynk"ltlyfowm   hideki tojo was born of a ...

与之前 125 步训练的 "therererererererer" 相比,3000 步训练后的输出具有两方面的变化:

  1. 多样性增加:不同输入产生了不同的输出序列,不再是千篇一律的 "the..."
  2. 但仍非有效翻译:输出虽然看起来像英语单词(包含常见子串如 "the"、"is"、"are"、"every"),但没有语义意义

这说明:过拟合让模型记住了训练数据中的字符片段,但没有学会真正的语义映射。模型的输出只是将训练数据中的高频 n-gram 片段拼接在一起。

7.2 与第 35 篇手工计算对比

第 35 篇文章中,我们手工计算了"我爱深"经过编码器-解码器各层的前向传播,文章中手工推理的翻译结果是 "ilove"。现在让训练后的模型尝试同样输入:

复制代码
=== Comparing with Article 35: "我爱深" ===
Step 1:  input=""      -> Top-5: "i"(0.9559), "t"(0.0428), "e"(0.0007), ...
Step 2:  input="i"     -> Top-5: "t"(0.8731), "n"(0.0553), "s"(0.0241), ...
Step 3:  input="it"    -> Top-5: "i"(0.9999), "e"(0.0001), ...
Step 4:  input="iti"   -> Top-5: "s"(0.2616), "n"(0.2356), "e"(0.2179), ...
...
Full prediction: "itisnevero"

这个结果比之前的 "thesisthes" 更有启发性:

  1. 第 1 步 :模型在 <sos> 之后预测 "i" 的概率高达 95.6%,远高于其他选项。这与文章 35 的预期一致------"我"应该翻译为 "i"(尽管大小写不同)
  2. 后续步骤:模型从 "i" 出发,后续预测出了 "t"、"i"、"s" → "itis" 序列,这是一个英语中常见的词根片段
  3. 最终输出 "itisnevero":虽然不是一个有意义的单词,但包含多个英语常见子串

与文章 35 的预期对比

  • 文章 35 基于手工推理预测 "ilove"("我"→"i","爱"→"love"),这是理想的语义翻译
  • 训练后的模型输出 "itisnevero",提取了训练数据中的高频模式
  • 两者的差距揭示了语义映射需要更多数据------在小数据集上,模型更倾向于学习表面的统计规律

值得注意的是,3000 步训练后第一个 token 的预测从之前 125 步的 "t"(25.4%) 变成了 "i"(95.6%),这说明更多的训练使模型向正确的语义方向迈出了一大步。

7.3 训练后的交叉注意力

最后,我们观察训练后模型在"我爱深度学习"上的交叉注意力(解码器第 2 层,所有 4 个头):

与第 37 篇中随机初始化的交叉注意力相比,训练后的交叉注意力有几个显著变化:

  • 非均匀性显著增加:某些源-目标位置的注意力权重明显高于其他位置
  • 头部高度分化:4 个头的关注模式完全不同------每个头关注源句子的不同区域
  • 对齐选择性增强:目标位置开始有选择性地关注特定的源位置

这些变化在 3000 步训练后比 125 步时更加明显------更长训练使各头的专业化程度更高。


八、总结

通过本文的训练实战(120 epoch / 3000 步),我们完成了一个完整 Transformer 从随机初始化到过拟合的完整生命周期观察。以下是核心发现:

1. 损失曲线展示了完整学习过程

训练损失从 4.09 降至 0.41(降幅 90.0%)。验证损失在 500 步前同步下降,之后持续上升------清晰展示了过拟合的全过程。这验证了模型先学习通用模式、后记忆训练数据的典型行为。

2. Embedding 从随机到高度结构化

t-SNE 可视化展示了从完全随机分布到形成清晰语义聚类的过程。3000 步训练产生的聚类远强于 125 步------更长的训练使向量空间的结构更加锐利

3. 注意力头的功能分化

KL 散度分析揭示了多头的"分工"现象:

  • Head 0 和 Head 2 聚焦度增加
  • Head 1 和 Head 3 呈现不同趋势
  • 不同头朝不同方向演化,与 125 步训练时趋势一致但幅度更大

4. 参数范数的差异化增长揭示了学习阶段的转移

  • 125 步训练时:WQ/WK 增长最快(注意力机制主导)
  • 3000 步训练后:W2 增长 60%(FFN 输出投影主导)
  • 关键洞察:随着训练从"欠拟合"进入"过拟合",模型的学习重点从注意力机制转向了 FFN 记忆

5. 推理验证揭示统计学习的渐进性

  • 125 步:输出 "therererererererer"("the" 是最高频三字母组合)
  • 3000 步:输出 "itisnevero"(首个 token "i" 概率 95.6%,正确识别了"我"的翻译方向)
  • 更多训练使模型逐步向正确的语义方向收敛,但受限于数据量,尚不能产生真正的翻译
相关推荐
Asize10 小时前
重生之我在 Vibe Coding 时代当程序员:第六课,第一个全栈项目
人工智能
初心未改HD10 小时前
LLM应用开发之RAG检索增强生成详解
人工智能
用户6000718191010 小时前
【翻译】给Agent配上解释器
人工智能
明志数科10 小时前
仿真数据与真实数据:机器人训练的数据策略选择
人工智能·算法·机器学习
老司机张师傅10 小时前
AI第一章:虚拟环境库安装
人工智能
深度学习lover10 小时前
<数据集>yolo汉字识别<目标检测>
人工智能·yolo·目标检测·数据集·汉字识别
Master_oid10 小时前
机器学习43:线性回归进阶篇①
人工智能·机器学习·线性回归
香蕉鼠片10 小时前
CNN学习时的代码
人工智能·学习·cnn
AskHarries10 小时前
Google Trends 找蓝海赛道:独立开发者如何挖出没人做、但有人搜的项目
人工智能