YOLOV5解析

网络由三个主要组件组成:
1)Backbone:在不同图像细粒度上聚合并形成图像特征的卷积神经网络。
2)Neck:一系列混合和组合图像特征的网络层,并将图像特征传递到预测层。
3)output:对图像特征进行预测,生成边界框和并预测类别。
对于YOLOV5,无论是V5s,V5m,V5l还是V5x其Backbone,Neck和output一致。唯一的区别在与模型的深度和宽度设置。
总结构框架:
在这里插入图片描述
下面逐一解析:
1)Backbone
先代码,有个大概脉络:

# YOLOv5 backbone
backbone:
  # [from, number, module, args]
  [[-1, 1, Focus, [64, 3]],  # 0-P1/2
   [-1, 1, Conv, [128, 3, 2]],  # 1-P2/4
   [-1, 3, BottleneckCSP, [128]],
   [-1, 1, Conv, [256, 3, 2]],  # 3-P3/8
   [-1, 9, BottleneckCSP, [256]],
   [-1, 1, Conv, [512, 3, 2]],  # 5-P4/16
   [-1, 9, BottleneckCSP, [512]],
   [-1, 1, Conv, [1024, 3, 2]],  # 7-P5/32
   [-1, 1, SPP, [1024, [5, 9, 13]]],
   [-1, 3, BottleneckCSP, [1024, False]],  # 9
  ]

①Backbone第一层focus,从高分辨率图像中,周期性的抽出像素点重构到低分辨率图像中,即将图像相邻的四个位置进行堆叠,聚焦wh维度信息到c通道空间,提高每个点感受野,并减少原始信息的丢失,该模块的设计主要是减少计算量加快速度。
作者原话:Focus() module is designed for FLOPS reduction and speed increase, not mAP increase.

class Focus(nn.Module):
    # Focus wh information into c-space
    def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True):  # ch_in, ch_out, kernel, stride, padding, groups
        super(Focus, self).__init__()
        self.conv = Conv(c1 * 4, c2, k, s, p, g, act)

    def forward(self, x):  # x(b,c,w,h) -> y(b,4c,w/2,h/2)
        return self.conv(torch.cat([x[..., ::2, ::2], x[..., 1::2, ::2], x[..., ::2, 1::2], x[..., 1::2, 1::2]], 1))

在这里插入图片描述
在YOLOv5上的数据流向是:
YOLO V5默认3x640x640的输入,先复制四份,然后通过切片操作将这个四个图片切成了四个3x320x320的切片,接下来使用concat从深度上连接这四个切片,输出为12x320x320,之后再通过卷积核数为64的卷积层,生成64x320x320的输出,最后经过batch_borm 和leaky_relu将结果输入到下一个卷积层。
② Backbone第三层,BottleneckCSP模块。
BottleneckCSP模块主要包括Bottleneck和CSP两部分。

class BottleneckCSP(nn.Module):
    # CSP Bottleneck https://github.com/WongKinYiu/CrossStagePartialNetworks
    def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):  # ch_in, ch_out, number, shortcut, groups, expansion
        super(BottleneckCSP, self).__init__()
        c_ = int(c2 * e)  # hidden channels
        self.cv1 = Conv(c1, c_, 1, 1)
        self.cv2 = nn.Conv2d(c1, c_, 1, 1, bias=False)
        self.cv3 = nn.Conv2d(c_, c_, 1, 1, bias=False)
        self.cv4 = Conv(2 * c_, c2, 1, 1)
        self.bn = nn.BatchNorm2d(2 * c_)  # applied to cat(cv2, cv3)
        self.act = nn.LeakyReLU(0.1, inplace=True)
        self.m = nn.Sequential(*[Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)])

    def forward(self, x):
        y1 = self.cv3(self.m(self.cv1(x)))
        y2 = self.cv2(x)
        return self.cv4(self.act(self.bn(torch.cat((y1, y2), dim=1))))

在这里插入图片描述
其中,在yolov5的模型配置文件中,有的BottleneckCSP模块配置存在False,而有的没有,如下:
[-1, 9, BottleneckCSP, [512]],

[-1, 3, BottleneckCSP, [512, False]], # 13

