PyLearn Keras 01: Macbook/GPUによる深層学習

ここでは、Mac系のOSで深層学習を実施する方法について言及します。はじめに、最も基本的なモデルとして、画像に記載されている数字を当てるという問題を扱います。

0. 準備

こちらをご参照ください。

1. mnistデータセットの取得と描画

mnistデータを使用して、0〜9の手書き文字を当てるモデルを構築してみます。まずは、kerasのバックエンドをチェックします。ここで、plaidmlが帰って来ればokです。もしもtensorflowになっている場合は、MacではCPUで深層学習を行うことになるので、相当時間がかかりますので、注意してください。バックエンドが変わらない問題に遭遇したら、上にある「0.準備」の下の方を熟読のこと。

In [1]:
import keras
print(keras.backend.backend())
plaidml.keras.backend
Using plaidml.keras.backend backend.

続いて、mnistのデータを取得します。

In [2]:
from keras.datasets import mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()

以下の感じで代入できます。教師・テストデータの意味がわからない人は機械学習の項を勉強してください。

  • x_train: 教師データ(画像: 手書きの数字)
  • y_train: 教師データ(正答値: 0〜9までの数字)
  • x_test: テストデータ(画像: 手書きの数字)
  • y_test: テストデータ(正答値: 0〜9までの数字)

shapeで形状を見ると、構造がわかります。

In [3]:
x_train.shape
Out[3]:
(60000, 28, 28)

28x28ピクセルサイズの画像が、6万枚あることを意味しています。y_train[i]と指定することで、x_train[i, :, :]にある画像に書かれている数字の正答値を知ることができます。また、matplotlibのimshow関数を用いることで、画像を可視化できます。

In [4]:
import matplotlib.pyplot as plt
print("0枚目の画像に書かれている数字: ", y_train[0])
plt.imshow(x_train[0, :, :], cmap='gray')
plt.show()
0枚目の画像に書かれている数字:  5

2. 前処理

今回は、28x28サイズの画像を、784次元のベクトルに変換し、それを入力する階層型のニューラルネットワークを組んでみます。

  • 処理1: 28x28サイズの画像を、784次元のベクトルに変換
  • 処理2: 0-255レンジを0-1レンジに変換
  • 処理3: クラスラベルをカテゴリカル変数に変換

処理2は画像データの値の規格化です。グレースケール画像は0が黒、255が白ですが、ニューラルネットワークは値の範囲が規格化されていないとうまく学習できない場合が多いです。したがってここで規格化しておきます。また、処理3を行わないと、エラーが出るので注意が必要です。

In [5]:
import keras
from keras.models import Sequential
from keras.layers import Dense, Dropout
from keras.optimizers import RMSprop

# 各種サイズ取得
num_train = len(y_train) # 教師データ数
num_test = len(y_test) # テストデータ数
print("教師データサイズ: ", num_train)
print("テストデータサイズ: ", num_test)
size_0 = x_train.shape[1] # 横サイズ
size_1 = x_train.shape[2] # 縦サイズ
dim_input_vec = size_0 * size_1

# 画像(行列)をベクトルに変換
x_train = x_train.reshape(num_train, dim_input_vec)
x_test = x_test.reshape(num_test, dim_input_vec)
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')

# 規格化
# グレースケール画像の場合、0が黒、255が白となっている。
# ニューラルは値のレンジが0〜1が望ましいので、そのように規格化
x_train = x_train / 255
x_test = x_test / 255

# クラス分類問題なので、クラスラベルをカテゴリカル変数に変換
num_class = 10
y_train = keras.utils.to_categorical(y_train, num_class)
y_test = keras.utils.to_categorical(y_test, num_class)
教師データサイズ:  60000
テストデータサイズ:  10000

3. 深層学習のモデル定義(多層型ニューラルネットワーク)

今回は、以下の構造を持つ深層学習モデルを定義してみます。

  • 1層目: dim_input_vec次元ベクトル(入力層)
  • 2層目: 200次元ベクトル(中間層/隠れ層1)
  • 3層目: 100次元ベクトル(中間層/隠れ層2)
  • 4層目: num_class次元ベクトル(出力層)

