在博客笔记上微调Qwen3-4B | AI生成和翻译
以下是整个过程的完整端到端故事:
══════════════════════════════════════════════════════════════
如何在个人博客笔记上微调 QWEN3-4B
完整流水线:数据 → 训练 → 合并 → GGUF
══════════════════════════════════════════════════════════════
┌─────────────────────────────────────────────────────────────┐
│ 步骤 1: 数据 — build_dataset.py │
│ 来源: ~/projects/jekyll-ai-blog/_posts/{en,zh}/*.md │
│ │
│ 我们抓取了你个人 Jekyll 博客的英语和中文 Markdown 文章, │
│ 包含 frontmatter(标题、正文、元数据)。 │
│ │
│ 清洗流程: │
│ - 去除 Jekyll/Liquid 标签 ({% %}, {{ }}) │
│ - 去除 kramdown 属性列表 ({: .centered }) │
│ - 去除图片引用 () │
│ - 压缩多余空白 │
│ - 删除长度小于100字符的文章 │
│ │
│ 格式: 每个样本转换为一个 "conversations" 数组: │
│ { │
│ "conversations": [ │
│ {"role": "user", "content": "<博客标题>"}, │
│ {"role": "assistant", "content": "<清洗后的正文>"} │
│ ] │
│ } │
│ │
│ 任务: 给定一个博客标题,生成完整的博客正文。 │
│ 模型学会仅通过标题提示来重构你的写作风格、知识和内容。 │
│ │
│ 划分: 21,234 训练 / 200 评估 / 21,434 总计 │
│ 大小: 约86MB原始文本,约32.5M tokens 已处理 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 步骤 2: 训练 — train.py │
│ 框架: 纯 transformers + peft + trl (SFTTrainer) │
│ │
│ 基础模型: │
│ unsloth/Qwen3-4B-unsloth-bnb-4bit │
│ - Qwen3 4B参数,预量化为4-bit (BNB) │
│ - 适配12GB显存(RTX 4070),并留有训练空间 │
│ - 通过 HuggingFace 磁盘缓存加载 │
│ │
│ LoRA 配置: │
│ r = 32 (秩 — 低秩分解维度) │
│ alpha = 32 (缩放因子,alpha/r = 1.0) │
│ dropout = 0 │
│ 目标模块: │
│ q_proj, k_proj, v_proj, o_proj (注意力机制) │
│ gate_proj, up_proj, down_proj (MLP/FFN) │
│ → 每个 Transformer 层的全部7个权重矩阵 │
│ → 仅约1-2%的参数是可训练的(LoRA A/B) │
│ │
│ 为什么用 LoRA? 不是微调全部4B参数,而是 │
│ 在每个权重矩阵旁注入小型秩为32的矩阵。 │
│ 原始权重被冻结,只训练 LoRA 的差值部分。 │
│ 结果: 约80MB适配器 vs 约8GB完整模型。 │
│ │
│ 训练超参数: │
│ batch_size = 2 × grad_accum = 8 = 有效批次 16 │
│ epochs = 2 │
│ lr = 2e-4 (余弦调度,预热3%) │
│ bf16 = True │
│ max_seq_len = 4096 │
│ seed = 42 │
│ │
│ 运行时间: 约10小时52分钟(RTX 4070) │
│ 步数: 共2,656步 │
│ │
│ 最终指标: │
│ loss: 1.417 │
│ 平均 token 准确率: 65.6% │
│ 梯度范数: 约0.033(全程稳定) │
│ 学习率: 完全衰减至约3.6e-09 │
│ │
│ 输出: /mnt/data/zz/finetune/lzw-notes-lora/ │
│ adapter_config.json │
│ adapter_model.safetensors (约80MB) │
│ tokenizer.json + tokenizer_config.json │
│ chat_template.jinja │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 步骤 3: 合并 — merge.py │
│ │
│ 问题: LoRA 适配器是一个增量(DELTA)——它只在推理时 │
│ 与基础模型结合才能工作。 │
│ 为了独立部署,需要将 LoRA 权重合并回基础模型。 │
│ │
│ 流程: │
│ 1. 加载 FP16 基础模型: unsloth/Qwen3-4B │
│ (注意:不是训练时的4-bit副本——需要全精度) │
│ 2. 从 lzw-notes-lora/ 加载 LoRA 适配器 │
│ 3. 调用 model.merge_and_unload() │
│ → 计算: W_merged = W_base + alpha/r * (A @ B) │
│ → 对7个目标模块在每一层执行 │
│ 4. 保存合并后的模型(约8GB FP16) │
│ │
│ 输出: /mnt/data/zz/finetune/lzw-notes-merged/ │
│ 融合了你的博客知识的完整 Qwen3-4B 模型 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 步骤 4: GGUF 导出 — export_gguf.py │
│ │
│ GGUF = llama.cpp / ollama 的标准格式。 │
│ 我们将 FP16(8GB)量化到 Q4_K_M(2.4GB)。 │
│ │
│ 流程(通过 unsloth + llama.cpp): │
│ 1. 加载合并后的 FP16 模型 │
│ 2. 转换 HF → GGUF bf16 (约3分钟) │
│ 3. 量化 bf16 → Q4_K_M (约10分钟) │
│ → 4-bit,混合精度(K-quant,中等质量) │
│ → 模型大小缩减约3.3倍,质量损失极小 │
│ │
│ 输出: /mnt/data/zz/finetune/lzw-notes-merged_gguf/ │
│ lzw-notes-merged.Q4_K_M.gguf (2.4 GB) │
│ │
│ 可用于: │
│ - ollama create lzw-notes -f Modelfile │
│ - llama-cli --model ...Q4_K_M.gguf -p "prompt" │
│ - 任何兼容 GGUF 的运行时 │
└─────────────────────────────────────────────────────────────┘
══════════════════════════════════════════════════════════════
流水线总结
══════════════════════════════════════════════════════════════
博客文章 (.md)
│
▼ build_dataset.py (清洗 + 格式化为对话)
SFT 数据集(21K 样本,约32.5M tokens)
│
▼ train.py (4-bit Qwen3-4B 上的 LoRA,2轮,约11小时)
LoRA 适配器(约80MB)
│
▼ merge.py (将 LoRA 合并到 FP16 基础模型)
合并后的模型(约8GB FP16)
│
▼ export_gguf.py (量化到 Q4_K_M)
GGUF 文件(2.4GB)→ 通过 ollama / llama.cpp 部署
══════════════════════════════════════════════════════════════
关键设计决策
══════════════════════════════════════════════════════════════
1. 为什么选 QWEN3-4B?
在12GB显存下最佳质量/大小比。4B参数提供足够容量
来学习写作风格和内容,无需企业级硬件。
2. 为什么用4-bit基础 + LoRA(而非全量微调)?
基础模型的4-bit量化(QLoRA风格)将显存需求从约16GB
降至约4GB,为单张RTX 4070上的梯度和优化器状态留出空间。
然后LoRA仅训练约1-2%的参数,保持内存可控。
3. 为什么用标题→正文格式(而非聊天或指令)?
从博客数据中衍生出的最简单的SFT格式。模型学习:
"给定一个标题,以该作者的风格和知识生成完整文章。"
适用于内容生成、RAG增强或个人写作助手场景。
4. 为什么用 Q4_K_M 量化?
部署时最佳质量/大小权衡。K-quant方法使用混合精度
(某些层获得的比特数多于其他层)。2.4GB模型可在
任何消费级设备上运行。
══════════════════════════════════════════════════════════════