機械学習 実践(教師あり学習:分類)

前章では教師あり学習の中でも回帰を扱いました。教師あり学習には他にも分類があります。

具体的な scikit-learn での実装手順は同じなのですが、それぞれ使用目的が異なるので覚えておきましょう。本章では分類の実装手順と、代表的な分類のアルゴリズムを紹介していきます。

  • 回帰:数値(連続値)を予測する(売上、株価、販売数量 など)
  • 分類:カテゴリ(離散値)を予測する(犬/猫、男性/女性 など)

また、モデルを実装した後には様々な評価指標を用いて予測性能を確認する必要があります。それぞれの評価指標と、その必要性についても理解しましょう。

本章の構成

  • 分類
  • 決定木の実装で分類の全体像を理解
  • 代表的な分類のアルゴリズム
  • 分類の評価方法
  • scikit-learn で評価指標を確認

分類

分類と回帰の違いは何でしょうか。

17_7

分類の考え方は非常にシンプルで、上記の図のようにカテゴリが異なる複数のデータを見分けることができる境界線を求めることが目的です。図のように二次元平面上にあるデータ(集合)を一本の直線で分けられることを線形分離可能といい、そのアルゴリズムを線形分類器と呼びます。線形分類器として有名なものは、

  • 単純パーセプトロン
  • 線形サポートベクトルマシン
  • ロジスティック回帰

などです。

回帰の場合は線形回帰が困難な場合に、非線形回帰がありました。それでは、線形分離可能ではない場合にはどのように対応するでしょうか。

17_8

例えば上記の図の場合では、一本の直線では分けるのではなく、直線を複数組み合わせたような形で分類することを考えるのが自然です。このように線形ではない形で分類するアルゴリズムを非線形分類器と呼びます。非線形分類器として有名なものは、

  • k-近傍法
  • 決定木(分類木)
  • ランダムフォレスト
  • 非線形サポートベクトルマシン
  • ニューラルネットワーク

などがあります。ニューラルネットワークはディープラーニングの基礎で詳しく説明するディープラーニングの基本となるアルゴリズムですので、名前を覚えておきましょう。本章では、

  • 決定木(分類木)
  • サポートベクトルマシン
  • ロジスティック回帰

を理論と scikit-learn を使った実装を紹介していきます。まずは実装を通してそれぞれのアルゴリズムの概要を抑えることを目標とします。

決定木の実装で分類の全体像を理解

前章で実装した回帰と同様に、分類も scikit-learn を使用します。練習として scikit-learn の中の有名なアヤメ (iris) の花のデータセットを使用して実装方法を抑えましょう。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import load_iris
      

データセットの準備

今回使用するデータセットはアヤメの花に関する情報とその種類です。入力変数として扱うデータは下記の表になります。今回は表の情報をもとにアヤメの花の種類を分類する問題設定に取り組んでいきます。

列名 説明
petal length 花びらの長さ
petal width 花びらの幅
sepal length がく片の長さ
sepal width がく片の幅

17_1

# データセットの読み込み
dataset = load_iris()
columns_name = dataset.feature_names
x = dataset.data
t = dataset.target
      
# 読み込んだデータセットを DataFrame に変換
df = pd.DataFrame(data=x, columns=columns_name)
df['Target'] = t

df.head(3)
      
sepal length (cm) sepal width (cm) petal length (cm) petal width (cm) Target
0 5.1 3.5 1.4 0.2 0
1 4.9 3.0 1.4 0.2 0
2 4.7 3.2 1.3 0.2 0
# t のユニークな値を確認する
np.unique(t)
      
array([0, 1, 2])

目的変数 t には 3 種類の値が入っていることが分かりました。これは setosa versicolor virginica の文字列が数値に置き換えられているということです。

x.shape, t.shape
      
((150, 4), (150,))
# データセットの分割
from sklearn.model_selection import train_test_split
x_train, x_test, t_train, t_test = train_test_split(x, t, test_size=0.3, random_state=0)
      

モデルの学習

データの準備が整いましたので、分類アルゴリズムの実装を行っていきましょう。scikit-learn は問題設定が回帰、分類問わず、モデルの定義、学習、検証の 3 ステップで進んでいきます。

今回は決定木と呼ばれる基本的なアルゴリズムを使用します。アルゴリズムの詳細は後述しますので、まず実装してみましょう。sklearn.tree 以下の DecisionTreeClassifier が分類での決定木になります。

# モデルの定義
from sklearn.tree import DecisionTreeClassifier
dtree = DecisionTreeClassifier(random_state=0)
      
