Yolo2实现笔记(pytorch)

基于mileiston的github仓库,感谢m同学的无私分享。

 

 

label.py的制作逻辑

1.根据人工预设的几组year(如果2007,2012等)和预设的trainset(如 'train','val'),和预设的格式,组装出{trainset}.txt所在路径。

2.根据获得的路径,读取该pascalvoc数据集的{trainset}.txt文件,里面存着list of xml_paths。

3.根据获得的list of xml_paths,传入parser解析器(brambox.parser)。每个xml里面有img_id,annos等信息。每个anno里面又包含class和bbox等信息。

4.返回dict{'img_id': list_of_annos}

5.保存该dict为pkl文件

【brambox是lightnet的作者搞的另一个工具,解析几个常用数据集的文件。】

 

train.py里

1.根据传入的argument里的model_name,调用initEnv()方法

2.在initEnv()中,

->2.1先调用getConfig(),根据model_name进行yaml.load({对应modelname.yaml}),返回cur_cfg,是个yaml字典。

->2.2 向cur_cfg字典内加入 model_name 。

cur_cfg['model_name'] = model_name

->2.3根据yaml里的参数组合出三个dir路径。

work_dir = os.path.join(root_dir, model_name, version)
backup_dir = os.path.join(work_dir, backup_name)
log_dir = os.path.join(work_dir, log_name)

>建立对应的dir文件夹。

>再把backup_dir添加到cur_cfg字典中。(trainflag==1)

->2.4不明意义的gpu环境设置

gpus = cur_cfg['train']['gpus'] 

os.environ['CUDA_VISIBLE_DEVICES'] = gpus

->2.5完成上述工作后,调用combineConfig(cur_cfg, train_flag)。

这个函数的作用是根据trainflag,例如=1时,把test和speed都去掉,把train下面的参数的层级拔高一层。

举例:原来是cfg['train']['trainset']=xxx,现在是cfg['trainset']=xxx。

->2.6 initEnv() end,得到config字典。

 

3.根据config字典,实例化一个HyperParams类的对象。

->3.1 基本把config里面的所有{key:value}结构都转变成了self.key = value的结构。

->3.2 新增了几个attr,这是用户config的yaml中所不包含的。

self.jitter = 0.3
self.flip = 0.5
self.hue = 0.1
self.sat = 1.5
self.val = 1.5

self.rs_steps = []
self.rs_rates = []

->3.3 自适应self.cuda = True or False

->3.4根据cfg['labels']这个list,获得类别数量

self.classes = len(self.labels)

->3.5 HyperParams end。

主要是把key变成了attr,方便调用。又隐藏了几个参数,对用户不可见。

经过此步骤,我们获得hyper_params。

 

4.根据hyper_params,实例化一个VOCTrainingEngine类对象。

->4.1 部分attr赋值为self.attr

->4.2 搭建网络

根据model_name,在本地包models.__dict__内找到对应的model类。

net = models.__dict__[model_name](hyper_params.classes,
                hyper_params.weights,     # pre-trained文件路径
                train_flag=1,             # 1表示train mode ,2 表示test mode
                clear=hyper_params.clear) # clear表示是否重置self.seen

#本例中,是找到了class Yolov2(YoloABC):这个类,定义了backbone和head两部分。
#self.backbone = backbone.Darknet19()
#self.head = head.Yolov2(num_anchors=len(anchors_mask[0]), num_classes=num_classes)

if self.cuda:
    net.cuda()

 

->4.3 定义optimizer,使用SGD

log.debug('Creating optimizer')
learning_rate = hyper_params.learning_rate
momentum = hyper_params.momentum
decay = hyper_params.decay
batch = hyper_params.batch

optim = torch.optim.SGD(net.parameters(), 
                lr=learning_rate/batch,
                momentum=momentum, 
                dampening=0, 
                weight_decay=decay*batch)

 

->4.4 定义dataloader

#VOCDataset是torch.utils.data.dataset的子类
dataset = VOCDataset(hyper_params)

#引入了4个不同的transform操作
#
#rf  = data.transform.RandomFlip(flip)
#rc  = data.transform.RandomCropLetterbox(self, jitter)
#hsv = data.transform.HSVShift(hue, sat, val)
#it  = tf.ToTensor()     # torchvision.transforms as tf
#
#img_tf = data.transform.Compose([rc, rf, hsv, it])
#anno_tf = data.transform.Compose([rc, rf])
#
#这样,在每次loader调用__getitem__时,
#都会根据概率0.5,来决定返回(raw_img,raw_anno)
#还是返回 img_tf和anno_tf之后的(img,anno)
#
#@Dataset.resize_getitem
#def __getitem__(self, index):
#


#data.Dataloder是torch.utils.data.dataloader的子类
#
#使用自定义的子类,主要是为了实现的对不同尺寸图片的训练。
#def change_input_dim(self, multiple=32, random_range=(10, 19), finish=False):
#显然,input_dim可以在320~608之间以32的步长来切换。

