社畜エンジニア発掘戦線

駆けだしAIエンジニア

アルゴリズムの関数化1

おつかれさまです。

処理を関数にして外部のファイルにまとめてしまって、コードの流れをすっきり見やすくさせよう、という試みです。

練習として、これまで作ったプログラムを使って、その中に書かれている処理を関数に書き直して、外部ファイルへ分離してみます。とりあえず、MNISTのシンプルニューラルネットワークのコードでトライしてみたいと思います。

関数化にトライ

では、既存のコードを上からなぞっていきながら、処理を関数に書き換えていくことにします。ニューラルネットワークのコードを書くのは「MNIST_Perceptron.py」で、外部ファイルとして関数を書き溜めていきのは「function.py」とします。この2つのファイルは同じフォルダに格納されています。

f:id:sutokun:20190506111659p:plain:w400

まずは「MNIST_Perceptron.py」に「function.py」のインポートして2つのファイルを紐付けます。
▼MNIST_Perceptron.py

import function

その他、関数以外でも使う場面があるので、必要なライブラリもインポートしておきます。
▼MNIST_Perceptron.py

import numpy as np
import matplotlib.pyplot as plt
import time


「function.py」にもライブラリのインポートが必要です。関数の中で用いるライブラリをインポートしておきます。
▼function.py

import numpy as np
import matplotlib.pyplot as plt
import pickle
MNISTのデータロードの処理

最初はMNISTデータのロードからでした、まずはもとのコードを確認。

#load dataset
import pickle

save_file = '/Users/FUTOSHI/Desktop/MNIST_test/mnist.pkl'

with open(save_file, 'rb') as f:
    dataset = pickle.load(f)

train_img,train_label,test_img,test_label = dataset

次に、データセットの前処理を行いました。

#pixel normalization
X_train, X_test = train_img/255, test_img/255

#transform_OneHot
T_train = np.eye(10)[list(map(int,train_label))] 
T_test = np.eye(10)[list(map(int,test_label))]


「データロード〜前処理」は関数でひとまとめにできそうです。イメージはこんな感じ。

f:id:sutokun:20190506105619j:plain:w400

MNISTのpickleデータのPATHを入力変数として、出てくるのは前処理された各データX_train〜T_testまでです。なので、コードの骨格としては、こんな感じ。

def minist(PATH):
       ・・・・・・・・・
    return [X_train,T_train,X_test,T_test]


ではここの処理を関数に書き換えていきます。

まず、pickleライブラリのインポートは外に出してimportの部分でまとめて行うことにします、なので関数の中には含みません。上のコードで「save_file」で記述したPATH部分を「PATH」という入力変数にして書いていきます。それ以外はコピペです。
▼function.py

def mnist(PATH):
    #mnist_load
    save_file = PATH

    with open(save_file, 'rb') as f:
        dataset = pickle.load(f)

    train_img,train_label,test_img,test_label = dataset

    #pixel normalization
    X_train, X_test = train_img/255, test_img/255

    #transform_OneHot
    T_train = np.eye(10)[list(map(int,train_label))] 
    T_test = np.eye(10)[list(map(int,test_label))]

    return [X_train,T_train,X_test,T_test]

この処理を「function.py」に記述します。

では、「function.py」に記述した関数を用いて、「MNIST_Perceptron.py」の内容を書いてみたいと思います。

▼MNIST_Perceptron.py

#mnist_dataset
save_file = '/Users/FUTOSHI/Desktop/MNIST_test/mnist.pkl'
X_train,T_train,X_test,T_test = function.mnist(save_file)

なんと2行で書けてしまいました、これは見やすい。こんな感じで進めていきましょう。

MNISTデータの確認

少し本筋からはそれますが、データがきちんとロードされているか確認できるようにしておきましょう。データの内容(画像とラベルの出力)を確認するコードを書いてみます。

i=0
img = np.reshape(X_train[i],(28,28))

