深度学习不是什么神秘的黑科技,它本质上是一个数学函数拟合器——给它大量输入输出数据,它自动找出它们之间的复杂关系。

举个例子:你给它一百万张猫的图片,每张图片对应"猫"这个标签。深度学习自动找出什么样的像素组合看起来像猫。

再比如:你给它十亿句人类语言,每句话的下一个词作为目标。深度学习自动学会给定上文,下一个词最可能是什么。

深度学习的核心优势是:随着数据量增加,它的效果持续提升。这是传统机器学习做不到的。

在这一章,我们会从零开始,一步步拆解深度学习的每个组件:

  • 神经网络的数学基础
  • 前向传播的计算过程
  • 激活函数的作用
  • 损失函数的设计
  • 反向传播的原理
  • 优化器的选择
  • 正则化技术
  • 归一化方法
  • 学习率调度

最后,我们用 PyTorch 实现一个完整的多层感知机(MLP),把所有知识点串联起来。

这一章会涉及一些数学,但不用担心——我们会用直觉解释每一个公式,而不是让你死记硬背。重点是理解为什么这样设计,而不是怎么计算。


神经网络数学基础

深度学习建立在三个数学分支之上:线性代数、微积分、概率论。

你不需要成为数学专家,但需要理解几个核心概念。

线性代数:矩阵乘法

神经网络最基本的运算就是矩阵乘法。

为什么用矩阵?因为它能简洁地表示"多个输入经过多个神经元变换"这个过程。

先看一个直观的例子:

实例

# ============================================
# 矩阵乘法的直观理解
# ============================================

import numpy as np

# 假设输入是一个 3 维向量:[身高, 体重, 年龄]
# 单位分别是:厘米、公斤、岁
x = np.array([175, 70, 25])  # 输入向量,形状 (3,)
print(f"输入 x: {x}")
print(f"输入形状: {x.shape}")

# 权重矩阵 W:2 个神经元,每个神经元接收 3 个输入
# 形状是 (输出维度, 输入维度) = (2, 3)
W = np.array([
    [0.1, 0.2, 0.3],   # 第 1 个神经元的权重
    [0.4, 0.5, 0.6],   # 第 2 个神经元的权重
])
print(f"\n权重矩阵 W:\n{W}")
print(f"权重形状: {W.shape}")

# 偏置 b:每个神经元一个偏置
b = np.array([0.1, 0.2])  # 形状 (2,)
print(f"\n偏置 b: {b}")
print(f"偏置形状: {b.shape}")

# 矩阵乘法:y = W · x + b
# 注意:numpy 的 @ 运算符表示矩阵乘法
y = W @ x + b
print(f"\n输出 y: {y}")
print(f"输出形状: {y.shape}")

# 手动计算验证一下
y1 = 0.1 * 175 + 0.2 * 70 + 0.3 * 25 + 0.1  # 第 1 个神经元
y2 = 0.4 * 175 + 0.5 * 70 + 0.6 * 25 + 0.2  # 第 2 个神经元
print(f"\n手动计算验证: [{y1}, {y2}]")

矩阵乘法的几何意义是"线性变换"——它可以旋转、缩放、拉伸向量空间。

但只有线性变换是不够的。

如果每一层都只是矩阵乘法,那么无论网络有多深,整个网络等价于一个单层网络。因为多个线性变换的组合仍然是线性变换。

这就是为什么我们需要激活函数——引入非线性。

微积分:导数与链式法则

训练神经网络的核心是"梯度下降"——找到让损失最小的参数方向。

梯度就是"多元函数的导数"——它告诉我们,每个参数变化一点,损失会变化多少。

先看一个简单的例子:

实例

# ============================================
# 导数的直观理解
# ============================================

def f(x):
    """一个简单的函数:f(x) = x²"""
    return x ** 2

def numerical_derivative(f, x, h=1e-6):
    """数值导数:用微小增量 h 近似计算导数
    导数定义:f'(x) = lim(h→0) [f(x+h) - f(x)] / h
    """
    return (f(x + h) - f(x)) / h

# 在 x=3 处计算导数
x = 3.0
df_dx = numerical_derivative(f, x)
print(f"f({x}) = {f(x)}")
print(f"f'({x}) ≈ {df_dx}")
print(f"解析解(精确值):2 * {x} = {2 * x}")

# 理解导数的意义:
# 导数 6 表示:在 x=3 处,x 增加 1,f(x) 大约增加 6
x_new = x + 0.01
f_new = f(x_new)
print(f"\nx 从 {x} 增加到 {x_new}")
print(f"f(x) 从 {f(x)} 变为 {f_new}")
print(f"实际增加:{f_new - f(x)}")
print(f"导数预测:{df_dx * 0.01}")

对于多元函数,我们需要计算每个变量的偏导数,然后把它们组合成一个向量,这就是梯度。

链式法则是反向传播的数学基础。它让我们能够"层层拆解"复合函数的导数。

实例

# ============================================
# 链式法则的直观理解
# ============================================

# 考虑复合函数:y = f(g(x))
# 其中:g(x) = x², f(z) = z³
# 那么:y = (x²)³ = x⁶

def g(x):
    return x ** 2

def f(z):
    return z ** 3

def y(x):
    return f(g(x))

# 手动计算导数(链式法则):
# dy/dx = df/dz * dz/dx = 3*z² * 2*x = 3*(x²)² * 2*x = 6*x⁵

x = 2.0
z = g(x)  # z = 4
dy_dx_chain = 3 * (z ** 2) * 2 * x  # 链式法则计算
print(f"链式法则计算:dy/dx = {dy_dx_chain}")

# 数值导数验证
def numerical_derivative_y(x, h=1e-6):
    return (y(x + h) - y(x)) / h

dy_dx_numerical = numerical_derivative_y(x)
print(f"数值导数验证:dy/dx ≈ {dy_dx_numerical}")

# 解析解:6*x^5 = 6*32 = 192
print(f"解析解:6 * {x}^5 = {6 * (x ** 5)}")

链式法则的核心思想是:把复杂函数拆成简单函数,分别求导再相乘。

反向传播就是链式法则在神经网络中的应用——从输出开始,一层一层往回传梯度。

概率论:条件概率

分类问题中,神经网络输出的往往是"概率分布"——给定输入,每个类别的概率是多少。

条件概率 P(Y|X) 表示"在已知 X 发生的情况下,Y 发生的概率"。

举个例子:P(下雨|乌云密布) 表示"看到乌云密布时,下雨的概率"。

在深度学习中,我们用神经网络来估计这个条件概率:

实例

# ============================================
# 用神经网络输出概率分布
# ============================================

import numpy as np

def softmax(x):
    """Softmax 函数:把任意实数转换成概率分布
    输出所有值的和为 1,每个值在 [0, 1] 之间
    """
    # 减去最大值防止数值溢出
    exp_x = np.exp(x - np.max(x))
    return exp_x / np.sum(exp_x)

