kaeken(嘉永島健司)のTech探究ブログ

主に情報科学/情報技術全般に関する知見をポストします。(最近は、特にData Science、機械学習、深層学習、統計学、Python、数学、ビッグデータ)

2〜4章まとめの確認問題『ゼロから作るDeep Learning』

いったん4章までの復習がてら、2・3・4章末まとめから問題を作成してみる。

2章
・パーセプトロンは、ある◯◯を与えたら、決まった◯◯を行うアルゴリズムである。
・パーセプトロンでは、◯◯と◯◯をパラメータとして設定する。
・パーセプトロンを用いれば、◯◯や◯◯ゲートなどの論理回路を表現できる。
・単層パーセプトロンでは◯◯ゲートを表現できないが、2層パーセプトロンなら表現できる。
・単層パーセプトロンでは◯◯領域だけしか表現できないが、二層パーセプトロンでは◯◯領域を表現できる。
・多層パーセプトロンは、理論上◯◯を表現できる。


3章
・ニューラルネットワーク(NN)では、活性化関数として、◯◯や◯◯のような滑らかに変化する関数を利用する。
・NumPyの◯◯を上手く使うことでNNを効率良く実装できる。
・機械学習(ML)の問題は、◯◯1問題と◯◯2問題に大別できる。
・出力層で使用する活性化関数は、◯◯1問題では◯◯関数、◯◯2問題では◯◯関数を一般的に利用する。
・◯◯2問題では、出力層のニューロン数を◯◯に設定する。
・入力データのまとまりである◯◯単位で推論処理を行えば、計算が高速化する。


4章
・MLで使用するデータセットは、◯◯1データと◯◯2データに分けて使用する。
・◯◯1データで学習を行い、◯◯能力を◯◯2データで評価する。
・NNの学習は、◯◯を指標とし、◯◯の値が小さくなるように◯◯3を更新する。
・◯◯3を更新する際には、◯◯3の◯◯を利用して、◯◯方向に◯◯を更新する作業を繰り返す。
・微小な値を与えたときの差分によって微分を求めることを◯◯4という。
・◯◯4によって、◯◯を求めることができる。
・◯◯4の計算は低速だが実装は簡単である一方、◯◯の実装は複雑だが高速に◯◯を求められる。


3章辺りから理解曲線勾配の上昇率が高くなってきてキツいが、3歩進んで2歩下がっても、理論上最終ゴールに到達できるのでコツコツ進む。

4章ニューラルネットワークの学習 学習アルゴリズムの実装『ゼロから作るDeep Learning』

4章続き。 学習アルゴリズムの実装

改めて学習とは:適応可能な重みとバイアスを訓練データに適応するように調整すること

学習の4ステップ

ステップ1:ミニバッチをランダムに選択
ステップ2:ミニバッチの損失関数を減らす勾配を算出
ステップ3:重みパラメータを勾配方向に微小量だけ更新
ステップ4:ステップ1−3を反復

ミニバッチをランダム選択することから、
確率的勾配下降法(SGD, stochastic gradient descent)と呼ぶ。

2層ニューラルネットワークのクラスプログラムを参照。今までの総まとめ。

# cat two_layer_net.py


# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
from common.functions import *
from common.gradient import numerical_gradient


class TwoLayerNet:

    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, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']

        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)

        return y

    # x:入力データ, t:教師データ
    def loss(self, x, t):
        y = self.predict(x)

        return cross_entropy_error(y, t)

    def accuracy(self, x, t):
        y = self.predict(x)
        y = np.argmax(y, axis=1)
        t = np.argmax(t, axis=1)

        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy

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

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

        return grads

    def gradient(self, x, t):
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
        grads = {}

        batch_num = x.shape[0]

        # forward
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)

        # backward
        dy = (y - t) / batch_num
        grads['W2'] = np.dot(z1.T, dy)
        grads['b2'] = np.sum(dy, axis=0)

        da1 = np.dot(dy, W2.T)
        dz1 = sigmoid_grad(a1) * da1
        grads['W1'] = np.dot(x.T, dz1)
        grads['b1'] = np.sum(dz1, axis=0)

        return grads

そして、バッチサイズ100を10000回SGDでパラメータ更新するサンプルプログラム

# cat train_neuralnet_save.py

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
import numpy as np
import matplotlib.pyplot as plt
plt.switch_backend('agg')
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet

# データの読み込み
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

iters_num = 10000  # 繰り返しの回数を適宜設定する
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1

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

iter_per_epoch = max(train_size / batch_size, 1)

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

    # 勾配の計算
    #grad = network.numerical_gradient(x_batch, t_batch)
    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)

    if i % iter_per_epoch == 0:
        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("train acc, test acc | " + str(train_acc) + ", " + str(test_acc))

# グラフの描画
markers = {'train': 'o', 'test': 's'}
x = np.arange(len(train_acc_list))
plt.plot(x, train_acc_list, label='train acc')
plt.plot(x, test_acc_list, label='test acc', linestyle='--')
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
#plt.show()
plt.savefig('train_neuralnet.png')

そして、実行結果。1エポックごとの認識精度が向上していることがわかる。

