聊聊感知机

最近在入门机器学习,从感知机学起。本来以为这个 1958 年的老模型结构那么简单,半小时就能翻篇,结果真看进去,卡住我的地方一个接一个:偏置到底为什么必须存在、超平面在二维以外长什么样、"训练就是找分割线"这个说法对不对、高维空间的数据凭什么能画在二维图上。最后干脆用 C++ 把它实现了一遍,还拿到真实数据上跑了跑。

这篇就是这个学习过程的整理。昨天那篇《加权求和式子的各种写法》其实也是这次研究过程中的一个小分支,单独摘出去了。

先摆一条时间线

感知机在神经网络的历史里是什么位置?列一下就清楚了:

年份 事件
1958 Frank Rosenblatt 提出感知机(Perceptron),最简单的单层全连接网络
1986 Rumelhart 等人推广反向传播,多层全连接网络(MLP)开始实用
1989 Yann LeCun 等人把卷积神经网络用于手写数字识别(LeNet 雏形)
1998 LeNet-5 正式发表,第一个广为人知的 CNN 架构

全连接网络比 CNN 早了大约 30 年。CNN 的出现,是为了解决全连接网络处理图像时的两个毛病:参数太多、忽略像素之间的空间结构。而这个可以追溯到 1958 年的感知机,就是下面要聊的主角。

感知机是什么

结构:加权求和 + 阶跃函数

感知机本质上是一个二元线性分类器,结构简单到一句话能说完:输入加权求和、加个偏置、过一遍阶跃函数,输出 0 或 1。

感知机的结构和阶跃激活函数

数学表达式:

$$\hat{y} = f\Big(\sum_{i=1}^{n} w_i x_i + b\Big), \qquad f(z) = \begin{cases} 1, & z > 0 \\ 0, & z \le 0 \end{cases}$$

几何意义:一条分割线

感知机的决策规则是 \(\sum_i w_i x_i + b > 0\)。在二维平面上,等号成立的所有点构成一条直线,把平面分成两半:一侧输出 1,另一侧输出 0。

用 AND 和 XOR 两个经典问题对比最直观:

AND问题与XOR问题的线性可分性对比

学习规则:错误驱动

感知机的学习方式也很朴素:预测错了就调整权重,让决策边界往减少错误的方向移动

$$w \leftarrow w + \eta (y - \hat{y}) x$$

其中 \(\eta\) 是学习率(比如 0.1),\(y\) 是真实标签,\(\hat{y}\) 是预测值。直观理解:

感知机的学习过程

上图是一次真实的模拟:从随机权重开始,每一轮把误分的样本挨个拿出来修,决策边界一步步挪过去,第 4 轮全部分类正确,收敛。

这背后有个著名的感知机收敛定理(Rosenblatt 自己证的):只要数据线性可分,这个算法保证在有限步内收敛。反过来,数据线性不可分时,它会永远震荡下去——XOR 就是这种情况。这个"震荡"不是理论上的吓唬人,本文最后的实测会亲眼看到它。

偏置到底为什么必须存在

这是我第一个真正卡住的地方。\(b\) 看起来像个无关紧要的常数,但它的数学本质是:给决策边界一个"平移自由度"

偏置的作用

可以从三个层面理解:

几何层面:没有偏置,边界必须过原点。 决策边界是满足 \(\sum_i w_i x_i + b = 0\) 的所有点。如果强制 \(b=0\),方程变成 \(w \cdot x = 0\),原点 \(x=0\) 天然满足——边界被钉死在原点上,只剩"旋转"这一个自由度。上图左半就是这种困境:那组数据明明一条斜线就能分开,但所有过原点的直线都做不到。

代数层面:\(b\) 就是直线方程的常数项。 二维直线的标准写法是 \(ax + by + c = 0\)\(c\) 决定位置,\(a\)\(b\) 决定方向。\(w\) 管方向、\(b\) 管位置,缺一不可。少了 \(b\),表达能力直接腰斩。

统一视角:\(b\) 是对"恒为 1 的虚拟输入"的权重。 这是个很优雅的理解:把输入向量增广一个维度,

$$\tilde{x} = (x_1, \dots, x_n, 1), \qquad \tilde{w} = (w_1, \dots, w_n, b)$$

那么 \(\tilde{w} \cdot \tilde{x} = \sum_i w_i x_i + b\),感知机又变回了"纯加权求和"的形式,偏置不需要特殊处理。很多神经网络框架内部就是这么实现的,文末的 C++ 代码里也用了这个技巧。

还可以打个直观的比方:把感知机想象成录取系统,没有偏置相当于"加权总分必须大于 0 才录取"——这个 0 的门槛写死了。偏置让你能独立调整录取线的高低,而不用动各科权重。

一句话:偏置不是锦上添花,而是线性模型完整表达能力的必要组件

