前言

反向传播(Backpropagation, BP)算法作为深度学习的核心算法,运用链式法则高效计算神经网络中各参数的梯度,为模型优化指明方向。本文将详细讲解该算法的原理、实现及优化技巧,并附上完整的代码实例。

1. 前向传播

1.1 数学表达

前向传播是神经网络进行预测的过程,数据从输入层经过隐藏层最终到达输出层。

1.1.1 输入层到隐藏层

假设我们有一个简单的三层神经网络(输入层、隐藏层、输出层),输入层到隐藏层的计算可以表示为:

其中:

  • x是输入向量

  • W1​ 是输入层到隐藏层的权重矩阵

  • b1是偏置向量

  • σ 是激活函数(如Sigmoid、ReLU等)

1.1.2 隐藏层到输出层

隐藏层到输出层的计算类似:

1.2 作用

前向传播的主要作用是根据当前参数计算网络的输出,为后续的损失计算和反向传播做准备。

代码实现

import numpy as np

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

def forward_propagation(x, W1, b1, W2, b2):
    """
    前向传播实现
    
    参数:
    x -- 输入数据 (n_x, 1)
    W1 -- 第一层权重矩阵 (n_h, n_x)
    b1 -- 第一层偏置向量 (n_h, 1)
    W2 -- 第二层权重矩阵 (n_y, n_h)
    b2 -- 第二层偏置向量 (n_y, 1)
    
    返回:
    h -- 隐藏层输出
    y -- 输出层输出
    """
    # 输入层到隐藏层
    z1 = np.dot(W1, x) + b1
    h = sigmoid(z1)
    
    # 隐藏层到输出层
    z2 = np.dot(W2, h) + b2
    y = sigmoid(z2)
    
    return h, y

# 示例数据
n_x = 3  # 输入层维度
n_h = 4  # 隐藏层维度
n_y = 1  # 输出层维度

# 随机初始化参数
np.random.seed(1)
W1 = np.random.randn(n_h, n_x) * 0.01
b1 = np.zeros((n_h, 1))
W2 = np.random.randn(n_y, n_h) * 0.01
b2 = np.zeros((n_y, 1))

# 输入数据
x = np.array([[0.1], [0.2], [0.3]])

# 前向传播
h, y = forward_propagation(x, W1, b1, W2, b2)
print("隐藏层输出:", h)
print("网络输出:", y)

2. BP基础之梯度下降算法

2.1 数学描述

2.1.1 数学公式

梯度下降算法的核心是通过损失函数对参数的梯度来更新参数:

其中:

  • θ 是模型参数

  • α 是学习率

  • J(θ) 是损失函数

2.1.2 过程阐述
  1. 计算损失函数关于参数的梯度

  2. 按照负梯度方向更新参数:反向传播算法

  3. 重复上述过程直到收敛

2.2 传统下降方式

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

使用全部训练数据计算梯度:

def batch_gradient_descent(X, Y, W1, b1, W2, b2, learning_rate, iterations):
    """
    批量梯度下降实现
    
    参数:
    X -- 输入数据 (n_x, m)
    Y -- 真实标签 (n_y, m)
    W1, b1, W2, b2 -- 网络参数
    learning_rate -- 学习率
    iterations -- 迭代次数
    
    返回:
    parameters -- 更新后的参数
    """
    m = X.shape[1]  # 样本数量
    
    for i in range(iterations):
        # 前向传播
        Z1 = np.dot(W1, X) + b1
        A1 = sigmoid(Z1)
        Z2 = np.dot(W2, A1) + b2
        A2 = sigmoid(Z2)
        
        # 计算损失
        loss = -np.mean(Y * np.log(A2) + (1-Y) * np.log(1-A2))
        
        # 反向传播
        dZ2 = A2 - Y
        dW2 = np.dot(dZ2, A1.T) / m
        db2 = np.sum(dZ2, axis=1, keepdims=True) / m
        
        dA1 = np.dot(W2.T, dZ2)
        dZ1 = dA1 * A1 * (1 - A1)
        dW1 = np.dot(dZ1, X.T) / m
        db1 = np.sum(dZ1, axis=1, keepdims=True) / m
        
        # 更新参数
        W1 = W1 - learning_rate * dW1
        b1 = b1 - learning_rate * db1
        W2 = W2 - learning_rate * dW2
        b2 = b2 - learning_rate * db2
        
        if i % 100 == 0:
            print(f"Iteration {i}, Loss: {loss}")
    
    return W1, b1, W2, b2

