"BOKU"のITな日常

BOKUが勉強したり、考えたことを頭の整理を兼ねてまとめてます。

Pythonでニューラルネットワークの学習を算術微分だけでやったらどうなるか確かめる

f:id:arakan_no_boku:20200109220616p:plain

目次

誤差逆伝播法は「微分」だと知った

ディープラニングの学習の肝である誤差逆伝播法またはバックプロパゲーション( Backpropagation)(以後、誤差逆伝播法)についての、ちょっとした実験です。

誤差逆伝播法はディープラーニングで、学習時のウエイトやバイアスを更新する勾配の値を求めるのに使います。

すっごい難しく複雑な技術のような印象をうけるのですが、実はそんなことはなくて、単なる微分なんだけど、普通にやったら遅くて使い物にならないから、微分を高速に計算できる方法として工夫されたものが「誤差逆伝播法」らしい・・というのを、こちらのサイトで知りました。

fresopiya.com

説明をよーく見ると、誤差逆伝播法の説明の前に微分の「連鎖律」を説明されていて。

連鎖律とは、複数の関数によって構成される関数の微分は、それぞれの関数の微分の積によって表すことができるといった性質のことです。

後ろの方には

各重みパラメータに関する損失関数の勾配を、連鎖律を利用して求める

とか

出力から入力側に向かって順に学習パラメータを更新していくことから、誤差逆伝播法と呼ばれ、各パラメータの偏微分の計算を効率化します。

などと書かれています。

整理すると、学習時に各重みパラメータを更新に使う勾配を求める方法は「微分」なんですけど、普通に微分計算(算術微分)すると、計算効率が悪く恐ろしく遅い。

なので、効率的かつ高速に微分計算ができるよう工夫した方法が「誤差逆伝播法」なんだということのようです。

算術微分を使ったミニニューラルネットワークのソース

誤差逆伝播法は、微分を高速に計算できるように工夫した方法である。

じゃあ、算術微分でも学習はできる・・ということです。

かなり遅い・・ということですが、どの程度遅いのかはとても興味があります。

ということで。

誤差逆伝播法を使わず、普通の算術微分で学習できるかやってみます。

numpyだけでニューラルネットワークモデルを作って、算術微分だけを使って学習させてみます。

Pythonのソース全文です。

import numpy as np


# サンプルデータの生成クラス
class Data:
    def __init__(self, size):
        self.rasio = 10
        self.size = size

    def data_create(self, num, padding):
        datasets = []
        labels = []
        for i in range(1, num + 1):
            rt = i % 2
            if (rt == 0):
                datasets.append(np.random.randn(self.size))
            else:
                datasets.append([((i + padding) %
                                  100) / self.rasio for k in range(self.size)])
            labels.append(rt)
        return np.array(datasets), np.array(labels)


# 誤差逆伝播法を使わない「なんちゃってニューラルネットワーク」サンプルクラス
class SampleNet:

    def __init__(
            self,
            input_size,
            hidden_size,
            output_size,
            weight_init_std=0.01):
        # 重みとバイアスの初期化
        self.params = {}
        self.params['W1'] = weight_init_std * \
            np.random.randn(input_size, hidden_size)
        self.params['b1'] = np.zeros(hidden_size)
        self.params['W2'] = weight_init_std * \
            np.random.randn(hidden_size, output_size)
        self.params['b2'] = np.zeros(output_size)

    # 順方向に計算処理を行って、ラベル毎の確率を求める
    def predict(self, x):
        W1 = self.params['W1']
        W2 = self.params['W2']
        b1 = self.params['b1']
        b2 = self.params['b2']
        # Affine層
        a1 = np.dot(x, W1) + b1
        # Sigmoid層
        z1 = 1 / (1 + np.exp(a1))
        # Affine層
        a2 = np.dot(z1, W2) + b2
        # Softmax層
        a2t = a2.T
        a2t = a2t - np.max(a2t, axis=0)
        yy = np.exp(a2t) / np.sum(np.exp(a2t), axis=0)
        y = yy.T
        return y

    # x:入力データ, t:教師データでロス率を求める
    def loss(self, x, t):
        y = self.predict(x)
        batch_size = y.shape[0]
        # クロスエントロピーです。
        return -np.sum(np.log(y[np.arange(batch_size), t])) / batch_size

    # 正解率を求める
    def accuracy(self, x, t):
        y = self.predict(x)
        y = np.argmax(y, axis=1)
        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy

    # x:入力データ, t:教師データ
    def gradient(self, x, t):
        def loss_W(W):
            return self.loss(x, t)

        grads = {}
        grads['W1'] = self.__bibun_np__(loss_W, self.params['W1'])
        grads['b1'] = self.__bibun_np__(loss_W, self.params['b1'])
        grads['W2'] = self.__bibun_np__(loss_W, self.params['W2'])
        grads['b2'] = self.__bibun_np__(loss_W, self.params['b2'])
        return grads

    # 行列を微分する関数
    def __bibun_np__(self, f, x):
        h = 1e-4  # 0.0001
        grad = np.zeros_like(x)
        it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
        while not it.finished:
            idx = it.multi_index
            tmp_val = x[idx]
            x[idx] = float(tmp_val) + h
            fxh1 = f(x)  # f(x+h)
            x[idx] = float(tmp_val) - h
            fxh2 = f(x)  # f(x-h)
            grad[idx] = (fxh1 - fxh2) / (2 * h)
            x[idx] = tmp_val
            it.iternext()
        return grad


