PyTorch 学習率スケジューラ
学習率(Learning Rate)はニューラルネットワークのトレーニングにおいて最も重要なハイパーパラメータのひとつです。学習率が大きすぎると、トレーニングが振動し、場合によっては発散します。小さすぎると、収束が極めて遅く、局所最適に陥りやすくなります。
学習率スケジューラ(LR Scheduler)トレーニング中に学習率を動的に調整することで、トレーニング初期の高速な収束と後期の細かい調整を両立させます。
1. 基本概念と使用パターン
標準的な使用手順
例
import torch.nn as nn
import torch.optim as optim
model = nn.Linear(10, 1)
optimizer = optim.SGD(model.parameters(), lr=0.1)
# 1. スケジューラを作成し、optimizer を渡します
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)
for epoch in range(100):
# 2. トレーニング
train(model, optimizer)
# 3. 各エポックの終了後に scheduler.step() を呼び出します
scheduler.step()
# 4. 現在の学習率を確認します
current_lr = scheduler.get_last_lr()[0]
print(f"Epoch {epoch+1}, LR: {current_lr:.6f}")
step() の呼び出しタイミング
例
optimizer.step()
scheduler.step()
# 誤り:scheduler.step() が optimizer.step() より前にある
# PyTorch 1.1.0 以降では警告が発生し、一部のスケジューラが正常に動作しなくなります
scheduler.step()
optimizer.step()
注意:
optimizer.step()必ずscheduler.step()前に呼び出してください。
学習率の状態の確認と保存
例
current_lr = optimizer.param_groups[0]['lr']
current_lr = scheduler.get_last_lr()[0] # 前回の step() 後の LR
# チェックポイントを保存(scheduler の状態も同時に保存が必要)
torch.save({
'epoch': epoch,
'model': model.state_dict(),
'optimizer': optimizer.state_dict(),
'scheduler': scheduler.state_dict(), # これを忘れないでください
}, 'checkpoint.pth')
# チェックポイントから復元
ckpt = torch.load('checkpoint.pth')
model.load_state_dict(ckpt['model'])
optimizer.load_state_dict(ckpt['optimizer'])
scheduler.load_state_dict(ckpt['scheduler'])
2. 固定減衰スケジューラ
2.1 StepLR ステップごとの減衰
每隔固定step_sizeエポックごとに、学習率を掛けるgamma。最もシンプルで、最もよく使われるスケジューラの一つです。
公式: lr = lr_base * gamma^(floor(step / step_size))
例
scheduler = optim.lr_scheduler.StepLR(
optimizer,
step_size=30, # 每 30 个 epoch 衰减一次
gamma=0.1 # 毎回 0.1 を掛ける(つまり元の 1/10 に縮小される)
)
# LR 变化:
# Epoch 0-29: 0.1
# Epoch 30-59: 0.01
# Epoch 60-89: 0.001
# Epoch 90+: 0.0001
适用场景:訓練テンポが固定され、段階が明確なタスク、例えば ImageNet における ResNet の訓練(90エポック、第30、60エポックで減衰)。
2.2 MultiStepLR マイルストーンによる減衰
指定された複数のエポック(マイルストーン)で学習率を減衰させ、StepLR よりも柔軟です。
例
scheduler = optim.lr_scheduler.MultiStepLR(
optimizer,
milestones=[30, 60, 80], # 30、60、80 epoch で減衰
gamma=0.1
)
# LR 变化:
# Epoch 0-29: 0.1
# Epoch 30-59: 0.01
# Epoch 60-79: 0.001
# Epoch 80+: 0.0001
适用场景:どのエポックでモデルを微調整する必要があるか分かっている場合、例えば分類モデルの中後期の精密な収束段階などです。
2.3 ExponentialLR 指数減衰
毎エポック減衰し、学習率が指数関数的に継続的に下降するため、減衰がより滑らかです。
公式: lr = lr_base * gamma^epoch
例
scheduler = optim.lr_scheduler.ExponentialLR(
optimizer,
gamma=0.95 # 每个 epoch 乘以 0.95
)
# LR 变化(前 5 个 epoch):
# Epoch 1: 0.0100
# Epoch 2: 0.0095
# Epoch 3: 0.0090
# Epoch 4: 0.0086
# Epoch 5: 0.0081
gamma設定が小さすぎる(例えば0.5)と学習率が急速にゼロに近づくため、通常は0.9〜0.99の間に設定します。
3. 適応型スケジューラ
3.1 ReduceLROnPlateau メトリクス監視による減衰
最もインテリジェントなスケジューラの一つ:ある指標(検証セットの損失など)を監視し、その指標の改善が止まったときに自動的に学習率を下げます。どのエポックで減衰させるかを事前に知る必要はありません。
例
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
optimizer,
mode='min', # 'min': 监控值越小越好(loss);'max': 越大越好(accuracy)
factor=0.1, # 触发时 lr = lr × factor
patience=10, # 指標の停滞を許容するエポック数。超過した場合は減衰する。
threshold=1e-4, # 改善幅がこの値未満の場合は未改善とみなす
min_lr=1e-6, # 学习率下限,不会低于此值
verbose=True # 打印衰减信息
)
for epoch in range(100):
train_loss = train(model, optimizer)
val_loss = evaluate(model)
# 他のスケジューラとは異なり、ここでは監視指標を渡す
scheduler.step(val_loss)
Accuracy の監視の書き方
例
optimizer,
mode='max', # accuracy 越大越好
factor=0.5,
patience=5,
)
scheduler.step(val_accuracy)
适用场景:ほぼすべてのタスクでデフォルトの第一選択であり、特に訓練エポック数が不明確な場合や、訓練が不安定な場合に適しています。
3.2 CosineAnnealingLR コサインアニーリング
学习率按余弦曲线初期値から最小値まで滑らかに下降し(eta_min)、階段状減衰の急変を回避する。
公式: lr_t = eta_min + 0.5 * (eta_max - eta_min) * (1 + cos(t * pi / T_max)
例
scheduler = optim.lr_scheduler.CosineAnnealingLR(
optimizer,
T_max=100, # 半周期の長さ(通常は総エポック数に設定)
eta_min=1e-6 # 学习率最小值(默认 0)
)
# LR 変化軌跡(T_max=10 の場合の図):
# 0.1 -> 0.095 -> 0.079 -> 0.055 -> 0.026 -> 0.001
# (余弦曲线平滑下降)
适用场景:訓練エポック数が固定されているシナリオで、Vision Transformer、ResNet などの論文で広く使用され、収束品質が良好です。
3.3 CosineAnnealingWarmRestarts
コサインアニーリングのアップグレード版であり、対応する周期性重启(Warm Restarts):各周期の終了後に学習率が初期値にリセットされ、新しい余弦減衰が始まります。モデルが局所最適解から抜け出すことを可能にします。
例
scheduler = optim.lr_scheduler.CosineAnnealingWarmRestarts(
optimizer,
T_0=10, # 最初の周期の長さ(エポック数)
T_mult=2, # 再起動ごとの周期長の倍数(1=等長、2=徐々に長く)
eta_min=1e-6
)
# T_mult=2 のときの周期長:10 -> 20 -> 40 -> 80 ...
# LR 变化(T_0=10, T_mult=1):
# 0.1 -> ... -> 0 -> 0.1 -> ... -> 0 -> 0.1(每 10 个 epoch 重启一次)
适用场景:大規模モデルを訓練する場合、またはモデルが複数の収束点から最適なものを選択できるようにしたい場合、Snapshot Ensemble と併用すると顕著な効果があります。
4. ウォームアップスケジューラ
Warmup(预热)トレーニング開始後の数ステップの間、学習率を極小値から目標値まで徐々に上昇させることを指す。バッチサイズが大きいトレーニングやTransformer系のモデルでは、ほぼ必ずウォームアップが必要である。そうでなければ初期の勾配更新が急激すぎて、モデルが安定しにくい。
4.1 LinearLR 線形スケジューリング
指定した epoch 内で学習率を線形に変更します(線形ウォームアップや線形減衰に使用できます)。
例
# 線形ウォームアップ:最初の 5 epoch で、lr は 0.01×0.1=0.001 から 0.01 へ線形に増加
warmup_scheduler = optim.lr_scheduler.LinearLR(
optimizer,
start_factor=0.1, # 初期 lr = base_lr × start_factor
end_factor=1.0, # 終了 lr = base_lr × end_factor
total_iters=5 # 5 epoch かけて完了
)
# LR 変化:0.001 -> 0.003 -> 0.005 -> 0.007 -> 0.009 -> 0.01
4.2 ConstantLR 定数段階
指定した epoch 数の間、学習率をbase_lr × factor、その後元の値に戻します。
例
scheduler = optim.lr_scheduler.ConstantLR(
optimizer,
factor=0.5,
total_iters=5
)
4.3 SequentialLR 組み合わせスケジューリング
複数のスケジューラを順番に直列に接続し、「ウォームアップ + 減衰」の組み合わせ戦略を実現する標準的な方法です。
例
# フェーズ1:ウォームアップ(最初の 5 epoch で、LR は 0.001 から 0.01 へ線形に増加)
warmup = optim.lr_scheduler.LinearLR(
optimizer, start_factor=0.1, end_factor=1.0, total_iters=5
)
# フェーズ2:コサインアニーリング(残り 95 epoch)
cosine = optim.lr_scheduler.CosineAnnealingLR(
optimizer, T_max=95, eta_min=1e-6
)
# 組み合わせ:最初に warmup を実行し、5 epoch 後に cosine に切り替え
scheduler = optim.lr_scheduler.SequentialLR(
optimizer,
schedulers=[warmup, cosine],
milestones=[5] # 5 epoch 目で切り替え
)
# 使用方法は通常のスケジューラと完全に同じ
for epoch in range(100):
train(...)
scheduler.step()
適用シナリオ:Transformer 学習の標準構成(ウォームアップ + コサインアニーリング)。BERT、GPT、ViT の事前学習はすべてこの戦略を採用しています。
5. 循環スケジューラ
5.1 CyclicLR 循環学習率
学習率はbase_lrとmax_lrの間で周期的に循環します、モデルがより広いパラメータ空間を探索し、鞍点から抜け出すのに役立ちます。
例
scheduler = optim.lr_scheduler.CyclicLR(
optimizer,
base_lr=0.001, # 学習率の下限
max_lr=0.01, # 学習率の上限
step_size_up=2000, # base_lr から max_lr へ上昇する反復ステップ数
step_size_down=2000, # max_lr から base_lr へ下降する反復ステップ数(デフォルトでは step_size_up と同じ)
mode='triangular', # 三角形循環(等振幅)
# mode='triangular2' # 各周期で振幅が半分になる
# mode='exp_range' # 振幅が指数的に減衰
)
# CyclicLR は step(batch)単位で呼び出し、epoch 単位ではない
for epoch in range(num_epochs):
for inputs, labels in train_loader:
optimizer.zero_grad()
loss = criterion(model(inputs), labels)
loss.backward()
optimizer.step()
scheduler.step() # 各 batch の後に呼び出し
3つのモードの比較:
| mode | 振幅の変化 | 特徴 |
|---|---|---|
triangular | 不変 | 安定した探索、初期に適する |
triangular2 | 各周期で半分 | 探索してから収束 |
exp_range | 指数減衰 | 最終的に安定して収束 |
5.2 OneCycleLR 単一周期戦略
最も性能の良いスケジューラの1つ、fastai が提案した 1-Cycle Policy。トレーニング全体で周期は1つだけ:学習率は最初に上昇し、その後下降します。運動量(momentum)は逆方向に変化します。
トレーニング速度が速く、通常は従来のトレーニングの1/5~1/10の epoch 数だけで同じ精度に達します。
例
steps_per_epoch = len(train_loader)
scheduler = optim.lr_scheduler.OneCycleLR(
optimizer,
max_lr=0.1, # 学習率の最大点
steps_per_epoch=steps_per_epoch, # 各 epoch の step 数
epochs=10, # 総 epoch 数
pct_start=0.3, # 最初の 30% をウォームアップ上昇に使用
anneal_strategy='cos', # 減衰戦略('cos' または 'linear')
div_factor=25, # 初期 lr = max_lr / div_factor
final_div_factor=1e4 # 最終 lr = max_lr / final_div_factor
)
# 初期 lr = 0.1 / 25 = 0.004
# ピーク lr = 0.1(30% の時点)
# 最終 lr = 0.1 / 10000 = 0.00001
# 同様に batch 単位で呼び出し
for epoch in range(10):
for inputs, labels in train_loader:
optimizer.zero_grad()
loss = criterion(model(inputs), labels)
loss.backward()
optimizer.step()
scheduler.step() # 各 batch の後に呼び出し
適用シナリオ:計算リソースが限られていて、アイデアを素早く検証する必要がある場合に最適です。
max_lrLR Finder ツールを使って最適な値を決定することをお勧めします。
6. カスタムスケジューラ
6.1 LambdaLR 関数によるカスタム定義
Lambda 関数(epoch 数を受け取り、学習率の乗数を返す)を渡すことで、スケジュール戦略を完全にカスタマイズできます。
例
# 例1:Transformer の古典的なウォームアップ戦略
# lr ∝ min(step^-0.5, step × warmup_steps^-1.5)
def transformer_lr(epoch, warmup_epochs=10, d_model=512):
if epoch == 0:
return 1e-7 / 0.01 # ゼロ除算を避ける
step = epoch + 1
warmup = warmup_epochs
return (d_model ** -0.5) * min(step ** -0.5, step * warmup ** -1.5) / 0.01
scheduler = optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=transformer_lr)
# 例2:多項式減衰(lr を初期値から線形/多項式で 0 まで下げる)
def polynomial_decay(epoch, total_epochs=100, power=1.0):
return max((1 - epoch / total_epochs) ** power, 0.0)
scheduler = optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=polynomial_decay)
# 例3:グループ別学習率(パラメータグループごとに異なる戦略)
optimizer = optim.Adam([
{'params': model.backbone.parameters(), 'lr': 1e-4},
{'params': model.head.parameters(), 'lr': 1e-3},
])
# 各パラメータグループに1つの lambda 関数
scheduler = optim.lr_scheduler.LambdaLR(
optimizer,
lr_lambda=[
lambda epoch: 0.95 ** epoch, # backbone:ゆっくり減衰
lambda epoch: 0.85 ** epoch, # head:速く減衰
]
)
6.2 LRScheduler の継承
より複雑なロジックが必要な場合は、継承LRScheduler(PyTorch >= 2.0、旧バージョンでは_LRScheduler)を継承して完全にカスタマイズします:
例
import math
class WarmupCosineScheduler(LRScheduler):
"""
ウォームアップ + コサインアニーリング組み合わせスケジューラ(手動実装版)
- 最初の warmup_epochs epoch で線形ウォームアップ
- その後コサインアニーリングで min_lr まで減衰
"""
def __init__(self, optimizer, warmup_epochs, total_epochs,
min_lr=1e-6, last_epoch=-1):
self.warmup_epochs = warmup_epochs
self.total_epochs = total_epochs
self.min_lr = min_lr
super().__init__(optimizer, last_epoch)
def get_lr(self):
epoch = self.last_epoch
# ウォームアップ段階:線形に増加
if epoch < self.warmup_epochs:
warmup_factor = (epoch + 1) / self.warmup_epochs
return [base_lr * warmup_factor for base_lr in self.base_lrs]
# コサインアニーリング段階
progress = (epoch - self.warmup_epochs) / (
self.total_epochs - self.warmup_epochs
)
cosine_factor = 0.5 * (1 + math.cos(math.pi * progress))
return [
self.min_lr + (base_lr - self.min_lr) * cosine_factor
for base_lr in self.base_lrs
]
optimizer = optim.Adam(model.parameters(), lr=0.01)
scheduler = WarmupCosineScheduler(
optimizer,
warmup_epochs=10,
total_epochs=100,
min_lr=1e-6
)
7. スケジューラの可視化比較
以下のコードで各スケジューラの LR 変化曲線を描画でき、直感的に比較できます:
例
import torch.optim as optim
import matplotlib.pyplot as plt
def simulate_lr(scheduler_fn, epochs=100, steps_per_epoch=None):
"""スケジューラの学習率変化をシミュレーションして記録"""
model = torch.nn.Linear(1, 1)
optimizer = optim.SGD(model.parameters(), lr=0.1)
scheduler = scheduler_fn(optimizer)
lrs = []
if steps_per_epoch:
# step 単位で呼び出すスケジューラ
for _ in range(epochs):
for _ in range(steps_per_epoch):
optimizer.step()
scheduler.step()
lrs.append(optimizer.param_groups[0]['lr'])
else:
# epoch 単位で呼び出すスケジューラ
for _ in range(epochs):
optimizer.step()
scheduler.step()
lrs.append(optimizer.param_groups[0]['lr'])
return lrs
schedulers = {
'StepLR(step=30, gamma=0.1)':
lambda opt: optim.lr_scheduler.StepLR(opt, 30, 0.1),
'CosineAnnealingLR':
lambda opt: optim.lr_scheduler.CosineAnnealingLR(opt, T_max=100),
'ExponentialLR(gamma=0.95)':
lambda opt: optim.lr_scheduler.ExponentialLR(opt, 0.95),
'CosineWarmRestarts(T0=25)':
lambda opt: optim.lr_scheduler.CosineAnnealingWarmRestarts(opt, T_0=25),
}
plt.figure(figsize=(12, 5))
for name, fn in schedulers.items():
plt.plot(simulate_lr(fn), label=name)
plt.xlabel('Epoch')
plt.ylabel('Learning Rate')
plt.title('PyTorch LR Scheduler Comparison')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('lr_schedulers.png', dpi=150)
plt.show()
8. 完全なトレーニングテンプレート
ウォームアップ + コサインアニーリング、モデルの保存と復元を統合した本番用トレーニングテンプレート:
例
import torch.nn as nn
import torch.optim as optim
from torch.optim.lr_scheduler import SequentialLR, LinearLR, CosineAnnealingLR
# ハイパーパラメータ
EPOCHS = 100
WARMUP_EPOCHS = 5
BASE_LR = 1e-3
MIN_LR = 1e-6
SAVE_PATH = 'best_model.pth'
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# モデル / オプティマイザ / スケジューラ
model = MyModel().to(device)
optimizer = optim.AdamW(model.parameters(), lr=BASE_LR, weight_decay=1e-4)
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
# ウォームアップ 5 epoch + コサインアニーリング 95 epoch
warmup_sched = LinearLR(optimizer, start_factor=0.1, total_iters=WARMUP_EPOCHS)
cosine_sched = CosineAnnealingLR(optimizer, T_max=EPOCHS - WARMUP_EPOCHS, eta_min=MIN_LR)
scheduler = SequentialLR(optimizer, [warmup_sched, cosine_sched], milestones=[WARMUP_EPOCHS])
# チェックポイントの復元
start_epoch = 0
best_acc = 0.0
try:
ckpt = torch.load(SAVE_PATH, map_location=device)
model.load_state_dict(ckpt['model'])
optimizer.load_state_dict(ckpt['optimizer'])
scheduler.load_state_dict(ckpt['scheduler'])
start_epoch = ckpt['epoch'] + 1
best_acc = ckpt['best_acc']
print(f"Epoch {start_epoch} から復元、最高精度 {best_acc:.4f}")
except FileNotFoundError:
print("ゼロからトレーニングを開始")
# トレーニングループ
history = {'train_loss': [], 'val_acc': [], 'lr': []}
for epoch in range(start_epoch, EPOCHS):
# トレーニング
model.train()
total_loss = 0.0
for inputs, labels in train_loader:
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
loss = criterion(model(inputs), labels)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) # 勾配クリッピング
optimizer.step()
total_loss += loss.item()
# 検証
model.eval()
correct = 0
with torch.no_grad():
for inputs, labels in val_loader:
inputs, labels = inputs.to(device), labels.to(device)
correct += (model(inputs).argmax(1) == labels).sum().item()
avg_loss = total_loss / len(train_loader)
val_acc = correct / len(val_loader.dataset)
cur_lr = scheduler.get_last_lr()[0]
# 記録 & 表示
history['train_loss'].append(avg_loss)
history['val_acc'].append(val_acc)
history['lr'].append(cur_lr)
print(f"Epoch {epoch+1:3d}/{EPOCHS} | "
f"Loss: {avg_loss:.4f} | Acc: {val_acc:.4f} | LR: {cur_lr:.2e}")
# スケジューラ更新
scheduler.step()
# 最良モデルの保存
if val_acc > best_acc:
best_acc = val_acc
torch.save({
'epoch': epoch,
'model': model.state_dict(),
'optimizer': optimizer.state_dict(),
'scheduler': scheduler.state_dict(),
'best_acc': best_acc,
}, SAVE_PATH)
print(f" 最良モデルを保存、Acc: {best_acc:.4f}")
print(f"\nトレーニング完了、最高検証精度: {best_acc:.4f}")
9. スケジューラ選択ガイド
タスクタイプ別のおすすめ
| シナリオ | 推奨スケジューラ | 理由 |
|---|---|---|
| 高速実験 / プロトタイプ検証 | ReduceLROnPlateau | ハイパーパラメータ調整不要、適応的減衰 |
| 画像分類(固定epoch) | CosineAnnealingLR | 滑らかな減衰、収束品質が良い |
| Transformer / BERT | LinearLR + CosineAnnealingLR | ウォームアップが必須 |
| リソースが限られている、高速トレーニング | OneCycleLR | 1/5 の時間で同等の精度 |
| 大規模モデルの微調整(fine-tune) | LinearLR(ウォームアップ) +ConstantLR | 低学習率で安定した微調整 |
| epoch 数が不明 | ReduceLROnPlateau | 自動応答、固定リズムに依存しない |
| 物体検出(YOLO/Faster-RCNN) | MultiStepLR / OneCycleLR | 明確な減衰ポイントがある、または速度を追求 |
| 局所最適解から抜け出したい | CosineAnnealingWarmRestarts | 周期的再起動でパラメータ空間を探索 |
一般的な設定の組み合わせ
例
SequentialLR([LinearLR(5 epochs), CosineAnnealingLR(95 epochs)])
# 組み合わせ2:検証セットの監視 + 自動減衰(汎用的な第一候補)
ReduceLROnPlateau(mode='min', patience=5, factor=0.5)
# 組み合わせ3:OneCycleLR(高速トレーニングの第一候補)
OneCycleLR(max_lr=0.1, total_steps=total_steps, pct_start=0.3)
# 組み合わせ4:従来の階段状減衰(CV の古典)
MultiStepLR(milestones=[30, 60, 90], gamma=0.1)
注意事項の早見表
| 問題 | 解決方法 |
|---|---|
| スケジューラが効果がない | 確認scheduler.step()かどうかoptimizer.step() の後呼び出し |
| CyclicLR / OneCycleLR 変化なし | この2つはbatch呼び出しであり、epoch によるものではない |
| ReduceLROnPlateau が発動しない | 確認mode正しく設定されているか(loss には'min'、accuracy には'max') |
| 訓練再開後の LR が正しくない | 保存とロードが行われたか確認するscheduler.state_dict() |
| 複数パラメータグループの LR 設定 | 使用LambdaLRリストを渡し、各パラメータグループに1つの lambda を指定 |