# 假设神经网络最后一层输出了 3 个"分数"(logits)
# 分别对应"猫"、"狗"、"鸟"
logits = np.array([2.0, 1.0, 0.5])
print(f"神经网络输出(logits):{logits}")

# 用 Softmax 转换成概率
probabilities = softmax(logits)
print(f"概率分布:{probabilities}")
print(f"概率和:{np.sum(probabilities)}")

# 解读:
# P(猫|输入图片) ≈ 0.67
# P(狗|输入图片) ≈ 0.24
# P(鸟|输入图片) ≈ 0.09
print("\n类别概率:")
print(f"  猫: {probabilities[0]:.2%}")
print(f"  狗: {probabilities[1]:.2%}")
print(f"  鸟: {probabilities[2]:.2%}")

三个数学基础的核心要点:

数学分支 核心概念 在深度学习中的用途
线性代数 矩阵乘法、向量、张量 表示网络结构,高效计算前向传播
微积分 导数、链式法则、梯度 反向传播,更新参数
概率论 条件概率、概率分布 建模不确定性,设计损失函数

前向传播

前向传播就是输入数据"流过"神经网络,从输入层到隐藏层再到输出层的计算过程。

计算图

把神经网络表示成计算图,能让前向传播和反向传播的逻辑更清晰。

计算图由节点(运算)和边(数据流)组成。

一个简单的两层神经网络的计算图:

实例

# ============================================
# 用计算图理解前向传播
# ============================================

import numpy as np

def relu(x):
    """ReLU 激活函数:max(0, x)"""
    return np.maximum(0, x)

# 一个简单的两层神经网络
# 输入层 (2) → 隐藏层 (3) → 输出层 (2)

# 输入:x = [x1, x2]
x = np.array([1.0, 2.0])
print(f"输入 x: {x}")

# 第一层:输入 → 隐藏层
W1 = np.array([
    [0.1, 0.2],  # 第 1 个隐藏神经元
    [0.3, 0.4],  # 第 2 个隐藏神经元
    [0.5, 0.6],  # 第 3 个隐藏神经元
])
b1 = np.array([0.1, 0.2, 0.3])

# 第二层:隐藏层 → 输出层
W2 = np.array([
    [0.1, 0.2, 0.3],  # 第 1 个输出神经元
    [0.4, 0.5, 0.6],  # 第 2 个输出神经元
])
b2 = np.array([0.1, 0.2])

# 前向传播计算图
# 步骤 1: 隐藏层线性变换
z1 = W1 @ x + b1
print(f"\n隐藏层线性变换 z1: {z1}")

# 步骤 2: 隐藏层激活函数
a1 = relu(z1)
print(f"隐藏层激活后 a1: {a1}")

# 步骤 3: 输出层线性变换
z2 = W2 @ a1 + b2
print(f"输出层线性变换 z2: {z2}")

# 步骤 4: 输出层激活(如果是分类问题,通常用 Softmax)
a2 = softmax(z2)  # 使用前面定义的 softmax 函数
print(f"输出层激活后 a2: {a2}")

计算图的好处是:每一步都很清晰,反向传播时只要按照相反的顺序计算梯度即可。

矩阵乘法视角下的神经网络

从矩阵乘法的角度看,神经网络就是一连串的"线性变换 + 非线性激活"。

每一层可以抽象成:

aᵢ = activation(Wᵢ · aᵢ₋₁ + bᵢ)

其中 a₀ 就是输入 x。

这个公式简洁但强大——它能表示从简单的逻辑回归到复杂的 Transformer 的几乎所有神经网络结构。

注意矩阵乘法的维度匹配:如果输入向量是 n 维,下一层有 m 个神经元,那么权重矩阵 W 的形状一定是 m×n。这样 W·x 之后得到的是 m 维向量。

激活函数的作用

前面提到过:没有激活函数,深层网络等价于单层网络。

用一个简单的例子证明这一点:

实例

# ============================================
# 为什么需要激活函数
# ============================================

import numpy as np

# 假设有一个两层网络,但都没有激活函数

# 输入
x = np.array([1.0, 2.0])

# 第一层权重和偏置
W1 = np.array([[0.1, 0.2], [0.3, 0.4]])
b1 = np.array([0.1, 0.2])

# 第二层权重和偏置
W2 = np.array([[0.5, 0.6], [0.7, 0.8]])
b2 = np.array([0.3, 0.4])

# 两层分别计算
z1 = W1 @ x + b1
z2 = W2 @ z1 + b2
print(f"两层分别计算结果: {z2}")

# 合并成一层计算
# 数学上可以证明:z2 = (W2·W1)·x + (W2·b1 + b2)
W_combined = W2 @ W1
b_combined = W2 @ b1 + b2
z_combined = W_combined @ x + b_combined
print(f"合并成一层计算结果: {z_combined}")

# 结果完全一样!
print(f"\n两个结果相等: {np.allclose(z2, z_combined)}")
print("结论:没有激活函数,深层网络 = 单层网络")

激活函数就是那个让深层网络有意义的关键组件——它引入非线性,让网络能够学习复杂的模式。


激活函数详解

激活函数决定了一个神经元的输出如何响应输入。

好的激活函数应该满足几个条件:

  • 非线性:这是必须的
  • 可微:这样才能计算梯度
  • 计算简单:前向和反向传播都要快
  • 不会饱和:梯度不会消失或爆炸

我们来看看最常用的几个激活函数。

Sigmoid:饱和与梯度消失

Sigmoid 是最早使用的激活函数之一。它把任意实数压缩到 (0, 1) 之间。

公式:σ(x) = 1 / (1 + e⁻ˣ)

实例

# ============================================
# Sigmoid 激活函数
# ============================================

import numpy as np
import matplotlib.pyplot as plt

def sigmoid(x):
    """Sigmoid 函数"""
    return 1 / (1 + np.exp(-x))

def sigmoid_derivative(x):
    """Sigmoid 的导数:σ'(x) = σ(x) * (1 - σ(x))"""
    s = sigmoid(x)
    return s * (1 - s)

# 测试一些值
x_values = [-10, -5, -2, 0, 2, 5, 10]
print("x    | sigmoid(x) | sigmoid'(x)")
print("-" * 40)
for x in x_values:
    s = sigmoid(x)
    d = sigmoid_derivative(x)
    print(f"{x:4} | {s:10.6f} | {d:12.6f}")

# 观察:当 x 很大或很小时,导数接近 0
# 这就是"梯度消失"问题
x_big = 10.0
print(f"\n当 x = {x_big} 时:")
print(f"  sigmoid(x) = {sigmoid(x_big):.10f} (几乎是 1)")
print(f"  sigmoid'(x) = {sigmoid_derivative(x_big):.10f} (几乎是 0)")
print("梯度消失了!反向传播时梯度传不回去。")

