尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

卷积神经网络入门:从核心组件到PyTorch图像分类实战

卷积神经网络入门:从核心组件到PyTorch图像分类实战 处理图像分类问题时很多新手先想到的是“把图片拉成一维向量丢进全连接网络”。这个思路逻辑上没错但真正动手后会遇到两个问题一是图像尺寸稍大全连接层的参数数量就会爆炸二是把像素顺序打散后图像里的空间结构、边缘纹理、局部关系全部丢失。卷积神经网络CNN就是为了解决这两个问题出现的。它用卷积层做局部特征提取用池化层做下采样用激活函数引入非线性最后用全连接层输出分类结果。这篇文章会围绕 CNN 的核心组件逐一拆解并结合 LeNet-5、AlexNet 两个经典网络说明架构演化逻辑最后用 PyTorch 跑通一个最小可用的图像分类示例。学完后你能理解卷积核尺寸、步长、填充、输出尺寸计算这些概念也能在遇到训练不收敛、shape 不匹配、过拟合这些问题时知道从哪一层、哪一个参数开始排查。1. 先理解卷积神经网络为什么能解决图像分类问题1.1 图像分类的本质是从像素到特征一张 32x32 的彩色图片在计算机里就是 32x32x3 的数组三个通道对应 RGB。要判断“这张图是不是猫”不能直接拿 3072 个数字和“猫”这个标签做比较。机器需要先找到有区分度的模式边缘、角点、颜色块、纹理、器官形状。传统机器学习方法会手工设计特征比如颜色直方图、HOG、SIFT然后交给 SVM 或随机森林分类。问题在于不同数据集上有效的特征差异很大手工设计特征既耗时又难以覆盖复杂场景。CNN 的思路是把“特征提取”也变成可学习的一部分让卷积核在训练过程中自动调整从数据里学出不同层次的模式。这也是为什么现代计算机视觉任务几乎都默认使用卷积网络或它的变体而不是回到手工特征时代。1.2 全连接网络处理图像的三个痛点第一个痛点是参数爆炸。假设输入是一张 224x224 的彩色图片拉平后是 150528 维。如果第一个全连接层有 1024 个神经元这一层的权重参数就有 1.5 亿个。在普通显卡上很难训练而且很容易过拟合。第二个痛点是丢失空间结构。图像拉平后相邻像素之间的空间关系被打乱模型很难再学习“邻近像素构成边缘”这种规律。第三个痛点是位置敏感。全连接网络对位置变化非常敏感同一只猫从图片左边移到右边它会当成两个完全不同的输入。CNN 通过局部连接、权值共享和池化从机制上缓解了这三个问题让模型既能控制参数量又能保留图像本身的空间信息。1.3 CNN 的三大设计思想第一个思想是局部感知。每一个卷积核只观察输入的一个小窗口比如 3x3 或 5x5 的区域而不是看整张图。这符合视觉系统先看局部再看整体的特点。第二个思想是权值共享。同一个卷积核在整张图上滑动时权重是相同的这样既大幅减少参数又让同一个模式在不同位置都能被检测到。第三个思想是层次化抽象。底层卷积核通常学到边缘和颜色块中层学到纹理和局部形状高层学到眼睛、轮子这类语义组件最后由全连接层组合成类别判断。理解了这三个思想后面看每一层的设计就不会觉得是孤立的。2. CNN 核心组件拆解卷积层、激活函数、池化层2.1 卷积层的计算过程卷积层的核心是卷积核filter。以一个二维输入为例输入是 5x5 的矩阵卷积核是 3x3 的矩阵步长为 1不填充。卷积核从左上角开始与输入对应位置做逐元素乘法然后求和得到一个输出值。之后卷积核向右滑动一个步长继续计算直到遍历整张输入。这样得到的输出通常叫特征图feature map。一个卷积层通常有多个卷积核每个卷积核负责提取一种模式所以输出会有多个通道。实际项目中卷积层的输入输出都是四维张量形状为(batch_size, channels, height, width)PyTorch 中的nn.Conv2d也遵循这个约定。2.2 输出尺寸公式和三个关键参数卷积输出尺寸的计算公式是输出尺寸 floor((输入尺寸 2 * padding - kernel_size) / stride) 1kernel_size 是卷积核大小stride 是滑动步长padding 是在输入四周补零的宽度。这三个参数相互影响。kernel_size 决定感受野大小3x3 是当前最常用的选择因为两个 3x3 卷积堆叠可以获得与一个 5x5 卷积相同的感受野但参数量更少。stride 大于 1 时可以快速缩小特征图尺寸但过大会丢失细节。padding 用来控制输出尺寸常用配置是 padding1、kernel_size3、stride1这样输出尺寸与输入一致方便搭建深层网络。参数含义常见值调大影响调小影响kernel_size卷积核大小3x3、5x5感受野变大、参数增多感受野变小、参数减少stride步长1、2输出变小计算量下降输出变大保留更多细节padding补零宽度0、1、2输出变大保留边界信息输出变小边界信息丢失out_channels卷积核数量32、64、128特征更丰富、计算量上升特征不足可能欠拟合2.3 激活函数的作用让网络具备非线性表达能力如果不加激活函数卷积层和全连接层无论堆叠多少层本质上都是一个线性变换无法拟合图像分类这种高度非线性的边界。激活函数在每一层输出之后对结果做非线性变换。ReLU 是目前最常用的选择公式是 f(x)max(0,x)。它计算简单、收敛快正区间梯度恒为 1能缓解梯度消失问题。它的缺陷是负区间输出恒为 0如果学习率过大部分神经元可能永远不会被激活这就是“神经元死亡”。Leaky ReLU 和 ELU 是常见替代方案但实际项目中 ReLU 仍作为默认起点。Sigmoid 和 tanh 在早期网络中使用较多但在深层网络中容易出现梯度消失现在一般只用于最后一层做概率输出或特殊场景。2.4 池化层下采样与增强平移不变性池化层的作用是降维。常用最大池化Max Pooling和平均池化Average Pooling。最大池化在每个窗口内取最大值平均池化取平均值。池化操作没有可学习参数它把特征图分成若干窗口每个窗口输出一个值从而缩小特征图尺寸减少后续计算量同时保留相对显著的特征。池化还带来一定程度的平移不变性特征稍微移动几个像素池化后的结果变化不大。窗口大小常见为 2x2步长 2输出尺寸刚好缩小一半。要注意池化不改变通道数只改变每个通道的空间尺寸。在分类网络中池化层还承担了扩大感受野的作用让后续卷积能看到更大范围的上下文信息。3. 全连接层与经典网络结构LeNet-5 和 AlexNet3.1 全连接层把特征映射成类别概率经过若干卷积和池化之后特征图已经包含从输入中学到的高层特征。全连接层的作用是把这些特征“摊平”再做几次线性变换加激活最终映射到类别数量。在 PyTorch 中摊平使用view或flatten然后送入nn.Linear。最后一个全连接层的输出维度等于类别数通常接一个 softmax 或交叉熵损失。需要注意的是全连接层是网络参数的主要来源也是最容易过拟合的地方因此现代网络中常用全局平均池化替代部分全连接层比如 ResNet 的最后一个阶段。理解这一点有助于理解为什么 LeNet-5 和 AlexNet 的参数量差异如此之大。3.2 LeNet-5 逐层结构分析LeNet-5 是 1998 年提出的经典网络最初用于手写数字识别输入是 32x32 的灰度图。它的结构如下第一个卷积层 C1用 6 个 5x5 卷积核输出 6 张特征图尺寸 28x28然后是池化层 S22x2 平均池化尺寸变为 14x14接着卷积层 C3用 16 个 5x5 卷积核输出 16 张特征图尺寸 10x10再接池化层 S4尺寸变为 5x5然后 C5 是 120 个 5x5 卷积核输出 120 张特征图F6 是 84 维全连接层最后输出层是 10 个类别。LeNet-5 的参数量大约 6 万个在当年是里程碑式的设计。它的核心贡献是确立了“卷积层 池化层 全连接层”的基本组合模式后续 CNN 的结构大多是在这个框架上演进。3.3 AlexNet 的关键改进AlexNet 在 2012 年把 ImageNet 分类错误率大幅拉低是深度学习视觉方向进入主流视野的标志。它输入 224x224 彩色图包含 5 个卷积层和 3 个全连接层约 6000 万参数。相比 LeNet-5AlexNet 的关键改进包括使用 ReLU 替代 tanh训练速度更快使用 Dropout 随机丢弃部分神经元缓解全连接层过拟合使用 max pooling 加 overlapping pooling使用数据增强比如随机裁剪和水平翻转在 GPU 上并行训练。从工程角度看AlexNet 证明了更大数据、更深网络、更强算力三者结合能带来质的提升。今天很少直接复现完整 AlexNet 训练但它的思想仍是理解现代 CNN 的基础。LeNet-5 和 AlexNet 对比表对比项LeNet-5AlexNet提出时间19982012输入尺寸32x32 灰度224x224 彩色卷积层数量3含 C55全连接层数量23激活函数tanh/sigmoidReLU是否使用 Dropout否是参数量约 6 万约 6000 万主要用途手写数字识别大型图像分类3.4 为什么后来网络越来越深LeNet-5 用两个卷积块就完成了数字识别AlexNet 用五个卷积层处理了复杂自然图像。后来的 VGG 进一步把卷积核统一为 3x3把网络加深到 16 至 19 层ResNet 通过残差连接解决了深层网络退化问题把网络加深到百层以上。深度增加的本质是让网络有机会学习更抽象、更全局的特征。但深度不是唯一因素还要配合归一化、残差连接、更好的初始化策略。学习初期不建议一上来就啃 ResNet 的完整推导先把 LeNet-5 和 AlexNet 的结构读懂再看 VGG、ResNet 会顺畅得多。4. 用 PyTorch 实现一个最小可运行的 CNN 分类器4.1 环境准备与依赖确认这里用 PyTorch 实现目标数据集是 CIFAR-10包含 10 个类别、50000 张训练图和 10000 张测试图每张 32x32 彩色图非常适合学习 CNN 基础。环境建议如下依赖建议版本说明Python3.9 或更高PyTorch 官方支持版本torch2.x根据本机 CUDA 版本安装torchvision与 torch 版本匹配提供数据集和预训练模型硬件CPU 可学习GPU 训练更快小数据集上 CPU 也能跑通安装命令仅作示例实际版本号以本地环境为准pip install torch torchvision如果只有 CPU或者公司内网限制安装后可以先打开 Python 验证是否可用import torch print(torch.__version__) print(torch.cuda.is_available())cuda.is_available()为 False 不代表不能用只是当前环境没有可用的 CUDA 设备代码在 CPU 上也能运行。这个步骤的目的是提前暴露 import 错误和版本冲突避免后续训练到一半才发现环境有问题。4.2 定义网络结构针对 32x32 的 CIFAR-10可以定义一个三层结构的简单 CNN。第一层卷积从 3 通道提取到 32 通道第二层从 32 通道提取到 64 通道然后经过两次最大池化特征图从 32x32 缩到 8x8最后展平送入全连接层。import torch import torch.nn as nn import torch.nn.functional as F class SimpleCNN(nn.Module): def __init__(self, num_classes10): super(SimpleCNN, self).__init__() self.conv1 nn.Conv2d(3, 32, kernel_size3, stride1, padding1) self.conv2 nn.Conv2d(32, 64, kernel_size3, stride1, padding1) self.pool nn.MaxPool2d(kernel_size2, stride2) self.fc1 nn.Linear(64 * 8 * 8, 256) self.fc2 nn.Linear(256, num_classes) def forward(self, x): x self.pool(F.relu(self.conv1(x))) x self.pool(F.relu(self.conv2(x))) x x.view(x.size(0), -1) x F.relu(self.fc1(x)) x self.fc2(x) return x代码中有几个关键点。第一卷积层使用 padding1所以输入 32x32 经过卷积后仍是 32x32池化后变成 16x16第二次卷积后仍是 16x16再次池化变成 8x8。第二全连接层的输入维度必须是 64x8x84096如果改了卷积核数量或池化方式这里要同步修改。第三最后一个全连接层输出 10对应 10 个类别训练时配合交叉熵损失不需要手动加 softmax因为 PyTorch 的CrossEntropyLoss内部已经包含 softmax 计算。4.3 训练循环数据加载、损失函数、优化器训练一个简单分类器需要三部分数据集加载、损失函数定义、优化器更新。下面代码使用 torchvision 加载 CIFAR-10并对图片做归一化。import torch.optim as optim from torch.utils.data import DataLoader from torchvision import datasets, transforms transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) ]) train_dataset datasets.CIFAR10(root./data, trainTrue, downloadTrue, transformtransform) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) model SimpleCNN(num_classes10) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001) for epoch in range(5): running_loss 0.0 for images, labels in train_loader: optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() avg_loss running_loss / len(train_loader) print(fEpoch {epoch 1}, Loss: {avg_loss:.4f})这里的交叉熵损失内部包含 softmax 计算所以模型最后一层直接输出原始 logits 即可。Adam 优化器在入门项目中是很好的默认选择初始学习率 0.001 通常会有一个稳定的收敛过程。train_loader的shuffleTrue会在每个 epoch 打乱数据避免模型学习到数据顺序。每次迭代需要调用optimizer.zero_grad()清空梯度否则梯度会累加导致参数更新方向错误。4.4 期望输出第一次运行时会自动下载 CIFAR-10 数据集网络较慢时需要耐心等待。训练 5 个 epoch 后loss 通常从 1.8 左右逐渐下降到 1.2 左右具体数值取决于数据划分和随机种子。如果你的 CPU 训练每个 epoch 可能需要几分钟这是正常现象。如果看到 loss 在 2.3 附近不动首先检查数据加载是否正常以及模型最后一层输出维度是否为 10。这里要用一个假输入快速验证模型结构是否能走通前向传播dummy torch.randn(1, 3, 32, 32) print(model(dummy).shape)如果输出torch.Size([1, 10])说明网络结构没有问题问题可能在数据或训练配置。5. 验证模型效果和保存模型5.1 用测试集评估准确率训练损失下降只代表模型拟合训练数据真正有效果要看测试集准确率。测试时不需要反向传播所以要包裹在torch.no_grad()中。test_dataset datasets.CIFAR10(root./data, trainFalse, transformtransform) test_loader DataLoader(test_dataset, batch_size64, shuffleFalse) correct 0 total 0 with torch.no_grad(): for images, labels in test_loader: outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() print(fAccuracy: {100 * correct / total:.2f}%)torch.max(outputs, 1)返回每一行最大值和对应索引索引就是预测类别。准确率计算是预测正确的样本数除以总样本数。简单模型在 CIFAR-10 上大概能到 60% 左右这个结果虽然远低于现代预训练模型但它能验证“网络结构、数据加载、训练流程”是通的。如果准确率接近 10%也就是随机猜测的水平说明训练流程有问题不要继续调超参数先检查数据和标签是否对应。5.2 保存和加载模型训练完成后要保存模型PyTorch 推荐保存state_dict它只包含模型参数不包含网络结构加载时需要重新创建模型实例。torch.save(model.state_dict(), simple_cnn_cifar10.pth) model SimpleCNN(num_classes10) model.load_state_dict(torch.load(simple_cnn_cifar10.pth)) model.eval()调用model.eval()很关键。模型在训练模式和评估模式下某些层的表现不同比如 Dropout、BatchNorm。如果没有调用eval()推理结果可能不稳定。保存和加载时要保证网络结构一致否则load_state_dict会报 key 不匹配。这里有一个实践建议保存模型时在旁边加一个json或txt文件记录输入尺寸、类别映射、归一化参数避免模型换了机器后无法正确推理。5.3 单张图片推理示例实际使用中输入往往不是标准化的数据集图片而是一张任意尺寸的图片。推理前需要把图片缩放到模型要求的尺寸并做相同的归一化。from PIL import Image image Image.open(cat_test.png).convert(RGB) single_transform transforms.Compose([ transforms.Resize((32, 32)), transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) ]) input_tensor single_transform(image).unsqueeze(0) class_names [airplane, automobile, bird, cat, deer, dog, frog, horse, ship, truck] with torch.no_grad(): outputs model(input_tensor) _, predicted torch.max(outputs, 1) print(fPredicted: {class_names[predicted.item()]})注意这里归一化参数必须和训练时一致否则输入分布偏移准确率会明显下降。unsqueeze(0)是为输入增加一个 batch 维度因为模型默认第一个维度是 batch。如果图片是灰度图需要先转成三通道 RGB否则输入通道与模型第一层不匹配。6. 参数调优与常见问题排查6.1 学习率、batch size、epochs 的配合学习率决定参数更新的步长。学习率太大会导致 loss 震荡甚至发散太小会导致收敛过慢。batch size 影响梯度估计的稳定性越大梯度越稳定但内存占用越高。epochs 是完整遍历训练集的次数过多会导致过拟合过少会导致欠拟合。入门阶段建议固定 batch size 为 64学习率从 0.001 开始先训练小 epoch 观察 loss 趋势再调整。超参数偏小的影响偏大的影响建议学习率收敛慢震荡、不收敛0.001 起步参考 loss 曲线调整batch size梯度不稳定内存不足、泛化下降32 到 128 之间选epochs欠拟合过拟合观察验证集准确率何时停止提升6.2 过拟合的表现和预防如果训练集准确率很高测试集准确率明显低说明过拟合。CIFAR-10 规模不大简单 CNN 在训练后期经常出现这种趋势。常见的处理方法包括增加数据增强比如随机裁剪、水平翻转在全连接层加入 Dropout减小模型容量使用权重衰减。学习阶段建议先不做复杂优化而是记录训练准确率和测试准确率的差值理解过拟合是怎么发生的。下面是一个加入 Dropout 的改造示例class SimpleCNNWithDropout(nn.Module): def __init__(self, num_classes10): super(SimpleCNNWithDropout, self).__init__() self.conv1 nn.Conv2d(3, 32, kernel_size3, stride1, padding1) self.conv2 nn.Conv2d(32, 64, kernel_size3, stride1, padding1) self.pool nn.MaxPool2d(kernel_size2, stride2) self.fc1 nn.Linear(64 * 8 * 8, 256) self.dropout nn.Dropout(0.5) self.fc2 nn.Linear(256, num_classes) def forward(self, x): x self.pool(F.relu(self.conv1(x))) x self.pool(F.relu(self.conv2(x))) x x.view(x.size(0), -1) x F.relu(self.fc1(x)) x self.dropout(x) x self.fc2(x) return x6.3 常见问题排查表排错时按顺序检查输入、路径、维度、配置、日志。下面表格整理了初学者最常见的几类问题。问题现象常见原因检查方式处理建议多维形状不匹配全连接输入维度计算错误打印每层输出 shape按输出尺寸公式重新计算loss 不下降学习率不合适或数据未归一化打印前几个 batch 的 loss调学习率检查 transform训练集准确率接近随机标签和图片不对应可视化一个 batch 图片检查 dataset 加载逻辑显存不足batch size 过大看报错 OOM调小 batch size预测结果全为同一个类模型未训练或加载错误查看 loss 和参数状态重新训练或正确加载权重6.4 输出尺寸计算错误是最常见的坑一个很容易踩的坑是把卷积输出尺寸当作全连接输入维度或者修改了池化层后忘记同步更新全连接层。修改网络结构时推荐先让模型处理一个假输入打印每一层输出的 shape确认无误后再进入训练循环。def print_shape_hook(module, input, output): print(module.__class__.__name__, output.shape) model.conv1.register_forward_hook(print_shape_hook) model.conv2.register_forward_hook(print_shape_hook) model.fc1.register_forward_hook(print_shape_hook)通过注册 forward hook可以看到每个关键层输出的 shape。这个技巧在调试深层网络时非常有用比在 forward 里逐个加 print 更干净。还有一个常见坑是 torchvision 数据集首次下载失败报连接超时。这时可以把下载好的数据集文件放到./data目录下或者换用公司镜像避免每次都在同一个位置卡住。7. 从入门实验到工程实践的建议7.1 学习顺序建议如果把 CNN 当作一个完整知识体系建议按这个顺序学习先写代码跑通一个简单 CNN理解数据流再手动计算一次卷积输出的尺寸然后逐个分析 LeNet-5、AlexNet、VGG 的结构接着学习 BatchNorm、Dropout、数据增强再尝试 ResNet 的残差连接。每次学新结构时都把网络输出尺寸和参数量记录下来做横向对比。不要一上来就加载预训练模型做迁移学习那样会跳过很多底层细节。手动算一次输出尺寸比看十遍公式更有效。7.2 学习环境与生产环境的差异跑通入门实验时几行代码就够了。但要真正把模型用到生产环境还需要考虑更多因素训练时要有 GPU 资源、数据版本管理、训练日志和指标监控推理时要把模型导出成 ONNX 或 TorchScript部署成 HTTP 服务还要关注模型输入的预处理一致性、异常输入处理、推理耗时和并发量。下面用表格整理主要差异。维度学习环境生产环境数据CIFAR-10 等公开数据集业务数据需清洗和脱敏训练单机单卡或 CPU分布式训练任务调度监控打印 loss指标采集、告警、日志部署本地 notebook 推理服务化或边缘部署稳定性重跑即可需要版本管理、回滚方案7.3 可复用的 CNN 学习检查清单清单可以当作每次训练前、训练后、部署前的核对项具体内容如下。数据确认图片尺寸、通道数、标签数量与模型匹配。预处理训练和推理使用相同的归一化参数。网络结构卷积后输出尺寸按公式核对全连接输入维度正确。训练配置损失函数适合分类任务优化器和学习率合理。验证用与训练集分布一致的测试集评估准确率。保存保存 state_dict 时记录输入尺寸、类别映射等元信息。部署推理前调用model.eval()开启no_grad避免梯度计算占用内存和产生随机性。7.4 下一步可以尝试的方向跑通这个最小示例后可以尝试三个方向。一是把数据集换成 Fashion-MNIST 或自定义图片文件夹体验从数据、训练到推理的完整流程。二是把网络换成带 BatchNorm 和 Dropout 的结构观察训练收敛速度变化。三是用 torchvision 自带的预训练 ResNet 做迁移学习感受特征提取和微调的区别。每个方向做完都应该用上面的检查清单确认一遍再进入下一个实验。相比追着新模型跑先把卷积、池化、全连接这几块的计算逻辑吃透后续看任何网络结构都会快很多。
返回列表