python神经网络编程入门(五)----误差算出来了,权重到底怎么改?
📌 本文属于《Python神经网络入门:零基础保姆级路线图》专栏
上一篇:python神经网络编程入门(四)----神经网络误差反向传播完全图解
下一篇:python神经网络编程入门(六)——从零开始,敲出你的第一个神经网络完整目录 & 更新记录:
《Python神经网络入门:零基础保姆级路线图(附全系列免费源码)》
上一期,我们终于搞懂了误差是怎么"甩锅"回去的。误差就像一张传单,从输出层一路传回了输入层,告诉每一个权重:"你错了多少!"
但光知道错了还不够啊!我们到底该怎么改?改多少? 这就好比一个学生拿到了考卷,知道自己哪道题做错了(误差反向传播),但下一步呢?难道要瞎蒙一个正确答案吗?
当然不是!这一期,我们就来解决这个最核心、最激动人心的问题:如何利用这些误差,精准地调整每一个权重,让神经网络越学越聪明。
这背后的魔法,就叫——梯度下降。
一、为什么不能"一步到位"?
让我们先冷静一下,问问自己:为啥我们不干脆点,直接用数学算出那个"完美"的权重组合呢?
想想看,我们的神经网络,哪怕只有3层,每层3个神经元,它要算的数学公式已经复杂到让人想哭。更别提那些有几百层、几百万个参数的现代网络了。
那不用数学算,我们用笨办法——穷举呢?
假设每个权重有1000种可能的值,对于一个有18个权重的网络,就有 18000 种组合。每测试一种要1秒钟,那更新一次就要 5个小时!如果是一个有500个节点的网络,这个时间会飙升到 1.6万年!
所以,无论是精密的代数计算,还是愚蠢的暴力穷举,都走不通。我们需要一个更聪明、更实际的方法。
二、摸黑下山:梯度下降的智慧
想象一下这个场景:你被蒙上眼睛,扔在了一片连绵起伏的群山中。你的任务,是找到这片山区的最低点(也就是"全局最小值")。
你看不见路,手里只有一个能探测脚下几米地形的"手电筒"。你会怎么做?
正确的做法是:用脚探一探周围,找到最陡的斜坡,然后朝着下坡的方向迈出一小步。 到了新地方,再重复这个动作——探路、下坡、迈步。就这样,一步一步,你终将抵达某个山谷的底部。
这个"摸黑下山"的过程,就是梯度下降的核心思想!
这里的"山",就是我们的误差函数;"你的位置",就是当前的权重组合;"最陡的下坡方向",就是梯度的反方向。

三、为什么选"误差的平方"当导航地图?
在"摸黑下山"的比喻里,我们得先定义"山"在哪。这座"山"就是误差函数,它告诉我们当前的权重组合有多"糟糕"。
但我们该选哪个函数来当这座"山"呢?这里有三个常见的选项:
-
直接相减 (
目标值 - 实际值):比如,两个节点的误差分别是+0.1和-0.1,加起来等于0!这相当于告诉你"你没犯错",这显然是错误的导航。 -
取绝对值 (
|目标值 - 实际值|):解决了正负抵消的问题。但它的图像像一个"V"字,在谷底有个尖角。如果你用梯度下降,会在那个尖角附近反复横跳,永远无法精准地停在最低点。 -
取平方 (
(目标值 - 实际值)²):这是最常用的选择!因为它:-
图像平滑:像个光滑的碗,没有尖角,梯度下降可以稳稳地走到碗底。
-
数学友好:对平方函数求导非常简单,这为后面的计算铺平了道路。
-
自带"减速":越靠近谷底,坡度越平缓,你的步子会自动变小,不容易冲过头。
-

