news 2026/10/8 14:22:15

AI-For-Beginners 感知机实验指南:用 One-vs-All 策略实现 MNIST 十类手写数字识别

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AI-For-Beginners 感知机实验指南:用 One-vs-All 策略实现 MNIST 十类手写数字识别
  • 教程
  • 人工智能
  • 机器学习
  • 深度学习

【免费下载链接】AI-For-Beginners

12 Weeks, 24 Lessons, AI for All!

项目地址:https://gitcode.com/GitHub_Trending/ai/AI-For-Beginners
点击查看免费下载

本篇技术指南对应 AI-For-Beginners 课程中"感知机(Perceptron)"一课的实验(Lab)任务。课程此前已用感知机完成了 MNIST 手写数字的二分类,本实验要求将其扩展为能够识别 0~9 任意数字的多分类器,并计算训练集与测试集上的分类准确率、输出混淆矩阵。读完本文,你将掌握 One-vs-All 多分类的构造思路、如何复用感知机训练代码训练 10 个二分类器、如何通过argmax完成数字预测,以及如何用"一次矩阵乘法"同时应用全部 10 个感知机的进阶技巧。

实验背景:从二分类感知机说起

感知机(Perceptron)是最简单的神经网络结构,由 Frank Rosenblatt 于 1957 年提出。它是一个二分类模型:对于每个输入向量x,感知机的输出为 +1 或 -1,由以下公式计算:

y(x) = f(wᵀx)

其中f是阶跃激活函数:

f(x) = +1 (当 x ≥ 0) f(x) = -1 (当 x < 0)

训练感知机的目标是找到一组权重向量w,使大部分样本被正确分类,即最小化**感知机准则(perceptron criterion)**定义的误差:

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

其中求和只针对被错误分类的训练样本,tᵢ对正负样本分别取 +1 / -1。通过梯度下降迭代更新权重:w(t+1) = w(t) + Σ η·xᵢ·tᵢ,其中η是学习率。上述理论在课程讲义中有完整推导。

感知机是线性分类器,只能区分线性可分的两类数据。MNIST 数据集包含 60000 张手写数字训练样本和 10000 张测试样本,均为 28×28 的灰度图(每张图即 784 维特征向量)。课程在 讲稿 Notebook 中已用感知机完成了"0 vs 1""2 vs 5"等二分类任务。本实验的核心挑战是:单个感知机只能回答"是/否",如何让它从 10 个数字中选出一个正确答案?

实验任务

任务原文(英文原版见 lab/README.md):

利用本课中为 MNIST 手写数字二分类开发的代码,创建一个能够识别任意数字的多分类器。分别计算训练集与测试集上的分类准确率,并输出混淆矩阵(confusion matrix)。

解题思路:三条 Hint 与一条关键技巧

实验说明给出了三条核心提示,外加一条可选的进阶技巧,完整继承如下:

  1. 为每个数字构造一个二分类数据集:对数字 0~9 中的每一个,构建"该数字 vs 其他所有数字"(One-vs-All)的二分类训练集;
  2. 训练 10 个不同的感知机:每个数字对应一个二分类感知机(共 10 个),分别负责判断"输入是否是数字 k";
  3. 定义分类函数:给定一个输入数字,综合 10 个感知机的输出,判定其最可能属于哪个数字类别。

进阶 Hint(原文档强调):如果把这 10 个感知机的权重合并成一个矩阵,就可以通过一次矩阵乘法同时把 10 个感知机应用到输入数字上。然后只需对输出结果执行argmax操作,即可找到最可能的数字。

这条 Hint 背后的直觉是:10 个感知机各自输出一个"激活分数"(wᵀx),分数越高代表该感知机越"确信"输入属于它的数字。哪个感知机的分数最大,就把输入判给哪个数字——这正是argmax的含义,也是 One-vs-All 多分类的经典范式。

起点 Notebook:仓库已备好的骨架代码