# py train_neuralnet_save.py
train acc, test acc | 0.0987166666667, 0.098
train acc, test acc | 0.788433333333, 0.7953
train acc, test acc | 0.874716666667, 0.8796
train acc, test acc | 0.898033333333, 0.9014
train acc, test acc | 0.9083, 0.9109
train acc, test acc | 0.9141, 0.9171
train acc, test acc | 0.919933333333, 0.9215
train acc, test acc | 0.924516666667, 0.9264
train acc, test acc | 0.927233333333, 0.9293
train acc, test acc | 0.930866666667, 0.9316
train acc, test acc | 0.93405, 0.9344
train acc, test acc | 0.937083333333, 0.9367
train acc, test acc | 0.93835, 0.9372
train acc, test acc | 0.941066666667, 0.9402
train acc, test acc | 0.94335, 0.9426
train acc, test acc | 0.945, 0.9442
train acc, test acc | 0.946916666667, 0.9459

f:id:kaeken:20161108192243p:plain

訓練データとテストデータに差異がほぼないことから過学習が起きていないことも分かった。

4章ニューラルネットワークの学習 数値微分、偏微分、勾配法、学習率『ゼロから作るDeep Learning』

『ゼロから作るDeep Learning』4章続き。

微分とは:ある瞬間の変化量

数値微分numerical differentiationとは:微小な差分によって微分を求めること

def numerical_diff(f, x):
  h = 1e-4 #0.0001程度の値が適当な微小値
  return ( f(x+h) - f(x-h) ) / (2*h)

サンプルコードを改変してグラフを生成してみる

# cat gradient_1d_save.py
# coding: utf-8
import numpy as np
import matplotlib.pylab as plt
plt.switch_backend('agg')


def numerical_diff(f, x):
    h = 1e-4 # 0.0001
    return (f(x+h) - f(x-h)) / (2*h)


def function_1(x):
    return 0.01*x**2 + 0.1*x


def tangent_line(f, x):
    d = numerical_diff(f, x)
    print(d)
    y = f(x) - d*x
    return lambda t: d*t + y

x = np.arange(0.0, 20.0, 0.1)
y = function_1(x)
plt.xlabel("x")
plt.ylabel("f(x)")

tf = tangent_line(function_1, 5) # 5->10に変更すれば2つ目の画像になる
y2 = tf(x)

plt.plot(x, y)
plt.plot(x, y2)
plt.savefig('gradient_1d.png')

f:id:kaeken:20161107192131p:plain f:id:kaeken:20161107192138p:plain

続いて、偏微分

偏微分とは:複数の変数からなる関数の微分

x0, x1の2変数関数の定義とグラフを表示

def function_2(x):
  return x[0]**2 + x[1]**2

# 3Dグラフを描画
# cat multivariate_func_save.py
# coding: utf-8
import numpy as np
import matplotlib.pylab as plt
plt.switch_backend('agg')
from mpl_toolkits.mplot3d import Axes3D

x = np.meshgrid(np.arange(-3, 3, 0.1), np.arange(-3, 3, 0.1))
z = x[0]**2 + x[1]**2

fig = plt.figure()
ax = Axes3D(fig)
ax.plot_wireframe(x[0], x[1], z)

plt.xlim(-3.5, 3.5)
plt.ylim(-4.5, 4.5)
plt.xlabel("x0")
plt.ylabel("x1")
plt.savefig('multivariate_func.png')

f:id:kaeken:20161107195025p:plain

ところで、Python微分コマンドなどあるのだろうか、と調査するとSympyで数学できるらしいので、さっそく導入

# conda install -c anaconda sympy=1.0
# py
>>> from sympy import *
>>> var('x')
x
>>> diff(sin(x),x)
cos(x)

こちらがドキュメント

Welcome to SymPy’s documentation! — SymPy 1.0 documentation

さて、続いて、勾配。

勾配gradientとは:すべての変数の偏微分をベクトルとしてまとめたもの

# f(x0, x1) = x0**2 + x1**2の勾配図を描くサンプルコード実行

# cat gradient_2d_save.py

# coding: utf-8
# cf.http://d.hatena.ne.jp/white_wheels/20100327/p3
import numpy as np
import matplotlib.pylab as plt
plt.switch_backend('agg')
from mpl_toolkits.mplot3d import Axes3D


def _numerical_gradient_no_batch(f, x):
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x)

    for idx in range(x.size):
        tmp_val = x[idx]
        x[idx] = float(tmp_val) + h
        fxh1 = f(x) # f(x+h)

        x[idx] = tmp_val - h
        fxh2 = f(x) # f(x-h)
        grad[idx] = (fxh1 - fxh2) / (2*h)

        x[idx] = tmp_val # 値を元に戻す

    return grad


def numerical_gradient(f, X):
    if X.ndim == 1:
        return _numerical_gradient_no_batch(f, X)
    else:
        grad = np.zeros_like(X)

        for idx, x in enumerate(X):
            grad[idx] = _numerical_gradient_no_batch(f, x)

        return grad


def function_2(x):
    if x.ndim == 1:
        return np.sum(x**2)
    else:
        return np.sum(x**2, axis=1)


def tangent_line(f, x):
    d = numerical_gradient(f, x)
    print(d)
    y = f(x) - d*x
    return lambda t: d*t + y

if __name__ == '__main__':
    x0 = np.arange(-2, 2.5, 0.25)
    x1 = np.arange(-2, 2.5, 0.25)
    X, Y = np.meshgrid(x0, x1)

    X = X.flatten()
    Y = Y.flatten()

    grad = numerical_gradient(function_2, np.array([X, Y]) )

    plt.figure()
    plt.quiver(X, Y, -grad[0], -grad[1],  angles="xy",color="#666666")#,headwidth=10,scale=40,color="#444444")
    plt.xlim([-2, 2])
    plt.ylim([-2, 2])
    plt.xlabel('x0')
    plt.ylabel('x1')
    plt.grid()
    plt.legend()
    plt.draw()
    #plt.show()
    plt.savefig('gradient_2d.png')

