DIVE

Part 7: 確率とAI・LLM

第 19 章

ニューラルネット自作 — 知能を、自分の手で組み立てる

ついに、この瞬間が来た。第17章でベクトルと行列を、第18章で勾配降下法を手にした。役者は揃った。この章では、それらを組み上げて、自分の手でニューラルネットを動かす。フレームワークの .fit() を呼ぶのではない。学習の心臓部を、numpyだけで一行ずつ組み立てる。

AIの「知能」は、決して魔法ではない。予測し、間違いを測り、反省して修正する——この単純なループの繰り返しにすぎない。その全貌を、脳の神経細胞という原点から解体していこう。


一個のニューロンから — 歴史の必然

ニューラルネットの発想は、脳の神経細胞(ニューロン)の模倣から始まった。1958年、フランク・ローゼンブラットがパーセプトロンを発表する。一個の人工ニューロンは、驚くほど単純な仕事をする。

  1. 複数の入力を受け取る
  2. それぞれに重みを掛けて合計する(=重要度をつけて足す)
  3. その合計を、ある関数に通して出力する

第17章で見た y⃗=Wx⃗+b⃗\vec{y} = W\vec{x} + \vec{b} が、まさにステップ1〜2だ。入力ベクトル x⃗\vec{x} に重み行列 WW を掛け、バイアス b⃗\vec{b} を足す。ニューロンとは、この「重み付きの足し算」を行う小さな装置なのだ。

一個のニューロン:
  x1 ──w1──┐
  x2 ──w2──┼─▶ Σ(重み付き和) + b ─▶ 活性化関数 ─▶ 出力
  x3 ──w3──┘        ↑
                 z = w1·x1 + w2·x2 + w3·x3 + b

だが、単純な重み付き和だけをいくら重ねても、表現できるのは直線的な関係だけだ(線形性)。現実の複雑なパターン——曲がった境界、非線形な関係——は捉えられない。ここに、決定的な一工夫が要る。それが活性化関数だ。


活性化関数 — 「非線形」という命の吹き込み

活性化関数は、ニューロンの出力に「曲がり」を与える非線形な関数だ。これがあるからこそ、ニューラルネットは複雑なパターンを学習できる。層を重ねる意味も、ここから生まれる。

代表的なものを挙げる。

シグモイドは、任意の値を 0〜1 の範囲に滑らかに押し込む。「発火する/しない」を確率的に表現できる:

σ(z)=11+e−z\sigma(z) = \frac{1}{1 + e^{-z}}

ReLU(Rectified Linear Unit)は、現代のディープラーニングの主役だ。「負なら0、正ならそのまま」という驚くほど単純な関数だが、計算が軽く、深い層でも学習が進みやすい:

ReLU(z)=max⁡(0,z)\text{ReLU}(z) = \max(0, z)
シグモイド                    ReLU
 1┤      ╭────                 │      ╱
  │    ╱                       │    ╱
0.5┤  ╱                        │  ╱
  │╱                        0──┼─╱────────
 0┤────                        │ 0
  └──────────                  └──────────
 なめらかに0〜1へ              負を切り捨て、正はそのまま

厨房で言えば、活性化関数は「味の閾値判断」だ。塩気の量(重み付き和)が一定を超えたら「しょっぱい」と認識する——その「ある閾値で質が変わる」非線形な感覚が、ReLUの「0を境に振る舞いが変わる」のと重なる。単なる比例(足し算)では表せない、質的な変化を捉えるのが活性化関数の役割だ。


順伝播 — ネットワークで「予測」する

ニューロンを層状に並べ、前の層の出力を次の層の入力にする。入力層 → 隠れ層 → 出力層とデータが流れ、最終的な予測を出す。この「入力から出力へ計算を流す」処理を順伝播(forward propagation)と呼ぶ。

各層は、第17章の行列変換と、本章の活性化関数の組み合わせだ:

a⃗(l)=f ⁣(W(l)a⃗(l−1)+b⃗(l))\vec{a}^{(l)} = f\!\left( W^{(l)} \vec{a}^{(l-1)} + \vec{b}^{(l)} \right)

ff が活性化関数、W(l)W^{(l)} が第 ll 層の重み行列。データが層を通るたびに「変換して、曲げて」を繰り返し、複雑な判断が形作られる。

順伝播(左から右へ計算を流す)
  入力層        隠れ層         出力層
  x ──▶ [W1·x+b1] ──ReLU──▶ [W2·a+b2] ──▶ 予測 ŷ
        変換+曲げ            変換

損失と逆伝播 — 「反省」の仕組み

予測 y^\hat{y} が出たら、正解 yy とのズレ=損失を測る(第18章)。そして学習の核心が来る。この損失を減らすには、数百万個ある各重みを、どちらへ動かせばいいのか?

第18章の答えは「勾配(各重みの偏微分)を求め、その逆向きに動かす」だった。だが、何層も重なったネットワークで、奥の層の重みが損失にどう影響するかを、どう計算するのか。ここで登場する天才的なアイデアが誤差逆伝播法(Backpropagation)だ。

発想はこうだ。出力側の誤差を、入力側へ向かって「逆流」させる。そして、その逆流に連鎖律(合成関数の微分)を適用する。連鎖律とは「aa が bb に影響し、bb が cc に影響するなら、aa が cc に与える影響は、途中の影響の掛け算で求まる」という微分の法則だ:

∂L∂w=∂L∂y^⋅∂y^∂z⋅∂z∂w\frac{\partial L}{\partial w} = \frac{\partial L}{\partial \hat{y}} \cdot \frac{\partial \hat{y}}{\partial z} \cdot \frac{\partial z}{\partial w}

これにより、出力の誤差から遡って、各層・各重みの「責任の度合い(勾配)」を効率よく計算できる。順伝播が「予測」なら、逆伝播は「どの重みが、どれだけ間違いに寄与したかの反省」だ。

逆伝播(右から左へ「誤差の責任」を流す)
  入力層        隠れ層         出力層
  ◀── 勾配 ──[責任分配]◀── 勾配 ──[責任分配]◀── 損失 L
  各重みが「間違いにどれだけ寄与したか」を連鎖律で遡って計算

厨房の比喩なら、こうだ。完成した料理が「しょっぱすぎた」(損失)。ならば、その原因を工程を遡って探る。「盛り付けは無関係、味付けの塩が主犯、下ごしらえの塩も少し寄与」——各工程の責任を、結果から逆算して割り振る。逆伝播がやっているのは、この責任の遡及配分そのものだ。


自作ニューラルネット — XORを解く

理論を、動くコードにしよう。線形分離できない古典的難問「XOR」((0,0)→0, (0,1)→1, (1,0)→1, (1,1)→0)を、2層のニューラルネットに学習させる。フレームワークを使わず、numpyだけで順伝播・逆伝播・勾配降下法を実装する。

import numpy as np
np.random.seed(0)

# --- データ: XOR ---
X = np.array([[0,0],[0,1],[1,0],[1,1]], dtype=float)
Y = np.array([[0],[1],[1],[0]], dtype=float)

# --- 活性化関数(シグモイド)と、その微分 ---
def sigmoid(z):  return 1 / (1 + np.exp(-z))
def d_sigmoid(a): return a * (1 - a)   # aはsigmoidの出力

# --- パラメータ初期化: 2入力 → 隠れ4 → 出力1 ---
W1 = np.random.randn(2, 4);  b1 = np.zeros((1, 4))
W2 = np.random.randn(4, 1);  b2 = np.zeros((1, 1))
lr = 0.5