活性化関数はすべてReLuにしました。入力層と出力層の次元は解く問題に依存するので、確定値です。自由には決められないので注意してください。中間層の次元は自由に決められます。なお、出力層の活性化関数は必ずsoftmaxにしなければなりません。これは、0〜9個あるニューロンのうち、最大値となるニューロンを取り出すという処理により、クラス分類を確定させるためです。

compileで記述している部分は、機械学習の理論を読んでいないと理解できないので、ここでは解説しません。ニューラルの基礎理論を理解した人のみ、聴きに来てください。

In [15]:
# モデル構造の定義
model = Sequential()
model.add(Dense(200, activation='relu', input_shape=(dim_input_vec,)))
model.add(Dropout(0.2))
model.add(Dense(100, activation='relu'))
model.add(Dropout(0.2))
model.add(Dense(num_class, activation='softmax'))

model.summary() # 構造出力

# 最適化法などの定義
model.compile(loss='categorical_crossentropy', optimizer=RMSprop(), metrics=['accuracy'])
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_10 (Dense)             (None, 200)               157000    
_________________________________________________________________
dropout_7 (Dropout)          (None, 200)               0         
_________________________________________________________________
dense_11 (Dense)             (None, 100)               20100     
_________________________________________________________________
dropout_8 (Dropout)          (None, 100)               0         
_________________________________________________________________
dense_12 (Dense)             (None, 10)                1010      
=================================================================
Total params: 178,110
Trainable params: 178,110
Non-trainable params: 0
_________________________________________________________________

metal_intel(r) hd graphics 615.0 と出てきました。ここで使用されているCPU/GPUが出てきます。なお今回は、しょぼいintel製GPUが出てきています。

Paramの総数が学習に大きな影響を与えるので、注意してみてください。詳しくはバイアス・バリアンス分解の理論を参照。

  • Paramたくさん: 教師データが莫大にある場合、高い精度になりやすいが、教師データが少ない場合は精度が悪くなりやすい。
  • Paramすくない: 教師データが少なくても精度が安定しやすい。一方、極端に高い精度にはなりにくい。

ちなみに、dense_1のparamは157000になっています。これは、dim_input_vec次元(784次元)ベクトルを、200次元ベクトルに変換する際、200x784サイズのウェイト行列を、200サイズのバイアスベクトルがパラメータになるためです。200x784+200で157000になります。詳しくは、ニューラルネットワークの理論を読んでいる人しかわからないことですので、理論はほっとく人は気にしないでいいです。

4. モデルの学習開始

続いて、学習を開始します。深層学習はかなりたくさんのパラメータがあります。詳しくはscikit-learnのニューラルネットワークを解説している項で説明しているので、そちらを参照ください。

In [16]:
# loss が nan になる場合には、batch_sizeを適宜変更しましょう。
# plaidmlの場合、2^n + 1 が推奨らしい? web参照
# 再度やり直す場合には、model = Sequential()のコードを再実行してからにします。
# そうしないと、前の学習からの続きになり、nanからのスタートになってしまいます。
# nan問題は、issue 168を参照 → https://github.com/plaidml/plaidml/issues/168

batch_size = 129
epochs = 20 # 学習回数

learning_process = model.fit(x_train, y_train,  # 画像とラベルデータ
                                batch_size=batch_size, # バッチサイズ
                                epochs=epochs,     # エポック数
                                verbose=1, # 学習過程を表示する1, しない0
                                validation_data=(x_test, y_test) # <- 検証データの指定
                            )
