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

上一篇:python神经网络编程入门(四)----神经网络误差反向传播完全图解
下一篇:​​​​​​​​​​​​​​python神经网络编程入门(六)——从零开始,敲出你的第一个神经网络

完整目录 & 更新记录:
《Python神经网络入门:零基础保姆级路线图(附全系列免费源码)》

上一期,我们终于搞懂了误差是怎么"甩锅"回去的。误差就像一张传单,从输出层一路传回了输入层,告诉每一个权重:"你错了多少!"

但光知道错了还不够啊!我们到底该怎么改?改多少? 这就好比一个学生拿到了考卷,知道自己哪道题做错了(误差反向传播),但下一步呢?难道要瞎蒙一个正确答案吗?

当然不是!这一期,我们就来解决这个最核心、最激动人心的问题:如何利用这些误差,精准地调整每一个权重,让神经网络越学越聪明。

这背后的魔法,就叫——梯度下降


一、为什么不能"一步到位"?

让我们先冷静一下,问问自己:为啥我们不干脆点,直接用数学算出那个"完美"的权重组合呢?

想想看,我们的神经网络,哪怕只有3层,每层3个神经元,它要算的数学公式已经复杂到让人想哭。更别提那些有几百层、几百万个参数的现代网络了。

那不用数学算,我们用笨办法——穷举呢?

假设每个权重有1000种可能的值,对于一个有18个权重的网络,就有 18000 种组合。每测试一种要1秒钟,那更新一次就要 5个小时!如果是一个有500个节点的网络,这个时间会飙升到 1.6万年

所以,无论是精密的代数计算,还是愚蠢的暴力穷举,都走不通。我们需要一个更聪明、更实际的方法。

二、摸黑下山:梯度下降的智慧

想象一下这个场景:你被蒙上眼睛,扔在了一片连绵起伏的群山中。你的任务,是找到这片山区的最低点(也就是"全局最小值")。

你看不见路,手里只有一个能探测脚下几米地形的"手电筒"。你会怎么做?

正确的做法是:用脚探一探周围,找到最陡的斜坡,然后朝着下坡的方向迈出一小步。 到了新地方,再重复这个动作——探路、下坡、迈步。就这样,一步一步,你终将抵达某个山谷的底部。

这个"摸黑下山"的过程,就是梯度下降的核心思想!

这里的"山",就是我们的误差函数;"你的位置",就是当前的权重组合;"最陡的下坡方向",就是梯度的反方向

三、为什么选"误差的平方"当导航地图?

在"摸黑下山"的比喻里,我们得先定义"山"在哪。这座"山"就是误差函数,它告诉我们当前的权重组合有多"糟糕"。

但我们该选哪个函数来当这座"山"呢?这里有三个常见的选项:

  1. 直接相减 (目标值 - 实际值):比如,两个节点的误差分别是 +0.1-0.1,加起来等于 0!这相当于告诉你"你没犯错",这显然是错误的导航。

  2. 取绝对值 (|目标值 - 实际值|):解决了正负抵消的问题。但它的图像像一个"V"字,在谷底有个尖角。如果你用梯度下降,会在那个尖角附近反复横跳,永远无法精准地停在最低点。

  3. 取平方 ((目标值 - 实际值)²):这是最常用的选择!因为它:

    • 图像平滑:像个光滑的碗,没有尖角,梯度下降可以稳稳地走到碗底。

    • 数学友好:对平方函数求导非常简单,这为后面的计算铺平了道路。

    • 自带"减速":越靠近谷底,坡度越平缓,你的步子会自动变小,不容易冲过头。

四、寻路神器:计算"梯度"

好,地图选好了(误差的平方),也知道了方法(梯度下降),那最关键的一步来了:我们怎么知道"最陡的下坡方向"在哪?

这个"最陡的下坡方向",在数学上就叫梯度。它是一个向量(可以理解为一个带方向的箭头),指明了误差函数上升最快的方向。而我们需要的,是它相反的方向——也就是负梯度方向

这个方向,就告诉了我们每一个权重具体应该调大还是调小,以及调多少最有效

这个计算过程,就是反向传播在做的事情。上一期我们费尽心思把误差传回去,就是为了计算这个"梯度"做准备。

五、解密核心公式:权重到底怎么改?

现在,我们要揭开神经网络最核心的秘密:权重更新的数学公式。别怕,我们一步步拆解它。

我们关注的是隐藏层输出层之间的一个权重,叫它 w_{j,k}。这个公式告诉我们,w_{j,k} 应该调整多少。

这个神奇的公式长这样:

Δw_{j,k} = -α × (目标值ₖ - 实际值ₖ) × S'(信号ₖ) × 输出ⱼ

看起来有点吓人?别怕,我们把它拆成四块,每一块都有非常直观的含义。

  1. (负的学习率):这是我们的"步长"控制器。

    • α (学习率):决定我们每次迈多大的步子。步子太大(α 太大)容易冲过头;步子太小(α 太小)则走得太慢。

    • 负号 -:非常重要!因为我们要往下坡走,所以要在梯度的前面加个负号,反着来

  2. (目标值ₖ - 实际值ₖ):这是输出层的误差。误差越大,说明错得越离谱,需要调整的幅度自然就越大。

  3. S'(信号ₖ):这是激活函数的斜率

    • S 代表S型函数(Sigmoid)。

    • 信号ₖ 是进入输出节点的加权和。

    • 这个部分告诉你,在当前这个"信号"强度下,改变权重对输出的影响有多大。如果S型函数已经"饱和"了(比如输出接近0或1),那它的斜率就接近0,改变权重也没什么效果了。

  4. 输出ⱼ:这是前一个隐藏层节点的输出。这个权重连接的前一个节点贡献的信号越强,调整它对最终结果的影响就越大。

这个公式把误差、信号的敏感度和前一层的影响力完美地结合在一起,告诉我们每一个权重具体该怎么调。

六、从"一个"到"一片":矩阵的终极简化

我们刚刚搞定了一个权重的调整公式。但一个神经网络里有成千上万个权重,难道我们要用for循环一个一个算吗?

当然不!这时候,我们的老朋友——矩阵,又登场了。

我们可以把所有公式打包成一个优雅的矩阵运算:

权重更新矩阵 = -α × (误差矩阵) × (S'矩阵) × (上一层输出矩阵)ᵀ

七、最终:把一切都串起来

现在,我们把这一期和上一期的所有知识串起来,看看神经网络的一次完整训练循环是怎样的:

  1. 前向传播:输入数据,通过现有的权重,一路计算,得到预测结果。

  2. 计算误差:用"误差的平方"作为地图,计算预测结果和真实值的差距。

  3. 反向传播:从输出层开始,将误差"甩"回每一层,为计算梯度做准备。

  4. 计算梯度:用我们刚学的核心公式,计算出每一个权重的调整方向(梯度)。

  5. 更新权重:朝着梯度的反方向,迈出一小步(由学习率 α 控制),更新所有权重。

然后,拿着更新后的权重,再去处理下一个数据样本。如此循环往复,成千上万次。

这,就是神经网络学习的全部秘密!它没有在"思考",只是在不断重复"犯错-计算-调整"的过程,直到把误差这座"山"踩在脚下。

现在你已经掌握了神经网络最核心的训练算法——反向传播梯度下降。下一期,我们将把这些公式变成可以运行的代码,让你亲手训练出第一个能"学习"的神经网络!
 

代码片段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外积

你可以在此基础上扩展成循环训练多轮,或者加入批量样本处理,但当前的代码已经完整展示了一次迭代的全流程,非常适合配合文章学习。

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