---
title: "大模型微调：从 Step 到 Loss 到 LoRA，把训练拆明白"
description: "沿着训练循环的数据流，拆解 LLM/VLM 微调实验中从数据加载到参数更新的每一个设计决策"
pubDate: 2026-06-29
tags: ["LLM","微调","LoRA","训练策略","深度学习","VLM"]
category: "AI"
lang: "zh"
math: true
---


很多关于 LLM 微调的文章把 steps/epochs、loss 设计、LoRA 配置、评估策略等拆成独立话题讲解。但实际上，这些全都是**同一个训练循环**中相互咬合的齿轮 —— 改动任何一个，都会连锁影响其他所有环节。

本文沿着**一次训练实验的数据流**，从数据加载到参数更新，完整拆解每一个设计决策。

---

## 一、训练循环：一个 Step 的全景

理解微调的第一步，是看清**一个 training step 内部到底发生了什么**：

```
数据加载 → Forward Pass → Loss 计算 → Backward Pass → 参数更新
  (batch)    (模型预测)     (算 loss)    (算梯度)      (优化器 step)
```

这就是训练的**原子操作**。一个 step = 一次完整的"看数据 → 算 loss → 修正参数"循环。

### Step 是训练引擎的时钟

所有训练机制都挂在 step 上 —— 学习率调度、日志记录、评估触发、checkpoint 保存、early stopping。**Epoch**（遍历一次完整数据集）只是 steps 的派生量：

```
steps_per_epoch = num_samples / effective_batch_size
total_steps = steps_per_epoch × num_epochs
```

以一个典型的小数据集微调为例：3000 条样本，`batch_size=2`，`gradient_accumulation_steps=4`（effective_batch_size=8），跑 5 epochs：

```
steps_per_epoch = 3000 / 8 = 375
total_steps = 375 × 5 = 1875
```

为什么框架不直接用 epoch？五个原因：

1. **大规模预训练中 epoch 概念失效** —— 万亿 token 语料，训练通常不到一个 epoch
2. **流式数据没有自然边界** —— HuggingFace 对流式数据把 epochs 设为天文数字，完全靠 `max_steps` 停止
3. **Steps 跨配置可比** —— 1000 条样本的一个 epoch 和 100 万条的一个 epoch 计算量天差地别，step 把这个标准化了
4. **调度粒度** —— "前 10% 做 warmup" 只有 step 粒度能表达，epoch 粒度只能选 0 或 1
5. **PyTorch 设计哲学** —— 原子操作就是 `zero_grad → forward → backward → step`

`max_steps` 设了正值会覆盖 `num_epochs`，Trainer 反算需要多少 epoch 循环但到 step 数就中断。`num_train_epochs` 支持浮点数（如 2.5 = 最后一个 epoch 跑 50%）。

---

## 二、数据流入：样本怎么进入模型

训练循环的起点是数据加载。三个关键决策：

### 2.1 Shuffling

每个 epoch 开始时数据重新打乱（HuggingFace Trainer 默认每 epoch 不同 seed）。

- **不 shuffle 的后果**：模型学到数据顺序中的假模式（如"正面情感之后总是负面情感"、"短文本后总跟长文本"），产生偏置学习
- **Shuffle 的真正价值**：每个 epoch 的打乱顺序不同，同一条样本每次都和不同的样本组成 batch，模型在不同的上下文中反复学习同一条数据 —— 这才是多 epoch 训练的意义
- 验证集**永远不要** shuffle

### 2.2 Batch 构成与有效批大小

这是影响训练动态的最关键公式：

$$
\text{effective\_bs} = \text{per\_device\_bs} \times \text{grad\_accum\_steps} \times \text{num\_gpus}
$$

**为什么重要**：改变有效批大小会连锁影响一切 ——

- total_steps 变了 → LR schedule 形状变了
- 梯度噪声水平变了 → 泛化特性变了
- eval/save 频率（如果 step-based）变了

**等效配置表**（都达到 effective_batch_size = 1024）：

| per_device_bs | accum_steps | GPUs | effective_bs |
|------|------|------|------|
| 16 | 8 | 8 | 1024 |
| 16 | 16 | 4 | 1024 |
| 16 | 64 | 1 | 1024 |

增大 `per_device_batch_size` 更快但更耗显存；增大 `gradient_accumulation_steps` 省显存但更慢（顺序处理）；增大 GPU 数量更快但需要更多硬件。

### 2.3 Padding vs Packing

短序列填充到最大长度（padding）会浪费大量算力 —— pad token 走完整 forward pass 但不贡献 loss。对长度差异大的数据集，浪费可达 ~50%。

Packing 把多条短序列拼进同一个 context window：

| 方式 | 加速比 | 风险 |
|------|--------|------|
| 朴素 packing（无隔离） | 2-5x | 交叉污染：一条样本的 token 注意到另一条 |
| Packing + position IDs + attention mask | 2-5x | 无风险，现代实现的标准做法 |
| Padding-free（TRL 0.19+） | ~2x | 无风险，FFD 算法自动分组 |

[Wang et al. (ACL Findings 2025)](https://aclanthology.org/2025.findings-acl.256/) 发现：packing 的模型在各种 benchmark 上**普遍优于** padding，且优势随模型规模增大。

### 2.4 VLM 的特殊性：图片 Token 的上下文占用

VLM 中图片经过 ViT 后变成大量视觉 token（LLaVA: 576, 早期融合模型: ~4096, Qwen2.5-VL 压缩后: ~144）。这些 token **不参与 loss 计算**（label=-100），但占据大量 context。

一条典型 VLM SFT 样本的 token 分布：

```
[system ~200] [image ~800] [user ~100] [assistant ~150]
                                        只有这150个算loss
```

有效 loss token 可能只占总序列的 ~12%。这解释了 VLM 训练显存高但 loss 降得快 —— 每 step 真正学的信号不多。

ICLR 2025 缩放律研究的反直觉发现：**推理最优策略是最大化 LLM 参数，最小化视觉 token 数** —— 有时压缩到每张图仅 1 个 token。

---

## 三、Loss 计算：模型怎么知道自己错了

数据进入模型后，forward pass 产生预测，接下来就是算 loss。这里有四层设计决策。

### 3.1 第一层：单 Token 的 Cross-Entropy

每个时间步 t，模型对整个词表输出一个概率分布，loss 就是正确 token 的负对数概率：

$$
L_t = -\log\, p(y_t \mid y_1, \dots, y_{t-1})
$$

概率来自 softmax 归一化：

$$
p(y_t \mid y_{<t}) = \frac{\exp(h_t^\top \cdot e_{y_t})}{\displaystyle\sum_{w \in V} \exp(h_t^\top \cdot e_w)}
$$

其中 $h_t$ 是第 t 步的隐状态向量，$e_w$ 是词表中 token w 对应的输出嵌入向量，$V$ 是完整词表。

整个序列的 loss 就是所有参与计算的 token 的平均：

$$
L = \frac{1}{N} \sum_{t=1}^{N} L_t
$$

**为什么 cross-entropy 好用？** 它和 softmax 配合的梯度极其简洁：`prediction - truth`，log 和 exp 互相抵消，产生干净稳定的梯度。

### 3.2 第二层：哪些 Token 参与计算

SFT 和预训练的**核心区别**就在这里 —— **只对模型回答部分计算 loss，prompt 部分 mask 掉**：

```
tokens:  [系统提示 ... 用户消息 ... ] [assistant 回答内容]
labels:  [-100  -100  ... -100       ] [token_id ... EOS  ]
              不算 loss                     算 loss
```

PyTorch 的 `nn.CrossEntropyLoss` 默认 `ignore_index=-100`，该位置自动排除在 loss 和梯度之外。

**为什么 mask prompt？**
- 推理时模型只需生成回答，训练它"预测" prompt 浪费梯度更新
- 不同样本的 system prompt 各不相同，训练预测它们会产生矛盾梯度信号
- 模型学习容量有限，应集中在回答质量上

**2025 年的新讨论**：WIT 论文发现对 prompt token 施加小的非零权重（PLW 约 0.1）对短回复可能有益。但长回复时完全 mask（PLW=0）仍更好。Meta Llama 3/3.1 使用完全 prompt masking。

### 3.3 第三层：样本间加权

当数据集中不同类别样本数量不均衡时，可以通过加权让模型更关注少数但重要的类别。常见的做法：

```python
# 按样本类别设置权重
category_weights = {
    "reasoning": 2.0,   # 推理类样本较少但重要
    "coding": 1.5,      # 代码类样本
    "general": 1.0,     # 通用对话类样本
}
sample_loss *= category_weights[sample_category]
```

一条权重为 2.0 的样本对梯度的贡献 = 2 条权重 1.0 的样本。这迫使模型在数据不均衡时优先学好稀缺类别。

#### 更细粒度的前沿方法（2025-2026）

上面是样本级的加权，近年研究开始探索**token 级**的加权 —— 不是所有 token 对学习同等重要：

- **EAFT（Entropy-Adaptive Fine-Tuning）**
  [arXiv 2601.02151, 2025](https://arxiv.org/abs/2601.02151) 提出根据模型对每个 token 的预测熵来动态调整权重。高熵 token（模型不确定、需要学习的位置）获得更高权重，低熵 token（模型已经能正确预测的位置）权重降低。核心思路是把监督信号集中在模型真正薄弱的地方，避免在已掌握的 token 上浪费梯度。实验发现 mask 掉底部 15% 的"自信但预测错误"的 token 能有效减少通用能力退化。

- **DFT（Dynamic Fine-Tuning）**
  将每个 token 的 loss 乘以模型对该 token 的预测概率作为缩放因子。模型已经能高概率预测正确的 token 自动获得更低的 loss 权重，而模型预测概率低的 token（更需要学习的位置）保持原始权重。相比 EAFT 使用熵，DFT 直接用概率值做缩放，实现更简单。

- **SFT-GO（SFT with Group Optimization）**
  [arXiv 2602.01227, 2025](https://arxiv.org/abs/2602.01227) 借鉴 DRO（Distributionally Robust Optimization）思想，先用 TF-IDF 等统计指标将回答中的 token 按信息量分组（如高信息量词 vs 停用词），然后优化各组中**表现最差的那组**的 loss。这避免了模型在大量低信息量 token（如"the"、"is"）上刷低 loss 而忽视真正承载语义的关键词。

### 3.4 第四层：Loss 归一化

| 方式 | 公式 | 特点 |
|------|------|------|
| **Per-token 平均**（默认） | 总 token loss / 非 mask token 数 | 长序列贡献更大，梯度更稳定 |
| **Per-sample 平均** | 每条样本 loss 独立归一化后取平均 | 每条样本权重相同，不受长度影响 |
| **两阶段**（NVIDIA Nemotron） | Stage 1 per-token → Stage 2 per-sample | 先充分学习，后均衡调优 |

使用 packing 时这个选择更关键 —— 一个 packed 序列包含多条不同长度的样本。

### Loss 值解读速查表

由于单 token loss 等于正确 token 概率的负对数，可以反推平均置信度：

| Loss 值 | 平均概率 | 困惑度 | 含义 |
|---------|---------|--------|------|
| 0.1 | ~90% | ~1.1 | 非常自信 |
| **0.5** | **~61%** | **~1.65** | **SFT 正常收敛范围** |
| 1.0 | ~37% | ~2.7 | 训练早期，模型仍在学习 |
| 2.0 | ~14% | ~7.4 | 低置信，检查数据或学习率 |
| 5.0+ | < 1% | > 100 | 接近随机，通常是训练刚开始的状态 |

> 平均概率 = exp(-loss)，困惑度 = exp(loss)

**注意**：不要追求特定 loss 数字。低 loss 不等于好的下游表现。Cross-entropy 有自然下界（语言本身的随机性），永远不会到 0。

---

## 四、梯度与参数更新：从 Loss 到学习

Loss 算完后，backward pass 计算梯度，然后优化器更新参数。这一环节有三组关键设计。

### 4.1 梯度累积

当 GPU 显存放不下目标 batch size 时，用梯度累积：每看 1 条样本算一次梯度，攒够 K 次后做一次参数更新（= 1 step）。

**`batch_size=1 + accum=8` 和 `batch_size=8` 不完全等价**，两个差异源：

**① Loss 归一化 Bug（影响最大）**

跨不同长度 mini-batch 累积时，简单平均会产生错误的总 loss —— 可能比真实 full-batch loss 大 G 倍（G 为累积步数）。正确做法是所有累积步骤的 token loss 总和除以所有非 padding token 总数。

这个 bug 由 Unsloth 在 2024 年发现并修复，随后被 [HuggingFace Transformers 采纳](https://huggingface.co/blog/gradient_accumulation)。**确保你的框架版本包含修复。**

**② 浮点精度**

BFloat16 只有约 2.4 位十进制精度，累加多个小梯度的数值结果与一次大 batch 计算不同。效果一般可忽略，但在极端配置下（如 accum=64）可能有可测量的偏差。

**建议**：能放下就不用 accum，纯为省显存时才用。

### 4.2 学习率调度

所有 LR scheduler 都以 **step** 为单位参数化：

```python
get_scheduler(
    name="cosine",
    num_warmup_steps=56,     # 0.03 × 1875 ≈ 56 steps
    num_training_steps=1875  # 总 steps
)
```

#### Warmup

在训练最初几十个 steps 中，学习率从 0（或接近 0）线性升到目标值。目的是在优化器动量统计量（AdamW 的一阶和二阶矩估计）尚未稳定时，避免大梯度更新。

**LoRA 需要更少的 warmup**，四个原因：
1. 可训练参数少（~1-2%），优化景观更平滑
2. 冻结的基础权重防止灾难性遗忘
3. 低秩约束本身就是隐式正则化
4. 更高的学习率容忍度

| 方法 | warmup_ratio | 典型 LR |
|------|-------------|---------|
| LoRA | **0.03** | 1e-4 ~ 5e-4 |
| 全参数微调 | 0.05 ~ 0.10 | 1e-5 ~ 5e-5 |

[Thinking Machines Lab 的 "LoRA Without Regret"](https://thinkingmachines.ai/blog/lora/) 研究甚至发现**完全不用 warmup**也能取得竞争力的结果。

#### 常见 Schedule

- **Cosine decay**（主流）：慢起步衰减 → 中期加速 → 末期趋缓。形状由 `current_step / total_steps` 决定
- **Linear decay**：线性下降到 0
- **WSD（Warmup-Stable-Decay）**：维持峰值 LR 的"稳定期"后快速衰减，适合不确定总步数的场景
- **Constant**：LoRA 短训练中也能用

**关键**：改 batch size 会改 total_steps，进而改变整个 schedule 形状。每次调 batch 相关参数都要重算 warmup_steps。

### 4.3 LoRA 适配器设计

LoRA 是微调中参数更新的"怎么改"决策，直接影响模型从 loss 中学到什么、学多少。

#### 初始化：为什么 Step 0 的 Loss 反映基础模型能力

LoRA 的权重增量为 $\Delta W = BA$，其中 A 用 Kaiming 初始化，**B 初始化为零**，所以训练开始时 $\Delta W = 0$，模型行为与预训练模型完全一致。

因此 step 0 的 loss 反映的是 base model 对当前任务格式的先验水平。如果初始 loss 很高（如 > 5），说明预训练模型对你的数据格式几乎没有先验 —— 这在自定义输出格式时很常见。

> **ICML 2025 新发现**：["Beyond Zero Initialization"](https://arxiv.org/abs/2505.23194) 发现 A 和 B 同时非零初始化可提高对次优学习率的鲁棒性。但标准零初始化仍是安全默认。

#### alpha/rank 比值：缩放有效学习率

LoRA 的参数更新公式为：

$$
W' = W + \frac{\alpha}{r} \times BA
$$

其中 $\alpha$ 是缩放超参数，$r$ 是 LoRA 的 rank。$\alpha / r$ 这个比值直接控制 LoRA 更新的幅度：

| 配置 | 缩放因子 | 效果 |
|------|---------|------|
| alpha = rank | 1.0 | 保守稳定，安全默认 |
| alpha = 2 × rank | 2.0 | 有效 LR 翻倍，更激进 |

**2026 年 6 月的重要发现**（[arXiv 2606.12883](https://arxiv.org/abs/2606.12883)）：alpha 和学习率的作用**不可互换** —— alpha 是有效优化的主驱动力，其增益无法通过调 LR 复制。LoRA 的频谱抑制效应平滑了优化景观，alpha 在不增加漂移比的情况下放大任务信号。

#### 层选择：覆盖比深度更重要

[QLoRA 论文（Dettmers et al., NeurIPS 2023）](https://arxiv.org/abs/2305.14314) 的关键发现：**增加 adapter 覆盖的层数比增加 rank 更有效。**

Transformer 中的 linear 层：
- **注意力层**：`q_proj`, `k_proj`, `v_proj`, `o_proj` —— 控制信息路由和组合
- **MLP/FFN 层**：`gate_proj`, `up_proj`, `down_proj` —— 存储和检索事实知识

[Thinking Machines Lab (2025)](https://thinkingmachines.ai/blog/lora/) 发现 MLP + 注意力 > 仅 MLP > 仅注意力。[Amazon Science (2026)](https://www.amazon.science/blog/optimizing-lora-target-module-selection-for-efficient-fine-tuning) 发现 `o_proj` 单独作为目标就能达到多层配置 98% 的准确率（但延迟降低 22.6%）。

**最佳实践**：`target_modules="all-linear"`。

#### Rank 选择

| Rank | 适用场景 | 说明 |
|------|---------|------|
| r = 8-16 | 简单任务（格式、风格） | QLoRA 发现 r > 8 后收益递减 |
| r = 16-32 | 中等任务（指令遵循） | 可靠起点 |
| r = 32-64 | 复杂任务（领域知识、多任务） | 监控过拟合 |

---

## 五、训练监控：跑多久、怎么停

前面四节描述了单个 step 内部发生的一切。但训练由成百上千个 steps 组成，需要宏观层面的监控和终止决策。

### 5.1 Eval 策略

| 参数 | 典型值 | 作用 |
|------|--------|------|
| `eval_steps` | 150-500（或浮点数如 0.04） | 每 N 步跑验证集 |
| `save_steps` | 与 eval_steps 对齐 | 每 N 步保存 checkpoint |
| `save_total_limit` | 2-5 | 磁盘上最多保留几个 checkpoint |
| `load_best_model_at_end` | True | 训练结束后恢复到最佳 checkpoint |

**对齐规则**：`save_steps` 必须是 `eval_steps` 的整数倍，否则最佳 checkpoint 可能没被保存。

**eval_loss 不等于实际效果** —— 这是最常踩的坑。eval_loss 衡量 token 级预测准确性，不是输出质量。一个 eval_loss 稍高的模型可能产生结构更好、事实更准确的输出。

**推荐**：定义 `compute_metrics` 返回任务特定指标（准确率、F1、BLEU 等），设 `metric_for_best_model="your_metric"`。

**小技巧**：`eval_steps` 设为浮点数（如 0.04）时被解释为总 steps 的比例，可以自适应不同数据集大小。

### 5.2 Early Stopping

| 参数 | 推荐值 | 说明 |
|------|--------|------|
| patience | 2-3 | 连续 N 次 eval 无改善就停 |
| min_delta | 0.001-0.01 | 最小改善阈值，防噪声触发 |
| restore_best | True | 停后恢复最佳权重 |

Early stopping 通常减少 20-40% 训练时间，同时保持或提升泛化性能。

### 5.3 多 Epoch 训练的退化风险

神经网络先学干净模式，再记忆噪声标签。[Tian et al. (arXiv 2604.12469, 2026)](https://arxiv.org/abs/2604.12469) 识别出三阶段：**拟合 → 稳定 → 记忆化**。

| 阶段 | 行为 | Loss 表现 |
|------|------|----------|
| Epoch 1-2 | 学通用模式 | 快速下降 |
| Epoch 3-4 | 精修边界 | 缓慢下降 |
| Epoch 5+ | 开始记忆噪声 | 训练 loss 继续降，eval loss 上升 |

训练集中的标注错误在多 epoch 下被放大 —— 模型分不清"该学的规律"和"标错的标注"。这是"**数据质量 > 参数调优**"一直成立的根本原因。

**缓解手段**：
1. Early stopping（主要防线）
2. LoRA（低秩约束限制记忆容量）
3. Dropout（配合高 LR + 多 epoch）
4. 数据清洗（从源头减少噪声）

**反直觉发现**（[Kopiczko et al., arXiv 2602.11149, 2026](https://arxiv.org/abs/2602.11149)）：在推理 SFT 任务上，400 条样本训练 128 epochs **超过了** 51200 条样本训练 1 epoch。性能增益在模型完全记忆训练数据后趋于饱和。但这主要适用于 chain-of-thought 推理任务。

---

## 六、经验法则

| 决策点 | 建议 |
|--------|------|
| 训练前 | 先算 `total_steps`，它决定 LR schedule、warmup 时长、eval 频率 |
| Batch size | 改了就重算一切 warmup_steps、eval_steps |
| 学习率 | LoRA 用 `1e-4`，全参用 `1e-5`；LoRA warmup `0.03` |
| LoRA 层 | `target_modules="all-linear"` 比提高 rank 更有效 |
| LoRA rank | 大多数任务 r=16-32 足够；r > 8 后收益递减 |
| alpha | 从 `2 × rank` 开始，发散则降到 `= rank` |
| Packing | 能用就用，确保有 attention mask 隔离 |
| Eval | 用任务特定指标，不只盯 eval_loss |
| 停止 | 永远开 early stopping，patience=2-3 |
| 多 epoch | 3-5 epoch + early stop 覆盖大多数场景 |

---

## 参考文献

- [Hu et al. — LoRA (2021)](https://arxiv.org/abs/2106.09685)
- [Dettmers et al. — QLoRA (NeurIPS 2023)](https://arxiv.org/abs/2305.14314)
- [Beyond Zero Initialization (ICML 2025)](https://arxiv.org/abs/2505.23194)
- [The Hidden Power of Scaling Factor (June 2026)](https://arxiv.org/abs/2606.12883)
- [muA: LR Scaling across Ranks (Feb 2026)](https://arxiv.org/abs/2602.06204)
- [Kopiczko et al. — Data Repetition Beats Data Scaling (Feb 2026)](https://arxiv.org/abs/2602.11149)
- [Tian et al. — Analyzing Noise in LLM Fine-tuning (2026)](https://arxiv.org/abs/2604.12469)
- [Unsloth — Gradient Accumulation Bug Fix](https://unsloth.ai/blog/gradient)
- [HuggingFace — Fixing Gradient Accumulation](https://huggingface.co/blog/gradient_accumulation)
- [Inference Optimal VLMs (ICLR 2025)](https://arxiv.org/abs/2411.03312)
- [LoRA Without Regret (Thinking Machines Lab, 2025)](https://thinkingmachines.ai/blog/lora/)
- [Amazon Science — LoRA Target Modules (2026)](https://www.amazon.science/blog/optimizing-lora-target-module-selection-for-efficient-fine-tuning)
- [Wang et al. — Packing Analysis (ACL Findings 2025)](https://aclanthology.org/2025.findings-acl.256/)
- [EAFT: Entropy-Adaptive Fine-Tuning (2025)](https://arxiv.org/abs/2601.02151)
- [SFT-GO: Token Priority in SFT (2025)](https://arxiv.org/abs/2602.01227)
- [HuggingFace TRL SFT Trainer](https://huggingface.co/docs/trl/sft_trainer)
- [NVIDIA Nemotron SFT Docs](https://docs.nvidia.com/nemotron/latest/nemotron/super3/sft.html)
- [Sebastian Raschka — Practical Tips for LoRA](https://magazine.sebastianraschka.com/p/practical-tips-for-finetuning-llms)

