Transformer工程三线:BERT微调、RoBERTa预训练与生成避坑
简介:《Transformers for Natural Language Processing》是面向NLP开发者与深度学习学习者的英文原版专著,从零讲解自注意力机制及Transformer架构,帮助读者用Python和主流框架理解并应用预训练模型。全书分三阶段展开:从原始Transformer与小型模型训练入门,过渡到自然语言理解与生成任务,再深入社交网络数据分析、假新闻识别及认知科学视角下的模型解释;同时覆盖主流NLP平台,以及情感分析、文本摘要、语音识别、机器翻译等相关实践场景,并讨论模型性能、适用范围与生产限制,配有模型选择、微调与评估思路。资源包内含1个PDF文件,约4.72MB,便于读者阅读与检索,内容兼顾原理、代码与实战,适合作为案头参考。目前已有1969人深入学习,适合希望系统梳理Transformer原理、预训练模型应用与NLP项目实践的读者参考。
1. 从一份 PDF 到能跑的工程:这份材料把 Transformer 拆成了哪几层
去年帮一个做招投标文本抽取的团队看模型,他们手里堆了七八份教程,卡住的却是同一个问题:Transformer 的注意力到底怎么落到自己的数据上。这份 Transformers for Natural Language Processing 的 PDF,属于比较少见的、从架构图一路写到能跑代码的材料——自然语言处理里最常被问的几件事它都覆盖了:原始 Transformer 的编码器和解码器怎么拆、BERT 与 RoBERTa 怎么微调和从头预训练、T5 与 GPT-2 怎么做摘要和生成。十二章是一条有坡度的线,先让你把 self-attention 的矩阵形状算明白,再交给 Hugging Face 的预训练模型去接具体任务。它适合会用 PyTorch 搭简单网络、但没系统跑过预训练模型微调的开发者,也适合要把情感分析、语义角色标注、机器翻译往项目里塞的人。下面按我拆书的顺序讲,重点放在哪几章值得抄代码、哪些参数一改就翻车。
2. 环境与章节映射:把十二章拆成三条能跑的流水线
2.1 先选路线:有没有标注数据,决定你从第几章开始读
这本书的十二章不是线性依赖的,硬从头读到尾会浪费大量时间。我一般按手里的数据形态分三条线,选一条主攻,剩下的当工具书查。
第一条是架构理解线,对应原始 Transformer 架构、GPT 系列、T5 与文本摘要那几章。它的价值不在于跑出多高的分数,而在于让你能对着hidden_size、num_attention_heads、max_position_embeddings这些参数说清楚模型在干什么。做模型裁剪、改结构、排查精度异常的活,这条线是底子。
第二条是微调线,对应 BERT 微调、下游任务评测、语义角色标注、情感分析这几章。判据很直接:手里有几百到几万条标注样本,任务边界清楚(二分类、多分类、抽取),单卡显存 8GB 以上,就从这条线进。它产出的东西能直接进业务,也是多数人真正需要的部分。
第三条是从零预训练线,对应 RoBERTa 从头训练、tokenizer 与数据集匹配、机器翻译那几章。这条线门槛最高:语料至少几百 MB 纯净文本,显卡至少 24GB 或者能接受梯度累积把时间拉长,而且要有心理准备——自己训出来的小模型多数情况下打不过直接微调开源权重。它真正的用武之地是领域词表严重偏离通用词表的时候,比如大量化学式、代码标识符、内部缩略语,通用 tokenizer 切出来全是碎片。
| 路线 | 主要章节 | 前置条件 | 可交付产物 |
|---|---|---|---|
| 架构理解 | 原始 Transformer、GPT、T5 | 能读 PyTorch 代码 | 参数对照表、注意力可视化 |
| 微调 | BERT 微调、下游任务、SRL、情感分析 | 千级标注样本、单卡 | 分类/抽取模型、评测报告 |
| 从零预训练 | RoBERTa 从头训练、tokenizer 匹配、翻译 | 百 MB 级语料、大显存 | 领域 tokenizer、MLM 权重 |
我通常建议先花半天走微调线,把一条完整链路跑通,再回头补架构线的细节。反过来做的人,往往在矩阵形状里绕三天,最后连一个能预测的模型都没落着。
2.2 依赖清单与目录约定:版本锁不住,后面全是玄学
这本书写作时间早,示例围绕 transformers 库的 4.x 早期版本展开,直接pip install最新版会遇到一堆参数改名。血泪经验是:先把版本钉死在一个能互相兼容的区间,再谈复现。下面这份清单是我按书里的调用方式反推的,装完能把绝大多数示例跑起来。
# 建议单独建虚拟环境,Trax 那一章依赖冲突最凶,单独再开一个 python -m venv venv-nlp source venv-nlp/bin/activate pip install --upgrade pip # torch 按本机 CUDA 版本装,别直接装默认包 pip install torch==1.13.1 --index-url https://download.pytorch.org/whl/cu117 # 4.2x 之前的 transformers 还保留了书里大量旧参数名 pip install "transformers>=4.18,<4.27" pip install "tokenizers>=0.12,<0.14" pip install "datasets>=2.4,<2.10" pip install sentencepiece==0.1.97 # T5 / 多语种 tokenizer 必需 pip install scikit-learn==1.1.3 # Matthews 相关系数在这里 pip install accelerate==0.15.0 # Trainer 的设备放置 pip install matplotlib seaborn # BertViz 那几章的图这段命令的逻辑是分层装:先固定 Python 和 torch,再装 transformers 主库,最后装 tokenizers 和 datasets 这两个容易被主库版本牵制的依赖。transformers卡在 4.18 到 4.27 之间,是因为再往后的版本把evaluation_strategy改成了eval_strategy、部分 pipeline 的任务名也换了,书里的示例会直接报unexpected keyword argument。sentencepiece单独指定版本,是因为 T5 的词表加载走它,版本漂移会报“无法识别的模型文件”。
目录上我会把数据和代码分开,避免 notebook 跑完找不到权重:
project/ ├── data/ # 原始语料与标注文件,只读 │ ├── raw/ │ └── processed/ ├── tokenizer/ # 自己训的词表,vocab.json + merges.txt ├── checkpoints/ # 训练中间产物,按 step 命名 ├── notebooks/ # 按章节编号,便于回查 └── src/ ├── dataset.py ├── train_mlm.py └── evaluate.py2.3 章节到代码产物的映射表
书里每一步基本都对应一个可运行片段,但不是每段都值得抄。按我的使用习惯,把十二章归到“必跑”“选跑”“只读”三档:
| 章节主题 | 关键产出 | 建议档位 | 备注 |
|---|---|---|---|
| 原始 Transformer 架构 | 注意力形状推导、位置编码实现 | 必跑 | 手写一遍 attention 比调库有用 |
| BERT 微调 | 分类训练循环、MCC 评测 | 必跑 | 整本书最通用的一套模板 |
| RoBERTa 从头预训练 | 领域 tokenizer、MLM 权重 | 选跑 | 语料不够就别碰 |
| 下游任务与 SuperGLUE | 指标定义、任务格式 | 只读 | 看评测口径即可 |
| 机器翻译与 BLEU | WMT 预处理、平滑 BLEU | 选跑 | 没有翻译业务可以直接跳 |
| GPT-2 文本生成 | 采样参数、条件生成 | 必跑 | 生成任务的参数直觉全靠这章 |
| T5 文本摘要 | 前缀式任务格式 | 必跑 | 摘要落地最快的一条路 |
| tokenizer 与数据集匹配 | 六个词表边界 case | 只读 | 出了问题回来查 |
| 语义角色标注 | 预训练 SRL 模型调用 | 选跑 | 做抽取式问答时是兜底手段 |
| 情感分析对比 | 多模型横向评测 | 选跑 | 选型阶段值得抄 |
| 假新闻分析 | 组合式分析流程 | 只读 | 方法论多于代码 |
映射表里“必跑”的三块——原始架构、BERT 微调、T5 摘要——加起来大约占全书代码量的三分之一,但覆盖了八成实际场景。其余章节当字典用,遇到具体问题再翻,比顺序读完效率高得多。
3. 微调 BERT 走通一遍:从数据到 MCC 指标
3.1 为什么先拿 BERT 微调当第一块试验田
选它不是因为效果最好,而是因为它的链路最短、变量最少。一个bert-base模型加上分类头,参数量在一亿出头,单张 8GB 卡把max_length压到 128、batch_size压到 16 就能跑;训练脚本里从分词、掩码、优化器到评测,每一环的输入输出都很直白,出错时容易定位。相比之下 GPT-2 的生成任务没有标准答案,T5 的摘要要靠人工看质量,调试反馈都来得慢。
还有个现实原因:书里这套微调代码的组织方式,挪到 RoBERTa、DistilBERT、甚至多语种模型上,改的只是from_pretrained那一个字符串。先把模板跑顺,后面换模型就是换名字。我见过不少人一上来就啃从头预训练,卡在 loss 不降,最后连是数据问题还是模型问题都分不清。
需要提醒的是任务类型。书里的示例偏句子对分类(判断两句话的关系),如果你要做的是单句多分类或序列标注,num_labels和标签对齐方式要自己改,不能照抄。
3.2 数据到张量:tokenizer、attention mask 与 Dataset 封装
第一步是把文本变成定长张量。这里最容易出问题的是“每个 batch 长度不一”,手写 collate 很容易把 padding 位置算错,导致 attention mask 和 input_ids 错位。
from transformers import BertTokenizerFast import torch # fast tokenizer 底层是 Rust 实现,批量编码快,且能返回 offset mapping tokenizer = BertTokenizerFast.from_pretrained("bert-base-uncased") batch = tokenizer( ["the contract shall be governed by the laws of the state", "payment terms are net thirty days"], max_length=128, # 定长便于堆 batch,短文本 128 足够 padding="max_length", # 不用 longest,避免每个 batch 形状变化 truncation=True, # 超长直接截断,不做滑窗 return_tensors="pt", return_attention_mask=True, ) print(batch["input_ids"].shape) # torch.Size([2, 128]) print(batch["attention_mask"].sum(1)) # 每条真实 token 数,用于核对截断max_length=128是个折中:法律、金融类条款句往往在 60 到 100 个 subword 之间,128 能覆盖九成以上;设到 256 显存翻倍,收益很小。padding="max_length"看着浪费,但换来的是整个训练过程张量形状恒定,省掉了动态 padding 带来的 collate 复杂度。attention_mask那行打印是我固定的自检动作,如果某条样本的 mask 全是 1,说明根本没截断,反过来如果短得异常,多半是分词器把特殊符号切碎了。
数据封装成 Dataset,把分词放在__init__里一次性做完,不要放到__getitem__,否则每个 epoch 重复分词,训练时间能翻倍:
from torch.utils.data import Dataset class PairDataset(Dataset): def __init__(self, texts_a, texts_b, labels, tokenizer, max_length=128): # 一次性编码,避免每个 epoch 重复分词 self.enc = tokenizer( texts_a, texts_b, max_length=max_length, padding="max_length", truncation=True, return_attention_mask=True, ) self.labels = labels def __len__(self): return len(self.labels) def __getitem__(self, idx): item = {k: torch.tensor(v[idx]) for k, v in self.enc.items()} # 分类任务标签必须是 long,float 会直接让 CrossEntropyLoss 报错 item["labels"] = torch.tensor(self.labels[idx], dtype=torch.long) return item参数上只有两个需要按数据调:max_length按语料长度分布的第 95 百分位取,别拍脑袋;labels的类型必须是torch.long,这一条踩过坑的人不少——用 pandas 读进来的标签默认是 int64,转 tensor 时若被astype(float)污染,训练会在 loss 计算处直接抛类型错误,报错信息还指向模型内部,很难第一时间联想到标签。
3.3 优化器分组与训练循环里真正影响收敛的几个参数
BERT 微调对优化器很敏感。书里的做法是分组设置 weight decay:偏置项和 LayerNorm 参数不做衰减,其余权重做。这不是玄学,LayerNorm 的缩放和平移参数如果被衰减,归一化行为会漂移,收敛变慢。
from transformers import BertForSequenceClassification, AdamW model = BertForSequenceClassification.from_pretrained( "bert-base-uncased", num_labels=2, # 二分类;多分类改成实际类别数 ).to("cuda") no_decay = ["bias", "LayerNorm.weight"] optimizer_grouped = [ {"params": [p for n, p in model.named_parameters() if not any(nd in n for nd in no_decay)], "weight_decay": 0.01}, {"params": [p for n, p in model.named_parameters() if any(nd in n for nd in no_decay)], "weight_decay": 0.0}, ] optimizer = AdamW(optimizer_grouped, lr=2e-5, eps=1e-8) # 线性预热 + 线性衰减,预热占总步数 10% from transformers import get_linear_schedule_with_warmup total_steps = len(train_loader) * 3 scheduler = get_linear_schedule_with_warmup( optimizer, num_warmup_steps=int(0.1 * total_steps), num_training_steps=total_steps, )lr=2e-5是微调预训练模型的常用起点,比从头训练小两个数量级;再大容易把预训练权重冲掉,表现为第一个 epoch 损失抖动剧烈。weight_decay=0.01对 BERT 这类模型比较稳,文本分类任务上试过 0.1 会明显掉点。预热步数取总步数 10%,是为了让优化器在初期不要用大学习率去动那些还没适配新任务的层。
训练循环本身没什么花样,关键是每步之后检查梯度范数:
for epoch in range(3): model.train() for step, batch in enumerate(train_loader): batch = {k: v.to("cuda") for k, v in batch.items()} outputs = model(**batch) loss = outputs.loss loss.backward() # 梯度裁剪,防止个别 batch 把权重带偏 torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() scheduler.step() optimizer.zero_grad()clip_grad_norm_的阈值 1.0 是我固定加的后悔药。文本分类里偶尔会出现一条超长或者标点异常的样本,梯度范数突然上到几十,不裁剪的话后面几个 step 都在还债。如果打印出来长期超过 5,说明学习率偏大或者数据里有脏样本,得先查数据而不是调裁剪阈值。
3.4 用 MCC 验收:为什么准确率在这里会骗人
类别不均衡时准确率会严重失真。比如正样本只占 8%,模型全预测负类也有 92% 的准确率,但这个模型毫无用处。书里用 Matthews 相关系数(MCC)做验收,它同时考虑混淆矩阵的四个格子,取值在 -1 到 1 之间,0 表示跟随机猜没区别。
from sklearn.metrics import matthews_corrcoef, f1_score import numpy as np model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for batch in eval_loader: batch = {k: v.to("cuda") for k, v in batch.items()} logits = model(**batch).logits preds = torch.argmax(logits, dim=-1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(batch["labels"].cpu().numpy()) print("MCC:", matthews_corrcoef(all_labels, all_preds)) print("F1 :", f1_score(all_labels, all_preds, average="binary"))这段的逻辑是先收集全量预测再统一算指标,不要在每个 batch 内算完取平均——小 batch 里可能全是同一类,MCC 会变成 0 或未定义,平均下来毫无意义。我一般把 MCC 和 F1 一起看:MCC 掉到 0.3 以下说明模型基本没学到东西,先查标签是否错位;MCC 正常但 F1 偏低,多半是阈值问题,二分类可以把argmax换成对正类概率卡阈值再扫一遍。
到这里一条最小可用链路就闭环了:分词、封装、训练、评测。这套模板的价值在于,把BertForSequenceClassification换成RobertaForSequenceClassification、把 tokenizer 换成对应版本,其余代码几乎不用动。
4. 从零预训练 RoBERTa:tokenizer、config 与 Trainer 的十五步
4.1 从头训练的判断标准:词表覆盖率比语料规模更关键
很多人问我什么时候该自己训一个模型。我的判断顺序是:先算通用 tokenizer 在你自己语料上的切分碎片率。做法很简单,拿一万条样本过一遍bert-base的分词器,统计被切成单字符的比例,超过 15% 就说明词表不匹配,这时候从头训一个领域 tokenizer 是合理的;低于 5%,直接微调开源权重,别浪费算力。
语料规模上,书里的示例走的是“小模型 + 中等语料”的路子,hidden_size768、6 层、12 头,参数量六千万上下。这个规模在单张 24GB 卡上,几百万到上千万 token 的语料能在一两天内跑完几个 epoch。再大就没必要自己训了,收益远不如拿现成权重继续预训练。
要清楚自己训出来的东西是什么定位:它是一个领域语言模型,擅长补全和表征,不是任务模型。要拿它做分类,还得接分类头再微调一轮。
4.2 训练一个领域 tokenizer 并对齐 config
分词器训练和模型配置必须严格对齐,vocab_size差一个数,后面加载权重就会越界。这是最容易翻车的地方之一。
from tokenizers import ByteLevelBPETokenizer # ByteLevel 的 BPE 对未登录词友好,适合领域文本 bpe = ByteLevelBPETokenizer() bpe.train( files=["data/processed/domain_corpus.txt"], vocab_size=52000, # 与后面 config 必须完全一致 min_frequency=2, # 低频词不单独成词,避免词表虚胖 special_tokens=["<s>", "<pad>", "</s>", "<unk>", "<mask>"], ) bpe.save_model("tokenizer") # 产出 vocab.json 与 merges.txtvocab_size=52000是个经验值:通用英文 BERT 是 30522,扩到五万左右能给领域词留出空间,又不至于让 embedding 层占太多显存。min_frequency=2意味着只出现一次的词不单独建词,直接被拆成子词,这对小语料很重要,否则词表里全是只出现一次的长尾词,训练时这些位置的梯度几乎没有。
训练完要立刻验证两件事:特殊 token 的 id 是否落在预期位置,以及<mask>的 id 是否和 config 里写的一致。
from transformers import RobertaConfig, RobertaForMaskedLM, RobertaTokenizerFast tokenizer = RobertaTokenizerFast.from_pretrained("tokenizer") mask_id = tokenizer.mask_token_id # 必须取出来喂给 config config = RobertaConfig( vocab_size=52000, # 与训练时一字不差 max_position_embeddings=514, # RoBERTa 惯例,比 BERT 的 512 多 2 hidden_size=768, num_hidden_layers=6, # 小模型,方便单卡跑 num_attention_heads=12, # hidden_size 必须能被它整除 intermediate_size=3072, # 一般是 hidden_size 的 4 倍 type_vocab_size=1, # RoBERTa 不分段,设为 1 mask_token_id=mask_id, ) model = RobertaForMaskedLM(config) print("参数量:", sum(p.numel() for p in model.parameters()) / 1e6, "M")max_position_embeddings=514是 RoBERTa 系列的惯例偏移,直接用 512 在某些实现里会导致位置索引越界。type_vocab_size=1是因为 RoBERTa 取消了下一句预测任务,不需要区分句子 A 和 B。这几项写错,训练不一定立刻报错,但会在加载预训练权重时出现形状不匹配,或者在长文本上莫名其妙崩掉。
4.3 用 Trainer 跑 MLM 预训练与 FillMask 自检
掩码语言建模的样本构造交给DataCollatorForLanguageModeling,它会动态选 15% 的位置做掩码,其中 80% 换成<mask>、10% 换随机词、10% 保持不变。这个比例是原论文的设定,不建议改。
from transformers import (DataCollatorForLanguageModeling, Trainer, TrainingArguments) collator = DataCollatorForLanguageModeling( tokenizer=tokenizer, mlm=True, mlm_probability=0.15, ) args = TrainingArguments( output_dir="checkpoints", per_device_train_batch_size=8, gradient_accumulation_steps=4, # 等效 batch 32,省显存 learning_rate=5e-5, # 从头训可以比微调大 num_train_epochs=3, warmup_steps=500, fp16=True, # 有 Tensor Core 的卡必开 logging_steps=50, save_steps=1000, save_total_limit=2, # 只留最近两个,省磁盘 report_to=[], ) trainer = Trainer( model=model, args=args, data_collator=collator, train_dataset=train_dataset, ) trainer.train() trainer.save_model("checkpoints/final") tokenizer.save_pretrained("checkpoints/final")参数上,gradient_accumulation_steps=4配合batch_size=8是 8GB 卡跑 768 隐藏维度的可行组合;显存更大的话直接加 batch 更划算,梯度累积会让 LayerNorm 的统计略有偏差。learning_rate=5e-5比微调高一个档,是从头训练需要更快更新权重的缘故。save_total_limit=2是实打实的经验,预训练动辄几十个 checkpoint,不限制的话磁盘两天就满。
训练完别急着接下游任务,先用 FillMask 管道做一次直觉检查:
from transformers import pipeline fill = pipeline("fill-mask", model="checkpoints/final", tokenizer="checkpoints/final") print(fill("the <mask> shall be governed by the laws of the state"))如果填出来的词全是高频虚词(the、of、and),说明模型只学到了词频分布,语料太少或者训练不充分;如果能填出contract、agreement这类领域词,表征基本可用了。这一步花不了两分钟,但能省掉后面接分类头发现效果还不如通用模型的时间。
4.4 生成侧的两个口径:T5 摘要前缀与 GPT-2 采样参数
书里把摘要和生成分成两套完全不同的用法。T5 走的是“文本到文本”的统一格式,所有任务都靠前缀区分:
from transformers import pipeline summarizer = pipeline("summarization", model="t5-large", tokenizer="t5-large", device=0) text = "..." # 长文档正文 result = summarizer( "summarize: " + text, # 前缀必须带,且冒号后有空格 max_length=180, min_length=60, truncation=True, # 超过模型最大长度时必须截断 do_sample=False, # 摘要默认贪心,结果稳定可复现 )"summarize: "这个前缀不是装饰,T5 在预训练阶段就是靠它区分任务的,漏掉会退化成续写。max_length和min_length控制输出区间,摘要任务上我一般按原文长度的 15% 到 25% 设,设太小会把关键条款切掉。
GPT-2 则完全靠采样参数控制风格,同一段 prompt 换一组参数,输出能从新闻体变成胡言乱语:
gen = pipeline("text-generation", model="gpt2-medium", device=0) out = gen( "The supplier shall", max_new_tokens=80, do_sample=True, top_p=0.92, # 核采样,只从累积概率 92% 的词里选 temperature=0.8, # 低于 1 收敛,高于 1 发散 repetition_penalty=1.15, # 抑制复读,1.0 表示不惩罚 num_return_sequences=3, )top_p和temperature不要同时大调,先把top_p定在 0.9 附近,再用温度微调。repetition_penalty超过 1.3 之后文本会变得别扭,代词和冠词被强行替换,读起来像机器翻译。这三个参数的关系是理解生成任务的入口,比记住任何一个具体数值都重要。
5. 避坑与排查:五个最容易翻车的点
5.1 显存看着空闲,跑起来却 OOM
现象:nvidia-smi显示只占了两三个 G,一开训练就抛CUDA out of memory。
原因通常有三个:上一个进程没退干净,显存被僵尸进程占着;max_length设得比预想大,注意力矩阵随长度平方增长;或者 batch 里混进了一条超长样本,动态 padding 时把整个 batch 撑大。排查顺序是先看进程,再看形状。
解决:torch.cuda.empty_cache()只能回收缓存,救不了僵尸进程,得用nvidia-smi找到 PID 直接 kill。长度上把padding="max_length"和固定max_length一起用,杜绝动态变化。显存实在紧张,开gradient_checkpointing=True,用算力换显存,速度大概降两成。
5.2 词表对不上,embedding 越界
现象:加载自己训的 tokenizer 配开源模型权重,报index out of range in self,或者预测结果里大量[UNK]。
原因:tokenizer 的vocab_size和模型 embedding 矩阵的行数不一致。自己训的词表五万二,模型是按三万词表初始化的,多出来的 id 找不到对应的向量。
解决:要么在 config 里把vocab_size设成和 tokenizer 一致再初始化模型,要么用model.resize_token_embeddings(len(tokenizer))扩表。扩表后新增的那些行是随机初始化的,必须再训一轮才能用,直接拿来推理等于给模型喂噪声。
5.3 标签和损失函数错配,loss 不降
现象:训练几个 epoch,loss 卡在 0.69 附近不动,正好是二分类的随机水平。
原因:num_labels设成 1,却用交叉熵;或者标签是 float 类型。前者模型只输出一个 logit,框架按回归处理;后者交叉熵要求 long 型整数标签,类型不对时框架可能静默转换,导致标签全被截断成 0。
解决:二分类用num_labels=2配交叉熵,或者num_labels=1配BCEWithLogitsLoss,两者不能混。加载数据后立刻打印labels.dtype和前十个值,确认是整数且类别分布合理。这一条检查我写进了模板,每次新建任务都跑一遍。
5.4 版本漂移,Trainer 参数直接报错
现象:照书里写的evaluation_strategy="epoch",运行报unexpected keyword argument。
原因:transformers 在 4.3x 之后把部分参数改名,evaluation_strategy变成eval_strategy,tokenizer相关参数也调整过位置。书里成文时的版本和现在装上的版本差了好几个大版本。
解决:优先降版本,把 transformers 锁在 4.2x 之前,一次性解决所有命名问题;如果非要用新版,就按报错逐个替换参数名,TrainingArguments的字段可以直接查当前版本的文档字符串。我的习惯是项目开始时就把版本写进requirements.txt并锁定,不给自己留“下次一定”的机会。
5.5 生成结果复读或者跑偏
现象:GPT-2 输出一段话之后开始循环,同一句反复出现;或者温度调高后语义完全散掉。
原因:贪心解码在开放生成上必然复读;温度过高会让低概率词被大量采样;top_p和temperature同时调大,两个随机性叠加,输出不可控。
解决:打开采样并设置top_p在 0.9 附近,temperature控制在 0.7 到 0.9,加repetition_penalty=1.1到1.2。判断标准不是“读起来像人话”,而是任务相关:做数据增强要多样性,做摘要要稳定,两者参数取向相反,别用一套配置打天下。
6. 把模型推到验收:SRL 兜底与一条可复用的检查链
模型训完只是开始,真正的分水岭在验收。书里语义角色标注那一章给了我一个很实用的思路:当抽取式问答或信息抽取的结果说不清对错时,用 SRL 把句子拆成“谁对谁做了什么”,再拿谓词和论元去核对答案是否落在合理位置。举个实际场景,从合同里抽“付款方”,分类模型给出一个实体,但它在句子里其实是个地点状语,SRL 一跑就能发现谓词和论元的关系对不上,直接把这条结果打回。
这套兜底不复杂,调用预训练 SRL 模型即可,关键是把它放进验收链而不是主链路,只在低置信度样本上触发:
| 验收项 | 通过标准 | 不通过时回看 |
|---|---|---|
| 词表一致性 | tokenizer 长度等于 embedding 行数 | 第 5.2 节 |
| 损失曲线 | 前 100 步稳定下降,无尖峰 | 第 3.3 节 |
| 指标口径 | MCC 与 F1 同时记录 | 第 3.4 节 |
| 生成稳定性 | 同输入三次结果语义一致 | 第 5.5 节 |
| 结构合理性 | SRL 谓词论元与抽取结果匹配 | 本章 |
我现在的固定动作是:任何模型上线前,先从验证集里抽五十条低置信度样本,过一遍 SRL 结构检查,人工只看这五十条。经验是这一步能捞出一半以上的系统性错误,比全量人工便宜得多,也比只看指标靠谱。从那以后我每次交付 NLP 模型,都强制走一遍“词表—损失—指标—生成—结构”这条五步链,缺一步都不敢签字。希望帮到你。
本文还有配套的精品资源,点击获取