python神经网络编程入门(四十三)——从零训练一个小型 Transformer 做文本分类

📍 路标 :本篇位于《从零构建 Transformer》系列 第 14/16 章 · 实战篇

系列主线:注意力思想 → 自注意力 → 多头 → 位置编码 → 编解码架构 → 手撕实现 → 预训练模型 → 实战微调 → 知识收官

进度:▸ 基石篇(1-5) ▸ 架构篇(6-10) ▸ 预训练篇(11-13) ▸ 实战篇(14-16·本篇) ▸ 知识收官
摘要:第 14 章进入实战篇:把前 13 章所有零件组装成一台"能干活"的文本分类器。用 encoder-only 结构(嵌入+位置编码+Encoder 层+CLS 分类头)从零训练,走完"数据准备→模型搭建→训练→评估→预测"完整闭环。全章手写:规则生成的情感数据集(含 10% 标签噪声)、数据管线(分词/padding/mask)、分类器前向、数值梯度训练(loss 0.71→0.41,测试准确率 42.5%→92.5%)、新句预测演示、与瞎猜基线(50%)对比。
上一篇(四十二):GPT:自回归生成式预训练


引言:前 13 章造了一堆零件,这一章把它装成"能干活"的机器

回顾一下我们手里的家当:第 2-9 章造了零件(注意力、多头、位置编码、Encoder、Decoder、前向传播),第 10 章装了操作系统(标签平滑、AdamW、warmup),第 11 章造了字典(BPE 分词),第 12、13 章看懂了两个大模板(BERT、GPT)。但直到现在,我们还没有用一台"自己造的模型"去完成一个真实任务。

这一章补上这一课:从零训练一个小型 Transformer,做文本情感二分类(判断一条影评是积极还是消极)。它是实战篇的第一章,也是整个系列第一次"模型真正干活":

  1. 数据 :没有现成数据集也能开始------先用规则生成 200 条带标签的短句(含情感词),顺便故意掺入 10% 的标签噪声(模拟真实世界的标注错误);
  2. 模型 :第 12 章 BERT 的最小版------"嵌入 + 位置编码 + 1 层 Encoder + CLS 分类头",共 842 个参数,每一个零件都是本系列手撕过的
  3. 训练:第 10 章三件套(平滑 CE + AdamW + warmup)原样登场,用数值梯度(第 12/13 章同款)完成 60 步训练;
  4. 结果 :测试准确率从"瞎猜的 42.5%"升到 92.5% ,新句子也能正确判断------前 13 章的理论,第一次变成能用的成果

读完这一章,你会彻底明白"深度学习项目"的最小闭环长什么样:数据 → 模型 → 训练 → 评估 → 预测,缺一环都不叫"干活"。

本章的地图也提前画好:一、数据准备 (造数据集 + 数据管线)→ 二、模型搭建 (encoder-only 分类器)→ 三、前向与损失 (未训练时的"出厂状态")→ 四、训练 (60 步见证准确率起飞)→ 五、预测 (新句子判对)→ 六、评估与对比(92.5% vs 50% 基线 + 与 RNN 对照)。六节正好对应闭环的五步(训练和评估合并看),读完你会对这五个环节各自"为什么存在"有完整理解。

另外交代一句本章的定位变化:前 13 章我们一直在"验证原理"(形状对不对、数学对不对、机制能不能跑),这一章开始是"验证工程"(模型能不能完成一个真实任务)。所以你会看到两个新东西:①数据集 ------真实任务必须有数据,我们先用规则造一个(原理与真实数据一致);②评估------训练完要看"测试集准确率",而不是只看 loss。这两样东西,是"玩具"和"工具"的分水岭。

🎯 本章目标

  1. 搭出 encoder-only 的文本分类器:嵌入 + 位置编码 + Encoder 层 + CLS 分类头(demo2);
  2. 理解数据管线的完整流程:文本 → 分词 → id → padding + mask → batch(demo1);
  3. 跑通一次完整训练:平滑 CE + 数值梯度 + AdamW + warmup(demo4);
  4. 学会评估:准确率、混淆矩阵,看懂"训练集和测试集分开"的意义(demo4);
  5. 让模型对没见过的句子做预测(demo5);
  6. 与"瞎猜基线"对比,理解"模型到底学没学到东西"(demo6)。

一、数据准备:没有数据,也要造数据

1.1 任务定义

情感二分类:输入一条句子,输出"积极(1)/消极(0)"。它是 NLP 最经典的任务之一,也是 RNN 系列(第 16 章)用过 IMDB 影评做过的任务------本系列在这里与 RNN 系列"接轨"。

