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

上一篇:​​​​​​​python神经网络编程入门(五)----误差算出来了,权重到底怎么改?
下一篇:​​​​​​​​​​​​​​​​​​​​​python神经网络编程入门(七)——跑通MNIST手写数字识别验证训练成果与测试准确率。

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

我们来把上一期的理论,变成实实在在能跑的代码。

上一期我们花了大把时间,搞懂了梯度下降这个“摸黑下山”的道理,也看明白了那个让很多人头大的权重更新公式:

Δw = -α × (误差) × (激活函数斜率) × (上一层输出)

公式摆在那,但光看它还是觉得有点虚。这一期,我们就亲手把这个公式变成Python代码,让你亲眼看着一个神经网络从“啥也不会”到“慢慢学会”的过程。

我们从最简单的开始,一步一步来。你不需要任何机器学习框架,只用最基本的 numpy,就能搭建出一个完整的、能学习的神经网络。


先别急,聊聊我们到底要做什么?

在敲任何代码之前,我们先想清楚一个问题:一个神经网络,最核心、最不能少的东西是什么?

📌 停一下,想一想再往下看。

你的答案可能有很多:节点、层、激活函数……都没错。但如果我们把“漂亮的外表”都剥掉,只剩骨架,神经网络其实就两样东西:

  1. 一堆权重(数字)

  2. 能做乘法、加法、乘法的能力(计算)

对,就这么简单。输入来了,乘以权重,加上偏置(这里我们先忽略偏置),再经过激活函数,得到输出。整个过程就是 数字的流动

所以,我们的代码核心任务就是:管理好这一堆数字,让它们能正确地流动起来。

第一步:先搭架子——写一个空的类

好,既然核心是“管理数据”,用Python的类(class)来管理是最合适的。这就好比我们用一个文件夹把所有相关的文件整理在一起。

先写一个最空的架子:

import numpy as np          # 我们依赖numpy做矩阵运算
import scipy.special        # 我们用它提供的S函数

class NeuralNetwork:
    """一个三层神经网络"""
    
    def __init__(self):
        # 这里先什么都不做
        pass
    
    def query(self):
        # 这里也先什么都不做
        pass
    
    def train(self):
        # 这里也先什么都不做
        pass

好,停!

📌 我们来拆解一下:

  • class NeuralNetwork: 是我们定义了一个“蓝图纸”。

  • __init__ 是“建造师”,每次创建一个新的网络,都会先调用它来搭骨架。

  • query 是“问路员”,你给输入,它告诉你输出。

  • train 是“教练员”,你给输入和正确答案,它负责调整网络内部。

这一部分不要求能跑通,它只是我们的“计划书”。 你先敲完这三段 pass,感受一下这个空架子。

第二步:让“建造师”干活——完善 __init__

现在,我们来告诉 __init__,它到底要搭些什么。

一个网络得知道自己有几层、每层几个节点。所以我们先给它三个数字:输入节点数、隐藏节点数、输出节点数。另外,还得告诉它学习率(也就是我们迈步子的大小)。

class NeuralNetwork:
    def __init__(self, input_nodes, hidden_nodes, output_nodes, learning_rate):
        # 记住这些数字,以后要用
        self.inodes = input_nodes
        self.hnodes = hidden_nodes
        self.onodes = output_nodes
        self.lr = learning_rate

好,到这里停一下。你现在创建了一个网络对象,它能记住自己的“身材”了。但我们还需要它“长肉”——也就是权重。

📌 先问自己一个问题:权重应该是什么形状?

  • 输入层到隐藏层:隐藏层有多少个节点,就要有多少行;输入层有多少个节点,就要有多少列。所以是 (hidden_nodes, input_nodes)

  • 隐藏层到输出层:同理,是 (output_nodes, hidden_nodes)

为了避免网络“太胖”或者“太瘦”,我们使用小的随机数来初始化权重:

        # 初始化权重矩阵:用正态分布采样,均值0,标准差为1/sqrt(传入链接数)
        self.wih = np.random.normal(0.0, pow(self.hnodes, -0.5), (self.hnodes, self.inodes))
        self.who = np.random.normal(0.0, pow(self.onodes, -0.5), (self.onodes, self.hnodes))

注意这里: pow(self.hnodes, -0.5) 就是 1 / sqrt(隐藏层节点数)。这是经验规则,防止权重太大导致网络“吃饱了撑的”(饱和)。

sqrt是算数平方根的意思哈!!!(这里前面一期提到过哈)

公式: 权重范围 ≈ ±1/√(传入链接数)

为什么用 1/√n

假设一个节点有 n 条输入链接,每条链接的权重是 w,输入信号是 x(假设 x 在0~1之间)。

这个节点收到的总信号是:总信号 = w₁x₁ + w₂x₂ + ... + wₙxₙ

如果每个 w 都很大(比如 ±1),那么 n 个加起来,总信号会大到离谱(比如100个±1加起来可能到几十),S函数直接就"饱和"了(输出接近0或1,梯度几乎为0,学不动)。

那 1/√n 是怎么来的?

假设权重在 ±a 之间均匀分布,输入信号在 0~1 之间。那么总信号的方差大约是:

方差(总信号) ≈ n × 方差(w) × 方差(x)

如果 w 在 ±a 之间均匀分布,它的方差是 a²/3。为了让总信号的方差保持在1左右(不爆炸也不消失),需要:

n × a²/3 ≈ 1 → a ≈ √(3/n)

约等于 1/√n(常数3被经验调整掉了)。

所以 1/√n 是一个经验法则,保证:

  • 信号不会太大 → S函数不饱和

  • 信号不会太小 → 梯度不消失

这个 ,是对"初始化"的保障

最后,我们还需要激活函数。S函数是必须的,我们把它绑定到对象上,方便以后调用:

        # 激活函数:S型函数
        self.activation_function = lambda x: scipy.special.expit(x)

好了,现在 __init__ 已经完成了。你把这几块拼在一起,就得到了一个“有血有肉”的网络骨架。


第三步:让“问路员”带路——实现 query

有了权重,我们就能让信号流动了。query 的任务就是:你给一个输入,它算出输出。

这个过程我们非常熟悉了:输入 → 乘以权重 → 过S函数 → 输出。

我们来写:

    def query(self, inputs_list):
        # 1. 把输入列表转成列向量(2D数组)
        inputs = np.array(inputs_list, ndmin=2).T
        
        # 2. 计算隐藏层的输入信号
        hidden_inputs = np.dot(self.wih, inputs)
        # 3. 计算隐藏层的输出信号
        hidden_outputs = self.activation_function(hidden_inputs)
        
        # 4. 计算最终输出层的输入信号
        final_inputs = np.dot(self.who, hidden_outputs)
        # 5. 计算最终输出层的输出信号
        final_outputs = self.activation_function(final_inputs)
        
        return final_outputs

我们分解一下:

  • np.array(inputs_list, ndmin=2).T:这行代码把 [0.5, 0.3] 这样的列表变成了一个 列向量。为什么要转置?因为矩阵乘法要求形状匹配:(隐藏节点数, 输入节点数) × (输入节点数, 1) = (隐藏节点数, 1)。如果不转置,维度就对不上了。

  • np.dot 是矩阵乘法。它把权重矩阵和信号矩阵相乘,得到下一层的输入信号。

你现在可以停下来,手动创建一个网络,随便给个输入,看看它会不会报错。 虽然输出的数字毫无意义(因为没训练过),但只要代码不报错,就说明你的网络“活着”!

第四步:让“教练员”发力——实现 train

现在到了最激动人心的部分。train 要做的事情就是:用误差来调整权重。

它的前半段跟 query 一模一样——先做一次前向传播。然后,计算误差,反向传播,更新权重。

4.1 前向传播(跟 query 一样)

    def train(self, inputs_list, targets_list):
        # 转成列向量
        inputs = np.array(inputs_list, ndmin=2).T
        targets = np.array(targets_list, ndmin=2).T
        
        # 前向传播(跟query完全一样)
        hidden_inputs = np.dot(self.wih, inputs)
        hidden_outputs = self.activation_function(hidden_inputs)
        
        final_inputs = np.dot(self.who, hidden_outputs)
        final_outputs = self.activation_function(final_inputs)

4.2 计算输出层误差

        # 输出层误差 = 目标值 - 实际值
        output_errors = targets - final_outputs

4.3 计算隐藏层误差(反向传播)

        # 隐藏层误差 = 输出层误差 × 权重矩阵的转置
        hidden_errors = np.dot(self.who.T, output_errors)

4.4 更新权重(核心中的核心)

这就是我们花了两期才搞懂的梯度下降公式。它的代码实现非常简洁:

        # 更新隐藏层 → 输出层的权重
        self.who += self.lr * np.dot(
            (output_errors * final_outputs * (1.0 - final_outputs)),
            np.transpose(hidden_outputs)
        )
        
        # 更新输入层 → 隐藏层的权重
        self.wih += self.lr * np.dot(
            (hidden_errors * hidden_outputs * (1.0 - hidden_outputs)),
            np.transpose(inputs)
        )

这里有两个细节需要你特别注意:

  1. output_errors * final_outputs * (1.0 - final_outputs) 这三项相乘,就是公式里的 误差 × 激活函数的斜率

  2. np.transpose(hidden_outputs) 把隐藏层输出转置了,这样才能跟前面的矩阵相乘,得到和权重矩阵相同形状的更新量。

第五步:把整个拼图组装起来

现在,你手上有三块拼图:__init__querytrain。把它们放在一起,就是我们完整的神经网络类。

import numpy as np
import scipy.special

