新闻详情

新闻详情

首页 / 资讯中心 / 详情

PyTorch实现YOLOv3-tiny:从网络结构到OpenCV部署的完整指南

发布时间:2026/9/26 10:30:13来源:尧图网络
PyTorch实现YOLOv3-tiny:从网络结构到OpenCV部署的完整指南
简介这份资源是面向深度学习入门者与边缘计算开发者的PyTorch版YOLOv3-tiny目标检测实现适合希望在硬件资源受限环境下搭建实时检测系统的读者。压缩包共22个文件以14个Python脚本为核心辅以png架构图、names类别文件、ttf字体与md说明文档整体约1.17MB体积轻巧便于快速部署。脚本覆盖预训练、微调与推理全流程并包含网络结构定义、数据预处理、锚点聚类计算与损失函数实现还提供LMDB数据构建工具方便加速训练时的数据读取。配套图片样本与网络架构图可用于验证模型效果、理解轻量级检测器的设计思路。目前已有48人学习适合想从零跑通YOLOv3-tiny训练与推理链路、并进一步定制化实时检测方案的开发者参考。1. 拆开这个 YOLOv3-tiny 的 PyTorch 实现它到底能跑出什么如果你手头有一批标注好的小目标数据想在一台没有顶级显卡的机器上把检测模型跑起来又不想被 YOLOv5/v8 那一套工程脚手架绕晕那这个PyTorch实现YOLOv3-tiny.zip值得先拆开看看。它给的是一个用 PyTorch 从零搭出来的 YOLOv3-tiny 网络配合 OpenCV 做推理后处理属于机器学习里目标检测方向最经典的轻量级组合。YOLOv3-tiny 只有两个 YOLO 检测头、主干是带 leaky ReLU 的卷积堆叠参数量在千万级以下CPU 上也能勉强跑通单张图非常适合拿来理解 anchor、置信度、NMS 这套检测流程的底层逻辑。它适合两类人一类是想搞懂检测网络前向传播到底吐出什么的入门者另一类是需要在边缘设备或低配环境里塞一个能用的检测器、又不想引入庞大依赖的从业者。下面我按「网络结构 → 数据与推理 → 训练与调参 → 踩坑」的顺序把这份资源拆到能直接复现的程度。2. YOLOv3-tiny 的网络结构与 PyTorch 模块拆解2.1 为什么是 tiny 而不是完整版YOLOv3 完整版用了 Darknet-53 主干加三个尺度的检测头精度高但推理慢在 1080Ti 上跑 416×416 大概能到 30 FPS 左右换到普通笔记本 CPU 就只剩个位数。YOLOv3-tiny 砍掉了大部分残差块主干只保留卷积加最大池化检测头从三个减到两个分别对应 13×13 和 26×26 的特征图。这个设计取舍很明确牺牲小目标召回换推理速度。常见做法是把它用在 416×416 或 320×320 输入上320 的输入在树莓派这类设备上都能跑到几帧。选它的理由不是精度而是「能在没有 CUDA 的机器上把整条链路跑通」这对学习和部署验证都很关键。2.2 用 PyTorch 定义主干与检测头这份资源里的网络定义一般会拆成Darknet主干和YOLOLayer两部分。下面是我按常见实现整理的核心结构参数命名和原版 Darknet 配置对齐方便你对照.cfg文件排查。import torch import torch.nn as nn # 基础卷积块Conv2d BatchNorm LeakyReLU class ConvBNLeaky(nn.Module): def __init__(self, in_ch, out_ch, k3, s1, p1): super().__init__() self.conv nn.Conv2d(in_ch, out_ch, k, s, p, biasFalse) self.bn nn.BatchNorm2d(out_ch) self.act nn.LeakyReLU(0.1, inplaceTrue) def forward(self, x): return self.act(self.bn(self.conv(x))) # YOLOv3-tiny 主干7 层卷积 5 次最大池化 class TinyBackbone(nn.Module): def __init__(self): super().__init__() self.layers nn.Sequential( ConvBNLeaky(3, 16, 3, 1, 1), nn.MaxPool2d(2, 2), # 208 ConvBNLeaky(16, 32, 3, 1, 1), nn.MaxPool2d(2, 2), # 104 ConvBNLeaky(32, 64, 3, 1, 1), nn.MaxPool2d(2, 2), # 52 ConvBNLeaky(64, 128, 3, 1, 1), nn.MaxPool2d(2, 2), # 26 ConvBNLeaky(128, 256, 3, 1, 1), nn.MaxPool2d(2, 2), # 13 ConvBNLeaky(256, 512, 3, 1, 1), nn.MaxPool2d(2, 1, 1), # 13最后一步池化不降尺寸 ConvBNLeaky(512, 1024, 3, 1, 1), ) def forward(self, x): return self.layers(x)这段代码里有两个容易忽略的点。第一最后一个MaxPool2d(2, 1, 1)的 stride 是 1、padding 是 1目的是保持 13×13 尺寸不变如果你照搬前面的(2,2)会直接把特征图压到 6×6后面检测头就对不上了。第二biasFalse是因为后面接了 BatchNorm卷积层的偏置会被 BN 的减均值操作抵消加上去纯属浪费参数。输入尺寸默认按 416×416 算如果你改成 320池化次数不变但每层尺寸会整体缩小anchor 的分配逻辑不用动因为 YOLO 是按特征图网格相对位置回归的。2.3 两个检测头的输出通道怎么算YOLOv3-tiny 每个检测头输出通道数是3 × (5 类别数)其中 3 是每个网格的 anchor 数量5 是tx, ty, tw, th, obj类别数按你的数据集来。以 VOC 的 20 类为例通道数就是 3 × 25 75。13×13 那个头负责大目标26×26 那个头负责中小目标。下面是把两个头接回主干的写法class YOLOv3Tiny(nn.Module): def __init__(self, num_classes20, anchorsNone): super().__init__() self.backbone TinyBackbone() self.num_classes num_classes # 每个头 3 个 anchor共 6 个 self.anchors anchors or [ (10,14), (23,27), (37,58), # 13x13 头 (81,82), (135,169), (344,319) # 26x26 头 ] # 13x13 分支 self.head1_conv ConvBNLeaky(1024, 256, 3, 1, 1) self.head1_out nn.Conv2d(256, 3 * (5 num_classes), 1, 1, 0) # 26x26 分支13x13 上采样后与主干 26x26 特征拼接 self.head2_conv ConvBNLeaky(256 256, 256, 3, 1, 1) self.head2_out nn.Conv2d(256, 3 * (5 num_classes), 1, 1, 0) self.upsample nn.Upsample(scale_factor2, modenearest) def forward(self, x): # 这里需要主干在 26x26 处提前引出实际实现用 hook 或改 forward feat13 self.backbone(x) # [B,1024,13,13] h1 self.head1_conv(feat13) out1 self.head1_out(h1) # [B,75,13,13] up self.upsample(h1) # [B,256,26,26] # 假设 feat26 是主干 26x26 的输出需在 TinyBackbone 里返回 feat26 self._get_feat26(x) h2 self.head2_conv(torch.cat([up, feat26], dim1)) out2 self.head2_out(h2) # [B,75,26,26] return out1, out2head1_out和head2_out都是 1×1 卷积作用是把通道数压到目标维度不做空间变换。anchor 的数值是 COCO 上聚类出来的如果你换自己的数据集建议先用 k-means 重新聚一遍否则大目标框回归会明显偏。Upsample用nearest而不是双线性是因为检测里上采样只是为了对齐尺寸插值方式对精度影响很小nearest 更快。3. 数据准备、推理与 OpenCV 后处理落地3.1 数据集格式与 DataLoader 写法YOLO 系列吃的是归一化后的(x_center, y_center, w, h)每张图对应一个.txt每行类别 x y w h数值都在 0 到 1 之间。常见做法是目录结构按images/和labels/分开文件名一一对应。下面这个 Dataset 是最小可用版本import os import cv2 import torch from torch.utils.data import Dataset class YOLODataset(Dataset): def __init__(self, img_dir, label_dir, img_size416): self.img_dir img_dir self.label_dir label_dir self.img_size img_size self.names [f for f in os.listdir(img_dir) if f.lower().endswith((.jpg, .png, .jpeg))] def __len__(self): return len(self.names) def __getitem__(self, idx): name self.names[idx] img cv2.imread(os.path.join(self.img_dir, name)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) h0, w0 img.shape[:2] # 保持长宽比的 letterbox 填充 r self.img_size / max(h0, w0) new_w, new_h int(w0 * r), int(h0 * r) img cv2.resize(img, (new_w, new_h)) canvas torch.full((self.img_size, self.img_size, 3), 114, dtypetorch.uint8) canvas[:new_h, :new_w] torch.from_numpy(img) img_t canvas.permute(2, 0, 1).float() / 255.0 label_path os.path.join(self.label_dir, name.rsplit(., 1)[0] .txt) boxes [] if os.path.exists(label_path): with open(label_path) as f: for line in f: c, x, y, w, h map(float, line.split()) boxes.append([c, x, y, w, h]) return img_t, torch.tensor(boxes)letterbox填充值是 114这是 YOLO 官方用的灰度值换成 0 也能跑但边缘会出现黑边伪影影响小目标。r取max(h0, w0)是为了保证长边缩放到 416短边按比例缩再补到正方形。标签不做缩放因为 YOLO 标签本身就是相对坐标跟图像尺寸无关这一点和很多检测框架不一样别顺手去乘原图宽高。3.2 用 OpenCV 做推理后处理网络输出的是tx, ty, tw, th, obj, cls要还原成像素坐标得走一遍解码加 NMS。下面这段是推理脚本的核心OpenCV 只负责读图和画框解码用 PyTorch 张量算import numpy as np import cv2 import torch def decode(pred, anchors, img_size416, conf_thres0.5, nms_thres0.4): # pred: [B, 3*(5C), H, W] B, _, H, W pred.shape pred pred.view(B, 3, 5 20, H, W).permute(0, 1, 3, 4, 2).contiguous() # 网格坐标 grid_y, grid_x torch.meshgrid(torch.arange(H), torch.arange(W)) grid torch.stack((grid_x, grid_y), 2).float().view(1, 1, H, W, 2) anchor_w torch.tensor([a[0] for a in anchors]).view(1, 3, 1, 1) anchor_h torch.tensor([a[1] for a in anchors]).view(1, 3, 1, 1) pred[..., 0:2] (torch.sigmoid(pred[..., 0:2]) grid) / W # 中心点归一化 pred[..., 2:4] torch.exp(pred[..., 2:4]) * torch.stack( (anchor_w, anchor_h), -1) / img_size pred[..., 4] torch.sigmoid(pred[..., 4]) # obj pred[..., 5:] torch.sigmoid(pred[..., 5:]) # 类别 # 过滤低置信度 pred pred.view(B, -1, 5 20) mask pred[..., 4] conf_thres pred pred[mask] if pred.numel() 0: return None # 转成 xyxy cx, cy, w, h pred[:, 0], pred[:, 1], pred[:, 2], pred[:, 3] x1, y1 cx - w / 2, cy - h / 2 x2, y2 cx w / 2, cy h / 2 scores, cls pred[:, 4], pred[:, 5:].argmax(1) boxes torch.stack([x1, y1, x2, y2], 1) * img_size # NMS keep torch.ops.torchvision.nms(boxes, scores, nms_thres) return boxes[keep], scores[keep], cls[keep]解码里sigmoid和exp是 YOLOv3 的固定公式tx, ty过 sigmoid 后加上网格偏移再除以特征图尺寸得到归一化中心点tw, th过 exp 后乘 anchor 尺寸再除以输入尺寸。conf_thres一般设 0.5nms_thres设 0.4这两个值调高会漏检、调低会重复框。torchvision.nms比手写快但要注意它要求 boxes 是xyxy格式且坐标是像素值别传归一化坐标进去。3.3 画框与保存结果拿到 boxes 后用 OpenCV 画出来注意坐标要按 letterbox 的缩放比例还原回原图def draw_and_save(img_path, boxes, scores, cls, out_path, r, pad): img cv2.imread(img_path) for box, s, c in zip(boxes, scores, cls): x1, y1, x2, y2 box.tolist() # 还原 letterbox 偏移 x1 (x1 - pad[0]) / r y1 (y1 - pad[1]) / r x2 (x2 - pad[0]) / r y2 (y2 - pad[1]) / r cv2.rectangle(img, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.putText(img, f{int(c)}:{s:.2f}, (int(x1), int(y1) - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imwrite(out_path, img)r和pad是 letterbox 时算出来的缩放比和填充偏移如果推理时没做 letterbox 而是直接 resize那r就是416/w0和416/h0两个值画框会变形。这也是很多人第一次跑检测发现框位置对不上的原因不是模型错了是坐标还原漏了填充。4. 训练、损失函数与参数调优的实操细节4.1 损失函数三件套框回归、置信度、类别YOLOv3 的损失分三部分框回归用 MSE原版或 CIoU改进版置信度和类别用 BCE。下面是一个常见实现import torch.nn.functional as F def yolo_loss(preds, targets, anchors, num_classes20): # preds: list of [B, 3*(5C), H, W] # targets: [B, N, 6] - [img_idx, cls, x, y, w, h] loss_box, loss_obj, loss_cls 0, 0, 0 for pred, anchor_set in zip(preds, [anchors[:3], anchors[3:]]): B, _, H, W pred.shape pred pred.view(B, 3, 5 num_classes, H, W).permute(0, 1, 3, 4, 2) obj_mask torch.zeros(B, 3, H, W, dtypetorch.bool) noobj_mask torch.ones(B, 3, H, W, dtypetorch.bool) tx torch.zeros(B, 3, H, W) ty torch.zeros(B, 3, H, W) tw torch.zeros(B, 3, H, W) th torch.zeros(B, 3, H, W) tcls torch.zeros(B, 3, H, W, num_classes) for b in range(B): for t in targets[b]: cls, x, y, w, h t[1:] gi, gj int(x * W), int(y * H) # 选与目标框 IoU 最大的 anchor ious [iou_wh(w, h, aw / 416, ah / 416) for aw, ah in anchor_set] a int(np.argmax(ious)) obj_mask[b, a, gj, gi] True noobj_mask[b, a, gj, gi] False tx[b, a, gj, gi] x * W - gi ty[b, a, gj, gi] y * H - gj tw[b, a, gj, gi] torch.log(w * 416 / anchor_set[a][0] 1e-16) th[b, a, gj, gi] torch.log(h * 416 / anchor_set[a][1] 1e-16) tcls[b, a, gj, gi, int(cls)] 1 # 框回归只算正样本 loss_box F.mse_loss(pred[..., 0:2][obj_mask], torch.stack((tx, ty), -1)[obj_mask]) loss_box F.mse_loss(pred[..., 2:4][obj_mask], torch.stack((tw, th), -1)[obj_mask]) # 置信度正负样本都算负样本权重降为 0.5 loss_obj F.binary_cross_entropy_with_logits( pred[..., 4][obj_mask], torch.ones_like(pred[..., 4][obj_mask])) loss_obj 0.5 * F.binary_cross_entropy_with_logits( pred[..., 4][noobj_mask], torch.zeros_like(pred[..., 4][noobj_mask])) # 类别只算正样本 loss_cls F.binary_cross_entropy_with_logits( pred[..., 5:][obj_mask], tcls[obj_mask]) return loss_box loss_obj loss_clsnoobj_mask的权重设 0.5 是为了压制背景样本数量远大于前景带来的梯度失衡这个值在 0.5 到 1.0 之间调设 1.0 容易让模型倾向于预测背景。tw, th用log是因为网络输出要过exp还原训练时就得取对数。anchor 匹配用 IoU 最大的那个不是所有 IoU 大于阈值的都算正样本这是 YOLOv3 和 v2 的区别之一。4.2 学习率、batch size 与 warmupYOLOv3-tiny 参数量小batch size 可以设大一点常见是 16 或 32。学习率用余弦退火加 warmup前 1000 步从 0 线性升到 1e-3之后按余弦降到 1e-5。优化器用 SGD 加 momentum 0.9 和 weight decay 5e-4比 Adam 收敛更稳。如果你只有 CPUbatch size 降到 4 也能跑但 BN 在 batch 太小时统计量不准建议把 BN 的 momentum 调到 0.01 让它更新慢一点。optimizer torch.optim.SGD(model.parameters(), lr1e-3, momentum0.9, weight_decay5e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max200) # warmup 手动实现 for epoch in range(epochs): for i, (imgs, targets) in enumerate(loader): if epoch 0 and i 1000: lr 1e-3 * (i / 1000) for pg in optimizer.param_groups: pg[lr] lr ... scheduler.step()warmup 的作用是防止一开始梯度太大把 BN 的 running mean 带偏尤其是随机初始化的主干。跳过 warmup 直接上 1e-3前几个 batch 的 loss 经常直接飙到 nan这是血泪经验。4.3 数据增强的取舍YOLOv3-tiny 容量小增强太猛反而学不动。常见做法是只开随机水平翻转、随机缩放0.8 到 1.2和 HSV 色调扰动 mosaic 和 mixup 这类强增强留给大模型。翻转时标签的x要变成1 - x别只翻图不翻标签否则模型会学到反向的框。缩放后如果框超出边界要么裁掉要么丢弃别保留越界框否则回归目标会异常。5. 避坑与常见问题排查5.1 现象loss 一直不降obj 损失卡在 0.6 左右原因通常是 anchor 尺寸和数据集不匹配或者标签格式写错了。YOLO 标签是归一化的x_center, y_center, w, h不是xmin, ymin, xmax, ymax如果你直接拿 VOC 的 XML 转过来没做转换框会全部挤在左上角。解决方法是写个脚本检查标签值是否都在 0 到 1 之间并用 k-means 重新聚 anchor聚类时用1 - IoU作为距离度量。5.2 现象推理时框位置整体偏移九成是 letterbox 的填充偏移没还原。训练时如果做了 letterbox推理也必须做同样的 letterbox画框时按(x - pad) / r还原。如果训练用直接 resize、推理用 letterbox或者反过来框就会系统性偏移。统一预处理方式是排查这类问题的第一步。5.3 现象CPU 推理一张图要好几秒YOLOv3-tiny 在 CPU 上正常应该 200 到 500 毫秒一张416 输入。如果慢到几秒检查是不是没设torch.set_num_threads或者模型还在 train 模式导致 BN 和 dropout 没关。推理前必须model.eval()并torch.no_grad()否则会构建计算图显存和内存都吃不消。5.4 现象NMS 后同一个目标出现多个框nms_thres设太高比如 0.7会让重叠框都留下设太低0.2会把相邻目标误删。0.4 到 0.5 是常用区间。另外注意 NMS 是按类别做的还是跨类别做的YOLO 默认按类别分别 NMS如果你把所有类别混在一起做不同类别的重叠框会互相压制。5.5 现象训练到一半 loss 变 nan学习率太大或者梯度爆炸。先加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), 10)再把学习率降一个数量级。如果用了exp解码tw, th的 log 里要加1e-16防止 log(0)。BN 层在 batch size 小于 2 时也会出问题至少保证 batch 为 4。6. 把模型导出 ONNX 并用 OpenCV DNN 加速推理训练完的 PyTorch 模型直接推理依赖 torch部署到没有 Python 环境的机器上不方便。常见做法是导出 ONNX再用 OpenCV 的dnn模块加载这样只需要一个 OpenCV 就能跑C 和 Python 都能调。导出时注意输入输出名字和动态轴import torch model.eval() dummy torch.randn(1, 3, 416, 416) torch.onnx.export( model, dummy, yolov3_tiny.onnx, input_names[input], output_names[out1, out2], dynamic_axes{input: {0: batch}, out1: {0: batch}, out2: {0: batch}}, opset_version11 )opset_version用 11 是因为 OpenCV 4.x 对 11 支持最稳用 12 以上可能遇到不支持的算子。导出后可以用onnxsim简化一下去掉多余的 transpose 和 reshape。用 OpenCV 加载时import cv2 net cv2.dnn.readNetFromONNX(yolov3_tiny.onnx) blob cv2.dnn.blobFromImage(img, 1/255.0, (416, 416), swapRBTrue, cropFalse) net.setInput(blob) outs net.forward(net.getUnconnectedOutLayersNames())swapRBTrue是因为 OpenCV 读进来是 BGR而训练时用的是 RGB不换通道颜色会反。cropFalse表示不做裁剪配合 letterbox 使用。OpenCV DNN 在 CPU 上比原生 PyTorch 快大概 1.5 到 2 倍因为它做了算子融合。导出后一定要用同一张图对比 PyTorch 和 OpenCV 的输出数值差异在 1e-3 以内才算正常差太多说明某个算子导出有问题。我现在的习惯是每次改完网络结构先导出 ONNX 跑一遍单图确认输出 shape 和数值都对得上再开始训练。这个顺序能省掉大量「训练完才发现导出失败」的后悔药。从那以后我每次动检测头或 anchor 配置都强制走一遍「PyTorch 前向 → ONNX 导出 → OpenCV 加载 → 单图对比」的流程希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

