python神经网络编程入门(四)----神经网络误差反向传播完全图解

📅 2026/7/8 1:44:16 👁️ 阅读次数 📝 编程学习
python神经网络编程入门(四)----神经网络误差反向传播完全图解

上一期我们学会了用矩阵乘法这个“偷懒神器”做前向传播——把输入信号一层层往前推,最后算出输出结果。

但算完一看,结果跟正确答案差了一大截。问题出在哪?出在我们当初瞎编的那些权重上。

那怎么让这些权重“知错能改”呢?这就需要误差反向传播——把输出端的误差,一层层“甩锅”回前面的每一个权重,告诉它们:“你这次贡献了多少错误,下次该往哪个方向调整”。

别怕,这一期我们继续用矩阵这个“超级压缩包”,把反向传播也变成一行代码的事。


一、先搞懂一个核心问题:这个误差该谁来背?

假设你是一个项目组长,手下有两个组员:小权干活特别卖力(权重3.0),小弱干活比较划水(权重1.0)。现在项目搞砸了,总共产生了4个单位的误差

问题来了:这4个误差,怎么分给两个人?

  • 方案一:平均分——每人背2个。但小权明明干了更多的活,让他背跟小弱一样多的锅,公平吗?

  • 方案二:按贡献分——小权出力是3,小弱出力是1,总共4份力气。小权背 3/4 的锅(3个),小弱背 1/4 的锅(1个)。

神经网络用的是方案二。

为什么要按权重比例分?因为权重越大,说明这条链接对最终结果的影响越大,那它“制造”的误差自然也应该承担更多。

二、从1个输出节点开始:误差怎么往回传?

先看最简单的情况:1个输入节点 → 1个隐藏节点 → 1个输出节点

假设输出节点的误差是e = 0.5。隐藏层和输出层之间的权重是w = 2.0

那这个0.5的误差要传给隐藏层多少?全部传过去——因为只有这一条路,没有别人跟它分。

所以隐藏层收到的误差就是0.5 × 2.0 = 1.0


三、多个输入节点:按权重比例分误差

现在复杂一点:2个输入节点 → 1个输出节点

两个输入节点通过两条链接连到同一个输出节点,权重分别是w₁ = 3.0w₂ = 1.0。输出节点的误差是e = 4.0

误差怎么分?

  • 链接1(权重3.0)分到的误差:4.0 × 3.0 / (3.0 + 1.0) = 4.0 × 3/4 = 3.0

  • 链接2(权重1.0)分到的误差:4.0 × 1.0 / (3.0 + 1.0) = 4.0 × 1/4 = 1.0

公式就是:

text

链接i分到的误差 = 总误差 × 链接i的权重 / (所有链接权重之和)

这个分母(w₁ + w₂)归一化因子,它的作用是让所有分到的误差加起来正好等于总误差。

四、多个输出节点:对每个输出节点重复操作

现在更复杂一点:2个输入节点 → 2个输出节点

两个输出节点各有各的误差:e₁e₂

做法很简单:对每个输出节点,分别做一次“按比例分割”。

  • 对于输出节点1(误差e₁):把e₁按权重w₁₁w₂₁的比例分给两条输入链接。

  • 对于输出节点2(误差e₂):把e₂按权重w₁₂w₂₂的比例分给两条输入链接。

两条路径互不影响,各算各的

五、隐藏层的误差怎么算?——把分到的误差加起来

现在到了最关键的问题:中间隐藏层的节点,它的误差是多少?

输出层的节点有目标值,可以直接算误差e = 目标值 - 实际值。但隐藏层的节点没有目标值啊!训练数据只告诉我们“最终输出应该是多少”,没告诉我们“中间层应该输出多少”。

那怎么办?

答案:隐藏层节点的误差 = 从它出发的所有链接分到的误差之和。

举个例子。假设隐藏层有2个节点(H₁、H₂),输出层有2个节点(O₁、O₂)。

  • 从 H₁ 到 O₁ 的权重是w₁₁ = 2.0,到 O₂ 的权重是w₁₂ = 3.0

  • O₁ 的误差e₁ = 0.5,O₂ 的误差e₂ = 0.6

