大模型LoRA微调demo

61 阅读4分钟

项目地址:[github.com/MoyuNOOB/Lo…] 。本项目基于 Qwen3-0.6B,在本地 4GB 显存环境下,通过 LoRA 微调让模型学习一套自制的技术问答数据(FastAPI、并发、数据库、MQ、Agent、RAG 等),最终实现:

  • 会按“我的风格”回答常见后端/大模型工程问题;
  • 微调过程轻量,适合笔记本环境。

制作数据集

  1. 原始数据形式

    一开始在教学项目中,样本是:

    {"prompt": "问题", "completion": "答案"}
    

    比如:

    {"prompt": "FastAPI 相比 Flask 和 Django 有哪些性能优势?", "completion": "FastAPI基于ASGI标准..." }
    
  2. 转换为 Chat messages 格式

    为了适配 Qwen3 的对话范式,将其转为:

    {
      "messages": [
        {"role": "user", "content": "问题"},
        {"role": "assistant", "content": "答案"}
      ]
    }
    

    并保存为 JSONL:

    • LoRA/data/train.jsonl:约 201 条训练样本;
    • LoRA/data/val.jsonl:少量验证样本。
  3. 数据特点

    • 主题集中:FastAPI、并发、ORM、Redis、MQ、Celery、MLOps、大模型工程、Agent、RAG 等;
    • 答案风格:短段落 + 通俗解释 + 面试常见表述,目的是让模型“说人话”和“抓重点”。

选择并下载模型到本地

  1. 基座模型选择

    • 选用轻量级的 Qwen3-0.6B[github.com/QwenLM/Qwen…] :
      • 中等规模,4GB 显存也能 LoRA 微调;
      • 中文能力和工程知识较强,适合技术问答。
  2. 本地下载

    • 使用 git clone + huggingface 镜像,将模型仓库克隆到:

      my_gpt/Qwen3-0.6B/
      
    • 目录中包含:

      • config.json
      • generation_config.json
      • tokenizer.*
      • 权重文件(model.safetensors 等)
  3. 配置关注点

    • config.json 决定模型结构(层数、隐藏维、头数等);
    • generation_config.json 控制推理行为(温度、top_p、max_new_tokens 等)。

模型脚本(model.py)

LoRA/model.py 负责:

  1. 加载配置

    def load_cfg(path: str):
        with open(path, "r", encoding="utf-8") as f:
            return yaml.safe_load(f)
    
    • 返回一个 dict,包含:
      • model.*base_model_pathmax_seq_len
      • training.*:lr、batch、epoch 等;
      • lora.*:r、alpha、dropout、target_modules;
      • data.*:train/val 路径。
  2. 构建基座 + LoRA 模型

    tokenizer = AutoTokenizer.from_pretrained(model_cfg["base_model_path"], use_fast=True)
    model = AutoModelForCausalLM.from_pretrained(
        model_cfg["base_model_path"],
        torch_dtype="auto",
        device_map="cuda:0",
    )
    

    使用 PEFT 注入 LoRA:

    lora_config = LoraConfig(
        r=lora_cfg["r"],
        lora_alpha=lora_cfg["alpha"],
        target_modules=lora_cfg["target_modules"],  # ["q_proj","k_proj","v_proj","o_proj"]
        lora_dropout=lora_cfg["dropout"],
        bias="none",
        task_type="CAUSAL_LM",
    )
    model = get_peft_model(model, lora_config)
    

    最终返回 (tokenizer, model) 给训练脚本使用。


数据脚本(data.py)

LoRA/data.py 负责将 messages 数据转成训练可用张量:

  1. 加载数据集

    raw_ds = load_dataset("json", data_files={"train": data_cfg["train_file"]})
    
  2. 拼接文本 + 编码

    • 对于每个样本,将 messages 中的 userassistant 提取出来:

      texts = []
      for msgs in examples["messages"]:
          user, assistant = "", ""
          for m in msgs:
              if m["role"] == "user": user = m["content"]
              elif m["role"] == "assistant": assistant = m["content"]
          texts.append(f"用户: {user}\n助手:{assistant}")
      
    • 用 tokenizer 编码,并设置 labels = input_ids

      enc = tokenizer(
          texts,
          truncation=True,
          max_length=model_cfg["max_seq_len"],
          padding=False,
      )
      enc["labels"] = enc["input_ids"].copy()
      
  3. 返回训练集

    ds = raw_ds.map(_map_fn, batched=True, remove_columns=raw_ds["train"].column_names)
    return ds["train"]
    

