学習した機械学習のモデルが与えたデータに対してどのように分類したかを知りたいことは多いです。ここら先は違うクラスになるという境界がわかられば、分類モデルの理解が深まりますし、改善ポイントもわかるようになります。学生の頃に隣のクラスになろうと居座っていたことはありますが、チャイムともに戻されました。そこには明確な境界があったわけです。
今回は、学習した機械学習モデルが作る分類の境界を可視化できるようにします。その境界のことを決定境界と呼んでいます。
決定境界とは何かを見てみる
決定境界の説明をする前に実装した結果の画像をさっそく見てみます。皆さんにおなじみの「iris」のデータを使っいます。
この図を見てわかる通り、決定境界とは分類クラスの境目のことです。それぞれのクラスに正しく分類しているのが良い決定境界であり、良い決定境界を作るために機械学習ではモデルをチューニングするわけです。
上の図を見てみると、決定境界と一言で言ってもアルゴリズムによって描き方が様々です。大きな特徴としては、ロジスティック回帰・決定木の境界線は直線であるのに対して、K近傍探索・SVM・ランダムフォレストは曲線です。また、ランダムフォレストは外れ値のような点にも影響され過学習習している様子が見られます。テスト勉強で丸暗記していっても、実際のテストでは全く異なる問題に対応できないタイプのようです。それぞれの特徴があって面白いですね。
決定境界を描くまでの流れ
- プロットを目視できるようにデータを被説明変数と説明変数2変数分のみを抽出
- 1.のデータを使ってモデルを学習
- 1.の説明変数に合わせた細かい入力データを作り、学習したモデルで分類をする
これをプロットすることで、色別れの領域を描ける - 1.のデータを3.に重ねる
決定境界を描く部分に焦点を当てて説明
見出し決定境界を描くまでの流れの3.、4.の部分を先に説明します。第一に、今回は3クラスの分類のため3種類のマーカと色を用意しています。
import numpy as np from matplotlib.colors import ListedColormap import matplotlib.pyplot as plt markers = ('s', 'x', 'o') cmap = ListedColormap(('red', 'blue', 'green'))
次に決定境界を描くためのデータを用意します。決定境界を描くためにはプロットする入力データが取り得る範囲をクラスごとに色付けをしていく必要があります。そのため、入力データを満たす領域を描けるような細かなデータを作成します。その方法がメッシュデータです。グラフを格子状に区切った際の交点のことです。この交点に高さを加えることで等高線を引くことができます。それではメッシュデータを作成してみます。与えられたデータの最小値と最大値までのメッシュデータを0.01刻みで作っています。\(x\)は2変数の入力データのデータフレームです。
x1_min, x1_max = x[:, 0].min()-1, x[:, 0].max()+1 x2_min, x2_max = x[:, 1].min()-1, x[:, 1].max()+1 x1_mesh, x2_mesh = np.meshgrid(np.arange(x1_min, x1_max, 0.01), np.arange(x2_min, x2_max, 0.01))
メッシュデータの作成において簡単に試してみます。下の例では0~5の1刻みの連番においてメッシュデータを作成しています。出力した結果を見ると0~5の1刻みの2次元マップの交点ができていることがわかります。
import numpy as np x = np.arange(0, 5) x1_mesh, x2_mesh = np.meshgrid(x,x) print(x1_mesh.ravel()) print(x2_mesh.ravel()) [0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4] [0 0 0 0 0 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4]
メッシュデータのすべてに対して学習したモデルで分類をしています。これによりメッシュデータに分類結果の高さを加えることで、クラスごとの色分けを行うことができます。
z = model.predict(np.array([x1_mesh.ravel(), x2_mesh.ravel()]).T) z = z.reshape(x1_mesh.shape)
matplotlibのcontourfを使って決定境界を描いています。contourfはz軸の範囲ごとに色を割り当てて描いており、contour(fをとると)にすると、色を塗らずにプロットするだけになるので決定境界線を描けます。
plt.contourf(x1_mesh, x2_mesh, z, alpha=0.4, cmap=cmap)
plt.xlim(x1_mesh.min(), x1_mesh.max())
plt.ylim(x2_mesh.min(), x2_mesh.max())
上記までで決定境界面の色付けは終わり、残りは入力データをその決定境界面にプロットするのみとなりました。各クラスごとに上で指定したマーカ/色の種類でプロットします。\(y\)は入力データに対応した教師データです。
for idx, cl in enumerate(np.unique(y)): plt.scatter(x=x[y == cl, 0], y=x[y == cl, 1], alpha=0.6, c=cmap(idx), edgecolors='black', marker=markers[idx], label=cl)
今回のコード
import numpy as np from sklearn import datasets from matplotlib.colors import ListedColormap from sklearn.model_selection import train_test_split import matplotlib.pyplot as plt from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression from sklearn.neighbors import KNeighborsClassifier from sklearn.svm import SVC from sklearn.tree import DecisionTreeClassifier from sklearn.ensemble import RandomForestClassifier import math # # 決定境界プロット関数 # def plot_decision_regions(x, y, model, resolution=0.01): ## 今回は被説明変数が3クラスのため散布図のマーカータイプと3種類の色を用意 ## クラスの種類数に応じて拡張していくのが良いでしょう markers = ('s', 'x', 'o') cmap = ListedColormap(('red', 'blue', 'green')) ## 2変数の入力データの最小値から最大値まで引数resolutionの幅でメッシュを描く x1_min, x1_max = x[:, 0].min()-1, x[:, 0].max()+1 x2_min, x2_max = x[:, 1].min()-1, x[:, 1].max()+1 x1_mesh, x2_mesh = np.meshgrid(np.arange(x1_min, x1_max, resolution), np.arange(x2_min, x2_max, resolution)) ## メッシュデータ全部を学習モデルで分類 z = model.predict(np.array([x1_mesh.ravel(), x2_mesh.ravel()]).T) z = z.reshape(x1_mesh.shape) ## メッシュデータと分離クラスを使って決定境界を描いている plt.contourf(x1_mesh, x2_mesh, z, alpha=0.4, cmap=cmap) plt.xlim(x1_mesh.min(), x1_mesh.max()) plt.ylim(x2_mesh.min(), x2_mesh.max()) for idx, cl in enumerate(np.unique(y)): plt.scatter(x=x[y == cl, 0], y=x[y == cl, 1], alpha=0.6, c=cmap(idx), edgecolors='black', marker=markers[idx], label=cl) # # データの取得 # data = datasets.load_iris() x_data = data.data y_data = data.target # 2変数だけを抽出 x_data = x_data[:, [0,1]] # 入力データの各変数が平均0,標準偏差1になるように正規化 # 各アルゴリズムのプロット結果を比較しやすいように予め全入力データを正規化 sc = StandardScaler() sc.fit(x_data) x_data = sc.transform(x_data) # データを学習用/テスト用に分割している x_train, x_test, y_train, y_test = train_test_split(x_data, y_data, test_size=0.2) # # 機械学習アルゴリズムの定義 # lr = LogisticRegression(C=10) knn = KNeighborsClassifier(n_neighbors=5) svm = SVC(kernel='rbf', C=1.0) dc = DecisionTreeClassifier(criterion='entropy', max_depth=3) rf = RandomForestClassifier(criterion='entropy', n_estimators=10) models = [lr, knn, svm, dc, rf] model_names = ['logistic regression', 'k nearest neighbor', 'svm', 'decision tree', 'random forest'] # # それぞれのモデルにおいて決定境界をプロット # plt.figure(figsize=(8,6)) plot_num = 1 for model_name, model in zip(model_names, models): plt.subplot(math.ceil(len(models)/2), 2, plot_num) # モデルの学習 model.fit(x_train, y_train) # 決定境界をプロット plot_decision_regions(x_data, y_data, model) plt.title(model_name) plot_num += 1 plt.tight_layout() plt.savefig('./images/decision_region.png') plt.show()
決定境界に関して様々なアルゴリズムで記載されています。更にデータの前処理からkerasを使ってのディープラーニングの作り方まで、この本を読めば機械学習を一通り抑えられます。
終わりに
説明変数が2変数より多い場合には、説明変数を2変数に絞るにあたってどれを選べばいいのかは悩ましい問題です。飲み会で誰の隣になると楽しいかを見極めるのと同じくらい難しいです。そんな感じです。それはさておき、変数選択の方法は色々あるので、勉強しながら今後まとめていきたいと思います。