かざんぶろぐ

だらだらと備忘録とってます

Pythonでフレームワークを使わずにニューラルネットワークを実装

Chainerや、TensorFlowといった、深層学習のフレームワークを使用せず、NumpyとMatplotlibのみでニューラルネットワークを実装した。

全てのコードなどが公開されたリポジトリはこちら

やったこと

3クラス分類を行うニューラルネットワークPythonで実装した。
(ついでに2クラス分類を行ったり、XORを解いたりしたのでリポジトリに公開しておく)

データの準備

先ほどドヤ顔で深層学習のフレームワークを使用しないと宣言した手前情けないのだが、データの作成に関しては、自分で1から作成せず、scikit-learnmake_classificationという機能を使って作成した。

import sklearn.datasets
x , label = sklearn.datasets.make_classification(
n_features=2, n_samples=300, n_redundant=0, 
n_informative=2,n_clusters_per_class=1, n_classes=3)

これでxに入力データが、labelに教師データが格納される。
make_classification関数の引数は(3)の方の記事に詳しく書いてある。

import matplotlib.pyplot as plt
plt.scatter(x[:, 0], x[:, 1],c=label,cmap=plt.cm.jet)
plt.show()

f:id:okuya-KAZAN:20170523164550p:plain
このような感じでデータが生成された。
xは2つの入力値(座標)を持つサンプルを300個含んだ配列であり、
labelは各サンプルのクラス(0,1,2)を含んだ配列である。

print x[:10]
array([[ 0.52358787, -1.15423424],
       [-0.52633513, -1.76794057],
       [ 0.55418671, -0.46817551],
       [-1.17471257, -0.44251872],
       [-1.34866206, -0.54916791],
       [ 0.67537883, -0.53149583],
       [ 1.61055345,  0.0481599 ],
       [-0.09616488, -1.51676462],
       [ 0.22409607,  1.40511859],
       [ 2.24986887,  1.18862095]])
print label[:10]
array([2, 0, 0, 0, 0, 2, 1, 0, 1, 1])

ニューラルネットワークの構造

以下のようにニューラルネットワークを設計した。


f:id:okuya-KAZAN:20170523173356p:plain


層は入力層、隠れ層、出力層の3つで、各層のユニット数は(2,3,3)とし、
隠れ層の活性化関数にはTanh関数、
出力層の活性化関数にはSoftmax関数を使用した。

class Function(object):
    def tanh(self, x):
        return np.tanh(x)

    def dtanh(self, x):
        return 1. - x * x

    def softmax(self, x):
        e = np.exp(x - np.max(x))  # オーバーフローを防ぐ
        if e.ndim == 1:
            return e / np.sum(e, axis=0)
        else:
            return e / np.array([np.sum(e, axis=1)]).T # サンプル数が1の時

実装

※これ以降定義する関数は全てNewral_Networkクラスの中のメソッドである。

コンストラクタ

コンストラクタとは、新たにオブジェクトが生成される際に呼び出されて内容の初期化などを行なうメソッドのことであり、メッソド名は__init__とする。
ここでニューラルネットワークの構築を行う。

class Newral_Network(object):
    def __init__(self, unit):
        print "Number of layer = ",len(unit)
        print unit
        print "-------------------------"
        self.F = Function()
        self.unit = unit
        self.W = []
        self.B = []
        self.dW = []
        for i in range(len(self.unit) - 1):
            w = np.random.rand(self.unit[i], self.unit[i + 1])
            self.W.append(w)
            dw = np.random.rand(self.unit[i], self.unit[i + 1])
            self.dW.append(dw)
            b = np.random.rand(self.unit[i + 1])
            self.B.append(b)

unitは各層のユニット数が格納された配列である(今回の場合、unit=[2,3,3])。
unitの情報をもとに、学習するパラメータ(W, B)をランダムに初期化している。
dwは重みの修正量を保持する配列であり、モメンタムでの計算に使う。

順伝搬

構築したネットワークに入力値を与え、出力を得るforwardメソッドを定義する。
配列Zを用意し、そこに各層の出力値を加えていく。

# 順伝搬
def forward(self, _inputs):
    self.Z = []
    self.Z.append(_inputs)
    for i in range(len(self.unit) - 1):
        u = self.U(self.Z[i], self.W[i], self.B[i])
        if(i != len(self.unit) - 2):
            z = np.tanh(u)
        else:
            z = self.F.softmax(u)
        self.Z.append(z)
    return np.argmax(z, axis=1)

# ユニットへの総入力を返す関数
def U(self, x, w, b):
        return np.dot(x, w) + b

forwardメソッドの返り値だが、
出力層の各ユニットの出力値の中で最も高い値を出力したユニットの番号、つまり入力値の属するクラスの予測値である。
f:id:okuya-KAZAN:20170523184335p:plain

誤差の算出