# モデルの学習
dtree.fit(x_train, t_train)
      
DecisionTreeClassifier(ccp_alpha=0.0, class_weight=None, criterion='gini', max_depth=None, max_features=None, max_leaf_nodes=None, min_impurity_decrease=0.0, min_impurity_split=None, min_samples_leaf=1, min_samples_split=2, min_weight_fraction_leaf=0.0, presort='deprecated', random_state=0, splitter='best')
# モデルの検証
print('train score : ', dtree.score(x_train, t_train))
print('test score : ', dtree.score(x_test, t_test))
      
train score : 1.0 test score : 0.9777777777777777

ここで確認できる値は回帰の時に使用した指標である決定係数ではありません。分類の評価指標はいくつか種類がありますが、決定木の score() メソッドで表示される値には正解率 (Accuracy) が用いられています。例えば、100 個の要素に対して分類を行い 90 個の予測を正しく行えた場合、正解率は 90%(0.9) になります。そのため最小の値は 0 となり、最大値は 1 です。

もう一つの回帰との違いとして、目的関数が挙げられます。回帰では平均二乗誤差が用いられていましたが、分類では交差エントロピーが主に用いられことも最初は用語として覚えておきましょう。どちらの目的関数も正解と予測の差を評価したいという考えは同じです。

このように分類の問題設定でも、scikit-learn を用いての実装は回帰の場合と大きな違いはなく行うことができ、推論も同様に predict() メソッドで実行することが可能です。

# 推論
dtree.predict(x_test)
      
array([2, 1, 0, 2, 0, 2, 0, 1, 1, 1, 2, 1, 1, 1, 1, 0, 1, 1, 0, 0, 2, 1, 0, 0, 2, 0, 0, 1, 1, 0, 2, 1, 0, 2, 2, 1, 0, 2, 1, 1, 2, 0, 2, 0, 0])

予測値を確認すると、回帰の時の連続値ではなく、離散値であることが分かります。このように分類モデルは当てはまるカテゴリを表す数値が出力されます。今回の問題設定では、3 種類のアヤメの花の種類を分類するため、0 ~ 2 の 3 つのカテゴリが定められていることが確認できます。

決定木の特徴

決定木は木のような構造を用いて、回帰・分類を行うアルゴリズムです。回帰に使用するものを特に回帰木と呼ぶこともあります。決定木はどのように分類を行っているかのイメージを掴むためにも最初は画像を見て大枠を掴みましょう。

17_2

決定木は上記の図のように複数の分岐を繰り返すことによって、分類を行います。 一番上の分岐を見ると、petal length(cm) <= 2.35 という条件が確認できます。そして、次のステップではその条件に適合していれば True へ、そうでなければ False へと分岐を繰り返します。このように条件分岐を繰り返すことによって、分類を行います。

図内の gini という表記はジニ係数を意味しており、分岐されたノードの不純度を表しています。もしも、カテゴリが 1 つしか存在しない場合、ジニ係数は 0 となります。

それでは決定木の特徴や代表的なハイパーパラメータを確認しましょう。

項目 説明
強み 解釈が容易。必要な前処理が少ない。
弱み 過学習になる場合が多く、汎用性の低いモデルになる傾向がある。
主なハイパーパラメータ
max_depth(木構造の深さの上限) 過学習を抑えるためのハイパーパラメータ。上限が低いとモデルの表現力は低下し、過学習を抑える。
min_samples_split(木構造の分岐先の値) 分岐先のノード数の最低値を設定するハイパーパラメータ。過学習に陥る可能性が上がるので調整が必要。

他にも多くのハイパーパラメータがありますが、まずはこの 2 つを抑えておきましょう。決定木のアルゴリズムの詳細に関しては公式ドキュメントに記載されているので気になる方は参考にしてください。

木構造と Feature importance の確認

木構造の書き出しには graphviz というパッケージを使用します。Colab にはデフォルトでインストールされているのですが、お手元の PC などで使用する際にはこちらからインストールを行ってください。

# 木構造の書き出し
import graphviz
from sklearn.tree import export_graphviz
dot_data = export_graphviz(dtree)
      
# 木構造の表示
graph_tree = graphviz.Source(dot_data)
graph_tree
      
<graphviz.files.Source at 0x11f43c080>

決定木はアルゴリズムの特性から、どの入力変数の影響度が高いかを知ることができます。なぜなら、木構造は分岐を繰り返すので、分岐の上に行くほどモデル全体に対する影響度が高くなるためです。この性質も決定木ベースの手法が使われる理由のひとつになります。

