yolov5源码分析_yolo.py

models/Yolo.py

Detect类

class Detect(nn.Module):
    stride = None  # strides computed during build
    onnx_dynamic = False  # ONNX export parameter
 
    def __init__(self, nc=80, anchors=(), ch=(), inplace=True):  # detection layer
        #yolov5中的anchors(3个,对应Neck出来的那3个输出),初始anchor是由w,h宽高组成,用的是原图的像素尺寸,设置为每层3个,所以共有3 * 3 = 9个
        super().__init__()
        self.nc = nc  # 预测的类的数量
        self.no = nc + 5  
        # 每一个预测框(anchor)输出的数量,对应每种类的置信度(nc),预测框的高宽,中心点坐标,预测框内是否有物体的置信度,共5种信息。
        self.nl = len(anchors)  # 预测层的数量
        self.na = len(anchors[0]) // 2  #预测框的数量
        self.grid = [torch.zeros(1)] * self.nl  # 初始网格,对于每个预测层都有初始网格的生成
        self.anchor_grid = [torch.zeros(1)] * self.nl  # 初始预测框网格
        self.register_buffer('anchors', torch.tensor(anchors).float().view(self.nl, -1, 2))  
        # shape(nl,na,2)
        self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch)  
        # output conv,输出结果:每个预测框的输出结果 * 预测框个数
        self.inplace = inplace  # use in-place ops (e.g. slice assignment)
 
    def forward(self, x):
        z = []  # inference output
        for i in range(self.nl): #每层循环着处理
            x[i] = self.m[i](x[i])  # conv卷积
            bs, _, ny, nx = x[i].shape  
            #bs第几个预测层的意思吧
            x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
            #view()变换形状,数据不变, x(bs,255,20,20) to x(bs,3,85,20,20),将一个预测层里的3个anchor的信息分出来,每个预测框预测信息数量为self.no(这里为85)
            #permute(0, 1, 3, 4, 2),x[i]有5个维度,(2,3,4)变成(3,4,2),x(bs,3,85,20,20)to x(bs,3,20,20,85)
            #contiguous()进行一个拷贝
            if not self.training:  # inference
                if self.onnx_dynamic or self.grid[i].shape[2:4] != x[i].shape[2:4]:
                    self.grid[i], self.anchor_grid[i] = self._make_grid(nx, ny, i)
                    #制作第几预测层的网格
 
                y = x[i].sigmoid()
                #激活函数,完成逻辑回归的软判决,变量映射到0,1之间的S型函数,所以最后的y就是相对于网格占了几分之几的意思(对center的x,y,w,h都做了归一化处理)
                if self.inplace:
                    y[..., 0:2] = (y[..., 0:2] * 2 - 0.5 + self.grid[i]) * self.stride[i]  # xy
                    #box center的x,y的预测被乘以2并减去了0.5,让他的预测范围变成(-0.5,1.5)就是能跨半个网格预测
                    #然后加上self.grid[i],就是加上网格的宽度/高度
                    #最后乘上self.stride[i],就是步长,定位到原先预测的那个点
                    y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i]  # wh
                    #对预测框高宽的处理
                else:  # for YOLOv5 on AWS Inferentia https://github.com/ultralytics/yolov5/pull/2953
                    xy = (y[..., 0:2] * 2 - 0.5 + self.grid[i]) * self.stride[i]  # xy
                    wh = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i]  # wh
                    y = torch.cat((xy, wh, y[..., 4:]), -1)
                z.append(y.view(bs, -1, self.no))
                #将结果填入z:(第几层的预测层),(预测出的center的x,y,以及预测框的w和h),(对应的85种信息--这里个人认为这85种信息中的x,y,w,h不会再用到,主要是取出置信度信息)
 
        return x if self.training else (torch.cat(z, 1), x)
 
    def _make_grid(self, nx=20, ny=20, i=0):
        #准备网格,所有的预测的单位长度都是基于grid层面的而不是原图,并且每一层的grid的尺寸都是不一样的,和每一层输出的尺寸w,h是一样的。
        d = self.anchors[i].device
        if check_version(torch.__version__, '1.10.0'):  # torch>=1.10.0 meshgrid workaround for torch>=0.7 compatibility
            yv, xv = torch.meshgrid([torch.arange(ny, device=d), torch.arange(nx, device=d)], indexing='ij')
            #torch.meshgrid()生成网格,可以用于生成坐标,尺寸nx * ny;ny范围是竖向坐标;nx范围是横向坐标
        else:
            yv, xv = torch.meshgrid([torch.arange(ny, device=d), torch.arange(nx, device=d)])
        grid = torch.stack((xv, yv), 2).expand((1, self.na, ny, nx, 2)).float()
        anchor_grid = (self.anchors[i].clone() * self.stride[i]) \
            .view((1, self.na, 1, 1, 2)).expand((1, self.na, ny, nx, 2)).float()
        return grid, anchor_grid #制成网格返回