实验以 PerceptronMultiClass.ipynb 为起点。该 Notebook 已替你准备好了数据加载、训练函数和准确率函数,需要你在此基础上完成 One-vs-All 扩展。

依赖导入

import matplotlib.pyplot as plt import numpy as np import pickle import os

从课程讲稿复用的感知机训练函数

Notebook 中直接给出了课堂上开发的训练函数(Notebook 第 50-78 行):

def train(positive_examples, negative_examples, num_iterations = 100): num_dims = positive_examples.shape[1] 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): pos = random.choice(positive_examples) neg = random.choice(negative_examples) z = np.dot(pos, weights) if z < 0: # 正样本被误判为负 weights = weights + pos.reshape(weights.shape) z = np.dot(neg, weights) if z >= 0: # 负样本被误判为正 weights = weights - neg.reshape(weights.shape) # 每 10 轮报告一次当前正负样本准确率 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

注意两点:

  • 权重初始化为全零向量(讲稿 Notebook 的版本额外提供了learning_rate参数,默认0.01,并指出"更大的学习率(如1.0)学得更快但可能越过最优解,更小的学习率(如0.001)收敛更慢但更精细");
  • 训练中每隔report_frequency = 10轮会打印一次正/负样本的分类正确率,便于观察收敛过程。

准确率函数

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))

(见 Notebook 第 102-107 行)

该函数通过np.c_[test_x, np.ones(...)]为测试特征追加一列全 1 的 bias 维度,再与权重做点积,并统计预测符号与标签(±1)一致的比例。这意味着训练特征也应当采用同样的"附加 bias 维"约定,否则权重维度与accuracy()中的test_x维度不匹配——详见下文"常见坑"。

读取 MNIST 数据集

Notebook 提供了两种取数方式:从网络下载并解压mnist.pkl.gz,或直接从本仓库根目录的 data/mnist.pkl.gz 复制(讲稿 Notebook 的取数路径即为仓库内的../../../data/mnist.pkl.gz,见 Perceptron.ipynb 第 765-766 行)。解压后以 pickle 读取:

with open('mnist.pkl', 'rb') as mnist_pickle: MNIST = pickle.load(mnist_pickle)

数据结构为MNIST['Train']['Features'](训练特征)、MNIST['Train']['Labels'](训练标签),以及对应的MNIST['Test']子集。Notebook 中还对训练特征做了归一化展示:MNIST['Train']['Features'].astype(np.float32) / 256.0,把像素值从 0~255 缩放到 0~1,这对感知机训练的数值稳定性很有帮助。

已有的二分类数据构造函数(需要你改造)

Notebook 提供的是两个数字之间的 one-vs-other 数据构造函数(第 209-219 行):

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

实验的第一步改造:将其改为 one-vs-all 版本——正样本仍然是某个数字的全部图像,负样本则取除该数字外所有其他数字的图像(判定条件从j == negative_label改为j != positive_label)。

Notebook 中的待办清单

Notebook 末尾明确列出了你需要依次完成的工作(第 225-231 行):

  1. 为全部 10 个数字创建 10 份 one-vs-all 数据集;
  2. 训练 10 个感知机;
  3. 定义classify函数执行数字分类;
  4. 测量分类准确率并打印混淆矩阵;
  5. (可选)改进classify函数,使其用一次矩阵乘法完成分类。

完整实现:One-vs-All 参考解法

下面给出一套与仓库骨架代码一致的参考实现(注意训练与测试特征统一附加 bias 维)。

第 1 步:构造 10 个 one-vs-all 数据集

def get_one_vs_all(positive_label, mnist): """为指定数字构造 one-vs-all 数据集""" positive_indices = [i for i, j in enumerate(mnist['Labels']) if j == positive_label] negative_indices = [i for i, j in enumerate(mnist['Labels']) if j != positive_label] # 关键改动:其余全是负样本 positive_images = mnist['Features'][positive_indices] negative_images = mnist['Features'][negative_indices] return positive_images, negative_images def add_bias(X): """附加全 1 的 bias 列,与 accuracy() 的约定保持一致""" return np.c_[X, np.ones(len(X))]

