简介:这份资源是面向高校计算机、人工智能相关专业学生的毕业设计完整项目包,主题为基于深度学习实现驾驶员分心驾驶行为识别。项目已通过导师指导与认可,并经过严格调试,可直接运行,适合作为毕设参考、课程设计或深度学习入门实战案例。压缩包共18个文件,约22.65MB,包含13个Python源码文件、3个CSV数据文件、1个Markdown说明文档及1个gitignore配置,源码覆盖AlexNet、VGG16、ResNet、Inceptionv4、LeNet5等多种经典网络结构,并附带迁移学习与多模型融合测试脚本,CSV文件则用于记录训练与测试结果。目前已有535人学习下载,读者可从中获得完整的模型搭建、训练、评估与对比实验思路,理解不同骨干网络在分心驾驶识别任务上的表现差异,并借助README快速理清项目结构与运行流程,为毕业设计撰写和后续改进提供扎实参考。
1. 从一份能跑通的分心驾驶识别源码说起
分心驾驶识别这个题目,在计算机视觉毕业设计里属于"看着简单、做起来全是坑"的典型。数据集里十类行为——安全驾驶、右手打字、左手打电话、喝水、拿手机、调收音机、后座回头、整理头发化妆、和乘客说话——每一类之间的视觉差异其实很小,尤其是"右手打字"和"拿手机"这两类,手部区域高度重叠,模型很容易学混。这份基于深度学习实现驾驶员分心驾驶行为识别源码+全部数据(python毕业设计)的资源包,把从 LeNet-5 到 ResNet152、InceptionV4、DenseNet161 的完整对比实验链路都放进来了,还带了训练日志 CSV 和迁移学习脚本,对正在做计算机毕业设计、需要一套能复现、能改、能写进论文的实验框架的人来说,省掉了大量搭骨架的时间。它适合两类人:一类是刚入门深度学习、想拿一个真实多分类任务练手的同学;另一类是已经跑过 MNIST、CIFAR,但没做过迁移学习和模型对比实验的从业者。
2. 模型选型与迁移学习:为什么包里塞了这么多网络
2.1 从 LeNet-5 到 ResNet152 的梯度设计
打开压缩包,第一眼会看到一堆以网络名命名的 py 文件:lenet5.py、AlexNet.py、VGG16.py、resnet.py、Inceptionv4.py,还有对应的-transfer版本。这不是随便堆的,而是一条从浅到深、从自建到迁移的对比实验线。分心驾驶数据集规模通常在两万张量级,类别十类,如果直接用 LeNet-5 这种上世纪的结构,参数量不到 6 万,特征提取能力严重不足,验证集准确率往往卡在 60% 上下;而 ResNet152 参数量 6000 万级别,直接从头训练会过拟合到怀疑人生。所以包里的正确用法是:浅层网络用来跑 baseline 证明"深度不够不行",深层网络全部走迁移学习,加载 ImageNet 预训练权重后只微调分类头。
常见做法是冻结主干的前若干层,只训练后面的 stage 和全连接层。以 ResNet 为例,resnet18-transfer.py和resnet.py的区别就在于前者在构造模型后做了权重加载和参数冻结。下面这段是迁移学习脚本里最核心的骨架,我按包里resnet18-transfer.py的写法还原出来:
import torch import torch.nn as nn from torchvision import models # 加载 ImageNet 预训练权重,num_classes 先保持 1000 model = models.resnet18(pretrained=True) # 冻结主干所有参数,只让分类头参与训练 for param in model.parameters(): param.requires_grad = False # 替换最后的全连接层,输出改成 10 类分心行为 num_features = model.fc.in_features model.fc = nn.Linear(num_features, 10) # 只把新分类头的参数交给优化器 optimizer = torch.optim.Adam(model.fc.parameters(), lr=1e-3) criterion = nn.CrossEntropyLoss()逻辑说明:pretrained=True会去拉 ImageNet 权重,这一步需要本机已经缓存过模型文件,否则会走网络下载。冻结主干是为了在小数据集上防止灾难性遗忘——如果解冻全部参数用大学习率,预训练学到的边缘、纹理特征会被迅速冲掉。model.fc.in_features对 ResNet18 是 512,换成 ResNet152 就是 2048,这个值不能写死,必须动态取。优化器只传model.fc.parameters(),如果误传model.parameters(),虽然冻结层梯度为 None 不会更新,但优化器状态里会多出一堆无用参数,显存和速度都吃亏。
参数怎么改:学习率 1e-3 适合只训分类头;如果你想解冻最后一个 stage 做微调,学习率要降到 1e-4 甚至 1e-5,否则预训练权重会被破坏。batch size 在 8G 显存上 ResNet18 可以给到 32,ResNet152 只能给 8 到 12,再大就 OOM。
2.2 数据组织与 CSV 日志的用法
包里那几个 CSV——test_dense161+resnet152+L2.csv、test_dense161.csv、test152+152l2.csv——是训练过程记录,不是数据集标签。命名规则透露了实验配置:dense161指 DenseNet161,resnet152指 ResNet152,L2指用了 L2 正则化。这些文件里一般有 epoch、train_loss、train_acc、val_loss、val_acc 几列,用来画论文里的收敛曲线。真正喂给模型的数据要靠utils.py里的 Dataset 类来组织,目录结构通常是train/类别名/图片和val/类别名/图片两级。
from torchvision import datasets, transforms from torch.utils.data import DataLoader # 训练集增强:随机裁剪、翻转、颜色抖动,缓解小数据集过拟合 train_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(10), transforms.ColorJitter(brightness=0.2, contrast=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) # 验证集只做 resize 和归一化,不做增强 val_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) train_set = datasets.ImageFolder('data/train', transform=train_tf) val_set = datasets.ImageFolder('data/val', transform=val_tf) train_loader = DataLoader(train_set, batch_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_set, batch_size=32, shuffle=False, num_workers=4)逻辑说明:ImageFolder要求目录名就是类别名,它会自动按文件夹排序生成类别索引,所以训练集和验证集的文件夹命名必须完全一致,否则标签会对不上。归一化的 mean 和 std 是 ImageNet 的统计值,用预训练权重就必须用这套值,自己算一套反而会掉点。num_workers在 Windows 上建议设 0,设大了容易在 DataLoader 处卡死,这是血泪经验。
参数说明:Resize((224,224))是给 ResNet、VGG、DenseNet 用的;InceptionV4 输入是 299,用Inceptionv4.py时要把这里改成 299,否则尺寸不匹配会报错。RandomRotation(10)对分心驾驶场景要慎用,因为方向盘角度、手部位置是有语义的,旋转太大会制造出物理上不存在的姿态,我一般控制在 10 度以内。
3. 训练主流程与多模型对比实验怎么落地
3.1 main.py 的调用链与训练循环
main.py是整个工程的入口,它负责解析参数、选模型、跑训练循环、存最优权重。包里同时存在test.py和temp.py,前者是推理脚本,后者通常是调试用的临时文件,正式跑实验别用temp.py。训练循环里最关键的是验证集评估和最优模型保存逻辑,很多同学写论文时曲线画不出来,就是因为没在每个 epoch 后记录指标。
import torch, time from utils import get_model, get_dataloader device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = get_model('resnet18', num_classes=10).to(device) train_loader, val_loader = get_dataloader(batch_size=32) criterion = torch.nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.fc.parameters(), lr=1e-3) best_acc = 0.0 for epoch in range(30): model.train() running_loss = 0.0 for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(imgs) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() # 每个 epoch 结束后在验证集上评估 model.eval() correct, total = 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels = imgs.to(device), labels.to(device) preds = model(imgs).argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) acc = correct / total print(f'epoch {epoch} loss {running_loss/len(train_loader):.4f} val_acc {acc:.4f}') # 只保存验证集最优权重,避免存一堆无用 checkpoint if acc > best_acc: best_acc = acc torch.save(model.state_dict(), f'best_{epoch}_{acc:.4f}.pth')逻辑说明:model.train()和model.eval()必须成对出现,前者开启 BatchNorm 和 Dropout 的训练行为,后者固定它们。验证阶段套torch.no_grad()能省显存、提速。保存文件名里带上 epoch 和 acc,方便回溯是哪一轮的最优,比只存一个best.pth实用得多。
参数说明:epoch 数在迁移学习下 20 到 30 就够收敛,从头训练浅层网络要 50 以上。学习率如果发现 loss 震荡不降,先降到 1e-4 试。get_model和get_dataloader是utils.py里的封装函数,如果你要加新网络,在这里注册一个分支即可,不用改main.py。
3.2 多模型对比实验的表格化记录
毕业设计论文里通常要有一张"不同模型在自建数据集上的性能对比"表,这份源码的多网络结构正好支撑这件事。跑完每个模型后,把验证集准确率、参数量、单 epoch 耗时记下来,整理成下面这种表,比堆一堆截图有说服力。
| 模型 | 参数量 | 输入尺寸 | 验证集准确率 | 单 epoch 耗时 |
|---|---|---|---|---|
| LeNet-5 | 约 6 万 | 32×32 | 60% 左右 | 极快 |
| AlexNet | 约 6000 万 | 224×224 | 85% 上下 | 中等 |
| VGG16 | 约 1.38 亿 | 224×224 | 90% 上下 | 较慢 |
| ResNet18 | 约 1100 万 | 224×224 | 92% 上下 | 快 |
| ResNet152 | 约 6000 万 | 224×224 | 94% 上下 | 慢 |
| DenseNet161 | 约 2800 万 | 224×224 | 94% 上下 | 较慢 |
| InceptionV4 | 约 4300 万 | 299×299 | 93% 上下 | 较慢 |
表里的准确率是这类数据集上的常见区间,具体数值取决于你的数据划分和增强策略,不要照抄,要自己跑出来。参数量一列可以直接用sum(p.numel() for p in model.parameters())打印。耗时用time.time()在 epoch 前后夹一下即可。这张表的价值在于:它解释了为什么最终选 ResNet 系列——准确率和速度的平衡最好,而 VGG16 参数量大、InceptionV4 输入尺寸特殊,都不如 ResNet 好部署。
4. 环境配置与数据准备里的常见坑
4.1 依赖版本与 CUDA 匹配
这份源码基于 PyTorch,但包里没有requirements.txt,这是最容易被忽略的坑。torchvision 的版本必须和 torch 主版本对应,比如 torch 1.13 配 torchvision 0.14,错配会直接报ImportError。另外Inceptionv4.py如果是手写的实现,可能依赖torchvision.models.inception的某些层,版本不对也会挂。
# 先确认显卡驱动和 CUDA 版本 nvidia-smi # 按官方对应关系装 torch 和 torchvision,以 CUDA 11.8 为例 pip install torch==2.0.1 torchvision==0.15.2 --index-url https://download.pytorch.org/whl/cu118 # 其余常用依赖 pip install numpy pandas matplotlib pillow tqdm逻辑说明:nvidia-smi右上角显示的 CUDA Version 是驱动支持的最高版本,不是你实际装的版本,装 torch 时选的 cu118 只要不超过它就行。用官方 index-url 装能避免 pip 从源码编译,省掉一堆编译错误。如果机器没有独显,把 index-url 换成 cpu 版本,代码里torch.cuda.is_available()会自动回退到 CPU,只是训练会慢到不适合跑 ResNet152。
4.2 数据集划分与类别不平衡
分心驾驶数据集天然不平衡,"安全驾驶"这一类往往占一半以上,其余九类分剩下的。如果直接按 8:2 随机划分,验证集里某些类可能只有几十张,准确率波动极大。常见做法是按类别分层抽样,保证每个类在训练集和验证集里的比例一致。
from sklearn.model_selection import train_test_split import os # 收集所有图片路径和标签 paths, labels = [], [] for idx, cls in enumerate(sorted(os.listdir('data/all'))): cls_dir = os.path.join('data/all', cls) for img in os.listdir(cls_dir): paths.append(os.path.join(cls_dir, img)) labels.append(idx) # stratify=labels 保证按类别比例分层 X_train, X_val, y_train, y_val = train_test_split( paths, labels, test_size=0.2, stratify=labels, random_state=42)逻辑说明:stratify=labels是分层抽样的关键参数,不加它划分就是纯随机,小类别可能全被分到一边。random_state=42固定随机种子,保证你每次划分结果一致,论文里的实验才可复现。划分完再按ImageFolder要求的目录结构把文件拷过去,或者自己写一个接收路径列表的 Dataset 类。
参数说明:test_size=0.2是常规比例,数据量小于一万时建议 0.3,让验证集更稳。如果某个类样本少于 200 张,考虑用WeightedRandomSampler在采样阶段做重加权,而不是简单复制图片,后者容易过拟合。
5. 避坑与排查:那些让实验白跑的细节
现象一:训练 loss 一直不降,准确率停在 10%。原因通常是标签没对上,ImageFolder按文件夹名排序生成索引,如果训练集和验证集文件夹命名大小写不一致,或者中间多了个无关文件夹,索引就错位了。解决:打印train_set.classes和val_set.classes,确认两个列表完全一致。
现象二:验证准确率比训练准确率还高。这在迁移学习初期很常见,原因是训练时开了 Dropout 和强增强,验证时全关掉了,模型在验证集上"发挥更正常"。但如果差距一直很大,要检查验证集是不是混进了训练集图片。解决:用图片的 md5 去重,确认两个集合没有交集。
现象三:跑到一半报 CUDA out of memory。多半是 batch size 给大了,或者忘了在验证阶段加torch.no_grad(),导致计算图一直累积。解决:先把 batch size 减半,再检查验证循环有没有with torch.no_grad():。ResNet152 在 8G 卡上 batch size 超过 12 基本必炸。
现象四:InceptionV4 报尺寸不匹配。这个网络的输入是 299×299,而其他网络是 224×224,如果你共用了一个 transform,跑到 InceptionV4 就会在第一个卷积层报错。解决:给 InceptionV4 单独配一套Resize((299,299))的 transform,在get_dataloader里按模型名分支。
现象五:CSV 日志里的准确率和自己复现的对不上。那些 CSV 是原作者在特定数据划分和超参下跑出来的,你的划分不同、随机种子不同,数值必然有差异。解决:把 CSV 当参考曲线看趋势,别当标准答案对数值,论文里写自己的实测结果。
6. 把最优模型接进推理脚本:一个能演示的收尾技巧
训练完拿到best_xx.pth之后,答辩现场最需要的是一个能实时出结果的推理脚本,而不是再跑一遍训练。包里test.py就是干这个的,但默认写法往往只输出类别索引,看不出置信度,演示效果差。我一般会改成输出 top-3 概率和对应类别名,再叠一个简单的耗时统计,让评委直观看到单张推理在几十毫秒级别。
import torch, time from PIL import Image from torchvision import transforms # 类别名顺序必须和训练时 ImageFolder 的 classes 一致 class_names = ['安全驾驶', '右手打字', '左手打电话', '喝水', '拿手机', '调收音机', '后座回头', '整理头发', '化妆', '和乘客说话'] model = get_model('resnet18', num_classes=10) model.load_state_dict(torch.load('best_18_0.9421.pth', map_location='cpu')) model.eval() tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) img = Image.open('demo.jpg').convert('RGB') x = tf(img).unsqueeze(0) # 加 batch 维度 start = time.time() with torch.no_grad(): probs = torch.softmax(model(x), dim=1)[0] elapsed = (time.time() - start) * 1000 top3 = torch.topk(probs, 3) for score, idx in zip(top3.values, top3.indices): print(f'{class_names[idx]}: {score.item()*100:.2f}%') print(f'单张推理耗时 {elapsed:.1f} ms')逻辑说明:load_state_dict的map_location='cpu'让权重能在没有 GPU 的机器上加载,答辩用的笔记本不一定有独显。unsqueeze(0)是补上 batch 维度,模型永远按 batch 输入。softmax把 logits 转成概率,top-3 输出能体现模型在易混类别上的犹豫程度——比如"右手打字"和"拿手机"经常同时出现在前三,这本身就是可以写进论文讨论的点。
参数说明:class_names的顺序必须和训练时ImageFolder生成的classes完全一致,最稳妥的做法是训练完把train_set.classes存成 json,推理时读进来,别手敲。耗时统计只算前向,不含图片读取和预处理,如果要报端到端延迟,把Image.open和tf(img)也包进计时区间。
从那以后我每次交付这类视觉项目,都会强制走一遍"训练脚本→最优权重→独立推理脚本"的闭环,确认换一台机器、不装 CUDA 也能跑出结果,再交给别人。希望帮到你。
本文还有配套的精品资源,点击获取