f:id:kaeken:20161107201154p:plain 確かに3Dグラフの谷に向かってベクトルが向いている

次に、勾配法。

勾配法gradient methodとは:現在の場所から勾配方向に一定距離だけ進み、移動先でも同様に繰り返し勾配方向へ移動することで関数の値を徐々に減らす方法

勾配降下法gradient descent methodとは:最小値を探す勾配法で、ニューラルネットワークの分野で使われる

学習率learning rateとは:一回の学習でどれだけ学習しパラメータを更新するのかを決める割合。大きすぎても小さすぎても最適化できないので調整・確認が必要。

ハイパーパラメータhyperparameterとは:学習率のように、ニューラルネットワークのパラメータとは性質の異なる人為設定するパラメータ。

以下勾配法で最小値を探索するサンプルコード

# cat gradient_method_save.py
# coding: utf-8
import numpy as np
import matplotlib.pylab as plt
plt.switch_backend('agg')
from gradient_2d import numerical_gradient


def gradient_descent(f, init_x, lr=0.01, step_num=100):
    x = init_x
    x_history = []

    for i in range(step_num):
        x_history.append( x.copy() )

        grad = numerical_gradient(f, x)
        x -= lr * grad

    return x, np.array(x_history)


def function_2(x):
    return x[0]**2 + x[1]**2

init_x = np.array([-3.0, 4.0])

lr = 0.1
step_num = 20
x, x_history = gradient_descent(function_2, init_x, lr=lr, step_num=step_num)

plt.plot( [-5, 5], [0,0], '--b')
plt.plot( [0,0], [-5, 5], '--b')
plt.plot(x_history[:,0], x_history[:,1], 'o')

plt.xlim(-3.5, 3.5)
plt.ylim(-4.5, 4.5)
plt.xlabel("X0")
plt.ylabel("X1")
#plt.show()
plt.savefig('gradient_method.png')

f:id:kaeken:20161107202238p:plain

最後にニューラルネットワークの勾配を求めるサンプルコード

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
import numpy as np
from common.functions import softmax, cross_entropy_error
from common.gradient import numerical_gradient


class simpleNet:
    def __init__(self):
        self.W = np.random.rand(2,3)

    def predict(self, x):
        return np.dot(x, self.W)

    def loss(self, x, t):
        z = self.predict(x)
        y = softmax(z)
        loss = cross_entropy_error(y, t)

        return loss

x = np.random.rand(2)
t = np.array([0,0,1])

net = simpleNet()

f = lambda w: net.loss(x, t)
dW = numerical_gradient(f, net.W)

print(dW)

4章ニューラルネットワークの学習 データ駆動アプローチ、損失関数、ミニバッチ『ゼロから作るDeep Learning』

3章ではニューラルネットワークの「推論」を実装したが、4章からニューラルネットワークの「学習」を実装する。

「学習」とは:訓練データから最適な重みパラメータ値を自動で獲得すること

パラメータの数は、実際数千〜数億にも及ぶため、手動で調整することは不可能

「データ駆動アプローチ」:いままでの「人」を中心としたアプローチではなく、「データ」を中心としたアプローチ

ニューラルネットワークディープラーニングでは、従来の機械学習以上に、属人性を排している

・機械学習(ML)以前 入力データ → 人力処理 → 出力データ
↓
・ML 入力データ → 人力特徴量 → ML自動処理 → 出力データ
↓
・NNやDL 入力データ → NN/DL自動処理 → 出力データ

機械学習におけるデータの取扱について

2種類のデータ:「訓練(教師)データ」と「テストデータ」

まず「訓練(教師)データ」で学習し、最適なパラメータを探索

つぎに「テストデータ」で汎化能力を評価し、一部のデータセットだけ過度に対応した「過学習overfitting」を避ける

「損失関数loss function」:ニューラルネットワークの学習で用いられる指標で、主に「二乗和誤差」「交差エントロピー誤差」が用いられる

「二乗和誤差」について。

# 二乗和誤差
def mean_squared_error(y, t): #y: ニューラルネットワークの出力、t:訓練データ
  return np.sum( (y - t)**2 ) / 2

# 実行例
# cat mean_squared_error.py
#!/usr/bin/env python

import numpy as np
import my_module as my

# 訓練データ(「2」を正解とするone-hot表現
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

# 例1: '2'の確率が最も高い場合
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
print(np.sum(y)) #=> 1.0

y = my.mean_squared_error(np.array(y), np.array(t))
print(y) #=> 0.0975

# 例2: '7'の確率が最も高い場合
y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
print(np.sum(y)) #=> 1.0

y = my.mean_squared_error(np.array(y), np.array(t))
print(y) #=> 0.5975

「交差エントロピー誤差」について。

#交差エントロピー誤差
def cross_entropy_error(y, t):
  delta = 1e-7 # マイナス無限大を回避するための微小値
  return - np.sum( t * np.log( y + delta ) )


# 実行例
# cat cross_entropy_error.py
#!/usr/bin/env python

import numpy as np
import my_module as my

# 訓練データ(「2」を正解とするone-hot表現
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

# 例1: '2'の確率が最も高い場合
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
print(np.sum(y)) #=> 1.0

y = my.cross_entropy_error(np.array(y), np.array(t))
print(y) #=> 0.510825457099

# 例2: '7'の確率が最も高い場合
y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
print(np.sum(y)) #=> 1.0

