detectron2-train篇

学习学习!

Posted by Sun on March 24, 2020

Hooks

detectron2是个高扩展性,灵活的框架,那么为了能够实现训练过程的灵活性,detectron2采用了hook的机制来定义训练的各个部分:

  • 迭代数据集之前
  • 每一次迭代batch之前
  • 迭代batch进行训练
  • 迭代完一个batch之后
  • 结束数据集的迭代

那么有些人可能会疑惑为啥只有迭代一次数据集呢?

这个也是detectron2的一个特点,我们是在定义配置文件的时候进行定义epoch的数量,但epoch并不是显示去定义的,而是通过max_iter的数量,不断重复迭代数据集,比如batch=8,一共有800张图片,则迭代一次数据集需要100次迭代,若Max_iter=1000,则代表epoch=10。

我们的重中之重就是做好run-step,找到最好的optimizer,最好的loss,最好的lr_scheduler,就行了!

理解这一点之后我们回到Hooks,上面说到了训练的各个部分,接下来看一下Hooks的基类代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class HookBase:
   

    def before_train(self):
        """
       迭代数据集前的步骤
        """
        pass

    def after_train(self):
        """
        迭代完数据集后的步骤
        """
        pass

    def before_step(self):
        """
        每一步迭代之前
        """
        pass

    def after_step(self):
        """
        迭代完一步之后
        """
        pass

对于这四个步骤要干什么就是个人设计的流程问题了,这里不多说。

现在我们的重点就是这几个如何连接在一起呢?

因为这四个都是独立的函数,而每一种hook的每一个功能他需要的参数可能都不一样,有些需要loss,有些需要metric,有些需要训练的图片记录下来,有些需要当前的迭代次数,有些需要当前的学习率,那么这些信息是如何进行传递呢?

我们看这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
  def train(self, start_iter: int, max_iter: int):
        self.iter = self.start_iter = start_iter
        self.max_iter = max_iter
        with EventStorage(start_iter) as self.storage:
            try:
                self.before_train()
                for self.iter in range(start_iter, max_iter):
                    self.before_step()
                    self.run_step()
                    self.after_step()
            finally:
                self.after_train()

这段代码使用了一个叫做EventStorage的类,那么这个类就是传递信息的关键.

EventStorage

这个eventstorage就是储存信息以及传递信息的关键。

那么怎么使用呢?首先这个EventStorage在训练过程中相当于一个全局的字典,所以我们可以在任何地方获取以及存储任何值

要获得这么一个字典,只需调用下面的函数

1
storage=get_event_storage()

那么我们就获取到了这个对象,然后就是存储以及获取信息了

对于信息,我们分成两类:标量信息和非标量信息

为什么要这样分呢,我们来看一下

对于标量信息:

我们使用put_scalar,put_scalars两个函数进行信息的储存,两者的区别就是一个一次储存一个标量,后者可以储存多个

1
2
3
4
5
6
7
8
#设置标量的名字以及其值
storage.put_scalar('loss',20)

#通过关键字参数进行多个标量的存储
#不过关键字仍然是作为字符串变成字典的值
storage.put_scalars(loss=20,metric=10)


我们可以看一下下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
def run_step():
  storage=get_event_storage()
  print(storage.latest())
  

with EventStorage(0) as storage:
  for i in range(10):

    storage.put_scalars(loss=i*10,metric=i*5)
    storage.put_scalar('lr',i)
    run_step()
  
1
2
3
4
5
6
7
8
9
10
{'loss': 0.0, 'metric': 0.0, 'lr': 0.0}
{'loss': 10.0, 'metric': 5.0, 'lr': 1.0}
{'loss': 20.0, 'metric': 10.0, 'lr': 2.0}
{'loss': 30.0, 'metric': 15.0, 'lr': 3.0}
{'loss': 40.0, 'metric': 20.0, 'lr': 4.0}
{'loss': 50.0, 'metric': 25.0, 'lr': 5.0}
{'loss': 60.0, 'metric': 30.0, 'lr': 6.0}
{'loss': 70.0, 'metric': 35.0, 'lr': 7.0}
{'loss': 80.0, 'metric': 40.0, 'lr': 8.0}
{'loss': 90.0, 'metric': 45.0, 'lr': 9.0}

那么如何获取标量信息呢?

  • storage.latest() —–>这个返回的是当前迭代时赋予的最新值(因为一直在更新这个字典)
  • sotrage.histories() —->这个返回的是HistoryBuffer对象,储存了以前所有赋值的信息

对于latest,不多说,从上面的输出就可以看出,就是返回当前字典的最新值

对于HistoryBuffer,这个就是为什么标量信息可以单独拿出来谈论的重点

historybuffer的处理在detectron2中并没有说到,而这个处理是在fvore(facebook的另外一个第三方库)

HistoryBuffe文档

对于HistoryBuffer,具体就是三种操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
history=storage.histories()

#获取最新的信息
history.latest()