for epoch in range(10000):
    # === 順伝播: 予測する ===
    z1 = X @ W1 + b1;   a1 = sigmoid(z1)      # 隠れ層
    z2 = a1 @ W2 + b2;  a2 = sigmoid(z2)      # 出力層(予測 ŷ)

    # === 損失: 予測と正解のズレ(平均二乗誤差)===
    loss = np.mean((a2 - Y) ** 2)

    # === 逆伝播: 各重みの勾配を連鎖律で遡って計算 ===
    d_a2 = 2 * (a2 - Y) / len(X)              # ∂L/∂ŷ
    d_z2 = d_a2 * d_sigmoid(a2)               # 出力層の誤差
    d_W2 = a1.T @ d_z2;  d_b2 = d_z2.sum(0, keepdims=True)

    d_a1 = d_z2 @ W2.T                        # 誤差を隠れ層へ逆流
    d_z1 = d_a1 * d_sigmoid(a1)               # 連鎖律で責任を分配
    d_W1 = X.T @ d_z1;   d_b1 = d_z1.sum(0, keepdims=True)

    # === 勾配降下法: 勾配の逆向きにパラメータ更新(第18章の更新式)===
    W2 -= lr * d_W2;  b2 -= lr * d_b2
    W1 -= lr * d_W1;  b1 -= lr * d_b1

    if epoch % 2000 == 0:
        print(f"epoch {epoch:5d}: loss={loss:.4f}")

# --- 学習結果を確認 ---
z1 = sigmoid(X @ W1 + b1)
pred = sigmoid(z1 @ W2 + b2)
print("予測:", pred.round(2).ravel())   # [0, 1, 1, 0] に近づく

このコードを走らせると、loss が下がり続け、最終的に予測が [0, 1, 1, 0]——XORの正解——に収束する。フレームワークなしで、知能の学習ループがちゃんと動く。ここにあるのは、順伝播(予測)・損失(採点)・逆伝播(反省)・勾配降下法(修正)という、たった4ステップの繰り返しだけだ。

順伝播⏟予測→損失⏟採点→逆伝播⏟反省→更新⏟修正→(繰り返し)\underbrace{\text{順伝播}}_{\text{予測}} \to \underbrace{\text{損失}}_{\text{採点}} \to \underbrace{\text{逆伝播}}_{\text{反省}} \to \underbrace{\text{更新}}_{\text{修正}} \to \text{(繰り返し)}

PyTorchやTensorFlowが loss.backward() の一行で自動微分してくれるのは、この逆伝播の連鎖律計算を裏で全部やっているからだ。そして第16章のGPUが速いのは、X @ W1 のような行列演算を超並列で処理するからだ。ここまでの全21章の知識が、この数十行に凝縮されている。


まとめ — 「おまじない」の消し方

  1. ニューロンは重み付き和:Wx⃗+b⃗W\vec{x}+\vec{b}。入力に重要度を掛けて足す小さな装置
  2. 活性化関数が命を吹き込む:ReLUやシグモイドの非線形性が、複雑なパターンの学習を可能にする
  3. 順伝播で予測:入力から出力へ、変換と活性化を繰り返してデータを流す
  4. 逆伝播で反省:出力の誤差を連鎖律で逆流させ、各重みの「責任(勾配)」を効率よく求める
  5. 学習ループ:予測→採点→反省→修正の繰り返し。これがAIの「学習」の正体のすべて

「AIはどうやって賢くなるのか」——その答えは、脳を模した単純な計算装置が、間違いを反省して自らを修正し続ける、地道なループにあった。

さて、私たちは数値のパターンを学習できた。だが、現代AIの主役は言語だ。「猫」や「王様」といった言葉を、どうやって数値(ベクトル)に変換し、意味を捉えさせるのか。次章、言語の分散表現へ。


「知能とは、完璧さではなく、間違いから学ぶ能力だ。ニューラルネットが教えてくれるのは、賢さの正体が『反省し、修正し、また試す』という、ひどく人間的な営みだったということかもしれない。」