算法概述#

什么是反向传播?#

反向传播(Backpropagation) 是训练神经网络的核心算法,用于高效计算损失函数对网络参数的梯度。

为什么需要反向传播?#

问题:神经网络有大量参数,如何高效计算梯度?

解决方案

  • 前向传播:计算预测值
  • 反向传播:从输出层向输入层传播误差,计算梯度

核心思想#

  1. 前向传播:计算每层的输出
  2. 计算损失:比较预测值和真实值
  3. 反向传播:从输出层开始,逐层计算梯度
  4. 更新参数:使用梯度下降更新参数

链式法则#

数学基础#

链式法则(Chain Rule) 是反向传播的数学基础。

对于复合函数 $y = f(g(x))$ :

$$ \frac{dy}{dx} = \frac{dy}{dg} \cdot \frac{dg}{dx} $$

多变量链式法则#

对于 $z = f(x, y)$ ,其中 $x = g(t)$ , $y = h(t)$ :

$$ \frac{dz}{dt} = \frac{\partial z}{\partial x} \cdot \frac{dx}{dt} + \frac{\partial z}{\partial y} \cdot \frac{dy}{dt} $$

神经网络中的应用#

在神经网络中,损失函数是参数的复合函数:

$$ L = L(y, \hat{y}) = L(y, f_N(\ldots f_2(f_1(x, w_1, b_1), w_2, b_2) \ldots, w_N, b_N)) $$

使用链式法则计算梯度:

$$ \frac{\partial L}{\partial w_1} = \frac{\partial L}{\partial \hat{y}} \cdot \frac{\partial \hat{y}}{\partial z_N} \cdot \ldots \cdot \frac{\partial z_2}{\partial a_1} \cdot \frac{\partial a_1}{\partial z_1} \cdot \frac{\partial z_1}{\partial w_1} $$

反向传播原理#

符号定义#

  • $L$ :损失函数
  • $z^{(l)}$ :第 $l$ 层的线性输出(logits)
  • $a^{(l)}$ :第 $l$ 层的激活输出
  • $W^{(l)}$ :第 $l$ 层的权重矩阵
  • $b^{(l)}$ :第 $l$ 层的偏置向量
  • $\delta^{(l)}$ :第 $l$ 层的误差项(error term)

前向传播回顾#

$$ z^{(l)} = W^{(l)} \cdot a^{(l-1)} + b^{(l)} $$ $$ a^{(l)} = f(z^{(l)}) $$

反向传播步骤#

1. 输出层误差#

对于输出层 $L$ :

$$ \delta^{(L)} = \frac{\partial L}{\partial z^{(L)}} = \frac{\partial L}{\partial a^{(L)}} \cdot f'(z^{(L)}) $$

2. 隐藏层误差传播#

对于隐藏层 $l$ :

$$ \delta^{(l)} = (W^{(l+1)})^T \cdot \delta^{(l+1)} \odot f'(z^{(l)}) $$

其中 $\odot$ 表示逐元素相乘(Hadamard积)。

3. 参数梯度#

权重梯度:

$$ \frac{\partial L}{\partial W^{(l)}} = \delta^{(l)} \cdot (a^{(l-1)})^T $$

偏置梯度:

$$ \frac{\partial L}{\partial b^{(l)}} = \delta^{(l)} $$

完整流程#

前向传播:

$$ x → z¹ → a¹ → z² → a² → ... → z^L → a^L → L $$

反向传播:

L → δ^L → δ^(L-1) → ... → δ² → δ¹
         ↓          ↓            ↓
      ∂W^L/∂b^L  ∂W^(L-1)/∂b^(L-1)  ∂W¹/∂b¹

梯度计算#

输出层梯度#

均方误差损失#

对于回归问题,使用MSE损失:

$$ L = \frac{1}{2}(y - \hat{y})^2 $$

输出层误差:

$$ \delta^{(L)} = \hat{y} - y $$

交叉熵损失#

对于分类问题,使用交叉熵损失:

$$ L = -\sum_i y_i \log(\hat{y}_i) $$

如果使用Softmax + 交叉熵:

$$ \delta^{(L)} = \hat{y} - y \quad \text{(形式与MSE相同!)} $$

激活函数导数#

Sigmoid#

$$ f(z) = \frac{1}{1 + e^{-z}} $$ $$ f'(z) = f(z)(1 - f(z)) $$

Tanh#

$$ f(z) = \tanh(z) $$ $$ f'(z) = 1 - \tanh^2(z) $$

ReLU#

$$ f(z) = \max(0, z) $$ $$ f'(z) = \begin{cases} 1 & \text{if } z > 0 \\ 0 & \text{if } z \leq 0 \end{cases} $$

Leaky ReLU#

$$ f(z) = \max(\alpha z, z) \quad \text{($\alpha$ 通常为 0.01)} $$ $$ f'(z) = \begin{cases} 1 & \text{if } z > 0 \\ \alpha & \text{if } z \leq 0 \end{cases} $$