#获取所有标量值,返回元组(标量值,第几个迭代设置的这个标量值)
history.values()

#获取标量值的平均值

#前window_size个元素的平均值
history.avg(window_size)

#全部元素的平均值
history.global_avg()


就是因为提供了这些直接对标量值的操作,所以对于标量,我们就采用上面的方法进行存储读取

对于非标量值 对于一些非标量值,比如布尔向量,存储某些重要信息的字典甚至是图片,我们都会存储到EventStorage中

图片比较特殊,EventStorage本身就提供了存储图片读取图片的方法

1
2
3
4
5
6
7
#存储图片
storage.put_image(image_name,image)

#读取图片
#返回的是[(image_name,image,iteration),....]
storage.vis_data()

通常存储图片是为了可视化,没什么情况不需要使用

而对于其他一些重要的非标量值

1
2
3
storage.early_stop=True

storage.inforamtion={}

读取也是直接引用对应的变量名即可

1
2
if storage.early_stop:
      break

这就是各个hook之间,hook的函数之间,进行数据传输的过程。

训练模型两件套:optimizer,lr_scheduler

没错,到了重要的两件套时刻。对于一个模型,最终泛化性的好坏,这两件套的重要性不言而喻。接下来我们就探讨一下detectron2这两件套是怎么样去使用的。(其实真正是三件套,还缺一个loss,但是loss是跟model挂钩的,而model的部分非常难,还会单独写一个博客来介绍model和loss,所以这里先不介绍loss了)

optimizer

在介绍如何定义自己的optimizer之前,同样的按照老方法我们先看一看detectron2官方默认的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def build_optimizer(cfg: CfgNode, model: torch.nn.Module) -> torch.optim.Optimizer:
    norm_module_types = (
        torch.nn.BatchNorm1d,
        torch.nn.BatchNorm2d,
        torch.nn.BatchNorm3d,
        torch.nn.SyncBatchNorm,
        # NaiveSyncBatchNorm inherits from BatchNorm2d
        torch.nn.GroupNorm,
        torch.nn.InstanceNorm1d,
        torch.nn.InstanceNorm2d,
        torch.nn.InstanceNorm3d,
        torch.nn.LayerNorm,
        torch.nn.LocalResponseNorm,
    )
    params: List[Dict[str, Any]] = []
    memo: Set[torch.nn.parameter.Parameter] = set()
    for module in model.modules():
        for key, value in module.named_parameters(recurse=False):
            if not value.requires_grad:
                continue
            # Avoid duplicating parameters
            if value in memo:
                continue
            memo.add(value)
            lr = cfg.SOLVER.BASE_LR
            weight_decay = cfg.SOLVER.WEIGHT_DECAY
            if isinstance(module, norm_module_types):
                weight_decay = cfg.SOLVER.WEIGHT_DECAY_NORM
            elif key == "bias":
                lr = cfg.SOLVER.BASE_LR * cfg.SOLVER.BIAS_LR_FACTOR
                weight_decay = cfg.SOLVER.WEIGHT_DECAY_BIAS
            params += [{"params": [value], "lr": lr, "weight_decay": weight_decay}]

    optimizer = torch.optim.SGD(params, cfg.SOLVER.BASE_LR, momentum=cfg.SOLVER.MOMENTUM)
    optimizer = maybe_add_gradient_clipping(cfg, optimizer)
    return optimizer

我们可以看一下官方的定义:

首先它是循环model的各个部分,对于不需要梯度下降的部分不予理会,然后根据config文件确定该部分的learning_rate,weight_decay。对于Bias部分,还做了特殊的处理,同样也是根据config进行处理的。

然后就建立了一个SGD,然后做了一个给optimizer加上梯度裁剪功能的操作,这样optimizer在进行更新参数时还会对梯度进行裁剪。

综上所述,对于实现一个optimizer,咱们要做的不多,第一就是在config文件确定好学习率,weight_decay以及其他重要参数。第二就是确定使用哪个optimizer

上面这些确定后,其他都可以保持不变,这样我们就建立了一个自定义的optimizer

lr_scheduler lr_scheduler比起更加需要自定义的部分,我们先来看一下官方的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def build_lr_scheduler(
    cfg: CfgNode, optimizer: torch.optim.Optimizer
) -> torch.optim.lr_scheduler._LRScheduler:
  
    name = cfg.SOLVER.LR_SCHEDULER_NAME
    if name == "WarmupMultiStepLR":
        return WarmupMultiStepLR(
            optimizer,
            cfg.SOLVER.STEPS,
            cfg.SOLVER.GAMMA,
            warmup_factor=cfg.SOLVER.WARMUP_FACTOR,
            warmup_iters=cfg.SOLVER.WARMUP_ITERS,
            warmup_method=cfg.SOLVER.WARMUP_METHOD,
        )
    elif name == "WarmupCosineLR":
        return WarmupCosineLR(
            optimizer,
            cfg.SOLVER.MAX_ITER,
            warmup_factor=cfg.SOLVER.WARMUP_FACTOR,
            warmup_iters=cfg.SOLVER.WARMUP_ITERS,
            warmup_method=cfg.SOLVER.WARMUP_METHOD,
        )
    else:
        raise ValueError("Unknown LR scheduler: {}".format(name))