y = my.cross_entropy_error(np.array(y), np.array(t))
print(y) #=> 2.30258409299

続いて、大量にある全データのうち一部を選出して近似とする「ミニバッチ学習」について。

# 0から指定された数字(60000)未満までの数字をランダムに指定個数(10)選択する処理
>>> import numpy as np
>>> np.random.choice(60000, 10)
array([54904, 15528, 35786, 44249, 25077, 37764, 46607,   552, 33463, 12885])
>>> np.random.choice(60000, 10)
array([38745,  8181,  8602, 37811, 24747, 18214, 50371, 13052, 13100, 36289])
>>> np.random.choice(60000, 10)

MNISTデータセットで動作確認

# cat mini_batch.py
# coding: utf-8
import sys, os
sys.path.append(os.pardir)
import numpy as np
from dataset.mnist import load_mnist

(x_train, t_train), (x_test, t_test) = \
  load_mnist(normalize=True, one_hot_label=True)

print(x_train.shape) #=>(60000, 784)
print(t_train.shape) #=>(60000, 10)

# mini_batch
train_size = x_train.shape[0]
batch_size = 10
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]

print(x_batch) #=>ランダム結果
'''
[[ 0.  0.  0. ...,  0.  0.  0.]
 [ 0.  0.  0. ...,  0.  0.  0.]
 [ 0.  0.  0. ...,  0.  0.  0.]
 ...,
 [ 0.  0.  0. ...,  0.  0.  0.]
 [ 0.  0.  0. ...,  0.  0.  0.]
 [ 0.  0.  0. ...,  0.  0.  0.]]
'''

print(t_batch) #=>ランダム結果
'''
[[ 0.  0.  0.  0.  0.  1.  0.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.  0.  0.  1.  0.  0.]
 [ 0.  0.  0.  1.  0.  0.  0.  0.  0.  0.]
 [ 1.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
 [ 1.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
 [ 1.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
 [ 0.  0.  0.  1.  0.  0.  0.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.  0.  0.  0.  0.  1.]
 [ 0.  0.  0.  0.  1.  0.  0.  0.  0.  0.]
 [ 0.  0.  0.  1.  0.  0.  0.  0.  0.  0.]]
'''

交差エントロピー誤差関数の改変

#交差エントロピー誤差(バッチ対応版)
def cross_entropy_error_batch(y, t):
  if y.ndim == 1:
    t = t.reshape(1, t.size)
    y = y.reshape(1, y.size)

  batch_size = y.shape[0]
  return - ( np.sum( t * np.log( y ) ) / batch_size )


#交差エントロピー誤差(バッチ対応、教師データラベル版)
#one-hot表現ではなく'2'などのラベル
def cross_entropy_error_batch_label(y, t):
  if y.ndim == 1:
    t = t.reshape(1, t.size)
    y = y.reshape(1, y.size)

  batch_size = y.shape[0]
  return - ( np.sum( t * np.log( y[np.arange(batch_size), t] ) ) / batch_size )

ニューラルネットワークの学習では、

認識精度を「指標」にしてはいけない。

なぜなら、認識精度を指標にすると、

パラメータの微分がほとんどの場所で0

になってしまうから。

だから、損失関数が必要なのである。

手書き数字認識とバッチ処理『ゼロから作るDeep Learning』

『ゼロから作るDeep Learning』3章最後

いよいよ手書き数字認識に入る。ここでは、「学習」フェーズは完了している前提で、「推論」フェーズのみ順方向伝播方式で実施。

# MNISTという手書き数字画像セットを準備
# git clone https://github.com/oreilly-japan/deep-learning-from-scratch.git
# cd deep-learning-from-scratch/ch03
# ls ../dataset/mnist.py
# ../dataset/mnist.py
# cat mnist_check.py

import sys, os
sys.path.append(os.pardir) #親ディレクトリのファイルをインポート
from dataset.mnist import load_mnist

# load_minst((訓練画像、訓練ラベル), (テスト画像、テストラベル)
# 初回呼び出しはネットDL、2回目以降はローカルpickleファイル読込
# 引数
# normalize: 0.0..1.0に正規化
# flatten: 1次元配列化
# one_hot_label: 正解ラベルのみ1でそれ以外は0にするone-hot表現として格納するか
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=False)

print(x_train.shape) # (60000, 784)
print(t_train.shape) # (60000,)
print(x_test.shape) # (10000, 784)
print(t_test.shape) # (10000,)

続いて、MNIST画像の表示スクリプトmnist_show.pyを参考に、保存処理mnist_save.pyを作って確認

# cat mnist_save.py
# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
import numpy as np
from dataset.mnist import load_mnist
from PIL import Image

def img_save(img):
    pil_img = Image.fromarray(np.uint8(img))
    pil_img.save('mnist_save_sample.png')

(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=False)

img = x_train[0]
label = t_train[0]
print(label)  # 5

print(img.shape)  # (784,)
img = img.reshape(28, 28)  # 形状を元の画像サイズに変形
print(img.shape)  # (28, 28)

img_save(img)

# py mnist_save.py
5
(784,)
(28, 28)

出力された画像

f:id:kaeken:20161107013732p:plain

続いて、ニューラルネットワークの推論処理を確認

# cat neuralnet_mnist.py
# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
import numpy as np
import pickle
from dataset.mnist import load_mnist
from common.functions import sigmoid, softmax


def get_data():
  # 前処理pre-processingとして、正規化normalizationを実施
    (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, flatten=True, one_hot_label=False)
    return x_test, t_test