函数__init__

    def __init__(self, nc=80, anchors=(), ch=(), inplace=True):  # detection layer
        #yolov5中的anchors(3个,对应Neck出来的那3个输出),初始anchor是由w,h宽高组成,用的是原图的像素尺寸,设置为每层3个,所以共有3 * 3 = 9个
        super().__init__()
        self.nc = nc  # 预测的类的数量
        self.no = nc + 5  
        # 每一个预测框(anchor)输出的数量,对应每种类的置信度(nc),预测框的高宽,中心点坐标,预测框内是否有物体的置信度,共5种信息。
        self.nl = len(anchors)  # 预测层的数量
        self.na = len(anchors[0]) // 2  #预测框的数量

🎈yolov5中的anchors(3个,对应Neck出来的那3个输出),初始anchor是由w,h宽高组成,用的是原图的像素尺寸,设置为每层3个,所以共有3 * 3 = 9个

🎈nc:待预测的类的数量

🎈no:每一个预测框(anchor)输出的数量,对应每种类的置信度(nc),预测框的高宽,中心点坐标,预测框内是否有物体的置信度,共5 + 80 = 85种信息。

🎈nl:预测层的数量,一般3层,每个anchors3有3个预测层

🎈na:预测框的数量,一般为3,3个anchors

self.grid = [torch.zeros(1)] * self.nl  # 初始网格,对于每个预测层都有初始网格的生成
self.anchor_grid = [torch.zeros(1)] * self.nl  # 初始预测框网格
self.register_buffer('anchors', torch.tensor(anchors).float().view(self.nl, -1, 2))  
# shape(nl,na,2)
self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch)  
# output conv,输出结果:每个预测框的输出结果 * 预测框个数
self.inplace = inplace  # use in-place ops (e.g. slice assignment)

🎈grid:初始网格,对于每个预测层都有初始网格的生成

🎈anchor_grid:初始预测框网格

🎈self.m:output conv,输出结果:每个预测框的输出结果 * 预测框个数

函数_make_grid

     def _make_grid(self, nx=20, ny=20, i=0):
        #准备网格,所有的预测的单位长度都是基于grid层面的而不是原图,并且每一层的grid的尺寸都是不一样的,和每一层输出的尺寸w,h是一样的。
        d = self.anchors[i].device
        if check_version(torch.__version__, '1.10.0'):  # torch>=1.10.0 meshgrid workaround for torch>=0.7 compatibility
            yv, xv = torch.meshgrid([torch.arange(ny, device=d), torch.arange(nx, device=d)], indexing='ij')
            #torch.meshgrid()生成网格,可以用于生成坐标,尺寸nx * ny;ny范围是竖向坐标;nx范围是横向坐标
        else:
            yv, xv = torch.meshgrid([torch.arange(ny, device=d), torch.arange(nx, device=d)])
        grid = torch.stack((xv, yv), 2).expand((1, self.na, ny, nx, 2)).float()
        anchor_grid = (self.anchors[i].clone() * self.stride[i]) \
            .view((1, self.na, 1, 1, 2)).expand((1, self.na, ny, nx, 2)).float()
        return grid, anchor_grid #制成网格返回

🎈基本大框架是先检查版本,针对不同版本进行不同的同款操作

