以代码的思想去详细讲解yolov3算法的实现原理和训练过程,并教使用visdrone2019数据集和自己制作数据集两种方式去训练自己的pytorch搭建的yolov3模型,吐血整理万字长文,纯属干货 !
实现思路第一步:Pytorch搭建yolo3目标检测平台
模型yolov3和预训练权重下载 yolo3算法原理实现思路 一、预测部分 1、yolo3的网络模型架构和实现 2、主干特征网络darknet53介绍和结果(获取3个初始特征层) 3、从初始特征获取预测结果(最终的3个有效的特征层) 4、预测结果的解码(对最终的3个有效特征层的结果进行解码) 5、在原图上进行绘制(对解码的结果数据在原图绘制展现) 二、训练部分 1、计算loss所需参数 2、pred是什么 3、target是什么。 4、loss的计算过程 5、正式开始训练 第二步:使用Visdrone2019训练自己的模型yolov3模型 yolov3整体的文件夹结构 一、数据集准备 1.visdrone数据集训练 2.自己制作数据集训练 二、训练和效果展示 3.正式开始训练 4.训练效果 三、利用训练好了的模型进行预测01
yolo算法原理实现思路
模型yolov3和预训练权重下载,后台回复关键字:yolov3,获取。 一.预测部分 1.yolo3的网络模型结构如下:![775be699b0e36f0e4147d90484dc5b32.png](https://i-blog.csdnimg.cn/blog_migrate/1825a9a41e4381a1b52fceda18838de1.png)
![f257923abdd92d88d4c42d57699a90eb.png](https://i-blog.csdnimg.cn/blog_migrate/5f176dc266ea0a95e5bf90a5325b7806.png)
2.主干特征网络darknet53介绍
![c2c3cbdd2d26714cddafa7019ee20599.png](https://i-blog.csdnimg.cn/blog_migrate/cce78846a938071cdfbe43706684c0c5.png)
![bbdda55a3835638764e314763adf02ac.png](https://i-blog.csdnimg.cn/blog_migrate/ac95f0a69d0896e57415b94a7da49f77.png)
import torch
import torch.nn as nn
import math
from collections import OrderedDict
#Residual Block
class BasicBlock(nn.Module):
#初始化操作
def __init__(self, inplanes, planes):
super(BasicBlock, self).__init__()
self.conv1 = nn.Conv2d(inplanes, planes[0], kernel_size=1,
stride=1, padding=0, bias=False)
self.bn1 = nn.BatchNorm2d(planes[0])
self.relu1 = nn.LeakyReLU(0.1)
self.conv2 = nn.Conv2d(planes[0], planes[1], kernel_size=3,
stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(planes[1])
self.relu2 = nn.LeakyReLU(0.1)
#定义残差快
def forward(self, x):
residual = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu1(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu2(out)
out += residual
return out
#darknet53网络结构
class DarkNet(nn.Module):
def __init__(self, layers):
super(DarkNet, self).__init__()
self.inplanes = 32
self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=3, stride=1, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(self.inplanes)
self.relu1 = nn.LeakyReLU(0.1)
self.layer1 = self._make_layer([32, 64], layers[0])
self.layer2 = self._make_layer([64, 128], layers[1])
self.layer3 = self._make_layer([128, 256], layers[2])
self.layer4 = self._make_layer([256, 512], layers[3])
self.layer5 = self._make_layer([512, 1024], layers[4])
self.layers_out_filters = [64, 128, 256, 512, 1024]
# 进行权值初始化
for m in self.modules():
if isinstance(m, nn.Conv2d):
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
def _make_layer(self, planes, blocks):
layers = []
# 下采样,步长为2,卷积核大小为3
layers.append(("ds_conv", nn.Conv2d(self.inplanes, planes[1], kernel_size=3,
stride=2, padding=1, bias=False)))
layers.append(("ds_bn", nn.BatchNorm2d(planes[1])))
layers.append(("ds_relu", nn.LeakyReLU(0.1)))
# 加入darknet模块
self.inplanes = planes[1]
for i in range(0, blocks):
layers.append(("residual_{}".format(i), BasicBlock(self.inplanes, planes)))
return nn.Sequential(OrderedDict(layers))
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu1(x)
x = self.layer1(x)
x = self.layer2(x)
out3 = self.layer3(x)
out4 = self.layer4(out3)
out5 = self.layer5(out4)
return out3, out4, out5
def darknet53(pretrained, **kwargs):
model = DarkNet([1, 2, 8, 8, 4])
if pretrained:
if isinstance(pretrained, str):
model.load_state_dict(torch.load(pretrained))
else:
raise Exception("darknet request a pretrained path. got [{}]".format(pretrained))
return model
3、从初始特征获取预测结果![c2c3cbdd2d26714cddafa7019ee20599.png](https://i-blog.csdnimg.cn/blog_migrate/cce78846a938071cdfbe43706684c0c5.png)
如果使用的是coco训练集,类则为80种,最后的维度应该为255 = 3x85,三个特征层的shape为(13,13,255),(26,26,255),(52,52,255)。
其实际情况就是,由于我们使用得是Pytorch,它的通道数默认在第一位,输入N张416x416的图片,在经过多层的运算后,会输出三个shape分别为(N,255,13,13),(N,255,26,26),(N,255,52,52)的数据,对应每个图分为13x13、26x26、52x52的网格上3个先验框的位置。 实现代码如下: 详情请见:yolo3.py(定义yolo3的整个网络结构模型)import torch
import torch.nn as nn
from collections import OrderedDict
from nets.darknet import darknet53
def conv2d(filter_in, filter_out, kernel_size):
pad = (kernel_size - 1) // 2 if kernel_size else 0
return nn.Sequential(OrderedDict([
("conv", nn.Conv2d(filter_in, filter_out, kernel_size=kernel_size, stride=1, padding=pad, bias=False)),
("bn", nn.BatchNorm2d(filter_out)),
("relu", nn.LeakyReLU(0.1)),
]))
def make_last_layers(filters_list, in_filters, out_filter):
m = nn.ModuleList([
conv2d(in_filters, filters_list[0], 1),
conv2d(filters_list[0], filters_list[1], 3),
conv2d(filters_list[1], filters_list[0], 1),
conv2d(filters_list[0], filters_list[1], 3),
conv2d(filters_list[1], filters_list[0], 1),
conv2d(filters_list[0], filters_list[1], 3),
nn.Conv2d(filters_list[1], out_filter, kernel_size=1,
stride=1, padding=0, bias=True)
])
return m
class YoloBody(nn.Module):
def __init__(self, config):
super(YoloBody, self).__init__()
self.config = config
# backbone
self.backbone = darknet53(None) # darknert53用于提取初始特征
out_filters = self.backbone.layers_out_filters
# last_layer0
final_out_filter0 = len(config["yolo"]["anchors"][0]) * (5 + config["yolo"]["classes"])
self.last_layer0 = make_last_layers([512, 1024], out_filters[-1], final_out_filter0)
# embedding1
final_out_filter1 = len(config["yolo"]["anchors"][1]) * (5 + config["yolo"]["classes"])
self.last_layer1_conv = conv2d(512, 256, 1)
self.last_layer1_upsample = nn.Upsample(scale_factor=2, mode='nearest')
self.last_layer1 = make_last_layers([256, 512], out_filters[-2] + 256, final_out_filter1)
# embedding2
final_out_filter2 = len(config["yolo"]["anchors"][2]) * (5 + config["yolo"]["classes"])
self.last_layer2_conv = conv2d(256, 128, 1)
self.last_layer2_upsample = nn.Upsample(scale_factor=2, mode='nearest')
self.last_layer2 = make_last_layers([128, 256], out_filters[-3] + 128, final_out_filter2)
def forward(self, x):
def _branch(last_layer, layer_in):
for i, e in enumerate(last_layer):
layer_in = e(layer_in)
if i == 4:
out_branch = layer_in
return layer_in, out_branch
# backbone
x2, x1, x0 = self.backbone(x)
# yolo branch 0
out0, out0_branch = _branch(self.last_layer0, x0)
# yolo branch 1
x1_in = self.last_layer1_conv(out0_branch)
x1_in = self.last_layer1_upsample(x1_in)
x1_in = torch.cat([x1_in, x1], 1)
out1, out1_branch = _branch(self.last_layer1, x1_in)
# yolo branch 2
x2_in = self.last_layer2_conv(out1_branch)
x2_in = self.last_layer2_upsample(x2_in)
x2_in = torch.cat([x2_in, x2], 1)
out2, _ = _branch(self.last_layer2, x2_in)
return out0, out1, out2
4、预测结果的解码和最终预测框筛选
由第三步我们可以获得最终三个有效特征层的预测结果,shape分别为(N,255,13,13),(N,255,26,26),(N,255,52,52)的数据,对应每个图分为13x13、26x26、52x52的网格上3个预测框的位置。
但是这个预测结果并不对应着最终的预测框在图片上的位置,还需要解码才可以完成。我们利用yolov3的网络预测结果会对我们的预先设定好了的先验框进行调整,获得最终的预测框,对先验框进行调整的过程我们称作解码的过程。
总结:先验框解码的过程就是利用yolov3网络的预测结果(3个有效的特征层)对先验框进行调整的过程,调整完就是预测框。
此处要讲一下yolo3的预测原理,yolo3的3个特征层分别将整幅图分为13x13、26x26、52x52的网格,每个网络点负责一个区域的检测。
我们知道特征层的预测结果对应着三个预测框的位置,若是coco数据集,我们先将其reshape一下,其结果为(N,3,85,13,13,),(N,3,85,26,26),(N,3,85,52,52)。维度中的85包含了4+1+80,分别代表x_offset、y_offset、h和w、置信度、分类结果,如果是voc数据,则为25。
yolo3的具体解码过程:在代码中就是首先生成特征层大小的网格,然后将我们预先设置好了的在原图中416*416先验框的尺寸调整到有效特征层大小上,最后从yolov3的网络预测结果获得先验框的中心调整参数x_offset和y_offset和宽高的调整参数h和w,对在特征层尺寸大小上的先验框进行调整,将每个网格点加上它对应的x_offset和y_offset的结果就是调整后的先验框的中心,也就是预测框的中心,然后再利用先验框和h、w结合 计算出调整后的先验框的的长和宽,也就是预测框的高和宽,这样就能得到在特征层上整个预测框的位置了,最后我们将在有效特征层上的预测框的位置再调整到原图416*416的大小上。
以13*13有效特征层为例:左图是先验框在有效特征层调整的可视化,右图是在原图上绘制的调整后的先验框,即真实的预测框。![31c07c3ada916a50cf9ce3782874facc.png](https://i-blog.csdnimg.cn/blog_migrate/b499ad0a4265b55c63e668a0f33e5fd9.png)
![8e51dd86ff3dcba6d9764bcbb16c6a84.png](https://i-blog.csdnimg.cn/blog_migrate/356dd932b33373eeff7efd965d203d09.png)
class DecodeBox(nn.Module):
def __init__(self, anchors, num_classes, img_size):
super(DecodeBox, self).__init__()
self.anchors = anchors
self.num_anchors = len(anchors)
self.num_classes = num_classes
self.bbox_attrs = 5 + num_classes
self.img_size = img_size
def forward(self, input):
batch_size = input.size(0)
input_height = input.size(2)
input_width = input.size(3)
# 计算步长
stride_h = self.img_size[1] / input_height
stride_w = self.img_size[0] / input_width
# 归一到特征层上
scaled_anchors = [(anchor_width / stride_w, anchor_height / stride_h) for anchor_width, anchor_height in self.anchors]
# 对预测结果进行resize
prediction = input.view(batch_size, self.num_anchors,
self.bbox_attrs, input_height, input_width).permute(0, 1, 3, 4, 2).contiguous()
# 先验框的中心位置的调整参数
x = torch.sigmoid(prediction[..., 0])
y = torch.sigmoid(prediction[..., 1])
# 先验框的宽高调整参数
w = prediction[..., 2] # Width
h = prediction[..., 3] # Height
# 获得置信度,是否有物体
conf = torch.sigmoid(prediction[..., 4])
# 种类置信度
pred_cls = torch.sigmoid(prediction[..., 5:]) # Cls pred.
FloatTensor = torch.cuda.FloatTensor if x.is_cuda else torch.FloatTensor
LongTensor = torch.cuda.LongTensor if x.is_cuda else torch.LongTensor
# 生成网格,先验框中心,网格左上角
grid_x = torch.linspace(0, input_width - 1, input_width).repeat(input_width, 1).repeat(
batch_size * self.num_anchors, 1, 1).view(x.shape).type(FloatTensor)
grid_y = torch.linspace(0, input_height - 1, input_height).repeat(input_height, 1).t().repeat(
batch_size * self.num_anchors, 1, 1).view(y.shape).type(FloatTensor)
# 生成先验框的宽高
anchor_w = FloatTensor(scaled_anchors).index_select(1, LongTensor([0]))
anchor_h = FloatTensor(scaled_anchors).index_select(1, LongTensor([1]))
anchor_w = anchor_w.repeat(batch_size, 1).repeat(1, 1, input_height * input_width).view(w.shape)
anchor_h = anchor_h.repeat(batch_size, 1).repeat(1, 1, input_height * input_width).view(h.shape)
# 计算调整后的先验框中心与宽高
pred_boxes = FloatTensor(prediction[..., :4].shape)
pred_boxes[..., 0] = x.data + grid_x
pred_boxes[..., 1] = y.data + grid_y
pred_boxes[..., 2] = torch.exp(w.data) * anchor_w
pred_boxes[..., 3] = torch.exp(h.data) * anchor_h
# 用于将输出调整为相对于416x416的大小
_scale = torch.Tensor([stride_w, stride_h] * 2).type(FloatTensor)
output = torch.cat((pred_boxes.view(batch_size, -1, 4) * _scale,
conf.view(batch_size, -1, 1), pred_cls.view(batch_size, -1, self.num_classes)), -1)
return output.data
5、在原图上进行绘制
通过第四步,我们就可以获得预测框在原图上的位置,当然得到最终的预测结果后还要进行得分排序与非极大抑制筛选,因为右图我们可以看到,由于一个网格点有3个先验框,则调整后有3个预测框,在原图上绘制的时候,同一个目标就有3个预测框,那要找出最合适的预测框,我们需要进行筛选。 如下图举例:假设3个蓝色的是我们获得的预测框,黄色的是真实框,红色的是用与预测目标的网格,我们就需要对这检测同一个目标的网格点上的3个调整后的先验框(也就是预测框)进行筛选。![8e51dd86ff3dcba6d9764bcbb16c6a84.png](https://i-blog.csdnimg.cn/blog_migrate/356dd932b33373eeff7efd965d203d09.png)
二、训练部分
1、计算loss所需参数
在计算loss的时候,实际上是网络预测结果prediction和目标target之间的对比: prediction:就是你输入一张图片给yolov3网络模型最终的预测结果,也就是3个有效特征层,每一张图片最后都对应3个有效特征层。 target:就是你制作的训练集中标注图片中的数据信息,这是网络的真实框情况。2、prediction是什么
对于yolo3的模型来说,网络最后输出的内容就是三个有效特征层,3个有效特征层的每个网格点(特征点)对应着预测框及其种类,即三个特征层分别对应着图片被分为不同size的网格后,每个网格点上三个先验框对应的位置、置信度及其种类。 输出层的shape分别为 (13,13,75),(26,26,75),(52,52,75),最后一个维度为75是因为是基于voc数据集的,它的类为20种,yolo3的每一个特征层的每一个特征点(网格点)都预先设置3个先验框,每个先验框包含1+4=20个参数信息,1代表这个先验框内部是否有目标,4代表框的xywh参数信息,20代表框的种类信息,所以每一个特征点对应3*25参数, 即最后维度为3x25。 如果使用的是coco训练集,类则为80种,最后的维度应该为255 = 3x85,三个特征层的shape为(13,13,255),(26,26,255),(52,52,255) 注意:此处得到的yolov3的网络预测结果(3个有效特征层)y_prediction此时并没有解码,也就是yolov3.py中yolobody类的输出结果,有效特征层解码了之后才是真实图像上的情况。3、target是什么。
target就是一个真实图像中,真实框的情况。 第一个维度是batch_size,第二个维度是每一张图片里面真实框的数量,第三个维度内部是真实框的信息,包括位置以及种类。4、loss的计算过程
拿到pred和target后,不可以简单的减一下作为对比,需要进行如下步骤。
第一步:对yolov3网络的预测结果进行解码,获得网络预测结果对先验框的调整数据第二步:对真实框进行处理,获得网络应该真正有的对先验框的调整数据,也就是网络真正应该有的预测结果 ,然后和我们得到的网络的预测结果进行对比,代码中get_target函数
- 判断真实框在图片中的位置,判断其属于哪一个网格点去检测。
- 判断真实框和哪个预先设定的先验框重合程度最高。
- 计算该网格点应该有怎么样的预测结果才能获得真实框(利用真实框的数据去调整预先设定好了的先验框,得到真实框该网格点应该预测的先验框的调整数据)
- 对所有真实框进行如上处理。
获得网络应该有的预测结果,将其与yolov3预测实际的预测结果对比。
![1467a38190f11dc52f18b996e533e9d1.png](https://i-blog.csdnimg.cn/blog_migrate/a1af722eb88a9eb525e14d194216d9c3.png)
5.正式训练
正式训练:包括数据集的加载和预处理(图片的归一化、框的坐标格式的转换、图片的通道的改变、数据增强等)请见yolotrain.py中的 Generator类,预训练权重的导入、网路模型的正向传播和反向传播梯度下降,请见train.py。 yolov3预训练权重的下载,在后台回复关键字:yolov3,获取。01
训练自己的yolov3模型
yolo3整体的文件夹构架:![dc35c04d8c797b0551d98976aedb04ac.png](https://i-blog.csdnimg.cn/blog_migrate/3a9f74c928513719bd779e951227bfd8.png)
![d10f3c69c57d8ca5f0924ee30e74eca6.png](https://i-blog.csdnimg.cn/blog_migrate/16f252c225b70acdef7da7e877e56370.png)
![ebcb940d9ca6f904232a15f76e4fa63b.png](https://i-blog.csdnimg.cn/blog_migrate/c2b6946dddfe3582fdf8ee06bf104401.png)
![803066bb73c14692441177abed89b435.png](https://i-blog.csdnimg.cn/blog_migrate/d993696fd9e77a4530a1b4e123ac84a3.png)
![18ffaea75a9f8d70cb84366e70ac6067.png](https://i-blog.csdnimg.cn/blog_migrate/d2280f404e0d90202ad1f7f183a08a87.png)
![10d05a33cdd88489b01e494c643cbe46.png](https://i-blog.csdnimg.cn/blog_migrate/8bb5c6c0ba059dcf5bba5e0260ab7162.png)
![23b4383f15b1803d92b3d2e2c46731ad.png](https://i-blog.csdnimg.cn/blog_migrate/7917750ded516fabb7343085c6505338.png)
![7ebff85495567525d4e812d315aa95b0.png](https://i-blog.csdnimg.cn/blog_migrate/a486b003bf6017c719091d50dab12a1c.png)
![1e6f22f5b46b3e3ba7d23686d1df90a0.png](https://i-blog.csdnimg.cn/blog_migrate/907e0c802c989b081c40486bc1aa2b73.png)
3.预测效果
![9c7b6710364a1151cc1c2f6a462cc89d.png](https://i-blog.csdnimg.cn/blog_migrate/ffad2c547c74de752d003c6e73a283d7.png)
![ec564a9ad957c28bfa1f27e6bff44180.png](https://i-blog.csdnimg.cn/blog_migrate/8ec58ac4a8d562d7162b793dc5be6882.png)
推荐阅读
(点击标题可跳转阅读)
干货 | 公众号历史文章精选
我的深度学习入门路线
我的机器学习入门路线图
重磅!
AI有道年度技术文章电子版PDF来啦!
扫描下方二维码,添加 AI有道小助手微信,可申请入群,并获得2020完整技术文章合集PDF(一定要备注:入群 + 地点 + 学校/公司。例如:入群+上海+复旦。
长按扫码,申请入群
(添加人数较多,请耐心等待)
感谢你的分享,点赞,在看三连↓