AI 模型训练实录:从数据采集到 LoRA 微调的完整实践
> **一句话总结**:用 989 条微信聊天记录对 Qwen2.5-1.5B 进行 LoRA 微调,让一个 1.5B 参数的小模型学会特定人物的说话风格,并搭建 Web 聊天界面和语音合成。
---
## 文章导览
| 章节 | 内容 |
|------|------|
| 一、项目背景 | 为什么训练模型、开发目标 |
| 二、数据准备 | 微信聊天记录 → 训练样本的转换过程 |
| 三、训练方案选型 | 3 次失败尝试 + 最终方案(含显存计算) |
| 四、训练流程详解 | 超参数、输出文件、学习曲线 |
| 五、推理服务部署 | FastAPI 服务搭建 |
| 六、Web 聊天界面 | Gradio 交互 + 流式输出 + TTS |
| 七、本地 vs 云端对比 | 深度对比分析 |
| 八、环境配置与运行 | 训练和推理的环境准备 |
| 九、资源与源码 | GitHub 地址 |
| 十、经验总结 | 避坑指南 |
---
## 一、项目背景与动机
### 为什么要训练自己的模型?
有了第一个项目([微信智能机器人](./01-微信智能机器人-从零搭建AI自动回复系统.md),基于 DeepSeek API)之后,我在思考一个问题:**如果我不想依赖云端 API、想完全离线运行,该怎么办?**
答案是自己训练一个模型。但大模型动辄 7B、13B 参数,我的 RTX 4060 只有 8GB 显存,能做什么?
经过调研,我选择了 **Qwen2.5-1.5B-Instruct** 作为基座模型,配合 **LoRA** 参数高效微调。1.5B 参数虽然不大,但在 8GB 显存下可以流畅运行,且中文能力在同参数量级中表现优秀。
### 开发目标
1. ✅ 用微信聊天记录作为训练数据
2. ✅ 在 **8GB 显存**限制下完成 SFT 微调
3. ✅ 部署 FastAPI 推理服务
4. ✅ 搭建 Gradio Web 聊天界面
5. ✅ 集成 Edge-TTS 语音合成
---
## 二、数据准备:从微信到训练样本
### 数据采集流程
```
微信聊天记录 → WeClone 导出 CSV → 格式清洗 → Qwen 模板格式化 → LoRA 微调
```
数据来源是 [WeClone](https://github.com/xming521/WeClone) 工具导出的微信聊天记录 CSV。原始数据包含时间戳、发送者、消息内容。
### 训练数据格式
采用 Qwen2.5 的 ChatML 格式进行指令微调:
```
<|im_start|>user
今天天气真好,出去玩吗?<|im_end|>
<|im_start|>assistant
好呀~不过下午有节线代课,得先上完再出去[捂脸]<|im_end|>
```
> **数据集统计**:
> - 总样本数:**989 条**
> - 数据来源:真实微信聊天记录
> - 数据特点:**单侧视角**(只有对方的输入 + 周文慧的输出)
> - 数据局限:缺少多轮对话的长序列样本
### 踩坑:huggingface datasets 的 segfault
**问题**:使用 `datasets.Dataset` 加载数据时,Windows 下发生 segfault 崩溃。
```python
# ❌ 会 segfault 的写法
from datasets import Dataset
dataset = Dataset.from_dict({"instruction": texts, "output": replies})
# → Windows 上 datasets 库的数据预处理多进程会崩溃
```
**解决方案**:手写 PyTorch Dataset,完全绕开 datasets 库:
```python
"""
ZhwDataset — 自定义 PyTorch Dataset
替代 huggingface datasets,避免 Windows 多进程 segfault
"""
from torch.utils.data import Dataset
class ZhwDataset(Dataset):
"""
手写 Dataset,不依赖 huggingface datasets 库。
设计要点:
1. 直接使用 tokenizer,避免 datasets 库的多进程问题
2. 使用 Qwen2.5 的 ChatML 模板格式
3. labels = input_ids(自回归语言建模)
"""
def __init__(self, data: list, tokenizer, max_length: int = 512):
self.inputs = []
for item in data:
# 构造 Qwen2.5 ChatML 格式
text = (
f"<|im_start|>user\n{item['instruction']}<|im_end|>\n"
f"<|im_start|>assistant\n{item['output']}<|im_end|>"
)
tokenized = tokenizer(
text,
truncation=True,
max_length=max_length,
padding="max_length",
return_tensors="pt",
)
input_ids = tokenized["input_ids"][0]
self.inputs.append({
"input_ids": input_ids,
"attention_mask": tokenized["attention_mask"][0],
"labels": input_ids.clone(), # CRUCIAL: labels = input_ids
})
def __len__(self):
return len(self.inputs)
def __getitem__(self, idx):
return self.inputs[idx]
```
---
## 三、训练方案选型:一段充满挫折的探索之路
这是整个项目中最艰难的部分。在 8GB RTX 4060 的限制下,我经历了三次失败的尝试。
### ❌ 方案一:Qwen2.5-3B + LoRA fp16
一开始天真地以为 8GB 够跑 3B 模型。我们来算一笔账:
| 项目 | 显存占用 | 说明 |
|------|----------|------|
| 模型权重 (fp16) | ~6 GB | 3B × 2 bytes |
| AdamW 优化器状态 | ~12 GB | 2× 模型大小 |
| 梯度 | ~6 GB | 同模型大小 |
| 激活值 | ~2 GB | 取决于 batch size |
| **总计** | **~26 GB** | **远超 8GB** |
> **结果:OOM(显存不足)**,连模型加载都失败。
### ❌ 方案二:Qwen2.5-3B + QLoRA 4-bit 量化
4-bit 量化可以把模型压缩到 ~3GB,看起来很有希望:
```
显存估算:
- 模型 (4-bit):~1.5 GB
- LoRA 参数:~50 MB
- 优化器状态 (AdamW):~6 GB (LoRA 参数仅可训练)
- 梯度 + 激活值:~1 GB
- 总计:~8.5 GB → 勉强可跑
```
但在 Windows 上遇到了致命问题:
```
bitsandbytes segfault
```
> ⚠️ **bitsandbytes 在 Windows 上的 4-bit 量化存在已知的 segfault 问题**。如果你在 Linux 上,QLoRA 4-bit 是可行的选择。Windows 用户只能使用 fp16 或 8-bit。
### ❌ 方案三:Qwen2.5-3B + CPU offloading
尝试用 `device_map="auto"` 自动分配,把部分层卸载到 CPU:
```python
# ❌ 会报错的写法
model = AutoModelForCausalLM.from_pretrained(
model_name,
device_map="auto", # 部分层自动卸载到 CPU
torch_dtype=torch.float16,
)
# → RuntimeError: gradient_checkpointing + device_map 不兼容
```
**结果**:`gradient_checkpointing`(必须的显存节省技术)与 `device_map` 在 Transformers 中存在兼容性问题。
### ✅ 最终方案:Qwen2.5-1.5B + LoRA fp16
回归现实,降级到 1.5B 模型:
| 参数 | 值 | 说明 |
|------|------|------|
| **基座模型** | Qwen/Qwen2.5-1.5B-Instruct | 2.9GB 文件大小 |
| **微调方法** | LoRA | 秩 r=8, alpha=16 |
| **精度** | fp16 | 混合精度训练 |
| **device_map** | `"cuda:0"` | 单 GPU 直接加载(关键!) |
| **学习率** | 2e-4 | 标准 LoRA 学习率 |
| **优化器** | AdamW | β1=0.9, β2=0.999 |
| **梯度累积** | 4 步 | 等效 batch size = 4 |
| **训练轮次** | 3 epochs | |
| **训练步数** | 372 步 | 989 样本 × 3 epochs ÷ 8 batch |
| **显存峰值** | **~3.2 GB** | **RTX 4060 无压力** |
| **训练耗时** | **847 秒** | 约 14 分钟 |
> 🔑 **关键技巧**:使用 `device_map="cuda:0"` 而不是 `"auto"`。这绕过了 Windows 上 `os error 1455`(页面文件不足)的问题。`"auto"` 会尝试内存映射,而在 Windows 上这经常失败。
最终 Loss:**3.296**
---
## 四、训练流程详解
### 4.1 完整训练命令
```bash
# 方式一:使用 WeClone CLI(推荐)
weclone-cli train-sft
# 方式二:使用训练脚本
python WeClone/train_zhouwenhui_v2.py
# 方式三:从命令行直接调用 LLaMA Factory
llamafactory-cli train \
--model_name_or_path Qwen/Qwen2.5-1.5B-Instruct \
--dataset_dir ./data \
--dataset zhouwenhui \
--finetuning_type lora \
--lora_target all \
--lora_rank 8 \
--lora_alpha 16 \
--output_dir ./model_output \
--num_train_epochs 3 \
--per_device_train_batch_size 8 \
--gradient_accumulation_steps 4 \
--learning_rate 2e-4 \
--fp16
```
### 4.2 训练输出文件
| 文件 | 大小 | 用途 |
|------|------|------|
| `model_output/adapter_config.json` | ~1 KB | LoRA 配置(rank, alpha, target modules) |
| `model_output/adapter_model.safetensors` | **8.7 MB** | **LoRA 权重——这就是训练成果** |
| `model_output/merged/model.safetensors` | 3 GB | 合并后的完整模型(可删除,推理时动态合并) |
| `model_output/checkpoint-124/` | — | 每 124 步的检查点(用于恢复训练) |
| `model_output/checkpoint-496/` | — | 最后检查点 |
> 💡 LoRA 总参数量:**2.18M**,仅占模型总参数的 **0.14%**。这意味着 99.86% 的模型能力来自预训练。
### 4.3 学习曲线
```
Loss 曲线(372 steps,约 14 分钟):
Loss
4.0 | ◆
3.8 | ◆
3.6 | ◆
3.4 | ◆────◆
3.2 | ◆───◆───◆ (最终 3.296)
└──────────────────────────
0 124 248 372 Steps
├ epoch 1 ┤ epoch 2 ┤epoch 3│
```
3.296 的 Loss 意味着:
- ✅ 模型已学会基本的对话模式(问答结构、语言连贯性)
- ⚠️ 对特定人物风格的模仿仍不够精准(需要更多数据)
- 📈 如果数据量增加到 5000+ 条,预期 Loss 可降至 2.5 以下
---
## 五、推理服务部署
训练完成后,需要部署推理服务以供 wechat-bot 调用。
```python
"""
推理服务 — FastAPI
运行: uvicorn inference_server:app --host 0.0.0.0 --port 8000
"""
from fastapi import FastAPI
from pydantic import BaseModel
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
app = FastAPI(title="周文慧 AI 推理服务")
class ChatRequest(BaseModel):
message: str
history: list = []
class ChatResponse(BaseModel):
reply: str
# 全局加载模型(服务启动时加载一次)
base_model = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen2.5-1.5B-Instruct",
device_map="cuda:0",
torch_dtype=torch.float16,
)
model = PeftModel.from_pretrained(base_model, "./model_output")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-1.5B-Instruct")
@app.post("/chat", response_model=ChatResponse)
async def chat(req: ChatRequest):
# 拼接提示词
messages = [{"role": "user", "content": req.message}]
text = tokenizer.apply_chat_template(messages, tokenize=False)
# 模型推理
inputs = tokenizer(text, return_tensors="pt").to("cuda")
outputs = model.generate(
**inputs,
max_new_tokens=256,
do_sample=True,
temperature=0.7,
)
reply = tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)
return ChatResponse(reply=reply)
```
---
## 六、Web 聊天界面
为了让模型更易用,基于 **Gradio** 搭建了 Web 聊天界面。
### 界面架构
```
app.py
│
├── load_model()
│ └── 加载 LoRA 权重 + 基座模型(约 10 秒)
│
├── predict(message, history)
│ ├── 拼接 Qwen ChatML 格式
│ ├── model.generate(stream=True) ← 流式生成
│ └── yield partial_text ← 逐 token 输出
│
├── gr.ChatInterface(predict)
│ └── Gradio 自动生成聊天 UI
│
└── 回复完成后 → Edge-TTS 合成语音
└── 浏览器自动播放
```
### 核心代码:流式输出
```python
"""
Gradio 聊天界面 — 流式输出实现
依赖: gradio, transformers, edge-tts
"""
import gradio as gr
from transformers import TextIteratorStreamer
from threading import Thread
def predict(message: str, history: list):
"""流式生成回复,逐 token 输出到 Gradio 界面"""
# 构建消息历史
messages = []
for user_msg, bot_msg in history:
messages.append({"role": "user", "content": user_msg})
messages.append({"role": "assistant", "content": bot_msg})
messages.append({"role": "user", "content": message})
# Tokenize
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer(text, return_tensors="pt").to("cuda")
# 流式生成器
streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
generation_kwargs = dict(
**inputs,
streamer=streamer,
max_new_tokens=256,
do_sample=True,
temperature=0.7,
)
# 在后台线程中生成
thread = Thread(target=model.generate, kwargs=generation_kwargs)
thread.start()
# 逐 token 输出到 Gradio
for token_text in streamer:
yield token_text # Gradio 会自动累积显示
# 启动界面
gr.ChatInterface(
fn=predict,
title="🌸 周文慧 AI 聊天",
description="一个会说话的 AI 女孩~",
).launch()
```
> 💡 **技术亮点**:使用 `TextIteratorStreamer` + 后台线程实现流式输出,用户不需要等待完整回复,而是实时看到文字逐字出现,大大提升了交互体验。
---
## 七、本地模型 vs 云端 API 的对比
在实际对比测试中,一个残酷的事实浮出水面:
| 维度 | 本地 LoRA 模型 | DeepSeek API + StyleLearner |
|------|---------------|---------------------------|
| **回复自然度** | ❌ 碎片化、答非所问 | ✅ 非常自然、上下文连贯 |
| **风格模仿** | ⚠️ 一般,有时不像 | ✅ 极好,难以分辨 |
| **GPU 需求** | 需要 3GB 显存 | 完全不需要 |
| **网络需求** | ✅ 完全离线 | ❌ 必须联网 |
| **每次推理成本** | 免费(电费忽略) | ~0.001 元/次 |
| **响应速度** | 慢(3-5 秒) | 快(1-2 秒) |
| **模型版本** | Qwen2.5-1.5B | DeepSeek V3(671B MoE) |
| **参数规模** | 1.5B | ~37B 激活参数 |
> 🔑 **核心结论**:在数据量不足(<1000 条)的场景下,**API + 提示词工程完胜小模型微调**。这不是 LoRA 的问题,而是基座模型能力差距太大——1.5B 和 671B MoE 的差距是数量级的。
### 什么时候应该用微调?
尽管这次微调效果不如 API,但在以下场景中微调仍然不可替代:
| 场景 | 说明 |
|------|------|
| **离线部署** | 内网环境、无网络条件 |
| **数据隐私** | 敏感数据不能发送到外部 API |
| **大批量调用** | API 费用过高时(每天 10 万+ 次) |
| **超低延迟** | 需要毫秒级响应 |
| **特定领域** | 法律、医疗等需要精确领域知识的场景 |
---
## 八、环境配置与运行
### 训练环境
```yaml
硬件:
GPU: NVIDIA RTX 4060 (8GB VRAM)
RAM: 32 GB
CPU: Intel i7-13700H
Disk: 512GB NVMe SSD (剩余 ≥ 20GB)
软件:
OS: Windows 11
Python: 3.10.11
CUDA: 12.1+
PyTorch: 2.1+
Transformers: 4.45+
PEFT: 最新版
LLaMA Factory: 可选
```
### 安装依赖
```bash
# 基础依赖
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
pip install transformers accelerate peft
# 训练框架
pip install llamafactory # 或直接使用 WeClone
# Web 界面
pip install gradio
# 语音合成
pip install edge-tts
```
### 启动推理服务
```bash
# 启动 FastAPI 推理服务
python inference_server.py
# → http://127.0.0.1:8000
# 启动 Gradio Web 界面
python app.py
# → http://127.0.0.1:7860
```
---
## 九、项目地址与资源
### GitHub 仓库
- **[AI Chat Bot](https://github.com/starlight001219/ai)** — Web 聊天界面 + TTS 语音合成
### 相关项目
| 项目 | 地址 |
|------|------|
| **微信智能机器人**(对接该模型) | [github.com/starlight001219/wechat-clone-bot](https://github.com/starlight001219/wechat-clone-bot) |
| **WeClone**(数据采集框架) | [github.com/xming521/WeClone](https://github.com/xming521/WeClone) |
> ⚠️ **重要提示**:模型文件因体积过大(基座模型 2.9GB + LoRA 权重 8.7MB)未包含在 Git 仓库中。使用前需要:
> 1. 从 HuggingFace 下载 Qwen2.5-1.5B-Instruct
> 2. 将 LoRA 权重放在 `model_output/` 目录
---
## 十、经验总结与避坑指南
### 训练类
| # | 问题 | 解决方案 |
|---|------|----------|
| 1 | **3B 模型 OOM** | 降级到 1.5B,VRAM 从 ~6GB 降至 ~3GB |
| 2 | **bitsandbytes Windows segfault** | 放弃 4-bit 量化,使用 fp16 |
| 3 | **Windows 页面文件不足 (1455)** | 使用 `device_map="cuda:0"` 直接 GPU 加载 |
| 4 | **datasets 库 segfault** | 手写 PyTorch Dataset,不用 huggingface datasets |
| 5 | **gradient_checkpointing + device_map 不兼容** | 单 GPU 直接加载,不用 `"auto"` |
### 方案选型类
```
先试 API → 效果不够再考虑微调
↓
API 能满足需求 → 用 API(更省钱、效果更好)
API 不能满足需求(离线/隐私/成本)→ 微调
↓
微调仍然效果不好 → 检查数据量和数据质量
```
### 部署类
- ❌ **不要**把完整模型文件(3GB+)提交到 GitHub
- ✅ **只提交** LoRA 权重(8.7MB)和训练脚本
- ⚠️ 首次加载模型需要 ~10 秒(6GB 文件从磁盘读入内存)
- ✅ 使用 FastAPI 可以方便地对接微信机器人
### 后续优化方向
- [ ] 采集更多数据(目标 5000+ 条)
- [ ] 构建多轮对话样本(目前只有单轮)
- [ ] 尝试 Qwen2.5-7B 云 GPU 训练
- [ ] 加入 RLHF / DPO 进一步对齐
- [ ] 使用 vLLM 加速推理
---
> **后记**:虽然最终在实际使用中选择了 DeepSeek API 方案,但这次微调训练的经历让我对模型训练的全流程有了深刻理解——从数据准备、显存计算、训练参数调优到推理部署。这个过程中踩的每一个坑,都是最宝贵的学习资源。如果你也在 8GB 显存的 GPU 上做微调,希望这篇踩坑实录能帮你节省 3 小时的调试时间。
更多推荐


所有评论(0)