【python】活性化関数の完全ガイド|特徴と効果的な選び方について|勾配消失問題

こんにちは、青の統計学です。

ディープラーニングは、近年の技術革新において大きなインパクトをもたらしており、画像認識や自然言語処理など、多くの分野で広く利用されています。

このコンテンツでは、ディープラーニングの中心的な要素である活性化関数に焦点を当て、その役割と重要性、一般的な活性化関数の特徴や使い方について解説します。

多層パーセプトロン

まずは、ディープラーニングの基本概念であるニューラルネットワークについて簡単に説明しましょう。

ニューラルネットワークは、生物の神経回路網に触発された複雑なモデルで、構成要素であるニューロンが層状に結合されています。

各ニューロンは、入力信号を受け取り、それに基づいて出力信号を生成します。

ここで活性化関数が重要な役割を果たします。

活性化関数(activation function)とは?

活性化関数は、ニューロンの出力を決定する非線形関数で、ニューラルネットワークに非線形性をもたらします

非線形性がなければ、ニューラルネットワークは線形変換の連続に過ぎず、複雑な問題を解決する能力が制限されます。

活性化関数は、入力信号の加重和に適用されます。

以下に、一般的な活性化関数をいくつか紹介します。

勾配」「勾配消失問題」「誤差逆伝播法」がよくわからない方は、後述する部分を先に見ていただければと思います。

ReLU (Rectified Linear Unit)

ReLUは、ディープラーニングで最も一般的に使用される活性化関数です。

数学的には、以下のように定義されます。

$$f(x) = max(0, x)$$

ReLUは、入力が正の場合にそのまま出力し、負の場合には0を出力します。

ReLUの利点は、計算が非常に簡単でありながら、勾配消失問題(後述します)を軽減する能力があり、画像処理のアルゴリズム(CNN等)の活性化関数としてよく使われます。

しかし、負の入力に対して勾配が0になるため、ニューロンが「死んで」学習が停滞する可能性があります

シグモイド関数(sigmoid)

$$f(x) = \frac{1}{1 + exp(-x)}$$

ロジスティック回帰でお馴染みの活性化関数です。

【分類タスク】ロジスティック回帰の使い方|python

【二項分布】ロジスティック回帰について|R

-∞から+∞の範囲の実数を0から1という範囲にマッピングすることができるため、確率を表す際に使われます。

これにより、確率的な解釈が可能であり、二値分類問題の出力層でよく使われます

しかし、Sigmoid関数は、入力の絶対値が大きい場合に勾配が極端に小さくなるため、勾配消失問題が発生しやすくなります。

そもそも、シグモイド関数の微分係数の最大値は0.25なので、誤差逆伝播法においては入力層に近づくほど勾配がなくなっていきます

ReLuだと微分係数は1ですね(0<なら)

Tanh (Hyperbolic Tangent)

Tanh関数は、Sigmoid関数の双曲線版であり、入力を-1から1の範囲にマッピングします。

数学的には以下のように定義されます。

$$f(x) = \frac{exp(x) – exp(-x)}{exp(x) + exp(-x)}$$

Tanh関数は、Sigmoid関数と同様に、勾配消失問題が発生しやすいですが、出力が平均0になるため、訓練が効率的に進むことがあります。

ソフトマックス(softmax)

ソフトマックス関数は、多クラス分類問題の出力層で使用されることが一般的です。

入力ベクトルを確率分布に変換します。

数学的には以下のように定義されます。

$$f(x_i) = \frac{exp(x_{I})}{ \sum_{j}exp(x_{j})}$$

ソフトマックス関数は、各クラスの確率を求めるために使用され、確率の合計は1になります。

画像の多分類については、以下のコンテンツで紹介しています。

【python】畳み込みニューラルネットワークによる画像判別プログラムの開発

活性化関数の選択

活性化関数を選択する際には、タスクやニューラルネットワークのタイプ(CNN、RNN、Transformerなど)に応じて適切なものを選ぶ必要があります。

一般的なガイドラインは以下の通りです。

隠れ層:ReLUやその派生関数(Leaky ReLU、PReLU、ELUなど)が一般的に良い結果をもたらします。

出力層:二値分類タスクではSigmoid、多クラス分類タスクではソフトマックスが適切です。

回帰タスクでは、活性化関数は通常使用されません。

CODE|python

乳がんデータを使って、簡単な学習を行います。

乳がんデータは腫瘍なが悪性か良性かを判断するので「2値分類問題」ですね。

→つまり、最後の活性化関数としてはシグモイド関数が適切なようです。

import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout, Activation


# 乳がんデータセットの読み込み
data = load_breast_cancer()
X = data.data
y = data.target

# データを訓練データとテストデータに分割
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# データの標準化
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

