
简介这份资源是面向深度学习入门者与边缘端部署开发者的 PyTorch 版 YOLOv3-tiny 完整实现聚焦轻量级实时目标检测适合在算力受限环境下做模型训练与推理实践。压缩包共 22 个文件约 1.17MB以 14 个 Python 脚本为核心辅以 png 架构图、names 类别文件、ttf 字体与 md 说明文档覆盖模型定义、数据预处理、锚点聚类、LMDB 数据构建、预训练、微调与推理等环节。其中网络结构、损失函数与锚点计算模块相互独立便于按需替换或二次开发图片样本与架构图则可用于快速验证与结果展示。目前已有 48 人学习下载适合希望理解 YOLO 系列工程细节、搭建定制化实时检测系统的读者参考。1. 从 PyTorch实现YOLOv3-tiny.zip 说起一个压缩包背后到底藏着什么拿到PyTorch实现YOLOv3-tiny.zip这个标题的人多半不是想听我讲目标检测发展史而是想知道这个压缩包拆开之后我能不能在本地把它跑起来跑起来之后能不能换成自己的数据换完数据之后精度会不会崩。这三个问题才是这个标题真正的分量。YOLOv3-tiny 是 YOLOv3 的轻量版本主干网络只保留了两个尺度输出卷积层数量砍到不足原版的三分之一参数量大约在 870 万上下模型文件通常不到 35MB。它牺牲的是小目标召回率和高密度场景下的精度换来的是在普通显卡甚至 CPU 上都能跑到实时帧率。PyTorch 实现版本的价值在于训练链路是透明的你可以改损失函数、换数据增强、导出 ONNX而不是被某个黑盒框架锁死。这个方向适合三类人一是要在边缘设备或低算力板子上做检测的工程师二是想拿一个完整可训练的目标检测项目练手的学生三是需要快速验证某个业务场景是否值得投入检测方案的产品侧技术负责人。如果你属于这三类往下看。2. 拆包之前先想清楚YOLOv3-tiny 的结构与 PyTorch 复现的选型逻辑2.1 为什么是 tiny而不是完整版 YOLOv3完整版 YOLOv3 有三个尺度输出分别对应 13×13、26×26、52×52 的特征图能覆盖从大目标到小目标的检测需求。tiny 版本只保留 13×13 和 26×26 两个尺度去掉了 52×52 那一支。这意味着当你的检测目标在图像中占比小于 5% 时tiny 版本大概率会漏检。但反过来看如果你的场景是固定机位、目标尺寸稳定、帧率要求高比如流水线上的零件计数、停车场出入口的车牌区域定位、工地安全帽检测tiny 版本完全够用。我一般会先拿 tiny 跑一轮 baseline如果 mAP 差距在可接受范围内就不上完整版。因为 tiny 的训练时间大约是完整版的四分之一到三分之一迭代速度快得多。从 PyTorch 实现的角度看tiny 版本还有一个隐性优势网络结构简单你在调试梯度消失、损失不收敛这类问题时排查路径短。完整版 YOLOv3 的残差块堆叠多了之后有时候一个错误的 padding 设置能让你查一整天。2.2 压缩包拆开后你应该看到的目录结构一个正常的 PyTorch 实现版本拆开后通常包含以下内容。如果你拿到的包结构差异很大先别急着跑先确认它是不是一个可训练的完整工程而不是只有推理脚本的半成品。目录/文件作用缺失后果models/网络定义含 backbone 和 head无法构建模型utils/数据加载、NMS、坐标转换训练和推理都会报错config/超参数、anchor 尺寸、类别数需要手动硬编码weights/预训练权重存放位置只能从零训练train.py训练入口无法微调detect.py单图/批量推理入口无法验证效果data/数据集配置文件需要自己写注意有些打包版本会把utils里的函数直接塞进models.py这种结构虽然能跑但后期你想换数据增强策略时会很痛苦。遇到这种包建议先花半小时做一次目录重构。2.3 PyTorch 环境搭建从 anaconda 到 CUDA 版本对齐热词里大量出现 pytorch安装、anaconda配置pytorch环境、cuda pytorch下载说明这一步卡住了很多人。我自己的习惯是用 conda 建独立环境不污染 base。# 创建独立环境python 版本选 3.8 或 3.9兼容性最好 conda create -n yolov3tiny python3.9 -y conda activate yolov3tiny # 安装 PyTorch注意 cuda 版本要和你驱动匹配 # 如果你用的是 30 系或 40 系显卡cu118 是稳妥选择 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 验证 GPU 是否可用 python -c import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))这段命令的逻辑是先隔离环境再装带 CUDA 支持的 PyTorch最后验证。参数说明cu118对应 CUDA 11.8如果你驱动版本较老可以换成cu117或cu116。torch.cuda.is_available()返回False时不要急着怀疑显卡坏了先检查驱动版本和 CUDA 运行时版本是否匹配。热词里还出现了 pytorch环境搭建wsl 和 7900xtx pytorch wsl说明有人在 WSL 里做开发。WSL2 现在对 CUDA 的支持已经比较成熟但要注意WSL 里的 CUDA 版本不能超过 Windows 主机驱动支持的上限。我一般会在 Windows 侧装好驱动然后在 WSL 里只装 PyTorch 的 CUDA 运行时不单独装完整 CUDA Toolkit。3. 把 PyTorch实现YOLOv3-tiny 跑起来训练、推理与数据替换的最小闭环3.1 用预训练权重做一次推理验证在动训练之前先确认推理链路是通的。这一步能帮你排除掉环境问题、权重加载问题、后处理问题。import torch from models import YOLOv3Tiny # 假设网络定义在 models 模块下 from utils.utils import non_max_suppression, load_classes # 加载模型结构 model YOLOv3Tiny(num_classes80) # COCO 数据集是 80 类 # 加载预训练权重map_location 确保在 CPU 上也能加载 GPU 训练的权重 state_dict torch.load(weights/yolov3-tiny.pt, map_locationcpu) model.load_state_dict(state_dict) model.eval() # 如果有 GPU移到 GPU 上 device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) # 构造一个假输入验证前向传播 dummy_input torch.randn(1, 3, 416, 416).to(device) with torch.no_grad(): outputs model(dummy_input) # outputs 通常是两个尺度的特征图打印形状确认 for i, out in enumerate(outputs): print(fscale {i}: {out.shape})这段代码的关键在于map_location和model.eval()。map_locationcpu保证你在没有 GPU 的机器上也能加载权重做结构验证。eval()会关闭 dropout 和 batch norm 的训练模式否则推理结果会不稳定。打印出的特征图形状应该是[1, 255, 13, 13]和[1, 255, 26, 26]其中 255 3 × (80 5)3 是每个格子的 anchor 数量80 是类别数5 是坐标加置信度。如果形状对不上大概率是类别数设错了或者 anchor 数量不是 3。3.2 替换成自己的数据集从 VOC 到 YOLO 格式的转换热词里有 pytorch实战实战的第一步就是换数据。YOLOv3-tiny 在 PyTorch 下通常吃的是 YOLO 格式的标注每张图对应一个.txt文件每行是class_id x_center y_center width height全部归一化到 0 到 1 之间。如果你手上是 VOC 格式的 XML转换脚本如下import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, img_w, img_h, class_map): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.iter(object): cls_name obj.find(name).text if cls_name not in class_map: continue cls_id class_map[cls_name] bbox obj.find(bndbox) x1 float(bbox.find(xmin).text) y1 float(bbox.find(ymin).text) x2 float(bbox.find(xmax).text) y2 float(bbox.find(ymax).text) # 归一化并转成中心点加宽高 x_center (x1 x2) / 2.0 / img_w y_center (y1 y2) / 2.0 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) return lines # 类别映射根据你的数据集改 class_map {person: 0, car: 1, dog: 2} # 假设图片尺寸已知实际使用时从图片读取 lines voc_to_yolo(sample.xml, 640, 480, class_map) with open(sample.txt, w) as f: f.write(\n.join(lines))参数说明x_center和y_center是边界框中心点相对于图像宽高的比例w和h是边界框宽高相对于图像宽高的比例。归一化是必须的因为 YOLO 在训练时会先把输入缩放到固定尺寸如果不归一化坐标会对不上。转换完之后你需要一个.data文件告诉训练脚本去哪里找图片和标注classes 3 train data/train.txt valid data/val.txt names data/classes.names backup backup/train.txt和valid.txt里每行是一张图片的路径。classes.names每行是一个类别名顺序要和class_map一致。3.3 训练参数怎么设anchor、学习率和 batch size 的联动YOLOv3-tiny 的默认 anchor 是针对 COCO 数据集聚类出来的。如果你的目标尺寸和 COCO 差异大比如你要检测的是显微镜下的细胞默认 anchor 会严重不匹配。我一般会用自己的数据跑一遍 k-means 聚类重新生成 anchor。常见做法是# 假设你有一个脚本可以计算 anchor python utils/gen_anchors.py --data data/mydata.data --num_anchors 6num_anchors 6是因为 tiny 版本有两个尺度每个尺度 3 个 anchor总共 6 个。聚类时用的距离度量是1 - IOU不是欧氏距离这一点很多教程写错了。学习率方面如果你是从预训练权重微调初始学习率设在 0.001 左右用余弦退火或者 step 衰减。如果是从零训练学习率可以设到 0.01但要有 warmup否则前几个 epoch 损失会炸。batch size 受显存限制。416×416 输入下YOLOv3-tiny 在 8GB 显存上大概能跑到 batch size 32 到 48。如果显存不够不要硬撑用梯度累积模拟大 batchaccumulation_steps 4 # 等效 batch size 翻 4 倍 optimizer.zero_grad() for i, (imgs, targets) in enumerate(dataloader): loss model(imgs, targets) loss loss / accumulation_steps loss.backward() if (i 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()这段代码的逻辑是每算 4 次梯度才更新一次参数等效于 batch size 乘以 4。注意 loss 要除以accumulation_steps否则梯度会累积得过大。4. 避坑与排查PyTorch实现YOLOv3-tiny 最容易翻车的五个地方4.1 损失不下降但梯度范数正常现象训练了十几个 epochtotal loss 在某个值附近震荡既不上升也不下降梯度范数打印出来是正常数量级。原因最常见的是 anchor 和标注框的匹配策略出了问题。YOLOv3 在分配正样本时会计算标注框和 anchor 的 IOU如果所有 anchor 的 IOU 都低于阈值这个标注框就会被忽略。当你的目标尺寸和默认 anchor 差异过大时大量标注框被忽略模型实际上在学一个空任务。解决打印每个 epoch 被忽略的标注框比例。如果超过 30%重新聚类 anchor。另一个可能是类别标签从 0 开始还是从 1 开始的问题有些实现要求类别从 0 开始有些从 1 开始差一位会导致所有标签错位。4.2 推理时框的位置整体偏移现象检测出来的框能框住目标但整体往左上或右下偏移了十几个像素。原因坐标解码时忘了加 grid 偏移或者 grid 的生成顺序和特征图的维度顺序对不上。YOLO 的输出是[batch, anchor, grid_y, grid_x, 5classes]如果你在解码时把grid_x和grid_y弄反了框就会偏移。解决检查解码代码里grid_x和grid_y的生成方式。常见做法是用torch.meshgrid生成注意indexing参数在 PyTorch 不同版本里默认值不同。我一般会显式写成indexingij或indexingxy避免版本差异带来的玄学问题。4.3 训练 loss 正常但 mAP 极低现象训练集和验证集的 loss 都降到了合理范围但用detect.py跑出来的结果要么全是背景要么框得乱七八糟。原因NMS 的置信度阈值和 IOU 阈值设错了。YOLOv3-tiny 的输出经过 sigmoid 之后置信度分布和完整版不同tiny 版本更容易出现大量低置信度的冗余框。如果置信度阈值设得太低NMS 之后会保留大量噪声框设得太高又会把真正的目标滤掉。解决先用--conf-thres 0.25 --iou-thres 0.45跑一轮看结果。如果框太多提高conf-thres到 0.4如果漏检多降到 0.1。这个参数没有万能值必须根据你的验证集调。4.4 换了自己的数据后类别数改了但模型没改现象训练时 loss 计算报维度不匹配或者推理时输出的通道数对不上。原因YOLOv3-tiny 的最后一层卷积输出通道数是3 × (num_classes 5)。如果你把类别数从 80 改成 3但忘了改网络定义里的num_classes输出通道数还是 255和标注的维度对不上。解决在模型初始化时显式传入num_classes并且检查最后一层卷积的out_channels。有些实现会在加载预训练权重时自动跳过最后一层有些不会需要手动处理。4.5 多卡训练时 batch norm 报错现象用DataParallel或DistributedDataParallel训练时报错说 batch norm 的 running mean 维度不匹配。原因YOLOv3-tiny 里大量使用了 batch norm多卡训练时如果 batch size 太小batch norm 的统计量会不稳定。更常见的是在加载预训练权重时多卡环境的 key 名多了module.前缀。解决加载权重时做一次 key 名清洗from collections import OrderedDict state_dict torch.load(weights/yolov3-tiny.pt) new_state_dict OrderedDict() for k, v in state_dict.items(): # 去掉 module. 前缀 name k.replace(module., ) new_state_dict[name] v model.load_state_dict(new_state_dict)这段代码的作用是去掉多卡训练保存权重时自动加上的module.前缀让单卡和多卡之间可以互相加载。5. 从能跑到好用YOLOv3-tiny 的量化导出与边缘部署验证把模型训练到 mAP 满意之后下一步通常是导出成 ONNX 或者量化成 INT8再部署到目标设备上。这一步的坑不比训练少。先看导出 ONNX 的最小命令import torch from models import YOLOv3Tiny model YOLOv3Tiny(num_classes3) model.load_state_dict(torch.load(weights/best.pt, map_locationcpu)) model.eval() dummy_input torch.randn(1, 3, 416, 416) torch.onnx.export( model, dummy_input, yolov3-tiny.onnx, opset_version11, # opset 11 对 YOLO 系列支持较好 input_names[input], output_names[output1, output2], dynamic_axes{input: {0: batch}, output1: {0: batch}, output2: {0: batch}} )参数说明opset_version11是经过验证对 YOLO 系列支持比较稳定的版本太低不支持某些算子太高有些推理引擎还没跟上。dynamic_axes允许 batch 维度动态变化方便你后面用不同 batch size 做推理。导出之后用onnxruntime做一次数值比对确认 ONNX 模型的输出和 PyTorch 输出一致import onnxruntime as ort import numpy as np sess ort.InferenceSession(yolov3-tiny.onnx) input_name sess.get_inputs()[0].name # 用同样的输入跑一遍 ort_inputs {input_name: dummy_input.numpy()} ort_outputs sess.run(None, ort_inputs) # 和 PyTorch 输出比对 with torch.no_grad(): pt_outputs model(dummy_input) for i, (pt_out, ort_out) in enumerate(zip(pt_outputs, ort_outputs)): diff np.abs(pt_out.numpy() - ort_out).max() print(foutput {i} max diff: {diff})如果max diff在 1e-4 以内说明导出没问题。如果超过 1e-2检查是否有算子被降级实现或者输入预处理不一致。量化到 INT8 时YOLOv3-tiny 的精度损失通常在 1 到 3 个 mAP 点。如果你的场景对精度敏感建议只量化 backbone检测头保持 FP16。我自己的习惯是先在验证集上跑 FP32 的 mAP再跑 INT8 的 mAP如果差距超过 5 个点就不用量化版本改用 FP16。最后说一个验证技巧不要只看 mAP还要看单张图的推理耗时分布。用time.perf_counter()跑 100 次推理去掉前 10 次预热取后 90 次的平均值和 P99 值。P99 值比平均值更能反映实际部署中的卡顿情况。这个方案值不值得做取决于你的场景是否接受 tiny 版本的精度上限。如果你能接受它带来的迭代速度和部署便利性是完整版比不了的。我自己的习惯是任何检测项目先用 tiny 跑通全链路再根据 mAP 缺口决定要不要换大模型。这个习惯帮我省下了大量在环境配置和部署调试上浪费的时间。希望帮到你。本文还有配套的精品资源点击获取