plt.figure()
plt.imshow(img, cmap='gray_r')
plt.show()

print(T_train[i])

iは60000枚のうち、何番目の画像を出力するか、という値です。とりあえず0番目を出力してみます。

f:id:sutokun:20190506113524p:plain:w400

[0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]

バッチリですね。ではこの内容を関数に落としていきます。

関数に入力する情報は画像のデータ(img)、正解ラベル(label)、そして順番(i)です。それぞれ「X_train、T_train、i」に対応しています。そして欲しい出力は、画像の表示(plt.show)と正解ラベルの表示(print(label))です。
▼function.py

def show(img,label,i):
    img = np.reshape(img[i],(28,28))

    plt.figure()
    plt.imshow(img, cmap='gray_r')
    plt.show()

    print(label[i])

このコードを「function.py」に記述して準備は完了です。「MNIST_Perceptron.py」でに下記のコードを記述して動作確認をしてみます。
▼MNIST_Perceptron.py

function.show(X_train,T_train,0)

記述するコードはこれだけ、同じ結果が返ってきます。うまくいっているかどうかの確認も少ないコードで行えるのは良いことです。

まだ全然序盤ですが、これだけでも十分スッキリさせられました。どんどん進んでいきましょう。

活性化関数と損失関数の処理

次はシグモイド関数と損失関数の記述です。ただ、ここはもともと関数として記述していたので、そのまま「function.py」に書いてしまいましょう。
▼function.py

def softmax(z):
    return np.exp(z) / np.sum(np.exp(z), axis=1, keepdims=True)

def loss(y, t):
    delta = 1e-7
    return -np.sum(np.multiply(t, np.log(y+delta)) + np.multiply((1 - t), np.log(1 - y+delta)))

今までと同じ記述です。

初期パラメータの設定とイテレーションの開始

ここまでいろいろと関数化して外部ファイルに逃してきましたが、ここはそのままです、触りようがないっす。
▼MNIST_Perceptron.py

#initial parameters
W = np.random.randn(784, 10)
B = np.random.randn(1,10)
learning_rate = 0.001

E_save = []
accuracy_save = []
start_time = time.time()

次にイテレーションの開始、
▼MNIST_Perceptron.py

#iteration
num_of_itr=3000
for i in range(num_of_itr):

ここもそのままですね。

ミニバッチ処理

続いてはミニバッチ処理です。まずはもとのコードの確認、イテレーションの中なのでインテンドされています。

    train_size = X_train.shape[0]
    batch_size = 100
    batch_mask = np.random.choice(train_size,batch_size)

    X_batch = X_train[batch_mask]
    T_batch = T_train[batch_mask]

ここも関数化できそうです。処理のイメージとしてはこんな感じ。

f:id:sutokun:20190507113055j:plain:w400
もとの画像とラベルデータ、バッチサイズを入力として、バッチサイズの数だけランダムにピックアップされたX_batchとT_batchを出力します。なので、関数の骨格としてはこんな感じ。

def batch(img,label,batch_size):
       ・・・・・・・・・
    return [X_batch,T_batch]

ではコードに書いてみます、といってもほとんどコピペですが。追記するファイルは「function.py」です。
▼function.py

def batch(img,label,batch_size):
    img_size = img.shape[0]
    batch_mask = np.random.choice(img_size,batch_size)

    X_batch = img[batch_mask]
    T_batch = label[batch_mask]

    return [X_batch,T_batch]

この関数を用いるためには「batch_size」を定義しておかなければいけないので、イテレーションの外の初期パラメータのところに足しておきます。
▼MNIST_Perceptron.py

batch_size = 100

ではこの関数を用いてミニバッチの処理を書いてみます。
▼MNIST_Perceptron.py

    X_batch,T_batch = function.batch(X_train,T_train,batch_size)

1行でまとまります、素晴らしい。

出力層の計算

