多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

HuggingFace Trainer实战:从文本分类到问答的微调指南

HuggingFace Trainer实战:从文本分类到问答的微调指南 1. 训练循环琐事缠身Trainer 到底帮你省了什么刚接触 HuggingFace 的时候我干过一阵子蠢事明明只是调个模型做文本分类却要自己写 batch 循环、管梯度清零、算 loss、写断点续训还要老着脸皮在 validation 上手动跑一遍算准确率。每一步都不难合在一起却能把你一半的心智占用掉真正该做的模型分析和数据调试反而没精力做。后来我才把目光真正转向 Trainer。它不是给那些自己造轮子的人准备的也不是什么黑魔法它就是 HuggingFace Transformers 生态提供的一套完整训练封装你只需要把模型、数据集、训练参数丢给它剩下的事情它替你兜底——梯度累积、学习率调度、混合精度、分布式训练、checkpoint 保存与恢复、评估循环、日志上传。听起来像广告实际操作下来确实基本是这个体验。这个项目实战文章不是讲 API 文档而是用我实际跑通过的两类任务把 Trainer 的训练链路从头拆到尾数据集怎么预处理、TrainingArguments 里哪些参数最值得调、Trainer 实例怎么组装、训练日志怎么看、常见报错怎么排查。适合两类人一类是把 PyTorch 训练循环写得足够熟了、想把手从样板代码里解放出来的人另一类是刚转过来、被网友推荐用 Trainer 但不知道从哪下手的新手。2. 训练一个文本分类模型Trainer 的标准起手式2.1 安装依赖与数据准备先说明环境。我在 Python 3.10 环境下用的是 PyTorch 2.1 版本。安装 Transformers 和 Datasets 库的时候我建议直接装带评估功能的版本因为 Trainer 在训练过程中如果指定了compute_metrics它会自动在验证集上跑评估并打印指标这不是可选项而是核心用法pip install transformers[torch] datasets accelerate evaluate这个命令里我特别加了accelerate不是闲的。Trainer 在底层做分布式训练或者混合精度调度的时候依赖 Accelerate 库不装的话你用fp16True或者多 GPU 启动会报一堆看不懂的错。evaluate库则是后面计算 F1 分数要用的。数据我用的是一组中文商品评论情感分类数据二分类positive 和 negative。数据格式如下这个耳机的降噪效果出乎意料地好 - positive 续航能力太差了两小时就没电 - negative加载本地数据用load_dataset直接读 CSV 文件就行。这一步有新手容易踩坑的点Trainer 对数据集的格式要求是每个样本都能根据字典里的 key 索引出来也就是说你的数据得是Dataset对象不能是 pandas DataFrame。我第一次就直接塞了个 DataFrame 进去结果 Trainer 在构建 dataloader 的时候当场报错。所以数据准备这个环节我们认真走一遍from datasets import load_dataset dataset load_dataset(csv, data_files./data/reviews.csv, splittrain) dataset dataset.train_test_split(test_size0.2, seed42)2.2 分词与数据集映射Tokenizer 这步没人能躲掉。我踩过一次印象很深的坑把 tokenizer 的paddingTrue和truncationTrue一次性写到map函数里结果训练的时候显存直接爆了——因为我在每个样本上做了 padding 到最大长度而 Trainer 本身会在 collator 阶段按 batch 动态 padding。两次 padding 叠加浪费了大量显存。正确做法是在预处理阶段保持数据原始长度只做截断padding 交给DataCollatorWithPadding在组 batch 时动态做from transformers import AutoTokenizer tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) def preprocess_function(examples): return tokenizer(examples[text], truncationTrue, max_length128) tokenized_datasets dataset.map(preprocess_function, batchedTrue, remove_columns[text])remove_columns[text]这个步骤很多人忽略。不删掉原始文本的话这个列会一直跟着数据传到模型输入里Trainer 组装 batch 的时候会因为多出来的字段直接崩报错信息还特别隐晦。以我的经验预处理完就清理不需要的列是这类问题最省心的预防手段。最后要把标签转成数字 ID这里直接用一个简单的字典映射就行label_mapping {positive: 1, negative: 0} tokenized_datasets tokenized_datasets.map(lambda x: {labels: label_mapping[x[label]]})3. 核心实操用 Trainer 跑完一次完整的训练流程3.1 关键的训练参数解析与配置这里我把 TrainingArguments 的每个常用参数都过一遍同时标注我的推荐值。这个环节是 Trainer 实践里最值得花时间的地方。from transformers import TrainingArguments training_args TrainingArguments( output_dir./results, num_train_epochs3, per_device_train_batch_size16, per_device_eval_batch_size32, learning_rate2e-5, weight_decay0.01, warmup_ratio0.1, logging_steps50, eval_strategyepoch, save_strategyepoch, load_best_model_at_endTrue, metric_for_best_modelf1, fp16True, report_tonone, )逐个说。output_dir是所有 checkpoint 和日志的存放目录这个目录如果不存在Trainer 会自动创建。per_device_train_batch_size和per_device_eval_batch_size是“每张卡”的 batch size。如果你有两张 GPU设置16意味着实际 batch size 是32如果用了分布式。新手往往把单卡 batch size 当成全局 batch size然后调多卡并行时发现显存开销超出预期。eval_strategy这个参数在新版库中取代了evaluation_strategy官方废弃了旧名字。我见过好几个人在升级 transformers 4.46 之后还接着用旧参数名跑起来报 “unexpected keyword argument” 错误。如果你升级库后旧代码失效了多半是这个问题。warmup_ratio0.1表示前 10% 的训练步数里学习率从 0 线性增长到预设的learning_rate。对 BERT 这类模型特别是用 AdamW 优化器时warmup 能避免前期因为学习率过高导致的 loss 剧烈震荡。这个比值在大模型上常常设到 0.05 或 0.1如果你数据量不大也可以设 0.06 左右。load_best_model_at_endTrue加上metric_for_best_modelf1是标准的“训练结束后自动加载验证集上 F1 最高的 checkpoint”的组合。注意这个机制只有在save_strategy和eval_strategy在频率上保持一致时才能生效——比如两者都设成 “epoch”或者两者都设成steps500。一个设 epoch 一个设 steps 的话模型的加载逻辑可能找不到 checkpoint。fp16True是在支持混合精度的 GPU 上开启半精度训练。这个设置能节省约一半显存同时训练速度提升 20% 到 40%。如果你的 CPU 训练这个选项必须关掉否则会报错。report_tonone是为了关掉默认的 wandb 日志上报。我经常在本地测试时忘记关这个然后 wandb 库没装或者没登录训练一启动就卡住或者报错。纯本地实验场景建议显式设置成none想需要看可视化再手动开启。3.2 组装 Trainer 并完成训练把模型、参数、数据集、分词器和数据收集器组合在一起的这一段是整个 Trainer 实战的组装环节from transformers import AutoModelForSequenceClassification, Trainer, DataCollatorWithPadding from evaluate import load import numpy as np model AutoModelForSequenceClassification.from_pretrained( bert-base-chinese, num_labels2 ) data_collator DataCollatorWithPadding(tokenizertokenizer) accuracy_metric load(accuracy) f1_metric load(f1) def compute_metrics(eval_pred): logits, labels eval_pred predictions np.argmax(logits, axis-1) accuracy accuracy_metric.compute(predictionspredictions, referenceslabels) f1 f1_metric.compute(predictionspredictions, referenceslabels, averagebinary) return {accuracy: accuracy[accuracy], f1: f1[f1]} trainer Trainer( modelmodel, argstraining_args, train_datasettokenized_datasets[train], eval_datasettokenized_datasets[test], compute_metricscompute_metrics, data_collatordata_collator, tokenizertokenizer, )这里如果你传了tokenizer给 Trainer它会在训练前自动把所有 checkpoint 里的模型权重和词典对齐。这对微调路径上 tokenizer 发生过变化的模型很关键——比如你用一个大模型继续微调原始训练用的 tokenizer 版本和当前版本 vocabulary 不一致不处理的话加载权重就会静默出错。compute_metrics接收的是EvalPrediction对象里面包含模型原始的 logits 和 label。很多人第一次在这写成接收predictions, labels两个位置参数结果报错说参数数量不匹配。记住Trainer 只传一个EvalPrediction对象进来。一切就绪之后执行trainer.train()训练结束后模型已经是最优检查点的状态。我推荐在训练之后把模型和 tokenizer 单独保存一份不跟 checkpoint 混在一起。checkpoint 目录里有一堆优化器状态和训练配置直接拿去推理不仅浪费空间还容易把pytorch_model.bin跟model.safetensors搞混trainer.save_model(./best_model) tokenizer.save_pretrained(./best_model)推理阶段加载方式可以和微调前完全一致from transformers import pipeline classifier pipeline(text-classification, model./best_model) result classifier(音质非常清晰人声还原度高) print(result)3.3 训练日志解读loss 曲线与学习率变化训练启动后终端里会滚动输出这样的日志{epoch: 1.0, eval_loss: 0.2345, eval_accuracy: 0.9123, eval_f1: 0.9001, learning_rate: 1.2e-05}看一眼 loss 的数值与变化趋势就能判断训练状态是否正常。有一个常见的误区是 loss 最开始很高就觉得模型出了问题。正常的 BERT 微调在 2e-5 左右的学习率下第一个 step 的 loss 在 0.5 到 0.8 附近完全正常后面逐步降到 0.1 以下。如果你第一个 step loss 就是个位数甚至十几多半是标签映射写错了。另一个值得关注的值是learning_rate。注意它每步都在变化因为 warmup 加线性衰减的调度器在工作。我见过有人自己写了 lr scheduler 又同时传给了 Trainer结果训练时两边互相覆盖。Trainer 的原则是args里的参数已经定义了 optimizer 和 scheduler 的行为除非你特别清楚自己在做什么否则不要手动再传 optimizer 和 scheduler。4. 进阶实战用 Trainer 训练一个问答模型分类任务跑通之后建议再来一个 QA 任务巩固。我在实际项目里用的是bert-base-chinese在中文阅读理解数据集上做抽取式问答这一过程可以充分体验 Trainer 在处理多输入特征时的感知。抽取式问答的输入是 question context输出是答案在原文中的起始位置和结束位置。4.1 问答数据的处理方式预处理的区别主要在 tokenizer 的调用方式上def preprocess_qa(examples): questions [q.strip() for q in examples[question]] contexts examples[context] inputs tokenizer( questions, contexts, max_length384, truncationonly_second, stride128, return_overflowing_tokensTrue, return_offsets_mappingTrue, paddingFalse, )这里有两个关键设置。truncationonly_second保证超长文本只截断 context 那一部分question 保持完整。比如 SQuAD 和中文 DuReader 数据里很常见的长上下文场景如果两边一起截断问题的重要部分很容易被切掉。stride128和return_overflowing_tokensTrue则实现滑动窗口切分一个超长 context 被切成多段每段和 question 组成一个训练样本窗口之间重叠 128 个 token防止答案正好落在边界被切没。返回的offset_mapping用来把字级别的位置映射回原文位置。这一步最容易出 bug窗口滑动后样本里 token 对应的字符位置偏移了如果不重新映射训练出来的模型预测位置全是错的但 loss 还正常下降。4.2 用 Trainer 做 QA 训练与预测模型换成AutoModelForQuestionAnswering训练参数几乎和分类任务一样——这也是 Trainer 最方便的地方from transformers import AutoModelForQuestionAnswering model AutoModelForQuestionAnswering.from_pretrained(bert-base-chinese) trainer Trainer( modelmodel, argstraining_args, train_datasettokenized_qa[train], eval_datasettokenized_qa[validation], data_collatordefault_data_collator, ) trainer.train()评估指标这里我没有用内置的compute_metrics因为 QA 任务的指标是 EMExact Match和 F1需要把模型的预测位置解码成文本后和标准答案字符串做对比。这个解码过程强烈建议单独写后处理脚本不要在compute_metrics里跑。原因很简单一次评估要对整个验证集预测解码耗时较长在训练过程中频繁跑评估会拖慢很多时间。我的习惯是先训练完拿到最优 checkpoint再用单独的推理脚本去验证集上算 EM 和 F1predictions trainer.predict(tokenized_qa[validation]) # 从 logits 里拿到 start/end 位置 start_logits, end_logits predictions.predictions start_idx start_logits.argmax(-1) end_idx end_logits.argmax(-1) # 然后结合 offset_mapping 映射回原始文本切片这种“训练和评估解耦”的写法相比所有指标都绑在 Trainer 上在高强度迭代实验时更灵活。4.3 断点续训中途停了不用从头再来Trainer 另一个非常实用的能力是断点续训。我的经验是长训练任务几乎一定会遇到断点恢复的需求。训练中途如果断了只要 output_dir 里有 checkpoint这样就可以接上trainer.train(resume_from_checkpointTrue)它默认会读取 output_dir 里最新的 checkpoint然后接着原来的 epoch 和 global step 继续跑学习率调度器也会从当前进度恢复。如果你希望从指定 checkpoint 续训直接传路径字符串trainer.train(resume_from_checkpoint./results/checkpoint-1500)resume 之后需要注意日志里的 epoch 是从小数接下去的而不是从 0 开始。比如你训练到第 3 个 epoch 的第 45% 断了恢复后 epoch 显示 2.45 之类。很多人看到这个数值以为日志错了其实这是 global step 换算回 epoch 的正确表现。5. 离线场景的缓存策略与模型复用5.1 在受限网络环境中使用预训练模型接下来说一个我实际开发中经常遇到的场景目标机器没法直连公网。模型文件动辄几百 MB每次都在线拉肯定不现实而且会随机卡在某个权重文件上下到一半。我建议取网速条件好或流量不受限的环境提前把模型下好然后拷到目标机器上改用本地路径加载model_path ./local_models/bert-base-chinese model AutoModelForSequenceClassification.from_pretrained(model_path) tokenizer AutoTokenizer.from_pretrained(model_path)这里的目录结构只需要有config.json和model.safetensors或pytorch_model.bin以及 tokenizer 的三件套vocab.txt、tokenizer_config.json、tokenizer.json即可。如果你用save_pretrained存出来的路径这个格式就是齐的直接当成一个本地模型库目录来用。5.2 设置浏览器缓存目录与多环境复用Transformers 默认把从网络下载的模型统一缓存在用户目录下的~/.cache/huggingface里。这个缓存的路径是可以改的对于离线部署或者多用户共享机器非常有用。在 bash 环境中可以通过设置环境变量指定export HF_HOME/data/shared/hf_cache或者在 Python 代码内部于 import transformers 之前设置。我推荐放到项目根目录的配置文件里统一管理这样不同项目各用各的缓存不会互相污染import os os.environ[HF_HOME] /data/shared/hf_cache我实际用下来的体会是Transformers 库版本的变动偶尔会导致缓存结构变化如果你想重新下载全部模型而非使用旧缓存把 HF_HOME 指向一个全新的空目录是最干净的方案。同时本地路径加载方式不受缓存机制限制稳定性更高。6. 常见问题排查与实操技巧6.1 报错实录与处理方案这里整理了我实际踩过的高频坑每一条都对应具体的报错或异常现象CUDA out of memory显存不足是新手第一杀手。出现这个报错的固定套路是先看是否在预处理阶段做了多余 padding这是最常被忽略的原因然后把per_device_train_batch_size往下调比如从 16 降到 8。如果还不行开gradient_accumulation_steps2这样实际效果等同于 batch size 翻倍但显存不增。不过要注意开启梯度累积后优化器的更新频率会下降所以总训练步数也会相应减少学习率可能需要相应微调。TypeError:init() got an unexpected keyword argument evaluation_strategy这个几乎可以确定你是新版 transformers 的eval_strategy写成了旧版evaluation_strategy。Transformers 4.46 起evaluation_strategy被正式弃用Trainer 强制使用eval_strategy。ValueError: label names are not unique标签列中的类别重复了。用dataset.unique(label)检查一下确认只有两类后重跑。Trainer 训练时评估指标全是 0 或 None这个通常不是逻辑错误而是compute_metrics返回的字典键名写得不对。Trainer 期望返回的 key 和你在metric_for_best_model里选的名字能对应上比如我用f1就必须在返回字典里有这个键。RuntimeError: expected scalar type Half but found Float半精度训练下模型某个模块的数据类型不匹配。先关闭fp16True验证是否恢复正常如果关掉没问题那么定位是哪个模块在 fp16 下需要特殊处理再把该层的 dtype 强制设成 float32。6.2 如何利用日志和回调快速定位问题很多人面对训练报错只会截图发群里等别人分析。我的建议是先把日志看完90% 的问题在最后 30 行内会给出线索。举例如果看到这样的提示UserWarning: To use fp16, please specify it in the TrainingArguments.说明你的模型是在 CPU 或者不支持 fp16 的设备上跑但你用了fp16True。把这个参数关掉即可。Trainer 还支持通过回调机制在训练的关键节点注入自定义逻辑。比如想在每个 checkpoint 保存后把模型立刻部署到测试接口或者想统计每步的显存峰值可以自定义TrainerCallback子类重写on_save方法from transformers import TrainerCallback class SaveDeployCallback(TrainerCallback): def on_save(self, args, state, control, **kwargs): # 每个checkpoint保存后自动触发一次部署脚本 print(fCheckpoint saved at step {state.global_step})这个机制在长时间训练时特别好用。我可以一边让模型训着一边在回调里把当前最新 checkpoint 同步到推理服务做灰度验证省去了人工盯训练过程。6.3 一把梭小技巧Trainer 结合早停策略早停EarlyStopping能帮你在验证指标不再提升时及时止损。Trainer 使用早停需要配合EarlyStoppingCallbackfrom transformers import EarlyStoppingCallback trainer Trainer( modelmodel, argstraining_args, train_datasettokenized_datasets[train], eval_datasettokenized_datasets[test], compute_metricscompute_metrics, data_collatordata_collator, callbacks[EarlyStoppingCallback(early_stopping_patience2)], )它监控metric_for_best_model指定的指标连续 2 次评估不提升就提前终止训练。这个机制配合load_best_model_at_endTrue使用效果最好既能提前停又能保证拿到的还是验证集上最好的模型。我在长训练任务中一般会同时开EarlyStoppingCallback和定期 checkpoint这是最稳妥的组合。早停用于防止过拟合checkpoint 用于灾备恢复两者不冲突且互补。7. 我的几个补充建议Trainer 用顺了之后再回头看手写训练循环你会深刻体会到哪里浪费时间。不过如果你的研究场景真的需要完全自定义训练逻辑比如模型有多个输出头且每个头用不同 loss 加权手写循环的灵活性确实更大。我之前在断点恢复和自动调整学习率上花过不少时间用 Trainer 之后这些全都省了。对多数日常微调任务Trainer 是效率最高的方案。最后分享一个我实际使用下来的建议第一次在项目里用 Trainer不要贪多。先拿一个小模型、小数据集从头到尾跑通一遍再看日志、看每个 checkpoint 的指标变化逐步把参数加上去。这个过程中你会自然地形成对 Trainer 工作机制的感觉——它不是一个黑盒而是一个带着默认参数的司机你需要做的是教会它怎么开得更稳。
返回列表