资讯动态

Transformer NLP实战:编码器拆解、BERT微调与RoBERTa预训练

发布时间:2026/10/30 5:08:30 来源:尧图企业网站定制
简介《Transformers for Natural Language Processing》是一本围绕Transformer架构与自然语言处理实战展开的英文专著适合具备Python和深度学习基础、希望系统理解预训练模型并提升NLP项目能力的开发者与高年级学生阅读。资源包内仅含1个PDF文件大小约4.72MB便于在电脑或阅读器中随时查阅也可作为课程学习与项目实践的配套参考。目前已有1969人学习下载主题关注度较高。书中按三阶段推进先讲解原始Transformer、自注意力机制与小模型训练思路再过渡到BERT、RoBERTa、DistilBERT、GPT-2、T5等模型在自然语言理解与生成中的应用最后涉及社交网络数据分析、假新闻识别等进阶场景同时结合Hugging Face、Trax、AllenNLP等平台以及Python、TensorFlow、Keras在情感分析、文本摘要、语音识别和机器翻译中的实践并讨论模型性能、适用范围与生产限制帮助读者建立从原理到落地的完整知识框架。1. Transformers 与 NLP 的第一次真正落地「书买了、翻了三十页、卡在自注意力最后回去调参」——这是我见过最常见的情况。这份 Transformers for Natural Language Processing 的 PDF 不是又一本只讲原理的教材它的顺序是反的先把原始 Transformer 的编码器栈拆到每一层的张量形状再直接把 BERT 微调、RoBERTa 从头预训练、T5 摘要、GPT-2 续写这些活干一遍中间夹着 BLEU、MCC、SuperGLUE 这些「到底行不行」的定量口径。它适合已经能用 PyTorch 跑通一个分类 demo、但面对 transformers 库只会from_pretrained的人也适合带团队做 NLP 落地、需要一套能对上级解释「为什么选这个模型、参数为什么这么设」的熟手。后面几章我按资源里的技术栈走一遍代码都能直接抄。2. 拆开编码器栈位置编码与多头注意力的形状账很多人第一次读 Transformer 论文卡住的地方不是「注意力是什么」而是「这个张量的第二维到底是序列长度还是特征维度」。资源第一章把编码器栈切成六个部件顺序是输入嵌入、位置编码、多头注意力、残差加 LayerNorm、前馈网络、再一次残差加 LayerNorm。这六块里只有两块需要自己写剩下四块是模板化的胶水。2.1 六个部件与两条数据流先把数据流说清楚后面写代码才不会被 shape 绕晕。一条是主路(B, L, d_model)进来每经过一个子层形状都不变所以编码器可以堆 N 层而不用改任何维度配置。另一条是注意力内部的临时支路(B, L, d_model)被拆成(B, heads, L, d_k)算完再拼回(B, L, d_model)其中d_k d_model / heads。这个「进出一致」的设计是整份资源里最值得先记住的一点。它意味着你调层数、调头数、调序列长度都不需要动下游代码。常见的翻车是把heads设成不能整除d_model的数比如d_model768配heads10拆头时view直接报错。我在项目里一般会在构造函数里硬写一句assert d_model % heads 0让它早点炸而不是训练到第 200 步才炸。残差和 LayerNorm 的位置也有讲究。原始架构是 Post-LN也就是LayerNorm(x Sublayer(x))后来一大批模型改成 Pre-LN把 norm 放到子层前面。资源里对这两种结构的训练稳定性差异有专门讨论我的经验是层数少于 8 层时两者差别不大一旦堆到 12 层以上、又没有大规模 warmupPost-LN 很容易在头几千步出现 loss 尖刺。这不是玄学是梯度回传路径长度不同导致的。2.2 位置编码不加它模型就是个词袋自注意力本身对顺序完全不敏感。你把一句话的词序打乱注意力算出来的加权和只是换了个求和顺序结果一模一样。所以位置编码不是「锦上添花」是让模型能区分「狗咬人」和「人咬狗」的唯一信息来源。资源里用的是经典正弦编码偶数维走 sin奇数维走 cos频率按10000^(2i/d_model)递减。这个设计的妙处在于它不引入额外参数而且任意两个位置的编码可以通过线性变换互相表示模型理论上能学到「相隔 k 个位置」这种相对关系。下面是可直接用的实现import math import torch import torch.nn as nn class PositionalEncoding(nn.Module): def __init__(self, d_model512, max_len5000, dropout0.1): super().__init__() self.dropout nn.Dropout(dropout) pe torch.zeros(max_len, d_model) # 位置 x 维度 的空表 pos torch.arange(0, max_len).unsqueeze(1) # 每行一个位置序号 # 频率项第 2i 维用 10000^(-2i/d_model) div torch.exp(torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model)) pe[:, 0::2] torch.sin(pos * div) # 偶数维 sin pe[:, 1::2] torch.cos(pos * div) # 奇数维 cos self.register_buffer(pe, pe.unsqueeze(0)) # 不进梯度但跟着 .to(device) 走 def forward(self, x): # x: (B, L, d_model)截取前 L 个位置再加 return self.dropout(x self.pe[:, :x.size(1)])几个参数值得单独说。d_model必须和词嵌入维度一致否则加法直接报广播错误max_len决定你能处理的最长序列设 5000 意味着超过 5000 的位置没有编码实际项目里按数据集 99 分位长度往上留 20% 就够不要无脑开到 10000那张表会白占显存。register_buffer这句是关键很多人的模型在 CPU 上写完搬不到 GPU就是位置矩阵被当成了普通属性。2.3 多头注意力拆头、缩放、mask 三步多头注意力的代码只有十几行但每一步都有坑。核心是三个线性变换各自把(B, L, d_model)映射一遍然后 reshape 成多头形态做点积最后拼接再过一个输出投影。class MultiHeadAttention(nn.Module): def __init__(self, d_model512, heads8, dropout0.1): super().__init__() assert d_model % heads 0, d_model 必须能被 heads 整除 self.d_k d_model // heads self.heads heads self.w_q nn.Linear(d_model, d_model) self.w_k nn.Linear(d_model, d_model) self.w_v nn.Linear(d_model, d_model) self.w_o nn.Linear(d_model, d_model) self.dropout nn.Dropout(dropout) def forward(self, q, k, v, maskNone): B, L, _ q.shape # 拆头(B, L, d_model) - (B, heads, L, d_k) def split(w, x): return w(x).view(B, -1, self.heads, self.d_k).transpose(1, 2) q, k, v split(self.w_q, q), split(self.w_k, k), split(self.w_v, v) scores q k.transpose(-2, -1) / math.sqrt(self.d_k) # 缩放点积 if mask is not None: # mask 形状 (B, 1, 1, L)0 表示 padding 位置成极小值 scores scores.masked_fill(mask 0, -1e9) attn self.dropout(scores.softmax(dim-1)) out (attn v).transpose(1, 2).contiguous().view(B, L, -1) return self.w_o(out)逐段解释。split里的transpose(1, 2)把头维提到序列维前面这是为了让q k^T变成矩阵乘而不是逐元素乘。除以sqrt(d_k)是必须的d_k64时点积结果的方差随维度线性增长不缩放的话 softmax 会饱和成 one-hot梯度接近 0训练看起来「不动」很多人以为是学习率问题其实是漏了这一步。masked_fill用-1e9而不是-inf是为了避免整行全 mask 时出现nan。mask 的形状(B, 1, 1, L)是为了自动广播到(B, heads, L, L)一个 batch 共享同一套 padding 位置不需要为每个头复制一份。最后那个.contiguous()不能省view要求内存连续漏了在某些 PyTorch 版本上会直接抛错。2.4 把前向跑通形状对不上的四种报错写完两个模块先别急着堆 12 层。资源里的做法是先构造一个假输入把形状走通再谈训练这一步能省下大量排查时间。torch.manual_seed(0) x torch.randn(2, 16, 512) # 批 2序列 16维度 512 mask torch.ones(2, 1, 1, 16) # 全 1 表示没有 padding mask[:, :, :, 12:] 0 # 人为把后 4 个位置标成 padding h PositionalEncoding(d_model512)(x) h MultiHeadAttention(d_model512, heads8)(h, h, h, mask) print(h.shape) # 期望 torch.Size([2, 16, 512]) print(h[0, :, :3]) # 看 padding 位是否还有意义我踩过的四种报错按出现频率排一是size mismatch多半是d_model和词表嵌入维度不一致或者位置编码的d_model写死成默认 512 而模型是 768二是view size is not compatible就是前面说的漏了.contiguous()三是expected scalar type Float but found Half混用了 fp16 的位置矩阵和 fp32 的输入四是nan出现在 softmax 之后回去查 mask 是不是把整行都填了-1e9。最后提醒一点关于这个章节的定位。原始 Transformer 的实现在今天直接拿去训练几乎没有胜算因为预训练模型的权重已经把这些参数调好了。但你必须自己写一遍才知道from_pretrained加载进来的那几百兆权重每一个对应的是什么。后面几章调 BERT 的num_labels、调 RoBERTa 的mlm_probability、调生成任务的top_p心里才有底。3. BERT 微调流水线从 tokenizer 到 MCC 的七步资源第二章把 BERT 微调拆成七步顺序是激活 GPU、装 PyTorch 接口、导入模块、指定设备、加载数据集、构造句子与标签、分词与掩码、切分训练验证、转张量、建迭代器、配模型、载预训练权重、分组优化器、设超参、跑训练、评估。看着步骤多其实骨架就三段数据进得去、梯度传得回、指标读得准。这一章把三段分别写实。3.1 数据侧三件套input_ids、attention_mask、labelBERT 的输入不是「一句话」是三个张量。input_ids是词表索引attention_mask标记哪些位置是真词哪些是补齐的label是监督信号。这三个东西任何一个错了训练都能跑完但结果会诡异地差。import torch from transformers import BertTokenizer tokenizer BertTokenizer.from_pretrained(bert-base-uncased, do_lower_caseTrue) MAX_LEN 128 # 按数据集 99 分位长度取128 覆盖多数短文本分类场景 PAD_ID tokenizer.pad_token_id # 通常是 0 def encode(sentences, labels): ids, masks [], [] for s in sentences: enc tokenizer.encode_plus( s, max_lengthMAX_LEN, paddingmax_length, # 统一补齐免去批次内二次对齐 truncationTrue, # 超长直接截断别让单条样本撑爆显存 return_attention_maskTrue, ) ids.append(enc[input_ids]) masks.append(enc[attention_mask]) return (torch.tensor(ids, dtypetorch.long), torch.tensor(masks, dtypetorch.long), torch.tensor(labels, dtypetorch.long))paddingmax_length和paddingTrue的区别值得单独讲。前者把每条都补到 128后者补到本批最长。前者浪费一点显存但形状固定适合写死batch_size的常规训练后者省显存但每批形状不同在做序列级任务比如句对分类时更容易出问题。我一般先用max_length把流程跑通稳定之后再换成动态 padding 优化速度。MAX_LEN的取法也有讲究。直接拍 512 是最常见的新手动作结果显存翻三倍、训练时间翻两倍而实际数据里超过 128 个 token 的样本可能不到 5%。正确做法是先跑一遍分词统计长度分布取 99 分位如果长尾样本特别重要再做分段或滑动窗口而不是全局拉满。3.2 优化器分组weight decay 别乱加在 bias 和 LayerNorm 上这一步是很多人直接抄AdamW(model.parameters(), lr2e-5)就过去的但资源里专门强调了分组。原因是 weight decay 的作用是抑制权重变大而 bias 和 LayerNorm 的缩放参数本来就不该被压小——把它们一起衰减等于给模型加了一个不该有的约束。from transformers import BertForSequenceClassification model BertForSequenceClassification.from_pretrained( bert-base-uncased, num_labels2, # 二分类多分类直接改成类别数 ) 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}, # bias 与 LayerNorm不衰减 ] optimizer torch.optim.AdamW(optimizer_grouped, lr2e-5, eps1e-8)参数含义lr2e-5是 BERT 微调的经典起点全量微调时大于 5e-5 很容易把预训练学到的表示冲掉小于 1e-5 又常常在 3 个 epoch 内收敛不完eps1e-8是 AdamW 的默认护栏稀疏梯度场景可以提到 1e-6weight_decay0.01在数据量小于一万条时可以降到 0.001 甚至 0因为小数据上正则过强会让模型欠拟合。3.3 训练循环的四个旋钮lr、batch、clip、epoch训练循环本身的代码没几行但四个旋钮的相互关系决定了这次实验是「跑完」还是「跑对」。from torch.utils.data import TensorDataset, DataLoader, RandomSampler, SequentialSampler train_ids, train_masks, train_labels encode(train_texts, train_labels_raw) val_ids, val_masks, val_labels encode(val_texts, val_labels_raw) batch_size 16 train_loader DataLoader( TensorDataset(train_ids, train_masks, train_labels), samplerRandomSampler(range(len(train_ids))), # 每轮打乱 batch_sizebatch_size, ) val_loader DataLoader( TensorDataset(val_ids, val_masks, val_labels), samplerSequentialSampler(range(len(val_ids))), # 验证集不打乱 batch_sizebatch_size, ) device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) for epoch in range(3): model.train() for ids, masks, labels in train_loader: ids, masks, labels ids.to(device), masks.to(device), labels.to(device) optimizer.zero_grad() out model(input_idsids, attention_maskmasks, labelslabels) loss out.loss loss.backward() # 梯度裁剪防止个别 batch 把权重带飞 torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() print(fepoch {epoch} done)batch_size16在 128 序列长度、base 规模模型上大约占 6 到 8 GB 显存单卡 16 GB 可以拉到 32。显存不够时优先降 batch 而不是降MAX_LEN因为后者会直接丢掉信息。clip_grad_norm_的 1.0 是经验值做文本分类一般不会触发但一旦触发就说明这个 batch 的梯度异常裁掉比让它更新权重更安全。epoch3是老规矩小数据集上第 4 轮开始验证集指标往往就掉头了这时候早停比多训有用。3.4 用 MCC 而不是准确率收口评估这一步资源里强调了 Matthews 相关系数。原因很实际NLP 数据集普遍类别不平衡一个 95% 负样本的数据集模型全预测负类也能拿到 95% 的准确率但这个模型毫无价值。MCC 同时考虑了 TP、TN、FP、FN取值在 -1 到 1 之间0 表示和随机猜没区别。from sklearn.metrics import matthews_corrcoef, accuracy_score, f1_score model.eval() preds, golds [], [] with torch.no_grad(): for ids, masks, labels in val_loader: ids, masks, labels ids.to(device), masks.to(device), labels.to(device) logits model(input_idsids, attention_maskmasks).logits preds logits.argmax(dim-1).cpu().tolist() golds labels.cpu().tolist() print(Accuracy:, accuracy_score(golds, preds)) print(F1 :, f1_score(golds, preds, averagebinary)) print(MCC :, matthews_corrcoef(golds, preds))三个指标一起看能定位问题类型。准确率高但 MCC 低说明模型偏向多数类需要调类别权重或者重采样MCC 和 F1 都高但准确率一般说明少数类识别得不错、多数类有漏通常可以接受三个都低那就不是指标问题回去看数据里 label 和文本是不是对错了行——这个错误我见过不止一次尤其在做 CSV 拼接的时候。torch.no_grad()别漏不然验证阶段会一路建计算图显存越跑越少跑到第三个 epoch 突然 OOM然后你以为是模型太大。4. 从零预训练 RoBERTa分词器、配置与 FillMask 验证微调是「在别人调好的权重上改最后一层」预训练是「从随机初始化开始让模型自己学语言规律」。资源第三章用一个虚构的小语料把整个流程走了一遍从训练分词器到保存模型到用 FillMask 检验15 个步骤。这一章挑出真正决定成败的四段。4.1 先训分词器vocab_size 是最容易拍错的参数预训练第一个反直觉的点不能直接用现成的 BERT 分词器。如果你的语料是特定领域的法律条文、医学报告、某个行业的内部文档通用词表会把大量专业词切成碎片模型要花额外容量去重建这些词。资源里的做法是在自己的语料上重新训练一个字节级 BPE 分词器。from tokenizers import ByteLevelBPETokenizer paths [corpus.txt] # 一行一句的纯文本 tokenizer ByteLevelBPETokenizer() tokenizer.train( filespaths, vocab_size52000, # 词表大小小语料 30k 够用通用语料 50k 起步 min_frequency2, # 出现少于 2 次的子词不进词表抑制噪声 special_tokens[s, pad, /s, unk, mask], ) tokenizer.save_model(my-roberta) # 产出 vocab.json 与 merges.txtvocab_size是最容易拍错的。设太小常见词被拆成多个子词序列变长、显存吃紧设太大低频子词占满词表嵌入矩阵大部分参数永远得不到充分训练。我的经验公式是语料总 token 数在百万级时用 30k千万级用 50k再多可以考虑 64k但超过这个数收益递减得非常快。min_frequency2这一句能显著减少词表里的「孤词」尤其在爬取来的脏语料上。special_tokens的顺序和命名不要随意改。RoBERTa 系的模型默认mask是掩码位pad是补齐位如果你训分词器时把这两个顺序写反后面DataCollatorForLanguageModeling会往错的位置填 mask训练 loss 照样下降但模型学出来的是垃圾。这个坑很隐蔽因为不报错。4.2 显存三角层数、序列长度、批大小预训练的资源配置是个三角约束动一个另外两个必须让。资源里在配置模型之前专门插了一步「检查 GPU 和 CUDA 资源」这不是形式主义。from transformers import RobertaConfig, RobertaForMaskedLM config RobertaConfig( vocab_size52000, # 必须与分词器词表完全一致 max_position_embeddings514, # 512 2 个特殊位 hidden_size768, num_attention_heads12, num_hidden_layers6, # 完整版是 12 层先用 6 层验证流程 type_vocab_size1, # RoBERTa 不用句对类型 ) model RobertaForMaskedLM(configconfig) print(参数量:, sum(p.numel() for p in model.parameters()) / 1e6, M)先把参数量打出来再决定后面怎么配这个习惯能救命。6 层 768 维的模型大约 80M 参数单卡 16 GB 在block_size128、batch_size8下能跑如果堆到 12 层参数量翻倍激活值也翻倍同样的 batch 直接 OOM。资源里选择 6 层不是妥协是让读者能在一张消费级卡上把流程完整走一遍——流程走通了换大卡只是改数字。max_position_embeddings514这个数看着奇怪是因为 RoBERTa 的位置索引从 2 开始0 和 1 留给 padding 和特殊位实际可用序列长度还是 512。写 512 会在训练时报索引越界这是一个只在预训练阶段才会遇到的错。4.3 Trainer 与 data collatorMLM 的 15% 是怎么来的掩码语言模型的训练数据不需要人工标注collator 会自动挖空。这一步的配置比模型结构更影响最终效果。from transformers import (LineByLineTextDataset, DataCollatorForLanguageModeling, Trainer, TrainingArguments) dataset LineByLineTextDataset( tokenizertokenizer, file_pathcorpus.txt, block_size128, # 单条样本的最大 token 数与显存强相关 ) collator DataCollatorForLanguageModeling( tokenizertokenizer, mlmTrue, mlm_probability0.15, # 掩码比例原论文值低于 0.1 学得慢高于 0.2 上下文不够 ) args TrainingArguments( output_dirmy-roberta/out, overwrite_output_dirTrue, num_train_epochs3, per_device_train_batch_size8, save_steps500, # 小语料上 checkpoint 存太密会把盘塞满 save_total_limit2, # 只留最近两个 prediction_loss_onlyTrue, logging_steps50, ) trainer Trainer(modelmodel, argsargs, data_collatorcollator, train_datasetdataset) trainer.train() trainer.save_model(my-roberta/out) # 权重 配置一起存方便后续加载mlm_probability0.15是原论文的值背后是个权衡掩得太少模型看到的上下文太多任务太简单学不到东西掩得太多上下文信息不足模型退化成猜词。15% 在大多数语料上是个稳的起点。block_size128和batch_size8的组合是显存和效果的折中语料本身句子很短的话可以考虑把多句拼进一个 block减少 padding 浪费。save_total_limit2这一句是血泪经验。有次跑一个 200 万行的语料save_steps100两天下来 checkpoint 占了 40 GB磁盘写满之后训练进程直接挂掉前面白跑。4.4 FillMask 验证loss 降了不代表模型能用训练日志里 loss 从 8 降到 3看着很漂亮但这只说明模型在「猜被挖掉的词」这个任务上变好了不代表它学到了语义。资源里最后一步用 FillMask 管道做人工检查我觉得这一步比看 loss 曲线重要得多。from transformers import pipeline fill pipeline( fill-mask, modelmy-roberta/out, tokenizermy-roberta/out, # 分词器必须和训练时是同一个 ) print(fill(the mask of the city was rebuilt after the war)) print(fill(the contract shall be governed by the laws of the mask))判断标准不是「填得对不对」而是「填出来的词在词性和语义范畴上合不合理」。如果第一句填的是economy、infrastructure、center说明模型学到了名词位置的分布如果填的是the、and、of这类功能词说明它只学到了局部共现语料量或者训练步数不够。第二句是领域测试法律语料训出来的模型应该填地名或法域词填出cat就说明语料太杂或者词表没训好。我一般会准备五个不同领域的探针句模型训完先跑这五句全都离谱就直接回去查分词器别浪费算力继续训。这一步五分钟能省下半天。5. 避坑五个能让实验白跑一天的常见故障下面这五条都是我在照着类似流程复现时真撞过的按「现象 → 原因 → 解决」写不按严重程度排。5.1 分词器与语料词表错配现象微调后验证集指标比随机猜还差把input_ids反解回文本发现句子里大段[UNK]。原因用了从头训练的分词器但加载的却是通用预训练权重两边的词表索引完全对不上。模型看到的 id 序列在它眼里是乱码等于在用随机噪声做输入。解决分词器和模型必须成对。要么都用官方的bert-base-uncased要么都用自己训的my-roberta。如果一定要混用比如用自己的分词器配官方权重必须做词表对齐把重叠的 token 映射过去、其余随机初始化并且重新预训练一轮让新词嵌入收敛。做这一步之前先想清楚值不值。5.2 padding 侧与 attention mask 错位现象单条推理结果正常批量推理时短句的预测开始漂移同一句话放在不同 batch 里结果不一样。原因BERT 系是左侧 paddingpadding_sideleftGPT 系是右侧如果手动拼 batch 时 padding 方向写反而 mask 又按另一个方向生成模型会把补齐位当成真实 token。更隐蔽的是自己手写 mask 时把1和0的含义搞反——attention_mask里 1 表示有效、0 表示补齐。解决不要手写 mask一律用tokenizer.encode_plus的返回值。如果必须手拼先打印一条样本的input_ids和attention_mask对照着看确认[PAD]位置在 mask 里是 0。批量推理前先做一次「单条 vs 批量」的一致性检查两条路径结果差异超过 1e-3 就说明对齐有问题。5.3 显存溢出与「假 batch」现象训练到第二个 epoch 中途报CUDA out of memory但第一个 epoch 跑得好好的。原因验证阶段忘了torch.no_grad()计算图一直累积或者验证集的DataLoader复用了训练集的batch_size而验证时某些样本特别长动态 padding 把这一批撑大了。解决验证和推理一律包torch.no_grad()或torch.inference_mode()。显存紧张时用梯度累积模拟大 batchper_device_train_batch_size4配gradient_accumulation_steps4等效 batch 16显存占用按 4 算。注意学习率要按等效 batch 调别用累积之后还按小 batch 的 lr 跑那样收敛会明显变慢。5.4 预训练 loss 漂亮但生成胡话现象MLM loss 稳定降到 2 以下FillMask 输出却全是高频功能词或者同一个词反复出现。原因三种可能。语料太小几十万 token 撑不起一个 80M 参数的模型训练步数不够loss 还在快速下降期就停了或者掩码比例设得不对导致任务太简单。还有一种是自己训的分词器vocab_size开得太大低频子词占了一半词表实际有效词表很小。解决先看 loss 曲线是否进入平台期没进平台期就继续训再看语料规模经验上参数量与 token 数的比例在 1:20 以上比较稳80M 参数至少要有 1.6B token不够就减层数或者加语料。最后检查分词器vocab.json里翻一遍如果有大量只出现一次的怪字符串说明语料清洗没做好。5.5 指标读反BLEU 高不等于译文好现象机器翻译任务 BLEU 从 22 提到 28看起来提升明显但人工读译文发现句式更僵硬、专有名词错得更多。原因BLEU 基于 n-gram 精确率对短句和高频片段有偏好。模型学会多输出「安全」的常见短语就能把分数刷上去而这些短语恰好是参考译文的常见搭配。再加上分词粒度不同按词 vs 按子词不同实现算出来的 BLEU 根本不可比。解决BLEU 只作为回归检测用固定同一套分词和同一个脚本只比较自己前后两次的结果。评估要配人工抽查每次随机抽 50 条读一遍重点看数字、专有名词、否定词有没有翻错。资源里对平滑技术Chencherry 等有专门讨论短句多的时候不加平滑BLEU 会直接算成 0看着像模型崩了其实是评估脚本的问题。6. 选型与验证把书里的模型清单压成一张表书里出现的模型和任务很多直接照着顺序做容易迷失。我把它压成一张按「任务 → 模型族 → 看什么指标 → 数据量门槛」组织的表实际开工前先在这张表上定位比从头翻目录快。任务起步模型主指标数据量门槛备注短文本分类BERT-baseMCC / F1每类 500 条以上类别不平衡必看 MCC情感分析RoBERTa-large 或 DistilBERT准确率 人工抽查千级蒸馏版推理快精度掉 1 到 2 个点文本摘要T5 系 text-to-textROUGE 人工万级输入长度是主要瓶颈机器翻译原始 Transformer / TraxBLEU固定脚本十万句对以上短句必须加平滑语义角色标注BERT 系序列标注论元 F1万级标注成本高优先用现成权重问答检索RoBERTa 检索框架Top-k 命中率千级文档先跑通检索再谈生成从头预训练RoBERTa 小配置FillMask 人工探针千万 token 以上低于这个量别碰表里最值得说的是最后两行。「语义角色标注」和「问答检索」在书里占了整整两章但它们对标注数据的依赖远超分类任务没有现成标注就先用预训练权重跑推理别一上来就想微调。而「从头预训练」这一行我的建议是把它当成理解流程的训练不要指望小语料训出来的模型能上生产——它最大的价值是让你在调别人模型的时候知道每个超参在动什么。验证方法我固定用三层。第一层是形状与流程验证就是第 2 章那种假输入跑通五分钟内必须过。第二层是过拟合验证拿 100 条样本训到 loss 接近 0如果连这 100 条都拟合不了问题一定在数据管道而不是模型容量。第三层是小样本泛化验证从完整训练集里切 10% 训一次看指标离全量训练差多少差太多说明数据分布不均得回去看采样。这套三层验证我现在的习惯是写进脚本每次换模型或者换数据集跑一遍再开始正式训练。有一次赶进度跳过了第二层结果在一个标签错位的 CSV 上训了一整天指标一直在 0.5 附近晃我以为是模型不够大换了两版配置才发现是label列和text列错开了一行。从那以后我每次动数据管道都强制走一遍这 100 条过拟合的检查哪怕它看起来很傻。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