这里的False表示在这个BottleneckCSP中,不进行shortcut操作。
比如:
下图左图是无False的BottleneckCSP,右图是有False的BottleneckCSP。
在这里插入图片描述
④ SPP模块(空间金字塔池化模块), 分别采用5/9/13的最大池化,再进行concat融合,提高感受野。
SPP的输入是512x20x20,经过1x1的卷积层后输出256x20x20,然后经过并列的三个Maxpool进行下采样,将结果与其初始特征相加,输出1024x20x20,最后用512的卷积核将其恢复到512x20x20
在这里插入图片描述

2)Neck(PANet)
PANET基于 Mask R-CNN 和 FPN 框架,加强了信息传播,具有准确保留空间信息的能力,这有助于对像素进行适当的定位以形成掩模。
3)loss函数
边框回归:CIOU (GIOU的一种改进)
Objectness:GIOU
在这里插入图片描述
IOU:交并比
GIOU公式意思:先计算两个框的最小闭包区域面积Ac (通俗理解:同时包含了预测框和真实框的最小框的面积),再计算出IoU,再计算闭包区域Ac中不属于两个框的区域占闭包区域的比重,最后用IoU减去这个比重得到GIoU。
作为损失函数即:
在这里插入图片描述
分类:BCE(交叉熵损失)
损失平衡:ciou=0.05,giou=1, bce=0.5
训练
1;环境
ubuntu ,python3.8, torch 1.6, torchvision 0.7
2;送到网络中训练的数据格式
在这里插入图片描述
每行5个浮点型数据,第一个为标签序号,第二个和第三个为目标归一化后的中心点坐标,第四个和第五个表示目标归一化后的长宽。
3:配置文件中参数说明

train_path: ./source_data/traindata  #训练数据集
val_path: ./source_data/valdata   #测试数据集

convertor_path: ./convertor/chouyan   #转换成训练模型所需要的的数据格式保存路径

task_name: chouyan_s    #任务名称,最后在这个文件夹下保存生成的模型

names: ["xiangyan"]    #类别名称


gpu_ids: "2"
imgsz: 416
epochs: 50     #训练总共跑50个epoch
batch_size: 4
eval_interval: 5    #每5个epoch保存一次模型

weights: ./weights/yolov5s.pt     #预训练模型

#weights: /data/dj/yolov5/work_dir/chouyan_s/2020-11-14/2020-11-14_15:17:32/epoch_15.pth   # 在测试的时候,指定#测试的模型

source: ./test_data/chouyan2/neg   #测试的数据集路径,只需要图像

output_pos: output_s_pos_neg    #测试结果保存的路径,可根据自己需要保存

3:训练程序中需要注意的地方
1)配置参数

if __name__ == '__main__':
	parser = argparse.ArgumentParser()
	parser.add_argument('--cfg', type=str, default='models/yolov5s.yaml', help='model.yaml path') #如果预训练模型是#yolov5s,这里需要保持一致,最终训练的模型就是yolov5s版本
	parser.add_argument('--data', type=str, default='config.yaml', help='data.yaml path')  # 配置文件名称
	parser.add_argument('--hyp', type=str, default='', help='hyp.yaml path (optional)')
	parser.add_argument('--epochs', type=int, default=300)
	parser.add_argument('--batch-size', type=int, default=16, help="Total batch size for all gpus.")
	parser.add_argument('--img-size', nargs='+', type=int, default=[416, 416], help='train,test sizes')  #图像resize到的尺寸,可根据自己实际任务需求,改成640等,需要32的倍数
	parser.add_argument('--rect', action='store_true', help='rectangular training')
	parser.add_argument('--resume', nargs='?', const='get_last', default=False,
						help='resume from given path/to/last.pt, or most recent run if blank.')
	parser.add_argument('--nosave', action='store_true', help='only save final checkpoint')
	parser.add_argument('--notest', action='store_true', help='only test final epoch')
	parser.add_argument('--noautoanchor', action='store_true', help='disable autoanchor check')
	parser.add_argument('--evolve', action='store_true', help='evolve hyperparameters')
	parser.add_argument('--bucket', type=str, default='', help='gsutil bucket')
	parser.add_argument('--cache-images', action='store_true', help='cache images for faster training')
	parser.add_argument('--weights', type=str, default='', help='initial weights path')
	parser.add_argument('--name', default='', help='renames results.txt to results_name.txt if supplied')
	parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
	parser.add_argument('--multi-scale', action='store_true', help='vary img-size +/- 50%%')
	parser.add_argument('--single-cls', action='store_true', help='train as single-class dataset')
	parser.add_argument('--sync-bn', action='store_true', help='use SyncBatchNorm, only available in DDP mode')
	parser.add_argument('--local_rank', type=int, default=-1, help='DDP parameter, do not modify')
	opt = parser.parse_args()

