AI for Beginners 第 3 课实战解析:从感知机(Perceptron)到多分类手写数字识别

原创2026-10-07 10:52:361,632 阅读
文章标签:教程人工智能机器学习深度学习

AI for Beginners 第 3 课实战解析:从感知机(Perceptron)到多分类手写数字识别

感知机(Perceptron)是神经网络的起点,也是 AI for Beginners 课程中"神经网络"章节的第一课(lessons/3-NeuralNetworks/03-Perceptron/README.md)。本篇文章以该课为核心骨架,结合仓库内的 Perceptron.ipynb 交互式笔记与 多分类实验,系统讲解感知机的数学原理、梯度下降训练流程、Python 实现,以及如何将其从二分类扩展到十类手写数字(MNIST)识别。读完本文,你将能够独立实现一个感知机、用它对 MNIST 做二分类并理解其线性分类的边界,还能掌握"一对多"策略将其升级为多分类器。

Frank Rosenblatt 肖像,感知机的提出者,来自康奈尔航空实验室

Mark 1 感知机硬件照片,展示了布满线路的 20x20 光电管阵列结构

从 Mark-1 讲起:感知机的历史原型

感知机的第一代实现来自 Frank Rosenblatt。1957 年,他在康奈尔航空实验室完成了名为 Mark-1 的硬件设备,这是最早尝试实现"类似现代神经网络"的机器之一。Mark-1 被设计用于识别简单的几何图形,如三角形、正方形和圆形。

Mark-1 的工作方式非常直观:

  • 输入图像由一个 20 × 20 的光电管阵列表示,因此网络共有 400 个输入;
  • 网络只有一个二值输出;
  • 整个网络只包含一个神经元,这种神经元也被称为阈值逻辑单元(threshold logic unit);
  • 神经网络的权重在当时扮演着"电位器(potentiometer)"的角色——需要在训练阶段手动调节(电位器是一种允许使用者调节电路电阻的装置)。

当时《纽约时报》对感知机的描述颇具时代色彩:称其为"电子计算机的胚胎,[海军]期待它能够行走、说话、观看、书写、自我繁殖,并意识到自身的存在"。尽管今天的视角看这些期待过于乐观,但感知机作为第一个可操作的神经网络模型,其地位无可替代。

感知机模型:一个带阶跃激活的线性二元分类器

数学定义

假设模型有 N 个特征,那么输入向量 x 的维度就是 N。感知机是一个二分类模型(binary classification model),即它只能在两类输入数据之间做出区分。

对每个输入向量 x,感知机的输出为 +1 或 -1,具体取决于样本所属的类别。输出由下面的公式计算:

y(x) = f(wᵀx)

其中 w 是权重向量,f 是阶跃激活函数(step activation function):

感知机的阶跃激活函数:f(x) = +1 当 x ≥ 0,f(x) = -1 当 x < 0

从几何上看,wᵀx = 0 定义了一个(超)平面;样本落在平面一侧输出 +1,落在另一侧输出 -1。这就是感知机作为线性分类器的本质:它在样本空间中用一个超平面把两类数据分开。

偏置项的处理技巧

在 Perceptron.ipynb 中,作者特别说明了一个实现细节:一个通用的线性模型通常还应该包含偏置项 b,即理想情况下应计算 y = f(wᵀx + b)。但为了简化模型,可以给输入特征额外增加一维、其值恒为 1,从而把偏置项"吸收"进权重向量。笔记中的代码正是这样做的:

pos_examples = np.array([ [t[0], t[1], 1] for i,t in enumerate(train_x)
                          if train_labels[i]>0])
neg_examples = np.array([ [t[0], t[1], 1] for i,t in enumerate(train_x)
                          if train_labels[i]<0])

这样,二维数据变成三维(第三个维度恒为 1),决策边界从直线 w₀x₀+w₁x₁ = 0 变成 w₀x₀+w₁x₁+w₂ = 0,偏置项 w₂ 也一并被学习出来。

训练感知机:感知机准则与梯度下降

感知机准则(Perceptron Criterion)

训练感知机的目标,是找到一个权重向量 w,使绝大多数样本被正确分类,也就是让误差最小。误差 E 由感知机准则定义:

E(w) = -Σ wᵀxᵢtᵢ

其中:

  • 求和只针对那些被错误分类的训练数据点 i;
  • xᵢ 是输入数据,tᵢ 对负例和正例分别取 -1 和 +1。

由于 E 是权重 w 的函数,训练问题就变成了"最小化 E(w)"。

