はじめに:活性化関数の重要性
ニューラルネットワークにおいて活性化関数は、学習の非線形性・収束の速さ・性能改善に 大きく影響します。本記事では、特に普及しているReLU・LeakyReLU・ELU・GELU の4種類を、数学的な定義から実践的な性能比較まで、詳細に解説します。
比較する活性化関数
各関数の数式・グラフ・利点・欠点を整理します。
- ReLU… f(x)=max(0,x)。計算がシンプルで高速だが、「死んだReLU」問題がある。
- LeakyReLU… 負領域も小さく通すことで死んだユニットを防止。
- ELU… 負領域で指数的に変化し、平均を0に近づける効果で収束が速い。
- GELU… ガウス分布に基づくスムーズな応答。BERT等の高性能モデルで使用。
補足:死んだReLU(Dying ReLU)問題とは?
「死んだReLU」とは、ReLU関数が負の入力に対して常に0を返す性質により、一度活性化されなくなったニューロンが訓練データのどの入力に対しても反応しなくなる現象です。
これにより、そのニューロンは勾配が入らず、以降一切の学習貢献ができなくなってしまいます。
補足:なぜ問題なのか?
一度ニューロンが“死ぬ”と、その後勾配がゼロのままになり、学習が停止してしまいます。
ニューロンの数が減ることで、ネットワーク全体の表現力が低下し、性能悪化の原因にもなります。学習率が高すぎると、最大で40%ほどのニューロンが死んでしまうケースもあると言われています。
補足:どうして起きる?
- 重みの過度な更新:学習率が大きすぎると、活性化入力が大きく負方向にシフトし、回復不能な状態に入る。
- 負のバイアス:バイアス項が大きく負だと、入力が負になりやすく、ReLUが常に0を返す状態になる。
実験概要
目的:4関数の学習速度・汎化性能を定量比較。
- データセット:MNIST(6万枚)
- ライブラリ:TensorFlow 2.x / Keras
- 構成:Conv層→Flatten→Denseモデル。各関数を使い分け。
環境とライブラリ
import tensorflow as tf
from tensorflow.keras import layers, models
import numpy as np
import matplotlib.pyplot as plt
# GELUは tf.nn.gelu で使用可能
データ準備(MNIST)
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()
x_train = x_train.astype("float32") / 255.0
x_test = x_test.astype("float32") / 255.0
x_train = np.expand_dims(x_train, -1)
x_test = np.expand_dims(x_test, -1)
モデル構築関数
def build_model(activation_fn):
model = models.Sequential([
layers.Input(shape=(28, 28, 1)),
layers.Conv2D(32, (3, 3)),
activation_fn,
layers.MaxPooling2D((2, 2)),
layers.Flatten(),
layers.Dense(64),
activation_fn,
layers.Dense(10, activation='softmax')
])
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])
return model
それぞれのモデル
models_dict = {
'ReLU': build_model(layers.Activation('relu')),
'LeakyReLU': build_model(layers.LeakyReLU(negative_slope=0.1)),
'ELU': build_model(layers.ELU(alpha=1.0)),
'GELU': build_model(layers.Activation(tf.nn.gelu))
}
訓練と精度の記録
history_dict = {}
for name, model in models_dict.items():
print(f"Training with {name}")
history = model.fit(x_train, y_train, epochs=5, batch_size=128,
validation_split=0.1, verbose=1)
history_dict[name] = history
実行結果
Training with ReLU Epoch 1/5 422/422 ━━━━━━━━━━━━━━━━━━━━ 7s 9ms/step - accuracy: 0.8406 - loss: 0.5529 - val_accuracy: 0.9693 - val_loss: 0.1072 Epoch 2/5 422/422 ━━━━━━━━━━━━━━━━━━━━ 2s 4ms/step - accuracy: 0.9713 - loss: 0.0998 - val_accuracy: 0.9815 - val_loss: 0.0692 Epoch 3/5 422/422 ━━━━━━━━━━━━━━━━━━━━ 2s 4ms/step - accuracy: 0.9819 - loss: 0.0631 - val_accuracy: 0.9857 - val_loss: 0.0553 Epoch 4/5 422/422 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.9873 - loss: 0.0452 - val_accuracy: 0.9837 - val_loss: 0.0617 Epoch 5/5 422/422 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.9891 - loss: 0.0373 - val_accuracy: 0.9878 - val_loss: 0.0506 Training with LeakyReLU Epoch 1/5 422/422 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.8536 - loss: 0.5220 - val_accuracy: 0.9730 - val_loss: 0.0992 Epoch 2/5 422/422 ━━━━━━━━━━━━━━━━━━━━ 2s 4ms/step - accuracy: 0.9719 - loss: 0.0961 - val_accuracy: 0.9820 - val_loss: 0.0665 Epoch 3/5 422/422 ━━━━━━━━━━━━━━━━━━━━ 2s 4ms/step - accuracy: 0.9823 - loss: 0.0607 - val_accuracy: 0.9838 - val_loss: 0.0608 Epoch 4/5 422/422 ━━━━━━━━━━━━━━━━━━━━ 2s 3ms/step - accuracy: 0.9864 - loss: 0.0464 - val_accuracy: 0.9837 - val_loss: 0.0568 Epoch 5/5 422/422 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.9893 - loss: 0.0353 - val_accuracy: 0.9865 - val_loss: 0.0546 Training with ELU Epoch 1/5 422/422 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.8654 - loss: 0.4912 - val_accuracy: 0.9700 - val_loss: 0.1047 Epoch 2/5 422/422 ━━━━━━━━━━━━━━━━━━━━ 2s 3ms/step - accuracy: 0.9677 - loss: 0.1089 - val_accuracy: 0.9800 - val_loss: 0.0689 Epoch 3/5 422/422 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.9801 - loss: 0.0657 - val_accuracy: 0.9805 - val_loss: 0.0689 Epoch 4/5 422/422 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.9874 - loss: 0.0440 - val_accuracy: 0.9833 - val_loss: 0.0598 Epoch 5/5 422/422 ━━━━━━━━━━━━━━━━━━━━ 2s 3ms/step - accuracy: 0.9898 - loss: 0.0341 - val_accuracy: 0.9847 - val_loss: 0.0564 Training with GELU Epoch 1/5 422/422 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.8462 - loss: 0.5652 - val_accuracy: 0.9703 - val_loss: 0.1155 Epoch 2/5 422/422 ━━━━━━━━━━━━━━━━━━━━ 2s 4ms/step - accuracy: 0.9631 - loss: 0.1264 - val_accuracy: 0.9825 - val_loss: 0.0694 Epoch 3/5 422/422 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.9800 - loss: 0.0713 - val_accuracy: 0.9825 - val_loss: 0.0670 Epoch 4/5 422/422 ━━━━━━━━━━━━━━━━━━━━ 3s 3ms/step - accuracy: 0.9855 - loss: 0.0488 - val_accuracy: 0.9847 - val_loss: 0.0526 Epoch 5/5 422/422 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.9891 - loss: 0.0375 - val_accuracy: 0.9872 - val_loss: 0.0498
結果の可視化(バリデーション精度)
全モデル5エポック終了後、val_accuracyの変化を可視化しました。
for name, history in history_dict.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()
精度グラフ
結果として、以下の傾向が見られました:
- ReLU:シンプルだが依然として有力。
- LeakyReLU:ReLUより若干滑らかで安定性向上。
- ELU:学習初期が速いが中盤で成果が頭打ちに。
- GELU:最初の収束がやや遅いが最終精度が高く安定。
考察:どの活性化関数が最適か?
各活性化関数の訓練結果を比較すると、最終的な精度(accuracy)はどの関数も非常に高く、0.989前後に到達しており、明確な差は見られませんでした。ただし、学習の進み方や汎化性能(val_accuracy)には微妙な違いがありました。
活性化関数 | 最終Accuracy | 最終Loss | Val Accuracy | Val Loss |
---|---|---|---|---|
ReLU | 0.9891 | 0.0373 | 0.9878 | 0.0506 |
LeakyReLU | 0.9893 | 0.0353 | 0.9865 | 0.0546 |
ELU | 0.9898 | 0.0341 | 0.9847 | 0.0564 |
GELU | 0.9891 | 0.0375 | 0.9872 | 0.0498 |
全体精度は0.989〜0.990と大差はないものの、学習の安定性・収束速度・目的による使い分け が重要だと判断できます。
- 高速処理が求められる場合 → ReLU または LeakyReLU
- 滑らかな出力分布が必要なタスク → ELU・GELU(Transformer系におすすめ)
まとめ
活性化関数はモデルの学習挙動に影響を与える重要な要素です。今回の比較では、大きな性能差は見られなかったものの、ELUやGELUはより滑らかで安定した学習挙動を示しました。
一方で、ReLUは依然として堅実でシンプルな選択肢であることが分かりました。タスクやデータに応じて適切な関数を選択することが、より良いモデル構築につながります。
0 件のコメント:
コメントを投稿