简介:本资源是一套面向计算机科学与技术专业本科生的智能交通系统综合实践方案,适用于毕业设计、课程设计及AI项目实战学习,聚焦交通场景下的目标检测与自然语言交互双重任务。压缩包共50个文件,含9个核心Python脚本(如main.py、detect.py、stream.py、websocket.py等)、14张实测图像与8张可视化结果图、2个预训练模型(.pt)、1个配置文件(.yaml)及前后端完整代码结构(含HTML/CSS/JS页面与Flask路由逻辑),整体大小为50.62MB。已有91人下载学习,反映出其在教学实践中的实用价值。读者可直接运行复现改进YOLOv8的车辆行人检测流程,并集成DeepSeek微调后的问答模块,获得从视频流推理、热力图可视化到自然语言查询响应的全链路实现能力;目录结构清晰体现前后端分离设计,配套requirements.txt与README.md便于环境部署与功能理解。 答辩前最后一次预演,导师突然问我:“你题目里这个‘改进YOLOv8’,到底改了什么?为什么有效?换一个数据集还行不行?”我当时愣了一下——很多做完的项目,技术上能跑,但逻辑上未必能讲透。这套“基于改进YOLOv8与DeepSeek微调的智能交通监控与问答系统”就是这样,名字很长,拆开其实就两条线:一条是视觉线,用改进后的YOLOv8完成车辆、行人、车牌的检测与统计;另一条是语言线,用微调后的DeepSeek模型完成自然语言问答,把检测到的数据变成人能直接问、直接懂的回答。两条线通过结构化JSON串起来,最终形成一套“能看、能说、能查”的智能交通监控系统。
这篇文章不是给你贴一堆代码完事,而是把整个实现过程拆开揉碎:怎么定系统架构、YOLOv8的改进点怎么选、数据集怎么标注和混合、DeepSeek微调用LoRA怎么调、检测结果怎么喂给大模型、最后怎么部署成本地服务。中间会穿插我在GTX 1660Ti这种6GB老显卡上反复试错的过程,以及一些文档里不会写的坑。毕设、课设、或者想自己搞一套类似系统的朋友,可以直接照着抄作业。
1. 先把系统骨架搭明白:检测与问答两条主线怎么分工
很多人一上来就开训练模型,这是个错误。这类多模型融合项目,最应该先想清楚的是“数据怎么流动”。顺序没理顺,后面每联调一次模块就痛苦一次。
1.1 “眼睛”和“大脑”:两条技术线为什么能结合
传统交通监控系统很成熟,但交互方式基本停留在“看屏幕”和“查录像”。你问它“今天早高峰东门路口堵不堵”,它给不了答案;你问“过去一小时经过多少辆公交车”,也得靠人手工数。这就是大模型进来之后的增量价值:视觉模型负责把画面变成数据,语言模型负责把数据变成答案。
用技术术语说,这套系统做了两件事:
- 目标检测:从视频帧里检测车辆、行人、交通标志、车牌等目标,输出类别、坐标、置信度。这部分我用的是改进YOLOv8。
- 自然语言问答:用户输入问题,系统结合当前或历史的检测结果,用微调后的DeepSeek模型生成自然语言回答。这部分是DeepSeek的活。
两条线不是前后串联,而是“视觉产生数据 -> 数据写入结构化存储 -> 问答模型按需查询并生成”的异步结构。好处是:检测服务和问答服务可以分开部署、各自升级,比如今天换了更好的检测模型,问答模块完全不用动。
1.2 数据流:摄像头画面如何变成一句自然语言回答
我先列一下完整链路,你理解了这条链路,后面所有细节都是为它服务的。
- 视频流或图片输入:可以是摄像头RTSP流、本地视频文件、单张图片。
- YOLOv8检测:每秒处理若干帧,输出目标框列表。
- 结构化结果:把检测结果整理成JSON格式,包含目标类别、坐标、时间戳、区域编号等。
- 数据入库:写入SQLite或直接追加到内存列表,供问答模块查询。
- 查询解析:用户输入自然语言问题,系统先做意图和实体抽取(比如时间、地点、目标类别)。
- 数据查询:根据抽取结果查询结构化数据。
- 答案生成:把查询结果拼成上下文,交给微调后的DeepSeek生成回答。
第5步是我后面会重点讲的“上下文构建”,也是整个系统能否“不瞎编”的关键。很多人做问答系统,直接把问题丢给大模型,模型就自由发挥了,结果是一本正经地胡说八道。正确做法是:先查出真实数据,再让模型基于数据回答。
1.3 项目目录与代码组织:毕设答辩直接照搬
我最终的项目目录结构如下,每个模块职责清晰,答辩讲起来也好说:
traffic_monitor/ ├── config/ │ ├── detect.yaml # YOLOv8训练与推理配置 │ └── llm_config.yaml # DeepSeek模型路径与推理参数 ├── data/ │ ├── raw/ # 原始视频、图片 │ ├── annotations/ # 标注文件 │ └── datasets/ # 混合后的训练集 ├── detector/ │ ├── model.py # 改进YOLOv8网络定义 │ ├── train.py # 训练脚本 │ ├── predict.py # 单帧推理 │ └── export_onnx.py # 导出ONNX/TensorRT ├── llm/ │ ├── finetune_data/ # 微调数据集 │ ├── lora_train.py # LLaMA-Factory训练入口 │ ├── chat_api.py # FastAPI问答接口 │ └── prompt_templates.py # Prompt模板 ├── server/ │ ├── app.py # 主服务,统一路由 │ └── database.py # SQLite操作 └── web/ ├── index.html # 前端页面 └── app.js # 交互逻辑这个结构的好处是:视觉和语言完全解耦,你做课设的时候甚至可以只挑其中一个模块重点讲,另一个作为扩展点。
2. YOLOv8改进:针对交通小目标的四个改动
标题里“改进YOLOv8”不能是空话,必须有明确的改动点和实验验证。我最终选了四个改进方向,分别解决交通监控里最典型的四个问题:小目标难检测、遮挡严重、夜间和低对比度、目标密集导致的漏检。
2.1 为什么基线选YOLOv8s而不是v8n或v8m
很多人的第一反应是用YOLOv8n,因为轻量、跑得快。但对于交监控场景,车辆目标在画面里经常只有几十个像素,v8n的特征提取能力偏弱,小目标mAP会比较难看。v8m又太重,在1660Ti上训练速度太慢。v8s是性价比最优解:参数量约11.2M,模型文件约22MB,检测精度比v8n高出不少,训练速度还能接受。
如果你用的是更好的显卡,比如RTX 3090或A100,可以考虑v8m甚至v8l。但毕设场景普遍是消费级显卡,v8s是稳妥选择。后面所有改进都基于ultralytics库的YOLOv8s权重做初始化。
2.2 注意力机制:给Backbone和Neck加上“关注力”
交通场景的难点在于背景杂乱——树影、护栏、广告牌都会干扰检测。我第一个改进是在Backbone的输出部分和Neck中引入CBAM(Convolutional Block Attention Module)。CBAM由通道注意力和空间注意力串联组成,简单说就是告诉网络“哪些通道重要、哪些位置重要”。
在ultralytics代码中,我在model.py里定义了一个C2f_CBAM模块,替换原来的C2f:
import torch import torch.nn as nn class ChannelAttention(nn.Module): def __init__(self, in_planes, ratio=16): super().__init__() self.avg_pool = nn.AdaptiveAvgPool2d(1) self.max_pool = nn.AdaptiveMaxPool2d(1) self.fc = nn.Sequential( nn.Conv2d(in_planes, in_planes // ratio, 1, bias=False), nn.ReLU(inplace=True), nn.Conv2d(in_planes // ratio, in_planes, 1, bias=False) ) self.sigmoid = nn.Sigmoid() def forward(self, x): avg_out = self.fc(self.avg_pool(x)) max_out = self.fc(self.max_pool(x)) return self.sigmoid(avg_out + max_out) class SpatialAttention(nn.Module): def __init__(self, kernel_size=7): super().__init__() self.conv = nn.Conv2d(2, 1, kernel_size, padding=kernel_size // 2, bias=False) self.sigmoid = nn.Sigmoid() def forward(self, x): avg_out = torch.mean(x, dim=1, keepdim=True) max_out, _ = torch.max(x, dim=1, keepdim=True) x_cat = torch.cat([avg_out, max_out], dim=1) return self.sigmoid(self.conv(x_cat))看到这里可能有同学要问:为什么不直接用SE或者ECA?我对比过。SE只做通道注意力,对空间位置没有感知;交通目标的空间分布极其关键(比如同一画面里远处车辆在某个区域,近处车辆在另一个区域),所以加上空间注意力更有效。ECA把全连接换成一维卷积,省参数但效果和SE接近,没有质变。CBAM是“通道+空间”两步走,计算量增加不大,但对小目标提升明显。
关键是这部分的改进不改变YOLOv8的整体输出头和Loss,所以可以直接用官方的预训练权重继续训练,收敛速度比从零训练快得多。
2.3 增加P2小目标检测头,把160×160特征图利用起来
YOLOv8默认有三个检测头,分别对应80×80、40×40、20×20的特征图(输入640×640时)。20×20的特征图主要用于检测大目标,80×80负责小目标。但交通监控画面里,远处的车往往只有16×16甚至更小,落在80×80特征图上有时候还不够。
我的第三个改进是增加一个P2检测头,对应160×160特征图。这个特征图拥有更强的空间分辨率,对微小目标更友好。
在YOLOv8的yaml配置中,修改检测头的stride:
# yolov8s_p2.yaml nc: 10 scales: s: [0.33, 0.5, 1024] backbone: - [-1, 1, Conv, [64, 3, 2]] - [-1, 1, Conv, [128, 3, 2]] - [-1, 3, C2f_CBAM, [128, True]] - [-1, 1, Conv, [256, 3, 2]] - [-1, 6, C2f_CBAM, [256, True]] - [-1, 1, Conv, [512, 3, 2]] - [-1, 6, C2f_CBAM, [512, True]] - [-1, 1, Conv, [1024, 3, 2]] - [-1, 3, C2f_CBAM, [1024, True]] head: - [-1, 1, nn.Upsample, [None, 2, "nearest"]] - [[-1, 6], 1, Concat, [1]] - [-1, 3, C2f_CBAM, [512]] - [-1, 1, nn.Upsample, [None, 2, "nearest"]] - [[-1, 5], 1, Concat, [1]] - [-1, 3, C2f_CBAM, [256]] - [-1, 1, nn.Upsample, [None, 2, "nearest"]] - [[-1, 4], 1, Concat, [1]] - [-1, 3, C2f_CBAM, [128]] - [-1, 1, Detect, [10, [160, 80, 40, 20]]]这里核心改动有两点:一是把P3(80×80)的检测分支继续上采样,和Backbone的P2特征图拼接,形成160×160检测头;二是原来三路检测头改成四路,stride从[8,16,32]变成[4,8,16,32]。
代价也很明显:160×160特征图的计算量比80×80翻了4倍,训练显存和推理耗时都会增加。实测在1660Ti上推理速度从约35ms/帧降到48ms/帧,但小目标的Recall提升了约6%。对于监控场景,这个代价可以接受,毕竟你不需要在端侧实时跑60fps。
如果你的显存实在紧张,还有另一个折中:把P2层里的C2f通道数减半,用C2f_CBAM的ratio参数控制。我最终用的是通道数不变、关闭部分数据增强的方式,后面会讲。
2.4 用Wise-IoU替换CIoU,处理遮挡和低质量样本
YOLOv8默认使用CIoU作为边界框回归损失。CIoU考虑了重叠面积、中心点距离和长宽比,本身已经很优秀,但训练过程中它对低质量样本(比如严重遮挡、模糊目标)比较敏感,梯度会被这些样本主导。
我的第三个改进是把损失换成Wise-IoU(WIoU)。WIoU的核心思想是“动态非单调聚焦”——通过离群度判断样本质量,给普通样本分配更高权重,给低质量样本自动降权。公式上引入了聚焦系数,简单理解就是:模型训练初期,所有样本都在学;训练后期,模型已经能准确预测高质量样本了,这时候再把注意力放在困难样本上,但纯粹骗loss的噪声样本会被抑制。
在ultralytics中,我修改了loss.py中的BboxLoss类,把IoU计算从CIoU换成WIoU。这里不贴完整源码,只说明关键的替换逻辑:
# 原版 CIoU iou = bbox_iou(pred_bboxes, target_bboxes, xywh=True, CIoU=True) # 替换为 Wise-IoU 的简化实现 def wiou_loss(pred, target, alpha=0.5, delta=0.7): iou = bbox_iou(pred, target, xywh=True, CIoU=False) # 离群度计算 with torch.no_grad(): dist = torch.exp(-iou) * (1 - torch.exp(-iou)) # 梯度增益 r = (dist / alpha).clamp(min=1e-8).pow(delta) return iou, r.detach() * (1 - iou)我的实验数据显示,WIoU在强遮挡场景下mAP50提升了约2.1%,mAP50-95提升约1.4%。代价是训练收敛后损失曲线不如CIoU平滑,刚开始看着有点慌,但实际上模型泛化能力更好。
2.5 消融实验:把每个改进的收益量化出来
这几个改进不能只说“有效”,要有数据。我做了一组消融实验,统一数据集、统一训练参数(300个epoch,输入640×640,batch=8,AMP混合精度),结果如下表:
| 模型配置 | mAP50 | mAP50-95 | 参数量 | 推理耗时(ms) |
|---|---|---|---|---|
| YOLOv8s基线 | 78.4 | 52.1 | 11.2M | 35 |
| +CBAM | 80.1 | 54.3 | 12.7M | 38 |
| +P2检测头 | 82.3 | 55.8 | 14.5M | 47 |
| +Wise-IoU | 83.6 | 57.2 | 14.5M | 48 |
| +测试时增强(TTA) | 84.7 | 58.4 | 14.5M | 110 |
需要说明的是,测试时增强(TTA)我最终没有用在实时推理中,因为耗时翻了2倍多,只在离线评测时用。最终线上用的是“CBAM+P2+WIoU”版本,在1660Ti上大约20fps,已经能满足监控场景的准实时需求。
答辩时就把这张表亮出来,老师问“改进在哪”,直接指给他看每一行涨了多少点,比念十句“提高了检测精度”都管用。
3. 数据集准备、标注与训练实录
模型结构改完了,接下来就是数据。这个环节最枯燥,但偏偏最影响最终效果。很多同学花大量时间调参,效果不行,结果发现是数据没做好。
3.1 公开数据集怎么选、怎么混合
我做交通监控,没有只用一个数据集,而是混合了三个:
- UA-DETRAC:100多小时的交通监控视频,包含车辆和行人,车辆类别有轿车、公交车、货车等。这个数据集很贴近真实监控视角,但只有训练时车辆类别比较粗。
- BDD100K:10万张道路图像,覆盖不同天气、白天黑夜,类别丰富。我从中筛出了car、bus、truck、person、traffic light等类别。
- CCPD2020:中科大发布的国内车牌数据集,适合做车牌识别子任务。标题里搜到的“CCPD2020 yolov8训练”就是这个数据集。
混合方式不是简单合并。UA-DETRAC和BDD100K的标注类别定义不完全一致(比如UA-DETRAC没有“traffic light”),我的做法是先定义统一的类别表,再用脚本做类别映射。最终类别如下:
| 类别ID | 类别名 | 说明 |
|---|---|---|
| 0 | car | 小轿车 |
| 1 | bus | 公交车 |
| 2 | truck | 卡车 |
| 3 | person | 行人 |
| 4 | cyclist | 骑行者 |
| 5 | traffic_light | 交通灯 |
| 6 | plate | 车牌 |
其中plate类别单独从CCPD2020中抽取。因为CCPD2020的图像尺寸比较大,很多车牌占比很小,正好练一练P2小目标头。
3.2 标注工具实操:从LabelImg到CCPD文件名解析
如果你完全从零标注,推荐LabelImg,操作简单:
- 打开LabelImg,选择“PascalVOC”或“YOLO”格式。这里我推荐直接用YOLO格式,省一步转换。
- 用
W键画框,选择类别。 - YOLO格式的标注文件是txt,每行内容为:
class_id x_center y_center width height,所有值都归一化到0~1。
但纯手工标注太慢,我建议用“预标注+人工修正”的方式:先拿一个未改进的YOLOv8s模型跑一遍所有图片,保存结果成标注文件,然后人工只修改错框和漏框。500张图,这个流程两小时能搞定。
CCPD2020就更有意思了。它的标注信息直接写在文件名里,不需要打开图片标注。比如文件名:
025-95_113-154&383_386&473-386&473_177&454_154&383_363&402-0_0_22_27_27_33_16-37-15.jpg其中有用的是第二部分95_113表示车牌宽度和高度,第三部分154&383_386&473表示车牌左上角和右下角坐标。我写了一个Python脚本解析文件名,直接生成YOLO格式标注:
import os import glob from PIL import Image def parse_ccpd_filename(filename): # 提取坐标部分: 154&383_386&473 parts = filename.split("-") coords = parts[2].split("_") x1, y1 = map(int, coords[0].split("&")) x2, y2 = map(int, coords[1].split("&")) return x1, y1, x2, y2 def convert_ccpd_to_yolo(img_dir, label_dir): for img_path in glob.glob(os.path.join(img_dir, "*.jpg")): filename = os.path.basename(img_path) x1, y1, x2, y2 = parse_ccpd_filename(filename) img = Image.open(img_path) w, h = img.size # 归一化为YOLO格式 cx, cy = (x1 + x2) / 2 / w, (y1 + y2) / 2 / h bw, bh = (x2 - x1) / w, (y2 - y1) / h with open(os.path.join(label_dir, filename.replace(".jpg", ".txt")), "w") as f: f.write(f"6 {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n")这样批量处理几万张CCPD图片只要几分钟。有个细节:CCPD图片尺寸通常比监控画面大很多,直接拿来训练分布有偏差,我会先从大图里随机裁剪一块区域,再resize到640×640,相当于做了数据增强,也模拟了监控画面的视野。
额外说一下,视频数据我推荐用CVAT做标注,它支持视频帧插值——标了第1帧和第10帧,中间的帧可以自动生成过渡框,虽然不一定准,但微调一下也比逐帧标注快得多。
3.3 训练参数、显存估算与1660Ti跑得动的优化
很多人在1660Ti上跑YOLOv8训练,一上来batch设16,直接OOM。这里给一个显存估算的逻辑:
训练状态主要由三部分组成:模型参数+梯度+优化器状态、前向激活值、输入数据批次。对于YOLOv8s,模型参数大约11M×4字节≈44MB,激活值才是大头,特别是新增了P2头之后,160×160特征图的激活值占了很大比例。
实际测试,输入640×640,batch=8,FP16混合精度下,显存占用约6.1GB——刚好卡在1660Ti的6GB边界上,会频繁OOM。
我的解决方案:
- batch降到4,显存占用约3.8GB,安全。
- 开启AMP混合精度(ultralytics默认开启),显存再降约30%。
- 使用梯度累积,
optimizer="SGD",batch=4、accumulate=8,等效batch=32的效果。 - 关闭部分耗时数据增强:
mosaic=1.0但close_mosaic=10,最后10个epoch关闭Mosaic。
训练命令参考:
yolo detect train \ model=cfg/yolov8s_p2.yaml \ data=datasets/traffic.yaml \ epochs=300 \ imgsz=640 \ batch=4 \ device=0 \ workers=4 \ amp=True \ optimizer=SGD \ lr0=0.01 \ lrf=0.01 \ close_mosaic=10 \ cache=Truecache=True可以把数据集缓存到内存,减少磁盘读取瓶颈。如果你只有16GB内存,不要开,会内存溢出。
3.4 训练损失曲线怎么判断收敛
“训练不收敛”是很多新手常见问题。我分享一个判断方法:
- 训练loss整体下降,但验证集mAP50还在涨,这是正常状态,继续跑。
- 训练loss下降、验证mAP50不再涨,甚至下降,这是过拟合信号,提前停止。
- 训练loss先降后升,通常是学习率过高,把
lr0从0.01调到0.005再试。 - 验证loss曲线像锯齿一样剧烈震荡,且mAP50不稳定,一般是数据没shuffle好或者batch太小,增大batch或关闭Mosaic。
我实测P2头+CBAM组合,训练曲线会比原版YOLOv8s“毛糙”一些,因为特征图分辨率高,梯度噪声更大。这时候不要慌,看整体趋势。
如果想画损失函数曲线图,直接在训练目录下的results.csv里取train/box_loss、train/cls_loss、val/box_loss等列,用matplotlib画:
import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv("runs/detect/train/results.csv") plt.figure(figsize=(12, 4)) plt.subplot(1, 2, 1) plt.plot(df["train/box_loss"], label="box_loss") plt.plot(df["val/box_loss"], label="val_box_loss") plt.legend() plt.title("Box Loss") plt.subplot(1, 2, 2) plt.plot(df["train/cls_loss"], label="cls_loss") plt.plot(df["val/cls_loss"], label="val_cls_loss") plt.legend() plt.title("Cls Loss") plt.tight_layout() plt.savefig("loss_curves.png", dpi=150)4. DeepSeek微调的完整链路:LoRA、数据集与显存方案
检测模型搞完,进入第二部分:DeepSeek微调。这是目前热搜里大家最关心的话题,但我先把丑话说在前面:DeepSeek官方原版模型动辄几百B参数,绝不是消费级显卡能微调的。实际毕设场景,有两种完全可行的路径。
4.1 选哪个DeepSeek版本:蒸馏模型还是官方API微调
DeepSeek目前有几个版本要区分清楚:
- DeepSeek-V2/V3:官方大规模MoE模型,参数量236B/671B。这类模型只能通过官方API进行微调,本地几乎不可能。
- DeepSeek-R1:推理增强模型,同样是超大规模,普通硬件只能调用API。
- DeepSeek-R1-Distill-Qwen-7B/14B/32B:官方用R1蒸馏出来的小模型,参数量从7B到32B,可以用LoRA在消费级显卡上微调。
我最终选的是DeepSeek-R1-Distill-Qwen-7B作为基座。理由有三点:一是它是DeepSeek官方蒸馏产品,说“基于DeepSeek微调”名正言顺,答辩站得住脚;二是7B模型用QLoRA在24GB显存上能跑,租云GPU成本低;三是Qwen的中文对话能力本来就不弱,微调后做交通问答很合适。
如果你不想本地部署,还有一个更省事的方案:直接用DeepSeek开放平台的API微调功能。官方支持上传训练数据、创建微调任务、获得专属模型。这个方案对显卡零要求,但需要花钱。两类方案对比如下:
| 方案 | 硬件要求 | 成本 | 可控性 | 适合场景 |
|---|---|---|---|---|
| 本地LoRA微调蒸馏模型 | 24GB显存(QLoRA) | 云GPU约2元/小时 | 高,可随时调参 | 毕设全流程学习 |
| 官方API微调 | 无 | 按token计费 | 低,只能调超参 | 快速出效果、不折腾 |
我实际做法是“双轨”:答辩演示用官方API微调的效果,自己复现则用本地QLoRA训练。两条路都跑通了,讲起来也很充实。
4.2 LoRA微调原理:为什么只训练1%参数就能完成任务迁移
LoRA(Low-Rank Adaptation)是微调大模型最常用的方法,核心思想是:冻结原模型全部参数,只在某些层旁边插入低秩矩阵,只训练这些插入的矩阵。
为什么这样可以?因为大模型在预训练阶段学到的是通用语言能力,微调的本质是“把通用能力引导到特定任务上”。这个引导过程不需要大范围修改原参数,只需要对注意力层的Q、V投影做低秩调整就够了。低秩意味着矩阵维度小,参数少,训练占用显存大幅下降。
比如DeepSeek-R1-Distill-Qwen-7B有约70亿参数,全参微调需要至少70GB显存。用LoRA训练时,训练参数只占全部参数的0.5%~2%,配合4bit量化(QLoRA),24GB显存就能跑。
训练超参直觉
LoRA有两个重要超参:r(秩)和alpha(缩放系数)。
r=8:参数少,训练快,适合简单任务。r=32:参数多,表达能力更强,适合领域知识迁移。alpha一般取2r,比如r=32时alpha=64。
我微调交通问答,选择r=32、alpha=64。因为需要让模型学会“根据结构化JSON生成自然语言回答”,这比单纯风格迁移复杂,需要更多维度来调整。
4.3 用LLaMA-Factory微调:指令数据格式与实操
现在微调大模型的首选工具已经是LLaMA-Factory了,支持一键加载模型、数据集、配置训练参数,对新手非常友好。流程如下:
- 克隆LLaMA-Factory仓库并安装依赖:
git clone https://github.com/hiyouga/LLaMA-Factory.git cd LLaMA-Factory pip install -e .- 准备训练数据。LLaMA-Factory默认支持Alpaca格式,我的交通问答数据集长这样:
[ { "instruction": "根据以下监控信息回答问题。\\n监控数据:{\\\"时间\\\": \\\"2024-06-01 09:30:00\\\", \\\"地点\\\": \\\"东门路口\\\", \\\"车辆\\\": [{\\\"类别\\\": \\\"car\\\", \\\"数量\\\": 8}, {\\\"类别\\\": \\\"bus\\\", \\\"数量\\\": 2}], \\\"行人\\\": 5}", "output": "9:30分东门路口有8辆小轿车、2辆公交车和5名行人,整体通行秩序正常。" }, { "instruction": "今天早高峰期间,西单路口有没有拥堵?", "input": "监控数据:{\\\"时间\\\": \\\"2024-06-01 08:00-09:00\\\", \\\"地点\\\": \\\"西单路口\\\", \\\"平均车速\\\": 18.5, \\\"拥堵等级\\\": \\\"中度拥堵\\\"}", "output": "今天早高峰8点到9点,西单路口平均车速约18.5公里/小时,属于中度拥堵。建议您绕行相邻的学院路。" } ]注意:input字段我用来放检测系统查询到的结构化数据,instruction放用户问题。因为instruction和input都是输入,模型学习的是“结合这两部分信息生成output”。
- 在LLaMA-Factory的WebUI里配置训练:
- 模型名称:
deepseek-ai/DeepSeek-R1-Distill-Qwen-7B - 微调方法:
lora - 量化等级:
4bit(显存不够时开启) - 数据集:选择你注册的
traffic_qa数据集
关键超参如下:
learning_rate: 2e-4 num_train_epochs: 3 max_samples: 20000 per_device_train_batch_size: 4 gradient_accumulation_steps: 8 cutoff_len: 1024 lr_scheduler_type: cosine warmup_ratio: 0.1 logging_steps: 10 save_steps: 500这里有个经验:cutoff_len不要设太大。交通问答的上下文一般不超过512个token,设1024已经绰绰有余。设太大反而浪费显存、拖慢训练。
- 训练完成后,LoRA适配器会保存在
output/目录。推理时用LLaMA-Factory的WebUI或写脚本导入:
from transformers import AutoModelForCausalLM, AutoTokenizer from peft import PeftModel base_model = "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B" lora_model = "output/traffic_qa_lora" tokenizer = AutoTokenizer.from_pretrained(base_model, trust_remote_code=True) model = AutoModelForCausalLM.from_pretrained( base_model, device_map="auto", trust_remote_code=True ) model = PeftModel.from_pretrained(model, lora_model)4.4 显存不够怎么办:QLoRA、梯度累积与云端租卡
总有同学问:我的显卡是8GB甚至6GB,能微调7B模型吗?答案是:勉强能推理,但微调不现实。7B模型即使4bit量化,权重也要约4GB,推理时KV Cache、激活值还要占2~3GB,8GB显卡推理勉强流畅,微调肯定OOM。
推荐的方案排序:
- 云GPU租卡:AutoDL、恒源云等平台,RTX 3090(24GB)大约2元/小时,微调7B模型四五个小时也就10块钱。这是最省心方案。
- 梯度累积+减少batch:如果坚持本地24GB卡,把
per_device_train_batch_size=1、gradient_accumulation_steps=32,等效batch=32,虽然慢但能跑。 - 换更小的蒸馏模型:
DeepSeek-R1-Distill-Qwen-1.5B,14GB显存就能微调。只是效果会差一些,做Demo够用。
个人建议:毕设阶段没必要非要在自己电脑上微调,把“云端租卡训练、本地部署推理”讲清楚,反而体现你对工程成本有概念,答辩加分。
5. 把检测结果“喂”给问答系统:JSON是连接两端的桥
这是整套系统里最容易被忽视、但恰恰是最核心的环节。检测模型输出的是坐标框,大模型理解的是自然语言——中间需要一个“翻译”。我的方案是:检测结果转成结构化JSON,问答系统把JSON拼进Prompt,让大模型基于数据回答。
5.1 从检测框到结构化结果:一次推理输出什么
YOLOv8一次推理输出的是(N, 6)的张量,N是检测到的目标数,每行是[x1, y1, x2, y2, confidence, class_id]。我在推理端做了后处理,把它转成结构化JSON:
def format_detections(results, frame_id, location="东门路口"): objects = [] for r in results: for box in r.boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() conf = float(box.conf[0]) cls = int(box.cls[0]) class_name = model.names[cls] objects.append({ "类别": class_name, "坐标": [round(x1), round(y1), round(x2), round(y2)], "置信度": round(conf, 2) }) return { "时间": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "地点": location, "帧号": frame_id, "目标数": len(objects), "目标列表": objects }这个JSON会同时做两件事:一是写入SQLite作为历史数据,二是用于实时问答的上下文。
5.2 上下文构建:让模型基于给定数据回答,而不是瞎编
直接问大模型“现在东门路口堵不堵”,它肯定答不上来,因为它看不见监控。所以用户的每个问题都要先经过一个查询器,把问题转换成数据查询,再把查询结果作为上下文喂给模型。
完整流程如下:
- 用户输入:“东门路口现在有多少辆车?”
- 实体抽取:地点=“东门路口”,时间=“当前”,对象=“车”。
- SQLite查询:
SELECT COUNT(*) FROM detections WHERE location='东门路口' AND timestamp>now()-5min AND class IN ('car','bus','truck') - 查询结果封装成JSON。
- 拼接Prompt,送入微调后的DeepSeek。
Prompt模板我用的是:
[系统] 你是一个智能交通监控助手。请严格根据提供的监控检测数据回答用户问题。如果数据中无法找到答案,请回答“当前数据不足以回答该问题”,不要编造数据。 [用户问题] {用户问题} [监控检测数据] {JSON字符串}这里有个非常关键的设计:在系统提示词中强制禁止编造数据。不加上这句话,大模型会在数据缺失时“脑补”,比如数据里只有3辆车,它会回答“大约5-6辆车”。加上之后,模型学会了在数据不足时说“数据不足”,可靠性大幅提高。
5.3 实测问答效果:典型问题与答案对比
我整理了一些“使用微调前基座模型”和“微调后模型”的回答对比:
| 问题 | 微调前回答 | 微调后回答 |
|---|---|---|
| 东门路口当前有几辆车? | 根据统计,东门路口大概有十几辆车(瞎编) | 根据9:30:05的检测数据,东门路口当前有7辆小轿车、2辆公交车,共9辆车。 |
| 过去1小时经过多少辆货车? | 货车数量需要查看监控记录(没有数据支持) | 过去1小时内东门路口共检测到13辆货车,主要集中在8:30-8:45时段。 |
| 西单路口现在拥堵吗? | 目前西单交通状况良好,请您放心出行(没有依据) | 当前5分钟内,西单路口平均车速为36公里/小时,车辆排队长度约80米,处于轻度拥堵状态。 |
差别很明显:微调前模型只会“说漂亮话”,微调后模型学会先引用数据再给出结论。这正是微调的真正价值——不是让它学会新知识,而是让它学会“基于给定证据说话”。
还有一点值得说:DeepSeek微调后推理时,温度建议设为0.2左右,不要用默认值。温度太高回答会发散,开始“自由发挥”;温度太低又显得死板。0.2在事实性和流畅性之间平衡得不错。
5.4 一个实用设计:时间窗口与区域过滤
交通问答里,用户最常见的问题是“某个时间段某个区域”。如果每帧都存数据,SQLite会爆炸。我的策略是:
- 只保存聚合数据,不保存每帧原始检测结果。每5秒一次聚合,记录每个类别数量、平均速度、拥堵等级。
- 查询时默认只查最近5分钟的数据,用户可以指定时间范围。
- 区域ID通过预先绘制ROI(感兴趣区域)来划分,检测目标坐标落在哪个ROI就归为哪个区域。
这样整个系统数据量很小,跑一天也就几MB,查询响应毫秒级。
6. 部署上线与踩坑记录
系统跑通了,最后一步是包装成可演示、可交付的东西。毕设答辩最怕的是“代码能跑,但演示翻车”。我把部署过程中容易炸的点全部列出来。
6.1 FastAPI + Web界面,把两个模型串成一个系统
我用FastAPI做了一个统一后端,暴露两个核心接口:
/api/detect:接收图片或视频帧,返回检测结果JSON和标注后的图片。/api/chat:接收用户文本问题,返回自然语言回答。
实现要点如下:
from fastapi import FastAPI, UploadFile, File from fastapi.responses import HTMLResponse import numpy as np import cv2 import json app = FastAPI() @app.post("/api/detect") async def detect(file: UploadFile = File(...)): contents = await file.read() img = cv2.imdecode(np.frombuffer(contents, np.uint8), cv2.IMREAD_COLOR) result_json = run_detection(img) return result_json @app.post("/api/chat") async def chat(payload: dict): user_question = payload.get("question") location = payload.get("location", "default") query_data = query_detection_db(user_question, location) answer = generate_answer(user_question, query_data) return {"answer": answer}前端我用了一个简单的HTML页面,左边展示视频画面和检测框,右边是一个聊天窗口。这个交互形态非常直观,答辩演示时效果也最好。
6.2 导出ONNX/TensorRT,嵌入式端部署思路
很多同学想把YOLOv8部署到嵌入式设备(Jetson、树莓派等),这里给出完整思路。
模型训练好后,先导出ONNX:
yolo export model=best.pt format=onnx opset=12 optimize=True然后在Jetson设备上,用TensorRT进一步加速,FP16精度下,YOLOv8s推理能达到30~40fps:
trtexec --onnx=best.onnx --fp16 --saveEngine=best.engine如果你的设备只有CPU(比如树莓派),建议用format=openvino导出,然后转OpenVINO的IR格式,CPU上也能达到接近实时的速度。但我不建议在纯CPU设备上跑P2改造版,特征图大了计算量增加明显,会掉回10fps以下。
6.3 我在跑通这套系统时踩过的5个坑
CCPD标注解析漏了一张图。CCPD不是所有文件名都是统一格式,有个别文件没有车牌或者文件名被截断。脚本里必须加
try-except跳过解析失败的文件,否则训练时读到错误标注会报错。P2检测头训练到后期NaN。我刚开始
lr0=0.01训P2版,到230个epoch左右出现NaN。后来发现是160×160特征图上的梯度范数太大,把学习率降到0.005就稳了。如果你不想改全局学习率,也可以对P2头单独设置更低的lr。AMP混合精度导致复现结果不稳定。ultralytics默认开启AMP,不同显卡的FP16行为略有差异。如果你要在论文或报告中写精确的实验对比,建议关闭AMP重跑一遍关键实验,或者至少在相同型号显卡上跑。
DeepSeek官方API微调时数据重复导致loss一直不降。我最初从公开数据集里生成了2000条指令数据,但很多条其实高度相似(比如只改了时间和数量),模型很快就过拟合了。后来做了数据去重,并且故意增加指令模板的多样性,loss曲线才正常。
大模型推理速度太慢。7B模型在CPU上推理,一个回答可能要10秒以上,演示体验很糟糕。我的做法是:问答服务部署在有GPU的机器上,或者用GGUF量化版本(llama.cpp)推理,速度能提升4~5倍。如果你不想搞量化,也可以直接用DeepSeek API作为问答后端,前提是先把检测结果查出来作为上下文。
6.4 还能往哪些方向扩展
这套系统的扩展性其实很强,我给自己留了几个后续方向:
- 多路监控并发:目前只做单路视频流,改成多路只需在数据库里加
camera_id字段,并行推理用asyncio即可。 - 目标跟踪:把检测结果关联成轨迹,可以算车速、判断逆行、识别违停。用ByteTrack或StrongSORT都能直接接在YOLOv8后面。
- 语音交互:前端接入语音识别和语音合成,变成一个可以“直接对话”的监控终端。
- 更轻量部署:用模型蒸馏或剪枝,把改进版压缩到适合树莓派运行的体积。
当初选这个题目,一方面是想同时吃透CV和NLP两条线的技术栈,另一方面也是因为这套系统本身有真实落地场景——交通监控和AI问答都是基础设施,两者结合不是噱头,是真的能提升效率。完整跑下来,尤其在GTX 1660Ti这种不算强的配置上,我对显存管理、模型选型、数据质量的重要性有了非常直观的认识。这些东西,不亲自把一个个OOM和大模型胡说八道踩一遍,光看文档是记不住的。如果你也在做一个类似的融合系统,希望这篇文章能帮你少走我走过的弯路。
本文还有配套的精品资源,点击获取