梯度下降更新公式

最小化误差最常用的方法是梯度下降(gradient descent):从某组初始权重 w⁽⁰⁾ 出发,每一步沿梯度方向更新权重:

w⁽ᵗ⁺¹⁾ = w⁽ᵗ⁾ - η∇E(w)

其中 η 是学习率(learning rate),∇E(w) 表示 E 的梯度。计算出梯度之后,更新公式可以进一步写为:

w⁽ᵗ⁺¹⁾ = w⁽ᵗ⁾ + Σ ηxᵢtᵢ

也就是:每遇到一个被误分类的正例,就把该样本与学习率的乘积加进权重;每遇到一个被误分类的负例,则将其减去。直观地说,感知机在"纠正错误"的过程中不断调整决策边界。

Python 实现(课程原版代码)

课程 README 给出了一个最简洁的训练函数(lessons/3-NeuralNetworks/03-Perceptron/README.md):

def train(positive_examples, negative_examples, num_iterations = 100, eta = 1):

    weights = [0,0,0] # Initialize weights (almost randomly :)

    for i in range(num_iterations):
        pos = random.choice(positive_examples)
        neg = random.choice(negative_examples)

        z = np.dot(pos, weights) # compute perceptron output
        if z < 0: # positive example classified as negative
            weights = weights + eta*weights.shape

        z  = np.dot(neg, weights)
        if z >= 0: # negative example classified as positive
            weights = weights - eta*weights.shape

    return weights

完整版:带学习率与准确率报告的实现

Perceptron.ipynb 提供了更完整、可直接运行的版本,它显式加入了学习率参数 learning_rate(默认 0.01),并每隔若干轮打印一次正例/负例的分类准确率:

def train(positive_examples, negative_examples, num_iterations = 100, learning_rate = 0.01):
    num_dims = positive_examples.shape[1]

    # Initialize weights.
    # We initialize with 0 for simplicity, but random initialization is also a good idea
    weights = np.zeros((num_dims,1))

    pos_count = positive_examples.shape[0]
    neg_count = negative_examples.shape[0]

    report_frequency = 10

    for i in range(num_iterations):
        # Pick one positive and one negative example
        pos = random.choice(positive_examples)
        neg = random.choice(negative_examples)

        z = np.dot(pos, weights)
        if z < 0: # positive example was classified as negative
            weights = weights + learning_rate * pos.reshape(weights.shape)

        z  = np.dot(neg, weights)
        if z >= 0: # negative example was classified as positive
            weights = weights - learning_rate * neg.reshape(weights.shape)

        # Periodically, print out the current accuracy on all examples
        if i % report_frequency == 0:
            pos_out = np.dot(positive_examples, weights)
            neg_out = np.dot(negative_examples, weights)
            pos_correct = (pos_out >= 0).sum() / float(pos_count)
            neg_correct = (neg_out < 0).sum() / float(neg_count)
            print("Iteration={}, pos correct={}, neg correct={}".format(i,pos_correct,neg_correct))

    return weights

笔记中的实际运行输出显示:初始准确率约为 50%,很快上升到接近 90%,例如第 90 轮时负例准确率已达到 100%、正例约为 84%:

Iteration=0, pos correct=0.263, neg correct=0.619
Iteration=10, pos correct=0.895, neg correct=0.857
Iteration=20, pos correct=0.842, neg correct=1.0
...
Iteration=90, pos correct=0.842, neg correct=1.0
[[-0.66042328  4.90850882 -1.]]

训练结束后,返回的权重 [[-0.66, 4.91, -1]] 就定义了解释训练数据的决策直线 -0.66·x₀ + 4.91·x₁ - 1 = 0。

学习率:收敛速度与稳定性的权衡

笔记专门用一个章节讨论学习率的影响,它直接落实了梯度下降更新公式中的 η:

  • 较大的学习率(如 1.0)会让感知机学得更快,但可能"冲过头"(overshoot),跳过最优解;
  • 较小的学习率(如 0.001)收敛更慢,但可能更精确地逼近最优解;
  • 可以通过 train(pos_examples, neg_examples, learning_rate=0.1) 这样的调用自由实验。

笔记中还提供了两类直观的实验手段:

  1. 批量对比:用 learning_rates = [0.001, 0.01, 0.1, 1.0] 分别训练,在 2×2 子图中对比四条决策边界的差异;
  2. 交互式滑杆:通过 ipywidgets.interact 与 FloatSlider 动态调整学习率,实时观察决策边界与最终权重(w₀、w₁、bias 的柱状图)的变化。

