One-Cycle Policy vs Cosine Annealing、学習率スケジュールはどちらが有利?【Keras×CIFAR-10実験】

投稿日:2026年10月11日日曜日 最終更新日:

callbacks CIFAR-10 CNN Cosine Annealing Google Colab Keras Learning Rate One-Cycle Policy 画像分類

X f B! L
アイキャッチ画像 One-Cycle Policy vs Cosine Annealing、学習率スケジュールはどちらが有利?【Keras×CIFAR-10実験】
KerasのCNNで学習率スケジュールを組むとき、「Cosine Annealingにするか、それとも今話題のOne-Cycle Policyにするか」で迷ったことはありませんか? Cosine Annealingは前回の実験でReduceLROnPlateauと比較しましたが、今回はディープラーニング界隈で「短時間で高精度」と評判のOne-Cycle Policyを新たに加え、Google ColabとCIFAR-10で3パターンを比較検証します。結論から言うと、One-Cycle Policyが他の2パターンに大差をつけました。
📘 この記事でわかること
  • One-Cycle PolicyとCosine Annealingの仕組みの違い
  • KerasでOne-Cycle Policyを自作コールバックとして実装する方法
  • 固定学習率・Cosine Annealing・One-Cycle Policyの精度・収束速度・過学習の比較結果(test_accuracyで最大9.1ptの差)

One-Cycle PolicyとCosine Annealingの仕組み

Cosine Annealingは学習率を開始から終了まで一貫して減衰させるのに対し、One-Cycle Policyは学習前半でいったん学習率を引き上げ、後半で急激に下げるという逆転の発想を取ります。前半の高学習率フェーズが局所最適解からの脱出を促し、後半の急減衰フェーズが精密な収束を実現するとされています。

項目固定学習率Cosine AnnealingOne-Cycle Policy
学習率の推移一定滑らかに単調減少上昇→下降(山型)
ピーク学習率なし開始時が最大訓練中盤が最大
ハイパーパラメータlr のみeta_min のみmax_lr・pct_start・div_factor
実装方法optimizerに直接指定keras.optimizers.schedules.CosineDecay自作コールバック(Keras未実装)

One-Cycle Policyの学習率は、上昇フェーズ(\(t \le t_1\))と下降フェーズ(\(t_1 < t \le T\))で以下のように定義されます。 \[ \eta(t) = \begin{cases} \eta_{min} + (\eta_{max} - \eta_{min}) \cdot \dfrac{t}{t_1} & (t \le t_1) \\[6pt] \eta_{end} + \dfrac{1}{2}(\eta_{max} - \eta_{end})\left(1 + \cos\left(\pi \cdot \dfrac{t - t_1}{T - t_1}\right)\right) & (t_1 < t \le T) \end{cases} \]

KerasにはCosineDecayのようなOne-Cycle Policyの組み込み実装は存在しないため、on_train_batch_beginでバッチ単位に学習率を書き換えるカスタムコールバックとして自作します。


実験コード

使用環境はGoogle Colab(GPU:T4)、データセットはCIFAR-10です。学習率スケジュール以外の条件は全て同一にし、比較実験のためtf.config.experimental.enable_op_determinism()で乱数シードを固定しています。

環境準備(最初に一度だけ実行)

# ── 環境準備(最初に一度だけ実行)──────────────────────
!apt-get -y install fonts-ipafont-gothic
!rm -rf /root/.cache/matplotlib
!pip install -q japanize_matplotlib
print("環境準備完了")
実行結果をクリックして内容を開く
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following additional packages will be installed:
  fonts-ipafont-mincho
The following NEW packages will be installed:
  fonts-ipafont-gothic fonts-ipafont-mincho