🎈torch.meshgrid()生成网格,可以用于生成坐标,尺寸nx * ny;ny范围是竖向坐标;nx范围是横向坐标

函数forward

主结构是循环每层每层的处理

def forward(self, x):
    z = []  # inference output
    for i in range(self.nl): #每层循环着处理

循环里面进行核心操作

bs, _, ny, nx = x[i].shape  #bs第几个预测层的意思吧
x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
#view()变换形状,数据不变, x(bs,255,20,20) to x(bs,3,85,20,20),将一个预测层里的3个anchor的信息分出来,每个预测框预测信息数量为self.no(这里为85)
#permute(0, 1, 3, 4, 2),x[i]有5个维度,(2,3,4)变成(3,4,2),x(bs,3,85,20,20)to x(bs,3,20,20,85)
#contiguous()进行一个拷贝

🎈view()变换形状,数据不变, x(bs,255,20,20) to x(bs,3,85,20,20),将一个预测层里的3个anchor的信息分出来,每个预测框预测信息数量为self.no(这里为85)

🎈permute(0, 1, 3, 4, 2),x[i]有5个维度,(2,3,4)变成(3,4,2),x(bs,3,85,20,20)to x(bs,3,20,20,85)

🎈contiguous()进行一个拷贝

后又进入一个是否进行训练的框架中(if not self.training:)

if not self.training:  # inference
	if self.onnx_dynamic or self.grid[i].shape[2:4] != x[i].shape[2:4]:
		self.grid[i], self.anchor_grid[i] = self._make_grid(nx, ny, i)
		#制作第几预测层的网格

判断是否需要制作第i预测层的网格

 y = x[i].sigmoid() 
#激活函数,完成逻辑回归的软判决,变量映射到0,1之间的S型函数,所以最后的y就是相对于网格占了几分之几的意思(对center的x,y,w,h都做了归一化处理)

激活函数,完成逻辑回归的软判决,变量映射到0,1之间的S型函数,所以最后的y就是相对于网格占了几分之几的意思(对center的x,y,w,h都做了归一化处理)

if self.inplace:
    y[..., 0:2] = (y[..., 0:2] * 2 - 0.5 + self.grid[i]) * self.stride[i]  # xy
    #box center的x,y的预测被乘以2并减去了0.5,让他的预测范围变成(-0.5,1.5)就是能跨半个网格预测
    #然后加上self.grid[i],就是加上网格的宽度/高度
    #最后乘上self.stride[i],就是步长,定位到原先预测的那个点
    y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i]  # wh
    #对预测框高宽的处理
else:  # for YOLOv5 on AWS Inferentia https://github.com/ultralytics/yolov5/pull/2953
    xy = (y[..., 0:2] * 2 - 0.5 + self.grid[i]) * self.stride[i]  # xy
    wh = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i]  # wh
    y = torch.cat((xy, wh, y[..., 4:]), -1)

self.inplace里面代码分析

🎈第一句是对中心点x,y的操作。

(1)box center的x,y的预测被乘以2并减去了0.5,让他的预测范围变成(-0.5,1.5)就是能跨半个网格预测

(2)然后加上self.grid[i],就是加上网格的宽度/高度

(3)最后乘上self.stride[i],就是步长,定位到原先预测的那个点

🎈第二句是对预测框的宽高进行操作。

z.append(y.view(bs, -1, self.no))
#将结果填入z:(第几层的预测层),(预测出的center的x,y,以及预测框的w和h),(对应的85种信息--这里个人认为这85种信息中的x,y,w,h不会再用到,主要是取出置信度信息)

将结果填入z:(第几层的预测层),(预测出的center的x,y,以及预测框的w和h),(对应的85种信息--这里个人认为这85种信息中的x,y,w,h不会再用到,主要是取出置信度信息)

return x if self.training else (torch.cat(z, 1), x)

最后来个判断后return,如果还要训练就是返回x,如果训练完毕,那就返回(torch.cat(z, 1), x)

