上一期,我们费了九牛二虎之力,终于搞懂了误差是怎么"甩锅"回去的。误差就像一张传单,从输出层一路传回了输入层,告诉每一个权重:"你错了多少!"
但光知道错了还不够啊!我们到底该怎么改?改多少?这就好比一个学生拿到了考卷,知道自己哪道题做错了(误差反向传播),但下一步呢?难道要瞎蒙一个正确答案吗?
当然不是!这一期,我们就来解决这个最核心、最激动人心的问题:如何利用这些误差,精准地调整每一个权重,让神经网络越学越聪明。
这背后的魔法,就叫——梯度下降。
一、为什么不能"一步到位"?
让我们先冷静一下,问问自己:为啥我们不干脆点,直接用数学算出那个"完美"的权重组合呢?
想想看,我们的神经网络,哪怕只有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 | 外积 |
你可以在此基础上扩展成循环训练多轮,或者加入批量样本处理,但当前的代码已经完整展示了一次迭代的全流程,非常适合配合文章学习。