淘先锋技术网

首页 1 2 3 4 5 6 7

目录

1. 代码结构和流程

2. 网络结构解析

2.1 Backbone-YOLOPAFPN

2.1.1 主干网络CSPDarknet

2.1.1.1 Focus

2.1.1.2 CSPlayer

2.2 head-YOLOXHead

2.3 YOLOX

3. 训练细节

3.1 坐标回归

3.2 正负样本和loss


1. 代码结构和流程

pytorch版yolox代码解析,以yolox-s为例,走一遍demo的运行流程。首先设置命令行参数:

运行demo.py,第一步解析参数,第二步,调用get_exp(), 

get_exp函数根据输入的实验名称或者实验文件描述文件来获取对应网络的Exp对象。类似于paddle里面用的反射机制。我们这里指定了args.name参数为:yolox-s,所以调用get_exp_by_name(exp_name) 

get_exp_by_name(exp_name) 会根据字典里的键值对,找输入的实验名(网络名)对应的python文件。

 

 这些文件都存在:YOLOX\exps\default\ 文件夹下。

 在来看下这个yolox_s.py文件:

里面定义了Exp类,继承自MyExp类,再看看MyExp类:

init函数里面定义了网络结构配置参数,数据加载相关参数 ,网络训练和测试参数等。其他成员函数用来获取网络结构,获取数据加载器,已经其他的处理。

 找到路径下面的py文件之后,根据这个文件来生成对应的Exp对象(疑似反射机制)