超平面:不止二维

这里有个概念值得澄清:直线是二维空间的超平面吗?答案是:对,但只是特例。

超平面的定义是:\(n\) 维空间中,维度为 \(n-1\) 的线性子空间。"超"(hyper)不是说它比平面更高级,而是说它比所在空间恰好低一维。

超平面在2维和3维空间中的形态

空间维度 超平面是什么 超平面维度
2 维 直线 1
3 维 平面 2
784 维(28×28 灰度图的像素) 无法想象的"扁平"结构 783

不管多少维,超平面都有两个不变的性质:维度比空间少一;把空间恰好切成两个半空间。感知机的决策边界在任何维度下都满足这两条,所以它永远是一个超平面——二维时你看得见它是条线,784 维时它存在但画不出来。

训练就是找超平面,但找到的不一定是最好的

到这里可以总结一句:感知机的训练就是在找那个分割超平面。这话大方向对,但要加两个限定

第一,感知机不是解析地"算出"超平面,而是迭代试错逼近:每次更新 \(w \leftarrow w + \eta(y-\hat{y})x\),几何上等价于旋转和平移边界,让它朝减少错误的方向挪。

第二,也是更关键的:收敛定理只保证找到"某一个"能完美分类的超平面,不保证是"最好的"。它可能刚好擦着样本边缘过去,对噪声非常敏感。

感知机与SVM找到的超平面对比

这正是感知机和 SVM 的分水岭:

感知机 SVM
目标 找到一个能分开的就行 间隔最大的那个
解的性质 可行解,不唯一 最优解,唯一
对新样本 可能贴着旧样本边缘,容易翻车 离两类都尽量远,更稳

所以后来 SVM 解决"找最优超平面"的问题,多层神经网络解决"数据不是线性可分"的问题——感知机的两个短板,各自催生了一条技术路线。

加权求和算出来的到底是什么

这是我第二个真正卡住的地方:看感知机的示意图时,总觉得是把高维的值"映射"到了二维平面上,但说不清其中的数学含义。拆开其实是两个问题。

问题一:\(z = \sum_i w_i x_i + b\) 的结果是什么? 是一个标量,但这个标量的几何意义很丰富——它是"有符号距离"的缩放版本

换个等价的说法:\(z\) 是向量 \(x\) 在法向量 \(w\) 方向上的投影长度(乘以 \(\|w\|\) 再加 \(b\))。

加权求和的几何意义与高维切片

问题二:784 维的数据凭什么画在二维图上? 答案是:我们并没有"映射",只是切片

打个比方:给一个立方体拍正面照,照片是二维的,但立方体还是三维的,你只是看不见深度轴。图片数据同理——一张 28×28 的灰度图,把像素拉平了就是一个 784 维向量;二维散点图只显示其中 \(x_i\)\(x_j\) 两个坐标,其余 782 维仍然存在,只是没画。

而且这种切片图不是近似的比喻,是精确的数学事实:固定其他 782 个变量后,\(w_i x_i + w_j x_j + (\text{常数}) = 0\)\((x_i, x_j)\) 平面上就是一条直线。783 维超平面与二维坐标平面的交线,必然是直线

顺带一提,\(\sum_i w_i x_i\) 这个式子本身还有很多等价写法(点积、矩阵乘法、爱因斯坦求和约定……),我单独整理在了上一篇里,这里不重复了。

能不能只用一维来观察

顺着切片的思路再进一步:既然 2D 只是切片,那能不能用 1D 来观察分割?答案是可以,但 1D 观察会丢信息,甚至产生误导。

把边界方程 \(w_1 x_1 + w_2 x_2 + b = 0\) 里的 \(x_2\) 固定住,解出来就是一个 1D 阈值点:

$$x_1 = -\frac{w_2 x_2 + b}{w_1}$$

问题在于:这个阈值依赖你藏起来的那个维度

1D切片与2D观察的对比

上图左边是完整的 2D 视图,中间和右边是沿 \(x_2 = 1.0\)\(x_2 = 3.2\) 切出来的两条 1D 数轴——同一个超平面,切出来的阈值一个在 3.1、一个在 1.0,完全不同。而且中间那张图里,两个红点和蓝方块的投影挤在阈值附近,在 1D 上你根本判断不了它们的真实空间关系。

打比方说:1D 观察像透过门缝看房间,2D 观察像透过窗户看——窗户仍是投影,但至少能看清两个维度如何共同决定分类。

当然 1D 也不是没用:分析单个特征的贡献、理解决策树(决策树本质上就是一串 1D 阈值分割)、向非技术人员解释"这个指标超过某值就判正类",这些场景下 1D 阈值反而是最直观的。只是要心里清楚:那只是高维超平面在你选定的切片方向上的一个"影子"

动手实现:C++ 写一个感知机