第 2 步:训练 10 个感知机

weights = [] # 存放 10 个感知机的权重,每个形状为 (785, 1) for digit in range(10): pos, neg = get_one_vs_all(digit, MNIST['Train']) pos, neg = add_bias(pos), add_bias(neg) # 训练特征也加 bias 维 w = train(pos, neg) weights.append(w)

训练过程中观察每 10 轮的打印输出,正负样本正确率应逐步攀升。这里可以直接复用train()函数,无需改动训练算法本身——多分类能力不是靠改算法,而是靠"10 个二分类器投票"组合出来的。

第 3 步:定义 classify 函数

def classify(x, weights): """返回输入数字 x 最可能属于的类别 0~9""" x_b = np.r_[x, 1] # 与训练一致,附加 bias 维 scores = np.array([np.dot(x_b, w)[0] for w in weights]) # 10 个感知机的分数 return np.argmax(scores) # 取分数最大的类别

classify对单个样本分别计算 10 个感知机的激活值,argmax返回得分最高的感知机所对应的数字。

第 4 步:计算训练/测试准确率并打印混淆矩阵

def compute_accuracy(features, labels, weights): X = add_bias(features) preds = [classify(x, weights) for x in X] return np.mean(preds == labels) print("Train accuracy:", compute_accuracy(MNIST['Train']['Features'], MNIST['Train']['Labels'], weights)) print("Test accuracy:", compute_accuracy(MNIST['Test']['Features'], MNIST['Test']['Labels'], weights))

混淆矩阵可用sklearn.metrics.confusion_matrix(sklearn 在讲稿 Notebook 中已有使用)输出:行表示真实标签、列表示预测标签,对角线元素即各类别被正确识别的数量,非对角线元素暴露了"哪些数字之间容易混淆"——例如 2 与 5、3 与 8 常出现互相误判。

进阶:用一次矩阵乘法替代 10 次点积

按实验说明的 Hint,把 10 个权重向量横向拼接成一个大矩阵,即可向量化批量打分:

# 将 10 个 (785,1) 权重向量拼接为 (785, 10) 的权重矩阵 W = np.hstack(weights) # 对整批样本一次矩阵乘法:X_bias 形状 (N, 785),结果形状 (N, 10) scores = np.dot(add_bias(X), W) preds = np.argmax(scores, axis=1) # 每行取分数最大的列索引

这一写法不仅代码更简洁,也充分体现了 Hint 中"所有感知机权重合并为矩阵 + 一次矩阵乘法 +argmax"的设计思想,并且便于直接向量化计算准确率与混淆矩阵。

常见坑与原理剖析

  • bias 维度的一致性:Notebook 提供的accuracy()内部用np.c_[test_x, np.ones(...)]补了一列 bias,而train()的权重维度取决于传入特征的列数。因此建议在训练前就给正/负样本统一附加 bias 列,保证权重维度为 785,与accuracy()及classify()的约定吻合。这也正是讲稿 Notebook 在玩具问题中"显式增加一维恒为 1 的特征来消去偏置项"的做法。
  • 特征归一化:将像素值除以 256.0 归一化到 [0,1] 区间,可显著改善梯度下降的数值稳定性,建议在训练前对训练与测试特征统一处理。
  • 线性可分性的边界:感知机本质是线性分类器。讲稿中通过 PCA 降维分析发现,"0 vs 1"在二维投影下可用直线清晰分开,而"2 vs 5"难以找到好的分离投影,因此 2 与 5 之间会出现误判(Perceptron.ipynb 讨论小节)。One-vs-All 方案把 10 类问题拆成 10 个二分类,每个二分类仍然受限于线性可分性——这正是实验预期观察到的现象,不必追求完美准确率。
  • 类别不平衡:One-vs-All 中每个感知机的负样本约为正样本的 9 倍。train()每轮只随机抽取一对正负样本,可以缓解不平衡影响;如需进一步提升,可考虑在每轮迭代中按比例采样。
  • XOR 局限:单层感知机无法解决 XOR 这类非线性问题(讲稿中有专门演示,准确率始终不超过 75%),这是后续课程引入多层神经网络的动机。

