资讯动态

感知机入门:Python实现、异或局限到多层感知机

发布时间:2026/10/13 12:19:23 来源:尧图企业网站定制
我刚开始学机器学习的时候第一个迎面撞上的模型就是感知机。当时的感觉是这不就是一个带权重的打分器嘛等到后来亲手把感知机实现出来再看着它在异或问题上怎么都学不会才明白这个看似简单的模型里藏着神经网络最核心的基因。这篇文章我会从感知机概念开始讲给出可以运行的Python实现然后重点拆解它的局限性最后用多层感知机把这个坑填上。整个过程不涉及复杂数学只要你写过一点点代码就能跟下来。很多人会把感知机当成“神经网络的最简版本”这个说法没错但它更容易被误读成“一个玩具”。实际上感知机是上世纪五十年代提出来的早期模型它第一次让机器可以通过数据自动学习一套权重参数而不是靠人手工写规则。后来深度学习里的神经元、激活函数、梯度更新几乎都能从感知机里找到影子。理解了它再看多层感知机和反向传播会顺畅很多。今天这篇文章就是帮你把这一连串概念串成一条线。1. 感知机到底解决什么问题从一枚逻辑门说起1.1 先抛开数学用生活例子理解感知机感知机要做的事情用一句话说就是给我一堆输入每个输入有不同的重要程度我根据一份“加权打分结果”来判断属于哪一类。听起来很抽象但实际生活里到处都是这种决策。举个例周末要不要出门跑步。你可能看三个因素天气好不好、公园人多不多、自己累不累。这三个因素的重要性不一样。天气权重高人多的权重中等累不累可能权重也很高。如果总得分超过某个内心的“阈值”就出门否则就待在家里。感知机就是把这个过程参数化了输入是 (x_1, x_2, x_3)权重是 (w_1, w_2, w_3)加上一个偏置 (b)最后用加权和的正负来判断输出 (y) 是 1 还是 0。这里有个关键点偏置 (b) 本质上是那个“阈值”的相反数。如果你心里是“得分超过 0.5 就出门”那就等价于把 0.5 移到不等式左边变成一个 (-0.5) 的偏置。所以感知机的学习实际上是在自动调整每一件事的重要程度以及调整整体门槛的高低。这种理解方式比直接背公式有用得多因为后面所有神经网络本质上都还在做“加权求和 偏置 非线性判断”这三件事。当然生活决策没有绝对正确但感知机面对的通常是有明确标签的数据比如“是不是垃圾邮件”“图片里有没有猫”。它可以通过样本不断修正自己的权重让判断越来越准。这就是“从数据中学习”的含义。1.2 感知机的数学模型权重、偏置、激活函数现在把上面的例子写成标准形式。假设输入是一个向量 (x (x_1, x_2, ..., x_n))感知机的计算分两步计算加权和(z w_1 x_1 w_2 x_2 ... w_n x_n b)其中 (w) 是权重向量(b) 是偏置。经过激活函数(y \text{step}(z))这里 step 通常指阶跃函数即 (z 0) 输出 1否则输出 0。注意感知机里的“激活函数”用的是阶跃函数不是后来神经网络里常见的 Sigmoid 或 ReLU。阶跃函数的好处是直接给出明确的分类结果坏处是它不可导所以后来多层网络不用它做中间层激活。符号含义说明(x_i)第 i 个输入特征例如天气评分、公园人数(w_i)第 i 个特征的权重学习得到代表重要程度(b)偏置表示分类阈值学习得到(z)加权和线性组合的结果step(z)阶跃函数z 大于 0 输出 1否则输出 0这组数学其实就描述了一条直线分类边界。以二维输入为例让 (z w_1 x_1 w_2 x_2 b 0)这是一条直线。感知机最终学出来的就是把正负样本分开的那条直线。理解到这一步下一步你自然会问万一正负样本不是一条直线能分开的呢这就是第三节要说的局限性先按下不表。1.3 为什么“线性可分”是关键分水岭线性可分这个概念是理解感知机局限性的钥匙。所谓线性可分就是在特征空间里能找到一个超平面把所有正类样本和负类样本完整切开。二维里是直线三维里是平面更高维是超平面。能切开感知机就一定能学会切不开感知机再怎么调参也无能为力。举个例子。假设要分类苹果和香蕉特征取“重量”和“颜色饱和度”。如果苹果普遍更重并且颜色偏红香蕉轻一点并且偏黄那你完全可以在二维平面里画一条直线把它们分开这就是线性可分。感知机能搞定。但现实问题往往不是这么干净。比如同一堆水果里有一些青苹果重量轻但颜色绿有一些熟香蕉颜色黄但偏重数据点在平面上揉成一团一条直线根本不可能切开。这时候线性可分条件不满足单层感知机就会失效。它并不是因为训练不够而是模型本身的表达能力只有“一条直线”当你需要的是曲线边界时它天然做不到。这个认知非常重要因为很多初学者看到感知机在某个数据集上准确率上不去第一反应是学习率调大、迭代次数拉满折腾半天发现还是不行。其实问题可能根本不在训练参数而在模型容量。你要么换非线性模型要么给感知机增加能够产生非线性变换的结构多层感知机就是为了解决这个问题出现的。2. 手写一个感知机核心代码与训练过程2.1 从零实现初始化、前向传播、更新规则纸上谈兵没意思直接手写一个感知机类。我习惯把代码分成三块初始化、预测、训练。初始化就是把权重和偏置置成 0 或者很小的随机数预测是计算加权和再通过阶跃函数训练的核心是更新规则。感知机的训练规则很朴素拿一个样本进去如果预测对了就什么都不做如果预测错了就把权重往正确方向拉一点。数学上写成[ w w \eta \times (y_{true} - y_{pred}) \times x ] [ b b \eta \times (y_{true} - y_{pred}) ]其中 (\eta) 是学习率。你可能好奇为什么这样就能收敛。如果真实标签是 1预测是 0那么 (y_{true} - y_{pred} 1)相当于把权重朝 x 的方向加让加权和变大一点下一次更可能输出 1。反过来如果真实标签是 0预测是 1误差是 -1权重就朝 x 的反方向减让加权和变小下一次更可能输出 0。这个逻辑完全说得通。下面是完整代码。为了让大家能直接跑我没有用任何第三方库少数几个函数手写即可。import numpy as np class Perceptron: def __init__(self, n_features, learning_rate0.1, max_epochs100): self.w np.zeros(n_features) self.b 0.0 self.lr learning_rate self.max_epochs max_epochs def predict(self, X): z np.dot(X, self.w) self.b return np.where(z 0, 1, 0) def fit(self, X, y): for epoch in range(self.max_epochs): errors 0 for xi, yi in zip(X, y): y_pred self.predict(xi.reshape(1, -1))[0] if yi ! y_pred: update self.lr * (yi - y_pred) self.w update * xi self.b update errors 1 if errors 0: break return self注意predict里np.where(z 0, 1, 0)就是阶跃函数。这里我把阈值设成 0因为偏置已经包含了门槛。训练时只要还有错误分类就更新权重如果某一轮全对就直接结束循环。这种提前停止很实用能减少无效迭代。2.2 和或门实验眼见为实为了验证代码最经典的做法是用逻辑门数据集。AND 门和 OR 门都非常简单输入是 (x_1, x_2)取值 0 或 1输出也是 0 或 1。我们用感知机去学一下。import numpy as np X np.array([[0, 0], [0, 1], [1, 0], [1, 1]]) y_and np.array([0, 0, 0, 1]) y_or np.array([0, 1, 1, 1]) p_and Perceptron(n_features2, learning_rate0.1) p_and.fit(X, y_and) print(AND weights:, p_and.w, bias:, p_and.b) print(AND predictions:, p_and.predict(X)) p_or Perceptron(n_features2, learning_rate0.1) p_or.fit(X, y_or) print(OR weights:, p_or.w, bias:, p_or.b) print(OR predictions:, p_or.predict(X))输出大致会是AND weights: [0.2 0.1] bias: -0.3 AND predictions: [0 0 0 1] OR weights: [0.2 0.2] bias: -0.1 OR predictions: [0 1 1 1]权重和偏置会根据初始值、学习率略有不同但分类结果一定是对的。看到没感知机学出来的权重都为正表示两个输入对结果都是正向贡献偏置为负表示默认倾向输出 0只有加权和足够大时才翻转成 1。这跟逻辑门真值表完全一致。2.3 参数细节学习率、迭代次数、随机初始化写代码时有些细节会影响训练结果。首先是学习率 (\eta)。感知机的参数更新只依赖误差的正负不依赖误差大小所以学习率理论上只要大于 0 都能收敛但实际数值会影响收敛速度。我一般习惯取 0.1 到 1 之间。取太小比如 0.0001可能迭代几百轮还没学完取太大比如 10权重会来回震荡虽然最终还可能收敛但过程看着会很暴躁。其次是迭代次数。感知机收敛定理告诉我们只要训练数据线性可分无论权重怎么初始化算法都会在有限步内找到一个最优解。所以迭代次数设成一个足够大的上限即可。我的代码里用提前停止遇到全对就退出这样不用盲目跑到上限。最后是初始化。这里我用了全零初始化因为感知机的决策边界完全由权重和偏置决定全零完全能学。但你可能会在其他资料里看到“不要用全零初始化”的说法那是针对多层神经网络的原因后面到了多层感知机的部分再细说。总之单层感知机能用全零但不代表引入多层后还能这么任性。3. 感知机的局限异或问题带来的警醒3.1 异或为什么学不出来异或XOR也是个逻辑门但它和 AND、OR 不一样。它的真值表是输入相同输出 0输入不同输出 1。也就是说00 输出 001 输出 110 输出 111 输出 0。如果拿这个数据集去跑上面写好的感知机你会发现一个非常尴尬的结果无论怎么调学习率、迭代多少次模型始终无法做到四个样本全对。原因非常直观。把四个点画在二维坐标里两个类别分别是对角线上的两个点。(0,0) 和 (1,1) 是一类(0,1) 和 (1,0) 是另一类。你拿着一条直线不管怎么挪、怎么转都不可能同时把两个对角点圈在里面、另两个对角点圈在外面。一条直线最多把平面切成两半而异或需要的是“切两块对角线”的分法一条直线做不到。这就是感知机的典型死穴。它不是某个参数没调好而是模型本身只能表达线性边界。遇到异或这种最基础的非线性问题它就直接缴械投降。这个问题的历史意义也很大当年研究者发现感知机无法解决异或问题时直接导致神经网络进入了一段低谷期。3.2 几何直觉线性分割线 vs 非线性边界如果你觉得上面的描述不够有画面感可以想象在棋盘格上贴标签。输入可以用两个位表示第一个位是横坐标第二个位是纵坐标。标签 1 的两个点像一条从左上到右下的斜线标签 0 的两个点像从右上到左下的另一条斜线。这种模式需要的是一个弯折的边界——你至少要用两条直线拼一个“拐弯”出来才可能把两类分开。再换个角度理解决策边界。线性分类器产生的边界是“半边空间”定义一条线之后线上方全部判成正类线下方全部判成负类。异或需要的边界是“一片区域”比如把 (0,1) 和 (1,0) 附近围起来或者反过来把 (0,0) 和 (1,1) 围起来。要想围出一片区域至少得有两条线参与比如一条线划分上部区域一条线划分下部区域然后取交集或并集。单层感知机只有一条直线所以天然没有这种表达能力。这个几何直觉很重要它直接引出下一节多层感知机的设计思路如果你让第一层学习两条直线再让第二层把这两条直线的结果组合起来是不是就能得到非线性边界了答案是可以。异或问题就是经典证明。3.3 感知机收敛定理与局限的本质这里值得把感知机收敛定理稍微展开一下。定理的通俗版本是如果训练数据是线性可分的那么上述的“每次遇到错误就更新”的算法在有限步内一定收敛且能找到一个把训练集全部分类正确的解。这个结论看起来很美好但它有一个致命前提——“线性可分”。如果数据线性不可分感知机算法就无法收敛到全对解。它会在不同样本之间反复横跳今天改对了这个明天又错那个权重震荡不止。实际表现就是损失一直下不去或者准确率卡在某个百分比上上不去。所以感知机的局限本质不是训练技巧而是模型容量。模型容量可以粗暴理解为“能表达的决策边界有多复杂”。单层感知机只有一条直线/超平面容量太有限。要突破这个限制更聪明的办法是让多个线性分类器组合起来或者对输入先做一次非线性变换。多层感知机正是沿着这个思路出现的。4. 多层感知机突破局限的第一把钥匙4.1 增加隐藏层的意义多层感知机的核心思想是在输入层和输出层之间加入一个或多个隐藏层。每一层做的事情和感知机一样加权求和、加偏置、过激活函数。但多层的意义不是简单地“多算几遍”而是让信息在逐层传递中发生非线性重组。拿异或来说我们可以在隐藏层安排两个神经元一个学会“OR”一个学会“AND”。然后输出层不再直接看原始输入而是看这两个隐藏神经元的输出。它学会的规则变成如果 OR 结果是 1 且 AND 结果是 0就输出 1否则输出 0。这条规则就是异或的定义。原始输入空间里的非线性边界被隐藏层转换成了另一个空间里一条直线输出层在变换后的空间里做线性分类就足够了。这也解释了为什么隐藏层里的激活函数不能用阶跃函数之外的东西。早期确实用过阶跃函数但因为不可导训练多层网络时没法用梯度下降后来才引入了可导的 Sigmoid、Tanh、ReLU。激活函数给每一层都注入了非线性层与层叠加之后整个函数才会具备拟合复杂边界的能力。4.2 激活函数的选择从阶跃函数到Sigmoid/ReLU如果多层感知机每一层都只用线性变换不管加多少层最终效果都等价于一层线性变换因为线性函数的复合还是线性函数。所以要真正拥有非线性能力必须让每一层经过一个非线性激活函数。阶跃函数作为最早的激活函数问题在于处处不可导。单层感知机可以用“遇到错误就更新”这种简单规则不依赖导数。但多层网络要同时调整所有层的权重就不能再一个个试了必须靠反向传播计算梯度。计算梯度需要函数可导阶跃函数显然不合格。Sigmoid 是当时最容易想到的选择。它的输出范围在 0 到 1 之间平滑可导并且输出很接近“概率”的感觉。但 Sigmoid 也有毛病两端梯度接近 0容易造成梯度消失而且它用指数运算计算量稍大。后来出现的 ReLU 更简单(激活值 max(0, z))正区间导数为 1负区间导数为 0。计算快梯度不容易消失成了现代神经网络的默认选择。激活函数公式优点缺点阶跃函数(1 \text{ if } z0 \text{ else } 0)输出确定简单不可导无法反向传播Sigmoid(1/(1e^{-z}))平滑可导类似概率梯度消失计算稍贵ReLU(max(0,z))计算快缓解梯度消失负区间梯度为0可能神经元坏死在入门代码里我建议先用 Sigmoid因为它和反向传播的配合更容易理解。等熟悉了再换 ReLU 观察差异。4.3 多层感知机能解决异或问题吗——手推一遍光说“能”不够我们来亲手设计一组权重让一个 2-2-1 结构的感知机解决异或问题。网络结构两个输入节点隐藏层两个节点输出层一个节点。激活函数先用阶跃函数来手推因为这样最直观。隐藏层第一个神经元设计成 OR[ h_1 \text{step}(x_1 x_2 - 0.5) ]隐藏层第二个神经元设计成 AND[ h_2 \text{step}(x_1 x_2 - 1.5) ]输出层设计成(y \text{step}(h_1 - 2h_2 - 0.5))。我们来验证四个输入(x_1)(x_2)OR结果 (h_1)AND结果 (h_2)(h_1 - 2h_2 - 0.5)输出 y0000-0.5001100.5110100.511111-1.50完美输出和异或真值表完全一致。看到没我们并没有用多复杂的操作只是把两个线性分类器OR 和 AND的结果再组合一次就得到了非线性决策边界。这组权重是靠人肉设计的而训练过程要做的就是让网络自动学习出类似这样的一组权重。这就是多层感知机的威力。5. 实战用Python实现一个能跑的多层感知机5.1 代码结构前向传播、反向传播、梯度下降接下来用 numpy 手写一个 2-4-1 的多层感知机训练数据还是异或。我特意没用深度学习框架因为你只有亲手把反向传播的矩阵运算写一遍才能真正理解梯度是怎么流动的。结构上分成三块。前向传播就是一层层计算加权和、激活值反向传播就是根据最终损失逐层倒推每个权重的梯度权重更新就是沿负梯度方向迈一小步。下面是代码的核心框架import numpy as np def sigmoid(x): return 1 / (1 np.exp(-x)) def sigmoid_derivative(x): return x * (1 - x) np.random.seed(0) X np.array([[0, 0], [0, 1], [1, 0], [1, 1]]) y np.array([[0], [1], [1], [0]]) # 输入2维隐藏层4个神经元输出1维 w1 np.random.randn(2, 4) * 0.5 b1 np.zeros((1, 4)) w2 np.random.randn(4, 1) * 0.5 b2 np.zeros((1, 1))注意这里初始化没有用全零而是用随机小值等会解释原因。5.2 训练异或分类器完整代码与输出训练过程用梯度下降。代码如下lr 0.5 for epoch in range(10000): # 前向传播 z1 X.dot(w1) b1 a1 sigmoid(z1) z2 a1.dot(w2) b2 a2 sigmoid(z2) loss np.mean((a2 - y) ** 2) # 反向传播 delta2 (a2 - y) * sigmoid_derivative(a2) delta1 delta2.dot(w2.T) * sigmoid_derivative(a1) w2 - lr * a1.T.dot(delta2) b2 - lr * np.sum(delta2, axis0, keepdimsTrue) w1 - lr * X.T.dot(delta1) b1 - lr * np.sum(delta1, axis0, keepdimsTrue) if epoch % 1000 0: print(fepoch {epoch}, loss {loss:.6f}) predictions (a2 0.5).astype(int) for i, xi in enumerate(X): print(f{xi} - {a2[i][0]:.4f} - {predictions[i][0]})跑出来的结果大概是这样epoch 0, loss 0.255000 epoch 1000, loss 0.002712 epoch 2000, loss 0.001015 ... epoch 9000, loss 0.000053 [0 0] - 0.0090 - 0 [0 1] - 0.9908 - 1 [1 0] - 0.9906 - 1 [1 1] - 0.0059 - 0损失一路下降最终预测完全正确。从单层感知机的“怎么都学不会”到多层感知机的“轻松学会”这就是隐藏层带来的直观差异。5.3 调参与避坑学习率、初始化、损失函数的选择实际写多层网络时踩过的坑比理论多得多。第一学习率不能太大。多层网络反向传播时梯度经过了多层连乘数值本身就不稳定学习率太大会直接震荡甚至发散。我习惯从 0.1 开始试表现不好再降。第二权重初始化不要全零。如果第一层权重全零所有隐藏神经元的输入相同、输出相同反向传播时梯度也完全相同等于多个神经元退化成一个网络就失去了表达能力。所以至少要用随机小值打破对称性。第三损失函数选择要有意识。上面代码用的是均方误差简单直观但分类问题用交叉熵损失往往收敛更快、效果更好。深度框架里默认的分类损失基本都是交叉熵。另外判断阈值一般取 0.5但训练出的输出可能非常接近 0.5说明这个样本比较模糊。实际项目中我不直接硬取阈值而是把输出概率当成置信度再用验证集去选最适合的阈值这样准确率往往更稳。6. 从感知机到现代神经网络的扩展脉络6.1 感知机是深度学习的“地基”很多人上来就啃深度学习框架反而忽略了感知机这是比较可惜的。现代神经网络的核心思想和感知机几乎没有本质区别准备一堆可学习的参数设计一个可导的损失函数用梯度下降不断调整参数让预测逼近真实标签。感知机只有一个输出神经元网络加宽加深成了深度网络训练技巧从最朴素的随机梯度下降变成了 Adam、动量等优化器但骨架没变。可以这么说你一旦真正理解了感知机和多层感知机再去看卷积神经网络、循环神经网络你会发现它们不过是在“输入特征怎么提取”上做了各种创新网络内部的节点计算方式依然还是加权求和、加偏置、过激活、反向传播。地基打好了上面盖什么楼都容易。6.2 多层感知机在实际项目中的定位当然不要觉得多层感知机可以包打天下。它有它的最佳适用场景输入特征数量适中、数据量中等、特征之间确实存在非线性关系并且你不太介意特征工程成本。在这样的场景下MLP 可以作为一个强 baseline。但在图像、文本、音频这种高维结构化数据上MLP 并不占优。图像需要把局部模式提炼成平移不变特征文本需要处理变长序列和词序关系这些直接用全连接层来做参数量巨大还容易过拟合。所以现在的实际项目里MLP 更多被嵌入到更大的网络里作为最后的分类层或回归头。理解它的位置比学会怎么用框架搭模型更重要。6.3 下一步该学什么反向传播、优化器、过拟合如果你看完这篇文章已经能自己从零写出感知机和多层感知机下一步建议按顺序补齐这几块反向传播的矩阵推导优化器的原理与选择过拟合的识别与处理方法还有交叉熵损失为什么比均方误差更适合分类。每一样都可以用“手写 小实验”的方式去学而不是只看理论。我个人的经验是每学一个新概念就回去看一眼感知机代码里哪一步受到了影响。比如学习率对应梯度更新里的lr过拟合对应训练集准确率远高于测试集优化器对应权重更新公式里多出的动量项。你会发现原来所有抽象概念都藏在实打实的代码里之间的联系一目了然。最后再分享一个小技巧训练完多层感知机后把隐藏层的输出和权重打印出来当作普通表格数据去观察。你会看到第一层多个神经元在自动学习不同的特征组合第二层再把它们拼成最终边界。这种直观感受比背诵一百遍“神经网络有很强表达能力”都更让人信服。希望这篇文章能让你少走几步弯路真正把感知机这个“神经网络原点”踩实。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