实现细节#

批量梯度计算#

对于批量数据(batch size = $m$ ):

误差项#

$$ \Delta^{(l)} = [\delta_1^{(l)}, \delta_2^{(l)}, \ldots, \delta_m^{(l)}] \quad \text{($[m, \text{输出维度}]$)} $$

权重梯度#

$$ \frac{\partial L}{\partial W^{(l)}} = \frac{1}{m} \cdot (\Delta^{(l)})^T \cdot A^{(l-1)} $$

形状:

  • $\Delta^{(l)}$ :$[m, \text{输出维度}]$
  • $A^{(l-1)}$ :$[m, \text{输入维度}]$
  • $\frac{\partial L}{\partial W^{(l)}}$ :$[\text{输出维度}, \text{输入维度}]$

偏置梯度#

$$ \frac{\partial L}{\partial b^{(l)}} = \frac{1}{m} \sum_{i=1}^{m} \delta_i^{(l)} $$

梯度累积#

对于多个样本,梯度是平均的:

$$ \frac{\partial L}{\partial W} = \frac{1}{m} \sum_{i=1}^{m} \frac{\partial L_i}{\partial W} $$

数值稳定性#

梯度消失问题#

问题:深层网络中,梯度可能变得非常小。

原因

  • Sigmoid/Tanh的导数在饱和区域很小
  • 多层相乘导致梯度指数衰减

解决方案

  • 使用ReLU等激活函数
  • 残差连接(ResNet)
  • 梯度裁剪

梯度爆炸问题#

问题:梯度可能变得非常大。

原因

  • 权重初始化不当
  • 深层网络梯度累积

解决方案

  • 梯度裁剪(gradient clipping)
  • 权重初始化(Xavier、He初始化)
  • 批量归一化

梯度裁剪#

# 方法1:按值裁剪
torch.nn.utils.clip_grad_value_(model.parameters(), clip_value=1.0)

# 方法2:按范数裁剪
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

代码实现#

手动实现反向传播#

import torch
import torch.nn as nn

class SimpleNet(nn.Module):
    def __init__(self):
        super(SimpleNet, self).__init__()
        self.fc1 = nn.Linear(2, 3)
        self.fc2 = nn.Linear(3, 1)
        self.sigmoid = nn.Sigmoid()
    
    def forward(self, x):
        z1 = self.fc1(x)
        a1 = self.sigmoid(z1)
        z2 = self.fc2(a1)
        a2 = self.sigmoid(z2)
        return a2

# 手动计算梯度(仅用于理解)
def manual_backward(model, x, y_true, y_pred):
    # 假设使用MSE损失
    loss = (y_pred - y_true) ** 2
    
    # 输出层误差
    delta2 = 2 * (y_pred - y_true) * model.sigmoid.derivative(z2)
    
    # 隐藏层误差
    delta1 = delta2 @ model.fc2.weight * model.sigmoid.derivative(z1)
    
    # 计算梯度
    grad_w2 = delta2 @ a1.T
    grad_b2 = delta2
    grad_w1 = delta1 @ x.T
    grad_b1 = delta1
    
    return grad_w1, grad_b1, grad_w2, grad_b2

PyTorch自动求导#

# 定义模型
model = SimpleNet()
criterion = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

# 训练循环
for epoch in range(100):
    # 前向传播
    y_pred = model(x)
    loss = criterion(y_pred, y_true)
    
    # 反向传播(自动计算梯度)
    optimizer.zero_grad()  # 清零梯度
    loss.backward()        # 计算梯度
    optimizer.step()       # 更新参数
    
    if epoch % 10 == 0:
        print(f'Epoch {epoch}, Loss: {loss.item():.4f}')

检查梯度#

# 打印梯度
for name, param in model.named_parameters():
    if param.grad is not None:
        print(f'{name}: {param.grad.norm():.6f}')
    else:
        print(f'{name}: No gradient')

梯度可视化#

import matplotlib.pyplot as plt

def plot_gradients(model):
    gradients = []
    names = []
    
    for name, param in model.named_parameters():
        if param.grad is not None:
            gradients.append(param.grad.norm().item())
            names.append(name)
    
    plt.bar(names, gradients)
    plt.xlabel('Layer')
    plt.ylabel('Gradient Norm')
    plt.title('Gradient Norms by Layer')
    plt.xticks(rotation=45)
    plt.tight_layout()
    plt.show()

总结#

  1. 反向传播:高效计算梯度的算法
  2. 链式法则:反向传播的数学基础
  3. 误差传播:从输出层向输入层传播误差
  4. 梯度计算:使用误差项计算参数梯度
  5. 数值稳定性:注意梯度消失和爆炸问题

关键要点

  • 反向传播利用链式法则高效计算梯度
  • 误差从输出层向输入层反向传播
  • PyTorch等框架自动实现反向传播
  • 注意梯度消失和爆炸问题