在华为Atlas 300V上从零部署YOLOv5的实战记录 2026/9/26 13:18:36

在华为Atlas 300V上从零部署YOLOv5的实战记录

前阵子一个做安防项目的朋友给我打电话,说他们团队拿到一张华为Atlas 300V 24G的卡,想在这上面把已有的YOLOv5检测模型跑起来,结果在环境配置那一步就卡了三天。我问他卡在哪,他说网上资料零零散散,有的说这是推理卡&a…

阅读更多 →
MiMo-V2.6硬核拆解:强化学习工业级落地的系统工程实践 2026/9/26 13:18:29

MiMo-V2.6硬核拆解:强化学习工业级落地的系统工程实践

1. 这不是一篇“读论文”的笔记,而是一次对强化学习工程化边界的硬核拆解如果你最近刷技术社区,大概率已经看到过《MiMo-V2.6: The Hard Road to Scaling Up RL》这份报告的标题——它不像传统AI论文那样堆砌公式或炫技新架构,而是用近乎坦诚…

阅读更多 →
AI智能体协作与自动化:agency-agents、deer-flow、page-agent三大项目实战解析 2026/9/26 13:18:23

AI智能体协作与自动化:agency-agents、deer-flow、page-agent三大项目实战解析

1. 三个项目到底在解决什么问题先把结论摆在前面:agency-agents、deer-flow、page-agent这三个项目,本质上都在回答同一个问题——怎么让 AI 从“聊天玩具”变成“能干活的生产力工具”。但它们切入的角度完全不同,分别对应了三种真实存在的需…

阅读更多 →
ASP购物系统毕业设计全攻略:IIS部署、代码解析与答辩演示 2026/9/26 13:18:23

ASP购物系统毕业设计全攻略:IIS部署、代码解析与答辩演示

简介:面向计算机专业毕业生的ASP.NET Web购物系统毕业设计资料包,完整覆盖论文、源代码、开题报告、答辩PPT与操作说明,可满足毕业设计选题、系统开发和答辩展示的全程需求。压缩包共1124个文件,核心含384个asp程序文件、554个gif…

阅读更多 →
League Akari:基于LCU API的英雄联盟Windows本地化效率中枢 2026/9/26 13:18:23

League Akari:基于LCU API的英雄联盟Windows本地化效率中枢

1. 这不是插件,是英雄联盟玩家的本地化“操作系统”级工具League Akari 这个名字乍一听像某个新出的皮肤系列或者赛事代号,但如果你是连续打了五年以上排位、每天打开客户端前都要手动调三次分辨率、反复确认语音设置没被重置、为了解决“好友列表不刷新…

阅读更多 →
COSCon‘25十年之约:中国开源从社区聚会到基础设施的进化之路 2026/9/26 13:18:22

COSCon‘25十年之约:中国开源从社区聚会到基础设施的进化之路

1. 十年之约:COSCon‘25 为什么值得被记录1.1 这届年会的第一感受:从“小众聚会”到“基础设施级”话题COSCon 走到第十届,很多老人儿都有一种“孩子长大了”的感觉。我走进北京会场时,第一眼看到的是比往年更大的场地、更多的展台…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