ゼロから作るディープラーニングの中で、ニューラルネットワークをフレームワーク無しで実装し 根本的な理解を深めるというものがあります。
その内容を少しだけ改変し、自分なりに更に理解するために以下実装します。
クラスで定義
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にあります