# pickleファイルに保存された学習済重みパラメータの読込
def init_network():
    with open("sample_weight.pkl", 'rb') as f:
        network = pickle.load(f)
    return network


def predict(network, x):
    W1, W2, W3 = network['W1'], network['W2'], network['W3']
    b1, b2, b3 = network['b1'], network['b2'], network['b3']

    a1 = np.dot(x, W1) + b1
    z1 = sigmoid(a1)
    a2 = np.dot(z1, W2) + b2
    z2 = sigmoid(a2)
    a3 = np.dot(z2, W3) + b3
    y = softmax(a3)

    return y


x, t = get_data()
network = init_network()
accuracy_cnt = 0
for i in range(len(x)):
    y = predict(network, x[i])
    p= np.argmax(y) # 最も確率の高い要素のインデックスを取得
    if p == t[i]:
        accuracy_cnt += 1

print("Accuracy:" + str(float(accuracy_cnt) / len(x)))
#=>Accuracy:0.9352で、
# 93.52%正しく分類できた

続いてバッチ処理

バッチbatch:ひとまとまりの入力データ束

バッチ処理によって1枚あたりの処理時間を短縮できる

# cat neuralnet_mnist_batch.py
# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
import numpy as np
import pickle
from dataset.mnist import load_mnist
from common.functions import sigmoid, softmax


def get_data():
    (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, flatten=True, one_hot_label=False)
    return x_test, t_test


def init_network():
    with open("sample_weight.pkl", 'rb') as f:
        network = pickle.load(f)
    return network


def predict(network, x):
    w1, w2, w3 = network['W1'], network['W2'], network['W3']
    b1, b2, b3 = network['b1'], network['b2'], network['b3']

    a1 = np.dot(x, w1) + b1
    z1 = sigmoid(a1)
    a2 = np.dot(z1, w2) + b2
    z2 = sigmoid(a2)
    a3 = np.dot(z2, w3) + b3
    y = softmax(a3)

    return y


x, t = get_data()
network = init_network()

batch_size = 100 # バッチの数
accuracy_cnt = 0

for i in range(0, len(x), batch_size):
    x_batch = x[i:i+batch_size]
    y_batch = predict(network, x_batch)
    p = np.argmax(y_batch, axis=1)
    accuracy_cnt += np.sum(p == t[i:i+batch_size])

print("Accuracy:" + str(float(accuracy_cnt) / len(x)))

実際に比較してみると、確かに高速化していることが確認できた

# バッチなし
# time py neuralnet_mnist.py
Accuracy:0.9352

real    0m2.220s
user    0m2.859s
sys 0m0.521s

# バッチあり
# time py neuralnet_mnist_batch.py
Accuracy:0.9352

real    0m0.991s
user    0m0.857s
sys 0m0.312s

出力層の設計(分類問題で使うソフトマックス関数)『ゼロから作るDeep Learning』

『ゼロから作るDeep Learning』3章続き

# ニューラルネットワークの問題は、回帰問題と分類問題に大別できる
# 回帰問題とは、入力データから数値予測を行う問題(例 入力:人物画像 → 出力:体重予測)
# 分類問題とは、入力データがどのクラスに属するかという問題(例 入力:人物画像 → 出力:男性クラスor女性クラス)
# 回帰問題では恒等関数を使い、分類問題ではソフトマックス関数softmaxを使う
# ソフトマックス関数は、分子に入力信号の指数関数、分母にすべての入力信号の指数関数の和から構成される
def softmax(x):
  return np.exp(x) / np.sum(np.exp(x))

# 指数関数の値が巨大になりすぎる場合のオーバーフロー対策として、
# 補正値を導入する必要がある(通常、補正値は入力信号の中で最大の値)
def softmax(x):
  c = np.max(x) # オーバーフロー対策の補正値
  return np.exp(x - c) / np.sum(np.exp(x - c))

# 例
import numpy as np
import my_module as my

a = np.array([0.3, 2.9, 4.0])
y = my.softmax(a)
print(y) #=> [ 0.01821127  0.24519181  0.73659691]
print(np.sum(y)) #=> 1.0


# ソフトマックス関数の出力は、0から1の間の実数で、総和は1なので確率として解釈できる
# ソフトマックス関数を適用しても、元データの大小関係は変わらない
# 機械学習の問題を解く手順として、「学習」と「推論」の2フェーズのうち、推論フェーズではソフトマックス関数は省略する
# 出力層のニューロン数は、分類したいクラス数に設定する (例 入力:数字画像 → 出力層:10クラス)

ニューラルネットワーク フォワード方向処理『ゼロから作るDeep Learning』3章

『ゼロから作るDeep Learning』3章続き。

ニューラルネットワークの順方向forward処理について。

# 順方向forward処理:入力から出力方向への伝達処理。あとで逆方向backward処理について学ぶ
# 以下3層ニューラルネットワーク構成とする
# 入力層(第0層)
# 隠れ層1(第1層)
# 隠れ層2(第2層)
# 出力層(第3層)

# cat forward.py
#!/usr/bin/env python

import numpy as np
import my_module as my

def init(): #初期化
  n = {}
  # weight
  n['W1'] = np.array([ [0.1, 0.3, 0.5], [0.2, 0.4, 0.6] ])
  n['W2'] = np.array([ [0.1, 0.4], [0.2, 0.5], [0.3, 0.6] ])
  n['W3'] = np.array([ [0.1, 0.3], [0.2, 0.4] ])
  # bias
  n['b1'] = np.array([0.1, 0.2, 0.3])
  n['b2'] = np.array([0.1, 0.2])
  n['b3'] = np.array([0.1, 0.2])

  return n