训练脚本(train.py)

LoRA/train.py 是整个微调的核心入口,职责:

  1. 整体流程

    cfg = load_cfg("config.yaml")
    tokenizer, model = build_model(cfg)
    train_dataset = build_dataset(cfg, tokenizer)
    
  2. 配置 Trainer

    • 准备 loss_history 收集日志;

    • 使用 TrainingArguments 控制训练超参:

      training_args = TrainingArguments(
          output_dir=training_cfg["output_dir"],
          per_device_train_batch_size=training_cfg["per_device_train_batch_size"],
          gradient_accumulation_steps=training_cfg["gradient_accumulation_steps"],
          num_train_epochs=training_cfg["num_epochs"],
          learning_rate=training_cfg["learning_rate"],
          weight_decay=training_cfg["weight_decay"],
          warmup_ratio=training_cfg["warmup_ratio"],
          logging_steps=training_cfg["logging_steps"],
          save_steps=training_cfg["save_steps"],
          save_total_limit=2,
          bf16=torch.cuda.is_available(),
          report_to="none",
      )
      
    • 使用 Trainer

      trainer = Trainer(model=model, args=training_args, train_dataset=train_dataset)
      
  3. 自定义回调记录 loss

    • 定义 LoggingCallback 继承 TrainerCallback

      • 在 on_log中收集 steploss
      • 可选记录当前学习率。
    • 注册回调:

      trainer.add_callback(LoggingCallback(loss_history))
      trainer.train()
      
  4. 训练后可视化与保存

    • 使用 draw_loss_pic将训练 loss 画到 output/training_curves.png

    • 遍历 model.named_modules(),将含 “lora” 的模块写入 output/lora_structure.txt

    • 保存 LoRA adapter 和 tokenizer:

      out_dir = os.path.join(training_cfg["output_dir"], "lora_adapter")
      model.save_pretrained(out_dir)
      tokenizer.save_pretrained(out_dir)
      

推理脚本(infer.py)

LoRA/infer.py 用于对比“基座模型 vs LoRA 后模型”的回答差异:

  1. 加载基座和 LoRA

    tokenizer = AutoTokenizer.from_pretrained(base_model_path, use_fast=True)
    base_model = AutoModelForCausalLM.from_pretrained(...)
    base_model.eval()
    
    lora_model = PeftModel.from_pretrained(base_model, lora_path)
    lora_model.eval()
    
  2. 统一的生成函数

    def generate(model, question: str):
        prompt = f"用户: {question}\n助手:"
        inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
        outputs = model.generate(
            **inputs,
            max_new_tokens=256,
            do_sample=True,
            temperature=0.6,
            top_p=0.9,
            eos_token_id=tokenizer.eos_token_id,
        )
        return tokenizer.decode(outputs[0], skip_special_tokens=True)
    
  3. 基座 vs LoRA 对比

    def compare(question: str):
        print("=" * 80)
        print(f"问题: {question}")
        print("-" * 80)
        print("[基座模型] 输出:")
        print(generate(base_model, question))
        print("-" * 80)
        print("[LoRA 微调后] 输出:")
        print(generate(lora_model, question))
        print("=" * 80)
    

    运行:

    python -m infer
    

    对比可以看到:LoRA 版在用词、重点和风格上更接近你的数据。


最终效果

  1. 从“会不会回答”到“按我想要的方式回答”

    • Qwen3-0.6B 本身已经能很好回答“FastAPI vs Flask vs Django”这类问题;
    • LoRA 微调后,模型:
      • 更强调关心的点(异步、ASGI、微服务、快速迭代等);
      • 风格更像短小精悍的面试回答,而不是教程式长文。
  2. 训练曲线与结构

    • training_curves.png 显示 loss 随训练逐步下降,整体收敛正常;
    • lora_structure.txt 展示了 LoRA 注入到 Qwen3 各层的 Q/K/V/O 投影中,更容易理解。