news 2026/9/23 14:45:22

从零实现三层BP神经网络:反向传播与XOR分类实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零实现三层BP神经网络:反向传播与XOR分类实战

简介:这是一个使用Python从零实现三层BP神经网络的完整代码包,面向机器学习初学者、算法研究者及需要做回归/分类实验的开发者。资源以12个文件组织,包含6个Python源码(如BPNN、GradientDescent及测试脚本)、3个编译后的pyc文件、2个CSV数据集与1个readme说明,压缩包仅484KB,轻量易用。已有2460人学习,适合用于理解反向传播原理、动手调试网络训练流程。资源覆盖数据加载与预处理、网络结构定义、权重初始化、前向传播、误差计算、梯度下降更新和模型评估等关键环节,并附带豆瓣评分回归与鸢尾花分类两个数据集测试实例。通过源码对照阅读,可快速掌握三层BP神经网络的实现细节与调参思路,为后续深入学习人工智能打下基础。

1. 三层BP神经网络:为什么我劝你先自己写一遍再上框架

把三层BP神经网络用Python完整实现一遍,是我给所有想入门深度学习的人布置的第一个任务。理由很朴素:框架把前向传播、反向传播、参数更新全部封装成了黑匣子,一旦训练发散,你连梯度是在哪一步算错的都无从查起。而三层BP是结构上“五脏俱全”的最小模型——输入层、隐藏层、输出层一个不少,激活函数、损失函数、梯度下降一个不缺。它能解决异或分类这类经典非线性问题,也能迁移到真实数据的二分类和回归上。这篇写给两种人:学过Python基础但还没碰过深度学习框架的新手,以及调过框架但仍想彻底搞懂反向传播的熟手。下面按“原理 → 代码 → 调参 → 排障 → 验证”的顺序把这套东西讲透。

2. 三层BP的数学原理:前向传播、反向传播和梯度下降的完整闭环

2.1 网络结构:三层到底指哪三层,矩阵维度怎么定

先解决最容易混淆的名词问题。这里说的“三层BP”,指的是含输入层在内一共三层:输入层、一个隐藏层、输出层。如果只数带权重计算的层,那就是两层网络。很多教材和算法题里两种口径混着用,你看代码时先确认它说的是哪种,否则结构图会对不上。

输入层 x (2) 隐藏层 h (4) 输出层 y (1) [ x1 ] [ h1 ] [ x2 ] -- W1 4x2 --> [ h2 ] -- W2 1x4 --> [ y ] [ h3 ] + b1 4x1 [ h4 ] + b2 1x1

输入层的神经元个数由特征维度决定,比如XOR问题是2个输入、1个输出。隐藏层神经元个数是超参数,需要自己定,XOR这类小问题4个就够。输出层神经元个数由任务决定:二分类用1个神经元,多分类就用类别数。权重矩阵的形状完全由相邻两层的神经元个数决定,下面这张表对应上面的结构图,动手写代码前先把shape想明白,能少踩一半的坑。

参数形状说明
W1(4, 2)隐藏层4个神经元,每个接收2个输入特征
b1(4, 1)隐藏层每个神经元的偏置
W2(1, 4)输出层1个神经元,每个隐藏神经元都贡献一个权重
b2(1, 1)输出层偏置

为什么一定要有一个隐藏层?单层感知机本质上只能画一条直线,面对异或这种线性不可分的数据,无论怎么调权重都学不出来。而万能逼近定理告诉我们:一个隐藏层的前馈网络,只要神经元数量足够、激活函数是非线性的,就能逼近任意连续函数。这就是“三层BP够用”的数学底气。

2.2 前向传播:一个样本如何一步步算出预测值

前向传播就是把输入逐层向后算,每一步都是“线性加权加偏置,再过激活函数”。以二分类为例,整套计算可以写成:

z1 = W1·x + b1 a1 = σ(z1) z2 = W2·a1 + b2 a2 = σ(z2)

其中σ选sigmoid函数,公式是σ(z) = 1 / (1 + e^(-z)),它的输出被压缩在(0,1)区间里,天然适合表示概率。sigmoid的导数形式也很干净:σ'(z) = σ(z) · (1 - σ(z)),后面反向传播要反复用它。

