DeepSeek :使用 Unsloth + LoRA 微调模型

前言

在前几期,我们通过 RAG(检索增强生成)解决了模型“不知道”的问题。但在企业实战中,我们经常遇到另一种瓶颈——“学不会”

比如:

  • 医疗场景: 模型能查到病历,但写出的诊断书语气像科普文章,而不是医生风格。
  • 开发场景: 模型能写 Python,但永远学不会你们公司内部那套 MyCorpSDK.do_magic() 的写法。
  • 格式控: 你要求输出极度复杂的嵌套 JSON,模型在长文本下总是偶尔漏个括号。

这时候,RAG 已经不够用了。我们需要 SFT (Supervised Fine-Tuning)。如果说 RAG 是给学生一本书让他开卷考试,那么 SFT 就是让学生去实习三个月,彻底掌握内功。

本文将带你使用目前全球最快的微调框架 Unsloth,在单卡消费级显卡上,对 DeepSeek-R1-Distill-Llama-8B 进行微调,让它学会识别并使用特定的行业术语。


一、 SFT 的核心逻辑:LoRA 与 QLoRA

微调一个大模型(比如 671B 的 DeepSeek-V3)需要几百万美元的算力,这显然不现实。但我们有 LoRA (Low-Rank Adaptation)

  • 原理: 冻结模型原本的亿万参数,只在旁边“外挂”两个非常小的矩阵进行训练。
  • QLoRA: 进一步将模型量化为 4-bit,大幅降低显存需求。
  • 结论: 我们可以用不到 16GB 的显存(RTX 3060/4060Ti 16G 或 RTX 3090),去微调 DeepSeek 的 8B 蒸馏版模型。

二、 环境准备:神器 Unsloth

为什么选 Unsloth?

  1. 快: 训练速度比 HuggingFace 官方库快 2-5 倍。
  2. 省: 显存占用减少 50% 以上。
  3. 傻瓜化: 解决了复杂的依赖冲突问题。

1. 安装依赖

建议使用 Linux 环境或 WSL2(Windows Subsystem for Linux)。

pip install "unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git"
pip install --no-deps "xformers<0.0.27" "trl<0.9.0" peft accelerate bitsandbytes


三、 数据准备:高质量胜过大数据

微调的核心不是代码,是数据。SFT 需要的是 (Instruction, Input, Output) 格式的数据对。

假设我们要训练一个**“中医诊断助手”**,通用模型可能会说“建议去医院检查”,而我们需要它输出专业的中医术语。

构造数据集 (data.json)

[
  {
    "instruction": "根据患者描述进行中医诊断。",
    "input": "患者男,45岁。主诉失眠多梦,伴有心悸健忘,面色无华,舌淡苔白,脉细弱。",
    "output": "【诊断】心血不足证。\n【治法】养血安神。\n【方药】归脾汤加减。方中用黄芪、人参、白术、当归、茯神、远志、酸枣仁..."
  },
  {
    "instruction": "根据患者描述进行中医诊断。",
    "input": "患者自觉胃脘灼痛,吞酸嘈杂,口干口苦,舌红苔黄,脉弦数。",
    "output": "【诊断】肝火犯胃证。\n【治法】清肝泻火,和胃止痛。\n【方药】左金丸合化肝煎加减..."
  }
]

Pro Tip: 刚开始实验时,准备 50-100 条高质量的高人工撰写数据,效果往往好于 1 万条机器生成的垃圾数据。


四、 实战代码:开启微调之旅

创建一个 Python 脚本 train.py

1. 加载模型

from unsloth import FastLanguageModel
import torch

max_seq_length = 2048 # 支持 RoPE 缩放,可达 128k
dtype = None # 自动检测 Float16/Bfloat16
load_in_4bit = True # 开启 4bit 量化 (QLoRA)

# 加载 DeepSeek-R1 的 8B 蒸馏版 (基于 Llama3 架构)
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name = "unsloth/DeepSeek-R1-Distill-Llama-8B",
    max_seq_length = max_seq_length,
    dtype = dtype,
    load_in_4bit = load_in_4bit,
)

2. 添加 LoRA 适配器