今回は入力層と出力層しかないので、いきなり出力層を計算することになります。

    Y = softmax(np.dot(X_batch, W)+B)

もともと1行なので、このままでもええやん、という感じですが、分かりやすく体裁を整えるという意味で関数化してみます。

入力データはバッチ処理された画像、そしてパラメータWとBです。今回は画像を扱っているので画像データですが、パラメータとの行列の型さえあっていれば、別になんでもいいです。基礎問題でトライしたようなインプットデータでも大丈夫です。

ここは複雑ではないので一気にコード化してみます。記述するのは「function.py」です。
▼function.py

def affine(img,W,B):
    return softmax(np.dot(img, W)+B)

affinは全結合という意味(らしい)です。この関数の中でsoftmaxを使っているので、softmaxよりも下の行で記述しなければいけないので注意です。これでもとのコードを書きなおしてみます。
▼MNIST_Perceptron.py

    Y = function.affine(X_batch,W,B)

もともと1行なので、コード量は変わりませんが、意味するところは分かりやすくなりました。

損失量の計算

損失量Eについても同じように関数化してみます、こちらももともと1行なのでコード量の恩恵は少ないですが。

    E = loss(Y,T_batch)/len(Y)

入力は出力層で計算されたYと正解ラベル、今回はT_batchにあたります。
▼function.py

def error(Y,label):
    return loss(Y,label)/len(Y)

では、もとのコードを書きなおします。
▼MNIST_Perceptron.py

    E = function.error(Y,T_batch)

こちらもそのままですね。

あと、計算したEを保存する1行も付け足しておきます。これはもとのコードと同じです。
▼MNIST_Perceptron.py

    E_save = np.append(E_save, E)
精度の計算

続いては精度の計算です。もとのコードを確認してみます。

    Y_accuracy = np.argmax(Y, axis=1)
    T_accuracy = np.argmax(T_batch, axis=1)
    accuracy = 100*np.sum(Y_accuracy == T_accuracy)/batch_size

入力の情報は出力結果Y、そしてそれと比較するための正解ラベル、ここではT_batchになります。batch_sizeも必要ですが、Yの1列目がbatch_sizeにあたるので、この関数の中で再定義することにします。極力入力する変数は少なくしたいのです。ではこの内容を関数化してみます。
▼function.py

def accuracy(Y,label):
    batch_size = Y.shape[0]
    Y_accuracy = np.argmax(Y, axis=1)
    T_accuracy = np.argmax(label, axis=1)
    return 100*np.sum(Y_accuracy == T_accuracy)/batch_size

では、もとのコードを書きなおします。
▼MNIST_Perceptron.py

    Acc = function.accuracy(Y,T_batch)

ここは1行でまとまりました、素晴らしい、なんか関数名と変数名が一致するとエラーが出るっぽいのでAccにして回避しています。

保存するコードも足しておきます。
▼MNIST_Perceptron.py

    accuracy_save = np.append(accuracy_save, Acc)
パラメータの更新

次はパラメータの更新です。とりあえずもとのコードをみてみます。

    dW = np.dot(X_batch.T,(Y-T_batch))
    dB = np.reshape(np.sum(Y-T_batch, axis=0),(1,10))

    W = W - learning_rate*dW
    B = B - learning_rate*dB

ここも1行ずつしかないので関数化によるメリットが少ないんですよねぇ、まぁここまでやったし、やります。

まずは微分式のところから。
▼function.py

def delta_w(img,label,Y):
    return np.dot(img.T,(Y-label))

def delta_b(label,Y):
    return np.reshape(np.sum(Y-label, axis=0),(1,10))

それからパラメータの更新、
▼function.py

def update(p,learning_rate,delta):
    return p - learning_rate*delta

これとかもう、そのまんまですし、お寿司。

まあ、とりあえずこれらを使ってもとのコードを書き直してみましょう。
▼MNIST_Perceptron.py

    dW = function.delta_w(X_batch,T_batch,Y)
    dB = function.delta_b(T_batch,Y)

    W = function.update(W,learning_rate,dW)
    B = function.update(B,learning_rate,dB)