为了让你对矩阵运算有体感,我用手算跑一遍前向传播。为了省事,这里把隐藏层缩到2个神经元,输入取(0,1),标签是1。初始权重随便给一组:W1 = [[0.5, -0.2], [0.3, 0.8]],b1 = [0.1, -0.1],W2 = [0.6, -0.4],b2 = 0.2。

步骤计算过程结果
z10.5×0 - 0.2×1 + 0.1 = -0.1;0.3×0 + 0.8×1 - 0.1 = 0.7[-0.1, 0.7]
a1σ(-0.1) ≈ 0.475;σ(0.7) ≈ 0.668[0.475, 0.668]
z20.6×0.475 - 0.4×0.668 + 0.20.2178
a2σ(0.2178)≈ 0.5542

这个输出0.5542离真实标签1还有距离,因为权重是随手给的。训练要做的,就是通过反向传播不断调整W和b,让a2逼近真实标签。编程时我们不会一个样本一个样本地算,而是把所有样本按列拼成矩阵,一次做完整批前向传播,这就是代码里X的shape是(特征数, 样本数)的原因。

2.3 反向传播:链式法则与三个核心梯度公式

反向传播的目标是算出损失函数对每个参数的偏导数,然后用梯度下降更新参数。损失函数用均方误差MSE:L = (1 / 2m) · Σ(a2 - y)²,前面乘1/2是为了求导后抵消平方项的系数2,纯属让式子好看。

根据链式法则,先看输出层。误差对z2的导数记为δ2:

δ2 = (a2 - y) · σ'(z2) / m

这里面(a2 - y)是预测偏差,σ'(z2)是激活函数的“坡度”。直观理解:偏差越大、坡度越陡,参数该调整的幅度就越大。得到δ2之后,输出层权重和偏置的梯度是:

dW2 = δ2 · a1ᵀ db2 = δ2

隐藏层的误差信号δ1,是通过把输出层误差沿W2“送回去”得到的:

δ1 = W2ᵀ · δ2 ⊙ σ'(z1)

这个式子里的⊙是逐元素相乘。更重要的是W2ᵀ这个转置矩阵——它把输出层的误差按权重比例分配回每个隐藏神经元,这就是“反向传播”名字的来历:误差从输出层倒着流回隐藏层。于是隐藏层的梯度:

dW1 = δ1 · xᵀ db1 = δ1

最后用梯度下降更新四个参数,η是学习率:

W1 ← W1 - η·dW1 W2 ← W2 - η·dW2 b1 ← b1 - η·db1 b2 ← b2 - η·db2

整个训练循环就是反复执行“前向算损失、反向算梯度、更新参数”这三步。代码实现时,这些矩阵公式一行就能对应一个变量,比手推公式直观得多。这里需要特别记住一个检查点:dW1的shape一定和W1一致,dW2一定和W2一致。凡是shape对不上,八成是矩阵乘法顺序写反了。

2.4 激活函数与损失函数:四种搭配的选择逻辑

三层BP里激活函数的选择主要影响收敛速度和梯度稳定性,输出层还要配合损失函数一起定。下面这张表是三种常见激活函数的对比:

激活函数输出范围导数范围常见使用位置
sigmoid(0, 1)(0, 0.25]二分类输出层
tanh(-1, 1)(0, 1]隐藏层
ReLU[0, +∞)0 或 1隐藏层,深网络常用

早期BP网络在隐藏层用tanh比用sigmoid收敛快的现象很普遍,原因是tanh的均值为零,梯度方向不会全部偏向正方向,训练更平稳。但sigmoid在二分类输出层有天然优势:输出落在(0,1)里可以直接当概率。损失函数和输出层搭配的原则很简单:回归任务用线性输出层配MSE;二分类用sigmoid配交叉熵;多分类用softmax配交叉熵。

