第2章:ニューラルネットワークの仕組み

順伝播、重み行列、損失関数を理解する

📖 読了時間: 35-40分 💻 コード例: 10個 📝 演習: 6問 📊 難易度: 初級〜中級
この章では、ニューラルネットワークが入力から出力までどのようにデータを処理するかを学びます。順伝播の計算、重み行列とバイアスベクトルの役割、計算グラフ、そして予測誤差を測定する損失関数について理解します。

学習目標

1. 順伝播(Forward Propagation)

1.1 入力から出力までのデータフロー

順伝播とは、入力が与えられたときにニューラルネットワークの出力を計算するプロセスです。データは層ごとにネットワークを流れ、各層でデータが変換され、最終的な出力が生成されます。

graph LR subgraph 入力層 I1[x1] I2[x2] I3[x3] end subgraph 隠れ層 H1[h1] H2[h2] end subgraph 出力層 O1[y1] O2[y2] end I1 --> H1 I1 --> H2 I2 --> H1 I2 --> H2 I3 --> H1 I3 --> H2 H1 --> O1 H1 --> O2 H2 --> O1 H2 --> O2 style I1 fill:#e3f2fd style I2 fill:#e3f2fd style I3 fill:#e3f2fd style H1 fill:#fff3e0 style H2 fill:#fff3e0 style O1 fill:#e8f5e9 style O2 fill:#e8f5e9

各層での計算は以下のステップで行われます:

  1. 線形変換:入力に重みを掛けてバイアスを加える
  2. 活性化:非線形活性化関数を適用する

単一層の数式表現:

$$\mathbf{z} = \mathbf{W}\mathbf{x} + \mathbf{b}$$

$$\mathbf{a} = f(\mathbf{z})$$

ここで:

1.2 行列演算による効率的な計算

ニューロンの出力を1つずつ計算する代わりに、行列演算を使用して層内のすべてのニューロンを同時に処理します。これはより簡潔なだけでなく、最適化された線形代数ライブラリにより大幅に高速です。

なぜ行列演算?

import numpy as np

def forward_layer(X, W, b, activation_fn):
    """
    単一層の順伝播

    Parameters:
    -----------
    X : ndarray, shape (batch_size, n_input)
        入力データ
    W : ndarray, shape (n_input, n_output)
        重み行列
    b : ndarray, shape (1, n_output)
        バイアスベクトル
    activation_fn : function
        活性化関数

    Returns:
    --------
    A : ndarray, shape (batch_size, n_output)
        活性化出力
    Z : ndarray, shape (batch_size, n_output)
        活性化前の値(逆伝播用)
    """
    # 線形変換: Z = X @ W + b
    Z = np.dot(X, W) + b

    # 活性化関数を適用
    A = activation_fn(Z)

    return A, Z

# 使用例
np.random.seed(42)

# 入力: 4サンプル、3特徴量
X = np.array([
    [1.0, 2.0, 3.0],
    [4.0, 5.0, 6.0],
    [7.0, 8.0, 9.0],
    [10.0, 11.0, 12.0]
])

# 重み: 3入力 -> 2出力
W = np.random.randn(3, 2) * 0.01
b = np.zeros((1, 2))

# ReLU活性化
relu = lambda x: np.maximum(0, x)

A, Z = forward_layer(X, W, b, relu)
print("入力形状:", X.shape)
print("出力形状:", A.shape)

2. 重み行列とバイアスベクトル

2.1 重みの役割

重み行列 $\mathbf{W}$ は、各入力特徴量が各出力ニューロンにどれだけ強く影響するかを決定します。

2.2 重みの初期化

適切な重みの初期化は効果的な学習に不可欠です。

方法 適用場面
ゼロ初期化 $W = 0$ 使用禁止(対称性の問題)
小さなランダム値 $W \sim \mathcal{N}(0, 0.01)$ 単純なネットワーク
Xavier/Glorot $W \sim \mathcal{N}(0, \sqrt{\frac{2}{n_{in} + n_{out}}})$ Sigmoid、tanh
He $W \sim \mathcal{N}(0, \sqrt{\frac{2}{n_{in}}})$ ReLU

2.3 バイアスの重要性

バイアスベクトル $\mathbf{b}$ は活性化関数を水平方向にシフトさせます。バイアスがなければ、入力がゼロのときに活性化前の値 $z$ もゼロになり、ネットワークの表現力が制限されます。

3. 層の接続と計算グラフ

3.1 計算グラフの概念

計算グラフは、ニューラルネットワークにおける演算の順序を表す有向グラフです。各ノードは演算(加算、乗算、活性化関数)を表し、エッジはデータフローを表します。

3.2 自動微分の基礎

計算グラフの主な利点は自動微分を可能にすることです。中間値を保存し連鎖律を適用することで、すべてのパラメータに対する勾配を効率的に計算できます。

連鎖律(Chain Rule):$y = f(g(x))$ のとき

$$\frac{dy}{dx} = \frac{dy}{dg} \cdot \frac{dg}{dx}$$

これにより、出力から入力へ層ごとに勾配を計算できます。

4. 損失関数(MSE、Cross Entropy)

損失関数(コスト関数、目的関数とも呼ばれる)は、ネットワークの予測が真の値とどれだけ一致しているかを測定します。学習の目標はこの損失を最小化することです。

4.1 回帰問題:MSE(Mean Squared Error)

MSEは回帰問題の標準的な損失関数です。

$$\mathcal{L}_{MSE} = \frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2$$

import numpy as np