もう微分式と更新式をまとめて書いています。ごちゃごちゃした計算式がないので、ぱっと見は分かりやすそうです。

これでイテレーション部分の記述は完成です。最後に計算時間を測定するコードだけ足しておきます。ここは関数とか関係ないです。

end_time = time.time()
time = end_time - start_time
print(time)
グラフ化

学習の計算については記述が完了しました。あとはその結果をグラフ化するだけです。

ここも流れは同じです、もとのコードをみてみます。

#plot
plt.figure()
plt.title('ACCURACY')
plt.xlabel("LEARNING NUMBER(EPOCH)")
plt.ylabel("ACCURACY (%)")
plt.xlim(0, 3000)
plt.ylim(0, 100)
plt.grid(True)
plt.plot(accuracy_save, color='blue')
plt.show()

まずは精度のグラフ、プロットするデータはaccuracy_saveです。ここも関数化するわけですが、まぁほとんどコピペです。入力データはaccuracy_saveだけです。
▼function.py

def plot_acc(accuracy_save):
    plt.figure()
    plt.title('ACCURACY')
    plt.xlabel("LEARNING NUMBER(EPOCH)")
    plt.ylabel("ACCURACY (%)")
    plt.xlim(0, 3000)
    plt.ylim(0, 100)
    plt.grid(True)
    plt.plot(accuracy_save, color='blue')
    plt.show()

損失量のグラフについても同じようにグラフ化してみます。
▼function.py

def plot_loss(E_save):
    plt.figure()
    plt.title('LOSS FUNCTION')
    plt.xlabel("LEARNING NUMBER(EPOCH)")
    plt.ylabel("LOSS VALUE")
    # plt.xlim(0, 3000)
    # plt.ylim(0, 100)
    plt.grid(True)
    plt.plot(E_save, color='blue')
    plt.show()

あとはこの関数をもとのコードに記述するだけ、
▼MNIST_Perceptron.py

#show graph
function.plot_acc(accuracy_save)
function.plot_loss(E_save)

ここはずいぶんスッキリしました、グラフのテンプレはいくつか作っておいてもいいかも。

プログラムの実行

さてさて、関数を使ってコードをスッキリさせることができたので、後は動作確認をしてみます。

結果↓

f:id:sutokun:20190512103545p:plain:w400
f:id:sutokun:20190512103635p:plain:w400
計算時間↓

3.68457293510437

おお、以前と同じような結果が得られました、ひとまずは成功のようです。計算時間はちょっと遅くなったような…、関数を外に出したので、呼び出しにけっこう時間がかかるかなと思っていましたが、まぁ許容の範囲内ですかね。重たい計算をさせたときにどれぐらい差が出るのかってのを調べてみないといけないんですけどね。

まとめ

ひとまず関数化して外部ファイルにコードを逃がすことで、ニューラルネットワークの本質的な内容でコードを記述することができました。今回の練習で関数をたくさんつくりましたが、まだまだ足りないようです。ニューラルネットワークの学習を進める上でも必要なアルゴリズムをどんどん関数化してfunction.pyを大きくし、汎用性の高いファイルにできればと思います。

おそらく、何も考えずにファイルを大きくしていては計算時間の問題も出てくると思うので、そこは都度考慮しながら進めていきたいと思います。

とりあえず、もとのコードと今回編集したコードを載せておくので参考にしてみて下さい。

もとのコード
github.com

アレンジ後:function.py
github.com

アレンジ後:MNIST_Perceptron.py
github.com

他にもニューラルネットワークの形成に使えて、関数化できそうなアルゴリズムもあるので、もう少しトライしてみたいと思います。


元の記事
週末のディープラーニング - 社畜エンジニア発掘戦線

Twitter
世界の社畜 (@sekai_syachiku) | Twitter