def forward(n, x): #入力を出力へ変換する処理をまとめた関数
  W1, W2, W3 = n['W1'], n['W2'], n['W3']
  b1, b2, b3 = n['b1'], n['b2'], n['b3']

  # 1層目
  a1 = np.dot(x, W1) + b1
  z1 = my.sigmoid(a1)
  # 2層目
  a2 = np.dot(z1, W2) + b2
  z2 = my.sigmoid(a2)
  # 出力層
  a3 = np.dot(z2, W3) + b3
  y = my.identity(a3) #何もせずそのまま返す恒等関数

  return y


n = init()
x = np.array([1.0, 0.5])
y = forward(n, x)
print(y)

# 実行
 py forward.py
[ 0.31682708  0.69627909]

次は、出力層の設計に入る。

Pythonで自作関数をモジュールとして読み込む

参考: Python3のimport・下位/上位階層のモジュールをインポートしたい【import】【Python3】 - DRYな備忘録

『ゼロから作るDeep Learning』で自作関数がだんだん説明なしに使われていくので、いったんモジュールにまとめて読み込むことにする。

本番環境ならどこに配置するか考える必要はあるが、ひとまず同一ディレクトリ内にmy_module.pyとしてまとめておく。

$ cat my_module.py # 自作関数をまとめたモジュール
import numpy as np

def step(x): #step関数
  return np.array(x > 0, dtype=np.int)

def sigmoid(x): #シグモイド関数
  return 1 / (1 + np.exp(-x))

def relu(x): #ReLU関数
  return np.maximum(0, x)

def identity(x): #恒等関数
  return x

# 呼び出し
import my_module as my
...
print(my.sigmoid(param))
...

新たに定義された関数はmy_module.pyに追加していく。

ニューラルネットワーク計算準備としてNumPyで多次元配列処理(内積=ドット積)

f:id:kaeken:20161105113024p:plain 『ゼロから作るDeep Learning』3章続き。 ニューラルネットワークの実装で多次元配列が必要。 そこでPythonのNumPyの多次元配列操作を改めて詳しく学ぶ。

>>> import numpy as np
>>> A = np.array([ [1,2], [3,4], [5,6] ])

>>> A
array([[1, 2],
       [3, 4],
       [5, 6]])

>>> A.shape # 配列の各次元の大きさをタプルで返す
(3, 2)

>>> A.ndim # 配列の次元数
2

# 次元が統一されていない場合は、1次元扱いの様子
>>> B = np.array([ [1], [2,3], [4,5,6] ])
>>> B
array([[1], [2, 3], [4, 5, 6]], dtype=object)
>>> B.shape
(3,)
>>> B.ndim
1

>>> C = np.array([ [1,2,3], [4,5], [6] ])
>>> C
array([[1, 2, 3], [4, 5], [6]], dtype=object)
>>> C.shape
(3,)
>>> C.ndim
1

# 続いて内積
# 行列の内積とは、行と列の要素ごとの積と和によって計算する
# 2次元配列は行列matrixと呼び、横方向の行rowと縦方向の列columnがある

>>> A = np.array([ [1,2], [3,4] ]) 
>>> A
array([[1, 2],
       [3, 4]])
>>> A.shape
(2, 2)

>>> B = np.array([ [5,6], [7,8] ])
>>> B
array([[5, 6],
       [7, 8]])
>>> B.shape
(2, 2)

>>> np.dot(A, B) # 行列A,Bの内積(ドット積)
array([[19, 22],
       [43, 50]])

#計算内容内訳
>>> 1*5 + 2*7
19
>>> 3*5 + 4*7
43
>>> 1*6 + 2*8
22
>>> 3*6 + 4*8
50

#行列の積は、被演算子(A,B)の順番が異なると結果も異なる
>>> np.dot(B, A)
array([[23, 34],
       [31, 46]])

#続いて、2x3の行列と3x2の行列の積
>>> C = np.array([ [1,2,3], [4,5,6] ])
>>> C
array([[1, 2, 3],
       [4, 5, 6]])
>>> C.shape
(2, 3)

>>> D = np.array([ [1,2], [3,4], [5,6] ])
>>> D
array([[1, 2],
       [3, 4],
       [5, 6]])
>>> D.shape
(3, 2)

>>> np.dot(C, D)
array([[22, 28],
       [49, 64]])
>>> np.dot(C, D).shape
(2, 2)

#逆
>>> np.dot(D, C)
array([[ 9, 12, 15],
       [19, 26, 33],
       [29, 40, 51]])
>>> np.dot(D, C).shape
(3, 3)


>>> # 内積できる行列の形状には条件がある
... # 行列Aの1次元目の要素数(列数)と
... # 行列Bの0次元目の要素数(行数)を同じにする必要がある

>>> # 2x3行列と3x2行列は内積できるが、
... # 2x3行列と2x3行列の内積はできない

>>> C
array([[1, 2, 3],
       [4, 5, 6]])
>>> E = C
>>> E
array([[1, 2, 3],
       [4, 5, 6]])

>>> C.shape
(2, 3)
>>> E.shape
(2, 3)

>>> np.dot(C, E)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: shapes (2,3) and (2,3) not aligned: 3 (dim 1) != 2 (dim 0)

# 再度確認
>>> C.shape[1]
3
>>> D.shape[0]
3
>>> E.shape[0]
2

# バリデーション
>>> if C.shape[1] == D.shape[0]:
...   print('.dot() OK!')
... else:
...   print('.dot() NG!')
...
.dot() OK!

