目次を表示する

LLM完全解説:スクラッチから理解する大規模言語モデル

ニューラルネットワーク入門 ── パーセプトロンから誤差逆伝播まで

ニューラルネットワーク入門 ── パーセプトロンから誤差逆伝播まで

LLM との関係:LLM は「巨大なニューラルネットワーク」である。この章で扱う順伝播・逆伝播・勾配降下法は、LLM の訓練そのものの基盤。


ニューラルネットワーク基礎 — 順伝播・逆伝播・Adam

この章で何ができるようになるか:「なぜ重みが更新されるとモデルが賢くなるか」を数学的に理解し、PyTorch での基本的な訓練ループを書けるようになる。


パーセプトロン:1個のニューロン

$$y = f(w_1 x_1 + w_2 x_2 + \dots + w_n x_n + b)$$

import numpy as np

def perceptron(x, weights, bias, activation='sigmoid'):
    z = np.dot(weights, x) + bias  # 線形結合
    if activation == 'sigmoid':
        return 1 / (1 + np.exp(-z))   # 0〜1 に圧縮
    elif activation == 'relu':
        return max(0, z)               # 0 以下を切り捨て

活性化関数がなぜ必要か:線形変換を何層重ねても結果は線形。非線形な活性化関数を挟むことで、非線形な関数(現実世界の複雑なパターン)を表現できるようになる。

主要な活性化関数

def sigmoid(z):
    return 1 / (1 + np.exp(-z))
    # 範囲: (0, 1)。確率の出力に使う。勾配消失しやすい。

def tanh(z):
    return np.tanh(z)
    # 範囲: (-1, 1)。sigmoid より中心が0で扱いやすい。

def relu(z):
    return np.maximum(0, z)
    # 範囲: [0, ∞)。最も広く使われる。計算が速い。

def gelu(z):
    """Gaussian Error Linear Unit — Transformer で標準的に使われる"""
    return 0.5 * z * (1 + np.tanh(np.sqrt(2/np.pi) * (z + 0.044715 * z**3)))
    # ReLU の滑らかな近似。GPT/BERT で使用。

多層ニューラルネットワーク:順伝播

class SimpleNN:
    """2層のニューラルネットワーク(NumPy 実装)"""

    def __init__(self, input_dim, hidden_dim, output_dim):
        # 重みをランダムに初期化(Xavier 初期化)
        scale1 = np.sqrt(2.0 / input_dim)
        scale2 = np.sqrt(2.0 / hidden_dim)
        self.W1 = np.random.randn(hidden_dim, input_dim) * scale1
        self.b1 = np.zeros(hidden_dim)
        self.W2 = np.random.randn(output_dim, hidden_dim) * scale2
        self.b2 = np.zeros(output_dim)

    def forward(self, x):
        """順伝播:入力 → 隠れ層 → 出力"""
        # 隠れ層
        self.z1 = self.W1 @ x + self.b1
        self.h = np.maximum(0, self.z1)  # ReLU

        # 出力層
        self.z2 = self.W2 @ self.h + self.b2
        self.output = softmax(self.z2)   # 確率分布

        return self.output

誤差逆伝播法(Backpropagation)

「出力の誤差を、各重みがどれだけ寄与したかに応じて逆方向に伝播し、重みを修正する」。

連鎖律(Chain Rule)

$$\frac{\partial L}{\partial w} = \frac{\partial L}{\partial y} \cdot \frac{\partial y}{\partial z} \cdot \frac{\partial z}{\partial w}$$

def backward(self, x, y_true):
    """逆伝播:勾配を計算する"""
    batch_size = 1

    # 出力層の勾配(ソフトマックス + 交差エントロピーの勾配は単純)
    dz2 = self.output.copy()
    dz2[y_true] -= 1  # ∂L/∂z2 = predicted - one_hot(true)

    # W2, b2 の勾配
    self.dW2 = np.outer(dz2, self.h)  # ∂L/∂W2
    self.db2 = dz2                     # ∂L/∂b2

    # 隠れ層への勾配
    dh = self.W2.T @ dz2              # ∂L/∂h

    # ReLU の勾配(z > 0 なら 1、z <= 0 なら 0)
    dz1 = dh * (self.z1 > 0).astype(float)

    # W1, b1 の勾配
    self.dW1 = np.outer(dz1, x)
    self.db1 = dz1