H₁分到的误差来自两条路:

  • 从 O₁ 分来的:e₁ × w₁₁ / (w₁₁ + w₂₁)

  • 从 O₂ 分来的:e₂ × w₁₂ / (w₁₂ + w₂₂)

H₁的误差 = 这两部分加起来。

六、多层网络:从后往前,一层层往回传

如果网络有3层、4层、甚至更多层呢?

方法完全一样,就是“从后往前,逐层重复”。

  1. 先从输出层开始,算出输出层的误差。

  2. 把输出层的误差按权重比例分给隐藏层(最后一层),算出隐藏层每个节点的误差。

  3. 再把隐藏层的误差按权重比例分给更前面的一层

  4. 一直重复,直到传回输入层

这就是为什么叫“反向传播”(Backpropagation)——误差从输出端,

流回输入端。

七、矩阵闪亮登场:把反向传播变成一行代码

前面我们做的所有计算,如果一个个写出来,会是这样:

隐藏层误差₁ = e₁×w₁₁/(w₁₁+w₂₁) + e₂×w₁₂/(w₁₂+w₂₂) 隐藏层误差₂ = e₁×w₂₁/(w₁₁+w₂₁) + e₂×w₂₂/(w₁₂+w₂₂)

是不是已经开始头疼了?这才2×2,要是100×100呢?

这时候,矩阵乘法又来救你了。

仔细观察上面的式子,你会发现一个规律:隐藏层误差 = 权重矩阵的转置 × 输出误差

等等,“转置”是什么?

转置就是把矩阵的行和列互换。比如:

原矩阵 W = [1, 2] 转置后 W^T = [1, 3] [3, 4] [2, 4]

你看,原来在右上角的2,跑到了左下角;原来在左下角的3,跑到了右上角。

用转置矩阵,反向传播就变成了:

误差_隐藏层 = W^T × 误差_输出层

就这么简单!一行代码的事。

⚠️ 注意:这里我们偷偷省略了分母(归一化因子)。实践证明,省略归一化因子不影响最终效果,因为神经网络在后续的训练迭代中可以自己纠正。重要的是误差按权重比例分配这个原则。

八、完整流程串一遍(带具体数字)

我们用一个2输入 → 2隐藏 → 2输出的网络,把整个过程走一遍。

假设:

  • 输入:[0.5, 0.3]

  • 目标输出:[0.9, 0.1]

  • 实际输出(前向传播算出来的):[0.7, 0.2]

第1步:算输出层误差

text

e₁ = 0.9 - 0.7 = 0.2 e₂ = 0.1 - 0.2 = -0.1

输出误差矩阵:E_output = [0.2, -0.1]

第2步:拿出隐藏层→输出层的权重矩阵

text

W_hidden_output = [w₁₁, w₁₂] 假设 = [0.4, 0.6] [w₂₁, w₂₂] [0.3, 0.8]

第3步:转置权重矩阵

text

W_hidden_output^T = [0.4, 0.3] [0.6, 0.8]

第4步:矩阵乘法算隐藏层误差

text

E_hidden = W_hidden_output^T × E_output = [0.4, 0.3] × [0.2 ] = [0.4×0.2 + 0.3×(-0.1)] = [0.08 - 0.03] = [0.05] [0.6, 0.8] [-0.1] [0.6×0.2 + 0.8×(-0.1)] [0.12 - 0.08] [0.04]

结果:隐藏层两个节点的误差分别是0.050.04

第5步:继续往前传(如果有更多层,重复第2-4步)

九、总结:一张图看懂全部

方向做什么用什么工具
前向传播输入信号 → 权重 → 输出输出 = W × 输入
反向传播输出误差 → 权重^T → 隐藏层误差隐藏误差 = W^T × 输出误差

核心思想就两句话:

  1. 前向传播:信号从输入层流向输出层,用的是原矩阵W

  2. 反向传播:误差从输出层流回输入层,用的是转置矩阵W^T

为什么能这么干?因为矩阵乘法把成千上万条链接的计算,压缩成了一行代码。你不需要告诉计算机“先算这个再算那个”,只需要说“把这俩矩阵乘一下”,电脑唰一下就给你算完了。

