研究视角:DL/RL 理论到 LLM 训练的完整映射
📅 创建时间:2026-07-29 🏷️ 标签:#DeepLearning #ReinforcementLearning #TrainingPipeline #FineTuning #Research 📚 前置知识:[[11-training-primer]] [[08-transformer-by-hand]] [[16-rlhf-deep-dive]]
📋 本章目标
- 理解课堂上学的 DL/RL 理论在 LLM 训练的哪个具体环节被使用
- 理解每一行训练代码(
loss.backward(),optimizer.step())对应的数学原理 - 理解"模型代码"定义什么、"权重文件"存什么、训练脚本怎么串起一切
- 理解预训练、SFT、RLHF 三个阶段各自用了哪些 DL/RL 知识
- 能够从理论出发,读懂并修改一个训练脚本
第1部分:DL 理论 → 训练代码的精确映射
很多同学学完 DL 课之后有一个困惑:"我知道反向传播、知道 Adam、知道交叉熵——但这些东西在训练 LLM 时,对应代码里的哪一行?"
这一部分就是这张"黑板公式到代码行"的映射表。以后你写训练脚本时,看到每一行都知道它在数学上对应什么。
1.1 一个训练 step 的完整拆解
# ===== 一个训练 step,拆解每一行对应的 DL 理论 =====
# 数据:从 DataLoader 取一个 batch
batch = next(train_dataloader)
input_ids = batch["input_ids"].to("cuda") # (batch_size, seq_len)
labels = batch["labels"].to("cuda")
# ─── 前向传播(课堂上的 Forward Propagation)───
# 数据流过 Embedding → N 层 Transformer → LM Head
# 每层内部依次:LayerNorm → Attention(Q,K,V) → +Residual →
# LayerNorm → FFN(GELU) → +Residual
outputs = model(input_ids=input_ids, labels=labels)
# outputs.logits: (batch, seq_len, vocab_size) ← 每个位置对每个词的预测分数
# outputs.loss: 标量
loss = outputs.loss
# ↑ 这就是老师黑板上的 L(θ) = -1/N · Σᵢ Σⱼ yᵢⱼ·log(pᵢⱼ)
# 其中 yᵢⱼ 是 one-hot 的真实标签,pᵢⱼ 是 softmax(logits) 的预测概率
# ─── 反向传播(课堂上的 Backpropagation)───
# PyTorch 的 autograd 自动用链式法则算出每一个参数的 ∂L/∂W
# 你不需要手写 ∂L/∂W = ∂L/∂y · ∂y/∂h · ∂h/∂W
# autograd 替你做了——但原理就是你学的那套链式法则
loss.backward()
# 现在 model.layers[0].self_attn.q_proj.weight.grad 里存了 ∂L/∂W_q0
# 这个 .grad tensor 的形状和 weight 完全一样 (4096, 4096)
# ─── 梯度裁剪(防止梯度爆炸)───
# 如果某个参数的梯度模长 > max_norm,按比例缩小
# 为什么需要?长序列 + 深层网络 → 梯度可能指数级增长
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# ─── 参数更新(课堂上的 Gradient Descent + Adam)───
# SGD: W = W - lr × ∂L/∂W
# Adam: W = W - lr × m̂ / (√v̂ + ε)
# 其中 m̂ = β₁·m + (1-β₁)·∂L/∂W ← 一阶矩(动量)
# v̂ = β₂·v + (1-β₂)·(∂L/∂W)² ← 二阶矩(自适应学习率)
optimizer.step() # ← 就是这一行!所有 DL 课最终都归结于此
optimizer.zero_grad() # 清空所有参数的 .grad,准备下一个 step
# 为什么必须清零?因为 .backward() 是累加的,不清零会混入上一步的梯度
# ─── 学习率更新 ──
scheduler.step() # lr 按 Warmup+Cosine 曲线变化
# Warmup:前 N 步 lr 从 0 线性增长到 peak_lr(防止初期梯度不稳定)
# Cosine Decay:之后按余弦曲线衰减到接近 01.2 理论→代码 速查表
┌─────────────────────────────────────────────────────────────┐
│ 课堂上学的 DL 理论 ──→ 训练脚本里的哪一行 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 前向传播 (Forward) outputs = model(batch) │
│ ├─ Embedding 查表 embed_tokens(input_ids) │
│ ├─ Self-Attention (Q,K,V) self_attn(hidden_states) │
│ │ └─ softmax(QK^T/√d_k)·V ← 你手算过的那个 │
│ ├─ 残差连接 (Residual) x = x + attn_output │
│ ├─ 层归一化 (RMSNorm) RMSNorm / LayerNorm │
│ ├─ 激活函数 (GELU/SwiGLU) F.gelu / F.silu │
│ └─ Feed-Forward (FFN) mlp(hidden_states) │
│ │
│ 交叉熵损失 (Cross-Entropy) loss = F.cross_entropy( │
│ → 老师黑板上的 -Σ y·log(ŷ) │ logits, labels) │
│ │
│ 反向传播 (Backprop) loss.backward() │
│ → 链式法则计算所有 ∂L/∂W ← 就是这一行! │
│ │
│ 梯度下降 (Gradient Descent) optimizer.step() │
│ → SGD: W = W - lr × ∂L/∂W ← 就是这一行! │
│ Adam: W = W - lr × m̂/(√v̂+ε) │
│ │
│ 优化器 (Adam/AdamW) optimizer = AdamW( │
│ AdamW = Adam + 解耦 weight decay model.parameters(), │
│ weight_decay 直接作用于 W lr=3e-4, │
│ 而非混入梯度计算中 weight_decay=0.1) │
│ │
│ 学习率调度 (LR Schedule) scheduler = CosineWarmup │
│ → Warmup + Cosine Decay scheduler.step() │
│ │
│ 梯度裁剪 (Gradient Clipping) clip_grad_norm_( │
│ → 防止梯度爆炸 model.parameters(), 1.0)│
│ │
│ 混合精度 (Mixed Precision) with autocast(dtype=bf16):│
│ → 前向用 BF16 省显存 outputs = model(batch) │
│ → 权重更新用 FP32 保精度 ... │
│ │
│ Weight Decay (L2 正则) AdamW 的 weight_decay │
│ → 防止过拟合 │
│ │
│ Dropout nn.Dropout(0.1) │
│ → 训练时随机丢弃神经元 │
│ │
└─────────────────────────────────────────────────────────────┘1.3 Transformer 各组件用到了哪些课上知识
┌─────────────────────────────────────────────────────────────┐
│ Transformer 组件 ──→ 对应的 DL 基础概念 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Embedding 层 → 查表操作(不是矩阵乘法!) │
│ 本质:把离散 ID 映射到稠密向量 │
│ 训练后语义相近的词向量接近 │
│ │
│ Self-Attention → 矩阵乘法 + Softmax + 加权求和 │
│ ├─ Q,K,V 投影 → 三个 nn.Linear(就是矩阵乘法) │
│ ├─ Attention 分数 → QK^T 点积 = 线性代数里的内积 │
│ ├─ Softmax → 概率论:把分数变成概率分布 │
│ └─ 加权输出 → 概率论:期望值 = Σ p_i × v_i │
│ │
│ Positional Enc. → 正余弦函数(Sinusoidal)或 │
│ (RoPE) → 旋转矩阵(RoPE=复数乘法,线性代数) │
│ │
│ LayerNorm(RMS) → 统计学:减均值除标准差 │
│ 让每层的激活值保持稳定分布 │
│ LayerNorm ≠ BatchNorm! │
│ LayerNorm 沿特征维度归一化 │
│ BatchNorm 沿 batch 维度归一化 │
│ │
│ Feed-Forward → 两层全连接 + 激活函数 │
│ (FFN) → FFN(x) = W₂ · GELU(W₁·x + b₁) + b₂ │
│ GELU/SwiGLU 是 ReLU 的平滑版 │
│ │
│ 残差连接 → x = F(x) + x │
│ (Residual) → 没有残差,深层网络梯度消失,训不动 │
│ (这就是 ResNet 论文的核心贡献) │
│ │
│ LM Head → nn.Linear(hidden_dim, vocab_size) │
│ 投影到词表维度 + Softmax = 概率分布 │
│ 取 argmax = 预测下一个 token │
│ │
└─────────────────────────────────────────────────────────────┘第2部分:RL 理论 → RLHF 代码的精确映射
2.1 RL 概念到 RLHF 的一一对应
┌─────────────────────────────────────────────────────────────┐
│ 课堂上学的 RL 理论 ──→ RLHF 具体实现 │
├─────────────────────────────────────────────────────────────┤
│ │
│ RL 概念 在 RLHF 中对应什么 │
│ ──────── ──────────────── │
│ │
│ Agent(智能体) LLM 本身 = Policy π_θ │
│ → 感知环境、选择动作 → 给定 prompt,生成回答 │
│ │
│ Environment(环境) Reward Model + 对话上下文 │
│ → 接收动作、返回奖励 → 对完整回答输出一个标量分数 │
│ │
│ State sₜ 已生成的 token 序列 │
│ → 环境在时刻 t 的状态 → sₜ = [x₁, ..., xₜ] │
│ │
│ Action aₜ 生成下一个 token │
│ → Agent 在状态 sₜ 的选择 → 从|V|个 token 中选一个 │
│ │
│ Policy π_θ(a|s) LLM 输出的 token 概率 │
│ → 给定状态选动作的概率 → P(xₜ₊₁ | x₁, ..., xₜ) │
│ → 策略由参数 θ 决定 → θ = LLM 的 7B 个权重 │
│ │
│ Reward R(s,a) Reward Model 的标量输出 │
│ → 环境给的即时反馈 → RM(回答) → 一个分数 │
│ │
│ Value Function V(s) PPO 的 Value Model │
│ → 状态 s 的期望累计奖励 → 一个小网络,输入 hidden_state │
│ 输出 scalar:这个"局面"有多好 │
│ │
│ Advantage A(s,a) 核心计算 │
│ A = Q(s,a) - V(s) A = 实际奖励 - Value 预测 │
│ → 这个动作比"平均水平" → A > 0:这个 token 比预期好 │
│ 好多少 → A < 0:这个 token 比预期差 │
│ │
│ Policy Gradient PPO 的 surrogate loss │
│ ∇J = E[∇log π·A] L^CLIP = -min(ratio·A, │
│ → 让"好动作"概率上升 clip(ratio)·A) │
│ "坏动作"概率下降 ratio = π_new/π_old │
│ clip 限制更新幅度(e.g. ±20%) │
│ │
│ KL 散度 D_KL(P||Q) KL penalty in RLHF loss │
│ → 衡量两个分布的距离 → β·KL(π_θ || π_SFT) │
│ → 防止新策略偏离太远 防止模型为了刷分变成胡言乱语 │
│ │
│ GAE (Generalized PPO 的标准做法 │
│ Advantage Estimation) → 平衡偏差(bias)和方差(variance) │
│ → λ 参数控制看多远 │
│ │
└─────────────────────────────────────────────────────────────┘2.2 RLHF 训练循环——RL 理论全景
┌─────────────────────────────────────────────────────────────┐
│ RLHF 训练一个 PPO 迭代的全流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ for each PPO iteration: │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 1. Rollout(采样) │ │
│ │ prompt → Policy π_θ 生成完整回答(auto-regressive)│ │
│ │ 每个 token = 一次 Action,策略 = LLM 的概率分布 │ │
│ │ → 这就是 RL 课上说的"Agent 在环境中采取行动" │ │
│ └──────────────────────┬─────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 2. Reward(奖励打分) │ │
│ │ Reward Model(完整回答) → scalar reward │ │
│ │ → 这就是 RL 课上说的 R(s,a)——环境给的反馈 │ │
│ │ → Reward Model 本身是 SFT 模型微调来的 │ │
│ └──────────────────────┬─────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 3. GAE(计算 Advantage) │ │
│ │ Value Model 预测每个 token 位置的价值 V(sₜ) │ │
│ │ Aₜ = Rₜ + γ·V(sₜ₊₁) - V(sₜ) + γλ·Aₜ₊₁ │ │
│ │ → 如果 Advantage > 0,这个 token 强化 │ │
│ │ → 如果 Advantage < 0,这个 token 抑制 │ │
│ │ → 这就是 Policy Gradient 的核心——用 A 替代直接的 R│ │
│ └──────────────────────┬─────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 4. Update Policy(PPO Loss + KL 惩罚) │ │
│ │ │ │
│ │ ratio = π_new(token) / π_old(token) ← 新旧策略比│ │
│ │ │ │
│ │ L_CLIP = -min(ratio × A, │ │
│ │ clip(ratio, 0.8, 1.2) × A) │ │
│ │ ↑ clip 限制了单步更新的幅度(PPO 的关键创新) │ │
│ │ │ │
│ │ L_KL = β × KL(π_new || π_ref) │ │
│ │ ↑ 防止新策略偏离 SFT 太远 │ │
│ │ │ │
│ │ L_total = L_CLIP + L_KL │ │
│ │ → loss.backward() ← 所有 RL 理论最终落地 │ │
│ │ → optimizer.step() │ │
│ └──────────────────────┬─────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 5. Update Value Model │ │
│ │ L_V = MSE(V(sₜ), R_target) │ │
│ │ → 让 Value Model 更好地预测"这个局面的期望奖励" │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ 4 个模型同时加载在 GPU 上: │
│ • Policy (Actor) ← 正在训练(就是 LLM 本身) │
│ • Reference (SFT) ← 冻结,只用于计算 KL │
│ • Reward Model ← 冻结,只用于打分 │
│ • Value Model ← 正在训练(预测期望奖励) │
│ │
└─────────────────────────────────────────────────────────────┘2.3 三个训练阶段各自用到什么理论
┌─────────────────────────────────────────────────────────────┐
│ Pre-training / SFT / RLHF 使用的理论对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ │ │ Pre-training │ SFT │ RLHF │
│ ├───────────────┼─────────────┼──────────┼───────────────┤
│ │ 核心任务 │ 预测下一词 │ 预测下一词 │ 最大化奖励 │
│ │ 损失函数 │ Cross-Entropy│ CE(仅assistant)│ PPO+KL+MSE│
│ │ 优化器 │ AdamW │ AdamW │ AdamW/PPO │
│ │ 学习率 │ 3e-4→1e-5 │ 5e-6~2e-5 │ 1e-6~5e-6 │
│ │ 数据量 │ 万亿 token │ 千~万条 │ 万~十万条 │
│ │ 训练时长 │ 数周~数月 │ 数小时~天 │ 数天~周 │
│ │ 起点权重 │ 随机初始化 │ 预训练权重 │ SFT 权重 │
│ │ DL 知识 │ 全部 DL 理论 │ 全部 DL │ DL + RL 全部 │
│ │ RL 知识 │ 不需要 │ 不需要 │ Policy Gradient│
│ │ │ │ │ + PPO + GAE │
│ │
│ 核心洞察:三个阶段的训练代码看起来几乎一样。 │
│ Pre-training 和 SFT 只差在起点权重和数据量。 │
│ RLHF 才真正用到强化学习的 Policy Gradient/PPO 那套理论。 │
│ │
└─────────────────────────────────────────────────────────────┘第2.5部分:拿到手的到底是什么——从裸代码到完整模型的光谱
前面讲的是"训练时每行代码对应什么理论"。但在你开始训练之前,还有一个更实际的问题:你从别人那里拿到的东西,到底包含什么?能做什么?
这个问题没有唯一答案——取决于发布者给了你什么。
2.5.1 五种"拿到手"的形态
┌─────────────────────────────────────────────────────────────┐
│ 你拿到的到底是什么?——五种典型形态 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 形态1:只有论文 + 代码(无权重) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 你拿到: │ │
│ │ • 一篇 arXiv 论文(描述了架构和训练方法) │ │
│ │ • GitHub 上的 model.py(定义了 nn.Module 架构) │ │
│ │ • 可能有一个 train.py(训练脚本) │ │
│ │ │ │
│ │ 你拿不到: │ │
│ │ • 权重文件(.pt / .safetensors)——没有! │ │
│ │ │ │
│ │ 你能做什么: │ │
│ │ • 读懂架构,复现论文 │ │
│ │ • 用自己的数据从头训练(需要大量 GPU) │ │
│ │ • 从零开始:随机权重 + 你的数据 + 数周训练 │ │
│ │ │ │
│ │ 举例:大多数学术论文的 GitHub 仓库 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 形态2:代码 + 预训练权重(最常见) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 你拿到: │ │
│ │ • config.json(架构参数:几层、多宽) │ │
│ │ • modeling_xxx.py(模型代码) │ │
│ │ • pytorch_model.bin 或 model.safetensors(权重!) │ │
│ │ • tokenizer.json / tokenizer_config.json │ │
│ │ │ │
│ │ 这就是 HuggingFace 上 "Model Card" 的典型内容。 │ │
│ │ 模型代码几百 KB,权重文件 14GB(7B 模型)。 │ │
│ │ │ │
│ │ 你能做什么: │ │
│ │ • 直接做推理(问答、生成文本) │ │
│ │ • 微调(在自己数据上继续训练几小时) │ │
│ │ • 不能:复现预训练过程(没有训练数据和脚本) │ │
│ │ │ │
│ │ 举例:meta-llama/Llama-2-7b-hf、Qwen/Qwen2-7B │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 形态3:完整训练发布(代码 + 数据 + 权重 + 训练脚本) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 你拿到形态2的一切,外加: │ │
│ │ • 训练数据(或数据配方) │ │
│ │ • 完整的 train.py 脚本 │ │
│ │ • 中间 checkpoint(保存了不同训练阶段的状态) │ │
│ │ • 训练日志 / Wandb 曲线 │ │
│ │ • 评估脚本和基准测试结果 │ │
│ │ │ │
│ │ 你能做什么: │ │
│ │ • 完整复现训练过程 │ │
│ │ • 从 checkpoint 恢复训练 │ │
│ │ • 修改训练过程并对比效果 │ │
│ │ │ │
│ │ 举例:LLaMA-1 论文发布(权+代码)、Pythia、OLMo │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 形态4:量化权重(用于消费级推理) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 你拿到的是形态2的"压缩版": │ │
│ │ • GGUF 文件(llama.cpp 格式) │ │
│ │ • 或 GPTQ/AWQ 量化权重 │ │
│ │ • FP16 14GB → INT4 3.5GB(7B 模型) │ │
│ │ │ │
│ │ 你能做什么: │ │
│ │ • 在消费级硬件(RTX 3060 / MacBook)上做推理 │ │
│ │ • 通常不能微调(量化后的模型无法训练) │ │
│ │ │ │
│ │ 举例:TheBloke 在 HuggingFace 上发布的 GGUF 版本 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 形态5:只有 API(无代码,无权重) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 你拿到: │ │
│ │ • 一个 HTTP API 端点,按 token 计费 │ │
│ │ • 看不到模型代码、看不到权重、看不到训练细节 │ │
│ │ │ │
│ │ 你能做什么: │ │
│ │ • 调用 API 做推理(问答、生成、工具调用) │ │
│ │ • 不能微调、不能看内部结构、不能离线使用 │ │
│ │ │ │
│ │ 举例:GPT-4o、Claude Sonnet、Gemini │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘2.5.2 "预训练权重"到底是什么——最常见的误解
很多人以为"下载一个模型"就是下载了一个可执行文件。不是的。
┌─────────────────────────────────────────────────────────────┐
│ HuggingFace 上一个典型模型仓库里到底有什么 │
├─────────────────────────────────────────────────────────────┤
│ │
│ meta-llama/Llama-2-7b-hf 这个仓库里的文件: │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ config.json ← 超参数:hidden_size=4096│ │
│ │ num_layers=32 │ │
│ │ num_heads=32 │ │
│ │ 文件大小:~1KB │ │
│ ├──────────────────────────────────────────────────┤ │
│ │ tokenizer.json ← 词表:32000 个 token │ │
│ │ tokenizer_config.json 文件大小:~2MB │ │
│ ├──────────────────────────────────────────────────┤ │
│ │ model-00001-of-00002 ← 权重文件(7B 参数值) │ │
│ │ .safetensors 文件大小:~10GB │ │
│ │ model-00002-of-00002 │ │
│ │ .safetensors 文件大小:~4GB │ │
│ ├──────────────────────────────────────────────────┤ │
│ │ generation_config.json ← 生成参数:temperature │ │
│ │ 文件大小:~200B │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ 注意: │
│ • 没有 modeling_llama.py 文件! │
│ → 模型代码在 transformers 库里(pip install 时安装的) │
│ → from_pretrained() 时 HF 自动匹配 config 到正确的代码 │
│ • 权重是 FP16 的——每个参数 2 字节,7B = 14GB │
│ • 分片是因为 Git 不支持 >5GB 的单文件 │
│ │
└─────────────────────────────────────────────────────────────┘2.5.3 一个典型的工作流:从拿到权重到训练出自己的模型
┌─────────────────────────────────────────────────────────────┐
│ 研究生/开发者的典型工作流 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 第0天:拿到东西 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ pip install transformers torch peft │ │
│ │ │ │
│ │ model = AutoModelForCausalLM.from_pretrained( │ │
│ │ "meta-llama/Llama-2-7b-hf" │ │
│ │ ) │ │
│ │ # ↑ 自动下载 config + (分片的)safetensors 权重 │ │
│ │ # ↑ 根据 config 的 architectures 字段匹配 model 代码 │ │
│ │ │ │
│ │ 你现在有了可以推理的模型。但它只会续写文本。 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 第1-3天:构造训练数据 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 收集/构造 500-5000 条 instruction-output 对 │ │
│ │ 格式化成 JSON: │ │
│ │ {"instruction": "翻译:今天天气很好", │ │
│ │ "output": "The weather is nice today."} │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 第4天:LoRA 微调 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ lora_config = LoraConfig(r=16, ...) │ │
│ │ model = get_peft_model(base_model, lora_config) │ │
│ │ trainer = Trainer(model, train_dataset, ...) │ │
│ │ trainer.train() │ │
│ │ model.save_pretrained("./my-lora-adapter") │ │
│ │ │ │
│ │ 产出:adapter_model.safetensors (~10MB) │ │
│ │ 你没有修改原始的 14GB 权重 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 第5天:评估 + 发布 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 在 test set 上跑评估,写论文/报告 │ │
│ │ 发布你的 LoRA adapter 到 HuggingFace │ │
│ │ 其他人:下载 base model (14GB) + 你的 adapter (10MB) │ │
│ │ = 你的微调模型 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘2.5.4 "拿来即用"的能力光谱
┌─────────────────────────────────────────────────────────────┐
│ 拿到的东西 → 你能做的事 → 需要什么资源 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 只有 API (GPT-4/Claude) │
│ → 问答、生成、调用工具 │
│ → 需要:网络 + API Key │
│ │
│ + 代码 + 权重 (HuggingFace 开源模型) │
│ → 以上 + 推理 + 微调 + 研究内部结构 │
│ → 需要:GPU(推理 4-16GB,微调 16-80GB) │
│ │
│ + 量化权重 (GGUF) │
│ → 以上 + 在笔记本/手机上运行 │
│ → 需要:消费级 GPU 或 Apple Silicon │
│ │
│ + 训练脚本 + 数据 (完整发布) │
│ → 以上 + 完全复现 + 修改训练过程 │
│ → 需要:GPU 集群(数十到数百张卡) │
│ │
│ 每多拿到一层,你能做的事多一层,需要的资源也大一个数量级。 │
│ │
└─────────────────────────────────────────────────────────────┘第3部分:物理实体——代码、权重、数据
3.1 三类文件,三种角色
┌─────────────────────────────────────────────────────────────┐
│ 一个 LLM 项目的三类物理文件 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 模型代码(.py 文件) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 定义了:架构——多少层、多少头、hidden_dim 多大 │ │
│ │ 没有定义:具体的权重数值 │ │
│ │ → 建筑的蓝图(几层、每层多大) │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ 2. 权重文件(.safetensors / .pt) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 一个巨大的字典,300+ 个 tensor: │ │
│ │ {"model.layers.0.self_attn.q_proj.weight": │ │
│ │ Tensor([4096, 4096], dtype=float16), ...} │ │
│ │ → 建筑的实际材料(每块砖的具体位置) │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ 3. 训练脚本(train.py) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 组合 1+2+数据,循环 forward→loss→backward→update │ │
│ │ 产出新的权重文件 │ │
│ │ → 施工队(把建材按蓝图组装起来) │ │
│ └──────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘3.2 打开权重文件看看
from safetensors import safe_open
with safe_open("model-00001-of-00002.safetensors", framework="pt") as f:
for key in f.keys():
tensor = f.get_tensor(key)
print(f"{key}: {tensor.shape}, dtype={tensor.dtype}")
# 打印第一个元素
print(f" first value: {tensor[0,0].item():.6f}")输出(LLaMA-7B 有约 300 个这样的 tensor):
model.embed_tokens.weight: (32000, 4096), dtype=float16
first value: 0.003418
model.layers.0.input_layernorm.weight: (4096,), dtype=float16
first value: 1.023438
model.layers.0.self_attn.q_proj.weight: (4096, 4096), dtype=float16
first value: -0.014526 ← 这就是 08 那篇手算的 Wq 里(0,0)位置的值
model.layers.0.self_attn.k_proj.weight: (4096, 4096), dtype=float16
model.layers.0.self_attn.v_proj.weight: (4096, 4096), dtype=float16
model.layers.0.self_attn.o_proj.weight: (4096, 4096), dtype=float16
model.layers.0.mlp.gate_proj.weight: (11008, 4096), dtype=float16
... (重复 32 层)
lm_head.weight: (32000, 4096), dtype=float167B 参数 × 2 字节 (FP16) ≈ 14GB。训练时如果用 FP32 + Adam 优化器,一个 checkpoint 就要 140GB+。
3.3 加载模型 = 创建架构 + 填入权重
from transformers import AutoModelForCausalLM
# from_pretrained 做了三件事:
# 1. 读 config.json → 确定架构(几层、多宽、多少头)
# 2. 创建 nn.Module 对象(此时权重随机)
# 3. 加载 .safetensors 覆盖随机权重 → 模型可用
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-hf")第4部分:微调——三种场景,三种策略
┌─────────────────────────────────────────────────────────────┐
│ 微调的三种方式 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Full Fine-Tuning LoRA QLoRA │
│ ─────────────── ───── ────── │
│ 修改所有参数 只训增量矩阵 量化+增量矩阵 │
│ │
│ 7B: ~14GB 权重 7B: ~10MB adapter 7B: ~10MB │
│ +Adam: ~100GB 显存 +Adam: ~16GB 显存 +Adam: ~8GB │
│ 需要 A100 RTX 4090 即可 RTX 3060 即可 │
│ │
│ 科研用不起 → 实验室标配 → 消费级显卡 │
│ │
│ LoRA 原理:不修改原始 W,在旁边加可训练的 A 和 B │
│ h = W·x + α/r · B·A·x (A: 4096×r, B: r×4096) │
│ ↑ 冻结 ↑ r=16 时仅 13万参数 │
│ │
└─────────────────────────────────────────────────────────────┘第5部分:完整实验流程
┌─────────────────────────────────────────────────────────────┐
│ 一次科研微调的全流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 选底座:从 HuggingFace 下载 LLaMA/Mistral/Qwen/DeepSeek │
│ 2. 准备数据:写 JSON 文件 {"instruction":..., "output":...} │
│ 3. 选方法:显卡够 → Full FT / 不够 → LoRA / 很不够 → QLoRA │
│ 4. 写 train.py(200行):forward→loss→backward→step │
│ 5. 训练:几小时到几天,Wandb 监控 loss │
│ 6. 保存:LoRA adapter(~10MB)或完整权重 │
│ 7. 评测:test set 指标 + 人工评估 │
│ │
│ 你不需要保存 base model——那是从 HF 下载的。 │
│ 你只需要保存你训练的增量部分。 │
│ │
└─────────────────────────────────────────────────────────────┘核心总结
loss.backward() 就是反向传播:PyTorch 的 autograd 自动执行链式法则,算出每个参数的 ∂L/∂W。你不需要手写,但原理完全是你课堂上学的那一套。
optimizer.step() 就是梯度下降:SGD 做
W = W - lr×∂L/∂W,Adam 在此基础上加了动量和自适应学习率。训练 LLM 几乎全用 AdamW。PPO 的 surrogate loss + KL penalty = RLHF 的核心:RL 课上学的 Policy Gradient、Advantage、GAE 全部在这里用上。Policy = LLM,Reward Model = 环境,Advantage 决定强化还是抑制每个 token。
三个训练阶段用的理论不同:Pre-training 和 SFT 只需要 DL 理论(CE loss + AdamW + Backprop)。RLHF 才需要 RL 理论(PPO + GAE + KL)。大部分科研微调只做到 SFT,用不到 RL。
权重文件 = 命名的 tensor 字典:打开一个
.safetensors,里面是{layer_name: Tensor(shape, dtype)}。模型代码定义了 shape 和如何前向传播,权重文件提供了具体数值。
章节测试
测试1:loss.backward() 在数学上执行了什么操作?
A. 计算 loss 的值 B. 用链式法则计算每个参数的 ∂L/∂W C. 更新模型权重 D. 清空梯度
测试2:Adam 优化器相对于 SGD 多了哪两个组件?
A. 学习率和 batch size B. 动量(一阶矩)和自适应学习率(二阶矩) C. Dropout 和 Weight Decay D. LayerNorm 和 Residual Connection
测试3:RLHF 训练时,以下哪个是"Agent"的角色?
A. Reward Model B. LLM 本身(Policy π_θ) C. 训练数据 D. Value Model
测试4:PPO 为什么比 REINFORCE 更稳定?
A. PPO 不需要 Reward Model B. PPO 用 clipped objective 限制策略更新幅度 C. PPO 不需要 Value Model D. PPO 比 REINFORCE 更简单
测试5:Pre-training、SFT、RLHF 三个阶段分别使用哪些损失函数?各自用到什么 DL/RL 理论?
测试6:打开一个 .safetensors 文件,你会看到什么数据结构?
参考答案
测试1答案
答案:B。loss.backward() 从 loss 开始,沿计算图反向传播,用链式法则自动算出模型每一个参数的梯度 ∂L/∂W,存入 .grad 属性。它不更新参数——那是 optimizer.step() 的工作。
测试2答案
答案:B。Adam 维护两个状态:一阶矩 m(梯度的指数移动平均,提供动量效果)和二阶矩 v(梯度平方的指数移动平均,为每个参数提供自适应学习率)。AdamW 在此基础上把 weight decay 从梯度计算中解耦。
测试3答案
答案:B。LLM 本身 = Policy π_θ = Agent。它"观察"prompt(状态),"选择"每个 token(动作),Reward Model 给它打分(奖励)。Value Model 预测"这个局势期望得多少分"。
测试4答案
答案:B。PPO 的核心创新是 clipped surrogate objective:min(ratio·A, clip(ratio, 1-ε, 1+ε)·A)。当新旧策略差异过大时,clip 会截断梯度,防止单步更新"步子太大"。REINFORCE 没有这个机制。
测试5答案
Pre-training:Cross-Entropy Loss(对所有位置计算)。DL 理论:Forward→Loss→Backward→AdamW update。不需要 RL。 SFT:Cross-Entropy Loss(仅计算 assistant token 位置)。DL 理论同上。不需要 RL。 RLHF:PPO surrogate loss + KL penalty + MSE(Value Model)。DL 理论用于三个模型的 forward/backward,RL 理论用于 Policy Gradient(PPO+GAE+KL)。这是 RL 理论真正被用到的地方。
测试6答案
一个巨大的字典 {name: Tensor},约 300+ 个 key。每个 key 是参数名(如 model.layers.0.self_attn.q_proj.weight),value 是一个 PyTorch Tensor(shape 如 [4096, 4096],dtype 如 float16)。里面存的全部是具体的浮点数值(如 -0.014526)。没有任何架构信息——架构由 config.json 和 modeling_*.py 定义。
相关笔记
- [[11-training-primer]] — 训练循环的 DL 基础
- [[08-transformer-by-hand]] — Transformer 手动计算
- [[12-pretraining-panorama]] — 预训练全景
- [[15-sft-deep-dive]] — SFT 监督微调
- [[16-rlhf-deep-dive]] — RLHF 原理
- [[17-dpo-and-alignment]] — DPO 与对齐
下一步学习
- [ ] 用 PyTorch 写一个 50 行的训练循环:
model = ...; for batch: loss=model(batch); loss.backward(); optimizer.step() - [ ] 从 HuggingFace 下载 LLaMA-3-8B,用 LoRA 在自己的数据上跑一次 SFT
- [ ] 打开一个
.safetensors文件,打印前 20 个参数名和 shape
学习状态:🟡 开始学习