news 2026/10/5 1:57:31

用自研神经网络框架完成 MNIST 手写数字分类:AI-For-Beginners 多层感知机实验指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用自研神经网络框架完成 MNIST 手写数字分类:AI-For-Beginners 多层感知机实验指南
  • 教程
  • 人工智能
  • 机器学习
  • 深度学习

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

12 Weeks, 24 Lessons, AI for All!

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

本篇文章围绕 AI-For-Beginners 第 4 课《搭建自己的神经网络框架》配套实验展开,核心任务是使用课程中从零手写的神经网络框架,解决 MNIST 手写数字的 10 分类问题,并对比 1 层、2 层与 3 层感知机的效果。读者完成本文的实验后,将掌握Linear、Softmax、CrossEntropyLoss、Tanh、Net等模块化层的实现原理,理解前向传播、反向传播、随机梯度下降(SGD)在真实分类任务中的完整调用链,并能独立回答"激活函数是否影响性能""网络层数如何选择""训练中权重如何变化"等关键问题。

实验背景:从课程框架走向真实分类任务

在课程正文 README.md 中,我们学习了将单层感知机扩展为灵活框架的方法:支持多类分类、回归问题以及非线性可分的类别分离。课程配套笔记 OwnFramework.ipynb 逐步实现了完整的模块化框架——每一层都是一个带forward方法的 Python 类,可组合成任意深度的网络。

本实验(lab/README.md)则要求把这套框架从"玩具级"的二维二分类示例,迁移到 MNIST 手写数字识别这一经典的多分类真实任务上:分别用 1 层、2 层和 3 层感知机求解,并回答四个研究性问题。实验起点笔记为 MyFW_MNIST.ipynb。

实验准备:加载与拆分 MNIST 数据集

实验数据为 MNIST 手写数字集合。启动笔记中直接下载并解压数据:

!rm *.pkl !wget https://raw.githubusercontent.com/microsoft/AI-For-Beginners/main/data/mnist.pkl.gz !gzip -d mnist.pkl.gz

离线替代方案:该数据集同样已经存放在本仓库的 data/mnist.pkl.gz 目录中,可以直接复制使用,无需联网。随后用pickle载入,并取出训练集的特征与标签:

import pickle with open('mnist.pkl','rb') as f: MNIST = pickle.load(f) labels = MNIST['Train']['Labels'] data = MNIST['Train']['Features']

可以通过data.shape确认数据形状:MNIST 每张图片为 28×28 的灰度像素,展平后每行特征向量长度为 784,标签为 0~9 共 10 类。

接下来使用 Scikit-Learn 将数据拆分为训练集与测试集(80% / 20%):

from sklearn.model_selection import train_test_split features_train, features_test, labels_train, labels_test = train_test_split(data, labels, test_size=0.2) print(f"Train samples: {len(features_train)}, test samples: {len(features_test)}")

复用框架:把课程代码搬进实验

实验的第 1 条指令是:把课程中的框架代码粘贴到本笔记中,更好的做法是放进一个独立的 Python 模块。也就是说,本实验完全复用 OwnFramework.ipynb 中构建的框架,无需引入任何深度学习库。下面逐一回顾这些核心组件——它们是本实验的全部"武器"。

Linear 线性层

线性层完成z = W·x + b的计算。权重W使用np.random.normal(0, 1/sqrt(nin))初始化(方差按输入维度缩放,避免深层网络梯度消失),偏置b初始化为零。完整实现(含反向传播与参数更新)如下:

class Linear: def __init__(self,nin,nout): self.W = np.random.normal(0, 1.0/np.sqrt(nin), (nout, nin)) self.b = np.zeros((1,nout)) self.dW = np.zeros_like(self.W) self.db = np.zeros_like(self.b) def forward(self, x): self.x=x return np.dot(x, self.W.T) + self.b def backward(self, dz): dx = np.dot(dz, self.W) dW = np.dot(dz.T, self.x) db = dz.sum(axis=0) self.dW = dW self.db = db return dx def update(self,lr): self.W -= lr*self.dW self.b -= lr*self.db