Model类

  class Model(nn.Module):
    def __init__(self, cfg='yolov5s.yaml', ch=3, nc=None, anchors=None):  # model, input channels, number of classes
        super().__init__()
        if isinstance(cfg, dict): #判断cfg是不是dict(字典)类型
            self.yaml = cfg  # model dict
            #模型词典赋值给self.yaml
        else:  # is *.yaml
            import yaml  # for torch hub
            self.yaml_file = Path(cfg).name # 获取cfg的文件名
            with open(cfg, encoding='ascii', errors='ignore') as f:#用ascii编码,忽略错误的形式打开文件cfg
                self.yaml = yaml.safe_load(f)  # model dict
                #用yaml的文件形式加载cfg,赋值给self.yaml
 
        # Define model
        ch = self.yaml['ch'] = self.yaml.get('ch', ch)  # input channels
        if nc and nc != self.yaml['nc']:
            LOGGER.info(f"Overriding model.yaml nc={self.yaml['nc']} with nc={nc}")
            self.yaml['nc'] = nc  # override yaml value
        #上面是为了判断输入的channel和配置文件里的是否相同,不相同则变成输入的参数。
        if anchors:
            LOGGER.info(f'Overriding model.yaml anchors with anchors={anchors}')
            self.yaml['anchors'] = round(anchors)  # override yaml value
        #以上将anchors进行四舍五入(防止输入的是小数从而报错)
        self.model, self.save = parse_model(deepcopy(self.yaml), ch=[ch])  # model, savelist
        self.names = [str(i) for i in range(self.yaml['nc'])]  # default names
        #给那些种类编编号,从0到nc-1
        self.inplace = self.yaml.get('inplace', True)
 
        # Build strides, anchors
        m = self.model[-1]  # Detect()
        if isinstance(m, Detect):
            s = 256  # 2x min stride步长初始化
            m.inplace = self.inplace
            m.stride = torch.tensor([s / x.shape[-2] for x in self.forward(torch.zeros(1, ch, s, s))])  # forward
            m.anchors /= m.stride.view(-1, 1, 1)
            check_anchor_order(m) #根据YOLOv5 Detect()模块m的步幅顺序检查锚定顺序,必要时进行纠正
            self.stride = m.stride
            self._initialize_biases()  # only run once
            #将偏差初始化进Detect模块(没有偏差,因为初始类频率cf为None)
 
        # Init weights, biases
        initialize_weights(self)
        self.info()
        LOGGER.info('')
 
    def forward(self, x, augment=False, profile=False, visualize=False):
        if augment:
            return self._forward_augment(x)  # augmented inference, None
            #增强
        return self._forward_once(x, profile, visualize)  # single-scale inference, train
        #默认情况下不增强

函数_forward_once

    def _forward_once(self, x, profile=False, visualize=False):
        y, dt = [], []  # outputs
        for m in self.model:
            #m中的参数:m.f是从哪层开始,m.n是模块的默认深度,m.args是该层的参数(就是从yaml那来的)
            if m.f != -1:  # if not from previous layer
            #不是上一层,就是说不直接连接上一层(比如像Concat)
                x = y[m.f] if isinstance(m.f, int) else [x if j == -1 else y[j] for j in m.f]  # from earlier layers
                #取出对应的层的结果,准备后面的进入对应m的forward()
            if profile:
                self._profile_one_layer(m, x, dt)
            x = m(x)  # run
            #m是模块(某个层)的意思,所以x传入模块,相当于执行模块(比如说Focus,SPP等)中的forward()
            #第一层Focus的m.f是-1,所以直接跳到这一步开始
            y.append(x if m.i in self.save else None)  # save output
            #将每一层的输出结果保存到y
            if visualize:
                feature_visualization(x, m.type, m.i, save_dir=visualize)
        return x

