夜雨聆风学习资料网

ARTICLE · 1073390

AI + 量化开发工程师学习笔记 Day 15:Learning Rate Scheduler、Warmup、Gradient Clipping 与训练稳定性

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

今天重点掌握四件事:

  1. Learning Rate Scheduler
  2. Warmup
  3. Gradient Clipping
  4. 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 今日作业

  1. 为什么训练过程中可能需要逐渐降低 Learning Rate?
  2. Warmup 是什么?为什么 Transformer 经常使用?
  3. Scheduler 和 Optimizer 分别负责什么?
  4. 什么是 Gradient Explosion?
  5. 什么是 Gradient Norm?
  6. Gradient Clipping 应该放在 backward()前还是后?为什么?
  7. 为什么 Gradient Clipping 不能设置得无限小?
  8. 什么是 Vanishing Gradient?
  9. 为什么训练模型不能只观察 Loss?
  10. global_step和 Epoch 有什么区别?
  11. 如果出现 Loss = NaN,应该检查哪些问题?
  12. 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 天

现在回头看,这其实已经走了相当长的一段路。

Day
内容
你真正掌握的东西
1
AI / ML / DL / LLM
AI 整体地图
2
MLP
第一个神经网络
3
Embedding
离散 ID → 连续向量
4
Tokenizer
文本 → Token
5
Attention
Q / K / V
6
Self-Attention
Causal Mask
7
Transformer
Multi-Head、FFN、Residual、Norm
8
GPT
Next Token、Autoregressive
9
Position
Token + Position
10
Loss
Softmax、Cross Entropy
11
Training Loop
Forward / Backward / Update
12
Dataset / DataLoader
Mini-Batch
13
Optimizer
SGD / Momentum / Adam / AdamW
14
Generalization
Overfitting / Dropout / Validation
15
Training Stability
Warmup / Scheduler / Gradient Clip

前 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 模型,并亲手把它训练起来?”

答案从下一课开始就会变成:可以,而且我们自己一层一层写。

相关学习资料