ニューラルネットワークの出力値と目標出力値(label)から誤差を算出する。
誤差関数には交差エントロピーを用いている。
※出力値はforwardメソッド内で算出された配列Zの中の1番末尾にある配列であり、forwardメソッドの返り値ではない。

# 誤差の計算
def calc_loss(self, label):
    error = np.sum(label * np.log(self.Z[-1]), axis=1)
    return -np.mean(error)

誤差逆伝搬

今回のプログラムの肝となる誤差逆伝搬法によるパラメータの学習を行う部分。

# 誤差逆伝搬
def backPropagate(self, _label, eta, M):
    # calculate output_delta and error terms
    W_grad = []
    B_grad = []
    for i in range(len(self.W)):
        w_grad = np.zeros_like(self.W[i])
        W_grad.append(w_grad)
        b_grad = np.zeros_like(self.W[i])
        B_grad.append(b_grad)

    output = True

    delta = np.zeros_like(self.Z[-1])
    for i in range(len(self.W)):
        delta = self.calc_delta(
            delta, self.W[-(i)], self.Z[-(i + 1)], _label, output)
        W_grad[-(i + 1)], B_grad[-(i + 1)] = self.calc_grad(self.W[-(i + 1)], self.B[-(i + 1)], self.Z[-(i + 2)], delta)

        output = False

    # パラメータのチューニング
    for i in range(len(self.W)):
        self.W[i] = self.W[i] - eta * W_grad[i] + M * self.dW[i]
        self.B[i] = self.B[i] - eta * B_grad[i]
        # モメンタムの計算
        self.dW[i] = -eta * W_grad[i] + M * self.dW[i]

# デルタの計算
def calc_delta(self, delta_dash, w, z, label, output):
    # delta_dash : 1つ先の層のデルタ
    # w : pre_deltaとdeltaを繋ぐネットの重み
    # z : wへ向かう出力
    if(output):
        delta = z - label
    else:
        delta = np.dot(delta_dash, w.T) * self.F.dtanh(z)
    return delta

デルタの計算方法は出力層とそれ以外の層では異なるため、bool変数であるoutputを定義し、その値によってデルタの算出方法を変えている。
変数Mはモメンタムでの加算の割合を制御するハイパーパラメータ。

学習

これまでに定義したメソッドを駆使し、ニューラルネットワークの出力が目標出力(label)に近づいていくようパラメータをチューニングしていく。

def train(self, dataset, N, iterations=1000, minibatch=4, eta=0.5, M=0.1):
    print "-----Train-----"
    # 入力データ
    inputs = dataset[:, :self.unit[0]]
    # 訓練データ
    label = dataset[:, self.unit[0]:]

    errors = []

    for val in range(iterations):
        minibatch_errors = []
        for index in range(0, N, minibatch):
            _inputs = inputs[index: index + minibatch]
            _label = label[index: index + minibatch]
            self.forward(_inputs)
            self.backPropagate(_label, eta, M)

            loss = self.calc_loss(_label)
            minibatch_errors.append(loss)
        En = sum(minibatch_errors) / len(minibatch_errors)
        print "epoch", val + 1, " : Loss = ", En
        errors.append(En)
    print "\n"
    errors = np.asarray(errors)
    plt.plot(errors)

うーん...なんかコードが汚い...


実際に3000回学習させた時の誤差の変動を示したグラフがこち
f:id:okuya-KAZAN:20170523190031p:plain

結果

学習させたパラメータを引数に取るdraw_testメソッドを定義

def draw_test(self, x, label, W, B):
    self.W = W
    self.B = B
    x1_max = max(x[:, 0]) + 0.5
    x2_max = max(x[:, 1]) + 0.5
    x1_min = min(x[:, 0]) - 0.5
    x2_min = min(x[:, 1]) - 0.5
    xx, yy = np.meshgrid(np.arange(x1_min, x1_max, 0.01),
                         np.arange(x2_min, x2_max, 0.01))
    Z = self.forward(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)

    plt.contourf(xx, yy, Z, cmap=plt.cm.jet)
    plt.scatter(x[:, 0], x[:, 1], c=label, cmap=plt.cm.jet)
    plt.show()

f:id:okuya-KAZAN:20170523164550p:plain
f:id:okuya-KAZAN:20170523190637p:plain
認識できてる...!!

2クラス分類

scikit-learnmake_moonsという機能を使って2クラスのデータも作成し、
クラスの分類を行うニューラルネットワークを構築し学習させた。
層は入力層、隠れ層、出力層の3つで、各層のユニット数は(2,3,2)とした。
f:id:okuya-KAZAN:20170523191231p:plain
f:id:okuya-KAZAN:20170523191243p:plain


ちなみに、隠れ層のユニット数を20にした場合
f:id:okuya-KAZAN:20170523191511p:plain
多少オーバーフィットを起こしている恐れがある。