yolo-x仍然保留有yolo系列的整体的特征,首先我们先来回顾以下yolo系列的整体框架:
首先我们输入一张图片进入yolo网络,yolo会自动调整图片的大小到416*416的大小,为了防止失真,会自动增加黑条,详情见yolo5种的黑条添加。
然后yolo会分成三种不同尺寸的网格,其中13 * 13、26 * 26、52 * 52的网格分别用来预测大,中,小物体。
传统的yolo系列获得到的特征层,相当于将输入进来的原图进行网格的划分,每一个特征点都会对应若干个先验框,网络的预测结果相当于对先先验框内部是否包含物体进行判断,并调整它最终成为预测框。
其中yolox的特点主要有:
1.主干部分:
(1)使用了Focus网络结构,yolo5中有用到这个结构。
(2)仍然使用了CSPnet的网络结构,在残差模块堆叠的同时,狗构建大的产茶便,经过少量的处理直接连接到最后。
2.分类回归层:
(1)过去的yolo将分类和回归都在一个1*1的卷积里面实现,yolox认为这给网络的识别带来了不好的影响,所以在yolo head的部分被分为了2个部分,分别实现,最后在预测的时候才进行整合。
3.数据增强:
(1)采用了mosaic数据增强,四张图片进行拼接增强了数据集。详情见yolo4
4.Anchor Free:
(1)yolox采用了Anchor Free的解码,使得代码逻辑更加的简单,可读性更高。
5.Sim OTA动态匹配正样本
(1)为不同大小的目标动态匹配正样本
整个YoloX可以依然可以分为三个部分,分别是CSPDarknet,FPN以及Yolo Head。
1、开始的主干网络CSPDarknet可以被称作YoloX的主干特征提取网络
2、中间的FPN。FPN可以被称作YoloX的加强特征提取网络
3、最后的yolo head。Yolo Head是YoloX的分类器与回归器
首先通过一个Focus的网络结构,在经过卷积,一系列的残差结构。
Residual:残差:分为主干分支,侧分支,其中主干分支是1 * 1的一个卷积,在进行3 * 3的卷积操作,残差便不做处理,直接相加。
其中的BN层,就相当于在Resnet中的残差,很好的缓解了退化问题,以及深度加深带来的梯度消失,梯度爆炸的问题。
class Bottleneck(nn.Module):
# Standard bottleneck
def __init__(self, in_channels, out_channels, shortcut=True, expansion=0.5, depthwise=False, act="silu",):
super().__init__()
hidden_channels = int(out_channels * expansion)
Conv = DWConv if depthwise else BaseConv
self.conv1 = BaseConv(in_channels, hidden_channels, 1, stride=1, act=act)
self.conv2 = Conv(hidden_channels, out_channels, 3, stride=1, act=act)
self.use_add = shortcut and in_channels == out_channels
def forward(self, x):
y = self.conv2(self.conv1(x))
if self.use_add:
y = y + x
return y
跟v4中的整体基本一致,在主分支上就是采用上面一系列小的残差,最后一点就是在跟侧分支的小量计算的侧分支总体进行一个相加,类似于大的残差里有很多小的残差。
class CSPLayer(nn.Module):
def __init__(self, in_channels, out_channels, n=1, shortcut=True, expansion=0.5, depthwise=False, act="silu", ):
# 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)
一眼就能看明白,就是将原来的特征矩阵将深度变为原来的4倍,长和宽变为原来的一半。
采用不同的池核的大小进行最大池化进行特征提取,提高网络的感受野。
这里与V4中SPP结构在使用的位置上有差异,在V4中是在主干网络之后使用,而在X中使用在了主干提取网络中了。
我们可以从下到上进行特征融合,在进行下采样,分别输出三个YOLO Head 。
其中特征提取部分分别提取中间层(80,80,256),中下层(40,40,512),底层(20,20,1024)
FPN加强特征提取网络可以将不同shape的特征层进行特征融合,有利于提取出更好的特征
``
class YOLOPAFPN(nn.Module): #加强特征FPN机构
def __init__(self, depth = 1.0, width = 1.0, in_features = ("dark3", "dark4", "dark5"), in_channels = [256, 512, 1024], depthwise = False, act = "silu"):
super().__init__()
Conv = DWConv if depthwise else BaseConv
self.backbone = CSPDarknet(depth, width, depthwise = depthwise, act = act)
self.in_features = in_features
self.upsample = nn.Upsample(scale_factor=2, mode="nearest")
#-------------------------------------------#
# 20, 20, 1024 -> 20, 20, 512
#-------------------------------------------#
self.lateral_conv0 = BaseConv(int(in_channels[2] * width), int(in_channels[1] * width), 1, 1, act=act)
#-------------------------------------------#
# 40, 40, 1024 -> 40, 40, 512
#-------------------------------------------#
self.C3_p4 = CSPLayer(
int(2 * in_channels[1] * width),
int(in_channels[1] * width),
round(3 * depth),
False,
depthwise = depthwise,
act = act,
)
#-------------------------------------------#
# 40, 40, 512 -> 40, 40, 256
#-------------------------------------------#
self.reduce_conv1 = BaseConv(int(in_channels[1] * width), int(in_channels[0] * width), 1, 1, act=act)
#-------------------------------------------#
# 80, 80, 512 -> 80, 80, 256
#-------------------------------------------#
self.C3_p3 = CSPLayer(
int(2 * in_channels[0] * width),
int(in_channels[0] * width),
round(3 * depth),
False,
depthwise = depthwise,
act = act,
)
#-------------------------------------------#
# 80, 80, 256 -> 40, 40, 256
#-------------------------------------------#
self.bu_conv2 = Conv(int(in_channels[0] * width), int(in_channels[0] * width), 3, 2, act=act)
#-------------------------------------------#
# 40, 40, 256 -> 40, 40, 512
#-------------------------------------------#
self.C3_n3 = CSPLayer(
int(2 * in_channels[0] * width),
int(in_channels[1] * width),
round(3 * depth),
False,
depthwise = depthwise,
act = act,
)
#-------------------------------------------#
# 40, 40, 512 -> 20, 20, 512
#-------------------------------------------#
self.bu_conv1 = Conv(int(in_channels[1] * width), int(in_channels[1] * width), 3, 2, act=act)
#-------------------------------------------#
# 20, 20, 1024 -> 20, 20, 1024
#-------------------------------------------#
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):
out_features = self.backbone.forward(input)
[feat1, feat2, feat3] = [out_features[f] for f in self.in_features]
#-------------------------------------------#
# 20, 20, 1024 -> 20, 20, 512
#-------------------------------------------#
P5 = self.lateral_conv0(feat3)
#-------------------------------------------#
# 20, 20, 512 -> 40, 40, 512
#-------------------------------------------#
P5_upsample = self.upsample(P5)
#-------------------------------------------#
# 40, 40, 512 + 40, 40, 512 -> 40, 40, 1024
#-------------------------------------------#
P5_upsample = torch.cat([P5_upsample, feat2], 1)
#-------------------------------------------#
# 40, 40, 1024 -> 40, 40, 512
#-------------------------------------------#
P5_upsample = self.C3_p4(P5_upsample)
#-------------------------------------------#
# 40, 40, 512 -> 40, 40, 256
#-------------------------------------------#
P4 = self.reduce_conv1(P5_upsample)
#-------------------------------------------#
# 40, 40, 256 -> 80, 80, 256
#-------------------------------------------#
P4_upsample = self.upsample(P4)
#-------------------------------------------#
# 80, 80, 256 + 80, 80, 256 -> 80, 80, 512
#-------------------------------------------#
P4_upsample = torch.cat([P4_upsample, feat1], 1)
#-------------------------------------------#
# 80, 80, 512 -> 80, 80, 256
#-------------------------------------------#
P3_out = self.C3_p3(P4_upsample)
#-------------------------------------------#
# 80, 80, 256 -> 40, 40, 256
#-------------------------------------------#
P3_downsample = self.bu_conv2(P3_out)
#-------------------------------------------#
# 40, 40, 256 + 40, 40, 256 -> 40, 40, 512
#-------------------------------------------#
P3_downsample = torch.cat([P3_downsample, P4], 1)
#-------------------------------------------#
# 40, 40, 256 -> 40, 40, 512
#-------------------------------------------#
P4_out = self.C3_n3(P3_downsample)
#-------------------------------------------#
# 40, 40, 512 -> 20, 20, 512
#-------------------------------------------#
P4_downsample = self.bu_conv1(P4_out)
#-------------------------------------------#
# 20, 20, 512 + 20, 20, 512 -> 20, 20, 1024
#-------------------------------------------#
P4_downsample = torch.cat([P4_downsample, P5], 1)
#-------------------------------------------#
# 20, 20, 1024 -> 20, 20, 1024
#-------------------------------------------#
P5_out = self.C3_n4(P4_downsample)
return (P3_out, P4_out, P5_out)
我们可以获得三个加强特征,这三个加强特征的shape分别为(20,20,1024)、(40,40,512)、(80,80,256),然后我们利用这三个shape的特征层传入Yolo Head获得预测结果。
YoloX中的YoloHead与之前版本的YoloHead不同。以前版本的Yolo所用的解耦头是一起的,也就是分类和回归在一个1X1卷积里实现,YoloX认为这给网络的识别带来了不利影响。在YoloX中,Yolo Head被分为了两部分,分别实现,最后预测的时候才整合在一起。
对于每一个特征层,我们可以获得三个预测结果,分别是:
1、Reg(h,w,4)用于判断每一个特征点的回归参数,回归参数调整后可以获得预测框。
2、Obj(h,w,1)用于判断每一个特征点是否包含物体。
3、Cls(h,w,num_classes)用于判断每一个特征点所包含的物体种类。
将三个预测结果进行堆叠,每个特征层获得的结果为:
Out(h,w,4+1+num_classses)前四个参数用于判断每一个特征点的回归参数,回归参数调整后可以获得预测框;第五个参数用于判断每一个特征点是否包含物体;最后num_classes个参数用于判断每一个特征点所包含的物体种类。
以输出结果为(20,20,1024)为例子,先经过一次1*1的卷积操作,使得通道数降维256,两个分支并行操作分别是分类和回归,得到三个结果:
1、Reg预测结果,此时卷积的通道数为4,最终结果为(20,20,4)。其中的4可以分为两个2,第一个2是预测框的中心点相较于该特征点的偏移情况,第二个2是预测框的宽高相较于对数指数的参数
2、Obj预测结果,此时卷积的通道数为1,最终结果为(20,20,1),代表每一个特征点预测框内部包含物体的概率。
3、Cls预测结果,此时卷积的通道数为num_classes,最终结果为(20,20,num_classes),代表每一个特征点对应某类物体的概率,最后一维度num_classes中的预测值代表属于每一个类的概率;
以输出结果20,20为例子就是将图片分程20*20个网格,如果某个特征点落在哪个网格内,就预测该物体
其中reg中的4前面两个偏移量,后面两个参数就是下图中红色点相对于黑色点的偏移量,分别在1,2,3个网格中的偏移情况
reg后面两个参数经过求指数是下图中红色框的预测值了,对应红色框的长和宽
在经过于之前yolo系列一样的非极大值抑制,根究IOU值最终选择得分最高的预测框。
计算loss实际上是网络的预测结果和网络的真实结果的对比。
yoloX跟之前的yolo系列是一样的,也是大体上是三个框架,分别是定位的损失reg部分,obj是正样本的判定部分损失,cls是我们的分类损失
和网络的预测结果一样,网络的损失也由三个部分组成,分别是Reg部分、Obj部分、Cls部分。Reg部分是特征点的回归参数判断、Obj部分是特征点是否包含物体判断、Cls部分是特征点包含的物体的种类。
在yolox之中,不像之前的yolo系列,直接进行IOU设定阈值,如果大于阈值就是正样本,小于就是负样本。那么在yoloX之中,我们会引入simOTA这个概念来判断我们的正样本。
因为yolo X不像之前的系列会采用anchor的形式来进行调整预测狂,我们是通过reg预测直接预测4个值,分别为偏移量和预测狂的长和宽。
这里就是指YOLOX当中采用anchor free的思想。没有了先验矿的思想
在YoloX中,我们会计算一个Cost代价矩阵,代表每个真实框和每个特征点之间的代价关系,Cost代价矩阵由三个部分组成:
1、每个真实框和当前特征点预测框的重合程度;
2、每个真实框和当前特征点预测框的种类预测准确度;
3、每个真实框的中心是否落在了特征点的一定半径内。
每个真实框和当前特征点预测框的重合程度越高,代表这个特征点已经尝试去拟合该真实框了,因此它的Cost代价就会越小。
每个真实框和当前特征点预测框的种类预测准确度越高,也代表这个特征点已经尝试去拟合该真实框了,因此它的Cost代价就会越小。
每个真实框的中心如果落在了特征点的一定半径内,代表这个特征点应该去拟合该真实框,因此它的Cost代价就会越小。
Cost代价矩阵的目的是自适应的找到当前特征点应该去拟合的真实框,重合度越高越需要拟合,分类越准越需要拟合,在一定半径内越需要拟合。
在SimOTA中,不同目标设定不同的正样本数量(dynamick),以旷视科技官方回答中的蚂蚁和西瓜为例子,传统的正样本分配方案常常为同一场景下的西瓜和蚂蚁分配同样的正样本数,那要么蚂蚁有很多低质量的正样本,要么西瓜仅仅只有一两个正样本。对于哪个分配方式都是不合适的。
动态的正样本设置的关键在于如何确定k,SimOTA具体的做法是首先计算每个目标Cost最低的10特征点,然后把这十个特征点对应的预测框与真实框的IOU加起来求得最终的k。
因此,SimOTA的过程总结如下:
1、计算每个真实框和当前特征点预测框的重合程度。
2、计算将重合度最高的十个预测框与真实框的IOU加起来求得每个真实框的k,也就代表每个真实框有k个特征点与之对应。
3、计算每个真实框和当前特征点预测框的种类预测准确度。
4、判断真实框的中心是否落在了特征点的一定半径内。
5、计算Cost代价矩阵。
6、将Cost最低的k个点作为该真实框的正样本。
以上就是yolo x的主要i特征了。
分配方式都是不合适的。**
动态的正样本设置的关键在于如何确定k,SimOTA具体的做法是首先计算每个目标Cost最低的10特征点,然后把这十个特征点对应的预测框与真实框的IOU加起来求得最终的k。
因此,SimOTA的过程总结如下:
1、计算每个真实框和当前特征点预测框的重合程度。
2、计算将重合度最高的十个预测框与真实框的IOU加起来求得每个真实框的k,也就代表每个真实框有k个特征点与之对应。
3、计算每个真实框和当前特征点预测框的种类预测准确度。
4、判断真实框的中心是否落在了特征点的一定半径内。
5、计算Cost代价矩阵。
6、将Cost最低的k个点作为该真实框的正样本。
以上就是yolo x的主要特征了。欢迎补充