それぞれの入力変数の影響度を確認するには .feature_importances_ 属性を確認します。

# feature importance
feature_importance = dtree.feature_importances_
feature_importance
      
array([0. , 0.02150464, 0.39766951, 0.58082584])
# 可視化
y = columns_name
width = feature_importance

# 横向きで表示
plt.barh(y=y, width=width);
      
<Figure size 432x288 with 1 Axes>

実行結果から petal width の影響度が高いことを確認できました。

今回、分類の代表的なアルゴリズムとして決定木を紹介したのは大きく 2 点の理由があります。

  • 解釈がしやすい
  • さまざまなモデルの基礎概念となる

解釈がしやすいのはこれまでの説明で理解ができたと思います。特にビジネスの現場では、どの変数が結果に影響しているのかを求められることが多々あります。そういった場合にも決定木は選択肢に入ります。

2 点目のさまざまなモデルの基礎概念となるというのは、複雑な問題設定に対して決定木単体で高い性能を出すことは少ないかもしれませんが、決定木を基本とした高性能なモデルが最近では取り上げられています。

  • ランダムフォレスト
  • XGBoost
  • LightGBM

など、アンサンブル学習と呼ばれる、学習器を複数用意して一つのモデルにしようというアルゴリズムです。こちらも今後紹介していきますので、現時点では名前だけ覚えておいてください。

代表的な分類のアルゴリズム

決定木の他にも分類で使用されるアルゴリズムはあります。本節では、代表的なアルゴリズムである下記を説明と実装を共に紹介していきます。

  • サポートベクトルマシン
  • ロジスティック回帰

サポートベクトルマシン (SVM)

サポートベクトルマシンは、2 つのカテゴリを識別する分類器です。与えられたデータを線形分離ことを考えてみます。サポートベクトルマシンでは、境界線に最も近いサンプルとの距離マージン)が最大となるように境界線が定義されます。

17_9

サポートベクトルマシンは元々、線形分類器として使用されていました。その後、カーネル関数と呼ばれる関数を用い、高次元(無限次元)の特徴空間へ写像し、特徴空間上で線形分離を行う手法が提案されました。このカーネル関数を取り入れた一連の手法をカーネルトリックと呼びます。

svm

カーネルトリックの理論はレベルが高いため、イメージを掴むことを目標にしましょう。上記のアニメーションでは、2 次元のデータを 3 次元へ写像し平面で線形分離しています。

少し数学的な要素が入り混じってきましたが、イメージができたら十分です。下記の表に特徴を記載しているので、覚えておきましょう。

項目 説明
強み 未知のデータへの識別性能が比較的強い。ハイパーパラメータの数が少ない。
弱み 学習する際に必ずデータの標準化(もしくは正規化)を行う必要がある。
主なハイパーパラメータ
C(コストパラメータ) 誤った予測に対するペナルティ。大き過ぎると過学習を起こす。
gamma(ガンマ) モデルの複雑さを決定する。値が大きくなるほどモデルが複雑になり過学習を起こす。

それでは実装していきましょう。手順や流れは変わりません。

# モデルの定義
from sklearn.svm import SVC
svc = SVC()
      
# モデルの学習
svc.fit(x_train, t_train)
      
SVC(C=1.0, break_ties=False, cache_size=200, class_weight=None, coef0=0.0, decision_function_shape='ovr', degree=3, gamma='scale', kernel='rbf', max_iter=-1, probability=False, random_state=None, shrinking=True, tol=0.001, verbose=False)
# モデルの検証
print('train score : ', svc.score(x_train, t_train))
print('test score : ', svc.score(x_test, t_test))
      
train score : 0.9714285714285714 test score : 0.9777777777777777

サポートベクトルマシンは一般的にデータに対して標準化を適用する必要があります。今回のデータセットは全て cm を単位としているため、スケールが統一されており、標準化の必要はありませんが、scikit-learn を用いての実装方法を確認しておきましょう。実装方法は基本的にはモデルの学習を行う際と同じ流れです。

標準化 (Standardization) は下記の計算のように平均と標準偏差を算出し行います。

x^=xxσ\begin{array}{c} \hat x = \frac{x - \overline x}{\sigma} \end{array}

x^\hat x が標準化適用後の値、x\overline x は 平均、σ\sigma は標準偏差をそれぞれ表しています。標準化を行うには、sklearn.preprocessiong 以下の StandardScaler を用います。sklearn.preprocessiong には他にも前処理の手法がありますので、今後も使用していくモジュールです。

from sklearn.preprocessing import StandardScaler
std_scaler = StandardScaler()
      