2)模型存放路径

def init_logger(work_dir='./work_dir'):    #在ubuntu下训练一定不要写成work_dir='.\\work_dir',不然会找不到这个路径
	cur_time = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S")
	work_dir = os.path.join(work_dir, cur_time.split('_')[0], cur_time)

	mkdir_or_exist(os.path.abspath(work_dir))

	# log
	log_file = os.path.join(work_dir, 'log.log')
	logger = get_root_logger(log_file)
	return logger, work_dir

4,最后直接运行python train.py 就可直接训练。
测试
直接运行python detect.py 就可直接训练。
YOLOv5三种模型比较
在这里插入图片描述
训练代码梳理
一:加载训练配置文件

二:Create model
① 导入模型配置文件
self.yaml = yaml.load(f, Loader=yaml.FullLoader)
② 根据模型配置文件,定义模型
if nc and nc != self.yaml[‘nc’]:
self.yaml[‘nc’] = nc # override yaml value
self.model, self.save = parse_model(deepcopy(self.yaml), ch=[ch])

 from  n    params  module                                  arguments                     
  0                -1  1      8800  models.common.Focus                     [3, 80, 3]                    
  1                -1  1    115520  models.common.Conv                      [80, 160, 3, 2]               
  2                -1  1    315680  models.common.BottleneckCSP             [160, 160, 4]                 
  3                -1  1    461440  models.common.Conv                      [160, 320, 3, 2]              
  4                -1  1   3311680  models.common.BottleneckCSP             [320, 320, 12]                
  5                -1  1   1844480  models.common.Conv                      [320, 640, 3, 2]              
  6                -1  1  13228160  models.common.BottleneckCSP             [640, 640, 12]                
  7                -1  1   7375360  models.common.Conv                      [640, 1280, 3, 2]             
  8                -1  1   4099840  models.common.SPP                       [1280, 1280, [5, 9, 13]]      
  9                -1  1  20087040  models.common.BottleneckCSP             [1280, 1280, 4, False]        
 10                -1  1    820480  models.common.Conv                      [1280, 640, 1, 1]             
 11                -1  1         0  torch.nn.modules.upsampling.Upsample    [None, 2, 'nearest']          
 12           [-1, 6]  1         0  models.common.Concat                    [1]                           
 13                -1  1   5435520  models.common.BottleneckCSP             [1280, 640, 4, False]         
 14                -1  1    205440  models.common.Conv                      [640, 320, 1, 1]              
 15                -1  1         0  torch.nn.modules.upsampling.Upsample    [None, 2, 'nearest']          
 16           [-1, 4]  1         0  models.common.Concat                    [1]                           
 17                -1  1   1360960  models.common.BottleneckCSP             [640, 320, 4, False]          
 18                -1  1    922240  models.common.Conv                      [320, 320, 3, 2]              
 19          [-1, 14]  1         0  models.common.Concat                    [1]                           
 20                -1  1   5025920  models.common.BottleneckCSP             [640, 640, 4, False]          
 21                -1  1   3687680  models.common.Conv                      [640, 640, 3, 2]              
 22          [-1, 10]  1         0  models.common.Concat                    [1]                           
 23                -1  1  20087040  models.common.BottleneckCSP             [1280, 1280, 4, False]        
 24      [17, 20, 23]  1     40374  models.yolo.Detect                      [1, [[15.086, 10.896, 12.185, 27.484, 28.191, 13.015], [26.104, 23.376, 53.062, 15.104, 18.795, 51.715], [45.032, 25.534, 31.85, 40.063, 49.745, 47.505]], [320, 640, 1280]]

