PyLearn Keras 05: CNNで回帰問題を解く(mnist)

注意: 今回の説明は、PyLearn Keras 03: モノクロ画像に対するCNNの学習を行なった後の方がわかりやすいかもしれない。

ここでは、画像を入力とするCNNにより、回帰問題を解く方法について解説する。このため、まず、クラス分類問題と回帰問題の違いを復習しておく。

クラス分類問題: → 状態を判定する問題。例えば、

  • 機械の振動から、故障している、故障していないを判定
  • 画像から、犬、猫、馬、どれが写っているかを判定

回帰問題: → 連続状態を判定する問題。例えば、

  • 機械の振動から、故障するまでの期間を当てる
  • 人の顔の画像から、年齢を当てる
  • 画像から、距離を当てる

この例を見ればわかるように、画像から何かしらの数値を回帰できるというモデルは、色々と便利なことができる可能性を有している。ただし、回帰問題は、数学的に考えると、選択肢が無限大のクラス分類問題とみなせるので、性能を高めるには相当の努力が必要な場合が多い。

なお、回帰問題でもいいし、クラス分類問題でもいいという類の問題もある。例えば、

人の顔画像から、年齢を予測する問題は、

  • 「10代、20代、30代」や「若い、高齢」などと判定するモデルならば、クラス分類問題
  • 13歳、25歳、などと判定するモデルならば、回帰問題

となる。この場合は、色々やってしっくりくるものを実際の現場で使用することになる。

今回はmnist のデータセットを使用して回帰問題を解くCNNを構築する。ただし、mnistを回帰問題で解くというのは、チョイスとしてはあまり良くない。というのは、画像上の3と4のには、連続関係など一切ないためである。したがって、回帰モデルとして構築した際、仮に3.5という予想結果があったときに、そのモデルは3か4で迷っていることを意味しない。一方で、クラス分類問題として解いた場合に、3番目に相当する次元と4番目に相当する次元がどちらも高ければ、3と4のどちらかで迷っていると解釈できる。このような考察が可能になるので、明らかにクラス分類問題の方が良い。今回はあくまでデータを用意しやすいからmnistを回帰問題として解いているだけであることには注意されたい。

1. 前処理

まずは、バックエンドをplaidmlにします。

In [1]:
# kerasのバックエンドを plaidmlに変更
import plaidml.keras
plaidml.keras.install_backend()

import keras
print(keras.backend.backend())
plaidml

そして、mnistデータをロードします。

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

入力画像の縦と横のサイズを得て、画像の値を0-255スケールから0-1スケールに規格化します。

In [3]:
# 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

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

CNNで扱えるデータの形式になっているか、チェックします。

In [5]:
import numpy as np
num_train = len(x_train[:, 0,0]) # 教師データの数
num_test = len(x_test[:, 0,0]) # テストデータの数
size_rows = len(x_train[0, :,0]) # 画像、行サイズ
size_cols = len(x_train[0, 0, :]) # 画像、列サイズ

print("x_trainのshape: ", np.shape(x_train))
x_train_cnn = np.reshape(x_train, [num_train, size_rows, size_cols, 1])
x_test_cnn = np.reshape(x_test, [num_test, size_rows, size_cols, 1])
print("x_train_cnnのshape: ", np.shape(x_train_cnn))
x_trainのshape:  (60000, 28, 28)
x_train_cnnのshape:  (60000, 28, 28, 1)

ここまで述べたことは、すべて入力画像側のお話です。それで、PyLearn Keras 03: モノクロ画像に対するCNNの学習とまったく一緒です。

今回はクラス分類問題ではなく、回帰問題で解くので、出力の正解値の形式が変わってきます。

mnistをクラス分類問題として解く場合と、回帰問題として解く場合の違いは、正解値をOne-hot vevtorにするかどうかです。

  • クラス分類問題の場合の正解値(10次元のベクトル)
    • 数字の0であれば、 [1, 0, 0, 0, ...]
    • 数字の1であれば、 [0, 1, 0, 0, ...]
    • 数字の2であれば、 [0, 0, 1, 0, ...]
  • 回帰問題の場合の正解値(1次元の実数でok)
    • 数字の0であれば、 0
    • 数字の1であれば、 1
    • 数字の2であれば、 2