学习路径与延伸

完成本实验后,建议进一步:

  1. 重读课程讲义与讲稿 Notebook,巩固感知机准则、梯度下降推导与学习率影响;
  2. 用滑块交互式观察不同学习率(0.001~1.0)下决策边界的变化,理解收敛速度与稳定性之间的权衡;
  3. 在后续课程中学习多层神经网络、卷积网络等非线性分类器——讲稿预告,借助神经网络可将 MNIST 十分类准确率提升至 99% 以上,并能解决数字不对齐等问题。

相关资源索引:

  • 本实验英文原版说明:lessons/3-NeuralNetworks/03-Perceptron/lab/README.md
  • 起始 Notebook(含骨架代码与待办清单):lessons/3-NeuralNetworks/03-Perceptron/lab/PerceptronMultiClass.ipynb
  • 课程讲义与理论推导:lessons/3-NeuralNetworks/03-Perceptron/README.md
  • 讲稿 Notebook(MNIST 二分类、PCA、XOR 演示):lessons/3-NeuralNetworks/03-Perceptron/Perceptron.ipynb
  • MNIST 数据集(pickle/gzip 格式):data/mnist.pkl.gz
  • 教程
  • 人工智能
  • 机器学习
  • 深度学习

【免费下载链接】AI-For-Beginners

12 Weeks, 24 Lessons, AI for All!

项目地址:https://gitcode.com/GitHub_Trending/ai/AI-For-Beginners
点击查看免费下载

相关推荐

上一篇:Draggable 实战指南:使用 Droppable 模块实现可拖放元素与 Dropzone 交互
下一篇:electron-vue 测试指南:Karma + Mocha 单元测试与 Spectron 端到端测试全解析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

MonkeyCode实践:AI编程从失控到企业级流水线

前几天研发周会上&#xff0c;有个同事很兴奋地演示他新写的模块——用AI编程工具&#xff0c;一个下午搞定了平时两三天的活。代码评审的时候&#xff0c;我翻了翻他提交的内容&#xff0c;发现几个边界条件没处理&#xff0c;依赖版本也引错了&#xff0c;还有一段逻辑明显是…

作者头像 李华
网站建设 2026/10/8 14:17:49

某纯电牵引车整车控制系统

一、整车控制系统VCU主要功能 VCU接收来自驾驶员的开关信号&#xff0c;如钥匙开关信号、油门位置、刹车、档位、制动等等&#xff0c;然后通过计算和处理&#xff0c;来实现对整车驱动控制及其它控制功能。1、电机控制&#xff1a; 通过接收驾驶员指令&#xff0c;以及整车相关…

作者头像 李华
网站建设 2026/10/8 14:12:54

YOLO模型过拟合的早期预警信号与应对策略:验证集mAP开始下降时该做什么

导读:你花了三天三夜调优YOLO模型,训练集mAP一路飙升到0.85,满心欢喜地准备上线——结果验证集mAP在第80轮突然掉头向下,最终测试效果惨不忍睹。这不是你运气差,而是你错过了过拟合的早期预警信号。本文结合Ultralytics官方文档、YOLO11/YOLO26最新训练实践及工业部署一线…

作者头像 李华
网站建设 2026/10/8 14:12:24

Karpathy的四层输出阶梯:让LLM产出高质量内容

你可能在时间线上刷到过这样一张截图&#xff1a;Andrej Karpathy&#xff0c;那位在OpenAI和特斯拉都留下深刻印记的AI研究者&#xff0c;在分享大语言模型使用心得时&#xff0c;提出了一个“四层输出阶梯”的说法。那篇内容确实在海内外社区拿下了400多万浏览、5.7万多次收藏…

作者头像 李华