>>> if C.shape[1] == E.shape[0]:
...   print('.dot() OK!')
... else:
...   print('.dot() NG!')
...
.dot() NG!

以上、行列演算を活用してニューラルネットワークの計算を効率化していく。

参考:

行列の乗法 - Wikipedia

『ゼロから作るDeep Learning』ソースコード、正誤表

ソースコードは本を写経していたが、 独自のライブラリを使って説明があったので ソースコードリポジトリを参照した

github.com

正誤表もついていて、間違いを正せる。 5章まるごと「連鎖律」を「連鎖率」に間違えるという
盛大な誤植を見つけたので、ちょっと修正しながら読んでいこう。

3章ニューラルネットワーク(ステップ関数・シグモイド関数・ReLU関数)『ゼロから作るディープラーニング』

3章になって結構重くなってきたので、ゆっくりやっていく。

・活性化関数activation function:入力信号の総和を出力信号に変換する関数
・ステップ関数(階段関数):閾値を境にして出力が切り替わる関数
・パーセプトロンでは、ステップ関数を使っている

def step_function(x):
  if x > 0:
    return 1
  else:
    return 0

#配列引数対応版
def step_function(x):
  y = x > 0 # 配列要素に対して不等号の演算をしてTrue/Falseのbool値を返す
  return y.astype(np.int) # boolからintに型変換

def step_function(x):
  return np.array(x > 0, dtype=np.int) # 圧縮記述

以下、生成されたステップ関数グラフ例

import numpy as np
import matplotlib.pyplot as plt
plt.switch_backend('agg')

def step_func(x):
  return np.array(x > 0, dtype=np.int)

x = np.arange(-5.0, 5.0, 0.1)
y = step_func(x)
plt.plot(x, y)
plt.ylim(-0.1, 1.1)

plt.savefig('step_func.png')

f:id:kaeken:20161103223008p:plain

ニューラルネットワークについて。

・ニューラルネットワーク(多層パーセプトロン)では、活性化関数として、ステップ関数とは別の関数を使用する
・よく使われる関数はシグモイド関数sigmoid functionである。シグモイド関数は、
 ネイピア数の2.7182...の実装を表す e の-x乗に1を加えた式の逆数である(ネイピア数をあとで調べる?)

def sigmoid(x):
  return 1 / (1 + np.exp(-x))

import numpy as np
import matplotlib.pyplot as plt
plt.switch_backend('agg')

def sigmoid(x):
  return 1 / (1 + np.exp(-x))

x = np.arange(-5.0, 5.0, 0.1)
y = sigmoid(x)
plt.plot(x, y)
plt.ylim(-0.1, 1.1)

plt.savefig('sigmoid.png')

f:id:kaeken:20161103223450p:plain

ステップ関数とシグモイド関数の比較

・下記合成した図を確認すると、「滑らかさ」が異なることが重要
・ステップは0/1の離散だが、シグモイドは連続的な実数値
・共通点は、入力が小さい時は0に近く、大きくなるに従い1に近づき、常に0から1の間にある非線形関数

f:id:kaeken:20161103225819p:plain

xの範囲を10倍した場合 f:id:kaeken:20161103225848p:plain

・最近はシグモイド関数の代わりにReLU関数(Rectified Linear Unit)が使われている
・ReLUは、入力が0を超えていればその入力をそのまま出力し、0以下なら0を出力する関数

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

x = np.arange(-5.0, 5.0, 0.1)
y = relu(x)
plt.plot(x, y)
plt.ylim(-1,)

plt.savefig('relu.png')

f:id:kaeken:20161103232333p:plain

いったんここまで。

ゼロから作るDeep Learningで気になる用語

1章 Python入門
ベクトルvector:1次元配列
行列matrix:2次元配列
テンソルtensor:ベクトルや行列を一般化したもの
ブロードキャストbroadcast:形状の異なる配列同士の演算時に要素が拡大されて演算される機能

2章パーセプトロン
パーセプトロンperceptron:複数の信号を入力とし、ひとつの信号を出力とするアルゴリズム
閾値threshold:重み付けされた入力値の総和が超えた場合に発火する限界値のこと。ニューロンが発火する値
バイアスbias:発火のしやすさを調整するパラメータ
重みweight:入力への重要度をコントロールするパラメータ

読み進めていきながら、順次更新していく。

Pythonでパーセプトロンperceptron関数

『ゼロから作るDeep Learning』(以下、『ゼロからDL』) 2章パーセプトロンのAND,OR,NANDは重みとバイアス値だけが異なる。

パーセプトロンとは、
複数の信号を入力として受け取り、一つの信号を出力する処理。
信号は、「流す:1」「流さない:0」の2値のみ。

今後、
ニューラルネットワークや
ディープラーニングへと進む上で必要な考え方。
# cat and.py

import numpy as np

# 重みとバイアスを用いたANDゲート
def AND(x1, x2):
  x = np.array([x1, x2]) #入力
  w = np.array([0.5, 0.5]) #重みは入力への重要度をコントロールするパラメータ
  b = -0.7 #バイアスは発火のしやすさを調整するパラメータ
  tmp = np.sum(w*x) + b #重み付き入力の和とバイアスの合計
  if tmp <= 0:
    return 0
  else:
    return 1

print(AND(0,0), AND(0,1), AND(1,0), AND(1,1))


# cat nand.py
import numpy as np

