项目地址:[github.com/MoyuNOOB/Lo…] 。本项目基于 Qwen3-0.6B,在本地 4GB 显存环境下,通过 LoRA 微调让模型学习一套自制的技术问答数据(FastAPI、并发、数据库、MQ、Agent、RAG 等),最终实现:
- 会按“我的风格”回答常见后端/大模型工程问题;
- 微调过程轻量,适合笔记本环境。
制作数据集
-
原始数据形式
一开始在教学项目中,样本是:
{"prompt": "问题", "completion": "答案"}比如:
{"prompt": "FastAPI 相比 Flask 和 Django 有哪些性能优势?", "completion": "FastAPI基于ASGI标准..." } -
转换为 Chat messages 格式
为了适配 Qwen3 的对话范式,将其转为:
{ "messages": [ {"role": "user", "content": "问题"}, {"role": "assistant", "content": "答案"} ] }并保存为 JSONL:
LoRA/data/train.jsonl:约 201 条训练样本;LoRA/data/val.jsonl:少量验证样本。
-
数据特点
- 主题集中:FastAPI、并发、ORM、Redis、MQ、Celery、MLOps、大模型工程、Agent、RAG 等;
- 答案风格:短段落 + 通俗解释 + 面试常见表述,目的是让模型“说人话”和“抓重点”。
选择并下载模型到本地
-
基座模型选择
- 选用轻量级的 Qwen3-0.6B[github.com/QwenLM/Qwen…] :
- 中等规模,4GB 显存也能 LoRA 微调;
- 中文能力和工程知识较强,适合技术问答。
- 选用轻量级的 Qwen3-0.6B[github.com/QwenLM/Qwen…] :
-
本地下载
-
使用
git clone+ huggingface 镜像,将模型仓库克隆到:my_gpt/Qwen3-0.6B/ -
目录中包含:
config.jsongeneration_config.jsontokenizer.*- 权重文件(
model.safetensors等)
-
-
配置关注点
config.json决定模型结构(层数、隐藏维、头数等);generation_config.json控制推理行为(温度、top_p、max_new_tokens 等)。
模型脚本(model.py)
LoRA/model.py 负责:
-
加载配置
def load_cfg(path: str): with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f)- 返回一个 dict,包含:
model.*:base_model_path、max_seq_len;training.*:lr、batch、epoch 等;lora.*:r、alpha、dropout、target_modules;data.*:train/val 路径。
- 返回一个 dict,包含:
-
构建基座 + 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 数据转成训练可用张量:
-
加载数据集
raw_ds = load_dataset("json", data_files={"train": data_cfg["train_file"]}) -
拼接文本 + 编码
-
对于每个样本,将 messages 中的
user和assistant提取出来: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()
-
-
返回训练集
ds = raw_ds.map(_map_fn, batched=True, remove_columns=raw_ds["train"].column_names) return ds["train"]
训练脚本(train.py)
LoRA/train.py 是整个微调的核心入口,职责:
-
整体流程
cfg = load_cfg("config.yaml") tokenizer, model = build_model(cfg) train_dataset = build_dataset(cfg, tokenizer) -
配置 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)
-
-
自定义回调记录 loss
-
定义 LoggingCallback 继承
TrainerCallback:- 在 on_log中收集
step和loss; - 可选记录当前学习率。
- 在 on_log中收集
-
注册回调:
trainer.add_callback(LoggingCallback(loss_history)) trainer.train()
-
-
训练后可视化与保存
-
使用 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 后模型”的回答差异:
-
加载基座和 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() -
统一的生成函数
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) -
基座 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 版在用词、重点和风格上更接近你的数据。
最终效果
-
从“会不会回答”到“按我想要的方式回答”
- Qwen3-0.6B 本身已经能很好回答“FastAPI vs Flask vs Django”这类问题;
- LoRA 微调后,模型:
- 更强调关心的点(异步、ASGI、微服务、快速迭代等);
- 风格更像短小精悍的面试回答,而不是教程式长文。
-
训练曲线与结构
training_curves.png显示 loss 随训练逐步下降,整体收敛正常;lora_structure.txt展示了 LoRA 注入到 Qwen3 各层的 Q/K/V/O 投影中,更容易理解。