为什么从分类任务开始实战?三个理由:①数据好造 (规则生成即可,还能精确控制难度与噪声);②模型简单 (一个 Encoder + 一个分类头,没有生成循环、没有采样);③评估直观 (准确率一算就懂)。分类是"理解类任务"的样板,掌握了它,问答、抽取、文本匹配这些任务都只是"换个标签格式"。而且------分类任务能让你把前 13 章的零件逐个"点验":嵌入(第 5/9 章)、注意力(第 3/4 章)、Encoder(第 7/9 章)、CLS(第 12 章)、训练三件套(第 10 章)、数据管线(第 11 章),一台机器全用上。

1.2 规则生成数据集(并掺入噪声)

真实 IMDB 数据集有 2.5 万条,需要下载;本教学章先用规则生成 的方式造 200 条短句,原理与真实数据完全一致,且不用联网、结果可复现。生成规则:每条句子 = 1~2 个情感词(积极词或消极词)+ 2~4 个填充词随机拼接,标签由情感词决定。然后随机翻转 10% 的标签------这是关键一步:它模拟了现实世界"标注员也会犯错"的噪声,也让训练结果"达不到 100%"、更真实。

python 复制代码
rng = np.random.RandomState(42)
POS = ['good', 'great', 'love', 'nice', 'happy', 'wonderful']
NEG = ['bad', 'terrible', 'hate', 'sad', 'awful', 'boring']
FILL = ['the', 'movie', 'film', 'i', 'it', 'was', 'is', 'very', 'really',
        'this', 'that', 'and', 'but', 'my', 'we', 'they', 'a', 'an']

def make_sentence(label):
    pool = POS if label == 1 else NEG
    n_senti = rng.randint(1, 3)
    senti = list(rng.choice(pool, n_senti, replace=False))
    n_fill = rng.randint(2, 5)
    fills = list(rng.choice(FILL, n_fill, replace=True))
    words = senti + fills
    rng.shuffle(words)
    return ' '.join(words)

data = [(make_sentence(1), 1) for _ in range(100)] + [(make_sentence(0), 0) for _ in range(100)]
rng.shuffle(data)
# 10% 标签噪声(模拟现实标注错误)
noise_idx = rng.choice(len(data), int(0.1 * len(data)), replace=False)
for i in noise_idx:
    s, y = data[i]
    data[i] = (s, 1 - y)
train_data = data[:160]
test_data = data[160:]

真实输出:

复制代码
【demo1】生成情感分类数据集(200 条规则短句)
样本示例:
  label=0  'very boring a an but'
  label=1  'they wonderful is'
  label=0  'but bad movie'
  label=0  'my the i bad hate'
  label=0  'very terrible that'
总数=200,训练集=160,测试集=40(含 10% 标签噪声)
词表大小 V=32,最大长度 S=8
训练集 X shape: (160, 8)  mask shape: (160, 8)  Y shape: (160,)
样例(已 padding 的 id + mask):
  ids : [28, 7, 2, 3, 8, 0, 0, 0]
  mask: [1, 1, 1, 1, 1, 0, 0, 0]  label = 0

注意三个细节:①训练/测试分割是 160/40 ------测试集的 40 条模型从没见过,这是"评估"的诚实前提;②10% 的标签噪声 会让模型最多学到约 90% 准确率(另外 10% 的样本标签本身就是错的,"学对"反而错);③padding + mask 把不同长度的句子统一成 (8,) 并用 mask 标出 pad 位置(第 11 章全流程)。数据管线画成图:

🎤 一句话总结数据准备数据 = 特征(id 序列)+ 标签(0/1)+ 诚实(训练/测试分开)+ 现实(带噪声)------四个要素缺一不可,缺了"诚实",评估就失真;缺了"噪声",结果就太完美、没有教学意义。

1.3 为什么用规则数据,而不是真实 IMDB? 诚实交代:真实 IMDB 数据集(RNN 系列用过,约 2.5 万条影评)需要下载、加载、清洗,本教学章想让你把注意力集中在"训练循环"本身 ,而不是卡在数据工程上。规则数据有三个好处:①不用联网、可复现 (固定随机种子,任何人都能跑出一模一样的结果);②任务难度可控 (情感词决定标签,模型"该学到什么"一目了然);③能精确控制噪声 (10% 标签翻转),方便观察"数据质量对性能的影响"。真实 IMDB 将在第 15 章登场------到那时你会看到,换数据只改"数据加载"这一段,训练循环一行不动。这正是"数据与模型解耦"的工程价值。

