ai · 2025 · 技术研究笔记
DeepSeek 微调实战(Unsloth)
用 QLoRA 训练一个可以独立保存和评估的 DeepSeek 适配器
快速实现 DeepSeek 模型的高效微调
研究版 · 5 个章节 · 约 4 分钟 · 更新于 2025-01-19
微调脚本跑完,不等于模型真的学会了任务。我见过更常见的结果是:训练损失不断下降,模型却只是记住了几种固定句式。换一个问法,回答立即失效。
这篇使用 DeepSeek-R1-Distill-Qwen-1.5B 的 4-bit 版本做一次小规模监督微调。目标不是训出一个新通用模型,而是把数据准备、QLoRA、训练和对照评估连成一条可执行路径。
这次到底会修改什么
LoRA(Low-Rank Adaptation)不直接更新模型里的全部权重,而是在若干线性层旁增加两个较小的矩阵,训练时只更新这些新参数。QLoRA 又把原模型以 4-bit 形式加载,进一步减少显存占用。
代价也很明确:最终保存的是一份适配器,推理时需要原模型;4-bit 训练节省显存,但不会把数据质量问题一起解决。
先准备一份能验证的数据
每条数据保留 instruction、input 和 output 三个字段。下面的脚本会生成一份最小 JSONL 文件;JSONL 是每行一个 JSON 对象的文本格式。
import json
from pathlib import Path
samples = [
{
"instruction": "把错误日志改写成面向用户的说明",
"input": "ConnectionError: upstream timeout after 30s",
"output": "上游服务在 30 秒内没有响应。请稍后重试;若问题持续,请联系管理员检查上游服务。",
},
{
"instruction": "给出一条简短的修复建议",
"input": "Python 报错 ModuleNotFoundError: No module named 'yaml'",
"output": "当前 Python 环境缺少 PyYAML。在同一环境中执行 `python -m pip install PyYAML`,再重新运行程序。",
},
{
"instruction": "解释命令失败的原因",
"input": "docker: Error response from daemon: port is already allocated",
"output": "Docker 要绑定的主机端口已被其他进程或容器占用。先查出占用者,再停止它或更换主机端口。",
},
]
output_path = Path("train.jsonl")
with output_path.open("w", encoding="utf-8") as file:
for sample in samples:
file.write(json.dumps(sample, ensure_ascii=False) + "\n")
print(f"已写入 {len(samples)} 条样本: {output_path}")
这三条只用于检查数据管道,不足以改善模型。真实任务应保留一份从未参与训练的验证集,而且不能只是把训练问题换个名词。
安装和训练
这套路径需要 NVIDIA GPU 和可用的 CUDA 环境。安装命令会随 PyTorch 与 CUDA 组合变化,运行前应核对 Unsloth 安装说明。
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install unsloth datasets trl
训练脚本如下。max_seq_length 是单条样本允许的最大 Token 数;Token 是分词器处理文本的基本单位。r 是 LoRA 矩阵的秩,数值增大会增加可训练参数和显存用量。
from datasets import load_dataset
from trl import SFTConfig, SFTTrainer
from unsloth import FastLanguageModel
MODEL_NAME = "unsloth/DeepSeek-R1-Distill-Qwen-1.5B-unsloth-bnb-4bit"
MAX_LENGTH = 1024
model, tokenizer = FastLanguageModel.from_pretrained(
model_name=MODEL_NAME,
max_seq_length=MAX_LENGTH,
load_in_4bit=True,
)
model = FastLanguageModel.get_peft_model(
model,
r=16,
target_modules=[
"q_proj",
"k_proj",
"v_proj",
"o_proj",
"gate_proj",
"up_proj",
"down_proj",
],
lora_alpha=16,
lora_dropout=0,
bias="none",
use_gradient_checkpointing="unsloth",
random_state=3407,
)
def format_sample(sample: dict[str, str]) -> dict[str, str]:
"""把一条指令数据转换为模型的对话模板。"""
user_text = sample["instruction"]
if sample["input"].strip():
user_text += f"\n\n输入:{sample['input']}"
messages = [
{"role": "user", "content": user_text},
{"role": "assistant", "content": sample["output"]},
]
return {
"text": tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=False,
)
}
dataset = load_dataset("json", data_files="train.jsonl", split="train")
dataset = dataset.map(format_sample)
trainer = SFTTrainer(
model=model,
processing_class=tokenizer,
train_dataset=dataset,
args=SFTConfig(
dataset_text_field="text",
max_length=MAX_LENGTH,
per_device_train_batch_size=1,
gradient_accumulation_steps=4,
learning_rate=2e-4,
max_steps=20,
logging_steps=1,
output_dir="outputs/deepseek-lora",
report_to="none",
seed=3407,
),
)
trainer.train()
model.save_pretrained("outputs/deepseek-lora/adapter")
tokenizer.save_pretrained("outputs/deepseek-lora/adapter")
max_steps=20 同样只是管道检查值。真实训练应根据样本数量、批量大小和验证集曲线决定停止位置,不要把某份 Notebook 里的步数直接搬到自己数据上。
评估时不要只看 loss
loss 是模型对训练 Token 预测误差的聚合值。它能表明优化器正在学习,却不能单独证明任务质量提高。
对错误解释这类任务,我会额外检查:
- 未见过的错误类型能否给出正确原因。
- 回答是否虚构了日志中不存在的信息。
- 原模型已经会的通用问答是否明显退化。
- 格式、长度和禁止内容是否符合业务约束。
至少保留三个版本的输出:原模型、微调模型和只修改 Prompt 的原模型。如果 Prompt 已经能稳定完成任务,微调未必是成本最低的方案。
参考资料
版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。
(采用 CC BY-NC-SA 4.0 许可协议进行授权)
本文标题:DeepSeek 微调实战(Unsloth)
