ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

CNN图像识别实战:用Python和PyTorch从零搭建卷积神经网络

CNN图像识别实战:用Python和PyTorch从零搭建卷积神经网络 1. 从零开始为什么我建议你直接用CNN做图像识别先亮个底。这篇文章讲的是用Python跑一个完整的CNN图像识别项目从环境搭建到模型训练再到最后的准确率调优全程有代码、有参数、有踩坑记录。如果你已经看过不少三分钟上手深度学习的标题党文章被各种数学公式劝退过那这篇应该能给你一个不同的观感CNN没那么玄乎图像识别也没那么遥远动手跑通一次比看十遍理论都有用。先说说什么场景需要这个东西。识别图片里的猫和狗判断一张工业照片里有没有瑕疵自动把快递单号从照片里抠出来这些任务背后都可以用同一套技术栈解决就是Python加上CNN。我自己最早接触这个方向是因为要给一批零件照片做缺陷分类几百张图靠人工看眼睛都快瞎了后来用CNN做了个简单的二分类虽然初期准确率才85%但已经比肉眼稳定太多。从那以后我就发现图像识别不是算法工程师的专利只要你愿意花几个晚上跑通一个完整流程后面再遇到类似需求就有了底气。这篇文章适合谁适合那种懂点Python基础会装库、会写for循环就行但完全没碰过深度学习的朋友。我会尽量少讲数学多讲直觉因为我自己也是这么走过来的。CNN里那些卷积、池化的概念本质上都是一些很朴素的图像处理思路只不过被包装成了听起来很高档的术语。你不需要先修一门神经网络课再动手边跑边学反而是效率最高的方式。2. 环境准备先把Python、CUDA和PyTorch这三件套伺候明白2.1 为什么我推荐PyTorch而不是TensorFlow新手问得最多的一个问题就是到底学PyTorch还是TensorFlow我的建议很直接先学PyTorch。原因有三第一PyTorch的API设计更符合Python直觉调试的时候报错信息能看懂第二现在的学术论文和开源项目大部分基于PyTorch你照着GitHub上的代码学习几乎零门槛第三写代码时不用像TensorFlow早期版本那样先建静态图再执行写起来就是普通的Python逻辑。TensorFlow当然也很强尤其在工业部署场景有它的优势但上手阶段PyTorch的容错率高得多。版本选择上别一味追新。以2024年中期的稳定生态为准Python推荐3.10或3.11PyTorch推荐2.x版本目前2.3左右的版本很稳CUDA建议11.8或12.x。有个特别实用的建议先确认自己的显卡驱动支持哪个CUDA版本再用对应的版本安装PyTorch。你可以打开命令行输入nvidia-smi看右上角的CUDA Version那个数字是驱动支持的最高版本往下兼容就行。如果没有NVIDIA显卡也别慌PyTorch有纯CPU版本跑小数据集完全够用只是训练慢那么几分钟而已。2.2 安装过程与常见翻车点搭建环境的命令其实就三行核心conda create -n cnn_env python3.10 conda activate cnn_env pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118第一行创建虚拟环境第二行激活第三行装PyTorch全家桶。这里有个细节torchvision一定要装因为后面加载数据集、做图像预处理都用得上它。装完之后可以验证一下python -c import torch; print(torch.__version__, torch.cuda.is_available())返回值如果是2.x.x True说明GPU版本可用如果显示False也不是天塌了后面用CPU跑就行。我见过最多的问题就是装完PyTorch提示找不到CUDA动态库。这种情况99%是版本不匹配导致的检查你装的cu118是否对应当前驱动支持的版本或者干脆重装。还需要装两个常用库pip install numpy matplotlib pillow opencv-pythonnumpy是数据处理的地基matplotlib用来画训练曲线和查看图片结果OpenCV主要用于图片读取和简单预处理。别小看这些配套库后面几乎每一步都离不开它们。2.3 Jupyter Notebook还是IDE这个没什么好纠结的。做实验阶段用Jupyter Notebook写正式脚本用VS Code。Jupyter的交互特性非常适合探索性地调整模型哪个参数变了立刻重跑一个cell就能看到效果。VS Code则适合整理成一整套项目代码比如训练脚本、预测脚本拆分开来。两者共用同一个虚拟环境在VS Code里选择正确的解释器就行。3. 数据集模型就像小孩喂什么学什么3.1 这次实战选什么数据集为了让你能完整体验流程我建议用最经典的CIFAR-10数据集。它是10类小图片的集合飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船、卡车每张图32x32像素总共6万张。为什么选它因为它够小、够标准、足够有代表性。一张图才32x32CPU也能快速训练但分类难度又不会低到让你觉得无聊。这个数据集在PyTorch里自带下载接口不用手动去网站找。如果你做的是特定行业的识别任务比如零件缺陷、农作物病害往往需要自己整理数据集。核心原则就一条每个类别至少要有几百张图且图片背景要多样。我自己做零件缺陷分析的时候刚开始只收集了每类50张结果训练出来的模型换个拍摄角度就识别错了后来补到每类300张泛化能力立刻好了很多。3.2 数据加载与数据增强PyTorch加载数据用的是DataLoader配合torchvision.transforms做预处理。数据增强是这里面的重要一环但很多人忽略。数据增强是什么简单说原始数据就那些但我们可以在训练时对图片做随机翻转、裁剪、调亮度、加噪声这样模型每次看到的新图都略有不同相当于免费扩充了训练集。这个操作能显著提升模型的泛化能力让模型不至于死记硬背。别小看这一步我见过太多人训练集准确率99%测试集只有80%问题多半出在缺乏数据增强。这里给出一个标准的预处理组合from torchvision import transforms transform_train transforms.Compose([ transforms.RandomCrop(32, padding4), # 随机裁剪模拟偏移 transforms.RandomHorizontalFlip(), # 随机水平翻转 transforms.ToTensor(), # 转张量并归一化到0-1 transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)) # 标准化 ]) transform_test transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)) ])那三个均值方差是CIFAR-10官方统计好的RGB值直接用就行。加载训练数据from torchvision import datasets from torch.utils.data import DataLoader trainset datasets.CIFAR10(root./data, trainTrue, downloadTrue, transformtransform_train) trainloader DataLoader(trainset, batch_size64, shuffleTrue, num_workers2) testset datasets.CIFAR10(root./data, trainFalse, downloadFalse, transformtransform_test) testloader DataLoader(testset, batch_size64, shuffleFalse, num_workers2)注意batch_size64表示每次给模型64张图shuffleTrue表示每个epoch打乱顺序这两个参数的合理选择能避免模型陷入不利的训练模式。4. 理解CNN的眼睛卷积是怎么看图的4.1 从全连接到卷积一个生活化的类比在CNN出现之前神经网络处理图像的做法是把每个像素都当作输入特征一张32x32的彩色图就是3072个数值全部连接起来。听着没什么但换成一寸照片大小的图像素变成数百万级参数数量会被撑爆。CNN的思路完全不同。它不关心每一个像素位置而是关心局部模式。把图像想象成一幅拼图边缘、纹理、形状这些特征都是局部区域呈现的不用看全局。这就好比你看一张人脸不是先记住每一个像素点而是先看眼睛鼻子嘴巴这些局部器官再组合起来判断是谁。卷积层做的就是这件事。一个卷积核也叫滤波器其实就是一个小矩阵比如3x3它在整张图上从左到右、从上到下滑动每到一个位置就和那块的像素做加权求和得到一个响应值。不同的卷积核提取不同的特征有些对横线敏感有些对竖线敏感有些检测颜色变化。第一层卷积提取的是低级特征线条、边缘经过多层堆叠后网络就能组合出高级特征眼睛、轮胎。4.2 池化层压缩信息的智慧池化层简单粗暴作用就是给特征图降维度。最常用的是最大池化Max Pooling在一个2x2区域内取最大值作为代表。为什么取最大值因为它能保留这个区域内最明显的特征响应同时把无关细节扔掉。就好比你在开会时让每个部门只派一个代表来发言代表带来的一定是那个部门最强的声音。还有Dropout层它的作用是随机关闭一部分神经元让网络不能过度依赖某个通道从而抑制过拟合。训练时每个batch随机关掉一部分连接测试时全部打开。这个机制我后面还会重点讲。4.3 卷积核数量和感受野的直觉搭建CNN时总听到通道数64、通道数128之类的说法。通道数就是卷积核的数量。初始层特征简单用32个卷积核就够了越往深层特征越抽象需要更多的卷积核来捕捉不同模式。感受野则是指某一层神经元看到的原始图像范围。卷积层堆叠得越多感受野越大网络就能看到更全局的信息。一个常用的经验规律图像尺寸每经过一次池化缩小一半通道数扩大一倍。从32x32x3逐渐变成16x16x32再到8x8x64这样既控制了计算量又保持了信息丰富度。5. 搭出第一个CNN代码逐行走一遍5.1 网络结构设计清晰多了直接给一个适合CIFAR-10的经典小模型import torch.nn as nn import torch.nn.functional as F class SimpleCNN(nn.Module): def __init__(self): super(SimpleCNN, self).__init__() self.conv1 nn.Conv2d(3, 32, 3, padding1) self.bn1 nn.BatchNorm2d(32) self.conv2 nn.Conv2d(32, 64, 3, padding1) self.bn2 nn.BatchNorm2d(64) self.pool nn.MaxPool2d(2, 2) self.conv3 nn.Conv2d(64, 128, 3, padding1) self.bn3 nn.BatchNorm2d(128) self.fc1 nn.Linear(128 * 4 * 4, 256) self.fc2 nn.Linear(256, 10) self.dropout nn.Dropout(0.25) def forward(self, x): x F.relu(self.bn1(self.conv1(x))) x self.pool(x) x F.relu(self.bn2(self.conv2(x))) x self.pool(x) x F.relu(self.bn3(self.conv3(x))) x self.pool(x) x x.view(x.size(0), -1) x F.relu(self.fc1(x)) x self.dropout(x) x self.fc2(x) return x这段代码包含三层卷积、三层批归一化BatchNorm、三层池化以及两层全连接。结构图就是卷积 - 激活 - 池化循环三次然后展平接全连接层分类。5.2 关键参数选择背后的为什么注意到几个细节没有为什么每个卷积层后面都加一个BatchNorm2d因为批归一化能把每层的输出分布拉回到均值为0、方差为1的状态使得网络在训练过程中更加稳定梯度传播不容易消失收敛速度明显加快。很多新手搭的网络训练起来损失曲线像心电图一样跳动加一层BN就能缓解大半。F.relu是激活函数它的作用是引入非线性。如果不加激活函数无论堆多少层网络本质上还是一个线性变换学不到复杂模式。ReLU函数就是负数置零、正数保留简单粗暴但效果好。x.view(x.size(0), -1)这行很多人看不懂其实就是在打平。卷积和池化之后输出是一个多维的特征图比如64张图每张是4x4x128view操作把它变成一个一维向量好喂给全连接层。-1表示自动推导这个维度配合x.size(0)就是batch的大小。5.3 参数量估算别做无头苍蝇动手训练前花十秒钟估算一下参数量是明智的。以这个网络为例第一层卷积Conv2d(3, 32, 3)的参数是3*32*3*3 32 896权重加偏置。第三层卷积Conv2d(64, 128, 3)的参数约64*128*3*3 128 73856。全连接层Linear(128*4*4, 256)的参数是2048*256 256 524544。总参数在70万左右对于现代设备来说很轻量。参数太多意味着需要更多数据才能充分训练所以算参数量的意义在于判断自己的数据量够不够。CIFAR-10有五万张训练图训这个七十几万的模型绰绰有余。如果你只有几千张自拍图那就要减小模型规模或者大量增加数据增强。6. 训练环节损失函数、优化器与超参数调参实录6.1 损失函数与优化器的选择分类任务最常用的损失函数是交叉熵损失CrossEntropyLoss。你可以把它理解为模型给正确类别分配的概率越低损失值就越大。这个损失符合我们的直观预测对了惩罚小预测错了惩罚大。优化器选Adam还是SGD新手直接选Adam它会自动为每个参数调整学习率对初始化不敏感默认参数就能跑出不错的效果。SGD要手动调学习率配合动量momentum才能发挥威力不是新手该碰的东西。等后面你理解了学习率和梯度下降的关系再转SGD追求更好的泛化性也不迟。import torch.optim as optim criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001)学习率lr0.001是Adam的经典起点。如果你发现损失持续震荡不降可以尝试降到0.0003。6.2 完整训练循环训练一个epoch的代码长这样def train_one_epoch(model, trainloader, optimizer, criterion, epoch): model.train() running_loss 0.0 correct 0 total 0 for i, (images, labels) in enumerate(trainloader): outputs model(images) # 前向传播 loss criterion(outputs, labels) # 计算损失 optimizer.zero_grad() # 梯度清零 loss.backward() # 反向传播 optimizer.step() # 更新参数 running_loss loss.item() _, predicted torch.max(outputs.data, 1) total labels.size(0) correct (predicted labels).sum().item() if i % 100 99: print(fEpoch {epoch1} Batch {i1}: loss {running_loss/100:.3f}, acc {100*correct/total:.2f}%) running_loss 0.0一个关键细节optimizer.zero_grad()必须在loss.backward()之前执行。PyTorch在反向传播时是累加梯度不清零的话上一个batch的梯度会叠加到当前batch上导致参数更新方向混乱。我第一次跑的时候忘了这行结果损失死活降不下来查了半天才发现是这个问题。主循环num_epochs 20 for epoch in range(num_epochs): train_one_epoch(model, trainloader, optimizer, criterion, epoch)GPU上跑大概几分钟一个epochCPU大约十几分钟总共20个epoch也就一个多小时内完成。跑完记得在测试集上评估def evaluate(model, testloader): model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in testloader: outputs model(images) _, predicted torch.max(outputs.data, 1) total labels.size(0) correct (predicted labels).sum().item() return 100 * correct / total print(fTest Accuracy: {evaluate(model, testloader):.2f}%)注意评估时一定要开torch.no_grad()这样就不会为推理过程中的中间张量保存梯度省内存且提速同时要把model切换为eval()模式因为BN和Dropout在训练与推理时的行为不同。6.3 训练结果解读按上面的配置跑20个epoch之后CIFAR-10的测试集准确率通常能达到**75%到80%**之间训练集准确率则会到95%以上。如果你看到模型在训练集上准确率已经99%测试集只有70%那说明过拟合了后面我会专门讲怎么应对。一个靠谱的调参思路不是盲目试而是按顺序来先确认代码能跑通、损失能下降再逐步调学习率然后增加epoch同时观察验证集准确率最后根据过拟合程度增加正则化或数据增强。不要一上来就上大模型那样只是把你的训练时间拉长并不能提升最终效果。7. 进阶与调优把准确率再推高10个百分点7.1 数据增强加码前面用的数据增强只是基本款。想进一步提升可以加入RandomRotation、ColorJitter等transform_train_advanced transforms.Compose([ transforms.RandomCrop(32, padding4), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), # 随机旋转15度 transforms.ColorJitter(brightness0.2, contrast0.2), # 调亮度对比度 transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)) ])注意增强要合理。把猫的照片旋转180度猫还是猫这是合理的增强但如果是识别数字6和9旋转180度语义就变了这种增强会增加训练难度甚至误导模型。做数据增强前一定要问自己这个变换在不改变分类结果的前提下是否合理。7.2 学习率衰减学习率固定为0.001时训练后期往往会出现loss在某个平台期震荡的现象。这时候一个标准的做法是学习率衰减让模型在后期用小步子精细调整。常见策略是每10个epoch把学习率除以10scheduler optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.1)在每次epoch结束后调用scheduler.step()就行。7.3 迁移学习站在巨人的肩膀上数据量不够或想要更高的精度最快的方式是迁移学习。就是用别人在大数据集上预训练好的模型比如ResNet18在ImageNet上训练过把最后一层换成自己的分类层只训练最后一层或者只以很小的学习率微调整个网络。这一招在自定义数据集上效果立竿见影哪怕只有几千张图也能达到90%以上的准确率。在PyTorch里用预训练模型很简单import torchvision.models as models model models.resnet18(pretrainedTrue) num_ftrs model.fc.in_features model.fc nn.Linear(num_ftrs, 10) # 10是分类数替换最后一层注意因为CIFAR-10是32x32小图ResNet默认输入是224x224直接喂32x32会报尺寸错误。通用的解决办法是第一层之前加一个自适应池化把特征图缩到合适大小或者自己修改输入尺寸。这个细节不解决可能就卡半小时。我个人的经验是能用预训练模型就用预训练模型别自己从零硬训。你自己训练一个模型需要几天时间、大量数据和调参精力而ResNet18这类模型是人家用成千上万张图几个月调出来的白嫖它不香吗唯一的缺点是模型文件大一些推理速度略慢。7.4 Batch Size和SGD深水区的玩法当你把准确率推到80%以上瓶颈往往在学习率和优化器的配合上。此时可以试试SGD加动量optimizer optim.SGD(model.parameters(), lr0.01, momentum0.9, weight_decay5e-4)SGD加动量要求的学习率通常比Adam大0.01到0.1配合余弦退火效果很好。这也是很多论文里的标准配置。不过你要做好学习率衰减和调试的心理准备因为SGD对学习率敏感多了。8. 常见问题速查我踩过的坑你都别踩跑这类项目时每个环节都可能卡住。我把最常见的坑和对应的排查方式整理成表方便你对着查。现象可能原因解决方法训练loss不降学习率太大或太小换成0.001起步观察loss变化loss是nan学习率过大导致梯度爆炸降低学习率到0.0001模型在测试集上表现很差过拟合增加Dropout、数据增强、降低模型容量训练集准确率比测试集高很多过拟合的典型信号增加正则化或简化网络CPU训练极慢batch_size太大把batch_size从64降到16或8显存不够OOM模型太大或batch太大减小batch_size、减小图片尺寸、使用CPU验证集loss上升但训练集下降过拟合正是在发生早停或者加正则化图片显示成纯色/乱码忘记转Tensor或者归一化错误检查ToTensor和Normalize的顺序与参数运行报错找不到某模块版本冲突确认虚拟环境激活重新pip install颜色通道顺序错乱OpenCV读图是BGRPyTorch要求RGB用cv2.cvtColor转换或者直接用PIL读图有一个非常容易被忽略的问题在CPU上训练时第一次加载数据特别慢这是因为PyTorch在初始化数据集时要做预处理。对策是设置num_workers0减少多进程带来的开销或者耐心等待。反之在GPU上训练时num_workers2能很好地分担数据加载压力不要让GPU空等。另外关于环境变量的坑在Windows上训练深度学习经常遇到系统PATH里的Python版本混乱导致装错库的问题。强烈建议用conda建独立虚拟环境把环境激活后where python确认一下路径指向指向错了训练多久都是白费。9. 从Jupyter到项目工程化部署与扩展思路9.1 把模型保存下来训练完的模型要保存torch.save(model.state_dict(), ./cifar10_cnn.pth)加载时注意要新建一个结构相同的模型再用load_state_dict加载model SimpleCNN() model.load_state_dict(torch.load(./cifar10_cnn.pth)) model.eval()关于保存整模型还是只保存权重我建议只保存state_dict因为它体积小、跨版本兼容性好。如果你用torch.save(model, ...)整个保存换环境后经常遇到类定义路径不匹配的问题。9.2 单张图片预测部署时经常需要喂一张图进去出结果。核心步骤是读取图片、做和训练一样的预处理、放进模型推理from PIL import Image import torchvision.transforms.functional as TF def predict_single_image(model, image_path, device): img Image.open(image_path).convert(RGB) img TF.resize(img, (32, 32)) img TF.to_tensor(img) img TF.normalize(img, mean(0.4914, 0.4822, 0.4465), std(0.2023, 0.1994, 0.2010)) img img.unsqueeze(0) # 增加batch维度 img img.to(device) model.eval() with torch.no_grad(): outputs model(img) _, predicted torch.max(outputs, 1) return predicted.item()img.unsqueeze(0)这步很容易漏。模型训练时输入的维度是NCHW也就是batch、通道、高、宽单张图只有CHW三个维度必须补一个batch维度才能喂进去。如果想让输出更直观把数字标签映射回类别名classes [airplane, automobile, bird, cat, deer, dog, frog, horse, ship, truck] print(f预测结果: {classes[predicted.item()]})9.3 拓展到自己的数据集想在真实项目中用这套技术核心就是准备一个像CIFAR-10一样的数据集目录每个类别一个文件夹文件夹里放对应图片。然后可以用torchvision.datasets.ImageFolder加载几乎零改动就能复用上面的全部代码。有一个重要提醒真实场景的画面通常比32x32大得多。如果你的图片是几百像素的直接用原始尺寸训会非常慢。正确的做法是设置resize为64x64或128x128然后再调网络结构和池化层数量。经验规律是模型输出的特征图尺寸最终要能变成不小于4x4太小了全连接层就没有足够的信息了。9.4 边缘设备可行吗热词里出现了esp32s3cam图像识别确实有人想把这套技术往嵌入式设备上搬。我的建议是现实一点像上面这个SimpleCNN大约70万参数编译量化后在ESP32-S3这种平台上勉强能跑但推理速度很慢几秒到十几秒。更可行的路线是先训练一个大模型然后蒸馏成一个小模型比如MobileNetV3再转成TFLite格式部署。这个方向很有趣但不是一个晚上能搞定的建议先把PC端流程跑通再想边缘迁移。10. 关于GPU、CPU与算力的几句实在话很多初学者焦虑我的电脑没有GPU能学深度学习吗答案非常明确必须是、肯定能。CIFAR-10数据集只有6万张32x32小图一台2018年的老笔记本CPU也能在半小时内完成20轮训练。即使到了真实项目只要数据量控制在几千张、图片resize到适当大小CPU依然能完成训练只是效率低一些而已。如果要买卡没有必要一上来就上4090。入门阶段用RTX 3060或者二手2080完全足够。我的实操经验是当前这个模型在3060上训练一个epoch大约15秒20个epoch五分钟就能跑完而CPU可能要30分钟以上差距确实明显但不影响初学者在CPU上跑通流程。还有一点值得说一下跑深度学习确实对电脑的散热是一种考验。CPU长时间100%运转风扇会狂转这是正常的。如果笔记本过热自动关机建议调小batch_size降低负载而不是去清灰把机器拆了别问我为什么会说这个。11. 最后分享一点个人经验从第一次跑通LeNet在MNIST上识别手写数字到现在能快速搭建一个CNN原型我最大的感受是深度学习入门最难的不是理论而是把无数个小细节串成一条龙。环境要对、数据格式要对、维度要匹配、训练和评估模式要切换、梯度要记得清零。这些细节每个单独拎出来都简单但它们组合在一起对于新手就像掉进了一个满是隐藏机关的房间。我的建议是一步一步来不要贪多。第一次不追求准确率多高只要能跑通流程把代码从上到下运行一遍就已经是一次巨大的胜利。然后在此基础上尝试换一个优化器、调一个参数、加一层卷积看看结果有什么变化。这种做实验的积累比任何教程都更能帮你在脑子里建立起对模型的直觉。如果你准备动手今天就把环境搭好明天用周六晚上的时间跑完第一轮训练。看到准确率徐徐上升的那一刻你会觉得前面踩的所有坑都值了。这个项目既是入门的一次自我测试也是后面所有图像识别场景的一块坚实跳板。祝第一次运行就一次通过。
返回列表