③ 获取前向输出
m = self.model[-1] # Detect()
从上面模型结构上就可以看到,yolov5把三个尺度的输出结果都存在最后一层,所以获取前向结果,直接取模型最后一层结果即可。
④ 将配置好的anchors根据[8,16,32]的尺度归一化。
m.anchors /= m.stride.view(-1, 1, 1) #m.stride=[8,16,32]
三:根据创建的模型结构,加载预训练模型参数
四:设置一些训练技巧:
①model, optimizer = amp.initialize(model, optimizer, opt_level=‘O1’, verbosity=0)
amp.initialize函数的作用不是提高模型精度或者训练速度,它是用来降低显存消耗。
主要看opt_level参数的配置,00相当于原始的单精度训练。01在大部分计算时采用半精度,但是所有的模型参数依然保持单精度,对于少数单精度较好的计算(如softmax)依然保持单精度。02相比于01,将模型参数也变为半精度。03基本等于最开始实验的全半精度的运算。值得一提的是,不论在优化过程中,模型是否采用半精度,保存下来的模型均为单精度模型,能够保证模型在其他应用中的正常使用。
② 学习率的调整方式
pytorch有3中调整学习率的策略:
1)有序调整:等间隔调整(Step),按需调整学习率(MultiStep),指数衰减调整(Exponential)和余弦退火CosineAnnealing
2)自适应调整: ReduceLROnPlateau
3)自定义调整: LambdaLR
③多GPU训练处理
model = torch.nn.DataParallel(model)
如果调用torch.nn.DataParallel,在设置GPU训练的时候,就一定需要设置2块或2块以上,如果只设置单卡训练就会报错。
④ 对预模型参数做平均操作,增加模型训练时的鲁棒性
ema = torch_utils.ModelEMA(model)
五:训练数据集和验证数据集处理
create_dataloader()
六:给模型设置一些参数
包括类别数量,类别名称,类别权重等
七:anchor设置
是否需要修改anchor:
check_anchors(dataset, model=model, thr=hyp[‘anchor_t’], imgsz=imgsz)
如果自己数据集anchor大小尺寸分布不像coco数据集那样丰富,比如:小目标检测,建议根据自己数据集去聚类一份新的anchor

# anchors:
#   - [10,13, 16,30, 33,23]  # P3/8
#   - [30,61, 62,45, 59,119]  # P4/16
#   - [116,90, 156,198, 373,326]  # P5/32
anchors:
  - [15.086,10.896,12.185,27.484,28.191,13.015]  # P3/8
  - [26.104,23.376,53.062,15.104,18.795,51.715]  # P4/16
  - [45.032,25.534,31.85,40.063,49.745,47.505]  # P5/32

直接将utils中的kmeans_anchor复制到本地,在本地运行即可。
kmean_anchors(path=‘E:\projects\YOLO5\data\chouyan.yaml’, n=9, img_size=416, thr=8.0, gen=1000, verbose=True)
按照coco.yaml新建配置一个xxxx.yaml文件,里面设置好训练集路径;n=9,表示聚类9个点,这个不要改,img_size训练集图像大小,thr训练集目标长宽比例,因为我需要训练的目标是小细长条的形状,便将coco中的thr=4.0改成了8.0,增大目标长宽比。
八:训练

for epoch in range(start_epoch, epochs):
	...
	for i, (imgs, targets, paths, _) in enumerate(dataloader):  #分批训练
		...
	imgs = imgs.to(device, non_blocking=True).float() / 255.0 #uint8 to float32
	...
	pred = model(imgs)  #前向处理
	loss, loss_items = compute_loss(pred, targets.to(device), model) #计算loss
	...
	results, correct,maps, times = test.test(...)  #验证集验证
	torch.save(...)  #保存模型

loss梳理
1)前向处理结果:
在这里插入图片描述
8,16,32三个尺度的输出
② 计算loss
1)tcls, tbox, indices, anchors = build_targets(p, targets, model)

