神经网络是怎么算出结果的:前向传播与反向传播原理拆解
神经网络之所以能“学会”东西,是因为它把输入经过一系列加权求和与非线性变换得到输出,再根据输出和正确答案的差距反向调整参数。
理解这条链路,比背公式更重要。
下面用一个单隐藏层网络的手算例子,把前向传播和反向传播拆开讲清楚,最后用 NumPy 跑一遍验证。
一个神经元到底在算什么
先看最基础的结构。
一个神经元接收若干输入,每个输入乘一个权重,加上偏置,再经过激活函数得到输出:
z = w1*x1 + w2*x2 + b
a = f(z)
其中 w 是权重,决定某个输入有多重要;b 是偏置,调整激活的阈值;f 是激活函数,常见的有 ReLU、Sigmoid、Tanh。
没有激活函数,多层网络叠加起来仍然等价于一个线性变换,表达能力有限。
举例:输入 x1=1.0, x2=2.0,权重 w1=0.5, w2=-0.3,偏置 b=0.1,则 z = 0.5*1.0 + (-0.3)*2.0 + 0.1 = 0。
若用 ReLU,输出 a = max(0, 0) = 0;
若用 Sigmoid,输出约 0.5。
前向传播:把输入一层层推到输出
前向传播就是按网络顺序做上面的计算。
以一个两层网络为例:输入层 2 个节点,隐藏层 2 个节点,输出层 1 个节点。
第一层隐藏层计算:
z1_1 = w11*x1 + w12*x2 + b1_1
a1_1 = relu(z1_1)
z1_2 = w21*x1 + w22*x2 + b1_2
a1_2 = relu(z1_2)
输出层计算:
z2 = v1*a1_1 + v2*a1_2 + b2
y_hat = sigmoid(z2)
y_hat 就是模型给出的预测值。
整个过程只有矩阵乘法和激活函数,计算量集中在矩阵乘法上,所以实际框架会用 GPU 加速。
反向传播:误差怎么传回每一层
训练时我们有一组带标签的数据,比如输入对应真实标签 y。
先定义损失函数,二分类常用交叉熵:
L = -[y*log(y_hat) + (1-y)*log(1-y_hat)]
反向传播的目标是求损失对每个权重和偏置的偏导数,也就是梯度。
核心是链式法则:输出层的误差先算出来,再乘上权重传回隐藏层。
对输出层:
dL/dz2 = y_hat - y
dL/dv1 = dL/dz2 * a1_1
dL/db2 = dL/dz2
对隐藏层,误差通过 v1、v2 回传,再乘激活函数的导数。
ReLU 的导数在 z>0 时为 1,否则为 0:
dL/da1_1 = dL/dz2 * v1
dL/dz1_1 = dL/da1_1 * (z1_1 > 0)
dL/dw11 = dL/dz1_1 * x1
得到梯度后,用梯度下降更新参数:
w = w - learning_rate * dL/dw
学习率太大会震荡甚至发散,太小则收敛慢。
常见做法是从 0.001 到 0.01 开始试,观察损失曲线。
用 NumPy 跑一遍并做梯度检查
下面代码实现一个单隐藏层网络的前向和反向传播,并用数值梯度检查解析梯度是否正确:
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-x))
# 输入、真实标签
x = np.array([1.0, 2.0])
y = 1.0
# 参数初始化
W1 = np.array([[0.1, 0.2], [0.3, 0.4]])
b1 = np.array([0.0, 0.1])
W2 = np.array([0.5, -0.2])
b2 = 0.3
# 前向
z1 = W1 @ x + b1
a1 = np.maximum(0, z1)
z2 = W2 @ a1 + b2
y_hat = sigmoid(z2)
loss = -(y * np.log(y_hat) + (1 - y) * np.log(1 - y_hat))
# 反向
dz2 = y_hat - y
dW2 = dz2 * a1
db2_grad = dz2
da1 = dz2 * W2
dz1 = da1 * (z1 > 0)
dW1 = np.outer(dz1, x)
db1_grad = dz1
# 数值梯度检查(只检查 W2 第一个元素)
eps = 1e-6
W2_plus = W2.copy(); W2_plus[0] += eps
W2_minus = W2.copy(); W2_minus[0] -= eps
z2p = W2_plus @ a1 + b2
yp = sigmoid(z2p)
loss_p = -(y * np.log(yp) + (1 - y) * np.log(1 - yp))
z2m = W2_minus @ a1 + b2
ym = sigmoid(z2m)
loss_m = -(y * np.log(ym) + (1 - y) * np.log(1 - ym))
num_grad = (loss_p - loss_m) / (2 * eps)
print("解析梯度:", dW2[0], "数值梯度:", num_grad)
运行后两者应非常接近(差异在 1e-6 量级内)。
如果差距大,通常是链式法则漏乘了某一项,或者激活函数导数用错。
新手最容易卡住的几个地方
权重初始化不要全零。 全零会导致同一层所有神经元学到相同特征,对称性无法打破。
常用小随机数,如 np.random.randn(...) * 0.01。
损失不下降先查学习率和数据。 学习率过大时损失可能变成 nan,过小则几乎不动。
确认输入数据已归一化,标签编码正确。
ReLU 死亡问题。 如果某个神经元的 z 长期为负,梯度恒为 0,参数不再更新。
可以换 Leaky ReLU 或调小学习率。
矩阵维度对不上。 报错 shapes not aligned 时检查 W1 @ x 中 W1 的列数是否等于 x 的长度,反向传播时转置是否用对。
验证理解是否到位
能独立回答这三个问题,说明原理已经通了:给定一组权重和输入,能否手算出输出;
能否写出交叉熵对输出层输入的导数;
能否解释为什么反向传播要从后往前算。
再用上面的梯度检查代码跑一次,观察解析梯度和数值梯度是否一致,比反复看公式更有效。