运行这些单元依赖 ipywidgets、matplotlib、numpy、scikit-learn 等库,其中大部分已在仓库根目录的 requirements.txt 中声明(如 numpy、scikit-image 等,注意 pylab/ipywidgets 属于 notebook 运行环境的一部分)。

从玩具问题开始:数据生成与测试集评估

为了直观验证感知机,笔记先生成了一个"玩具问题":用 sklearn.datasets.make_classification 生成 50 个样本、每个样本含两个特征(例如医学上可以理解为"肿瘤大小"与"年龄"),并将标签从 0/1 转换为 -1/+1:

n = 50
X, Y = make_classification(n_samples = n, n_features=2,
                           n_redundant=0, n_informative=2, flip_y=0)
Y = Y*2-1 # convert initial 0/1 values into -1/1
X = X.astype(np.float32); Y = Y.astype(np.int32)

# Split the dataset into training and test
train_x, test_x = np.split(X, [ n*8//10])
train_labels, test_labels = np.split(Y, [n*8//10])

这里按 8:2 的比例切分了训练集与测试集。笔记随后用 plot_boundary 把正负样本和决策直线画在同一张图上,直观展示两类点的分离效果。

在测试集上计算准确率

训练完成后,需要对从未参与训练的测试集评估模型泛化能力。笔记中的 accuracy 函数体现了"加权内积与标签同号即正确"的思想:

def accuracy(weights, test_x, test_labels):
    res = np.dot(np.c_[test_x,np.ones(len(test_x))],weights)
    return (res.reshape(test_labels.shape)*test_labels>=0).sum()/float(len(test_labels))

accuracy(wts, test_x, test_labels)   # 输出:1.0

注意这里同样用 np.ones 为测试特征补了一维恒 1 的偏置列,与训练阶段的预处理保持一致——这是保证推理结果正确的前提。

观察训练过程

笔记还提供 train_graph 函数,它在训练过程中以固定频率(report_frequency = 15)把当前权重的快照和综合准确率 (pos_correct+neg_correct)/2.0 存入数组,再通过 interact + IntSlider 让读者"穿越"训练过程:左图展示决策边界随迭代轮次的变化,右图展示准确率曲线。

感知机的局限:XOR 问题

感知机是线性分类器。当两类数据线性可分时它能出色地工作;否则训练过程不会收敛。

最经典的不可解问题是 XOR 异或问题。XOR 布尔函数的真值表为:

0 1
0 0 1
1 1 0

笔记手工构造了正负样本:

pos_examples_xor = np.array([[1,0,1],[0,1,1]])
neg_examples_xor = np.array([[1,1,1],[0,0,1]])

snapshots_xor = train_graph(pos_examples_xor,neg_examples_xor,1000)

用训练好的感知机观察结果可以发现:准确率始终无法超过 75%,因为不存在一条直线能把四个点正确划分。XOR 问题由 Marvin Minsky 和 Seymour Papert 于 1969 年在《Perceptrons》一书中明确指出,这一结论一度使神经网络研究沉寂了将近十年——尽管正如课程后续内容将要展示的,多层感知机完全可以解决这类问题。这也是课程从"单层感知机"走向"多层神经网络"的关键动机。

实战:用感知机识别 MNIST 手写数字

尽管感知机解决不了 XOR,它却能胜任许多更复杂的问题,例如手写字符识别。

MNIST(Modified National Institute of Standards and Technology)是机器学习入门最常用的数据集:训练集包含 60000 张手写数字图像(采集自该研究所约 250 名学生与员工),测试集包含 10000 张(来自不同人群),所有图像为 28 × 28 像素的灰度图,即每个样本有 784 个输入特征。

仓库根目录的 data/mnist.pkl.gz 提供了数据文件,笔记通过 gzip + pickle 加载:

with gzip.open('../../../data/mnist.pkl.gz', 'rb') as mnist_pickle:
    MNIST = pickle.load(mnist_pickle, encoding='latin1')

如果不在克隆仓库内运行,笔记也给出提示:可以先手动获取数据文件再修正路径。加载后可用 features[i].reshape(28,28) 把像素向量还原为图像并可视化。

二分类:先分辨 0 和 1

由于感知机是二分类器,先限制在"只识别两个数字"。set_mnist_pos_neg(positive_label, negative_label) 负责按标签抽取正负样本:

def set_mnist_pos_neg(positive_label, negative_label):
    positive_indices = [i for i, j in enumerate(MNIST['Train']['Labels'])
                          if j == positive_label]
    negative_indices = [i for i, j in enumerate(MNIST['Train']['Labels'])
                          if j == negative_label]

    positive_images = MNIST['Train']['Features'][positive_indices]
    negative_images = MNIST['Train']['Features'][negative_indices]
    return positive_images, negative_images

然后分别训练"1 vs 0"与"2 vs 5"两个分类器(train_graph(pos1,neg1,1000)),并用滑杆观察训练过程。

权重矩阵的可解释性

笔记指出一个非常精彩的观察:训练结束后把权重向量 reshape 回 28 × 28 并画成热图,可以看到字段中间区域权重值高、两侧为负值——中间对应数字 1 通常会占用的像素,两侧对应数字 0 的竖向笔画。于是当输入确实是 1 时,中间像素乘以高权重产生正的输出;当输入是 0 时,两侧像素乘以负权重。这也揭示了感知机"记住模板"的本质。同时需要注意:如果数字 1 发生水平平移,其像素进入"0 的笔画区域",就可能被误判——MNIST 本身数字居中且对齐,感知机恰恰依赖了这一先验。

为什么 2 和 5 更难?

"2 vs 5"的训练准确率虽然能到 85% 以上,但明显出现"学不动"的停滞。笔记用 PCA(主成分分析) 解释原因:把 784 维输入降到 2 维再画散点图(正样本蓝色、负样本红色):

from sklearn.decomposition import PCA

def pca_analysis(positive_label, negative_label):
    positive_images, negative_images = set_mnist_pos_neg(positive_label, negative_label)
    M = np.append(positive_images, negative_images, 0)

    mypca = PCA(n_components=2)
    mypca.fit(M)

    pos_points = mypca.transform(positive_images[:200])
    neg_points = mypca.transform(negative_images[:200])

    pylab.plot(pos_points[:,0], pos_points[:,1], 'bo')
    pylab.plot(neg_points[:,0], neg_points[:,1], 'ro')

结论是:0 和 1 在 2 维投影下可以被一条直线清晰分开,说明它们在原始 784 维空间中也是线性可分的;而 2 和 5 找不到能清晰分离两类点的投影,因此必然存在误分类样本。这正好呼应了课程后面的承诺:借助神经网络构造非线性分类器、并解决数字未对齐的问题,MNIST 十类分类的准确率很快能突破 99%。

实验作业:从二分类扩展到多分类

课程作业要求完成 MNIST 的完整数字识别(十类),实验入口与说明见 lessons/3-NeuralNetworks/03-Perceptron/lab/README.md,起始代码见 lab/PerceptronMultiClass.ipynb。核心思路是经典的 one-vs-all(一对多) 策略:

  1. 对每个数字 d,构造"数字 d vs 其余所有数字"的二分类数据集;
  2. 训练 10 个感知机,每个负责一个数字的二分类;
  3. 定义 classify 函数完成对输入数字的分类;
  4. 在训练集与测试集上计算分类准确率,并打印混淆矩阵(confusion matrix);
  5. (选做)改进 classify:把 10 个感知机的权重拼成一个矩阵,这样只需一次矩阵乘法即可同时应用全部 10 个感知机,再用 argmax 取输出中最大的分量作为预测类别。

实验笔记中给出的 train 与 accuracy 直接复用课堂代码,set_mnist_pos_neg 则需要从"one-vs-other"改造成"one-vs-all"版本。完成该实验后,你就完成了从"手写单个神经元"到"组合出可用的十类分类器"的完整闭环,为后续课程学习多层神经网络打下了扎实基础。

小结

本课的核心结论可以归纳为四点:

  • 感知机是最简单的神经网络结构——单层、单神经元、阶跃激活,本质是一个线性二分类器,其分类能力与逻辑回归相当;
  • 训练感知机=最小化感知机准则定义的误差,核心手段是梯度下降,学习率 η 控制每步调整的幅度,需要在收敛速度与稳定性之间权衡;
  • 尽管结构简单,单层感知机也能解决诸如手写数字识别这类相当复杂的问题(在 MNIST 上进行"0 vs 1"、"2 vs 5"二分类训练即可达到 85% 以上的准确率);
  • 感知机只能用一个超平面分离两类数据,因此无法处理 XOR 等线性不可分问题——这一局限正是课程后续引入多层神经网络的动因。

你可以从 Perceptron.ipynb 开始动手运行每个单元,再挑战 多分类实验,亲手把准确率与混淆矩阵打印出来,完成从理论到实战的完整训练。

登录后查看全文
AI-For-Beginners