python神经网络编程入门(五)----误差算出来了,权重到底怎么改?

发布时间:2026/7/9 2:36:46
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矩阵) × (上一层输出矩阵)ᵀ七、最终把一切都串起来现在我们把这一期和上一期的所有知识串起来看看神经网络的一次完整训练循环是怎样的前向传播输入数据通过现有的权重一路计算得到预测结果。计算误差用误差的平方作为地图计算预测结果和真实值的差距。反向传播从输出层开始将误差甩回每一层为计算梯度做准备。计算梯度用我们刚学的核心公式计算出每一个权重的调整方向梯度。更新权重朝着梯度的反方向迈出一小步由学习率α控制更新所有权重。然后拿着更新后的权重再去处理下一个数据样本。如此循环往复成千上万次。这就是神经网络学习的全部秘密它没有在思考只是在不断重复犯错-计算-调整的过程直到把误差这座山踩在脚下。现在你已经掌握了神经网络最核心的训练算法——反向传播和梯度下降。下一期我们将把这些公式变成可以运行的代码让你亲手训练出第一个能学习的神经网络代码片段1import 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外积你可以在此基础上扩展成循环训练多轮或者加入批量样本处理但当前的代码已经完整展示了一次迭代的全流程非常适合配合文章学习。