注意:所有计算都针对整个 minibatch 进行,而非单条样本。dx、dW、db的维度分别为minibatch×nout、nout×nin、nout,对应公式:

  • ∂z/∂W = x,∂z/∂b = 1
  • Δx = Δz × W,ΔW = Δz × x,Δb = Δz

这正是反向传播时误差沿计算图向输入方向"回传"的落点。

Softmax:把输出变成概率

网络的原始输出可以是任意实数,需要经过softmax归一化为各类别的概率分布:

class Softmax: def forward(self,z): self.z = z zmax = z.max(axis=1,keepdims=True) expz = np.exp(z-zmax) Z = expz.sum(axis=1,keepdims=True) return expz / Z def backward(self,dp): p = self.forward(self.z) pdp = p * dp return pdp - p * pdp.sum(axis=1, keepdims=True)

实现中用zmax做数值稳定性处理(减去每行最大值再取指数),避免exp溢出。softmax 输出可解释为类别集合上的概率分布q = σ(z_c) = p̂(c|x),每一行概率之和恰为 1。

下图展示了带 softmax 输出的 MNIST 分类网络架构——输入 784 维像素向量,输出 10 类概率:

CrossEntropyLoss:交叉熵损失层

分类任务的损失是 logistic 损失的推广——交叉熵损失,用于度量网络输出概率分布与 one-hot 期望分布之间的差异。当网络对真实类别给出的概率为 1 时损失为 0,概率越接近 0 损失越大(可趋向无穷):

class CrossEntropyLoss: def forward(self,p,y): self.p = p self.y = y p_of_y = p[np.arange(len(y)), y] log_prob = np.log(p_of_y) return -log_prob.mean() # average over all input samples def backward(self,loss): dlog_softmax = np.zeros_like(self.p) dlog_softmax[np.arange(len(self.y)), self.y] -= 1.0/len(self.y) return dlog_softmax / self.p

forward中p[np.arange(len(y)), y]按每个样本的真实类别取出对应概率,取对数后求负平均。重要约定:损失函数必须对整个数据集或 minibatch 返回一个标量,因此用.mean()聚合所有样本的损失。backward实现了 log-softmax 与交叉熵合并后的解析梯度,从而可以跨过 softmax 层直接把误差回传到线性层。

Net:层容器与训练循环

Net类将各层堆叠为列表,forward按序执行、backward逆序回传、update只更新带update方法的层(如Linear,而Softmax、Tanh这类无参数层自动跳过):

class Net: def __init__(self): self.layers = [] def add(self,l): self.layers.append(l) def forward(self,x): for l in self.layers: x = l.forward(x) return x def backward(self,z): for l in self.layers[::-1]: z = l.backward(z) return z def update(self,lr): for l in self.layers: if 'update' in l.__dir__(): l.update(lr)

一次完整 epoch 的训练循环(SGD,minibatch size=4,学习率 lr)如下:

def train_epoch(net, train_x, train_labels, loss=CrossEntropyLoss(), batch_size=4, lr=0.1): for i in range(0,len(train_x),batch_size): xb = train_x[i:i+batch_size] yb = train_labels[i:i+batch_size] p = net.forward(xb) # forward pass l = loss.forward(p,yb) dp = loss.backward(l) # backward pass dx = net.backward(dp) net.update(lr)

每个训练回合由两部分组成:前向传播(对给定 minibatch 计算损失值)与反向传播(把误差沿计算图分配回模型参数)。整个过程可用计算图直观表示:

Tanh:层间非线性激活

课程强调了一个关键点:在线性层之间必须插入非线性激活函数(如tanh)。因为线性函数的复合仍是线性函数,若不加非线性,多个线性层叠加的表示能力与单层完全相同。Tanh层实现如下,其反向传播利用了tanh' = 1 - tanh²:

class Tanh: def forward(self,x): y = np.tanh(x) self.y = y return y def backward(self,dy): return (1.0-self.y**2)*dy

加入非线性后,多层感知机的数学形式为:

  • z₁ = W₁×x + b₁
  • z₂ = W₂×α(z₁) + b₂
  • f = σ(z₂)

其中α是非线性激活函数、σ是 softmax、参数集θ = ⟨W₁, b₁, W₂, b₂⟩。利用链式法则,各层权重的梯度可写成:

  • ∂L/∂W₂ = (∂L/∂σ)(∂σ/∂z₂)(∂z₂/∂W₂)
  • ∂L/∂W₁ = (∂L/∂σ)(∂σ/∂z₂)(∂z₂/∂α)(∂α/∂z₁)(∂z₁/∂W₁)

注意所有表达式最左侧的因子完全相同——这正是误差可以从损失出发、沿计算图逐层向前回传(backprop)的数学基础,也是我们能在Net中逆序遍历层列表完成反向传播的原因。

完成实验:1、2、3 层感知机的 MNIST 训练

实验笔记 MyFW_MNIST.ipynb 给出的操作步骤为:

  1. 将课程框架代码粘贴进笔记(或独立 Python 模块);
  2. 定义并训练单层感知机,训练过程中持续观察训练与验证准确率;
  3. 判断是否发生过拟合,必要时调整层参数以提升准确率;
  4. 对2 层、3 层感知机重复以上步骤,并在层间尝试不同的激活函数;
  5. 回答下方研究性问题。

以两层网络为例,用Net组合框架组件即可完成定义:

net = Net() net.add(Linear(784, 64)) # MNIST 输入 784 维 net.add(Tanh()) # 层间非线性激活 net.add(Linear(64, 10)) # 输出 10 类 net.add(Softmax()) loss = CrossEntropyLoss()

对于单层版本,只需去掉中间Tanh与第二层Linear,让Linear(784,10)直接接Softmax();三层版本则再叠加一组Linear + Tanh。之后反复调用train_epoch训练若干 epoch,并用类似get_loss_acc的辅助函数统计:

def get_loss_acc(x,y,loss=CrossEntropyLoss()): p = net.forward(x) l = loss.forward(p,y) pred = np.argmax(p,axis=1) acc = (pred==y).mean() return l,acc

np.argmax(p,axis=1)取每行概率最大的下标作为预测类别,与真实标签比较得到准确率。可分别在训练集与测试集上调用,同时监控loss与acc。

实验讨论:四个研究性问题的分析框架

完成训练后,需要针对实验原文提出的问题给出自己的观察结论。这里提供分析思路与判断依据,实际结论请以你自己训练得到的数据为准。

激活函数是否影响网络性能?

会。这正是课程引入Tanh的原因。若无激活函数,多层线性层的复合依然是线性变换,网络退化为一层的能力(见 OwnFramework.ipynb 第 45 个 cell 的说明)。在 MNIST 上对比同一深度、仅更换层间激活(如tanh与relu风格的自定义非线性)的网络,可以观察到收敛速度与最终准确率的差异。建议固定层数与隐藏单元数,仅改变激活函数做对照实验。

这个任务是否需要 2 层或 3 层网络?

MNIST 数据集类别较多且存在大量书写变形,单层线性分类器(线性可分限制)通常只能达到约 90% 左右的准确率;加入一层隐藏单元后,网络具备拟合非线性决策边界的能力,准确率会明显提升。理论依据是:足够多神经元的双层模型可以分类任意凸集,三层模型几乎可以分类任意集合(见 OwnFramework.ipynb 第 47 个 cell)。你可以记录三种深度下的最终准确率与训练耗时,判断继续加深是否带来收益。

训练中遇到了哪些困难?层数增加后尤为明显

随着层数增加,梯度需要经过更多层的链式乘法回传,容易出现梯度消失(浅层权重更新缓慢甚至停滞)问题。观察各层权重的更新幅度可以发现深层与浅层的不均衡。此外,更深的网络更容易过拟合:训练损失很低,但验证损失远高于训练损失、甚至随训练继续而上升——因为模型开始"记忆"训练样本而丢失了对数据整体规律的把握。课程中的train_and_plot工具(记录每个 epoch 的训练/验证准确率并绘制曲线)正是用于捕捉这类现象的。过拟合的直观对比如下:

图中x表示训练数据、o表示验证数据:左侧单层线性模型较好拟合了数据本质;右侧复杂模型对训练数据拟合完美,但对其他数据的验证误差非常高。

权重在训练中如何变化?

可以按实验原文建议,绘制每个 epoch 权重的最大绝对值(max abs weight)随 epoch 的变化曲线。一般观察:训练初期权重幅度随梯度更新快速增长,随后逐渐趋于平稳;如果出现幅度持续异常增大(尤其配合验证损失反弹),通常意味着过拟合或学习率过大。实现上只需在每个 epoch 结束后遍历net.layers中带W属性的层,记录np.max(np.abs(l.W))即可。该曲线能帮你直观理解学习率与权重正则化(可类比 L2 正则思想)的关系。

课程要点回顾与后续延伸

本实验直接复用了课程结论,可作为实验结果的解释依据(见 OwnFramework.ipynb 的 Takeaways):

  • 容量与过拟合:参数少的简单模型不易过拟合;复杂的多层模型容易过拟合,需持续监控验证误差是否在进一步训练中回升;
  • 数据需求:越复杂的模型需要越多训练数据;
  • 缓解过拟合的两条路径:简化模型,或增加训练数据量;
  • 偏差-方差权衡:需要在模型能力与数据量、过拟合与欠拟合之间取得折中;
  • 没有万能配方:层数与参数规模没有标准答案,最可靠的方式是像本实验一样动手对比。

完成本实验后,建议回到课程主笔记 OwnFramework.ipynb 复习训练循环与反向传播的逐步推导,再结合本实验的 MNIST 结果,即可把"自研框架 + 真实分类任务"这一完整链路彻底吃透。

  • 教程
  • 人工智能
  • 机器学习
  • 深度学习

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

12 Weeks, 24 Lessons, AI for All!

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

相关推荐

上一篇:FFmpeg-Kit终极指南:跨平台多媒体处理的革命性解决方案
下一篇:Smithbox游戏修改工具完整教程:从入门到精通

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

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

OpenCore Legacy Patcher 指南:5 步给老 Mac 装上新版 macOS

OpenCore Legacy Patcher 指南:5 步给老 Mac 装上新版 macOS 【免费下载链接】OpenCore-Legacy-Patcher Experience macOS just like before 项目地址: https://gitcode.com/GitHub_Trending/op/OpenCore-Legacy-Patcher 2013 款的 iMac 收不到系统更新&…

作者头像 李华
网站建设 2026/10/5 1:53:52

CarSim与Simulink联合仿真:Driver Model与5个Driver Sensors闭环控制实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 1:53:43

LAMMPS 命令分类详解:从 9 大功能域到完整输入脚本实战

科研科学计算高性能计算 【免费下载链接】lammps Public development project of the LAMMPS MD software package 项目地址: https://gitcode.com/gh_mirrors/la/lammps 点击查看 免费下载 导读 本文以 LAMMPS 官方命令分类索引页 Commands by category 为主体骨…

作者头像 李华
网站建设 2026/10/5 1:51:59

Aleph Alpha 开源 78B 参数 MoE 模型 Kolibri

德国 AI 实验室 Aleph Alpha 在 10 月 3 日发布了开源权重模型 Kolibri,总参数量 78B,采用 Apache 2.0 许可证。这是一个混合专家架构(MoE)模型,每次推理仅激活 3.46B 参数,在英文数学和代码测试中达到 90 …

作者头像 李华