mnistのデータセットでは、x_train, x_testに相当する画像の正解値が、y_train, y_testにそのまま格納されているため、そのまま使用すれば良いです。もし新たなモデルを作りたいとき、例えば、人の顔の画像から年齢を予測したいのならば、y_train, y_testに、x_train, x_testに格納されている顔の画像の順に、年齢をそのまま入れておけば良いです。

In [6]:
print(y_train)
print(y_test)
[5 0 4 ... 5 6 8]
[7 2 1 ... 4 5 6]

2. CNNの構築

mnistをクラス分類問題として解く場合は、0〜9の数字を判定させるので、個々の数字の確信度が出力となることから、出力層を

  • model.add(Dense(10, activation='softmax'))

としていました。1つの画像を入力すると、10個の数字が出力される、つまり、先ほど述べたOne-hot vectorの次元数と合わせているわけです。また、10次元のベクトルの数字の総和が1になるように、softmax関数を利用していました。

回帰問題の場合は、1枚の画像の入力に対し、1つの実数を出力します。したがって、出力の次元は1となります。また、1つの数字の総和を1にすると、どうやっても1という出力しか行えなくなってしまいますから、softmaxを指定するのはなしです。回帰問題の場合、出力値として、-無限大から無限大までを出力できることが望ましいので、activationにlinearを設定します。linearとは、線形という意味で、数学上の定義とはちょっと違いますが、計算された数字を特に加工せずにそのまま出す、といういいがあります。

  • model.add(Dense(10, activation='softmax')) → 正解ラベルの次元数と一致しないから、エラー
  • model.add(Dense(1, activation='softmax')) → 正解ラベルの次元数と一致するため、エラーにはならないが、1しか出力できないひどいモデルが出来上がる。
  • model.add(Dense(1, activation='linear')) → 正解ラベルの次元数と一致するし、-無限大から無限大までを出力できるので、ok

色々書きましたが、回帰モデルならば、出力層は、1次元、linearを使うと覚えておけば間違い無いです。

In [7]:
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D

model = Sequential()
model.add(Conv2D(filters=1, kernel_size=(5, 5), activation='relu', input_shape=(28, 28, 1)))
model.add(Conv2D(filters=1, kernel_size=(5, 5), activation='relu'))
model.add(Conv2D(filters=1, kernel_size=(5, 5), activation='relu'))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(30, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(1, activation='linear')) # ← ここが出力層!
INFO:plaidml:Opening device "metal_intel(r)_hd_graphics_615.0"

続いて、学習に進みます。CNNの学習では、頭の良さの定義付けが必要です。クラス分類問題では、10個の選択肢の中から1つを選び、正解だったら頭が良いということで、いわゆる正答率をなんらかの形で加工したものを頭の良さと定義していました(ごまかしているのは、加工の仕方がたくさんにあるためです)。

一方、回帰問題の場合、正答率ではなく、予測と正解の実数値上のずれ(実測が3、予測が3.4なら、ズレは0.4)が少ないほど頭が良いと定義すべきです。このように、頭の良さの定義を変更することが必要です。

  • クラス分類問題: model.compile(loss='categorical_crossentropy', optimizer=RMSprop(), metrics=['accuracy'])
  • 回帰問題: model.compile(loss="mean_squared_error", optimizer=RMSprop())

とすれば、okです。mean_squared_errorとは、平均平方誤差という意味です。ズレとは、マイナスのずれとプラスのずれがありますので、それを2乗で符号をキャンセルし、平方根をとって2乗をキャンセルし、最後にすべてのデータの平均をとることを意味しています。

In [8]:
# コンパイル
model.compile(loss="mean_squared_error", optimizer=RMSprop())
model.summary() # 構造出力
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 24, 24, 1)         26        
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 20, 20, 1)         26        
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 16, 16, 1)         26        
_________________________________________________________________
dropout_1 (Dropout)          (None, 16, 16, 1)         0         
_________________________________________________________________
flatten_1 (Flatten)          (None, 256)               0         
_________________________________________________________________
dense_1 (Dense)              (None, 30)                7710      
_________________________________________________________________
dropout_2 (Dropout)          (None, 30)                0         
_________________________________________________________________
dense_2 (Dense)              (None, 1)                 31        
=================================================================
Total params: 7,819
Trainable params: 7,819
Non-trainable params: 0
_________________________________________________________________

