sein's blog

ディープラーニング, 人工知能, 自然言語処理 ~ Deep Learning, AI, NLP ~

数式なしでDeep Learning入門1 多層パーセプトロン

はじめに

対象者

この記事はDeep Learningの基礎を勉強してみたが、すぐに挫折したという方が対象です。

前提条件

Python3とNumpyが使える

注意事項

ここで紹介しているニューラルネットワークは何かと簡略化されています。ちゃんと勉強したい人は他のサイトも参考にしてください。一般的でない事項を列挙します。

  • パーセプトロンのバイアスと活性化関数について言及していない
  • 出力層にSoftmaxなどの活性化関数を使用していない
  • 評価関数 (誤差関数)について言及がない
  • 確率的勾配降下法について説明がない

理論

多層パーセプトロン (MLP: Multi Layer Perceptron)

f:id:kanemura:20170818120510p:plain

まず、パーセプトロンの説明から。パーセプトロンは入力に重みをかけた値を全部足します。足した値が0より大きいとその値を出力します。そうでない場合は0を出力します。これをPythonで表現すると

import numpy as np
x = np.array([0, 1, 1]).astype(np.float32)
w = np.random.rand(3, 3).astype(np.float32)
y = np.maximum(0, np.dot(w, x))

f:id:kanemura:20170818121344p:plain

パーセプトロンを複数用意して各層をつなぎます。これが多層パーセプトロンです。パーセプトロン神経細胞の振る舞いを単純化していて「ニューロン」とも呼ばれます。以下では単にニューロンと呼びます。

ニューロンの振る舞いを「投票」のようにも解釈することができます。入力を全部足して出力を決定するからです。ただし入力に重みをかけるため、「投票」される一票は平等ではありません。入力の大きさを「声の大きさ」、一票の重みを「信頼度」と解釈してみましょう。ニューロンはつながっている他のニューロンの声の大きさに信頼度を上乗せして、自身の声の大きさを決めます。

以下では、入力の大きさを「声」、重みを「信頼」と置き換えて説明していきます。

学習方法: 誤差逆伝播法 (Back Propagation)

次に学習についてです。学習とは「信頼」を調整することです。正解との「ずれ」があるとそのぶん「信頼」を「上げたり」「下げたり」します。われわれ人間社会でも同じことを日常的にしているかもしれません。

f:id:kanemura:20170818121605p:plain

「信頼」の調整は、正解との「ずれ」すなわち出力層の誤差をもとに行われます。出力層から入力層に向けて誤差が伝わるので、誤差逆伝播という名前がついています。

出力層のあるニューロンの正解が1とします。実際の計算結果が0.3とします。誤差は0.7となります。このニューロンはもっと大きく反応すべきです。そのためには「信頼」を大きくする必要があります。ではどの程度「信頼」を修正すれば良いのでしょうか?まず、誤差が大きいと修正量もまた大きくなります。さらに「信頼」が大きいほど修正量も大きくなります。

f:id:kanemura:20170821111452p:plain

Pythonで実装します。正解をans、出力層の誤差をe_out、隠れ層の誤差をe_hidとします。

ans = np.array([1, 0, 0]).astype(np.float32)
e_out = ans - y_out
e_hid = np.dot(e_out, w_out) * (y_hid > 0)

y_hidとy_outはそれぞれ隠れ層と出力層の出力です。

前層の影響は「信頼」と「声」で決まりました。そこで、「信頼」が大きいものほど誤差に寄与したと考えます。np.dot(w_out.T, e_out)がこれに相当します。ただし、「声」が0の場合は誤差に貢献していなので、(y_hid > 0)をかけています。

続いて「声」の影響を考慮します。単純に誤差に「声」をかけるだけです。これで入ってきた「声」に応じて修正量が大きくなります。出力層と隠れ層の「信頼」の変化量をそれぞれdw_outとdw_hidとします。また、「声」をinpとします。

dw_out += np.outer(e_out, y_hid)
dw_hid += np.outer(e_hid, inp)

dw_outとdw_hidの初期値はどちらも0です。

学習は「信頼」を少しづつ変化させることで進めます。

w_out += dw_out * 0.01
w_hid += dw_hid * 0.01

0.01を学習率と呼びます。

学習は繰り返し行われます。全種類の「声」をネットワークに見せることを1 epochと呼びます。20回なら20 epochです。先人の知恵でepochごとの重み変化に0.9をかけて足すことで、学習が上手くいくことがわかっています。そこで、1 epochごとに

dw_out = dw_out*0.9
dw_hid = dw_hid*0.9

とします。0.9をモーメンタムと呼びます。

バッチ学習

ニューラルネットワークを学習させるには学習用のデータとテスト用のデータを必ず分ける必要があります。テスト用のデータを用いて学習することは決して行ってはいけません。ネットワークの性能をテスト用のデータを用いて計ります。