class NeuralNetwork:
    """
    三层神经网络类(输入层 → 隐藏层 → 输出层)
    包含初始化、前向查询、反向训练三大核心功能
    """
    
    def __init__(self, input_nodes, hidden_nodes, output_nodes, learning_rate):
        """
        【建造师】初始化网络的结构和参数
        
        参数:
            input_nodes: 输入层节点数
            hidden_nodes: 隐藏层节点数
            output_nodes: 输出层节点数
            learning_rate: 学习率(控制每次调整的步子大小)
        """
        # ----- 1. 记录网络结构 -----
        # 这些数字决定了网络的"身材",以后在 query 和 train 中都会用到
        self.inodes = input_nodes      # 输入层节点个数
        self.hnodes = hidden_nodes     # 隐藏层节点个数
        self.onodes = output_nodes     # 输出层节点个数
        self.lr = learning_rate        # 学习率,控制调整幅度
        
        # ----- 2. 创建权重矩阵(网络的核心"记忆")-----
        # 
        # 权重的形状说明:
        #   wih: (隐藏层节点数, 输入层节点数)
        #   who: (输出层节点数, 隐藏层节点数)
        #
        # 为什么要这样设计?
        #   矩阵乘法时,行数 = 目标层节点数,列数 = 源层节点数
        #   这样 wih × 输入 就能得到 隐藏层输入
        #   这样 who × 隐藏层输出 就能得到 输出层输入
        
        # 输入层 → 隐藏层的权重矩阵
        # 用正态分布随机初始化,均值0,标准差 = 1/sqrt(隐藏层节点数)
        # 这个经验规则可以防止权重太大导致网络"饱和"
        self.wih = np.random.normal(
            0.0,                          # 均值
            pow(self.hnodes, -0.5),       # 标准差 = 1/√隐藏节点数
            (self.hnodes, self.inodes)    # 矩阵形状
        )
        
        # 隐藏层 → 输出层的权重矩阵
        self.who = np.random.normal(
            0.0,
            pow(self.onodes, -0.5),       # 标准差 = 1/√输出节点数
            (self.onodes, self.hnodes)
        )
        
        # ----- 3. 定义激活函数 -----
        # S型函数(Sigmoid),将任意实数压缩到0~1之间
        # 使用 scipy.special.expit 实现,它数值稳定性更好
        # lambda 是一种快捷定义函数的方式,相当于:
        #   def activation_function(x):
        #       return scipy.special.expit(x)
        self.activation_function = lambda x: scipy.special.expit(x)
        
        # 初始化完成!
        # 现在这个网络有了"骨架"(结构)、"肌肉"(权重)和"神经"(激活函数)
        # 但它还没学过任何东西,就像一张白纸


    def query(self, inputs_list):
        """
        【问路员】给定输入,让网络计算输出
        
        参数:
            inputs_list: 输入数据列表,例如 [0.5, 0.3]
            
        返回:
            final_outputs: 网络的输出(列向量),例如 [[0.62], [0.35]]
        """
        # ----- 1. 把输入转换成列向量 -----
        # np.array(inputs_list, ndmin=2) 把 [0.5, 0.3] 变成 [[0.5, 0.3]]
        # .T 转置后变成 [[0.5], [0.3]],即列向量
        # 为什么要转置?因为矩阵乘法要求维度匹配:
        #   wih 的形状是 (隐藏节点数, 输入节点数)
        #   输入列向量的形状是 (输入节点数, 1)
        #   两者相乘得到 (隐藏节点数, 1),正好是隐藏层的输入
        inputs = np.array(inputs_list, ndmin=2).T
        
        # ----- 2. 计算隐藏层的输入信号 -----
        # 公式: hidden_inputs = wih × inputs
        # 这一步把"输入"和"权重"结合起来,算出每个隐藏节点收到的信号
        hidden_inputs = np.dot(self.wih, inputs)
        
        # ----- 3. 计算隐藏层的输出信号 -----
        # 把加权和信号通过S函数"挤压"成0~1之间的值
        # 这就是隐藏层神经元的"激活"过程
        hidden_outputs = self.activation_function(hidden_inputs)
        
        # ----- 4. 计算输出层的输入信号 -----
        # 公式: final_inputs = who × hidden_outputs
        # 把隐藏层的输出再跟输出层的权重结合
        final_inputs = np.dot(self.who, hidden_outputs)
        
        # ----- 5. 计算输出层的输出信号 -----
        # 再次经过S函数,得到最终输出
        final_outputs = self.activation_function(final_inputs)
        
        # 返回结果
        return final_outputs
        # 查询完成!
        # 整个过程就是:输入 → 加权 → 激活 → 加权 → 激活 → 输出
        # 信号从输入层一直"流"到了输出层


    def train(self, inputs_list, targets_list):
        """
        【教练员】用一组训练样本训练网络,调整权重
        
        参数:
            inputs_list: 输入数据列表,例如 [0.5, 0.3]
            targets_list: 目标输出列表(正确答案),例如 [0.9, 0.1]
        
        训练过程分为两个阶段:
            1. 前向传播:和 query 一样,算出当前网络的输出
            2. 反向传播:用误差来调整权重(梯度下降)
        """
        # ========== 第一阶段:前向传播(和 query 完全一样) ==========
        # 把输入转成列向量
        inputs = np.array(inputs_list, ndmin=2).T
        # 把目标输出也转成列向量
        targets = np.array(targets_list, ndmin=2).T
        
        # 计算隐藏层的输入和输出
        hidden_inputs = np.dot(self.wih, inputs)
        hidden_outputs = self.activation_function(hidden_inputs)
        
        # 计算输出层的输入和输出
        final_inputs = np.dot(self.who, hidden_outputs)
        final_outputs = self.activation_function(final_inputs)
        # 前向传播结束,现在我们知道了网络在当前权重下的"答案"
        
        # ========== 第二阶段:反向传播 ==========
        
        # ----- 1. 计算输出层误差 -----
        # 误差 = 正确答案 - 网络的答案
        output_errors = targets - final_outputs
        
        # ----- 2. 计算隐藏层误差(反向传播) -----
        # 把输出层的误差"甩"回隐藏层
        # 公式: hidden_errors = who^T × output_errors
        # who^T 是权重矩阵的转置,行和列互换
        # 这个操作实现了"按权重比例分配误差"
        hidden_errors = np.dot(self.who.T, output_errors)
        
        # ----- 3. 更新隐藏层→输出层的权重 -----
        # 这是整个网络最核心的一步!
        # 公式: Δwho = -α × output_errors × S'(final_inputs) × hidden_outputs^T
        #
        # 分解一下:
        #   output_errors              : 输出层的误差
        #   final_outputs * (1 - final_outputs) : S函数的斜率(导数)
        #   np.transpose(hidden_outputs)       : 隐藏层输出的转置
        #   self.lr                   : 学习率,控制步子大小
        #
        # 负号去哪了?因为我们直接用误差的"反方向"调整,
        # 这里 output_errors = targets - outputs,已经包含了方向信息
        self.who += self.lr * np.dot(
            (output_errors * final_outputs * (1.0 - final_outputs)),
            np.transpose(hidden_outputs)
        )
        
        # ----- 4. 更新输入层→隐藏层的权重 -----
        # 跟上面的公式完全对称,只是换成了隐藏层的误差和输入
        # 公式: Δwih = -α × hidden_errors × S'(hidden_inputs) × inputs^T
        self.wih += self.lr * np.dot(
            (hidden_errors * hidden_outputs * (1.0 - hidden_outputs)),
            np.transpose(inputs)
        )
        # 训练完成!
        # 两行代码,更新了网络里所有的权重
        # 这就是梯度下降的"魔力"——用矩阵运算一次性调整所有参数

