Skip to content
Published on

深度学习训练方法完全指南:从优化到分布式训练

分享
Authors

引言

过去十余年间,深度学习在计算机视觉、自然语言处理、语音识别、强化学习等几乎所有 AI 领域都取得了革命性的成果。但仅仅设计出神经网络结构,并不足以打造出优秀的模型。如何训练才是决定模型性能的核心要素。

本文系统梳理了高效训练深度学习模型所需的全部技巧。从最基础的梯度下降法出发,逐步深入到最新的优化器、学习率调度、各种正则化技巧、迁移学习、混合精度训练,直至大规模分布式训练,并配有实战代码。


1. 梯度下降法(Gradient Descent)基础

1.1 损失函数(Loss Function)的概念

在深度学习中,损失函数(Loss Function)是用数值表示模型预测值与真实答案之间误差的函数。模型训练的目标,就是找到能让这个损失值最小化的参数(权重)。

损失函数 L 依赖于模型参数 theta 和数据 (x, y)。用公式表示如下。

L(theta) = (1/N) * sum_{i=1}^{N} l(f(x_i; theta), y_i)

这里 f 是模型函数,l 是单个样本的损失,N 是数据数量。

1.2 直观理解梯度下降法

要直观理解梯度下降法,可以想象一位闭着眼睛在山上往下走的登山者。登山者会从当前位置朝最陡的下坡方向(即梯度的反方向)迈出一步。不断重复这个过程,最终就会到达谷底(最小值)。

用数学语言表示,遵循以下更新规则。

theta_{t+1} = theta_t - lr * grad_L(theta_t)

这里 lr 是学习率(learning rate),grad_L 是损失函数的梯度。

1.3 Batch GD vs Mini-batch GD vs SGD

Batch Gradient Descent(全批量梯度下降)

  • 用整个数据集计算梯度
  • 稳定,但内存消耗大、速度慢
  • 在大规模数据集上不实用

Stochastic Gradient Descent(随机梯度下降, SGD)

  • 用单个样本计算梯度
  • 快,但噪声大、不稳定
  • 适合在线学习

Mini-batch Gradient Descent(小批量梯度下降)

  • 通常用 32~512 个样本计算梯度
  • 结合了批量 GD 与 SGD 的优点
  • 实际中使用最多的方式
import torch
import torch.nn as nn
import numpy as np

# 用简单的线性回归实现梯度下降法
class LinearRegression(nn.Module):
    def __init__(self, input_dim):
        super().__init__()
        self.linear = nn.Linear(input_dim, 1)

    def forward(self, x):
        return self.linear(x)

# 生成数据
torch.manual_seed(42)
X = torch.randn(1000, 10)
true_w = torch.randn(10, 1)
y = X @ true_w + 0.1 * torch.randn(1000, 1)

