资讯动态

迁移学习实战:用Transformers库微调BERT与LoRA

发布时间:2026/9/10 5:47:33 来源:尧图企业网站定制
我在刚接触NLP那会儿总以为训练一个模型就得从零开始把整套网络结构重新设计一遍。直到有一次接到一个文本分类需求前辈丢给我一句“用BERT微调一下就行”我才真正理解什么叫迁移学习。现在无论你看哪篇大模型实战文章基本都绕不开“预训练微调”这套打法而Transformers库正是目前落地这套打法最主流的工具。这篇文章我打算把从环境搭建、数据准备、参数调到常见报错的一整套经验写出来主题就是“迁移学习怎么落地”重点放在Transformers库微调实操。内容会比较具体涉及可以直接复制的Python代码、参数选择和排查思路适合准备做文本分类、命名实体识别或者想把Qwen这类开源大模型往自己业务数据集上微调的同学参考。迁移学习听起来很高深但落到代码层面多数场景就是一个预训练模型加载进来改改输出层再用自己的数据训练几轮。这篇博文会先帮大家把概念捋清楚然后一步步实现一次完整的微调再往LoRA、LLaMA-Factory这类进阶玩法上延展。读完你会发现真正拦住你的往往不是模型结构而是版本、显存和数据质量。1. 先把迁移学习这件事想明白1.1 为什么NLP任务默认就走“预训练微调”深度学习刚火起来那几年很多NLP任务的做法是先随机初始化一个Embedding和编码器然后直接从自己的语料里学。问题是大多数实际任务的标注数据只有几千条、几万条这点数据量根本喂不饱一个深层模型。于是过拟合、泛化差、调参调到怀疑人生就成了家常便饭。迁移学习把这个问题换了个思路先在一个超大语料上用自监督任务把语言本身的规律学出来比如BERT用“盖住词猜原词”GPT用“预测下一个词”。这个阶段叫预训练产出的是通用语言表示不针对任何具体任务。到了下游任务我们不再随机初始化而是把预训练好的权重搬过来作为初始状态然后用少量标注数据接着训练。这个过程就是微调。我把迁移学习类比成“学开车”预训练阶段相当于在封闭场地练基本功微调阶段相当于拿到真实道路上跑一圈。你不需要每次都在封闭场地从头练只需要在真实路况下把方向盘、刹车这些动作再调到合适手感就行。对NLP来说预训练模型已经知道词怎么组合、句子怎么组织微调只是让这些知识服务于你的具体任务所以数据量要求低很多收敛也快很多。1.2 直推式迁移学习和其他迁移方式别搞混在文献里迁移学习分好几种类型面试和实操里容易绕晕的主要是归纳式迁移学习、直推式迁移学习和无监督迁移学习。很多人第一次看到“直推式迁移学习”这个术语会发懵其实它描述的是一个特定场景源领域和目标领域是同一个任务但两个领域的数据分布不一样。举个例子你在新闻文本上训练了一个情感分类模型现在要直接拿去给社交媒体评论做分类。新闻和评论的用词风格差异很大这就导致训练集和测试集分布不一致如果直接硬用效果会明显变差。直推式迁移学习要解决的就是这种“源任务和目标任务相同但领域数据分布不同”的情况。在文本分类、垃圾文本识别这类任务里很常见实操上常用领域自适应、数据增强加微调混合的方式来缓解。而我们平时常说的“先用BERT预训练再用BERT做分类”严格来说是归纳式迁移学习源任务预训练语言模型和目标任务分类不同但共用一个模型结构通过微调把任务差异补上。明白这个区别看论文时才不会被概念绕进去。1.3 微调到底在“调”什么很多人以为微调就是把整个模型重新训练其实完全不是。预训练模型的底层网络已经学到了非常稳定的通用语法和语义特征这些特征不大需要变动真正需要调整的是靠近输出层的部分。所以微调时的默认做法是加载预训练权重作为初始化替换掉模型顶部的任务相关层比如BERT分类模型会把输出层换成带num_labels的线性层然后用较小的学习率在全量参数上继续训练。一个值得注意的细节是如果我们把学习率设得太大预训练积累的特征会被新数据“冲掉”最终效果可能比训练不足还差。这也是为什么绝大多数Transformers微调案例会把学习率设置在2e-5到5e-5之间。微调过程中底层参数更新幅度通常远小于顶层但Transformers默认是整模型一起反向传播不做层冻结这样实现简单效果也足够好。只有在大模型微调的场景下为了节省显存和时间才会引入冻结部分参数或者LoRA这类高效微调方法。2. 环境准备版本匹配是第一道坑2.1 Transformers版本和PyTorch/CUDA到底怎么搭很多人拿到项目先去装包结果一跑就报错最后发现是版本和CUDA不匹配。这里我先泼一盆冷水如果你用的是比较旧的Transformers版本比如3.4.0那就要先确认它到底是哪个时代的东西。transformers3.4.0是2020年中旬发布的那个阶段官方推荐的是PyTorch 1.3到1.6CUDA 10.1或10.2。如果你在一个很新的CUDA 12.x环境里直接装它大概率会出现底层算子不兼容、编译报错或者GPU不可用的问题。下面这张表是我根据实际使用经验整理的参考匹配关系不是唯一答案但能省掉不少折腾时间Transformers 版本推荐 PyTorch推荐 CUDA使用说明3.4.01.3 ~ 1.610.1 / 10.2老项目维护用新项目不建议碰4.20 ~ 4.301.12 ~ 2.011.6 / 11.7比较稳的过渡版本4.31 ~ 4.442.0 ~ 2.211.8 / 12.1我目前主力使用的版本区间需要注意这里说的CUDA是指PyTorch自带的那一套CUDA运行时不一定要和系统驱动完全一致。你只需要保证显卡驱动版本足够新然后安装对应的PyTorch轮子就行。具体怎么看驱动是否支持可以在终端执行nvidia-smi查看右上角的CUDA Version只要这个版本号高于或等于你安装PyTorch需要的CUDA版本基本上就没问题。如果你的项目还在用transformers3.4.0我会建议你分两步处理先看项目依赖是否锁死如果只是历史代码最好把代码升级到新的Transformers API再换到PyTorch 2.x加CUDA 11.8组合性能和兼容性都会好很多。老版本除了API不兼容很多预训练模型新导出的格式它也不支持强行用只会给自己添堵。2.2 一套干净可复现的微调环境怎么建我比较推荐用conda建独立环境不要直接在base环境里乱装不然PyTorch、TensorFlow、Triton这些包很容易互相影响。下面这段命令是我创建一个微调环境的通用流程当前CUDA版本选择11.8对应PyTorch 2.1.1conda create -n finetune python3.10 -y conda activate finetune pip install torch2.1.1 torchvision0.16.1 torchaudio2.1.1 --index-url https://download.pytorch.org/whl/cu118 pip install transformers4.31.0 datasets2.14.6 evaluate0.4.1 accelerate0.24.1 peft0.7.1装完之后不要急着写代码先跑几行简单的Python命令验证环境能不能识别GPU和加载模型。我第一次装完后忘了装accelerate结果Trainer训练时报错说缺少分布式依赖这个问题只在训练阶段暴露所以提前验证很有必要。import torch from transformers import BertTokenizer, BertForSequenceClassification print(torch.cuda.is_available()) print(torch.__version__) tokenizer BertTokenizer.from_pretrained(bert-base-chinese) model BertForSequenceClassification.from_pretrained(bert-base-chinese, num_labels2) print(tokenizer(迁移学习到底怎么落地))如果能打印出模型输出说明基础链路已经通了。这一步还能顺便确认预训练模型的下载权限和网络状况避免真正训练时卡在模型下载上。2.3 模型下载不动或太慢怎么办国内访问Hugging Face有时候非常痛苦模型动不动几百MB甚至几个GB。常规做法是从国内镜像站点下载比如hf-mirror.com设置环境变量可以加速。在Linux或者Mac上临时设置export HF_ENDPOINThttps://hf-mirror.comWindows PowerShell可以用$env:HF_ENDPOINThttps://hf-mirror.com设置好之后from_pretrained会自动从镜像地址下载。这个方法我在很多项目里都在用速度比直接连官方源稳定不少。如果公司有内网缓存或者自己机器上已经下载过模型文件也可以先把模型下载到本地目录加载时直接传本地路径彻底绕开下载问题。3. 核心实战用Transformers微调一个文本分类模型3.1 准备数据别小看这个环节大多数教程都会用Hugging Face上的现成数据集但如果你在公司做项目数据往往在本地CSV、Excel或者数据库里。这里我就用本地的CSV文件做示范格式很简单一列text一列label。假设我们要做一个中文评论情感二分类positive和negative各两千条。用datasets库加载本地数据然后拆成训练集和验证集from datasets import load_dataset dataset load_dataset(csv, data_filesdata.csv, splittrain) dataset dataset.train_test_split(test_size0.2, seed42, stratify_by_columnlabel) train_dataset dataset[train] eval_dataset dataset[test] print(train_dataset) print(train_dataset.unique(label))这里有两个细节值得注意。第一train_test_split里面stratify_by_column参数可以在数据不平衡时保持训练集和验证集的标签比例一致如果二分类正负样本差距很大这个参数特别有用。第二数据量虽然不大但也要检查是否有重复样本、空文本、标签噪声否则后面训练出的模型会在验证集上表现很好一到线上就露馅。我见过太多项目模型训练曲线很漂亮最后坏在线上一查原因训练集里有一条同一条错误数据被复制了几十次。3.2 数据预处理Tokenizer环节不能省文本不能直接喂给Bert需要先转成input_ids、attention_mask和token_type_ids。这个过程看起来就是使用tokenizer但有几个细节容易出错。padding策略在训练集和验证集上不太一样训练集大多用长batch靠tokenizer动态padding到batch内最长实现效率最大验证集则推荐padding到模型最大长度或固定长度避免batch内样本长度差异过大导致验证结果波动。下面是对训练集做tokenize的函数from transformers import AutoTokenizer tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) def tokenize_function(examples): return tokenizer(examples[text], truncationTrue, max_length128, paddingmax_length) tokenized_train train_dataset.map(tokenize_function, batchedTrue, remove_columns[text]) tokenized_eval eval_dataset.map(tokenize_function, batchedTrue, remove_columns[text]) tokenized_train.set_format(torch) tokenized_eval.set_format(torch)truncation控制在128是因为很多短文本分类任务根本不需要512的长度而长度越长显存占用和训练时间都会明显增加。你可以在初步实验中先用128跑通再评估是否需要更长的上下文。如果是做文本匹配、长文档分类再考虑256或者512。3.3 加载预训练模型把BERT改造成分类模型用AutoModelForSequenceClassification加载非常省事。它的作用是把预训练模型顶部的池化输出接到一个Linear分类层上。这里num_labels设为2就是二分类。from transformers import AutoModelForSequenceClassification model AutoModelForSequenceClassification.from_pretrained( bert-base-chinese, num_labels2 )疑问来了这个模型的输出层权重是随机初始化的会不会破坏整体性能其实不会。预训练模型主体部分依然保留着知识顶部新加的线性层很快就能从数据中学到合适的映射关系这也是迁移学习比从零训练高效的关键。整个过程里只有顶层需要相对较大的梯度来学习新任务所以经验值是整体学习率设小一点比如2e-5。3.4 训练参数Trainer为什么大家都在用Transformers库的Trainer封装了训练循环、梯度累积、日志、评估、保存省去写PyTorch训练脚本的重复劳动。Trainer用起来很舒服但前提是你要理解TrainingArguments里这些参数都是什么意思下面这份是我在二分类任务里常用的配置from transformers import Trainer, TrainingArguments training_args TrainingArguments( output_dir./results, evaluation_strategyepoch, save_strategyepoch, learning_rate2e-5, per_device_train_batch_size16, per_device_eval_batch_size32, num_train_epochs3, weight_decay0.01, logging_dir./logs, logging_steps50, save_total_limit2, fp16True, report_to[], ) trainer Trainer( modelmodel, argstraining_args, train_datasettokenized_train, eval_datasettokenized_eval, tokenizertokenizer, )learning_rate为什么是2e-5而不像普通CNN训练那样用0.001因为预训练模型的权重本身已经处于一个很“好”的位置如果学习率太大一步更新就可能把学到的语言知识冲得七零八落。这个学习率是BERT时代无数实验总结出来的经验区间没有特殊理由不建议标新立异。fp16True使用混合精度训练在V100、A100、RTX 30/40系这些支持FP16的显卡上能明显减少显存占用并加速训练。如果显卡不支持跑起来会报错那就把它改成False。3.5 评估与推理别只看LossTrainer默认只打印loss我们还需要自定义评估指标。用evaluate库加载准确率或者F1都行import evaluate import numpy as np accuracy evaluate.load(accuracy) def compute_metrics(eval_pred): logits, labels eval_pred predictions np.argmax(logits, axis-1) return accuracy.compute(predictionspredictions, referenceslabels) trainer Trainer( modelmodel, argstraining_args, train_datasettokenized_train, eval_datasettokenized_eval, compute_metricscompute_metrics, ) trainer.train()训练完之后用Trainer.evaluate()看一下验证集指标。如果效果好接下来要做的是保存模型然后加载回来看几个真实例子trainer.save_model(./my_bert_classifier) tokenizer.save_pretrained(./my_bert_classifier) from transformers import pipeline classifier pipeline(text-classification, model./my_bert_classifier, tokenizer./my_bert_classifier) print(classifier(这家店的川味很正宗下次还会再来))这里重点检查的是训练数据和验证数据之外的新文本。如果你发现预测结果总偏向某一个类别大概率不是模型的问题而是数据本身存在倾向性。这个阶段多花点时间后续上线会少踩很多坑。4. 进阶玩法从全量微调到LoRA微调4.1 大模型全量微调为什么让人头疼BERT参数量是1亿级别全量微调还能勉强靠单卡跑。到了Qwen、Llama这类7B、13B甚至更大规模的模型全量微调对显存和算力的要求就变得非常苛刻。训练过程中优化器状态、梯度、激活值叠在一起动辄几十GB显存个人开发者的消费级显卡基本跑不动即使跑得动训练时间也很感人。而且全量微调还有个隐患当数据量少时大模型容易在微小数据集上过拟合学到的不是任务逻辑而是数据记忆导致通用能力下降也就是常说的“灾难性遗忘”。大模型时代行业更常用的是参数高效微调。这类方法只更新极少一部分参数却能达到接近全量微调的效果。LoRA是其中最出名的一种。4.2 LoRA的核心思路用低秩矩阵模拟大幅更新LoRA不需要调整原模型参数而是在模型权重旁边插入两个小矩阵用它们来模拟权重的增量。公式不展开打个比方原来一个全连接层需要更新4096×4096个参数LoRA把它拆成4096×8和8×4096两个小矩阵训练时只更新这两个小矩阵冻结原来的大矩阵。等到推理时再把增量合并回去甚至不增加额外推理耗时。可训练参数量一下子从千万级降到几十万级显存占用和训练时间自然大幅下降。在Transformers生态里配合微软开源的PEFT库LoRA的代码实现非常简洁。peft.LoraConfig里几个核心参数r秩通常取8或16增长能提高表达能力但也增加计算量alpha缩放因子一般设成r的2倍或与r相同控制更新强度target_modules目标模块名需要根据模型结构设置比如常见的qkv层task_type当前任务类型比如CAUSAL_LM对应自回归语言模型。4.3 用LoRA微调Qwen这类大模型这里我以Qwen系列为例实际操作也适用于其他GPT风格模型。首先是加载模型和分词器然后把模型转成bf16或fp16以降低显存import torch from transformers import AutoModelForCausalLM, AutoTokenizer from peft import LoraConfig, get_peft_model, TaskType model_name Qwen/Qwen2.5-0.5B-Instruct tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForCausalLM.from_pretrained( model_name, torch_dtypetorch.bfloat16, device_mapauto, ) lora_config LoraConfig( task_typeTaskType.CAUSAL_LM, r8, lora_alpha16, lora_dropout0.05, target_modules[q_proj, k_proj, v_proj, o_proj], ) model get_peft_model(model, lora_config) model.print_trainable_parameters()配置好之后训练部分和上一节很像同样可以使用Trainer。不过做指令微调时要特别注意数据集格式。以Qwen为例通常先把输入和输出拼成对话模板再tokenizedef format_example(example): prompt f|im_start|user\n{example[instruction]}\n|im_end|\n|im_start|assistant\n return tokenizer(prompt, paddingmax_length, max_length512, truncationTrue)指令微调数据集里的instruction、input、output三种字段在网上很容易找到模板代码。不过生成时模型的回答质量很大程度上取决于数据质量而不仅仅是LoRA参数。如果你想微调qwen-vl-4b这类视觉语言模型思路也是类似的只是加载模型时要使用对应的多模态模型类比如AutoModelForImageTextToText并确认Transformers版本支持该架构。视觉模块和文本模块可以一起LoRA也可以冻结视觉塔一般先用默认配置跑通再根据效果调整。4.4 用LLaMA-Factory这类工具省下写代码的时间如果你不是为了学习细节而是想快速验证某个大模型在自己数据上的效果强烈推荐用LLaMA-Factory这类工具。它把数据加载、LoRA、训练、推理都封装成了标准流程还提供了Web界面。我在给学生演示大模型微调时就经常用它因为只需要准备数据、选模型、填几个参数点启动就能跑训练曲线和Loss都直接展示出来。这样能避免初学者被一大堆环境配置劝退也能快速对比不同模型的微调效果。这类工具还内置了很多公认可用的指令数据格式支持全量微调、LoRA、QLoRA等对于要在本地部署和实验的场景非常方便。不过生产环境我通常还是会自己写训练脚本因为可控性更强出问题的时候更容易定位。4.5 指令微调数据集怎么造才不翻车大模型微调和传统分类微调有一个明显区别分类任务数据通常是“text label”而指令微调数据是“指令 输入 期望输出”的三元组结构。数据质量决定了模型能否学会按指令回答。我的建议是初期先用几百条高质量数据验证全流程不要一上来就投几千条。你会发现几百条精心设计的样本往往比几千条粗糙样本训练出来的效果更好这是大模型微调里普遍存在的情况。具体制作数据时尽量保证回答格式统一标点、换行、语气都要一致千万不要一会儿用简体中文回答一会儿又用英文回答。也不要指望模型通过微调学会新的知识微调更擅长改变输出风格和遵循特定指令。如果数据集里出现了超出模型本身知识范围的错误答案LoRA只会让模型在相关问题上“一本正经地胡说八道”。5. 实战常见问题与排查技巧5.1 GPU显存不够怎么办微调实战里遇到最多的坑就是显存爆炸。训练BERT模型时如果per_device_train_batch_size设为8都会OOM不要硬试先检查是不是开了fp16其次把batch size调小到4或2。但batch变小会影响梯度估计稳定性那就需要开梯度累积training_args TrainingArguments( per_device_train_batch_size2, gradient_accumulation_steps8, )这样相当于每16个样本更新一次参数效果接近于batch size16但显存占用却小得多。训练大模型时除了batch size和梯度累积还可以开启8bit或4bit量化加载配合QLoRA让消费级显卡也能跑7B模型的微调。另外如果是生成类任务在训练时把model.config.use_cache设为False不然保存模型或推理时可能出现缓存占用显存的问题。5.2 Loss一直在降但测试效果很差这个现象十有八九是过拟合或者数据泄漏。过拟合的典型表现是训练Loss降到很低验证Loss却在某个点开始回升。遇到这种情况优先检查训练数据是不是太少了可以考虑用预训练模型做数据增强或者把dropout调大一点。LoRA的lora_dropout也可以从0.05提高到0.1。其次是检查数据是否泄漏比如验证集里出现了训练集的重复样本或者文本预处理时把标签信息混进了特征里。我在一个命名实体识别项目里就遇到过清理数据时没有去掉标签字段结果模型在验证集上F1高达99%线上几乎不能用。5.3 老版本Transformers的相关坑遇到那种模型加载报错、API不存在、优化器报错先看Transformers版本。比如transformers3.4.0里面启用混合精度的写法还不太稳定需要对照当时的官方文档和现在新版代码有很多差异。最直接的排查方法是用pip show transformers看版本如果是3.x尽量迁移到4.x。如果必须使用老版本就按照第一节表格里的匹配关系装好对应的PyTorch和CUDA并保持依赖环境不随意升级。我的经验是老项目能不动就不动但新项目一定要用新版本长痛不如短痛。5.4 微调后模型出现重复生成或者胡言乱语在做大模型LoRA微调时经常有人发现模型训练完反而不会说话了输出要么重复要么乱码。原因主要有三类一是学习率太高破坏了预训练能力二是数据中普遍存在重复表达模型学会的是重复句式三是max_length设置太小模型生成到长度上限后开始自我重复。排查时先用一段指令看生成结果把num_beams设为1调大max_new_tokens再把temperature调低看是否改善。如果依然重复就把学习率降到1e-5以下再试。5.5 我的实操体会先跑通再优化在一次又一次踩坑之后我总结出一个非常朴素的方法不管用什么模型、什么数据先拿一个极小的数据集把完整流程跑通再上全量数据。先用batch_size1训练一个step确认训练脚本能正常反传然后用100条数据训练3个epoch看看Loss能不能下降验证集指标能不能算出来最后再上全量数据同时调学习率、batch size和epoch。这个习惯帮我省下了大量排查时间。迁移学习落到Transformers库上难度并不在于理解注意力机制而在于把数据和代码之间的链路一次次跑顺。每个任务都可能有自己的数据脏、格式乱、版本坑能跑通一次后面就再也不会觉得它们有多难。先动手把第一个模型跑出来你离“迁移学习落地”就已经不远了。

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

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

免费获取报价