std_scaler.fit(x_train)
      
StandardScaler(copy=True, with_mean=True, with_std=True)

.fit() メソッドを使用しました。こちらでは標準化を行うためにデータセットの平均と標準偏差を算出しています。算出した平均と標準偏差から実際にデータセットの値を変換するには transform() メソッドを使用すると値が変換されます。

# 標準化
x_train_std = std_scaler.transform(x_train)
x_test_std = std_scaler.transform(x_test)
      

以下を見てみると、平均 0、標準偏差 1 となっており標準化が適用されていることが確認できます。round() メソッドは Python の組み込み関数の一つで、小数部を任意の桁に丸めることができます。

# 平均
round(x_train_std.mean())
      
0.0
# 標準偏差
round(x_train_std.std())
      
1.0

標準化したデータをもとにもう一度学習をおこなってみましょう。

# モデルの定義
svc_std = SVC()
      
# モデルの学習
svc_std.fit(x_train_std, t_train)
      
SVC(C=1.0, break_ties=False, cache_size=200, class_weight=None, coef0=0.0, decision_function_shape='ovr', degree=3, gamma='scale', kernel='rbf', max_iter=-1, probability=False, random_state=None, shrinking=True, tol=0.001, verbose=False)
# モデルの検証
print('train score : ', svc.score(x_train, t_train))
print('test score : ', svc.score(x_test, t_test))

print('train score scaling : ', svc_std.score(x_train_std, t_train))
print('test score scaling : ', svc_std.score(x_test_std, t_test))
      
train score : 0.9714285714285714 test score : 0.9777777777777777 train score scaling : 0.9714285714285714 test score scaling : 0.9777777777777777

今回はスケールが統一されていたこともあり、変化がありませんでしたが、サポートベクトルマシンを使用する際には必ず標準化をおこなうことを覚えておきましょう。

ロジスティック回帰

分類アルゴリズムの紹介なのに、回帰と名前が付くモデルがでてきて驚いた方もいらっしゃるのではないでしょうか。

ロジスティック回帰 (Logistic regression) はあるデータがカテゴリに属する確率を予測するための回帰のアルゴリズムに該当します。しかし、確率を予測することが可能な特性から分類の問題設定で用いられることが多いため、本チュートリアルでは分類のアルゴリズムとして紹介しています。

また、色々な分野で用いられる手法なのですが、特に医学分野で特に発展を遂げてきた手法なので、医学論文等を読む方には馴染みが深いアルゴリズムになると思います。

数式から全体像を把握しましょう。ロジスティック回帰では、予測値 yy を求める数式は下記で表されます。

z=β+w1x1+w2x2y=11+exp(z)\begin{aligned} z &= \beta + w_1x_1+ w_2x_2 \cdots \\\\ y &= \frac{1}{1 + \exp(-z)} \end{aligned}

zz を求める式が重回帰分析で使用した数式と似ていることが分かるでしょうか。β\beta が切片を表し、wiw_i が重みを表します。exp()\exp(\cdot) とは見慣れない方もいらっしゃるかもしれませんが、自然対数 ee を底とした指数関数を数式の中で見やすいようにこの表現をしています。

また、11+exp(z)\frac{1}{1 + \exp(-z)}シグモイド関数と呼ばれます。後のチュートリアルで紹介するディープラーニングでも活性化関数としても扱われることがありますので、頭の片隅に置いておきましょう。シグモイド関数を図で表すと下記のようになります。

17_10

上記の図からも分かるように、このシグモイド関数の適用後の yy[0,1][0, 1] の値を取るため、ロジスティック回帰は確率を出力していると言えます。

ここからどのように分類に適用するのでしょうか。ロジスティック回帰は入力変数から目的変数に対して二値分類をおこなうモデルです。例えば、今回のように 3 クラス分類の問題設定ですと、クラス 0 を予測できる確率、クラス 1 を予測できる確率、クラス 2 を予測できる確率を出力します。二値分類をクラス数だけ行っているということです。最終的にそれぞれのクラスに対し確率を出力しているので、最も確率の高いクラスを予測値として採用します。

モデルの概要を把握しておきましょう。

項目 説明
強み 説明能力が高い。入力変数の重要度、オッズ比がわかる。
弱み 線形分類器のため、複雑な問題設定に対応できない場合がある。
主なハイパーパラメータ
C(コストパラメータ) 誤った予測に対するペナルティ。大きすぎると過学習を起こす。
penalty 正則化を行う方法を決定する。L1L1L2L2 のノルムから選択する。

