ニューラルネットワークをフレームワーク無しで試す。

Python, ML28 December 2020

ゼロから作るディープラーニングの中で、ニューラルネットワークをフレームワーク無しで実装し 根本的な理解を深めるというものがあります。

その内容を少しだけ改変し、自分なりに更に理解するために以下実装します。

参考

クラスで定義

Affine変換(入力X * 重みW + バイアスb)と、各活性化関数、評価指標をクラスで定義します。 また各クラスは、順方向(forward)と逆方向(backward)のメソッドを保持したいので、 インターフェース(抽象クラス)を先に定義します。

クラスで定義することで、各クラスを各1つ1つの層とみなしネットワークの構築が直感的且つ更新しやすくなります。

インターフェース

Neuronクラスをabc.ABCMetaで抽象基底クラスとして定義します。

from abc import ABCMeta, abstractmethod
from dataclasses import dataclass, InitVar, field
import numpy as np
from typing import List

class Neuron(metaclass=ABCMeta):
    @abstractmethod
    def forward(self):
        pass

    @abstractmethod
    def backward(self):
        pass   

Affine, ReLU, Sigmoid

入力や出力など全ての値は基本的にnp.ndarrayによる行列で表される。

Affine

  • x: 層の入力
  • W: 重み
  • b: バイアス
  • out: 層の出力
  • dx: 入力の微分
  • dW: 重みの微分
  • db: バイアスの微分
  • dout: 層の出力の微分
@dataclass
class Affine(Neuron):
    input_size: np.ndarray
    hidden_size: np.ndarray

    W: np.ndarray = field(init=False)
    b: np.ndarray = field(init=False)
    dW: np.ndarray = field(init=False)
    db: np.ndarray = field(init=False)
    x: np.ndarray = field(init=False)
    x_shape: tuple = field(init=False)
    learning_rate: float = field(default=0.1)

    def __post_init__(self):
        self.W = np.random.randn(self.input_size, self.hidden_size)
        self.b = np.zeros(self.hidden_size)

    def forward(self, x: np.ndarray) -> np.ndarray:
        self.x_shape = x.shape
        self.x = x.reshape(x.shape[0], -1)
        out = np.dot(self.x, self.W) + self.b
        return out

    def backward(self, dout: np.ndarray) -> np.ndarray:
        dx = np.dot(dout, self.W.T).reshape(*self.x_shape)

        self.dW = np.dot(self.x.T, dout)
        self.db = np.sum(dout, axis=0)
        self.W -= self.learning_rate * self.dW
        self.b -= self.learning_rate * self.db

        return dx

ReLU

  • x: 層の入力
  • out: 層の出力
  • dx: 層の入力の微分
  • dout: 層の出力の微分
  • mask: 0以下であればTrueとなるbool値の行列
@dataclass
class ReLU(Neuron):
    mask: bool = field(init=False)

    def forward(self, x: np.ndarray) -> np.ndarray:
        self.mask = (x <= 0)
        out = x.copy()
        out[self.mask] = 0
        return out

    def backward(self, dout: np.ndarray) -> np.ndarray:
        dout[self.mask] = 0
        dx = dout
        return dx

ReLUのforward処理をnp.maximumを使用しても良いが、E資格の試験ではmaskによる計算が出る可能性があるのでこっち 参考

Sigmoid

  • x: 層の入力
  • out: 層の出力
  • dx: 層の入力の微分
  • dout: 層の出力の微分
@dataclass
class Sigmoid(Neuron):
    out: np.ndarray

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

    def backward(self, dout: np.ndarray) -> np.ndarray:
        dx = dout * (1 - self.out) * self.out
        return dx

MSE(Mean Square Error)

平均2乗誤差で評価するためのclassを定義します。

@dataclass
class MSE(Neuron):
    n: int
    loss: np.ndarray = field(init=False)
    true_y: np.ndarray = field(init=False)
    pred_y: np.ndarray = field(init=False)

    def forward(self, true_y: np.ndarray, pred_y: np.ndarray) -> float:
        self.true_y = true_y
        self.pred_y = pred_y
        self.loss = (0.5 * np.sum((pred_y - true_y) ** 2))  / self.pred_y.shape[0]
        return self.loss

    def backward(self):
        dx = self.pred_y - self.true_y
        return dx / self.pred_y.shape[0]

ネットワーク作成

定義したclassを組み合わせて、ネットワークを定義します。

@dataclass
class Network:
    layers: List[Neuron]
    last_layer: Neuron

    def predict(self, x: np.ndarray) -> np.ndarray:
        for layer in self.layers:
            x = layer.forward(x)
        return x

    def loss(self, x: np.ndarray, true_y: np.ndarray) -> float:
        pred_y = self.predict(x)
        return self.last_layer.forward(true_y=true_y, pred_y=pred_y)

    def fit(self):
        dout = self.last_layer.backward()

        for layer in reversed(list(self.layers)):
            dout = layer.backward(dout)

インスタンス化し学習を作成

np.random.seed(0)

network = Network(
    layers=[
        Affine(input_size=2, hidden_size=2, learning_rate=0.2),
        ReLU(),
        Affine(input_size=2, hidden_size=2, learning_rate=0.2)
    ],
    lastLayer=MSE(n=input_size)
)

# 学習データを作成。2つの入力の和と乗を出力する
X_train = []
y_train = []
train_data_size = 1500

for i in range(train_data_size):
    x1 = np.random.rand()
    x2 = np.random.rand()
    y1 = x1 + x2
    y2 = x1 * x2

    X_train.append([x1, x2])
    y_train.append([y1, y2])

X_train = np.array(X_train)
y_train = np.array(y_train)

データ量は適当に1500にしてます。

学習の前に現時点でのpredictを確認

# とりあえず予測
X_test = np.array([[0.3, 0.5]])
y_test = network.predict(X_test)
print(y_test)

> [[ 1.43747606 -0.95416195]]

最高の結果としては、[[0.8, 0.15]]になるので、今は全然違う値が出てます。 これを学習で近づける。

学習スタート

確率的勾配降下法(SGD)で学習します

# 確率的勾配降下法で学習させる

n_epochs = 1500
losses = []

for i in range(n_epochs):
    e = network.loss(x=X_train, true_y=y_train)
    network.fit()
    e = network.loss(x=X_train, true_y=y_train)
    losses.append(e)

print(losses[-1])
> 0.003274294407734208

最終的な損失がかなり0に近づいたので、再度predictする

結果

print(network.predict(X_test))
> [[0.79125991 0.14192704]]

最適解にだいぶ近づきました。一応成功としよう。

p.s.

同じノリで、MNISTの手書き文字認識も著書から少し改変し実装してみました。
ソースコードはこちらのGistにあります

tags: Python, ML