はじめに
畳み込みニューラルネットワーク(CNN)は、画像認識をはじめとする多くの機械学習タスクで用いられる強力なモデルです。その中で「プーリング層」は、画像中の特徴を抽出・圧縮する重要な処理の一つです。プーリング層の主な役割は、空間的なサイズを縮小しながら、重要な特徴を保持することです。
プーリングの代表的な手法としては、MaxPooling(最大値プーリング)とAveragePooling(平均値プーリング)があります。MaxPooling は局所領域で最大の値を抽出し、AveragePooling は平均値を取ります。これらは処理方法が異なるため、学習への影響や得られる特徴量の性質が変わってきます。
では、どちらを使うべきなのでしょうか? 本記事ではこの問いに答えるべく、MNIST手書き数字データセットを用いたシンプルなCNNモデルにおいて、MaxPoolingとAveragePoolingを比較します。
精度・損失・汎化性能などの観点から違いを可視化し、どのような場合にどちらが有利かを考察します。これからCNNモデルを構築する方や、プーリング層の理解を深めたい方にとって、参考になる内容を目指します。
使用環境
- TensorFlow 2 / Keras
- MNISTデータセット
- 比較対象: MaxPooling2D, AveragePooling2D
モデル構成
2つのモデルは、構造・パラメータ・ハイパーパラメータすべてを統一し、 プーリング層の種類(Max または Average)だけを変更しています。 これにより、違いが学習に与える影響を純粋に比較できます。
データの準備 (MNIST)
本実験では、手書き数字の画像データセットである MNIST を使用します。 MNISTは、0〜9の数字が中心に書かれた28×28ピクセルのグレースケール画像70,000枚から構成されており、 機械学習やディープラーニングの入門用として広く利用されています。
以下のコードでは、Kerasが提供するユーティリティを用いて、データの読み込みと前処理を行っています。 読み込まれたデータは、訓練用データ(60,000件)とテスト用データ(10,000件)に分かれています。
各画像データは元々28×28の2次元配列ですが、畳み込みニューラルネットワークで扱うために
チャンネル数1を持つ4次元テンソル(形式:(サンプル数, 高さ, 幅, チャンネル))に変換しています。
また、画素値は0〜255の整数値なので、/ 255.0
によって0〜1の範囲に正規化しています。
これにより学習が安定しやすくなります。
from tensorflow.keras import layers, models, datasets
# データの読み込みと前処理
(x_train, y_train), (x_test, y_test) = datasets.mnist.load_data()
x_train = x_train.reshape(-1, 28, 28, 1) / 255.0
x_test = x_test.reshape(-1, 28, 28, 1) / 255.0
モデルの構築
本セクションでは、プーリング層の種類(MaxPooling または AveragePooling)のみが異なる
2つのCNNモデルを構築するための関数 build_model()
を定義しています。
モデル構造はシンプルに保ちつつ、プーリング手法が性能に与える影響を明確に比較できるように設計されています。
モデルは以下のような構成です:
- Input層: 画像サイズ28×28ピクセル、チャンネル数1(グレースケール)
- 畳み込み層(Conv2D): フィルター数32、カーネルサイズ3×3、活性化関数はReLU
- プーリング層: 引数に応じてMaxPoolingまたはAveragePooling(どちらも2×2)
- Flatten層: 多次元配列を1次元ベクトルに変換
- 全結合層(Dense): ユニット数64、ReLU
- 出力層: ユニット数10(0〜9の分類)、Softmaxでクラス確率を出力
model.compile()
では、最適化アルゴリズムにAdam、損失関数に
多クラス分類向けの sparse_categorical_crossentropy
を使用し、
精度(accuracy)を指標として学習を行うよう指定しています。
# モデル定義関数
def build_model(pooling='max'):
model = models.Sequential()
model.add(layers.Input(shape=(28, 28, 1)))
model.add(layers.Conv2D(32, (3,3), activation='relu'))
if pooling == 'max':
model.add(layers.MaxPooling2D((2,2)))
else:
model.add(layers.AveragePooling2D((2,2)))
model.add(layers.Flatten())
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(10, activation='softmax'))
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])
return model
モデルの訓練
ここでは、前のセクションで定義した build_model()
関数を使って、
MaxPooling を使用するモデルと AveragePooling を使用するモデルの2種類を構築し、それぞれ訓練を行っています。
trained_models
辞書には2つのモデルインスタンスが格納されており、キーとして 'max' または 'avg' を使用しています。
各モデルは model.fit()
によって訓練されます。訓練には以下の設定を使用しています:
- エポック数: 5
- 検証データ: 訓練データの10%(
validation_split=0.1
) - 表示形式: 各エポックごとに訓練の進行状況が表示される(
verbose=1
)
すべての訓練履歴は histories
辞書に保存され、後で可視化や分析に利用できるようになっています。
# モデルの訓練
trained_models = {
'max': build_model('max'),
'avg': build_model('avg')
}
histories = {}
for name, model in trained_models.items():
print(f"Training with {name}")
histories[name] = model.fit(x_train, y_train, epochs=5, validation_split=0.1, verbose=1)
モデルの訓練結果
ここでは、MaxPooling と AveragePooling をそれぞれ使用したモデルの訓練ログを表示しています。 各エポック(学習の1サイクル)ごとに、訓練精度(accuracy)、損失(loss)、検証精度(val_accuracy)、 検証損失(val_loss) が出力されており、モデルの学習の進み具合が確認できます。
Training with max Epoch 1/5 1688/1688 ━━━━━━━━━━━━━━━━━━━━ 11s 5ms/step - accuracy: 0.8922 - loss: 0.3666 - val_accuracy: 0.9800 - val_loss: 0.0781 Epoch 2/5 1688/1688 ━━━━━━━━━━━━━━━━━━━━ 5s 3ms/step - accuracy: 0.9803 - loss: 0.0671 - val_accuracy: 0.9835 - val_loss: 0.0532 Epoch 3/5 1688/1688 ━━━━━━━━━━━━━━━━━━━━ 5s 3ms/step - accuracy: 0.9869 - loss: 0.0413 - val_accuracy: 0.9848 - val_loss: 0.0580 Epoch 4/5 1688/1688 ━━━━━━━━━━━━━━━━━━━━ 5s 3ms/step - accuracy: 0.9906 - loss: 0.0296 - val_accuracy: 0.9878 - val_loss: 0.0466 Epoch 5/5 1688/1688 ━━━━━━━━━━━━━━━━━━━━ 6s 3ms/step - accuracy: 0.9938 - loss: 0.0195 - val_accuracy: 0.9865 - val_loss: 0.0531 Training with avg Epoch 1/5 1688/1688 ━━━━━━━━━━━━━━━━━━━━ 8s 4ms/step - accuracy: 0.8843 - loss: 0.4014 - val_accuracy: 0.9792 - val_loss: 0.0793 Epoch 2/5 1688/1688 ━━━━━━━━━━━━━━━━━━━━ 7s 3ms/step - accuracy: 0.9777 - loss: 0.0741 - val_accuracy: 0.9855 - val_loss: 0.0619 Epoch 3/5 1688/1688 ━━━━━━━━━━━━━━━━━━━━ 6s 3ms/step - accuracy: 0.9833 - loss: 0.0543 - val_accuracy: 0.9853 - val_loss: 0.0584 Epoch 4/5 1688/1688 ━━━━━━━━━━━━━━━━━━━━ 4s 3ms/step - accuracy: 0.9880 - loss: 0.0372 - val_accuracy: 0.9827 - val_loss: 0.0641 Epoch 5/5 1688/1688 ━━━━━━━━━━━━━━━━━━━━ 5s 3ms/step - accuracy: 0.9919 - loss: 0.0264 - val_accuracy: 0.9890 - val_loss: 0.0501
結果の可視化
上記のコードでは、訓練中に記録された各エポックごとの 検証精度(validation accuracy) を可視化しています。
history.history['val_accuracy']
を用いることで、fit()
実行中に得られた検証精度の履歴を取得できます。
for
ループを使って、MaxPooling と AveragePooling の両モデルについて精度の変化をプロットし、plt.legend()
でラベルを付けることで、
両者の性能差を一目で比較できるようにしています。
このようなグラフを作成することで、モデルがどの程度効率的に学習しているか、過学習が起きていないかなどを視覚的に判断しやすくなります。 また、検証精度の推移は汎化性能の傾向を確認するための重要な指標となります。
import matplotlib.pyplot as plt
for name, history in histories.items():
plt.plot(history.history['val_accuracy'], label=name)
plt.title('Validation Accuracy per Epoch')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.legend()
plt.grid()
plt.show()
精度グラフ
結果の分析・考察
5エポックの訓練結果を比較したところ、MaxPooling と AveragePooling の両方とも高い精度を示しましたが、以下の点で差異が見られました。
Pooling手法 | 最終訓練精度 | 最終検証精度 | 最終検証損失 |
---|---|---|---|
MaxPooling | 99.38% | 98.65% | 0.0531 |
AveragePooling | 99.19% | 98.90% | 0.0501 |
訓練精度は MaxPooling の方がわずかに高くなりましたが、検証精度と損失では AveragePooling の方がわずかに良い結果を示しました。
このことから、MaxPooling はより強く特徴を抽出しやすい反面、やや過学習の傾向がある可能性があり、AveragePooling は特徴を平均化するため、より汎化性能が高くなることもあると考えられます。
モデル構造やデータに応じて、どちらのプーリング手法を使うか選択することが重要です。
ただし、この差はごくわずかであり、統計的に有意であるかは更なる検証が必要です。 本実験のように単純なCNN構造では、どちらを使っても大きな性能差は出にくい可能性もあります。
まとめ
- MaxPooling: 特徴量の強調に優れるが、過学習の傾向が出る場合もある。
- AveragePooling: 特徴を平均化することで、よりなめらかな抽出が可能で、汎化性能がわずかに高い傾向。
- 今回の実験では、検証精度において AveragePooling が僅差で上回った。
- 実際のタスクやモデルの深さに応じて、適切なプーリング手法を選ぶことが大切。
プーリング層は目立たない部分かもしれませんが、モデルの性能に確実な影響を与える重要な要素であることが実感できました。
より複雑なモデルや実世界のデータセットでは、Pooling手法の影響がさらに顕著になるかもしれません。 今後はCIFAR-10や画像分類以外のタスクでも比較を行ってみたいと考えています。
0 件のコメント:
コメントを投稿