四、寻路神器:计算"梯度"
好,地图选好了(误差的平方),也知道了方法(梯度下降),那最关键的一步来了:我们怎么知道"最陡的下坡方向"在哪?
这个"最陡的下坡方向",在数学上就叫梯度。它是一个向量(可以理解为一个带方向的箭头),指明了误差函数上升最快的方向。而我们需要的,是它相反的方向——也就是负梯度方向。
这个方向,就告诉了我们每一个权重具体应该调大还是调小,以及调多少最有效。
这个计算过程,就是反向传播在做的事情。上一期我们费尽心思把误差传回去,就是为了计算这个"梯度"做准备。
五、解密核心公式:权重到底怎么改?
现在,我们要揭开神经网络最核心的秘密:权重更新的数学公式。别怕,我们一步步拆解它。
我们关注的是隐藏层和输出层之间的一个权重,叫它 w_{j,k}。这个公式告诉我们,w_{j,k} 应该调整多少。
这个神奇的公式长这样:
Δw_{j,k} = -α × (目标值ₖ - 实际值ₖ) × S'(信号ₖ) × 输出ⱼ
看起来有点吓人?别怕,我们把它拆成四块,每一块都有非常直观的含义。
-
-α(负的学习率):这是我们的"步长"控制器。-
α(学习率):决定我们每次迈多大的步子。步子太大(α太大)容易冲过头;步子太小(α太小)则走得太慢。 -
负号
-:非常重要!因为我们要往下坡走,所以要在梯度的前面加个负号,反着来。
-
-
(目标值ₖ - 实际值ₖ):这是输出层的误差。误差越大,说明错得越离谱,需要调整的幅度自然就越大。 -
S'(信号ₖ):这是激活函数的斜率。-
S代表S型函数(Sigmoid)。 -
信号ₖ是进入输出节点的加权和。 -
这个部分告诉你,在当前这个"信号"强度下,改变权重对输出的影响有多大。如果S型函数已经"饱和"了(比如输出接近0或1),那它的斜率就接近0,改变权重也没什么效果了。
-
-
输出ⱼ:这是前一个隐藏层节点的输出。这个权重连接的前一个节点贡献的信号越强,调整它对最终结果的影响就越大。
这个公式把误差、信号的敏感度和前一层的影响力完美地结合在一起,告诉我们每一个权重具体该怎么调。