# 实现小批量梯度下降法
def train_minibatch(model, X, y, batch_size=32, lr=0.01, epochs=100):
    optimizer = torch.optim.SGD(model.parameters(), lr=lr)
    criterion = nn.MSELoss()
    losses = []

    N = len(X)
    for epoch in range(epochs):
        # 打乱顺序
        perm = torch.randperm(N)
        X_shuffled = X[perm]
        y_shuffled = y[perm]

        epoch_loss = 0
        for i in range(0, N, batch_size):
            x_batch = X_shuffled[i:i+batch_size]
            y_batch = y_shuffled[i:i+batch_size]

            optimizer.zero_grad()
            pred = model(x_batch)
            loss = criterion(pred, y_batch)
            loss.backward()
            optimizer.step()

            epoch_loss += loss.item()

        losses.append(epoch_loss / (N // batch_size))
        if epoch % 10 == 0:
            print(f"Epoch {epoch}: Loss = {losses[-1]:.4f}")

    return losses

model = LinearRegression(10)
losses = train_minibatch(model, X, y)

1.4 学习率(Learning Rate)的重要性

学习率是深度学习中最重要的超参数之一。

  • 学习率过大时:损失值发散,或在最小值附近震荡
  • 学习率过小时:训练非常缓慢,且更容易陷入局部最小值
  • 合适的学习率:能快速收敛,同时到达较好的最小值

通常从 0.1、0.01、0.001 等数值开始尝试,具体取值取决于网络结构和数据。

1.5 数学推导(偏导数、链式法则)

神经网络中的反向传播(Backpropagation)利用链式法则(Chain Rule)来计算各层的梯度。

以 3 层网络为例,如下所示。

forward: x -> z1=W1*x -> a1=relu(z1) -> z2=W2*a1 -> output
loss: L = MSE(output, y)

backward (chain rule):
dL/dW2 = dL/d_output * d_output/dz2 * dz2/dW2
dL/dW1 = dL/d_output * ... * da1/dz1 * dz1/dW1
# 用 NumPy 直接实现反向传播
import numpy as np

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def sigmoid_deriv(x):
    s = sigmoid(x)
    return s * (1 - s)

class SimpleNet:
    def __init__(self, input_dim, hidden_dim, output_dim):
        # He 初始化
        self.W1 = np.random.randn(input_dim, hidden_dim) * np.sqrt(2/input_dim)
        self.b1 = np.zeros(hidden_dim)
        self.W2 = np.random.randn(hidden_dim, output_dim) * np.sqrt(2/hidden_dim)
        self.b2 = np.zeros(output_dim)

    def forward(self, x):
        self.x = x
        self.z1 = x @ self.W1 + self.b1
        self.a1 = sigmoid(self.z1)
        self.z2 = self.a1 @ self.W2 + self.b2
        return self.z2

    def backward(self, y, lr=0.01):
        N = len(y)
        # 输出层梯度 (MSE loss)
        dL_dz2 = 2 * (self.z2 - y.reshape(-1, 1)) / N

        # W2, b2 梯度
        dL_dW2 = self.a1.T @ dL_dz2
        dL_db2 = dL_dz2.sum(axis=0)

        # 反向传播到隐藏层
        dL_da1 = dL_dz2 @ self.W2.T
        dL_dz1 = dL_da1 * sigmoid_deriv(self.z1)

        # W1, b1 梯度
        dL_dW1 = self.x.T @ dL_dz1
        dL_db1 = dL_dz1.sum(axis=0)

        # 更新参数
        self.W2 -= lr * dL_dW2
        self.b2 -= lr * dL_db2
        self.W1 -= lr * dL_dW1
        self.b1 -= lr * dL_db1

# 测试
net = SimpleNet(10, 32, 1)
X_np = np.random.randn(100, 10)
y_np = np.random.randn(100)

for i in range(100):
    pred = net.forward(X_np)
    loss = np.mean((pred.flatten() - y_np) ** 2)
    net.backward(y_np)
    if i % 20 == 0:
        print(f"Step {i}: MSE = {loss:.4f}")

2. 高级优化器(Optimizers)

2.1 Momentum SGD

普通 SGD 只沿着梯度方向移动,因此在狭窄峡谷形状的损失曲面上会出现之字形移动。Momentum 引入了物理学中的惯性概念,让模型记住此前的移动方向。

v_t = beta * v_{t-1} + (1 - beta) * grad_t
theta_{t+1} = theta_t - lr * v_t

beta(momentum)值通常取 0.9。

import torch
import torch.nn as nn

# Momentum SGD
optimizer_momentum = torch.optim.SGD(
    model.parameters(),
    lr=0.01,
    momentum=0.9,
    nesterov=False  # 是否使用 Nesterov Momentum
)

# Nesterov Momentum (NAG) - 更精确的方向预测
optimizer_nag = torch.optim.SGD(
    model.parameters(),
    lr=0.01,
    momentum=0.9,
    nesterov=True
)

2.2 Adagrad(自适应学习率)

Adagrad 为每个参数应用各自独立的学习率。对于频繁更新的参数降低学习率,对于更新较少的参数保持学习率不变。

G_t = G_{t-1} + grad_t^2
theta_{t+1} = theta_t - (lr / sqrt(G_t + epsilon)) * grad_t

对稀疏(sparse)数据有效,但由于 G_t 不断累加,存在学习率趋近于 0 的问题。

optimizer_adagrad = torch.optim.Adagrad(
    model.parameters(),
    lr=0.01,
    eps=1e-8,
    weight_decay=0
)

2.3 RMSprop

为了解决 Adagrad 学习率消失的问题,RMSprop 使用了指数移动平均(Exponential Moving Average)。

E[g^2]_t = rho * E[g^2]_{t-1} + (1 - rho) * grad_t^2
theta_{t+1} = theta_t - (lr / sqrt(E[g^2]_t + epsilon)) * grad_t
optimizer_rmsprop = torch.optim.RMSprop(
    model.parameters(),
    lr=0.001,
    alpha=0.99,  # rho (decay factor)
    eps=1e-8,
    momentum=0,
    centered=False
)

2.4 Adam(Adaptive Moment Estimation)

Adam 是结合了 Momentum 和 RMSprop 的优化器,目前使用最广泛。它同时追踪一阶矩(均值)和二阶矩(方差)。

算法公式如下。

m_t = beta1 * m_{t-1} + (1 - beta1) * g_t        # 一阶矩 (偏差校正前)
v_t = beta2 * v_{t-1} + (1 - beta2) * g_t^2      # 二阶矩 (偏差校正前)

m_hat = m_t / (1 - beta1^t)                        # 偏差校正
v_hat = v_t / (1 - beta2^t)                        # 偏差校正

theta_{t+1} = theta_t - lr * m_hat / (sqrt(v_hat) + epsilon)

默认超参数:lr=0.001, beta1=0.9, beta2=0.999, epsilon=1e-8

optimizer_adam = torch.optim.Adam(
    model.parameters(),
    lr=1e-3,
    betas=(0.9, 0.999),
    eps=1e-8,
    weight_decay=0
)

2.5 AdamW(分离 Weight Decay)

在标准 Adam 中,L2 正则化与梯度耦合在一起,因此会受到自适应学习率的影响。AdamW 将权重衰减(weight decay)直接应用到参数更新中。

theta_{t+1} = theta_t - lr * (m_hat / (sqrt(v_hat) + epsilon) + lambda * theta_t)

在 Transformer 模型训练中,AdamW 已经成为标准做法(BERT、GPT 等)。

optimizer_adamw = torch.optim.AdamW(
    model.parameters(),
    lr=1e-4,
    betas=(0.9, 0.999),
    eps=1e-8,
    weight_decay=0.01  # L2 正则化强度
)

2.6 LARS 与 LAMB(大规模批量训练)

当使用大规模批量(数千个)时,普通 Adam 的性能会下降。LARS(Layer-wise Adaptive Rate Scaling)和 LAMB 会按层调整学习率。

LARS: lr_l = lr * ||w_l|| / (||g_l|| + lambda * ||w_l||)
LAMB: Adam 更新中应用逐层信赖比率
# pip install lars (或自行实现)
# LAMB 包含在 Hugging Face transformers 中
from transformers import optimization

# LAMB optimizer (使用 transformers 库)
# 或使用 apex 库中的 FusedLAMB

2.7 Lion Optimizer(2023)

Google Brain 于 2023 年发布的 Lion(EvoLved Sign Momentum)在使用比 Adam 更少内存的同时,展现出有竞争力的性能。由于只使用符号(sign),更新幅度始终保持相同大小。

class Lion(torch.optim.Optimizer):
    def __init__(self, params, lr=1e-4, betas=(0.9, 0.99), weight_decay=0.0):
        defaults = dict(lr=lr, betas=betas, weight_decay=weight_decay)
        super().__init__(params, defaults)

    def step(self, closure=None):
        loss = None
        if closure is not None:
            with torch.enable_grad():
                loss = closure()

        for group in self.param_groups:
            for p in group['params']:
                if p.grad is None:
                    continue

                grad = p.grad
                lr = group['lr']
                beta1, beta2 = group['betas']
                wd = group['weight_decay']

                state = self.state[p]
                if len(state) == 0:
                    state['exp_avg'] = torch.zeros_like(p)

                exp_avg = state['exp_avg']

                # Lion 更新
                update = exp_avg * beta1 + grad * (1 - beta1)
                p.data.mul_(1 - lr * wd)
                p.data.add_(update.sign_(), alpha=-lr)

                # 动量更新
                exp_avg.mul_(beta2).add_(grad, alpha=1 - beta2)

        return loss

2.8 优化器对比实验

import torch
import torch.nn as nn
import matplotlib.pyplot as plt

# 用简单模型比较优化器
class MLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(2, 64),
            nn.ReLU(),
            nn.Linear(64, 64),
            nn.ReLU(),
            nn.Linear(64, 1)
        )

    def forward(self, x):
        return self.net(x)

def train_and_compare(optimizers_dict, X, y, epochs=200):
    results = {}

    for name, opt_fn in optimizers_dict.items():
        model = MLP()
        optimizer = opt_fn(model.parameters())
        criterion = nn.MSELoss()
        losses = []

        for epoch in range(epochs):
            optimizer.zero_grad()
            pred = model(X)
            loss = criterion(pred, y)
            loss.backward()
            optimizer.step()
            losses.append(loss.item())

        results[name] = losses
        print(f"{name}: Final Loss = {losses[-1]:.4f}")

    return results

# 生成数据
X = torch.randn(500, 2)
y = (X[:, 0] * 2 + X[:, 1] * 3 + torch.randn(500) * 0.1).unsqueeze(1)

optimizers = {
    'SGD': lambda p: torch.optim.SGD(p, lr=0.01),
    'SGD+Momentum': lambda p: torch.optim.SGD(p, lr=0.01, momentum=0.9),
    'Adam': lambda p: torch.optim.Adam(p, lr=0.001),
    'AdamW': lambda p: torch.optim.AdamW(p, lr=0.001, weight_decay=0.01),
    'RMSprop': lambda p: torch.optim.RMSprop(p, lr=0.001),
}

results = train_and_compare(optimizers, X, y)

3. 学习率调度(LR Scheduling)

固定的学习率并非最优。通过学习率调度在训练过程中动态调整学习率,可以获得更快的收敛速度和更好的性能。

3.1 Step Decay 与 Exponential Decay

import torch
import torch.optim as optim

model = MLP()
optimizer = optim.SGD(model.parameters(), lr=0.1)

# Step Decay: 每隔固定轮数将学习率降为 gamma 倍
step_scheduler = optim.lr_scheduler.StepLR(
    optimizer,
    step_size=30,   # 每 30 个 epoch
    gamma=0.1       # 降低 10 倍
)

# MultiStep Decay: 在指定的 epoch 降低学习率
multistep_scheduler = optim.lr_scheduler.MultiStepLR(
    optimizer,
    milestones=[30, 60, 80],
    gamma=0.1
)

# Exponential Decay: 每个 epoch 按指数衰减
exp_scheduler = optim.lr_scheduler.ExponentialLR(
    optimizer,
    gamma=0.95  # 每个 epoch 衰减 5%
)

3.2 Cosine Annealing

Cosine Annealing 让学习率沿余弦函数平滑下降。周期性重启学习率的 Cosine Annealing with Warm Restarts 也经常被使用。

# Cosine Annealing
cosine_scheduler = optim.lr_scheduler.CosineAnnealingLR(
    optimizer,
    T_max=100,      # 周期 (epoch 数)
    eta_min=1e-6    # 最小学习率
)

# Cosine Annealing with Warm Restarts (SGDR)
cosine_restart = optim.lr_scheduler.CosineAnnealingWarmRestarts(
    optimizer,
    T_0=10,     # 初始周期
    T_mult=2,   # 周期倍数
    eta_min=1e-6
)

3.3 Warmup + Cosine Schedule

这是 Transformer 模型训练中已成为标准的调度方式。初期让学习率线性增加(warmup),之后再按余弦调度下降。

import math
from torch.optim.lr_scheduler import LambdaLR

def get_cosine_schedule_with_warmup(optimizer, num_warmup_steps, num_training_steps, num_cycles=0.5):
    def lr_lambda(current_step):
        # Warmup 区间
        if current_step < num_warmup_steps:
            return float(current_step) / float(max(1, num_warmup_steps))

        # Cosine 下降区间
        progress = float(current_step - num_warmup_steps) / float(
            max(1, num_training_steps - num_warmup_steps)
        )
        return max(0.0, 0.5 * (1.0 + math.cos(math.pi * float(num_cycles) * 2.0 * progress)))

    return LambdaLR(optimizer, lr_lambda)

# 使用示例
optimizer = optim.AdamW(model.parameters(), lr=5e-5)
scheduler = get_cosine_schedule_with_warmup(
    optimizer,
    num_warmup_steps=1000,
    num_training_steps=10000
)

3.4 OneCycleLR

OneCycleLR 是为了快速收敛而设计的调度方式,采用先快速提升再降低学习率的方式。该方法由 Leslie Smith 的论文提出,并经 FastAI 推广而普及。

optimizer = optim.SGD(model.parameters(), lr=0.01)
scheduler = optim.lr_scheduler.OneCycleLR(
    optimizer,
    max_lr=0.1,
    steps_per_epoch=len(train_loader),
    epochs=10,
    pct_start=0.3,          # warmup 比例
    anneal_strategy='cos',  # 下降方式
    div_factor=25.0,        # 初始 lr = max_lr / div_factor
    final_div_factor=1e4    # 最终 lr = max_lr / (div_factor * final_div_factor)
)

# 训练循环
for epoch in range(10):
    for batch in train_loader:
        optimizer.zero_grad()
        loss = criterion(model(batch[0]), batch[1])
        loss.backward()
        optimizer.step()
        scheduler.step()  # OneCycleLR 需要每个 batch 调用一次

3.5 Learning Rate Finder

Learning Rate Finder 是一种自动寻找合适学习率范围的技巧。

from torch_lr_finder import LRFinder

model = MLP()
optimizer = optim.SGD(model.parameters(), lr=1e-7, weight_decay=1e-2)
criterion = nn.MSELoss()

# 运行 LR Finder
lr_finder = LRFinder(model, optimizer, criterion, device="cuda")
lr_finder.range_test(train_loader, end_lr=100, num_iter=100)
lr_finder.plot()  # 显示损失-学习率曲线图
lr_finder.reset()  # 将优化器恢复到初始状态

# 在图中选择损失下降最陡峭处对应的学习率
# 通常取最小值的 1/10 ~ 1/3 左右

4. 损失函数(Loss Functions)

4.1 回归损失函数

import torch
import torch.nn as nn
import torch.nn.functional as F

# MSE (Mean Squared Error) - 对异常值敏感
mse_loss = nn.MSELoss()

# MAE (Mean Absolute Error) - 对异常值稳健
mae_loss = nn.L1Loss()

# Huber Loss - MSE 与 MAE 之间的折中
# |y - y_hat| < delta: 0.5 * (y - y_hat)^2
# |y - y_hat| >= delta: delta * (|y - y_hat| - 0.5 * delta)
huber_loss = nn.HuberLoss(delta=1.0)

# 手动实现
def huber_loss_manual(pred, target, delta=1.0):
    residual = torch.abs(pred - target)
    condition = residual < delta
    squared_loss = 0.5 * residual ** 2
    linear_loss = delta * residual - 0.5 * delta ** 2
    return torch.where(condition, squared_loss, linear_loss).mean()

4.2 分类损失函数

# Cross-Entropy Loss (多分类)
ce_loss = nn.CrossEntropyLoss()

# Binary Cross-Entropy (二分类)
bce_loss = nn.BCEWithLogitsLoss()

# Label Smoothing Cross-Entropy (防止过拟合)
ce_smooth = nn.CrossEntropyLoss(label_smoothing=0.1)

# Focal Loss (解决类别不均衡)
class FocalLoss(nn.Module):
    def __init__(self, gamma=2.0, alpha=None, reduction='mean'):
        super().__init__()
        self.gamma = gamma
        self.alpha = alpha
        self.reduction = reduction

    def forward(self, inputs, targets):
        # inputs: (N, C) logits, targets: (N,) class indices
        ce_loss = F.cross_entropy(inputs, targets, reduction='none')
        pt = torch.exp(-ce_loss)  # p_t = 模型分配给正确类别的概率
        focal_loss = ((1 - pt) ** self.gamma) * ce_loss

        if self.alpha is not None:
            alpha_t = self.alpha[targets]
            focal_loss = alpha_t * focal_loss

        if self.reduction == 'mean':
            return focal_loss.mean()
        elif self.reduction == 'sum':
            return focal_loss.sum()
        return focal_loss

4.3 分割损失函数

# BCE Loss for binary segmentation
def bce_loss(pred, target):
    return F.binary_cross_entropy_with_logits(pred, target)

# Dice Loss (对类别不均衡稳健)
def dice_loss(pred, target, smooth=1.0):
    pred = torch.sigmoid(pred)
    pred_flat = pred.view(-1)
    target_flat = target.view(-1)

    intersection = (pred_flat * target_flat).sum()
    dice = (2. * intersection + smooth) / (pred_flat.sum() + target_flat.sum() + smooth)
    return 1 - dice

# BCE + Dice 结合 (分割任务中常用)
def bce_dice_loss(pred, target, bce_weight=0.5):
    bce = bce_loss(pred, target)
    dice = dice_loss(pred, target)
    return bce_weight * bce + (1 - bce_weight) * dice

4.4 度量学习损失函数

# Contrastive Loss (相似样本拉近,不同样本推远)
class ContrastiveLoss(nn.Module):
    def __init__(self, margin=1.0):
        super().__init__()
        self.margin = margin

    def forward(self, output1, output2, label):
        # label=1: 同一类别, label=0: 不同类别
        euclidean_dist = F.pairwise_distance(output1, output2)
        loss = (label * euclidean_dist.pow(2) +
                (1 - label) * F.relu(self.margin - euclidean_dist).pow(2))
        return loss.mean()

# Triplet Loss (anchor, positive, negative)
class TripletLoss(nn.Module):
    def __init__(self, margin=0.3):
        super().__init__()
        self.margin = margin

    def forward(self, anchor, positive, negative):
        pos_dist = F.pairwise_distance(anchor, positive)
        neg_dist = F.pairwise_distance(anchor, negative)
        loss = F.relu(pos_dist - neg_dist + self.margin)
        return loss.mean()

5. 正则化技巧(Regularization)

用于防止过拟合(Overfitting)、提升模型泛化能力的技巧。

5.1 L1/L2 正则化

# L2 Regularization (Weight Decay)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)

# L1 Regularization (手动实现)
def l1_regularization(model, lambda_l1):
    l1_penalty = 0
    for param in model.parameters():
        l1_penalty += torch.abs(param).sum()
    return lambda_l1 * l1_penalty

# L1 + L2 (Elastic Net)
def elastic_net_loss(model, criterion, outputs, targets, lambda_l1=1e-5, lambda_l2=1e-4):
    # 基础损失
    base_loss = criterion(outputs, targets)

    # L1 惩罚项
    l1_penalty = sum(torch.abs(p).sum() for p in model.parameters())

    # L2 惩罚项
    l2_penalty = sum((p ** 2).sum() for p in model.parameters())

    return base_loss + lambda_l1 * l1_penalty + lambda_l2 * l2_penalty

5.2 Dropout

Dropout 在训练过程中随机让神经元失活,以防止共适应(co-adaptation)。Inverted Dropout 会在训练时除以 p,这样推理阶段就不需要再做缩放。

class ModelWithDropout(nn.Module):
    def __init__(self, dropout_rate=0.5):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(784, 512),
            nn.ReLU(),
            nn.Dropout(p=dropout_rate),  # Inverted Dropout (PyTorch 默认)
            nn.Linear(512, 256),
            nn.ReLU(),
            nn.Dropout(p=dropout_rate),
            nn.Linear(256, 10)
        )

    def forward(self, x):
        return self.net(x)