モデル詳細についてはこちらを参照し、実装に関しては scikit-learn のドキュメントを確認してください。

# モデルの定義
from sklearn.linear_model import LogisticRegression
log_reg = LogisticRegression(C=1.0)
      
# モデルの学習
log_reg.fit(x_train, t_train)
      
LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True, intercept_scaling=1, l1_ratio=None, max_iter=100, multi_class='auto', n_jobs=None, penalty='l2', random_state=None, solver='lbfgs', tol=0.0001, verbose=0, warm_start=False)
# モデルの検証
print('train score : ', log_reg.score(x_train, t_train))
print('test score : ', log_reg.score(x_test, t_test))
      
train score : 0.9809523809523809 test score : 0.9777777777777777

良い精度が出たことが確認できました。

重回帰分析で行った方法と同様に重みを確認してみましょう。今回の重みの形を確かめてみると (3, 4) となっています。これは (n_classes, n_features) に対応し、それぞれのクラスを予測する際のそれぞれの特徴量の重みを表しています。全て可視化してみましょう。

log_reg.coef_.shape
      
(3, 4)
# 重み(係数)
log_reg.coef_
      
array([[-0.39765925, 0.83421173, -2.28946117, -0.97844057], [ 0.54460765, -0.29081826, -0.232645 , -0.65832227], [-0.14694841, -0.54339348, 2.52210616, 1.63676284]])
# 切片
log_reg.intercept_
      
array([ 8.99766759, 1.54372138, -10.54138897])
# それぞれの重みを確認
fig = plt.figure(figsize=(7, 15))

for i in range(len(log_reg.coef_)):
    ax = fig.add_subplot(3, 1, i+1)
    ax.barh(y=dataset.feature_names, width=log_reg.coef_[i])
    ax.set_title('Class {} '.format(i))
      
<Figure size 504x1080 with 3 Axes>

ここからは現実課題を想定して考えてみましょう。今回モデルとしてはほとんど完璧な結果がでました。ビジネスの現場では、結果からどの特徴量が結果にどれくらい影響しているのかを定量評価したいです。ロジスティック回帰ではオッズ比を用いて、目的変数に対する各入力変数の影響の大きさを確認します。

ここで pp を「事象が起こる確率」とすると、「事象が起こらない確率」は (1p)(1-p) で表すことができます。ロジスティック回帰は確率を出力するアルゴリズムのため、これを数式で表すと、

z=β+w1x1+w2x2p=11+exp(z)1p=exp(z)1+exp(z)\begin{aligned} z &= \beta + w_1x_1+ w_2x_2 \cdots \\\\ p &= \frac{1}{1 + \exp(-z)} \\\\ 1 - p &= \frac{\exp(-z)}{1 + \exp(-z)} \end{aligned}

とできます。この「起こる確率 pp」と「起こらない確率 (1p)(1-p)」の比をオッズと呼びます。

p1p=exp(z)\begin{aligned} \frac{p}{1 - p} &= \exp(z) \end{aligned}

ここから目的変数に対して入力変数がどれだけ影響を与えているかをオッズ比を用いて比較していきます。オッズ比とは、ある事象の 1 つの群と 1 つの群におけるオッズの比として定義されます。オッズ比が 1 となると事象の起こりやすさが同じことを表しますし、1 よりも大きい(小さい)とオッズ A がオッズ B よりも起こりやすい(起こりにくい)ということになります。

このようにオッズ比を利用すると、各入力変数が目的変数に与える影響の大きさを比較することが可能です。オッズ比の値が大きいほど、その入力変数によって目的変数が大きく変動することを意味しており、これを影響の大きさとしています。

オッズ比を求めるには重み ww に対して、

exp(w)\begin{aligned} &\exp(w) \end{aligned}

exp()\exp(\cdot) を取ることで求めることができます。np.exp() を使用します。

# 各オッズ比を確認
fig = plt.figure(figsize=(7, 15))

for i in range(len(log_reg.coef_)):
    ax = fig.add_subplot(3, 1, i+1)
    odds_ratio = np.exp(log_reg.coef_[i])
    ax.barh(y=dataset.feature_names, width=odds_ratio)
    ax.set_title('Class {} '.format(i))
      
<Figure size 504x1080 with 3 Axes>
# カテゴリ 0 の場合
print('重み(係数):',log_reg.coef_[0])
print('オッズ比:',  np.exp(log_reg.coef_[0]))
      
重み(係数): [-0.39765925 0.83421173 -2.28946117 -0.97844057] オッズ比: [0.67189094 2.30299796 0.10132104 0.37589683]

例えば、上記の結果から sepal width が 1 増えるとカテゴリ 0 に当てはまる確率が約 2.30 倍になるとも表現できます。

このように入力変数と目的変数の関係性を捉えることができることもロジスティック回帰の大きな特徴です。

それでは最後に予測値を確認してみましょう。分類のアルゴリズムには確率を予測できるものと確率を予測できないものに分けることができます。特に、分類器では前者を識別モデル、後者を識別関数と呼びます。

ロジスティック回帰は識別モデルに該当し、predict() メソッドで推論を行い分類結果を取得でき、predict_proba() メソッドで確率を取得することができます。

また、scikit-learn を用いて推論を行う際の入力変数は行列である必要があります。例えばテストデータの 1 サンプル目を使用して、推論を行う際には [x_test[0]] という風にスライスした値を更に [] で囲う、もしくは reshape() メソッドでベクトルから行列に変換しなければいけません。

# 目標値の取得
log_reg.predict([x_test[0]])
      
array([2])
# 各カテゴリに対する確率の確認
log_reg.predict_proba([x_test[0]])
      
array([[1.31683748e-04, 5.98482489e-02, 9.40020067e-01]])

確かにカテゴリ 2 に対する確率が最も高いことが確認できました。

分類の評価方法

分類において、学習済みモデルを評価する指標には様々なものがありますが、その中でも代表的なものに下記の 4 つが挙げられます。

  • Accuracy\mathrm{Accuracy}(正解率)
  • Precision\mathrm{Precision}(適合率)
  • Recall\mathrm{Recall}(再現率)
  • F1score\mathrm{F1 score}(F 値)

分類の評価方法では、ここまで Accuracy(正解率)を使用してきました。しかし、実問題では Accuracy だけを用いて評価を行っていると危険性がある場合があります。本節では、分類で使用されるいくつかの評価方法を紹介していきます。

例えば、ラベルの種類が 2 種類しかないような二値分類の問題設定でデータセットの中身の 99% がラベル 0、そして残りの 1% がラベル 1 というような割合の場合に Accuracy を最大化するためにどうするでしょうか。 全てをラベル 0 と答え、ラベル 1 に対しては全く分類しないという選択を取ることが考えられます。

なぜなら、そのような選択を取ることにより、Accuracy は必然的に 99% になると言えるためです。このような結果が望ましい問題設定もあれば、望ましくない問題設定もあることが考えられます。

ここではがん診断の例を用いて Accuracy 以外のモデルの評価指標について確認しましょう。

人数
全体 260
健康な人 200
がん患者 60

上記は実測値です。Precision など Accuracy 以外の指標を理解するためには、この実測値(実際の状態)と予測値(診察結果)の関係性を理解することが重要です。この関係性を理解するために混同行列 (Confusion Matrix) と呼ばれる以下の表を使用します。

17_6

表の見方ですが、診察結果(縦の方向)と実際の状態(横方向)の関係を表したものです。それぞれの値には名前が付いており、下記のように表記され呼ばれます。

  • TP (True Positive、真陽性):を正例として、その予測が正しい場合の数
  • FP (False Positive、偽陽性):予測値を正例として、その予測が誤りの場合の数
  • TN (True Negative、真陰性):予測値を負例として、その予測が正しい場合の数
  • FN (False Negative、偽陰性):予測値を負例として、その予測が誤りの場合の数

こちらの混合行列を軸に各指標を確認していきます。今回のがん診断では、正例をがん患者負例を健康な患者として話を進めていきます。

また、突然英語が出てきたのですが True/False は予測したものが正しいか誤りか、Positive/Negative は予測値を正例としたか負例としたかと考えると組み合わせで理解しやすいです。

Accuracy(正解率)

Accuracy の式は下記になります。

TP+TNTP+FP+TN+FN=Accuracy10+19510+5+50+19579%\begin{aligned} \frac{TP + TN}{TP+FP+TN+FN} &= \mathrm{Accuracy} \\\\ \frac{10+195}{10+5+50+195} &\risingdotseq 79 \% \end{aligned}

全ての値を足し合わせて、実際にどれだけ合っているのか測るのが、Accuracy です。Accuracy は分類の精度を確認するための指標として最も一般的なものです。

17_11

上記の図では特に注目すべき箇所に斜め線を入れています。混合行列の斜めラインを注目しましょう。覚えるときはイメージで覚えると思い返すことができてオススメです。

Precision(適合率)

17_12

TPTP+FP=Precision1010+567%\begin{aligned} \frac{TP}{TP+FP} &= \mathrm{Precision} \\\\ \frac{10}{10+5} &\risingdotseq 67\% \end{aligned}