# 示例数据
X = np.random.randn(3, 100)  # 100个样本,每个样本3个特征
Y = np.random.randint(0, 2, (1, 100))  # 二分类标签

# 训练网络
W1, b1, W2, b2 = batch_gradient_descent(X, Y, W1, b1, W2, b2, 0.1, 1000)
2.2.2 随机梯度下降(Stochastic Gradient Descent)

每次使用单个样本更新参数:

def stochastic_gradient_descent(X, Y, W1, b1, W2, b2, learning_rate, iterations):
    """
    随机梯度下降实现
    
    参数:
    X -- 输入数据 (n_x, m)
    Y -- 真实标签 (n_y, m)
    W1, b1, W2, b2 -- 网络参数
    learning_rate -- 学习率
    iterations -- 迭代次数
    
    返回:
    parameters -- 更新后的参数
    """
    m = X.shape[1]  # 样本数量
    
    for i in range(iterations):
        for j in range(m):
            # 获取单个样本
            x = X[:, j].reshape(-1, 1)
            y = Y[:, j].reshape(-1, 1)
            
            # 前向传播
            Z1 = np.dot(W1, x) + b1
            A1 = sigmoid(Z1)
            Z2 = np.dot(W2, A1) + b2
            A2 = sigmoid(Z2)
            
            # 反向传播
            dZ2 = A2 - y
            dW2 = np.dot(dZ2, A1.T)
            db2 = dZ2
            
            dA1 = np.dot(W2.T, dZ2)
            dZ1 = dA1 * A1 * (1 - A1)
            dW1 = np.dot(dZ1, x.T)
            db1 = dZ1
            
            # 更新参数
            W1 = W1 - learning_rate * dW1
            b1 = b1 - learning_rate * db1
            W2 = W2 - learning_rate * dW2
            b2 = b2 - learning_rate * db2
        
        # 计算整体损失
        _, A2 = forward_propagation(X, W1, b1, W2, b2)
        loss = -np.mean(Y * np.log(A2) + (1-Y) * np.log(1-A2))
        
        if i % 10 == 0:
            print(f"Iteration {i}, Loss: {loss}")
    
    return W1, b1, W2, b2

# 使用同样的数据训练
W1, b1, W2, b2 = stochastic_gradient_descent(X, Y, W1, b1, W2, b2, 0.01, 100)
2.2.3 小批量梯度下降(Mini-batch Gradient Descent)

折中方案,使用小批量数据更新参数:

def mini_batch_gradient_descent(X, Y, W1, b1, W2, b2, learning_rate, iterations, batch_size=32):
    """
    小批量梯度下降实现
    
    参数:
    X -- 输入数据 (n_x, m)
    Y -- 真实标签 (n_y, m)
    W1, b1, W2, b2 -- 网络参数
    learning_rate -- 学习率
    iterations -- 迭代次数
    batch_size -- 批量大小
    
    返回:
    parameters -- 更新后的参数
    """
    m = X.shape[1]  # 样本数量
    batches = m // batch_size
    
    for i in range(iterations):
        # 打乱数据
        permutation = np.random.permutation(m)
        X_shuffled = X[:, permutation]
        Y_shuffled = Y[:, permutation]
        
        for j in range(batches):
            # 获取小批量数据
            start = j * batch_size
            end = start + batch_size
            x = X_shuffled[:, start:end]
            y = Y_shuffled[:, start:end]
            
            # 前向传播
            Z1 = np.dot(W1, x) + b1
            A1 = sigmoid(Z1)
            Z2 = np.dot(W2, A1) + b2
            A2 = sigmoid(Z2)
            
            # 反向传播
            dZ2 = A2 - y
            dW2 = np.dot(dZ2, A1.T) / batch_size
            db2 = np.sum(dZ2, axis=1, keepdims=True) / batch_size
            
            dA1 = np.dot(W2.T, dZ2)
            dZ1 = dA1 * A1 * (1 - A1)
            dW1 = np.dot(dZ1, x.T) / batch_size
            db1 = np.sum(dZ1, axis=1, keepdims=True) / batch_size
            
            # 更新参数
            W1 = W1 - learning_rate * dW1
            b1 = b1 - learning_rate * db1
            W2 = W2 - learning_rate * dW2
            b2 = b2 - learning_rate * db2
        
        # 计算整体损失
        _, A2 = forward_propagation(X, W1, b1, W2, b2)
        loss = -np.mean(Y * np.log(A2) + (1-Y) * np.log(1-A2))
        
        if i % 10 == 0:
            print(f"Iteration {i}, Loss: {loss}")
    
    return W1, b1, W2, b2

