共计 3091 个字符,预计需要花费 8 分钟才能阅读完成。
前言
反向传播算法是训练神经网络的核心,但很多初学者在面对公式推导时容易感到困惑。本文将通过一个具体的数值例题,手把手带你完成前向传播和反向传播的全过程计算,最后用 Python 代码实现。我们将采用 3 层网络结构(输入层、隐藏层、输出层),使用 Sigmoid 激活函数,并假设损失函数为均方误差(MSE)。

网络结构与例题数据
假设我们有一个简单的 3 层神经网络:
- 输入层:2 个神经元(x1, x2)
- 隐藏层:2 个神经元(h1, h2)
- 输出层:1 个神经元(y)
初始权重和偏置随机初始化如下:
w1 = [[0.1, 0.2], [0.3, 0.4]] # 输入层到隐藏层的权重
b1 = [0.5, 0.6] # 隐藏层的偏置
w2 = [[0.7], [0.8]] # 隐藏层到输出层的权重
b2 = [0.9] # 输出层的偏置
输入数据 x = [0.1, 0.2],真实标签 y_true = 0.5。
前向传播计算
- 输入层到隐藏层的计算:
z1 = x * w1 + b1
h = sigmoid(z1)
具体数值计算:
z1_1 = 0.1*0.1 + 0.2*0.3 + 0.5 = 0.01 + 0.06 + 0.5 = 0.57
z1_2 = 0.1*0.2 + 0.2*0.4 + 0.6 = 0.02 + 0.08 + 0.6 = 0.70
h1 = sigmoid(0.57) ≈ 0.638
h2 = sigmoid(0.70) ≈ 0.668
- 隐藏层到输出层的计算:
z2 = h * w2 + b2
y_pred = sigmoid(z2)
具体数值计算:
z2 = 0.638*0.7 + 0.668*0.8 + 0.9 ≈ 0.4466 + 0.5344 + 0.9 ≈ 1.881
y_pred = sigmoid(1.881) ≈ 0.868
反向传播计算
- 计算输出层误差:
δ_output = (y_pred - y_true) * sigmoid_derivative(z2)
其中 sigmoid_derivative(z) = sigmoid(z)*(1-sigmoid(z))
数值计算:
δ_output = (0.868 - 0.5) * (0.868*(1-0.868)) ≈ 0.368 * 0.114 ≈ 0.042
- 计算隐藏层误差:
δ_hidden = (w2 * δ_output) * sigmoid_derivative(z1)
数值计算:
δ_hidden1 = (0.7 * 0.042) * (0.638*(1-0.638)) ≈ 0.0294 * 0.231 ≈ 0.0068
δ_hidden2 = (0.8 * 0.042) * (0.668*(1-0.668)) ≈ 0.0336 * 0.222 ≈ 0.0075
- 更新权重和偏置:
学习率 η 设为 0.5
-
更新 w2 和 b2:
Δw2 = -η * δ_output * h Δb2 = -η * δ_output数值计算:
Δw2_1 = -0.5 * 0.042 * 0.638 ≈ -0.0134 Δw2_2 = -0.5 * 0.042 * 0.668 ≈ -0.0140 Δb2 = -0.5 * 0.042 ≈ -0.021 w2_1_new = 0.7 - 0.0134 ≈ 0.6866 w2_2_new = 0.8 - 0.0140 ≈ 0.7860 b2_new = 0.9 - 0.021 ≈ 0.879 -
更新 w1 和 b1:
Δw1 = -η * δ_hidden * x Δb1 = -η * δ_hidden数值计算:
Δw1_11 = -0.5 * 0.0068 * 0.1 ≈ -0.00034 Δw1_12 = -0.5 * 0.0068 * 0.2 ≈ -0.00068 Δw1_21 = -0.5 * 0.0075 * 0.1 ≈ -0.000375 Δw1_22 = -0.5 * 0.0075 * 0.2 ≈ -0.00075 Δb1_1 = -0.5 * 0.0068 ≈ -0.0034 Δb1_2 = -0.5 * 0.0075 ≈ -0.00375 w1_11_new = 0.1 - 0.00034 ≈ 0.09966 w1_12_new = 0.2 - 0.00068 ≈ 0.19932 w1_21_new = 0.3 - 0.000375 ≈ 0.299625 w1_22_new = 0.4 - 0.00075 ≈ 0.39925 b1_1_new = 0.5 - 0.0034 ≈ 0.4966 b1_2_new = 0.6 - 0.00375 ≈ 0.59625
Python 实现
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def sigmoid_derivative(x):
return x * (1 - x)
# 初始化参数
w1 = np.array([[0.1, 0.2], [0.3, 0.4]])
b1 = np.array([0.5, 0.6])
w2 = np.array([[0.7], [0.8]])
b2 = np.array([0.9])
# 输入数据
x = np.array([0.1, 0.2])
y_true = 0.5
# 前向传播
z1 = np.dot(x, w1) + b1
h = sigmoid(z1)
z2 = np.dot(h, w2) + b2
y_pred = sigmoid(z2)
# 计算损失
loss = 0.5 * (y_pred - y_true) ** 2
# 反向传播
# 输出层误差
delta_output = (y_pred - y_true) * sigmoid_derivative(y_pred)
# 隐藏层误差
delta_hidden = np.dot(w2, delta_output) * sigmoid_derivative(h)
# 更新参数
learning_rate = 0.5
# 更新 w2 和 b2
w2 -= learning_rate * np.dot(h.reshape(-1,1), delta_output.reshape(1,-1))
b2 -= learning_rate * delta_output
# 更新 w1 和 b1
w1 -= learning_rate * np.dot(x.reshape(-1,1), delta_hidden.reshape(1,-1))
b1 -= learning_rate * delta_hidden
print(f"预测值: {y_pred[0]:.4f}")
print(f"更新后的 w1:\n{w1}")
print(f"更新后的 b1: {b1}")
print(f"更新后的 w2:\n{w2}")
print(f"更新后的 b2: {b2}")
常见问题
- 梯度消失问题的成因:
- 当使用 sigmoid 或 tanh 激活函数时,它们的导数在输入值较大或较小时会趋近于 0
- 在深层网络中,这些小的梯度会通过链式法则连乘,导致前面层的梯度变得极小
-
解决方案包括使用 ReLU 激活函数、残差连接或梯度裁剪
-
学习率对收敛的影响:
- 学习率过大可能导致震荡甚至发散
- 学习率过小会导致收敛速度过慢
-
实践中可以使用学习率衰减或自适应学习率算法(如 Adam)
-
激活函数选择的考量:
- sigmoid 适合二分类输出层,但容易出现梯度消失
- ReLU 计算简单且能缓解梯度消失,但可能有 ” 死亡神经元 ” 问题
- tanh 输出范围 (-1,1),适合需要负输出的场景
- 输出层激活函数选择取决于任务类型(如 softmax 用于多分类)
延伸思考
- 如何验证反向传播实现的正确性?
- 可以使用数值梯度检验(finite difference method)
- 对每个参数加入微小扰动,观察损失函数变化
-
比较数值梯度与反向传播计算的梯度是否接近
-
批量训练与在线训练的差异比较:
- 批量训练使用全部数据计算梯度,更新更准确但计算量大
- 在线训练(随机梯度下降)每次用一个样本,更新快但波动大
- 小批量梯度下降是折中方案,兼顾稳定性和效率
总结
通过这个具体的计算示例,我们完整走了一遍 BP 神经网络的前向传播和反向传播过程。关键点包括:
- 前向传播时按层计算激活值
- 反向传播时从输出层开始,逐层计算误差并传播
- 参数更新时要使用学习率控制步长
- 激活函数的选择和实现细节会影响训练效果
建议读者尝试修改网络结构、激活函数或学习率,观察对训练效果的影响,这能帮助更深入地理解反向传播算法。
正文完
