LoginSignup
2
1

More than 3 years have passed since last update.

深層学習/ゼロから作るDeep Learning 第4章メモ

Last updated at Posted at 2020-04-30

1.はじめに

 名著、「ゼロから作るDeep Learning」を読んでいます。今回は4章のメモ。
 コードの実行はGithubからコード全体をダウンロードし、ch04の中で jupyter notebook にて行っています。

2.2層ニューラルネットワークの学習

 第4章の最後に、2層ニューラルネットワークを数値微分によってパラメータの勾配を計算する方法で学習するコード ( train_neuralnet.py ) があります。今回は、このコードを実行してから、細部を確認して行きます。但し、Githubのコードそのままではなく、下記の様に一部修正・追加します。

1)実行速度が遅いことへの対応
 数値微分を使うと莫大な時間が掛かるのにも関わらず精度表示が 600 iter 毎(1 epoch 毎)では、2回目の精度計算の表示が数時間後くらいになってしまいます。そこで、1時間ちょっとで結果が分かるように、精度表示を 1 iter 毎にし、実行回数を10000から100にし、学習率を0.1から1.0にします。

2)外部呼び出しコードを減らして見通しよく
 ゼロ作の特徴は、既に説明したコードは極力外部呼び出しにして、一度に表示するコードを出来るかぎり簡潔に表すことです。しかし、train_neuralnet.py から two_layer_net.py を呼び出し、two_layer_net.py から common フォルダー内の gradient.py を呼び出すというのは、とても分かり難いです。なので、このあたりは、train_neuralnet.py に追加して見通しをよくします。

 但し、commonフォルダーにある function.py だけは外部コードとして使っています(sigmoid, sofytmax, cross_entropy_errorなど)。

 では、まずは下記コードを実行してみましょう。

import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
from common.functions import *  # common フォルダーの function.py にある関数を全て使えるように設定
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist

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

    # ロス計算
    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

    # 勾配計算
    def numerical_gradient(self, x, t):   
        loss_W = lambda W: self.loss(x, t)        
        grads = {}
        grads['W1'] = numerical_gradient2(loss_W, self.params['W1'])
        grads['b1'] = numerical_gradient2(loss_W, self.params['b1'])
        grads['W2'] = numerical_gradient2(loss_W, self.params['W2'])
        grads['b2'] = numerical_gradient2(loss_W, self.params['b2'])        
        return grads

    # 数値微分
    def numerical_gradient2(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] = 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 # 値を元に戻す
            it.iternext()           
        return grad  

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

# TwoLayerNet をインスタンス化
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)  

# 初期設定
iters_num = 100  # 実行回数は 10000 → 100 に変更
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 1   # 学習率は 0.1 → 1.0 に変更
train_loss_list, train_acc_list, test_acc_list = [], [], []
iter_per_epoch = 1  # 精度表示は 1 epoch 毎から 1 iter 毎に変更

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)

    # パラメータの更新
    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)

        # 表示 (iter と train_loss を追加)
        print('[iter='+str(i)+'] '+'train_loss='+str(loss)+', '+'train_acc='+str(train_acc)+', '+'test_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("iter")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()

スクリーンショット 2020-04-29 16.40.37.png
 私の Macbook air で 100iter 実行するのに 75分掛かりました。数値微分の場合でも 16epoch (= 9600iter) を実行した精度グラフをテキストに載せるのは一体どうなの?とは思いますが、それはさておき、数値微分には膨大な時間が掛かるということですね。

3.クラス 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)

 class をインスタンス化する時に1度だけ実行される部分で、ここでは各パラメータの初期化をしています。np.random.randn() は、平均0, 分散1 の正規分布の乱数生成、np.zeros()は ゼロ行列の生成です。各行列の大きさは、下記の様です。
スクリーンショット 2020-04-29 17.32.04.png

    # 順伝播
    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

 順伝播の部分です。辞書 params に保存されている 重み'W1', 'W2' とバイアス 'b1', 'b2' の行列を読み出し、行列演算で順伝播させます。

    # ロス計算
    def loss(self, x, t):
        y = self.predict(x)        
        return cross_entropy_error(y, t)

 ロス計算をする部分です。先程の順伝播を呼び出して求めた y と教師データ t のクロスエントロピーを求めます。クロスエントロピーは、common フォルダー内の function.py の関数を使っています。一応みておくと、

