跳到正文
Joeplover
AI Agent·2026-06-23·约 4 分钟阅读

Qwen3-0.6B 东北话 LoRA 微调实战:从数据到部署

在 6GB 显存显卡上对 Qwen3-0.6B 做 LoRA 微调,让模型学会说东北话。完整覆盖数据准备、训练实现、踩坑记录、模型合并与部署。

AI 模型微调

Qwen3-0.6B 东北话 LoRA 微调实战:从数据到部署

项目背景

最近对大语言模型的微调产生了兴趣,手头正好有一张 6G 显存的显卡,于是拿 Qwen3-0.6B 做了一次完整的 LoRA 微调实验——目标是让这个小模型学会说东北话。

为什么选东北话?因为方言微调是一个很典型的"数据驱动"场景:基座模型本身已经具备语言能力,只需要少量领域数据就能学会新的表达风格。

技术栈

  • 基座模型:Qwen3-0.6B(通义千问 3 系列的最小版本)
  • 微调方法:LoRA(Low-Rank Adaptation)
  • 训练框架:Hugging Face Transformers + PEFT + Datasets
  • 数据格式:JSONL,instruction + input + output 结构
  • 硬件:6GB 显存 GPU

LoRA 原理

LoRA 的核心思路是:不修改模型原有的权重矩阵,而是在权重旁边挂一个小型的可训练矩阵。

原始权重 W (d×k)     LoRA 低秩矩阵 B×A
┌──────────────┐      ┌──┐ ┌──┐
│              │      │B │ │A │
│   冻结不动   │   +  │(d│×│r)│
│              │      │×r│ │  │
└──────────────┘      └──┘ └──┘
                      r << min(d,k)

对于 Qwen3-0.6B 这种 6 亿参数的模型,全量微调需要 >24G 显存,而 LoRA 只需要不到 6G。这次训练选择了 q_proj、k_proj、v_proj、o_proj、gate_proj、up_proj、down_proj 共 7 个投影层挂 LoRA,rank=8。

数据准备

每一条训练数据是 JSONL 格式,包含 instruction、input、output 三个字段:

{"instruction": "用东北话回答用户问题", "input": "今天天气咋样?", "output": "哎呀妈呀,今儿个天儿贼拉好,大太阳晒得杠杠的,出去溜达一圈可得劲儿了!"}
{"instruction": "用东北话回答用户问题", "input": "帮我推荐个好吃的地方", "output": "那必须滴,咱街口那家烧烤店老毕了,羊腰子烤得滋滋冒油,整两串再配瓶雪花,绝了!"}

数据集共 1000 条,覆盖问候、问答、建议、叙述、幽默五类场景,使用模板+随机组合方式生成。

训练代码

from transformers import AutoTokenizer, AutoModelForCausalLM, TrainingArguments, Trainer
from peft import LoraConfig, get_peft_model, TaskType
from datasets import load_dataset
import torch

# 1. 加载基座模型
model_name = "Qwen/Qwen3-0.6B"
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
tokenizer.pad_token = tokenizer.eos_token

model = AutoModelForCausalLM.from_pretrained(
    model_name,
    torch_dtype=torch.float16,
    device_map="auto",
    trust_remote_code=True
)

# 2. 配置 LoRA
lora_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    r=8,                    # 低秩矩阵的秩
    lora_alpha=16,          # 缩放系数
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                    "gate_proj", "up_proj", "down_proj"],
    lora_dropout=0.1,
    bias="none",
)

model = get_peft_model(model, lora_config)

# 3. 数据预处理 + Label masking
def preprocess(example):
    # 拼接 prompt + response
    prompt = f"Instruction: {example['instruction']}\nInput: {example['input']}\nOutput: "
    full_text = prompt + example['output'] + tokenizer.eos_token

    tokenized = tokenizer(
        full_text,
        truncation=True,
        max_length=512,
        padding="max_length",
        return_tensors="pt"
    )

    # Label masking:把 prompt 部分的 label 设为 -100,模型只学 output
    labels = tokenized["input_ids"].clone()
    prompt_len = len(tokenizer.encode(prompt, add_special_tokens=False))
    labels[:, :prompt_len] = -100  # 忽略 prompt 部分的 loss

    return {
        "input_ids": tokenized["input_ids"].squeeze(),
        "attention_mask": tokenized["attention_mask"].squeeze(),
        "labels": labels.squeeze()
    }

dataset = load_dataset("json", data_files="dongbei_data.jsonl", split="train")
dataset = dataset.map(preprocess)

# 4. 训练参数
training_args = TrainingArguments(
    output_dir="outputs/qwen3-dongbei-lora",
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,  # 等效 batch_size = 16
    num_train_epochs=3,
    learning_rate=2e-4,
    fp16=True,
    logging_steps=10,
    save_steps=100,
    save_total_limit=2,
    remove_unused_columns=False,
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=dataset,
)

# 5. 开始训练
trainer.train()

训练在 6G 显存 GPU 上跑了约 30 分钟(3 epoch),loss 从 2.1 下降到 0.3。训练产物保存在 outputs/qwen3-dongbei-lora/ 目录下。

推理与合并

# 推理:动态加载 LoRA adapter
from peft import PeftModel

base_model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-0.6B", device_map="auto")
lora_model = PeftModel.from_pretrained(base_model, "outputs/qwen3-dongbei-lora/checkpoint-100")

# 生成
inputs = tokenizer("Instruction: 用东北话回答\nInput: 你干啥去?\nOutput: ", return_tensors="pt")
outputs = lora_model.generate(**inputs, max_new_tokens=100)
print(tokenizer.decode(outputs[0]))

# 合并权重(部署用)
merged_model = lora_model.merge_and_unload()
merged_model.save_pretrained("outputs/qwen3-dongbei-merged")

合并后可以转成 GGUF 格式,用 llama.cpp 部署。

踩坑记录

  1. PyTorch 版本问题:初始装了 CPU 版 torch,cuda.is_available() 返回 False。需要用 CUDA wheel 重装。
  2. 显存不足:6G 显存做训练非常紧张,batch_size=8 直接 OOM。最终用 batch_size=4 + gradient_accumulation=4 + fp16 解决。
  3. Loss 不下降:Label masking 逻辑写错,prompt 部分的 label 没设 -100,导致模型在学"抄写 prompt"而不是"回答"。把 prompt 对应的 label 设置为 -100 后 loss 才正常下降。

总结

完整走通了"数据准备 → 训练 → 推理 → 合并 → 部署"全链路。核心认知:LoRA 是低资源微调最佳实践;Label masking 必须正确;数据质量决定模型效果上限。