# 重みとバイアスを用いたNANDゲート
def NAND(x1, x2):
  x = np.array([x1, x2]) #入力
  w = np.array([-0.5, -0.5]) #重みとバイアスだけ違う
  b = 0.7
  tmp = np.sum(w*x) + b #重み付き入力の和とバイアスの合計
  if tmp <= 0:
    return 0
  else:
    return 1

print(NAND(0,0), NAND(0,1), NAND(1,0), NAND(1,1))

# cat or.py
import numpy as np

# 重みとバイアスを用いたORゲート
def OR(x1, x2):
  x = np.array([x1, x2]) #入力
  w = np.array([0.5, 0.5]) #重みとバイアスだけ違う
  b = -0.2
  tmp = np.sum(w*x) + b #重み付き入力の和とバイアスの合計
  if tmp <= 0:
    return 0
  else:
    return 1

print(OR(0,0), OR(0,1), OR(1,0), OR(1,1))

実行結果 (.bashrcでalias py='python'にしてる)

py and.py
0 0 0 1
py nand.py
1 1 1 0
py or.py
0 1 1 1

ついでにNOR

# cat nor.py
import numpy as np

# 重みとバイアスを用いたNORゲート
def NOR(x1, x2):
  x = np.array([x1, x2]) #入力
  w = np.array([-0.5, -0.5]) #重みとバイアスだけ違う
  b = 0.2
  tmp = np.sum(w*x) + b #重み付き入力の和とバイアスの合計
  if tmp <= 0:
    return 0
  else:
    return 1

print(NOR(0,0), NOR(0,1), NOR(1,0), NOR(1,1))

→1 0 0 0 になった。

単体のパーセプトロンには限界があり、
排他的論理和XORゲートを実装するには、
複数のゲートを組み合わせて
多層パーセプトロンにする必要がある。

結論から言えば、
NANDとORを
ANDでつなぐ
# cat xor.py
import numpy as np

def AND(x1, x2):
  x = np.array([x1, x2])
  w = np.array([0.5, 0.5])
  b = -0.7
  tmp = np.sum(w*x) + b
  if tmp <= 0:
    return 0
  else:
    return 1

def NAND(x1, x2):
  x = np.array([x1, x2])
  w = np.array([-0.5, -0.5])
  b = 0.7
  tmp = np.sum(w*x) + b
  if tmp <= 0:
    return 0
  else:
    return 1

def OR(x1, x2):
  x = np.array([x1, x2])
  w = np.array([0.5, 0.5])
  b = -0.2
  tmp = np.sum(w*x) + b
  if tmp <= 0:
    return 0
  else:
    return 1

def XOR(x1, x2):
  s1 = NAND(x1, x2)
  s2 = OR(x1, x2)
  y = AND(s1, s2)
  return y

print(XOR(0,0), XOR(0,1), XOR(1,0), XOR(1,1))
#=> 0 1 1 0 排他的論理和になった

Python×数学×人工知能を平行して勉強していく

機械学習や深層学習など人工知能の技術は数学が必須だ。
ライブラリが細かい計算を隠蔽してくれるとはいえ、
「何のためにこの数式を使うか」を理解していないと、
使い方を間違えるし、変更することもできない。

数学は遠い昔にやったが忘れているので、
復習&新規分野を学習していく。

とはいえ、鉛筆を使って勉強するのもだるいので、
ここはプログラマらしくプログラミングしながら学ぶ。
以下、オライリーの既刊本からピックアップして、
人工知能プログラミングと平行してやっていこう。

また、
Pythonは書いていくうちに、
手に馴染んでくる。
Rubyほどの衝撃はないが、
確かに書きやすい言語みたいだ。

Python
数学
人工知能

三本柱で集中的に取り組んでいく。

http://www.oreilly.co.jp/catalog/

www.oreilly.co.jp www.oreilly.co.jp www.oreilly.co.jp www.oreilly.co.jp www.oreilly.co.jpwww.oreilly.co.jp

次にグラフ描画用にmatplotlib.pyplotモジュール

いきなりエラッタのでyum

>>> import matplotlib.pyplot as plt
...
ImportError: libXext.so.6: cannot open shared object file: No such file or directory
yum install libXext.x86_64
yum install libSM.x86_64
yum install libXrender.x86_64

気を取り直して、importしたら成功したがplotでXの指定がないのでコケた

>>> import matplotlib.pyplot as plt
>>> import numpy as np
>>> x = np.arange(0,6, 0.1)
>>> y = np.sin(x)
>>> plt.plot(x,y)
...
    raise RuntimeError('Invalid DISPLAY variable')
RuntimeError: Invalid DISPLAY variable


#以下をimport後に指定すればよいらしい
>>> plt.switch_backend('agg')
#すると
>>> plt.plot(x,y)
>>> plt.savefig('sin.png')
# これで画像が生成された

f:id:kaeken:20161103004351p:plain

さすがに対話モードは面倒なのでファイルに変更

import numpy as np
import matplotlib.pyplot as plt
plt.switch_backend('agg')

x = np.arange(0, 6, 0.1)
y1 = np.sin(x)
y2 = np.cos(x)

plt.plot(x, y1, label='sin')
plt.plot(x, y2, linestyle = '--', label='cos')
plt.xlabel('x')
plt.ylabel('y')
plt.title('sin & cos')
plt.legend()


plt.savefig('sin_cos.png')

sinとcos f:id:kaeken:20161103005056p:plain

あと画像読込もできるとのこと(未確認)

import matplotlib.pyplot as plt
from matplotlib.image import imread

img = imread('lena.png')
plt.imshow(img)

plt.show()