AI Day 54
AI Day 54: 实战(4):LoRA微调实战 — 训练你的专属模型
AI Day 54: 实战(4):LoRA微调实战 — 训练你的专属模型
2026-05-25
日期: 2026-05-25 | 阶段: 第五阶段 · 动手实战 (Day 51-60) | 主题: LoRA Fine-tuning in Practice
学习路径 / Learning Path
AI/LLM 深度技术学习 60天计划
├── 第一阶段:模型基础 (Day 1-15) ✅
│ ├── Day 1: Transformer与LLM基础 ✅
│ ├── Day 2: 量化与本地部署 ✅
│ ├── Day 3: 训练全流程 ✅
│ ├── Day 4: Prompt Engineering ✅
│ ├── Day 5: RAG架构 ✅
│ ├── Day 6: 向量数据库与Embedding ✅
│ ├── Day 7: 微调技术 ✅
│ ├── Day 8: 推理优化 ✅
│ ├── Day 9: 长上下文技术 ✅
│ ├── Day 10: 多模态模型 ✅
│ ├── Day 11: 推理模型 ✅
│ ├── Day 12: Agent框架 ✅
│ ├── Day 13: MCP协议 ✅
│ ├── Day 14: 模型评估 ✅
│ └── Day 15: 阶段一总结 ✅
├── 第二阶段:工程实践 (Day 16-30) ✅
│ ├── Day 16: LLM应用架构 ✅
│ ├── Day 17: 安全与护栏 ✅
│ ├── Day 18: 可观测性 ✅
│ ├── Day 19: 生产RAG·解析与分块 ✅
│ ├── Day 20: 生产RAG·检索与重排 ✅
│ ├── Day 21: 生产RAG·评估与迭代 ✅
│ ├── Day 22: Agent状态与恢复 ✅
│ ├── Day 23: Agent成本优化 ✅
│ ├── Day 24: 多Agent系统 ✅
│ ├── Day 25: Agent测试部署 ✅
│ ├── Day 26: LLM成本工程 ✅
│ ├── Day 27: 多模型编排 ✅
│ ├── Day 28: LLM应用测试 ✅
│ ├── Day 29: 企业LLM平台 ✅
│ └── Day 30: 阶段二总结 ✅
├── 第三阶段:金融零售AI应用 (Day 31-42) ✅
│ ├── Day 31: 金融AI风控 ✅
│ ├── Day 32: 智能投顾与量化 ✅
│ ├── Day 33: 合规与RegTech ✅
│ ├── Day 34: 信贷AI全链路 ✅
│ ├── Day 35: 金融AI总结 ✅
│ ├── Day 36: 零售AI推荐 ✅
│ ├── Day 37: 智能客服 ✅
│ ├── Day 38: 供应链AI ✅
│ ├── Day 39: 智能营销 ✅
│ ├── Day 40: 零售AI总结 ✅
│ ├── Day 41: CeFi×DeFi×AI融合 ✅
│ └── Day 42: AI融合案例与职业 ✅
├── 第四阶段:面试冲刺 (Day 43-50) ✅
│ ├── Day 43: 系统设计·LLM平台 ✅
│ ├── Day 44: 系统设计·RAG系统 ✅
│ ├── Day 45: 系统设计·Agent系统 ✅
│ ├── Day 46: 系统设计·推荐系统 ✅
│ ├── Day 47: 面试·产品AI ✅
│ ├── Day 48: 面试·架构AI ✅
│ ├── Day 49: 面试·行为AI ✅
│ └── Day 50: 学习总结 ✅
└── 第五阶段:动手实战 (Day 51-60)
├── Day 51: 本地大模型部署全流程 ✅
├── Day 52: RAG系统实战:从文档到问答 ✅
├── Day 53: RAG进阶:评估优化与生产化 ✅
├── Day 54: LoRA微调实战:训练你的专属模型 ← 你在这里
├── Day 55: Agent开发实战:构建工具调用Agent
├── Day 56: MCP Server开发:扩展AI能力边界
├── Day 57: 多模态应用:图文理解与文档分析
├── Day 58: AI应用全栈开发:前后端集成
├── Day 59: 性能调优与成本实战
└── Day 60: 总结与作品集
核心概念 / Core Concepts
微调 = 给通用模型注入专属知识和风格 / Fine-tuning = Injecting Domain Knowledge & Style
Day 7 学过微调的理论,今天来真正动手!
通用基座模型(如 Qwen2.5-7B):
├── 知道很多通用知识
├── 但不了解你的笔记内容
├── 不了解你的写作风格
├── 不了解你的领域偏好
└── 回答时"泛泛而谈"
微调后的模型:
├── 继承通用知识
├── ✅ 了解你笔记中的专业知识
├── ✅ 模仿你的表达风格
├── ✅ 在你的领域回答更精准
└── ✅ 像一个"懂你"的助手
RAG vs 微调:互补而非替代 / RAG vs Fine-tuning: Complementary
Day 52-53 做了 RAG,今天做微调
它们解决不同的问题:
RAG(检索增强生成):
├── 优势:实时更新、可追溯来源、不改变模型
├── 适合:事实查询、最新信息、需要引用
└── 类比:给模型一本"参考书",边翻边答
微调(LoRA Fine-tuning):
├── 优势:内化知识、风格模仿、更快推理
├── 适合:固定领域、风格一致性、离线场景
└── 类比:让模型"上课学习",记到脑子里
最佳实践 = RAG + 微调:
微调让模型"说得对"(领域知识 + 回答风格)
RAG 让模型"有依据"(检索最新内容 + 来源引用)
今天的目标 / Today's Goal
完成一次完整的微调流程:
环境准备 → 数据准备 → 训练 → 调试 → 导出 → 评估
↓ ↓ ↓ ↓ ↓ ↓
Unsloth QA对 QLoRA Loss GGUF 对比
↓
Alpaca格式
↓
500-2000条
最终产出:一个微调过的模型,能在 Ollama 中运行
知识点1:环境准备 / Environment Setup
Unsloth — 最快的微调框架 / The Fastest Fine-tuning Framework
为什么选 Unsloth?
Day 7 学微调时介绍了多种框架:
- HuggingFace PEFT: 最标准但速度一般
- Axolotl: 配置灵活但学习曲线陡
- LLaMA-Factory: 中文生态好但更新慢
- Unsloth: 速度最快、显存最省 ← 选这个!
Unsloth 的核心优势:
├── 速度: 比标准 PEFT 快 2-5 倍(手动优化了反向传播)
├── 显存: 节省 60-80%(梯度检查点 + 内核融合)
├── 易用: API 极简,几行代码开始训练
├── 兼容: 支持 QLoRA、LoRA、Full Fine-tuning
└── 导出: 直接导出 GGUF(不用手动转换)
8GB 显存的 RTX 4060 完全够用!
安装与依赖配置 / Installation & Dependencies
# === Step 1: 创建专用虚拟环境 ===
# 隔离微调环境,避免与 RAG 项目冲突
conda create -n finetune python=3.11 -y
conda activate finetune
# === Step 2: 安装 PyTorch (CUDA 12.1) ===
# RTX 4060 支持 CUDA 12.x
pip install torch==2.4.0 torchvision torchaudio \
--index-url https://download.pytorch.org/whl/cu121
# === Step 3: 安装 Unsloth ===
# 从 GitHub 安装最新版(PyPI 版本可能滞后)
pip install "unsloth[cu121-ampere] @ git+https://github.com/unslothai/unsloth.git"
# === Step 4: 安装其他依赖 ===
pip install datasets transformers accelerate peft
pip install bitsandbytes # 4-bit 量化支持
pip install trl # 训练循环工具
pip install wandb # (可选) 训练可视化
CUDA 验证 / CUDA Verification
"""
verify_env.py
验证微调环境是否就绪
"""
import torch
import sys
def verify_environment():
"""一键验证所有依赖"""
results = {}
# 1. Python 版本
results["python"] = sys.version.split()[0]
print(f"Python: {results['python']}")
# 2. PyTorch + CUDA
results["torch"] = torch.__version__
results["cuda_available"] = torch.cuda.is_available()
results["cuda_version"] = torch.version.cuda if torch.cuda.is_available() else "N/A"
print(f"PyTorch: {results['torch']}")
print(f"CUDA Available: {results['cuda_available']}")
print(f"CUDA Version: {results['cuda_version']}")
# 3. GPU 信息
if torch.cuda.is_available():
gpu_name = torch.cuda.get_device_name(0)
gpu_mem = torch.cuda.get_device_properties(0).total_mem / 1024**3
results["gpu_name"] = gpu_name
results["gpu_memory_gb"] = round(gpu_mem, 1)
print(f"GPU: {gpu_name}")
print(f"GPU Memory: {results['gpu_memory_gb']} GB")
else:
print("WARNING: No GPU detected! Fine-tuning will be extremely slow.")
return results
# 4. Unsloth
try:
from unsloth import FastLanguageModel
results["unsloth"] = "OK"
print(f"Unsloth: OK")
except ImportError as e:
results["unsloth"] = f"FAILED: {e}"
print(f"Unsloth: FAILED - {e}")
# 5. bitsandbytes (4-bit 量化)
try:
import bitsandbytes as bnb
results["bitsandbytes"] = bnb.__version__
print(f"bitsandbytes: {results['bitsandbytes']}")
except ImportError:
results["bitsandbytes"] = "FAILED"
print(f"bitsandbytes: FAILED")
# 6. 显存测试 — 能否加载 4-bit 7B 模型
print("\n--- Quick Memory Test ---")
free_mem = torch.cuda.mem_get_info()[0] / 1024**3
print(f"Free GPU Memory: {free_mem:.1f} GB")
if free_mem >= 5.0:
print("Memory OK: 足够加载 4-bit 7B 模型并训练")
elif free_mem >= 3.0:
print("Memory Tight: 可以训练但需要减小 batch_size")
else:
print("Memory Low: 建议关闭其他 GPU 程序")
return results
if __name__ == "__main__":
results = verify_environment()
all_ok = (
results.get("cuda_available", False)
and results.get("unsloth") == "OK"
and results.get("bitsandbytes") != "FAILED"
)
print(f"\n{'='*50}")
if all_ok:
print("ALL CHECKS PASSED — Ready to fine-tune!")
else:
print("SOME CHECKS FAILED — Fix issues above before continuing.")
RTX 4060 8GB 适配要点 / RTX 4060 8GB Optimization Tips
8GB 显存 = 够但需要精打细算
显存预算分配:
├── 4-bit 量化模型: ~3.5 GB (Qwen2.5-7B-Q4)
├── LoRA 权重: ~0.2 GB (rank=16)
├── 优化器状态: ~0.8 GB (Adam states)
├── 梯度: ~0.5 GB (gradient checkpointing)
├── 激活值: ~1.5 GB (batch_size=1)
└── 其余: ~1.5 GB (框架开销)
─────────
~8.0 GB ← 正好!
如果 OOM,依次尝试:
1. gradient_checkpointing = True (默认就开)
2. per_device_train_batch_size = 1
3. max_seq_length = 1024 → 512
4. 关闭 wandb 可视化 (省一点显存)
5. 降低 rank: 16 → 8
关键:训练前先关闭 Ollama!
Ollama 即使空闲也会占用 ~1GB 显存
taskkill /f /im ollama.exe (Windows)
知识点2:训练数据准备 / Training Data Preparation
从笔记中提取QA对 / Extracting QA Pairs from Notes
"""
prepare_data.py
从学习笔记中提取微调训练数据
数据来源:
- Day 1-53 的学习笔记 (docs/ai/day*.md)
- 已有的 Golden QA 集 (Day 53 构建的)
- 面试题答案 (Day 47-49)
目标数量:500-2000 条
太少:模型学不到什么
太多:过拟合 + 训练时间长
500-2000条:sweet spot for domain adaptation
"""
import json
import re
from pathlib import Path
class NoteQAExtractor:
"""从学习笔记中提取 QA 训练对"""
def __init__(self, notes_dir: str = "docs/ai"):
self.notes_dir = Path(notes_dir)
self.qa_pairs = []
def extract_all(self) -> list[dict]:
"""从所有笔记中提取 QA 对"""
note_files = sorted(self.notes_dir.glob("day*.md"))
print(f"Found {len(note_files)} note files")
for note_file in note_files:
content = note_file.read_text(encoding="utf-8")
day_num = self._parse_day_number(note_file.name)
# 策略1: 从标题和内容生成 QA
self._extract_from_sections(content, note_file.name, day_num)
# 策略2: 从代码块注释生成 QA
self._extract_from_code_comments(content, note_file.name, day_num)
# 策略3: 从"思考"部分生成 QA
self._extract_from_reflections(content, note_file.name, day_num)
print(f"Total QA pairs extracted: {len(self.qa_pairs)}")
return self.qa_pairs
def _parse_day_number(self, filename: str) -> int:
match = re.search(r"day(\d+)", filename)
return int(match.group(1)) if match else 0
def _extract_from_sections(self, content: str, filename: str, day: int):
"""从 ## 标题和下方内容提取 QA"""
sections = re.split(r"\n## ", content)
for section in sections[1:]: # 跳过第一段(标题前的内容)
lines = section.strip().split("\n")
title = lines[0].strip()
# 跳过非知识性标题
if any(skip in title for skip in ["学习路径", "明日预告", "资源"]):
continue
body = "\n".join(lines[1:]).strip()
if len(body) < 100:
continue
# 从标题生成问题
question = self._title_to_question(title)
if question:
# 提取关键内容作为答案(不要太长)
answer = self._extract_answer(body, max_len=500)
if answer:
self.qa_pairs.append({
"instruction": question,
"input": "",
"output": answer,
"source": filename,
"day": day,
})
def _extract_from_code_comments(self, content: str, filename: str, day: int):
"""从代码块的注释/docstring提取 QA"""
code_blocks = re.findall(r"```(?:python|bash)?\n(.*?)```", content, re.DOTALL)
for block in code_blocks:
# 提取 docstring 或注释
docstring = re.search(r'"""(.*?)"""', block, re.DOTALL)
if docstring:
doc = docstring.group(1).strip()
if len(doc) > 50:
# 第一行通常是"标题"
doc_lines = doc.split("\n")
title = doc_lines[0].strip()
explanation = "\n".join(doc_lines[1:]).strip()
if explanation:
self.qa_pairs.append({
"instruction": f"请解释:{title}",
"input": "",
"output": explanation[:500],
"source": filename,
"day": day,
})
def _extract_from_reflections(self, content: str, filename: str, day: int):
"""从'思考'部分提取 QA"""
# 匹配 ### 思考X:xxx 格式
reflections = re.findall(
r"### 思考\d+[::](.*?)\n```\n(.*?)```",
content,
re.DOTALL,
)
for title, body in reflections:
title = title.strip().split("/")[0].strip()
if len(body.strip()) > 100:
self.qa_pairs.append({
"instruction": f"你对「{title}」有什么思考?",
"input": "",
"output": body.strip()[:500],
"source": filename,
"day": day,
})
def _title_to_question(self, title: str) -> str:
"""将章节标题转化为自然语言问题"""
# 移除英文部分和特殊字符
cn_title = title.split("/")[0].strip()
cn_title = re.sub(r"[#*`]", "", cn_title).strip()
if not cn_title or len(cn_title) < 4:
return ""
# 生成问题
question_templates = [
f"请解释什么是{cn_title}?",
f"{cn_title}的核心原理是什么?",
f"请详细说明{cn_title}。",
]
# 轮流使用不同模板
idx = hash(cn_title) % len(question_templates)
return question_templates[idx]
def _extract_answer(self, body: str, max_len: int = 500) -> str:
"""从正文提取关键内容作为答案"""
# 优先提取 ``` 代码块内的文字说明(非代码的)
text_blocks = re.findall(r"```\n(.*?)```", body, re.DOTALL)
# 过滤纯代码块,保留文字说明块
text_content = []
for block in text_blocks:
# 如果没有 import/def/class 等代码关键字,认为是文字
if not re.search(r"\b(import|def |class |from |return )\b", block):
text_content.append(block.strip())
if text_content:
return "\n\n".join(text_content)[:max_len]
# 如果没有文字代码块,取正文前 N 字
plain = re.sub(r"```.*?```", "", body, flags=re.DOTALL)
plain = re.sub(r"\n{3,}", "\n\n", plain).strip()
return plain[:max_len] if len(plain) > 50 else ""
def save(self, output_path: str = "train_data.json"):
"""保存为 JSON 文件"""
with open(output_path, "w", encoding="utf-8") as f:
json.dump(self.qa_pairs, f, ensure_ascii=False, indent=2)
print(f"Saved {len(self.qa_pairs)} QA pairs to {output_path}")
if __name__ == "__main__":
extractor = NoteQAExtractor("docs/ai")
extractor.extract_all()
extractor.save("train_data.json")
Alpaca / ShareGPT 格式转换 / Format Conversion
"""
format_converter.py
将 QA 对转换为微调框架要求的标准格式
两种主流格式:
1. Alpaca 格式 — 最通用,Unsloth 默认支持
2. ShareGPT 格式 — 多轮对话,LLaMA-Factory 常用
"""
import json
# === Alpaca 格式 ===
# Unsloth、Axolotl、LLaMA-Factory 都支持
# 最简单直接,适合单轮QA
ALPACA_TEMPLATE = {
"instruction": "用户的问题/指令",
"input": "可选的额外上下文(大部分为空)",
"output": "模型应该给出的回答",
}
def convert_to_alpaca(qa_pairs: list[dict]) -> list[dict]:
"""
确保数据符合 Alpaca 格式
Alpaca 格式示例:
{
"instruction": "LoRA微调的核心原理是什么?",
"input": "",
"output": "LoRA通过在预训练模型的权重矩阵旁添加低秩分解矩阵..."
}
"""
alpaca_data = []
for qa in qa_pairs:
alpaca_data.append({
"instruction": qa["instruction"],
"input": qa.get("input", ""),
"output": qa["output"],
})
return alpaca_data
# === ShareGPT 格式 ===
# 支持多轮对话
def convert_to_sharegpt(qa_pairs: list[dict]) -> list[dict]:
"""
转换为 ShareGPT 格式
ShareGPT 格式示例:
{
"conversations": [
{"from": "human", "value": "LoRA微调的核心原理是什么?"},
{"from": "gpt", "value": "LoRA通过..."}
]
}
"""
sharegpt_data = []
for qa in qa_pairs:
conversation = {
"conversations": [
{"from": "human", "value": qa["instruction"]},
{"from": "gpt", "value": qa["output"]},
]
}
sharegpt_data.append(conversation)
return sharegpt_data
# === 手动补充高质量 QA 对 ===
# 自动提取的 QA 质量参差不齐
# 手动补充 50-100 条高质量 QA 可以显著提升效果
MANUAL_QA = [
{
"instruction": "什么是LoRA微调?和全量微调有什么区别?",
"input": "",
"output": (
"LoRA (Low-Rank Adaptation) 是一种参数高效微调方法。"
"核心原理:不修改原始模型权重W,而是在旁边添加两个小矩阵A和B,"
"使得 W' = W + BA。其中A和B是低秩矩阵,参数量只有原始的0.1-1%。\n\n"
"与全量微调的区别:\n"
"1. 参数量:LoRA只训练新增的A/B矩阵(几十MB),全量微调训练所有参数(几十GB)\n"
"2. 显存:LoRA在8GB显卡上可训练7B模型,全量微调7B需要60-80GB\n"
"3. 速度:LoRA快2-5倍\n"
"4. 效果:在下游任务上,LoRA接近全量微调的效果\n"
"5. 灵活性:LoRA权重可以热切换,一个基座配多个LoRA"
),
},
{
"instruction": "RAG和微调各自适合什么场景?应该怎么选?",
"input": "",
"output": (
"RAG适合的场景:\n"
"1. 知识库频繁更新(如新闻、文档)\n"
"2. 需要引用来源、可追溯\n"
"3. 数据量大、内容多样\n"
"4. 需要精确的事实查询\n\n"
"微调适合的场景:\n"
"1. 领域固定、知识相对稳定\n"
"2. 需要特定输出风格/格式\n"
"3. 需要更快的推理速度(无检索延迟)\n"
"4. 离线/边缘部署场景\n\n"
"最佳实践:两者结合。微调让模型'懂行',RAG提供'依据'。"
),
},
# ... 继续添加 50-100 条高质量 QA
]
数据清洗和质量检查 / Data Cleaning & Quality Check
"""
data_quality.py
微调数据质量检查 — 垃圾进垃圾出,数据质量决定微调效果
Day 3 学过:数据质量 > 数据数量
100条高质量数据 > 10000条低质量数据
"""
import json
import re
from collections import Counter
class DataQualityChecker:
"""微调数据质量检查器"""
def __init__(self, data: list[dict]):
self.data = data
self.issues = []
def run_all_checks(self) -> dict:
"""运行所有质量检查"""
print(f"\n{'='*60}")
print(f"Data Quality Check: {len(self.data)} samples")
print(f"{'='*60}")
stats = {
"total": len(self.data),
"passed": 0,
"issues": [],
}
# 检查1: 空值检测
self._check_empty_fields()
# 检查2: 长度分布
self._check_length_distribution()
# 检查3: 重复检测
self._check_duplicates()
# 检查4: 指令多样性
self._check_instruction_diversity()
# 检查5: 输出质量
self._check_output_quality()
# 检查6: 格式一致性
self._check_format_consistency()
stats["issues"] = self.issues
stats["passed"] = stats["total"] - len(self.issues)
self._print_summary(stats)
return stats
def _check_empty_fields(self):
"""检查空字段"""
empty_count = 0
for i, item in enumerate(self.data):
if not item.get("instruction", "").strip():
self.issues.append(f"#{i}: Empty instruction")
empty_count += 1
if not item.get("output", "").strip():
self.issues.append(f"#{i}: Empty output")
empty_count += 1
print(f" Empty fields: {empty_count}")
def _check_length_distribution(self):
"""检查长度分布"""
inst_lens = [len(d["instruction"]) for d in self.data]
out_lens = [len(d["output"]) for d in self.data]
print(f"\n Instruction length:")
print(f" Min={min(inst_lens)} Max={max(inst_lens)} "
f"Avg={sum(inst_lens)/len(inst_lens):.0f}")
print(f" Output length:")
print(f" Min={min(out_lens)} Max={max(out_lens)} "
f"Avg={sum(out_lens)/len(out_lens):.0f}")
# 标记过短的输出
short_count = sum(1 for l in out_lens if l < 20)
if short_count:
print(f" WARNING: {short_count} outputs shorter than 20 chars")
# 标记过长的输出
long_count = sum(1 for l in out_lens if l > 2000)
if long_count:
print(f" WARNING: {long_count} outputs longer than 2000 chars")
print(f" Recommend truncating to avoid training instability")
def _check_duplicates(self):
"""检查重复数据"""
instructions = [d["instruction"] for d in self.data]
dups = [inst for inst, count in Counter(instructions).items() if count > 1]
if dups:
print(f"\n Duplicates: {len(dups)} duplicate instructions found")
for d in dups[:5]:
print(f" - {d[:60]}...")
else:
print(f"\n Duplicates: None found")
def _check_instruction_diversity(self):
"""检查指令多样性"""
first_words = [d["instruction"].split()[0] if d["instruction"] else ""
for d in self.data]
word_dist = Counter(first_words).most_common(10)
print(f"\n Instruction starting words (top 10):")
for word, count in word_dist:
pct = count / len(self.data) * 100
print(f" '{word}': {count} ({pct:.1f}%)")
# 如果单一开头词超过50%,多样性不足
if word_dist[0][1] / len(self.data) > 0.5:
print(f" WARNING: Low diversity — '{word_dist[0][0]}' dominates")
def _check_output_quality(self):
"""检查输出质量"""
low_quality = 0
for i, item in enumerate(self.data):
output = item["output"]
# 检查是否只是复制了 instruction
if output.strip() == item["instruction"].strip():
self.issues.append(f"#{i}: Output == Instruction")
low_quality += 1
# 检查是否包含 markdown 残留
if output.startswith("```") or output.startswith("---"):
self.issues.append(f"#{i}: Raw markdown in output")
low_quality += 1
print(f"\n Low quality outputs: {low_quality}")
def _check_format_consistency(self):
"""检查格式一致性"""
has_input = sum(1 for d in self.data if d.get("input", "").strip())
print(f"\n Format:")
print(f" With input field: {has_input}/{len(self.data)}")
print(f" Without input: {len(self.data) - has_input}/{len(self.data)}")
def _print_summary(self, stats: dict):
"""打印总结"""
print(f"\n{'='*60}")
print(f"Summary:")
print(f" Total samples: {stats['total']}")
print(f" Issues found: {len(stats['issues'])}")
print(f" Quality rate: {stats['passed']/stats['total']*100:.1f}%")
if stats["total"] < 500:
print(f"\n RECOMMENDATION: Data too few ({stats['total']})")
print(f" Target: 500-2000 samples for good results")
elif stats["total"] > 5000:
print(f"\n RECOMMENDATION: Consider reducing to 2000-3000")
print(f" Too much data may cause overfitting on 7B model")
else:
print(f"\n Data quantity OK for fine-tuning.")
print(f"{'='*60}")
def clean_data(data: list[dict]) -> list[dict]:
"""清洗数据:去重、去空、截断过长"""
seen = set()
cleaned = []
for item in data:
inst = item["instruction"].strip()
output = item["output"].strip()
# 跳过空值
if not inst or not output:
continue
# 跳过重复
if inst in seen:
continue
seen.add(inst)
# 截断过长输出(保留前1500字)
if len(output) > 1500:
output = output[:1500] + "..."
cleaned.append({
"instruction": inst,
"input": item.get("input", ""),
"output": output,
})
print(f"Cleaned: {len(data)} → {len(cleaned)} samples")
return cleaned
知识点3:QLoRA 微调流程 / QLoRA Fine-tuning Pipeline
基座模型选择 / Base Model Selection
为什么选 Qwen2.5-7B?
Day 51 部署时的经验告诉我们:
模型选择矩阵:
┌──────────────────┬──────────┬───────────┬──────────┐
│ 模型 │ 显存需求 │ 中文能力 │ 微调支持 │
├──────────────────┼──────────┼───────────┼──────────┤
│ Qwen2.5-7B │ ~3.5GB │ ★★★★★ │ ★★★★★ │ ← 选这个
│ Llama-3.1-8B │ ~4.0GB │ ★★★ │ ★★★★★ │
│ Mistral-7B │ ~3.8GB │ ★★ │ ★★★★ │
│ Yi-1.5-9B │ ~4.5GB │ ★★★★ │ ★★★ │
│ Qwen2.5-3B │ ~2.0GB │ ★★★★ │ ★★★★★ │ 降级备选
└──────────────────┴──────────┴───────────┴──────────┘
选择理由:
1. 中文能力最强 — 笔记中英混合,中文为主
2. 显存友好 — 4-bit 量化只需 ~3.5GB
3. Unsloth 一等公民支持
4. 社区活跃,问题容易找到解答
完整训练脚本 / Full Training Script
"""
train.py
LoRA 微调完整训练脚本
Day 7 学的理论:
LoRA 添加低秩矩阵 A (d×r) 和 B (r×d)
W' = W + α/r × BA
只训练 A 和 B,参数量 ≈ 原始的 0.1%
今天的实践:
基座: Qwen2.5-7B (4-bit 量化)
方法: QLoRA (量化 + LoRA)
硬件: RTX 4060 8GB
数据: 笔记 QA 对 500-2000 条
"""
from unsloth import FastLanguageModel
import torch
from datasets import load_dataset
from trl import SFTTrainer
from transformers import TrainingArguments
# ==========================================
# 1. 加载基座模型 (4-bit 量化)
# ==========================================
print("Loading base model...")
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="unsloth/Qwen2.5-7B", # Unsloth 优化版
max_seq_length=2048, # 最大序列长度
dtype=None, # 自动选择 (float16)
load_in_4bit=True, # 4-bit 量化加载
)
print(f"Model loaded. GPU Memory: {torch.cuda.memory_allocated()/1024**3:.1f} GB")
# ==========================================
# 2. 添加 LoRA 适配器
# ==========================================
"""
超参选择理由 (Day 7 学的理论 → 实际取值):
rank = 16
理论:rank 越大,表达能力越强,但参数越多
实践:7B 模型用 rank=16 是甜点
类比:16维"笔记空间"足够编码领域知识
lora_alpha = 32
理论:alpha/rank = 缩放系数,控制 LoRA 的影响强度
实践:alpha = 2 × rank 是经验值
alpha/rank = 32/16 = 2,意味着 LoRA 调整被放大 2 倍
target_modules:
理论:应用 LoRA 到哪些权重矩阵
实践:q_proj/k_proj/v_proj (注意力) + 部分 FFN
Unsloth 会自动选择最优组合
"""
model = FastLanguageModel.get_peft_model(
model,
r=16, # LoRA rank
target_modules=[
"q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj",
],
lora_alpha=32, # 缩放系数
lora_dropout=0.05, # Dropout 防过拟合
bias="none", # 不训练 bias
use_gradient_checkpointing="unsloth", # Unsloth 优化版
random_state=42,
)
# 打印可训练参数量
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
all_params = sum(p.numel() for p in model.parameters())
print(f"Trainable params: {trainable_params:,} / {all_params:,} "
f"= {trainable_params/all_params*100:.2f}%")
# ==========================================
# 3. 准备数据集
# ==========================================
"""
Alpaca Prompt 模板
Unsloth 提供了内置模板,确保训练和推理格式一致
"""
alpaca_prompt = """Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.
### Instruction:
{instruction}
### Input:
{input}
### Response:
{output}"""
def formatting_prompts_func(examples):
"""将数据集格式化为 Alpaca 模板"""
instructions = examples["instruction"]
inputs = examples["input"]
outputs = examples["output"]
texts = []
for inst, inp, out in zip(instructions, inputs, outputs):
text = alpaca_prompt.format(
instruction=inst,
input=inp if inp else "",
output=out,
)
texts.append(text + tokenizer.eos_token)
return {"text": texts}
# 加载数据集
dataset = load_dataset("json", data_files="train_data.json", split="train")
print(f"Dataset: {len(dataset)} samples")
# 格式化
dataset = dataset.map(formatting_prompts_func, batched=True)
# ==========================================
# 4. 配置训练参数
# ==========================================
training_args = TrainingArguments(
# --- 输出 ---
output_dir="./output_lora",
# --- 训练轮次 ---
num_train_epochs=3, # 3 轮,500条数据足够
# 如果数据 > 1500 条,建议 2 轮
# 如果数据 < 300 条,可以 5 轮
# --- Batch Size ---
per_device_train_batch_size=2, # 8GB 显存用 2
gradient_accumulation_steps=4, # 等效 batch_size = 2 × 4 = 8
# --- 学习率 ---
learning_rate=2e-4, # QLoRA 经典学习率
lr_scheduler_type="cosine", # Cosine 衰减
warmup_steps=10, # 预热步数
# --- 精度 ---
fp16=not torch.cuda.is_bf16_supported(),
bf16=torch.cuda.is_bf16_supported(), # A100/4090 用 bf16
# --- 日志 ---
logging_steps=10, # 每 10 步打印 Loss
save_strategy="epoch", # 每轮保存
save_total_limit=3, # 最多保存 3 个检查点
# --- 优化 ---
optim="adamw_8bit", # 8-bit Adam 省显存
weight_decay=0.01,
max_grad_norm=0.3, # 梯度裁剪
seed=42,
)
# ==========================================
# 5. 创建 Trainer 并开始训练
# ==========================================
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
train_dataset=dataset,
dataset_text_field="text",
max_seq_length=2048,
args=training_args,
packing=False, # 不打包短样本(简单起见)
)
print("\n" + "=" * 60)
print("Starting training...")
print(f" Samples: {len(dataset)}")
print(f" Epochs: {training_args.num_train_epochs}")
print(f" Batch: {training_args.per_device_train_batch_size} "
f"× {training_args.gradient_accumulation_steps} "
f"= {training_args.per_device_train_batch_size * training_args.gradient_accumulation_steps}")
print(f" LR: {training_args.learning_rate}")
print(f" GPU Mem: {torch.cuda.memory_allocated()/1024**3:.1f} GB")
print("=" * 60 + "\n")
# 开始训练!
train_result = trainer.train()
# 打印训练结果
print(f"\n{'='*60}")
print(f"Training completed!")
print(f" Total steps: {train_result.global_step}")
print(f" Final loss: {train_result.training_loss:.4f}")
print(f" Runtime: {train_result.metrics['train_runtime']:.0f}s")
print(f" GPU Memory: {torch.cuda.max_memory_allocated()/1024**3:.1f} GB peak")
print(f"{'='*60}")
# 保存 LoRA 权重
model.save_pretrained("./output_lora/final")
tokenizer.save_pretrained("./output_lora/final")
print(f"\nLoRA weights saved to ./output_lora/final")
Loss 监控与训练日志 / Loss Monitoring & Training Logs
训练过程中你会看到这样的日志:
Step 10 | Loss: 2.3456 | LR: 1.8e-4 | GPU Mem: 7.2 GB
Step 20 | Loss: 1.8901 | LR: 2.0e-4 | GPU Mem: 7.3 GB
Step 30 | Loss: 1.5234 | LR: 2.0e-4 | GPU Mem: 7.3 GB
Step 40 | Loss: 1.2567 | LR: 1.9e-4 | GPU Mem: 7.3 GB
Step 50 | Loss: 1.0890 | LR: 1.8e-4 | GPU Mem: 7.3 GB
...
Step 180 | Loss: 0.4523 | LR: 0.3e-4 | GPU Mem: 7.3 GB
Step 190 | Loss: 0.4234 | LR: 0.1e-4 | GPU Mem: 7.3 GB
健康的训练曲线:
开始: Loss ~2.0-2.5 (模型还不会)
中期: Loss ~0.5-1.0 (在学习)
结束: Loss ~0.3-0.5 (学得差不多了)
Loss 下降的直觉理解:
Loss 2.5: 模型在"瞎猜"
Loss 1.0: 模型能猜出大概方向
Loss 0.5: 模型说得像模像样
Loss 0.2: 可能开始过拟合了(小心!)
Loss 0.05: 几乎肯定过拟合了
知识点4:训练调试 / Training Debugging
Loss 不下降 / Loss Not Decreasing
问题:训练了50步,Loss 还在 2.3 附近晃
可能原因和解决方案:
原因1: 学习率太小
症状: Loss 缓慢下降或不动
解决: 提高 LR: 2e-4 → 5e-4 → 1e-3
┌──────────────────────────────────┐
│ learning_rate=5e-4 # 试试更大 │
└──────────────────────────────────┘
原因2: 数据格式错误
症状: Loss 高且不稳定
检查:
# 打印一条格式化后的样本看看
print(dataset[0]["text"][:500])
# 确保 instruction/output 没有颠倒
# 确保 EOS token 正确添加
原因3: LoRA rank 太小
症状: Loss 下降到某个值后卡住
解决: rank: 8 → 16 → 32
但注意 rank 增大 → 显存增加
原因4: 数据质量差
症状: Loss 波动大,不稳定
解决: 回去检查数据质量(知识点2的质量检查器)
调试步骤:
1. 先确认数据格式正确
2. 用小数据集(50条)快速跑通
3. 确认 Loss 能下降后再用全量数据
4. 不要一上来就用 2000 条训练!
过拟合判断与处理 / Overfitting Detection
过拟合 = 模型"背答案"而非"学知识"
过拟合的信号:
├── Training Loss < 0.1 (太低了)
├── 回答和训练数据一模一样(原文复述)
├── 对训练集外的问题回答变差
├── 回答变得"机械",缺乏灵活性
└── 开始"编造"训练集里有但相关性不大的内容
判断方法:留出 10% 数据做验证集
# 分割数据
train_test = dataset.train_test_split(test_size=0.1, seed=42)
train_dataset = train_test["train"]
eval_dataset = train_test["test"]
# 训练时监控验证集 Loss
training_args = TrainingArguments(
...
evaluation_strategy="steps",
eval_steps=50, # 每50步评估一次
)
# 健康:eval_loss 和 train_loss 同步下降
# 过拟合:train_loss 下降但 eval_loss 开始上升
#
# Loss
# 2.0 ─ ╲
# │ ╲╲ train (下降)
# 1.0 ─ ╲───────────
# │ ╱ eval (开始上升!) ← 过拟合点
# 0.5 ─ ╱
# └──────────────── Steps
应对过拟合的策略:
1. 减少 Epoch: 3 → 2 → 1
2. 增大 Dropout: 0.05 → 0.1
3. 减小 rank: 16 → 8
4. 增加训练数据量
5. Early Stopping(在验证 Loss 上升时停止)
显存 OOM 排查 / Out of Memory Troubleshooting
OOM 是微调最常见的错误
典型报错:
torch.cuda.OutOfMemoryError: CUDA out of memory.
Tried to allocate 256.00 MiB (GPU 0; 8.00 GiB total capacity;
6.78 GiB already allocated; 128.00 MiB free...)
排查步骤:
Step 1: 确认显存占用情况
nvidia-smi # 看是否有其他程序占用
# 常见"偷"显存的程序: Ollama, Chrome, VS Code GPU渲染
Step 2: 逐步降低显存需求
┌────────────────────────────────────────────┐
│ 操作 │ 节省显存 │ 风险 │
├────────────────────────────────────────────┤
│ 关闭 Ollama │ ~1-2 GB │ 无 │
│ batch_size: 2→1 │ ~1 GB │ 变慢 │
│ max_seq_length: 2048→1024│ ~1.5 GB │ 截断 │
│ rank: 16→8 │ ~0.1 GB │ 质量 │
│ 关闭 wandb │ ~0.2 GB │ 无 │
│ 换 Qwen2.5-3B │ ~1.5 GB │ 质量 │
└────────────────────────────────────────────┘
Step 3: 如果还是 OOM
# 用 Unsloth 的超级省显存模式
model = FastLanguageModel.from_pretrained(
model_name="unsloth/Qwen2.5-7B",
max_seq_length=512, # 大幅缩短
load_in_4bit=True,
)
# 或者降级到 3B 模型
model = FastLanguageModel.from_pretrained(
model_name="unsloth/Qwen2.5-3B",
max_seq_length=2048,
load_in_4bit=True,
)
Step 4: 训练过程中监控
import torch
# 在训练脚本中定期打印
print(f"GPU Memory: {torch.cuda.memory_allocated()/1024**3:.1f} GB "
f"/ {torch.cuda.memory_reserved()/1024**3:.1f} GB reserved")
Epoch 选择与学习率调整 / Epoch & LR Tuning
Epoch 选择经验法则:
数据量 vs Epoch 推荐:
┌──────────────┬────────────┬──────────────┐
│ 数据量 │ 推荐 Epoch │ 原因 │
├──────────────┼────────────┼──────────────┤
│ < 200 条 │ 5-8 │ 数据少需多轮 │
│ 200-500 条 │ 3-5 │ 标准范围 │
│ 500-2000 条 │ 2-3 │ 数据够,防过拟合 │
│ > 2000 条 │ 1-2 │ 一轮可能就够 │
└──────────────┴────────────┴──────────────┘
学习率策略:
1. 起点: lr=2e-4 (QLoRA 标准值)
2. 如果 Loss 不降: 试 5e-4 或 1e-3
3. 如果 Loss 震荡: 降到 1e-4 或 5e-5
4. Cosine Schedule 比 Linear 更稳定
# Cosine 学习率曲线
#
# LR
# 2e-4 ─ ╲ warmup ╲
# │ ╱ ╲
# 1e-4 ─ ╲
# │ ╲
# 0 ─ ╲___
# └──────────────────── Steps
实用建议:
先用默认值跑一次
看 Loss 曲线决定是否调整
一次只改一个参数!
知识点5:模型导出与部署 / Model Export & Deployment
LoRA 合并 / LoRA Merging
"""
merge_and_export.py
将 LoRA 权重合并到基座模型,然后导出为 GGUF
流程:
基座模型 (Qwen2.5-7B) + LoRA 权重 → 合并后模型 → GGUF 量化 → Ollama
"""
from unsloth import FastLanguageModel
# ==========================================
# 1. 加载训练好的模型
# ==========================================
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="./output_lora/final", # LoRA 权重路径
max_seq_length=2048,
dtype=None,
load_in_4bit=True,
)
# 快速测试:看看微调后的效果
FastLanguageModel.for_inference(model)
prompt = alpaca_prompt.format(
instruction="什么是LoRA微调?",
input="",
output="",
)
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
outputs = model.generate(
**inputs,
max_new_tokens=256,
temperature=0.7,
top_p=0.9,
)
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
print(f"Test response:\n{response}")
# ==========================================
# 2. 导出 GGUF (Unsloth 直接支持!)
# ==========================================
"""
GGUF 量化级别选择 (Day 2 学过):
Q4_K_M: 平衡质量和大小(推荐)
Q5_K_M: 更好质量,稍大
Q8_0: 最佳质量,最大
"""
# 方法1: 直接导出 GGUF (推荐)
model.save_pretrained_gguf(
"my-finetuned-model", # 输出目录
tokenizer,
quantization_method="q4_k_m", # 4-bit 量化
)
print("GGUF exported: my-finetuned-model/")
# 方法2: 导出多个量化版本
for quant in ["q4_k_m", "q5_k_m", "q8_0"]:
model.save_pretrained_gguf(
f"my-model-{quant}",
tokenizer,
quantization_method=quant,
)
print(f"Exported: my-model-{quant}/")
# 方法3: 如果只想保存 LoRA (不合并)
model.save_pretrained("my-lora-adapter")
# 之后可以用 llama.cpp 手动合并
导入 Ollama / Importing to Ollama
# === Step 1: 确认 GGUF 文件 ===
ls my-finetuned-model/
# 应该看到: unsloth.Q4_K_M.gguf
# === Step 2: 创建 Modelfile ===
cat > Modelfile << 'EOF'
# 基于微调后的 GGUF 模型
FROM ./my-finetuned-model/unsloth.Q4_K_M.gguf
# 模型参数
PARAMETER temperature 0.7
PARAMETER top_p 0.9
PARAMETER top_k 40
PARAMETER num_ctx 2048
PARAMETER stop "<|im_end|>"
PARAMETER stop "<|endoftext|>"
# 系统提示词
SYSTEM """你是一个基于 AI/LLM 学习笔记微调的专属助手。
你掌握了 Transformer、RAG、Agent、微调、金融AI、零售AI 等领域的知识。
请基于你学到的知识,用清晰、结构化的方式回答问题。
如果不确定,请诚实说明。"""
# Alpaca 格式模板
TEMPLATE """{{ if .System }}<|im_start|>system
{{ .System }}<|im_end|>
{{ end }}{{ if .Prompt }}<|im_start|>user
{{ .Prompt }}<|im_end|>
{{ end }}<|im_start|>assistant
{{ .Response }}<|im_end|>"""
EOF
# === Step 3: 创建 Ollama 模型 ===
ollama create my-ai-notes -f Modelfile
# === Step 4: 测试运行 ===
ollama run my-ai-notes "什么是LoRA微调?和全量微调有什么区别?"
# === Step 5: 对比基座模型 ===
ollama run qwen2.5:7b "什么是LoRA微调?和全量微调有什么区别?"
微调前 vs 微调后效果对比 / Before vs After Comparison
同一个问题,对比三个模型的回答:
问题:"RAG系统中如何评估检索质量?"
──────────────────────────────────────────────
基座模型 (Qwen2.5-7B 原版):
──────────────────────────────────────────────
"RAG系统的检索质量可以通过以下方式评估:
1. 准确率和召回率
2. NDCG 指标
3. 人工评估
..."
→ 泛泛的教科书回答,没有深度
──────────────────────────────────────────────
微调模型 (my-ai-notes):
──────────────────────────────────────────────
"Day 21 学的 RAGAS 框架提供了四个核心指标:
1. Context Precision: 检索到的内容是否精确相关
2. Context Recall: 是否检索到了所有相关内容
3. Faithfulness: 回答是否忠于上下文
4. Answer Relevancy: 回答是否与问题相关
Day 53 实战中,我用 Golden QA 集做了基线评估,
发现 Hybrid Search 的 Precision 比纯向量搜索高 15%..."
→ 有具体框架名、有实战经验、有数据
──────────────────────────────────────────────
Claude (参考答案):
──────────────────────────────────────────────
"RAG检索质量评估可以从多个维度进行..."
→ 最全面、最准确,但没有个人经验视角
──────────────────────────────────────────────
关键差异:
基座 → 教科书式回答,适合通用场景
微调 → 融入个人学习经验,带有"我"的视角
Claude → 最强大但无法个性化
知识点6:微调效果评估 / Fine-tuning Evaluation
系统化对比评估 / Systematic Comparison
"""
eval_finetuned.py
对比评估:基座 vs 微调 vs Claude
用 Day 53 的 Golden QA 集
对三个模型同时提问,人工评分
"""
import json
from openai import OpenAI
class ModelComparator:
"""三模型对比评估器"""
def __init__(self):
# Ollama API (本地模型)
self.local_client = OpenAI(
base_url="http://localhost:11434/v1",
api_key="ollama",
)
def compare(self, question: str) -> dict:
"""同一问题,三个模型回答"""
results = {}
# 1. 基座模型
results["base"] = self._query_ollama("qwen2.5:7b", question)
# 2. 微调模型
results["finetuned"] = self._query_ollama("my-ai-notes", question)
# 3. Claude (如果有 API key)
# results["claude"] = self._query_claude(question)
return results
def _query_ollama(self, model: str, question: str) -> str:
"""查询 Ollama 模型"""
try:
response = self.local_client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": question}],
temperature=0.7,
max_tokens=500,
)
return response.choices[0].message.content
except Exception as e:
return f"Error: {e}"
def batch_compare(self, questions: list[str]) -> list[dict]:
"""批量对比"""
results = []
for i, q in enumerate(questions):
print(f"\n[{i+1}/{len(questions)}] {q[:50]}...")
comparison = self.compare(q)
comparison["question"] = q
results.append(comparison)
# 打印对比
for model, answer in comparison.items():
if model != "question":
print(f"\n [{model}]: {answer[:150]}...")
return results
# 评估题目集
EVAL_QUESTIONS = [
"LoRA微调的核心原理是什么?",
"RAG和微调各自适合什么场景?",
"Transformer中Self-Attention的计算过程是什么?",
"如何评估一个RAG系统的质量?",
"Agent中的ReAct模式是如何工作的?",
"信贷AI风控的关键环节有哪些?",
"推荐系统的冷启动问题如何解决?",
"CeFi和DeFi在风控架构上有什么区别?",
"LLM应用的成本优化策略有哪些?",
"如何设计一个多Agent协作系统?",
]
if __name__ == "__main__":
comparator = ModelComparator()
results = comparator.batch_compare(EVAL_QUESTIONS)
# 保存结果
with open("comparison_results.json", "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2)
print(f"\nResults saved. Please review and score manually.")
人工评分标准 / Manual Scoring Rubric
对每个回答,按三个维度打分 (1-5):
1. 领域知识 (Domain Knowledge)
1分: 回答错误或完全不相关
2分: 只有泛泛的概述
3分: 有正确的基本概念
4分: 有具体细节和框架
5分: 有深度见解和个人经验
2. 回答风格 (Response Style)
1分: 机械、无结构
2分: 有结构但生硬
3分: 清晰、有条理
4分: 自然流畅,像人写的
5分: 有独特风格,令人印象深刻
3. 准确率 (Accuracy)
1分: 多处错误
2分: 有少量错误
3分: 基本准确,有小瑕疵
4分: 准确,无明显错误
5分: 完全准确,可作为参考
评分模板:
┌─────────────────────────────────────────────────────┐
│ 问题: "LoRA微调的核心原理是什么?" │
├──────────┬───────────┬──────────┬──────────┬────────┤
│ 模型 │ 领域知识 │ 回答风格 │ 准确率 │ 总分 │
├──────────┼───────────┼──────────┼──────────┼────────┤
│ 基座 │ 3 │ 3 │ 4 │ 10/15 │
│ 微调 │ 5 │ 4 │ 4 │ 13/15 │
│ Claude │ 5 │ 5 │ 5 │ 15/15 │
└──────────┴───────────┴──────────┴──────────┴────────┘
值不值得微调?判断框架 / Is Fine-tuning Worth It?
完成整个流程后,问自己这些问题:
投入成本:
├── 数据准备: ~3-4 小时
├── 环境配置: ~1 小时
├── 训练时间: ~30-60 分钟 (500条/3 epochs/RTX 4060)
├── 调试时间: ~1-2 小时
├── 评估时间: ~1-2 小时
└── 总计: ~8-10 小时
获得收益:
├── 领域知识: 基座 3.5 → 微调 4.2 (+20%)
├── 回答风格: 基座 3.0 → 微调 4.0 (+33%)
├── 推理速度: 无额外延迟(不需要检索)
└── 模型大小: 和基座一样(LoRA 已合并)
值得微调的场景:
✅ 需要固定风格/格式的输出(如"用我的笔记风格回答")
✅ 领域知识相对稳定,不经常更新
✅ 需要离线/边缘部署
✅ 高频查询,想省掉 RAG 检索时间
✅ 模型需要"人设"(如客服、教练、导师)
不值得微调的场景:
❌ 知识库频繁更新(每天都有新内容)
❌ 需要精确引用来源
❌ Claude/GPT-4 的 API 成本可接受
❌ 训练数据量 < 100 条
❌ 只是想在特定问题上表现好(用 Prompt 就够了)
我的结论:
微调 + RAG 组合使用 = 最佳效果
微调负责"风格"和"基础领域知识"
RAG 负责"精确查询"和"最新信息"
两者互补,不是二选一
今日思考 / Today's Reflections
思考1:理论到实践的鸿沟比想象中大 / Theory-Practice Gap is Real
Day 7 学微调理论时,觉得自己"都懂了":
LoRA = 低秩分解,W' = W + BA
QLoRA = 4-bit 量化 + LoRA
rank 越大越好,alpha = 2 × rank
今天实操发现了理论没教的事:
1. 安装环境花了比预期多的时间(依赖冲突)
2. 数据准备的工作量远超训练本身
3. OOM 排查需要逐步尝试,没有万能公式
4. Loss 数字的"直觉"需要经验积累
5. 导出 GGUF 的格式兼容性问题
最大感悟:
看教程觉得微调"好简单"
实际做了才知道每一步都有坑
但每个坑都是真实的工程经验
面试时能讲出这些"坑"= 有实战能力的证明
思考2:数据质量 >> 模型大小 >> 训练技巧 / Data Quality Matters Most
一天的实验验证了 Day 3 学的原则:
"Garbage In, Garbage Out"
花时间的正确优先级:
60% 时间 → 数据准备和清洗
20% 时间 → 训练和调参
20% 时间 → 评估和迭代
常见的错误优先级:
10% 数据准备 → "差不多就行"
70% 调参 → "lr 到底是 2e-4 还是 3e-4?"
20% 评估 → "看起来还行"
500 条高质量数据 > 5000 条低质量数据
什么是"高质量":
✅ 问题自然、多样
✅ 答案准确、有深度
✅ 覆盖目标领域的关键知识
✅ 没有重复、没有矛盾
❌ 问题单一(全是"什么是xxx")
❌ 答案敷衍(一两句话)
❌ 有错误信息
思考3:微调是 PM 的新技能 / Fine-tuning as a PM Skill
传统 PM 不需要懂微调
AI 时代的 PM 必须理解微调
不是要你会写训练代码,而是要理解:
1. 什么时候该用微调 vs RAG vs Prompt Engineering
→ 产品架构决策
2. 微调需要多少数据、多少成本
→ 项目规划和资源评估
3. 微调能解决什么问题、不能解决什么问题
→ 设定正确的期望
4. 如何评估微调效果
→ 产品质量把控
今天的实操让我能在面试中说:
"我实际做过 QLoRA 微调,
在 RTX 4060 上用 500 条数据训练了一个领域模型,
领域知识评分从 3.5 提升到 4.2,
并且和 RAG 系统结合使用达到了最佳效果。"
这比"我了解微调的原理"有说服力 100 倍。
学习资源 / Resources
微调框架
- Unsloth: https://github.com/unslothai/unsloth
- HuggingFace PEFT: https://github.com/huggingface/peft
- LLaMA-Factory: https://github.com/hiyouga/LLaMA-Factory
- Axolotl: https://github.com/OpenAccess-AI-Collective/axolotl
教程与文档
- Unsloth Wiki: https://docs.unsloth.ai/
- QLoRA 论文: https://arxiv.org/abs/2305.14314
- LoRA 论文: https://arxiv.org/abs/2106.09685
- HuggingFace Fine-tuning Guide: https://huggingface.co/docs/transformers/training
数据集准备
- Alpaca 格式说明: https://github.com/tatsu-lab/stanford_alpaca
- ShareGPT 格式: https://huggingface.co/datasets/anon8231489123/ShareGPT_Vicuna_unfiltered
- 数据质量最佳实践: https://docs.anthropic.com/en/docs/build-with-claude/fine-tuning
模型导出
- llama.cpp GGUF: https://github.com/ggerganov/llama.cpp
- Ollama Modelfile: https://github.com/ollama/ollama/blob/main/docs/modelfile.md
明日预告 / Tomorrow's Preview
Day 55: Agent开发实战 — 构建能用工具的AI助手
从"微调模型"到"Agent":
微调 = 让模型更聪明(改变大脑)
Agent = 让模型能行动(给它手和脚)
明天将实际构建:
1. 用 LangGraph 搭建 Agent 框架
2. 开发自定义工具(文件读取、Web搜索、API调用)
3. 实现完整的 ReAct 循环
4. 给 Agent 加上记忆系统
5. 构建一个 Web3 研究 Agent
Day 12 学了 Agent 理论
Day 22-25 学了 Agent 工程实践
Day 55 = 真正动手写一个能用的 Agent!
需要提前准备:
pip install langgraph langchain langchain-community
pip install tavily-python # Web搜索
准备好 Ollama 本地模型(或 API key)
Day 54 完成! 从环境搭建到模型部署,走完了 LoRA 微调的完整流程。 Day 7 的理论终于变成了真实的模型权重文件。 明天开始 Agent 开发,让 AI 从"回答问题"进化到"完成任务"!