スクリーンショット 2020-04-30 08.44.59.png

 y[np.arange(batch_size), t] は、tに順番に入っている正解ラベルに従って、yの該当する要素をスライシングするという意味です。

    # 精度計算
    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からyを推論し、yと正解ラベルtのインデックスをそれぞれ取り出し、2つのインデクスが同じ場合の数をxのデータ数で割って精度を計算しています。

    # 勾配計算
    def numerical_gradient(self, x, t):   
        loss_W = lambda W: self.loss(x, t)        
        grads = {}
        grads['W1'] = numerical_gradient2(loss_W, self.params['W1'])
        grads['b1'] = numerical_gradient2(loss_W, self.params['b1'])
        grads['W2'] = numerical_gradient2(loss_W, self.params['W2'])
        grads['b2'] = numerical_gradient2(loss_W, self.params['b2'])        
        return grads

 勾配計算の部分です。この後出て来る numerical_gradient2 関数を使って、勾配の計算結果を辞書形式にまとめます。引数は、xと正解ラベルtを元にクロスエントロピーを求める先程の loss関数式、そしてパラメータの指定です。

 ちなみに、オリジナルは、grads['W1'] = numerical_gradient(loss_W, self.params['W1'])となっていて、あれ?再帰利用かな?と最初思ったりしたのですが、そうではありません。

 オリジナルでは、TwoLayerNet.py の冒頭に、from common.gradient import numerical_gradient と記載があり、common フォルダー内の gradient.py にある numerical_gradient 関数がインポートされているので、使っているのはこちらです。ここは名前を変えた方が間違いが少ないので、numerical_gradient2に名称変更しています。

    # 数値微分
    def numerical_gradient2(f, x):   
        h = 1e-4 # 0.0001

        # 勾配の計算結果を保管するゼロ行列 grad(サイズはxで指定)を準備
        grad = np.zeros_like(x)

        # 行列 x を順次インデックス指定(行と列を指定)する
        it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])

        # 行列 x の全ての行と列を指定するまで継続
        while not it.finished:

            idx = it.multi_index  # idxに(行,列)を代入            
            tmp_val = x[idx]  # idxで指定したxの値をtmp_valに退避

            # 微小数hを加えて順伝播しロスを計算
            x[idx] = tmp_val + h
            fxh1 = f(x) # f(x+h)

            # 微小数hを引いて順伝播しロスを計算
            x[idx] = tmp_val - h 
            fxh2 = f(x) # f(x-h)

            # 該当インデックスの勾配を計算
            grad[idx] = (fxh1 - fxh2) / (2*h)

            x[idx] = tmp_val # 退避した値を元に戻す
            it.iternext()  # 次のインデックスを実行          
        return grad  

 数値微分をする部分です。簡単に言うと、パラメータの1つ1つについて、小さな値hを足して順伝播・ロス計算、小さな値hを引いて順伝播・ロス計算し、2つのロス計算がどう変化したかで勾配を決めています。

 今回、パラメータ数は、W1が784*50=39,200個、W2が50*10=500個、b1が50個、b2が10個で、合計39,760個あります。パラメータ1個あたり、順伝播・ロス計算を2回行いますので、ネットワークの1回のパラメータ更新に、なんと79,520回の順伝播・ロス計算をすることになります。動作が激遅なわけです。

 さて、コードに見慣れない、np.nditer があります。通常、行列のインデッックス指定は行指定にforループ+列指定にforループと2重ループを使うわけですが、それを1回で済ませるのが、このnp.nditerです。

 なお、先程も説明しましたが、このコードはオリジナルでは、commonフォルダーgradient.pyに入っています。オリジナルの関数名は、numerical_gradient と紛らわしい名前(勾配計算のところの関数名と同一)をしているので、今回は numerical_gradient2 に変更しています。

4.本体部分

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

# TwoLayerNet をインスタンス化
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)  

# 初期設定
iters_num = 100  # 実行回数は 10000 → 100 に変更
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 1   # 学習率は 0.1 → 1.0 に変更
train_loss_list, train_acc_list, test_acc_list = [], [], []
iter_per_epoch = 1  # 精度表示は 1 epoch 毎から 1 iter 毎に変更

 データを読み込んだら、クラスTwoLayerNetをインスタンス化します。input_size=784, hidden_size=50, output_size=10なので、モデルと行列演算は下記の様です。
スクリーンショット 2020-04-30 12.56.08.png

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)

    # パラメータの更新
    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)

        # 表示 (iter と train_loss を追加)
        print('[iter='+str(i)+'] '+'train_loss='+str(loss)+', '+'train_acc='+str(train_acc)+', '+'test_acc='+str(test_acc))

 まず、ミニバッチ学習用データの準備です。np.random.choice(train_size, batch_size) は 60,000個あるtrainデータの中からランダムに100個を選んだ結果(何番目を選んだか)を batch_mask に代入し、それを使って学習データと正解ラベルを取得します。

 次に、勾配計算。TwoLayerNet クラスの numerical_gradient関数を呼び出し、先程説明した様に、パラメータ1つ1つについて、数値微分で勾配を求めます。

 そして、求めた勾配を使ってパラメータを更新し、ロスを計算して記録し、精度を表示します。

# グラフの描画
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("iter")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()

 学習完了後に、精度グラフを表示します。これは、特に説明は必要ないでしょう。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1