----神经网络误差反向传播完全图解)
上一期我们学会了用矩阵乘法这个“偷懒神器”做前向传播——把输入信号一层层往前推最后算出输出结果。但算完一看结果跟正确答案差了一大截。问题出在哪出在我们当初瞎编的那些权重上。那怎么让这些权重“知错能改”呢这就需要误差反向传播——把输出端的误差一层层“甩锅”回前面的每一个权重告诉它们“你这次贡献了多少错误下次该往哪个方向调整”。别怕这一期我们继续用矩阵这个“超级压缩包”把反向传播也变成一行代码的事。一、先搞懂一个核心问题这个误差该谁来背假设你是一个项目组长手下有两个组员小权干活特别卖力权重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.0O₁ 的误差e₁ 0.5O₂ 的误差e₂ 0.6H₁分到的误差来自两条路从 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步算输出层误差texte₁ 0.9 - 0.7 0.2 e₂ 0.1 - 0.2 -0.1输出误差矩阵E_output [0.2, -0.1]第2步拿出隐藏层→输出层的权重矩阵textW_hidden_output [w₁₁, w₁₂] 假设 [0.4, 0.6] [w₂₁, w₂₂] [0.3, 0.8]第3步转置权重矩阵textW_hidden_output^T [0.4, 0.3] [0.6, 0.8]第4步矩阵乘法算隐藏层误差textE_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})