上一期我们学会了用矩阵乘法这个“偷懒神器”做前向传播——把输入信号一层层往前推,最后算出输出结果。
但算完一看,结果跟正确答案差了一大截。问题出在哪?出在我们当初瞎编的那些权重上。
那怎么让这些权重“知错能改”呢?这就需要误差反向传播——把输出端的误差,一层层“甩锅”回前面的每一个权重,告诉它们:“你这次贡献了多少错误,下次该往哪个方向调整”。
别怕,这一期我们继续用矩阵这个“超级压缩包”,把反向传播也变成一行代码的事。
一、先搞懂一个核心问题:这个误差该谁来背?
假设你是一个项目组长,手下有两个组员:小权干活特别卖力(权重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.0,w₂ = 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.0O₁ 的误差
e₁ = 0.5,O₂ 的误差e₂ = 0.6
H₁分到的误差来自两条路:
从 O₁ 分来的:
e₁ × w₁₁ / (w₁₁ + w₂₁)从 O₂ 分来的:
e₂ × w₁₂ / (w₁₂ + w₂₂)
H₁的误差 = 这两部分加起来。
六、多层网络:从后往前,一层层往回传
如果网络有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.05和0.04。
第5步:继续往前传(如果有更多层,重复第2-4步)
九、总结:一张图看懂全部
| 方向 | 做什么 | 用什么工具 |
|---|---|---|
| 前向传播 | 输入信号 → 权重 → 输出 | 输出 = W × 输入 |
| 反向传播 | 输出误差 → 权重^T → 隐藏层误差 | 隐藏误差 = W^T × 输出误差 |
核心思想就两句话:
前向传播:信号从输入层流向输出层,用的是原矩阵
W。反向传播:误差从输出层流回输入层,用的是转置矩阵
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}")