# 训练模式:启用 dropout
model.train()

# 推理模式:关闭 dropout
model.eval()

# DropConnect (将部分权重随机置零)
class DropConnect(nn.Module):
    def __init__(self, p=0.5):
        super().__init__()
        self.p = p

    def forward(self, x):
        if not self.training:
            return x
        # 权重掩码在 nn.Linear 层级别应用
        mask = torch.bernoulli(torch.ones_like(x) * (1 - self.p))
        return x * mask / (1 - self.p)

5.3 Data Augmentation

from torchvision import transforms
import torchvision.transforms.functional as TF

# 基础图像增强
train_transform = transforms.Compose([
    transforms.RandomHorizontalFlip(p=0.5),
    transforms.RandomCrop(32, padding=4),
    transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1),
    transforms.RandomRotation(degrees=15),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# Mixup Augmentation
def mixup_data(x, y, alpha=1.0):
    if alpha > 0:
        lam = np.random.beta(alpha, alpha)
    else:
        lam = 1

    batch_size = x.size()[0]
    index = torch.randperm(batch_size)

    mixed_x = lam * x + (1 - lam) * x[index]
    y_a, y_b = y, y[index]
    return mixed_x, y_a, y_b, lam

def mixup_criterion(criterion, pred, y_a, y_b, lam):
    return lam * criterion(pred, y_a) + (1 - lam) * criterion(pred, y_b)

# CutMix Augmentation
def cutmix_data(x, y, alpha=1.0):
    lam = np.random.beta(alpha, alpha)
    batch_size, C, H, W = x.size()
    index = torch.randperm(batch_size)

    # 计算随机框坐标
    cut_ratio = np.sqrt(1. - lam)
    cut_w = int(W * cut_ratio)
    cut_h = int(H * cut_ratio)

    cx = np.random.randint(W)
    cy = np.random.randint(H)

    bbx1 = np.clip(cx - cut_w // 2, 0, W)
    bby1 = np.clip(cy - cut_h // 2, 0, H)
    bbx2 = np.clip(cx + cut_w // 2, 0, W)
    bby2 = np.clip(cy + cut_h // 2, 0, H)

    mixed_x = x.clone()
    mixed_x[:, :, bby1:bby2, bbx1:bbx2] = x[index, :, bby1:bby2, bbx1:bbx2]

    # 按实际框的大小重新计算 lambda
    lam = 1 - ((bbx2 - bbx1) * (bby2 - bby1) / (W * H))

    return mixed_x, y, y[index], lam

5.4 Early Stopping

class EarlyStopping:
    def __init__(self, patience=10, min_delta=0.001, restore_best_weights=True):
        self.patience = patience
        self.min_delta = min_delta
        self.restore_best_weights = restore_best_weights
        self.counter = 0
        self.best_loss = None
        self.best_weights = None
        self.early_stop = False

    def __call__(self, val_loss, model):
        if self.best_loss is None:
            self.best_loss = val_loss
            self.best_weights = {k: v.clone() for k, v in model.state_dict().items()}
        elif val_loss > self.best_loss - self.min_delta:
            self.counter += 1
            print(f"EarlyStopping counter: {self.counter}/{self.patience}")
            if self.counter >= self.patience:
                self.early_stop = True
        else:
            self.best_loss = val_loss
            self.best_weights = {k: v.clone() for k, v in model.state_dict().items()}
            self.counter = 0

    def restore(self, model):
        if self.restore_best_weights and self.best_weights:
            model.load_state_dict(self.best_weights)
            print("Restored best model weights")

# 使用示例
early_stopping = EarlyStopping(patience=10)

for epoch in range(max_epochs):
    train_loss = train_one_epoch(model, train_loader, optimizer)
    val_loss = evaluate(model, val_loader)

    early_stopping(val_loss, model)
    if early_stopping.early_stop:
        print("Early stopping triggered!")
        early_stopping.restore(model)
        break

6. 归一化层(Normalization Layers)

6.1 Batch Normalization

Batch Normalization(批归一化)由 Sergey Ioffe 和 Christian Szegedy 于 2015 年提出。它在每个小批量内对特征进行归一化,以解决内部协变量偏移(Internal Covariate Shift)问题。

批归一化的过程如下。

1. 小批量均值: mu_B = (1/m) * sum(x_i)
2. 小批量方差: sigma_B^2 = (1/m) * sum((x_i - mu_B)^2)
3. 归一化: x_hat_i = (x_i - mu_B) / sqrt(sigma_B^2 + epsilon)
4. 缩放与平移: y_i = gamma * x_hat_i + beta

这里 gamma(缩放)和 beta(平移)是可学习的参数。

import torch
import torch.nn as nn

class BatchNormNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(784, 512),
            nn.BatchNorm1d(512),
            nn.ReLU(),
            nn.Linear(512, 256),
            nn.BatchNorm1d(256),
            nn.ReLU(),
            nn.Linear(256, 10)
        )

    def forward(self, x):
        return self.net(x)

# 在 Conv 层中使用 BatchNorm
class ConvBNNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 64, 3, padding=1)
        self.bn1 = nn.BatchNorm2d(64)
        self.relu = nn.ReLU(inplace=True)

    def forward(self, x):
        return self.relu(self.bn1(self.conv1(x)))

# 手动实现
class BatchNorm(nn.Module):
    def __init__(self, num_features, eps=1e-5, momentum=0.1):
        super().__init__()
        self.gamma = nn.Parameter(torch.ones(num_features))
        self.beta = nn.Parameter(torch.zeros(num_features))
        self.eps = eps
        self.momentum = momentum

        self.register_buffer('running_mean', torch.zeros(num_features))
        self.register_buffer('running_var', torch.ones(num_features))

    def forward(self, x):
        if self.training:
            mean = x.mean(dim=0)
            var = x.var(dim=0, unbiased=False)

            # 更新移动平均
            self.running_mean = (1 - self.momentum) * self.running_mean + self.momentum * mean
            self.running_var = (1 - self.momentum) * self.running_var + self.momentum * var
        else:
            mean = self.running_mean
            var = self.running_var

        x_norm = (x - mean) / torch.sqrt(var + self.eps)
        return self.gamma * x_norm + self.beta

6.2 Layer Normalization(用于 Transformer)

Layer Normalization 不在批量维度上归一化,而是在特征维度上归一化。它不依赖批量大小,因此适合 RNN、Transformer。

class LayerNorm(nn.Module):
    def __init__(self, normalized_shape, eps=1e-5):
        super().__init__()
        if isinstance(normalized_shape, int):
            normalized_shape = (normalized_shape,)
        self.normalized_shape = normalized_shape
        self.gamma = nn.Parameter(torch.ones(normalized_shape))
        self.beta = nn.Parameter(torch.zeros(normalized_shape))
        self.eps = eps

    def forward(self, x):
        # 在最后 len(normalized_shape) 个维度上归一化
        mean = x.mean(dim=-1, keepdim=True)
        var = x.var(dim=-1, keepdim=True, unbiased=False)
        x_norm = (x - mean) / torch.sqrt(var + self.eps)
        return self.gamma * x_norm + self.beta

# PyTorch 内置 LayerNorm
layer_norm = nn.LayerNorm(512)

# 在 Transformer 块中使用
class TransformerBlock(nn.Module):
    def __init__(self, d_model, nhead, dim_feedforward):
        super().__init__()
        self.attention = nn.MultiheadAttention(d_model, nhead)
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.ffn = nn.Sequential(
            nn.Linear(d_model, dim_feedforward),
            nn.GELU(),
            nn.Linear(dim_feedforward, d_model)
        )

    def forward(self, x):
        # Pre-LayerNorm (最新的 GPT 风格)
        attn_out, _ = self.attention(self.norm1(x), self.norm1(x), self.norm1(x))
        x = x + attn_out
        x = x + self.ffn(self.norm2(x))
        return x

6.3 Instance、Group、RMS Normalization

# Instance Normalization (对每个样本、每个通道独立归一化)
# 在风格迁移(Style Transfer)中效果显著
instance_norm = nn.InstanceNorm2d(64)

# Group Normalization (将通道分组后归一化)
# 批量较小时可替代 BN
group_norm = nn.GroupNorm(num_groups=8, num_channels=64)

# RMS Normalization (用于 LLaMA、T5)
# 在 LayerNorm 的基础上去掉均值项,提升训练速度
class RMSNorm(nn.Module):
    def __init__(self, dim, eps=1e-6):
        super().__init__()
        self.eps = eps
        self.weight = nn.Parameter(torch.ones(dim))

    def _norm(self, x):
        return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)

    def forward(self, x):
        return self.weight * self._norm(x.float()).type_as(x)

# 各归一化方法对比总结
# BatchNorm: 批量维度归一化, 适合 CNN, 依赖批量大小
# LayerNorm: 特征维度归一化, 适合 Transformer/RNN
# InstanceNorm: 按通道归一化, 适合风格迁移
# GroupNorm: 通道分组归一化, 适合小批量
# RMSNorm: 轻量化的 LayerNorm, 适合 LLM

7. 权重初始化(Weight Initialization)

7.1 Xavier/He 初始化

权重初始化决定了训练的起点。错误的初始化可能引发梯度消失或梯度爆炸。

import torch
import torch.nn as nn
import math

class WeightInitDemo(nn.Module):
    def __init__(self, init_method='xavier'):
        super().__init__()
        self.layers = nn.ModuleList([
            nn.Linear(256, 256) for _ in range(5)
        ])
        self.apply_init(init_method)

    def apply_init(self, method):
        for layer in self.layers:
            if method == 'zeros':
                nn.init.zeros_(layer.weight)  # 不好的初始化: 对称性问题
            elif method == 'random_small':
                nn.init.normal_(layer.weight, std=0.01)
            elif method == 'xavier_uniform':
                nn.init.xavier_uniform_(layer.weight)  # 适合 sigmoid/tanh 激活
            elif method == 'xavier_normal':
                nn.init.xavier_normal_(layer.weight)
            elif method == 'kaiming_uniform':
                nn.init.kaiming_uniform_(layer.weight, mode='fan_in', nonlinearity='relu')
            elif method == 'kaiming_normal':
                nn.init.kaiming_normal_(layer.weight, mode='fan_out', nonlinearity='relu')  # 适合 ReLU
            nn.init.zeros_(layer.bias)

    def forward(self, x):
        for layer in self.layers:
            x = torch.relu(layer(x))
        return x

# 初始化对比实验
import matplotlib.pyplot as plt

def check_activations(model, x):
    activations = []
    hooks = []

    def hook(module, input, output):
        activations.append(output.detach())

    for layer in model.layers:
        hooks.append(layer.register_forward_hook(hook))

    with torch.no_grad():
        model(x)

    for hook in hooks:
        hook.remove()

    return activations

x = torch.randn(100, 256)
for method in ['zeros', 'random_small', 'xavier_uniform', 'kaiming_normal']:
    model = WeightInitDemo(method)
    acts = check_activations(model, x)
    print(f"{method}:")
    for i, act in enumerate(acts):
        print(f"  Layer {i+1}: mean={act.mean():.4f}, std={act.std():.4f}")

8. 梯度问题的解决

8.1 梯度消失与梯度爆炸

梯度消失(Vanishing Gradient):反向传播时,梯度随着层数增加而趋近于 0,导致靠前的层无法得到有效训练的问题。主要发生在 sigmoid、tanh 激活函数中。

梯度爆炸(Exploding Gradient):梯度呈指数级增大,产生 NaN 或 Inf 的问题。常见于 RNN 中。

# Gradient Clipping
import torch.nn.utils as utils

# 方法 1: 对梯度范数(norm)进行裁剪
max_norm = 1.0
total_norm = utils.clip_grad_norm_(model.parameters(), max_norm)
print(f"Gradient norm: {total_norm:.4f}")

# 方法 2: 对梯度值进行裁剪
utils.clip_grad_value_(model.parameters(), clip_value=0.5)

# 在训练循环中的使用方式
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)

for batch in train_loader:
    optimizer.zero_grad()
    loss = criterion(model(batch[0]), batch[1])
    loss.backward()

    # 反向传播之后, 优化器 step 之前进行裁剪
    torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

    optimizer.step()

8.2 Residual Connection(跳跃连接)

class ResidualBlock(nn.Module):
    def __init__(self, in_channels, out_channels, stride=1):
        super().__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, 3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU(inplace=True)
        self.conv2 = nn.Conv2d(out_channels, out_channels, 3, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)

        # 维度不同时使用 shortcut
        self.shortcut = nn.Sequential()
        if stride != 1 or in_channels != out_channels:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels, out_channels, 1, stride=stride, bias=False),
                nn.BatchNorm2d(out_channels)
            )

    def forward(self, x):
        out = self.relu(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        out += self.shortcut(x)  # Skip Connection
        out = self.relu(out)
        return out

8.3 Gradient Checkpointing

在非常深的模型中,为了节省内存,不保存部分激活值,在反向传播时重新计算它们。

from torch.utils.checkpoint import checkpoint, checkpoint_sequential

class DeepModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.layers = nn.Sequential(*[
            nn.Sequential(nn.Linear(512, 512), nn.ReLU())
            for _ in range(20)
        ])

    def forward(self, x):
        # 常规方式: 保存全部激活值 (内存 O(N))
        # return self.layers(x)

        # Gradient Checkpointing: 内存 O(sqrt(N))
        return checkpoint_sequential(self.layers, segments=5, input=x)

9. 迁移学习(Transfer Learning)与微调

9.1 Feature Extraction vs Fine-tuning

import torchvision.models as models

# Feature Extraction: 冻结预训练权重
def feature_extraction(num_classes):
    model = models.resnet50(pretrained=True)

    # 冻结全部参数
    for param in model.parameters():
        param.requires_grad = False

    # 只替换最后的分类层 (可训练)
    model.fc = nn.Linear(model.fc.in_features, num_classes)

    return model

# Fine-tuning: 训练部分或全部层
def fine_tuning(num_classes, unfreeze_layers=None):
    model = models.resnet50(pretrained=True)

    # 初始时全部冻结
    for param in model.parameters():
        param.requires_grad = False

    # 替换并激活最后一层
    model.fc = nn.Linear(model.fc.in_features, num_classes)

    # 激活指定层
    if unfreeze_layers:
        for name, param in model.named_parameters():
            for layer in unfreeze_layers:
                if layer in name:
                    param.requires_grad = True

    return model

9.2 渐进式解冻与 Discriminative Learning Rates

def progressive_unfreezing_setup(model, base_lr=1e-4):
    # ResNet50 的层分组
    layer_groups = [
        list(model.layer1.parameters()),
        list(model.layer2.parameters()),
        list(model.layer3.parameters()),
        list(model.layer4.parameters()),
        list(model.fc.parameters())
    ]

    # 初始时只训练 fc
    for group in layer_groups[:-1]:
        for p in group:
            p.requires_grad = False

    return layer_groups

def discriminative_lr_optimizer(model, base_lr=1e-4, lr_multiplier=10):
    # 为各层设置不同学习率 (前层学习率低, 后层学习率高)
    param_groups = [
        {'params': model.layer1.parameters(), 'lr': base_lr / (lr_multiplier**3)},
        {'params': model.layer2.parameters(), 'lr': base_lr / (lr_multiplier**2)},
        {'params': model.layer3.parameters(), 'lr': base_lr / lr_multiplier},
        {'params': model.layer4.parameters(), 'lr': base_lr},
        {'params': model.fc.parameters(), 'lr': base_lr * lr_multiplier},
    ]

    return torch.optim.Adam(param_groups)

9.3 LoRA(Low-Rank Adaptation)

LoRA 是一种用于大语言模型微调的参数高效技巧。它冻结原始权重矩阵,只训练低秩的矩阵分解部分。

当原始权重矩阵 W 的大小为 d x k 时,LoRA 学习 W' = W + BA。这里 B 是 d x r 矩阵,A 是 r x k 矩阵,秩 r 被设置为远小于 d 和 k。

import torch
import torch.nn as nn

class LoRALayer(nn.Module):
    def __init__(self, in_features, out_features, rank=4, alpha=1.0):
        super().__init__()
        self.rank = rank
        self.alpha = alpha
        self.scaling = alpha / rank

        # 原始权重 (冻结)
        self.weight = nn.Parameter(
            torch.randn(out_features, in_features),
            requires_grad=False
        )

        # LoRA 矩阵 A (随机初始化)
        self.lora_A = nn.Parameter(torch.randn(rank, in_features) * 0.01)
        # LoRA 矩阵 B (初始化为 0 -> 训练开始时与原始模型一致)
        self.lora_B = nn.Parameter(torch.zeros(out_features, rank))
        self.bias = nn.Parameter(torch.zeros(out_features))

    def forward(self, x):
        # 原始输出 + LoRA 增量
        base_output = nn.functional.linear(x, self.weight, self.bias)
        lora_output = (x @ self.lora_A.T @ self.lora_B.T) * self.scaling
        return base_output + lora_output

# 使用 HuggingFace PEFT 库 (真实 LLM 微调场景)
from peft import get_peft_model, LoraConfig, TaskType

lora_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    r=8,                          # LoRA 秩
    lora_alpha=32,                # 缩放系数
    target_modules=["q_proj", "v_proj"],  # 应用的模块
    lora_dropout=0.05,
    bias="none"
)

