中山高端网站建设,网站开发找聚脑网,用手机搭建wordpress,网络电商平台前面研究了一下YOLOX的网络结构#xff0c;在YOLOv5(tag7.0)集成了yolox的骨干网络#xff0c;现在继续下一步集成YOLOX的head模块。YOLOX的head模块是双分支解耦合网络#xff0c;把目标置信度的预测和目标的位置预测分成两条支路#xff0c;并验证双分支解耦合头性能要优…前面研究了一下YOLOX的网络结构在YOLOv5(tag7.0)集成了yolox的骨干网络现在继续下一步集成YOLOX的head模块。YOLOX的head模块是双分支解耦合网络把目标置信度的预测和目标的位置预测分成两条支路并验证双分支解耦合头性能要优于单分支耦合头。
1、关于双分支解耦合头decouplehead
主要是个人理解。这里的双分支解耦合和单分支耦合是根据网络的结构和承担的任务来分配的具体地讲就是head模块是几条之路如果是一条支路那目标的类别和位置预测任务就都在这一条支路上进行类别分类和位置预测使用同一套权重进行预测对于这两个任务来说这种单分支网络结构是耦合的我理解的就是绑定在一块经过同一个结构得到不同任务的结果。
基于任务特点一个是关注类别信息一个是关注位置信息在不同的支路上完成这两个任务这样的网络就可以针对不同任务独享各自的权重但这两个任务都是为了表达同一个目标的信息是什么在哪里所以这种双分支的结构对于同一个目标来说是解耦合的decouple.
了解上述区别后而只能使用单分支的yolov5已经性能显著了。那加上双分支是不是效果会更好呢这非常让人期待于是实践操作一下就可以进行验证亲自感受一下。而且yolov8使用的也是解耦合头和anchor free方式取得了更好的效果这也让我们看到了做这件事的意义。
在实践操作的过程中我发现直接在yolov5中复现yolox的head有点繁琐因为需要直接把 单分支、anchor-based 改为 双分支、anchoe-free所以我先把任务拆分先实现 双分支、anchor-based.
2、YOLOX的head结构
这个可以直接看yolox的官方代码找到YOLOXHead的定义代码。 其中__init__ 定义了head模块的基础结构forward则是定义网络结构连接的方式用以约束数据运算。
class YOLOXHead(nn.Module):def __init__(self,num_classes,width1.0,strides[8, 16, 32],in_channels[256, 512, 1024],actsilu,depthwiseFalse,):Args:act (str): activation type of conv. Defalut value: silu.depthwise (bool): whether apply depthwise conv in conv branch. Defalut value: False.super().__init__()self.num_classes num_classesself.decode_in_inference True # for deploy, set to Falseself.cls_convs nn.ModuleList()self.reg_convs nn.ModuleList()self.cls_preds nn.ModuleList()self.reg_preds nn.ModuleList()self.obj_preds nn.ModuleList()self.stems nn.ModuleList()Conv DWConv if depthwise else BaseConvfor i in range(len(in_channels)):self.stems.append(BaseConv(in_channelsint(in_channels[i] * width),out_channelsint(256 * width),ksize1,stride1,actact,))self.cls_convs.append(nn.Sequential(*[Conv(in_channelsint(256 * width),out_channelsint(256 * width),ksize3,stride1,actact,),Conv(in_channelsint(256 * width),out_channelsint(256 * width),ksize3,stride1,actact,),]))self.reg_convs.append(nn.Sequential(*[Conv(in_channelsint(256 * width),out_channelsint(256 * width),ksize3,stride1,actact,),Conv(in_channelsint(256 * width),out_channelsint(256 * width),ksize3,stride1,actact,),]))self.cls_preds.append(nn.Conv2d(in_channelsint(256 * width),out_channelsself.num_classes,kernel_size1,stride1,padding0,))self.reg_preds.append(nn.Conv2d(in_channelsint(256 * width),out_channels4,kernel_size1,stride1,padding0,))self.obj_preds.append(nn.Conv2d(in_channelsint(256 * width),out_channels1,kernel_size1,stride1,padding0,))self.use_l1 Falseself.l1_loss nn.L1Loss(reductionnone)self.bcewithlog_loss nn.BCEWithLogitsLoss(reductionnone)self.iou_loss IOUloss(reductionnone)self.strides stridesself.grids [torch.zeros(1)] * len(in_channels)def initialize_biases(self, prior_prob):for conv in self.cls_preds:b conv.bias.view(1, -1)b.data.fill_(-math.log((1 - prior_prob) / prior_prob))conv.bias torch.nn.Parameter(b.view(-1), requires_gradTrue)for conv in self.obj_preds:b conv.bias.view(1, -1)b.data.fill_(-math.log((1 - prior_prob) / prior_prob))conv.bias torch.nn.Parameter(b.view(-1), requires_gradTrue)def forward(self, xin, labelsNone, imgsNone):outputs []origin_preds []x_shifts []y_shifts []expanded_strides []for k, (cls_conv, reg_conv, stride_this_level, x) in enumerate(zip(self.cls_convs, self.reg_convs, self.strides, xin)):x self.stems[k](x)cls_x xreg_x xcls_feat cls_conv(cls_x)cls_output self.cls_preds[k](cls_feat)reg_feat reg_conv(reg_x)reg_output self.reg_preds[k](reg_feat)obj_output self.obj_preds[k](reg_feat)从上述代码可知head模块根据前面输出的三个维度的特征图数据进行网络构建根据数据的维度具体定义网络的结构具体方法是通过一个 for 循环来构建基础的结构self.stem, self.cls_convs, self.reg_convs, self.cls_preds, self.reg_preds和self.obj_preds.通过看forward函数可以了解这些模块是怎么衔接的进而可以按照自己的方式灵活的复现出一样的网络结构。
单独实例化YOLOXHead()得到的网络结构
YOLOXHead((cls_convs): ModuleList((0): Sequential((0): BaseConv((conv): Conv2d(256, 256, kernel_size(3, 3), stride(1, 1), padding(1, 1), biasFalse)(bn): BatchNorm2d(256, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue))(1): BaseConv((conv): Conv2d(256, 256, kernel_size(3, 3), stride(1, 1), padding(1, 1), biasFalse)(bn): BatchNorm2d(256, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue)))(1): Sequential((0): BaseConv((conv): Conv2d(256, 256, kernel_size(3, 3), stride(1, 1), padding(1, 1), biasFalse)(bn): BatchNorm2d(256, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue))(1): BaseConv((conv): Conv2d(256, 256, kernel_size(3, 3), stride(1, 1), padding(1, 1), biasFalse)(bn): BatchNorm2d(256, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue)))(2): Sequential((0): BaseConv((conv): Conv2d(256, 256, kernel_size(3, 3), stride(1, 1), padding(1, 1), biasFalse)(bn): BatchNorm2d(256, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue))(1): BaseConv((conv): Conv2d(256, 256, kernel_size(3, 3), stride(1, 1), padding(1, 1), biasFalse)(bn): BatchNorm2d(256, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue))))(reg_convs): ModuleList((0): Sequential((0): BaseConv((conv): Conv2d(256, 256, kernel_size(3, 3), stride(1, 1), padding(1, 1), biasFalse)(bn): BatchNorm2d(256, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue))(1): BaseConv((conv): Conv2d(256, 256, kernel_size(3, 3), stride(1, 1), padding(1, 1), biasFalse)(bn): BatchNorm2d(256, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue)))(1): Sequential((0): BaseConv((conv): Conv2d(256, 256, kernel_size(3, 3), stride(1, 1), padding(1, 1), biasFalse)(bn): BatchNorm2d(256, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue))(1): BaseConv((conv): Conv2d(256, 256, kernel_size(3, 3), stride(1, 1), padding(1, 1), biasFalse)(bn): BatchNorm2d(256, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue)))(2): Sequential((0): BaseConv((conv): Conv2d(256, 256, kernel_size(3, 3), stride(1, 1), padding(1, 1), biasFalse)(bn): BatchNorm2d(256, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue))(1): BaseConv((conv): Conv2d(256, 256, kernel_size(3, 3), stride(1, 1), padding(1, 1), biasFalse)(bn): BatchNorm2d(256, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue))))(cls_preds): ModuleList((0): Conv2d(256, 80, kernel_size(1, 1), stride(1, 1))(1): Conv2d(256, 80, kernel_size(1, 1), stride(1, 1))(2): Conv2d(256, 80, kernel_size(1, 1), stride(1, 1)))(reg_preds): ModuleList((0): Conv2d(256, 4, kernel_size(1, 1), stride(1, 1))(1): Conv2d(256, 4, kernel_size(1, 1), stride(1, 1))(2): Conv2d(256, 4, kernel_size(1, 1), stride(1, 1)))(obj_preds): ModuleList((0): Conv2d(256, 1, kernel_size(1, 1), stride(1, 1))(1): Conv2d(256, 1, kernel_size(1, 1), stride(1, 1))(2): Conv2d(256, 1, kernel_size(1, 1), stride(1, 1)))(stems): ModuleList((0): BaseConv((conv): Conv2d(256, 256, kernel_size(1, 1), stride(1, 1), biasFalse)(bn): BatchNorm2d(256, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue))(1): BaseConv((conv): Conv2d(512, 256, kernel_size(1, 1), stride(1, 1), biasFalse)(bn): BatchNorm2d(256, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue))(2): BaseConv((conv): Conv2d(1024, 256, kernel_size(1, 1), stride(1, 1), biasFalse)(bn): BatchNorm2d(256, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue)))(l1_loss): L1Loss()(bcewithlog_loss): BCEWithLogitsLoss()(iou_loss): IOUloss()
)3、重构YOLOX的DecoupledHead
基于yolov5的网络构建方式和自己的理解重新根据网络的结构写了一下。我写的结构里 由于涉及到锚框所以这里还不是完全体的yolox的head结构只写了一条线的数据处理结构因为yolov5的框架head模块要加入锚框还会再定义。
class DecoupledHead(nn.Module):def __init__(self,in_channels[256, 512, 1024],num_classes80,width0.5,anchors(),actsilu,depthwiseFalse,prior_prob1e-2,):super().__init__()self.num_classes num_classes# self.nl len(anchors)self.na len(anchors[0]) // 2# self.in_channels in_channels# import ipdb;ipdb.set_trace()Conv DWConv if depthwise else BaseConvself.stemsBaseConv(in_channelsint(in_channels * width),out_channelsint(256 * width),ksize1,stride1,actact,)self.cls_convsnn.Sequential(Conv(in_channelsint(256 * width),out_channelsint(256 * width),ksize3,stride1,actact,),Conv(in_channelsint(256 * width),out_channelsint(256 * width),ksize3,stride1,actact,),)self.cls_predsnn.Conv2d(in_channelsint(256 * width),out_channelsself.num_classes * self.na,kernel_size1,stride1,padding0,)self.reg_convsnn.Sequential(Conv(in_channelsint(256 * width),out_channelsint(256 * width),ksize3,stride1,actact,),Conv(in_channelsint(256 * width),out_channelsint(256 * width),ksize3,stride1,actact,),)self.reg_predsnn.Conv2d(in_channelsint(256 * width),out_channels4 * self.na,kernel_size1,stride1,padding0,)self.obj_predsnn.Conv2d(in_channelsint(256 * width),out_channels1 * self.na,kernel_size1,stride1,padding0,)#没用上初始化函数def initialize_biases(self):prior_prob self.prior_probfor conv in self.cls_preds:b conv.bias.view(1, -1)b.data.fill_(-math.log((1 - prior_prob) / prior_prob))conv.bias torch.nn.Parameter(b.view(-1), requires_gradTrue)for conv in self.obj_preds:b conv.bias.view(1, -1)b.data.fill_(-math.log((1 - prior_prob) / prior_prob))conv.bias torch.nn.Parameter(b.view(-1), requires_gradTrue)def forward(self,x):# import ipdb;ipdb.set_trace()x self.stems(x)cls_x xreg_x xcls_feat self.cls_convs(cls_x)cls_output self.cls_preds(cls_feat)reg_feat self.reg_convs(reg_x)reg_output self.reg_preds(reg_feat)obj_output self.obj_preds(reg_feat)# out torch.cat([cls_output,reg_output,obj_output], 1)out torch.cat([reg_output,obj_output,cls_output], 1)return out
可以看到YOLOXHead的 init 和 forward() 都包含两个for循环分别用来构建head结构和处理数据为了避免麻烦和匹配yolov5的框架我写了单流程结构。通过后面再在yolo.py进一步实现yoloxhead的双分支结构。
yolo.py下再定义的头部结构代码
class DetectDcoupleHead(nn.Module):stride None # strides computed during builddynamic False # force grid reconstructionexport False # export modedef __init__(self, nc80, anchors(), width1.0, ch(), inplaceTrue):super().__init__()self.prior_prob 1e-2self.in_ch [256, 512, 1024]self.nc nc # number of classesself.width widthself.no nc 5 # number of outputs per anchorself.nl len(anchors) # number of detection layersself.na len(anchors[0]) // 2 # number of anchorsself.grid [torch.empty(0) for _ in range(self.nl)] # init gridself.anchor_grid [torch.empty(0) for _ in range(self.nl)] # init anchor gridself.register_buffer(anchors, torch.tensor(anchors).float().view(self.nl, -1, 2)) # shape(nl,na,2)# self.DecoupledHead DecoupledHead()self.m nn.ModuleList(DecoupledHead(x, self.nc, self.width, anchors) for x in self.in_ch) # output convself.inplace inplace # use inplace ops (e.g. slice assignment)def forward(self, x):z [] # inference output# import ipdb;ipdb.set_trace()for i in range(self.nl):x[i] self.m[i](x[i]) # convbs, _, ny, nx x[i].shape # x(bs,255,20,20) to x(bs,3,20,20,85)# import ipdb;ipdb.set_trace()x[i] x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()# if not self.training: # inferenceif self.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)if isinstance(self, Segment): # (boxes masks)xy, wh, conf, mask x[i].split((2, 2, self.nc 1, self.no - self.nc - 5), 4)xy (xy.sigmoid() * 2 self.grid[i]) * self.stride[i] # xywh (wh.sigmoid() * 2) ** 2 * self.anchor_grid[i] # why torch.cat((xy, wh, conf.sigmoid(), mask), 4)else: # Detect (boxes only)xy, wh, conf x[i].sigmoid().split((2, 2, self.nc 1), 4)xy (xy * 2 self.grid[i]) * self.stride[i] # xywh (wh * 2) ** 2 * self.anchor_grid[i] # why torch.cat((xy, wh, conf), 4)z.append(y.view(bs, self.na * nx * ny, self.no))return x if self.training else (torch.cat(z, 1),) if self.export else (torch.cat(z, 1), x)def _make_grid(self, nx20, ny20, i0, torch_1_10check_version(torch.__version__, 1.10.0)):d self.anchors[i].devicet self.anchors[i].dtypeshape 1, self.na, ny, nx, 2 # grid shapey, x torch.arange(ny, deviced, dtypet), torch.arange(nx, deviced, dtypet)yv, xv torch.meshgrid(y, x, indexingij) if torch_1_10 else torch.meshgrid(y, x) # torch0.7 compatibilitygrid torch.stack((xv, yv), 2).expand(shape) - 0.5 # add grid offset, i.e. y 2.0 * x - 0.5anchor_grid (self.anchors[i] * self.stride[i]).view((1, self.na, 1, 1, 2)).expand(shape)return grid, anchor_griddef initialize_biases(self):prior_prob self.prior_probfor i in range(self.nl):conv_cls self.m[i].cls_predsb conv_cls.bias.view(1, -1)b.data.fill_(-math.log((1 - prior_prob) / prior_prob))conv_cls.bias torch.nn.Parameter(b.view(-1), requires_gradTrue)conv_obj self.m[i].obj_predsb_obj conv_obj.bias.view(1, -1)b_obj.data.fill_(-math.log((1 - prior_prob) / prior_prob))conv_obj.bias torch.nn.Parameter(b_obj.view(-1), requires_gradTrue)在DetectDcoupleHead的定义中可以发现self.m是最终的yolox的双分支head结构它通过输入数据的维度分别重新构建了三个decoupledhead的结构效果完全与yoloxhead一致即下面这行代码
self.m nn.ModuleList(DecoupledHead(x, self.nc, self.width, anchors) for x in self.in_ch)另外定义DetectDcoupleHead可以更好的解决锚框铺设问题并且使得该头部模块可以在yaml配置文件中进行参数配置最终实现yoloxhead的双分支、anchor-based结构
想要可以训练还要再完善一些细节主要是yolo.py中的网络构建代码
parse_model中添加
elif m in {DetectDcoupleHead}:args是yaml配置文件的字典中每行的列表里模块后的参数# import ipdb;ipdb.set_trace()args.append([ch[x] for x in f])#append导致输入维度参数在最后一个位置if isinstance(args[1], int): # 锚框 number of anchorsargs[1] [list(range(args[1] * 2))] * len(f)DetectionModel中添加:
if isinstance(m, DetectDcoupleHead):s 256 # 2x min stridem.inplace self.inplaceforward lambda x: self.forward(x)[0] if isinstance(m, Segment) else self.forward(x)m.stride torch.tensor([s / x.shape[-2] for x in forward(torch.zeros(1, ch, s, s))]) # forwardcheck_anchor_order(m)m.anchors / m.stride.view(-1, 1, 1)self.stride m.stridem.initialize_biases()
做了上面的这些修改工作基本上就是把yolox的head结构复现到yolov5上了。如下是复现构建后的head模块的结构可以发现self.m与yoloxhead的结构是一致的。 self.m
ModuleList((0): DecoupledHead((stems): BaseConv((conv): Conv2d(128, 128, kernel_size(1, 1), stride(1, 1), biasFalse)(bn): BatchNorm2d(128, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue))(cls_convs): Sequential((0): BaseConv((conv): Conv2d(128, 128, kernel_size(3, 3), stride(1, 1), padding(1, 1), biasFalse)(bn): BatchNorm2d(128, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue))(1): BaseConv((conv): Conv2d(128, 128, kernel_size(3, 3), stride(1, 1), padding(1, 1), biasFalse)(bn): BatchNorm2d(128, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue)))(cls_preds): Conv2d(128, 3, kernel_size(1, 1), stride(1, 1))(reg_convs): Sequential((0): BaseConv((conv): Conv2d(128, 128, kernel_size(3, 3), stride(1, 1), padding(1, 1), biasFalse)(bn): BatchNorm2d(128, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue))(1): BaseConv((conv): Conv2d(128, 128, kernel_size(3, 3), stride(1, 1), padding(1, 1), biasFalse)(bn): BatchNorm2d(128, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue)))(reg_preds): Conv2d(128, 12, kernel_size(1, 1), stride(1, 1))(obj_preds): Conv2d(128, 3, kernel_size(1, 1), stride(1, 1)))(1): DecoupledHead((stems): BaseConv((conv): Conv2d(256, 128, kernel_size(1, 1), stride(1, 1), biasFalse)(bn): BatchNorm2d(128, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue))(cls_convs): Sequential((0): BaseConv((conv): Conv2d(128, 128, kernel_size(3, 3), stride(1, 1), padding(1, 1), biasFalse)(bn): BatchNorm2d(128, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue))(1): BaseConv((conv): Conv2d(128, 128, kernel_size(3, 3), stride(1, 1), padding(1, 1), biasFalse)(bn): BatchNorm2d(128, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue)))(cls_preds): Conv2d(128, 3, kernel_size(1, 1), stride(1, 1))(reg_convs): Sequential((0): BaseConv((conv): Conv2d(128, 128, kernel_size(3, 3), stride(1, 1), padding(1, 1), biasFalse)(bn): BatchNorm2d(128, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue))(1): BaseConv((conv): Conv2d(128, 128, kernel_size(3, 3), stride(1, 1), padding(1, 1), biasFalse)(bn): BatchNorm2d(128, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue)))(reg_preds): Conv2d(128, 12, kernel_size(1, 1), stride(1, 1))(obj_preds): Conv2d(128, 3, kernel_size(1, 1), stride(1, 1)))(2): DecoupledHead((stems): BaseConv((conv): Conv2d(512, 128, kernel_size(1, 1), stride(1, 1), biasFalse)(bn): BatchNorm2d(128, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue))(cls_convs): Sequential((0): BaseConv((conv): Conv2d(128, 128, kernel_size(3, 3), stride(1, 1), padding(1, 1), biasFalse)(bn): BatchNorm2d(128, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue))(1): BaseConv((conv): Conv2d(128, 128, kernel_size(3, 3), stride(1, 1), padding(1, 1), biasFalse)(bn): BatchNorm2d(128, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue)))(cls_preds): Conv2d(128, 3, kernel_size(1, 1), stride(1, 1))(reg_convs): Sequential((0): BaseConv((conv): Conv2d(128, 128, kernel_size(3, 3), stride(1, 1), padding(1, 1), biasFalse)(bn): BatchNorm2d(128, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue))(1): BaseConv((conv): Conv2d(128, 128, kernel_size(3, 3), stride(1, 1), padding(1, 1), biasFalse)(bn): BatchNorm2d(128, eps1e-05, momentum0.1, affineTrue, track_running_statsTrue)(act): SiLU(inplaceTrue)))(reg_preds): Conv2d(128, 12, kernel_size(1, 1), stride(1, 1))(obj_preds): Conv2d(128, 3, kernel_size(1, 1), stride(1, 1)))
)4、总结
其实做完这些工作回过头来发现事情其实很简单我们如果自己想要弄一个双分支的head结构直接自己写一个或者在原来的基础直接改就可以了把一些数据维度上让网络正常跑起来就行。这样做的话主要是一个学习理解的过程。 如下是本文复现工作的训练截图 训练结果还是有一定优势的双分支比单分支效果更好一些有高几个点。
后续我打算把双分支、anchor-free也实现一下。目前已经做了一点工作已经基本上跑起来了anchor-free其实目前的代码没什么特殊的预测的数据都基本上一样不过训练的是一种位置解码方式用一种方式来表达位置信息同时去掉生成锚框的操作但是会基于每个特征图的特征点进行预测本质和锚框也差不多。