1.2 参数读取执行train_net.py-->
args = default_argument_parser().parse_args()加载参数-->
launch函数判断是否为多gpu训练,从launch末尾进入train_net的main函数-->
main函数主要是读取参数,同时判断是否先进入test(不进入)。之后根据从trainer = Trainer(cfg)进入Trainer类,同样是理解读取参数,具体操作包括加载数据集,模型,优化器等,
之后回到main函数-->
根据return trainer.train()函数进入Trainer类里的train函数-->
从self.train函数进入train_net.py的self.train_loop函数(开始训练)-->
训练分为三部分,如下:
self.before_step()
self.run_step()
self.after_step()
其中,hook函数只放在before和after中,而run函数专门负责训练,即执行如下语句
self._trainer.run_step() #位于defaults.py第495行
值得一提的是,根据公约,计算比较复杂的操作一般放在after_step()函数中,而不放在before_step中。
训练所需参数读取自/AdelaiDet/configs/SOLOv2/R50_3x.yaml文件,同时这个文件还继承自/AdelaiDet/configs/SOLOv2/base-SOLOv2.yaml文件。后者中与前者相同的信息会被前者覆盖。但是实际训练用到的参数远多于这些,这些没有在上文提及的参数取自文件/AdelaiDet/adet/config/defaults.py。
1.3 Trainer(cfg)部分trainer=Trainer(cfg)是将定义在train_net.py的Trainer类实例化的过程。这个类是继承自DefaultTrainer类,又继承自Trainerbase类(这两个被集成的类都有__init__方法而Trainer类没有)。因此会先执行他们的__init__方法。
在DefaultTrainer(&Trainerbase)类的__init__方法会进行一些必要的操作,包括上文提到的,根据cfg加载数据集,模型,优化器,保存权重轮数,钩子等。如下:
def __init__(self, cfg): """ Args: cfg (CfgNode): """ super().__init__() logger = logging.getLogger("detectron2") if not logger.isEnabledFor(logging.INFO): # setup_logger is not called for d2 setup_logger() cfg = DefaultTrainer.auto_scale_workers(cfg, comm.get_world_size()) # Assume these objects must be constructed in this order. model = self.build_model(cfg) # 得到SOLOV2的模型,搭积木核心 optimizer = self.build_optimizer(cfg, model) # 选择优化器 data_loader = self.build_train_loader(cfg) #数据加载器,保存了图片的信息。 训练时直接传入这三个参数即可 model = create_ddp_model(model, broadcast_buffers=False) #如果是多进程,则对model进行修改。这里默认是不修改(return model) self._trainer = (AMPTrainer if cfg.SOLVER.AMP.ENABLED else SimpleTrainer)( #执行trainer的初始化,也就是run.step函数所在类的init部分 model, data_loader, optimizer ) self.scheduler = self.build_lr_scheduler(cfg, optimizer) #从配置中调度lr程序。 self.checkpointer = DetectionCheckpointer( # Assume you want to save checkpoints together with logs/statistics model, cfg.OUTPUT_DIR, trainer=weakref.proxy(self), ) # self.checkpointer.save("model_999") self.start_iter = 0 self.max_iter = cfg.SOLVER.MAX_ITER #最大轮数 self.cfg = cfg self.register_hooks(self.build_hooks())
Trainer(cfg)有几个值得一提的函数,接下来将详细介绍他们。
1.3.0 self.build_model(cfg)函数这句话是读取参数的核心,用于构建模型。内容较多,之后在soloV2.py一节有详细阐述。
1.3.1 build_lr_scheduler函数这个函数的意义是从配置中构建LR调度程序,语句如下:
self.scheduler = self.build_lr_scheduler(cfg, optimizer)
这个函数定义在build.py,主要分为两部分。
1.3.1.1 sched = MultiStepParamScheduler函数
第一部分为sched = MultiStepParamScheduler()部分,这部分根据输入的各部分轮数(非base.yaml)得到了每部分的步长。在原版里,输入的是(210000,250000),270000(MAX),各部分间步长为1,0.1,0.01。(但我没在yaml文件中找到0.1的参数,可能是默认的)
sched = MultiStepParamScheduler( #得到每一段的步长。信息输入取自R50_3x.yaml配置文件。 文件中三个数210000,250000,270000,三段间步长分别为1,0.1,0.01 这个步长我没在yaml文件中找到,可能是默认的 values=[cfg.SOLVER.GAMMA ** k for k in range(len(steps) + 1)], # 每一段为0.1的n次方。n为段数,也就是1,0.1,0.01 milestones=steps, num_updates=cfg.SOLVER.MAX_ITER, )
1.3.1.2sched = WarmupParamScheduler()函数
第二部分为sched = WarmupParamScheduler()部分,这部分根据输入的各部分预热轮数(base.yaml)得到了各预热部分的步长,并对sched进行修改(相当于把前面那一小点儿变为预热训练)。
class WarmupParamScheduler(CompositeParamScheduler): """ Add an initial warmup stage to another scheduler. """ def __init__( self, scheduler: ParamScheduler, warmup_factor: float, #预热的初始值 warmup_length: float, #预热长度所占比例(乘以总长度即为预热长度) warmup_method: str = "linear", #预热方法:线性 ): """ Args: scheduler: warmup will be added at the beginning of this scheduler warmup_factor: the factor w.r.t the initial value of ``scheduler``, e.g、0.001 warmup_length: the relative length (in [0, 1]) of warmup steps w.r.t the entire training, e.g、0.01 warmup_method: one of "linear" or "constant" """ end_value = scheduler(warmup_length) # 预热结束时步长(不确定是实际步长还是步长与正常步长比值)the value to reach when warmup ends start_value = warmup_factor * scheduler(0.0) # 预热开始时步长 if warmup_method == "constant": warmup = ConstantParamScheduler(start_value) elif warmup_method == "linear": warmup = LinearParamScheduler(start_value, end_value) else: raise ValueError("Unknown warmup method: {}".format(warmup_method)) super().__init__( [warmup, scheduler], interval_scaling=["rescaled", "fixed"], lengths=[warmup_length, 1 - warmup_length], )
1.3.2 register_hooks函数DefaultTrainer类的__init__方法最后一句为登记钩子,即:
self.register_hooks(self.build_hooks())
DefaultTrainer类有自己的build_hooks()方法,但我们在Trainer类中重写了这个方法,所以程序会执行我们重写的方法(但我们重写的这个方法有继承自原来的方法,所以从结果上看是两个方法都执行了)
在DefaultTrainer类中,程序默认注册了六个钩子,如下:
hooks.IterationTimer(), #计算花费时间并在训练结束时打印总结 hooks.LRScheduler(), #torch内置LR调度程序 hooks.PreciseBN( # Run at the same freq as (but before) evaluation. cfg.TEST.eval_PERIOD, self.model, # Build a new data loader to not affect training self.build_train_loader(cfg), cfg.TEST.PRECISE_BN.NUM_ITER, ) #据说和BN层有关,有啥关我没太看懂 每period执行一轮(但是默认不加载这个hook) hooks.PeriodicCheckpointer(self.checkpointer, cfg.SOLVER.CHECKPOINT_PERIOD) #定期保存检查点(权重) 每period(默认5000)执行一轮 或者大于等于所设置的最高轮数时保存 # 这里我为了方便观察先将5000改为了1000 hooks.evalHook(cfg.TEST.eval_PERIOD, test_and_save_results) #定期进行评价每period(默认0轮,即不执行)执行一轮 hooks.PeriodicWriter(self.bu,ld_writers(), period=20) #定期将事件写入EventStorage 就是输出的loss等信息(调用write.write函数) #每period(默认20轮)输出一次信息
1.4 run_step部分这一部分对应4.1.1中提到的语句
def run_step(self): self._trainer.iter = self.iter self._trainer.run_step() #位于defaults.py第495行
其核心语句self._trainer.run_step()如下
def run_step(self): """ Implement the standard training logic described above. """ assert self.model.training, "[SimpleTrainer] model was changed to eval mode!" #判断是否处于training模式,否则输出: "[SimpleTrainer] model was changed to eval mode!" start = time.perf_counter() """ If you want to do something with the data, you can wrap the dataloader. """ data = next(self._data_loader_iter) #得到图片信息(路径,长宽,像素信息等) data_time = time.perf_counter() - start """ If you want to do something with the losses, you can wrap the model. """ loss_dict = self.model(data) #得到loss if isinstance(loss_dict, torch.Tensor): losses = loss_dict loss_dict = {"total_loss": loss_dict} else: losses = sum(loss_dict.values()) """ If you need to accumulate gradients or do something similar, you can wrap the optimizer with your custom `zero_grad()` method. """ self.optimizer.zero_grad() #清空原梯度 losses.backward() # 反向传播 self._write_metrics(loss_dict, data_time) #输出loss信息 """ If you need gradient clipping/scaling or other processing, you can wrap the optimizer with your custom `step()` method、But it is suboptimal as explained in https://arxiv.org/abs/2006.15704 Sec 3.2.4 """ self.optimizer.step()
二、solov2.py 2.1 总体流程放上一位大佬绘制的流程框图(红笔为笔者写的注释):
其中backbone包括resnet部分和FPN部分。结束时输出FPN的五层。
三条支路从左到右分别对应kernel预测(卷积核),cate预测(类别)和mask预测(实例)。
训练过程中分支分为ins分支和mask分支,ins分支得到的是kernel预测和cate预测,mask分支得到的是mask预测(而在loss输入里,又把mask预测看做ins预测)(注意,ins分支和ins预测没有任何关系)
最终cate预测得到类别信息(loss_cate),kernel预测和mask预测(即mask预测得到特征图,经过kernel一次卷积得到结果)共同得到掩码信息(loss_ins)
参考文献:
solov2网络结构图_Jumi爱笑笑的博客-CSDN博客_solov2网络结构
2.2 具体流程在solov2.py中,一共定义了三个类,分别是SOLOv2,SOLOv2InsHead,SOLOv2MaskHead。其中SOLOV2类内容较多,包括损失函数等。而其余两类只有__init__方法和forward方法。接下来将他们三个的介绍分别写在三个小节里(2.3-2.5)。具体的运算,难点等将在各自的模块中提及。本小节只负责介绍在训练过程中数据流动的总体流程,并简要概括一些操作。
2.2.1 __init__部分在执行1.3.0 self.build_model(cfg)函数时,会对SOLOv2类进行初始化(__init__)操作。具体各参数的注释写在了代码上。在初始化solov2类的过程中,有如下几行代码
# build the ins head. instance_shapes = [backbone_shape[f] for f in self.instance_in_features] self.ins_head = SOLOv2InsHead(cfg, instance_shapes) # build the mask head. mask_shapes = [backbone_shape[f] for f in self.mask_in_features] self.mask_head = SOLOv2MaskHead(cfg, mask_shapes)
在调用他们的过程中,同时对剩下两个类SOLOv2InsHead和SOLOv2MaskHead进行了初始化。
2.2.2 forward部分之后程序一直没有走进solov2.py,直到执行到函数1.4 run_step时,标志着训练正式开始。此时步入SOLOV2类的forward方法。
2.3 class:SOLOv2(核心) 2.3.1 __init__方法读取一些cfg中的参数,并通过函数
self.backbone = build_backbone(cfg)
构建主干网络 。
2.3.2 forward方法第一步:对图像进行预处理,即执行如下语句:
images = self.preprocess_image(batched_inputs)
其中,输入的batched_inputs包括了图片全部信息,而images是一个元素为tensor的list。通过此方法得到了图片预处理(包括归一化,转化均值方差等操作)过后的像素信息。
第二步:通过如下语句:
if "instances" in batched_inputs[0]: gt_instances = [x["instances"].to(self.device) for x in batched_inputs] elif "targets" in batched_inputs[0]: log_first_n( logging.WARN, "'targets' in the model inputs is now renamed to 'instances'!", n=10 ) gt_instances = [x["targets"].to(self.device) for x in batched_inputs] else: gt_instances = None
将图片的标注信息(如类别,gt框等)保存到gt_instances中。
第三步:通过如下语句得到p2-p6五个头
features = self.backbone(images.tensor)
第四步:ins分支(其实是cate和kernel部分)
ins分支执行如下语句
# ins branch ins_features = [features[f] for f in self.instance_in_features] ins_features = self.split_feats(ins_features) cate_pred, kernel_pred = self.ins_head(ins_features)
通过函数self.split_feats()对FPN得到的五层中最上层进行下采样,最下层进行上采样,如下(不同图片尺寸对应数据可能不同):
( 1,256,176,240)-->( 1,256,88,120)
( 1,256,88,120)-->( 1,256,88,120)
( 1,256,44,60)-->( 1,256,44,60)
( 1,256,22,30)-->( 1,256,22,30)
( 1,256,11,15)-->( 1,256,22,30)
然后通过self.ins_head()语句得到对种类(cate)和卷积核(kernel)的预测(详见2.4.2 )
第五步:mask分支
mask分支执行如下语句
# mask branch mask_features = [features[f] for f in self.mask_in_features] mask_pred = self.mask_head(mask_features)
通过self.mask_head得到对掩码(mask)的预测(详见2.5.2 )
第六步:判断模型是训练还是评价。如果是训练则计算损失函数并返回loss,如果是评价则进行inference并返回result。
2.4 class:SOLOv2InsHead 2.4.1 __init__方法这里__init__方法的作用就是构建网络。在调用__init__ 方法时,用到了如下语句
head_configs = {"cate": (cfg.MODEL.SOLOV2.NUM_INSTANCE_CONVS, #创建一个字典 cfg.MODEL.SOLOV2.USE_DCN_IN_INSTANCE, False), "kernel": (cfg.MODEL.SOLOV2.NUM_INSTANCE_CONVS, cfg.MODEL.SOLOV2.USE_DCN_IN_INSTANCE, cfg.MODEL.SOLOV2.USE_COORD_CONV) }
创造了一个字典。在之后又用了如下语句
for head in head_configs:
为两个分支(cate和kernel)添加卷积层。添加的结果如下
cate:256-->512-->512-->512-->80 kernel:258-->512-->512-->512-->256 (这里的258应该是256后面加入了xy坐标)
之后都是些辅助性步骤,对初始权重进行填充。构建出的网络如下(应该分别对应上图的中间支路和左支路)
2.4.2 forward方法SOLOv2InsHead(
(cate_tower): Sequential(
(0): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(1): GroupNorm(32, 512, eps=1e-05, affine=True)
(2): ReLU(inplace=True)
(3): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(4): GroupNorm(32, 512, eps=1e-05, affine=True)
(5): ReLU(inplace=True)
(6): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(7): GroupNorm(32, 512, eps=1e-05, affine=True)
(8): ReLU(inplace=True)
(9): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(10): GroupNorm(32, 512, eps=1e-05, affine=True)
(11): ReLU(inplace=True)
)
(kernel_tower): Sequential(
(0): Conv2d(258, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(1): GroupNorm(32, 512, eps=1e-05, affine=True)
(2): ReLU(inplace=True)
(3): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(4): GroupNorm(32, 512, eps=1e-05, affine=True)
(5): ReLU(inplace=True)
(6): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(7): GroupNorm(32, 512, eps=1e-05, affine=True)
(8): ReLU(inplace=True)
(9): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(10): GroupNorm(32, 512, eps=1e-05, affine=True)
(11): ReLU(inplace=True)
)
(cate_pred): Conv2d(512, 80, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(kernel_pred): Conv2d(512, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
)
从2.3.2 第四步 ins分支导入。方法都比较简单,每一步注释都写在后面了:
def forward(self, features): """ Arguments: features (list[Tensor]): FPN feature map tensors in high to low resolution. Each tensor in the list correspond to different feature levels. Returns: pass """ cate_pred = [] kernel_pred = [] for idx, feature in enumerate(features): # feature返回每个元素,idx返回每个元素的索引 ins_kernel_feat = feature # concat coord x_range = torch.linspace(-1, 1, ins_kernel_feat.shape[-1], device=ins_kernel_feat.device) #等间距生成从-1到1的120个数(即对应层宽度) y_range = torch.linspace(-1, 1, ins_kernel_feat.shape[-2], device=ins_kernel_feat.device) #等间距生成从-1到1的88个数(即对应层高度) y, x = torch.meshgrid(y_range, x_range) #用于划分单元网格,生成shape为(88,120),各行列分别被二者(y_range, x_range)对应元素填充的tensor 参考文献:https://blog.csdn.net/weixin_39504171/article/details/106356977 y = y.expand([ins_kernel_feat.shape[0], 1, -1, -1]) x = x.expand([ins_kernel_feat.shape[0], 1, -1, -1]) # 在最前面加两个一维,即(88,120)->(1,1,88,120) coord_feat = torch.cat([x, y], 1) # (1,1,88,120)-->(1,2,88,120) ins_kernel_feat = torch.cat([ins_kernel_feat, coord_feat], 1)# (1,1,88,120)-->(1,258,88,120) 这一段的作用就是将坐标xy添加到最后两个通道 # individual feature. kernel_feat = ins_kernel_feat seg_num_grid = self.num_grids[idx] #self.num_grids是一个列表,这里根据idx索引找到列表对应元素 应该是对不同层设置的grid个数 kernel_feat = F.interpolate(kernel_feat, size=seg_num_grid, mode='bilinear') #双线性插值,(1,1,88,120)-->(1,1,40,40) cate_feat = kernel_feat[:, :-2, :, :] #抓出前256维,即(1,256,40,40) # kernel kernel_feat = self.kernel_tower(kernel_feat) kernel_pred.append(self.kernel_pred(kernel_feat)) #执行针对kernel的卷积并保存结果 # cate cate_feat = self.cate_tower(cate_feat) cate_pred.append(self.cate_pred(cate_feat)) #执行针对cate的卷积并保存结果 return cate_pred, kernel_pred
2.5 class:SOLOv2MaskHead 2.4.1 __init__方法类似的,这里只用到了P2-P5四层,通过如下代码段构建神经网络层
for i in range(self.num_levels):
这里一共两个循环(i,j),创建了三个存储神经网络的列表,分别是
conv_tower:每个小循环刷新 (list类型)convs_per_level:每个大循环刷新 (sequential类型)self.convs_all_levels:不刷新 (modulelist类型)(最终保存的)
j循环比i循环小1;
当i=0时或j=0时,都进行一些相对特殊的操作;
此外,当i=3时,输入通道+2。
总之,通过这段代码构建出了如下网络(应该对应上图最右支路)
2.4.2 forward方法ModuleList(
(0): Sequential(
(conv0): Sequential(
(0): Conv2d(256, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(1): GroupNorm(32, 128, eps=1e-05, affine=True)
(2): ReLU()
)
)
(1): Sequential(
(conv0): Sequential(
(0): Conv2d(256, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(1): GroupNorm(32, 128, eps=1e-05, affine=True)
(2): ReLU()
)
(upsample0): Upsample(scale_factor=2.0, mode=bilinear)
)
(2): Sequential(
(conv0): Sequential(
(0): Conv2d(256, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(1): GroupNorm(32, 128, eps=1e-05, affine=True)
(2): ReLU()
)
(upsample0): Upsample(scale_factor=2.0, mode=bilinear)
(conv1): Sequential(
(0): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(1): GroupNorm(32, 128, eps=1e-05, affine=True)
(2): ReLU()
)
(upsample1): Upsample(scale_factor=2.0, mode=bilinear)
)
(3): Sequential(
(conv0): Sequential(
(0): Conv2d(258, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(1): GroupNorm(32, 128, eps=1e-05, affine=True)
(2): ReLU()
)
(upsample0): Upsample(scale_factor=2.0, mode=bilinear)
(conv1): Sequential(
(0): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(1): GroupNorm(32, 128, eps=1e-05, affine=True)
(2): ReLU()
)
(upsample1): Upsample(scale_factor=2.0, mode=bilinear)
(conv2): Sequential(
(0): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(1): GroupNorm(32, 128, eps=1e-05, affine=True)
(2): ReLU()
)
(upsample2): Upsample(scale_factor=2.0, mode=bilinear)
)
)
从2.3.2 第五步 mask分支导入。
如上图所说,先对FPN输出的前四层各自卷积,并上采样到跟第一层一样的大小(1,128,176,240)然后相加(应该是对应元素相加,不是concat) 。
def forward(self, features): """ Arguments: features (list[Tensor]): FPN feature map tensors in high to low resolution. Each tensor in the list correspond to different feature levels. Returns: pass """ assert len(features) == self.num_levels, print("The number of input features should be equal to the supposed level.") # bottom features first.,对FPN输出的前四层各自卷积,并上采样到跟第一层一样的大小(1,128,176,240)然后相加(应该是对应元素相加,不是concat) feature_add_all_level = self.convs_all_levels[0](features[0]) # 第一层初始(1,256,176,240) 注意这里和ins分支不同,因为没有经过spilt_feats的下采样 for i in range(1, self.num_levels): # 2-4层初始分别为(...,88,120,(...,44,60),(...,22,30) mask_feat = features[i] # 值得一提的是,第四层卷积前还加入了xy两个通道(第四层设计的卷积输入本来就是258,不影响输出), if i == 3: # add for coord. x_range = torch.linspace(-1, 1, mask_feat.shape[-1], device=mask_feat.device) y_range = torch.linspace(-1, 1, mask_feat.shape[-2], device=mask_feat.device) y, x = torch.meshgrid(y_range, x_range) y = y.expand([mask_feat.shape[0], 1, -1, -1]) x = x.expand([mask_feat.shape[0], 1, -1, -1]) coord_feat = torch.cat([x, y], 1) mask_feat = torch.cat([mask_feat, coord_feat], 1) # add for top features. feature_add_all_level += self.convs_all_levels[i](mask_feat) mask_pred = self.conv_pred(feature_add_all_level) return mask_pred
4.2 test部分 4.1.1 数据流动执行train_net.py-->
args = default_argument_parser().parse_args()加载参数-->
launch函数判断是否为多gpu训练,从launch末尾进入train_net的main函数-->
main函数主要是读取参数,同时判断是否先进入test(进入)-->
通过Trainer.build_model函数加载被评价的model信息和超参数信息(比如各个层是干什么的,不包括加载权重参数weights)-->
通过AdetCheckpointer函数向model加载权重信息-->
之后由main函数进入python3.7/site-packages/detectron2/engine/defaults.py的test函数,根据输入的model,cfg和评价器类型(默认NONE)进行评价-->
如果评价器为NONE,则会进入train_net.py的build_evaluatior函数帮你构建评价器。这个函数会根据你在builtin中的注册,来选择合适的评价器(所以千万不要注册错)
之后进入detectron2/evalution/evaluator.py的inference_on_dataset函数,通过model函数运行得到分割结果并进入评价阶段-->
评价阶段用的是evaluator.process(inputs, outputs) 函数,定义在adet/evalution/text_evaluation.py中。其中input是原照片的信息,output是预测分割的结果(为什么没有原图分割信息?)-->
之后从这个函数进入同py文件中的instances_to_coco_json函数。这个函数作用应该是理解得到的参数。但是会报bug3.2.2。因为