Python
MachineLearning
DeepLearning
Keras
TensorFlow

怠け者のためのディープラーニング入門 - 性能指標・検証手法

はじめに

モデルの評価に使われる性能指標と検証手法を紹介します。

  • イントロ
  • Scikit-learn・Keras
  • モデルの性能指標・評価方法 ← 今ココ!
  • データの前処理・データ拡張
  • 早期終了(early stopping)・正則化
  • 転移学習
  • ハイパーパラメータのチューニング
  • モデル圧縮
  • 応用

性能指標

TensorFlowやKerasなどの入門記事を読むと、サンプルコードの中で示されるニューラルネットの評価指標としてAccuracy(正解率、ややこしいが精度とも)だけが使われているケースが多いです。MNISTのようなベンチマークやKaggleでも、性能指標は統一されています。

今ここであるインプットに対してすべてAクラスと返すAIを考えてみます。

ai5.png
図1 : すべて負クラスと答えるAI

入力には負クラスに該当するものが9個、正クラスに該当するものが1個あります。このAIはすべて負クラスと返すので正解率は90%となります。はたして彼は優秀なAIと呼べるのでしょうか?正解率だけではAIの優秀さは測れません。他の指標からも考える必要があります。

陽性と陰性

正解率や適合率、再現率といった指標は真陽性(TP)や偽陰性(TN)といった値から求めることができます。それぞれを説明するのは手間なので、以下の図で理解していただけたら幸いです。

ai.png
図2 : 陽性と陰性

具体例(図3)を見てみましょう。

ai4.png
図3 : とあるAIの予測とその答え

  • $TP=3$
  • $FP=2$
  • $FN=2$
  • $TN=1$

正解率(Accuracy)

正しい予測を予測の合計で割った値。先に見たとおりデータのクラスに偏りがあるとあてにならない。

$$Accuracy=\frac{TP+TN}{TP+FP+FN+TN}$$

例 :

$$Accuracy=\frac{TP+TN}{TP+FP+FN+TN}=\frac{3+1}{3+2+2+1}=0.5$$

誤分類率(Error rate)

LeCun先生のMNISTのページとかに使われていたりする。

$$Error=1-Accuracy$$

適合率(Precision)

偽陽性をチェックします。 病気ではないのに病気と誤診してしまう可能性を表す(陽性:病気、陰性:非病気)。低いほど誤診しやすい。

$$Precision=\frac{TP}{TP+FP}$$

例 :

$$Precision=\frac{3}{3+2}=0.6$$

再現率(Recall)

偽陰性をチェックします。病気なのに病気でないと誤診してしまう可能性を表す。低いほど誤診しやすい。

$$Recall=\frac{TP}{TP+FN}$$

例 :

$$Recall=\frac{TP}{TP+FN}=\frac{3}{3+2}=0.6$$

F1値(F1 score、F1スコア)

適合率と再現率を要約する。この値が大きいほどよいとされる。(E値から1を引いたものでもある)

$${F1}=\frac{2\cdot Recall\cdot Precision}{Recall+Precision}$$

例 :

$$F1=\frac{2 \cdot Recall \cdot Precision}{Recall+Precision}=\frac{2 \cdot 0.6 \cdot 0.6}{0.6+0.6}=0.6$$

おまけ:すべて負と答えるAIは賢いといえるのか

図1をもとにまとめると

  • $TP=0$
  • $FP=0$
  • $FN=1$
  • $TN=9$

となるので以下のようになる。

  • $Accuracy=0.9$
  • $Precision=0.0$
  • $Recall=0.0$
  • $F1=0.0$

したがって、F1値が低いため賢くないと判断できますが、データが負クラスに偏っているため決断を急がないでデータ収集をしたほうがいいように思われます。

Scikit-learnのサンプルコード

import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.utils import shuffle
from sklearn import svm
from sklearn.metrics import precision_score
from sklearn.metrics import recall_score
from sklearn.metrics import f1_score