第六步:让网络真正跑起来

有了蓝图,我们来“盖房子”。

# 创建一个2-2-2的网络(2个输入,2个隐藏,2个输出)
net = NeuralNetwork(2, 2, 2, 0.1)

# 给一组输入和对应的目标
inputs = [0.5, 0.3]
targets = [0.9, 0.1]

# 训练一次
net.train(inputs, targets)

# 查询结果
output = net.query(inputs)
print(f"训练后的输出: {output}")

你会看到输出是两个数字,它们可能离 [0.9, 0.1] 还很远。这很正常,因为只训练了一次。

来,我们让它多学一会儿:

# 训练1000次
for epoch in range(1000):
    net.train(inputs, targets)
    if epoch % 100 == 0:
        output = net.query(inputs)
        error = np.sum((np.array(targets) - output.flatten()) ** 2)
        print(f"第{epoch}轮, 误差: {error:.6f}")

看着误差数字从零点几慢慢降到接近0,你会真切地感受到:它真的在学!

第七步:总结

回顾一下我们这期一起走过的路:

步骤你做的事情代码行数
1搭空架子3个pass
2告诉网络它的“身材”记住节点数
3给网络“长肉”创建权重矩阵
4让它会“问路”实现 query
5让它会“学习”实现 train
6组装并测试创建对象、训练、查询

你从零开始,亲手写出了一段能“学习”的代码。

不要小看这件事。很多人会用 PyTorch 或 TensorFlow,但只有很少的人知道它们底层是怎么工作的。你现在就是那“很少的人”之一。


下一期预告

既然网络已经会学习了,我们该让它干点正事了。下一期,我们会用这个网络去做一件很酷的事情——识别手写数字。(没办法,这个比较经典,争取数据集也自己做,嘿嘿😄😄😄)

它会学会看 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,就像你小时候学认数字一样。

代码我们已经写好了,到时候只需要把数据喂进去,调整一下参数,就能看到它从“文盲”变成“识字”的完整过程。

我们下期见!

完整代码片段1:
 

import numpy as np
import scipy.special