def build_targets(p, targets, model):  #p:三个尺度的特征图; targets:标签信息,det.anchors:预测的anchors
    # Build targets for compute_loss(), input targets(image,class,x,y,w,h)
    det = model.module.model[-1] if type(model) in (nn.parallel.DataParallel, nn.parallel.DistributedDataParallel) \
        else model.model[-1]  # Detect() module
    na, nt = det.na, targets.shape[0]  # number of anchors, targets
    tcls, tbox, indices, anch = [], [], [], []
    gain = torch.ones(6, device=targets.device)  # normalized to gridspace gain #shape[6]
    off = torch.tensor([[1, 0], [0, 1], [-1, 0], [0, -1]], device=targets.device).float()  # overlap offsets #shape[4,2]
    at = torch.arange(na).view(na, 1).repeat(1, nt)  # anchor tensor, same as .repeat_interleave(nt)  #shape[3,1]

    g = 0.5  # offset
    style = 'rect4'
    for i in range(det.nl):
        anchors = det.anchors[i]   #分别获取每个尺度上的基于特征图大小的的anchor #shape[3,2]
        gain[2:] = torch.tensor(p[i].shape)[[3, 2, 3, 2]]  # xyxy gain   #shape[6]  获取每个尺度上预测的类别,已经类别置信度,目标cx,cy,w,h

        # Match targets to anchors
        a, t, offsets = [], targets * gain, 0  #将gt的cx,cy,w,h换算到当前特征层对应的尺寸,以便和该层的anchor大小相对应
        if nt:
            r = t[None, :, 4:6] / anchors[:, None]  # wh ratio #t_w,h/anchors
            j = torch.max(r, 1. / r).max(2)[0] < model.hyp['anchor_t']  # compare  #判断了r和1/r与model.hyp['anchor_t']的大小关系,返回bool值
            # j = wh_iou(anchors, t[:, 4:6]) > model.hyp['iou_t']  # iou(3,n) = wh_iou(anchors(3,2), gwh(n,2))
            a, t = at[j], t.repeat(na, 1, 1)[j]  # filter  #过滤掉长宽比大于阈值anchor_t的预测长宽组合

            # overlaps 
            gxy = t[:, 2:4]  # grid xy  #获取gt的cx,cy
            z = torch.zeros_like(gxy)
            if style == 'rect2':
                j, k = ((gxy % 1. < g) & (gxy > 1.)).T
                a, t = torch.cat((a, a[j], a[k]), 0), torch.cat((t, t[j], t[k]), 0)
                offsets = torch.cat((z, z[j] + off[0], z[k] + off[1]), 0) * g
            elif style == 'rect4':
                j, k = ((gxy % 1. < g) & (gxy > 1.)).T
                l, m = ((gxy % 1. > (1 - g)) & (gxy < (gain[[2, 3]] - 1.))).T
                a, t = torch.cat((a, a[j], a[k], a[l], a[m]), 0), torch.cat((t, t[j], t[k], t[l], t[m]), 0)
                offsets = torch.cat((z, z[j] + off[0], z[k] + off[1], z[l] + off[2], z[m] + off[3]), 0) * g
            # t.shape=[9,6]  即扩充了gt的数量,由原来3个anchor扩充到现在9个anchor,在每个gt中心点附近再扩充2个gt中心点
        # Define
        b, c = t[:, :2].long().T  # image, class
        gxy = t[:, 2:4]  # grid xy
        gwh = t[:, 4:6]  # grid wh
        gij = (gxy - offsets).long()
        gi, gj = gij.T  # grid xy indices

        # Append
        indices.append((b, a, gj, gi))  # image, anchor, grid indices
        tbox.append(torch.cat((gxy - gij, gwh), 1))  # box
        anch.append(anchors[a])  # anchors
        tcls.append(c)  # class

    return tcls, tbox, indices, anch

训练过程中踩的坑:
现象:梯度nan
可能的原因:权重衰减严重,权重系数小,导致梯度过小,超出pytorch加速神器Apex存储范围,导致舍入误差。
现象:loss一直高居不下,召回率也是一直很低
原因:初始学习率设置过大。
为什么学习率对模型收敛影响这么大? 首先先梳理一下网络参数更新过程。
Pytorch 模型参数更新过程:
1,通过网络前向输出与真实标签之间的误差,得到loss。
2,通过loss.backward()完成误差的反向传播,通过pytorch的内在机制完成自动求导得到每个参数的梯度W_grad.

if mixed_precision:
				with amp.scale_loss(loss, optimizer) as scaled_loss:
					scaled_loss.backward()
			else:
				loss.backward()