需要提一个后面坑章节会详细讲的点:当sigmoid输出层配MSE损失时,反向传播的公式是δ2 = (a2 - y)·σ'(z2),多乘了一个sigmoid导数,训练后期输出饱和时梯度会变得非常小。所以现在更主流的是sigmoid配交叉熵,它的简化梯度形式直接变成δ2 = a2 - y,数值上友好得多。这篇代码为了把公式和反向传播的推导对齐,先用sigmoid+MSE这套最直观的组合把原理跑通,你理解了之后再换交叉熵,只是改一行梯度的问题。

3. 用Python和NumPy实现三层BP网络:核心代码与逐步拆解

3.1 环境准备与数据:只要一个小脚本就能跑通

这篇的实现只依赖NumPy,不需要TensorFlow或PyTorch。Python版本3.8以上基本都行。确认环境用下面几行命令:

python --version python -c "import numpy; print(numpy.__version__)"

如果你的机器还没装好Python解释器,先按常规的python安装教程把环境配好,VSCode里指对解释器路径,或者直接用IDLE跑都行。这里不展开环境配置,我们的重点在网络本身。

数据集选经典的异或逻辑XOR,它是单层感知机无法解决的线性不可分问题,正好用来检验三层BP是否真的学到了非线性映射。数据构造代码:

import numpy as np X = np.array([[0, 0], [1, 0], [0, 1], [1, 1]]).T # shape (2, 4),每列一个样本 y = np.array([[0, 1, 1, 0]]) # shape (1, 4),标签 0/1

这里特意把X转置成(特征数, 样本数)的列向量约定,是为了直接套用前面的矩阵公式。你自己写代码时最常犯的错就是行向量列向量搞混,导致矩阵乘法shape报错。四行数据分别表示(0,0)→0、(1,0)→1、(0,1)→1、(1,1)→0,注意最后一条是异或和或运算唯一的区别。

3.2 参数初始化:权重和偏置的初始值为什么不能随便给

权重初始化对三层BP来说,是决定“学得动”还是“学不动”的第一步。常见做法是用均值为0、标准差可控的随机数初始化,并配合一个缩放系数。下面是一个标准的初始化写法:

class ThreeLayerBP: def __init__(self, n_in, n_hidden, n_out, init_scale=0.5): # 权重用 randn 保证有正有负且中心在 0 附近 self.W1 = np.random.randn(n_hidden, n_in) * init_scale self.b1 = np.zeros((n_hidden, 1)) self.W2 = np.random.randn(n_out, n_hidden) * init_scale self.b2 = np.zeros((n_out, 1))

两个关键点。第一,用np.random.randn而不是np.random.rand,是因为rand生成的是(0,1)区间的均匀分布,全是正数,会让隐藏神经元进入sigmoid的单侧饱和区;而randn生成的是标准正态分布,有正有负,初始输出更可能落在激活函数的中间区域。第二,init_scale取0.5,相当于把权重限定在-1到1的范围内,对sigmoid和tanh都是安全的初始尺度。偏置b设成全零是可以的,因为权重矩阵的随机性已经打破了对称性。

如果这里偷懒把W1和W2都初始化为零,就会出现经典的对称性问题:所有隐藏神经元在训练中收到完全相同的梯度,永远学出一样的特征,等价于一个神经元,网络容量直接塌缩。第5章会专门讲这个现象。

3.3 前向传播与反向传播的代码实现:核心逻辑不到60行

前向传播就是按照第2章的公式,一层一层算出a2。反向传播则按链式法则反着算梯度。下面是完整实现,每一步都加了注释,建议你自己敲一遍而不是复制:

@staticmethod def sigmoid(z): # clip 防止 np.exp 溢出,这是深坑,后面细说 z = np.clip(z, -500, 500) return 1.0 / (1.0 + np.exp(-z)) @staticmethod def sigmoid_deriv(z): s = ThreeLayerBP.sigmoid(z) return s * (1 - s) def forward(self, X): # X: (n_in, m),返回预测值 (n_out, m) self.z1 = self.W1.dot(X) + self.b1 self.a1 = self.sigmoid(self.z1) self.z2 = self.W2.dot(self.a1) + self.b2 self.a2 = self.sigmoid(self.z2) return self.a2 def backward(self, X, y, lr): m = X.shape[1] # 输出层误差信号:预测偏差 * sigmoid斜率,再取平均 dz2 = (self.a2 - y) * self.sigmoid_deriv(self.z2) / m dW2 = dz2.dot(self.a1.T) db2 = np.sum(dz2, axis=1, keepdims=True) # 误差沿 W2 传回隐藏层,再乘隐藏层激活斜率 dz1 = self.W2.T.dot(dz2) * self.sigmoid_deriv(self.z1) dW1 = dz1.dot(X.T) db1 = np.sum(dz1, axis=1, keepdims=True) # 梯度下降更新 self.W2 -= lr * dW2 self.b2 -= lr * db2 self.W1 -= lr * dW1 self.b1 -= lr * db1

这段代码里最值得停下来看三分钟的是backward里除以m那句。因为损失函数定义中带了1/m的平均,梯度也要跟着平均。如果你去掉这个除法,在XOR只有4个样本时可能还能收敛,但换到几百个样本的真实数据,梯度累计起来会爆炸。dW2 = dz2.dot(self.a1.T)这一步的shape检查:dz2是(1, m),a1是(4, m),转置后dot得到(1, 4),正好和W2一致,说明矩阵乘法方向写对了。

3.4 训练循环与预测:损失怎么观察,结果怎么判定

训练就是不断重复“前向算损失 → 反向算梯度 → 更新参数”。这里给训练循环加一个verbose参数,定时打印损失值,方便观察收敛状态。最后用0.5作为阈值把输出转成0/1类别:

def loss(self, y): m = y.shape[1] return 0.5 * np.mean((self.a2 - y) ** 2) def train(self, X, y, epochs=3000, lr=0.5, verbose=500): for i in range(1, epochs + 1): self.forward(X) loss_val = self.loss(y) self.backward(X, y, lr) if i % verbose == 0: print(f"epoch {i}, loss {loss_val:.6f}") def predict(self, X): out = self.forward(X) return (out > 0.5).astype(int) model = ThreeLayerBP(n_in=2, n_hidden=4, n_out=1) model.train(X, y, epochs=3000, lr=0.5, verbose=500) pred = model.predict(X) print(pred.ravel()) # 期望输出 [0 1 1 0] print("accuracy:", np.mean(pred == y))

在XOR这个规模下,常见经验是几百轮内损失就会掉到0.1以下,3000轮通常能到0.001量级。但注意这和随机初始化有关,跑一次没收敛不代表代码错了,多初始化几次,取损失最低的模型,这是个非常重要的工程习惯。判断收敛的标准不是训练到最后一轮,而是看损失打印序列是否整体向下走。如果第500轮损失还在0.3左右晃,大概率是初始化点运气差,或者学习率不合适,直接跳到第4章。

4. 让三层BP真正收敛:四个比网络结构更关键的参数设置

4.1 学习率:0.5在XOR能收敛,换到真实数据先降一个量级

学习率是超参数里最玄学但也最值得先调的一个。XOR因为数据量小、损失曲面简单,lr=0.5跑得很舒服。但同样的lr拿到真实数据上,第一轮可能就把权重更新过头,损失直接上到天上。

我一般调学习率遵循“先粗扫再细调”的流程:固定网络结构,分别用0.1、0.01、0.001各跑50到100轮,看损失曲线。健康的曲线是前几十轮快速下降,然后进入平缓区。如果前10轮损失出现阶梯式上升,说明lr太大,直接降10倍。如果100轮过去损失几乎没有变化,说明lr太小,升10倍再试。一个值得警觉的现象是损失在前几个epoch略微上涨后回落——这不算病,因为初始梯度方向未必指向最陡的下坡,但超过10轮还在涨就必须停了。

实践里还有一个朴素有效的技巧:每500轮把学习率乘0.95,让训练后期步长变小,更容易落到更小的局部极小值。这个衰减口诀我现在的项目里还在用。给新手一个安全起点:三层BP用sigmoid激活时,lr从0.05开始试,不要一上来就0.5。

4.2 隐藏层神经元个数:经验公式与对称性陷阱