1.4 数据管线的三个细节:max_len、unk、批量打包 。demo1 里藏着三个容易忽略的工程点:①max_len=8 截断 ------句子超过 8 个词就砍掉尾巴(真实项目里超长文本也会截断或分段,因为注意力 O(S²) 扛不住太长);②**<unk> 兜底**------词表外的词(id=1)统一映射成"未知",保证编码永不崩溃(第 11 章讲过);③批量打包 ------160 条训练样本一次性转成三个数组(X/M/Y),训练时每次随机抽 16 条做 batch,这就是"数据管线"的最终形态。真实框架(HuggingFace)的 tokenizer(...) 返回的也是"input_ids + attention_mask"这对组合------和我们的 X/M 一一对应

1.5 为什么测试集必须"从没看过":数据泄漏 。demo1 里训练/测试分割(160/40)是最重要的工程纪律之一。如果测试集混进训练集(我第一版代码就犯过:切片写错导致测试集与训练集重叠,测试准确率虚高到 100%),评估就变成"开卷考试"------模型记住的是训练样本本身,测试"准确率"是自欺欺人,上线后立刻崩盘。这个概念叫数据泄漏(data leakage) :任何"测试阶段才该知道的信息"提前进了训练,都会让评估失真。除了"句子不能重叠",还有两个常见的泄漏源:①测试集的统计量 (如词表、max_len 要只在训练集上统计);②测试集的超参数调优 (用测试集反复调参,等于把测试集"训"进去了------正规做法是再分一个验证集)。评估的诚实,比评估的数字更重要


二、模型搭建:encoder-only 分类器

分类器长什么样?第 12 章 BERT 的最小版:

  • 嵌入层:词表 V=34(30 个词 + pad + unk + 特殊)→ d=8 向量;
  • 位置编码:正弦(第 5 章),最多 8 个位置;
  • Encoder 层 ×1:自注意力(双向,无因果掩码)+ 残差 + LN + FFN(第 9 章 encoder_block);
  • CLS 分类头 :取第 0 列输出(第 12 章"CLS 聚合全句")→ cls @ Wc + bc 得 2 类 logits → softmax。

其中 Encoder 的自注意力要加 padding mask(第 11 章):pad 位置的分数变成 -1e9,softmax 后权重为 0------模型不会去"看"那些填充的假词。参数量明细(真实输出):

复制代码
【demo2】模型定义:嵌入 + 位置编码 + 1 层 Encoder + [CLS] 分类头
参数量明细:
  emb                256
  Wq/Wk/Wv/Wo        256
  FFN                280
  LN                  32
  分类头 Wc/bc           18
  合计: 842 个参数

842 个参数------比第 12 章的小 BERT 还小(那边 640 + 输出层 12 类)。对照 BERT-base 的 1.1 亿参数,小 13 万倍,但结构分毫不差:嵌入、位置、Encoder、CLS、分类头,五个零件全是我们手撕过的。

2.2 为什么只用 1 层 Encoder? 真实 BERT 用 12 层,我们只用 1 层------原因有三个:①教学聚焦 :1 层已经能演示"自注意力看全句 + 残差 + LN + FFN"的全部机制,多层只是同样的块循环 N 次(第 7 章验证过形状保持);②数值梯度扛不住太多层 :每加一层,参数涨、前向变慢,数值梯度的时间成本线性上升;③任务足够简单 :情感分类这种"关键词驱动"的任务,1 层就够(92.5%)。"层数"这个超参数的意义在第 15 章会体现:真实 IMDB 上,1 层和 12 层的差距才会拉开------层数越多,能建模的关系越复杂(词法→句法→语义的层级抽象),但也越吃数据和算力。想亲手验证:把 Encoder 循环 2 次,你会看到训练变慢、但这个小任务上准确率几乎不变------"模型容量超出任务需求"是常见现象。

🎤 一句话总结模型"BERT 的骨架 + 一个小分类头"------把第 12 章的 CLS 用起来,就是分类器。整台机器 842 个参数,每个零件都有前 13 章的名字。

2.3 前向的完整流程(一行一行对) 。对照 demo2 的 forward 函数,五个步骤就是五句"老代码":①嵌入 emb[X] + pe------查表加位置(第 9 章工位一);②打分 q@kᵀ/√d 然后 scores + (1-mask)×(-1e9)------注意力打分 + padding mask(第 3、11 章);③加权求和 softmax(scores)@v------第 3 章公式;④残差+LN+FFN ------第 7 章的 encoder_block;⑤取第 0 列过分类头 ------第 12 章的 CLS整段前向没有任何新代码 ------这就是"前 13 章全部复用"的含义。特别是第 ④ 步:模型对整句话的所有位置都做了双向注意力(第 12 章),最后只取 CLS 一个向量做判断,其余位置的输出全部丢弃------注意力负责"读全句",CLS 负责"交卷"

