catlaの備忘録

忘れないように書いておきます。

PyTorchのSchedulerまとめ[torch v1.1.0]

目次

以前の記事のアクセス数がこのブログ全体の90%を占めているので最新版に更新します。ちなみに前のは v0.4だったかと思います。

Pytorchのscheduler公式ドキュメントは こちら

v1.1.0のソースはこちら

PyTorch公式のscheduler一覧

  • LambdaLR
  • StepLR
  • MultiStepLR
  • ExponentialLR
  • CosineAnnealingLR
  • ReduceLROnPlateau
  • CyclicLR <- New
  • CosineAnnealingWarmRestarts <- New

本題に移る前に

今回のschedulerのコードはgoogle colabで公開してあります。自分でパラメータをいじりながらschedulerの動きを見たい場合は、以下の手順に従ってもらえればと思います。colabについて分からなければ、「ググればええねん」って、ゆたぼんが言ってます。

まず、Googleアカウントがある前提として、以下を開いてください。

colab.research.google.com

f:id:katsura_jp:20190724140726p:plain

左上のPLAYGROUNDで開くを押してもらえれば使えます。

実行する時に、なんか色々出てきますが適当にぽちぽちしたら使えます、たぶん。

v1.1.0の問題点について

残念ながらv1.1.0のStepLR, MultiStepLRの二つがバグってます。各自で修正してください。

masterブランチやv1.1以外では正常に動作するので、pip show torch等のコマンドで場所調べて、直接ライブラリにコピペなど。

どんなバグかというと、学習率が下がるタイミングのとき、下げたい割合の二乗分下が ります るように見えます(見えるだけ)。 f:id:katsura_jp:20190723200535p:plain

以下では、StepLR, MultiStepLRだけv1.01における説明になります。

[追記(2019/07/24)]

scheduler側からget_lrで学習率を取得すると上記のようにおかしな挙動になりますが、optimizer側から学習率を取得すると期待通りの学習率が得られますので学習自体には問題はないと思われます。学習率を記録する際は気をつけましょう。@hara_pets さん、ご報告ありがとうございます。

f:id:katsura_jp:20190724191658p:plain

LambdaLR


引数一覧

  • optimizer : 最適化のインスタンスを指定
  • lr_lambda : ラムダ式や関数を指定
  • last_epoch : 指定したことないのでいじらなくていいと思う(個人的意見)。深く理解したい人はソースを見てください。

このスケジューラはlr_lambdaにオリジナルの式を入れられるのが最大のポイントです。引数には、stepが呼ばれた回数が与えられ、戻り値としてbase_lr(optimizerに指定したlr)からどれほど変化させたいかの比率を返すようにしてください。つまり、学習率はbase_lr * 戻り値となります。

また、継承を用いる事で、自作のschedulerを簡単に作成することができます。 継承でLambdaLRを使う際は pytorch_transformers/optimization.py を参考にしてみるといいかもしれません。

example

ラムダ式を与えた場合

scheduler = LambdaLR(optimizer, lr_lambda = lambda epoch: 0.95 ** epoch)

f:id:katsura_jp:20190724131255p:plain

関数を渡した場合

def func(epoch):
    if epoch < 40:
        return 0.5
    elif epoch < 70:
        return 0.5**2
    elif epoch < 90:
        return 0.5**3
    else:
        return 0.5**4
scheduler = LambdaLR(optimizer, lr_lambda = func)

f:id:katsura_jp:20190724131354p:plain

継承を用いた場合

class WarmupConstantSchedule(torch.optim.lr_scheduler.LambdaLR):
  # Reference : https://github.com/huggingface/pytorch-transformers/blob/master/pytorch_transformers/optimization.py#L33
    """ Linear warmup and then constant.
        Linearly increases learning rate schedule from 0 to 1 over `warmup_steps` training steps.
        Keeps learning rate schedule equal to 1. after warmup_steps.
    """
    def __init__(self, optimizer, warmup_steps, last_epoch=-1):

        def lr_lambda(step):
            if step < warmup_steps:
                return float(step) / float(max(1.0, warmup_steps))
            return 1.

        super(WarmupConstantSchedule, self).__init__(optimizer, lr_lambda, last_epoch=last_epoch)
optimizer = torch.optim.SGD(model.parameters(), lr=0.05, momentum=0.9, weight_decay=1e-5)
scheduler = WarmupConstantSchedule(optimizer, warmup_steps=10)
for step in range(100):
  scheduler.step()

f:id:katsura_jp:20190724131525p:plain

StepLR


引数一覧

  • optimizer : 省略
  • step_size : 何ステップごとに学習率を減少させるかの値
  • gamma : 学習率の減少率
  • last_epoch : 省略

example

