📌 本文属于《Python神经网络入门:零基础保姆级路线图》专栏
上一篇:python神经网络编程入门(五)----误差算出来了,权重到底怎么改?
下一篇:python神经网络编程入门(七)——跑通MNIST手写数字识别验证训练成果与测试准确率。完整目录 & 更新记录:
《Python神经网络入门:零基础保姆级路线图(附全系列免费源码)》
我们来把上一期的理论,变成实实在在能跑的代码。
上一期我们花了大把时间,搞懂了梯度下降这个“摸黑下山”的道理,也看明白了那个让很多人头大的权重更新公式:
Δw = -α × (误差) × (激活函数斜率) × (上一层输出)
公式摆在那,但光看它还是觉得有点虚。这一期,我们就亲手把这个公式变成Python代码,让你亲眼看着一个神经网络从“啥也不会”到“慢慢学会”的过程。
我们从最简单的开始,一步一步来。你不需要任何机器学习框架,只用最基本的numpy,就能搭建出一个完整的、能学习的神经网络。
先别急,聊聊我们到底要做什么?
在敲任何代码之前,我们先想清楚一个问题:一个神经网络,最核心、最不能少的东西是什么?
📌停一下,想一想再往下看。
你的答案可能有很多:节点、层、激活函数……都没错。但如果我们把“漂亮的外表”都剥掉,只剩骨架,神经网络其实就两样东西:
一堆权重(数字)
能做乘法、加法、乘法的能力(计算)
对,就这么简单。输入来了,乘以权重,加上偏置(这里我们先忽略偏置),再经过激活函数,得到输出。整个过程就是数字的流动。
所以,我们的代码核心任务就是:管理好这一堆数字,让它们能正确地流动起来。
第一步:先搭架子——写一个空的类
好,既然核心是“管理数据”,用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_outputs4.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) )这里有两个细节需要你特别注意:
output_errors * final_outputs * (1.0 - final_outputs)这三项相乘,就是公式里的误差 × 激活函数的斜率。np.transpose(hidden_outputs)把隐藏层输出转置了,这样才能跟前面的矩阵相乘,得到和权重矩阵相同形状的更新量。
第五步:把整个拼图组装起来
现在,你手上有三块拼图:__init__、query、train。把它们放在一起,就是我们完整的神经网络类。
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}")