dataloader = data.DataLoader(
            dataset,
            batch_size = self.mini_batch_size,
            shuffle = True,
            drop_last = True,
            num_workers = hyper_params.nworkers if self.cuda else 0,
            pin_memory = hyper_params.pin_mem if self.cuda else False,
            collate_fn = data.list_collate,
        )

 

->4.5 用父类engine.Engine的初始化方法。

super(VOCTrainingEngine, self).__init__(net, optim, dataloader)

其实就是把4.2的net,4.3的optim,4.4的dataloader传进去了,串成了self的成员对象。

 

->4.6 为之后设置loss function进行前置工作。

self.nloss = self.network.nloss  #after super,self.network=net ,so this equals net.nloss, since we have 5 anchors, so got 5 nloss

#预定义一个list of (dict of train_loss)
self.train_loss = [{'tot': [], 'coord': [], 'conf': [], 'cls': []} for _ in range(self.nloss)]

 

->4.7 VOCTrainingEngine 实例化完成。

结构还是很简单的,追根究底也只有三大块, 4.1,(4.2+4.3+4.4)->4.5,4.6。

结束后,我们得到实例对象eng。

 

5.获得eng.batch

因为我们可能会从中断处重新开始训练,所以获取这个还是有用的,可以在结束后告诉我们,本次训练从第n batch 运行到了第m batch。

#self.batch定义在父类engine.Engine中

    @property
    def batch(self):
        """ Get current batch number.

        Return:
            int: Computed as self.network.seen // self.batch_size
        """
        return self.network.seen // self.batch_size

self.batch是一个@property修饰的属性方法,计算方式是self.network.seen // self.batch_size。含义是当前处于第几批次。

解释一下。

self.network就是上文的net,本质为Yolov2类,继承自YoloABC->继承自Darknet->继承自lightnet类。

network里面含有backbone和head两层,还定义了_forward等相关的方法,不必细究。

network.seen的含义是,目前为止network处理了多少张图片。

底层的lightnet类的forward(self,x)方法里,每次会更新 self.seen += x.size(0) 。

我们再用处理了n张图片//batch_size,来计算当前处于第几批次。

 

例如,原代码中,取batch_size=64,mini_batch_size=16。每64张图片为1个batch。

这样每次loader给出16张图片和相应annos。每次network.forward之后,seen += 16。

当seen=16时,计算self.batch = 16//64 = 0 ,处于第0批次。

当seen=64时,计算self.batch = 64//64 = 1,处于第1批次。

以此类推。

 

6. 启动eng(),开始!

->6.1 运行eng(),会调用父类engine.Engine中定义的 __call__()方法, 在__call__中处理所有标准的训练步骤。

->6.2 先进行self.start(),这个函数由子类VOCTrainingEngine实现。

主要效果是取出一些self.hyper_params里的参数,然后用self.add_rate方法加入到自身的self._rates字典中。

做这步是因为,我们希望有阶梯式的learning_rate, 比如在400 batch的时候用中规中矩的0.0001来初训练,在1000 batch的时候用较大的0.001防止掉入局部最优解,在大后期60000 batch的时候用极小的0.00001来微调。

#start()由子类VOCTrainingEngine负责实现。
#lr_steps: [400,700,900,1000, 40000,60000] 
#lr_rates: [0.0001,0.0005,0.0005,0.001, 0.0001,0.00001] 

    def start(self):
        log.debug('Creating additional logging objects')
        hyper_params = self.hyper_params

        lr_steps = hyper_params.lr_steps
        lr_rates = hyper_params.lr_rates

        bp_steps = hyper_params.bp_steps
        bp_rates = hyper_params.bp_rates
        backup = hyper_params.backup    #cfg['backup_interval']

        rs_steps = hyper_params.rs_steps
        rs_rates = hyper_params.rs_rates
        resize = hyper_params.resize    #cfg['resize_interval']

        self.add_rate('learning_rate', lr_steps, [lr/self.batch_size for lr in lr_rates])
        self.add_rate('backup_rate', bp_steps, bp_rates, backup)
        self.add_rate('resize_rate', rs_steps, rs_rates, resize)

        self.dataloader.change_input_dim()



#add_rate()定义在父类engine.Engine中
#效果为,把预设的阶梯learning_rate等加入到self.__rates字典中。
self.__rates[name] = (steps, values)

->6.3 self._update_rates()

因为可能是从中断处开始的,self.batch可能是一个比较大的值,所以保险起见update一下,恢复到断点处的rates。

#定义在父类engine.Engine中
#核心代码就这么两句,如果到了我们需要的batch,就切换下一个阶段的rate

if self.batch >= steps[i]:
    new_rate = values[i]
else:
    break

 ->6.4 开始一个循环,while True:

不用担心结束的问题。

yaml中有max_batches,每个batch训练完之后都会检查一下self.batch是否超出这个值。