parse_model函数

 def parse_model(d, ch):  # model_dict, input_channels(3)
    LOGGER.info(f"\n{'':>3}{'from':>18}{'n':>3}{'params':>10}  {'module':<40}{'arguments':<30}")
    #日志记载,不管他
    anchors, nc, gd, gw = d['anchors'], d['nc'], d['depth_multiple'], d['width_multiple']
    na = (len(anchors[0]) // 2) if isinstance(anchors, list) else anchors  # number of anchors
    no = na * (nc + 5)  # number of outputs = anchors * (classes + 5)
    #以上是读取配置dict里面的参数
    layers, save, c2 = [], [], ch[-1]  # layers, savelist, ch out
    for i, (f, n, m, args) in enumerate(d['backbone'] + d['head']):  # from, number, module, args
        m = eval(m) if isinstance(m, str) else m  # 将模块类型m转为值
        for j, a in enumerate(args):#循环模块参数args
            try:
                args[j] = eval(a) if isinstance(a, str) else a  # 将每个模块参数args[j]转为值
            except NameError:
                pass
    #以上循环,开始迭代循环backbone与head的配置
    #f:从哪层开始;n:模块的默认深度;m:模块的类型;args:模块的参数
        n = n_ = max(round(n * gd), 1) if n > 1 else n  # depth gain
        #网络用(n*gd)控制模块的深度缩放。
        #深度在这里指的是像CSP这种模块的重复迭代次数,宽度一般我们指的是特征图的channel。
        if m in [Conv, GhostConv, Bottleneck, GhostBottleneck, SPP, SPPF, DWConv, MixConv2d, Focus, CrossConv,
                 BottleneckCSP, C3, C3TR, C3SPP, C3Ghost]:
            c1, c2 = ch[f], args[0]
            #ch是用来保存之前所有的模块输出的channle(所以ch[-1]代表着上一个模块的输出通道)。
            # ch[f]是第f层的输出。args[0]是默认的输出通道。
            if c2 != no:  # if not output
            #如果不是最终的输出
                c2 = make_divisible(c2 * gw, 8)#保证了输出的通道是8的倍数
 
            args = [c1, c2, *args[1:]] #args变为原来的args+module的输入通道数(c1)、输出通道数(c2)
            if m in [BottleneckCSP, C3, C3TR, C3Ghost]: #只有CSP结构的才会根据深度参数n来调整该模块的重复迭加次数
                args.insert(2, n)  # number of repeats
                #模块参数信息args插入n
                n = 1#n重置
        elif m is nn.BatchNorm2d:
            args = [ch[f]]#BN的参数只有输入通道数,即通道数保持不变
        elif m is Concat:
            c2 = sum(ch[x] for x in f)#Concat:f是所有需要拼接层的索引,则输出通道数c2是所有层的和
        elif m is Detect:
            args.append([ch[x] for x in f])#填入每个预测层的输入通道数
            if isinstance(args[1], int):  # number of anchors
                args[1] = [list(range(args[1] * 2))] * len(f) 
                #[list(range(args[1] * 2))]:初始化列表:预测框的宽高
                #最后乘上len(f) ,就是生成所有预测层对应的预测框的初始高宽
        elif m is Contract:
            c2 = ch[f] * args[0] ** 2
        elif m is Expand:
            c2 = ch[f] // args[0] ** 2
        else:
            c2 = ch[f]#其余情况都是输出通道数(c2)为输入通道数
 
        m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args)  # module
        #拿args里的参数去构建了module m,然后模块的循环次数用参数n控制。整体都受到宽度缩放,C3模块受到深度缩放。
        t = str(m)[8:-2].replace('__main__.', '')  # module type
        np = sum(x.numel() for x in m_.parameters())  # number params
        m_.i, m_.f, m_.type, m_.np = i, f, t, np  # attach index, 'from' index, type, number params
        LOGGER.info(f'{i:>3}{str(f):>18}{n_:>3}{np:10.0f}  {t:<40}{str(args):<30}')  # print
        #以上是日志文件信息(每一层module构建的编号、参数量等)
        save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1)  # append to savelist
        #保存需要用的层的输出(比如Concat层需要concat某些层,这些层的结果就需要存起来)
        
        layers.append(m_)#把构建的模块保存到layers里
        if i == 0:
            ch = []
        ch.append(c2)#把该层的输出通道数写入ch列表里
    #当循环结束后再构建成模型
    return nn.Sequential(*layers), sorted(save)

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×