# peft_model = get_peft_model(base_model, lora_config)
# peft_model.print_trainable_parameters()
# trainable params: 4,194,304 || all params: 6,742,609,920 || trainable%: 0.062

10. 超参数调优

10.1 使用 Optuna 进行贝叶斯优化

import optuna
import torch
import torch.nn as nn

def objective(trial):
    # 定义超参数搜索空间
    lr = trial.suggest_float('lr', 1e-5, 1e-1, log=True)
    n_layers = trial.suggest_int('n_layers', 1, 5)
    n_units = trial.suggest_categorical('n_units', [64, 128, 256, 512])
    dropout_rate = trial.suggest_float('dropout', 0.0, 0.5)
    optimizer_name = trial.suggest_categorical('optimizer', ['Adam', 'AdamW', 'SGD'])

    # 构建模型
    layers = []
    in_dim = 784
    for _ in range(n_layers):
        layers.extend([
            nn.Linear(in_dim, n_units),
            nn.ReLU(),
            nn.Dropout(dropout_rate)
        ])
        in_dim = n_units
    layers.append(nn.Linear(in_dim, 10))
    model = nn.Sequential(*layers)

    # 选择优化器
    if optimizer_name == 'Adam':
        optimizer = torch.optim.Adam(model.parameters(), lr=lr)
    elif optimizer_name == 'AdamW':
        optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=0.01)
    else:
        optimizer = torch.optim.SGD(model.parameters(), lr=lr, momentum=0.9)

    # 训练与验证
    # ... (省略训练循环)
    val_accuracy = 0.95  # 实际中应在训练后计算

    return val_accuracy