class NeuralNetwork:
    """
    三层神经网络类(输入层 → 隐藏层 → 输出层)
    包含初始化、前向查询、反向训练三大核心功能
    """

    def __init__(self, input_nodes, hidden_nodes, output_nodes, learning_rate):
        """
        【建造师】初始化网络的结构和参数

        参数:
            input_nodes: 输入层节点数
            hidden_nodes: 隐藏层节点数
            output_nodes: 输出层节点数
            learning_rate: 学习率(控制每次调整的步子大小)
        """
        # ----- 1. 记录网络结构 -----
        # 这些数字决定了网络的"身材",以后在 query 和 train 中都会用到
        self.inodes = input_nodes  # 输入层节点个数
        self.hnodes = hidden_nodes  # 隐藏层节点个数
        self.onodes = output_nodes  # 输出层节点个数
        self.lr = learning_rate  # 学习率,控制调整幅度

        # ----- 2. 创建权重矩阵(网络的核心"记忆")-----
        #
        # 权重的形状说明:
        #   wih: (隐藏层节点数, 输入层节点数)
        #   who: (输出层节点数, 隐藏层节点数)
        #
        # 为什么要这样设计?
        #   矩阵乘法时,行数 = 目标层节点数,列数 = 源层节点数
        #   这样 wih × 输入 就能得到 隐藏层输入
        #   这样 who × 隐藏层输出 就能得到 输出层输入

        # 输入层 → 隐藏层的权重矩阵
        # 用正态分布随机初始化,均值0,标准差 = 1/sqrt(隐藏层节点数)
        # 这个经验规则可以防止权重太大导致网络"饱和"
        self.wih = np.random.normal(
            0.0,  # 均值
            pow(self.hnodes, -0.5),  # 标准差 = 1/√隐藏节点数
            (self.hnodes, self.inodes)  # 矩阵形状
        )

        # 隐藏层 → 输出层的权重矩阵
        self.who = np.random.normal(
            0.0,
            pow(self.onodes, -0.5),  # 标准差 = 1/√输出节点数
            (self.onodes, self.hnodes)
        )

        # ----- 3. 定义激活函数 -----
        # S型函数(Sigmoid),将任意实数压缩到0~1之间
        # 使用 scipy.special.expit 实现,它数值稳定性更好
        # lambda 是一种快捷定义函数的方式,相当于:
        #   def activation_function(x):
        #       return scipy.special.expit(x)
        self.activation_function = lambda x: scipy.special.expit(x)

        # 初始化完成!
        # 现在这个网络有了"骨架"(结构)、"肌肉"(权重)和"神经"(激活函数)
        # 但它还没学过任何东西,就像一张白纸

    def query(self, inputs_list):
        """
        【问路员】给定输入,让网络计算输出

        参数:
            inputs_list: 输入数据列表,例如 [0.5, 0.3]

        返回:
            final_outputs: 网络的输出(列向量),例如 [[0.62], [0.35]]
        """
        # ----- 1. 把输入转换成列向量 -----
        # np.array(inputs_list, ndmin=2) 把 [0.5, 0.3] 变成 [[0.5, 0.3]]
        # .T 转置后变成 [[0.5], [0.3]],即列向量
        # 为什么要转置?因为矩阵乘法要求维度匹配:
        #   wih 的形状是 (隐藏节点数, 输入节点数)
        #   输入列向量的形状是 (输入节点数, 1)
        #   两者相乘得到 (隐藏节点数, 1),正好是隐藏层的输入
        inputs = np.array(inputs_list, ndmin=2).T

        # ----- 2. 计算隐藏层的输入信号 -----
        # 公式: hidden_inputs = wih × inputs
        # 这一步把"输入"和"权重"结合起来,算出每个隐藏节点收到的信号
        hidden_inputs = np.dot(self.wih, inputs)

        # ----- 3. 计算隐藏层的输出信号 -----
        # 把加权和信号通过S函数"挤压"成0~1之间的值
        # 这就是隐藏层神经元的"激活"过程
        hidden_outputs = self.activation_function(hidden_inputs)

        # ----- 4. 计算输出层的输入信号 -----
        # 公式: final_inputs = who × hidden_outputs
        # 把隐藏层的输出再跟输出层的权重结合
        final_inputs = np.dot(self.who, hidden_outputs)

        # ----- 5. 计算输出层的输出信号 -----
        # 再次经过S函数,得到最终输出
        final_outputs = self.activation_function(final_inputs)

        # 返回结果
        return final_outputs
        # 查询完成!
        # 整个过程就是:输入 → 加权 → 激活 → 加权 → 激活 → 输出
        # 信号从输入层一直"流"到了输出层

    def train(self, inputs_list, targets_list):
        """
        【教练员】用一组训练样本训练网络,调整权重

        参数:
            inputs_list: 输入数据列表,例如 [0.5, 0.3]
            targets_list: 目标输出列表(正确答案),例如 [0.9, 0.1]

        训练过程分为两个阶段:
            1. 前向传播:和 query 一样,算出当前网络的输出
            2. 反向传播:用误差来调整权重(梯度下降)
        """
        # ========== 第一阶段:前向传播(和 query 完全一样) ==========
        # 把输入转成列向量
        inputs = np.array(inputs_list, ndmin=2).T
        # 把目标输出也转成列向量
        targets = np.array(targets_list, ndmin=2).T

        # 计算隐藏层的输入和输出
        hidden_inputs = np.dot(self.wih, inputs)
        hidden_outputs = self.activation_function(hidden_inputs)

        # 计算输出层的输入和输出
        final_inputs = np.dot(self.who, hidden_outputs)
        final_outputs = self.activation_function(final_inputs)
        # 前向传播结束,现在我们知道了网络在当前权重下的"答案"

        # ========== 第二阶段:反向传播 ==========

        # ----- 1. 计算输出层误差 -----
        # 误差 = 正确答案 - 网络的答案
        output_errors = targets - final_outputs

        # ----- 2. 计算隐藏层误差(反向传播) -----
        # 把输出层的误差"甩"回隐藏层
        # 公式: hidden_errors = who^T × output_errors
        # who^T 是权重矩阵的转置,行和列互换
        # 这个操作实现了"按权重比例分配误差"
        hidden_errors = np.dot(self.who.T, output_errors)

        # ----- 3. 更新隐藏层→输出层的权重 -----
        # 这是整个网络最核心的一步!
        # 公式: Δwho = -α × output_errors × S'(final_inputs) × hidden_outputs^T
        #
        # 分解一下:
        #   output_errors              : 输出层的误差
        #   final_outputs * (1 - final_outputs) : S函数的斜率(导数)
        #   np.transpose(hidden_outputs)       : 隐藏层输出的转置
        #   self.lr                   : 学习率,控制步子大小
        #
        # 负号去哪了?因为我们直接用误差的"反方向"调整,
        # 这里 output_errors = targets - outputs,已经包含了方向信息
        self.who += self.lr * np.dot(
            (output_errors * final_outputs * (1.0 - final_outputs)),
            np.transpose(hidden_outputs)
        )

        # ----- 4. 更新输入层→隐藏层的权重 -----
        # 跟上面的公式完全对称,只是换成了隐藏层的误差和输入
        # 公式: Δwih = -α × hidden_errors × S'(hidden_inputs) × inputs^T
        self.wih += self.lr * np.dot(
            (hidden_errors * hidden_outputs * (1.0 - hidden_outputs)),
            np.transpose(inputs)
        )
        # 训练完成!
        # 两行代码,更新了网络里所有的权重
        # 这就是梯度下降的"魔力"——用矩阵运算一次性调整所有参数