六、从"一个"到"一片":矩阵的终极简化
我们刚刚搞定了一个权重的调整公式。但一个神经网络里有成千上万个权重,难道我们要用for循环一个一个算吗?
当然不!这时候,我们的老朋友——矩阵,又登场了。
我们可以把所有公式打包成一个优雅的矩阵运算:
权重更新矩阵 = -α × (误差矩阵) × (S'矩阵) × (上一层输出矩阵)ᵀ

七、最终:把一切都串起来
现在,我们把这一期和上一期的所有知识串起来,看看神经网络的一次完整训练循环是怎样的:
-
前向传播:输入数据,通过现有的权重,一路计算,得到预测结果。
-
计算误差:用"误差的平方"作为地图,计算预测结果和真实值的差距。
-
反向传播:从输出层开始,将误差"甩"回每一层,为计算梯度做准备。
-
计算梯度:用我们刚学的核心公式,计算出每一个权重的调整方向(梯度)。
-
更新权重:朝着梯度的反方向,迈出一小步(由学习率
α控制),更新所有权重。
然后,拿着更新后的权重,再去处理下一个数据样本。如此循环往复,成千上万次。
这,就是神经网络学习的全部秘密!它没有在"思考",只是在不断重复"犯错-计算-调整"的过程,直到把误差这座"山"踩在脚下。
现在你已经掌握了神经网络最核心的训练算法——反向传播和梯度下降。下一期,我们将把这些公式变成可以运行的代码,让你亲手训练出第一个能"学习"的神经网络!
代码片段1:
import numpy as np
# ============================= 1. 设置固定数据(保证可复现) =============================
# 输入样本(来自前文)
X = np.array([0.5, 0.3]) # 输入层 (2,)
target = np.array([0.9, 0.1]) # 目标输出 (2,)
# 初始权重(延续前文的数值)
# 输入层 -> 隐藏层 权重矩阵 (行: 隐藏节点, 列: 输入节点)
W_ih = np.array([
[0.1, 0.2], # 隐藏节点1
[0.3, 0.4] # 隐藏节点2
])
# 隐藏层 -> 输出层 权重矩阵 (行: 输出节点, 列: 隐藏节点)
W_ho = np.array([
[0.4, 0.3], # 输出节点1
[0.6, 0.8] # 输出节点2
])
# 学习率
alpha = 0.1
# ============================= 2. 定义激活函数及其导数 =============================
def sigmoid(x):
"""S型激活函数"""
return 1 / (1 + np.exp(-x))
def sigmoid_derivative(x):
"""S型函数的导数(假设x已经是经过激活后的输出,即 sigmoid(x))"""
# 如果用输出值 o 表示,则导数为 o * (1 - o)
return x * (1 - x)
# ============================= 3. 前向传播 =============================
print("--- 前向传播 ---")
# 隐藏层:加权和 + 激活
hidden_input = W_ih @ X # 进入隐藏层的信号 (2,)
hidden_output = sigmoid(hidden_input) # 隐藏层输出 (2,)
# 输出层:加权和 + 激活
final_input = W_ho @ hidden_output # 进入输出层的信号 (2,)
final_output = sigmoid(final_input) # 网络最终输出 (2,)
print(f"输入: {X}")
print(f"隐藏层输出: {hidden_output}")
print(f"网络输出: {final_output}")
print(f"目标输出: {target}")
# ============================= 4. 计算输出层误差 =============================
output_error = target - final_output # 输出层误差向量 (2,)
print(f"\n输出层误差: {output_error}")
# 总误差(平方和,可作监控)
loss = np.sum(output_error ** 2)
print(f"总误差 (平方和): {loss:.4f}")
# ============================= 5. 反向传播:计算梯度并更新输出层权重 =============================
# 计算输出层梯度
# 公式: dE/dW_ho = - (target - output) * sigmoid'(input) * hidden_output
# 其中 sigmoid'(input) = output * (1 - output)
# 注意:因为我们要做梯度下降(减梯度),所以直接计算负梯度即可。
# 这里我们计算 delta_output = (output - target) * sigmoid'(input) ,
# 然后权重更新量 = - alpha * delta_output * hidden_output^T
delta_output = (final_output - target) * sigmoid_derivative(final_output) # (2,)
gradient_ho = np.outer(delta_output, hidden_output) # (2,2) 外积
# 更新权重 (梯度下降)
W_ho_new = W_ho - alpha * gradient_ho
print("\n--- 更新隐藏层->输出层权重 ---")
print("更新前:\n", W_ho)
print("梯度:\n", gradient_ho)
print("更新后:\n", W_ho_new)
# ============================= 6. 反向传播:计算隐藏层误差并更新输入层权重 =============================
# 隐藏层误差 = W_ho^T * delta_output (因为 delta_output 已经包含了输出层的梯度)
# 注意:这里隐藏层没有目标值,误差由输出层反传回来。
hidden_error = W_ho.T @ delta_output # (2,)
# 计算隐藏层梯度
# delta_hidden = hidden_error * sigmoid'(hidden_output)
delta_hidden = hidden_error * sigmoid_derivative(hidden_output) # (2,)
gradient_ih = np.outer(delta_hidden, X) # (2,2) 外积
# 更新权重
W_ih_new = W_ih - alpha * gradient_ih
print("\n--- 更新输入层->隐藏层权重 ---")
print("更新前:\n", W_ih)
print("梯度:\n", gradient_ih)
print("更新后:\n", W_ih_new)
# ============================= 7. 验证更新后的效果(可选) =============================
# 用新权重再次前向传播,观察误差是否减小
hidden_input_new = W_ih_new @ X
hidden_output_new = sigmoid(hidden_input_new)
final_input_new = W_ho_new @ hidden_output_new
final_output_new = sigmoid(final_input_new)
new_error = target - final_output_new
new_loss = np.sum(new_error ** 2)
print("\n--- 更新后验证 ---")
print(f"更新后网络输出: {final_output_new}")
print(f"更新后误差: {new_error}")
print(f"更新前总误差: {loss:.4f}")
print(f"更新后总误差: {new_loss:.4f}")
print(f"误差减小: {loss - new_loss:.4f}")


注意:由于初始权重是随机选取且网络规模很小,单次更新的效果可能不明显,甚至误差可能略微上升(因为步长 α=0.1 可能略大)。在实际训练中,经过多次迭代后误差会稳定下降。你可以将 alpha 调小(如 0.01)并循环训练多次,即可看到更明显的效果。
代码结构说明:
| 步骤 | 对应数学公式 | 实现方式 |
|---|---|---|
| 前向传播 | O = sigmoid(W · X) | 矩阵乘法 + sigmoid() |
| 输出层误差 | E = target - output | 向量减法 |
| 输出层梯度 | δ_out = (output - target) * sigmoid'(output) | 逐元素乘 |
| 权重梯度 | ∇W_ho = δ_out · hidden^T | 外积 np.outer |
| 权重更新 | W_new = W - α · ∇W | 减法 |
| 隐藏层误差 | δ_hidden = (W_ho^T · δ_out) * sigmoid'(hidden) | 矩阵乘 + 逐元素乘 |
| 隐藏层权重梯度 | ∇W_ih = δ_hidden · X^T | 外积 |
你可以在此基础上扩展成循环训练多轮,或者加入批量样本处理,但当前的代码已经完整展示了一次迭代的全流程,非常适合配合文章学习。
更多推荐




所有评论(0)