def update(self, learning_rate=0.01):
    """勾配降下法で重みを更新"""
    self.W2 -= learning_rate * self.dW2
    self.b2 -= learning_rate * self.db2
    self.W1 -= learning_rate * self.dW1
    self.b1 -= learning_rate * self.db1

勾配降下法のバリエーション

SGD(確率的勾配降下法)

# 全データでなく、ミニバッチごとに更新
for epoch in range(100):
    for batch in dataloader:
        loss = model.forward(batch)
        loss.backward()
        for param in model.parameters():
            param.data -= learning_rate * param.grad

Adam(Adaptive Moment Estimation)

LLM の訓練で最も広く使われるオプティマイザ。

# Adam の直感:
# 1. 勾配の移動平均(momentum): 過去の方向を覚えて慣性をつける
# 2. 勾配の二乗の移動平均: パラメータごとに学習率を調整
#    → 勾配が大きいパラメータは学習率を下げ、小さいパラメータは上げる

import torch

optimizer = torch.optim.Adam(model.parameters(), lr=1e-4, betas=(0.9, 0.999))

for batch in dataloader:
    optimizer.zero_grad()        # 勾配をリセット
    output = model(batch.input)  # 順伝播
    loss = criterion(output, batch.target)  # 損失計算
    loss.backward()              # 逆伝播(自動微分)
    optimizer.step()             # パラメータ更新

PyTorch での実装

import torch
import torch.nn as nn

class SimpleLanguageModel(nn.Module):
    """最小の言語モデル(Transformer ではない簡易版)"""

    def __init__(self, vocab_size, embed_dim, hidden_dim):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim)
        self.fc1 = nn.Linear(embed_dim, hidden_dim)
        self.fc2 = nn.Linear(hidden_dim, vocab_size)
        self.gelu = nn.GELU()

    def forward(self, x):
        # x: (batch_size, seq_len) — トークンIDの列
        emb = self.embedding(x)        # (batch, seq, embed_dim)
        emb = emb.mean(dim=1)          # 全トークンの平均(簡易版)
        h = self.gelu(self.fc1(emb))   # (batch, hidden_dim)
        logits = self.fc2(h)           # (batch, vocab_size)
        return logits

# 訓練ループ
model = SimpleLanguageModel(vocab_size=10000, embed_dim=256, hidden_dim=512)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.CrossEntropyLoss()

for epoch in range(10):
    for input_ids, target_ids in dataloader:
        logits = model(input_ids)
        loss = criterion(logits, target_ids)
        optimizer.zero_grad()
        loss.backward()    # PyTorch が自動で逆伝播を計算
        optimizer.step()
    print(f"Epoch {epoch}: Loss = {loss.item():.4f}")

PyTorch の loss.backward() は何をしているか:計算グラフ(forward で記録された全演算の有向グラフ)を逆方向にたどり、連鎖律で全パラメータの勾配を自動計算する。手動で微分を書く必要はない。


過学習と正則化

過学習(Overfitting):
  訓練データには高い精度を出すが、未知のデータでは性能が低い
  → モデルが「訓練データを丸暗記」している

対策:
  Dropout: ランダムにニューロンを無効化(共適応を防ぐ)
  Weight Decay: 重みの大きさにペナルティ(L2正則化)
  Early Stopping: 検証損失が悪化し始めたら訓練を止める
class ModelWithDropout(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(256, 512)
        self.dropout = nn.Dropout(0.1)  # 10% のニューロンをランダムに0にする
        self.fc2 = nn.Linear(512, 10000)

    def forward(self, x):
        h = torch.relu(self.fc1(x))
        h = self.dropout(h)  # 訓練時のみ有効。推論時は無効化される
        return self.fc2(h)

まとめ

概念役割LLM への接続
順伝播入力 → 出力の計算LLM の推論はまさにこれ
逆伝播勾配の計算(連鎖律)LLM の訓練の核心
勾配降下法重みの更新Adam が標準
活性化関数非線形性の導入GELU が Transformer 標準
Dropout過学習防止LLM の全層に入っている
損失関数予測の良さの計測交差エントロピー

次章では「単語をどうやってベクトルに変換するか」──Word2Vec の仕組みを見ていく。