# 使用同样的数据训练
W1, b1, W2, b2 = mini_batch_gradient_descent(X, Y, W1, b1, W2, b2, 0.1, 100, 32)

2.3 存在的问题

传统梯度下降方法存在以下问题:

  1. 学习率选择困难

  2. 容易陷入局部最优

  3. 对于不同参数可能需要不同的学习率

  4. 对于稀疏数据表现不佳

2.4 优化下降方式

2.4.1 指数加权平均

指数加权平均(Exponentially Weighted Moving Average)是一种处理序列数据的平滑方法:

其中β是衰减率。

2.4.2 Momentum

动量法(Momentum)通过引入速度变量来加速学习:

def momentum_gradient_descent(X, Y, W1, b1, W2, b2, learning_rate, iterations, beta=0.9):
    """
    带动量的梯度下降
    
    参数:
    X, Y -- 训练数据和标签
    W1, b1, W2, b2 -- 初始参数
    learning_rate -- 学习率
    iterations -- 迭代次数
    beta -- 动量参数
    
    返回:
    更新后的参数
    """
    m = X.shape[1]
    
    # 初始化动量
    vW1 = np.zeros_like(W1)
    vb1 = np.zeros_like(b1)
    vW2 = np.zeros_like(W2)
    vb2 = np.zeros_like(b2)
    
    for i in range(iterations):
        # 前向传播
        Z1 = np.dot(W1, X) + b1
        A1 = sigmoid(Z1)
        Z2 = np.dot(W2, A1) + b2
        A2 = sigmoid(Z2)
        
        # 计算损失
        loss = -np.mean(Y * np.log(A2) + (1-Y) * np.log(1-A2))
        
        # 反向传播
        dZ2 = A2 - Y
        dW2 = np.dot(dZ2, A1.T) / m
        db2 = np.sum(dZ2, axis=1, keepdims=True) / m
        
        dA1 = np.dot(W2.T, dZ2)
        dZ1 = dA1 * A1 * (1 - A1)
        dW1 = np.dot(dZ1, X.T) / m
        db1 = np.sum(dZ1, axis=1, keepdims=True) / m
        
        # 更新动量
        vW1 = beta * vW1 + (1 - beta) * dW1
        vb1 = beta * vb1 + (1 - beta) * db1
        vW2 = beta * vW2 + (1 - beta) * dW2
        vb2 = beta * vb2 + (1 - beta) * db2
        
        # 更新参数
        W1 = W1 - learning_rate * vW1
        b1 = b1 - learning_rate * vb1
        W2 = W2 - learning_rate * vW2
        b2 = b2 - learning_rate * vb2
        
        if i % 100 == 0:
            print(f"Iteration {i}, Loss: {loss}")
    
    return W1, b1, W2, b2

# 使用动量法训练
W1, b1, W2, b2 = momentum_gradient_descent(X, Y, W1, b1, W2, b2, 0.01, 1000)
2.4.3 AdaGrad

AdaGrad算法自适应地为每个参数分配不同的学习率:

 

 

