鼠标点击下载 项目源代码免费下载地址
<计算机视觉一> 使用标定工具标定自己的目标检测
<计算机视觉二> labelme标定的数据转换成yolo训练格式
<计算机视觉三> pytorch读取自己标定的数据集
<计算机视觉四> pytorch版yolov3网络搭建
<计算机视觉 五> 模型训练时候标签数据的变换
<计算机视觉 六> 深度学习目标检测模型的评估标准
<计算机视觉 七> 模型训练模块的代码
在标注好了数据集并且成功的将数据转换成pytorch框架可读的数据以后,我们需要一个网络模型用来训练这些数据。
以比较经典的yolov3网路为例,讲解下网络结构的的大体框架,yolo系列其实让人眼前一亮的目前而言就开山之作yolo1和精巧干练的yolov3至于后来变种yolov4主要特点在于使用一些数据增强的trick和csp网络结构,但是并没有多大的惊艳。yolov5的头部Focus网络和训练的时候调参技巧更加技巧化,以至于后来yolox并没有在v4和v5的版本上进行研究,而是采用yolov3作为一个baseline进行重新的设计和训练。
yolov3的算法有很多博主写的很详细了,本着不重复造轮子的思想这里就直接上代码讲解下pytorch实现的时候一些细节把,如果这里有看不懂的可以留言,后期会把整个项目代码上传GitHub
目前公认的网络组成是由于通用的骨干网络、颈部分支网络、和头部推理网络组成。
骨干网络重要发展:
LeNet 开山之作,用来学习下卷积神经网络和练习下代码实现还是不错的,结构简单清晰明了。
AlexNet 首次使用多GPU训练实现了CNN的工程落地。
VGGNet 惊艳的网络框架简单明了使用多层网络代替大尺寸网络。
GoogLeNet网络后来叫Inception系列取得不错的效果,但是由于网络结构过于复杂,实现过于复杂,但提出的网中网结构为后来的网络发展提供了一个研究方向。
ResNet全体起立何凯明大神的作品,简单暴力高效,至今被那创造性的思路折服,在pytorch框架中代码表现为符号 “+” 即特征层相加。
DenseNet从不同于ResNet的角度出发重复利用特征层,在pytorch中表现为代码torch.cat。
SeNet注意力机制,用的不多但是也是一个创新网络,优点是可以添加到现有的网络中,不过有时候效果并不那么显著还增加了计算量。
DarkNet53这个骨干网络没什么好讲的,就从创新来说一般,好像几乎yolo系列用的多。
transform系列 这个2020年开始火起来的有统一NLP和CNN的趋势吧,据说是个数据怪兽,样本少的话就别想了,从实际项目前期就几千张图片的场景来看,根本不够塞牙缝。
从此时开始骨干网络开始忘轻量化发展。比如mobileNet shuffleNet等后面有机会在介绍。
首先给出pytorch版本的Darknet53骨干网络实现:
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@File : backbone.py
@Time : 2021/08/18 13:57:28
@Author : XIA Yan
@Contact : 微信 lingyanlove
@Version : 0.1
@License : Apache License Version 2.0, January 2004
@Language: python3.8
@Desc : 一些骨干网络
'''
import torch
import torch.nn as nn
# ++++++++++++++++++++++++++++++++++++++++++++++++++++
# Darknet53 YoloV3 input ie. [batch, 3, 416, 416] #
# ++++++++++++++++++++++++++++++++++++++++++++++++++++
def cbl(in_channels, out_channels, *args, **kwdargs):
'''
@description:
YOLOv3 中CBL模块,即conv + batchNorm + LeakyRelU
@Args:
in_channel : 输入层的通道数量
out_channel : 输出层的通道数量
@Return:
返回经过CBL操作的网络
'''
return nn.Sequential(
nn.Conv2d(in_channels, out_channels, *args, **kwdargs),
nn.BatchNorm2d(out_channels),
nn.LeakyReLU(0.1, inplace = True)
)
class DarknetBlock(nn.Module):
'''
@description:
Darknet53网络的1X1,3X3,參差网络模块
@Args:
in_channels: 模块的通道输入数量.
@Return:
经过残差的网络
'''
def __init__(self, channels):
super().__init__()
out_channels = channels//2
self.conv1 = cbl(channels, out_channels, kernel_size = 1)
self.conv2 = cbl(out_channels, channels, kernel_size = 3, padding = 1)
def forward(self, x):
return self.conv2(self.conv1(x)) + x
class Darknet53Backbone(nn.Module):
'''
@description:
Darknet53的骨干网络使用的是CBL的YOLOV3的骨干网络
@Args:
layers:(list)骨干网中残差层块数量.
block : (nn.Module)骨干网
@Return:
out : tuple([[b, 256, 52, 52],[b, 256, 26, 26],[b, 256, 13, 13]])
'''
def __init__(self, layers = [1, 2, 8, 8, 4], block = DarknetBlock):
super().__init__()
self.channels = [] # 存放经过每个layers的通道[64, 128, 256, 512, 1024]
self.selected_layers = [2,3,4] # 选择最后3层用于后期的分类检测等操作
self.in_channels = 32 # 设置初始的通道为32
self.layers = nn.ModuleList() # 创建一个List用于存网络
# 输入图像[b, 3, 416, 416] -> [b, 32, 416, 416]
self._pre_conv = cbl(in_channels = 3, out_channels= 32, kernel_size = 3, padding = 1)
# 网络块操作
self._make_layer(block, channels = 32 * 1, num_blocks = layers[0])
self._make_layer(block, channels = 32 * 2, num_blocks = layers[1])
self._make_layer(block, channels = 32 * 4, num_blocks = layers[2])
self._make_layer(block, channels = 32 * 8, num_blocks = layers[3])
self._make_layer(block, channels = 32* 16, num_blocks = layers[4])
def _make_layer(self, block, channels, num_blocks, stride = 2):
'''
@description:
创建layers = [1, 2, 8, 8, 4]的层结构
@Args:
block :(nn.Module)网络块结构
channels :(int)输入block的通道数量
num_layuers:(int)block操作执行的次数
stride :(default = 2)下采样conv的stride值
@Return:
_make_layer函数本身没有返回,但函数会填充Darknet53Backbone的属性值
'''
layers_list = []
# 1 首先进行下采样操作,这里和论文一样使用conv替代pooling操作
downsample = cbl(self.in_channels, self.in_channels * 2,
kernel_size = 3, stride = stride, padding = 1)
layers_list.append(downsample)
# 2 下采样后的网络进行残差模块处理, 每次通道数量就要 * 2
self.in_channels = self.in_channels * 2
layers_list += [block(self.in_channels) for _ in range(num_blocks)]
# 将layers_list变成torch能识别的net结构后存入self.layers中
self.channels.append(self.in_channels)
self.layers.append(nn.Sequential(*layers_list))
def forward(self, x):
# 只返回Darknet的最后3个层conv
x = self._pre_conv(x)
out = []
for idx, layer in enumerate(self.layers):
x = layer(x)
if idx >=2:
out.append(x)
return tuple(out)
# ++++++++++++++++++++++++++++++++++++++++++++++++++++
# Darknet53 YoloV4 input ie.[batch, 3, 640, 640] #
# ++++++++++++++++++++++++++++++++++++++++++++++++++++
if __name__ == "__main__":
if torch.cuda.is_available():
torch.set_default_tensor_type('torch.cuda.FloatTensor')
input_img = torch.rand(1,3,416,416)
net = Darknet53Backbone()
print(f"net的通道:{net.channels}")
print(f"net的选择层:{net.selected_layers}")
predict = net(input_img)
for out in predict:
print(out.shape)
骨干网络的还是比较简单,可以打印出网络结构查看,下面是yolov3的头部网络和推理结构
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@File : yolov3.py
@Time : 2021/08/18 16:01:39
@Author : XIA Yan
@Contact : [email protected]
@Version : 0.1
@License : Apache License Version 2.0, January 2004
@Language: python3.8
@Desc : YOLO v3的推理头文件部分
'''
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
from .backbone import Darknet53Backbone, cbl
from tools.utils import build_targets
class PredictionModule(nn.Module):
'''
@description:
YOLO 网络的推理头网络
@Args:
num_classes: 分类数量
backbone: 骨干网络
@Return:
out1,out2,out3:3个网络输出层
'''
def __init__(self, num_classes = 80, backbone = Darknet53Backbone):
super().__init__()
self.layers = nn.ModuleList() # 用于存储3个DBL的网络
self.backbone = backbone()
self.net_channels = backbone().channels[2:] # 选择骨干网络的最后3层 [256, 512, 1024]
for idx, num_channels in enumerate(self.net_channels):
out_channels = num_channels # DBL输出的通道数量[256, 512, 1024]
if idx < 2:
num_channels += num_channels//2 # 进入DBL的网络层数为 [384, 768, 1024]
self._dbl(num_channels,out_channels)
self.conv1 = cbl(512, 256, kernel_size = 1)
self.conv2 = cbl(256, 128, kernel_size = 1)
self.layer_out13 = self._conv_out(self.net_channels[0], num_classes)
self.layer_out26 = self._conv_out(self.net_channels[1], num_classes)
self.layer_out52 = self._conv_out(self.net_channels[2], num_classes)
def _dbl(self, in_channels,out_channels):
'''
@description:
YOLO v3推理头的 DBL网络结构 由多个(1×1 conv + 3×3 conv)的模块组成
@Args:
in_channels :(int)输入cbl的网络通道
@Return:
None
'''
layers_list = []
pre_channels = in_channels
temp_channels = out_channels//2
conv1x1_pre = cbl(pre_channels, temp_channels, kernel_size = 1)
conv3x3_1 = cbl(temp_channels, out_channels, kernel_size = 3, padding = 1)
conv1x1_2 = cbl(out_channels, temp_channels, kernel_size = 1)
conv3x3_2 = cbl(temp_channels, out_channels, kernel_size = 3, padding = 1)
conv1x1_3 = cbl(out_channels, temp_channels, kernel_size = 1)
layers_list.append(conv1x1_pre)
layers_list.append(conv3x3_1)
layers_list.append(conv1x1_2)
layers_list.append(conv3x3_2)
layers_list.append(conv1x1_3)
self.layers.append( nn.Sequential(*layers_list) )
def _conv_out(self, in_channels, num_classes):
'''
@description:
yolo层的输出 经过一个3×3的CBL操作后经过nn.Conv2d输出结果
@Args:
in_channels :(int)输入的通道数量
@Return:
返回网络输出层
'''
return nn.Sequential(
cbl(in_channels//2, in_channels, kernel_size = 3, padding = 1),
nn.Conv2d(in_channels, 3 * (5 + num_classes),kernel_size= 1)
)
def forward(self, x):
# x shapes[1,3,416,416]
# torch.Size([1, 256, 52, 52]) # 0
# torch.Size([1, 512, 26, 26]) # 1
# torch.Size([1, 1024, 13, 13]) # 2
outs = self.backbone(x)
x = self.layers[2](outs[2])
out1 = self.layer_out52(x)
x = F.interpolate(self.conv1(x), scale_factor = 2) # 上采样层
x = torch.cat((outs[1], x), 1) # 在第2个通道链接层
x = self.layers[1](x)
out2 = self.layer_out26(x)
x = F.interpolate(self.conv2(x), scale_factor = 2) # 上采样层
x = torch.cat((outs[0], x), 1) # 在第2个通道链接层
x = self.layers[0](x)
out3 = self.layer_out13(x)
return out1,out2,out3
class YoloLayer(nn.Module):
'''
@description:
依据,class的数量和anchor,处理PredictionModule的3个层
@Args:
num_classes:(int)class的数量,coco默认80个
@Return:
out_put, total_loss, self.metrics
'''
def __init__(self, anchors, num_classes = 80,img_dim = 416):
super().__init__()
self.anchors = anchors # 获取当前层对应的anchors
self.num_classes = num_classes
self.obj_scale = 1 # 正负样本中,正样本的比例
self.noobj_scale = 100 # 正负样本中,负样本的比例
self.smoothl1 = nn.SmoothL1Loss() # bbox中xywh的损失函数
self.bce_loss = nn.BCELoss() # 分类损失和有无目标的损失函数
self.metrics = dict() # 用list存放3层训练的评估
self.img_dim = img_dim # 训练图像的维度
self.grid_size = 0 # 用于比较当前层的grid尺寸如果相同就不用重复计算后面修改后的anchor
def compute_grid_offsets(self, grid_size, device):
'''
@description:
根据Darnet53网络层的grid尺寸计算原anchor应该的缩放比例
@Args:
grid_size :(int) yolo头推理层的gird尺寸 416输入的情况分别是13,26,52
device :(string)forward中输入的x设备名称,为了统一在一个设备上进行操作
@Return:
None
'''
self.grid_size = grid_size
self.stride = self.img_dim / self.grid_size # 计算例如 416/grid 获取缩放比例
# 使用meshgrid生成当前层的gird网格,并且存放在和forward中x相同的设备
self.grid_y, self.grid_x = torch.meshgrid(torch.arange(grid_size).to(device),
torch.arange(grid_size).to(device))
# 修改原始层的anchor尺寸,并且存放在和forward中x相同的设备
self.scaled_anchors = (self.anchors / self.stride).to(device)
self.anchor_w = self.scaled_anchors[:, 0:1].reshape((1, 3, 1, 1))
self.anchor_h = self.scaled_anchors[:, 1:2].reshape((1, 3, 1, 1))
def forward(self, x, targets = None):
'''
@description:
网络还是训练的数据入口,targets为None的时候就是推理,否则为训练
@Args:
x: (tensor[batch, 3*(5 + num_class), 13, 13]) PredictionModule网络的最后一层其中1个
targets:(tensor[batch_id, label_id, x,y,w,h])
数据类似,表示有4个batch图像其中 batch 0 有2个目标,且xywh是归一化后的数据
[ [0, 1, 0.15, 0.06, 0.35, 0.26],
[0, 2, 0.21, 0.16, 0.86, 0.36],
[1, 0, 0.41, 0.05, 0.25, 0.26],
[2, 1, 0.15, 0.26, 0.36, 0.21],
[3, 3, 0.15, 0.60, 0.34, 0.86],]
@Return:
out_put, total_loss, self.metrics
'''
# 1 获取当前输入x的相关信息 并转网络shape结构
batch = x.size(0) # 获取batch数量
grid_size = x.size(-1) # 获取当前层的网格尺寸
device = x.device # 获取x的设备
# prediction shapes[b,3+num_cls,grid_size,grid_size] -> [b,3,grid_size,grid_size,5+num_cls]
prediction = (x.view(batch, 3, 5 + self.num_classes, grid_size, grid_size)).permute(0, 1, 3, 4, 2).contiguous()
# 2 获取输出
x = torch.sigmoid(prediction[..., 0]) # center x
y = torch.sigmoid(prediction[..., 1]) # center y
w = prediction[..., 2] # 预测的bbox的宽度
h = prediction[..., 3] # 预测的bbox的高度
pred_conf = torch.sigmoid(prediction[..., 4]) # 有无目标conf的预测
pred_cls = torch.sigmoid(prediction[..., 5:]) # 每个格子预测的类别分类
# 3 依据当前gird修改 anchor尺寸
# if self.grid_size != grid_size:
self.compute_grid_offsets(grid_size, device)
# 4 计算box框的偏移,并且融合结果输出
# BUG 即使不用torch.cuda.FloatTensor 使用下面的new依然触发使用cuda:0的bug
# pred_boxes = prediction.new(prediction[..., :4].shape)
pred_boxes = torch.FloatTensor(prediction[..., :4].shape).to(device) # 创建数据格式存放修正后的box
pred_boxes[..., 0] = x.detach() + self.grid_x
pred_boxes[..., 1] = y.detach() + self.grid_y
pred_boxes[..., 2] = torch.exp(w.detach()) * self.anchor_w
pred_boxes[..., 3] = torch.exp(h.detach()) * self.anchor_h
out_put = torch.cat(
(pred_boxes.reshape(batch, -1, 4) * self.stride, # 将box坐标缩放回到基于416的图尺寸
pred_conf.reshape(batch, -1, 1),
pred_cls.reshape(batch, -1, self.num_classes)), dim = -1)
if targets is None: # 推理
return out_put, 0, self.metrics
else: # 训练
iou_scores, class_mask, obj_mask, noobj_mask, tx, ty, tw, th, tcls = build_targets(
pred_boxes = pred_boxes,
pre_cls = pred_cls,
target = targets,
anchors = self.scaled_anchors,
)
tconf = obj_mask.float()
# 依据mask填充计算loss
loss_x = self.smoothl1(x[obj_mask], tx[obj_mask])
loss_y = self.smoothl1(y[obj_mask], ty[obj_mask])
loss_w = self.smoothl1(w[obj_mask], tw[obj_mask])
loss_h = self.smoothl1(h[obj_mask], th[obj_mask])
loss_obj = self.bce_loss(pred_conf[obj_mask], tconf[obj_mask])
loss_noobj = self.bce_loss(pred_conf[noobj_mask], tconf[noobj_mask])
loss_conf = self.obj_scale * loss_obj + self.noobj_scale * loss_noobj
loss_cls = self.bce_loss(pred_cls[obj_mask], tcls[obj_mask])
total_loss = loss_x + loss_y + loss_w + loss_h + loss_conf + loss_cls
# 收集每层的评价数据Metrics
cls_acc = class_mask[obj_mask].mean() * 100 # 计算当前层的精度
conf_obj = pred_conf[obj_mask].mean() # 预测中有目标的位置的均值
conf_noobj = pred_conf[noobj_mask].mean() # 预测中无目标的位置的均值
conf50 = (pred_conf > 0.5).float() # 预测有无目标的pred_conf > 0.5的位置
iou50 = (iou_scores > 0.5).float() # iou_scores > 0.5的位置
iou75 = (iou_scores > 0.75).float() # iou_scores > 0.75的位置
detected_mask = conf50 * class_mask * tconf # 预测有目标大于0.5位置、class分类对的位置、真实有目标位置的mask
# precision是从预测数据出发,recall是从标记数据出发的统计
# precision 预测iou>0.5,conf>0.5,class分类正确,obj_mask正目标都满足 / 预测conf>0.5的所有位置比例
# recall50 预测iou>0.5,conf>0.5,class分类正确,obj_mask正目标都满足 / obj_mask正目标数量
precision = torch.sum(iou50 * detected_mask) / (conf50.sum() + 1e-16)
recall50 = torch.sum(iou50 * detected_mask) / (obj_mask.sum() + 1e-16)
recall75 = torch.sum(iou75 * detected_mask) / (obj_mask.sum() + 1e-16)
self.metrics = {
'loss':total_loss.detach().cpu().item(),
'x_loss':loss_x.detach().cpu().item(),
'y_loss':loss_y.detach().cpu().item(),
'w_loss':loss_w.detach().cpu().item(),
'h_loss':loss_h.detach().cpu().item(),
'conf_loss':loss_conf.detach().cpu().item(),
'class_loss':loss_cls.detach().cpu().item(),
'class_acc':cls_acc.detach().cpu().item(),
'recall50':recall50.detach().cpu().item(),
'recall75':recall75.detach().cpu().item(),
'precision':precision.detach().cpu().item(),
'conf_obj':conf_obj.detach().cpu().item(),
'conf_noobj':conf_noobj.detach().cpu().item(),
'grid_size':grid_size,
}
return out_put, total_loss, self.metrics
class YoloV3(nn.Module):
'''
@description:
YOLO V3的最后网络层,可以用于推理和训练
@Args:
num_classes :(int) 网络的类别创建的时候80区的是coco数据数量
predictnet :(nn.Module)头部网路
@Return:
'''
def __init__(self, num_classes = 80, img_dim = 416, predictnet = PredictionModule, yololayer = YoloLayer):
super().__init__()
self.anchors = torch.tensor([ 10.,13., 16.,30., 33.,23.,
30.,61., 62.,45., 59.,119.,
116.,90., 156.,198., 373.,326.]).reshape(3,3,2)
self.yolo_layers = []
self.predictnet = predictnet(num_classes) # Darknet最后的输出层是3个layer
self.yololayer1 = yololayer(self.anchors[0],num_classes, img_dim) # yolo推理层1 对应[13,13]
self.yololayer2 = yololayer(self.anchors[1],num_classes, img_dim) # yolo推理层1 对应[26,26]
self.yololayer3 = yololayer(self.anchors[2],num_classes, img_dim) # yolo推理层1 对应[52,52]
def forward(self, x, targets = None):
out1, out2, out3 = self.predictnet(x)
yolo_out1, loss1, metrics1 = self.yololayer1(out1, targets)
yolo_out2, loss2, metrics2 = self.yololayer2(out2, targets)
yolo_out3, loss3, metrics3 = self.yololayer3(out3, targets)
loss = loss1 + loss2 + loss3
yolo_outputs = torch.cat((yolo_out1, yolo_out2, yolo_out3),dim = 1).detach().cpu()
self.yolo_layers = [metrics1, metrics2, metrics3]
return yolo_outputs if targets is None else (loss, yolo_outputs)
#----保存和加载模型 ---
def save_darknet_weights(self, path):
'''保存yolo的网络参数'''
print(f"正在保存网络结构到{path}中.........")
torch.save(self.state_dict(),path) # 这里不保存网络结构
print("保存完毕!")
def load_checkpoint(self,path):
'''加载网络权重参数'''
state_dict = torch.load(path)
self.load_state_dict(state_dict)
本人整理了自己代码中的关于网络结构和实现的流程逻辑图
本图说明了骨干网络Darknet各层的输出后如果经过yolo推理头网络进行特征提取,yolov3使用3个特征层,每个层有3个anchor box (后来的free anchor已经不需预设框,在部署的时候也方便许多)。小特征预测大目标,大特征预测小目标。
下一章介绍如何之前的pytorch Dataset模块产生的数据具体如何生成上图能够训练的数据。
鼠标点击下载 项目源代码免费下载地址
<计算机视觉一> 使用标定工具标定自己的目标检测
<计算机视觉二> labelme标定的数据转换成yolo训练格式
<计算机视觉三> pytorch读取自己标定的数据集
<计算机视觉四> pytorch版yolov3网络搭建
<计算机视觉 五> 模型训练时候标签数据的变换
<计算机视觉 六> 深度学习目标检测模型的评估标准
<计算机视觉 七> 模型训练模块的代码