def mse_loss(y_true, y_pred):
    """
    平均二乗誤差損失

    Parameters:
    -----------
    y_true : ndarray
        真の値
    y_pred : ndarray
        予測値

    Returns:
    --------
    loss : float
        MSE損失値
    """
    return np.mean((y_true - y_pred) ** 2)

# 使用例
y_true = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
y_pred = np.array([1.2, 1.8, 3.1, 3.9, 5.2])

loss = mse_loss(y_true, y_pred)
print(f"MSE損失: {loss:.4f}")

4.2 分類問題:Cross Entropy Loss

Cross Entropyは分類問題の標準的な損失関数です。

多クラス分類のCategorical Cross Entropy

$$\mathcal{L}_{CE} = -\frac{1}{n} \sum_{i=1}^{n} \sum_{c=1}^{C} y_{i,c} \log(\hat{y}_{i,c})$$

import numpy as np

def categorical_cross_entropy(y_true, y_pred, epsilon=1e-15):
    """
    Categorical Cross Entropy損失

    Parameters:
    -----------
    y_true : ndarray, shape (n_samples, n_classes)
        真のラベル(one-hot encoded)
    y_pred : ndarray, shape (n_samples, n_classes)
        予測確率

    Returns:
    --------
    loss : float
        CCE損失値
    """
    y_pred = np.clip(y_pred, epsilon, 1 - epsilon)
    return -np.mean(np.sum(y_true * np.log(y_pred), axis=1))

# 使用例
y_true = np.array([
    [1, 0, 0, 0],  # クラス0
    [0, 1, 0, 0],  # クラス1
    [0, 0, 0, 1]   # クラス3
])

y_pred_good = np.array([
    [0.9, 0.05, 0.03, 0.02],
    [0.1, 0.7, 0.1, 0.1],
    [0.05, 0.05, 0.1, 0.8]
])

loss = categorical_cross_entropy(y_true, y_pred_good)
print(f"Cross Entropy損失: {loss:.4f}")

4.3 損失関数の選択基準

タスク 出力活性化 損失関数
回帰 線形(なし) MSE、MAE、Huber
二値分類 Sigmoid Binary Cross Entropy
多クラス分類 Softmax Categorical Cross Entropy

5. 完全結合層の実装

5.1 NumPyによる実装

import numpy as np

class DenseLayer:
    """
    全結合(Dense)層の実装
    """

    def __init__(self, n_input, n_output, activation='relu'):
        self.n_input = n_input
        self.n_output = n_output
        self.activation = activation

        # He初期化
        self.W = np.random.randn(n_input, n_output) * np.sqrt(2.0 / n_input)
        self.b = np.zeros((1, n_output))

        self.cache = {}

    def _activate(self, Z):
        if self.activation == 'relu':
            return np.maximum(0, Z)
        elif self.activation == 'softmax':
            exp_Z = np.exp(Z - np.max(Z, axis=1, keepdims=True))
            return exp_Z / np.sum(exp_Z, axis=1, keepdims=True)
        else:
            return Z

    def forward(self, X):
        self.cache['X'] = X
        Z = np.dot(X, self.W) + self.b
        self.cache['Z'] = Z
        A = self._activate(Z)
        self.cache['A'] = A
        return A

# ネットワーク構築
np.random.seed(42)
layer1 = DenseLayer(4, 8, activation='relu')
layer2 = DenseLayer(8, 3, activation='softmax')

X = np.array([[5.1, 3.5, 1.4, 0.2]])
A1 = layer1.forward(X)
output = layer2.forward(A1)

print("入力形状:", X.shape)
print("出力形状:", output.shape)
print("予測確率:", output)

5.2 PyTorchによる実装

import torch
import torch.nn as nn

class SimpleNetwork(nn.Module):
    """
    PyTorchによるシンプルな全結合ネットワーク
    """

    def __init__(self, input_size, hidden_sizes, output_size):
        super(SimpleNetwork, self).__init__()

        layers = []
        prev_size = input_size

        for hidden_size in hidden_sizes:
            layers.append(nn.Linear(prev_size, hidden_size))
            layers.append(nn.ReLU())
            prev_size = hidden_size

        layers.append(nn.Linear(prev_size, output_size))

        self.network = nn.Sequential(*layers)

    def forward(self, x):
        return self.network(x)

# モデル作成
model = SimpleNetwork(input_size=4, hidden_sizes=[8, 4], output_size=3)
print("モデルアーキテクチャ:")
print(model)

演習問題

演習1:手動での順伝播計算

問題:2入力、2隠れニューロン、2出力のネットワークで、与えられた重みとバイアスを使って出力を手計算し、コードで検証してください。

演習2:損失関数の比較

問題:二値分類問題で、MSEとBinary Cross Entropyの損失がどのように変化するかを比較し、なぜ分類にはCross Entropyが好まれるか説明してください。

演習3:Huber損失の実装

問題:MSEとMAEを組み合わせたHuber損失を実装し、外れ値に対する挙動をMSE、MAEと比較してください。

演習4:バッチ処理

問題:ミニバッチでデータを処理する関数を実装し、バッチサイズ1、32、128、1000での処理時間を測定してください。

演習5:重み初期化の実験

問題:5層ネットワークで、ゼロ、ランダム小、Xavier、He初期化を比較し、各層での活性化の統計量をプロットしてください。

演習6:計算グラフの可視化

問題:2層ネットワークの計算グラフを描き、すべての演算をノードとして、エッジにテンソル形状をラベル付けしてください。

まとめ

この章では、ニューラルネットワークがデータを処理する仕組みを学びました:

次章の予告:第3章では、学習アルゴリズム—勾配降下法と誤差逆伝播法を使ってニューラルネットワークが損失を最小化するために重みをどのように調整するかを学びます。

免責事項