scheduler = StepLR(optimizer, step_size=200, gamma=0.5)

f:id:katsura_jp:20190724132211p:plain

MultiStepLR


引数一覧

  • optimizer : 省略
  • milestones : 減少させたいstepのリスト
  • gamma : 学習率の減少率
  • last_epoch : 省略

StepLRは減衰ステップが一つに対し、これは複数取れます。注意点として、milestonesには、ステップの小さい順のリストを与えてください。 つまり、10,30,50のステップ数で減衰させたい場合は、[10,30,50]と与えてください。

このschedulerはImageNet等のベンチマークでSOTAなモデルの実験でも使われていて、最適化関数はmomentumSGD、全体のepoch数に対して50%, 75%くらいで学習率を0.1倍にしていることが多いような気がします。

example

scheduler = MultiStepLR(optimizer, milestones=[200, 350], gamma=0.5)

f:id:katsura_jp:20190724132340p:plain

ExponentialLR


引数一覧

  • optimizer : 省略
  • gamma : 学習率の減少率
  • last_epoch : 省略

簡潔に説明するとgamma**step回数が学習率に乗算されます。

example

scheduler = ExponentialLR(optimizer, gamma=0.95)

f:id:katsura_jp:20190724141706p:plain

CosineAnnealingLR


引数一覧

  • optimizer : 省略
  • T_max : 半周期のステップサイズ
  • eta_min : 下限学習率
  • last_epoch : 省略

example

scheduler = CosineAnnealingLR(optimizer, T_max=100, eta_min=0.001)

f:id:katsura_jp:20190724132845p:plain

ReduceLROnPlateau


引数一覧

  • optimizer : 省略
  • mode : 監視されている要因が'min'だと下がっているか、'max'だと上がっているかを指定
  • factor : 学習率の減衰率
  • patience : 何ステップ向上しなければ減衰するかの値
  • verbose : 減衰するとき出力してれるかどうか
  • threshold : threshold_modeで説明するのに使われてる値
  • threshold_mode : 'rel'と'abs'の2種類のどちらかを取ります。 'rel'の場合、減衰の起因となる閾値をmaxモードだとbest*(1+threshold)、minモードだとbest*(1-threshold)となります。'abs'の場合は、maxモードだとbest-threshold、minモードだとthresh+thresholdとなります。
  • cooldown : 学習率が下がってから、factorを再監視するまでのステップ数
  • min_lr : 最小学習率
  • eps : nanとかInf回避用の微小数

また、監視する要因はschedulerの関数に存在するstepの引数に与えます。

また、大げさに例をあげると、 - mode : 'min' - patience : 10 - threshold : 0.5 - threshold_mode : 'abs' のように指定すると、監視している値のbestが0.01とすると、 0.01+0.5=0.51よりも高い値を10回連続で取ると減衰します。

example

scheduler = ReduceLROnPlateau(optimizer, 'min') 
for epoch in range(10):
    scheduler.step(val_loss) #val_lossが下がらなければ減衰

CyclicLR


引数一覧

  • optimizer : 省略
  • base_lr (float or list) : 下限学習率(初期値)
  • max_lr : 上限学習率
  • step_size_up : 学習率上昇のサイクル数
  • step_size_down : 学習率減少のサイクル数。Noneの時は、step_size_upと一致。
  • mode : {"triangular", "triangular2", "exp_range"}から選択。scale_fnNoneでなければ、無視される。
  • gamma : modeが'exp_range'の場合の減少率
  • scale_fn : 任意のスケールを満たしたい場合、関数(ラムダ式等)を渡す。渡した場合、modeは無視される。
  • scale_mode : {'cycle', 'iterations'}から選択。scale_fnの引数がcycle数なのかiteration数なのかを決定。
  • cycle_momentum : Trueならば、base_momentummax_momentumを逆に循環させる。また、max_momentumを初期値にする。
  • base_momentum : 最適化関数におけるmomentumのサイクルの下限
  • max_momentum : momentumのサイクルの上限
  • last_epoch : 省略

新たに追加されたschedulerです。Fast Geometric Ensembling等で使われています。

example1

scheduler = CyclicLR(optimizer, base_lr=0.001, max_lr=0.1,
                                     step_size_up=50, step_size_down=100, 
                                     mode='triangular')

f:id:katsura_jp:20190724133120p:plain

example2

scheduler = CyclicLR(optimizer, base_lr=0.001, max_lr=0.1,
                                      step_size_up=50, step_size_down=None, 
                                      mode='triangular2')

f:id:katsura_jp:20190724133252p:plain

example3

scheduler = CyclicLR(optimizer, base_lr=0.001, max_lr=0.1,
                                     step_size_up=50, step_size_down=None, 
                                     mode='exp_range', gamma=0.995)

