基于CNN的机动车与非机动车识别实战:从数据集到部署全流程

发布时间:2026/10/28 11:54:37
基于CNN的机动车与非机动车识别实战:从数据集到部署全流程 做计算机视觉方向的朋友大概都有过这样的经历手里攒了一堆交通场景的图片想做一个能自动区分机动车和非机动车的模型结果卡在数据标注、模型选型、训练调参这几个环节上反复折腾。我最近刚把一套基于卷积神经网络的机动车与非机动车识别方案完整跑通从数据集整理到模型训练再到推理部署整个链路都走了一遍。这套东西说白了就是一个图像二分类任务——给一张道路图片模型判断里面是机动车还是非机动车。听起来简单但真正落地的时候数据清洗、类别平衡、模型结构设计、过拟合控制这些坑一个都躲不掉。这篇文章适合两类人看一类是刚入门深度学习、想找一个完整项目练手的同学另一类是有一定基础、想快速搭一套交通场景识别原型的开发者。我会把整个项目的设计思路、核心代码、实操步骤和踩过的坑都摊开讲你照着做基本能复现出来。1. 项目整体设计与技术选型拆解1.1 为什么选CNN而不是传统方法机动车和非机动车的识别本质上是一个图像分类问题。早些年做这类任务主流思路是手工提取特征——HOG、SIFT、LBP这些算子配合SVM分类器。我最早也试过这条路实测下来问题很明显手工特征对光照变化、拍摄角度、背景干扰极其敏感。比如一辆电动车停在树荫下HOG特征就乱得一塌糊涂分类准确率直接掉到七成出头。卷积神经网络的优势在于它能自动从数据里学特征。浅层卷积核学到的是边缘、纹理这些低级特征深层卷积核能组合出车轮、车把、车身轮廓这些语义特征。这种层次化的特征提取能力是手工特征没法比的。而且CNN对光照、遮挡、尺度变化有天然的鲁棒性只要训练数据覆盖够广泛化能力就上得去。具体到机动车和非机动车的区分有几个视觉线索特别关键机动车的车身通常更大、更方正有明显的进气格栅和车灯结构非机动车自行车、电动车车身窄、有辐条轮毂、骑行者姿态明显。CNN通过多层卷积能把这些局部模式捕捉得很到位。我选的是轻量级的自定义CNN结构没有直接上ResNet或者VGG原因后面会细说。1.2 数据集构成与类别定义这个项目的数据集分两部分机动车图片和非机动车图片。机动车涵盖轿车、SUV、面包车、卡车这几类非机动车主要是自行车和电动车。每类大概准备了1500到2000张总量在3500张左右。图片来源是公开的交通场景数据集加上一部分自己采集的道路监控截图分辨率统一缩放到224×224。这里有个关键决策类别怎么定义。我一开始想分得更细比如轿车、卡车、自行车、电动车各算一类做成四分类。但实际跑下来发现细分类别之间的样本量差异太大卡车样本只有几百张训练时严重欠拟合。后来改成二分类——机动车vs非机动车类别平衡好做模型收敛也快。对于大多数应用场景来说先粗粒度区分机动车和非机动车已经够用了后续如果有需要再在机动车内部做细分。数据集的目录结构我按标准的ImageFolder格式组织训练集和验证集按8:2划分dataset/ ├── train/ │ ├── motor/ # 机动车约1400张 │ └── non_motor/ # 非机动车约1400张 └── val/ ├── motor/ # 机动车约350张 └── non_motor/ # 非机动车约350张注意划分训练集和验证集的时候一定要保证同一场景的图片不会同时出现在两边。我一开始随机划分结果同一段路的连续帧被分到了训练集和验证集验证准确率虚高到98%实际测试惨不忍睹。后来改成按拍摄场景分组划分验证集准确率降到93%左右但这个数字才是真实的。1.3 模型结构选型的考量模型结构这块我纠结了挺久。ResNet50预训练模型效果确实好但参数量大、推理慢部署到边缘设备上吃不消。VGG16结构简单但参数更多全连接层就占了上亿参数。最后我选了一个自定义的轻量CNN结构如下3个卷积块每个块包含Conv2d BatchNorm ReLU MaxPool卷积核数量依次是32、64、128最后接全局平均池化 两层全连接256→2加Dropout防止过拟合这个结构参数量大概在120万左右模型文件不到5MB在CPU上单张推理时间约15毫秒完全能满足实时性要求。为什么不用预训练模型因为交通场景的图片和ImageNet的自然图像分布差异较大预训练权重的迁移收益有限而且自定义小模型训练速度快、可控性强调参的时候心里有数。2. 核心细节解析与实操要点2.1 数据预处理的关键操作数据预处理这块看着简单实际上坑最多。我列一下完整的处理流程和每步的理由统一分辨率所有图片缩放到224×224。选这个尺寸是因为它是大多数CNN的标准输入而且224×224在保留足够细节和计算效率之间平衡得比较好。缩放用双线性插值比最近邻插值平滑不会产生锯齿。归一化像素值从0-255归一化到0-1再用均值0.5、标准差0.5做标准化。这一步很关键不归一化的话梯度爆炸风险很高训练loss会直接飙到nan。数据增强训练时随机做水平翻转、±15度旋转、亮度对比度微调。增强幅度不能太大交通场景的图片翻转后语义不变但旋转超过30度就不符合实际拍摄角度了反而引入噪声。类别平衡检查每轮训练前统计各类样本数如果偏差超过1.2倍就做加权采样。这里重点说下数据增强的度怎么把握。我试过比较激进的增强策略——随机裁剪、颜色抖动、高斯噪声全加上结果模型在验证集上表现反而变差了。原因是过度增强让训练样本偏离了真实分布模型学到的是一些不真实的模式。后来把增强收敛到水平翻转加轻微旋转验证准确率提升了两个点。这个经验说明数据增强不是越多越好要贴合实际场景的成像特点。2.2 训练超参数的设定逻辑超参数这块我做了几组对比实验把关键参数的选择过程记录一下参数初值调整后调整理由学习率0.010.0010.01时loss震荡剧烈降到0.001后收敛平稳Batch Size163216时梯度噪声大32在显存允许范围内更稳定Epoch数503030轮后验证loss不再下降继续训练开始过拟合Dropout率0.30.50.3时训练集准确率99%但验证集只有88%加大到0.5缓解过拟合优化器SGDAdamSGD需要精细调学习率调度Adam自适应更省心学习率的选择我多说两句。初始用0.01配合SGD前几个epoch loss直接从2.3跳到0.8又弹回1.5震荡得厉害。换成Adam优化器、学习率0.001之后loss曲线平滑下降第8个epoch就降到了0.3以下。Adam的自适应学习率对新手确实友好不用花太多精力调调度策略。不过要注意Adam的默认学习率0.001对这个小模型来说刚好如果模型更大可能需要降到0.0001。2.3 损失函数与评估指标的选择二分类任务用交叉熵损失就够了PyTorch里CrossEntropyLoss直接搞定。但评估指标不能只看准确率。交通场景里如果机动车样本远多于非机动车模型全预测成机动车也能有不错的准确率这就失去了识别意义。所以我同时监控三个指标准确率Accuracy整体判断正确的比例召回率Recall非机动车被正确识别的比例这个指标在实际应用中最重要漏检非机动车可能导致后续决策失误F1分数准确率和召回率的调和平均综合衡量模型表现实测下来模型在验证集上的准确率93.2%非机动车召回率91.5%F1分数92.3%。这个水平对于原型验证来说够用了如果要做产品级应用还需要补充更多困难样本继续迭代。3. 完整实操流程与核心代码实现3.1 环境搭建与依赖安装环境这块没什么特别的Python 3.8以上PyTorch 1.10以上再加几个辅助库。我习惯用conda建独立环境避免和系统里的包打架conda create -n vehicle_cls python3.8 conda activate vehicle_cls pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install numpy pillow matplotlib scikit-learn这里装的是CPU版本的PyTorch因为我的部署目标是普通服务器没有GPU。如果你有显卡把index-url换成CUDA版本对应的地址就行。数据集加载用torchvision.datasets.ImageFolder配合DataLoader做批处理这部分代码很标准不展开细讲。3.2 模型定义代码详解模型结构我用PyTorch的nn.Module封装代码不长但每层都有讲究import torch.nn as nn class VehicleCNN(nn.Module): def __init__(self, num_classes2): super(VehicleCNN, self).__init__() self.features nn.Sequential( # 第一个卷积块输入3通道输出32通道 nn.Conv2d(3, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 224 - 112 # 第二个卷积块32 - 64 nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 112 - 56 # 第三个卷积块64 - 128 nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 56 - 28 ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d(1), # 全局平均池化28x28 - 1x1 nn.Flatten(), nn.Linear(128, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(256, num_classes) ) def forward(self, x): x self.features(x) x self.classifier(x) return x几个设计细节解释一下。卷积核统一用3×3、padding1这样每次卷积后特征图尺寸不变只有池化层做下采样尺寸变化清晰可控。每个卷积后接BatchNorm作用是稳定训练过程、加速收敛实测加了BN之后训练轮数能减少三分之一。全局平均池化替代了传统的全连接展平大幅减少参数量同时有一定正则化效果。Dropout放在第一个全连接层后面率设0.5这是防止过拟合最有效的一招。3.3 训练循环与验证流程训练循环的代码框架如下我把关键注释写在代码里import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms # 数据预处理定义 train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(mean[0.5, 0.5, 0.5], std[0.5, 0.5, 0.5]) ]) val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.5, 0.5, 0.5], std[0.5, 0.5, 0.5]) ]) # 数据集加载 train_dataset datasets.ImageFolder(dataset/train, transformtrain_transform) val_dataset datasets.ImageFolder(dataset/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4) # 模型、损失、优化器 device torch.device(cuda if torch.cuda.is_available() else cpu) model VehicleCNN(num_classes2).to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr0.001) # 训练循环 best_acc 0.0 for epoch in range(30): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() # 验证阶段 model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() val_acc correct / total print(fEpoch {epoch1}, Loss: {running_loss/len(train_loader):.4f}, Val Acc: {val_acc:.4f}) # 保存最佳模型 if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth)这段代码有几个实操要点。num_workers4在Linux下能加速数据加载Windows下建议设成0否则容易报多进程相关的错。验证阶段一定要加model.eval()和torch.no_grad()前者切换BN和Dropout到推理模式后者关闭梯度计算节省显存。模型保存只存state_dict而不是整个模型对象这样加载时更灵活也不容易因为类定义变动出问题。3.4 推理与单张图片预测训练完模型后写一个简单的推理脚本验证效果from PIL import Image def predict(image_path, model, device): model.eval() img Image.open(image_path).convert(RGB) transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.5, 0.5, 0.5], std[0.5, 0.5, 0.5]) ]) img_tensor transform(img).unsqueeze(0).to(device) with torch.no_grad(): output model(img_tensor) prob torch.softmax(output, dim1) _, pred torch.max(output, 1) classes [motor, non_motor] return classes[pred.item()], prob[0][pred.item()].item() # 使用示例 model VehicleCNN(num_classes2).to(device) model.load_state_dict(torch.load(best_model.pth, map_locationdevice)) label, confidence predict(test.jpg, model, device) print(f预测结果: {label}, 置信度: {confidence:.4f})推理时注意unsqueeze(0)给图片加一个batch维度因为模型期望输入是[batch, channel, height, width]四维张量。torch.softmax把输出转成概率分布方便看置信度。实测下来置信度高于0.9的样本基本不会错置信度在0.6到0.8之间的样本建议人工复核。4. 常见问题排查与避坑经验实录4.1 训练过程中的典型问题速查我把训练过程中遇到的问题整理成了一张速查表方便对照排查问题现象可能原因排查与解决Loss变成nan学习率过大或数据未归一化降低学习率到0.0001检查Normalize是否生效训练准确率高但验证准确率低过拟合加大Dropout、增加数据增强、减少模型参数量训练和验证准确率都低欠拟合或数据有问题检查标签是否正确、增加模型容量、延长训练轮数Loss震荡不收敛Batch Size太小或学习率不当增大Batch Size、换Adam优化器验证准确率虚高数据泄漏检查训练集和验证集是否有重复或同源图片推理结果全是同一类类别严重不平衡检查各类样本数做加权采样或补充少数类样本4.2 数据层面的隐蔽坑数据这块我踩过最深的坑是标签噪声。数据集里有一部分图片标注错了——比如把三轮摩托车标成了非机动车把电动三轮标成了机动车。这种错误肉眼不仔细看根本发现不了但模型训练时会学到错误的模式。我的处理办法是训练完第一轮后把验证集里预测置信度很高但预测错的样本挑出来人工复核发现大概有3%的标签有问题修正后重新训练验证准确率提升了1.5个点。另一个坑是背景干扰。很多交通场景图片里机动车和非机动车同时出现在画面中如果标注时只框了主体但裁剪时把旁边的车也带进来了模型就会困惑。我的做法是裁剪时尽量让目标物体占据画面中心区域边缘留白不超过20%。如果一张图里两类目标都有且都很显著直接剔除不用避免歧义样本污染训练集。4.3 模型部署时的性能优化模型训练好只是第一步实际部署时还要考虑推理效率。我做了几项优化效果比较明显模型量化用PyTorch的动态量化把权重从float32转成int8模型体积从4.8MB压到1.3MBCPU推理速度提升约40%准确率只掉了0.3个点性价比很高。输入尺寸权衡224×224是精度和速度的平衡点。我试过降到160×160推理速度快了一倍但准确率掉了4个点不太划算。如果对实时性要求极高可以接受这个精度损失。批处理推理如果一次要处理多张图片攒成batch一起推理比逐张推理快很多GPU上batch8比batch1吞吐量高3倍以上。提示模型量化后一定要在验证集上重新评估精度有些模型对量化比较敏感精度掉得厉害就不建议用。我这个模型量化后精度损失在可接受范围内所以最终部署用的是量化版本。4.4 提升模型上限的几个实用技巧如果基础版本跑通了想继续往上提精度我试过几个有效的手段。困难样本挖掘是最管用的——把验证集里预测错的样本单独拿出来分析错误模式然后针对性地补充类似场景的训练数据。比如我发现模型对夜间低光照条件下的非机动车识别率偏低就专门补了一批夜间图片针对性训练后这类样本的召回率从82%提到了89%。模型集成也有一定效果训练三个不同随机种子初始化的模型推理时取平均概率。实测准确率能提升1到2个点但推理成本翻三倍看场景取舍。迁移学习值得一试虽然前面说预训练权重迁移收益有限但如果你的数据集比较小每类不到500张用ImageNet预训练的ResNet18做特征提取器再微调效果会比从头训练好不少。最后分享一个调参的小心得不要一次性改多个超参数。我早期犯过这个错学习率、batch size、dropout一起调结果模型表现变好了但根本不知道是哪个参数起的作用。正确做法是固定其他参数一次只动一个记录每次的结果这样才能积累出对参数敏感度的直觉。这套项目跑下来我对这个小模型的脾气算是摸透了后面再遇到类似的图像二分类任务基本能一次调到位。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询