Train on 60000 samples, validate on 10000 samples
Epoch 1/20
60000/60000 [==============================] - 10s 172us/step - loss: 0.3462 - acc: 0.8959 - val_loss: 0.1677 - val_acc: 0.9473
Epoch 2/20
60000/60000 [==============================] - 10s 160us/step - loss: 0.1560 - acc: 0.9537 - val_loss: 0.1084 - val_acc: 0.9658
Epoch 3/20
60000/60000 [==============================] - 11s 189us/step - loss: 0.1155 - acc: 0.9656 - val_loss: 0.0888 - val_acc: 0.9724
Epoch 4/20
60000/60000 [==============================] - 10s 161us/step - loss: 0.0938 - acc: 0.9714 - val_loss: 0.0873 - val_acc: 0.9741
Epoch 5/20
60000/60000 [==============================] - 10s 160us/step - loss: 0.0815 - acc: 0.9753 - val_loss: 0.0793 - val_acc: 0.9770
Epoch 6/20
60000/60000 [==============================] - 10s 167us/step - loss: 0.0704 - acc: 0.9784 - val_loss: 0.0879 - val_acc: 0.9751
Epoch 7/20
60000/60000 [==============================] - 11s 177us/step - loss: 0.0637 - acc: 0.9810 - val_loss: 0.0742 - val_acc: 0.9804
Epoch 8/20
60000/60000 [==============================] - 12s 204us/step - loss: 0.0580 - acc: 0.9825 - val_loss: 0.0772 - val_acc: 0.9780
Epoch 9/20
60000/60000 [==============================] - 10s 161us/step - loss: 0.0549 - acc: 0.9831 - val_loss: 0.0850 - val_acc: 0.9782
Epoch 10/20
60000/60000 [==============================] - 9s 152us/step - loss: 0.0511 - acc: 0.9842 - val_loss: 0.0789 - val_acc: 0.9799
Epoch 11/20
60000/60000 [==============================] - 12s 200us/step - loss: 0.0454 - acc: 0.9858 - val_loss: 0.0766 - val_acc: 0.9792
Epoch 12/20
60000/60000 [==============================] - 10s 161us/step - loss: 0.0445 - acc: 0.9866 - val_loss: 0.0815 - val_acc: 0.9791
Epoch 13/20
60000/60000 [==============================] - 14s 240us/step - loss: 0.0409 - acc: 0.9872 - val_loss: 0.0854 - val_acc: 0.9801
Epoch 14/20
60000/60000 [==============================] - 12s 200us/step - loss: 0.0396 - acc: 0.9879 - val_loss: 0.0910 - val_acc: 0.9793
Epoch 15/20
60000/60000 [==============================] - 11s 183us/step - loss: 0.0361 - acc: 0.9897 - val_loss: 0.0871 - val_acc: 0.9819
Epoch 16/20
60000/60000 [==============================] - 11s 181us/step - loss: 0.0348 - acc: 0.9894 - val_loss: 0.0934 - val_acc: 0.9806
Epoch 17/20
60000/60000 [==============================] - 10s 171us/step - loss: 0.0351 - acc: 0.9899 - val_loss: 0.0819 - val_acc: 0.9816
Epoch 18/20
60000/60000 [==============================] - 10s 171us/step - loss: 0.0333 - acc: 0.9898 - val_loss: 0.0911 - val_acc: 0.9817
Epoch 19/20
60000/60000 [==============================] - 9s 157us/step - loss: 0.0310 - acc: 0.9901 - val_loss: 0.0886 - val_acc: 0.9817
Epoch 20/20
60000/60000 [==============================] - 10s 174us/step - loss: 0.0315 - acc: 0.9902 - val_loss: 0.0924 - val_acc: 0.9812

CPUとGPUで処理時間を比べてみると、大前の環境では以下のようになりました。安価なGPUでも、CPUよりも速いことがわかります。

1エポック所要時間

  • CPU(llvm_cpu.0): 50秒
  • GPU(metal_intel(r) hd graphics 615.0): 10秒

速く終わりたかったので、10エポックにしましたが、精度が悪い場合は100や1000など、大きくしてみてください。エポックとは、勾配法でコスト関数が減少するパラメータの方向を探す際、教師データセットをどのように分割するかに関する話ですが、理論を読んでないとわからないと思うのでこれもここでは説明しません。学習回数くらいの理解で良いと思います。

ここで、validation dataという記載があります。これは、学習に使用していないデータであり、学習過程の「val_acc, val_loss」などがvalidation dataを使用した際の性能になります。ニューラルネットワークにおける学習は、x_train, y_trainの性能を高めることを意味しますが、最終目的はあくまで未知のデータに当てられるかどうかです。したがって、validation dataの性能を逐次チェックし、学習に使用していないデータでも当てられるのか?ということを随時確認しているわけです。

