0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

numpyで実装するフルスクラッチ・ニューラルネットワーク

Posted at

初めに

機械学習・深層学習に関して、理論面で力不足を感じていたため、わかりやすいパターン認識という書籍でニューラルネットワークの理論について学習し、数式を元にニューラルネットワークを実装してみました。
以下のリポジトリが全体のプログラムです。

構成

  • activation_function.py
  • linear.py
  • loss_function
  • neural_network.py
  • demo.ipynb

demo.ipynbを実行することで学習・推論を行うことができます。

activation_function.py

import numpy as np


class Sigmoid:
    def forward(self, x):
        self.out = 1 / (1 + np.exp(-x))
        return self.out

    def backward(self, grad_output, learning_rate):
        learning_rate = learning_rate
        return grad_output * self.out * (1 - self.out)


class ReLU:
    def forward(self, x):
        self.out = np.maximum(0, x)
        return self.out

    def backward(self, grad_output, learning_rate):
        learning_rate = learning_rate
        return grad_output * (self.out > 0)


class Softmax:
    def forward(self, x):
        self.out = np.exp(x) / np.sum(np.exp(x), axis=1, keepdims=True)
        return self.out

    def backward(self, grad_output, learning_rate):
        learning_rate = learning_rate
        return grad_output * self.out * (1 - self.out)

各クラスのforwardメソッドでは、入力データに対してそれぞれの活性化関数を適用し、変換後の出力を計算しています。一方、backwardメソッドは、逆伝播時に受け取った勾配(grad_output)を各活性化関数の微分値で乗算することで、誤差を前の層に伝える役割を果たしています。

Sigmoid クラス

forward メソッド

入力に対してシグモイド関数を適用します。

σ(x)=\frac{1}{1+e^{−x}}

この計算結果を self.out に保存しています。

backward メソッド

逆伝播では、受け取った勾配grad_outputgrad_output に対し、シグモイド関数の微分を乗算して誤差を前の層へ伝達します。
シグモイドの微分は次のようになります。

\frac{dσ}{dx}=σ(x)(1−σ(x))

したがって戻り値はgrad_output × σ(x)(1−σ(x))となります。

ReLU

forward

入力に対し、ReLU関数

max(0,x)

を適用し、負の値は$0$に、正の値はそのままにします。

backward

逆伝播では、入力が正の部分のみ$1$、それ以外は$0$となる微分を用いて勾配を伝えます。

$x>0$の場合は微分が$1$、$x≤0$の場合は$0$となります。

これを指示関数 $I(x>0)$を用いて表すと、

\frac{d}{ReLUdx}=I(x>0)

よって、逆伝播時の勾配は

δ=grad \, output×I(x>0)

となります。

Softmax

forward

入力行列の各行に対して、指数関数を適用後、正規化を行い、確率分布としての出力を得ます。
Softmax 関数は

Softmax(x_i​)=\frac{e^{x^i}}{∑{_j} e^{x^j}}

と定義されます。

backward

本実装では、逆伝播時に簡略化し

δ=grad \, output×Softmax(x)(1−Softmax(x))

という式を用いています。

linear.py

import numpy as np

from scripts.neural_network import NeuralNetwork


# 活性化関数は変更可能にする
class Linear(NeuralNetwork):
    def __init__(self, input_size, output_size):
        self.weights = np.random.randn(input_size, output_size) * np.sqrt(
            1 / input_size
        )
        self.bias = np.zeros((1, output_size))

    def forward(self, x):
        """
        順伝播
        Parameters:
            x : 入力
        Returns:
            output : 出力
        """
        self.inputs = x
        # わかりやすいパターン認識 p36
        self.output = np.dot(x, self.weights) + self.bias
        return self.output

    def backward(self, output_error, learning_rate):
        """
        逆伝播
        Parameters:
            output_error : 出力誤差
            learning_rate : 学習率
        Returns:
            input_error : 入力誤差
        """
        input_error = np.dot(output_error, self.weights.T)
        # わかりやすいパターン認識 p62
        self.weights -= learning_rate * np.dot(self.inputs.T, output_error)
        # わかりやすいパターン認識 p67 図を参考
        self.bias -= learning_rate * np.sum(output_error, axis=0, keepdims=True)
        return input_error

初期化 (init メソッド)

重みの初期化

入力サイズ input_size と出力サイズ output_size を受け取り、重みは

W∼N(0,\frac{1}{input \, size})

のように初期化されています。ここでは、入力次元に依存する正規化を行っており、勾配消失などの問題を軽減すしています。

バイアスの初期化

バイアスは出力サイズに合わせてゼロベクトルで初期化されます。

順伝播 (forward メソッド)

このメソッドでは、入力に対して線形変換を行います。具体的には、以下の計算を行っています。

