活性化関数とは?ReLUをPythonで動かして「層を重ねる意味」を理解する
ニューラルネットワークに層を増やしても、重みを掛けてバイアスを足す計算だけを繰り返すなら、全体を1回の同じ形の計算にまとめられる。層を重ねる意味を作るのが、間に入る活性化関数だ。
前の「重み・バイアス」では、入力から を計算した。ここから先へ進むには、得られた をそのまま次の層へ渡すのか、条件に応じて形を変えるのかが重要になる。
線形層だけを重ねても、曲がらない
1つ目の層が 、2つ目の層が だとする。代入すると次のようになる。
は入力、 は重み、 はバイアスだ。括弧を外すと、新しい重みと新しいバイアスを持つ1つの層と同じ式になる。入力や出力がベクトルになっても、線形変換とバイアス加算だけの合成は、結局1つのアフィン変換として表せる。
このままでは「ある値を境に出力の変わり方を切り替える」ような形を表せない。そこで、層の間に非線形の活性化関数を入れる。
ReLUは負の値を0にする
最も分かりやすい例の1つがReLUである。
が0以下なら出力は0、0より大きければ をそのまま出す。PyTorchの torch.nn.ReLU もこの式で定義されている。
たとえば前の層が を出す場合、 が より小さい領域ではReLUの出力は0になり、それより大きい領域では になる。境目で出力の傾きが変わる。これが、線形層だけでは作れなかった「曲がり目」だ。
ただし、ReLU自身が重みを学習するわけではない。境目の位置を動かすのは、ReLUへ入る前の重みとバイアスである。活性化関数は、受け取った値を規則に従って変換する。
NumPyで層の間に入れて比べる
下の例では、同じ2つの層を使い、間にReLUを入れる場合と入れない場合を比べる。学習済みモデルではなく、違いを見るために係数を固定した例である。
import numpy as np
x = np.array([-2.0, -1.0, 0.0, 1.0, 2.0])first_layer = 3.0 * x + 1.0
without_activation = 2.0 * first_layer - 1.0with_relu = 2.0 * np.maximum(0.0, first_layer) - 1.0
print('input: ', x)print('without ReLU: ', without_activation)print('with ReLU: ', with_relu)実行結果は次のとおり。
input: [-2. -1. 0. 1. 2.]without ReLU: [-11. -5. 1. 7. 13.]with ReLU: [-1. -1. 1. 7. 13.]ReLUがなければ、全体は という1本の直線だ。ReLUを入れると、 と はどちらも最初の層で負になり、ReLU後は0になる。最後の層を通すと、両方とも だ。一方、 では値が変化する。1本の直線では表せない形になった。
この例の出力が一部で同じになるからといって、どんな入力でも「情報を捨てるほどよい」とは言えない。ReLUの負側では出力が0になり、その領域の勾配も0になる。学習の進み方や表現したい形に応じて、別の活性化関数を使うこともある。
LLMでも「間に非線形を入れる」
Transformerの原論文『Attention Is All You Need』では、位置ごとに適用するフィードフォワード部分を、2つの線形変換と、その間のReLUで構成している。したがって活性化関数は、注意機構とは別の場所でも表現の幅を作る要素だ。
ただし、すべてのLLMがReLUを使うわけではない。PyTorchにはGELUなど別の活性化関数もある。モデルの実装を読むときは「活性化関数があるか」だけでなく、「どの関数が、どの2つの変換の間にあるか」を確かめたい。
よくある誤解
活性化関数を「ニューロンをON/OFFにするだけの部品」と説明すると、ReLUの正側が入力値をそのまま通すことや、GELUのように連続的に変える関数を見落とす。ReLUを理解する入口としては便利な比喩だが、一般的な定義ではない。
もう1つ、活性化関数はモデルの最後に必ず置くものでもない。どの値を出力として使うかによって、最後の層の扱いは変わる。まずは「線形層を重ねただけでは作れない変化を、間に入れる関数が作る」と押さえるのがよい。
重みとバイアスが入力の組み合わせ方を決め、活性化関数がその結果を非線形に変える。次は、出力が望む答えからどれだけずれたかを測る損失関数へ進む。
参考資料: