尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

卷积神经网络原理及PyTorch特征图可视化实战:MNIST手写数字识别

发布时间:2026/9/25 2:10:30

资讯中心
01
ARTICLE

卷积神经网络原理及PyTorch特征图可视化实战:MNIST手写数字识别

卷积神经网络原理及PyTorch特征图可视化实战:MNIST手写数字识别
各位读者朋友大家好。之前在做图像识别相关的学习项目时我在理解卷积神经网络时卡了很久。网上资料虽然很多但要么上来就是一堆数学公式要么只给代码不讲原理对新手非常不友好。直到后来自己手动把卷积、池化、特征图一个个画出来才真正理解了 CNN 的内部工作方式。今天这篇文章我会从一个“可视化”的角度把卷积神经网络拆开揉碎来讲并用MNIST 手写数字识别作为实战案例带你从零跑通一个完整的识别程序同时把每层提取到的特征图打印出来。文章里的代码都是可以直接复制运行的跟着操作一遍你会直观地看到一张 28x28 的灰度数字图片是如何一步步变成最终分类结果的。本文适合以下读者正在学习深度学习、想理解卷积神经网络原理的入门者。需要快速完成图像分类 Demo、但不想只做“调包侠”的开发者。已经跑通过 LeNet-5、ResNet 之类的模型但想补一补特征图可视化细节的同学。读完本文你将掌握CNN 的核心结构卷积层、汇聚层、全连接层、MNIST 数据集的加载方式、用 PyTorch 训练一个分类模型以及如何从模型中抽取每一层特征图进行可视化展示。说明本文涉及的代码基于 PyTorch 框架。具体版本需要根据你的本机环境调整下面会给出完整的版本适配说明。1. 卷积神经网络到底是什么1.1 从全连接网络的问题说起在卷积神经网络出现之前我们如果要用神经网络处理图片最直接的想法是把图片拉长成一个一维向量。假设一张图片是 28x28 像素那么输入特征就是 784 个数值这不是一个很大的数量但如果是一张 224x224 的彩色图片特征数就会变成 224x224x3 150528。如果你第一层网络选择 1024 个神经元那么这一层的参数量就是 150528x1024也就是大约 1.5 亿个参数。这样的网络训练非常慢。容易过拟合。输入尺寸一旦改变网络结构就需要重新设计。忽略了像素之间的空间位置关系。图像本身是一个高度结构化的数据相邻像素之间往往有很强的关系比如数字“1”的竖线是一整条连续的亮色像素区域。如果强行把图片变成一维向量这种空间结构就丢了。1.2 CNN 的思路局部感知与权值共享卷积神经网络的核心思想很简单既然相邻像素之间有关系那我们就不要全连接改为用一个小窗口去图像上滑动扫描每次只看一小块局部区域。这个小窗口就是“卷积核”也叫滤波器。卷积核在整张图片上移动同一个卷积核的权重是固定的这就是“权值共享”。这样做的好处参数量大幅下降。网络能够提取图片的局部特征比如边缘、角落、纹理。由于同一个卷积核扫描全图模型对目标在图片中出现的位置有一定容忍度。你可以把卷积层理解为“特征提取器”。第一层卷积核善于提取线条和边缘深层的卷积核会把边缘组合成眼睛、轮子、笔画这样的高级特征。1.3 我们这篇文章要演示什么通常学习 CNN 有一个经典项目手写数字识别。数据集是 MNIST里面包含 0 到 9 的灰度数字图片每张图片大小为 28x28。很多初学者跑完训练代码只知道模型准确率不错但完全不知道网络内部到底发生了什么。所以本文除了编写训练代码之外还会额外写一个“可视化”脚本。这个脚本会加载一张测试图片。把图片送入网络。提取第一个卷积层的输出特征图。用 Matplotlib 把所有特征图绘制成一张大图。通过这张大图你能清楚地看到原始图片经过卷积之后数字的轮廓被不同卷积核分别强化了。有的卷积核提取了横线有的提取了竖线有的提取了弧线。深度学习模型的“直觉”其实就是从这些简单的特征逐步组合而来的。2. 环境准备与版本说明2.1 运行环境本文的代码基于 Python 与 PyTorch推荐使用 64 位操作系统Windows、macOS、Linux 均可。示例环境如下依赖推荐版本说明Python3.8 或 3.9 及以上不建议使用 Python 2.xPyTorch1.10 或 2.x安装方法见下文torchvision与 PyTorch 同版本用于加载 MNIST 数据集Matplotlib3.5 及以上用于绘图可视化NumPy1.21 及以上PyTorch 会自动依赖这里不写死具体版本是因为 PyTorch 的安装命令会根据 CPU 还是 CUDA 改变。如果你的电脑没有独立显卡安装 CPU 版本即可本文的示例数据量完全跑得动。2.2 安装 Python 与 PyTorch如果你已经安装了 Anaconda可以创建一个虚拟环境conda create -n cnn_demo python3.9 conda activate cnn_demo然后安装 PyTorch。CPU 版本安装命令pip install torch torchvision如果你的机器有 NVIDIA 显卡并且已经安装好 CUDA 工具包可以前往 PyTorch 官网选择对应的 CUDA 版本安装例如pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118安装完成后建议先确认版本信息import torch import torchvision print(PyTorch 版本, torch.__version__) print(torchvision 版本, torchvision.__version__) print(CPU 是否可用, torch.cpu.is_available()) # 新版写法 print(GPU 是否可用, torch.cuda.is_available())如果你看到版本信息能正常输出说明环境准备完成。2.3 项目结构建议创建一个干净的文件夹结构如下cnn_mnist_demo/ ├── train.py # 训练脚本 ├── visualize.py # 特征图可视化脚本 ├── model.py # 网络结构定义也可以直接合并到 train.py └── models/ # 保存训练后的模型权重因为这是一个教学 Demo为了保证阅读简单我会把网络结构直接写进train.py中再用独立的visualize.py做可视化。这样你复制两个文件就能跑通全部流程。3. CNN 的三大核心结构拆解3.1 卷积层Convolution Layer卷积层的操作其实非常朴素拿一个相当于小矩阵的卷积核在输入图片上按步长滑动每次对应位置相乘再相加得到一个新的值。这个值组成的矩阵就叫“特征图”。用一个直观例子说明假设输入是一张 5x5 的灰度图卷积核是 3x3步长为 1。那么滑动区域总共会有 3x3 种位置输出特征图大小就是 3x3。当然实际图像通常有多个通道卷积核也有对应的通道数。用代码表示卷积层import torch.nn as nn conv_layer nn.Conv2d( in_channels1, # 输入通道数灰度图是 1 out_channels8, # 输出通道数也就是有多少个卷积核 kernel_size3, # 卷积核大小 3x3 stride1, # 每次滑动的步长 padding1 # 边缘填充 1 格保持尺寸不变 )这里out_channels8的含义是网络会学习 8 个不同的卷积核。每个卷积核负责提取一种特征。最终输出的特征图形状是(8, 28, 28)。卷积核的参数不是人为设计的而是通过训练自动学出来的。训练之前这些卷积核的值是随机的训练之后它们会自动变成能提取数字边缘、曲线、纹理的滤波器。3.2 汇聚层Pooling Layer汇聚层也叫池化层。它不引入新的参数作用是对特征图进行压缩。最常见的是最大池化Max Pooling。假设池化窗口是 2x2步长是 2那么每次取这个窗口内四个元素的最大值。这样做降低特征图尺寸减少后续计算量。保留相对突出的特征增强对轻微位移的鲁棒性。相当于做了下采样提取更高层语义中的关键信息。代码import torch.nn as nn pool_layer nn.MaxPool2d(kernel_size2, stride2)一个 28x28 的特征图经过这个池化层后会变成 14x14。初学者经常把“池化”和“卷积”混淆。简单记忆卷积提取特征池化浓缩特征。3.3 全连接层Fully Connected Layer经过多个卷积层和池化层之后特征图已经变成了一种“浓缩版”的图片表示。为了输出分类结果我们把这些特征图拉直成向量接上全连接层最后通过 Softmax 或 LogSoftmax 得到每个类别的概率。在 MNIST 数字识别中类别数是 100 到 9因此最后一层输出的神经元个数是 10。代码import torch.nn as nn fc_layer nn.Linear(in_features128, out_features10)全连接层这一阶段做的事情更像一种“决策”。前面卷积层负责说“我看到了哪些部件”全连接层负责说“这些部件组合起来最可能是数字几”。3.4 激活函数的作用如果不加激活函数多层卷积和全连接叠加在一起本质上还是线性变换模型表达能力不够。常用激活函数是 ReLUimport torch.nn as nn relu nn.ReLU()ReLU 的公式很简单把大于 0 的值保留把小于等于 0 的值变成 0。它的好处是计算快并且能缓解梯度消失问题。在本文的演示网络中每个卷积层和第一个全连接层后面都会接一个 ReLU。4. 完整实战基于 PyTorch 训练手写数字识别模型4.1 数据准备MNIST 数据集MNIST 是一个经典的手写数字数据集。它由 60000 张训练图片和 10000 张测试图片组成。每张图片是 28x28 的灰度图像素值范围是 0 到 255。为了方便网络训练我们通常会把像素值归一化到 0 到 1 之间。使用 torchvision 可以直接在线下载数据集。第一次运行需要联网下载后的数据会缓存到本地目录。如果下载缓慢可以手动下载数据集文件放到指定目录这里不做展开。数据加载代码import torch from torch.utils.data import DataLoader from torchvision import datasets, transforms transform transforms.Compose([ transforms.ToTensor(), # 转换为 Tensor并将像素值缩放到 [0,1] transforms.Normalize((0.1307,), (0.3081,)) # 按 MNIST 均值方差归一化 ]) train_dataset datasets.MNIST( root./data, trainTrue, downloadTrue, transformtransform ) test_dataset datasets.MNIST( root./data, trainFalse, downloadTrue, transformtransform ) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) test_loader DataLoader(test_dataset, batch_size64, shuffleFalse)关于NormalizeMNIST 的全局像素均值大约是 0.1307标准差大约是 0.3081。归一化后数据分布更接近标准正态分布能让训练更快更稳定。4.2 定义 CNN 网络为了既保证准确率又让特征图可视化有内容我设计了一个简化版的卷积网络import torch.nn as nn import torch.nn.functional as F class SimpleCNN(nn.Module): def __init__(self): super(SimpleCNN, self).__init__() # 第一层卷积输入1通道输出8通道卷积核3x3padding1保持尺寸不变 self.conv1 nn.Conv2d(1, 8, kernel_size3, padding1) self.pool1 nn.MaxPool2d(2, 2) # 第二层卷积输入8通道输出16通道卷积核3x3padding1保持尺寸不变 self.conv2 nn.Conv2d(8, 16, kernel_size3, padding1) self.pool2 nn.MaxPool2d(2, 2) # 全连接层16 x 7 x 7 784 self.fc1 nn.Linear(16 * 7 * 7, 128) self.fc2 nn.Linear(128, 10) def forward(self, x): x self.pool1(F.relu(self.conv1(x))) # 28x28 - 14x14 x self.pool2(F.relu(self.conv2(x))) # 14x14 - 7x7 x x.view(-1, 16 * 7 * 7) # 展平 x F.relu(self.fc1(x)) x self.fc2(x) return x模型结构解析conv1把 28x28 灰度图变成 8 张特征图尺寸仍然是 28x28。pool1把 28x28 缩小成 14x14。conv2把 8 张特征图变成 16 张尺寸仍然是 14x14。pool2把 14x14 缩小成 7x7。展平后得到 16x7x7 784 个数送入全连接层最终输出 10 个类别的分数。你可能注意到输出层没有加激活函数这是因为我们后续会配合交叉熵损失函数使用。PyTorch 的CrossEntropyLoss内部会自动套一层 Softmax。4.3 训练代码下面给出完整的train.py文件你可以直接复制保存运行。# 文件路径cnn_mnist_demo/train.py import torch import torch.nn as nn import torch.nn.functional as F from torch.utils.data import DataLoader from torchvision import datasets, transforms from tqdm import tqdm # 超参数设置 BATCH_SIZE 64 EPOCHS 10 LEARNING_RATE 0.001 DEVICE torch.device(cuda if torch.cuda.is_available() else cpu) print(使用设备, DEVICE) # 数据预处理 transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) # 加载数据集 train_dataset datasets.MNIST( root./data, trainTrue, downloadTrue, transformtransform ) test_dataset datasets.MNIST( root./data, trainFalse, downloadTrue, transformtransform ) train_loader DataLoader(train_dataset, batch_sizeBATCH_SIZE, shuffleTrue) test_loader DataLoader(test_dataset, batch_sizeBATCH_SIZE, shuffleFalse) # 定义网络 class SimpleCNN(nn.Module): def __init__(self): super(SimpleCNN, self).__init__() self.conv1 nn.Conv2d(1, 8, kernel_size3, padding1) self.pool1 nn.MaxPool2d(2, 2) self.conv2 nn.Conv2d(8, 16, kernel_size3, padding1) self.pool2 nn.MaxPool2d(2, 2) self.fc1 nn.Linear(16 * 7 * 7, 128) self.fc2 nn.Linear(128, 10) def forward(self, x): x self.pool1(F.relu(self.conv1(x))) x self.pool2(F.relu(self.conv2(x))) x x.view(-1, 16 * 7 * 7) x F.relu(self.fc1(x)) x self.fc2(x) return x # 初始化模型、损失函数、优化器 model SimpleCNN().to(DEVICE) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lrLEARNING_RATE) # 训练函数 def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss 0 correct 0 total 0 for images, labels in tqdm(loader): images images.to(device) labels labels.to(device) outputs model(images) loss criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() predicted outputs.argmax(dim1) correct (predicted labels).sum().item() total labels.size(0) return total_loss / len(loader), correct / total # 验证函数 def evaluate(model, loader, device): model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in loader: images images.to(device) labels labels.to(device) outputs model(images) predicted outputs.argmax(dim1) correct (predicted labels).sum().item() total labels.size(0) return correct / total # 开始训练 if __name__ __main__: for epoch in range(1, EPOCHS 1): train_loss, train_acc train_one_epoch( model, train_loader, criterion, optimizer, DEVICE ) val_acc evaluate(model, test_loader, DEVICE) print(fEpoch {epoch:02d} | 训练损失: {train_loss:.4f} | 训练准确率: {train_acc:.4f} | 测试准确率: {val_acc:.4f}) # 保存模型 torch.save(model.state_dict(), models/mnist_cnn.pth) print(模型已保存到 models/mnist_cnn.pth)关于优化器这里用的是 Adam。相比普通 SGDAdam 自带自适应学习率对新手来说更容易收敛。如果你想换成 SGD设置optimizer torch.optim.SGD(model.parameters(), lr0.01, momentum0.9)也是可以的。4.4 运行与验证在终端执行mkdir models python train.py训练过程中你应该会看到类似下面的输出使用设备 cpu Downloading http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz ... 100%|████████████| 9912422/9912422 [00:1000:00, 945000.00it/s] Epoch 01 | 训练损失: 0.2415 | 训练准确率: 0.9284 | 测试准确率: 0.9704 Epoch 02 | 训练损失: 0.0963 | 训练准确率: 0.9710 | 测试准确率: 0.9790 Epoch 03 | 训练损失: 0.0722 | 训练准确率: 0.9785 | 测试准确率: 0.9833 ... Epoch 10 | 训练损失: 0.0273 | 训练准确率: 0.9912 | 测试准确率: 0.9897如果一切顺利测试集准确率应该能达到 98% 以上。这个精度虽然比不上原版 LeNet-5但作为可视化演示已经足够。这里使用的tqdm是一个进度条库如果没有安装请先执行pip install tqdm。如果你不想要进度条也可以把tqdm去掉改成普通的for循环。5. 可视化演示从输入到预测的完整链路5.1 为什么要做特征图可视化很多同学完成了训练后模型在他眼里仍然是一个“黑盒”输入一张图片输出一个分类结果中间过程完全不透明。特征图可视化能帮助我们回答三个问题卷积层到底学到了什么哪些特征对分类结果贡献最大为什么模型会把某个数字识别错本文专门编写一个可视化脚本加载一张手写数字图片把第一个卷积层输出的 8 张特征图以及第二个卷积层输出的部分特征图用 Matplotlib 显示出来。5.2 从网络提取特征图PyTorch 中的一个常用技巧是在forward过程中把中间结果保存下来。为了不改动原来的模型结构可以在可视化脚本中重新定义一个带“钩子”的网络或者更简单一点直接在可视化脚本中手动执行每一层的计算。下面采用手动分步计算的方式因为更直观。import torch import torch.nn.functional as F import matplotlib.pyplot as plt from torchvision import datasets, transforms from train import SimpleCNN # 加载设备 DEVICE torch.device(cuda if torch.cuda.is_available() else cpu) # 数据预处理 transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) # 加载一张测试图片 test_dataset datasets.MNIST( root./data, trainFalse, downloadTrue, transformtransform ) image, label test_dataset[100] # 取第 100 张测试图可以自行修改 print(f原始标签{label}) # 增加 batch 维度shape 从 (1,28,28) - (1,1,28,28) image image.unsqueeze(0).to(DEVICE) # 加载训练好的模型 model SimpleCNN().to(DEVICE) model.load_state_dict(torch.load(models/mnist_cnn.pth)) model.eval()这里下标test_dataset[100]中的 100 可以随意换这样可以观察不同数字的特征图差异。5.3 手动前向传播得到中间特征图接下来我们手动调用卷积层、池化层和全连接层把中间结果保存下来# 第一层卷积 激活 池化 conv1_out F.max_pool2d(F.relu(model.conv1(image)), kernel_size2) # 第二层卷积 激活 池化 conv2_out F.max_pool2d(F.relu(model.conv2(conv1_out)), kernel_size2) # 展平后经过全连接层 flattened conv2_out.view(-1, 16 * 7 * 7) fc1_out F.relu(model.fc1(flattened)) fc2_out model.fc2(fc1_out) # 获取预测结果 predicted fc2_out.argmax(dim1).item() print(f预测标签{predicted}) print(f模型置信度分布{F.softmax(fc2_out, dim1).squeeze().detach().numpy()})5.4 用 Matplotlib 展示特征图先展示原始输入图片和第一层卷积输出的 8 张特征图def show_feature_maps(images, title, rows, cols, cmapgray, colormapNone): 将多张特征图排列显示在画布中 images: 形状为 (C, H, W) 的特征图组合 fig, axes plt.subplots(rows, cols, figsize(cols * 2, rows * 2)) for i, ax in enumerate(axes.flat): if i images.shape[0]: img images[i].detach().cpu().numpy() ax.imshow(img, cmapcmap) ax.set_xticks([]) ax.set_yticks([]) else: ax.axis(off) fig.suptitle(title) plt.tight_layout() plt.show() # 原始图片 plt.figure(figsize(2, 2)) plt.imshow(image.squeeze().detach().cpu().numpy(), cmapgray) plt.axis(off) plt.title(原始输入图片) plt.show() # 第一层卷积输出 show_feature_maps( imagesconv1_out.squeeze(0), # 去掉 batch 维度 titleConv1 层输出的 8 张特征图, rows2, cols4 ) # 第二层卷积输出 show_feature_maps( imagesconv2_out.squeeze(0), titleConv2 层输出的 16 张特征图, rows4, cols4 )运行之后你会看到类似下面的效果第一行图原始数字图片。中间 8 张小图经过 conv1 后得到的特征图每张图都强化了原图中的一种局部模式。某些图中数字的竖线变得很亮某些图中数字的横线变得很清晰。下方 16 张小图经过 conv2 和池化之后特征图尺寸变成了 7x7空间分辨率更低但每个通道代表的语义更抽象。如果你在imshow时觉得某些特征图反白看不清可以分别在负数和正数之间调整显示范围ax.imshow(img, cmapgray, vmin0, vmaximg.max())因为经过 ReLU 之后特征图大部分值是大于等于 0 的所以直接用 0 作为下界效果更好。5.5 完整可视化脚本为了方便直接使用我把visualize.py合并成一份完整代码。需要注意这里直接import train会重复定义模型所以我把模型类单独抽取在train.py里可视化脚本继续复用。# 文件路径cnn_mnist_demo/visualize.py import torch import torch.nn.functional as F import matplotlib.pyplot as plt from torchvision import datasets, transforms from train import SimpleCNN DEVICE torch.device(cuda if torch.cuda.is_available() else cpu) transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) test_dataset datasets.MNIST( root./data, trainFalse, downloadTrue, transformtransform ) image, label test_dataset[100] image image.unsqueeze(0).to(DEVICE) model SimpleCNN().to(DEVICE) model.load_state_dict(torch.load(models/mnist_cnn.pth)) model.eval() conv1_out F.max_pool2d(F.relu(model.conv1(image)), kernel_size2) conv2_out F.max_pool2d(F.relu(model.conv2(conv1_out)), kernel_size2) flattened conv2_out.view(-1, 16 * 7 * 7) fc1_out F.relu(model.fc1(flattened)) fc2_out model.fc2(fc1_out) predicted fc2_out.argmax(dim1).item() print(f真实标签{label}) print(f预测标签{predicted}) # 展示原始图片 plt.figure(figsize(2, 2)) plt.imshow(image.squeeze().detach().cpu().numpy(), cmapgray) plt.axis(off) plt.title(原始输入图片) plt.show() # 第一层特征图 conv1_images conv1_out.squeeze(0) fig, axes plt.subplots(2, 4, figsize(10, 5)) for i, ax in enumerate(axes.flat): img conv1_images[i].detach().cpu().numpy() ax.imshow(img, cmapgray, vmin0, vmaximg.max()) ax.set_title(fConv1 Channel {i}) ax.axis(off) fig.suptitle(第一层卷积特征图 (8 channels)) plt.tight_layout() plt.show() # 第二层特征图 conv2_images conv2_out.squeeze(0) fig, axes plt.subplots(4, 4, figsize(10, 10)) for i, ax in enumerate(axes.flat): img conv2_images[i].detach().cpu().numpy() ax.imshow(img, cmapgray, vmin0, vmaximg.max()) ax.set_title(fConv2 Channel {i}) ax.axis(off) fig.suptitle(第二层卷积特征图 (16 channels)) plt.tight_layout() plt.show()运行命令python visualize.py你会看到一个完整的前向传播过程可视化结果。这也是文章标题中“1 分钟可视化演示”的落地版。6. 常见问题与排查思路6.1 特征图全黑或几乎全黑这是一个非常常见的问题。如果你在可视化时发现某张特征图几乎全黑通常是两个原因这张特征图经过 ReLU 之后确实大部分值是 0代表对应卷积核没有在当前图片上提取到有效特征。没有设置合适的显示范围。原始特征图的最大值可能只有 0.5默认的显示范围是 0 到 1看起来对比度不够。排查方式打印特征图的最大值和最小值。在imshow中设置vmin和vmax。如果确实全黑换一张测试图片再观察。代码示例img conv1_images[i].detach().cpu().numpy() print(fChannel {i}: min{img.min():.4f}, max{img.max():.4f})6.2 MNIST 下载失败因为网络原因在线下载 MNIST 有时会失败。datasets.MNIST下载依赖yann.lecun.com而该域名在某些网络环境下访问不稳定。常见解决办法使用国内镜像源例如https://mirrors.tuna.tsinghua.edu.cn/下对应 MNIST 文件。手动下载四个 gz 文件后放到./data/MNIST/raw/目录中。多试几次有些运营商偶尔需要重试才能成功。手动放置文件时对应路径为data/ └── MNIST/ └── raw/ ├── train-images-idx3-ubyte.gz ├── train-labels-idx1-ubyte.gz ├── t10k-images-idx3-ubyte.gz └── t10k-labels-idx1-ubyte.gz6.3 训练准确率高但可视化不明显假如你的模型准确率很高但特征图看起来杂乱无章原因是特征图的可判别性未必体现在单张图上。某些通道可能混合了多种模式某些通道可能是冗余的。建议增加卷积核数量比如把out_channels从 8 改成 16会更容易观察到多样化的特征。观察最高激活值对应的测试图片这种方法叫“最大化激活可视化”需要额外编写代码。6.4 GPU 显存不足本文的网络规模非常小基本不会遇到显存不足。如果你在后续尝试更大模型时遇到 CUDA out of memory可以降低batch_size。降低图片输入尺寸。使用with torch.no_grad()包裹推理过程。6.5 预测结果和真实标签不一致这很正常MNIST 测试集上不存在 100% 准确率的模型。你可以把预测失败的图片收集起来构建一个“错误分析”集合。通过可视化对应图片的特征图往往能找到模型出错的原因比如数字写法过于潦草。图片存在笔画断裂。某些数字本身形状接近例如“4”和“9”。下面提供一个常见问题表格问题现象常见原因解决思路特征图全黑ReLU 后大量值为 0 或显示范围不对调整vmin、vmax换图片测试MNIST 下载失败外网下载不稳定使用镜像源或手动放置数据文件训练准确率低学习率设置过大或数据未归一化调整学习率或检测 transform可视化图片模糊特征图经过池化后分辨率过低展示 pool 之前的特征图预测错误数据本身模糊或模型容量不足增加训练轮数或加深网络运行报错 parameters 大小不匹配模型权重与当前网络结构不一致检查保存权重时网络是否与加载时一致7. 最佳实践与工程建议7.1 训练阶段注意超参数调节本文示例用了learning_rate 0.001这只是一个起点。在实际项目中建议先观察训练损失是否稳定下降如果不下降考虑降低学习率。如果过早过拟合考虑增加数据增强或减少网络层数。不要盲目加深网络。MNIST 这种简单数据集一个两层卷积网络已经足够。7.2 可视化代码的工程化上面的可视化脚本只是为了教学代码写得比较直接。在工程环境中如果你频繁需要可视化特征图建议使用 PyTorch 的 hook 机制。以下是使用 hook 保存中间特征图的标准姿势feature_maps {} def hook_fn(name): def forward_hook(module, input, output): feature_maps[name] output.detach() return forward_hook model.conv1.register_forward_hook(hook_fn(conv1)) model.conv2.register_forward_hook(hook_fn(conv2)) with torch.no_grad(): model(image) conv1_feat feature_maps[conv1] conv2_feat feature_maps[conv2]使用 hook 的好处是不改动网络结构不执行手动 forward也能拿到任意中间层输出。这在调试大型模型时尤其方便。7.3 模型保存与加载的注意事项本文保存权重的方法是torch.save(model.state_dict(), models/mnist_cnn.pth)加载时建议先构造模型再加载权重model SimpleCNN() model.load_state_dict(torch.load(models/mnist_cnn.pth, map_locationcpu))很多初学者直接保存整个模型torch.save(model, model.pth)虽然也能用但这种方式依赖代码中类名的位置工程上不如只保存state_dict稳妥。如果后续类名改变或者代码重构整个模型的保存版本会非常脆弱。7.4 数据增强是一个长效投资MNIST 数据量虽然不小但对于真实业务来说往往不够。常见的数据增强包括随机旋转 10 度。随机平移 2 个像素。随机缩放。加入少量噪声。在 torchvision 中可以这样组合train_transform transforms.Compose([ transforms.RandomRotation(degrees10), transforms.RandomAffine(degrees0, translate(0.1, 0.1)), transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ])数据增强能够提高模型的泛化能力尤其解决测试集与训练集分布不完全一致的问题。7.5 不要过早引入复杂模型有些人学 CNN 直奔 ResNet、Transformer反而把基础概念忽略了。我建议按这个顺序进阶先跑通本文的简单 CNN理解卷积、池化、特征图。再看 LeNet-5 的原始结构把每层的输入输出尺寸手算一遍。然后尝试在 CIFAR-10 上复现一个简单网络。最后再引入 BatchNorm、Dropout、残差连接、数据增强等技巧。8. 小结与学习路线本文从一个最简单的卷积神经网络出发完成了三件事用通俗语言解释了卷积层、汇聚层和全连接层的原理。用 PyTorch 完整实现了 MNIST 手写数字识别的训练过程准确率约 98%。通过特征图可视化直观展示了一张输入图片从 28x28 灰度图到输出分类结果的全过程。你在本地跑通了训练脚本和可视化脚本后对卷积神经网络的“体感”会和只看理论完全不一样。你会亲眼看到数字“5”的图片经过第一层卷积后竖线、弧线、交叉点分别被不同的卷积核点亮经过池化后特征图边长减半经过两层特征提取后空间信息逐步浓缩成 7x7 的“抽象地图”最后全连接层依据这些抽象特征完成分类决策。下一步建议你动手做这几件事修改test_dataset[100]的下标观察不同数字的特征图差异。增加一个卷积层观察更深层特征图发生什么变化。把out_channels从 8 改成 16重新训练对比准确率和可视化效果。尝试在visualize.py中找一个预测失败的样本分析它为什么被识别错。如果这篇教程对你有帮助可以收藏备用。后续如果接触 CIFAR-10、目标检测或语义分割你会发现本文中“特征图可视化”的思路依然起着核心作用——所有深度视觉模型本质上都在对图片做“层层抽象”。动手跑一遍比单纯看十遍文章更有用。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。