Kerasで学習を終えた後、「もう少し精度を上げたいけど、モデルの構造は変えたくない」と思ったことはありませんか?
そんなときに使えるテクニックが SWA(Stochastic Weight Averaging) です。学習の後半で得られた複数エポック分の重みを平均するだけで、追加のパラメータもアーキテクチャ変更も不要という手軽さが特徴です。ただしKerasには公式のSWA実装がなく、tf.keras.callbacksにも該当するものはありません。今回はGoogle ColabとCIFAR-10を使い、SWAなし/SWAあり(後半10エポック平均)/SWAあり(後半20エポック平均)の3パターンで、自作コールバックによる実装とあわせて比較しました。
- SWA(Stochastic Weight Averaging)の仕組みと、何が精度向上につながるのか
- KerasでSWAを自作コールバックとして実装する方法(公式実装がない中での対処)
- 平均するエポック数(10エポック vs 20エポック)で結果がどう変わるか
- SWAを使う上でハマりやすいポイント(BatchNormalizationとの相性など)
SWAとは何をしているのか
通常の学習では、最終エポック時点の重みをそのままモデルとして採用します。SWAはこれとは異なり、学習後半の複数エポックで得られた重みを単純平均してひとつのモデルにします。
$$ \theta_{SWA} = \frac{1}{n} \sum_{i=1}^{n} \theta_i $$
ここで $\theta_i$ は $i$ 番目に記録したエポック終了時点の重み、$n$ は平均に使ったエポック数です。損失関数の局面(loss landscape)において、終盤の重みは局所的な谷の中でバラついていることが多く、それらを平均することでより平坦な(=汎化しやすい)位置に着地しやすくなる、というのがSWAの基本的な考え方です。
| 項目 | 通常の学習 | SWA |
|---|---|---|
| 採用する重み | 最終エポックの重みのみ | 指定範囲のエポックの重みを平均 |
| 追加の学習コスト | なし | ほぼなし(重みの保持・平均化のみ) |
| 推論時のモデル構造 | 変化なし | 変化なし(重みが違うだけ) |
| BatchNormalization使用時の注意 | ー | 移動平均統計量の再計算が必要な場合あり |
今回のベースラインモデルはBatchNormalizationを使っていないため、この注意点は今回の実験には影響しませんが、後述のハマりポイントで詳しく触れます。
実験コード
使用環境はGoogle Colab(GPU:T4)、データセットはCIFAR-10です。ベースラインモデル(Conv2D×2層、GAP、Dropout=0.2、Adam lr=0.001、batch_size=64、30エポック)は固定し、SWAの有無と平均エポック数だけを変えて比較します。
環境準備(最初に一度だけ実行)
# ── 環境準備(最初に一度だけ実行)──────────────────────
!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 53 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 jammy/universe amd64 fonts-ipafont-gothic all 00303-21ubuntu1 [3,513 kB]
Get:2 http://archive.ubuntu.com/ubuntu jammy/universe amd64 fonts-ipafont-mincho all 00303-21ubuntu1 [4,724 kB]
Fetched 8,237 kB in 1s (9,081 kB/s)
Selecting previously unselected package fonts-ipafont-gothic.
(Reading database ... 122403 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.13.1-4.2ubuntu5) ...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.1/4.1 MB 57.0 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 random
import matplotlib.pyplot as plt
import japanize_matplotlib
import time
(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 set_seed(seed=42):
random.seed(seed)
np.random.seed(seed)
tf.random.set_seed(seed)
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 ━━━━━━━━━━━━━━━━━━━━ 10s 0us/step
SWAコールバックと3パターンの学習実行
# ── SWA用の自作コールバック ─────────────────────────
# Kerasに公式のSWA実装は存在しないため、on_epoch_endで重みを
# 蓄積・平均化するコールバックを自作する。
class SWACallback(keras.callbacks.Callback):
def __init__(self, swa_start_epoch):
super().__init__()
self.swa_start_epoch = swa_start_epoch # このエポック数以降を平均対象にする
self.swa_weights = None
self.n_models = 0
def on_epoch_end(self, epoch, logs=None):
# epochは0始まりなので+1して比較する
if (epoch + 1) >= self.swa_start_epoch:
current_weights = self.model.get_weights()
if self.swa_weights is None:
# 参照ではなくコピーを保持する(ハマりポイント参照)
self.swa_weights = [w.copy() for w in current_weights]
else:
self.swa_weights = [sw + w for sw, w in zip(self.swa_weights, current_weights)]
self.n_models += 1
def get_swa_weights(self):
return [sw / self.n_models for sw in self.swa_weights]
def compile_and_fit(model, swa_start_epoch=None):
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
callbacks = []
swa_cb = None
if swa_start_epoch is not None:
swa_cb = SWACallback(swa_start_epoch)
callbacks.append(swa_cb)
start = time.time()
history = model.fit(x_train, y_train, epochs=30, batch_size=64,
validation_split=0.2, verbose=1, callbacks=callbacks)
elapsed = time.time() - start
return history, elapsed, swa_cb
configs = [
("A_no_swa", None), # SWAなし(通常の学習)
("B_swa_last10", 21), # SWAあり:epoch21〜30の重みを平均
("C_swa_last20", 11), # SWAあり:epoch11〜30の重みを平均
]
histories, times, scores, swa_scores = {}, {}, {}, {}
for name, swa_start in configs:
print(f"\n=== {name} ===")
set_seed(42)
model = build_model(name)
h, t, swa_cb = compile_and_fit(model, swa_start_epoch=swa_start)
# 最終エポックの重みでの評価
final_score = model.evaluate(x_test, y_test, verbose=0)
label = name.split('_', 1)[1]
histories[label] = h
times[label] = t
scores[label] = final_score
print(f"[最終エポック] test_accuracy:{final_score[1]:.4f}")
# SWAパターンはさらに平均重みでも評価する
if swa_cb is not None:
swa_model = build_model(name + "_swa")
swa_model.set_weights(swa_cb.get_swa_weights())
swa_model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
swa_score = swa_model.evaluate(x_test, y_test, verbose=0)
swa_scores[label] = swa_score
print(f"[SWA平均重み ] test_accuracy:{swa_score[1]:.4f}(平均に使ったエポック数:{swa_cb.n_models})")
実行結果をクリックして内容を開く
=== A_no_swa === Epoch 1/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 10s 9ms/step - accuracy: 0.2565 - loss: 1.9329 - val_accuracy: 0.3560 - val_loss: 1.7227 Epoch 2/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.3604 - loss: 1.6939 - val_accuracy: 0.4240 - val_loss: 1.5825 Epoch 3/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.4237 - loss: 1.5654 - val_accuracy: 0.4708 - val_loss: 1.4690 Epoch 4/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 7ms/step - accuracy: 0.4636 - loss: 1.4671 - val_accuracy: 0.4922 - val_loss: 1.3976 Epoch 5/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.4887 - loss: 1.3946 - val_accuracy: 0.5087 - val_loss: 1.3416 Epoch 6/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5058 - loss: 1.3495 - val_accuracy: 0.5266 - val_loss: 1.3003 Epoch 7/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.5274 - loss: 1.3048 - val_accuracy: 0.5409 - val_loss: 1.2689 Epoch 8/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5393 - loss: 1.2686 - val_accuracy: 0.5523 - val_loss: 1.2276 Epoch 9/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5508 - loss: 1.2364 - val_accuracy: 0.5634 - val_loss: 1.2149 Epoch 10/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5616 - loss: 1.2087 - val_accuracy: 0.5687 - val_loss: 1.2015 Epoch 11/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5727 - loss: 1.1803 - val_accuracy: 0.5770 - val_loss: 1.1658 Epoch 12/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5804 - loss: 1.1586 - val_accuracy: 0.5917 - val_loss: 1.1402 Epoch 13/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5893 - loss: 1.1384 - val_accuracy: 0.5981 - val_loss: 1.1130 Epoch 14/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.5986 - loss: 1.1161 - val_accuracy: 0.6085 - val_loss: 1.0776 Epoch 15/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6039 - loss: 1.0961 - val_accuracy: 0.6113 - val_loss: 1.0776 Epoch 16/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6144 - loss: 1.0784 - val_accuracy: 0.6204 - val_loss: 1.0582 Epoch 17/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6177 - loss: 1.0600 - val_accuracy: 0.6302 - val_loss: 1.0446 Epoch 18/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6264 - loss: 1.0446 - val_accuracy: 0.6310 - val_loss: 1.0216 Epoch 19/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6331 - loss: 1.0253 - val_accuracy: 0.6348 - val_loss: 1.0099 Epoch 20/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6385 - loss: 1.0100 - val_accuracy: 0.6419 - val_loss: 1.0076 Epoch 21/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6425 - loss: 0.9953 - val_accuracy: 0.6457 - val_loss: 0.9902 Epoch 22/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6476 - loss: 0.9812 - val_accuracy: 0.6517 - val_loss: 0.9759 Epoch 23/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6533 - loss: 0.9710 - val_accuracy: 0.6497 - val_loss: 0.9897 Epoch 24/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6596 - loss: 0.9549 - val_accuracy: 0.6577 - val_loss: 0.9658 Epoch 25/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6637 - loss: 0.9436 - val_accuracy: 0.6589 - val_loss: 0.9675 Epoch 26/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6704 - loss: 0.9289 - val_accuracy: 0.6610 - val_loss: 0.9552 Epoch 27/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6704 - loss: 0.9178 - val_accuracy: 0.6678 - val_loss: 0.9388 Epoch 28/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6744 - loss: 0.9052 - val_accuracy: 0.6711 - val_loss: 0.9303 Epoch 29/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6777 - loss: 0.8957 - val_accuracy: 0.6702 - val_loss: 0.9354 Epoch 30/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6829 - loss: 0.8862 - val_accuracy: 0.6645 - val_loss: 0.9566 [最終エポック] test_accuracy:0.6631 === B_swa_last10 === Epoch 1/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 8s 9ms/step - accuracy: 0.2589 - loss: 1.9305 - val_accuracy: 0.3622 - val_loss: 1.7200 Epoch 2/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.3643 - loss: 1.6886 - val_accuracy: 0.4227 - val_loss: 1.5813 Epoch 3/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.4227 - loss: 1.5688 - val_accuracy: 0.4685 - val_loss: 1.4612 Epoch 4/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.4621 - loss: 1.4678 - val_accuracy: 0.4959 - val_loss: 1.3876 Epoch 5/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.4884 - loss: 1.3946 - val_accuracy: 0.5089 - val_loss: 1.3487 Epoch 6/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5056 - loss: 1.3496 - val_accuracy: 0.5261 - val_loss: 1.2936 Epoch 7/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.5229 - loss: 1.3118 - val_accuracy: 0.5396 - val_loss: 1.2627 Epoch 8/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5373 - loss: 1.2780 - val_accuracy: 0.5500 - val_loss: 1.2297 Epoch 9/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5496 - loss: 1.2439 - val_accuracy: 0.5562 - val_loss: 1.2061 Epoch 10/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.5595 - loss: 1.2156 - val_accuracy: 0.5727 - val_loss: 1.1812 Epoch 11/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5695 - loss: 1.1874 - val_accuracy: 0.5803 - val_loss: 1.1497 Epoch 12/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5782 - loss: 1.1661 - val_accuracy: 0.5857 - val_loss: 1.1481 Epoch 13/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.5883 - loss: 1.1468 - val_accuracy: 0.5909 - val_loss: 1.1237 Epoch 14/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5956 - loss: 1.1224 - val_accuracy: 0.5982 - val_loss: 1.1052 Epoch 15/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6029 - loss: 1.1048 - val_accuracy: 0.6021 - val_loss: 1.1016 Epoch 16/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6062 - loss: 1.0887 - val_accuracy: 0.6007 - val_loss: 1.0949 Epoch 17/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6141 - loss: 1.0709 - val_accuracy: 0.6154 - val_loss: 1.0754 Epoch 18/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6226 - loss: 1.0555 - val_accuracy: 0.6248 - val_loss: 1.0475 Epoch 19/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6283 - loss: 1.0389 - val_accuracy: 0.6361 - val_loss: 1.0162 Epoch 20/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6346 - loss: 1.0216 - val_accuracy: 0.6385 - val_loss: 1.0087 Epoch 21/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6380 - loss: 1.0077 - val_accuracy: 0.6442 - val_loss: 0.9846 Epoch 22/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6436 - loss: 0.9957 - val_accuracy: 0.6421 - val_loss: 0.9910 Epoch 23/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6484 - loss: 0.9805 - val_accuracy: 0.6486 - val_loss: 0.9803 Epoch 24/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6546 - loss: 0.9673 - val_accuracy: 0.6544 - val_loss: 0.9596 Epoch 25/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6564 - loss: 0.9548 - val_accuracy: 0.6520 - val_loss: 0.9580 Epoch 26/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6656 - loss: 0.9401 - val_accuracy: 0.6599 - val_loss: 0.9461 Epoch 27/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6640 - loss: 0.9335 - val_accuracy: 0.6658 - val_loss: 0.9306 Epoch 28/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6715 - loss: 0.9181 - val_accuracy: 0.6697 - val_loss: 0.9210 Epoch 29/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6756 - loss: 0.9068 - val_accuracy: 0.6725 - val_loss: 0.9135 Epoch 30/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6801 - loss: 0.8958 - val_accuracy: 0.6689 - val_loss: 0.9282 [最終エポック] test_accuracy:0.6683 [SWA平均重み ] test_accuracy:0.6584(平均に使ったエポック数:10) === C_swa_last20 === Epoch 1/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 8s 8ms/step - accuracy: 0.2572 - loss: 1.9301 - val_accuracy: 0.3584 - val_loss: 1.7192 Epoch 2/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.3650 - loss: 1.6834 - val_accuracy: 0.4326 - val_loss: 1.5632 Epoch 3/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.4335 - loss: 1.5494 - val_accuracy: 0.4737 - val_loss: 1.4511 Epoch 4/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.4684 - loss: 1.4541 - val_accuracy: 0.4965 - val_loss: 1.3825 Epoch 5/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.4938 - loss: 1.3843 - val_accuracy: 0.5134 - val_loss: 1.3383 Epoch 6/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5102 - loss: 1.3404 - val_accuracy: 0.5271 - val_loss: 1.2943 Epoch 7/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5276 - loss: 1.2994 - val_accuracy: 0.5394 - val_loss: 1.2648 Epoch 8/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.5386 - loss: 1.2668 - val_accuracy: 0.5486 - val_loss: 1.2344 Epoch 9/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5522 - loss: 1.2344 - val_accuracy: 0.5605 - val_loss: 1.2064 Epoch 10/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5628 - loss: 1.2057 - val_accuracy: 0.5714 - val_loss: 1.1757 Epoch 11/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5715 - loss: 1.1765 - val_accuracy: 0.5832 - val_loss: 1.1468 Epoch 12/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.5839 - loss: 1.1534 - val_accuracy: 0.5907 - val_loss: 1.1321 Epoch 13/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5895 - loss: 1.1345 - val_accuracy: 0.5966 - val_loss: 1.1183 Epoch 14/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5997 - loss: 1.1103 - val_accuracy: 0.6043 - val_loss: 1.0905 Epoch 15/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6070 - loss: 1.0897 - val_accuracy: 0.6042 - val_loss: 1.0977 Epoch 16/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.6140 - loss: 1.0717 - val_accuracy: 0.6194 - val_loss: 1.0608 Epoch 17/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6193 - loss: 1.0527 - val_accuracy: 0.6208 - val_loss: 1.0559 Epoch 18/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6287 - loss: 1.0391 - val_accuracy: 0.6335 - val_loss: 1.0248 Epoch 19/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6338 - loss: 1.0200 - val_accuracy: 0.6420 - val_loss: 1.0046 Epoch 20/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6391 - loss: 1.0036 - val_accuracy: 0.6364 - val_loss: 1.0063 Epoch 21/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6434 - loss: 0.9933 - val_accuracy: 0.6468 - val_loss: 0.9847 Epoch 22/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6468 - loss: 0.9793 - val_accuracy: 0.6505 - val_loss: 0.9763 Epoch 23/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6548 - loss: 0.9625 - val_accuracy: 0.6524 - val_loss: 0.9693 Epoch 24/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6573 - loss: 0.9525 - val_accuracy: 0.6580 - val_loss: 0.9499 Epoch 25/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6603 - loss: 0.9426 - val_accuracy: 0.6528 - val_loss: 0.9716 Epoch 26/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6660 - loss: 0.9312 - val_accuracy: 0.6546 - val_loss: 0.9746 Epoch 27/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6704 - loss: 0.9175 - val_accuracy: 0.6659 - val_loss: 0.9315 Epoch 28/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6759 - loss: 0.9017 - val_accuracy: 0.6732 - val_loss: 0.9244 Epoch 29/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6813 - loss: 0.8907 - val_accuracy: 0.6736 - val_loss: 0.9120 Epoch 30/30 625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6855 - loss: 0.8802 - val_accuracy: 0.6672 - val_loss: 0.9303 [最終エポック] test_accuracy:0.6689 [SWA平均重み ] test_accuracy:0.6415(平均に使ったエポック数:20)
グラフ+サマリー
# ── 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('swa_comparison.png', dpi=150)
plt.show()
print("\n===== 最終結果サマリー =====")
print(f"{'Pattern':>14} | {'最終epoch Acc':>13} | {'SWA平均 Acc':>11} | {'差分':>8} | {'Time(s)':>8}")
print("-" * 66)
for label in ['no_swa', 'swa_last10', 'swa_last20']:
final_acc = scores[label][1]
swa_acc = swa_scores.get(label, (None, None))[1]
diff = (swa_acc - final_acc) if swa_acc is not None else None
t = times[label]
swa_str = f"{swa_acc:.4f}" if swa_acc is not None else " ー"
diff_str = f"{diff:+.4f}" if diff is not None else " ー"
print(f"{label:>14} | {final_acc:>13.4f} | {swa_str:>11} | {diff_str:>8} | {t:>8.1f}")
print("-" * 66)
実行結果をクリックして内容を開く
===== 最終結果サマリー =====
Pattern | 最終epoch Acc | SWA平均 Acc | 差分 | Time(s)
------------------------------------------------------------------
no_swa | 0.6631 | ー | ー | 126.1
swa_last10 | 0.6683 | 0.6584 | -0.0099 | 128.7
swa_last20 | 0.6689 | 0.6415 | -0.0274 | 125.1
------------------------------------------------------------------
実験結果
精度グラフ
損失グラフ
| パターン | 最終epochのtest_accuracy | SWA平均重みのtest_accuracy | 差分 | 学習時間 |
|---|---|---|---|---|
| A:SWAなし | 66.31% | ー | ー | 126.1秒 |
| B:SWAあり(後半10epoch平均) | 66.83% | 65.84% | −0.99pt | 128.7秒 |
| C:SWAあり(後半20epoch平均) | 66.89% | 64.15% | −2.74pt | 125.1秒 |
結論から言うと、今回の設定ではSWAはむしろ精度を下げました。しかも平均範囲を広げるほど(10epoch→20epoch)悪化が大きくなっています。次の考察で、なぜこうなったのかを実際のエポック推移から検証します。
- 重みの蓄積は参照ではなくコピーで持つ:
get_weights()が返すNumPy配列をそのままリストに追加すると、後続エポックで上書きされて全て同じ値になる。初回のみ.copy()で複製する必要がある。 - BatchNormalizationを使う場合は移動平均統計量の再計算が必要:SWAで重みを平均しても、BN層が保持している
moving_mean/moving_varianceは平均化前の値のまま残ってしまう。BN入りモデルにSWAを適用する場合は、平均重みを設定した後に訓練データを一部forwardして統計量を更新し直す処理(tf.keras.backend.learning_phaseを訓練モードにしてバッチを流す等)が別途必要になる。今回のベースラインはBN未使用のため影響なし。 - 平均に使うエポック数が少なすぎると効果が出にくい:n=1(最終エポックのみ)では通常の学習と同じ結果になる。ある程度の幅(複数エポック)を確保する必要がある。
考察
① SWAは今回の設定で精度向上に寄与したか——むしろ逆効果だった
B・Cとも、SWA平均重みのtest_accuracyは自分自身の最終epoch重みより明確に低く(B:−0.99pt、C:−2.74pt)、SWAなし(A:66.31%)と比べても優位性はありませんでした。「後半の重みを平均すれば安定して精度が上がる」というSWAの一般的な説明とは逆の結果です。
原因を探るため、平均対象にした区間のval_accuracyの推移を確認しました。
| パターン | 平均対象区間 | 区間開始時点のval_accuracy | 区間終了時点のval_accuracy | 区間中の上昇幅 |
|---|---|---|---|---|
| B(10epoch平均) | epoch21〜30 | 64.42% | 66.89% | +2.47pt |
| C(20epoch平均) | epoch11〜30 | 58.32% | 66.72% | +8.40pt |
どちらの区間も、平均を取り始めた時点でモデルはまだ収束しておらず、val_accuracyが右肩上がりで伸びている最中でした。この状態で重みを平均すると、まだ精度が低かった初期エポックの「未熟な」重みが、後半の「良くなった」重みを引きずり下ろす形になります。SWAが本来効果を発揮するのは、学習率を一定または周期的にして意図的にモデルを収束後の平坦な谷の中で振動させ、その振動範囲内の重みを平均するケースです。今回のようにAdamの学習率を一定にしたまま、まだ精度が伸びている途中の30エポックで区切ったため、SWAの前提条件を満たせていなかったと考えられます。
② 平均エポック数(10 vs 20)で結果に違いは出たか——広げるほど悪化した
Cの区間(epoch11〜30)はBの区間(epoch21〜30)より長く、かつ区間内でのval_accuracyの上昇幅も大きい(+8.40pt vs +2.47pt)ため、平均に含まれる「未熟な」重みの割合が増え、劣化がより大きくなりました(−2.74pt vs −0.99pt)。これは①の仮説と整合的な結果です。収束前のモデルに対しては、SWAの平均範囲を広げるほど逆効果が大きくなることが今回のデータから確認できます。
なお、同じseed=42で3パターンとも学習していますが、A・B・Cの最終epoch精度はわずかに異なります(66.31%/66.83%/66.89%)。これはGPU上の並列演算に起因する数値的な非決定性によるもので、SWAコールバック自体が学習過程に影響を与えたわけではありません。ただしこの誤差幅(0.5〜0.6pt程度)は、SWAによる劣化幅(0.99〜2.74pt)より小さいため、今回確認された劣化は誤差では説明しきれない実質的な差と判断しています。
③ 学習率を一定にしたままSWAを使うことの限界
SWAの原論文では、平均化フェーズにおいて周期的な学習率(cyclic learning rate)や高めの一定学習率を使うことで、意図的に損失局面の異なる位置を探索してから平均化する手法が一般的です。今回は他の条件を固定するため、Adamの学習率を全パターンでlr=0.001のまま一定にしています。そのため、本来のSWAが持つ「複数の異なる局所解を横断してから平均する」効果は限定的だった可能性があります。学習率スケジュールと組み合わせた場合の効果については、別記事で検証する予定です。
④ SWAあり/なしで学習時間に差は出たか——ほぼ誤差範囲
A(SWAなし):126.1秒、B(10epoch平均):128.7秒、C(20epoch平均):125.1秒でした。Bで+2.6秒とやや増えていますが、Cではむしろ短くなっており、実行ごとの揺らぎの範囲内と考えられます。重みリストのコピー・加算・平均化はCPU上の軽い処理のため、SWAを追加しても学習時間への影響はほぼ無視できることが確認できました。精度が下がる原因はあくまで①②で述べた「収束前の重みを平均している」ことにあり、計算コストの問題ではありません。
実務での推奨
| 状況 | 推奨 |
|---|---|
| val_accuracyが最後まで右肩上がりで、収束していない学習(今回のCIFAR-10・30epochのような設定) | SWAは避ける。むしろ精度を下げる可能性がある |
| SWAを使いたい場合 | 学習率を減衰させて一度収束させた後、一定または周期的な学習率に切り替えてから平均対象の区間に入る(原論文の使い方に合わせる) |
| 平均対象のエポック数を決める際 | その区間でval_accuracyがまだ大きく伸びていないか、事前に学習曲線を確認してから範囲を決める |
| BatchNormalizationを使っているモデル | 平均重み設定後の統計量再計算を忘れずに行う |
- SWAはアーキテクチャ変更なしで既存モデルの重みを平均するだけの手軽な手法
- Kerasに公式実装はなく、
on_epoch_endで重みを蓄積する自作コールバックで対応する - BatchNormalization使用時は移動平均統計量の再計算が必要になる点に注意
- 今回の設定(Adam学習率一定・30epoch・CIFAR-10)ではSWAは精度を下げた(10epoch平均で−0.99pt、20epoch平均で−2.74pt)
- 原因は「モデルがまだ収束しておらず、val_accuracyが上昇中の重み」を平均してしまったこと。平均範囲を広げるほど悪化も大きくなった
- SWAは万能の後付けテクニックではなく、学習が収束してから使うという前提条件が精度向上には不可欠
関連記事もあわせてどうぞ:
- 学習率スケジュールの比較 → Cosine Annealing vs ReduceLROnPlateau【Keras×CIFAR-10実験】
- EarlyStoppingとBatchNormalizationの相性 → restore_best_weightsの効果は?EarlyStopping比較検証
- Optimizerの基礎比較 → optimizerを変えると精度はどう変わる?(Adam vs SGD vs RMSprop)



0 件のコメント:
コメントを投稿