<计算机视觉四> pytorch版yolov3网络搭建

 鼠标点击下载     项目源代码免费下载地址

<计算机视觉一> 使用标定工具标定自己的目标检测

<计算机视觉二> 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网络搭建

<计算机视觉 五> 模型训练时候标签数据的变换

<计算机视觉 六> 深度学习目标检测模型的评估标准

<计算机视觉 七> 模型训练模块的代码

你可能感兴趣的:(计算机视觉,计算机视觉,pytorch,深度学习)