Sigmoid 的问题很明显:

  • 当 |x| > 6 时,梯度几乎为 0,梯度消失
  • 输出不是以 0 为中心,这会影响优化
  • 计算指数比较慢

由于这些问题,Sigmoid 在现代深度学习中已经很少在隐藏层使用了。

ReLU 与变体

ReLU(Rectified Linear Unit)是现在最常用的激活函数。

公式:ReLU(x) = max(0, x)

简单但非常有效。

实例

# ============================================
# ReLU 及其变体
# ============================================

import numpy as np

def relu(x):
    """ReLU:max(0, x)"""
    return np.maximum(0, x)

def relu_derivative(x):
    """ReLU 的导数:x > 0 时为 1,否则为 0"""
    return (x > 0).astype(float)

def leaky_relu(x, alpha=0.01):
    """Leaky ReLU:x > 0 时为 x,否则为 alpha*x"""
    return np.where(x > 0, x, alpha * x)

def gelu(x):
    """GELU:Gaussian Error Linear Unit
    这是 Transformer 中常用的激活函数
    近似公式:0.5 * x * (1 + tanh(sqrt(2/pi) * (x + 0.044715*x^3)))
    """
    cdf = 0.5 * (1.0 + np.tanh(np.sqrt(2 / np.pi) * (x + 0.044715 * np.power(x, 3))))
    return x * cdf

# 测试
x_values = [-5, -2, -1, 0, 1, 2, 5]
print("x    | ReLU | Leaky | GELU")
print("-" * 45)
for x in x_values:
    r = relu(x)
    lr = leaky_relu(x)
    g = gelu(x)
    print(f"{x:4} | {r:4.2f} | {lr:5.2f} | {g:5.2f}")

# ReLU 的优势:
print("\nReLU 的优势:")
print("1. 计算简单:只需要 max 操作")
print("2. 不会饱和(正区间):梯度始终是 1")
print("3. 稀疏性:部分神经元输出为 0,增加模型鲁棒性")

# ReLU 的问题:"死亡 ReLU"
print("\nReLU 的问题:死亡 ReLU")
print("如果一个神经元的输入总是负的,那么:")
print("  - 输出始终为 0")
print("  - 梯度始终为 0")
print("  - 参数永远不会更新")
print("  - 这个神经元就'死'了")

几种常见激活函数的对比:

激活函数 公式 优点 缺点 适用场景
Sigmoid 1/(1+e⁻ˣ) 输出在 (0,1),可解释为概率 梯度消失、非零中心、计算慢 二分类输出层(已很少用于隐藏层)
ReLU max(0, x) 计算快、不饱和、稀疏性 死亡 ReLU、输出非零中心 大多数隐藏层(默认选择)
Leaky ReLU max(αx, x) 解决死亡 ReLU 问题 α 需要手动选择 ReLU 失效时的替代
GELU x·Φ(x) 平滑、在 Transformer 中效果好 计算稍复杂 Transformer、BERT、GPT 等
Swish/SwiGLU x·σ(βx) 在深层网络中优于 ReLU 计算复杂 PaLM、LLaMA 等大模型

选择原则

激活函数的选择建议:

  • 隐藏层:优先用 ReLU,简单有效
  • 如果遇到死亡 ReLU:试试 Leaky ReLU 或 GELU
  • Transformer:GELU 或 SwiGLU 是标准选择
  • 二分类输出层:Sigmoid(输出概率)
  • 多分类输出层:Softmax(输出概率分布)
  • 回归输出层:不加激活(输出任意实数)

不要过度纠结激活函数的选择。通常 ReLU 就足够好了。当你确认 ReLU 是瓶颈时,再换其他的也不迟。


损失函数

损失函数衡量"模型的预测"和"真实答案"之间的差距。

训练的目标就是让这个差距尽可能小。

交叉熵损失(分类)

分类问题最常用的损失函数是交叉熵损失(Cross-Entropy Loss)。

它的直观含义是:"模型预测的概率分布和真实分布之间有多大差异"。

实例

# ============================================
# 交叉熵损失
# ============================================

import numpy as np

def cross_entropy_loss(probs, target_one_hot):
    """交叉熵损失
    probs: 模型预测的概率分布,形状 (batch_size, num_classes)
    target_one_hot: 真实标签的 one-hot 编码,形状同上
    """
    # 加上 epsilon 防止 log(0)
    epsilon = 1e-12
    probs = np.clip(probs, epsilon, 1.0 - epsilon)
    # 交叉熵 = -sum(target * log(pred))
    return -np.sum(target_one_hot * np.log(probs)) / len(probs)

def softmax(x):
    """Softmax 函数"""
    exp_x = np.exp(x - np.max(x, axis=-1, keepdims=True))
    return exp_x / np.sum(exp_x, axis=-1, keepdims=True)

# 例子:三分类问题,类别是"猫"、"狗"、"鸟"

# 假设真实标签是"猫"(索引 0),one-hot 编码
target = np.array([[1, 0, 0]])  # 正确答案是第 0 类

# 场景 1:模型很自信地预测"猫"
logits1 = np.array([[5.0, 0.5, 0.1]])  # 猫的分数最高
probs1 = softmax(logits1)
loss1 = cross_entropy_loss(probs1, target)
print("场景 1:模型自信预测正确")
print(f"  概率分布: {probs1[0]}")
print(f"  损失: {loss1:.6f} (很小)")

# 场景 2:模型不确定
logits2 = np.array([[1.0, 0.9, 0.8]])  # 三个类别分数差不多
probs2 = softmax(logits2)
loss2 = cross_entropy_loss(probs2, target)
print("\n场景 2:模型不确定")
print(f"  概率分布: {probs2[0]}")
print(f"  损失: {loss2:.6f} (中等)")

# 场景 3:模型预测错误
logits3 = np.array([[0.1, 5.0, 0.5]])  # 狗的分数最高
probs3 = softmax(logits3)
loss3 = cross_entropy_loss(probs3, target)
print("\n场景 3:模型预测错误")
print(f"  概率分布: {probs3[0]}")
print(f"  损失: {loss3:.6f} (很大)")

交叉熵损失的一个简化版本:当标签是类别索引而不是 one-hot 编码时,可以用更高效的计算方式:

loss = -log(probs[target_class])

这就是为什么在 PyTorch 中,CrossEntropyLoss 直接接受类别索引作为标签。

MSE 损失(回归)

回归问题(预测连续值)最常用的损失是均方误差(Mean Squared Error,MSE)。

公式:MSE = mean((pred - target)²)

实例

# ============================================
# MSE 损失
# ============================================

import numpy as np

def mse_loss(pred, target):
    """均方误差损失"""
    return np.mean((pred - target) ** 2)