# 创建并运行 Optuna study
study = optuna.create_study(
    direction='maximize',
    sampler=optuna.samplers.TPESampler(),    # Tree-structured Parzen Estimator
    pruner=optuna.pruners.MedianPruner()     # 提前终止表现较差的 trial
)

study.optimize(objective, n_trials=100, timeout=3600)

print(f"Best trial: {study.best_trial.value:.4f}")
print(f"Best params: {study.best_trial.params}")

# 结果可视化
# optuna.visualization.plot_optimization_history(study)
# optuna.visualization.plot_param_importances(study)

11. 混合精度训练(Mixed Precision Training)

11.1 FP32 vs FP16 vs BF16

格式指数位尾数位表示范围主要用途
FP32823+-3.4e38默认训练
FP16510+-65504推理 / 训练 (注意溢出)
BF1687+-3.4e38LLM 训练 (A100, TPU)

11.2 PyTorch AMP(Automatic Mixed Precision)

import torch
from torch.cuda.amp import autocast, GradScaler

# GradScaler: 用于防止 FP16 下溢的损失缩放
scaler = GradScaler()

model = MyModel().cuda()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.CrossEntropyLoss()

for epoch in range(num_epochs):
    for inputs, labels in train_loader:
        inputs, labels = inputs.cuda(), labels.cuda()

        optimizer.zero_grad()

        # 在 autocast 上下文中执行 FP16 运算
        with autocast(dtype=torch.float16):
            outputs = model(inputs)
            loss = criterion(outputs, labels)

        # 损失缩放后再反向传播
        scaler.scale(loss).backward()

        # 梯度裁剪 (在调整缩放之后)
        scaler.unscale_(optimizer)
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

        # 优化器 step (自动跳过 NaN/Inf 梯度)
        scaler.step(optimizer)
        scaler.update()