model = FastLanguageModel.get_peft_model(
    model,
    r = 16, # LoRA 秩,建议 8, 16, 32
    target_modules = ["q_proj", "k_proj", "v_proj", "o_proj",
                      "gate_proj", "up_proj", "down_proj",],
    lora_alpha = 16,
    lora_dropout = 0, # Unsloth 建议为 0
    bias = "none",
    use_gradient_checkpointing = "unsloth", # 显存优化技术
    random_state = 3407,
)

3. 加载与格式化数据

我们需要将 JSON 数据转换成模型能听懂的 Prompt 格式(Alpaca 格式)。

alpaca_prompt = """Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.

### Instruction:
{}

### Input:
{}

### Response:
{}"""

from datasets import load_dataset

def formatting_prompts_func(examples):
    instructions = examples["instruction"]
    inputs       = examples["input"]
    outputs      = examples["output"]
    texts = []
    for instruction, input, output in zip(instructions, inputs, outputs):
        text = alpaca_prompt.format(instruction, input, output) + tokenizer.eos_token
        texts.append(text)
    return { "text" : texts, }

dataset = load_dataset("json", data_files="data.json", split="train")
dataset = dataset.map(formatting_prompts_func, batched = True)

4. 开始训练

使用 HuggingFace 的 SFTTrainer

from trl import SFTTrainer
from transformers import TrainingArguments

trainer = SFTTrainer(
    model = model,
    tokenizer = tokenizer,
    train_dataset = dataset,
    dataset_text_field = "text",
    max_seq_length = max_seq_length,
    dataset_num_proc = 2,
    args = TrainingArguments(
        per_device_train_batch_size = 2,
        gradient_accumulation_steps = 4,
        warmup_steps = 5,
        max_steps = 60, # 演示用,实际建议根据 loss 收敛情况设定 (如 1-3 epochs)
        learning_rate = 2e-4,
        fp16 = not torch.cuda.is_bf16_supported(),
        bf16 = torch.cuda.is_bf16_supported(),
        logging_steps = 1,
        optim = "adamw_8bit", # 8bit 优化器,进一步省显存
        output_dir = "outputs",
    ),
)

trainer.train()


五、 效果验证与保存

训练完成后,我们可以直接进行推理,看看它是否学会了“中医范儿”。

# 推理测试
FastLanguageModel.for_inference(model) # 开启原生 2倍速 推理
inputs = tokenizer(
[
    alpaca_prompt.format(
        "根据患者描述进行中医诊断。", # Instruction
        "患者女,痛经,小腹冷痛,得热痛减,手脚冰凉。", # Input
        "", # Output - leave blank for generation
    )
], return_tensors = "pt").to("cuda")

outputs = model.generate(**inputs, max_new_tokens = 128, use_cache = True)
print(tokenizer.batch_decode(outputs)[0])

预期结果:
模型不再说“多喝热水”,而是输出:“【诊断】寒凝血瘀证…【方药】少腹逐瘀汤…”

保存模型

为了在 Ollama 或其他地方使用,我们将 LoRA 权重保存下来,甚至直接合并为 GGUF 格式。

# 1. 保存 LoRA 适配器 (仅几百MB)
model.save_pretrained("lora_model")

# 2. 保存并合并为 GGUF (可直接导入 Ollama!)
model.save_pretrained_gguf("model_gguf", tokenizer, quantization_method = "q4_k_m")

拿到 model_gguf.gguf 后,你只需要在 Ollama 中创建一个 Modelfile

FROM ./model_gguf.gguf
SYSTEM "你是一个专业的中医AI助手。"

然后 ollama create my-tcm-model -f Modelfile,你就拥有了一个完全私有的、甚至可以离线运行的行业专家模型!


六、 避坑指南 (Pro Tips)

  1. 过拟合风险: 如果数据太少(<20条)且训练步数太多,模型会变成“复读机”,只能回答训练集里的问题。解决方法是增加数据多样性或降低 max_steps
  2. Prompt 格式: 训练时的 Prompt 格式(Alpaca)必须与推理时完全一致,连换行符都不能差,否则效果大打折扣。
  3. 灾难性遗忘: 微调后的模型可能会变“笨”,比如不再会做数学题。这是正常的,因为 SFT 是为了专门任务。如果需要保留通用能力,需要混合通用数据集一起训练。

七、 总结

通过 SFT,可以不再受限于 DeepSeek 原始的能力,而是可以改造模型,使其成为自己想要的助手。

请大家点点关注,谢谢大家了

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