output=xW+b

変数

self.inputs: 逆伝播の際に、入力 xx を利用するために保持しています。

self.output: 線形変換の結果を保持しています。

この順伝播処理は、ニューラルネットワーク内の各層で特徴量の抽出を行います。

逆伝播 (backward メソッド)

逆伝播では、出力側から受け取った誤差 output_error を元に、前の層に伝える勾配(入力誤差)を計算し、さらに重みとバイアスの更新を行います。

入力誤差の計算

入力誤差 input_error は、出力誤差を重み行列の転置と掛け合わせることで計算されます。

input \, error=output \, error×WT

これにより、出力層で計算された誤差が、入力層へ逆伝播されます。

重みの更新

重みは、学習率 learning_rate を用いて、勾配降下法により更新されます。
勾配は、入力の転置と出力誤差の積で求められます。

W−=learning \, rate×xT⋅output \, error

ここで $xT$ は入力 $x$ の転置です。
この式は、「わかりやすいパターン認識 p62」の内容に基づいています。

バイアスの更新

バイアスも同様に、学習率をかけた出力誤差の和で更新されます。
出力誤差を各サンプルごとに合計し、次のように更新します。

    b−=learning \, rate×∑output \, error

バイアスの更新は「わかりやすいパターン認識 p67」の図を参考にしています。

戻り値

最終的に、計算した入力誤差 input_error を返し、これが前の層へ伝播されます。

loss_function.py

import numpy as np


# Mean Squared Error (MSE)
def mse(y_true, y_pred):
    """
    平均二乗誤差
    Parameters:
        y_true : 真の値
        y_pred : 予測値
    Returns:
        mse : 平均二乗誤差
    """
    if y_true.shape != y_pred.shape:
        raise ValueError("Shapes of y_true and y_pred must be the same.")
    return np.mean((y_true - y_pred) ** 2)


# Cross Entropy Loss
def cross_entropy(y_true, y_pred):
    """
    クロスエントロピー誤差
    Parameters:
        y_true : 真の値
        y_pred : 予測値
    Returns:
        cross_entropy : クロスエントロピー誤差
    """
    if y_true.shape != y_pred.shape:
        raise ValueError("Shapes of y_true and y_pred must be the same.")
    return -np.sum(y_true * np.log(y_pred)) / y_true.shape[0]


# Binary Cross Entropy Loss
def binary_cross_entropy(y_true, y_pred):
    """
    二値クロスエントロピー誤差
    Parameters:
        y_true : 真の値
        y_pred : 予測値
    Returns:
        binary_cross_entropy : 二値クロスエントロピー誤差
    """
    if y_true.shape != y_pred.shape:
        raise ValueError("Shapes of y_true and y_pred must be the same.")
    return (
        -np.sum(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred))
        / y_true.shape[0]
    )


def return_loss_function(loss):
    """
    損失関数を返す
    Parameters:
        loss : 損失関数の名前
    Returns:
        loss function : 損失関数
    """
    if loss == "mse":
        return mse
    elif loss == "cross_entropy":
        return cross_entropy
    elif loss == "binary_cross_entropy":
        return binary_cross_entropy
    else:
        raise ValueError("Invalid loss function.")

Mean Squared Error (MSE)

予測値と真の値との誤差の二乗平均を計算します。
特に回帰問題でよく用いられ、誤差が大きいほどペナルティを大きく与える性質があります。

MSE=\frac{1}{N}∑_i=\frac{1}{N}(∑_{i=1}^{N}y_{true}^{(i)}−y_{pred}^{(i)})^2

ここで $N$ はデータサンプル数です。

Cross Entropy Loss

分類問題(特に多クラス分類)で、予測確率と真のクラスラベルとの誤差を測定します。
確率分布間の距離を評価するため、確率が小さいほど大きなペナルティが課されます。
損失は全サンプルに対する平均の形で計算されます。

CrossEntropy=−\frac{1}{N}∑_{i=1}^{N}∑_jy_{true_j}^{(i)}log⁡(y_{pred_j}^{(i)})

ここで、$j$ はクラスのインデックスです。

Binary Cross Entropy Loss

2値分類(バイナリ分類)において、各サンプルごとの損失を計算します。
正例と負例それぞれの確率に対してペナルティを課す形となっています。
損失は正例 $y=1$ の場合と負例 $y=0$ の場合で別々に考え、平均値を求めます。

BinaryCrossEntropy=−\frac{1}{N}∑_i=\frac{1}{N}[y_{true}^{(i)}log⁡(y_{pred}^{(i)})+(1−y_{true}^{(i)})log⁡(1−y_{pred}^{(i)})]

return_loss_function