2.1 分类任务为什么用 Encoder(双向)而不是 Decoder(单向)? 一个值得想清楚的问题:既然我们做过两种模板,为什么情感分类选 BERT 的 Encoder?答案在第 12、13 章的对比里:分类是"理解"任务,理解需要"看完整个句子再下结论" ------双向注意力(Encoder)能看到全部上下文,包括句子后半部分;而 Decoder 只看左边,会漏掉"but the acting was great"这种句尾的转折信息。反过来,生成任务必须用 Decoder (不能看未来)。所以:理解类任务(分类、抽取、问答)→ Encoder;生成类任务(续写、翻译、对话)→ Decoder------"任务决定架构"这句第 13 章的结论,在这里是第一次实战兑现。


三、前向与损失:先看模型"不会"时的样子

训练前先跑一次前向,看未训练模型的"出厂状态"(demo3):

真实输出:

复制代码
【demo3】未训练模型的一次前向:logits、概率、loss
logits shape: (16, 2) (batch, 2 类)
probs(前 3 个样本):
  label=1 → [0.1412, 0.8588]
  label=1 → [0.2658, 0.7342]
  label=0 → [0.6414, 0.3586]
平滑 CE loss(batch=16): 0.7117(接近随机猜的 ln(2)≈0.693)

未训练模型的三个"症状":①概率乱猜 (有的样本押积极、有的押消极,全看随机初始化的运气);②loss 约 0.71 ,接近随机猜测的 ln(2)≈0.693(二分类瞎猜的最优 loss);③部分样本"错得很自信" (比如 label=1 却给 0.8588 给消极)------随机初始化让模型"自信地胡说"。这三条就是训练的"起跑线":训练的全部意义,就是把 loss 从 0.71 往下压、让概率从"乱猜"变成"有依据"

3.1 怎么"读"这个 loss? 0.7117 这个数字本身没意义,它的意义来自两个参照:①对照随机猜测 :二分类瞎猜的 loss 是 ln(2)≈0.693,0.71 略高于它(因为随机初始化让模型"错得更自信"一点),所以训练前的 loss "约等于瞎猜";②对照训练后的下限 :第 10 章我们讲过平滑 CE 的 loss 有底------数据有 10% 噪声时,模型即使把"能学的 90%"全学会,那 10% 的错误标签仍会贡献一个"噪声下限"(约 0.3~0.4 量级),所以训练到 0.41 就"到顶了"。会用"瞎猜值"和"噪声下限"两个参照系读 loss,你就不会在训练时瞎焦虑------0.41 不是"没收敛",是"这个数据集的物理极限"。

3.2 softmax 方向:词表维度 vs 类别维度 。第 13 章我们踩过一次"softmax 做错方向"的坑,这里再强调一次:分类器的 softmax 是对类别维度 (最后一维,长度 2)做,得到"积极/消极的概率和 = 1";而注意力里的 softmax 是对序列维度 做。手撕代码时,务必确认 softmax_rows(logits) 作用在 (B, 2) 的最后一位------如果错按 (B,) 或按序列方向做,概率和不为 1、loss 全是 NaN,而且这种错误不会报错,只会静默地给出垃圾结果(这正是前 13 章反复做"性质验证"的原因:行和是否为 1 是最好的探针)。

3.3 loss 为什么对 batch 取平均? demo3 的 loss 是 16 条样本的"平均"交叉熵。为什么不求和?因为batch 大小是超参数 :如果求和,batch 从 16 换成 32,loss 就翻倍,梯度大小也翻倍------同样的学习率在两个 batch 下行为完全不同,超参数就不"可迁移"了。取平均后,loss 的量级与 batch 无关(都代表"平均每条样本错多少"),学习率、收敛行为在不同 batch 下基本一致,调参更省心。"loss 取平均"是让超参数(batch、学习率)可迁移的工程惯例 ------PyTorch 的 CrossEntropyLoss 默认也是 mean 而不是 sum。


四、训练:60 步,见证"从瞎猜到会看词"

训练循环与第 12/13 章完全同款:数值梯度 + AdamW + warmup + 平滑 CE,60 步,每步随机抽 16 条做 batch。每 10 步打印一次 loss、训练准确率、测试准确率(demo4):

真实输出:

复制代码
【demo4】训练 60 步:数值梯度 + AdamW + warmup(batch=16)
   step       loss  train_acc   test_acc
      0     0.7117      0.531      0.425
     10     0.6803      0.525      0.400
     20     0.5212      0.831      0.875
     30     0.5129      0.838      0.800
     40     0.3577      0.887      0.950
     50     0.4141      0.900      0.950
     60     0.4139      0.900      0.925