ただし、本来、validation dataにテストデータを割り当てるのはNGな行為なので注意してください。テストデータは構築したAIの性能を測るためのデータなので、学習時にチラチラ見るのは、本来はいけないことです。きちんとやりたい場合は、テストデータをvalidation dataにするのではなく、教師データの一部をvalidation dataにしましょう。

5. 学習結果の可視化

続いて、学習結果を可視化してみます。可視化する際は、learning_processをprintしてみて、何が格納されているのかみてから行うとわかりやすいです。

In [18]:
import numpy as np

plt.figure()
plt.plot(learning_process.epoch, np.array(learning_process.history['acc']),label='Train acc')
plt.plot(learning_process.epoch, np.array(learning_process.history['val_acc']),label='Validation acc')
plt.xlabel('Epoch')
plt.ylabel('Acc')
plt.grid()
plt.legend()

plt.figure()
plt.plot(learning_process.epoch, np.array(learning_process.history['loss']),label='Loss')
plt.plot(learning_process.epoch, np.array(learning_process.history['val_loss']),label='Validation loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.grid()
plt.legend()
Out[18]:
<matplotlib.legend.Legend at 0x122b72e80>

エポックが進むごとに、精度が向上していることを確認できました。重要なのはバリデーションの精度ですので、注意してください。もし、Loss/Accが改善しているのに、Validation loss/accが悪くなっている場合は、過学習が疑われます。

精度は以下のコードで算出できます。

In [24]:
score = model.evaluate(x_test, y_test, verbose=1, batch_size=129) # バッチサイズには2^n+1
print('Loss (test) :', score[0])
print('Accuracy (test) :', score[1])
10000/10000 [==============================] - 1s 70us/step
Loss (test) : 0.09239942717057392
Accuracy (test) : 0.9811999910354614
In [25]:
score = model.evaluate(x_train, y_train, verbose=1, batch_size=129) # バッチサイズには2^n+1
print('Loss (test) :', score[0])
print('Accuracy (test) :', score[1])
60000/60000 [==============================] - 3s 52us/step
Loss (test) : 0.007018999962608223
Accuracy (test) : 0.9979999990463256

6. 予測値の取得

入力された画像に対する予測値を得るには、以下のように記載します。 テストデータのi番目の画像に対する推定値を得ています。

In [26]:
i = 0
test_pred = model.predict(x_test[i:i+1,:], batch_size=32+1) # バッチサイズには2^n+1
res = np.argmax(test_pred[i]) # <- 何番目のニューロンの値が一番大きいか?
print("Predict: ", res)
print("True value: ", y_test[i])
Predict:  7
True value:  [0. 0. 0. 0. 0. 0. 0. 1. 0. 0.]

推定値が7で、正答値も7番目のニューロンが大きいです。したがって、正しく回答できていることがわかりました。本当にそうなのか、みてみます。28x28サイズの画像を784次元にしているので、28x28サイズにリシェイプし、描画してみます。

In [27]:
img = np.reshape(x_test[i, :], [size_0, size_1])
plt.imshow(img, cmap="gray")
Out[27]:
<matplotlib.image.AxesImage at 0x123415dd8>

確かに7が出てきました。本当に正しいことがわかりました。

7. モデルの保存とロード

何かしらに組み込む場合は、開発したモデルを保存し、ロードしながら使用することができねばなりません。エラーが出る場合は、pip3 install h5pyが必要かもしれません。

保存は以下の通りです。

In [28]:
model_json_str = model.to_json()
open('mnist_model.json', 'w').write(model_json_str)
model.save_weights('mnist_weights.h5')

ロードは以下の通りです。

In [29]:
from keras.models import model_from_json
# モデルのロード
load_mod = model_from_json(open('mnist_model.json').read())
# 学習済みパラメータの書き込み
load_mod.load_weights('mnist_weights.h5')

ロードしたモデルに予測させてみます。

In [30]:
i=0
res_load = load_mod.predict(x_test[i:i+1,:], batch_size=32+1) # バッチサイズには2^n+1
print(np.argmax(res_load))
7

一度保存させたモデルも、0番目の画像は7だと答えてくれました。