Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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 入力データの正規化、重みの初期化を単純化

Nezura
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away