赞
踩
目录
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的流程走完。下面就开始解析几个重点模块。
- 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部分网络结构如下图所示:
参考: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)采用了::2和1::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里面的层。
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。
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里面也包含了定位准确性)。
网络预测层输出(共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为示例:
功能:计算每个anchor的中心(格子的中心点),是否位于gtbox内,以及anchor是否位于gtbox的半径范围内(2.5*stride),最终返回的是候选区域,也就是与gtbox较为接近的anchor,如下图中的非白色区域的anchor(格子)。
图解:
计算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))
(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步,可以得到所有认为是正样本的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没有作用。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。