f:id:katsura_jp:20190724133403p:plain

CosineAnnealingWarmRestarts


引数一覧

  • optimizer : 省略
  • T_0 : 初期の繰りかえし回数
  • T_mult : サイクルのスケール倍率
  • eta_min : 下限学習率
  • last_epoch : 省略

これも新たに追加されたschedulerです。SnapShot Ensemble等で使われています。

example

scheduler = CosineAnnealingWarmRestarts(optimizer, T_0=50, T_mult=2, eta_min=0.001)

f:id:katsura_jp:20190724134004p:plain

Schedulerの自作

Schedulerを自作する場合、_LRScheduler または LambdaLRを継承すると便利です。LambdaLRの継承を使った自作は上で書いたので、ここでは_LRSchedulerを継承した場合での作成を行います。

optimizerの学習率更新は、scheduler.step()が呼ばれた際に更新されますが、その際、self.last_epochがインクリメントされます。厳密には、このget_lrが呼ばれる前に行われます。step内でself.get_lrが呼ばれ、その戻り値を新たな学習率に置き換えるように設計されてます。

つまり、自作関数でschedulerを作成するならば、get_lrを自作するのにほとんど等しいです。

ということで、ここではCosineAnnealingWarmRestartsにWarmupとサイクル毎に上限学習率を減らしていくschedulerを作成します。

githubにもあります。 github.com

class CosineAnnealingWarmUpRestarts(_LRScheduler):
    def __init__(self, optimizer, T_0, T_mult=1, eta_max=0.1, T_up=0, gamma=1., last_epoch=-1):
        if T_0 <= 0 or not isinstance(T_0, int):
            raise ValueError("Expected positive integer T_0, but got {}".format(T_0))
        if T_mult < 1 or not isinstance(T_mult, int):
            raise ValueError("Expected integer T_mult >= 1, but got {}".format(T_mult))
        if T_up < 0 or not isinstance(T_up, int):
            raise ValueError("Expected positive integer T_up, but got {}".format(T_up))
        self.T_0 = T_0
        self.T_mult = T_mult
        self.base_eta_max = eta_max
        self.eta_max = eta_max
        self.T_up = T_up
        self.T_i = T_0
        self.gamma = gamma
        self.cycle = 0
        super(CosineAnnealingWarmUpRestarts, self).__init__(optimizer, last_epoch)
        self.T_cur = last_epoch
    
    def get_lr(self):
        if self.T_cur == -1:
            return self.base_lrs
        elif self.T_cur < self.T_up:
            return [(self.eta_max - base_lr)*self.T_cur / self.T_up + base_lr for base_lr in self.base_lrs]
        else:
            return [base_lr + (self.eta_max - base_lr) * (1 + math.cos(math.pi * (self.T_cur-self.T_up) / (self.T_i - self.T_up))) / 2
                    for base_lr in self.base_lrs]

    def step(self, epoch=None):
        if epoch is None:
            epoch = self.last_epoch + 1
            self.T_cur = self.T_cur + 1
            if self.T_cur >= self.T_i:
                self.cycle += 1
                self.T_cur = self.T_cur - self.T_i
                self.T_i = (self.T_i - self.T_up) * self.T_mult + self.T_up
        else:
            if epoch >= self.T_0:
                if self.T_mult == 1:
                    self.T_cur = epoch % self.T_0
                    self.cycle = epoch // self.T_0
                else:
                    n = int(math.log((epoch / self.T_0 * (self.T_mult - 1) + 1), self.T_mult))
                    self.cycle = n
                    self.T_cur = epoch - self.T_0 * (self.T_mult ** n - 1) / (self.T_mult - 1)
                    self.T_i = self.T_0 * self.T_mult ** (n)
            else:
                self.T_i = self.T_0
                self.T_cur = epoch
                
        self.eta_max = self.base_eta_max * (self.gamma**self.cycle)
        self.last_epoch = math.floor(epoch)
        for param_group, lr in zip(self.optimizer.param_groups, self.get_lr()):
            param_group['lr'] = lr

example1

scheduler = CosineAnnealingWarmUpRestarts(optimizer, T_0=150, T_mult=1, eta_max=0.1,  T_up=10, gamma=0.5)

f:id:katsura_jp:20190724134858p:plain

example2

scheduler = CosineAnnealingWarmUpRestarts(optimizer, T_0=50, T_mult=2, eta_max=0.1,  T_up=10, gamma=0.5)

f:id:katsura_jp:20190724134912p:plain

example3

scheduler = CosineAnnealingWarmUpRestarts(optimizer, T_0=100, T_mult=1, eta_max=0.1,  T_up=10, gamma=0.5)

f:id:katsura_jp:20190724134925p:plain