超出就会保存weights之后quit。

或者人为地ctrl+c发送中断signal也会quit。

->6.5 在循环内,不断遍历loader里的全部mini_batch。本例一次mini_batch=16张图片。

loader = self.dataloader
for idx, data in enumerate(loader):

    ...

    if (len(loader) - idx) <= self.batch_subdivisions:
        break

#且我们会对尾数进行抛弃
#batch_subdivisions = self.batch_szie / self.mini_batch_size
#本例 = 64/16 = 4。 即4个mini_batch等于一个batch。
#当loader内的mini_batch不足4个,即不足以构成一个batch时,予以drop

->6.6 遍历过程中,对每个mini_batch,先调用self.process_batch(data),定义在子类中。

先把data移到cuda内存区,然后用已经在cuda上的network计算loss。

最后把loss细分成 total_loss,coord_loss(坐标损失),confidence_loss(确信有物体),class_loss(分类)等,存入self.train_loss这个list of dict中。

def process_batch(self, data):
    data, target = data
    # to(device)
    if self.cuda:
        data = data.cuda()

    # network.forward function defined in _lightnet.py ,
    # firstly we run self._forward(x),
    # then we calculate loss for each self.loss, which is a list of loss_functions from loss.RegionLoss
    loss = self.network(data, target)
    loss.backward()

    for ii in range(self.nloss):
        self.train_loss[ii]['tot'].append(self.network.loss[ii].loss_tot.item() / self.mini_batch_size)
        self.train_loss[ii]['coord'].append(self.network.loss[ii].loss_coord.item() / self.mini_batch_size)
        self.train_loss[ii]['conf'].append(self.network.loss[ii].loss_conf.item() / self.mini_batch_size)
        if self.network.loss[ii].loss_cls is not None:
            self.train_loss[ii]['cls'].append(self.network.loss[ii].loss_cls.item() / self.mini_batch_size)

关于network的具体forward()方法,另寻一处说明。

总而言之,无论是什么网络,究其根底都不过是传入data和target计算loss然后backward罢了。

 

->6.7遍历过程中,对每个mini_batch,再调用self.train_batch(),定义在子类中。

主要是进行了optimizer.step()。这步的作用是根据6.6中loss.backward()算出的梯度,更新网络参数network.parameters()。

然后顺便的进行了收尾工作。

计算loss,print一下当前第几batch了,loss是多少。

每backup_rate,save_weights()一下weights_{batch}.pt。

固定每100batch,save_weights()一下backup.pt。

同时检查是否超出max_batches,若是则设置finish_flag=True。

 

->6.8 遍历过程中,对每个mini_batch,第三步是检查是否要停止,若不需要停止就self._update_rates()。

在前面的process_batch过程中,经过network算出了loss,如上文所述,network.seen会增加。

所以结束了process_batch()和train_batch()之后,self.batch数值可能会变动。此时update一下,就可以更新我们的阶梯速率。

 

->6.9 结束了 eng(),即 __call__() 过程。

 

最后附上__call__的代码,结合上面的文字来看。

    def __call__(self):
        """ Start the training cycle. """
        self.start()            #to add rates, implemented in subclass
        self._update_rates()    #then update rates
        if self.test_rate is not None:
            last_test = self.batch - (self.batch % self.test_rate)

        log.info('Start training')
        self.network.train()
        while True:
            loader = self.dataloader
            for idx, data in enumerate(loader):
                print('new loaded batch is ', self.batch)
                # Forward and backward on (mini-)batches
                self.process_batch(data)    # implemented in subclass, got self.train_loss as a list of dict of tot,coord,conf,cls
                if (idx + 1) % self.batch_subdivisions != 0:
                    continue

                # Optimizer step
                self.train_batch()          # implemented in subclass

                # Check if we need to stop training
                if self.quit() or self.sigint:
                    log.info('Reached quitting criteria')
                    return

                # Check if we need to perform testing
                if self.test_rate is not None and self.batch - last_test >= self.test_rate:
                    log.info('Start testing')
                    last_test += self.test_rate
                    self.network.eval()     #set to evaluation mode
                    self.test()
                    log.debug('Done testing')
                    self.network.train()    #set to train mode

                # Check if we need to stop training
                if self.quit() or self.sigint:
                    log.info('Reached quitting criteria')
                    return

                # Automatically update registered rates
                self._update_rates()

                # Not enough mini-batches left to have an entire batch
                if (len(loader) - idx) <= self.batch_subdivisions:
                    break

 

 

7. 结束了整个train过程。记录一下耗时和batch变动。

    # run eng
    b1 = eng.batch
    t1 = time.time()
    eng()
    t2 = time.time()
    b2 = eng.batch

    log.info(f'\nDuration of {b2-b1} batches: {t2-t1} seconds [{round((t2-t1)/(b2-b1), 3)} sec/batch]')

 

 

你可能感兴趣的:(草稿箱)