神经网络实战:从零实现5.1版本的前向传播、反向传播与训练过程

1次阅读
没有评论

共计 2868 个字符,预计需要花费 8 分钟才能阅读完成。

image.webp

神经网络训练的核心流程

神经网络的训练本质是通过前向传播计算预测值,反向传播计算梯度,最后用优化器更新参数的三步循环。数学上依赖链式法则求导和梯度下降算法,核心目标是让损失函数对参数的偏导数指导权重调整方向。整个过程需要处理矩阵运算的维度匹配、非线性激活的导数计算以及数值稳定性控制。

神经网络实战:从零实现 5.1 版本的前向传播、反向传播与训练过程

新手常见痛点分析

梯度消失 / 爆炸问题

当网络层数较深时,梯度在反向传播过程中连续乘以小于 1 的权重会导致梯度指数级减小(消失),而大于 1 的权重则会导致梯度指数级增大(爆炸)。典型场景如使用 Sigmoid 激活函数时,其导数值最大仅 0.25,多层连乘后梯度极易消失。

学习率设置误区

  • 过大学习率会导致参数在最优解附近震荡甚至发散
  • 过小学习率使得训练速度缓慢且可能陷入局部最优
  • 固定学习率难以适应不同参数层的更新需求

激活函数选择陷阱

  • Sigmoid 在极端值区梯度接近零(饱和区)
  • Tanh 虽然零中心化但仍有梯度消失问题
  • ReLU 的死亡神经元现象(负区间梯度为零)

5.1 版本神经网络实现

类结构定义

import numpy as np

class NeuralNetwork_5_1:
    """
    输入层维度: input_size
    隐藏层维度: hidden_size 
    输出层维度: output_size
    """
    def __init__(self, input_size, hidden_size, output_size):
        # 参数初始化(后续会讲 Xavier 方法)self.W1 = np.random.randn(input_size, hidden_size) * 0.01
        self.b1 = np.zeros((1, hidden_size))
        self.W2 = np.random.randn(hidden_size, output_size) * 0.01
        self.b2 = np.zeros((1, output_size))

前向传播实现

def forward(self, X):
    """
    输入: X (batch_size, input_size)
    输出: A2 (batch_size, output_size)
    """
    # 第一层计算
    self.Z1 = np.dot(X, self.W1) + self.b1  # (batch_size, hidden_size)
    self.A1 = np.tanh(self.Z1)  # 使用 tanh 激活

    # 输出层计算
    self.Z2 = np.dot(self.A1, self.W2) + self.b2  # (batch_size, output_size)
    self.A2 = self._sigmoid(self.Z2)  # 二分类输出用 sigmoid
    return self.A2

反向传播关键步骤

def backward(self, X, y, learning_rate):
    """
    输入:
        X (batch_size, input_size)
        y (batch_size, 1)
    """
    m = X.shape[0]  # 样本数

    # 输出层梯度
    dZ2 = self.A2 - y  # (batch_size, output_size)
    dW2 = np.dot(self.A1.T, dZ2) / m  # (hidden_size, output_size)
    db2 = np.sum(dZ2, axis=0, keepdims=True) / m

    # 隐藏层梯度
    dA1 = np.dot(dZ2, self.W2.T)  # (batch_size, hidden_size)
    dZ1 = dA1 * (1 - np.power(self.A1, 2))  # tanh 导数
    dW1 = np.dot(X.T, dZ1) / m  # (input_size, hidden_size)
    db1 = np.sum(dZ1, axis=0, keepdims=True) / m

    # 参数更新
    self.W2 -= learning_rate * dW2
    self.b2 -= learning_rate * db2
    self.W1 -= learning_rate * dW1
    self.b1 -= learning_rate * db1

SGD 优化器实现

def train(self, X, y, epochs, lr):
    """
    输入:
        X (n_samples, input_size)
        y (n_samples, 1)
    """
    losses = []
    for epoch in range(epochs):
        # 前向传播
        output = self.forward(X)

        # 计算损失(交叉熵)loss = -np.mean(y*np.log(output) + (1-y)*np.log(1-output))
        losses.append(loss)

        # 反向传播
        self.backward(X, y, lr)

        if epoch % 100 == 0:
            print(f'Epoch {epoch}, Loss: {loss:.4f}')
    return losses

避坑指南

梯度检查实现

def gradient_check(self, X, y, epsilon=1e-7):
    # 计算解析梯度
    self.forward(X)
    self.backward(X, y, lr=0.01)  # 不实际更新参数

    # 对 W1 做数值梯度近似
    grad_approx = np.zeros_like(self.W1)
    for i in range(self.W1.shape[0]):
        for j in range(self.W1.shape[1]):
            # 正向扰动
            self.W1[i,j] += epsilon
            loss_plus = self._compute_loss(X, y)

            # 负向扰动
            self.W1[i,j] -= 2*epsilon
            loss_minus = self._compute_loss(X, y)

            # 恢复原值
            self.W1[i,j] += epsilon

            # 中心差分
            grad_approx[i,j] = (loss_plus - loss_minus) / (2*epsilon)

    # 比较差异
    difference = np.linalg.norm(grad_approx - self.dW1) / \
                (np.linalg.norm(grad_approx) + np.linalg.norm(self.dW1))
    print(f"Gradient check: {difference}")

Loss 震荡调试

  1. 优先检查学习率:尝试将当前学习率除以 3 或 10
  2. 检查数据是否未做归一化(特别是输入值范围差异大时)
  3. 考虑添加动量(Momentum)项平滑更新方向

参数初始化经验

  • 使用 Xavier 初始化:W = np.random.randn(fan_in, fan_out) * np.sqrt(1/fan_in)
  • ReLU 系列激活建议用 He 初始化:sqrt(2/fan_in)
  • 避免全零初始化(会导致对称性问题)

思考题

  1. 如何改进代码支持 mini-batch 训练?
    需要增加数据 shuffle 逻辑,并在迭代时按 batch_size 切片数据

  2. 如果遇到 NaN 损失值应该检查哪些点?

  3. 检查是否存在 log(0) 情况(可加 epsilon 平滑)
  4. 验证梯度爆炸(添加梯度裁剪)
  5. 确认输入数据是否含非法值

  6. 为什么 ReLU 比 Sigmoid 更适合深层网络?

  7. 单边抑制特性缓解梯度消失
  8. 计算效率高(无需指数运算)
  9. 稀疏激活特性符合生物学认知
正文完
 0
评论(没有评论)