简介:面向计算机相关专业毕业生的深度学习车型识别系统项目,属于中等难度实战源码,适合正在筹备毕业设计或希望进行项目练习的学生使用。项目经导师指导并认可通过,评审分数为九十八分,源码均已完成本地编译与严格调试,确保能够直接运行。资源包以压缩包形式提供,内含十五个文件,压缩包大小约1.12MB;其中十个Python源码文件,实现了包括网络结构构建、数据集预处理、训练入口、模型复杂度计算、学习率调度、日志记录等核心模块,同时附带两份说明文档、一份运行说明、一份使用手册及环境配置文件,便于理解整个项目脉络。目前已有一百七十二人学习下载。通过这份资源,读者可以掌握从数据准备、模型设计训练到性能评估的完整工程方法,既可作为毕业设计参考,也可作为深度学习实战的练习项目。
1. 一台摄像头、70 类车型和一份能落地的 PyTorch 源码
做过车牌识别的人都知道,车牌只是入口,真正有业务价值的是「车是什么牌子、什么型号」。这个基于 PyTorch 的车型识别系统,把 MobileNetV2 和 ResNet 两条主干都放进一个工程里,配合 CLR 循环学习率、数据增强和 flops 计算脚本,训练完可以直接导出权重跑推理。对正在做 Python 毕业设计、或者想快速上手图像分类项目的开发者来说,它最大的价值不是“能跑通”,而是“每一步都能看懂为什么这么做”——从数据集目录组织到训练入口脚本 run.py 的启动方式,再到 start.py 里的推理流程,完整覆盖了一个小型图像识别项目从训练到部署的全部环节。项目代码在本地编译运行通过,调试痕迹和文档手册齐全,适合当作基线工程二次开发。
2. 模型选型逻辑:MobileNetV2、ResNet 与 flops_benchmark 的取舍决策
2.1 为什么同时保留 resnet.py 和 mobile_net.py 两个主干
车型识别本质上是一个细粒度图像分类任务,类别之间的差异常常集中在车灯、进气格栅、后视镜轮廓这些小区域。ResNet 通过残差结构缓解深层网络退化,在 ImageNet 上精度表现稳健;MobileNetV2 则用深度可分离卷积把计算量压到 ResNet 的十分之一左右。这两个模型在同一份数据上表现并不总是“越大越好”——数据集规模有限时,小模型的隐式正则化效果反而可能占优。
# utils 中加载模型的标准写法,两套主干共用同一套训练循环 import torchvision.models as models def build_model(arch="mobile_net", num_classes=70, pretrained=True): if arch == "mobile_net": model = models.mobilenet_v2(pretrained=pretrained) model.classifier[1] = torch.nn.Linear(model.last_channel, num_classes) elif arch == "resnet": model = models.resnet50(pretrained=pretrained) model.fc = torch.nn.Linear(model.fc.in_features, num_classes) return model这段代码的关键在两个替换点:MobileNetV2 的分类头是classifier[1],ResNet 是fc层,它们的输入维度分别来自last_channel和fc.in_features,这样替换后可以兼容任意预训练权重。项目里保留两个入口,是想让你在同样数据下做对照实验,而不是闷头选一个。
2.2 用 flops_benchmark.py 量化计算成本
光看精度选模型不够,还要看算力能不能扛得住。flops_benchmark.py 用 thop 库统计 FLOPs 和参数量,这个脚本在毕设答辩时也很有用,能直接回应“你的模型有多大计算量”这类问题:
python flops_benchmark.py --arch mobile_net --input-size 224# flops_benchmark.py 核心片段 from thop import profile, clever_format def run_benchmark(model, input_size=224): model.eval() dummy_input = torch.randn(1, 3, input_size, input_size) flops, params = profile(model, inputs=(dummy_input,)) flops, params = clever_format([flops, params], "%.3f") print(f"FLOPs: {flops} | Params: {params}")profile会逐层统计乘加运算量,clever_format把大数字转成 M/G 单位。我自己跑过的经验是:MobileNetV2 在 224x224 输入下约 3.0 GFLOPs,ResNet50 约 41 GFLOPs,差距在 13 倍以上。如果后续要部署到 Jetson Nano 或树莓派,这个数据直接决定选型方向。
2.3 模型选型的边界条件
| 场景 | 推荐主干 | 理由 |
|---|---|---|
| CPU 推理、嵌入式设备 | MobileNetV2 | 参数量小,前向延迟低 |
| GPU 训练、追求精度 | ResNet50 | 特征表征能力强,适合细粒度分类 |
| 数据量 < 1 万张 | MobileNetV2 | 小模型过拟合风险更低 |
| 迁移学习微调 | ResNet50 | 预训练特征更丰富 |
选型要结合设备算力和数据规模一起看,不能只看榜单精度。
3. 训练链路拆解:dataset.py、transfor.py 与 clr.py 的配合
3.1 数据集目录结构与 dataset.py 的加载方式
整个项目的训练数据组织遵循 ImageFolder 约定,这在 dataset.py 里体现得最直接。根目录下分 train 和 val 两个文件夹,每个类别的图片放在以类别名命名的子目录中:
data/ ├── train/ │ ├── Audi_A4/ │ ├── BMW_3_Series/ │ └── ... └── val/ ├── Audi_A4/ └── ...# dataset.py 核心片段 from torchvision import datasets, transforms def get_dataloader(data_root="data", batch_size=32, is_train=True): dataset = datasets.ImageFolder( root=os.path.join(data_root, "train" if is_train else "val"), transform=get_transform(is_train) ) loader = torch.utils.data.DataLoader( dataset, batch_size=batch_size, shuffle=is_train, num_workers=4, pin_memory=True ) return loader, dataset.classesImageFolder 会自动按子目录名生成类别索引,dataset.classes返回的就是按字典序排序的类别名列表,运行 start.py 推理时需要用这个列表做索引映射。num_workers=4在 Windows 上如果报错,改成 0 就行。
3.2 transfor.py 里的数据增强组合
车形识别的难点在于拍摄角度变化,需要在保持关键特征的前提下增强泛化性。transfor.py 里采用的策略组合很有代表性:
# transfor.py 核心增强逻辑 train_transform = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.6, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) val_transform = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])RandomResizedCrop的scale=(0.6, 1.0)很关键——车型图片通常包含大面积车身,裁剪比例太小会把车切碎,0.6 下限能模拟不同远近拍摄距离;ColorJitter用来对抗光照变化,适合停车场、路边等室外场景。注意验证集的变换不能加随机操作,必须用Resize(256) + CenterCrop(224)保证结果可复现。
3.3 clr.py 的循环学习率实现
循环学习率是这个项目比较有含金量的部分。传统 StepLR 需要手动调下降节点,而 CLR 让学习率在一个区间内周期性波动,既能跳出局部最优,又免去精细调参。clr.py 基于 PyTorch 的LambdaLR实现三角策略:
# clr.py 核心逻辑 def triangular_lr(step, base_lr, max_lr, step_size): cycle = math.floor(1 + step / (2 * step_size)) x = abs(step / step_size - 2 * cycle + 1) return base_lr + (max_lr - base_lr) * max(0, 1 - x) # 使用方式 scheduler = torch.optim.lr_scheduler.LambdaLR( optimizer, lr_lambda=lambda step: triangular_lr(step, 0.001, 0.01, 500) )step_size表示半个周期的步数,设为 500 意味着学习率在 1000 步内完成一次从低到高再降到底的波动。前几个周期学习率会冲得比较高,模型loss出现小幅震荡是正常的,不要因此调低max_lr。
3.4 训练入口 run.py 的参数组织
run.py 是整个训练的入口,核心超参数集中在文件头的配置区,修改一次即可全局生效:
# run.py 训练配置 BATCH_SIZE = 32 EPOCHS = 120 BASE_LR = 0.001 MAX_LR = 0.01 CLR_STEP_SIZE = 500 WEIGHT_DECAY = 1e-4 MOMENTUM = 0.9| 参数 | 建议值 | 说明 |
|---|---|---|
| BATCH_SIZE | 32 | 取决于显存,8GB 显卡建议不超过 64 |
| EPOCHS | 120 | 配合 CLR 足够收敛,也不至于过拟合 |
| MAX_LR / BASE_LR | 10 倍差距 | 差距过大容易震荡,过小失去 CLR 意义 |
| WEIGHT_DECAY | 1e-4 | 正则化弱一点,避免车型细节被压掉 |
训练时监控训练 loss 和验证集 top-1 准确率。如果发现训练 loss 下降但验证准确率停滞,优先检查transfor.py里有没有加RandomErasing——这种强增强在小数据集上反而可能拖慢收敛,我一般会先关掉它跑 20 个 epoch 做基线。
4. 推理环节:start.py 的工作流程与踩坑记录
4.1 加载权重并重组分类头
模型训练完成后,权重文件保存在 results 目录下。start.py 做推理时最常遇到的问题是在加载权重之前忘记重组模型分类头,导致参数形状不匹配:
# start.py 推理脚本核心逻辑 import torch import torchvision.transforms as transforms from PIL import Image def load_inference_model(arch="mobile_net", weights_path="results/best.pth", num_classes=70): model = build_model(arch=arch, num_classes=num_classes, pretrained=False) state_dict = torch.load(weights_path, map_location="cpu") # 兼容 DataParallel 保存的权重 if "module." in list(state_dict.keys())[0]: state_dict = {k.replace("module.", ""): v for k, v in state_dict.items()} model.load_state_dict(state_dict) model.eval() return model def predict(model, image_path, class_names, device="cpu"): img = Image.open(image_path).convert("RGB") tensor = val_transform(img).unsqueeze(0).to(device) with torch.no_grad(): output = model(tensor) prob = torch.softmax(output, dim=1) top1_idx = torch.argmax(prob, dim=1).item() return class_names[top1_idx], prob[0][top1_idx].item()map_location="cpu"让权重可以脱离 GPU 加载,方便在没有独立显卡的机器上演示。convert("RGB")必须写——有些摄像头拍摄的图片带 Alpha 通道,通道数不一致会直接报错。
4.2 CPU 推理的性能优化参数
如果只能 CPU 推理,有两个参数能明显提速。第一是开启 torch 的 CPU 线程优化:
torch.set_num_threads(4)第二是推理时关闭梯度计算并合并 batch:
# batch 推理减少 Python 循环开销 inputs = torch.stack([val_transform(Image.open(p).convert("RGB")) for p in img_paths]) with torch.no_grad(): logits = model(inputs) top1 = logits.argmax(dim=1)单张图片推理在 CPU 上大约 80-150ms,批量推理能压到每张 40ms 以内。在答辩现场用 CPU 演示时,建议先预热跑一次再计时,这样数据更稳定。
4.3 识别结果与类名映射不匹配
一个值得警惕的问题:ImageFolder 会按字母序排序类别,train 目录里第一个文件夹如果是Audi_A4,索引 0 就是 Audi_A4。如果后面你在 dataset.py 里新增了数据类别,索引顺序会整体改变。所以 start.py 里class_names的加载必须和训练时保持一致:
# 从训练目录重新构建类别列表,保证一致性 _, class_names = get_dataloader(data_root="data", is_train=False) model = load_inference_model(weights_path="results/best.pth", num_classes=len(class_names))这里如果发现识别结果张冠李戴,比如把宝马识别成奥迪,先不要怀疑模型没训练好,大概率是这里索引错位了。
5. 换数据集复用的三条实操经验
5.1 先冻结主干只训练分类头
拿到新的车型数据集时,不要直接全量微调。稳妥的做法是先冻结 Backbone,只训练分类层 15-20 个 epoch,等损失降下来再解冻主干用较小的学习率微调:
for param in model.features.parameters(): param.requires_grad = False # 只训练分类头 optimizer = torch.optim.Adam(model.classifier.parameters(), lr=0.001)冻结阶段学习率可以给高一点,因为分类头是随机初始化的;解冻后学习率要降到原来的十分之一,避免破坏预训练特征。
5.2 用早停配合 CLR 的终点
CLR 的学习率在周期结束时回到最小值,这天然适合做早停。在 run.py 里记录验证集准确率,如果连续 15 个 epoch 没有刷新最佳值,直接保存最后一次权重退出训练:
if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), "results/best.pth") patience = 0 else: patience += 1 if patience >= 15: break注意这个“15 个 epoch”是在 CLR 完整跑完至少两个周期后才开始计数的,否则可能在学习率上升阶段就误触发早停。
5.3 用 flops_benchmark 预判部署环境
把模型移植到嵌入式设备之前,先跑一遍 flops_benchmark.py 看看结果。如果 FLOPs 超过设备算力预算,优先检查输入分辨率能否从 224 降到 160,这一项能减少约一半计算量;其次是考虑把 MobileNetV2 换成 MobileNetV3-Small,结构上更激进,但精度往往只掉 1-2 个百分点。项目文档里已经有这两套主干的切换方法,按第 2 章的build_model改一个参数就行。
本文还有配套的精品资源,点击获取