# 使用 BF16 (更稳定, 需要 Ampere 及以上 GPU)
with autocast(dtype=torch.bfloat16):
    outputs = model(inputs)
    loss = criterion(outputs, labels)

12. 分布式训练(Distributed Training)

12.1 数据并行 (Data Parallelism)

将数据分散到多个 GPU 上,各 GPU 独立执行前向与反向传播,再汇总梯度。

import torch
import torch.nn as nn
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.utils.data.distributed import DistributedSampler

def setup(rank, world_size):
    import os
    os.environ['MASTER_ADDR'] = 'localhost'
    os.environ['MASTER_PORT'] = '12355'
    dist.init_process_group('nccl', rank=rank, world_size=world_size)

def cleanup():
    dist.destroy_process_group()

def train_ddp(rank, world_size, model_class, dataset):
    setup(rank, world_size)

    # 每个进程使用各自的 GPU
    device = torch.device(f'cuda:{rank}')
    model = model_class().to(device)

    # 用 DDP 包裹
    model = DDP(model, device_ids=[rank])

    # DistributedSampler: 让每个进程采样不同的数据
    sampler = DistributedSampler(
        dataset,
        num_replicas=world_size,
        rank=rank,
        shuffle=True
    )

    loader = torch.utils.data.DataLoader(
        dataset,
        batch_size=32,
        sampler=sampler,
        num_workers=4,
        pin_memory=True
    )

    optimizer = torch.optim.Adam(model.parameters(), lr=1e-3 * world_size)
    criterion = nn.CrossEntropyLoss()

    for epoch in range(num_epochs):
        sampler.set_epoch(epoch)  # 每个 epoch 更换 shuffle 种子

        for inputs, labels in loader:
            inputs, labels = inputs.to(device), labels.to(device)
            optimizer.zero_grad()

            with autocast():
                outputs = model(inputs)
                loss = criterion(outputs, labels)

            scaler.scale(loss).backward()
            scaler.step(optimizer)
            scaler.update()

        if rank == 0:
            print(f"Epoch {epoch}: Loss = {loss.item():.4f}")

    cleanup()