然后返回exp(对象,也就是返回了一个网络类的对象。

 

进入main函数,调用exp.get_model()来获取网络结构定义

这里需要调用网络的各个组件的类,YOLOPAFPN,YOLOXHead,然后组成一个完整的网络。

 网络结构获得之后,打印网络结构,加载预训练模型:

 然后,arg.fuse判断是否需要将conv和bn后推理,args.trt判断是否需要做TensorRT后推理。以上两个选项都是用来加速推理的。接下来实例化预测器类:

 

预测器对输入数据进行处理热火调用网络,进行推理,然后可视化最后的预测结果。如果指定了预测结果保存,则会将结果图片保存在

 

这里用yolo3Darknet53的来展示图片,不知道什么原因,还没细看,用yolox-s和yolox-m,yolox-l这三个网络跑出来的demo都没预测出结果。可能是预训练模型加载出错了。

至此,整个demo的流程走完。下面就开始解析几个重点模块。

2. 网络结构解析

2.1 Backbone-YOLOPAFPN

class YOLOPAFPN(nn.Module):
    """
    YOLOv3 model. Darknet 53 is the default backbone of this model.
    """

    def __init__(
        self,
        depth=1.0,
        width=1.0,
        in_features=("dark3", "dark4", "dark5"),
        in_channels=[256, 512, 1024],
        depthwise=False,
        act="relu",
    ):
        super().__init__()
        self.backbone = CSPDarknet(depth, width, depthwise=depthwise, act=act)
        self.in_features = in_features
        self.in_channels = in_channels
        Conv = DWConv if depthwise else BaseConv

        self.upsample = nn.Upsample(scale_factor=2, mode="nearest")
        self.lateral_conv0 = BaseConv(
            int(in_channels[2] * width), int(in_channels[1] * width), 1, 1, act=act
        )
        self.C3_p4 = CSPLayer(
            int(2 * in_channels[1] * width),
            int(in_channels[1] * width),
            round(3 * depth),
            False,
            depthwise=depthwise,
            act=act,
        )  # cat

        self.reduce_conv1 = BaseConv(
            int(in_channels[1] * width), int(in_channels[0] * width), 1, 1, act=act
        )
        self.C3_p3 = CSPLayer(
            int(2 * in_channels[0] * width),
            int(in_channels[0] * width),
            round(3 * depth),
            False,
            depthwise=depthwise,
            act=act,
        )

        # bottom-up conv
        self.bu_conv2 = Conv(
            int(in_channels[0] * width), int(in_channels[0] * width), 3, 2, act=act
        )
        self.C3_n3 = CSPLayer(
            int(2 * in_channels[0] * width),
            int(in_channels[1] * width),
            round(3 * depth),
            False,
            depthwise=depthwise,
            act=act,
        )

        # bottom-up conv
        self.bu_conv1 = Conv(
            int(in_channels[1] * width), int(in_channels[1] * width), 3, 2, act=act
        )
        self.C3_n4 = CSPLayer(
            int(2 * in_channels[1] * width),
            int(in_channels[2] * width),
            round(3 * depth),
            False,
            depthwise=depthwise,
            act=act,
        )

    def forward(self, input):
        """
        Args:
            inputs: input images.

        Returns:
            Tuple[Tensor]: FPN feature.
        """

        #  backbone
        out_features = self.backbone(input)
        features = [out_features[f] for f in self.in_features]
        [x2, x1, x0] = features

        fpn_out0 = self.lateral_conv0(x0)  # 1024->512/32
        f_out0 = self.upsample(fpn_out0)  # 512/16
        f_out0 = torch.cat([f_out0, x1], 1)  # 512->1024/16
        f_out0 = self.C3_p4(f_out0)  # 1024->512/16

        fpn_out1 = self.reduce_conv1(f_out0)  # 512->256/16
        f_out1 = self.upsample(fpn_out1)  # 256/8
        f_out1 = torch.cat([f_out1, x2], 1)  # 256->512/8
        pan_out2 = self.C3_p3(f_out1)  # 512->256/8

        p_out1 = self.bu_conv2(pan_out2)  # 256->256/16
        p_out1 = torch.cat([p_out1, fpn_out1], 1)  # 256->512/16
        pan_out1 = self.C3_n3(p_out1)  # 512->512/16

        p_out0 = self.bu_conv1(pan_out1)  # 512->512/32
        p_out0 = torch.cat([p_out0, fpn_out0], 1)  # 512->1024/32
        pan_out0 = self.C3_n4(p_out0)  # 1024->1024/32

        outputs = (pan_out2, pan_out1, pan_out0)
        return outputs

PA指的是PANet的结构,FPN指的是特征金字塔结构。

backbone-YOLOPAFPN部分网络结构如下图所示:

2.1.1 主干网络CSPDarknet

2.1.1.1 Focus

参考:4、Focus模块-in YOLO - 知乎 (zhihu.com)

代码实现:

class Focus(nn.Module):
    """Focus width and height information into channel space."""

    def __init__(self, in_channels, out_channels, ksize=1, stride=1, act="silu"):
        super().__init__()
        self.conv = BaseConv(in_channels * 4, out_channels, ksize, stride, act=act)

    def forward(self, x):
        # shape of x (b,c,w,h) -> y(b,4c,w/2,h/2)
        patch_top_left = x[..., ::2, ::2]
        patch_top_right = x[..., ::2, 1::2]
        patch_bot_left = x[..., 1::2, ::2]
        patch_bot_right = x[..., 1::2, 1::2]
        x = torch.cat(
            (
                patch_top_left,
                patch_bot_left,
                patch_top_right,
                patch_bot_right,
            ),
            dim=1,
        )
        return self.conv(x)

这个模块位于网络backbone(干)的一开始,紧接着数据层,称为stem(茎)。

 这一层具体做了什么,可以先看下代码:

# shape of x (b,c,w,h) -> y(b,4c,w/2,h/2)
patch_top_left = x[..., ::2, ::2]
patch_top_right = x[..., ::2, 1::2]
patch_bot_left = x[..., 1::2, ::2]
patch_bot_right = x[..., 1::2, 1::2]

x就表示输入的图像,维度是(b,c,w,h),实际是(b,3,640,640),这里四个变量的名字,分别代表 左上,右上,左下,右下,四个小块,因此刚开始误以为是直接把输入图片按照宽高,分成4个部分,每个部分大小为320x320,如下图所示:

 但是仔细看代码发现,这里对输入x的切片是对后面两个维度(w,h)采用了::21::2的操作,而这两个操作的含义分别是:

 所以依次看下上面的四个切片操作,可以参考这张图来理解:

(1)patch_top_left = x[..., ::2, ::2],就是前两维度不变,后面两个维度取偶数下标的对应的值,对于输入的一张3通道的图片,分别取第0,2,4,6,...行(列)的值,对应图中红色像素的值。

(2)patch_top_right = x[..., ::2, 1::2],表示前两个维度不变,w维度,取第0,2,4,6..行的值,而h维度,取第1,3,5,7,...列的值。对应上图中黄色像素的值。

(3)patch_bot_left = x[..., 1::2, ::2],表示前两个维度不变,w维度,取第1,3,5,7,...行的值,而h维度,取第0,2,4,6..列的值。对应上图中绿色像素的值。

(4)patch_bot_right = x[..., 1::2, 1::2],表示前两个维度不变,w维度,取第1,3,5,7,...行的值,而h维度,取第1,3,5,7,...列的值。对应上图中蓝色像素的值。

切片结束后,得到4个3通道的子图,维度为(b,3,320,320),然后按照通道维度拼接起来,得到一个(b,12,320,320)的特征图,再接一个3x3卷积,就构成了整个focus模块。

再回头看下代码里的focus类的注释:

"""Focus width and height information into channel space."""

字面翻译是将宽高信息聚焦到通道空间,通俗理解就是SpaceToDepth,也就是将空间信息转换到通道信息。这里引用一下别人的理解:

1、“Focus的作用无非是使图片在下采样的过程中,不带来信息丢失的情况下,将W、H的信息集中到通道上,再使用3 × 3的卷积对其进行特征提取,使得特征提取得更加的充分。虽然增加了一点点的计算量,但是为后续的特征提取保留了更完整的图片下采样信息”。

2、“Focus模块在v5中是图片进入backbone前,对图片进行切片操作,具体操作是在一张图片中每隔一个像素拿到一个值,类似于邻近下采样,这样就拿到了四张图片,四张图片互补,长的差不多,但是没有信息丢失,这样一来,将W、H信息就集中到了通道空间,输入通道扩充了4倍,即拼接起来的图片相对于原先的RGB三通道模式变成了12个通道,最后将得到的新图片再经过卷积操作,最终得到了没有信息丢失情况下的二倍下采样特征图”。

3、看下原作者的解释

 只是用于减少FLOPS和加速,不用来增加mAP。还有就是用来减少层数,1个Focus层可以替代3个yolo3或yolo4里面的层。

2.1.1.2 CSPlayer

CSPlayer在yolo v4中就已经使用,其论文中的原理如下图:

也就是将输入的特征图,按通道一分为二,分别经过两个分支,最后合并通道。而实际在pytorch的实现中,都是下面这种版本:

 输入通道先按原通达走两个分支,再在各自分支中将输出通道减半(1x1卷积通道降维),最后再合并通道。代码如下所示:

class CSPLayer(nn.Module):
    """C3 in yolov5, CSP Bottleneck with 3 convolutions"""

    def __init__(
        self,
        in_channels,
        out_channels,
        n=1,
        shortcut=True,
        expansion=0.5,
        depthwise=False,
        act="silu",
    ):
        """
        Args:
            in_channels (int): input channels.
            out_channels (int): output channels.
            n (int): number of Bottlenecks. Default value: 1.
        """
        # ch_in, ch_out, number, shortcut, groups, expansion
        super().__init__()
        hidden_channels = int(out_channels * expansion)  # hidden channels
        self.conv1 = BaseConv(in_channels, hidden_channels, 1, stride=1, act=act)
        self.conv2 = BaseConv(in_channels, hidden_channels, 1, stride=1, act=act)
        self.conv3 = BaseConv(2 * hidden_channels, out_channels, 1, stride=1, act=act)
        module_list = [
            Bottleneck(
                hidden_channels, hidden_channels, shortcut, 1.0, depthwise, act=act
            )
            for _ in range(n)
        ]
        self.m = nn.Sequential(*module_list)

    def forward(self, x):
        x_1 = self.conv1(x)
        x_2 = self.conv2(x)
        x_1 = self.m(x_1)
        x = torch.cat((x_1, x_2), dim=1)
        return self.conv3(x)

 两个分支中,其中一个分支只有1x1卷积,另外一个分支经过1x1卷积+bottleneck。

2.2 head-YOLOXHead

    YOLO模型的cls,obj和reg都是在同一个卷积层来预测,但其实其它的one-stage检测模型其实都采用decoupled head(这个其实是从RetinaNet开始的,后面的FCOS和ATSS都沿用),即将分类和回归任务分开来预测,因为这个两个任务其实是有冲突的。论文中做的第一个改进就是将YOLO改成了decoupled head,对于输入的FPN特征,首先通过1x1卷积将特征维度降低到256,然后分成两个并行的分支,每个分支包含2个3x3卷积,其中分类分支预测cls,而回归分支预测reg和obj(图中显示的是IoU分支,但实际上从代码来看和原始YOLO一样都是obj,不过按YOLO的本意其实obj里面也包含了定位准确性)。

2.3 YOLOX

3. 训练细节

3.1 坐标回归

网络预测层输出(共3层),每层输出

Output[...,:2]------>tx,ty

Output[...,2:4]---->tw,th

Output[...,4]------->object

Output[...,5:]------>class

坐标基于当前输出层的特征图大小,如80x80,40x40,20x20

Grid 是特征图分辨率大小的矩阵(用meshGrid方法生成),表示特征图的每个点(0-79)。映射回原图,表示将原图切分成一个个的格子,grid坐标映射回去,对应格子的左上角点。

bx,by,bw,bh为网络输出结果转换到特征图上的最终预测结果。tx,ty为相对于特征图中对应grid点的偏移量,tw和th表示特征图尺寸下的宽高,pw和ph是yolo2和yolo3中的anchor映射在特征图尺寸的宽高,这张图是从yolo2拿过来的,因此有很多的anchor,而YOLOX是anchor-free的,或者说是只有一个anchor,且大小为1x1。这里再借助yolov2里面的图来理解。

 用上面的公式转换后就将偏移量转换成了特征图中的坐标值,然后通过乘以缩放倍数将四个值放大获得原图中的x,y,w,h坐标。下图以特征图大小为8x8为示例:

3.2 正负样本和loss

3.2.1 get_in_boxes_info

功能:计算每个anchor的中心(格子的中心点),是否位于gtbox内,以及anchor是否位于gtbox的半径范围内(2.5*stride),最终返回的是候选区域,也就是与gtbox较为接近的anchor,如下图中的非白色区域的anchor(格子)。

图解:

3.2.2 bboxes_iou

计算gtbox和经过第一步筛选出来的anchor索引对应的网络预测结果的IOU,取log作为iou_loss。

然后计算gt和pred_cls的cls_loss,最后将cls_loss和iou_loss作为cost,计算dynamic_k。

cost = (pair_wise_cls_loss

       + 3.0 * pair_wise_ious_loss

       + 100000.0 * (~is_in_boxes_and_center))

3.2.3 dynamic_k_matching

(1)使用IOU确定dynamic_k,取与每个gt的最大的10个IOU。

n_candidate_k = min(10, ious_in_boxes_matrix.size(1))

      topk_ious, _ = torch.topk(ious_in_boxes_matrix, n_candidate_k, dim=1)

      dynamic_ks = torch.clamp(topk_ious.sum(1).int(), min=1)

然后将10个IOU相加,取整数,得到了每个gt对应的dynamic_k,如下如所示:

(2)为每个gt取cost排名最小的前dynamic_k个anchor作为正样本,其余为负样本。

3.2.4 构造target

    由前面3步,可以得到所有认为是正样本的anchor(实际是取对应到的gtbox的索引即可),假如8400个anchor里面有37个是最终得到的正样本,这长度为37的数组中,都是该anchor对应的当前图片中gtbox的类别下标,比如,第1322个anchor与gt[2]匹配,且gt[2]对应类别17,则这个anchor在这个长度为37的数组中为17。如下图所示(这里用了VOC数据集,所以是20类):

Cls_target的构造:

        取上述正样本对应的类别数组,按照onehot展开成[37,20]的样子,然后再乘以对应的IOU,维度是[37,20]。

Obj_target的构造:

        取正负样本索引的mask(F,F,F,..T,T,F,....),转成float型。维度是[8400,1]。

Reg_target的构造:

        取上述正样本索引,与cls的构造类似,将gtbox的坐标分配给每个匹配的正样本,作为reg_target,维度是[37,4]。

        计算loss时,将每张图片的8400个预测结果按正样本索引mask取出,与上面的target做loss.

除了上述选出来的正样本,剩下的作为负样本,只会对iou_loss和cls_loss起作用,对obj_loss没有作用。