隐藏层神经元个数决定了网络的“容量”。太少学不会非线性边界,太多则容易过拟合,还会让训练变慢。经典的经验公式有几个:n_h = sqrt(n_in × n_out)、n_h = (n_in + n_out) / 2,或者取这两者之间的值。对XOR这种n_in=2、n_out=1的问题,sqrt(2×1)≈1.4,但实践中4到8个神经元才稳,原因是XOR的决策边界形状对容量要求比公式预估的更高。

我个人的经验是:小数据集从4开始往上试,每次翻倍,观察训练集和验证集准确率的变化。训练集准确率上不去,是欠拟合,加神经元;训练集很好但验证集差,是过拟合,减神经元或加正则。三层BP这个规模谈不上什么结构搜索,多试几个值成本很低,别在这一步花太多时间。

对称性陷阱藏在初始化里:如果权重全是同一个常数或者全零,隐藏层神经元再多也白搭,因为每个神经元都在学一模一样的东西,网络实际容量是1。检查方法很简单,训练前打印一层W1,看每一行的值是否各不相同。

4.3 训练策略:全批量、随机梯度与小批量的取舍

样本量不同,梯度更新的策略完全不同。三种常见方式对比如下:

策略每轮更新次数梯度平稳性适用场景
全批量1次/epoch最平稳样本量小于几千
随机SGD样本数/epoch噪声大,震荡在线学习、数据流
Mini-batch样本数/batch_size折中几乎一切真实任务

XOR只有4个样本,全批量一次算完所有样本的梯度,方向准确又稳定。真实项目数据量上了几万,全批量每轮迭代计算量太大,而且全批量容易陷入尖锐的局部极小值;小批量每次用64或128个样本算梯度,更新方向带一点噪声,反而更容易跳出局部极小。

这里有个关键概念要先理清:epoch和迭代步数的区别。1个epoch定义为“所有训练样本被模型看过一遍”。全批量下一个epoch只更新一次参数,而mini-batch下一个epoch要更新ceil(样本数/batch_size)次。所以当你看到别人的代码里epochs=500但损失下降非常快时,先搞清楚他的batch_size是多少,再对比训练效果,否则会被训练曲线骗了。

4.4 输入归一化:把损失曲面从“长条碗”掰回“圆碗”

归一化常常被新手忽略,但它对梯度下降的影响是根本性的。假如一个特征取值范围是0到100,另一个是0到1,损失函数的等高线就会被拉成很扁的椭圆,梯度方向在椭圆的长轴和短轴之间来回震荡,训练慢得像蜗牛。而归一化之后,损失曲面接近一个圆碗,梯度方向直接指向碗底,收敛速度提升一个量级。

最简单的做法是用Z-score标准化:x_norm = (x - mean) / std,把每个特征变成均值0、方差1的分布。对量纲差异大的数据,这是我去掉训练慢和梯度爆炸问题的第一招。

这里必须加一个很多人翻车的地方:归一化参数只能从训练集上计算,保存下来,预测或测试时用同一组mean和std去变换新数据。如果你在测试集上又重新算了mean和std,等于把测试数据扔进了一个分布完全不同的空间,结果根本不可复现,前面训练再好的模型也白搭。

5. 三层BP网络避坑指南:5个高频异常现象与排障顺序

5.1 损失非但不降还在升高:学习率过大的教科书式症状

现象:训练第1轮loss是0.35,第10轮变成了1.4,而且还在涨。

原因:学习率设得太大,参数更新步长跨过了损失曲面的谷底,每走一步都跳到更高处,形成恶性循环。这是全批量梯度下降最容易出现的症状,因为每个epoch的更新方向太准了,大学习率会让更新步长超过合理范围。

解决:把学习率直接除以10,从0.5降到0.05,再观察前20轮。如果恢复下降,说明定位准确。如果损失还是在震荡,继续降。注意一个例外:前两三个epoch微微上升后立刻回落,这种不用管,因为初始化点可能处在陡峭区域的“错误一侧”,梯度自然会把它拉回来。超过10轮还在涨,才算真的踩坑。

5.2 所有输出都停在0.5附近:初始化失效与对称性问题

