迁移学习怎么落地?Transformers 库微调实战
关键词:迁移学习、Transfer Learning、Hugging Face Transformers、模型微调(fine-tuning)、预训练模型
适读人群:手里有几百到几万条标注数据、想训个分类/打标模型,但嫌从头训太慢太难的 Python 工程师;以及想搞清楚“预训练模型怎么为我所用”的人。
本文概览:迁移学习(Transfer Learning)的本质是“站在预训练模型的肩膀上”——不从头训,直接复用它在海量数据上已经学到的语言特征。这篇用 Hugging Face Transformers 库把这件事跑通:先用
pipeline看预训练模型开箱即用的能力,再用AutoModelForSequenceClassification做“换头”,接着用Trainer微调,最后闭环验证。全程以一段可运行代码为主线逐段拆解,结尾给出最小可运行模板和常见改动点。
目录
- 一、从零训练为何行不通?小数据集的困境
- 二、迁移学习到底在“迁移”什么?
- 三、pipeline 三行就能用预训练模型?
- 四、AutoModel 怎么“换头”:迁移学习的核心动作
- 五、准备数据:Tokenizer 与 Dataset
- 六、Trainer 微调:让预训练模型适配你的任务
- 七、闭环验证:加载微调后的模型再推理
- 八、Feature Extraction 还是 Fine-tuning?怎么选
- 九、微调好的模型怎么进大模型管线
- 十、迁移学习的边界:这几种情况别硬上
- 最小可运行模板与常见改动点
一、从零训练为何行不通?小数据集的困境
你接了个工单分类的需求:把用户反馈分成 bug / 咨询 / 建议 / 投诉 四类。手里只有 2000 条标注。
如果从头训一个 Transformer:
- 随机初始化:模型要从“什么是词、什么是句法”开始学,而你只有 2000 条样本;
- 容易过拟合:小数据喂给大模型,记住的是样本而不是规律;
- 慢且不稳:单卡训上半天,准确率可能还不如拍脑袋规则。
而迁移学习的思路是:已经有人在几亿条文本上训好了一个语言模型,它早就懂“词性、句法、语义”了。你只要把它学到的“语言能力”借过来,套上你自己的分类头就行。(这套思路在 CV 里同样成立:ResNet 在 ImageNet 上预训练好后,你换掉最后的全连接层就能做自己的图像分类,卷积特征直接复用——NLP 和 CV 的迁移学习是同一个思想的两套实现。)
在大模型时代,迁移学习不但没被取代,反而更底层了:今天所有“微调一个基座模型”的做法(LoRA、指令微调、领域适配)本质都是迁移学习,只是头和训练方式更花。理解它,是看懂后面整套微调技术的前提——这也是它值得单独写一篇的原因。若硬要从零训同一个工单分类器,你得 import torch.nn as nn 手写 nn.Module(嵌入层 + Transformer 编码器 + 线性头)、自己选初始化、写反向传播、调学习率与 warmup——几十行样板代码、几小时试错。迁移学习把这些封装进 from_pretrained,你只声明 num_labels=4。省下的不是几行代码,而是几亿条文本的训练和几十次试错。
这里有个容易忽略的点:从零训慢,不只是慢在算力,更慢在“要走完整个试错周期”——学习率怎么设、要不要 warmup、权重初始化用哪种、过拟合了加什么正则,每一项都要你自己试。迁移学习把这些“通用经验”提前封装进预训练权重,你剩下的旋钮只剩“学习率、轮数、批次”三四个,试错成本骤降。
图一:从零训练 vs 迁移学习
(图一:左边随机初始化 + 小数据 = 慢且易过拟合;右边预训练 Backbone 复用 + 只训新头 = 快且稳)
小结
迁移学习解决的核心矛盾是:你的数据少,但任务需要的“基础语言能力”别人已经训好了。下面看它到底迁移了什么。
二、迁移学习到底在“迁移”什么?
一个预训练语言模型可以拆成两半:
- Backbone(特征提取器):底层到中层学的是通用语言特征——词法、句法、语义角色。这部分和你具体任务无关,是“通用资产”。
- Head(任务头):最后一层把特征映射到具体输出(比如 SST-2 的正负向、ImageNet 的 1000 类)。这部分是“任务专属”的。从数学看,分类头就是一个线性变换
y = W·h + b:h是 Backbone 输出的句向量,W、b是随机初始化的参数。训练时梯度从y反传到W、b,再继续往 Backbone 传——但因为 Backbone 学习率小(或干脆冻结),它只被“轻轻推”,主体特征不被破坏。理解这个W·h+b,就理解了为什么“换头 + 小学习率”能成立。
为什么 Backbone 能直接复用?因为预训练任务(如 MLM 掩码语言建模、NLI 句子关系判断)逼模型去理解“词怎么组成句、句怎么表达意”,学到的特征是与具体下游任务解耦的通用语言结构。你做工单分类也好、做舆情分析也好,底层要理解的“语言”是同一套——这正是迁移学习成立的前提。从早期的 Word2Vec(只训静态词向量)到 ELMo(双向 LSTM 出上下文向量)、再到 BERT(Transformer + MLM),预训练模型从“给每个词一个向量”进化到“给每个上下文一个表示”,可复用的特征越来越深,迁移学习的效果也越来越好——今天用 from_pretrained 一把梭,正是这条演进链的终点。反过来,如果预训练语料和你的任务域差太远(比如用代码预训练模型去做医疗文本分类),复用收益就会打折,这点后面边界一节会讲。
迁移学习做的事,就是复用 Backbone,换掉 Head:
图二:迁移学习的结构
(图二:预训练 Backbone 冻结不动,新 Head 随机初始化后单独训练,梯度只更新 Head)
关键点:新 Head 是随机初始化的,预训练时根本不存在它,所以必须训练。而 Backbone 已经很懂语言了,通常冻结或小幅微调即可。
小结
“迁移”迁移的是 Backbone 的通用语言特征;Head 是你自己的任务层,必须重新训。这就是后面所有代码的底层逻辑。
三、pipeline 三行就能用预训练模型?
先不急着微调,用 pipeline 看一眼“预训练模型到底有多能打”——它甚至能在没见过你数据的情况下做零样本分类。开始前先装依赖:pip install transformers datasets accelerate evaluate。
from transformers import pipeline
# 零样本分类:预训练 NLI 模型没见过“工单”,却因为懂语言就能分
cls = pipeline("zero-shot-classification", model="facebook/bart-large-mnli")
out = cls("登录后页面一直转圈", candidate_labels=["bug", "咨询", "建议"])
print(out["labels"][0], out["scores"][0]) # → bug / 0.9x
三行就跑通,说明一件事:预训练模型已经把“语言理解”这项基础能力训好了,你看到的“开箱即用”就是上游迁移学习的结果。
pipeline 本身是把三件事包成了一步:用对应 AutoTokenizer 分词 → 调 AutoModel 拿 logits → 用后处理把 logits 变成可读标签和分数。它省的是“样板代码”,底层还是 Auto 那一套;等你要把控每一步(比如自定义截断、加特殊 token、改输出格式)时,就退回 AutoTokenizer + AutoModel 手动写——下一节正是这么做的。
首次运行会自动下载模型权重(如 bart-large-mnli 约 1.6GB),之后走 ~/.cache/huggingface 本地缓存,重复运行不重复下;离线或内网环境,提前用 huggingface-cli download facebook/bart-large-mnli 把权重拉进缓存即可,避免训练时卡在网络。
pipeline 适合“快速验证”和“简单任务”。但你的工单有 4 类、有自己的标注分布,零样本不一定准——零样本靠的是“语义匹配”,类别边界模糊或你的标签体系很专业时,准确率会掉。要真正适配你的数据,得自己做“换头 + 微调”。
小结
pipeline 是预训练能力的“快捷入口”;零样本能救急但上限有限,要适配自有数据,下一步用 AutoModel 显式换头。
四、AutoModel 怎么“换头”:迁移学习的核心动作
迁移学习的核心动作就一行——加载预训练 Backbone,并指定你自己的类别数:
from transformers import AutoModelForSequenceClassification, AutoTokenizer
MODEL = "distilbert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(MODEL)
# num_labels=4:我们的工单有 4 类。
# 这一层(分类头)是随机初始化的“新头”,预训练时不存在,所以必须训练。
model = AutoModelForSequenceClassification.from_pretrained(MODEL, num_labels=4)
AutoModelForSequenceClassification 做的事很明确:
- 下载
distilbert-base-uncased的预训练权重当 Backbone; - 在它上面拼一个随机初始化的线性分类头,输出维度 =
num_labels; - Backbone 权重来自预训练,Head 权重随机。
顺带一提,num_labels 决定的是分类头的输出维度。二分类有两种常见写法:设 num_labels=1 配 BCE 损失,或设 num_labels=2 走 softmax——看你的评估习惯。如果一条工单能同时是“bug”和“投诉”(多标签),则要换 BCEWithLogitsLoss、让 Head 输出 N 个独立 sigmoid,但 from_pretrained(num_labels=N) 这一行写法不变。本文聚焦单标签多类,多标签只是损失函数和输出层的小改动。
这里的 Auto 前缀值得记住:AutoModelForSequenceClassification 只是 Auto 家族一员,还有 AutoModel(拿隐藏状态)、AutoModelForTokenClassification(命名实体识别)、AutoModelForQuestionAnswering(抽取式问答)等。同一个 MODEL 名,换个不同的 AutoModelForX 就能适配不同任务——Backbone 复用、只换头,这正是迁移学习被 API 化的体现:你不必为每个任务重新理解模型结构,框架替你接好头。
常用基座怎么选:快速验证用 distilbert-base-uncased(小、快);要精度换 bert-base-uncased;英文更强用 roberta-base;中文用 bert-base-chinese 或 hfl/chinese-roberta-wwm-ext;长文本(>512 token)看 longformer / bigbird。关键点:选模型只改 MODEL 这一个字符串,换头、分词、训练代码全不变——又是“换头不换流程”的体现。建议先用小模型把整条链路跑通、确认标注和指标没问题,再换大模型提上限,避免一上来就和大模型搏显存、出问题还难定位。
这就是为什么后面必须 train():Head 是瞎的,得靠你的数据把它训亮。而 Backbone 已经很懂语言,通常是“顺手微调”而不是“从头学”。
经验:
AutoTokenizer必须和AutoModel用同一个模型名——分词方式要和预训练时一致,否则输入错位,模型直接瞎。
想先只训 Head、冻结 Backbone,把上面那行之后补一句即可:
# model.base_model 就是预训练 Backbone;锁死它,梯度只更新分类头
for p in model.base_model.parameters():
p.requires_grad = False
requires_grad=False 告诉 PyTorch“这层参数不参与反向传播、不更新”——这就是迁移学习里“冻结”的实现方式。后续两阶段工作流的第一步就是靠它先跑通 baseline。注意 AutoModelForSequenceClassification 的 base_model 才是 Backbone,分类头在 model.classifier / model.score 上,别冻错了对象。
from_pretrained 背后干了三件事:下载配置文件(模型层数、隐藏维度等)、下载权重文件、把它们缓存到本地 ~/.cache/huggingface(下次不重复下)。对应的 save_pretrained("./ticket-bert") 则把配置 + 权重 + 分词器一起写进目录,所以第七节才能用 pipeline(model="./ticket-bert") 直接加载——这两个方法是“下载”和“落地”的一对,记住它俩就记住了迁移学习的输入输出边界。
小结
“换头”= 指定 num_labels,让预训练 Backbone 接上你的任务层;Head 随机、必须训,这是迁移学习落地的总开关。requires_grad=False 则负责把 Backbone 冻住。
五、准备数据:Tokenizer 与 Dataset
模型只吃数字张量,文本得先分词。用 datasets 库把 CSV 工单变成模型能吃的格式:
from datasets import load_dataset
from transformers import DataCollatorWithPadding
ds = load_dataset("csv", data_files={"train": "tickets_train.csv", "test": "tickets_test.csv"})
def tokenize(batch):
# truncation:超长截断;max_length 按你的文本长度定
return tokenizer(batch["text"], truncation=True, max_length=128)
tok = ds.map(tokenize, batched=True, remove_columns=["text"])
tok = tok.rename_column("label", "labels") # Trainer 认 labels 这个列名
collator = DataCollatorWithPadding(tokenizer=tokenizer) # 按 batch 动态补齐,省显存
几个容易踩的点:
truncation=True必加:不加快长文本会超模型最大长度直接报错。DataCollatorWithPadding而非写死padding:它按每个 batch 里最长序列动态补齐,比全量补到最大长度省显存。- 列名
labels:Trainer默认读labels,你的 CSV 若叫label要rename_column。
你的 tickets_train.csv 长这样就行,两列足够:
text,label
"登录后页面一直转圈,刷新也没用",bug
"想问下上个月的账单怎么导出",咨询
"建议加个深色模式,晚上太刺眼",建议
"付了钱会员没到账,要求退款",投诉
label 用整数(0/1/2/3)最省事,和 num_labels=4 一一对应;用文字标签也行,Trainer 会按出现顺序编码,但整数更可控。切分比例上,数据少就 8:2(train:test),数据过万可以 9:1 甚至 95:5——测试集只要能稳定反映准确率就行,不必太大。
max_length 怎么定? 别拍脑袋填 128。先 tok.filter(lambda x: len(x["text"]) > 128) 看看你的文本长度分布:若 95% 的句子都短于 128,填 128 就够,更长的截断不亏;若大量样本超 256,填 128 会砍掉关键信息、伤准确率。原则是“覆盖绝大多数样本的最小长度”——短则浪费、长则撑显存。tokenizer 还会自动补 [CLS]/[SEP] 这类特殊 token(BERT 靠它们判断句子边界),你不用手动拼,但要知道它们在,长度要留 2 个位置的余量。
类别不平衡:若“投诉”只有几十条而其他类上千,直接训模型会倒向多数类。补救在数据侧——采样上对少数类 resample 或用 WeightedRandomSampler,损失上给少数类更大的 class_weight——评估时看 macro-F1 而非 accuracy。这不改变迁移学习本身,是训练前的常规处理,和换头、微调是正交的两件事。
小结
数据侧的固定动作:分词 + 截断 + 改名 labels + 动态补齐,外加按真实长度分布定 max_length。准备好就能交给 Trainer。
六、Trainer 微调:让预训练模型适配你的任务
Trainer 把训练循环(前向、反向、优化、评估)全包了,你只要给模型、数据和配置:
from transformers import Trainer, TrainingArguments
args = TrainingArguments(
output_dir="./ticket-bert",
per_device_train_batch_size=16,
num_train_epochs=3,
learning_rate=2e-5, # 比从头训小 10~100 倍:只微调,不破坏预训练特征
logging_steps=20,
evaluation_strategy="epoch",
)
trainer = Trainer(
model=model,
args=args,
train_dataset=tok["train"],
eval_dataset=tok["test"],
tokenizer=tokenizer,
data_collator=collator,
)
trainer.train()
trainer.save_model("./ticket-bert") # 落地:权重 + 分词器都存进目录
learning_rate=2e-5 是点睛之笔:微调要用比从头训小得多的学习率。原因在下一节讲。
怎么判断训练在收敛、没过拟合? 盯 evaluation_strategy="epoch" 吐出的 eval_loss:它应当随轮数下降并趋平;如果 train_loss 一路降但 eval_loss 开始回升,就是过拟合信号——此时要么少训一轮(num_train_epochs 减 1),要么加 weight_decay(如 0.01)或 warmup_steps。别等 train_loss 贴到 0 才停,那往往是背下了训练集。准确率不够高时,先确认测试集标签没标错,再考虑换更大的基座(distilbert → bert-base)或解冻更多层微调,而不是盲目加轮数。
其他常用旋钮(按需加进 TrainingArguments):
warmup_steps=100:训练前若干步学习率从 0 线性升到目标值,避免开头大梯度把预训练特征冲坏,和“小学习率”是同一个保护思路。weight_decay=0.01:L2 正则,抑制过拟合,微调时基本必开。fp16=True/bf16=True:半精度训练,显存砍半、速度翻倍,有支持的训练卡建议开。gradient_accumulation_steps=2:等效把批次翻倍但显存不涨,小显存跑大批次的常用技巧。load_best_model_at_end=True+metric_for_best_model="eval_loss":每轮评估后自动保留最优权重,省得你自己挑 epoch。
这些旋钮都不改变“迁移学习”的本质,只是让微调更稳更快——先跑通上面的最小配置,再按显存和效果逐个加。
小结
Trainer 一句话启动微调;save_model 把权重落盘。真正要调的只有学习率、轮数、批次这几个旋钮,判断好坏看 eval_loss 曲线,其余旋钮按需叠加。
七、闭环验证:加载微调后的模型再推理
微调完,直接拿保存的目录当模型用,验证它真的学会了你的工单:
from transformers import pipeline
# 直接喂目录,pipeline 会自动读里面的权重 + 分词器
cls = pipeline("text-classification", model="./ticket-bert")
print(cls("支付接口超时,用户投诉")) # → 投诉 / 0.97
这一步把“迁移学习”闭环了:预训练 Backbone(通用语言力)+ 你的 Head(工单 4 类)= 一个专属分类器。从 pipeline 初体验到 Trainer 微调再到这里推理,主线就是这一条代码链。
两个落地时的坑顺手提醒:其一,如果你不用 pipeline、而是直接 model(**inputs) 拿 logits,推理前务必 model.eval()——否则 Dropout/BatchNorm 处在训练态,同一条文本多次跑结果会飘。其二,训好的模型想分享或版本管理,用 trainer.push_to_hub("你的名/工单模型") 一键推到 Hugging Face Hub,比手动传文件省心,团队协同时尤其有用。
其三,批量推理用 tokenizer(texts, return_tensors="pt", padding=True, truncation=True) 一次喂多条,比逐条循环快一个数量级;padding=True 让不等长样本对齐到 batch 内最长,和训练时的 DataCollatorWithPadding 是同一个补齐思路,只是推理时你手动指定。
怎么定量确认它真学会了? 别只看一条样例“看起来对”,用 evaluate 在测试集上算准确率才靠谱:
import evaluate
acc = evaluate.load("accuracy")
def compute_metrics(eval_pred):
logits, labels = eval_pred
preds = logits.argmax(-1) # 取分数最高的类别
return acc.compute(predictions=preds, references=labels)
# 把 compute_metrics 传给 Trainer(...) ,每轮 eval 会打印 accuracy
类别不平衡时把 accuracy 换成 f1(宏平均),否则多数类会掩盖少数类的差。这个指标就是你判断“要不要解冻微调、学习率调多少”的客观依据——靠它而不是靠感觉调参。
小结
能用自己的目录跑出正确类别,说明迁移学习落地成功。直接调 model 推理记得先 model.eval(),要分享就 push_to_hub;微调出的目录下游用 pipeline("text-classification", model=...) 即可零改动接入,业务方不必感知背后是 DistilBERT 还是 BERT。用 compute_metrics 在测试集上量化效果才稳。下面看你该用哪种微调策略。
八、Feature Extraction 还是 Fine-tuning?怎么选
迁移学习落地有两种力度,区别在“Backbone 冻不冻”:
图三:Feature Extraction vs Fine-tuning
(图三:左 = 冻结 Backbone 只训 Head;右 = 解冻部分层一起训,学习率更小)
| 维度 | Feature Extraction(冻结) | Fine-tuning(微调) |
|---|---|---|
| 做法 | 冻结 Backbone,只训 Head | 解冻部分/全部层,小学习率一起训 |
| 适配数据量 | 很小(< 1k) | 中 |
| 可训练参数 | 约 0.5% | 全部 |
| 学习率 | 较大(Head 随机初始化) | 小(1e-5 ~ 1e-4) |
| 风险 | 上限受 Backbone 限制 | 学习率太大 → 灾难性遗忘 |
为什么微调要用更小的学习率? 预训练权重已经是“好特征”,如果学习率太大,几步就把它们冲坏,模型得重新学语言——这叫灾难性遗忘(Catastrophic Forgetting)。小学习率让 Backbone 只做“小幅适配”,不破坏已有知识。
经验法则:数据极少先冻结 Head 跑通;数据够再解冻 Backbone 微调,学习率从 2e-5 起调,看 eval_loss 增减。有公开实测可参考:在 Oxford Flowers 102(小数据图像分类)上,冻结 Backbone 的“特征提取”达到 85.7%,解冻微调达到 92.5%——那 7 个点的差距,正来自 Backbone 是否被允许适配你的特定域。NLP 上同理:同域数据微调通常比纯冻结 Head 高几个点,差距大小取决于你的数据和预训练域的贴近程度。到了大模型时代,这种“解冻部分层微调”被发扬成 LoRA、QLoRA 等参数高效微调(PEFT):不改预训练权重,只训一小撮低秩适配矩阵,显存从“全量微调”降到几分之一——思想和本文“冻结 Backbone、只训一小块”一脉相承,只是适配的“小块”更巧、更省卡。理解迁移学习,就是理解这套 PEFT 技术的地基。
推荐的两阶段工作流(绝大多数场景够用):
- 阶段一·冻结跑通:先
for p in model.base_model.parameters(): p.requires_grad = False把 Backbone 锁死,只训 Head,1~2 个 epoch。这一步几分钟就能出 baseline,用来验证“数据标注、分词、标签映射”这条链路本身对不对。 - 阶段二·解冻微调:baseline 达标后,放开 Backbone(或只放最后几层),学习率降到
2e-5甚至1e-5,再训 2~3 个 epoch。此时在 baseline 之上小幅提升,且因为学习率小,不会把预训练特征冲坏。
两阶段分开做的好处:第一阶段能快速暴露数据/代码问题,避免你花半小时微调完才发现标签列名写错。
小结
选哪种看数据量:少 → 冻结 Head,多 → 解冻微调。微调记住“小学习率防遗忘”,并用两阶段工作流先验证链路再提精度。
九、微调好的模型怎么进大模型管线
迁移学习不是“被大模型取代”,而是大模型管线里的高性价比零件。四个具体落点:
场景 1:意图路由——用微调小模型判断用户 query 意图,决定进哪个 Agent / 工具:
router = pipeline("text-classification", model="./ticket-bert")
intent = router("帮我查一下上月的账单")[0]["label"] # → 咨询
# if intent == "咨询": call_billing_agent(user_text)
场景 2:大模型兜底——高置信度小模型直答,低置信度才进 LLM,省 token:
out = router("这个报错怎么解")[0]
if out["score"] > 0.9:
return kb[out["label"]] # 小模型直答,毫秒级
else:
return llm_chain(user_text) # 低置信度才进大模型
实测上,微调小模型推理 p99 常在 10ms 级,而一次 LLM 调用动辄 1~2s——把 70% 的高频确定性问题拦在快路径,整体吞吐和 API 成本都能降一个数量级,这也是 RAG / Agent 系统普遍在 LLM 前加一层小模型过滤的原因。
场景 3:领域适配器——在通用基座上微调出“金融 / 医疗专用”分类头,当大模型前置过滤器:先粗分再交给对应专家模型。比如医疗问诊先分出“用药咨询 / 报告解读 / 挂号导诊”,大模型只处理最复杂的“报告解读”,其余走轻量流程,整体时延和 API 成本都降一截。
场景 4:训练数据初标——用微调模型给 LLM 的 SFT 数据打初标,人工只校不用从零标。几千条待标注语料,小模型先标一遍、人工只改错的,比纯人工从白纸开始快数倍,标注成本降 60%+,且先有基线模型本身就能反哺数据质量检查(标错的和模型拿不准的重合度最高)。
图四:微调模型在大模型管线中的位置
(图四:微调小模型卡在路由 / 兜底 / 初标等环节,LLM 只处理它搞不定的复杂样本)
小结
微调小模型是 LLM 管线的“快路径”:把简单、高频、低置信度门槛内的活揽了,复杂活留给大模型。
十、迁移学习的边界:这几种情况别硬上
迁移学习不是万金油。三种场景下它收益有限,甚至不如从头训:
- 预训练域和任务域差太远:用代码语料预训练的模型去做古文分类,Backbone 的通用语言特征几乎用不上。域差距大时,优先选同域预训练模型(中文任务用
bert-base-chinese而非英文bert-base-uncased),实在没有再考虑从头训或做领域继续预训练。 - 你的数据其实够大:如果干净标注有几十万条,从零训一个中等模型往往上限更高——迁移学习的优势本就是“用小数据补短板”,数据管够时这块板不存在。
- 任务本质和预训练目标冲突:预训练学的是“理解”,如果你的任务是“生成长文”或“多模态对齐”,单纯换头式迁移学习不够,得上生成式微调(指令微调 / LoRA)那套,不在本文换头范畴内。
另有一个常见混淆:迁移学习 ≠ 持续学习(lifelong learning)。迁移学习是“一次预训练、一次适配”就定格,产物是个固定的工单模型;持续学习则要求模型在不断来的新任务上一直学、还不忘记旧任务,要专门对抗灾难性遗忘——而本文说的“小学习率防遗忘”恰恰是在单轮微调里避免把预训练特征冲坏,和持续学习的“跨任务不忘”是两件事。实际落地,新来一批标注直接重训或增量微调即可,不必追求“一个模型永远学”,那会引入持续学习的额外复杂度,对小团队往往得不偿失。
一句话判断:小数据 + 同域 + 理解类任务 = 迁移学习最舒服的区间;超出这个区间,先想清楚该换模型、加数据还是换方法,别无脑 from_pretrained。
最小可运行模板与常见改动点
把前面五段拼成一条最小可运行链(以工单 4 分类为例):
from transformers import (pipeline, AutoTokenizer,
AutoModelForSequenceClassification,
Trainer, TrainingArguments)
from datasets import load_dataset
from transformers import DataCollatorWithPadding
MODEL = "distilbert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(MODEL)
model = AutoModelForSequenceClassification.from_pretrained(MODEL, num_labels=4)
ds = load_dataset("csv", data_files={"train": "tickets_train.csv", "test": "tickets_test.csv"})
tok = ds.map(lambda b: tokenizer(b["text"], truncation=True, max_length=128),
batched=True).rename_column("label", "labels")
collator = DataCollatorWithPadding(tokenizer=tokenizer)
trainer = Trainer(
model=model,
args=TrainingArguments("./ticket-bert", per_device_train_batch_size=16,
num_train_epochs=3, learning_rate=2e-5,
evaluation_strategy="epoch"),
train_dataset=tok["train"], eval_dataset=tok["test"],
tokenizer=tokenizer, data_collator=collator,
)
trainer.train(); trainer.save_model("./ticket-bert")
cls = pipeline("text-classification", model="./ticket-bert")
print(cls("登录后白屏"))
常见改动点:
- 换数据集:CSV 改 JSON / 直接
load_dataset("imdb")等内置集;列名不同就改rename_column。 - 换模型:
distilbert-base-uncased→bert-base-chinese(中文)、roberta-base(英文更强);换完num_labels不变。 - 调学习率:
2e-5起;eval_loss不降就降到1e-5,震荡就加warmup_steps。 - 加评估指标:
from evaluate import load; acc = load("accuracy"),在compute_metrics里算 F1,类别不平衡时别只看准确率。 - 多标签分类:一条样本可属多类时,损失换
BCEWithLogitsLoss,Head 输出 N 个独立 sigmoid(num_labels不变),评估用 micro/macro F1。 - 多卡 / 大批次:单卡显存不够就
gradient_accumulation_steps+fp16顶上,或accelerate launch多卡起训,TrainingArguments不用大改。 - 中文场景:用
bert-base-chinese或hfl/chinese-roberta-wwm-ext,分词器和模型名保持一致。
#迁移学习 #HuggingFace #Transformers #模型微调 #预训练模型