尧图精选

Transformers微调实战指南:从迁移学习原理到LoRA中文情感分析

🕒 发布时间:2026/9/8 12:17:35 📁 来源:尧图网络
做迁移学习和 Transformers 微调我踩过不少坑也总结出一套能直接上手的路径。这篇文章不讲虚的全部是实操层面的东西版本怎么选、数据怎么喂、三种微调方式怎么取舍、训练时监控什么、出了错怎么排查最后再带一个完整的中文情感分析微调案例。不管你是给学生做演示还是自己项目里要改造一个开源模型按这条路走能少走很多弯路。1. 先理清思路迁移学习到底解决什么问题1.1 为什么讲迁移学习而不是从头训练很多初学者容易陷入一个误区以为做 NLP 任务就是要自己设计网络结构、从头训练一个模型。但实际上在绝大多数真实场景里从头训练是没有性价比的。预训练模型比如 BERT、RoBERTa、Qwen 这些已经在海量通用语料上学会了语言的基本规律——词法、句法、语义常识、上下文理解——这些能力相当于一个“通用底座”。迁移学习的核心思想就是把别人已经训练好的底座拿过来用你自己的数据做一次“定向改造”。打个生活化的比方一个已经会开车的司机换一辆新车只需要熟悉一下方向盘和刹车的感觉不用重新去驾校学一遍交规。模型也是这样它在预训练阶段已经耗了大量算力和数据你只需要用少量标注数据做“微调”Fine-tuning把它从“通用模型”变成“你的业务模型”。这里要特别说清楚一个概念迁移学习是一个大类微调是它最常用的实现手段。除了微调迁移学习还包括特征提取把预训练模型当固定特征提取器只改最后的分类头和直接迁移严格意义上参数不动、只做输出映射等。但在 Transformers 生态里大家讨论最多、落地最容易的就是微调。微调又分成全量微调、Freeze冻结部分层微调和 LoRA低秩适配等后面我会逐一展开。1.2 Transformers 库到底在做什么Hugging Face 的 Transformers 库本质上做了一件非常“省事”的事情把上千个预训练模型的权重、分词器、配置类统一封装成了同一套 API。你不需要关心某个模型是用 TensorFlow 还是 PyTorch 训练的也不用关心它的词表长什么样只要用AutoModel系列接口几行代码就能把模型加载出来。它的核心抽象有三个Model模型主体包含预训练权重。Tokenizer分词器把原始文本变成模型能读懂的 input_ids、attention_mask 等输入格式。Config模型配置负责指定层数、隐藏层大小、类别数等参数。微调的本质是训练过程训练过程就要涉及优化器、损失函数、学习率调度、数据分批。Transformers 库本身不处理训练循环它配合Trainer、datasets、evaluate、peft这些生态库才形成了一条完整的微调流水线。我见过很多同学一开始就在原始代码里自己写训练循环结果模型加载对了、数据处理错了排了半天发现是 padding 方向写反了。我的建议是先用 Transformers 官方推荐的标准流水线把流程跑通再根据自己的需求去魔改。2. 环境搭建与版本匹配第一步错后面全废2.1 版本匹配方法论PyTorch、CUDA、Transformers 怎样才不会打架在微调项目里环境问题占掉的时间比训练本身还多。尤其是老项目比如热词里面提到的transformers3.4.0很多人一装上就报错报错信息五花八门tokenizer加载不出来、Trainer参数不兼容、CUDA 版本不匹配等等。先说结论新项目不要用 3.x 版本直接上 4.x 甚至更新的版本。3.4.0 是 2020 年底的版本那时候的 API 设计和现在差别很大很多教程里的写法在那个版本下根本跑不通。如果你是因为某些老项目的requirements.txt锁定了这个版本那么你需要知道它对应的 PyTorch、CUDA 和 Python 版本组合是比较苛刻的。根据当时 Transformers 3.4.0 的发布背景常见的稳定组合是transformers 版本推荐 pytorch推荐 CUDA推荐的 Python3.4.01.5.0 / 1.6.010.2 / 11.03.6 / 3.7 / 3.84.302.0.0 / 2.1.011.8 / 12.13.8 / 3.9 / 3.104.402.212.13.9 / 3.10 / 3.11注意这个表格不是随意给的它遵循一个基本原则Transformers 的版本越新对 PyTorch 的要求越高对应的 CUDA 版本也越高。如果你非要用 3.4.0我实测可行的组合是torch1.5.0 transformers3.4.0 cudatoolkit10.2这个组合下用BertForSequenceClassification跑基线模型是没问题的。但如果你是第一次接触微调别在这个旧版本上浪费时间直接装 4.31 以上的版本生态最成熟、教程最多、报错也最好搜。2.2 我的实际安装方案与验证流程我的新项目环境配置一般是这样的conda create -n finetune python3.10 -y conda activate finetune conda install pytorch torchvision torchaudio pytorch-cuda11.8 -c pytorch -c nvidia -y pip install transformers datasets evaluate accelerate peft安装完之后一定不要直接上手训练先跑一个最小验证脚本确认环境没问题from transformers import AutoTokenizer, AutoModelForSequenceClassification, AutoConfig model_name bert-base-chinese tokenizer AutoTokenizer.from_pretrained(model_name) config AutoConfig.from_pretrained(model_name, num_labels2) model AutoModelForSequenceClassification.from_pretrained(model_name, configconfig) inputs tokenizer([我觉得这个电影很好看, 这个产品太难用了], paddingTrue, truncationTrue, return_tensorspt) outputs model(**inputs) print(outputs.logits.shape)如果这一步能输出torch.Size([2, 2])说明你的 Transformers、PyTorch、CUDA 链路是通的。很多人在这一步就挂了最常见的原因是tokenizer下载时网络中断导致缓存文件不完整。解决办法是提前用huggingface-cli download或者设置镜像源把模型下载到本地。另外强调一点GPU 不是必须的。如果你只是验证流程、做小批量试验CPU 也能跑只是慢。真正训练的时候再租卡或者用实验室服务器也不迟。但要注意如果你装了 CPU 版 PyTorch 之后再装 GPU 版经常出现不兼容。最干净的方式是创建独立环境把 GPU 版本一次装对。3. 数据与预处理模型吃不下原始文本3.1 从原始语料到 Dataset微调的第一步不是建模型而是把数据准备好。Transformers 生态推荐用datasets库它在底层帮我们做好了内存映射、批量处理、缓存比手动写 DataLoader 方便很多。我常用的做法是先把数据整理成一个标准的 dialect 格式再通过datasets加载。比如做中文情感分析数据可能是 CSV 文件里面有text列和label列from datasets import Dataset, DatasetDict import pandas as pd train_df pd.read_csv(train.csv) eval_df pd.read_csv(eval.csv) train_dataset Dataset.from_pandas(train_df) eval_dataset Dataset.from_pandas(eval_df) dataset_dict DatasetDict({train: train_dataset, eval: eval_dataset}) print(dataset_dict)这里有个小细节如果你的原始数据是 JSON 格式或者每条样本是{text: ..., label: 0}的字典列表直接用Dataset.from_list()会更高效。datasets库会自动识别字段名后面预处理的时候就不用再手动对列名了。如果你做的是大模型微调比如用 Qwen 这类指令模型数据格式通常是对话结构。常见的做法是每一行包含instruction、input、output三个字段然后通过一个模板拼成模型的输入。这一点和 BERT 这类模型不一样BERT 是直接输入一段文本和一个标签而生成式模型需要构造完整的指令模板。所以你要先搞清楚自己用的模型是“判别式”还是“生成式”这两种模型的预处理逻辑完全不同。3.2 Tokenizer 的使用要点padding、truncation 和 attention_maskTokenizer 是微调环节里最容易出错的部分。很多新手直接对整句话做tokenizer.encode()然后发现不同句子的长度不一样放进 batch 之后维度对不上。正确的姿势是使用批量处理函数并且设置paddingTrue和truncationTruefrom transformers import AutoTokenizer tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) def preprocess_function(examples): return tokenizer( examples[text], paddingmax_length, truncationTrue, max_length128, return_tensorspt ) tokenized_datasets dataset_dict.map(preprocess_function, batchedTrue)注意这里padding有几种策略paddingmax_length把每一条都 padding 到固定长度比如 128缺点是会浪费算力因为短句子后面全是[PAD]。paddingTrue只在 batch 内部 padding 到最长的那条节省算力但需要dynamic padding。Trainer内置的DataCollatorWithPadding可以实现这一点。我的推荐是在Trainer里用DataCollatorWithPadding而不是在map阶段就做max_lengthpadding。这样训练效率更高推理的时候也灵活因为长度不固定处理新数据时不需要每次重算。另外一个关键点是attention_mask。它告诉模型哪些 token 是真实内容哪些是 padding 出来的。如果你忘了返回attention_mask模型会把 padding 位置也当作有效内容去算注意力相当于把一堆“空字符”也纳入语义理解效果自然会差。Transformers 的tokenizer默认会生成attention_mask但如果你手动处理数据、直接用input_ids构造 batch那就要自己补上。4. 三种微调方法的落地对比全量、Freeze、LoRA4.1 全量微调效果最直接但显存压力最大全量微调就是把预训练模型的全部参数都放进训练过程更新每一层权重。这种方式的优点是模型可以充分适应新任务在数据量足够的前提下效果通常是最稳的。缺点是显存开销大因为优化器需要保存每个参数的梯度、动量等状态。以 BERT-base 为例参数量 1.1 亿如果用 AdamW 优化器每个参数需要 8 字节以上的额外内存参数本身 4 字节、梯度 4 字节、动量 4 字节、方差 4 字节这么一算仅优化器状态就超过 3GB。再算上激活值一张 11GB 显存的卡做 batch size 16 的 BERT-base 微调都很紧张。所以全量微调一般适合两种场景一是数据量足够大确实需要让模型所有层都重新适应二是显存充足比如 A100、多卡且任务和预训练领域差异较大。使用Trainer时全量微调不需要额外设置默认就是全量更新。只要你加载模型的时候没有加requires_gradFalse所有参数都是可训练的。训练代码里要注意设置per_device_train_batch_size和gradient_accumulation_steps显存不足时优先减小 batch size再用梯度累积弥补。4.2 Freeze 微调冻结一部分层省显存也防灾难性遗忘Freeze 微调指的是把模型的一部分层冻结不参与更新只训练剩下的层。常见做法是冻结 BERT 的所有 transformer encoder 层只训练最后的分类头或者是冻结前几层 encoder微调靠近输出端的高层。它的逻辑是靠近输入的层学到的是通用的词法、句法特征这些特征在你的下游任务中并不需要改变靠近输出的层学到的是和预训练任务有关的语义特征需要针对新任务做调整。所以冻结底层可以减少显存、加速训练同时也能缓解数据量小时微调导致的“灾难性遗忘”也就是模型把预训练知识忘光只记住了少量新样本。代码实现方式很简单加载模型后手动设置requires_gradfor param in model.bert.parameters(): param.requires_grad False # 如果只微调分类头BERT 参数全部冻结 for param in model.classifier.parameters(): param.requires_grad True然后训练时优化器只接收requires_gradTrue的参数optimizer AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr5e-5)这样显存占用明显下降实测在 6GB 显存下跑 BERT-base 情感分类没有问题。但要注意如果任务和你预训练模型的领域差异特别大比如用纯英文 BERT 处理中文业务数据冻结底层可能会限制效果因为模型压根没学会中文的表示你连底层特征都没训练等于用“瞎眼”的模型在硬撑。Freeze 微调更适合预训练模型本身已经覆盖了任务语言但任务形态不同的情况。4.3 LoRA 微调现在的首选方案参数少、效果好LoRALow-Rank Adaptation是目前大模型微调事实上的标准做法。它的思想很巧妙不更新原始权重矩阵 W而是把权重更新量 ΔW 分解成两个低秩矩阵 A 和 B 的乘积ΔW A × B其中 A 是一个 (in_dim, r) 的矩阵B 是一个 (r, out_dim) 的矩阵r 远小于 in_dim 和 out_dim。训练时只更新 A 和 B原始权重 W 冻结不动。这样一个 7B 参数的大模型LoRA 真正训练的参数量可能只有几百万到几千万显存需求断崖式下降。有人可能会疑惑既然只训练了小矩阵效果能比全量微调好吗实际上大量实验表明在大多数下游任务上LoRA 的效果和全量微调非常接近甚至在某些数据量小的场景下更好因为它天然带了正则化作用不容易过拟合。在 Transformers 生态里LoRA 的落地用peft库from peft import LoraConfig, get_peft_model, TaskType lora_config LoraConfig( task_typeTaskType.SEQ_CLS, r8, lora_alpha16, lora_dropout0.1, target_modules[query, value] ) model get_peft_model(model, lora_config) model.print_trainable_parameters()r是秩lora_alpha是缩放系数lora_dropout是丢弃率。这里我可以给一个经验值BERT 这类模型r8~16就够再大收益不明显反而增加显存大模型7B 以上常用r16~64。lora_alpha通常设置成r的 2 倍这是一个比较稳的起调。三种方式对比下来方式更新参数量显存占用适用场景训练速度风险点全量微调所有参数最高数据充足、算力充足慢过拟合、灾难性遗忘Freeze 微调部分参数中数据量中等、显存有限中需要人工设计冻结层LoRA低秩矩阵最低大模型微调、显存有限快秩太小可能欠拟合我现在实际工作里除了比赛冲榜会考虑全量微调其余默认 LoRA。原因很现实显存便宜不常有时间省下来才是真的。5. 实战案例用 Transformers 微调一个中文情感分析模型5.1 完整代码与训练脚本这里给一个可以直接跑通的完整案例数据集用简单的 CSV 格式字段为text和label标签 0 表示负面、1 表示正面。我用bert-base-chinese作为底座采用 LoRA 方式微调。import pandas as pd import torch from datasets import Dataset, DatasetDict from transformers import ( AutoTokenizer, AutoModelForSequenceClassification, DataCollatorWithPadding, Trainer, TrainingArguments ) from peft import LoraConfig, get_peft_model, TaskType # 1. 加载数据 train_df pd.read_csv(train.csv) eval_df pd.read_csv(eval.csv) train_dataset Dataset.from_pandas(train_df) eval_dataset Dataset.from_pandas(eval_df) # 2. 加载 tokenizer tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) def preprocess(examples): return tokenizer( examples[text], truncationTrue, max_length128, paddingFalse ) train_dataset train_dataset.map(preprocess, batchedTrue) eval_dataset eval_dataset.map(preprocess, batchedTrue) # 3. 加载模型并应用 LoRA model AutoModelForSequenceClassification.from_pretrained( bert-base-chinese, num_labels2 ) lora_config LoraConfig( task_typeTaskType.SEQ_CLS, r8, lora_alpha16, lora_dropout0.1, target_modules[query, value] ) model get_peft_model(model, lora_config) # 4. 配置训练参数 training_args TrainingArguments( output_dir./finetuned_model, per_device_train_batch_size16, per_device_eval_batch_size32, gradient_accumulation_steps1, num_train_epochs3, learning_rate2e-5, fp16True, evaluation_strategyepoch, logging_steps50, save_strategyepoch, load_best_model_at_endTrue, metric_for_best_modeleval_loss, ) # 5. 使用 Trainer 训练 data_collator DataCollatorWithPadding(tokenizertokenizer) trainer Trainer( modelmodel, argstraining_args, train_datasettrain_dataset, eval_dataseteval_dataset, tokenizertokenizer, data_collatordata_collator, ) trainer.train() # 6. 保存模型 model.save_pretrained(./finetuned_model_lora) tokenizer.save_pretrained(./finetuned_model_lora)这段代码有几个地方值得注意paddingFalse在map阶段先不 padding交给DataCollatorWithPadding动态处理这样每条训练样本都按自己的真实长度传入batch 内部再 padding省算力。fp16True在你显卡支持混合精度时开启能明显降低显存占用训练速度也会提升。load_best_model_at_endTrue配合metric_for_best_modeleval_loss训练结束后会自动把验证集上 loss 最低的 checkpoint 加载回来。5.2 训练过程监控与参数调整经验训练过程中最需要盯的三个指标是loss、eval_loss和learning_rate。loss下降速度太快不一定是好事。比如第一个 epoch 训练 loss 就掉到 0.1 以下说明学习率可能过大或者模型在死记样本通常这时候 eval loss 会先降后升出现典型的过拟合曲线。正常的训练曲线应该是训练 loss 平稳下降eval loss 在第二个 epoch 附近趋于平稳后面开始微升。如果 eval loss 迟迟不降排查顺序是学习率是否太小BERT 通常用 2e-5 到 5e-5低于 1e-5 训练速度会非常慢。数据预处理有没有问题可以打印一条 tokenizer 的输出人工看看内容是否符合预期。标签是否平衡如果标签比例严重失衡需要设置class_weight或用 F1 作为评估指标。如果显存不足优先改per_device_train_batch_size比如 16 降到 8再用gradient_accumulation_steps2保持等效 batch size 不变。等效 batch size 的计算方式是per_device_train_batch_size × 梯度累积步数 × GPU 数量。这个数值不要随意改动因为调参的时候你调整的是“全局 batch size”带来的训练稳定性而不是单纯看单卡显存。我一般建议先跑一个很小的子集几百条样本让训练流程完整跑通确认没有报错、能正常保存 checkpoint再用全量数据训练。这样可以避免数据格式问题在训练一天之后才暴露。6. 大模型微调的实际落地路径从 BERT 到 Qwen、LLaMA6.1 生成式模型微调与判别式模型微调的区别前面案例用的是 BERT 这类 encoder-only 模型它适合分类、实体识别这类任务。但从 2023 年之后大家聊的“大模型微调”更多指的是 Qwen、LLaMA、DeepSeek 这类 decoder-only 的生成式模型。这两类模型的微调方式在数据构造和训练目标上差别很大。判别式模型微调的目标是“输出一个标签”所以数据是(文本, 标签)配对生成式模型微调的目标是“输出一段文本”所以数据通常是(指令, 输入, 期望输出)的三元组在训练时拼成完整的指令模板。比如用户请把下面这段话翻译成英文我今天很开心。 助手I am very happy today.训练时模型读入的是左边这段完整文本但计算 loss 时只计算“助手”部分对应 token 的损失。这个“只计算输出部分损失”的操作非常关键如果连指令部分都算 loss模型会学着去“预测用户的提问”训练出来的模型回答问题会非常啰嗦。在 Transformers 生态里做生成式模型微调时要注意tokenizer是否设置了pad_token。很多生成式模型的 tokenizer 没有默认 pad_token需要手动设置if tokenizer.pad_token is None: tokenizer.pad_token tokenizer.eos_token否则在数据批处理阶段会出现各种奇怪的报错。6.2 大模型微调工具链为什么大家都用 llama-factory对于 7B 以上的模型自己手写训练循环还是有点吃力。这时候我更推荐用现成的训练框架比如 llama-factory 这类工具。它是一个把数据预处理、LoRA/QLoRA 配置、训练、评估、推理打包好的工具适合快速实验。我自己的使用体验是llama-factory 的价值在于它把“数据格式”统一了你只需要按照它的对话模板整理 JSON然后写一个 YAML 配置文件就能启动训练。它支持的模型种类很多包括 Qwen、LLaMA、DeepSeek 这些主流模型也内置了 LoRA、Freeze、全量微调等选项。但也别把它当成万能钥匙。工具化之后很多细节被隐藏了出了问题反而不容易排查。我遇到最多的是“数据格式对不上”导致训练时 loss 一直不变后来打开它生成的训练样本才发现指令和回答拼接符号错位了。所以如果你用这类工具一定要在训练前开启“预览样本”功能肉眼检查至少 10 条训练数据确认格式没问题再真正训练。大模型微调还有一个绕不开的话题是量化。如果显存不足以加载 7B 模型的 fp16 权重可以考虑 QLoRA也就是把基座模型量化成 4-bit再在量化后的底座上做 LoRA。这种方式让很多人在消费级显卡上也能微调 7B 模型。但要注意量化版本的效果和 fp16 版本会有细微差异如果你对效果要求很高还是优先保证显存足够再上 LoRA 而不是 QLoRA。7. 常见问题与排查技巧实录7.1 显存不足batch size、梯度累积与混合精度的组合拳显存不够是最常见的报错英文通常是一句CUDA out of memory。我见过有人为了凑一个大的 batch size 反复重启训练其实完全没有必要。按优先级处理先开fp16True这是零成本降低显存的方案。减小per_device_train_batch_size从 16 减到 8、4、2直到能跑起来。用gradient_accumulation_steps补回等效 batch size比如 batch size 减半到 4梯度累积步数设为 4等效 batch size 还是 16。检查是否加载了不必要的中间变量比如把数据一股脑tensor.to(cuda)放到显存里应该用 DataLoader 分批加载。一个容易忽略的问题验证阶段也会占显存。per_device_eval_batch_size设得过大同样会 OOM。验证阶段不需要梯度显存需求小一些但也要合理设置。7.2 模型加载与 Tokenizer 的版本兼容问题很多人卡在transformers版本导致的加载错误。比如有些老模型权重文件是基于旧版pytorch_model.bin格式新版 Transformers 加载时会有警告但一般能自动兼容。真正麻烦的是tokenizerBERT 系加载时如果缺少do_lower_case之类的配置会导致英文大小写处理方式不一致。排查思路很简单遇到加载报错先看版本号print(transformers.__version__)然后去对应版本的官方文档查 API 是否变更。如果项目日志里明确写了某个参数不识别大概率是新版本废弃了老参数删掉就好。7.3 微调效果不好先别急着换模型检查这四件事训练跑通了但效果不达标这种情况也很常见。我的排查清单如下数据质量有没有 label 标注错误有没有空文本有没有明显重复的样本这些都会直接影响效果。数据量太少的话换 LoRA 或 Freeze 而不是全量微调太少的话加数据增强同义词替换、回译、随机裁剪也是一个方案。学习率与训练轮数BERT 系模型一般 3 epoch 左右太久了容易过拟合。可以设置early stopping回调监控 eval loss 连续 2 个 epoch 不降就提前结束。指标定义分类任务用准确率不够全面时改用 F1生成任务用 BLEU 不一定合理可以人工抽样看生成结果。我见过太多人效果不好就马上换更大的模型结果更大的模型对数据量的要求更高反而更差。先把上面四件事排查掉往往能解决 80% 的问题。最后分享一个我自己实践下来很受益的习惯每次微调训练都把TrainingArguments的关键参数和验证指标记录成一份简单的实验日志标明“改了哪个参数、结果提升了多少”。这样你不会在反复调参的时候忘记自己已经试过哪些组合而且三个月后再回来看这个项目也能快速回忆起当时的决策过程。微调这件事与其说是技术问题不如说是一个“用系统化的方法逼近最优解”的工程过程。希望你也能在调试模型的路上慢慢建立起自己的实验感觉。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →