機械学習
DeepLearning
ディープラーニング
ニューラルネットワーク
深層学習

ディープラーニングを実装から学ぶ~ (まとめ1)実装は、実は簡単

MNISTの予測をディープラーニング(ニューラルネットワーク)で行います。実は、ディープラーニング(ニューラルネットワーク)の実装は簡単です。数十ステップで98%程度の精度を達成できます。
(注意事項)
ディープラーニング(ニューラルネットワーク)の学習方法を理解すること目的としたプログラムです。MNISTデータ程度のデータを想定しています。大量のデータやデータによっては、この実装だけは対応できません。。あくまでもディープラーニング(ニューラルネットワーク)の基本を理解するという視点でご覧ください。

ニューラルネットワーク

以下のような図を見たことがありますか?脳を模倣したニューラルネットワークです。

NN1.png

緑枠が脳細胞を表すニューロンです。ニューロンとニューロンの間はシナプス(青の◆)で結合しています。シナプスでのデータの受け渡し度合いを重み($ w $)で表します。ニューロン間の結合の度合い、太さとも言えます。
図の関係で、中間層の階層数は2階層としています。この階層数をいくらでも深くできることからディープラーニング(深層学習)と言われます。また、各層のノード数(ニューロンの数)も図では2~3個にしていますが、実際にはもっと多くのノード数となります。

学習

ニューラルネットワークを通して学習を行います。学習は、正解データを持ったデータで行います。

NN2.png

入力層から入ったデータをもとに各層を通じて予測データを求めます。次に、正解データとの誤差を計算します。最後に、誤差をもとに誤差がより小さくなるように重みの調整を行います。

実装

実装は、pythonで行います。行列演算、数値計算を行うことが可能なnumpyを用います。

affine変換

シナプスによる結合部分の計算になります。図で青枠の部分です。

NN3.png

$ u_1^{(1)} $の計算は、以下の式で表せます。

u_1^{(1)} = w_{11}^{(1)}x_1 + w_{12}^{(1)}x_2 + w_{13}^{(1)}x_3 + b_1^{(1)}

入力層と中間層(1)の間を行列で表すと以下になります。

\begin{pmatrix}
u_1^{(1)}\\
u_2^{(1)}
\end{pmatrix}
=
\begin{pmatrix}
w_{11}^{(1)} & w_{12}^{(1)} & w_{13}^{(1)}\\
w_{21}^{(1)} & w_{22}^{(1)} & w_{23}^{(1)}
\end{pmatrix}
\begin{pmatrix}
x_1\\
x_2\\
x_3
\end{pmatrix}
+
\begin{pmatrix}
b_1^{(1)}\\
b_2^{(1)}
\end{pmatrix}

行列の内積(ドット積)で表せます。numpyでの実装は以下になります。

def affine(z, W, b):
    return np.dot(z, W) + b

活性化関数(中間層)

活性化関数は、ニューロン内での発火を表します。図の青枠の部分です。

NN4.png

最近は、活性化関数としてReLUが用いられることが多いようです。
ReLUは、以下の関数です。入力が負になれば以降に情報は伝えません。正の場合は、そのままの値を伝えます。

f_{ReLU}(u) = 
\left\{
\begin{array}{ll}
u & (u \gt 0) \\
0 & (u \le 0)
\end{array}
\right.

ReLUのグラフです。単純です。

NN_ReLU.png

実装は、以下になります。

def relu(u):
    return np.maximum(0, u)

活性化関数(出力層)

MNISTのように0~9の数値に分類する分類問題の場合は、出力層の活性化関数としてsoftmax関数が利用されます。これは、各分類ごとの確率を求めるためです。総和は、1になります。

NN5.png

softmaxの式です。

f(u_i) = \frac{\exp(u_i)}{\sum_{k=1}^{n}\exp(u_k)}

実装は、以下になります。

def softmax(u):
    max_u = np.max(u, axis=1, keepdims=True)
    exp_u = np.exp(u-max_u)
    return exp_u/np.sum(exp_u, axis=1, keepdims=True)

誤差関数

予測データと正解データの比較に誤差関数を用います。分類問題の場合は、交差エントロピー誤差を利用します。

NN6.png

交差エントロピー誤差の式です。

E= -\sum_kt_k\log{y_k}

実装は以下です。

def cross_entropy_error(y, t):
    return -np.sum(t * np.log(np.maximum(y,1e-7)))/y.shape[0]

1e-7は、0のlogを取らないようにするための定数です。

重みの調整

誤差を小さくするように重みを調整します。
調整には、微分を利用します。誤差関数に対して微分することで傾き(勾配)を求めます。青の線を誤差関数とします。下の図のように、オレンジの場合は、勾配は正となり、値を小さくすることで誤差を小さくできます。逆に緑のように勾配が負となった場合は、値を大きくすることで誤差を小さくできます。

NN_diff.png

微分値(勾配)が、正となった場合は、重みを小さくするように調整します。負となった場合は、重みを大きくするように調整します。

最終的に、誤差Eに対する重みの微分を求めます。正式には偏微分を用います。
細かい説明は省略しますが、合成微分を用いて各重みの偏微分は以下のように表せます。

\begin{align}
\frac{\partial E}{\partial w^{(3)}} &= 
\frac{\partial E}{\partial y}
\frac{\partial y}{\partial u^{(3)}}
\frac{\partial u^{(3)}}{\partial w^{(3)}}
\\
\frac{\partial E}{\partial w^{(2)}} &= 
\frac{\partial E}{\partial y}
\frac{\partial y}{\partial u^{(3)}}
\frac{\partial u^{(3)}}{\partial z^{(2)}}
\frac{\partial z^{(2)}}{\partial u^{(2)}}
\frac{\partial u^{(2)}}{\partial w^{(2)}}
\\
\frac{\partial E}{\partial w^{(1)}} &= 
\frac{\partial E}{\partial y}
\frac{\partial y}{\partial u^{(3)}}
\frac{\partial u^{(3)}}{\partial z^{(2)}}
\frac{\partial z^{(2)}}{\partial u^{(2)}}
\frac{\partial u^{(2)}}{\partial z^{(1)}}
\frac{\partial z^{(1)}}{\partial u^{(1)}}
\frac{\partial u^{(1)}}{\partial w^{(1)}}
\end{align}

難しそうに見えますが、逆から順番に微分値を掛けていくだけです。
各層の勾配を見ていきましょう。

誤差関数、活性化関数(出力層)の勾配

交差エントロピー誤差およびsoftmax関数の式は、以下でした。

E= -\sum_kt_k\log{y_k}
f(u_i) = \frac{\exp(u_i)}{\sum_{k=1}^{n}\exp(u_k)}

交差エントロピー+softmaxの勾配は、以下で表せます。

\frac{\partial E}{\partial u_i^{(3)}} = y_i - t_i

ここでは、微分を求めることが目的ではありませんので、詳細は省略します。詳しく知りたい方は、以下に書いておりますので参照してみてください。
ディープラーニングを実装から学ぶ(4-2)学習(誤差逆伝播法2)

実装は、以下となります。

def softmax_cross_entropy_error_back(y, t):
    return (y - t)/y.shape[0]

複数データを同時に処理することを想定していますので、1データ当たりとするよう調整を入れています。

活性化関数(中間層)の勾配

ReLU関数は、以下でした。

f_{ReLU}(u) = 
\left\{
\begin{array}{ll}
u & (u \gt 0) \\
0 & (u \le 0)
\end{array}
\right.

こちらの微分は簡単ですね。

f_{ReLU}^{'}(u) = 
\left\{
\begin{array}{ll}
1 & (u \gt 0) \\
0 & (u \le 0)
\end{array}
\right.

実装です。

def relu_back(dz, u):
    return dz * np.where(u > 0, 1, 0)

前の勾配、dzを掛けています。

Affine変換の勾配

例えば、$ u_1^{(3)} $は、以下の式となります。

u_1^{(3)} = w_{11}^{(3)}z_1^{(2)} + w_{12}^{(3)}z_2^{(2)} + w_{13}^{(3)}z_3^{(2)} + b_1^{(3)}

それぞれ偏微分してみます。偏微分を行う変数以外は定数とみなします。

\begin{align}
\frac{\partial u_1^{(3)}}{\partial z_1^{(2)}} &= w_{11}^{(3)}\\
\frac{\partial u_1^{(3)}}{\partial w_{11}^{(3)}} &= z_1^{(2)}\\
\frac{\partial u_1^{(3)}}{\partial b_1^{(3)}} &= 1
\end{align}

一般的には、$ u $を$ z $で偏微分すると$ z $に掛けた重みに、重みで偏微分すると重みに掛けた$ z $に、バイアスは、1ですね。
実装は、以下です。

def affine_back(du, z, W, b):
    dz = np.dot(du, W.T)
    dW = np.dot(z.T, du)
    db = np.dot(np.ones(z.shape[0]).T, du)
    return dz, dW, db

それぞれ$ u $の勾配を掛けることで、それ以前の勾配を掛けることになります。

重み調整

最後に、求めた勾配から重みを調整します。求めた勾配に、学習率lrを掛けて引きます。

\begin{align}
w^{(1)} &= w^{(1)} - lr * \frac{\partial E}{\partial w^{(1)}}\\
w^{(2)} &= w^{(2)} - lr * \frac{\partial E}{\partial w^{(2)}}\\
w^{(3)} &= w^{(3)} - lr * \frac{\partial E}{\partial w^{(3)}}
\end{align}

勾配が正であれば、値を小さくします。勾配が負であれば、値を大きくします。

学習全体

全体の流れをもう一度示します。

NN7.png

入力データから予測データを求めるまでを順伝播と言います。
中間層の2階層の実装は、以下です。

    # 順伝播
    u1 = affine(x, W1, b1)
    z1 = relu(u1)
    u2 = affine(z1, W2, b2)
    z2 = relu(u2)
    u3 = affine(z2, W3, b3)
    y  = softmax(u3)

逆に勾配の計算を求め、重みの更新を行う部分を逆伝播と言います。

    # 逆伝播
    dy = softmax_cross_entropy_error_back(y, t)
    dz2, dW3, db3 = affine_back(dy, z2, W3, b3)
    du2 = relu_back(dz2, u2)
    dz1, dW2, db2 = affine_back(du2, z1, W2, b2)
    du1 = relu_back(dz1, u1)
    dx, dW1, db1 = affine_back(du1, x, W1, b1)

重みの更新です。

    # 重み、バイアスの更新
    W1 = W1 - lr * dW1
    b1 = b1 - lr * db1
    W2 = W2 - lr * dW2
    b2 = b2 - lr * db2
    W3 = W3 - lr * dW3
    b3 = b3 - lr * db3

今回は、分かりやすくするため2階層固定としました。多階層とするには、W,b,u,zなどを配列とし、for分でループさせると任意の階層に対応できます。

プログラム全体

今回作成したプログラムの全体です。

関数部分

import numpy as np
# affine変換
def affine(z, W, b):
    return np.dot(z, W) + b
# affine変換勾配
def affine_back(du, z, W, b):
    dz = np.dot(du, W.T)
    dW = np.dot(z.T, du)
    db = np.dot(np.ones(z.shape[0]).T, du)
    return dz, dW, db
# 活性化関数(ReLU)
def relu(u):
    return np.maximum(0, u)
# 活性化関数(ReLU)勾配
def relu_back(dz, u):
    return dz * np.where(u > 0, 1, 0)
# 活性化関数(softmax)
def softmax(u):
    max_u = np.max(u, axis=1, keepdims=True)
    exp_u = np.exp(u-max_u)
    return exp_u/np.sum(exp_u, axis=1, keepdims=True)
# 誤差(交差エントロピー)
def cross_entropy_error(y, t):
    return -np.sum(t * np.log(np.maximum(y,1e-7)))/y.shape[0]
# 誤差(交差エントロピー)+活性化関数(softmax)勾配
def softmax_cross_entropy_error_back(y, t):
    return (y - t)/y.shape[0]

学習部分

def learn(x, t, W1, b1, W2, b2, W3, b3, lr):
    # 順伝播
    u1 = affine(x, W1, b1)
    z1 = relu(u1)
    u2 = affine(z1, W2, b2)
    z2 = relu(u2)
    u3 = affine(z2, W3, b3)
    y  = softmax(u3)
    # 逆伝播
    dy = softmax_cross_entropy_error_back(y, t)
    dz2, dW3, db3 = affine_back(dy, z2, W3, b3)
    du2 = relu_back(dz2, u2)
    dz1, dW2, db2 = affine_back(du2, z1, W2, b2)
    du1 = relu_back(dz1, u1)
    dx, dW1, db1 = affine_back(du1, x, W1, b1)
    # 重み、バイアスの更新
    W1 = W1 - lr * dW1
    b1 = b1 - lr * db1
    W2 = W2 - lr * dW2
    b2 = b2 - lr * db2
    W3 = W3 - lr * dW3
    b3 = b3 - lr * db3

    return W1, b1, W2, b2, W3, b3

たったこれだけです。
これで、MNISTで約98%の正解率まで学習が可能です。

予測は、以下で行います。

def predict(x, W1, b1, W2, b2, W3, b3):
    # 順伝播
    u1 = affine(x, W1, b1)
    z1 = relu(u1)
    u2 = affine(z1, W2, b2)
    z2 = relu(u2)
    u3 = affine(z2, W3, b3)
    y  = softmax(u3)
    return y

学習の実行

実行プログラム

実際に、MNISTで98%の正解率となるか確認するためのプログラムです。
今回の目的は、学習プログラムを作成することでしたので、詳細な説明はしません。(今後、ハイパーパラメータについてまとめたいと考えています。)
実行は、中間層2層、1階層目のノード数100、2階層目のノード数50で実行します。学習は、100データごとに、50回(50エポック)行います。

データは、MNISTのページからダウンロードしてください。

  • train-images-idx3-ubyte.gz: 学習用画像
  • train-labels-idx1-ubyte.gz: 学習用正解ラベル
  • t10k-images-idx3-ubyte.gz: テスト用画像
  • t10k-labels-idx1-ubyte.gz: テスト用正解ラベル

データは、c:\mnist\に格納することを想定しています。別のフォルダに格納した場合は、load_mnistのパラメータを変更してください。

import gzip
import numpy as np
# MNIST読み込み
def load_mnist( mnist_path ) :
    return _load_image(mnist_path + 'train-images-idx3-ubyte.gz'), \
           _load_label(mnist_path + 'train-labels-idx1-ubyte.gz'), \
           _load_image(mnist_path + 't10k-images-idx3-ubyte.gz'), \
           _load_label(mnist_path + 't10k-labels-idx1-ubyte.gz')
def _load_image( image_path ) :
    # 画像データの読み込み
    with gzip.open(image_path, 'rb') as f:
        buffer = f.read()
    size = np.frombuffer(buffer, np.dtype('>i4'), 1, offset=4)
    rows = np.frombuffer(buffer, np.dtype('>i4'), 1, offset=8)
    columns = np.frombuffer(buffer, np.dtype('>i4'), 1, offset=12)
    data = np.frombuffer(buffer, np.uint8, offset=16)
    image = np.reshape(data, (size[0], rows[0]*columns[0]))
    image = image.astype(np.float32)
    return image
def _load_label( label_path ) :
    # 正解データ読み込み
    with gzip.open(label_path, 'rb') as f:
        buffer = f.read()
    size = np.frombuffer(buffer, np.dtype('>i4'), 1, offset=4)
    data = np.frombuffer(buffer, np.uint8, offset=8)
    label = np.zeros((size[0], 10))
    for i in range(size[0]):
        label[i, data[i]] = 1
    return label

# 正解率
def accuracy_rate(y, t):
    max_y = np.argmax(y, axis=1)
    max_t = np.argmax(t, axis=1)
    return np.sum(max_y == max_t)/y.shape[0]
# MNISTデータ読み込み
x_train, t_train, x_test, t_test = load_mnist('c:\\mnist\\')

# 入力データの正規化(0~1)
nx_train = x_train/255
nx_test  = x_test/255

# ノード数設定
d0 = nx_train.shape[1]
d1 = 100 # 1層目のノード数
d2 = 50  # 2層目のノード数
d3 = 10
# 重みの初期化(-0.1~0.1の乱数)
np.random.seed(8)
W1 = np.random.rand(d0, d1) * 0.2 - 0.1
W2 = np.random.rand(d1, d2) * 0.2 - 0.1
W3 = np.random.rand(d2, d3) * 0.2 - 0.1
# バイアスの初期化(0)
b1 = np.zeros(d1)
b2 = np.zeros(d2)
b3 = np.zeros(d3)

# 学習率
lr = 0.5
# バッチサイズ
batch_size = 100
# 学習回数
epoch = 50

# 予測(学習データ)
y_train = predict(nx_train, W1, b1, W2, b2, W3, b3)
# 予測(テストデータ)
y_test = predict(nx_test, W1, b1, W2, b2, W3, b3)
# 正解率、誤差表示
train_rate, train_err = accuracy_rate(y_train, t_train), cross_entropy_error(y_train, t_train)
test_rate, test_err = accuracy_rate(y_test, t_test), cross_entropy_error(y_test, t_test)
print("{0:3d} train_rate={1:6.2f}% test_rate={2:6.2f}% train_err={3:8.5f} test_err={4:8.5f}".format((0), train_rate*100, test_rate*100, train_err, test_err))

for i in range(epoch):
    # 学習
    for j in range(0, nx_train.shape[0], batch_size):
        W1, b1, W2, b2, W3, b3 = learn(nx_train[j:j+batch_size], t_train[j:j+batch_size], W1, b1, W2, b2, W3, b3, lr)

    # 予測(学習データ)
    y_train = predict(nx_train, W1, b1, W2, b2, W3, b3)
    # 予測(テストデータ)
    y_test = predict(nx_test, W1, b1, W2, b2, W3, b3)
    # 正解率、誤差表示
    train_rate, train_err = accuracy_rate(y_train, t_train), cross_entropy_error(y_train, t_train)
    test_rate, test_err = accuracy_rate(y_test, t_test), cross_entropy_error(y_test, t_test)
    print("{0:3d} train_rate={1:6.2f}% test_rate={2:6.2f}% train_err={3:8.5f} test_err={4:8.5f}".format((i+1), train_rate*100, test_rate*100, train_err, test_err))

プログラムは、各実行回ごとに、学習データの正解率、テストデータの正解率、学習データの誤差、テストデータの誤差の順に表示しています。

実行結果

実行結果です。

  0 train_rate= 11.67% test_rate= 12.18% train_err= 2.30623 test_err= 2.30601
  1 train_rate= 92.39% test_rate= 92.38% train_err= 0.24541 test_err= 0.24307
  2 train_rate= 96.78% test_rate= 96.34% train_err= 0.10314 test_err= 0.11774
  3 train_rate= 97.67% test_rate= 97.02% train_err= 0.07395 test_err= 0.10026
  4 train_rate= 98.03% test_rate= 97.06% train_err= 0.06057 test_err= 0.09716
  5 train_rate= 98.34% test_rate= 97.18% train_err= 0.05275 test_err= 0.09951
  6 train_rate= 98.65% test_rate= 97.30% train_err= 0.04209 test_err= 0.09596
  7 train_rate= 98.51% test_rate= 96.95% train_err= 0.04488 test_err= 0.10893
  8 train_rate= 98.79% test_rate= 97.39% train_err= 0.03560 test_err= 0.09737
  9 train_rate= 99.01% test_rate= 97.47% train_err= 0.03033 test_err= 0.09725
 10 train_rate= 99.12% test_rate= 97.57% train_err= 0.02574 test_err= 0.09797
 11 train_rate= 99.16% test_rate= 97.47% train_err= 0.02525 test_err= 0.10192
 12 train_rate= 99.38% test_rate= 97.67% train_err= 0.01840 test_err= 0.09298
 13 train_rate= 99.44% test_rate= 97.70% train_err= 0.01623 test_err= 0.10091
 14 train_rate= 99.17% test_rate= 97.52% train_err= 0.02395 test_err= 0.10848
 15 train_rate= 99.53% test_rate= 97.76% train_err= 0.01352 test_err= 0.10865
 16 train_rate= 99.48% test_rate= 97.68% train_err= 0.01517 test_err= 0.10646
 17 train_rate= 99.63% test_rate= 97.84% train_err= 0.01055 test_err= 0.10101
 18 train_rate= 99.19% test_rate= 97.48% train_err= 0.02559 test_err= 0.12495
 19 train_rate= 99.68% test_rate= 97.83% train_err= 0.01083 test_err= 0.10719
 20 train_rate= 99.72% test_rate= 97.87% train_err= 0.00812 test_err= 0.10383
 21 train_rate= 99.17% test_rate= 97.40% train_err= 0.02394 test_err= 0.13422
 22 train_rate= 99.66% test_rate= 97.90% train_err= 0.00967 test_err= 0.11079
 23 train_rate= 99.64% test_rate= 97.65% train_err= 0.01141 test_err= 0.11507
 24 train_rate= 99.84% test_rate= 97.91% train_err= 0.00505 test_err= 0.11271
 25 train_rate= 99.83% test_rate= 97.81% train_err= 0.00559 test_err= 0.11566
 26 train_rate= 99.97% test_rate= 98.06% train_err= 0.00120 test_err= 0.10587
 27 train_rate=100.00% test_rate= 98.16% train_err= 0.00046 test_err= 0.10365
 28 train_rate=100.00% test_rate= 98.14% train_err= 0.00028 test_err= 0.10521
 29 train_rate=100.00% test_rate= 98.16% train_err= 0.00020 test_err= 0.10636
 30 train_rate=100.00% test_rate= 98.14% train_err= 0.00017 test_err= 0.10702
 31 train_rate=100.00% test_rate= 98.16% train_err= 0.00015 test_err= 0.10763
 32 train_rate=100.00% test_rate= 98.17% train_err= 0.00014 test_err= 0.10802
 33 train_rate=100.00% test_rate= 98.19% train_err= 0.00013 test_err= 0.10863
 34 train_rate=100.00% test_rate= 98.18% train_err= 0.00012 test_err= 0.10904
 35 train_rate=100.00% test_rate= 98.17% train_err= 0.00011 test_err= 0.10938
 36 train_rate=100.00% test_rate= 98.18% train_err= 0.00010 test_err= 0.10971
 37 train_rate=100.00% test_rate= 98.19% train_err= 0.00009 test_err= 0.11007
 38 train_rate=100.00% test_rate= 98.18% train_err= 0.00009 test_err= 0.11035
 39 train_rate=100.00% test_rate= 98.19% train_err= 0.00008 test_err= 0.11066
 40 train_rate=100.00% test_rate= 98.19% train_err= 0.00008 test_err= 0.11096
 41 train_rate=100.00% test_rate= 98.19% train_err= 0.00008 test_err= 0.11121
 42 train_rate=100.00% test_rate= 98.20% train_err= 0.00007 test_err= 0.11134
 43 train_rate=100.00% test_rate= 98.20% train_err= 0.00007 test_err= 0.11166
 44 train_rate=100.00% test_rate= 98.20% train_err= 0.00007 test_err= 0.11189
 45 train_rate=100.00% test_rate= 98.20% train_err= 0.00007 test_err= 0.11215
 46 train_rate=100.00% test_rate= 98.19% train_err= 0.00006 test_err= 0.11237
 47 train_rate=100.00% test_rate= 98.20% train_err= 0.00006 test_err= 0.11256
 48 train_rate=100.00% test_rate= 98.20% train_err= 0.00006 test_err= 0.11272
 49 train_rate=100.00% test_rate= 98.22% train_err= 0.00006 test_err= 0.11297
 50 train_rate=100.00% test_rate= 98.22% train_err= 0.00006 test_err= 0.11314

26回目で、テストデータに対する正解率は、98%を超えました。

最後にもう一度言います。信じられないかもしれませんが、たった数十ステップで98%を超える正解率になりました。
別に、1,2などの数字の形状を教えたわけではありません。自分で学習しました。○△×でもアルファベットでもプログラムを変更なしで、そのまま学習できます。また、画像でなくても数値データであれば入力データから予測が可能となります。

最初にも書きましたが、このプログラムは、ディープラーニング(ニューラルネットワーク)を理解するためのものです。大きなデータやデータによっては対応できません。どんなデータでも対応できるわけではありません。その点の理解をお願いします。

改版履歴
2018/6/23 入力データの正規化、重みの初期化を単純化