现象:训练了500轮,所有样本的预测输出全部是0.5,loss恒定不变,像是模型完全没在学。

原因:两个。第一,权重初始化为全零,所有隐藏神经元梯度相同,形成对称性,网络退化成一个只带偏置的弱分类器。第二,权重缩放系数设得太小,比如init_scale=1e-4,所有z都接近0,而sigmoid(0)=0.5,同时初始梯度小到几乎无法推动参数更新。

解决:如果是全零初始化,改成np.random.randn乘0.5重新跑,这是血泪经验换来的教训。如果是初始化尺度问题,把init_scale从1e-4调到0.1或0.5。判断时不要盯着准确率,先打印第一轮前向传播的a2——如果所有值都精确等于0.5,那基本就是初始化的问题,跟你的网络结构好不好没关系。

5.3 训练集接近100%,测试集只有50%:过拟合的信号与对策

现象:训练集上准确率99%,但换到没见过的测试数据上准确率只有50%出头,跟抛硬币差不多。这在XOR这种4个样本的小任务上不会出现,所以这条主要针对你把代码用到真实数据集之后。

原因:神经网络把训练样本的噪声也“背”下来了,学到了样本特有但不具备泛化性的模式。常见诱因是隐藏层神经元太多、训练轮数太长、数据量太少。

解决:优先做三件事。第一,减少隐藏神经元数量,从32降到8试试,模型容量小了,拟合死记硬背的空间就小。第二,加L2正则,更新权重时多减一项λ·W,λ取0.001到0.01之间。第三,把训练数据划分成训练集和验证集,用验证集损失做早停,当验证集loss连续50轮不下降就停止训练,而不是傻跑固定epoch数。这三个方法都试过还不行,看看是不是数据本身的问题,比如标签噪声太大,那就不是网络结构的锅了。

5.4 梯度变nan:数据尺度和学习率的组合陷阱

现象:训练过程中loss突然变成nan,再往后所有输出都是nan。

原因:数据没有归一化且学习率偏大时,某一步权重更新过大,导致下一轮前向传播的z值巨大,间接让梯度矩阵中出现inf,再叠加反向传播的链式乘法,梯度爆炸成nan。另一个常见源头是np.exp(x)中x超过700,指数函数上溢直接得inf。这就是sigmoid函数里那段np.clip(z, -500, 500)存在的意义——把指数计算限制在安全范围内。

解决:先检查数据是否归一化,没做归一化的先做,这条能解决大半问题。其次把学习率降一个量级,比如0.1降到0.01。最后是代码层面的防御:sigmoid里做clip,权重初始化scale从0.5降到0.1。我自己的排查顺序是数据归一化 → 学习率 → 初始化scale,三步走完,基本不会再到nan这一步。

5.5 决策边界歪歪扭扭或者学不到边界:容量不足的两种表现

现象:XOR四个点分类结果总是一个对角线对、另一个对角线错,或者整个边界画成了一条直线,怎么调学习率都无济于事。

原因:典型的容量不足。隐藏层只有1到2个神经元时,网络能表示的决策边界形状非常有限,学不出异或这条“X”型对角线。另一个容易被忽略的原因是激活函数写错了,比如反向传播激活函数求导用了正向的函数值,但忘记把占位变量换成z本身,相当于梯度方向错乱。

解决:先确认正向里已经用了激活函数,而不是裸的z1 = W1·x + b1就往后传。然后加大隐藏层到4到8个神经元,理论上这个容量足够表达异或。还不行,就多做几次随机初始化,选loss最低的一次——BP本身可能掉进局部极小值,这个现象在XOR上都真实存在。多跑几次取最优不叫玄学,是工程上默认的习惯。

6. 验证三层BP网络学对了没:梯度检查 + 决策边界可视化

6.1 梯度检查:用数值差分给反向传播做一次体检

反向传播公式推错一个符号,模型也能跑,但收敛极慢或者行为诡异。最可靠的验证方法是梯度检查:用数值差分逼近梯度,和反向传播算出的解析梯度对比。只要相对误差在1e-5以下,说明梯度算法没问题。