正常的程序,得到梯度之后,就启动优化算法,对权重进行更新。但是yolov5中,使用梯度累计的trick,即

if ni % accumulate == 0:
				optimizer.step()
				optimizer.zero_grad()

就是,在计算出梯度之后,不是马上启动优化算法,而是继续下一个batch过来计算loss,再在原来梯度基础上,计算这一个batch的梯度,也就实现了多个batch的梯度积累成一个梯度,根据这个累计的梯度,去对模型参数更新,也就完成了一次模型迭代。
3,通过优化算法,对模型参数更新。
①优化过程:
优化算法的通用的公式是:
W_data=W_data+W_grad*lr
其中W_data为模型参数,W_grad为参数梯度,lr为学习率。

optimizer = optim.SGD(pg0, lr=hyp['lr0'], momentum=hyp['momentum'], nesterov=True)

pytorch框架,一般的优化算法中,都会对模型参数进行L2正则化的权重衰减处理,经过权值衰减处理后,输出的权重会变小,目的是为了防止过拟合。

@torch.no_grad()
    def step(self, closure=None):
        """Performs a single optimization step.

        Arguments:
            closure (callable, optional): A closure that reevaluates the model
                and returns the loss.
        """
        loss = None
        if closure is not None:
            with torch.enable_grad():
                loss = closure()

        for group in self.param_groups:
            weight_decay = group['weight_decay']
            momentum = group['momentum']
            dampening = group['dampening']
            nesterov = group['nesterov']

            for p in group['params']:
                if p.grad is None:
                    continue
                d_p = p.grad
                if weight_decay != 0:
                    d_p = d_p.add(p, alpha=weight_decay)  #权值L2正则化衰减
                if momentum != 0:
                    param_state = self.state[p]
                    if 'momentum_buffer' not in param_state:
                        buf = param_state['momentum_buffer'] = torch.clone(d_p).detach()
                    else:
                        buf = param_state['momentum_buffer']
                        buf.mul_(momentum).add_(d_p, alpha=1 - dampening)
                    if nesterov:
                        d_p = d_p.add(buf, alpha=momentum)
                    else:
                        d_p = buf

                p.add_(d_p, alpha=-group['lr'])  #权值更新

        return loss

② 模型参数更新:

optimizer.step()

③ 梯度归零。

optimizer.zero_grad()

一次梯度更新一次模型,下一轮模型的更新,需要下一轮新算出来的梯度。
4,对更新的模型参数均值处理

ema = torch_utils.ModelEMA(model) if rank in [-1, 0] else None

ema.update(model)

针对学习率优化的几个方式:
① Warmup
可以使得开始训练的几个epoches或者一些steps内学习率较小,在预热的小学习率下,模型可以慢慢趋于稳定,等模型相对稳定后再选择预先设置的学习率进行训练,使得模型收敛速度变得更快,模型效果更佳。
有助于减缓模型在初始阶段对mini-batch的提前过拟合现象,保持分布的平稳
有助于保持模型深层的稳定性。

if ni <= nw:
				xi = [0, nw]  # x interp
				# model.gr = np.interp(ni, xi, [0.0, 1.0])  # giou loss ratio (obj_loss = 1.0 or giou)
				accumulate = max(1, np.interp(ni, xi, [1, nbs / total_batch_size]).round())
				for j, x in enumerate(optimizer.param_groups):
					# bias lr falls from 0.1 to lr0, all other lrs rise from 0.0 to lr0
					x['lr'] = np.interp(ni, xi, [0.1 if j == 2 else 0.0, x['initial_lr'] * lf(epoch)])
					if 'momentum' in x:
						x['momentum'] = np.interp(ni, xi, [0.9, hyp['momentum']])

② 学习率调整方案
Pytorch 提供的学习率调整策略分为三大类:
a. 有序调整:等间隔调整(Step),按需调整学习率(MultiStep),指数衰减调整(Exponential)和 余弦退火CosineAnnealing。
b. 自适应调整:自适应调整学习率 ReduceLROnPlateau。
c. 自定义调整:自定义调整学习率 LambdaLR。

猜你喜欢

转载自blog.csdn.net/jiafeier_555/article/details/109052569