测试集混淆矩阵:
  预测=积极: 真积极 15 / 误报 2
  预测=消极: 真消极 22 / 漏报 1
  测试准确率: 92.5%

读这张训练日志,五件事值得注意:

  • 前 10 步几乎没动(loss 0.71→0.68,acc 还在 50% 上下)------这是 warmup 阶段(学习率从 0 爬升),模型还在"热身";
  • 20 步是转折点:loss 掉到 0.52,训练准确率 83%、测试准确率 87.5%------模型开始"看懂"情感词;
  • 40 步后:测试准确率 95%、最终 92.5%(10% 噪声的存在让 100% 不可达,94% 就是理论上限);
  • 训练 90% vs 测试 92.5%:测试集居然比训练集还高?这是小测试集(40 条)的方差------正常现象,不必惊讶;
  • 混淆矩阵 :40 条测试样本中,真积极 15、误报 2、漏报 1、真消极 22------对角线上 37/40 判对,3 条判错(大概率来自那 10% 的噪声标签)

loss 曲线与准确率曲线画成图:

🎤 一句话总结训练60 步、842 参数、纯 NumPy------loss 从 0.71 压到 0.41,测试准确率从 42.5% 涨到 92.5%。训练三件套 + 数值梯度的组合,第 4 次被验证"真实可跑"。

4.1 数值梯度这 60 步跑了多少计算? 账要算清楚:842 个参数 × 每步 2 次前向(+δ 和 −δ)× 60 步 ≈ 10 万次前向 ;每次前向是 batch 16 × 序列 8 × d=8 的小矩阵运算,Python 单次约 0.5 毫秒,总计约 1~2 分钟。刚够教学,但这就是数值梯度的天花板 :参数翻 10 倍(8420 个),时间就翻 10 倍;真实 BERT(1.1 亿参数)用数值梯度是天文数字。这也是为什么第 15 章必须切换到 PyTorch 的 backward()------它的计算量约等于一次前向(反向传播),而不是"参数个数 × 前向" 。但请注意:数值梯度给了我们"零实现错误"的保障------第 15 章用 PyTorch 时,如果梯度算得不对,你永远无法靠肉眼发现;而这一章我们亲手验证过"梯度方向正确"(loss 确实在降、准确率确实在涨),这份"手感"会帮你以后排查 bug

4.2 训练日志里的"波动"怎么解读? 仔细看 demo4 的表格,有两个反直觉现象:①第 50 步 loss 反而比第 40 步高 (0.4141 vs 0.3577)------因为每 10 步打印时用的 batch 是随机抽取 的 16 条,不同 batch 的 loss 天然有波动,这不是"训练变差",是"取样噪声";②测试准确率在 40~60 步间是 0.950/0.950/0.925 ------40 条小测试集上差 1 条样本就是 2.5 个百分点,同样是小样本方差。识别"信号"和"噪声"是训练老手的基本功:看趋势(前 20 步大幅下降)别盯单点(某一步的抖动)。如果要更稳的曲线,把"每 10 步打印一次"改成"打印最近 5 次的平均"(滑动平均)------这是所有训练日志工具的标配。

4.3 本章超参数速览。把这一章用到的"旋钮"汇总成一张表,方便对照第 10 章:

超参数 取值 来源/说明
嵌入维度 d 8 第 5/9 章(越大容量越大,越吃算力)
Encoder 层数 1 本任务 1 层够用
batch 大小 16 每次抽样训练样本数
学习率峰值 0.1 AdamW + warmup 5 步(第 10 章)
平滑系数 ε 0.1 标签平滑(第 10 章)
权重衰减 λ 0(无衰减) 小数据/小模型可不加
训练步数 60 收敛后继续也无益
max_len 8 数据集句子长度上限

这套配置没有"调参"------全是前几章的经验值直接搬来。真正的调参(学习率、层数、d)在真实数据上才有意义,第 15 章你会看到 PyTorch 版怎么调。

4.4 换算一下:60 步 = 多少轮(epoch)? 60 步 × 每步 16 条 = 960 条"样本见过量";训练集有 160 条,所以 60 步 ≈ 6 个 epoch(每轮把训练集完整过一遍)。用第 10 章的术语说:epoch = 完整过一遍训练集的次数,step = 一次 batch 更新 ,二者关系是 epoch = step × batch / 训练集大小。真实项目里"几个 epoch"比"几百步"更好沟通(BERT 微调常说 2~4 个 epoch)。本章 60 步 ≈ 6 轮,在第 20 步(≈2 轮)时就已收敛------任务简单,模型学得快;这也解释了为什么常见建议是"先少训几个 epoch 看趋势,不够再加"。