def mae_loss(pred, target):
    """平均绝对误差损失"""
    return np.mean(np.abs(pred - target))

# 例子:预测房价(单位:万元)

# 真实房价
target = np.array([100, 200, 150])

# 场景 1:预测很准
pred1 = np.array([102, 195, 148])
mse1 = mse_loss(pred1, target)
mae1 = mae_loss(pred1, target)
print("场景 1:预测准确")
print(f"  预测: {pred1}")
print(f"  真实: {target}")
print(f"  MSE: {mse1:.2f}")
print(f"  MAE: {mae1:.2f}")

# 场景 2:预测有一个大误差
pred2 = np.array([102, 280, 148])  # 第二个房子预测高了 80 万
mse2 = mse_loss(pred2, target)
mae2 = mae_loss(pred2, target)
print("\n场景 2:有一个大误差")
print(f"  预测: {pred2}")
print(f"  真实: {target}")
print(f"  MSE: {mse2:.2f} (被大误差放大很多)")
print(f"  MAE: {mae2:.2f}")

# MSE vs MAE
print("\nMSE vs MAE:")
print("- MSE 对大误差更敏感(因为平方)")
print("- MAE 对异常值更鲁棒")
print("- 一般情况下用 MSE 更多")

语言模型的预测下一词损失

大语言模型(如 GPT)的训练目标很简单:给定上文,预测下一个词。

这本质上是一个多分类问题——词汇表中的每个词都是一个类别。

实例

# ============================================
# 语言模型的损失函数
# ============================================

import numpy as np

def language_model_loss(logits, targets, vocab_size):
    """语言模型损失
    本质上是对每个位置的交叉熵损失
    """
    batch_size, seq_len, _ = logits.shape
    total_loss = 0.0

    # 对序列中的每个位置计算损失
    for i in range(seq_len):
        # 这个位置的预测分数
        step_logits = logits[:, i, :]
        # 转换成概率
        step_probs = softmax(step_logits)
        # 目标词的索引
        step_target = targets[:, i]
        # 只取目标词的概率
        for b in range(batch_size):
            target_idx = step_target[b]
            target_prob = step_probs[b, target_idx]
            # 损失 = -log(p)
            total_loss += -np.log(target_prob + 1e-12)

    return total_loss / (batch_size * seq_len)

# 一个简单的例子
vocab_size = 10000  # 假设词汇表大小是 1 万
batch_size = 2
seq_len = 3

# 模型输出:(batch_size, seq_len, vocab_size)
logits = np.random.randn(batch_size, seq_len, vocab_size)

# 真实目标:每个位置应该预测的词索引
targets = np.array([
    [123, 456, 789],   # 第一个序列的目标词
    [234, 567, 890],   # 第二个序列的目标词
])

loss = language_model_loss(logits, targets, vocab_size)
print(f"语言模型损失: {loss:.4f}")
print("\n这个损失的含义:")
print("- 对序列中的每个位置")
print("- 预测下一个词")
print("- 计算所有位置的平均交叉熵")

常用损失函数总结:

任务类型 损失函数 输出层激活 标签格式
二分类 Binary Cross-Entropy Sigmoid 0 或 1
多分类 Cross-Entropy Softmax 类别索引
多标签分类 Binary Cross-Entropy Sigmoid 多个 0/1
回归 MSE / MAE 连续值
语言模型 Cross-Entropy Softmax 词索引

反向传播

反向传播(Backpropagation)是训练神经网络的核心算法。

它的目标是:计算损失函数对每个参数的梯度,然后用梯度下降更新参数。

链式法则推导

我们用一个简单的例子,一步步推导反向传播。

实例

# ============================================
# 手动实现反向传播
# ============================================

import numpy as np

def relu(x):
    return np.maximum(0, x)

def relu_derivative(x):
    return (x > 0).astype(float)

def mse_loss(pred, target):
    return np.mean((pred - target) ** 2)

# 一个最简单的神经网络:一层线性变换
# y = w * x + b
# 损失 = (y - target)^2

# 输入和目标
x = np.array(2.0)
target = np.array(7.0)

# 初始化参数
w = np.array(1.0)
b = np.array(0.0)

print(f"初始参数: w = {w}, b = {b}")
print(f"目标: {target}")

# ============================================
# 第一步:前向传播
# ============================================
y = w * x + b
loss = (y - target) ** 2
print(f"\n前向传播:")
print(f"  y = {y}")
print(f"  loss = {loss}")

# ============================================
# 第二步:反向传播(手动计算梯度)
# ============================================
# 我们需要计算:d_loss/d_w, d_loss/d_b

# 用链式法则一步步来:
# loss = (y - target)^2
# d_loss/d_y = 2 * (y - target)
d_loss_d_y = 2 * (y - target)

# y = w * x + b
# d_y/d_w = x
# d_y/d_b = 1
d_y_d_w = x
d_y_d_b = 1.0

# 链式法则:
# d_loss/d_w = d_loss/d_y * d_y/d_w
# d_loss/d_b = d_loss/d_y * d_y/d_b
d_loss_d_w = d_loss_d_y * d_y_d_w
d_loss_d_b = d_loss_d_y * d_y_d_b

print(f"\n反向传播:")
print(f"  d_loss/d_y = {d_loss_d_y}")
print(f"  d_loss/d_w = {d_loss_d_w}")
print(f"  d_loss/d_b = {d_loss_d_b}")

# ============================================
# 第三步:梯度下降更新参数
# ============================================
learning_rate = 0.1
w_new = w - learning_rate * d_loss_d_w
b_new = b - learning_rate * d_loss_d_b

print(f"\n参数更新:")
print(f"  w: {w} -> {w_new}")
print(f"  b: {b} -> {b_new}")

# 验证:用新参数前向传播
y_new = w_new * x + b_new
loss_new = (y_new - target) ** 2
print(f"\n验证:")
print(f"  旧预测: {y}, 旧损失: {loss}")
print(f"  新预测: {y_new}, 新损失: {loss_new}")
print(f"  损失下降了!")

这个例子虽然简单,但包含了反向传播的所有核心思想:

  1. 前向传播:计算所有中间变量
  2. 反向传播:从输出开始,用链式法则层层计算梯度
  3. 参数更新:用梯度下降更新参数

计算图中的梯度流动

对于多层神经网络,梯度在计算图中反向流动。

每一层的梯度依赖于后一层传来的梯度。

实例

# ============================================
# 两层网络的反向传播
# ============================================

import numpy as np

def relu(x):
    return np.maximum(0, x)

def relu_derivative(x):
    return (x > 0).astype(float)

# 一个两层网络
# 输入 (2) → 隐藏层 (3) → 输出 (1)

# 数据
x = np.array([1.0, 2.0])  # 输入
target = np.array([5.0])  # 目标输出