与えられた文字列(例:"mse", "cross_entropy", "binary_cross_entropy")に対応する損失関数を返すためのヘルパー関数です。
条件分岐により、対応する損失関数を返し、未知の名前の場合はエラーを発生させます。

neural_network.py

from scripts.loss_function import return_loss_function


class NeuralNetwork:
    """
    ニューラルネットワーク
    Parameters:
        layers : レイヤーのリスト
        loss : 損失関数
        learning_rate : 学習率
        debug_loss_interval : デバッグ用の損失関数の表示間隔

    Functions:
        forward : 順伝播
        backward : 逆伝播
        train : 学習

    """

    def __init__(self, layers, loss, learning_rate, debug_loss_interval=100):
        self.layers = layers
        self.loss_func = return_loss_function(loss)
        self.learning_rate = learning_rate
        self.debug_loss_interval = debug_loss_interval

    def forward(self, x):
        for layer in self.layers:
            x = layer.forward(x)
        return x

    def backward(self, y, output):
        error = (output - y) / y.shape[0]
        for layer in reversed(self.layers):
            error = layer.backward(error, self.learning_rate)

    def train(self, X, y, epochs):
        for epoch in range(epochs):
            output = self.forward(X)
            self.backward(y, output)
            if epoch % self.debug_loss_interval == 0 or epoch == 0:
                loss = self.loss_func(y, output)
                print(f"Epoch {epoch}, Loss: {loss}")

init メソッド

  • レイヤーのリスト:
    layers にはネットワーク内の各層のインスタンスが格納されます

  • 損失関数:
    loss で指定された損失関数の名前をreturn_loss_function を使って関数オブジェクトに変換し、self.loss_func に保存します
    たとえば、"mse" と指定すれば、平均二乗誤差が用いられます

  • 学習率:
    learning_rate はパラメータ更新時のステップサイズとして利用され、各層の backward メソッドに渡されます。

  • デバッグ用設定:
    debug_loss_interval は学習中に損失値を表示する間隔(エポック数)を指定します。

forward メソッド

入力データ $x$ を各層を通して順に処理し、最終的な出力を計算します。
各層の forward メソッドを順番に呼び出し、次の層の入力として渡します。
数式で表すと、入力 $x$ に対して各層 $f_1,f_2,…,f_L​$ を適用し、

    output=f_L(⋯f_2(f_1(x))⋯ )

のように表現されます。

backward メソッド

誤差の初期化

順伝播で得た出力と正解ラベル yy との誤差を各層に逆伝播させ、パラメータの更新を行うための勾配を計算します。
ネットワーク全体の出力と正解との差を正規化して初期誤差を計算します。

        error=\frac{output−y}{N}

ここで $N$ はサンプル数y.shape[0]です。

層ごとの逆伝播

各層の backward メソッドを逆順に呼び出し、誤差を前の層に伝えます。
各層では、受け取った勾配に基づいてパラメータ(重みやバイアス)の更新と、次の層に渡す入力誤差の計算が行われます。

train メソッド

ネットワークを指定されたエポック数だけ訓練します。各エポックで、順伝播・逆伝播によるパラメータの更新を行います。

順伝搬

入力 $X$ を用いて forward メソッドを呼び出し、出力を計算します。

逆伝搬

得られた出力と正解 $y$ をもとに、backward メソッドで勾配計算とパラメータ更新を実施します。

損失の表示

指定されたエポック(debug_loss_interval の間隔または最初のエポック)ごとに、現在の損失値を計算し表示します。
損失値は、前述の損失関数(例:MSE やクロスエントロピー)を用いて、

        Loss=loss \, func(y,output)

として求められます。

demo.ipynb

import sys
from pathlib import Path
current_dir = Path().resolve().parent
sys.path.append(str(current_dir))

import numpy as np
from scripts.neural_network import NeuralNetwork
from scripts.linear import Linear
from scripts.activation_function import Sigmoid, ReLU, Softmax
np.random.seed(1)
X = np.array([[0, 0],
              [0, 1],
              [1, 0],
              [1, 1]])
y = np.array([[0],
              [1],
              [1],
              [0]])

Layers = [Linear(2, 4), 
          Sigmoid(), 
          Linear(4, 1)]

nn = NeuralNetwork(Layers, loss="mse", learning_rate=0.4, debug_loss_interval=100)

nn.train(X, y, epochs=1000)

pred = nn.forward(X)
print("Predictions:")
print(pred)

必要なライブラリをimportし、パスを通し、作成したメソッドを利用することで、学習・推論を行うことができます。

最後に

理論を実装してみると、Pytorchなどのフレームワークに関する知識がつくのでおすすめです。
CNNなどもフルスクラッチで作成したら面白いかな?とか思っています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?