def gradient_check(model, X, y, eps=1e-7): model.forward(X) model.backward(X, y, lr=0.0) # 只算梯度不更新参数 params = [model.W1, model.b1, model.W2, model.b2] names = ["W1", "b1", "W2", "b2"] for name, param in zip(names, params): numerical = np.zeros_like(param) it = np.nditer(param, flags=["multi_index"]) while not it.finished: idx = it.multi_index old = param[idx] param[idx] = old + eps loss_plus = model.loss(y) param[idx] = old - eps loss_minus = model.loss(y) param[idx] = old numerical[idx] = (loss_plus - loss_minus) / (2 * eps) it.iternext() analytical = {"W1": model.dW1, "b1": model.db1, "W2": model.dW2, "b2": model.db2}[name] rel_err = np.linalg.norm(numerical - analytical) / ( np.linalg.norm(numerical) + np.linalg.norm(analytical) + 1e-12) print(f"{name} rel_err: {rel_err:.2e}")

梯度检查不需要每次训练都跑,它是在你刚写完backward、怀疑梯度写错了的时候用的。判断阈值:相对误差小于1e-7说明实现几乎完美;小于1e-4也可以放心用;如果超过1e-2,基本可以确定反向传播公式里有bug,而且大概率是某个矩阵转置写错或者损失函数定义和梯度对不上。

6.2 决策边界可视化:把XOR的四个点画成一张分类地图

损失曲线能告诉你模型在收敛,但收敛到正确的地方了吗?决策边界图是最直观的答案。对XOR,模型真正应该学到的是一个“X”型对角划分:左上和右下区域属于类别1,另外两个角属于类别0。

import matplotlib.pyplot as plt xx, yy = np.meshgrid(np.linspace(-0.5, 1.5, 200), np.linspace(-0.5, 1.5, 200)) grid = np.c_[xx.ravel(), yy.ravel()].T # (2, 40000) Z = model.predict(grid).reshape(xx.shape) plt.contourf(xx, yy, Z, alpha=0.5, cmap=plt.cm.RdYlBu) plt.scatter(X[0], X[1], c=y.ravel(), cmap=plt.cm.RdYlBu, edgecolors="k") plt.title("XOR Decision Boundary") plt.show()

如果边界干净利落地把红色和蓝色分成X型对角,说明模型学到了异或的本质;如果边界歪歪扭扭逼近训练点,要警惕过拟合;如果边界几乎是一条直线,回到第5.5节查容量。可视化这步还能帮你抓到数值上的隐性问题,比如边界抖动剧烈,往往对应数据没归一化或者学习率偏大。

我现在的习惯是:每写完一个带着梯度的模型,先跑一遍梯度检查,再画决策边界,最后才看准确率。这两步能省下的排障时间,比任何调参技巧都多。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 14:38:30

OpenCV+Python实现毫秒级NCC旋转匹配

简介:本资源是一套基于OpenCV与Python实现归一化互相关(NCC)旋转匹配的完整实践方案,面向计算机视觉初学者、AI开发工程师及图像算法学习者,解决目标图像在任意旋转角度下的鲁棒匹配难题。方案融合圆投影建模、积分图加…

作者头像 李华
网站建设 2026/9/23 14:37:37

米斗APP逆向分析:360壳脱壳与核心逻辑还原实战

1. 米斗APP逆向分析的整体思路与方案选型1.1 为什么选择从加固识别入手拿到一个APK,第一步永远不是急着拖进反编译工具,而是先搞清楚它到底穿了什么“衣服”。米斗APP这个样本,我最初用常规的apktool反编译,出来的classes.dex只有…

作者头像 李华
网站建设 2026/9/23 14:33:50

免费小游戏平台怎么选?CrazyGames、itch.io、Poki深度推荐

1. 为什么我推荐这三款免费小游戏平台1.1 免费游戏平台的生态现状这些年周围朋友经常问我一个问题:想找个地方玩游戏,不想下载几十个G的大型客户端,也不想一打开就跳充值弹窗,到底有什么靠谱的选择?说实话,…

作者头像 李华