学習条件の設定が終わったら、実際の学習に進みます。ここはクラス分類問題のときと同様です。

In [9]:
# plaidmlをバックエンドにする場合は、バッチサイズを「2^n+1」にすること
# それ以外だと、lossがnanになる模様
batch_size = 129
epochs = 10 # 学習回数

learning_process = model.fit(x_train_cnn, y_train,  # 画像とラベルデータ
                                batch_size=batch_size, # バッチサイズ
                                epochs=epochs,     # エポック数
                                verbose=1)#, # 学習過程を表示する1, しない0
Epoch 1/10
60000/60000 [==============================] - 13s 211us/step - loss: 6.8413
Epoch 2/10
60000/60000 [==============================] - 8s 126us/step - loss: 4.6389
Epoch 3/10
60000/60000 [==============================] - 6s 100us/step - loss: 4.2300
Epoch 4/10
60000/60000 [==============================] - 5s 88us/step - loss: 3.9271
Epoch 5/10
60000/60000 [==============================] - 5s 88us/step - loss: 3.6751
Epoch 6/10
60000/60000 [==============================] - 6s 98us/step - loss: 3.4812
Epoch 7/10
60000/60000 [==============================] - 5s 91us/step - loss: 3.3259
Epoch 8/10
60000/60000 [==============================] - 6s 95us/step - loss: 3.2244
Epoch 9/10
60000/60000 [==============================] - 8s 141us/step - loss: 3.0885
Epoch 10/10
60000/60000 [==============================] - 8s 129us/step - loss: 3.0045

lossが減少していればokです。もっと下げたい人は、epochを増やしたり、モデル構造を変えたりしてみてください。このページの結果はまだまだ不十分ですが、これでテストデータを予測してみましょう。

In [10]:
test_pred = model.predict(x_test_cnn, batch_size=32+1) # バッチサイズには2^n+1

view_num = 20 # 表示枚数 
error = []
for i in range(view_num):
    error.append( np.abs(y_test[i] - test_pred[i]) )
    print("実測: ", y_test[i], " / 予測: ", np.round(test_pred[i], 2), " / ズレ: ", np.round(error[i], 2))
    
print("\nズレの平均: ", np.mean(error))
実測:  7  / 予測:  [6.38]  / ズレ:  [0.62]
実測:  2  / 予測:  [1.75]  / ズレ:  [0.25]
実測:  1  / 予測:  [1.34]  / ズレ:  [0.34]
実測:  0  / 予測:  [1.78]  / ズレ:  [1.78]
実測:  4  / 予測:  [4.38]  / ズレ:  [0.38]
実測:  1  / 予測:  [1.51]  / ズレ:  [0.51]
実測:  4  / 予測:  [5.42]  / ズレ:  [1.42]
実測:  9  / 予測:  [5.99]  / ズレ:  [3.01]
実測:  5  / 予測:  [4.72]  / ズレ:  [0.28]
実測:  9  / 予測:  [8.29]  / ズレ:  [0.71]
実測:  0  / 予測:  [0.66]  / ズレ:  [0.66]
実測:  6  / 予測:  [4.63]  / ズレ:  [1.37]
実測:  9  / 予測:  [8.04]  / ズレ:  [0.96]
実測:  0  / 予測:  [1.65]  / ズレ:  [1.65]
実測:  1  / 予測:  [1.84]  / ズレ:  [0.84]
実測:  5  / 予測:  [3.55]  / ズレ:  [1.45]
実測:  9  / 予測:  [8.35]  / ズレ:  [0.65]
実測:  7  / 予測:  [5.86]  / ズレ:  [1.14]
実測:  3  / 予測:  [4.76]  / ズレ:  [1.76]
実測:  4  / 予測:  [4.3]  / ズレ:  [0.3]

ズレの平均:  1.0043472

回帰問題のベーシックな解説は以上になります。

以下は、過去のkerasの資料に書いていますので、ご参照ください。

  • 1枚ごとに予測したい
  • モデルを外部ファイルに保存したい
  • lossカーブを描きたい