バッチ学習とは学習用のデータをある程度まとめてネットワークに「みせる」(入力する)ことです。100個程度まとめることが一般的なようです。手書き数字による実装例では学習用のデータを100個みせて重みを更新することを繰り返します。

実装例

MNIST

MNISTは28x28ピクセルの0~9の手書きデータです。中には「a」にしか見えない「2」があったりします。入力層のニューロン数は28x28=784となります。MNISTを使用するにはsklearnをインストールします。

sklearn Installation

$ pip3 install sklearn

sklearn Download

$ python3
>>> from sklearn.datasets import fetch_mldata
>>> mnist = fetch_mldata('MNIST original')

初めての場合、ダウンロードが始まるので結構時間がかかります。ホームディレクトリのscikit_learn_data/mldata/mnist-original.matがデータの実体です。

mlp.py

いよいよ実装です。python3用のスクリプトです。入力層は784個でした。隠れ層はとりあえず64個とします。出力層は0から9の数字を識別するので10個とします。

正解の配列をあらかじめ作っておく方式に変更しました。

import numpy as np
from sklearn.datasets import fetch_mldata

# ネットワーク定数
n_inp = 784
n_hid = 64
n_out = 10

# 学習定数
batch = 100
learn_rate = 0.01

# 重み
w_hid = np.random.rand(n_hid, n_inp).astype(np.float32)/n_inp
w_out = np.random.rand(n_out, n_hid).astype(np.float32)/n_hid
dw_hid = np.zeros(w_hid.shape).astype(np.float32)
dw_out = np.zeros(w_out.shape).astype(np.float32)

# MNIST
n_train = 60000
mnist = fetch_mldata('MNIST original')
data = mnist.data.astype(np.float32)
target = mnist.target.astype(np.int32)
x_train, x_test = np.split(data, [n_train])
d_train, d_test = np.split(target, [n_train])
n_test = d_test.size

# 入力値を0~1に正規化
x_train /= 255
x_test /= 255

# 正解配列
ans = []
for i in range(n_out):
    x = np.zeros(n_out).astype(np.float32)
    x[i] = 1
    ans.append(x)

# 各層の出力を計算
def output(x):
    y_hid = np.maximum(0, np.dot(w_hid, x))
    y_out = np.maximum(0, np.dot(w_out, y_hid))
    return y_hid, y_out

# 学習していない数字を正しく識別できるか予想
def predict():
    score = 0
    for i in range(0, n_test):
        inp = x_test[i]
        y_hid, y_out = output(inp)
        if d_test[i] == np.argmax(y_out):
            score += 1
    print('test: {0}%'.format(score / 100))

# 学習用の数字で重みを更新
for epoch in range(1, 11):
    print('epoch: {0}'.format(epoch))
    # ランダムな順序に学習
    perm = np.random.permutation(n_train)
    # 100個単位で学習
    for i in range(0, n_train, batch):
        x_batch = x_train[perm[i:i+batch]]
        d_batch = d_train[perm[i:i+batch]]
        # 学習データを100個みせる
        for j in range(batch):
            inp = x_batch[j]
            y_hid, y_out = output(inp)
            # 誤差計算
            e_out = ans[d_batch[j]] - y_out
            e_hid = np.dot(e_out, w_out)*(y_hid > 0)
            dw_out += np.outer(e_out, y_hid)
            dw_hid += np.outer(e_hid, inp)
        # 100個みせたので重み更新
        w_out += dw_out * learn_rate / batch
        w_hid += dw_hid * learn_rate / batch
        dw_out *= 0.9
        dw_hid *= 0.9
        # 学習が進んでいるか1000個ごとに識別予想
        if (i%1000) == 0:
            predict()

実行結果

$ python3 mlp.py
epoch: 1
test: 10.28%
test: 10.32%
test: 10.32%
test: 10.66%
test: 26.44%
test: 35.07%
test: 44.5%
test: 58.98%
test: 64.65%
...
epoch: 10
test: 97.03%
test: 96.96%
test: 97.03%
test: 97.06%

学習が進むにつれ、正解率が上がっていきます。10 epoch程度で97%の正解率が得られました。

さいごに

mlp.pyにはいくつかパラメーターがあります。色々いじってみると理解が深まります。

  • n_hid: 隠れ層のニューロン数を16とか128にしてみる
  • learn_rate: 学習率を0.1とか0.001にしてみる
  • 重みを初期化する際に入力数で割るのではなく、np.sqrt(入力数)で割ってみる

冒頭で申し上げた通り、ここで紹介したニューラルネットワークはテキトーです。なんとなくわかった気になったら、他のサイトや書籍できちんと勉強してください。この記事がディープラーニング入門の手助けになれば幸いです。

おすすめ書籍

ニューラルネットワーク自作入門

ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装

イラストで学ぶ ディープラーニング (KS情報科学専門書)

深層学習 (機械学習プロフェッショナルシリーズ)