GELUとは?GPTが採用する活性化の仕組み
GELUという活性化関数をご存知でしょうか?
GELUとは、Gaussian Error Linear Unit(ガウス誤差線形ユニット)の略称です。OpenAIのGPTやGoogleのBERTなど、よく知られた言語モデルで使われている活性化関数です。
よく使われる活性化関数の代表としてReLUがありますが、GELUの論文ではGELUはReLUと比較してニューラルネットワーク訓練の収束がより高速になるという実験結果が掲載されています。
この記事では、GELUがどんな仕組みで機能するのかについて解説します。
GELUの定義
GELUの定義は以下になります。
$$
\text{GELU}(x) = x \Phi(x)
$$
つまり、GELUは入力値$${x}$$と$${\Phi(x)}$$の積です。そして$${\Phi(x)}$$は標準正規分布の累積分布関数です。これは後で解説します。
図にすると以下のようになります。ReLUに似ていますね。

ReLUとGELUの間には共通点と相違点があります。これについても後で解説します。
まずは、標準正規分布と累積分布関数について順番に見ていきましょう。
標準正規分布と累積分布関数
標準正規分布は下図の釣鐘の形をしています。平均が0で分散(標準偏差)が1です。

この分布に従うデータは平均0の周りによく現れる特徴があります。また左右対称なので平均を境に50%の確率を上まるかどうかが決まります。

つまり、正規分布に従うデータからランダムに値を引き出したとにき$${-\infty}$$から平均値の間、つまり平均値以下である確率が50%になります。
しかし、(平均値以外で)データがある値以下になる確率をこの図から読み取るのは難しいです。例えば、データが -0.8以下である確率は確率分布の図から直接には知ることができません。

これは確率分布の縦軸は確率密度を示しており、確率を求めるには赤の曲線と青の線で囲まれた部分の面積を知る必要があるからです。
そこで累積分布が役に立ちます。一般に、累積分布関数が与える値は確率密度を横軸に沿って$${-\infty}$$からある値$${x}$$まで積分したものです。つまり、累積分布関数の図の縦軸はデータが$${x}$$以下の値になる確率を示しています。
下図は標準正規分布の累積分布関数です。

これを見るとデータが$${-0.8}$$以下の値になる確率は20%より少し高いのがわかります。また、平均である$${0}$$以下の値になる確率は50%です。
まとめると、一般に累積分布関数はある確率分布に従うデータが値$${x}$$以下になる確率を返します。標準正規分布を基にした累積分布では平均値が0でそれ以下の値になる確率が50%となっています。累積分布関数があれば基になった確率分布を知らなくともデータがある値以下になる確率を計算することができます。
シグモイドとの比較
標準正規分布の累積分布関数は、シグモイド(Sigmoid)関数にも似ています。シグモイドにある値$${x}$$を与えると、その値に対応する確率が返されます。
よくある使い方としては、ある値$${x}$$に対して50%以上の確率かどうかで何らかの判断を下したりします。例えば、センチメント分析のモデルが出力した値をシグモイドに与えて、確率が50%以上なら映画が楽しいものであると判断するといった使い方をします。
このように確率を使うと何らかの判断するのに役立ちます。そういった意味では標準正規分布でも一般の正規分布でもシグモイドでも考えは同じです。
実際、GELUの論文でもSiLU(Sigmoid Linear Unit)を使うこともできると書かれています。SiLUでは標準正規分布の累積分布関数の代わりにシグモイド関数を使います。
$$
\text{SiLU}(x) = x \sigma(x)
$$
また、一般の正規分布の累積分布関数でも平均が0で無くなるだけで考え方は同じです。
ただし、GELUの論文では主に標準正規分布の累積分布関数を扱っています。そこには理由があります。
なぜ標準正規分布なのか
GELUの論文は主に標準正規分布の累積分布関数を前提として書かれています。論文によると、標準正規分布が選ばれた理由は、バッチ正規化によって入力値が標準正規分布に従う傾向があるからです。
ここで再びGELUの定義を確認しましょう。
$$
\text{GELU}(x) = x \Phi(x)
$$
つまり、GELUは入力値$${x}$$と$${\Phi(x)}$$の積で、下図のようになっています。

データが標準正規分布に従うならば、ちょうど平均値$${x=0}$$の所でグラフの形が大きく変わった非線形性になっていることになります。つまり、GELUを使うと値が平均以上なら活性化して次の層へのシグナルになります。また、平均以下の値はほぼ無視されるようになっています。
とするならばReLUと同じではないか、と思われるかもしれません。
ReLUとの比較
GELUの図は、ReLUとよく似ています。下図はReLU(青)をGELU(赤)に重ね合わせて表示したものです。