理论说完了,不如动手写一遍。感知机简单到核心代码 30 行就够了(C++17,零依赖):

// 单层感知机:ŷ = step(w·x + b)
struct Perceptron {
    std::vector<double> w;
    double b = 0.0;
    double lr = 0.1;                 // 学习率 η

    explicit Perceptron(size_t n) : w(n, 0.0) {}

    int predict(const std::vector<double>& x) const {
        double z = b;
        for (size_t i = 0; i < x.size(); ++i) z += w[i] * x[i];
        return z > 0 ? 1 : 0;
    }

    // 错误驱动:预测错了才更新 w ← w + η(y-ŷ)x
    void update(const std::vector<double>& x, int y) {
        int d = y - predict(x);
        if (d == 0) return;
        for (size_t i = 0; i < w.size(); ++i) w[i] += lr * d * x[i];
        b += lr * d;
    }
};

predict 就是前文的 \(\hat{y} = f(\sum_i w_i x_i + b)\)update 就是学习规则 \(w \leftarrow w + \eta(y-\hat{y})x\),逐行对应,没有别的东西了。

先跑 AND:6 轮收敛

拿 AND 问题的 4 个样本喂给它,输出:

AND: epoch 6 收敛, w=(0.20, 0.10), b=-0.20
  (0, 0) -> 0 (期望 0)
  (0, 1) -> 0 (期望 0)
  (1, 0) -> 0 (期望 0)
  (1, 1) -> 1 (期望 1)

第 6 轮收敛,学到的边界是 \(0.2x_1 + 0.1x_2 - 0.2 = 0\),即 \(2x_1 + x_2 = 2\)——正好把 \((1,1)\) 和其他三个点分开。和收敛定理说的一样:线性可分,有限步内搞定。

再上真实数据:亲眼看到"震荡"

再拿它跑一个真实任务:一个经典的手写数字图片数据集,60,000 张训练图 + 10,000 张测试图,每张 28×28 灰度、共 10 类。文件格式很简单——4 字节大端魔数 + 维度信息 + 原始像素字节,十几行代码就能解析。数据可以从官方镜像下载:storage.googleapis.com/cvdf-datasets/mnist

感知机是二分类器,做 10 分类的标准做法是one-vs-rest:训练 10 个感知机,第 \(k\) 个只负责回答"是不是数字 \(k\)";预测时取加权和 \(z\) 最大的那个。像素归一化到 \([0,1]\),输入末尾增广一维常数 1,把偏置并进权重——就是偏置那节说的技巧。实测输出(学习率 0.1,逐样本更新,10 轮):

epoch  1: 测试集准确率 85.95%
epoch  2: 测试集准确率 85.88%
epoch  3: 测试集准确率 86.05%
epoch  4: 测试集准确率 87.42%
epoch  5: 测试集准确率 84.95%
epoch  6: 测试集准确率 85.07%
epoch  7: 测试集准确率 89.03%
epoch  8: 测试集准确率 87.95%
epoch  9: 测试集准确率 88.52%
epoch 10: 测试集准确率 87.52%

注意这个曲线的形状:它不稳定上升,也不收敛,就在 85%~89% 之间来回晃——第 4 轮 87.42%,第 5 轮反而掉到 84.95%。这正是收敛定理的另一面:手写数字的像素空间不是线性可分的,不存在零误分的超平面,感知机只能永远修修补补。理论预言的震荡,实测真的看到了。

对照组:加两层隐藏层值多少钱

我之前从零写过一个三层全连接网络(784→256→128→10,ReLU + Softmax + 交叉熵 + 反向传播)跑同一份数据,实测测试集准确率 97.35%,而且准确率随训练稳定上升。摆在一起看:

单层感知机 ×10 三层全连接网络
结构 784 → 10(纯线性) 784 → 256 → 128 → 10
决策边界 10 个超平面 分片线性的"折面"
测试集准确率 85%~89%,震荡不收敛 97.35%,稳定收敛
训练方式 错误驱动的更新规则 反向传播 + SGD

这中间约 10 个点的差距,就是那两层隐藏层买来的东西:把"只能画直线"升级成"能折出曲面"。完整的 IDX 解析和数据加载代码就不贴了(读魔数、读维度、逐字节归一化,都是体力活),核心逻辑全在上面那 30 行里。

最后

一圈研究下来,感知机这个 1958 年的老模型比我想象的耐嚼:

没有感知机的失败,可能就没有后来的深度学习——这话不算夸张。而我自己的收获是,学这类模型时,把每个符号都追问到几何意义上(偏置是什么、标量 \(z\) 是什么、图是怎么画出来的),再动手把代码写出来跑一遍,比背公式有用得多。

这篇是学习笔记性质的整理,我自己的理解未必都对,有说错的地方欢迎指教。

参考资料