ニューラルネットワークには、データの標準化がおすすめです。

単純な標準化を行なっていますが、外れ値がある場合には中央値を使ってロバストzスコアのような、より頑健な標準化を行うことをお勧めします。

ロバストzスコアに関しては、以下のコンテンツで扱っています。

【python】Lasso(ラッソ)回帰で疎なデータに対応しよう|機械学習

# ニューラルネットワークの定義
model = Sequential()

# 第1層: 入力層 (30 ノード) から 隠れ層 (128 ノード) への全結合層
model.add(Dense(128, input_dim=30, kernel_initializer='he_normal'))
model.add(Activation('relu'))
model.add(Dropout(0.5))

# 第2層: 隠れ層 (128 ノード) から 隠れ層 (64 ノード) への全結合層
model.add(Dense(64, kernel_initializer='he_normal'))
model.add(Activation('relu'))
model.add(Dropout(0.5))

# 第3層: 隠れ層 (64 ノード) から 出力層 (1 ノード) への全結合層
model.add(Dense(1, kernel_initializer='he_normal'))
model.add(Activation('sigmoid'))

# モデルのコンパイル
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])

# モデルのサマリを表示
model.summary()

入力層 (30 ノード)というのは、入力データには幾つの特徴量があるのか?というところで決定します。

さて、2値分類問題では損失関数としてbinary_crossentropyを使うのが一般的で、評価指標としては正解率を見ます。

正解率(accuracy)は、以下のような式です。

$$ACC(accuracy)=(\frac{TP+TN}{TP+TN+FP+FN})$$

悪性と診断良性と診断
悪性TPFN
良性FPTN
混同行列

model.summary()では、以下のようにニューラルネットワークの層の構成がパラメータの数と主に要約されて出力されます。

Model: "sequential_1"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 dense_2 (Dense)             (None, 128)               3968      
                                                                 
 activation (Activation)     (None, 128)               0         
                                                                 
 dropout (Dropout)           (None, 128)               0         
                                                                 
 dense_3 (Dense)             (None, 64)                8256      
                                                                 
 activation_1 (Activation)   (None, 64)                0         
                                                                 
 dropout_1 (Dropout)         (None, 64)                0         
                                                                 
 dense_4 (Dense)             (None, 1)                 65        
                                                                 
 activation_2 (Activation)   (None, 1)                 0         
                                                                 
=================================================================
Total params: 12,289
Trainable params: 12,289
Non-trainable params: 0
_________________________________________________________________

では最後に学習して、テストデータで測った正解率を見てみましょう。

(正解率は、訓練データの0or1の分布が大体均等であることを前提に使ってください(超大事))

# モデルの学習
model.fit(X_train, y_train, epochs=50, batch_size=32, validation_split=0.2)

# モデルの評価
test_loss, test_accuracy = model.evaluate(X_test, y_test)

print(f"Test loss: {test_loss:.4f}")
print(f"Test accuracy: {test_accuracy:.4f}")
Test loss: 0.0911
Test accuracy: 0.9737

エポックが50と結構多いです。

1エポックとは、機械学習においてモデルが訓練データセット全体を一度通過することを指します。

データセット全体を複数回繰り返してモデルを学習させることで、学習アルゴリズムが最適なパラメータ(重みやバイアス)を見つける助けとなります。エポック数が多すぎると、過学習(オーバーフィッティング)が起こる可能性があります。逆に、エポック数が少なすぎると、未学習(アンダーフィッティング)が起こる可能性があります。

適切なエポック数を選ぶことで、モデルの性能が向上します。

さて、コードで出たいくつかのスキームについて紹介します。

ドロップアウト:ドロップアウトは、過学習を防ぐための正則化手法で、活性化関数の直後に適用されることが一般的です。

訓練時にランダムにニューロンを無効にすることで、ネットワークの過度な依存を防ぎます。

→過学習の対策になります。

重み初期化:ニューラルネットワークの重みを適切に設定することで、学習の速度や収束性能を向上させるために重要です。

kernel_initializerという引数に、適切な初期化方法を入力すれば重みを初期化できます。

活性化関数によって最適な重み初期化方法が異なり、ReLU関数ではHe初期化が、SigmoidやTanh関数ではXavier初期化が適切です。

勾配と勾配消失問題

さて、これまで「勾配」「勾配消失問題」と説明してきましたが、機械学習においてどのような立ち位置なのでしょうか?

勾配とは

簡単に説明すると、勾配とは「損失関数の傾き」です。

損失関数とは、モデルと正解(教師データ)の差を表す関数ですね。

線形代数の文脈で言えば、勾配は損失関数の微分によって得られるベクトルです。

勾配は、各パラメータ(重みとバイアス)に対する損失関数の偏微分から構成されます。

