ARTICLE · 1073390
AI + 量化开发工程师学习笔记 Day 15:Learning Rate Scheduler、Warmup、Gradient Clipping 与训练稳定性
今天是进入 MiniGPT 之前最后一节“训练基础课”。
Day10~14,我们已经知道:
Day10 Loss
↓
Day11 Backward
↓
Day12 Dataset / DataLoader
↓
Day13 Optimizer
↓
Day14 Validation / Regularization
但是,一个真正的训练系统还会遇到很多问题:
Loss 突然爆炸怎么办? 训练前期为什么特别不稳定? Learning Rate 为什么不能永远固定? 为什么 Transformer 经常使用 Warmup? Gradient 太大怎么办? 怎么判断模型训练是否健康?
所以 Day15 要补齐:
训练稳定性 Training Stability
今天重点掌握四件事:
Learning Rate Scheduler Warmup Gradient Clipping Training Diagnostics
今天结束以后,我们训练模型所需的基础零件就基本齐了。
一、为什么 Learning Rate 不能永远固定?
Day13 已经知道:
θ_new = θ - η g
其中:
η = Learning Rate
假设:
lr = 0.01
整个训练:
Epoch 1 0.01
Epoch 10 0.01
Epoch 100 0.01
Epoch 1000 0.01
有没有问题?
不一定错误,但经常不是最优选择。
二、用“下山”理解 Learning Rate
假设我们的目标是走到山谷最低点:
●
/
/
/
/
/ ← 刚开始离目标很远
/
●
\__
\___
★ ← 最优点
刚开始:
离最优点很远
可以:
步子大一点。
到了最优点附近:
已经非常接近
如果还大步走:
左边
↓
右边
↓
左边
↓
右边
可能不断震荡。
所以更合理的思路:
训练前期
LR 较大
↓
快速接近较优区域
训练后期
LR 较小
↓
精细调整参数
这就是:
Learning Rate Decay
三、什么是 Scheduler?
Optimizer 决定:
怎么根据 Gradient 更新参数。
Scheduler 决定:
Learning Rate 随训练过程怎么变化。
所以:
Gradient
↓
Optimizer
↑
Learning Rate
↑
Scheduler
PyTorch 里:
optimizer = torch.optim.AdamW(...)
scheduler = torch.optim.lr_scheduler.xxx(
optimizer,
...
)
四、最简单:StepLR
例如:
scheduler = torch.optim.lr_scheduler.StepLR(
optimizer,
step_size=20,
gamma=0.5
)
意思:
每 20 个 Epoch,把 LR 乘 0.5。
如果:
初始 LR = 0.01
那么:
Epoch 0
0.01
Epoch 20
0.005
Epoch 40
0.0025
Epoch 60
0.00125
不断降低。
五、第一个实践:观察 Learning Rate
创建:
day15_step_lr.py
import torch
import torch.nn as nn
model = nn.Linear(
10,
2
)
optimizer = torch.optim.SGD(
model.parameters(),
lr=0.1
)
scheduler = torch.optim.lr_scheduler.StepLR(
optimizer,
step_size=5,
gamma=0.5
)
for epoch in range(20):
# 假装这里进行了训练
#
# optimizer.zero_grad()
# loss.backward()
# optimizer.step()
current_lr = (
optimizer
.param_groups[0]["lr"]
)
print(
f"Epoch={epoch:2d} "
f"LR={current_lr:.6f}"
)
scheduler.step()
观察:
0.100000
0.100000
...
↓
0.050000
↓
0.025000
↓
...
六、常见 Scheduler 有哪些?
你以后会经常看到:
StepLR ExponentialLR CosineAnnealingLR ReduceLROnPlateau Linear Decay Cosine Decay
我们不用今天全部展开。
LLM / Transformer 里特别值得知道的是:
Cosine Decay
七、Cosine Learning Rate
它大致:
LR
│\
│ \
│ \
│ \
│ ╲
│ ╲__
│  ̄ ̄
└──────────────── Step
利用余弦曲线逐渐降低 Learning Rate。
简化公式:
lr(t) = lr_min + 1/2 × (lr_max - lr_min) × (1 + cos(π t / T))
不用背。
只要理解:
开始
LR 较大
↓
平滑下降
↓
结束
LR 较小
八、PyTorch CosineAnnealingLR
scheduler = (
torch.optim.lr_scheduler
.CosineAnnealingLR(
optimizer,
T_max=100,
eta_min=1e-5
)
)
例如:
初始:
1e-3
↓
逐渐下降
↓
最低:
1e-5
九、为什么 Transformer 还有 Warmup?
这里开始今天一个非常重要的概念:
Warmup
你可能会觉得:
既然前期离最优点远,直接使用最大的 LR 不就好了?
问题是:
模型刚初始化的时候非常不稳定。
特别是深层 Transformer:
随机初始化
↓
Embedding 还没学好
↓
Attention 还没学好
↓
各层表示都很混乱
↓
Gradient 可能非常不稳定
如果一上来:
LR = 很大
可能直接:
Loss 爆炸
NaN
训练失败
所以:
训练刚开始先小步走,等模型稳定以后再把 LR 提上去。
这就是 Warmup。
十、Warmup + Decay
LLM 训练中非常经典:
Learning Rate
│
│ /\
│ / \
│ / \
│ / \
│ / \
│ / \
│ / \____
│ /
│ /
│/
└──────────────────── Step
Warmup Decay
也就是:
开始
LR 很小
↓
逐渐增加
↓
达到 Peak LR
↓
逐渐降低
↓
训练结束
这比:
从第一步直接最大 LR
通常更稳定。
十一、Warmup Steps
假设:
Total Steps
=
10000
我们设置:
Warmup Steps
=
1000
那么:
Step 0~1000
LR 逐渐增加
Step 1000~10000
LR 逐渐降低
Warmup Ratio:
1000 / 10000
=
10%
十二、手写最简单 Warmup
假设目标 LR:
max_lr = 0.001
Warmup:
warmup_steps = 100
那么:
if step < warmup_steps:
lr = (
max_lr
*
step
/
warmup_steps
)
例如:
Step 10
→ 0.0001
Step 50
→ 0.0005
Step 100
→ 0.001
十三、Warmup + Cosine Decay
这就是我们以后 MiniGPT 可以使用的思路。
创建:
day15_warmup_cosine.py
import math
import matplotlib.pyplot as plt
# ============================================================
# 参数
# ============================================================
max_lr = 1e-3
min_lr = 1e-5
total_steps = 1000
warmup_steps = 100
# ============================================================
# Learning Rate Function
# ============================================================
defget_lr(step):
# --------------------------------------------------------
# Warmup
# --------------------------------------------------------
if step < warmup_steps:
return (
max_lr
*
(step + 1)
/
warmup_steps
)
# --------------------------------------------------------
# Cosine Decay
# --------------------------------------------------------
progress = (
(step - warmup_steps)
/
(total_steps - warmup_steps)
)
cosine = (
0.5
*
(
1
+
math.cos(
math.pi
*
progress
)
)
)
lr = (
min_lr
+
(
max_lr
-
min_lr
)
*
cosine
)
return lr
# ============================================================
# 生成曲线
# ============================================================
lr_history = []
for step in range(
total_steps
):
lr = get_lr(
step
)
lr_history.append(
lr
)
# ============================================================
# Plot
# ============================================================
plt.figure(
figsize=(9, 5)
)
plt.plot(
lr_history
)
plt.xlabel(
"Training Step"
)
plt.ylabel(
"Learning Rate"
)
plt.title(
"Warmup + Cosine Decay"
)
plt.grid(
True,
alpha=0.3
)
plt.tight_layout()
plt.show()
你应该看到:
LR
│
│ /\
│ / ╲
│ / ╲
│ / ╲
│ / ╲
│ / ╲___
│/
└──────────────── Step
十四、为什么我们开始用 Step 而不是 Epoch?
以前数据很小:
Epoch
很方便。
但是 LLM:
几十亿 Token
可能根本不太适合把训练进度主要理解成:
Epoch 1
Epoch 2
Epoch 3
实际工程更常关注:
Global Step
因为每一次:
optimizer.step()
就是:
1 Training Step
Scheduler 往往也是:
根据 Step 调 LR
所以以后 MiniGPT 会开始引入:
global_step
这个变量。
十五、第二个问题:Gradient Explosion
假设某一层:
Gradient = 0.2
正常。
但训练过程中突然:
Gradient = 100
甚至:
100000
参数更新:
Parameter
=
Parameter
-
LR × Gradient
那么参数可能突然被推得非常远。
导致:
Loss 突然暴涨
↓
Gradient 继续异常
↓
参数爆炸
↓
Inf
↓
NaN
↓
训练失败
这就是:
Exploding Gradient
十六、什么是 Gradient Norm?
模型有很多 Gradient:
g1
g2
g3
...
gn
我们可以计算整体大小:
||g||_2 = sqrt(g1² + g2² + ... + gn²)
叫:
Gradient Norm
例如:
Grad Norm = 0.8
可能正常。
突然:
Grad Norm = 3000
就值得警觉。
十七、Gradient Clipping
如果:
Gradient Norm
太大怎么办?
最常见方法:
限制 Gradient 的最大 Norm。
例如:
torch.nn.utils.clip_grad_norm_(
model.parameters(),
max_norm=1.0
)
如果:
Gradient Norm <= 1
不处理。
如果:
Gradient Norm = 100
就按比例缩小,让整体 Norm 不超过约:
1
十八、Clipping 放在哪里?
非常重要。
正确顺序:
optimizer.zero_grad()
logits = model(x)
loss = criterion(
logits,
y
)
loss.backward()
torch.nn.utils.clip_grad_norm_(
model.parameters(),
max_norm=1.0
)
optimizer.step()
也就是:
Backward
↓
已经产生 Gradient
↓
Gradient Clipping
↓
Optimizer Step
为什么?
因为没有:
loss.backward()
以前:
Gradient 还不存在
而 optimizer.step()后参数已经更新,才裁剪就晚了。
十九、一个非常重要的顺序
以后可以记成:
zero_grad
↓
forward
↓
loss
↓
backward
↓
CLIP GRADIENT
↓
optimizer.step
↓
scheduler.step
这已经非常接近一个完整 Transformer Training Loop。
二十、实践:制造 Gradient Explosion
创建:
day15_gradient_clipping.py
import torch
import torch.nn as nn
torch.manual_seed(42)
# ============================================================
# 一个简单模型
# ============================================================
model = nn.Sequential(
nn.Linear(
10,
100
),
nn.ReLU(),
nn.Linear(
100,
1
)
)
# ============================================================
# 故意制造非常大的输入
# ============================================================
x = (
torch.randn(
32,
10
)
*
1000
)
y = torch.randn(
32,
1
)
criterion = nn.MSELoss()
optimizer = torch.optim.SGD(
model.parameters(),
lr=0.01
)
# ============================================================
# Forward
# ============================================================
optimizer.zero_grad()
prediction = model(
x
)
loss = criterion(
prediction,
y
)
# ============================================================
# Backward
# ============================================================
loss.backward()
# ============================================================
# Clip Gradient
# ============================================================
#
# 返回值是裁剪前的 total norm,
# 因此可以用于观察梯度规模。
#
grad_norm_before = (
torch.nn.utils.clip_grad_norm_(
model.parameters(),
max_norm=1.0
)
)
print(
"Loss:",
loss.item()
)
print(
"Gradient Norm Before Clip:",
grad_norm_before.item()
)
# ============================================================
# Update
# ============================================================
optimizer.step()
这个实验的重点不是模型本身,而是观察:
Gradient Norm Before Clip
可能非常大。
然后在更新前被限制。
二十一、Gradient Clipping 能解决所有训练不稳定吗?
不能。
这是很重要的一点。
如果你发现:
Gradient 不断爆炸
不能只:
max_norm = 0.0001
强行压住。
还应该检查:
Learning Rate 是否过大? 输入是否没有标准化? 初始化是否合理? Loss 是否写错? 模型结构是否有问题? 数据中是否存在异常值? 数值精度是否有问题?
Gradient Clipping 是:
安全措施之一。
不是万能药。
二十二、相反的问题:Vanishing Gradient
还有一种:
Gradient Vanishing
梯度越来越小:
Layer 10:
0.1
Layer 9:
0.01
Layer 8:
0.001
...
Layer 1:
0.000000001
于是前面的层:
几乎学不动
这就是:
梯度消失。
Transformer 为什么有:
Residual Connection LayerNorm 合理初始化
其中一个重要原因就是帮助深层网络训练稳定和梯度传播。
所以 Day7 的:
x = x + attention(...)
现在又和 Day15 连起来了。
二十三、什么叫一次“健康”的训练?
以后你训练 MiniGPT,不应该只看:
Loss
至少应该观察:
Training Loss Validation Loss Learning Rate Gradient Norm
更完整还可以看:
Accuracy / Task Metric Step Time GPU / MPS Memory Parameter Norm NaN / Inf Throughput
这就是:
Training Monitoring
二十四、典型健康训练
例如:
Step Train Loss Val Loss LR GradNorm
100 4.82 4.91 1e-4 0.72
500 3.91 4.02 5e-4 0.88
1000 3.21 3.35 1e-3 1.02
5000 2.18 2.32 5e-4 0.64
9000 1.92 2.11 1e-4 0.51
这种趋势通常比:
最终 Loss = 1.92
有信息得多。
二十五、几种异常训练现象
情况 1
Loss:
5
10
30
100
NaN
可能:
LR 过大 Gradient Explosion 数值问题
情况 2:Loss 下降极慢
Loss:
5.00
4.99
4.98
4.97
4.96
...
可能原因:
Learning Rate 太小 模型容量不足 Gradient 太小 数据 / Label 存在问题 输入尺度不合理
不能看到 Loss 降得慢,就直接:
增加模型层数
应该先诊断。
情况 3:Training Loss 降,Validation Loss 升
Train:
4.0 → 3.0 → 2.0 → 1.0 → 0.2
Validation:
4.1 → 3.2 → 2.7 → 3.1 → 4.0
这是什么?
Day14 刚学过:
Overfitting
应该考虑:
Early Stopping Dropout Weight Decay 更多数据 降低模型复杂度
情况 4:Loss 剧烈震荡
2.1
5.7
1.9
8.2
2.4
6.1
...
可能:
Learning Rate 太大 Batch 太小导致梯度噪声较大 异常样本 Gradient 不稳定
可以检查:
Learning Rate Gradient Norm Batch Size Data
情况 5:突然出现 NaN
例如:
Step 1000:
Loss = 2.1
Step 1001:
Loss = 2.3
Step 1002:
Loss = NaN
这是非常严重的警告。
需要检查:
Gradient Explosion Learning Rate 除 0 log(0) Inf 输入异常值 混合精度数值问题
以后训练 MiniGPT,我们会加入:
ifnot torch.isfinite(loss):
...
来检测。
二十六、今天最重要的工程实践:完整 Stable Training Loop
现在把 Day10~Day15 全部拼起来。
创建:
day15_stable_training.py
这是今天最值得保留的代码模板。
import math
import torch
import torch.nn as nn
from torch.utils.data import (
Dataset,
DataLoader
)
# ============================================================
# 1. Random Seed
# ============================================================
torch.manual_seed(42)
# ============================================================
# 2. 模拟 Dataset
# ============================================================
#
# 今天重点是 Training Loop,
# 所以使用简单的数值数据。
#
# X:
# 10维 Feature
#
# y:
# 3分类
# ============================================================
classSimpleDataset(Dataset):
def__init__(
self,
sample_count=1000
):
self.X = torch.randn(
sample_count,
10
)
# 人为制造一个可学习规律
score = (
self.X[:, 0]
+
0.5 * self.X[:, 1]
-
self.X[:, 2]
)
self.y = torch.zeros(
sample_count,
dtype=torch.long
)
self.y[
score > 0.5
] = 1
self.y[
score < -0.5
] = 2
def__len__(self):
return len(
self.X
)
def__getitem__(
self,
index
):
return (
self.X[index],
self.y[index]
)
# ============================================================
# 3. Dataset / DataLoader
# ============================================================
dataset = SimpleDataset(
sample_count=1000
)
loader = DataLoader(
dataset,
batch_size=32,
shuffle=True
)
# ============================================================
# 4. Model
# ============================================================
model = nn.Sequential(
nn.Linear(
10,
64
),
nn.ReLU(),
nn.Linear(
64,
64
),
nn.ReLU(),
nn.Linear(
64,
3
)
)
# ============================================================
# 5. Loss
# ============================================================
criterion = (
nn.CrossEntropyLoss()
)
# ============================================================
# 6. Optimizer
# ============================================================
MAX_LR = 1e-3
MIN_LR = 1e-5
optimizer = torch.optim.AdamW(
model.parameters(),
lr=MAX_LR,
weight_decay=0.01
)
# ============================================================
# 7. Training Parameters
# ============================================================
EPOCHS = 20
steps_per_epoch = len(
loader
)
total_steps = (
EPOCHS
*
steps_per_epoch
)
warmup_steps = int(
total_steps
*
0.1
)
print(
"Steps per Epoch:",
steps_per_epoch
)
print(
"Total Steps:",
total_steps
)
print(
"Warmup Steps:",
warmup_steps
)
# ============================================================
# 8. Warmup + Cosine LR
# ============================================================
defget_lr(step):
# --------------------------------------------------------
# Warmup
# --------------------------------------------------------
if (
warmup_steps > 0
and
step < warmup_steps
):
return (
MAX_LR
*
(step + 1)
/
warmup_steps
)
# --------------------------------------------------------
# Cosine Decay
# --------------------------------------------------------
if total_steps <= warmup_steps:
return MIN_LR
progress = (
step
-
warmup_steps
) / (
total_steps
-
warmup_steps
)
# 防止超范围
progress = min(
max(progress, 0.0),
1.0
)
cosine = (
0.5
*
(
1.0
+
math.cos(
math.pi
*
progress
)
)
)
return (
MIN_LR
+
(
MAX_LR
-
MIN_LR
)
*
cosine
)
# ============================================================
# 9. Training
# ============================================================
global_step = 0
for epoch in range(
EPOCHS
):
model.train()
epoch_loss = 0.0
sample_count = 0
for batch_x, batch_y in loader:
# ====================================================
# A. 根据当前 Step 设置 Learning Rate
# ====================================================
current_lr = get_lr(
global_step
)
for param_group in (
optimizer.param_groups
):
param_group["lr"] = (
current_lr
)
# ====================================================
# B. Clear Gradient
# ====================================================
optimizer.zero_grad()
# ====================================================
# C. Forward
# ====================================================
logits = model(
batch_x
)
# ====================================================
# D. Loss
# ====================================================
loss = criterion(
logits,
batch_y
)
# ====================================================
# E. 检查 Loss 是否有限
# ====================================================
ifnot torch.isfinite(
loss
):
raise RuntimeError(
f"Non-finite loss "
f"at step {global_step}: "
f"{loss.item()}"
)
# ====================================================
# F. Backward
# ====================================================
loss.backward()
# ====================================================
# G. Gradient Clipping
# ====================================================
#
# 返回裁剪前的 Gradient Norm。
#
grad_norm = (
torch.nn.utils
.clip_grad_norm_(
model.parameters(),
max_norm=1.0
)
)
# ====================================================
# H. Optimizer Update
# ====================================================
optimizer.step()
# ====================================================
# I. Statistics
# ====================================================
batch_size = (
batch_x.size(0)
)
epoch_loss += (
loss.item()
*
batch_size
)
sample_count += (
batch_size
)
# ====================================================
# J. Training Monitoring
# ====================================================
if global_step % 100 == 0:
print(
f"Step={global_step:4d} "
f"Loss={loss.item():.4f} "
f"LR={current_lr:.8f} "
f"GradNorm="
f"{float(grad_norm):.4f}"
)
# ====================================================
# K. Global Step
# ====================================================
global_step += 1
# ========================================================
# Epoch Loss
# ========================================================
average_loss = (
epoch_loss
/
sample_count
)
print(
f"Epoch={epoch:2d} "
f"Average Loss="
f"{average_loss:.6f}"
)
二十七、这个 Training Loop 为什么重要?
因为这已经不再是:
教学版五行代码
而开始有真正训练系统的样子:
Dataset
↓
DataLoader
↓
Batch
↓
设置当前 LR
↓
zero_grad
↓
Forward
↓
Loss
↓
NaN 检查
↓
Backward
↓
Gradient Clipping
↓
Optimizer Step
↓
记录 Loss
↓
记录 LR
↓
记录 Gradient Norm
↓
Global Step
Day16 写 MiniGPT 时,我们不会一次把所有高级功能全塞进去,但今天这个模板以后会重新出现。
二十八、特别理解 global_step
假设:
Dataset = 1000
Batch = 32
那么大约:
1 Epoch
≈
32 Steps
如果:
20 Epoch
那么:
Total Steps
≈
640
global_step:
Epoch0:
0
1
2
...
31
Epoch1:
32
33
...
不会每个 Epoch 重新归零。
所以:
Scheduler Warmup Logging Checkpoint
都可以统一围绕:
global_step
工作。
二十九、Warmup + Cosine + Clip 的关系
三者解决的是完全不同的问题。
Warmup
解决:
训练刚开始模型不稳定
策略:
小 LR
↓
逐渐增大
Cosine Decay
解决:
训练后期需要更精细更新
策略:
Peak LR
↓
逐渐降低
Gradient Clipping
解决:
某一步 Gradient 异常大
策略:
限制 Gradient Norm
所以:
Warmup
≠
Scheduler
≠
Gradient Clipping
它们可以同时存在。
三十、为什么不能把 Gradient Clip 得特别小?
例如:
max_norm=0.000001
看起来:
再也不会梯度爆炸了!
但问题是:
所有梯度都被压得极小
↓
参数几乎不更新
↓
模型学不动
所以 Gradient Clipping 同样不是:
越小越安全
而是训练超参数。
例如:
max_norm = 1.0
是很常见的起点之一,但不是任何模型的固定答案。
三十一、和量化模型有什么关系?
以后我们训练深度量化模型:
PE
PB
ROE
Momentum
新闻因子
...
↓
MLP / LSTM / Transformer
↓
未来收益预测
↓
Loss
同样可能遇到:
Loss 震荡 Gradient 爆炸 LR 过大 Overfitting NaN
尤其金融数据:
异常值 Fat Tail 极端收益
非常常见。
例如某只股票:
普通收益:
±2%
某天:
+100%
如果输入 / Label 处理不合理,可能导致 Gradient 非常异常。
所以量化模型训练也需要:
Winsorization Normalization Robust Loss Gradient Clipping Training Monitoring
以后第二阶段会再次碰到。
三十二、Day15 今日作业
为什么训练过程中可能需要逐渐降低 Learning Rate? Warmup 是什么?为什么 Transformer 经常使用? Scheduler 和 Optimizer 分别负责什么? 什么是 Gradient Explosion? 什么是 Gradient Norm? Gradient Clipping 应该放在 backward()前还是后?为什么?为什么 Gradient Clipping 不能设置得无限小? 什么是 Vanishing Gradient? 为什么训练模型不能只观察 Loss? global_step和 Epoch 有什么区别?如果出现 Loss = NaN,应该检查哪些问题?Warmup + Cosine Decay 的 LR 大致经历什么过程?
三十三、作业参考答案
1
训练前期离较优区域较远,可以使用较大的 LR 加快优化;训练后期接近较优区域后,降低 LR 可以进行更细致的参数调整,并减少在较优区域附近震荡。
2
Warmup 是:
训练最开始从较小 Learning Rate 逐渐增加到目标 Learning Rate。
因为深层 Transformer 在随机初始化后的早期训练可能不稳定,直接使用较大的 LR 可能导致参数更新过猛。
3
Optimizer:
Gradient
↓
决定参数怎么更新
Scheduler:
训练进度
↓
决定 Learning Rate 怎么变化
二者合作。
4
Gradient Explosion:
反向传播过程中梯度变得异常巨大。
可能导致:
参数剧烈更新
↓
Loss 暴涨
↓
Inf / NaN
↓
训练失败
5
Gradient Norm 用一个数衡量整体梯度大小。
常见 L2 Norm:
||g||_2 = sqrt(Σ_i g_i²)
可以用于监控训练稳定性和 Gradient Clipping。
6
应该:
loss.backward()
↓
clip_grad_norm_
↓
optimizer.step()
因为 backward()后 Gradient 才被计算出来,而 optimizer.step()前必须完成裁剪,否则参数已经按异常梯度更新。
7
如果 max_norm太小,正常 Gradient 也会被持续大幅压缩。
结果:
参数更新过小
↓
训练速度极慢
甚至学不动
所以它同样需要合理设置。
8
Vanishing Gradient:
梯度在深层网络反向传播过程中变得越来越小,使前面的层几乎得不到有效更新。
Residual、Normalization、合理初始化等设计都和深层网络训练稳定性密切相关。
9
因为 Loss 只是训练状态的一部分。
还需要观察:
Validation Loss Learning Rate Gradient Norm 任务指标 NaN / Inf 训练速度
例如:
Train Loss 持续下降
同时:
Validation Loss 上升
就是 Day14 的 Overfitting。
10
Epoch:
完整遍历一次训练 Dataset。
Global Step:
整个训练过程中已经完成了多少次参数更新。
例如:
100 Batch / Epoch
10 Epoch
大约:
1000 global steps
11
可以检查:
Learning Rate 是否过大 Gradient 是否爆炸 输入是否存在 NaN / Inf Label 是否异常 Loss 实现是否存在 log(0) / 除 0 数据是否有极端异常值 数值精度是否稳定 模型结构是否存在问题
而不是只加 Gradient Clipping 掩盖问题。
12
大致:
训练开始
LR 很小
↓ Warmup
逐渐升高
↓ Peak LR
达到最大值
↓ Cosine Decay
平滑下降
↓
训练结束时较小
Warmup + Cosine Decay 可以直观记成:
Learning Rate
│
│ /\
│ / \
│ / ╲
│ / ╲
│ / ╲
│ / ╲
│ / ╲___
│ /
│ /
│ /
│/
└──────────────────────── Training Step
Warmup Decay
即:
先小 → 逐渐变大 → 达到 Peak LR → 再逐渐变小。
三十四、Day15 建议真正运行的三个实验
今天不要只看代码,至少实际运行这三个。
实验 1:观察 Scheduler
day15_step_lr.py
重点观察:
Epoch
↓
Learning Rate
例如:
Epoch 0 0.100
Epoch 5 0.050
Epoch 10 0.025
Epoch 15 0.0125
目的:
理解 Learning Rate 可以在训练过程中动态变化。
实验 2:Warmup + Cosine
day15_warmup_cosine.py
这个实验我认为今天最值得看图。
观察:
Step 0
↓
Warmup
↓
Peak LR
↓
Cosine Decay
↓
Min LR
尤其修改:
warmup_steps = 100
分别试:
0
50
100
300
看看曲线怎么变化。
你会直观理解:
Warmup Ratio
到底意味着什么。
实验 3:完整 Stable Training Loop
day15_stable_training.py
这是今天最重要的代码。
重点观察终端:
Step=0
Loss=...
LR=...
GradNorm=...
Step=100
Loss=...
LR=...
GradNorm=...
不要只盯 Loss。
第一次尝试同时观察三个指标:
Loss Learning Rate Gradient Norm
这已经开始像真正的模型训练日志了。
三十五、建议再做一个故障实验
这个实验很有价值。
把:
MAX_LR = 1e-3
故意改大:
MAX_LR = 1
甚至:
MAX_LR = 10
观察:
Loss Gradient Norm
会发生什么。
然后再恢复:
MAX_LR = 1e-3
你会第一次非常直观地体会:
训练失败很多时候不是模型结构错了,而是训练配置出了问题。
不过实验结束记得恢复正常参数。
三十六、再做一个 Gradient Clipping 对照实验
把:
torch.nn.utils.clip_grad_norm_(
model.parameters(),
max_norm=1.0
)
暂时去掉。
和开启 Clipping 比较:
No Clipping
VS
max_norm = 1.0
注意:
不一定每个小模型都能看到巨大区别。
因为 Gradient Clipping 是:
梯度异常时
才特别重要。
我们的教学模型本来就比较稳定。
所以如果两者结果差不多:
完全正常。
不要为了“证明 Clipping 有效”故意解释不存在的差异。
三十七、Day15 之后,你应该能读懂这种训练代码了
以后看到:
for batch in train_loader:
optimizer.zero_grad()
logits = model(
batch["input_ids"]
)
loss = criterion(
logits,
batch["labels"]
)
loss.backward()
grad_norm = (
torch.nn.utils
.clip_grad_norm_(
model.parameters(),
1.0
)
)
optimizer.step()
scheduler.step()
global_step += 1
你不应该再觉得:
“这是一堆 PyTorch API。”
而应该在脑子里自动翻译成:
拿一个 Batch
↓
清空旧 Gradient
↓
模型预测
↓
计算错误程度
↓
反向传播
↓
检查 / 限制 Gradient
↓
Optimizer 修改参数
↓
调整下一步 Learning Rate
↓
Step + 1
这就是这几天真正想建立的能力。
三十八、我们已经完成第一阶段前 15 天
现在回头看,这其实已经走了相当长的一段路。
前 9 天主要解决:
模型内部是什么?
Day10~15 解决:
模型到底怎么训练?
现在这两条线终于可以汇合了。
三十九、为什么 Day16 是一个重要节点?
因为从 Day3 开始,我们一直在造零件:
Tokenizer Embedding Position Embedding Multi-Head Attention Causal Mask LayerNorm Residual FFN LM Head Cross Entropy Dataset DataLoader AdamW Dropout Scheduler Gradient Clipping
现在零件基本齐了。
所以 Day16 不再继续增加很多新的基础概念。
而是:
开始组装。
四十、Day16 我们要造什么?
真正从零写:
classMiniGPT(nn.Module):
架构:
Token IDs
│
┌──────────┴──────────┐
↓ ↓
Token Embedding Position Embedding
└──────────┬──────────┘
+
↓
Dropout
↓
Transformer Block
↓
Transformer Block
↓
...
↓
LayerNorm
↓
LM Head
↓
Logits
[B, T, V]
Transformer Block:
x
│
├→ LayerNorm
│ ↓
│ Multi-Head Causal Self-Attention
│ ↓
└──────+ Residual
↓
x
│
├→ LayerNorm
│ ↓
│ FFN
│ ↓
└──────+ Residual
↓
Output
这一次不会再是:
分别写几个教学 Demo
而是开始形成一个真正的:
MiniGPT Project
四十一、Day16~20 路线
接下来五天非常关键:
Day16
从零组装 MiniGPT
↓
模型结构完整
Day17
Language Model Dataset
↓
文本切片
Context Window
X/Y Shift
Day18
真正训练 MiniGPT
↓
AdamW
Warmup
Loss
Gradient
Checkpoint
Day19
让自己的 MiniGPT 生成文字
↓
Greedy
Temperature
Top-K
Top-P
训练曲线
模型评估
Day20
第一阶段综合项目
↓
整理代码
模型测试
项目答辩
解释整个 GPT
这五天会把前 15 天所有知识真正串起来。
Day 15 最重要的一句话
训练模型不仅是让 Loss 下降,还要控制 Learning Rate、监控 Gradient、处理数值异常,并让整个优化过程稳定地走向更好的参数区域。
到这里,我们已经有了一套比较完整的训练思维:
数据正确
↓
Dataset / DataLoader
↓
模型结构正确
↓
Loss 合理
↓
Backward
↓
Gradient 正常
↓
AdamW
↓
Warmup
↓
LR Decay
↓
Gradient Clipping
↓
Validation
↓
Early Stopping
↓
Checkpoint
↓
最终模型
Day15 到这里完整结束。
预告:Day 16
下一课 Day16:从零实现 MiniGPT。
从 Day16 开始,我们终于可以把前面学过的所有东西放进同一个工程里,真正回答最开始那个目标:
“我能不能自己写一个小型 Transformer / GPT 模型,并亲手把它训练起来?”
答案从下一课开始就会变成:可以,而且我们自己一层一层写。