"BOKU"のITな日常

62歳・文系システムエンジニアの”BOKU”は日々勉強を楽しんでます

ディープラーニング(ニューラルネットワーク)の「学習」の肝は「微分」らしいよ。

今回は、ディープラーニングの「学習」の肝になるのは「微分」なんだということを、numpyだけで簡単なニューラルネットワークの学習をするプログラムを書いて確かめてみようかと思います。

f:id:arakan_no_boku:20200109220616p:plain

 

学習=誤差を小さくなるようにパラメータを更新する

 

ディープラーニングの流れのざっくりした下図で、太字の赤線にしている部分が「学習する」の部分です。

f:id:arakan_no_boku:20200115231052p:plain

前回のSOFTMAX→損失関数で計算した「誤差(ロス率)」を計算するところにフォーカスをあててみました。

arakan-pgm-ai.hatenablog.com

そこでも書いてますが、損失関数が出力する誤差って

でも共通していることは。

  • 正解に近いほど誤差として出力される結果は小さくなる。
  • 正解から遠いほど誤差として出力される結果は大きくなる。

ということです。

・・なわけですから、結局、学習の目指すところはそれをゼロに近づけていくことなわけです。

その手段が「重み」とか「バイアス」などと呼ばれるパラメータの変更なのは、変更可能なものがそれしかないことを考えれば・・まあ、当然ですね。

ということで。

上の図をもっと乱暴にデフォルメしてみます。

結局、こんな感じのことをグルグルと繰り返して、正解との誤差率(ロス率)をゼロに近づけていく行為が「学習」というもの・・みたいです。

f:id:arakan_no_boku:20200117195730p:plain

 

問題は「どれくらい更新するか」なんだな

 

誤差が小さくなる方向にパラメータを更新する。

口で言えば簡単ですが、どのくらい、どっちの方向(プラスかマイナスか)に変更するかをどうやって決めるのか?

これなのですが。

いろいろ本を読んだ感じでは、どうやら「微分」して求めるみたいです。

微分とは「変数の微小な変化に対応する、関数の変化の割合の極限(=微分係数)を求めること。」などと説明されますが。

正直。

自分も学校で習った時にはちんぷんかんぷんでした。

だけど、プログラムで書くと多少はわかりやすくなるので、ここはPythonの疑似プログラムで書いてみます。

def bibun(fx):
    h = 1e-4
    y1 = f(x + h)
    y2 = f(x - h)
    return (y1 - y2) / (2 * h)

 こんな感じですね。

ちょっとだけ変化させてその差をとって計算するので、hに極めて小さな数(1e-4=1.0 × 0.0001)の値を足し引きして、関数(f)で値をもとめて、差をとることで極限をもとめる・・、というわけです。

で・・。

$$y = x^3$$

Pythonで書くと

def function1(x):
    return x**3

y = function1(x)

みたいになるので、この式の微分を求める式は

yy = bibun(function1, x)

 になります。

ためしに、x=10でやってみると、1000(10の3乗)で、微分の結果が300(3×10の2乗・・微分の公式)になりますから・・、あってます。

微分は、よくグラフなんかで傾きとか勾配とか呼ばれます。

同じように、ディープラーニングの学習でも「どこくらい増減させるか」を決めるのに、微分の計算値を使っているということらしいです。

 

微分を行列でやってみるとイメージがわく

 

実際には上記みたいに、ひとつだけの値を求めるということはあまりなくて、INPUTの「x」は行列データになり、微分のプログラムもこんな感じになります。

import numpy as np

def bibun_np(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]
        fxh1 = f(float(tmp_val) + h)  # f(x+h)
        fxh2 = f(tmp_val - h)  # f(x-h)
        grad[idx] = (fxh1 - fxh2) / (2 * h)
        it.iternext()
    return grad

行列を扱いやすいように「numpy」を使ってます。

ループをすっきり書こうとして「np.nditer」なんかを使っているのと、行列データをひとつずつ取り出して微分計算をしているので複雑に見えますけど、基本やっていることは上記のひとつの値の場合と一緒です。

ためしに、インプットを行列でやってみると。

def function1(x):
    return x**3


x = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0])
grad = []
grad = bibun_np(function1, x)
print(grad)

結果は

[ 3.00000001 12.00000001 27.00000001 48.00000001 75.00000001
108.00000001 147.00000001 192.00000001 243.00000001 300.00000001]

みたいに行列ででてきます。

ふむふむ。

 

ディープラーニングの勾配も考え方は同じ??

 

実は。

ディープラーニングの学習でいう「勾配」も、つまるところ、さっき計算したみたいな「微分した結果の集まりのこと」らしいです。

ということは。

上記の「bibun_np」の引数の「f」の部分・・つまり「f(x) =関数」の部分に、ニューラルネットワークのモデル部分である「入力データに対して計算を適用してロス率をもとめる関数」をはめ込めば、ディープラーニングのパラメータを更新するために必要な「勾配」を求めることができる。

ということではないかと思いました。

でも。

本とかを読んでも、ディープラーニングの勾配を求めるには「誤差逆伝播法 またはバックプロパゲーション( Backpropagation)」という、もっと難しいアルゴリズムを使うと書いてあります。

例えば、このサイトとかでもそうです。

fresopiya.com

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

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

後ろの方には

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

とか

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

などと書かれています。

つまり。

たぶん、普通に微分計算(算術微分)してもいいんだけど、それだと計算効率が悪くて恐ろしく遅いので使い物にならないから、効率的かつ高速に微分の計算ができるように工夫した方法が「誤差逆伝播法」なんだということですかね。

 

試しにnumpy+算術微分だけで学習させてみよう

 

 勾配が計算できたら、後は学習率をかけて引いていくだけです。

疑似コードで書くとこんな感じ。

param -= learning_rate * grad

これでパーツがそろったので、一度、tensorflowなどのフレームワークを使わず、かつ、誤差逆伝播法(バックブロゲーション)も使わずに、普通の微分を使った簡単なニューラルネットワークの学習をやってみようと思います。

これで、ちゃんと学習できるなら「学習の肝は微分らしい」というのが、納得できますからね。

まず。

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%正解)までいってます。

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

まあ。

こんなものは、学習用というか頭の整理用以外の何の役にも立ちません。

だけど。

誤差逆伝播法を使わなくても、まがりなりにも学習ができた。

この結果は面白いです。

やっぱ根本のところは「微分」なんだな・・と納得もできました。

良い感じです。

今回はこんなところで。

ではでは。