np.random.seed(1234)
n_data = 200
n_class = n_data//2
X_positive = np.random.normal(loc=1.0, scale=1.0, size=(n_class, 2))
X_negative = np.random.normal(loc=-1.0, scale=1.0, size=(n_class, 2))
X = np.r_[X_positive, X_negative]
y = np.r_[[1]*n_class, [0]*n_class]
X, y = shuffle(X, y)
X_train, X_test, y_train, y_test =\
        train_test_split(X, y, test_size=0.2)
clf = svm.LinearSVC(C=1.0)
clf.fit(X_train, y_train)
print("Accuracy:", clf.score(X_test, y_test))
y_pred = clf.predict(X_test)
print("Precision:", precision_score(y_true=y_test, y_pred=y_pred))
print("Recall:", recall_score(y_true=y_test, y_pred=y_pred))
print("F1-score:", f1_score(y_true=y_test, y_pred=y_pred))

実行結果 :

Accuracy: 0.9
Precision: 0.9
Recall: 0.9
F1-score: 0.9

Kerasのサンプルコード

Keras2.xでは再現率や精度は自分で実装する必要があります。

import numpy as np
np.random.seed(1234)
from sklearn.model_selection import train_test_split
from sklearn.utils import shuffle
import keras
from keras.models import Sequential
from keras.layers import Dense, Activation
from keras import optimizers
import keras.backend as K
n_data = 200
n_class = n_data//2
X_positive = np.random.normal(loc=1.0, scale=1.0, size=(n_class, 2))
X_negative = np.random.normal(loc=-1.0, scale=1.0, size=(n_class, 2))
X = np.r_[X_positive, X_negative]
y = np.r_[[1]*n_class, [0]*n_class]
X, y = shuffle(X, y)
X_train, X_test, y_train, y_test =\
        train_test_split(X, y, test_size=0.2)

model = Sequential()
model.add(Dense(units=128, input_dim=2))
model.add(Activation('relu'))
model.add(Dense(units=128))
model.add(Activation('relu'))
model.add(Dense(units=1))
model.add(Activation('sigmoid'))

# 以下を参考に実装
# https://github.com/keras-team/keras/issues/5400#issuecomment-314747992
def recall_score(y_true, y_pred):
    true_positives = K.sum(K.round(K.clip(y_true * y_pred, 0, 1)))
    possible_positives = K.sum(K.round(K.clip(y_true, 0, 1)))
    recall = true_positives / (possible_positives + K.epsilon())
    return recall

def precision_score(y_true, y_pred):
    true_positives = K.sum(K.round(K.clip(y_true * y_pred, 0, 1)))
    predicted_positives = K.sum(K.round(K.clip(y_pred, 0, 1)))
    precision = true_positives / (predicted_positives + K.epsilon())
    return precision

def f1_score(y_true, y_pred):
    pre = precision_score(y_true, y_pred)
    rec = recall_score(y_true, y_pred)
    return 2 * pre * rec / (pre + rec)

model.compile(loss='binary_crossentropy',
              optimizer=optimizers.SGD(lr=0.01),
              metrics=['accuracy',precision_score, recall_score, f1_score])
model.fit(X_train, y_train)
score = model.evaluate(X_test, y_test)

print("\nLoss:", score[0], "Accuracy:", score[1], "Precision", score[2], "Recall:", score[3], "F1-Score:", score[4], )

実行結果 :

Epoch 1/1
160/160 [==============================] - 1s 3ms/step - loss: 0.6637 - acc: 0.7500 - precision_score: 0.9137 - recall_score: 0.5452 - f1_score: 0.6792
40/40 [==============================] - 0s 1ms/step

Loss: 0.6533831954 Accuracy: 0.85 Precision 0.893333339691 Recall: 0.8 F1-Score: 0.842396295071

参考


検証手法

ホールド・アウト法

ホールド・アウト法とは、機械学習モデルの性能を検証するために使われるシンプルな手法で、1つのデータセットを任意の割合で学習用とテスト用のデータセットに分割して評価を行います。この手法の問題点として、あるクラスのデータに偏ってデータセットを分割してしまう場合である。以下の図のようにクルマ・馬・犬の画像を学習し画像を分類を行うモデルを考えた場合、クルマの画像ばかりを学習したものをテストデータで評価することになってしまいます。