$$∇L = (\frac{∂L}{∂_{w1}}, \frac{∂L}{∂_{w2}}, …, \frac{∂L}{∂_{wn}})$$

ここで、\(\frac{∂L}{∂w_i}\) は損失関数 L に対するパラメータ \(wi\) の偏微分です。

損失関数が \(L(w1, w2, …, wn\) で表される場合、勾配は各パラメータに対する損失関数の偏微分から構成されるベクトルです。)

勾配が示す方向は、損失関数の値が最も急速に増加する方向です。

機械学習では、「最急勾配降下法」という手法が主流で、文字通り勾配が最も大きい方の逆(超大事)に進んで損失関数をどんどん小さくしていきます。

→そして最適なパラメータは、損失関数を最小化するパラメータということです。

以下のように勾配を更新し、損失関数を小さくしていきます。

$$w_{new} = w_{old} – α × ∇L$$

w_old: 更新前のパラメータ

w_new: 更新後のパラメータ

α: 学習率(ステップサイズ)

∇L: 損失関数 L に対する勾配

損失関数には、「勾配が最大のところにどれくらい進むか」を設定する「学習率α」というパラメータがあり、学習率が小さすぎるといつまで経っても学習が終わらないなどの弊害があったります。逆に学習率が大きすぎると、損失関数の最小値を通り過ぎて発散してしまします。

これを勾配爆発と呼びます。

勾配消失問題

勾配消失問題は、ディープニューラルネットワークにおいて、誤差逆伝播時に勾配が急速に小さくなり、最適化が困難になる現象です。

これは、ニューラルネットワークの学習が遅くなったり、停滞したりする原因となります。

勾配消失問題は、主に活性化関数とネットワークの深さに関連しています。

活性化関数について掘り下げます。

たとえば、Sigmoid関数やTanh関数では、入力の絶対値が大きい場合に勾配が極端に小さくなります

シグモイド関数

$$f(x) = \frac{1}{1 + exp(-x)}$$

\(f'(x) = f(x) * (1 – f(x))\)より、この導関数(微分係数)の最大値は0.25です。

Tanh関数

$$f(x) = \frac{exp(x) – exp(-x))}{(exp(x) + exp(-x)}$$

\(f'(x) = 1 – f(x)^2\)より、この導関数(微分係数)の最大値は1です。

これらの活性化関数では、入力が 0 から離れるにつれて導関数の値が急速に 0 に近づきます。誤差逆伝播時に、勾配が連続的にこれらの導関数によって乗算されるため、ネットワークが深いほど勾配が指数関数的に小さくなります

これが勾配消失問題の原因です。

勾配消失問題への対策

さて、では勾配消失問題を回避し、機械学習を進ませて最適化を円滑にするにはどのような方法があるのでしょうか。

いくつか方法を挙げました。

①活性化関数の変更

勾配消失問題を軽減するために、ReLUやその派生関数(Leaky ReLU、PReLU、ELUなど)を使用することが推奨されます。

これらの関数は、正の入力に対して勾配が一定であるため、勾配消失問題が発生しにくくなります。

②重みの初期化

先ほどもコード紹介の部分で説明しましたが、適切な重み初期化方法を使用することで、勾配消失問題を軽減できます。

例えば、ReLU関数ではHe初期化が、SigmoidやTanh関数ではXavier初期化が適切です。

これらの初期化方法は、各層の出力の分散が一定になるように重みを設定します。

③ネットワークアーキテクチャの変更

勾配消失問題を回避するために、ネットワークアーキテクチャの変更も検討できます。

Residual Network(ResNet)のようなアーキテクチャでは、ショートカット接続が導入されており、勾配が層をスキップして伝播できるため、勾配消失問題が軽減されます。

④バッチ正規化(batch normalization)

バッチ正規化は、各層の入力分布を正規化することで、勾配消失問題を緩和します。

使い方としては、活性化関数の直前または直後に適用します。

学習率を大きく設定できるため、学習が速くなり、また勾配消失問題や過学習を軽減する効果があります。

各層の活性化関数の入力が適切な範囲に保たれ、勾配の伝播が円滑になります。

⑤勾配クリッピング

勾配クリッピングは、勾配がある閾値を超えた場合に、勾配を制限することで、勾配消失問題や勾配爆発問題を防ぐ手法です。

これにより、学習が安定し、収束が向上することがあります。

⑥LSTMやGRUの使用

再帰型ニューラルネットワーク(RNN)は、時系列データを扱う際に勾配消失問題が顕著に発生します。

そのため、Long Short-Term Memory(LSTM)やGated Recurrent Unit(GRU)のようなゲート付きRNNアーキテクチャを使用することで、勾配消失問題を緩和できます。

FOLLOW ME !