可以看出官方提供两种的lr_scheduler,一个是余弦退火,一个是根据step进行退火,接下来我们看看提供的这两种scheduler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
class WarmupMultiStepLR(torch.optim.lr_scheduler._LRScheduler):
    def __init__(
        self,
        optimizer: torch.optim.Optimizer,
        milestones: List[int],
        gamma: float = 0.1,
        warmup_factor: float = 0.001,
        warmup_iters: int = 1000,
        warmup_method: str = "linear",
        last_epoch: int = -1,
    ):
        if not list(milestones) == sorted(milestones):
            raise ValueError(
                "Milestones should be a list of" " increasing integers. Got {}", milestones
            )
        self.milestones = milestones
        self.gamma = gamma
        self.warmup_factor = warmup_factor
        self.warmup_iters = warmup_iters
        self.warmup_method = warmup_method
        super().__init__(optimizer, last_epoch)

    def get_lr(self) -> List[float]:
        warmup_factor = _get_warmup_factor_at_iter(
            self.warmup_method, self.last_epoch, self.warmup_iters, self.warmup_factor
        )
        return [
            base_lr * warmup_factor * self.gamma ** bisect_right(self.milestones, self.last_epoch)
            for base_lr in self.base_lrs
        ]

    def _compute_values(self) -> List[float]:
        # The new interface
        return self.get_lr()


class WarmupCosineLR(torch.optim.lr_scheduler._LRScheduler):
    def __init__(
        self,
        optimizer: torch.optim.Optimizer,
        max_iters: int,
        warmup_factor: float = 0.001,
        warmup_iters: int = 1000,
        warmup_method: str = "linear",
        last_epoch: int = -1,
    ):
        self.max_iters = max_iters
        self.warmup_factor = warmup_factor
        self.warmup_iters = warmup_iters
        self.warmup_method = warmup_method
        super().__init__(optimizer, last_epoch)

    def get_lr(self) -> List[float]:
        warmup_factor = _get_warmup_factor_at_iter(
            self.warmup_method, self.last_epoch, self.warmup_iters, self.warmup_factor
        )
        # Different definitions of half-cosine with warmup are possible. For
        # simplicity we multiply the standard half-cosine schedule by the warmup
        # factor. An alternative is to start the period of the cosine at warmup_iters
        # instead of at 0. In the case that warmup_iters << max_iters the two are
        # very close to each other.
        return [
            base_lr
            * warmup_factor
            * 0.5
            * (1.0 + math.cos(math.pi * self.last_epoch / self.max_iters))
            for base_lr in self.base_lrs
        ]

    def _compute_values(self) -> List[float]:
        # The new interface
        return self.get_lr()

可以看出scheduler是通过get_lr返回某个时刻更新后的学习率。

把目光聚焦到余弦退火的scheduler,可以看出跟torch.optimi.lr_scheduler基本差不多,只不过它要一些参数要通过config实现。而最重要的更新学习率同样有get_lr函数实现

所以自定义的scheduler就是更改config文件的配置以及重写自己的get_lr方法

而如何通过scheduler进行学习率的更新呢?由于学习率的更新需要迭代次数的信息,所以学习率更新通常放在hook中,在run_step后通常是after_step进行

1
lr_scheduler.step()

这一步。

我们也可以看到,detectron2官方本来就提供了lr_scheduler的hook,只要把optimizer和lr_scheduler传进去,就可以进行学习率的更新了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class LRScheduler(HookBase):
    """
    A hook which executes a torch builtin LR scheduler and summarizes the LR.
    It is executed after every iteration.
    """

    def __init__(self, optimizer, scheduler):
        """
        Args:
            optimizer (torch.optim.Optimizer):
            scheduler (torch.optim._LRScheduler)
        """
        self._optimizer = optimizer
        self._scheduler = scheduler

        # NOTE: some heuristics on what LR to summarize
        # summarize the param group with most parameters
        largest_group = max(len(g["params"]) for g in optimizer.param_groups)

        if largest_group == 1:
            # If all groups have one parameter,
            # then find the most common initial LR, and use it for summary
            lr_count = Counter([g["lr"] for g in optimizer.param_groups])
            lr = lr_count.most_common()[0][0]
            for i, g in enumerate(optimizer.param_groups):
                if g["lr"] == lr:
                    self._best_param_group_id = i
                    break
        else:
            for i, g in enumerate(optimizer.param_groups):
                if len(g["params"]) == largest_group:
                    self._best_param_group_id = i
                    break

    def after_step(self):
        lr = self._optimizer.param_groups[self._best_param_group_id]["lr"]
        self.trainer.storage.put_scalar("lr", lr, smoothing_hint=False)
        self._scheduler.step()

未完待续……..