python神经网络编程入门(六)——从零开始,敲出你的第一个神经网络
📌 本文属于《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_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)
)
这里有两个细节需要你特别注意:
-
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}")
更多推荐



所有评论(0)