0002.png

しかし、ベンチマークとして使われるMNISTやCFIAR-10などでは、十分にデータの件数が大きいためホールド・アウト法にしたがい予め学習データとテストデータが用意されているケースもあるようです。

サンプルコード

以下のサンプルコードでは、データセットを学習用データセットを80%、テストデータを20%として分割します。

X_train, X_test, y_train, y_test =\
        train_test_split(X, y, test_size=0.2)

K-分割交差検証

ホールド・アウト法の解決策として考えられる手法がK-分割交差検証です。K-分割とあるようにデータセットをK個に分割します。それぞれの分割されたデータセットをテストデータ、それ以外を学習データセットとして活用します。

以下の図では、10個のデータを5組になるように分割し、5通りの学習データとテストデータに対して機械学習アルゴリズムを適用し、それぞれのテストデータを使って性能指標を算出します。そして各性能指標の平均値がモデルの性能となります。ホールド・アウト法よりもバラエティに富んだデータを用いることで、モデルの性能を多角的に調べてることができます。

0004.png
図 : クルマ・馬・犬の3クラス分類を行うモデルに対するK-分割交差検証

サンプルコード

Scikit-learnでは交差検証法はsklearn.model_selection.StratifiedKFoldから使うことができます。

前回紹介した2層のニューラルネットを交差検証法により評価する。

import numpy as np
np.random.seed(1234)
from sklearn.model_selection import StratifiedKFold
from keras.models import Sequential
from keras.layers import Dense, Activation
from keras import optimizers
# Setup Dataset
n_data = 200
n_class = n_data//2
X_positive = np.random.normal(loc=1.0, scale=1.0, size=(n_class, 2))
X_negative = np.random.normal(loc=-1.0, scale=1.0, size=(n_class, 2))
X = np.r_[X_positive, X_negative]
y = np.r_[[1]*n_class, [0]*n_class]
# Build Model
def mlp():
    model = Sequential()
    model.add(Dense(units=128, input_dim=2))
    model.add(Activation('relu'))
    model.add(Dense(units=128))
    model.add(Activation('relu'))
    model.add(Dense(units=1))
    model.add(Activation('sigmoid'))
    model.compile(loss='binary_crossentropy',
                  optimizer=optimizers.SGD(lr=0.01),
                  metrics=['accuracy',])
    return model
scores = []
# Divided into 10 groups
kfold = StratifiedKFold(n_splits=10, random_state=seed)
# Cross Validation
for train, test in kfold.split(X, y):
    X_train, y_train = X[train], y[train]
    X_test, y_test = X[test], y[test]
    model = mlp()
    model.fit(X_train, y_train, batch_size=20, verbose=0)
    score = model.evaluate(X_test, y_test, verbose=0)
    scores.append(score)
print("Mean:", np.mean(scores), "Std:", np.std(scores))

実行結果 :

前回の結果よりも正解率(Accuracy)が低くなっていることが分かります。

Mean of accuracy: 0.698384742439 Std: 0.226718946138

参考

追記

huyu398さんからいただいたご指摘を受けて、修正をしました。huyu398さん、ありがとうございました! (2018/03/19)

ShikaTechさんからいただいたご指摘を受けて、ソースコードを修正致しました。ShikaTech、ありがとうございました!(2018/03/20)

おわりに

性能指標と検証手法を紹介しました。この2つはディープラーニングに限らず機械学習全般で使われていますし、今後も共通言語とし使われていくように思われます。単一の指標で満足せず、できる限り多角的にモデルの性能を評価することで、いざ現実世界で動かしてみるとうまくいかないケースを減らすことができます。

性能評価はできる限り厳しくしてみましょう。自分のAIにがっかりすることはあっても、自信過剰になることはなくなります。「かわいい子には旅をさせよ」と言いますが、自分のかわいいAIにはできるかぎり多様なデータを学習させて厳しい旅をさせるといいのではないでしょうか。