0 upgraded, 2 newly installed, 0 to remove and 33 not upgraded.
Need to get 8,237 kB of archives.
After this operation, 28.7 MB of additional disk space will be used.
Get:1 http://archive.ubuntu.com/ubuntu noble/universe amd64 fonts-ipafont-gothic all 00303-21ubuntu1 [3,513 kB]
Get:2 http://archive.ubuntu.com/ubuntu noble/universe amd64 fonts-ipafont-mincho all 00303-21ubuntu1 [4,724 kB]
Fetched 8,237 kB in 3s (3,024 kB/s)
Selecting previously unselected package fonts-ipafont-gothic.
(Reading database ... 127332 files and directories currently installed.)
Preparing to unpack .../fonts-ipafont-gothic_00303-21ubuntu1_all.deb ...
Unpacking fonts-ipafont-gothic (00303-21ubuntu1) ...
Selecting previously unselected package fonts-ipafont-mincho.
Preparing to unpack .../fonts-ipafont-mincho_00303-21ubuntu1_all.deb ...
Unpacking fonts-ipafont-mincho (00303-21ubuntu1) ...
Setting up fonts-ipafont-mincho (00303-21ubuntu1) ...
update-alternatives: using /usr/share/fonts/opentype/ipafont-mincho/ipam.ttf to provide /usr/share/fonts/truetype/fonts-japanese-mincho.ttf (fonts-japanese-mincho.ttf) in auto mode
Setting up fonts-ipafont-gothic (00303-21ubuntu1) ...
update-alternatives: using /usr/share/fonts/opentype/ipafont-gothic/ipag.ttf to provide /usr/share/fonts/truetype/fonts-japanese-gothic.ttf (fonts-japanese-gothic.ttf) in auto mode
Processing triggers for fontconfig (2.15.0-1.1ubuntu2) ...
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.1/4.1 MB 35.2 MB/s eta 0:00:00
  Preparing metadata (setup.py) ... done
  Building wheel for japanize_matplotlib (setup.py) ... done
環境準備完了

import・データ準備・モデル構築関数・乱数シード固定

import tensorflow as tf
from tensorflow import keras
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib
import time

# ── 乱数シード固定(比較実験の再現性確保のため)───────
tf.config.experimental.enable_op_determinism()
tf.random.set_seed(42)
np.random.seed(42)

(x_train, y_train), (x_test, y_test) = keras.datasets.cifar10.load_data()
x_train = x_train.astype('float32') / 255.0
x_test  = x_test.astype('float32')  / 255.0

def build_model(name):
    return keras.Sequential([
        keras.layers.Input(shape=(32, 32, 3)),
        keras.layers.Conv2D(64, (3, 3), activation='relu', padding='same'),
        keras.layers.MaxPooling2D((2, 2)),
        keras.layers.Conv2D(128, (3, 3), activation='relu', padding='same'),
        keras.layers.MaxPooling2D((2, 2)),
        keras.layers.GlobalAveragePooling2D(),
        keras.layers.Dense(128, activation='relu'),
        keras.layers.Dropout(0.2),
        keras.layers.Dense(10, activation='softmax'),
    ], name=name)
実行結果をクリックして内容を開く
Downloading data from https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz
170498071/170498071 ━━━━━━━━━━━━━━━━━━━━ 1277s 7us/step

One-Cycle Policyの自作コールバック

class OneCyclePolicy(keras.callbacks.Callback):
    """One-Cycle Policy:前半でLRを上昇させ、後半でコサイン状に減衰させる"""
    def __init__(self, max_lr, total_steps, pct_start=0.3,
                 div_factor=10.0, final_div_factor=100.0):
        super().__init__()
        self.max_lr = max_lr
        self.total_steps = total_steps
        self.up_steps = int(total_steps * pct_start)
        self.down_steps = total_steps - self.up_steps
        self.initial_lr = max_lr / div_factor
        self.final_lr = self.initial_lr / final_div_factor
        self.step_count = 0
        self.lr_history = []

    def on_train_batch_begin(self, batch, logs=None):
        if self.step_count <= self.up_steps:
            progress = self.step_count / max(self.up_steps, 1)
            lr = self.initial_lr + (self.max_lr - self.initial_lr) * progress
        else:
            progress = (self.step_count - self.up_steps) / max(self.down_steps, 1)
            lr = self.final_lr + 0.5 * (self.max_lr - self.final_lr) * \
                 (1 + np.cos(np.pi * min(progress, 1.0)))
        self.model.optimizer.learning_rate.assign(lr)
        self.lr_history.append(float(lr))
        self.step_count += 1

3パターンの学習実行