ReLUでは0以下の値を切り捨てる(0にする)ことで非線形性を導入しています。
$$
\text{ReLU}(x) = \max(0,x)
$$
ReLUの非線形が導入される境目が0であるのはバッチ正規化などによる標準正規分布を前提として考えると平均以上を活性化させることになり GELUと基本的に同じ考え方です。
つまり、活性化関数としてGELUとReLUは同じような働きをするのがわかります。では、ReLUと比べてGELUの利点とは何でしょうか。
ReLUはシンプルな関数であり、計算が速くて効率的であり、シグモイドなどと比べて勾配消失問題を緩和する効果があることから広く使われています。
しかし、ReLUにも欠点があります。例えば、
入力値が0以下の場合に勾配が0になってしまい、学習が停滞する可能性がある(死んだニューロン)。
入力値が0より大きい場合に微分係数(傾き)が1なので誤差逆伝播法で計算される勾配が大きくなって重みのパラメータが更新量が不安定になる可能性がある(爆発的勾配)。
入力値0では微分可能ではなく滑らかではない(不連続点)ので特別な配慮が必要となる。
これに対してGELUでは、
入力値が0以下ですべての勾配が0にはならないでニューロンが死ぬことはない。
入力値0でも微分可能で連続である。
このようにしてGELUはReLUと同じような機能を持ちつつ利点があり、学習の効率の向上が実験的に確認されています。
なお、勾配爆発についてはGELUもReLUもバッチ正規化などの標準化がある前提であればあまり気にする必要はないでしょう。
なお、GELUはReLUよりも計算量が多いので、実装では近似値を計算する手法で高速化することも可能となっています。また、最近のライブラリでは近似しなくともそれほどの差はないとも言われており、PyTorchでは近似しない計算法をデフォルトにしています。
ドロップアウトとの関係
論文ではGELUの確率的なドロップアウト(Dropout)を含んでいると述べられています。つまり、入力値にある確率$${p}$$を掛け合わせることと、確率$${1 - p}$$でドロップアウトをしたのと同じ効果があるという意味です。
確かにGELUをある値$${x}$$に対して何度も使うとすると、平均すれば$${1 - \Phi(x)}$$の確率でドロップアウトを行っているのに等しいと言えるでしょう。そうすることで大きな値はドロップアウトの確率が低くなるので活性化全体に与える影響も通常のドロップアウトよりも小さいと言えます。
ただし、これについてはそういう解釈もあるぐらいの理解でいいと思います。なぜなら、GELUを使うことでドロップアウトが不要になるわけでもないからです。
また、ドロップアウトは訓練のためのものでテストやプロダクション環境ではドロップアウトは使われません。しかし、GELUは活性化関数なので常に使われます。
GELUを使う際の注意点
実際にGELUを使うかどうかはモデルでテストをして効果を見極めてから決める必要があります。論文でもさまざまなケースで実験してGELUによる訓練の収束が速くなることを示唆していますが、結局のところデータやモデルによって効果は変わってくることに変わりはありません。
GPTやBERTではGELUによる効果があったということでその後の派生モデルも引き続きGELUを使ってるケースがほとんどです。
しかし、活性化関数にはたくさんの種類があり、新しいモデルを開発するならば、いろいろと試して効果を確かめることを勧めます。
なお、GELUの前提としてバッチ正規化などの標準化があります。訓練するモデルがこのような条件を満たしているかどうかも確認しておくと良いでしょう。
ちなみにReLUやGELUを使うときにデータが絶対に標準化されないといけないということはないです。ただし、データがほとんど負の値だとしたらほとんど活性化しないことになります。GELUでも問題になりますし、ReLUの場合なら特に問題です。逆にデータの値に大きな正の値が多い場合も勾配爆発の問題につながりやすいので問題です。結局のところ何らかの標準化をしたデータを入力値とするのが懸命です。
グラフを出力するPythonのコード
最後に、グラフを出力するのに使ったPythonコードを掲載します。
標準正規分布
ここではNumPyとSciPyを使っています。
# 標準正規分布の描写
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm
plt.style.use("ggplot")
# x軸の値を設定
x = np.linspace(-5, 5, 100)
# 平均値と標準偏差を設定
mu = 0
sigma = 1
# 確率密度関数の値を計算
y = norm(mu, sigma).pdf(x)
# グラフを描画
fig, ax = plt.subplots(1,1)
ax.plot(x, y)
ax.set_title("標準正規分布", fontname="Hiragino sans")
ax.set_xlabel("x")
ax.set_ylabel("確率密度", fontname="Hiragino sans")
ax.set(ylim=(0, 0.5), xlim=(-3, 3))
ax.set(xlim=(-3, 3))
plt.show()標準正規分布の累積分布関数
ここでもNumPyとSciPyを使っています。
# 標準正規分布の累積分布関数
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm
plt.style.use("ggplot")
#正規分布のパラメータ
mu = 0 #平均
sigma = 1 #標準偏差
#データ生成
x = np.linspace(-5, 5, 100) #x軸の値
y = norm.cdf(x, mu, sigma) #CDFの値
#プロット
plt.plot(x, y)
plt.xlabel("x")
plt.ylabel("確率", fontname="Hiragino sans")
plt.title("標準正規分布の累積分布関数", fontname="Hiragino sans")
plt.xlim(-3, 3)
plt.show()GELUとReLU
これはPyTorchの機能を使っています。
# GELU活性化関数
import matplotlib.pyplot as plt
import torch
import torch.nn.functional as F
plt.style.use("ggplot")
#GELU関数の定義
#データ生成
x = torch.linspace(-5, 5, 100) #x軸の値
y = F.gelu(x)
relu = F.relu(x)
#プロット
plt.plot(x, y)
plt.plot(x, relu)
plt.xlabel("x")
plt.ylabel("GELU")
plt.title("Gaussian Error Linear Units")
plt.show()