# 参数初始化
W1 = np.array([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])  # (3, 2)
b1 = np.array([0.1, 0.2, 0.3])  # (3,)
W2 = np.array([[0.7, 0.8, 0.9]])  # (1, 3)
b2 = np.array([0.4])  # (1,)

print("初始参数已设置")

# ============================================
# 前向传播
# ============================================
print("\n=== 前向传播 ===")

# 第一层
z1 = W1 @ x + b1  # (3,)
a1 = relu(z1)     # (3,)
print(f"z1 = {z1}")
print(f"a1 = {a1}")

# 第二层
z2 = W2 @ a1 + b2  # (1,)
a2 = z2  # 回归任务,输出层不加激活
print(f"z2 = {z2}")
print(f"a2 = {a2}")

# 损失
loss = np.mean((a2 - target) ** 2)
print(f"loss = {loss}")

# ============================================
# 反向传播
# ============================================
print("\n=== 反向传播 ===")

# 输出层梯度
d_loss_d_a2 = 2 * (a2 - target)  # (1,)
d_a2_d_z2 = 1.0  # 没有激活函数
d_loss_d_z2 = d_loss_d_a2 * d_a2_d_z2  # (1,)
print(f"d_loss_d_z2 = {d_loss_d_z2}")

# 第二层参数梯度
d_z2_d_W2 = a1  # (3,),因为 z2 = W2@a1 + b2
d_z2_d_b2 = 1.0  # (1,)
d_z2_d_a1 = W2[0]  # (3,)

# 计算参数梯度
d_loss_d_W2 = d_loss_d_z2.reshape(-1, 1) @ d_z2_d_W2.reshape(1, -1)  # (1, 3)
d_loss_d_b2 = d_loss_d_z2 * d_z2_d_b2  # (1,)
print(f"d_loss_d_W2 = {d_loss_d_W2}")
print(f"d_loss_d_b2 = {d_loss_d_b2}")

# 传到隐藏层的梯度
d_loss_d_a1 = d_loss_d_z2 @ d_z2_d_a1  # (3,)
print(f"d_loss_d_a1 = {d_loss_d_a1}")

# 隐藏层梯度
d_a1_d_z1 = relu_derivative(z1)  # (3,)
d_loss_d_z1 = d_loss_d_a1 * d_a1_d_z1  # (3,)
print(f"d_loss_d_z1 = {d_loss_d_z1}")

# 第一层参数梯度
d_z1_d_W1 = x  # (2,)
d_z1_d_b1 = 1.0  # (3,)

# 计算参数梯度
d_loss_d_W1 = d_loss_d_z1.reshape(-1, 1) @ d_z1_d_W1.reshape(1, -1)  # (3, 2)
d_loss_d_b1 = d_loss_d_z1 * d_z1_d_b1  # (3,)
print(f"d_loss_d_W1 = {d_loss_d_W1}")
print(f"d_loss_d_b1 = {d_loss_d_b1}")

# ============================================
# 参数更新
# ============================================
print("\n=== 参数更新 ===")
learning_rate = 0.01

W1_new = W1 - learning_rate * d_loss_d_W1
b1_new = b1 - learning_rate * d_loss_d_b1
W2_new = W2 - learning_rate * d_loss_d_W2
b2_new = b2 - learning_rate * d_loss_d_b2

print("参数已更新")

这个例子展示了反向传播的完整流程。

核心规律是:

  • 对于线性层 y = Wx + b:dL/dW = (dL/dy)·xᵀ,dL/db = dL/dy
  • 对于激活函数 y = f(z):dL/dz = dL/dy ⊙ f'(z)(⊙ 是逐元素相乘)
  • 梯度从后往前传,每一层都用链式法则"积累"梯度

好消息是:现代深度学习框架(如 PyTorch、TensorFlow)会自动为你计算反向传播。你只需要定义前向传播,框架会用自动微分(autograd)搞定所有梯度计算。


梯度下降优化器

计算出梯度后,我们需要用优化器来更新参数。

最简单的优化器是随机梯度下降(SGD),但还有很多更高级的优化器。

SGD:随机梯度下降

公式:θ = θ - η·∇L(θ)

其中 η 是学习率。

实例

# ============================================
# SGD 优化器
# ============================================

import numpy as np

def f(x):
    """我们要最小化的函数:f(x) = x²"""
    return x ** 2

def f_grad(x):
    """梯度:f'(x) = 2x"""
    return 2 * x

# SGD 优化
x = 10.0  # 初始值
learning_rate = 0.1  # 学习率
steps = 20

print(f"初始 x = {x}, f(x) = {f(x)}")
print("-" * 40)

for step in range(steps):
    grad = f_grad(x)
    x = x - learning_rate * grad  # SGD 更新
    print(f"Step {step+1}: x = {x:.6f}, f(x) = {f(x):.6f}")

print("-" * 40)
print(f"最终 x = {x:.6f}, f(x) = {f(x):.6f}")
print("理论最优:x = 0, f(x) = 0")

SGD 简单但有缺点:

  • 对所有参数使用相同的学习率
  • 收敛可能很慢
  • 容易卡在局部最优或鞍点
  • 对噪声敏感

Momentum:动量加速

Momentum 引入了"速度"的概念——让梯度更新带有惯性。

公式:

v = β·v + (1-β)·∇L(θ)
θ = θ - η·v

β 通常设为 0.9。

实例

# ============================================
# Momentum 优化器
# ============================================

import numpy as np

def f(x):
    """目标函数"""
    return x ** 2

def f_grad(x):
    """梯度"""
    return 2 * x

def sgd_optimize(x_init, learning_rate, steps):
    """纯 SGD"""
    x = x_init
    history = [x]
    for _ in range(steps):
        grad = f_grad(x)
        x = x - learning_rate * grad
        history.append(x)
    return np.array(history)

def momentum_optimize(x_init, learning_rate, beta, steps):
    """Momentum"""
    x = x_init
    v = 0.0  # 初始速度
    history = [x]
    for _ in range(steps):
        grad = f_grad(x)
        v = beta * v + (1 - beta) * grad
        x = x - learning_rate * v
        history.append(x)
    return np.array(history)

# 对比
x_init = 10.0
learning_rate = 0.1
beta = 0.9
steps = 20

sgd_history = sgd_optimize(x_init, learning_rate, steps)
momentum_history = momentum_optimize(x_init, learning_rate, beta, steps)

print("Step |    SGD    |  Momentum")
print("-" * 35)
for i in range(steps + 1):
    print(f"{i:4} | {sgd_history[i]:9.6f} | {momentum_history[i]:9.6f}")

print("\n观察:Momentum 收敛更快!")
print("因为它累积了过去的梯度,有惯性。")

Adam:自适应学习率

Adam(Adaptive Moment Estimation)是目前最常用的优化器之一。

它结合了 Momentum(一阶矩)和 RMSProp(二阶矩)的思想,为每个参数维护自适应的学习率。

公式:

m = β₁·m + (1-β₁)·∇L(θ)
v = β₂·v + (1-β₂)·(∇L(θ))²
m̂ = m / (1-β₁ᵗ)
v̂ = v / (1-β₂ᵗ)
θ = θ - η·m̂ / (√v̂ + ε)

超参数默认值:β₁=0.9, β₂=0.999, ε=1e-8, η=1e-3。

实例

# ============================================
# Adam 优化器
# ============================================

import numpy as np

def adam_optimize(x_init, learning_rate, beta1, beta2, epsilon, steps):
    """Adam 优化"""
    x = x_init
    m = 0.0  # 一阶矩
    v = 0.0  # 二阶矩
    history = [x]

    for t in range(1, steps + 1):
        grad = 2 * x  # f(x) = x² 的梯度
        # 更新一阶和二阶矩
        m = beta1 * m + (1 - beta1) * grad
        v = beta2 * v + (1 - beta2) * (grad ** 2)
        # 偏差修正
        m_hat = m / (1 - beta1 ** t)
        v_hat = v / (1 - beta2 ** t)
        # 参数更新
        x = x - learning_rate * m_hat / (np.sqrt(v_hat) + epsilon)
        history.append(x)

    return np.array(history)

# 参数
x_init = 10.0
learning_rate = 0.5
beta1 = 0.9
beta2 = 0.999
epsilon = 1e-8
steps = 20

# 优化
history = adam_optimize(x_init, learning_rate, beta1, beta2, epsilon, steps)

print("Adam 优化过程:")
print("Step |     x     |   f(x)")
print("-" * 35)
for i in range(steps + 1):
    print(f"{i:4} | {history[i]:9.6f} | {history[i]**2:9.6f}")

AdamW:权重衰减修正

AdamW 是 Adam 的一个改进版本——它把权重衰减(Weight Decay)从梯度中分离出来。

这在正则化中很有用。

优化器对比:

优化器 公式 优点 缺点 适用场景
SGD θ = θ - η·∇L 简单、稳定 收敛慢、对学习率敏感 数据量大、调参经验丰富
SGD + Momentum v = βv + (1-β)∇L
θ = θ - ηv
加速收敛、减少振荡 需要调 β 一般比纯 SGD 好
Adam 自适应学习率 收敛快、需要较少调参 可能泛化不如 SGD 大多数场景(默认选择)
AdamW Adam + 权重衰减 更好的正则化 需要调权重衰减系数 Transformer、大模型

优化器选择建议:先试 Adam,学习率设为 1e-3(3e-4 是更稳妥的选择)。如果效果不满意,再尝试调参或换其他优化器。


正则化技术

正则化的目标是防止过拟合——让模型在未见过的数据上也能表现良好。

L1/L2 正则化

L1 和 L2 正则化是最基本的正则化方法。

它们在损失函数中加上对参数的惩罚:

L_total = L + λ·L_reg

L1 正则化(Lasso):L_reg = |w|

L2 正则化(Ridge):L_reg = w²

实例

# ============================================
# L1/L2 正则化
# ============================================

import numpy as np

def l1_regularization(weights, lambda_l1):
    """L1 正则化:返回正则化项和梯度"""
    reg = lambda_l1 * np.sum(np.abs(weights))
    grad = lambda_l1 * np.sign(weights)
    return reg, grad

def l2_regularization(weights, lambda_l2):
    """L2 正则化:返回正则化项和梯度"""
    reg = 0.5 * lambda_l2 * np.sum(weights ** 2)
    grad = lambda_l2 * weights
    return reg, grad

# 例子
weights = np.array([1.0, -2.0, 3.0, 0.5])
print(f"权重: {weights}")

lambda_l1 = 0.1
lambda_l2 = 0.1

l1_reg, l1_grad = l1_regularization(weights, lambda_l1)
print(f"\nL1 正则化项: {l1_reg:.4f}")
print(f"L1 梯度: {l1_grad}")

l2_reg, l2_grad = l2_regularization(weights, lambda_l2)
print(f"\nL2 正则化项: {l2_reg:.4f}")
print(f"L2 梯度: {l2_grad}")

print("\nL1 vs L2:")
print("- L1:倾向于产生稀疏解(很多参数变 0),可做特征选择")
print("- L2:倾向于让所有参数都很小,但不会变 0")
print("- 深度学习中 L2 更常用(或用 AdamW 的权重衰减)")

Dropout

Dropout 是一个简单但有效的正则化技术:训练时随机"关掉"一部分神经元。

这迫使模型学习更鲁棒的特征——它不能依赖任何特定的神经元。

实例

# ============================================
# Dropout
# ============================================

import numpy as np

def dropout_forward(x, dropout_rate, training=True):
    """Dropout 前向传播"""
    if not training:
        # 测试时不 dropout,直接返回
        return x

    # 生成 mask:随机选择一部分神经元置 0
    mask = (np.random.rand(*x.shape) > dropout_rate).astype(float)
    # 缩放:测试时不需要缩放,所以训练时要除以 (1-dropout_rate)
    scale = 1.0 / (1.0 - dropout_rate)
    # 应用 dropout
    out = x * mask * scale

    return out, mask

# 测试
x = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
dropout_rate = 0.4

print(f"输入 x: {x}")
print(f"Dropout 率: {dropout_rate}")

# 多次运行,观察随机性
print("\n多次 Dropout 结果:")
for i in range(3):
    out, mask = dropout_forward(x, dropout_rate, training=True)
    print(f"  第 {i+1} 次: mask={mask}, out={out}")

# 测试模式
out_test = dropout_forward(x, dropout_rate, training=False)
print(f"\n测试模式(不 Dropout): {out_test}")

数据增强

数据增强是最有效的正则化方法之一——通过变换现有数据来创造更多训练样本。

对于图像,常用的数据增强有:翻转、旋转、缩放、裁剪、颜色抖动等。

对于文本,常用的数据增强有:同义词替换、随机插入、随机删除、回译等。

正则化技术总结:

方法 原理 优点 缺点
L2 正则化 惩罚大的权重 简单、稳定 效果有限
Dropout 随机关掉神经元 简单有效 减慢训练
数据增强 增加训练数据多样性 效果显著 需要领域知识
早停(Early Stopping) 验证集不提升就停止 简单有效 需要监控验证集
批归一化 归一化层输入 加速训练、正则化 增加计算开销

批归一化 vs 层归一化

归一化技术可以加速训练,让优化更稳定。

内部协变量偏移

训练深度网络时,每一层的输入分布会随着前一层参数的变化而变化。这就是"内部协变量偏移"(Internal Covariate Shift)。

这会导致训练变慢,因为每一层都要不断适应新的分布。

归一化技术的目标就是:让每一层的输入分布保持稳定。

批归一化(Batch Normalization)

批归一化(BatchNorm)在 batch 维度上进行归一化。

公式:

μ = 1/N · sum(xᵢ)
σ² = 1/N · sum((xᵢ - μ)²)
x̂ᵢ = (xᵢ - μ) / √(σ² + ε)
yᵢ = γ·x̂ᵢ + β

其中 γ 和 β 是可学习参数。

实例

# ============================================
# 批归一化
# ============================================

import numpy as np

def batch_norm(x, gamma, beta, epsilon=1e-5, training=True, running_mean=None, running_var=None, momentum=0.1):
    """批归一化
    x: 输入,形状 (batch_size, features)
    gamma: 缩放参数
    beta: 偏移参数
    """
    if training:
        # 训练模式:用当前 batch 的统计量
        batch_mean = np.mean(x, axis=0)
        batch_var = np.var(x, axis=0)

        # 更新 running 统计量(用于测试时)
        if running_mean is not None and running_var is not None:
            running_mean[:] = momentum * running_mean + (1 - momentum) * batch_mean
            running_var[:] = momentum * running_var + (1 - momentum) * batch_var

        # 归一化
        x_normalized = (x - batch_mean) / np.sqrt(batch_var + epsilon)
    else:
        # 测试模式:用 running 统计量
        x_normalized = (x - running_mean) / np.sqrt(running_var + epsilon)

    # 缩放和偏移
    out = gamma * x_normalized + beta
    return out

# 测试
batch_size = 4
features = 3

# 一个 batch 的数据
x = np.array([
    [1.0, 2.0, 3.0],
    [2.0, 3.0, 4.0],
    [3.0, 4.0, 5.0],
    [4.0, 5.0, 6.0],
])
print(f"输入 x:\n{x}")

# 初始参数
gamma = np.ones(features)  # 初始不缩放
beta = np.zeros(features)  # 初始不偏移
print(f"\ngamma: {gamma}")
print(f"beta: {beta}")

# 应用批归一化
out = batch_norm(x, gamma, beta, training=True)
print(f"\n批归一化后:\n{out}")

# 验证:每个特征的均值应该接近 0,方差接近 1
print(f"\n归一化后的均值: {np.mean(out, axis=0)}")
print(f"归一化后的方差: {np.var(out, axis=0)}")

批归一化的优点:

  • 加速训练
  • 允许更大的学习率
  • 减少对初始化的敏感
  • 有轻微的正则化效果

批归一化的缺点:

  • 依赖 batch 大小——batch 太小效果不好
  • 训练和测试时行为不同
  • 不适用于序列模型(如 RNN、Transformer)

层归一化(Layer Normalization)

层归一化(LayerNorm)在特征维度上进行归一化,而不是 batch 维度。

这让它不依赖 batch 大小,非常适合序列模型。

实例

# ============================================
# 层归一化
# ============================================

import numpy as np

def layer_norm(x, gamma, beta, epsilon=1e-5):
    """层归一化
    x: 输入,形状 (batch_size, features) 或 (batch_size, seq_len, features)
    """
    # 在最后一个维度(特征维度)上计算均值和方差
    mean = np.mean(x, axis=-1, keepdims=True)
    var = np.var(x, axis=-1, keepdims=True)

    # 归一化
    x_normalized = (x - mean) / np.sqrt(var + epsilon)

    # 缩放和偏移
    out = gamma * x_normalized + beta
    return out

# 测试
batch_size = 2
seq_len = 3
features = 4

# 模拟 Transformer 中的输入:(batch_size, seq_len, features)
x = np.array([
    [[1.0, 2.0, 3.0, 4.0],
     [2.0, 3.0, 4.0, 5.0],
     [3.0, 4.0, 5.0, 6.0]],
    [[4.0, 5.0, 6.0, 7.0],
     [5.0, 6.0, 7.0, 8.0],
     [6.0, 7.0, 8.0, 9.0]],
])
print(f"输入 x 形状: {x.shape}")

# 初始参数
gamma = np.ones(features)
beta = np.zeros(features)

# 应用层归一化
out = layer_norm(x, gamma, beta)
print(f"层归一化后形状: {out.shape}")

# 验证:每个位置的均值接近 0,方差接近 1
mean_check = np.mean(out, axis=-1)
var_check = np.var(out, axis=-1)
print(f"\n每个位置的均值:\n{mean_check}")
print(f"每个位置的方差:\n{var_check}")

两种归一化方法的对比:

方法 归一化维度 依赖 batch 适用场景 代表模型
BatchNorm batch 维度 CNN、固定 batch 大小 ResNet、VGG
LayerNorm 特征维度 Transformer、RNN、变长序列 GPT、BERT、LLaMA

对于 Transformer 和大语言模型,LayerNorm 是标准选择。它在每个位置独立计算统计量,不依赖 batch 大小,也不需要维护 running 统计量。


学习率调度

学习率是最重要的超参数之一——太大不收敛,太小收敛太慢。

学习率调度(Learning Rate Scheduling)就是在训练过程中动态调整学习率。

预热(Warmup)

训练初期,先用小学习率"预热",让模型稳定下来,然后再提升到目标学习率。

这对 Transformer 尤其重要。

余弦退火

余弦退火(Cosine Annealing)让学习率按照余弦函数的形状逐渐下降。

实例

# ============================================
# 学习率调度
# ============================================

import numpy as np

def warmup_schedule(step, warmup_steps, base_lr):
    """线性预热调度"""
    if step < warmup_steps:
        return base_lr * (step + 1) / warmup_steps
    return base_lr

def cosine_annealing_schedule(step, total_steps, base_lr, min_lr=0.0):
    """余弦退火调度"""
    return min_lr + 0.5 * (base_lr - min_lr) * (1 + np.cos(np.pi * step / total_steps))

def warmup_cosine_schedule(step, warmup_steps, total_steps, base_lr, min_lr=0.0):
    """预热 + 余弦退火"""
    if step < warmup_steps:
        return base_lr * (step + 1) / warmup_steps
    return min_lr + 0.5 * (base_lr - min_lr) * (1 + np.cos(np.pi * (step - warmup_steps) / (total_steps - warmup_steps)))

# 参数
total_steps = 100
warmup_steps = 10
base_lr = 1e-3
min_lr = 1e-5

print("不同学习率调度:")
print("Step |  Warmup  |  Cosine  |  Warmup+Cosine")
print("-" * 55)

for step in range(0, total_steps + 1, 10):
    lr_warmup = warmup_schedule(step, warmup_steps, base_lr)
    lr_cosine = cosine_annealing_schedule(step, total_steps, base_lr, min_lr)
    lr_combined = warmup_cosine_schedule(step, warmup_steps, total_steps, base_lr, min_lr)
    print(f"{step:4} | {lr_warmup:.6f} | {lr_cosine:.6f} | {lr_combined:.6f}")

学习率对训练的影响