epochs = 30
batch_size = 64
steps_per_epoch = int(len(x_train) * 0.8 // batch_size)
total_steps = steps_per_epoch * epochs

configs = ['A_fixed_lr', 'B_cosine_annealing', 'C_onecycle']
histories, times, scores, onecycle_lr_log = {}, {}, {}, None

for name in configs:
    keras.backend.clear_session()
    tf.random.set_seed(42)
    model = build_model(name)
    fit_callbacks = []

    if name == 'A_fixed_lr':
        optimizer = keras.optimizers.Adam(learning_rate=0.001)
    elif name == 'B_cosine_annealing':
        lr_schedule = keras.optimizers.schedules.CosineDecay(
            initial_learning_rate=0.001, decay_steps=total_steps, alpha=0.01)
        optimizer = keras.optimizers.Adam(learning_rate=lr_schedule)
    else:  # C_onecycle
        optimizer = keras.optimizers.Adam(learning_rate=0.001)
        onecycle_cb = OneCyclePolicy(max_lr=0.005, total_steps=total_steps)
        fit_callbacks.append(onecycle_cb)

    model.compile(optimizer=optimizer,
                  loss='sparse_categorical_crossentropy',
                  metrics=['accuracy'])

    print(f"\n=== {name} ===")
    start = time.time()
    h = model.fit(x_train, y_train, epochs=epochs, batch_size=batch_size,
                  validation_split=0.2, callbacks=fit_callbacks, verbose=1)
    t = time.time() - start
    s = model.evaluate(x_test, y_test, verbose=0)

    label = name.split('_', 1)[1]
    histories[label] = h
    times[label] = t
    scores[label] = s
    if name == 'C_onecycle':
        onecycle_lr_log = onecycle_cb.lr_history

    print(f"学習時間:{t:.1f}秒 test_accuracy:{s[1]:.4f}")
実行結果をクリックして内容を開く
=== A_fixed_lr ===
Epoch 1/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 21s 15ms/step - accuracy: 0.2573 - loss: 1.9450 - val_accuracy: 0.3654 - val_loss: 1.7162
Epoch 2/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.3662 - loss: 1.6904 - val_accuracy: 0.4100 - val_loss: 1.6059
Epoch 3/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.4217 - loss: 1.5711 - val_accuracy: 0.4627 - val_loss: 1.4900
Epoch 4/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.4624 - loss: 1.4705 - val_accuracy: 0.4868 - val_loss: 1.4111
Epoch 5/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 10ms/step - accuracy: 0.4889 - loss: 1.3972 - val_accuracy: 0.5004 - val_loss: 1.3603
Epoch 6/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.5110 - loss: 1.3435 - val_accuracy: 0.5114 - val_loss: 1.3191
Epoch 7/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.5260 - loss: 1.2992 - val_accuracy: 0.5365 - val_loss: 1.2662
Epoch 8/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.5367 - loss: 1.2654 - val_accuracy: 0.5448 - val_loss: 1.2484
Epoch 9/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.5510 - loss: 1.2344 - val_accuracy: 0.5501 - val_loss: 1.2222
Epoch 10/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.5610 - loss: 1.2066 - val_accuracy: 0.5728 - val_loss: 1.1800
Epoch 11/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.5728 - loss: 1.1730 - val_accuracy: 0.5816 - val_loss: 1.1515
Epoch 12/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.5822 - loss: 1.1480 - val_accuracy: 0.5855 - val_loss: 1.1382
Epoch 13/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.5931 - loss: 1.1250 - val_accuracy: 0.5885 - val_loss: 1.1392
Epoch 14/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6010 - loss: 1.1037 - val_accuracy: 0.5982 - val_loss: 1.1126
Epoch 15/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.6112 - loss: 1.0796 - val_accuracy: 0.6037 - val_loss: 1.0803
Epoch 16/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 11s 10ms/step - accuracy: 0.6176 - loss: 1.0599 - val_accuracy: 0.6150 - val_loss: 1.0688
Epoch 17/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6249 - loss: 1.0412 - val_accuracy: 0.6108 - val_loss: 1.0678
Epoch 18/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6345 - loss: 1.0214 - val_accuracy: 0.6230 - val_loss: 1.0415
Epoch 19/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6385 - loss: 1.0050 - val_accuracy: 0.6317 - val_loss: 1.0208
Epoch 20/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6432 - loss: 0.9904 - val_accuracy: 0.6327 - val_loss: 1.0143
Epoch 21/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.6500 - loss: 0.9782 - val_accuracy: 0.6298 - val_loss: 1.0158
Epoch 22/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6557 - loss: 0.9632 - val_accuracy: 0.6393 - val_loss: 0.9927
Epoch 23/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.6581 - loss: 0.9494 - val_accuracy: 0.6486 - val_loss: 0.9735
Epoch 24/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 10s 8ms/step - accuracy: 0.6628 - loss: 0.9383 - val_accuracy: 0.6484 - val_loss: 0.9720
Epoch 25/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.6707 - loss: 0.9230 - val_accuracy: 0.6525 - val_loss: 0.9630
Epoch 26/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6715 - loss: 0.9127 - val_accuracy: 0.6567 - val_loss: 0.9532
Epoch 27/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.6763 - loss: 0.9036 - val_accuracy: 0.6568 - val_loss: 0.9548
Epoch 28/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6822 - loss: 0.8898 - val_accuracy: 0.6580 - val_loss: 0.9523
Epoch 29/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6821 - loss: 0.8834 - val_accuracy: 0.6652 - val_loss: 0.9289
Epoch 30/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6904 - loss: 0.8682 - val_accuracy: 0.6671 - val_loss: 0.9296
学習時間:184.0秒 test_accuracy:0.6679

=== B_cosine_annealing ===
Epoch 1/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 8s 10ms/step - accuracy: 0.2514 - loss: 1.9643 - val_accuracy: 0.3529 - val_loss: 1.7305
Epoch 2/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.3537 - loss: 1.7089 - val_accuracy: 0.4050 - val_loss: 1.6090
Epoch 3/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.4110 - loss: 1.5875 - val_accuracy: 0.4546 - val_loss: 1.5047
Epoch 4/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.4560 - loss: 1.4863 - val_accuracy: 0.4923 - val_loss: 1.3972
Epoch 5/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.4841 - loss: 1.4121 - val_accuracy: 0.5069 - val_loss: 1.3506
Epoch 6/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.5044 - loss: 1.3548 - val_accuracy: 0.5177 - val_loss: 1.3123
Epoch 7/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.5210 - loss: 1.3090 - val_accuracy: 0.5382 - val_loss: 1.2620
Epoch 8/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.5339 - loss: 1.2728 - val_accuracy: 0.5548 - val_loss: 1.2260
Epoch 9/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.5490 - loss: 1.2399 - val_accuracy: 0.5553 - val_loss: 1.2140
Epoch 10/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.5583 - loss: 1.2096 - val_accuracy: 0.5698 - val_loss: 1.1835
Epoch 11/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.5697 - loss: 1.1898 - val_accuracy: 0.5743 - val_loss: 1.1757
Epoch 12/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.5777 - loss: 1.1638 - val_accuracy: 0.5841 - val_loss: 1.1473
Epoch 13/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.5854 - loss: 1.1441 - val_accuracy: 0.5963 - val_loss: 1.1187
Epoch 14/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.5935 - loss: 1.1276 - val_accuracy: 0.6023 - val_loss: 1.0997
Epoch 15/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.5993 - loss: 1.1077 - val_accuracy: 0.6061 - val_loss: 1.0858
Epoch 16/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6048 - loss: 1.0933 - val_accuracy: 0.6101 - val_loss: 1.0758
Epoch 17/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.6097 - loss: 1.0779 - val_accuracy: 0.6149 - val_loss: 1.0610
Epoch 18/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6173 - loss: 1.0648 - val_accuracy: 0.6228 - val_loss: 1.0455
Epoch 19/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.6202 - loss: 1.0521 - val_accuracy: 0.6249 - val_loss: 1.0347
Epoch 20/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6276 - loss: 1.0405 - val_accuracy: 0.6257 - val_loss: 1.0301
Epoch 21/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6287 - loss: 1.0347 - val_accuracy: 0.6281 - val_loss: 1.0294
Epoch 22/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.6334 - loss: 1.0225 - val_accuracy: 0.6318 - val_loss: 1.0189
Epoch 23/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6360 - loss: 1.0122 - val_accuracy: 0.6334 - val_loss: 1.0150
Epoch 24/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.6402 - loss: 1.0074 - val_accuracy: 0.6342 - val_loss: 1.0117
Epoch 25/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6386 - loss: 0.9999 - val_accuracy: 0.6358 - val_loss: 1.0047
Epoch 26/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.6425 - loss: 0.9924 - val_accuracy: 0.6371 - val_loss: 1.0030
Epoch 27/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6441 - loss: 0.9914 - val_accuracy: 0.6361 - val_loss: 1.0031
Epoch 28/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6423 - loss: 0.9898 - val_accuracy: 0.6392 - val_loss: 0.9990
Epoch 29/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.6449 - loss: 0.9869 - val_accuracy: 0.6394 - val_loss: 0.9980
Epoch 30/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6432 - loss: 0.9852 - val_accuracy: 0.6399 - val_loss: 0.9970
学習時間:161.4秒 test_accuracy:0.6390

=== C_onecycle ===
Epoch 1/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 8s 9ms/step - accuracy: 0.2450 - loss: 1.9812 - val_accuracy: 0.3422 - val_loss: 1.7517
Epoch 2/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.3554 - loss: 1.7080 - val_accuracy: 0.4212 - val_loss: 1.5868
Epoch 3/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.4245 - loss: 1.5643 - val_accuracy: 0.4730 - val_loss: 1.4477
Epoch 4/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.4654 - loss: 1.4618 - val_accuracy: 0.5142 - val_loss: 1.3409
Epoch 5/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 10s 8ms/step - accuracy: 0.4965 - loss: 1.3735 - val_accuracy: 0.5371 - val_loss: 1.2770
Epoch 6/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.5233 - loss: 1.3030 - val_accuracy: 0.5499 - val_loss: 1.2452
Epoch 7/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.5457 - loss: 1.2481 - val_accuracy: 0.5792 - val_loss: 1.1550
Epoch 8/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 10ms/step - accuracy: 0.5697 - loss: 1.1891 - val_accuracy: 0.6035 - val_loss: 1.1061
Epoch 9/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.5866 - loss: 1.1462 - val_accuracy: 0.6100 - val_loss: 1.0784
Epoch 10/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.6036 - loss: 1.0987 - val_accuracy: 0.6162 - val_loss: 1.0699
Epoch 11/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6231 - loss: 1.0506 - val_accuracy: 0.6384 - val_loss: 1.0008
Epoch 12/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 11s 9ms/step - accuracy: 0.6398 - loss: 0.9991 - val_accuracy: 0.6565 - val_loss: 0.9641
Epoch 13/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6517 - loss: 0.9670 - val_accuracy: 0.6570 - val_loss: 0.9693
Epoch 14/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.6670 - loss: 0.9369 - val_accuracy: 0.6619 - val_loss: 0.9490
Epoch 15/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6794 - loss: 0.8923 - val_accuracy: 0.6708 - val_loss: 0.9225
Epoch 16/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 10s 8ms/step - accuracy: 0.6894 - loss: 0.8676 - val_accuracy: 0.6778 - val_loss: 0.9048
Epoch 17/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.7036 - loss: 0.8322 - val_accuracy: 0.6833 - val_loss: 0.8960
Epoch 18/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.7129 - loss: 0.8035 - val_accuracy: 0.6851 - val_loss: 0.8926
Epoch 19/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.7255 - loss: 0.7707 - val_accuracy: 0.6999 - val_loss: 0.8700
Epoch 20/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.7344 - loss: 0.7440 - val_accuracy: 0.7015 - val_loss: 0.8577
Epoch 21/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 11s 10ms/step - accuracy: 0.7453 - loss: 0.7124 - val_accuracy: 0.7064 - val_loss: 0.8413
Epoch 22/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.7511 - loss: 0.6909 - val_accuracy: 0.7150 - val_loss: 0.8283
Epoch 23/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.7649 - loss: 0.6633 - val_accuracy: 0.7152 - val_loss: 0.8305
Epoch 24/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.7718 - loss: 0.6408 - val_accuracy: 0.7149 - val_loss: 0.8423
Epoch 25/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.7805 - loss: 0.6179 - val_accuracy: 0.7200 - val_loss: 0.8260
Epoch 26/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.7860 - loss: 0.6002 - val_accuracy: 0.7241 - val_loss: 0.8129
Epoch 27/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.7939 - loss: 0.5820 - val_accuracy: 0.7319 - val_loss: 0.7921
Epoch 28/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.7953 - loss: 0.5735 - val_accuracy: 0.7320 - val_loss: 0.7883
Epoch 29/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.7981 - loss: 0.5622 - val_accuracy: 0.7315 - val_loss: 0.7854
Epoch 30/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.8010 - loss: 0.5587 - val_accuracy: 0.7344 - val_loss: 0.7826
学習時間:194.1秒 test_accuracy:0.7301
  

グラフ+サマリー

# ── val_accuracy / val_loss 比較グラフ ───────────────
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
for label, h in histories.items():
    axes[0].plot(h.history['val_accuracy'], label=label)
    axes[1].plot(h.history['val_loss'],     label=label)
axes[0].set_title('val_accuracy の比較(全30エポック)')
axes[1].set_title('val_loss の比較(全30エポック)')
for ax in axes:
    ax.set_xlabel('Epoch'); ax.legend(); ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('onecycle_vs_cosine_comparison.png', dpi=150)
plt.show()

# ── 学習率の推移グラフ(Cosine Annealing vs One-Cycle)──
fig2, ax2 = plt.subplots(figsize=(8, 5))
cosine_lrs = [keras.optimizers.schedules.CosineDecay(
    initial_learning_rate=0.001, decay_steps=total_steps, alpha=0.01)(s).numpy()
    for s in range(0, total_steps, steps_per_epoch)]
onecycle_lrs = onecycle_lr_log[::steps_per_epoch]
ax2.plot(cosine_lrs, label='Cosine Annealing')
ax2.plot(onecycle_lrs, label='One-Cycle Policy')
ax2.set_xlabel('Epoch'); ax2.set_ylabel('Learning Rate')
ax2.set_title('学習率スケジュールの推移比較')
ax2.legend(); ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('onecycle_vs_cosine_lr_schedule.png', dpi=150)
plt.show()

print("\n===== 最終結果サマリー =====")
print(f"{'Pattern':>18} | {'Val Acc':>8} | {'Test Acc':>9} | {'Time(s)':>8}")
print("-" * 55)
for label in ['fixed_lr', 'cosine_annealing', 'onecycle']:
    val_acc  = histories[label].history['val_accuracy'][-1]
    test_acc = scores[label][1]
    t        = times[label]
    print(f"{label:>18} | {val_acc:>8.4f} | {test_acc:>9.4f} | {t:>8.1f}")
print("-" * 55)
実行結果をクリックして内容を開く
===== 最終結果サマリー =====
           Pattern |  Val Acc |  Test Acc |  Time(s)
-------------------------------------------------------
          fixed_lr |   0.6671 |    0.6679 |    184.0
  cosine_annealing |   0.6399 |    0.6390 |    161.4
          onecycle |   0.7344 |    0.7301 |    194.1
-------------------------------------------------------
  

実験結果

精度・損失グラフ

精度グラフ

精度グラフ

損失グラフ

損失グラフ

val_accuracyのグラフでは、C:One-Cycle Policyだけがエポック12以降に他の2本から明確に離れ、右肩上がりのまま30エポック目まで伸び続けます。val_lossも同様に、One-Cycleだけがエポック20以降も下降を続けているのに対し、AとBはエポック20前後で横ばいに近づきます。

学習率推移グラフ

学習率推移グラフ

One-Cycle Policyはエポック9付近でmax_lr=0.005(固定学習率の5倍)に到達したあと、そこから約21エポックかけてほぼゼロ近くまで減衰します。Cosine Annealingは開始直後(0.001)から一貫して減衰する滑らかな曲線です。

パターン最終 val_accuracy最終 test_accuracy学習時間
A:固定学習率66.71%66.79%184.0秒
B:Cosine Annealing63.99%63.90%161.4秒
C:One-Cycle Policy73.44%73.01%194.1秒
⚠️ ハマりポイント
  • model.optimizer.learning_rate.assign(lr)は、optimizerのlearning_rateがschedulesオブジェクトではなく通常の変数(Variable)である場合のみ有効です。BパターンでCosineDecayを使った直後にCパターンを実行する際、keras.backend.clear_session()とoptimizerの再生成を忘れると意図しない挙動になります。
  • One-Cycle Policyのmax_lrは基準学習率(0.001)よりもかなり高め(今回は0.005=5倍)に設定するのが効果的でした。今回はこの設定で発散(lossがnanになる等)は起きませんでしたが、モデルやデータセットが変わればmax_lrの再調整が必要です。
  • on_train_batch_beginはエポックではなくバッチ単位で呼ばれるため、total_stepsの計算を誤ると学習終盤で学習率が0付近に到達しない、または早期に到達してしまいます。

考察

① 精度はOne-Cycle PolicyとCosine Annealingのどちらが上回ったか

test_accuracyで比較すると、One-Cycle Policy(73.01%)がCosine Annealing(63.90%)を9.11pt、固定学習率(66.79%)を6.22pt上回りました。3パターンの中で唯一、大きな差をつけて優勢だったのがOne-Cycle Policyです。

② 学習率の「山型」推移が収束に与えた影響

One-Cycle Policyの学習曲線を見ると、エポック9〜12あたり(LRがピーク0.005に到達する時期)はval_accuracyが61〜65%程度でAやBと大差ありません。ところがエポック13以降、LRが下降フェーズに入ると同時にval_accuracyが急伸し始め、エポック20で70.15%、エポック30で73.44%まで伸びています。これはOne-Cycle Policyの理論通り、「高LRで広く探索→急減衰で精密に着地」という挙動が実際に表れた結果と言えます。

参考として、MNISTで実施した学習率スケジューラ5種の比較実験では、OneCycleScheduler(val_accuracy 98.98%)がReduceLROnPlateau(99.03%)に僅差で次ぐ結果でした。今回のCIFAR-10・GAP構成では、One-Cycleが他手法にさらに大きく差をつける結果となり、タスクによる効果の出方の違いが見えた形です。

③ 学習時間への影響

学習時間はA:184.0秒、B:161.4秒、C:194.1秒でした。One-Cycleが最も長くなっていますが、固定学習率との差はわずか10秒程度で、バッチ単位のassign()によるオーバーヘッドはほぼ無視できるレベルです。一方でCosine AnnealingがAより約23秒短い点は、学習率スケジュール自体の計算コストというより、Colab実行環境側のGPU割り当て変動によるノイズの範囲と考えられます。

④ 過学習の度合いに差はあったか

最終エポックのtrain_accuracyとval_accuracyの差を見ると、A:2.33pt(0.6904→0.6671)、B:0.33pt(0.6432→0.6399)、C:6.66pt(0.8010→0.7344)でした。Cosine Annealingはほとんど過学習していない代わりに、そもそも到達した精度自体が低く「学習不足のまま収束」に近い状態です。One-Cycle Policyは3パターン中もっとも過学習の兆候(trainとvalの乖離)が大きいものの、val_accuracy自体がもっとも高いため、トレードオフとしては十分見合っています。ただしエポック数をさらに増やす場合は、Dropout率の引き上げなど過学習対策とのセット運用が必要になりそうです。

興味深いのは、固定学習率がCosine Annealingを上回った点です。これは前回のCosine Annealing vs ReduceLROnPlateau実験(固定学習率69.60% > Cosine Annealing68.76%)とも方向が一致しており、今回のalpha=0.01設定でのCosine Annealingは、固定学習率0.001よりも実質的に低い学習率で学習している時間が長く、30エポックという限られた予算の中では「まだ下げなくていい時期に下げてしまっている」可能性があります。2回の独立した実験で同じ傾向が出ているため、単発のノイズではなく再現性のある挙動と見て良さそうです。


実務での推奨

状況推奨スケジュール理由
エポック数が事前に固定されていて、精度を最優先したいOne-Cycle Policy今回の実験でtest_accuracy +6〜9ptと最も効果が大きかった
max_lrのチューニングに時間をかけられない固定学習率Cosine Annealing(alpha=0.01設定)より安定して良い結果だった
過学習を極力避けたい・train/valの乖離を抑えたいCosine Annealing3パターン中もっとも過学習が少なかった(ただし到達精度も最も低い)
val_lossの停滞を見ながら安全運用したいReduceLROnPlateau(前回記事参照)実測ベースで調整するため事故が少ない
✅ まとめ
  • One-Cycle Policyは学習率を「上昇→下降」させる山型スケジュールで、Kerasには組み込み実装がないため自作コールバックで実現する
  • CIFAR-10・30エポックの実験では、One-Cycle Policy(test_accuracy 73.01%)が固定学習率(66.79%)を6.2pt、Cosine Annealing(63.90%)を9.1pt上回った
  • Cosine Annealingが固定学習率にすら劣るという結果は、前回のCosine Annealing vs ReduceLROnPlateau実験でも同様の傾向が見られており、alpha=0.01設定での「早すぎる減衰」が原因と考えられる
  • One-Cycle Policyはtrain/valの乖離がやや大きく出るため、長期学習では過学習対策と組み合わせるのが望ましい

関連記事もあわせてどうぞ: