📌 本文属于《Python神经网络入门:零基础保姆级路线图》专栏

上一篇:python神经网络编程入门(三)----矩阵乘法真是神经网络的“偷懒神器”
下一篇:​​​​​​​python神经网络编程入门(五)----误差算出来了,权重到底怎么改?

完整目录 & 更新记录:
《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.05 和 0.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}")

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