# 多进程执行
import torch.multiprocessing as mp

if __name__ == '__main__':
    world_size = torch.cuda.device_count()
    mp.spawn(train_ddp, args=(world_size, MyModel, dataset), nprocs=world_size, join=True)

12.2 FSDP(Fully Sharded Data Parallel)

FSDP 将模型参数、梯度、优化器状态分散到所有 GPU 上,以节省内存。适合 GPT-3 级别的超大规模模型训练。

from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
from torch.distributed.fsdp import ShardingStrategy, MixedPrecision
from torch.distributed.fsdp.wrap import transformer_auto_wrap_policy
import functools

# Mixed Precision 设置
bf16_policy = MixedPrecision(
    param_dtype=torch.bfloat16,
    reduce_dtype=torch.bfloat16,
    buffer_dtype=torch.bfloat16
)

# Transformer 层自动包装策略
auto_wrap_policy = functools.partial(
    transformer_auto_wrap_policy,
    transformer_layer_cls={TransformerBlock}
)

# 创建 FSDP 模型
model = FSDP(
    model,
    sharding_strategy=ShardingStrategy.FULL_SHARD,    # 完全分片
    mixed_precision=bf16_policy,
    auto_wrap_policy=auto_wrap_policy,
    device_id=rank
)

12.3 Gradient Accumulation

当 GPU 显存不足时,通过多次使用小批量来实现大批量的效果。

model = MyModel().cuda()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.CrossEntropyLoss()

# 有效批量大小 = micro_batch_size * accumulation_steps
micro_batch_size = 8
accumulation_steps = 8  # 有效批量大小: 64

optimizer.zero_grad()
for step, (inputs, labels) in enumerate(train_loader):
    inputs, labels = inputs.cuda(), labels.cuda()

    with autocast():
        outputs = model(inputs)
        loss = criterion(outputs, labels)
        # 将损失除以 accumulation_steps (保持均值)
        loss = loss / accumulation_steps

    scaler.scale(loss).backward()

    if (step + 1) % accumulation_steps == 0:
        scaler.unscale_(optimizer)
        torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
        scaler.step(optimizer)
        scaler.update()
        optimizer.zero_grad()

13. 大语言模型训练技巧

13.1 Instruction Tuning

Instruction Tuning 是让模型学会遵循自然语言指令的技巧。它是 FLAN、InstructGPT、LLaMA-2 成功的核心因素之一。

# Instruction Tuning 数据格式示例
instruction_data = [
    {
        "instruction": "请分析以下文本的情感。",
        "input": "今天天气好极了,我心情特别好!",
        "output": "积极情感。文本表达了对天气的满意和幸福感。"
    },
    {
        "instruction": "请根据给定信息撰写摘要。",
        "input": "...(长文本)...",
        "output": "...(摘要)..."
    }
]

# 用 Alpaca 格式组织提示词
def format_instruction(sample):
    if sample.get('input'):
        return f"""### Instruction:
{sample['instruction']}

### Input:
{sample['input']}

### Response:
{sample['output']}"""
    else:
        return f"""### Instruction:
{sample['instruction']}

### Response:
{sample['output']}"""

13.2 RLHF(人类反馈强化学习)