五、预测:让模型"干活"

训练完,模型要面对的是从没见过的新句子(demo5):

真实输出:

复制代码
【demo5】对新句子做预测(训练集中没有这些句子)
  'this movie is great'      → 积极 0.919 / 消极 0.081 → 判定=积极
  'that film was terrible'   → 积极 0.062 / 消极 0.938 → 判定=消极
  'i really love it'         → 积极 0.918 / 消极 0.082 → 判定=积极
  'this film is bad'         → 积极 0.069 / 消极 0.931 → 判定=消极
  'it was so boring and awful' → 积极 0.065 / 消极 0.935 → 判定=消极
  'my happy day'             → 积极 0.917 / 消极 0.083 → 判定=积极

6 句全部判对,而且概率很有区分度 (积极的句子"积极概率 0.92",消极的句子"消极概率 0.93")------模型不仅知道"是积极还是消极",还相当"确信"。注意这些句子的词序、组合都是训练集里没有的(比如 "my happy day" 只有 3 个词,"it was so boring and awful" 同时含两个消极词),但模型能泛化------这正是"学习"和"背答案"的区别。画成图:

🎤 一句话总结预测泛化是模型的毕业考------训练集里的句子 100% 背下来不算本事,没见过的句子也判对才是"学会"。demo5 的 6 句全对 + 高置信度,证明这台 842 参数的机器真的"会看情感词"了。

5.1 模型到底"看"什么? 一个值得说透的问题:这个模型没有"情感词典",它凭什么判断?答案在嵌入向量 里:训练过程中,积极词(good/great/love...)的嵌入向量被推向"容易输出积极 logits"的方向,消极词被推向反方向------模型学到的是**"情感词的嵌入方向与类别强相关"**这条规律,而不是"背下某个词"(训练集里根本没有 demo5 那些句子)。你可以做一个实验验证:打印训练后词表中 "good" 与 "bad" 的嵌入向量(d=8),计算它们的余弦相似度------大概率是负值(方向相反)。注意力负责"看哪里",嵌入负责"记住含义"------这和第 12 章 BERT 的结论完全一致:语言知识藏在嵌入与权重里,而不是藏在某个显式的"规则表"里。

5.2 置信度怎么用? demo5 输出里每个预测都带概率(0.92、0.94......),这个"置信度"不只是好看,它有三个实用价值:①筛掉没把握的 ------真实产品里可以设阈值(比如"置信度 < 0.6 就转人工/不表态"),避免模型在模糊句子上乱下结论;②做多级分类 ------概率 0.5~0.6 可以标成"中性",0.9+ 才叫"强烈积极",把二分类扩展成五级评分;③诊断模型 ------如果模型对某些句子总是低置信度,说明它没见过这类模式,是收集更多数据的信号。概率输出是深度模型最值钱的副产品之一------分类器不只是给一个标签,而是给一个"它有多确信"的完整分布。

5.3 预测时 mask 还重要吗? 重要。预测(推理)时虽然不做 padding(单句没有对齐问题),但 forward 里 mask 的处理逻辑仍然生效------demo5 里每个单句都调用了 encode() 生成 mask,保证 CLS 的注意力不会"看"到句尾的 pad 位置。更普遍的场景是批量预测 :一次给 10 条不同长度的句子,必须 padding 到同一长度,mask 就派上用场(和第 11 章 batch 构建一模一样)。训练和推理共用同一个 forward 是深度学习的工程美德------模型只有一套前向逻辑,换个输入形态(单句/批量、有无 padding)都能跑,这正是"数据与模型解耦"的体现。


六、评估与对比:模型到底学到没有?

最后用两个"参照物"给结果定位(demo6):

复制代码
【demo6】与基线对比
瞎猜基线(随机/多数类)准确率: 50.0%
本章小型 Transformer 测试准确率: 92.5%
提升: 42.5 个百分点
对照:RNN 系列在真实 IMDB 上约 79.8%(第16章),本模型是教学版小数据,重点看"训练循环"本身
  • 瞎猜基线 50%:随机猜(或永远猜"积极"),对半开------这是"什么都不学"的下限;
  • 本模型 92.5% :比基线高 42.5 个百分点------模型确实从数据里学到了"情感词 → 类别"的规律
  • RNN 系列 79.8% (真实 IMDB,第 16 章):那不是本模型能直接比的------数据规模差了两个数量级、任务难度不同。这里的对照意义是:"训练循环"这个骨架完全一致,换真实数据 + 更大模型,就是第 15 章要做的微调。