学习率的影响:

  • 太大:训练不稳定,可能发散
  • 太小:收敛慢,可能卡在局部最优
  • 合适:快速稳定收敛

一个常用的策略是"学习率范围测试"(Learning Rate Finder):先用从小到大的学习率训练几步,看哪个学习率下损失下降最快。

学习率调度方法总结:

调度方法 特点 适用场景
固定学习率 最简单 小模型、简单任务
Step Decay 每隔几步降一次 CNN 等
Cosine Annealing 平滑下降 大多数场景
Warmup + Cosine 先升后降 Transformer、大模型(推荐)
Reduce on Plateau 验证集不提升就降 需要监控验证集

实战:用 PyTorch 从零实现 MLP

现在我们把前面所有知识点整合起来,用 PyTorch 实现一个完整的多层感知机(MLP),并在一个简单的分类任务上训练它。

实例

# ============================================
# PyTorch 实现 MLP 完整训练流程
# ============================================

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import numpy as np

# ============================================
# 1. 准备数据
# ============================================

class SimpleDataset(Dataset):
    """一个简单的二分类数据集"""

    def __init__(self, n_samples=1000, n_features=10):
        # 生成合成数据
        np.random.seed(42)
        self.X = np.random.randn(n_samples, n_features).astype(np.float32)
        # 简单的分类规则:y = 1 if sum(x[:5]) > 0 else 0
        self.y = (np.sum(self.X[:, :5], axis=1) > 0).astype(np.int64)

    def __len__(self):
        return len(self.X)

    def __getitem__(self, idx):
        return torch.tensor(self.X[idx]), torch.tensor(self.y[idx])

# 创建数据集和数据加载器
train_dataset = SimpleDataset(n_samples=800)
val_dataset = SimpleDataset(n_samples=200)

train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False)

print(f"训练集大小: {len(train_dataset)}")
print(f"验证集大小: {len(val_dataset)}")

# ============================================
# 2. 定义模型
# ============================================

class MLP(nn.Module):
    """多层感知机"""

    def __init__(self, input_dim, hidden_dims, output_dim, dropout_rate=0.1):
        super().__init__()

        layers = []
        prev_dim = input_dim

        # 隐藏层
        for hidden_dim in hidden_dims:
            layers.append(nn.Linear(prev_dim, hidden_dim))
            layers.append(nn.ReLU())
            layers.append(nn.LayerNorm(hidden_dim))  # 层归一化
            layers.append(nn.Dropout(dropout_rate))  # Dropout
            prev_dim = hidden_dim

        # 输出层
        layers.append(nn.Linear(prev_dim, output_dim))

        self.model = nn.Sequential(*layers)

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

# 创建模型
model = MLP(
    input_dim=10,
    hidden_dims=[64, 32],
    output_dim=2,
    dropout_rate=0.1
)
print("\n模型结构:")
print(model)

# ============================================
# 3. 定义损失函数和优化器
# ============================================

criterion = nn.CrossEntropyLoss()  # 交叉熵损失
optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)  # AdamW 优化器

# 学习率调度:预热 + 余弦退火
total_epochs = 20
warmup_epochs = 2
total_steps = len(train_loader) * total_epochs
warmup_steps = len(train_loader) * warmup_epochs

def lr_lambda(step):
    if step < warmup_steps:
        return (step + 1) / warmup_steps
    else:
        progress = (step - warmup_steps) / (total_steps - warmup_steps)
        return 0.5 * (1 + np.cos(np.pi * progress))

scheduler = optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=lr_lambda)

print("\n训练配置:")
print(f"  Epochs: {total_epochs}")
print(f"  Warmup epochs: {warmup_epochs}")
print(f"  Total steps: {total_steps}")
print(f"  Warmup steps: {warmup_steps}")

# ============================================
# 4. 训练循环
# ============================================

print("\n开始训练...")
print("-" * 50)

global_step = 0
best_val_acc = 0.0

for epoch in range(total_epochs):
    # 训练阶段
    model.train()
    train_loss = 0.0
    train_correct = 0
    train_total = 0

    for batch_X, batch_y in train_loader:
        # 前向传播
        logits = model(batch_X)
        loss = criterion(logits, batch_y)

        # 反向传播
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        scheduler.step()

        # 统计
        train_loss += loss.item()
        predicted = torch.argmax(logits, dim=1)
        train_total += batch_y.size(0)
        train_correct += (predicted == batch_y).sum().item()

        global_step += 1

    train_loss /= len(train_loader)
    train_acc = train_correct / train_total

    # 验证阶段
    model.eval()
    val_loss = 0.0
    val_correct = 0
    val_total = 0

    with torch.no_grad():
        for batch_X, batch_y in val_loader:
            logits = model(batch_X)
            loss = criterion(logits, batch_y)

            val_loss += loss.item()
            predicted = torch.argmax(logits, dim=1)
            val_total += batch_y.size(0)
            val_correct += (predicted == batch_y).sum().item()

    val_loss /= len(val_loader)
    val_acc = val_correct / val_total

    # 保存最好的模型
    if val_acc > best_val_acc:
        best_val_acc = val_acc
        # 这里可以保存模型 checkpoint

    # 打印日志
    current_lr = optimizer.param_groups[0]['lr']
    print(f"Epoch {epoch+1:2d} | lr: {current_lr:.6f} | "
          f"Train Loss: {train_loss:.4f} | Train Acc: {train_acc:.4f} | "
          f"Val Loss: {val_loss:.4f} | Val Acc: {val_acc:.4f}")

print("-" * 50)
print(f"训练完成!最好验证准确率: {best_val_acc:.4f}")

# ============================================
# 5. 推理示例
# ============================================

print("\n推理示例:")
model.eval()

# 几个测试样本
test_samples = [
    np.array([1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], dtype=np.float32),  # 应该是 1
    np.array([-1.0, -1.0, -1.0, -1.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0], dtype=np.float32),  # 应该是 0
]

for i, sample in enumerate(test_samples):
    with torch.no_grad():
        input_tensor = torch.tensor(sample).unsqueeze(0)
        logits = model(input_tensor)
        probs = torch.softmax(logits, dim=1)
        predicted_class = torch.argmax(probs, dim=1).item()

    print(f"  样本 {i+1}: {sample[:5]}...")
    print(f"    预测类别: {predicted_class}")
    print(f"    概率: 类0={probs[0,0]:.4f}, 类1={probs[0,1]:.4f}")

这个例子包含了深度学习训练的所有标准组件:

  • 数据处理:Dataset、DataLoader
  • 模型定义:MLP 带 LayerNorm 和 Dropout
  • 损失函数:CrossEntropyLoss
  • 优化器:AdamW
  • 学习率调度:Warmup + Cosine
  • 训练循环:训练阶段 + 验证阶段
  • 模型保存和推理

这个流程可以直接推广到更复杂的任务和模型。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