简介:《Transformers for Natural Language Processing》是Packt于2021年推出的NLP进阶电子书,面向已具备Python基础、希望系统掌握Transformer架构的开发者与算法学习者。全书按三个阶段展开:先剖析自注意力机制与原始Transformer的训练方法,说明小型模型在特定任务上可胜过GPT-3的缘由;再过渡到BERT、RoBERTa、DistilBERT、GPT-2、T5等预训练模型在自然语言理解与生成任务中的应用;最后延伸到社交网络数据优化、假新闻识别等高级场景,并演示Hugging Face、Trax、AllenNLP平台以及TensorFlow、Keras在情感分析、文本摘要、语音识别、机器翻译中的实践,同时讨论如何衡量关键模型的性能边界。压缩包内为1个PDF文件,约4.72MB,排版完整便于离线阅读与检索。目前已有1969人学习,适合作为从原理到落地的系统参考。
1. 从一份 300 页 PDF 说起:它到底能帮你解决什么
同事把一份三百多页的 PDF 甩给我,说里面有一章讲 BERT 微调的完整代码,让我把项目里的情感分类从 TF-IDF 换掉。翻开目录才发现,这份《Transformers for Natural Language Processing》并不是只讲注意力公式的教材:它从原始 Transformer 的编码器堆栈一路写到 BERT、RoBERTa、T5、GPT-2,每一章都落到能跑的 Python 代码上,连 WMT 机器翻译的 BLEU 平滑、SuperGLUE 的评测口径都写进去了。
适合谁?手上会点 PyTorch 或 TensorFlow、被具体 NLP 任务卡住、想直接拿预训练模型改自己数据集的开发者。不适合谁?完全没写过训练循环、指望复制粘贴就出活的人。中间那些张量维度对齐、显存不够、分词器和数据集对不上的坑,书里给了提示,但真得自己踩一遍才知道疼。
2. 三阶段路线与环境搭建:从目录结构到本地能跑的训练环境
这份资源的组织方式很清晰,前四章打地基,中间四章做任务,最后四章攻高级理解。读之前先想清楚自己落在哪一段,比从头翻到尾高效得多。
2.1 十二个章节怎么分成三段
第一段是架构认知,含原始 Transformer 的编码器堆栈、输入嵌入、位置编码、多头注意力、前馈子层、解码器堆栈和 Post-LN 的顺序;接着落到 BERT 的微调流程和从零预训练一个小 RoBERTa 的十五个步骤。这一段的目标不是背公式,是让你能对着张量形状说清楚每一步在干什么。
第二段是 NLU 与 NLG 的实战:下游任务评测体系(从 GLUE 到 SuperGLUE,含 BoolQ、CB、MultiRC、ReCoRD、RTE、WiC、WSC 这些子任务)、WMT 数据集预处理与 BLEU 评测、GPT-2 的文本补全与语言模型训练、T5 的文本摘要。这一段最像工程活,数据清洗占的时间比模型多。
第三段是高级语言理解:分词器与数据集的匹配(Word2Vec 分词的六种边界情况)、基于 BERT 的语义角色标注、用 NER 和 SRL 反推问答对、情感分析模型横评(RoBERTa-large、DistilBERT、MiniLM、mnli 版本、多语种版本)、假新闻识别的分析路线图。这一段开始讲“怎么判断模型结论靠不靠谱”,含金量比前面高。
我的建议是:如果你只想把预训练模型接到自己的业务上,2、3、4 章加第 11 章的情感分析就够开工;想做检索问答或者文档摘要,再补第 7、9、10 章。
2.2 环境:两套后端共存别硬来
书里前后用了 PyTorch 和 TensorFlow 两套后端,还夹着 Trax 和 AllenNLP。最省事的做法不是装一个大杂烩环境,而是按章节拆环境变量、拆虚拟环境。我一般会建一个主环境跑 PyTorch 部分,另一个单独跑 Trax 那章的翻译实验,因为 Trax 对 jax 版本很挑,混装之后 torch 那边的 CUDA 又玄学失效,排查起来很难受。
# 1) 独立环境,Python 3.9 是这套库兼容性比较稳的区间 conda create -n nlp-tf python=3.9 -y && conda activate nlp-tf # 2) 先看驱动支持到哪一档 CUDA,再决定装哪个 torch 轮子 nvidia-smi # 右上角 CUDA Version 是上限,不是你必须装的版本 # 3) torch 从官方 whl 索引装,别直接 pip install torch 拿到 CPU 版 pip install torch torchvision # 4) 这三件套必须锁版本,transformers / datasets / tokenizers 错配是报错重灾区 pip install "transformers>=4.10,<4.20" "datasets<2.0" "tokenizers<0.11" pip install scikit-learn pandas matplotlib装完先跑一段自检,确认 CUDA 真的可用,别等到训练脚本跑了两小时才发现全程在 CPU 上磨。
import torch, transformers # 三个值要一起看:torch 版本、CUDA 是否可用、torch 编译时的 CUDA 版本 print("torch:", torch.__version__) print("cuda available:", torch.cuda.is_available()) print("cuda build:", torch.version.cuda) print("transformers:", transformers.__version__) # 能打印出卡型号才算真通 if torch.cuda.is_available(): print(torch.cuda.get_device_name(0)) print("显存 GB:", torch.cuda.get_device_properties(0).total_memory / 1024**3)这里三个输出要一起看。torch.cuda.is_available()为 True 但torch.version.cuda是 None,说明装的是 CPU 轮子;两个都不为空但训练时报 CUDA error,多半是驱动版本低于 torch 编译时的 CUDA 版本,降 torch 比升级驱动省事。显存那行决定你后面 batch size 能开多大,8G 卡上 BERT-base 微调 batch 开 8 就打满,32G 卡上同样的脚本能开到 32 甚至更多。
2.3 目录约定:别让各章互相污染
书里每个案例都是独立脚本,直接堆在一个目录里,跑到第三章就会出现vocab.json被覆盖、checkpoint 找不到的情况。我按章拆目录,原始语料只读,产物统一进 runs。
# 一本书一个工作区,按章隔离输入输出 nlp-transformers/ ├── ch02_bert_finetune/ # 微调 BERT:数据脚本 + 训练脚本 ├── ch03_roberta_scratch/ # 从零训练分词器与 RoBERTa ├── ch05_translation/ # WMT 预处理 + BLEU 评测 ├── ch07_t5_summary/ # T5 文本摘要 ├── data/ # 原始语料,只读,任何脚本都不许往里写 └── runs/ # checkpoint、日志、预测结果,按时间戳分目录这么拆的好处是回滚成本低:某一章的依赖炸了,删掉那个目录重来即可,不会牵连已经跑通的部分。原始语料只读这一条尤其重要,书里好几个预处理脚本默认是原地覆盖的,一旦跑错参数,原始数据就被截断了,补救代价很大。
3. 核心模块拆解:位置编码、多头注意力与残差连接的逐行实现
第一章讲架构时给的是图和公式,真正要动手,得把这两块写成能跑通的nn.Module。这一章我把书里拆开讲的三个部件按代码重新组织一遍,顺手补上几个书里没强调但会直接导致训练失败的细节。
3.1 输入嵌入与位置编码:为什么不能只靠词向量
Transformer 本身是置换不变的,同一句话打乱词序,自注意力的输出集合是一样的。位置编码就是把顺序信息显式塞进去。原始论文用的是正弦余弦的固定编码,好处是不需要训练、可以外推到比训练集更长的序列;BERT 这类模型改成了可学习的位置嵌入,简单但受max_position_embeddings硬限制。
import math import torch import torch.nn as nn class PositionalEncoding(nn.Module): def __init__(self, d_model: int, max_len: int = 5000, dropout: float = 0.1): super().__init__() self.dropout = nn.Dropout(dropout) # pe 形状 [max_len, d_model],偶数维用 sin,奇数维用 cos pe = torch.zeros(max_len, d_model) pos = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) # [max_len, 1] div = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] = torch.sin(pos * div) pe[:, 1::2] = torch.cos(pos * div) # register_buffer:不算参数,但会跟着 .to(device) 一起搬,也不会进 optimizer self.register_buffer("pe", pe.unsqueeze(0)) # [1, max_len, d_model] def forward(self, x): # x: [batch, seq_len, d_model],直接相加,不改变形状 x = x + self.pe[:, : x.size(1)] return self.dropout(x)register_buffer这行是重点。用普通属性存self.pe = pe,模型to("cuda")之后这个张量还在 CPU 上,前向立刻报 device mismatch;用nn.Parameter又会被优化器当成可训练参数,白白多一份梯度。d_model必须是偶数,否则0::2和1::2切出来的列数对不上,直接抛 shape 异常。max_len要给足,输入超过它就会静默截断到缓冲区长度,序列尾部的词直接丢失顺序信息。
3.2 多头注意力:一次线性变换切出 Q/K/V
多头注意力的本质是把d_model维空间切成 h 份,每份独立算一次缩放点积注意力,再拼回来过一层输出投影。书里给的实现是三个独立Linear,我在实际项目里会合并成一个Linear(d_model, 3 * d_model)再chunk,少两次 kernel launch,小模型上能省 5% 左右的训练时间。
class MultiHeadAttention(nn.Module): def __init__(self, d_model: int = 512, n_heads: int = 8, dropout: float = 0.1): super().__init__() assert d_model % n_heads == 0, "d_model 必须能被 head 数整除" self.d_k = d_model // n_heads self.n_heads = n_heads self.w_qkv = nn.Linear(d_model, d_model * 3) # 一次算出 Q、K、V self.w_o = nn.Linear(d_model, d_model) # 多头拼接后的输出投影 self.dropout = nn.Dropout(dropout) def forward(self, x, mask=None): b, s, _ = x.shape q, k, v = self.w_qkv(x).chunk(3, dim=-1) def split_heads(t): # [b, s, h*d_k] -> [b, h, s, d_k],把 head 维提到前面 return t.view(b, s, self.n_heads, self.d_k).transpose(1, 2) q, k, v = split_heads(q), split_heads(k), split_heads(v) # 缩放点积:除以 sqrt(d_k) 防止维度变大时点积方差爆炸 scores = q @ k.transpose(-2, -1) / math.sqrt(self.d_k) # [b, h, s, s] if mask is not None: # mask 里 1 表示可见,0 表示屏蔽;用 -1e9 而不是 -inf,避免 softmax 出 nan scores = scores.masked_fill(mask == 0, -1e9) attn = self.dropout(torch.softmax(scores, dim=-1)) out = (attn @ v).transpose(1, 2).contiguous().view(b, s, -1) return self.w_o(out), attn几个容易翻车的参数:mask的语义必须全局统一,书里编码器用 0/1 的 padding mask,解码器还要叠一个上三角的 causal mask,两处语义搞反了模型照样能训,只是效果差一截,很难定位。-1e9这个常量比float("-inf")安全,整行全被 mask 时 softmax 不会产生 NaN。transpose之后必须contiguous()再view,不然 PyTorch 会直接报内存不连续的错误。
3.3 残差、LayerNorm 与 Post-LN 的顺序问题
子层之后的Add & Norm顺序看着是小事,实际决定了训练稳定性。原始论文用的 Post-LN:先残差相加,再过 LayerNorm;后来的很多大模型改成了 Pre-LN:先 LayerNorm 再进子层,残差直连到底。两者的差别在深层网络上非常明显,Post-LN 需要 learning rate warmup 才能训得动,Pre-LN 可以省掉 warmup 但收敛点略有不同。
class Sublayer(nn.Module): """一个可复用的子层:Pre-LN 还是 Post-LN 由 post_ln 开关控制""" def __init__(self, d_model, layer, dropout=0.1, post_ln=True): super().__init__() self.layer = layer self.norm = nn.LayerNorm(d_model) self.dropout = nn.Dropout(dropout) self.post_ln = post_ln def forward(self, x, *args, **kwargs): if self.post_ln: # 先残差再归一化:需要 warmup,但论文原始设定 return self.norm(x + self.dropout(self.layer(x, *args, **kwargs))) # 先归一化再进子层:深层更稳,省掉 warmup 也能跑 return x + self.dropout(self.layer(self.norm(x), *args, **kwargs))我一般默认用 Post-LN 复现书里的实验,保证结果能和书中对照;真正要堆到 12 层以上、又不想调 warmup 的时候切 Pre-LN。nn.LayerNorm的eps默认 1e-5,BERT 用的是 1e-12,做逐层对照实验时这个值要跟着改,不然数值差异会被误判成实现错误。
4. BERT 微调与 RoBERTa 从零预训练:两条链路的对照
这一章是全书最值得抄的两段代码。一段是拿预训练权重做下游微调,另一段是从零训练分词器再预训练一个小模型。两段的输入输出完全不同,但骨架高度相似:准备张量、配优化器、写循环、评估、存盘。
4.1 从原始句子到张量:分词与 attention mask
BERT 的输入不是一串 token,而是三件套:input_ids、attention_mask、labels。第一件是词表索引,第二件告诉模型哪些位置是真实 token、哪些是补齐的 pad,第三件是任务的监督信号。书里处理的是一个二分类数据集,句子和标签分别存两个列表,再统一转张量。
import torch from transformers import BertTokenizer, BertForSequenceClassification tokenizer = BertTokenizer.from_pretrained("bert-base-uncased") sentences = ["the plot was flat but the acting saved it", "a complete waste of two hours"] labels = [1, 0] # padding 到固定长度,超长截断;return_tensors="pt" 直接给张量,省一步转换 enc = tokenizer(sentences, padding="max_length", truncation=True, max_length=64, return_tensors="pt") input_ids = enc["input_ids"] # [batch, 64] attention_mask = enc["attention_mask"] # 1=真实 token,0=pad labels = torch.tensor(labels) # 分类标签,二分类用 0/1max_length要根据数据分布定。书里用的是短句数据集,64 足够;换成评论或合同条款,64 会把大半句子截掉,模型只看前半段当然学不到东西。判断方法很简单,对全量数据跑一遍 tokenizer 的encode拿长度,看 95 分位在哪,取那个值向上取整到 8 的倍数,兼顾显存和覆盖率。attention_mask交给模型之后,pad 位置的注意力权重会被压掉,这一列千万别自己手动造,直接取 tokenizer 的输出。
4.2 优化器分组与训练循环:bias 不做 weight decay
这一步是整段微调里最容易被抄漏的。BERT 微调的标准做法是分层设置 weight decay:权重矩阵做衰减,bias 和 LayerNorm 的参数不做。这样在中小数据集上泛化会稳一些,尤其是样本量几百条的冷启动场景。
from transformers import AdamW model = BertForSequenceClassification.from_pretrained( "bert-base-uncased", num_labels=2) # 分组:bias 和 LayerNorm 不衰减,其余权重衰减 no_decay = ["bias", "LayerNorm.weight"] optimizer_grouped_parameters = [ {"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}, ] # 微调学习率通常在 2e-5 到 5e-5,比从零训练小两个数量级 optimizer = AdamW(optimizer_grouped_parameters, lr=2e-5, eps=1e-8)训练循环本身没什么特别的,但有两行值得单独说:梯度裁剪和model.zero_grad()的位置。梯度裁剪能救回那些因为单条异常样本导致 loss 尖峰、直接训崩的批次;zero_grad放在反向传播之前还是之后都行,但要在同一次迭代内保持固定,混着写就会出现梯度累积。
from torch.utils.data import TensorDataset, DataLoader, RandomSampler train_data = TensorDataset(input_ids, attention_mask, labels) train_loader = DataLoader(train_data, sampler=RandomSampler(train_data), batch_size=8) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model.to(device) for epoch in range(3): model.train() for batch in train_loader: batch = [t.to(device) for t in batch] model.zero_grad() outputs = model(input_ids=batch[0], attention_mask=batch[1], labels=batch[2]) loss, logits = outputs[0], outputs[1] loss.backward() # 裁剪到 1.0,防止个别长句把梯度顶飞 torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step()batch_size=8是 8G 显存上的保守值,卡好的话开到 16 甚至 32 收敛更稳;但如果同时把max_length拉到 256,显存占用是平方级上升的,先降 batch 再降长度。三个 epoch 对微调通常够,再多容易过拟合,尤其训练集只有几千条的时候。
4.3 评估:为什么书里主推 MCC 而不是准确率
书里在第 2 章和第 4 章反复用 Matthews 相关系数(MCC),原因在类别不均衡的数据上非常直观。假设正样本只占 5%,模型全预测为负,准确率也有 95%,看着很美,实际一个正样本都没抓到;MCC 会直接掉到 0 附近,因为它是混淆矩阵四个格的综合指标,正类和负类任何一边塌了都会被惩罚。
from sklearn.metrics import matthews_corrcoef, accuracy_score, f1_score # preds 是 argmax 后的 0/1 列表,labels 是真实标签 print("accuracy:", accuracy_score(labels, preds)) # 不均衡时容易虚高 print("f1:", f1_score(labels, preds, average="binary")) # 只看正类 print("mcc:", matthews_corrcoef(labels, preds)) # 两边都看,-1 到 1评估集要单独切一份 holdout,别拿验证集反复调到最优再报那个分数。书里的流程是先按批次记录分数,最后在全量 holdout 上算一次总分,这个顺序值得照做:批次分数波动大,只看最后一个批次很容易误判收敛情况。
4.4 从零预训练一个小 RoBERTa:分词器先行
第 3 章那十五步是全书最完整的一条流水线,我按依赖关系重新排一遍,核心就三段:训分词器、配模型、扔进 Trainer。
分词器必须先在目标语料上训练,否则通用词表切你的领域文本会碎成单字符。这一步产出两个文件:vocab.json和merges.txt。
from tokenizers import ByteLevelBPETokenizer # 语料一行一句,纯文本即可 tokenizer = ByteLevelBPETokenizer() tokenizer.train(files=["data/corpus.txt"], vocab_size=52000, # 小语料 3 万到 5 万足够,太大反而稀疏 min_frequency=2, special_tokens=["<s>", "<pad>", "</s>", "<unk>", "<mask>"]) tokenizer.save_model("./roberta-small") # 落盘 vocab.json + merges.txt词表大小是个权衡:5 万多是 RoBERTa 的量级,语料只有几 MB 时开这么大,很多 token 只出现一两次,嵌入层学不动;我一般在百万字符级语料上开 3 万,千万级再上 5 万。min_frequency设成 2 能过滤掉大量只出现一次的词,减少无效词表。
模型配置里vocab_size必须和分词器完全一致,max_position_embeddings要比实际最大长度多留几个位置(RoBERTa 系惯例是max_len + 2),少一个都会在训练中途抛索引越界。
from transformers import (RobertaConfig, RobertaTokenizer, RobertaForMaskedLM, DataCollatorForLanguageModeling, Trainer, TrainingArguments) config = RobertaConfig( vocab_size=52000, # 必须等于分词器的词表大小 max_position_embeddings=514, # 最大长度 512 + 2 个特殊位 num_attention_heads=12, num_hidden_layers=6, # 从零训练用小层数,先跑通再加 type_vocab_size=1, # RoBERTa 没有句子对任务,就是 1 ) tokenizer = RobertaTokenizer.from_pretrained("./roberta-small", max_len=512) model = RobertaForMaskedLM(config=config) # MLM 的 collator 会自动做 15% 的随机遮盖,不用自己写 collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=True, mlm_probability=0.15) args = TrainingArguments( output_dir="./runs/roberta-small", overwrite_output_dir=True, num_train_epochs=5, per_device_train_batch_size=16, save_steps=500, save_total_limit=2, # 只留最近两个 checkpoint,不然磁盘很快满 ) trainer = Trainer(model=model, args=args, data_collator=collator, train_dataset=dataset) trainer.train() trainer.save_model("./runs/roberta-small/final") # 权重 + 配置 + 分词器一起存训完用FillMaskPipeline验证是最快的手段:扔一句带<mask>的话进去,看补出来的词是否在语义上合理。如果补出来全是高频虚词,说明训练轮数不够或者语料太单一,跟代码 bug 关系不大。
5. 避坑与排查:显存、分词器、版本这五类翻车现场
这一章全是真踩过的坑,按“现象 → 原因 → 解决”写,方便你直接拿去对照日志。
5.1 训练到一半 CUDA out of memory
现象:脚本刚开始跑正常,跑到第几十个 batch 突然 OOM,重跑位置还不固定。
原因:多数是变长输入没有统一 padding,某个批次恰好碰上一批长句,显存峰值被顶穿;也可能是评估阶段没加torch.no_grad(),中间激活值全留着。
解决:数据侧统一padding="max_length",别用longest动态补齐;评估和推理一律套with torch.no_grad():;训练前先torch.cuda.empty_cache(),但别指望它解决根本问题。真要压显存,开梯度累积把等效 batch 拉上去,比硬降 batch 温和。
5.2 分词器加载成功,但输出 id 全在词表外
现象:model(input_ids=...)抛 index out of range,或者补出来的 token 全是<unk>。
原因:分词器和模型权重不是同一套。从零训练的 RoBERTa 用自建分词器,如果配置文件里的vocab_size和分词器的实际词表对不上,或者加载时把路径指到了默认的通用分词器,就会错位。
解决:训练前打印len(tokenizer)和config.vocab_size对比,两个数必须相等;save_pretrained时把 tokenizer 一起存进同一个目录,加载时也从同一个目录取,不要一个从本地取、一个从远端拉。
5.3 装完就是跑不了,报 CUDA driver version is insufficient
现象:torch.cuda.is_available()返回 True,但一执行前向就崩,报驱动版本不足。
原因:torch 编译时用的 CUDA 版本高于驱动支持的版本。nvidia-smi显示的是驱动上限,torch.version.cuda显示的是 torch 自带的运行时,两者不匹配就会在第一次分配显存时炸。
解决:优先降 torch 版本去匹配驱动,而不是动驱动,尤其在共用服务器上,升级驱动会影响别人的环境。装之前先看nvidia-smi右上角那一行,再决定装哪个索引下的 whl。
5.4 loss 不降,或者降到某个值就不动了
现象:前几十步 loss 从 2.3 掉到 0.8,然后一条直线,验证集准确率和瞎猜差不多。
原因:微调学习率开太大(比如直接用了 1e-3),预训练权重被冲掉;或者标签编码错了,labels里混进了 -1 或者字符串;还有一种情况是数据里正负样本几乎全是一类,模型学到的就是常数输出。
解决:微调学习率从 2e-5 起试,配合 10% 的 warmup;训练前labels.unique()打一下,确认只有预期内的取值;labels.float().mean()看下正样本比例,低于 5% 就换 MCC 或 F1 做监控指标,别盯 accuracy。
5.5 保存的模型换台机器就加载不了
现象:本地save_pretrained之后拷到另一台机器,from_pretrained报缺文件或配置不匹配。
原因:只存了pytorch_model.bin,没存config.json和分词器文件;或者存的时候用了自定义的 head 结构,加载时用的是默认BertModel,键名对不上。
解决:统一用trainer.save_model(dir)加tokenizer.save_pretrained(dir),一个目录里放全套;自定义 head 的话,加载时先用AutoConfig读出配置,再用你自己的类实例化,别指望from_pretrained猜出你的结构。加载报 missing keys 时先看是不是classifier.*前缀,那通常是正常的(新初始化的分类头),unexpected keys 才要警惕。
6. 二次开发:把书里的示例改成自己的任务流水线
照着书跑通只是第一步,真正省时间的是把那些脚本压成一条能复用的流水线。我的做法是固定四层:数据层、模型层、训练层、验证层,换任务时只动前两层。
| 层 | 固定下来的东西 | 换任务时要改的 |
|---|---|---|
| 数据层 | 读取、切分、张量化、DataLoader | 分词器、max_length、标签映射 |
| 模型层 | AutoConfig+AutoModelFor* | 模型名、num_labels、是否冻结底层 |
| 训练层 | 优化器分组、warmup、梯度裁剪、checkpoint 策略 | 学习率、epoch、batch |
| 验证层 | 指标函数、holdout 评估、错误样本导出 | 指标(MCC / F1 / BLEU / ROUGE) |
具体改造时有两个技巧。第一个是先冻结再解冻:拿到一个新数据集,头一版只训分类头,把主干requires_grad=False,几个 epoch 就能看出数据本身有没有问题,避免一上来全量微调、跑两小时才发现标签是错的。第二个是把pipeline当回归测试用,每次改完数据处理逻辑,扔十条人工标注过的样本进去,看输出是否还符合预期,比盯着 loss 曲线直观得多。
from transformers import pipeline # 换任务后先拿它做冒烟测试,别急着上完整训练 clf = pipeline("text-classification", model="./runs/my-task/final", tokenizer="./runs/my-task/final", device=0) samples = ["这句话是正面的", "完全不行,浪费钱"] for s in samples: print(clf(s, truncation=True, max_length=128))这里truncation=True一定要显式给,pipeline默认不截断,遇到超长输入会直接抛异常;max_length要和训练时保持一致,不然分词边界不同,预测分布会漂。
从那以后我每次接新数据集,都强制先跑一遍“冻结主干 + 只训头”的基线,再决定要不要放开全量微调。这个习惯帮我省掉的返工时间,比多调几组超参加起来还多。希望帮到你。
本文还有配套的精品资源,点击获取