6.1 从 92.5% 到真实 IMDB:还差什么? 诚实地说,这台 842 参数的模型换到真实 IMDB 上会跌到 60% 以下------因为真实影评长、含否定("not bad" 其实是积极)、含反讽("so bad it's good")、词汇量大得多。要从"玩具准确率"走向"真实准确率",需要的不是改模型,而是堆规模 :数据从 200 条到 2.5 万条、词表从 32 到 3 万(第 11 章 BPE)、模型从 842 参数到 1.1 亿(第 12 章 BERT)。这三样堆完,就是第 15 章------与其自己从零堆,不如直接用别人堆好的 (预训练 BERT)。这也是"预训练 + 微调"范式存在的意义:小数据 + 大模型,胜过大数据 + 小模型

6.2 评估指标进阶:准确率不是唯一 。92.5% 是"准确率"(Accuracy = 判对/总数),但对不均衡数据(比如 90% 积极 10% 消极),"永远猜积极"也能拿 90%------所以工程上还要看:精确率 (预测为积极里真积极的比例 = TP/(TP+FP))、召回率 (真积极里被找出来的比例 = TP/(TP+FN))、F1 (两者的调和平均)。用 demo4 的混淆矩阵算一遍:精确率 = 15/17 ≈ 88.2%,召回率 = 15/16 ≈ 93.8%,F1 ≈ 90.9%------三个数字一起看才完整(有的业务宁可精确率低也要召回率高,比如医疗筛查)。做项目时先想清楚"这个任务更怕误报还是漏报",再选指标。

6.3 Transformer vs GRU:目录里的那道思考题 。系列目录在第 14 章列了一个思考题:"Transformer 在短文本上是否一定比 GRU 强?"现在可以认真回答了:不一定 。短文本 + 小数据下,两者差距很小(甚至 GRU 更快更省显存);Transformer 的优势在长序列和并行 ------自注意力一次看全句(O(S²) 但并行)、能建模任意距离的依赖;RNN 串行(O(S) 但只能一步步来)、长依赖会梯度消失(第 1 章)。本系列 RNN 篇在真实 IMDB 上拿到 79.8%(16 章),而 Transformer 要用真实数据 + 足够规模才能体现出优势(第 15 章微调 BERT 会到 90%+)。选型口诀:数据少、句子短 → GRU 够用且省;数据多、序列长、要并行 → Transformer------"先进"不等于"任何场景都更好"。

6.4 实战篇的三步节奏 。第 14-16 章是递进关系:第 14 章(本篇)自己造 ------从零训练 842 参数的模型,理解"训练循环"的每个环节;第 15 章站在巨人肩上 ------加载预训练 BERT(1.1 亿参数),只微调几轮,用更少的时间和数据拿到更高准确率,对比"从零训练 vs 微调"的巨大差距;第 16 章收官 ------把 RNN 与 Transformer 两条知识线织成一张认知树。这三步其实复刻了业界真实的工作流:先会从头训(懂原理),再会用预训练(提效率),最后能纵观全局(会选型)。本篇是第一步------如果第 14 章的五步闭环你能独立跑通,后面两章会非常轻松,因为它们只是"同一个闭环"的规模放大和零件替换。


七、常见坑与自查

  • 训练/测试数据没分开 :测试集必须和训练集完全无交集(demo1 的 160/40 分割)。如果测试集混进训练集(我第一版代码就犯过这错:data[40:] 导致测试集与训练集重叠,准确率虚高到 100%),评估就完全失真------"高准确率"可能是自欺欺人
  • 忘了 padding mask :只 padding 不 mask,模型会把 <pad>(id=0)当真词去算注意力,等于往句子里掺噪声。自查:batch 里 mask 的 0 位置必须与 pad 位置一一对应;
  • 用未训练的模型下结论:训练前 loss 0.71、准确率 50%,这是"出厂状态";任何"看效果"的操作(预测、对比)都要在训练完成后做;
  • 把训练准确率当最终成绩 :训练 90% 不代表测试 92.5% 或反之------永远以测试集为准(小测试集有方差,别对单次数字较真);
  • 对噪声数据期望 100%:数据集有 10% 标签噪声,理论上限就是 90%~95%------模型"学对"噪声样本反而会错。看到 92.5% 应该满意,而不是怀疑"为什么不是 100%";
  • 数值梯度只适合教学 :842 参数 × 60 步 ≈ 10 万次前向,约 1-2 分钟;真实项目用 PyTorch 的 backward()(第 15 章起),速度差几千倍;
  • 混淆矩阵误读:混淆矩阵看的是"对角线的占比"(真积极+真消极)除以总数;"误报"(把消极判成积极)和"漏报"(把积极判成消极)在不同业务里代价不同(医疗诊断更怕漏报),实际工程要按业务调阈值;
  • 没有固定随机种子 :demo1 用了 RandomState(42),否则每次运行数据不同、结果不可复现。真实项目也建议固定 seed(数据、模型初始化、batch 抽取都要固定),否则你无法判断"准确率变化是改进带来的还是随机波动";
  • 把"准确率 +0.5%"当成就:小测试集(40 条)上 92.5% 和 95% 的差异可能只是 1 条样本的运气------改进模型前先算"测试集方差"(用多次不同 seed 重跑看波动范围),别对着单次数字过度调参。