if __name__ == '__main__':
    # 固定随机种子,保证每次运行结果一致(与图4数据对应)
    np.random.seed(42)

    # 创建2-2-2网络
    net = NeuralNetwork(2, 2, 2, 0.1)

    # 固定训练数据(延续文章中的数值)
    inputs = [0.5, 0.3]
    targets = [0.9, 0.1]

    # 训练前查看初始输出
    print("训练前的输出:", net.query(inputs).flatten())
    print("目标输出:", targets)

    # 训练1000轮,并记录每一轮的误差
    loss_history = []
    for epoch in range(1000):
        net.train(inputs, targets)
        output = net.query(inputs)
        error = np.sum((np.array(targets) - output.flatten()) ** 2)
        loss_history.append(error)

        # 每100轮打印一次进度
        if epoch % 100 == 0:
            print(f"第{epoch:3d}轮, 误差: {error:.6f}")

    # 训练后查看最终输出
    print("\n训练后的输出:", net.query(inputs).flatten())
    print("目标输出:", targets)

    # 打印关键数据,方便与图4对照
    print("\n【关键误差数据")
    print(f"初始误差: {loss_history[0]:.6f}")
    print(f"第100轮误差: {loss_history[100]:.6f}")
    print(f"第500轮误差: {loss_history[500]:.6f}")
    print(f"第1000轮误差: {loss_history[999]:.6f}")
Logo

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

更多推荐