Precision は、混同行列を縦方向に捉えます。正例(がん)と予測したもののうち、本当に正しく診断できた数の割合を表します。誤診を少なくしたい場合は Precision を重視することになります。

Recall(再現率)

17_13

TPTP+FN=Recall1010+5016%\begin{aligned} \frac{TP}{TP+FN} &= \mathrm{Recall} \\\\ \frac{10}{10+50} &\risingdotseq 16\% \end{aligned}

Recall は、混同行列を横方向に捉えます。実際の状態が正例(がん)のうち、どの程度正例であると予測できた数の割合です。誤診は許容するが、正例の見逃しを避けたい場合に Recall を重視することになります。

F1 score(F 値)

Precision と Recall は互いにトレードオフの関係にあります。どちらかの値を上げようとすると、もう一方の値が下がることになります。つまり、どちらかの指標を考慮しなければ、もう片方を 1 に近づけることができるので指標として少し極端な評価指標ということになります。

そこで、Precision と Recall の両者のバランスを取るために調和平均で計算される指標が F1-score です。

2RecallPrecisionRecall+Precision=F1score\begin{array}{c} \frac{2 \cdot \mathrm{Recall} \cdot \mathrm{Precision}}{\mathrm{Recall} + \mathrm{Precision}} = \mathrm{F_1 score} \end{array}

これらの指標は、取り組みたい問題設定によってどの評価指標を選択するかは異なります。問題設定合わせて重視するポイントをしっかり考慮し、最適な指標を選択しましょう。

scikit-learn で評価指標を確認

分類における評価指標を議論する際には不均衡データ (Imbalanced data) と呼ばれるデータ量に偏りがあるデータを使用すると分かりやすいので、こちらから用意した classification_imb.csv をダウンロードして Colab 上にアップロードしてください。

from google.colab import files
uploaded = files.upload()
      
df = pd.read_csv('classification_imb.csv')
df.head()
      
x1 x2 x3 x4 x5 x6 x7 x8 x9 x10 x11 x12 x13 x14 x15 x16 x17 x18 x19 x20 x21 x22 x23 x24 x25 x26 x27 x28 x29 x30 x31 x32 x33 x34 x35 x36 x37 x38 x39 x40 x41 x42 x43 x44 x45 x46 x47 x48 x49 x50 x51 x52 x53 x54 x55 x56 x57 Target
0 0 2 0 0 6 1 0 0 0 0 0 0 0 0 0 0 0 0 0.3 0.2 -1.000000 10 1 -1 0 0 11 1 1 0 1 3 2 0.424264 1.048415 0.434971 3.464102 0.5 0.7 0.2 0 2 10 1 10 1 8 4 4 3 9 0 1 1 0 1 0 1
1 1 3 8 0 0 1 0 0 0 0 0 0 0 0 13 0 1 0 0.4 0.7 1.401116 10 1 -1 0 1 1 1 1 0 1 74 3 0.374166 0.716511 0.386135 3.162278 0.7 0.6 0.5 1 0 9 1 10 1 10 7 3 3 8 0 1 1 1 1 0 1
2 2 1 1 1 0 0 0 0 1 0 0 0 0 0 3 0 0 1 0.6 0.7 1.348610 6 1 -1 0 0 0 1 1 0 1 34 2 0.400000 0.804109 0.378021 3.316625 0.8 0.0 0.4 2 2 9 4 9 1 3 4 1 2 8 0 1 1 1 0 0 1
3 5 1 9 0 0 1 0 0 0 0 0 0 0 0 12 1 0 0 0.2 0.3 -1.000000 5 1 0 0 1 1 1 1 2 1 65 3 0.316228 0.393385 0.293258 0.000000 0.0 0.2 0.6 2 2 9 1 7 0 9 5 1 3 8 0 1 1 0 0 0 1
4 0 1 2 0 0 1 0 0 0 0 0 0 0 0 10 1 0 0 0.1 0.1 -1.000000 11 1 0 0 0 0 1 1 2 1 39 1 0.316070 0.622063 -1.000000 2.645751 0.5 0.8 0.9 2 2 9 4 9 1 7 4 0 3 6 0 1 0 1 0 1 1
df.shape
      
(29760, 58)

まずは、今回のデータセットの目的変数である Target のデータの個数を集計し可視化しましょう。

sns.countplot(x='Target', data=df);
      
<Figure size 432x288 with 1 Axes>

0 と 1 のラベルが付いた二値分類です。0 のラベル数が極端に少ない、不均衡なデータの問題設定になります。

