- 教程
- 人工智能
- 机器学习
- 深度学习
【免费下载链接】AI-For-Beginners
12 Weeks, 24 Lessons, AI for All!
本篇文章围绕 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.pforward中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 给出的操作步骤为:
- 将课程框架代码粘贴进笔记(或独立 Python 模块);
- 定义并训练单层感知机,训练过程中持续观察训练与验证准确率;
- 判断是否发生过拟合,必要时调整层参数以提升准确率;
- 对2 层、3 层感知机重复以上步骤,并在层间尝试不同的激活函数;
- 回答下方研究性问题。
以两层网络为例,用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,accnp.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!
相关推荐
用自研神经网络框架完成 MNIST 手写数字分类:AI-For-Beginners 多层感知机实验全解析
用自研神经网络框架完成 MNIST 手写数字分类:AI For Beginners 多层感知机实验全解析 本篇技术指南围绕 AI For Beginners 课
教程人工智能机器学习深度学习AI for Beginners 实验指南:用自研神经网络框架完成 MNIST 手写数字分类
AI for Beginners 实验指南:用自研神经网络框架完成 MNIST 手写数字分类 本篇技术指南围绕 AI for Beginners 课程(12 周
教程人工智能机器学习深度学习用自研神经网络框架完成 MNIST 手写数字分类:AI-For-Beginners 自建框架实验实战指南
用自研神经网络框架完成 MNIST 手写数字分类:AI For Beginners 自建框架实验实战指南 本篇技术指南围绕 AI For Beginners 课
教程人工智能机器学习深度学习
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考