Transformers迁移学习实战:环境配置、微调方法对比与中文情感分类代码

发布时间:2026/9/8 8:48:06
Transformers迁移学习实战:环境配置、微调方法对比与中文情感分类代码 先说个很多人都有的困惑辛辛苦苦看了几天迁移学习的理论什么源域、目标域、特征分布都背得滚瓜烂熟结果一打开 IDE看着pip install transformers之后那堆报错人直接傻掉。尤其是当我看到“迁移学习的python代码”“哪个版本的pytorch和cuda支持transformers3.4.0”这种搜索词频繁出现在技术社区里时我就知道真正卡住大家的不是原理而是落地那一哆嗦。这篇文章我就以 Transformers 库为主线把“预训练微调”这条最主流的迁移学习落地路线彻底讲透。不绕弯子不堆术语从环境版本怎么配、三种微调方式怎么选、到文本分类的完整可跑代码再到调试过程中那些让人抓狂的坑挨个过一遍。适合刚接触 NLP 迁移学习的学生、想快速验证效果的算法工程师以及准备在业务里引入预训练模型但被环境劝退的朋友。看完之后你至少能独立跑通一个微调实验并且知道每一步在干什么、为什么这么干。1. 先搞懂迁移学习到底在解决什么问题1.1 从零训练和迁移训练的核心差异很多人第一次接触迁移学习的时候脑子里冒出的第一个问题是我直接拿数据训练一个模型不就行了吗为什么非要搞“预训练微调”这套这个问题问得非常关键它决定了你是否真的理解迁移学习的价值。从零训练一个深度模型意味着所有的知识都只能从你手头这批标注数据里学。假设你要做一个中文评论情感分类任务攒了 5 万条标注数据这听起来已经不少了吧但对模型来说它要从零开始学习汉字怎么写、词语怎么组合、句子结构长什么样更要理解“这部电影让我哭笑不得”这种复杂情绪的微妙之处——5 万条数据根本不够学模型很快会过拟合在测试集上的表现惨不忍睹。而迁移学习的思路则完全不同。我们先用海量无标注文本比如几十 GB 的网页、书籍、百科预训练一个语言模型这个模型已经学会了词法、句法、语义甚至一部分世界知识。然后在下游任务上我们只需要在这个“基础能力很强的大模型”基础上用小规模标注数据做“定向训练”也就是微调把手头的任务知识接上就行。预训练相当于读了十二年书微调相当于入职前一个月的岗位培训两者完全不是一个量级的工作量。这里我还想补充一个概念辨析因为最近的搜索热词里总有人把“直推式迁移学习”和常规微调混在一起。经典的微调属于“归纳式迁移学习”源域和目标域任务不同但数据分布相似模型学到的是泛化能力。而直推式迁移学习更典型的是领域自适应场景源域有标注、目标域只有无标注样本模型需要在测试时对目标域样本特别处理。在 Transformers 库语境下我们日常接触到的“微调”基本都是归纳式迁移学习的落地方案这个定位先清楚后面才不会迷糊。1.2 为什么“预训练微调”成了事实标准其实在 Transformers 库出现之前NLP 领域也尝试过不少迁移方案比如 ELMo 那种只拿预训练词向量做特征输入的方案再比如把预训练模型的输出拼接到下游模型上的做法。但都有一点“隔着靴子挠痒痒”的感觉因为预训练模型内部那些蕴含丰富语义的层次化特征并没有真正参与下游任务的训练。BERT 这类基于 Transformer 架构的模型出现后情况彻底变了。Transformer 的 encoder 结构天然适合做各种 NLP 任务的底座它的每一层 self-attention 都能捕捉到不同粒度的语言特征从词法到句法再到语义。于是 BertForSequenceClassification、BertForQuestionAnswering 这种“模型主体 任务头”的组合方式就成了标准范式——把预训练模型当作骨架在它上面挂一个适合具体任务的输出层微调的时候整个骨架和输出层一起训练。Transformers 库把这些封装得极其利落三五行代码就能加载一个预训练模型再配一个 Trainer 类数据处理、训练循环、评估流程全都标准化了。有人担心“整个模型一起训练”会不会让预训练能力被冲掉这正是微调这个动作的精妙之处。实际训练中下游数据量相对于预训练语料来说很小学习率也压得很低通常 2e-5 这个量级模型只会做“温和的调整”不会发生灾难性遗忘。这就是迁移学习落地中最核心的哲学用大规模通用知识做底座用小规模领域知识做修正两边都不耽误。2. 环境选型PyTorch、CUDA 与 Transformers 版本的真·匹配指南2.1 3.4.0 这个经典版本的前世今生“哪个版本的pytorch和cuda支持transformers3.4.0”看到这个搜索词我一下就乐了。如果你是从网上找了一份老教程大概率会碰上这个版本。Transformers 3.4.0 是 2020 年末发布的版本当时对应的生态大致是 Python 3.6-3.8、PyTorch 1.5-1.6、CUDA 10.1-10.2。后来 PyTorch 升级到 1.8、CUDA 升级到 11.x 之后老版本 Transformers 的某些依赖约束开始出现兼容性问题最典型的就是tokenizers库的编译版本对不上。我个人的建议是如果是全新开始的项目没必要死守 3.4.0。直接装新版的 transformers比如 4.x 系列接口更规范、bug 修复更多、社区支持也更好。但如果你确实需要复现一个基于 3.4.0 的旧项目那么环境组合我的经验是组件我的推荐版本组合Python3.7 或 3.8PyTorch1.6.0CUDA Toolkit10.2显卡驱动440.33 以上这个组合是我实测过最稳的transformers3.4.0 在 requirements 里写的torch1.0.0理论上 1.6 完全满足而 CUDA 10.2 对 20 系显卡的支持非常成熟兼容性和稳定性都好。如果你的显卡是 30 系及以上就得换个思路了因为 30 系显卡需要 CUDA 11.x 以上这时候要么升级 transformers 版本到 4.4.0 以上配合 PyTorch 1.8.1 CUDA 11.1要么就得找别的替代方案。2.2 手把手锁定一套不出错的环境在这里我直接推荐一套“大众化、踩坑最少”的组合适用于绝大多数文本分类、命名实体识别、文本匹配等微调任务Python 3.9 或 3.10PyTorch 2.0.1对应 CUDA 11.7 的预编译包transformers 4.31.0datasets 2.14.3accelerate 0.21.0peft 0.5.0后面讲 LoRA 微调要用安装命令我习惯这么写因为它把依赖顺序和版本都限制住了conda create -n finetune python3.9 -y conda activate finetune # 安装对应 CUDA 11.7 的 PyTorch pip install torch2.0.1 torchvision0.15.2 torchaudio2.0.1 --index-url https://download.pytorch.org/whl/cu117 pip install transformers4.31.0 datasets2.14.3 accelerate0.21.0 peft0.5.0 # 可选用于指标计算 pip install scikit-learn evaluate装完之后一定要在 Python 里跑一次这个测试确认 GPU 真正可用import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))如果torch.cuda.is_available()返回 False先别急着去改 transformers大概率是 PyTorch 装成了 CPU 版本或者 CUDA 驱动对不上。可以用nvidia-smi看驱动支持的 CUDA 版本然后去 PyTorch 官网挑对应的安装命令。这一步搞不定后面跑再好的代码都是纸上谈兵。2.3 版本组合背后的依赖逻辑我在帮别人排查环境问题的时候发现很多人其实是被“哪个版本的pytorch和cuda支持transformers3.4.0”这种问题带偏了思路。版本兼容不是看某个库单独要求什么而是看依赖树上所有包的综合约束。比如说 transformers 3.4.0 底层依赖tokenizers0.10.1这个版本的 tokenizers 在编译时对absl-py、protobuf等库有隐性要求如果你装了一个过新的 protobuf比如 4.x就会出现“import tokenizers 时直接段错误”这种让人崩溃的问题。所以锁环境的时候我的习惯是先锁 PyTorch 和 CUDA再装 transformers然后让 pip 自动解析依赖最后再单独固定训练相关的包版本。顺序反了或者中间乱插版本很容易把依赖树搅成一锅粥。另外强烈建议用 conda 管理 Python 环境每次实验都新建独立环境脏了直接删掉重建比在那里折腾回滚强一百倍。3. 三种主流微调方案对比全量微调、Freeze 微调与 LoRA 微调3.1 从“改全部”到“改一丢丢”的思路演进“全量微调、freeze微调以及lora微调”这三个词最近在社区里讨论度极高因为大模型时代显存就是金钱谁都不想为了一个简单任务租 8 张 A100。我把这三种方式用一个生活化的类比讲清楚。全量微调就像你要装修整间屋子承重墙、水管、电路全都要重新弄一遍效果最彻底但工程量和成本也最大——它的显存开销包括模型参数、梯度、优化器状态Adam 会额外存一阶和二阶动量再加上前向传播的激活值一个 1.5B 参数的模型光用 fp16 训显存就往 20GB 上走了。Freeze 微调则是把整栋楼的主体结构锁死只装修某一层。实际操作中我们把 BERT 的 embedding 层和前 11 层 transformer 层的参数全部冻结只训练最后一层和分类头这样反向传播时被冻结层无需计算梯度显存占用直接砍半以上训练速度也明显加快。缺点也明显底层特征完全不动如果下游任务和预训练领域差异较大效果会打折扣。LoRA 微调就更有意思了它不动原模型的任何参数而是在某些层旁边加装一个“小型旁路模块”——用两个低秩矩阵来模拟参数的更新量。训练的时候只更新这个旁路原模型参数连梯度都不算。用数学语言说原本需要更新的参数量是 d×dLoRA 把它拆成 d×r 和 r×d 两个矩阵当 r 远小于 d 时训练参数量缩减成原来的几十分之一甚至几百分之一。一个 7B 模型的 LoRA 微调可能只需要不到 1% 的参数。3.2 显存占用量化估算与对比为了让这个对比更直观我以 BERT-base1.1 亿参数约 440MB 存储为例给一个工程上的估算微调方式可训练参数量训练阶段显存需求batch size 8序列长度 128消费级显卡能否训练全量微调110M约 8-10GB需要 16GB 或以上Freeze冻结前 11 层约 7.7M约 4-6GB8GB 可以LoRArank8约 0.5M约 3-5GB6GB 可以这里要说明一下显存估算不只是参数本身的体积。全量微调的显存大头在优化器状态和激活值上fp16 参数占 220MB梯度同理占 220MBAdam 优化器在混合精度下还需要维护 fp32 的模型参数副本和两个动量状态大约 1.3GB再加上自动求导过程中保存的中间激活值那才是真正的“隐形内存杀手”。所以我常跟朋友说不要只看参数量要按“参数量 × 2梯度 参数量 × 12优化器状态 激活值”这个粗粒度公式去估算至少心里有个底。LoRA 之所以能省显存本质上是因为它把“梯度”和“优化器状态”这两块大头砍掉了绝大部分。对于大模型微调尤其是 Qwen、Llama 这类动辄几十亿参数的模型LoRA 几乎成了默认选项。我在用peft库给 Qwen 做 LoRA 微调的时候rank 取 8、alpha 取 16、只把 attention 层的 q_proj 和 v_proj 注入 LoRA训练参数量只有原模型的 0.2% 左右一张 24GB 的 3090 就能跑得很舒服。3.3 到底怎么选项目需求驱动的决策框架讲完原理和参数很多人会问那我做项目到底用哪种我的建议是按这三个维度来决策。数据集规模与领域相关度是首要因素。如果下游标注数据很少几千条领域和预训练语料比较接近那么全量微调容易过拟合LoRA 反而是更稳的选择因为它限制了模型的更新幅度。如果数据量中等几万条且任务对细节敏感比如专业领域实体识别全量微调或 Freeze 都有机会拿到更好的效果。如果数据量很大那就放开手脚做全量微调甚至可以考虑用更大规模的基座模型。硬件资源是现实约束。只有 8GB 显存那就基本告别全量微调 BERT 了LoRA 或者 Freeze 是唯一选择。训练效率也要考虑Freeze 因为冻结了大部分层反向传播只算很少一部分梯度速度大约是全量微调的 1.5-2 倍LoRA 虽然反向传播的计算图还是要走但优化器更新量极小速度也很可观。我自己在大部分中小心愿场景下的经验排序是LoRA 优先Freeze 兜底全量微调只在数据量大且硬件充足时用。这不仅仅是显存问题更是工程效率问题。LoRA 训练出来的低秩矩阵只有几十 MB部署时可以独立保存随意切换任务而不影响底座模型这在多任务并行的业务场景下简直是降维打击——底座模型只需一份任务旁路各存各的换来换去也就几秒钟。顺带一提现在像 LlamaFactory 这类工具已经把 LoRA 微调的流程封装到“改配置就能训”的程度如果你懒得写训练循环直接用它的界面把数据集和模型路径填进去跑起来基本无脑。像一些云平台提供的托管微调服务底层也是这套思路只是帮你把环境运维也省了。4. 实战用 Transformers 微调一个中文情感分类模型4.1 准备一份可以直接上手的数据集实践出真知我这里选一个最经典的任务中文情感二分类。为了让大家能立刻上手跑通我直接基于一份小型示例数据来演示字段就是text和label两列label 为 0 表示负面、1 表示正面。数据量很小没关系我们的核心目标是跑通整个微调闭环理解了流程之后你自然知道怎么替换成自己的业务数据。建议把数据整理成 CSV 文件格式如下text,label 这个电影特效很棒剧情也很紧凑,1 剧情太拖沓了看了半小时就想睡觉,0 演员演技在线非常推荐,1 逻辑混乱观感太差,0然后用datasets库的load_dataset方法读入。注意load_dataset(csv, data_filestrain.csv)在版本 2.x 里返回的是一个 DatasetDict 结构包含train字段。为了方便演示我会做一次train_test_split留出 10% 的数据做验证集。4.2 加载预训练模型与分词器这里有一个新手非常容易踩的坑直接用 BertForSequenceClassification 的默认权重随机初始化然后拿小数据硬训。这等于抛弃了迁移学习最核心的预训练权重退回从零训练。正确做法是先指定一个预训练模型名称比如bert-base-chinese让 Transformers 自动下载权重。from transformers import AutoTokenizer, AutoModelForSequenceClassification model_name bert-base-chinese tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForSequenceClassification.from_pretrained(model_name, num_labels2)AutoTokenizer和AutoModelForSequenceClassification是 Transformers 库里的自动派发接口你只要给出模型名称它们会自动匹配对应的分词器和模型结构。这也是我说“新版 transformers 省心”的原因老版本里你可能还要自己去 importBertTokenizer、BertForSequenceClassification换一个模型就要改一堆代码。对中文任务分词器会自动按字切分BERT 的中文模型用的是字级别词表所以不需要你额外做 jieba 分词直接传原始中文文本就行。这一点跟很多人的直觉相反但确实如此中文 BERT 是字符级建模字和字之间天然隔开模型通过 attention 机制自己学会词语边界。4.3 数据预处理Tokenize 与标签映射这一步是整个流程里最容易出 bug 的部分但它非常机械。核心逻辑是把原始文本转成模型能吃的input_ids、attention_mask再加一个labels字段。def preprocess_function(examples): # 这里 tokenizer 会自动做 padding 和截断 encoded tokenizer( examples[text], truncationTrue, max_length128, paddingmax_length, ) encoded[labels] examples[label] return encoded encoded_dataset dataset.map(preprocess_function, batchedTrue)这里三个参数的作用要理解清楚truncationTrue会把超过 128 个 token 的文本截断max_length128是序列统一长度paddingmax_length则是把短文本补齐到 128。为什么要统一长度因为 GPU 上的 tensor 要求同批次内所有样本形状一致不统一长度就无法并行计算。但这里我要提醒一个很多教程不会讲的性能细节用paddingmax_length其实是很浪费的做法。如果大部分文本都很短只有少数长文本那么所有样本都会被打到 128 长度造成大量[PAD]token 的无效计算。更高效的做法是paddingTrue让 tokenizer 按当前 batch 内最长文本动态补齐。配合DataCollatorWithPadding可以做到每个 batch 只有少量填充训练速度快 20% 都不稀奇。from transformers import DataCollatorWithPadding data_collator DataCollatorWithPadding(tokenizertokenizer, paddinglongest)还有一个隐藏的坑bert-base-chinese的 tokenizer 没有设置pad_token默认是[PAD]实际上它是有的但某些模型没有。保险起见可以在加载后检查if tokenizer.pad_token is None: tokenizer.pad_token tokenizer.eos_token # 或手动设为 [PAD]如果 pad_token 没设对训练时 loss 会一直不收敛而且损失值飘忽不定排查半天才发现是 mask 的问题。4.4 配置训练参数并启动微调接下来就是重头戏用 Trainer 接口启动训练。Transformers 库的Trainer类把训练循环、梯度累积、学习率调度、日志记录等全都封装好了你只需要提供一个TrainingArguments配置对象。from transformers import Trainer, TrainingArguments training_args TrainingArguments( output_dir./results, # 输出目录 num_train_epochs3, # 训练轮数 per_device_train_batch_size16, # 单卡 batch size per_device_eval_batch_size32, learning_rate2e-5, # BERT 微调经典学习率 warmup_ratio0.1, # 前 10% 训练步数做学习率预热 logging_steps50, eval_strategyepoch, # 新版参数名旧版本是 evaluation_strategy save_strategyepoch, load_best_model_at_endTrue, metric_for_best_modeleval_loss, fp16True, # 混合精度训练省显存加速 save_total_limit1, ) trainer Trainer( modelmodel, argstraining_args, train_datasetencoded_dataset[train], eval_datasetencoded_dataset[test], tokenizertokenizer, data_collatordata_collator, ) trainer.train()这里的learning_rate2e-5是 BERT 微调的标准值为什么不是 1e-3 这种常规值因为预训练模型已经收敛到一个比较好的局部最优区域学习率过大会直接把这个区域震碎破坏学到的特征表示业内称之为灾难性遗忘。微调的本质是在原有最优区域附近做小范围游走所以学习率必须低。fp16True是混合精度训练用半精度计算大幅度减少显存占用和计算量。如果你的显卡不支持 fp16老显卡这个参数会导致崩溃需要删掉。如果你的显卡是 30 系及以上强烈建议开启 fp16BERT-base 32GB 的训练显存需求能直接降到 16GB 左右。训练完成后保存模型和分词器model.save_pretrained(./my_sentiment_model) tokenizer.save_pretrained(./my_sentiment_model)4.5 评估与推理看看迁移学习到底有没有效果训练完要验货。用 Trainer 的evaluate()方法可以输出验证集指标但默认只有eval_loss。想看到准确率需要自己传一个compute_metrics函数import numpy as np from evaluate import load accuracy_metric load(accuracy) def compute_metrics(eval_pred): predictions, labels eval_pred predictions np.argmax(predictions, axis1) return accuracy_metric.compute(predictionspredictions, referenceslabels) trainer.compute_metrics compute_metrics eval_result trainer.evaluate() print(eval_result)推理阶段更简单加载保存好的模型然后走一遍“分词 - 模型前向 - argmax”的流程from transformers import pipeline classifier pipeline(text-classification, model./my_sentiment_model, tokenizer./my_sentiment_model) result classifier(这部电影太棒了强烈推荐) print(result) # [{label: LABEL_1, score: 0.987}]pipeline是 Transformers 库提供的高级封装它会自动完成 tokenize、模型推理、结果后处理。如果你的数据有自定义标签名可以在保存模型时通过model.config.id2label设置映射加载后 pipeline 输出的 label 就更友好。我自己在做业务系统集成时很少直接调pipeline更习惯写一个轻量的推理回调把模型输出和业务规则串起来但排查问题阶段用它非常方便。4.6 小数据量的过拟合控制技巧这里要多说一句如果你手头的数据只有几百条直接全量微调 BERT 几乎是必过拟合的。一个行之有效的组合策略是用较小的 max_length比如 64 较强的 weight decay early stopping。Trainer 本身不内置 early stopping但你可以在训练循环外监控 eval_loss连续两个 epoch 不降就停止。另一个很实用的技巧是分层学习率。BERT 底层学到的是通用语言特征顶层更接近任务特征所以底层可以给一个更低的学习率比如 5e-6顶层保持 2e-5。实现方法不是 Trainer 开箱即有的需要写一点自定义优化器逻辑from transformers import AdamW # 给模型不同层设置不同学习率 optimizer_grouped_parameters [ { params: [p for n, p in model.bert.parameters()], lr: 5e-6, }, { params: [p for n, p in model.classifier.parameters()], lr: 2e-5, }, ] optimizer AdamW(optimizer_grouped_parameters, weight_decay0.01) trainer.optimizer optimizer这个技巧在领域差异大的任务上效果尤其明显比如用通用中文 BERT 去做金融舆情分析底层学习率压低能让预训练知识保留得更完整。有次我在一个法律文本分类项目里对比过加了分层学习率之后F1 直接涨了两个点。4.7 大模型微调场景下的 LoRA 实操思路刚才讲的 BERT 是小模型的代表但最近搜索热词里“lora微调实战教程qwen”明显指向大模型场景。如果你要微调的是一个 7B 甚至 14B 的模型全量微调基本不用想重点就是 LoRA。这里我把代码思路也放一下用peft库非常简洁from peft import LoraConfig, get_peft_model, TaskType lora_config LoraConfig( task_typeTaskType.CAUSAL_LM, # 生成式模型的 LoRA 配置 r8, # 低秩矩阵的秩 lora_alpha16, # 缩放系数 target_modules[q_proj, v_proj], # 通常是 attention 里的线性层 lora_dropout0.05, ) model AutoModelForCausalLM.from_pretrained(Qwen/Qwen-7B, torch_dtypetorch.float16, device_mapauto) model get_peft_model(model, lora_config) model.print_trainable_parameters() # 看看可训练参数到底有多少peft库会把原模型参数全部冻住只给指定的q_proj、v_proj模块注入 LoRA 旁路。r是 LoRA 最核心的超参数它决定了旁路矩阵的宽度lora_alpha则是缩放系数实际更新强度近似于lora_alpha / r再乘以注入规模。经验上r8或r16起步如果任务简单可以更小如果任务复杂或者数据充足可以加大到 32、64。target_modules的选择也很有讲究只注入 attention 的 q 和 v 是最省显存的方案但知识容量有限如果显存宽裕可以加上 k_proj、o_proj甚至 feed forward 里的gate_proj、up_proj、down_proj效果会有提升但训练显存和速度也会相应变化。LoRA 训练完成后的产物是一个几十 MB 的 adapter 文件部署的时候可以把它和底座模型合并成一个完整模型也可以动态加载 adapter 推理。我之前在一个工单分类系统里就是只保存 adapter底座模型只放一份多个任务 adapter 轮流加载整个推理服务的内存开销非常可控。5. 常见问题与排查技巧实录5.1 环境类与显存类报错速查训练微调模型环境问题占了一半的报错比例。我把实际项目里遇到的高频问题整理成了一张速查表都是能直接照着干的排查思路遇到问题先去核对这张表能省至少一个小时的排查时间。报错现象根本原因解决路径CUDA out of memorybatch size 过大 / 激活值占用过高调小 batch size开启fp16使用gradient_checkpointing切到 LoRA 方案RuntimeError: Expected all tensors to be on the same device数据或模型混用了 CPU 和 GPU检查to(device)逻辑确保batch里的 tensor 都在 cuda使用 Trainer 时很少出现ImportError: tokenizers ... has no attributetransformers 和 tokenizers 版本不匹配统一升级到新版pip install -U transformers tokenizersCUDA error: no kernel image available显卡架构和 PyTorch 编译的 CUDA 版本不匹配换对应显卡架构的 PyTorch 预编译版本30 系用 cu11.740 系用 cu12 或更高训练 loss 不降学习率过大 / 数据未正确 mask / 标签严重错乱检查 tokenizer 的 pad_token调小学习率到 2e-5 附近检查标签映射loss 正常但 eval 不涨验证集分布和训练集不一致 / 指标计算代码有 bug重新切分验证集检查compute_metrics的 argmax 是否在正确维度显存问题是最常见的很多人一上来就买大卡其实很多问题用工程手段就能解决。gradient_checkpointing这个功能非常值得关注它以少量计算换显存把前向传播时的中间激活值不保存反向传播时重新计算轻松省下 50% 以上的激活值显存代价只是约 20% 的训练变慢。对想在单卡 8GB 上微调 BERT 的场景开这个开关之后基本都能跑起来。5.2 数据与训练过程中的经典翻车现场训练过程中还有几类问题代码层面一点错都没有但效果就是不对这类问题最磨人。第一个是标签失衡导致的“假收敛”。比如你的数据集里 95% 是正样本模型什么都不学一直输出正类宏观准确率随便就有 95%但实际对负样本毫无识别能力。这种问题要从数据分布层面解决比如做类别加权采样或者在 loss 里给少数类更大权重。我在跑情感分类的时候会先在验证集上打印分类报告而不是只看 accuracy。第二个是微调轮数过多导致的过拟合。BERT 在小数据集上微调通常 2-3 个 epoch 就已经足够再训下去验证集准确率不升反降。如果是几百条数据1-2 个 epoch 就该停了。很多教程为了展示“训练过程”把 epoch 设成 5 甚至 10这在教学数据上可能没事但在你自己的小数据上绝对会过拟合。用load_best_model_at_endTrue加上验证集监控让模型自动选择最优 checkpoint 是更稳妥的方案。第三个是数据泄漏。切分数据的时候没有 shuffle或者重复样本被同时分到训练集和验证集导致验证集损失虚低、线下指标和线上效果完全对不上。这个问题做 NLP 的人最容易忽视尤其是从数据库导出数据时可能默认按时间排序同类别数据聚集在一起不 shuffle 直接切分后果很严重。我每次做数据切分之前都会先检查label的分布是否在训练集和验证集中大致一致再跑dataset dataset.shuffle()。最后一个非常隐蔽的坑你在保存 model 的时候没有同时保存 tokenizer推理时用了错误的 tokenizer。训练时用的bert-base-chinese推理时手滑用了bert-base-uncased词表完全不同效果直接崩盘。我的习惯是永远成对保存和加载 model 与 tokenizer并且打一个带时间戳的 tag防止版本混乱。6. 写在最后的一些实在经验整个 Transformers 微调的链路走下来我的切身体会是这样一句话迁移学习的理论门槛不高但只要落到实际项目里环境、数据、训练策略每一个环节都可能让你卡上半天。这也是我为什么在这篇文章里花了大量篇幅在版本匹配、数据预处理和问题排查上因为这些内容才是社区搜索热词背后真正的痛点。如果你现在正准备开始做自己的第一个微调实验我的建议是别贪多、别贪大。先用bert-base-chinese配一份几百条的数据把全流程跑通感受一下预训练权重带来的“快速收敛”究竟是什么样的体验。然后再逐步尝试 Freeze、LoRA 这些进阶方案去对比不同方案在效果和资源上的取舍。等你把一个小模型、一整套流程玩熟了再上大模型微调你会发现所有思路都是相通的——底座模型换成了 Qwen 或 Llama训练方式换成 LoRA但数据处理、训练参数、排查问题的那套方法论完全一样。最后再分享一个小技巧每次实验开始时建议把transformers.__version__、torch.__version__、数据集规模、超参数配置全部打印出来存成一个文本文件。微调这个事非常依赖实验记录你能记住上一次效果好的完整配置就等于拥有了最快的模型复现速度。