RLHF 由三个阶段组成。

第一阶段:SFT(Supervised Fine-tuning) - 用人类撰写的高质量回答进行微调 第二阶段:Reward Model 训练 - 训练模型在多个回答中偏好更好的回答 第三阶段:用 PPO 优化策略 - 使用 Reward Model 进行强化学习

# 阶段 2: Reward Model (Bradley-Terry 模型)
class RewardModel(nn.Module):
    def __init__(self, base_model):
        super().__init__()
        self.base_model = base_model
        self.reward_head = nn.Linear(base_model.config.hidden_size, 1)

    def forward(self, input_ids, attention_mask):
        outputs = self.base_model(input_ids=input_ids, attention_mask=attention_mask)
        # 使用最后一个 token 的 hidden state
        last_hidden = outputs.last_hidden_state[:, -1, :]
        reward = self.reward_head(last_hidden).squeeze(-1)
        return reward

# Reward Model 训练 (偏好损失)
def preference_loss(reward_chosen, reward_rejected):
    # Bradley-Terry 模型: p(chosen > rejected) = sigmoid(r_chosen - r_rejected)
    return -torch.log(torch.sigmoid(reward_chosen - reward_rejected)).mean()

13.3 DPO(Direct Preference Optimization)

DPO 将 RLHF 中复杂的 PPO 训练简化为直接对偏好数据进行优化。

import torch
import torch.nn.functional as F

def dpo_loss(
    policy_chosen_logps,    # 策略模型对偏好回答的对数概率
    policy_rejected_logps,  # 策略模型对非偏好回答的对数概率
    reference_chosen_logps, # 参考模型对偏好回答的对数概率
    reference_rejected_logps, # 参考模型对非偏好回答的对数概率
    beta=0.1                # KL 惩罚强度
):
    # 策略模型与参考模型之间的 log ratio
    chosen_rewards = beta * (policy_chosen_logps - reference_chosen_logps)
    rejected_rewards = beta * (policy_rejected_logps - reference_rejected_logps)

    # DPO 损失: -log(sigmoid(chosen_rewards - rejected_rewards))
    loss = -F.logsigmoid(chosen_rewards - rejected_rewards).mean()

    # 用于记录的 reward
    chosen_reward = chosen_rewards.detach().mean()
    rejected_reward = rejected_rewards.detach().mean()
    reward_accuracy = (chosen_rewards > rejected_rewards).float().mean()

    return loss, chosen_reward, rejected_reward, reward_accuracy

14. 完成实战训练流水线

14.1 综合训练循环

import torch
import torch.nn as nn
from torch.cuda.amp import autocast, GradScaler
import wandb  # 实验跟踪

class Trainer:
    def __init__(
        self,
        model,
        train_loader,
        val_loader,
        optimizer,
        scheduler,
        criterion,
        device='cuda',
        use_amp=True,
        grad_clip=1.0,
        accumulation_steps=1,
        log_wandb=False
    ):
        self.model = model.to(device)
        self.train_loader = train_loader
        self.val_loader = val_loader
        self.optimizer = optimizer
        self.scheduler = scheduler
        self.criterion = criterion
        self.device = device
        self.use_amp = use_amp
        self.grad_clip = grad_clip
        self.accumulation_steps = accumulation_steps
        self.scaler = GradScaler() if use_amp else None
        self.log_wandb = log_wandb

        if log_wandb:
            wandb.watch(model, log='all', log_freq=100)

    def train_epoch(self):
        self.model.train()
        total_loss = 0
        self.optimizer.zero_grad()

        for step, (inputs, labels) in enumerate(self.train_loader):
            inputs, labels = inputs.to(self.device), labels.to(self.device)

            if self.use_amp:
                with autocast():
                    outputs = self.model(inputs)
                    loss = self.criterion(outputs, labels) / self.accumulation_steps
                self.scaler.scale(loss).backward()
            else:
                outputs = self.model(inputs)
                loss = self.criterion(outputs, labels) / self.accumulation_steps
                loss.backward()

            if (step + 1) % self.accumulation_steps == 0:
                if self.use_amp:
                    self.scaler.unscale_(self.optimizer)

                if self.grad_clip:
                    nn.utils.clip_grad_norm_(self.model.parameters(), self.grad_clip)

                if self.use_amp:
                    self.scaler.step(self.optimizer)
                    self.scaler.update()
                else:
                    self.optimizer.step()

                if self.scheduler:
                    self.scheduler.step()

                self.optimizer.zero_grad()

            total_loss += loss.item() * self.accumulation_steps

        return total_loss / len(self.train_loader)

    @torch.no_grad()
    def evaluate(self):
        self.model.eval()
        total_loss = 0
        correct = 0
        total = 0

        for inputs, labels in self.val_loader:
            inputs, labels = inputs.to(self.device), labels.to(self.device)

            with autocast() if self.use_amp else torch.no_grad():
                outputs = self.model(inputs)
                loss = self.criterion(outputs, labels)

            total_loss += loss.item()
            _, predicted = outputs.max(1)
            total += labels.size(0)
            correct += predicted.eq(labels).sum().item()

        return total_loss / len(self.val_loader), 100. * correct / total

    def fit(self, epochs, save_path=None):
        best_val_acc = 0
        early_stopping = EarlyStopping(patience=10)

        for epoch in range(epochs):
            train_loss = self.train_epoch()
            val_loss, val_acc = self.evaluate()

            print(f"Epoch {epoch+1}/{epochs}: "
                  f"Train Loss: {train_loss:.4f}, "
                  f"Val Loss: {val_loss:.4f}, "
                  f"Val Acc: {val_acc:.2f}%")

            if self.log_wandb:
                wandb.log({
                    'train_loss': train_loss,
                    'val_loss': val_loss,
                    'val_acc': val_acc,
                    'lr': self.optimizer.param_groups[0]['lr']
                })

            if val_acc > best_val_acc:
                best_val_acc = val_acc
                if save_path:
                    torch.save(self.model.state_dict(), save_path)

            early_stopping(val_loss, self.model)
            if early_stopping.early_stop:
                print("Early stopping!")
                break

        return best_val_acc

结论与最佳实践

关于深度学习训练需要考虑的核心原则,整理如下。

优化器选择

  • 常规任务:AdamW (lr=1e-3 ~ 1e-4, weight_decay=0.01)
  • Transformer:AdamW + Warmup + Cosine Schedule
  • 大规模批量:LAMB 或 LARS
  • 内存受限:Lion

正则化策略

  • Dropout 通常取 0.1 ~ 0.5
  • 小数据集:使用较强的正则化 (更大的 weight decay, 更高的 dropout)
  • 大规模数据:使用较弱的正则化或不使用

学习率调度

  • CNN:OneCycleLR 或 Step Decay
  • Transformer:Warmup + Cosine 或 Inverse Square Root

混合精度

  • 始终使用 AMP (速度 1.5~3 倍, 内存节省 2 倍)
  • A100/H100 及以上:推荐 BF16
  • 较旧的 GPU:FP16 + Loss Scaling

分布式训练

  • 单机多 GPU:DDP + NCCL
  • 数十亿参数模型:FSDP
  • 始终用 Gradient Accumulation 来提高有效批量大小

参考资料