import numpy as np
np.random.seed(1) # 擬似乱数シード: 毎回同じ乱数を出す
# Class1: 成績上位群
mu = [7, 7]
sigma = [[0.5, 0.05], [0.05, 0.5]]
Dat01 = np.random.multivariate_normal(mu, sigma, 100)
Label01=[]
for i in range(0, len(Dat01)):
Label01.append("Good")
# Class 2: 成績中位群
mu = [5.5, 4]
sigma = [[0.8, -0.5], [-0.5, 0.8]]
Dat02 = np.random.multivariate_normal(mu, sigma, 100)
Label02=[]
for i in range(0, len(Dat02)):
Label02.append("Middle")
# Class 3: 成績下位群
mu = [2, 5]
sigma = [[0.3, 0.0], [0.0, 3]]
Dat03 = np.random.multivariate_normal(mu, sigma, 100)
Label03=[]
for i in range(0, len(Dat01)):
Label03.append("Bad")
# データセット作成
x=np.concatenate([Dat01, Dat02, Dat03]) # 問題
y=np.concatenate([Label01, Label02, Label03]) # 正解ラベル
xとyのペアが教師データです。xに入力データ、yに出力ラベルが入っています。Good, Middle, Badラベルがそれぞれ100例、合計300人分のデータとなります。
機械学習における教師データとは、決定木、SVM、ニューラルネットワークがより良いパラメータを獲得するためのデータセットとなります。問題と正答のペアによって構成されます。例えば、勉強時間と理解傾向から試験得点を予測したい場合には、勉強時間と理解傾向が問題となり、試験得点が正答ラベルとなります。機械学習の基本的な性質として、教師データとして与えた問題を正答できるようなパラメータを獲得します。
しかし、教師データを良く当てるようなモデルを作ったとしても、それが直ちに良いモデルだということができません。人間の学力調査において、教材の問題はよく解けるが、期末テストではボロボロであることを通常、頭が良いとは言いません。教師データの精度が良いとはすなわち、教材の問題をよく解けることと等価で、期末テストでどのくらい解けるかを意味するわけではありません。人工知能の分野でも、期末テストを用意して、本当の頭の良さを測ってあげる必要があります。このためのデータセットのことを、テストデータと言います。
これまでの例では、作成したデータ(集めたデータ)をすべて教師データとして決定木やSVMなどを構築していました。しかしこれだと、テストデータがなくなってしまいます。通常、集めたデータを教師:テスト=7:3くらいの比率で分割します(8:2や5:5などの場合もあります)。このプログラムは以下の通りです。
from sklearn.model_selection import train_test_split
(X_train, X_test, Y_train, Y_test) = train_test_split(x, y, test_size=0.3, random_state=1)
print('教師データサイズ: ', np.shape(Y_train))
print('テストデータサイズ: ', np.shape(Y_test))
print('教師データの中身: ', Y_train)
print('テストデータの中身: ', Y_test)
train_test_split関数の第一、第二引数は全データセットの問題と正答ラベルのセットとなります。test_sizeはテストデータの割合であり、上のでは30%となっています。データセットのサンプルサイズは300でしたので、その7割が教師データ(210例)、3割がテストデータ(90例)として割り振られたことがわかります。random_stateは乱数のシードです。random_state=Noneにしたときと、具体的な数値を与えたときの結果の違いを見てみてください。Noneを与えると、ランダムに教師・テストデータを分割しますので、毎回異なる結果が出ます。しかしそれでは研究や分析をする上で、同じ結果を再現できなくなります。したがって通常は、具体的な数字を入れて、毎回出現する乱数を固定化させます。1でも2でもなんでもokです。
7:3にデータセットを分割しましたが、Good, Middle, Badの内訳はどうなっているでしょうか。Y_train[Y_train=='Good']とかくと、Y_trainの中で、Goodのみの行成分が取り出されます。shapeでその行列のサイズが出るので、以下のコードで内訳を確認できます。
print('教師データ(Good):', np.shape(Y_train[Y_train=='Good']))
print('教師データ(Middle):', np.shape(Y_train[Y_train=='Middle']))
print('教師データ(Bad):', np.shape(Y_train[Y_train=='Bad']))
print('テストデータ(Good):', np.shape(Y_test[Y_test=='Good']))
print('テストデータ(Middle):', np.shape(Y_test[Y_test=='Middle']))
print('テストデータ(Bad):', np.shape(Y_test[Y_test=='Bad']))
内訳が出ましたが、各ラベルを見ると、必ずしも7:3の比率になっていないことを確認できます。train_test_splitは指定した比率にデータセットをランダムに分割してくれますが、細かいラベルの内訳すらも指定した比率にしてくれるわけではないので、注意しましょう。偶然、ある特定ラベルが教師データにほとんどない、という状況も起こり得ます。こういった場合は当然学習もうまくいかないので、必ず教師データの内訳も確認する癖をつけましょう。
さて、続いて教師データを用いてモデルを作成します。今回はガウシアンカーネルによる非線形SVMを使用してみます。Cとgammaを色々変えてみます。fitにはテストデータを入れないように注意してくださいね。
from sklearn import svm
# モデル構造の決定
SVMModel_RBF01 = svm.SVC(kernel='rbf', C=0.1, gamma=0.1)
SVMModel_RBF02 = svm.SVC(kernel='rbf', C=0.1, gamma=10)
SVMModel_RBF03 = svm.SVC(kernel='rbf', C=10, gamma=0.1)
SVMModel_RBF04 = svm.SVC(kernel='rbf', C=10, gamma=10)
# 学習
SVMModel_RBF01 = SVMModel_RBF01.fit(X_train, Y_train)
SVMModel_RBF02 = SVMModel_RBF02.fit(X_train, Y_train)
SVMModel_RBF03 = SVMModel_RBF03.fit(X_train, Y_train)
SVMModel_RBF04 = SVMModel_RBF04.fit(X_train, Y_train)
...とかいて、ちょっとバカらしくならないでしょうか。プログラムを書き慣れている人であれば、for文でCとgammaを変化させ、一気にモデルを作れるのでは?と思うものです。実際、pythonではリスト型の変数を利用すると、これを実現できます。リストを忘れた人は「PyLearn02: リスト、for文」を見直してください。リストは、文字や数字を混在させて入れられると解説しましたが、なんと、機械学習のモデルもリストに入れることができるのです。
以下は、2重ループでCとgammaを0.1ずつ増やしながらSVMを1万個構築するコードです。100サイズの2重ループなので100*100で1万個となるわけです。せっかくなので、時間を計測してみます。
import time
start = time.time() # 時間計測スタート
# *************************************
# リスト変数の定義
RBF_SVM = []
# for文で大量のSVMを定義
for i in range(0, 100):
for j in range(0, 100):
cost = 0.1 * i + 0.1 # SVMパラメータ C
gam = 0.1 * j + 0.1 # SVMパラメータ gamma
TempMod = svm.SVC(kernel='rbf', C = cost, gamma = gam)
RBF_SVM.append(TempMod)
# for文で大量のモデルを学習
for k in range(0, len(RBF_SVM)):
RBF_SVM[k].fit(X_train, Y_train)
# *************************************
CalTime= time.time() - start # 時間計測終了
print('SVMを1万個作り学習させるために要した時間: ', CalTime, '[秒]')
こちらの環境では、1万個のSVMの構築に21秒かかったようです。かなり速い感じがしますね。カーネル関数などを変えると、計算時間がぐっと変わったりするにで、興味がある場合は調べてみてもいいでしょう。ところで、個々のSVMのパラメータがわからないじゃないかと文句が出そうです。これをチェックするには、以下のように書けばパラメータを見ることができます。
print(RBF_SVM[3544])
3544番目のSVMは、Cが3.6で、gammaが4.5だそうです。このように、リストに番号を与え、printで出力すると具体的なパラメータを見ることができます。
分類問題における精度評価の指標として、一般に、適合率、再現率、F値を用います。
適合率と再現率、一見似ていますがまったく別物の指標です。少し、頭の中で以下の事例を考えてみましょう。
ターゲットクラスを「糖尿病である」すなわち「1」と考えてみます。こうした場合、予測モデルが糖尿病だと出力した値は、100%信用できますね。したがって、「このモデルの適合率は高い」と判断できます。一方、実際の糖尿病患者は4名おり、3名取り漏らしています。ゆえに、「このモデルの再現率は低い」と判断できます。では、以下の事例ではどうでしょうか。
超極端な例です。この予測モデルの糖尿病だという回答は、全然信用できません。ゆえに「このモデルの適合率は低い」と判断されます。一方、実際の糖尿病患者を取り漏らしているでしょうか。一切取り漏らしていません。ゆえに「このモデルの再現率は高い」と判断できます。
では、適合率と再現率、どちらが高いモデルがいいのでしょうか。これは扱う問題の性質によって異なります。具体例として、火災報知器を例に考えてみましょう。あれは適合率と再現率、どちらが高く、どちらが低いでしょうか。火災報知器がブーブー鳴っても、実際に火事であることはあまりない気がします。しかし火災報知器は、実際の火事を取り漏らすことはほとんどありません。つまり、適合率が低く、再現率が高い、というのが正解です。詳しいことはわかりませんが、ああいう類の問題は、再現率を極めて高くしよう、という思想が備わっています(再現率が低いと「火事なのに鳴らなかった!!!」と激怒する人が出そうです)。このように、適合率と再現率、重視すべきはいずれかという問題は、状況によって異なることを覚えておくといいでしょう。
ところで、察しのいい人は気がつくかもしれませんが、再現率を高めようとすると、適合率が下がります。逆に、適合率を高めようとすると、再現率が下がります。実は、適合率と再現率にはトレードオフの関係があります。科学というのはなかなか辛辣で、うまい話はないのです。
...とは言ったものの、火災報知器のような特殊な例を除いて、適合率も再現率も、どちらも重要です。したがって、特にどちらを大事にしたいという考えがない場合には、適合率と再現率の相乗平均であるF値をモデルの精度評価指標として採用することが普通です。論文などでどのモデルがいいのか議論したい場合には、F値を使うといいでしょう。
とはいえ、何でもかんでもF値を見るのではなく、適合率・再現率もしっかりと観察するようにしましょう。知能が有する個性を明瞭に知る一つの手がかりとなります。
大事な話なので前置きが長くなりました。先ほど作成した1万個の適合率、再現率、F値を算出してみます。
from sklearn.metrics import precision_recall_fscore_support
# まずは、教師データとテストデータの推定結果を得る。
Res_train = []
Res_test = []
for k in range(0, len(RBF_SVM)):
# リストに入れるので、appendを利用
Res_train.append(RBF_SVM[k].predict(X_train))
Res_test.append(RBF_SVM[k].predict(X_test))
# 次に、推定結果と正答ラベルを入れ、適合率などを算出
ScoreSVM_train = []
ScoreSVM_test = []
ScoreSVM_train_Ave = []
ScoreSVM_test_Ave = []
for k in range(0, len(RBF_SVM)):
# 教師データの精度評価
ScoreSVM_train.append(precision_recall_fscore_support(Res_train[k], Y_train))
ScoreSVM_train_Ave.append(precision_recall_fscore_support(Res_train[k], Y_train, average='weighted'))
# テストデータの精度評価
ScoreSVM_test.append(precision_recall_fscore_support(Res_test[k], Y_test))
ScoreSVM_test_Ave.append(precision_recall_fscore_support(Res_test[k], Y_test, average='weighted'))
適合率などの算出には、まず、predictで予測結果を入手しておき、予測結果と実測をprecision_recall_fscore_supportに入れることが必要です。precision_recall_fscore_supportのaverageにweightedを与えることで、各クラスの重み付き適合率などの平均を得ることができます。重み付きとは、クラスの数が違うので、平均値にその分重みをつけるという意味です。では、具体的な結果を見てみます。
ScoreSVM_train[1000]
これは、1000番目のSVMの教師データの結果です。4行分出てきました。今回は、Good, Middle, Badの3クラス分類問題で、アルファベット順に結果がソートされています。Bad, Good, Middleの順番になっています。
指標がたくさんあって見るのが大変です。この重み付き平均を見てみます。
ScoreSVM_train_Ave[1000]
モデル1000を利用した場合の適合率、再現率、F値の平均が出てきました。F値がを重視しますということで、F値だけみてみましょう(まあもう見えていますが)。リスト配列の1000番目の、2番目を見ると良いので、以下のようにかけば取り出せます。
print('モデル12の教師データのF値:', ScoreSVM_train_Ave[1000][2])
ところで、重要なのは教師データの精度ではなく、テストデータの精度だったはずです。これも出してみましょう。
print('モデル12のテストデータのF値:', ScoreSVM_test_Ave[1000][2])
F値の最良値は1です。教材の問題を0.98くらいで解けて、期末テストも0.97くらい(切り下げ)で解けたということですので、性能がいいモデルであることがわかりますね! ちなみに、学習に使用しなかったデータにおける精度を、汎化能力を呼ぶことがあります。汎化能力はどのくらいなの?ときかれたら、未知のデータにどのくらい対応できるんだ?と聞かれていることを意味しますので、教師データの精度などは答えないように注意しましょう。
1000番目のSVMの精度がいいということがわかりました。こんな感じで1万個のモデルのF値を見るのは大変です。最良のモデルはなんでしょうか? 最良の定義として「テストデータのF値が最大となるSVM」とすると、以下のコードで最良のモデルを発見することができます。
# まず、テストデータのF値だけ、for文で取り出す。
F_test=[]
for i in range(0, len(RBF_SVM)):
F_test.append(ScoreSVM_test_Ave[i][2])
# argmax関数により、F_testの値が最大となるインデックスを取得
OptimalModel_ID = np.argmax(F_test)
print('テストデータのF値が最大となるモデル:', OptimalModel_ID)
print('RBF_SVM[', OptimalModel_ID, ']のFテストデータのF値:', ScoreSVM_test_Ave[OptimalModel_ID][2])
print('最適モデルのパラメータ:', RBF_SVM[OptimalModel_ID])
argmaxは、最大となるインデックス番号を返してくれるnumpyの関数です。これを実行すると、647番目のモデルが、テストデータのF値が最大となるようです。647番目のSVMは、テストデータをすべて正答できた訳ですから、このモデルを使えばまあいいんじゃないか、ということを意味します。
もうちょっと素朴な探し方として、1万個のモデルについて、横軸を適合率、縦軸を再現率として、散布図をみるという探し方もあります。視覚的にいい感じのモデルを見るけることができます。このコードは以下の通りです。
# データ整形
P_test=[] # テストデータの適合率(precision)を入れるリスト
R_test=[] # テストデータの再現率(recall)を入れるリスト
for i in range(0, len(RBF_SVM)):
P_test.append(ScoreSVM_test_Ave[i][0]) # 適合率は0番目
R_test.append(ScoreSVM_test_Ave[i][1]) # 再現率は1番目
# 散布図生成
import matplotlib.pyplot as plt
plt.scatter(P_test, R_test)
plt.title("P-R plot of Test data") # タイトル
plt.xlabel("Precision")
plt.ylabel("Recall")
plt.grid(True)
plt.show() # グラフの表示
散布図をみてみると、再現率(Recall)がかなり低いモデルがあることがわかります。こういったモデルを使ってしまうと、かなり危ないことを意味します。こんなモデルを採用しないように、注意しましょう。一方、適合率も再現率も最良となる1のモデルがあることも確認できます。ぜひこのようなモデルを採用しましょう。
次に、1万個のモデルの、教師データとテストデータのF値を観察して見ましょう。これを見ると、ちょっとだけ面白いことがわかります。
# データ整形
F_train=[] # 教師データのF値を入れるリスト
F_test=[] # テストデータのF値を入れるリスト
for i in range(0, len(RBF_SVM)):
F_train.append(ScoreSVM_train_Ave[i][2]) # F値は2番目
F_test.append(ScoreSVM_test_Ave[i][2]) # F値は2番目
# 散布図生成
import matplotlib.pyplot as plt
plt.scatter(F_train, F_test)
plt.title("Train-Test plot of F score") # タイトル
plt.xlabel("F train")
plt.ylabel("F test")
plt.grid(True)
plt.show() # グラフの表示
左下は、教師データもテストデータも、精度が悪いですね。つまり、教材の問題もあまり解けないし、期末テストも解けない状態です。一方、右上は教材も期末テストも解けるモデルです。右上のモデルを採用するといいことになりますね。
ところで、教師データのF値が0.92程度と高いのに、テストデータのF値が0.76程度と低いモデルがあることがわかります。教材の問題はよく解けるのに、期末テストはあまり解けないモデルです。勉強の仕方がおかしい、よくいる暗記タイプです。このように、教師データの精度は高いのに、テストデータの精度が低いような学習を、過学習モデルと呼びます。過学習の回避が機械学習における至上命題の一種ですので、興味がある人は調べて見ましょう。
本ページでは、精度評価とモデル選択の基本について学びました。以下の質問に答えられればokです。
より深く理解した人は、SVM以外にも、決定木やニューラルネットワークで、for文で大量のモデルを作り、精度評価を行ってみましょう。
分類問題でモデル選択の基本について述べましたが、回帰問題でも流れはまったく同じです。
ただし、指標は適合率や再現率ではなく、PyLearnST 07: 線形回帰モデルで述べた決定係数やMSEを使用してください。