目次
以前の記事のアクセス数がこのブログ全体の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
左上のPLAYGROUNDで開くを押してもらえれば使えます。
実行する時に、なんか色々出てきますが適当にぽちぽちしたら使えます、たぶん。
v1.1.0の問題点について
残念ながらv1.1.0のStepLR, MultiStepLRの二つがバグってます。各自で修正してください。
masterブランチやv1.1以外では正常に動作するので、pip show torch
等のコマンドで場所調べて、直接ライブラリにコピペなど。
どんなバグかというと、学習率が下がるタイミングのとき、下げたい割合の二乗分下が ります るように見えます(見えるだけ)。
以下では、StepLR, MultiStepLRだけv1.01における説明になります。
[追記(2019/07/24)]
scheduler側からget_lrで学習率を取得すると上記のようにおかしな挙動になりますが、optimizer側から学習率を取得すると期待通りの学習率が得られますので学習自体には問題はないと思われます。学習率を記録する際は気をつけましょう。@hara_pets さん、ご報告ありがとうございます。
引数一覧
- 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)
関数を渡した場合
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)
継承を用いた場合
class WarmupConstantSchedule(torch.optim.lr_scheduler.LambdaLR):
""" 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()
引数一覧
- optimizer : 省略
- step_size : 何ステップごとに学習率を減少させるかの値
- gamma : 学習率の減少率
- last_epoch : 省略
example
scheduler = StepLR(optimizer, step_size=200, gamma=0.5)
引数一覧
- 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)
引数一覧
- optimizer : 省略
- gamma : 学習率の減少率
- last_epoch : 省略
簡潔に説明するとgamma**step回数
が学習率に乗算されます。
example
scheduler = ExponentialLR(optimizer, gamma=0.95)
引数一覧
- optimizer : 省略
- T_max : 半周期のステップサイズ
- eta_min : 下限学習率
- last_epoch : 省略
example
scheduler = CosineAnnealingLR(optimizer, T_max=100, eta_min=0.001)
引数一覧
- 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)
引数一覧
- optimizer : 省略
- base_lr (float or list) : 下限学習率(初期値)
- max_lr : 上限学習率
- step_size_up : 学習率上昇のサイクル数
- step_size_down : 学習率減少のサイクル数。Noneの時は、step_size_upと一致。
- mode : {"triangular", "triangular2", "exp_range"}から選択。
scale_fn
がNone
でなければ、無視される。
- gamma : modeが
'exp_range'
の場合の減少率
- scale_fn : 任意のスケールを満たしたい場合、関数(ラムダ式等)を渡す。渡した場合、
mode
は無視される。
- scale_mode : {'cycle', 'iterations'}から選択。
scale_fn
の引数がcycle数なのかiteration数なのかを決定。
- cycle_momentum :
True
ならば、base_momentum
とmax_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')
example2
scheduler = CyclicLR(optimizer, base_lr=0.001, max_lr=0.1,
step_size_up=50, step_size_down=None,
mode='triangular2')
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)
引数一覧
- 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)
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)
example2
scheduler = CosineAnnealingWarmUpRestarts(optimizer, T_0=50, T_mult=2, eta_max=0.1, T_up=10, gamma=0.5)
example3
scheduler = CosineAnnealingWarmUpRestarts(optimizer, T_0=100, T_mult=1, eta_max=0.1, T_up=10, gamma=0.5)