下一期,我们要拿着算出来的误差,去真正调整权重——让神经网络从“瞎猜”变成“越猜越准”。保持你的好奇心,我们下期见!


代码片段1:用矩阵转置实现误差的“反向分配”

import numpy as np # ==================== 固定数据(保证前后连贯,无随机数) ==================== # 输出层误差(由 目标值 - 实际值 得到,延续文章中的数值) error_output = np.array([0.2, -0.1]) # [e1, e2] # 隐藏层 -> 输出层 权重矩阵 (行: 输出节点, 列: 隐藏节点) W_ho = np.array([ [0.4, 0.3], # 输出节点1 连接 隐藏节点1(0.4) 和 隐藏节点2(0.3) [0.6, 0.8] # 输出节点2 连接 隐藏节点1(0.6) 和 隐藏节点2(0.8) ]) # ==================== 1. 纯手工循环版(看懂底层原理) ==================== print("【1. 手工循环版】") # 为了避免下标混乱,把数值拆开 w11, w21 = 0.4, 0.3 # 输出节点1的权重 w12, w22 = 0.6, 0.8 # 输出节点2的权重 e1, e2 = 0.2, -0.1 # 输出误差 # 隐藏节点1 收到来自两个输出节点的误差之和 eh1 = (e1 * w11) + (e2 * w12) # 0.2*0.4 + (-0.1)*0.6 = 0.02 # 隐藏节点2 收到来自两个输出节点的误差之和 eh2 = (e1 * w21) + (e2 * w22) # 0.2*0.3 + (-0.1)*0.8 = -0.02 print(f"隐藏层误差: [{eh1}, {eh2}]") # ==================== 2. 矩阵优雅版(一行代码搞定) ==================== print("\n【2. 矩阵乘法版】") # 转置:行变列,列变行 # 原矩阵 [[0.4, 0.3], [0.6, 0.8]] 转置后变成 [[0.4, 0.6], [0.3, 0.8]] W_ho_T = W_ho.T # 核心公式:隐藏层误差 = 权重矩阵的转置 × 输出误差向量 # 计算过程: # 第一行 [0.4, 0.6] · [0.2, -0.1] = 0.4*0.2 + 0.6*(-0.1) = 0.02 # 第二行 [0.3, 0.8] · [0.2, -0.1] = 0.3*0.2 + 0.8*(-0.1) = -0.02 error_hidden = W_ho_T @ error_output print(f"隐藏层误差: {error_hidden}") # ==================== 3. 继续往前甩锅(多层网络反向传播) ==================== print("\n【3. 继续向前传播到输入层】") # 假设输入层 -> 隐藏层 权重矩阵 (行: 隐藏节点, 列: 输入节点) W_ih = np.array([ [0.1, 0.2], # 隐藏节点1:来自输入1的0.1,输入2的0.2 [0.3, 0.4] # 隐藏节点2:来自输入1的0.3,输入2的0.4 ]) # 把刚才算出的 error_hidden 当作新的“输出层误差”,重复转置乘法操作 error_input = W_ih.T @ error_hidden # 就这么简单,复制粘贴改个名 # 手算验证: # 输入节点1 = 0.1*0.02 + 0.3*(-0.02) = 0.002 - 0.006 = -0.004 # 输入节点2 = 0.2*0.02 + 0.4*(-0.02) = 0.004 - 0.008 = -0.004 print(f"输入层误差: {error_input}") # ==================== 4. 封装成函数(实战直接调用) ==================== print("\n【4. 封装函数实战】") def backpropagate_error(weight_matrix, output_error): """ 将输出层的误差反向传播到上一层 :param weight_matrix: 当前层到下一层的权重矩阵 (行: 下一层节点数, 列: 当前层节点数) :param output_error: 下一层的误差向量 :return: 当前层的误差向量 """ return weight_matrix.T @ output_error # 测试:从输出层传到隐藏层 e_hidden = backpropagate_error(W_ho, error_output) print(f"调用函数得到隐藏层误差: {e_hidden}") # 测试:继续从隐藏层传到输入层 e_input = backpropagate_error(W_ih, e_hidden) print(f"调用函数得到输入层误差: {e_input}")