入力変数 xx と目的変数 tt の切り分けを行い、学習用データセットとテスト用データセットへの分割を行います。

# 入力変数と目的変数の切り分け
x = df.drop('Target', axis=1).values
t = df['Target'].values

print(x.shape, t.shape)
      
(29760, 57) (29760,)
# 学習用データセットとテスト用データセットの分割
from sklearn.model_selection import train_test_split
x_train, x_test, t_train, t_test = train_test_split(x, t, test_size=0.3, random_state=0)
      

モデル構築

ロジスティック回帰でモデル構築を行いましょう。

# モデルの定義
log_reg = LogisticRegression()
      
# モデルの学習
log_reg.fit(x_train, t_train)

# モデルの検証
print(log_reg.score(x_train, t_train))
print(log_reg.score(x_test, t_test))
      
0.9632776497695853 0.961581541218638

ロジスティック回帰の score() メソッドでは Accuracy が算出されます。約 96% の学習済みモデルができたように見えます。実際にはどうなのでしょうか。確認してみましょう。

# 推論
y_predict = log_reg.predict(x_test)
      

予測値の中身を確認するために、NumPy の .unique() メソッドを使用することでベクトル内の重複を除いた固有の値(ユニークな値)を出力してくれます。

# ユニークな値
np.unique(y_predict)
      
array([1])

すると、予測値が全てラベル 1 となっています。目標値も確認してみましょう。return_counts=True とすると、固有な値のそれぞれの個数も集計してくれます。

np.unique(t_test, return_counts=True)
      
(array([0, 1]), array([ 343, 8585]))

ラベル 0 も 343 サンプルあることが分かりました。しかし、今回はデータセット全体のほとんどをラベル 1 が占めていることからとりあえずラベル 1 と予測しておけば Accuracy の値は高くすることができます。

このように Accuracy のみを評価指標として取り扱うと今回の不均衡データセットを取り扱う際などには適切にモデルの性能を評価することができないことが分かりました。

混同行列 (Confusion Matrix)

混同行列を表示して、他の評価指標でも確認しましょう。混同行列やその他の評価指標には sklearn.metrics 以下の関数を使用します。

from sklearn import metrics
      
# ラベルの取り出し
labels = list(np.unique(t_train))
labels
      
[0, 1]
# 混同行列の取得
confusion_matrix = metrics.confusion_matrix(t_test, y_predict)
confusion_matrix
      
array([[ 0, 343], [ 0, 8585]])

metrics.confusion_matrix を使うと混同行列が取得できます。改めてヒートマップを使って見やすく表示します。

# ヒートマップで可視化
plt.figure(figsize=(10, 7))
sns.heatmap(confusion_matrix, annot=True, fmt='.0f', cmap='Blues');
      
<Figure size 720x504 with 2 Axes>

確かに全てをラベル 1 と予測していることが確認できました。

Precision・Recall・F1score

それぞれのスコアは metrics モジュール内の関数に、目標値と予測値を渡すことで確認できます。また、引数に average=None と指定することによって、それぞれのラベルを正例としたスコアを確認することができます。

precision = metrics.precision_score(t_test, y_predict, average=None)
precision
      
array([0. , 0.96158154])

ここから、正例がラベル 1 のときにラベル 1 と予測した内の約 4% が間違っていることがわかります。

recall = metrics.recall_score(t_test, y_predict, average=None)
recall
      
array([0., 1.])

正例がラベル 1 のとき、目標値がラベル 1 のデータを正しく全て予測できているため値が 1 となっています。見逃ししないモデルはできているということになります。

f1_score = metrics.f1_score(t_test, y_predict, average=None)
f1_score
      
array([0. , 0.98041455])

F1score では Precision と Recall の間のような値を取得できていることが確認できます。上記の評価指標を一括でまとめて確認する際には metrics.precision_recall_fscore_support を使用します。

precision, recall, f1_score, total = metrics.precision_recall_fscore_support(t_test, y_predict)
      
# ヒートマップで可視化
df_total = pd.DataFrame(
    np.array([total, precision, recall, f1_score]), 
    index=['Total', 'Precision', 'Recall', 'F1_score'], 
    columns=['Label 0','Label 1']
)

plt.figure(figsize=(10, 7))
sns.heatmap(df_total, annot=True, fmt='.6f', cmap='Blues');
      
<Figure size 720x504 with 2 Axes>

このように分類の問題設定に取り組む際は、データの偏りなどに応じて評価指標を正しく選択し、評価を行うことが重要です。

shareアイコン