ニューラルネットワークを1から理解する — 1個のニューロンから多層まで
ニューロン1個の中身から多層ネットワークまでを、比喩→式→動く図→numpyコードの順で。「なぜ活性化関数が必要なのか」を、線形の合成が線形にしかならないという一行の証明から腹落ちさせます。
会議室にいる、たった1人の審査員
新しい取引先と契約するかを1人の審査員が決めているとします。彼が見るのは3つの数字だけ——年間の取引額、付き合いの年数、支払い遅延の回数。心の中でそれぞれに重みを付け、「取引額は3点分、年数は2点分、遅延は1回あたりマイナス5点」と足し合わせ、合計が基準を超えたら「契約する」と判定する。
これがニューロンです。ニューラルネットワークは、この審査員を何百人と横に並べ、さらに何段にも縦に重ねただけの仕組みです。難しいのは「重みをどう決めるか」だけで、中身そのものは驚くほど単純です。
なお本記事は、損失関数と勾配降下——間違い具合を1つの数値にし、それが減る方向にパラメータを動かすこと——を知っている前提で進みます。曖昧なら先に損失関数と最適化を読んでおくと後半が地続きになります。
ニューロン1個の中身
審査員の仕事を式にすると、これだけです。
記号を1つずつ見ます。
- は入力ベクトル。例なら という3つの数値の並び。
- は重みベクトル。 と同じ長さで、各項目の重要度を表します。
- は内積。 という、掛けて足すだけの計算( は入力の次元数)。
- はバイアス。全体に足す下駄で、判定の厳しさを決めます。
- は活性化関数。合計値を変換する非線形の関数です。
- は出力、つまりこのニューロンの「判定」。
この式が言っているのは要するに、入力を重み付きで足し合わせ、下駄を履かせ、最後に1つの関数に通す、それだけです。 は の多変数版にすぎません。
なぜ活性化関数が要るのか
は本当に必要でしょうか。「合計値をそのまま出せばいい」と思うのが自然です。確かめるために、 が「何もしない関数」(恒等関数)だとして、1層目の出力を2層目に入れるだけの2層ネットワークを書き下します。
この式が言っているのは要するに、括弧を外して整理すると2層が1層に潰れるということです。 はただの行列の積なので と呼び、 を と呼べば、右辺は ——最初から1層だったのと同じ形です。
100層重ねても、行列を100個掛け合わせた1つの行列に潰れるだけ。活性化関数がなければ、深さは1ミリも表現力を生みません。これが「線形を何段重ねても線形にしかならない」という意味です。
具体例で見ましょう。XOR という関数——入力 なら出力0、 なら1、 なら1、 なら0——の4点を平面に打ち、1本の直線で「0の組」と「1の組」に分けようとしてください。できません。0の2点が対角線上、1の2点がもう一方の対角線上にあるからです。ところが間に非線形関数を1つ挟むと解けます。非線形性は、モデルに空間を折り曲げる自由を与えるのです。
層を重ねると何が表現できるようになるか
理論的には「隠れ層が1つでも、ユニット数を十分に増やせば連続関数をいくらでも高い精度で近似できる」ことが1980年代末から1990年代初頭に証明されています(万能近似定理)。ただしこの定理は、必要なユニット数がどれだけかも、それを学習で見つけられるかも保証しません。
それでも深くするのは、経験的に同じ表現を、浅く広い形より深く細い形のほうが少ないパラメータで組み立てられることが多いからです。深い層は浅い層の出力を「部品」として再利用できます。画像なら、入り口の層は明暗の境目のような単純な特徴に、奥の層はそれを組み合わせた模様や部品に反応する。1段目で作った部品を2段目で組み合わせる——だから深さが効きます。
順伝播を行列で一気に書く
1つの層は、同じ入力を見る 個のニューロンの束です。各ニューロンの重みベクトル を行として積めば重み行列 になり、層1つぶんの計算が行列とベクトルの積1回で終わります。3層ならこうです。
記号の説明です。 は1層目の出力(隠れ層の値。入力でも最終出力でもない中間表現なので「隠れ」と呼びます)。 と は 層目の重み行列とバイアスベクトル。(ワイ・ハット)はモデルの予測値。正解 と区別するため帽子を付けます。 は要素ごとに適用されます——各成分に別々に同じ関数をかけるだけで、成分同士は混ざりません。
この3行が言っているのは要するに、「行列を掛けて、ずらして、曲げる」を3回繰り返しているだけ。入力から出力へ順に計算するので順伝播(forward pass)と呼びます。
次元を追うと具体的です。入力が784次元(28×28の白黒画像を一列に並べたもの)で1層目が128ユニットなら、 は 128×784 の行列。実装では1件ずつでなく 件まとめた行列 を流し、行列積1回で 件ぶん片付けます。
シグモイドからReLUへ
活性化関数として長く使われたのがシグモイド関数です。
は入力( の値)、 は自然対数の底(約2.718)です。この式が言っているのは要するに、どんな実数を入れても0から1の間に押し込むということ。大きな正の値はほぼ1、大きな負の値はほぼ0で、真ん中がなめらかに繋がります。
ところが深いネットワークでは重大な欠点が出ます。シグモイドの微分は
で、 が0〜1の値なので、この積は のときの 0.25 が最大値です。 が大きくても小さくても、急速に0へ近づきます。
なぜこれが問題か。学習では出力側の誤差を入力側へ遡らせて各層の勾配(パラメータをどちらにどれだけ動かすべきかの指示)を求めますが、層を1つ遡るたびに活性化関数の微分が掛かります。10層あれば0.25以下の数が10回。 はおよそ100万分の1で、入り口に近い層に届く指示は事実上ゼロに潰れます。これが勾配消失です。詳しくは誤差逆伝播の記事で追います。
そこで主流になったのが ReLU(Rectified Linear Unit)です。
負なら0、正ならそのまま。折れ曲がった直線というだけの関数です。しかし正の領域では微分がちょうど1なので、何層遡っても勾配が減衰しません。計算も比較1回で済み指数関数より軽い。深いネットワークが現実的に学習できるようになった要因の1つがこの乗り換えでした。
弱点もあります。負の領域では出力も微分も0なので、入力が常に負に張り付いたニューロンは勾配0のまま二度と更新されません(dying ReLU)。緩和策が、負側にわずかな傾きを残す Leaky ReLU やなめらかに繋いだ GELU です。
numpyで書く順伝播
ここまでの話は、これだけのコードです。
import numpy as np
def relu(z):
return np.maximum(0, z)
def forward(X, params):
h = X # (N, 784) N件まとめて流す
for W, b in params[:-1]: # 隠れ層: 線形変換 → 非線形
h = relu(h @ W + b)
W, b = params[-1] # 出力層は活性化なし
return h @ W + b # (N, 10) 各クラスのスコア
h @ W + b が行列積、relu が非線形。この2行の繰り返しがニューラルネットワークの正体です。出力層で活性化を掛けていないのは、損失関数の側で softmax まで含めて計算したほうが数値的に安定するためで、実装上の定番です。
実務で効く3つのポイント
1. 重みをゼロで初期化してはいけない 全部0から始めると、同じ層のニューロンが全員同じ値を出し、同じ勾配を受け取ります。対称性が壊れないので、何ユニット並べても1ユニットぶんの働きしかしません。乱数での初期化が必須で、ReLU系ならHe初期化、シグモイド/tanhならXavier(Glorot)初期化が定番です。
2. 隠れ層と出力層は役割が違う 隠れ層の活性化は迷ったらReLU系で構いません。出力層は目的で決まります——回帰なら活性化なし、2値分類ならシグモイド、多クラス分類ならsoftmax。ここを取り違えると構造が正しくても学習は進みません。
3. 層を増やす前に、入力の標準化と学習率を疑う 学習が進まないとき、まず深くしたくなります。しかし原因は、入力のスケールが揃っていないか、学習率が大きすぎる/小さすぎるかであることが圧倒的に多い。深さは最後の手段です。
まとめ
- ニューロン1個 = 重み付き和 + バイアス + 活性化関数。 がすべて
- 活性化関数がなければ、何層重ねても という1層に潰れる
- 順伝播は「行列を掛けて、ずらして、曲げる」の繰り返し
- シグモイドは微分が最大0.25で層を遡るたびに掛け算され勾配が消える。ReLUは正側の微分が1なのでこれを避けられる
次は、その勾配を各層へ遡らせる仕組み——誤差逆伝播です。連鎖律という高校数学の道具1つで全部が説明できます。Attention機構のような現代的な部品も、分解すればここでやった線形変換と非線形の組み合わせ。土台は同じです。
コメント
コメントにはログインが必要です