iters_num = 300
# 繰り返しの回数を適宜設定する
test_size = 200
size = 256
# データを生成する。奇数を1000で割った数字G=1、偶数を1000で割ったG=0 としてカテゴライズ。
d = Data(size)
x_train, t_train = d.data_create(iters_num, 0)
x_test, t_test = d.data_create(test_size, size + 1)

# ネットワークを構築する
network = SampleNet(input_size=size, hidden_size=64, output_size=2)
train_size = x_train.shape[0]

# ミニバッチ(一回のループで処理する数)
batch_size = 100
# 学習率
learning_rate = 0.1

train_loss_list = []
train_acc_list = []
test_acc_list = []

# 繰り返しの数の1単位を計算する
iter_per_epoch = max(train_size / batch_size, 1)

for i in range(int(iter_per_epoch * 5)):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]

    # 勾配を算術微分を使って更新する
    grad = network.gradient(x_batch, t_batch)

    # パラメータの更新
    for key in ('W1', 'b1', 'W2', 'b2'):
        network.params[key] -= learning_rate * grad[key]

    # 更新したパラメータで推論を行いロス率を求める
    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)

    # 更新したパラメータでの推論結果を評価する
    train_acc = network.accuracy(x_train, t_train)
    test_acc = network.accuracy(x_test, t_test)
    train_acc_list.append(train_acc)
    test_acc_list.append(test_acc)
    print(
        "loss:" +
        str(loss) +
        "/train acc:" +
        str(train_acc) +
        "/test acc:" +
        str(test_acc))

ちょっと長いですが、ご容赦を。

ミニニューラルネットワークプログラムの補足説明

ニューラルネットワークの各レイヤー(層)は、シンプルに計算式をそのままPyhonの式に展開して書いてます。

  • Affine層(全結合層): np.dot(x, W1) + b1
  • Sigmoid層(活性化関数):1 / (1 + np.exp(a1))
  • SoftMax層:np.exp(a2t) / np.sum(np.exp(a2t), axis=0)
  • 損失関数(クロスエントロピー):-np.sum(np.log(y[np.arange(batch_size), t])) / batch_size

勾配は、誤差逆伝播を使わず、素の微分(__bibun_np__関数)を使って求めてます。

関数名に__がついているのは、クラス内部でのみ使う命名規則に従ってるだけです。

最初は、MNISTなどのありものの画像データを使おうとしたのですが、予想していた以上に遅くて、とても我慢ができないので、今回は、無理くり、小さなデータセットを生成して使うことにしました。

ラベルは「0」と「1」の2つだけです。

片方は

f:id:arakan_no_boku:20200118143632p:plain

もう一方は

f:id:arakan_no_boku:20200118143707p:plain

みたいな感じで、同じ数字ばかりで構成されたデータと、ランダムな数字で構成されたデータという、あからさまに分類しやすい(笑)データにしてみました。

実行してみたら遅いけど学習はできたね

実行してみます。

毎回、データが変わるので結果は変化します。

以下は、ある回のログです。

loss:0.6122528366269887/train acc:0.5/test acc:0.5
loss:0.6171530277844487/train acc:0.85/test acc:0.85
loss:0.5603608447954526/train acc:0.89/test acc:0.89
loss:0.49351229689788956/train acc:0.9133333333333333/test acc:0.89
loss:0.4756502123891248/train acc:0.95/test acc:0.95
loss:0.4287426828536864/train acc:0.94/test acc:0.93
loss:0.377243078578998/train acc:0.97/test acc:0.96
loss:0.35549749153641685/train acc:0.95/test acc:0.95
loss:0.34160940378343/train acc:0.96/test acc:0.95
loss:0.31175165373977165/train acc:0.95/test acc:0.95
loss:0.2643838977401827/train acc:0.96/test acc:0.95
loss:0.2630312131351989/train acc:0.97/test acc:0.96
loss:0.23896073382588487/train acc:0.96/test acc:0.96
loss:0.23168796117264637/train acc:0.97/test acc:0.96
loss:0.19577869153575023/train acc:0.96/test acc:0.96

最初、0.5(まったくでたらめ)の状態から、0.96(96%正解)までいってます。

学習はできていると思って良さげです。

我慢できないくらい遅いですけどね。

さてさて。 

誤差逆伝播法が、実は微分計算を効率的・高速にやっているのに近い・・というのを知ったとき、自分的にはすごく興奮しました。

なので。

それを少しでも共有できれば・・と記事を書いてみました。

今回はこんなところで。

ではでは。