小结

这是实战篇的第一章------第一次让模型"干活"。核心收获八条:

  • 任务与数据:情感二分类 + 规则生成 200 条短句(含 10% 噪声),160/40 分割,padding + mask 打包(demo1、图 1-3);
  • 模型:encoder-only = 嵌入 + 位置编码 + 1 层 Encoder + CLS 分类头,842 个参数,全手撕零件(demo2、图 2);
  • 前向与损失:未训练 loss 0.7117(≈随机猜 ln2),概率乱猜(demo3);
  • 训练:平滑 CE + 数值梯度 + AdamW + warmup,60 步 loss 0.71→0.41(demo4、图 4、图 6);
  • 评估:测试准确率 42.5%→92.5%,混淆矩阵 37/40 判对(demo4、图 7、图 9);
  • 预测:6 句新句子全部判对且高置信度(积极 0.92 / 消极 0.93)------真正的泛化(demo5、图 8);
  • 对比:vs 瞎猜基线 50%,提升 42.5 个百分点;vs RNN 系列 79.8%(真实 IMDB)------骨架相同、规模不同(demo6、图 9);
  • 闭环意义 :数据 → 模型 → 训练 → 评估 → 预测,五步闭环跑通------前 13 章的理论,第一次变成了能用的成果
  • 工程与科学的边界:准确率是"工程目标",loss 是"科学信号"------两者都要看;评估指标(精确率/召回率/F1)按业务选;
  • 一句话记住本章 :842 个参数、200 条数据、60 步训练,一台"会看情感词"的机器就从零长出来了------深度学习项目的规模可大可小,但闭环永远是这个五步
  • 下一章的钩子 :92.5% 是"从零训练"的天花板(842 参数 + 200 条数据),第 15 章微调 1.1 亿参数的 BERT,会在真实 IMDB 上把这个数字甩开一个身位------"预训练 + 微调"为什么是王道,下一章见分晓

第 14 章证明了"自己造的机器能干活"。但 842 个参数 + 200 条数据,天花板很低------92.5% 就是这台小机器在这个小数据集上的"极限成绩",再往上必须换更大的船。下一章我们来一次"站在巨人肩上"的操作:加载别人用海量数据预训练好的 BERT,只微调几轮,就能在真实 IMDB 上吊打我们从零训练的模型------这就是迁移学习的威力。


下一篇(四十四):微调预训练 BERT 做下游任务

相关推荐
DsirNg2 天前
性能告警不是红灯:把 Web Vitals 变成发布决策系统
性能监控·cls·rum·lcp·前端性能·web vitals·inp
Lee_jerome16 天前
python神经网络编程入门(二十七)——RNN IMBD搭建情感分类器与基础训练
pytorch·rnn·深度学习·gru·词嵌入·情感分析·imdb
All The Way North-1 个月前
【TorchMetrics精通系列①】核心设计哲学 + Accuracy 超详解
机器学习·分类·模型评估·accuracy·准确率·评估指标·torchmetrics
All The Way North-1 个月前
【NLP文本分类实战】随机森林 + TF-IDF 完整流程,准确率82.5%(数据分析/分词/模型训练)
随机森林·机器学习·nlp·tf-idf·文本分类·sklearn·保姆级教程
All The Way North-2 个月前
FastText核心API train_supervised 完全指南:参数详解、学习率衰减、预测评估与中英文数据避坑
机器学习·自然语言处理·nlp·api·文本分类·fasttext·多标签分类
王小王-1232 个月前
基于深度学习的景区口碑情感分析可视化系统
人工智能·深度学习·bert·情感分析·关键词提取·主题分析·景区评论分析
王小王-1232 个月前
基于商品评价的评论情感分析与可视化系统
hive·情感分析·商品评价分析·主题分析·商品评论分析
cesske3 个月前
机器学习模型评估指标|准确率、召回率、F1详解
人工智能·深度学习·机器学习·模型评估·召回率·准确率
极光代码工作室4 个月前
基于深度学习的微博情感分析系统
人工智能·深度学习·神经网络·nlp·情感分析