def adagrad(X, Y, W1, b1, W2, b2, learning_rate, iterations, epsilon=1e-8):
    """
    AdaGrad优化算法
    
    参数:
    X, Y -- 训练数据和标签
    W1, b1, W2, b2 -- 初始参数
    learning_rate -- 初始学习率
    iterations -- 迭代次数
    epsilon -- 防止除零的小常数
    
    返回:
    更新后的参数
    """
    m = X.shape[1]
    
    # 初始化累积平方梯度
    GW1 = np.zeros_like(W1)
    Gb1 = np.zeros_like(b1)
    GW2 = np.zeros_like(W2)
    Gb2 = np.zeros_like(b2)
    
    for i in range(iterations):
        # 前向传播
        Z1 = np.dot(W1, X) + b1
        A1 = sigmoid(Z1)
        Z2 = np.dot(W2, A1) + b2
        A2 = sigmoid(Z2)
        
        # 计算损失
        loss = -np.mean(Y * np.log(A2) + (1-Y) * np.log(1-A2))
        
        # 反向传播
        dZ2 = A2 - Y
        dW2 = np.dot(dZ2, A1.T) / m
        db2 = np.sum(dZ2, axis=1, keepdims=True) / m
        
        dA1 = np.dot(W2.T, dZ2)
        dZ1 = dA1 * A1 * (1 - A1)
        dW1 = np.dot(dZ1, X.T) / m
        db1 = np.sum(dZ1, axis=1, keepdims=True) / m
        
        # 更新累积平方梯度
        GW1 += dW1 ** 2
        Gb1 += db1 ** 2
        GW2 += dW2 ** 2
        Gb2 += db2 ** 2
        
        # 更新参数
        W1 = W1 - learning_rate * dW1 / (np.sqrt(GW1) + epsilon)
        b1 = b1 - learning_rate * db1 / (np.sqrt(Gb1) + epsilon)
        W2 = W2 - learning_rate * dW2 / (np.sqrt(GW2) + epsilon)
        b2 = b2 - learning_rate * db2 / (np.sqrt(Gb2) + epsilon)
        
        if i % 100 == 0:
            print(f"Iteration {i}, Loss: {loss}")
    
    return W1, b1, W2, b2

# 使用AdaGrad训练
W1, b1, W2, b2 = adagrad(X, Y, W1, b1, W2, b2, 0.1, 1000)
2.4.4 RMSProp

RMSProp改进了AdaGrad的累积梯度方式:

 

 

 

def rmsprop(X, Y, W1, b1, W2, b2, learning_rate, iterations, beta=0.9, epsilon=1e-8):
    """
    RMSProp优化算法
    
    参数:
    X, Y -- 训练数据和标签
    W1, b1, W2, b2 -- 初始参数
    learning_rate -- 初始学习率
    iterations -- 迭代次数
    beta -- 衰减率
    epsilon -- 防止除零的小常数
    
    返回:
    更新后的参数
    """
    m = X.shape[1]
    
    # 初始化累积平方梯度
    GW1 = np.zeros_like(W1)
    Gb1 = np.zeros_like(b1)
    GW2 = np.zeros_like(W2)
    Gb2 = np.zeros_like(b2)
    
    for i in range(iterations):
        # 前向传播
        Z1 = np.dot(W1, X) + b1
        A1 = sigmoid(Z1)
        Z2 = np.dot(W2, A1) + b2
        A2 = sigmoid(Z2)
        
        # 计算损失
        loss = -np.mean(Y * np.log(A2) + (1-Y) * np.log(1-A2))
        
        # 反向传播
        dZ2 = A2 - Y
        dW2 = np.dot(dZ2, A1.T) / m
        db2 = np.sum(dZ2, axis=1, keepdims=True) / m
        
        dA1 = np.dot(W2.T, dZ2)
        dZ1 = dA1 * A1 * (1 - A1)
        dW1 = np.dot(dZ1, X.T) / m
        db1 = np.sum(dZ1, axis=1, keepdims=True) / m
        
        # 更新累积平方梯度
        GW1 = beta * GW1 + (1 - beta) * dW1 ** 2
        Gb1 = beta * Gb1 + (1 - beta) * db1 ** 2
        GW2 = beta * GW2 + (1 - beta) * dW2 ** 2
        Gb2 = beta * Gb2 + (1 - beta) * db2 ** 2
        
        # 更新参数
        W1 = W1 - learning_rate * dW1 / (np.sqrt(GW1) + epsilon)
        b1 = b1 - learning_rate * db1 / (np.sqrt(Gb1) + epsilon)
        W2 = W2 - learning_rate * dW2 / (np.sqrt(GW2) + epsilon)
        b2 = b2 - learning_rate * db2 / (np.sqrt(Gb2) + epsilon)
        
        if i % 100 == 0:
            print(f"Iteration {i}, Loss: {loss}")
    
    return W1, b1, W2, b2

# 使用RMSProp训练
W1, b1, W2, b2 = rmsprop(X, Y, W1, b1, W2, b2, 0.01, 1000)
2.4.5 Adam

Adam(Adaptive Moment Estimation)结合了Momentum和RMSProp的优点:

 

 

 

 

 

2.5 总结

不同优化算法的比较:

