本文是微调链路的逐步执行记录,与概念梳理文 AMD ROCm 上基于 LoRA 微调 Gemma 4 情绪分类实操手记 互补:这里只跟 Notebook 走,不写与部署/推理的逐项对照。
前置条件:同一台 48 GB ROCm 机器,PyTorch 已能识别 GPU;基座权重 google/gemma-4-E4B-it 已通过魔搭下载至 ./models/(若目录已存在,Step 4 可跳过)。实操 Notebook:gemma4_emotion_lora_modelscope_single_gpu.ipynb。
任务:用 LoRA 在 AI-ModelScope/emotion 上微调 Gemma 4,使模型在 system 约束下稳定输出六类情绪标签,并完成微调前后量化对比。

每步结构:目标 → 方法 → 数据结构(若有)→ 结果里盯什么。
Step 1:安装依赖
目标
补齐微调训练栈:trl(训练循环)、peft(LoRA)、datasets(数据管道)、scikit-learn(评估指标)。
方法
1 | -U:升级到最新兼容版本 |
ROCm 环境若已装定制版 PyTorch,只补上述包即可,不必改动底层 torch 构建。
结果里盯什么
import trl, peft, datasets无报错。- 若后续
MsDataset桥接报错,本流程不依赖它(Step 5 用 parquet 直读)。
Step 2:导入依赖与全局配置
目标
集中定义模型 ID、数据集 ID、数据量上限、训练精度、任务 prompt 与输出目录,供后续各步引用。
方法
1 | MODELSCOPE_MODEL_ID = "google/gemma-4-E4B-it" # 魔搭基座模型 ID |
结果里盯什么
启动时打印 torch.cuda.is_available(): True 与设备名;ROCm 下仍显示 cuda 属 PyTorch 统一接口,属正常现象。
Step 3:固定随机种子
目标
让数据 shuffle、LoRA 初始化、dropout 尽量可复现,便于对比不同超参实验。
方法
1 | def setup_seed(seed=42): |
结果里盯什么
同一配置重复跑,loss 曲线与评估指标应高度接近(算子非确定性可能带来微小漂移)。
Step 4:准备基座模型
目标
拿到本地 Gemma 4 E4B-it 权重路径,供 transformers 加载 tokenizer 与 AutoModelForCausalLM。
方法
权重若已在 ./models/google/gemma-4-E4B-it,直接设 LOCAL_MODEL_DIR 即可;否则:
1 | LOCAL_MODEL_DIR = snapshot_download( |
仓库需含 model.safetensors、tokenizer.json、chat_template.jinja 等;Gemma 系列首次需在魔搭网页接受许可协议。
结果里盯什么
LOCAL_MODEL_DIR 指向含 config.json 与 model.safetensors(约 14.9 GB)的目录。
Step 5:加载情绪分类数据集
目标
得到带 train / validation / test 三个 split 的标注数据,且 label 能映射到六类情绪名称。
方法
1 | dataset_dir = dataset_snapshot_download( |
数据结构:原始分类格式
每条样本是 Hugging Face Dataset 的一行 dict:
1 | { |
| label id | 类别名 |
|---|---|
| 0 | sadness |
| 1 | joy |
| 2 | love |
| 3 | anger |
| 4 | fear |
| 5 | surprise |
全量规模:train 16000 / validation 2000 / test 2000。本子集:4000 / 400 / 400。
为何 ClassLabel cast? 从 parquet 直读时 label 仅为 int64;cast 后可通过 features["label"].names[i] 取类别字符串,供 Step 6 拼 completion。
为何不用 MsDataset.load()? 规避 modelscope 与 datasets 版本错配时的 verification_mode 报错。
结果里盯什么
print(dataset) 三个 split 行数符合 limit;dataset["train"][0] 能正确读出 text 与 label。
Step 6:构造 SFT 格式数据
目标
把 (text, label) 分类样本转为 SFTTrainer 所需的 prompt-completion 对话结构,使训练目标与 chat 模型接口一致。
方法
1 | def to_prompt_completion(example): |
数据结构:SFT prompt-completion 格式
每条训练样本变为:
1 | { |
| 字段 | 类型 | 含义 |
|---|---|---|
prompt |
list[dict] |
模型条件输入:role + content 消息列表 |
completion |
list[dict] |
监督目标:assistant 应生成的标准标签文本 |
SFTTrainer 内部调用 tokenizer.apply_chat_template 将上述结构 tokenize;配合 completion_only_loss=True(Step 12),loss 只算 completion 段,不对 system/user 前缀回传梯度。
段末注释:SFT(Supervised Fine-Tuning,监督微调)在此指用「输入对话 + 标准回答」对继续训练 Causal LM;Causal LM 按序预测下一个 token。
结果里盯什么
sft_dataset["train"].column_names 应为 ['prompt', 'completion'];抽查一条,completion[0]["content"] 与 label_names[原 label id] 一致。
Step 7:加载 tokenizer 与基座模型
目标
以训练态加载 Gemma 4:tokenizer 可用官方 chat template;基座模型置于 GPU,并关闭 KV cache。
方法
1 | tokenizer = AutoTokenizer.from_pretrained( |
结果里盯什么
chat_template probe输出含<|turn>system等 Gemma 4 标记。next(base_model.parameters()).device为cuda:0。- 加载后显存占用明显上升,属正常。
Step 8:推理辅助函数
目标
封装「文本 → 情绪标签」的生成式预测,供微调前/后评估共用,保证对比公平。
方法
1 | def extract_label(raw_text: str) -> str: |
为何需要 generate_label?
Gemma-it 是 Causal LM,接口返回的是续写 token 解码后的字符串(可能是 joy、Joy.、The emotion is joy 等),不是固定的 6 维 logits 分类结果。generate_label 负责:chat 模板编码 → 短续写 → 正则抽取合法标签 → 非法则记 INVALID。
结果里盯什么
对 "I feel so happy and excited today!" 试跑,基座模型通常返回 joy 或接近标签。
Step 9:评估函数
目标
在指定 split 上批量调用 generate_label,汇总分类指标与逐条预测明细。
方法
1 | def evaluate_model(model, tokenizer, split="test", limit=EVAL_LIMIT): |
数据结构:评估输出
metrics dict(一次评估的汇总):
1 | { |
pred_df(逐条明细,每行):
1 | { |
| 指标 | 含义 |
|---|---|
accuracy |
预测与真实完全一致的比例 |
macro_f1 |
六类 F1 宏平均,对类别不平衡更敏感 |
invalid_predictions |
预测不在六类词表内的条数 |
结果里盯什么
函数能跑完 EVAL_LIMIT 条而不 OOM;pred_df 行数等于评估条数。
Step 10:微调前评估(基线)
目标
在未训练 LoRA 的基座模型上建立对照分数,后续才能量化微调收益。
方法
1 | pre_metrics, pre_report, pre_preds = evaluate_model( |
实测结果(400 条 test)
| 指标 | 数值 |
|---|---|
| accuracy | 0.625 |
| macro_f1 | 0.482 |
| invalid_predictions | 2 |

基座 + SYSTEM_PROMPT 已有零样本能力,但 macro_f1 明显低于 accuracy,少数类 recall 偏弱;混淆矩阵上 sadness ↔ joy 互混较多。
结果里盯什么
基线数字落盘或记录在案;此后勿改 SYSTEM_PROMPT,否则前后不可比。
Step 11:配置 LoRA
目标
指定低秩适配器挂载方式:冻结基座,只训练少量 LoRA 参数。
方法
1 | lora_config = LoraConfig( |
对线性层权重 (W),LoRA 增加 (\Delta W = BA)((B \in \mathbb{R}^{d \times r}),(A \in \mathbb{R}^{r \times k}),(r=16))。前向输出为 (Wx + \frac{\alpha}{r}BAx);lora_alpha 与 r 共同决定 adapter 修正强度,而非单独由 r 决定。
结果里盯什么
Step 13 创建 Trainer 后检查:
1 | Trainable LoRA parameters: 50,499,584 |
若为 0,说明 target_modules 未命中,需调整后再训练。
Step 12:定义训练参数
目标
配置 SFTConfig:batch、学习率、精度、截断长度及 ROCm 兼容项。
方法
1 | training_args = SFTConfig( |
结果里盯什么
output_dir 目录已创建;配置与 Step 2 的 BF16/SEED 一致。
Step 13:开始 LoRA 微调
目标
创建 SFTTrainer,完成 tokenize → 训练 → 验证 → checkpoint。
方法
1 | trainer = SFTTrainer( |
训练日志(节选)
| Step | Training Loss | Validation Loss | Mean Token Accuracy |
|---|---|---|---|
| 25 | 0.577 | 0.405 | 0.846 |
| 50 | 0.339 | 0.302 | 0.899 |
| 100 | 0.154 | 0.182 | 0.940 |
| 150 | 0.123 | 0.152 | 0.944 |
总 step 约 250(4000 条 ÷ 等效 batch 16 × 1 epoch)。train / val loss 同步下降,mean token accuracy 升至 ~0.94。
结果里盯什么
- 无 OOM;loss 曲线单调下降为主。
Trainable LoRA parameters> 0。- checkpoint 写入
OUTPUT_DIR/checkpoint-*。
Step 14:保存 LoRA adapter
目标
持久化 adapter 权重与 tokenizer,供后续评估、推理或合并部署。
方法
1 | trainer.model.save_pretrained(OUTPUT_DIR) # 保存 LoRA adapter(adapter_model.safetensors 等) |
数据结构:输出目录
1 | gemma4-it-emotion-lora-ms-single-gpu/ |
产物是 adapter,不是 15 GB 全量基座;推理时需基座 + adapter 组合加载。
结果里盯什么
adapter_model.safetensors 与 adapter_config.json 存在且非空。
Step 15:微调后评估
目标
用与 Step 10 完全相同的 evaluate_model(),在 test 集上测量微调后指标。
方法
1 | ft_model = trainer.model # 直接使用内存中已训练模型,避免重载 OOM |
优先在内存中直接评估,避免训练后立即重载模型导致显存碎片 OOM。
结果里盯什么
对比 Step 10:accuracy、macro_f1 应上升,invalid_predictions 应下降或归零;少数类 recall 改善可从 post_report 读出。
Step 16:对比微调前后效果
目标
将前后指标与逐条预测合并,便于写报告或排查退化样本。
方法
1 | comparison_df = pd.DataFrame([ |
数据结构:对比表
comparison_df:

changed_predictions:仅含微调前后预测标签发生变化的样本,用于分析「纠错」与「退化」案例。
结果里盯什么
macro_f1 delta 比 accuracy delta 更有参考价值;关注是否出现「训练 loss 很低但 macro_f1 几乎不变」的假提升。
Step 17:手动测试
目标
用自定义句子快速冒烟,确认模型能稳定输出合法标签。
方法
1 | test_texts = [ |
结果里盯什么
六条均应返回单一合法标签,无长句解释或越界输出。
Step 18:保存评估结果
目标
将全部评估明细导出 CSV,便于画图或写实验报告。
方法
1 | comparison_df.to_csv(f"{OUTPUT_DIR}/gemma4_emotion_before_after_metrics.csv", index=False) # 前后指标汇总 |
结果里盯什么
OUTPUT_DIR 下 CSV 齐全,行数与 EVAL_LIMIT 一致。
Step 19(可选):重新加载 adapter 推理
目标
验证「重启 kernel 后仅凭磁盘文件」能否恢复微调模型推理能力。
方法
1 | RUN_RELOAD_TEST = False # 开关:默认 False,避免与 Step 13~15 训练/评估同 kernel 抢显存 |
结果里盯什么
输出与 Step 17 一致;若 OOM,先释放训练占用显存再跑。
Step 20:常见问题
| 现象 | 对策 |
|---|---|
| parquet 路径匹配失败 | 检查 dataset_dir/data/{split}-*.parquet 实际 layout |
MsDataset / verification_mode 报错 |
沿用 Step 5 parquet 直读路径 |
| 训练 OOM | 降 batch_size、max_length、TRAIN_LIMIT |
| Trainable params = 0 | 调整 target_modules |
| 微调后指标没涨 | 检查 SYSTEM_PROMPT 训练/评估是否一致 |
| 生成式评估太慢 | 降低 EVAL_LIMIT |
| 多卡需求 | 改 .py + accelerate launch,勿在 Notebook 硬上 |
显存调参顺序:per_device_train_batch_size → max_length → TRAIN_LIMIT → 收窄 target_modules。
执行路径一览
1 | 安装依赖 → 全局配置 → 固定种子 |
建议首次实验保持 TRAIN_LIMIT=4000、1 epoch 跑通全链路,确认 macro_f1 相对基线有提升后,再放开数据量与 epoch 做正式训练。