konchangakita

KPSを一番楽しんでいたブログ 会社の看板を背負いません 転載はご自由にどうぞ

【DeepLearning特訓】MLPの基礎 学習アルゴリズムの Python実装

E資格向けの自習アウトプット
自分用メモ

学習アルゴリズムを、実装してみて実際の学習の推移を確認してみたいと思います
今回は Python のクラスをちゃんと用いて学習モデルを作っていきます
一気に本格的な感じに

まず挑む問題ですが、過去にも Pytorch でも取り組んだ基本中の基本の MNIST のデータセットを使います
PyTorchを使ってDeep Learningのお勉強 画像認識編(MNIST) - konchangakita

こんなふうな数字の画像と 正解ラベル「4」がセットになっています
f:id:konchangakita:20210109144758p:plain

画像データセットの読み込み

MNIST データセットを読み込み
正解ラベルを One-Hot-Vector に変換します

import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split

#乱数シード指定
np.random.seed(seed=0)

# MNIST画像データ読み込み 
from sklearn.datasets import fetch_openml
mnist = fetch_openml(name='mnist_784', version=1)

# 画像とラベルを取得
X, T = mnist.data, mnist.target
# 訓練データとテストデータに分割
x_train, x_test, t_train, t_test = train_test_split(X, T, test_size=0.2)

# ラベルデータをint型にし、one-hot-vectorに変換
t_train = np.eye(10)[t_train.astype("int")]
t_test = np.eye(10)[t_test.astype("int")]

# Dataframe を numpy に変換
x_train = np.array(x_train.values)
x_test = np.array(x_test.values)

print('訓練データ(数, サイズ) \t', x_train.shape)
print('テストデータ(数, サイズ)\t', t_test.shape)


訓練データ(数, サイズ) 	 (56000, 784)
テストデータ(数, サイズ)	 (14000, 10)


読み込んだデータを確認します

# 先頭から5個
for i in range(5):
    plt.gray()
    plt.imshow(x_train[i].reshape((28,28)))
    plt.show()
    print("label: ", t_train[i])

画像と正解ラベルが one hot vector で表示されていればOK
f:id:konchangakita:20210109145301p:plain


学習モデルを定義

順伝播から誤差逆伝播を定義

# クロスエントロピー誤差
def cross_entropy_error(t, y):
    delta = 1e-8
    error = -np.mean(t * np.log(y + delta))
    return error

# ソフトマックス
def softmax(x):
    x = x.T
    _x = x - np.max(x)
    _x = np.exp(_x) / np.sum(np.exp(_x), axis=0)
    return _x.T

# モデルの定義
# MNIST layer1:784 > layer2:100 > layer3:100 > layer4:10
class Net:
    def __init__(self, input_size=784, hidden_size=[100, 100], output_size=10, batch_size=128):
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.all_size = [self.input_size] + self.hidden_size + [self.output_size]
        self.batch_size = batch_size

        self.params = {}    # w, b の保存
        self.layers = {}    # 各層の出力
        self.grads = {}     # 勾配

        # 重みとバイアスパラメータの初期化
        for idx in range(1, len(self.all_size)):
            self.params['w' + str(idx)] = np.random.randn(self.all_size[idx-1], self.all_size[idx]) * 0.055
            self.params['b' + str(idx)] = np.zeros(self.all_size[idx], dtype=float)
    
    # 順伝播、コスト関数
    def forward(self, x, t):
        relu = lambda x : np.maximum(0, x)
        self.layers[1] = x

        # 全層分を順伝播
        for idx in range(1, len(self.all_size)):
            x = self.layers[idx]
            w = self.params['w' + str(idx)]
            b = self.params['b' + str(idx)]

            # ReLU からの最後の層だけソフトマックス
            if idx == len(self.all_size)-1:
                y = self.layers[idx + 1] = softmax(np.dot(x, w) + b)
            else:
                self.layers[idx + 1] = relu(np.dot(x, w) + b)
        loss = cross_entropy_error(t, y)
        return y, loss

    # 誤差逆伝播
    def backward(self, t, y):
        layer = len(net.all_size)
        dout = (y - t) / y.shape[0]

        # 最後の層だけはソフトマックス
        for idx in range(layer-1, 0, -1):
            if idx != layer-1:
                dout = np.dot(dout, self.params['w' + str(idx+1)].transpose())
                dout = dout * (self.layers[idx+1] > 0)

            self.grads['w' + str(idx)] = np.dot(self.layers[idx].transpose(), dout)
            self.grads['b' + str(idx)] = np.sum(dout, axis=0)

        return self.grads


学習アルゴリズムの実装

とりあえず、違いが分かりそうな
基本の SGD確率的勾配降下法)と Adam を実装

# SGD
class SGD:
    def __init__(self, lr=0.01):
        self.lr = lr

    def update(self, params, grads):

        for key in params.keys():
            
            params[key] -= self.lr * grads[key]

# Adam
class Adam:
    def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
        self.lr = lr
        self.beta1 = beta1
        self.beta2 = beta2
        self.iter = 0
        self.m = None
        self.v = None
        
    def update(self, params, grads):
        if self.m is None:
            self.m, self.v = {}, {}
            for key, val in params.items():
                self.m[key] = np.zeros_like(val)
                self.v[key] = np.zeros_like(val)
        
        self.iter += 1
        
        for key in params.keys():
            self.m[key] = self.beta1 * self.m[key] + (1 - self.beta1) * grads[key]
            self.v[key] = self.beta2 * self.v[key] + (1 - self.beta2) * (grads[key]**2)
            m_unbias = self.m[key] / (1 - self.beta1**self.iter)
            v_unbias = self.v[key] / (1 - self.beta2**self.iter)
            params[key] -= self.lr * m_unbias / (np.sqrt(v_unbias) + 1e-7)

結果表示

学習の速度に差が出る

誤差

# クラス定義
batch_size = 128
sgd_net = Net(batch_size=batch_size)
adam_net = Net(batch_size=batch_size)
sgd = SGD()
adam = Adam()

# グラフ表示に履歴
sgd_loss = []
sgd_acc = []
adam_loss = []
adam_acc = []

# エポック回数
for epoch in range(20):
    # バッチサイズごとに実行
    for idx in range(int(len(x_train) / batch_size + 1)):
        x = x_train[idx * batch_size: idx * batch_size + batch_size]
        t = t_train[idx * batch_size: idx * batch_size + batch_size]

        # SGDクラス
        y, loss = sgd_net.forward(x, t)
        grads = sgd_net.backward(t, y)
        sgd.update(sgd_net.params, grads)

        sgd_loss.append(loss)
        sgd_acc.append((y.argmax(axis=1) == t.argmax(axis=1)).mean())


        # Adamクラス
        y, loss = adam_net.forward(x, t)
        grads = adam_net.backward(t, y)
        adam.update(adam_net.params, grads)

        #print('loss: ', loss)
        adam_loss.append(loss)
        adam_acc.append((y.argmax(axis=1) == t.argmax(axis=1)).mean())

f:id:konchangakita:20210109151141p:plain


まとめ

だんだんとそれっぽいカタチになってきました
パラメータ更新の方法以外にも、まだまだ学習精度を上げる方法はたくさんあるので、これからそちらへ