算法 优点 缺点 适用场景
批量GD 稳定收敛 计算开销大,内存要求高 小数据集
随机GD 计算快,可在线学习 收敛不稳定 大数据集,在线学习
小批量GD 平衡计算效率和稳定性 需要调batch size 大多数场景
Momentum 加速收敛,减少震荡 需要调动量参数 深网络,高维参数
AdaGrad 自适应学习率 累积梯度导致学习率过早减小 稀疏数据
RMSProp 解决AdaGrad学习率衰减问题 需要调衰减率 非平稳目标
Adam 结合Momentum和RMSProp优点 超参数较多 大多数深度学习任务

3. 使用PyTorch实现反向传播

现代深度学习框架如PyTorch已经内置了自动微分和优化算法,可以大大简化实现:

import torch
import torch.nn as nn
import torch.optim as optim

# 定义网络结构
class SimpleNN(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(SimpleNN, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size)
        self.sigmoid1 = nn.Sigmoid()
        self.fc2 = nn.Linear(hidden_size, output_size)
        self.sigmoid2 = nn.Sigmoid()
    
    def forward(self, x):
        out = self.fc1(x)
        out = self.sigmoid1(out)
        out = self.fc2(out)
        out = self.sigmoid2(out)
        return out

# 参数设置
input_size = 3
hidden_size = 4
output_size = 1
learning_rate = 0.1
num_epochs = 1000

# 创建模型
model = SimpleNN(input_size, hidden_size, output_size)

# 定义损失函数和优化器
criterion = nn.BCELoss()  # 二分类交叉熵损失
optimizer = optim.Adam(model.parameters(), lr=learning_rate)  # 使用Adam优化器

# 准备数据 (转换为PyTorch张量)
X_tensor = torch.from_numpy(X.T).float()
Y_tensor = torch.from_numpy(Y.T).float()

# 训练循环
for epoch in range(num_epochs):
    # 前向传播
    outputs = model(X_tensor)
    loss = criterion(outputs, Y_tensor)
    
    # 反向传播和优化
    optimizer.zero_grad()  # 清空梯度
    loss.backward()  # 反向传播计算梯度
    optimizer.step()  # 更新参数
    
    if (epoch+1) % 100 == 0:
        print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')

# 测试模型
with torch.no_grad():
    test_input = torch.tensor([[0.1, 0.2, 0.3]], dtype=torch.float)
    predicted = model(test_input)
    print(f'测试输入: {test_input}, 预测输出: {predicted.item()}')

4. 反向传播的数学推导

为了更深入理解反向传播,让我们推导一下简单的两层神经网络的梯度计算。

4.1 定义网络和损失函数

网络结构:

  1. 输入层到隐藏层:$z_1 = W_1 x + b_1$, $a_1 = \sigma(z_1)$

  2. 隐藏层到输出层:$z_2 = W_2 a_1 + b_2$, $a_2 = \sigma(z_2)$

损失函数(二分类交叉熵):

J=−1m∑i=1m[y(i)log⁡(a2(i))+(1−y(i))log⁡(1−a2(i))]J=−m1​i=1∑m​[y(i)log(a2(i)​)+(1−y(i))log(1−a2(i)​)]

4.2 梯度计算

  1. 输出层梯度:

∂J∂z2=a2−y∂z2​∂J​=a2​−y∂J∂W2=∂J∂z2⋅a1T∂W2​∂J​=∂z2​∂J​⋅a1T​∂J∂b2=∂J∂z2∂b2​∂J​=∂z2​∂J​

  1. 隐藏层梯度:

∂J∂a1=W2T⋅∂J∂z2∂a1​∂J​=W2T​⋅∂z2​∂J​∂J∂z1=∂J∂a1⊙σ′(z1)=∂J∂a1⊙a1⊙(1−a1)∂z1​∂J​=∂a1​∂J​⊙σ′(z1​)=∂a1​∂J​⊙a1​⊙(1−a1​)∂J∂W1=∂J∂z1⋅xT∂W1​∂J​=∂z1​∂J​⋅xT∂J∂b1=∂J∂z1∂b1​∂J​=∂z1​∂J​

5. 总结

反向传播算法是深度学习训练的核心,通过链式法则高效计算梯度。本文从基础的前向传播开始,详细介绍了各种梯度下降算法及其优化方法,并提供了完整的实现代码。现代深度学习框架虽然自动实现了反向传播,但理解其原理对于调试模型和解决实际问题至关重要。

实际应用中,Adam通常是首选优化器,但在特定场景下其他优化器可能表现更好。理解不同优化算法的特性有助于我们根据具体问题选择合适的训练策略。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

更多推荐