決定境界/クラス分類の分布を描く関数

概要

2つの特徴量を持つデータセットを学習したモデルに対し、2次元の特徴量空間における決定境界やクラス分類の分布を描く関数の例。

draw_decision_boundary()で決定境界の線を描き、draw_decision_area()で領域のクラス分布を色分けで表示する。

関数の使い方

それぞれの関数単体では特にパッケージは必要ないが、いくつかのパラメーターは一定のクラスを想定している。

draw_decision_boundary()

draw_decision_boundary(clf, ax, x0s, x1s, threshold, color, alpha)

clf
学習済みのクラス分類モデルのインスタンスを指定する。predict()メソッドを持つこと(引数は2次元配列を想定)。
ax
決定境界を描くAxesオブジェクト。
x0s, x1s
クラスを計算する領域の計算点の座標を1次元配列で指定。
threshold
決定境界の値を整数で与える。デフォルトは0。決定値がクラスラベル(例えば0と1)で与えられる場合はその平均(たとえば0.5)を与える。
color
決定境界の場合はカラーコード。デフォルトは’k’(黒)
alpha
分布図の場合の塗りつぶしの透明度を実数で指定。デフォルトは1(不透明)

draw_decision_field()

draw_decision_field(clf, ax, x0s, x1s, n_areas=2, colors, alpha)

clf
学習済みのクラス分類モデルのインスタンスを指定する。predict()メソッドを持つこと(引数は2次元配列を想定)。
ax
決定境界を描くAxesオブジェクト。
x0s, x1s
クラスを計算する領域の計算点の座標を1次元配列で指定。
n_areas
分割される領域の数を整数で指定。デフォルトは2(2つの領域)
colors
分割される領域を塗りつぶす色をカラーコードの配列で与える。デフォルトは['tab:blue', 'tab:oranbe']
alpha
分布図の場合の塗りつぶしの透明度を実数で指定。デフォルトは0.5(半透明)。

関数の内容

draw_decision_boundary()

pyplotのcontourを利用している。

draw_decision_field()

pyplotのcontourfを利用している。

 

決定木の境界描画

概要

書籍”Pythonではじめる機械学習”の決定木のところで、ノードの分割をするごとの境界を描いている。

書籍ではmglearnパッケージを使っているが、これを自前の関数で再現した例。

関数の仕様

描画用の関数draw_tree_boundary()の引数は以下の通り。

draw_tree_boundary(tree, ax, left, right, bottom, top, i_node=0, stop_level=None, n_level=0)

tree
描きたい決定木モデルのtree_オブジェクトを渡す。
ax
境界図を描くターゲットのAxesオブジェクトを渡す。
left, right, bottom, top
その時点でのノードの描画範囲をaxに即した座標で指定する。
i_node
エリアを描画するノード。省略した場合のデフォルトは0で、ルートノード(全域)以下を描画。
stop_level
描画する木の深さを指定。デフォルトはNoneで、この場合は最深部まで描く。
n_level
この関数の再帰呼び出しの際に内部的に使われる。

この関数を呼び出し方の例は以下の通りで、stop_levelを省略しているので、リーフノードまで含めた木全体を描いている。

関数の処理内容

この関数の大まかな処理の流れは、以下の通り。

  • ルートノードの分割から初めて、リーフノードに行きつくまで分割と下の階層の探索を再帰的に進める
  • リーフノードであればそのノードのクラスで色を塗り、親のノードに戻る
  • あるノードの左の子ノードの下のリーフノードの処理が全部終わったら、右の子ノードの処理に移り、それも終わったら親のノードに戻る

関数の処理内容を最初の呼び出しから追うと以下の通り。

  1. i_nodestop_levelを省略して呼び出し→ルートノードから木全体を描く
  2. 現在のノードがリーフノード(子ノードのインデックスが–1)あるいは現在の深さがstop_levelに達したなら、以下を実行してreturn(親ノードに戻る)
    1. 現在のノードの卓越クラスに応じてtab:orangetab:blueでフェイスカラーを設定
    2. 引数で得られた矩形領域をフェイスカラーで塗りつぶす
    3. 塗りつぶした矩形をaxに追加
  3. 現在のノードがリーフノードでなく、終了深さにも達していない場合は、現在のノードを分割する特徴量によって以下を実行してreturn(親ノードに戻る)
    1. ノードの分割基準が特徴量0の場合
      1. 分割基準の特徴量0の値で領域の上から下まで境界線を引く
      2. 左側のエリアを指定して左子ノードを処理する
      3. 戻ってきたら右側のエリアを指定して右子ノードを処理する
    2. ノードの分割基準が特徴量1の場合
      1. 分割基準の特徴量1の値で領域の左から右まで境界線を引く
      2. 下側のエリアを指定して左子ノードを処理する
      3. 戻ってきたら上側のエリアを指定して右子ノードを処理する

 

 

決定木による回帰

概要

決定木を回帰に用いる場合、回帰木(regression tree)とも呼ぶ。ここでは決定木の回帰における性質・挙動を確認する。

回帰木の学習過程

以下は、sin関数に対して回帰木を適用し、剪定の深さを深くしていった場合の推移。

剪定深さ1の場合、特徴量を2つに分割しそれぞれの領域のデータから学習し予測値を得ている。剪定深さ2の場合、さらに各領域を2分割して4つの領域で予測値を得ている。このようにして剪定深さnに対して2nの領域のデータで学習する。この例の場合は訓練セットとして80個のデータを準備し、1000個のデータの予測をしている。

剪定深さ6で26=64訓練セットの個数と近くなるが、サインカーブの山と谷のところで区間が長く、誤差が出ている。これは、回帰木のノード分割がy = sin xの値に基づいて行われるとき、その値がかなり近くなる山・谷のところでなかなか分離されないからと考えられる。

剪定深さ10で210=1024のとき、分割数がテストセットと同じくらいの数になるので初めて値が近い点も区別され、全体がフィットする。

ここで、学習途上の状況を、剪定深さ2(max_depth=2)の時の状態で確認してみる。

分割された4つの領域に対する境界(0.517, 3.142, 5.766)のうち、最初の境界3.142はπの値で、0~2πの領域においてπの両側で対称なことから自然な結果。4つの領域における予測値(value)はグラフ上でも確認でき、やはりπの両側で対称な値となっている。

ノイズの影響と過学習

先のサインカーブにノイズが乗った場合の回帰木を見てみる。剪定深さ3、5としたときの回帰木による回帰線の形は以下の通りで、深さが深いと個別のデータに対して過学習となっている様子がわかる。

これらのモデルのスコアは以下の通りで、深さ5の場合には訓練スコアに対してテストスコアが低く、過学習となっている。

深さ3の場合に訓練スコアの方がテストスコアより低いが、これは訓練スコアにノイズが含まれるのに対してテストスコアのyの値をすべてノイズがないsin値としているためで、訓練セットにおいて乱数を加える程度を小さくするとこの逆転現象は解消される。

上記の実行コードは以下の通り。

同じデータで決定木の剪定深さを変えていったときの状況を如何に示す。訓練スコアとテストスコアの関係から、深さ3までは学習不足、深さ4以降は過学習となっていることが示され、過学習になるとノイズの影響を受けていることがわかる。

決定木の限界~外挿

決定木は、与えられた訓練データに対しては完全な予測も可能だが、訓練データの領域外のデータに対しては妥当な予測ができない。書籍”Pythonではじめる機械学習”で紹介されている、メモリー単価の推移によってこれを確認する(データについてはこちらのサイトのものを使わせてもらった)。

時間をx、メモリー単価をyとするとメモリー単価を対数で表したlog yxに対して概ね線形関係になっている。以下は、縦軸を対数目盛とした場合のメモリー単価、xとlog yについて線形回帰と決定木による学習と予測の結果を示したもので、2000年より前のデータによって双方のモデルを学習させ、2000年以降の価格を予測している。

線形回帰はデータの細かい傾向は再現できないが、訓練セットの外側についてもその傾向をある程度予測できている。一方決定木については、訓練セットについては完全に予測しているが、その外側になった途端に、外側の直前のデータの値をそのまま予測値としている。

 

決定木によるクラス分類

概要

決定木をクラス分類に用いる場合、分類木(classification tree)とも呼ぶ。決定木は、決定境界が単調ではなく混み入っていて線形モデルでは分類が難しい場合でも対応できる。ここでは決定木のクラス分類における性質・挙動を確認する。

決定木の構築過程

2つの特徴量を持ち、2つのクラスのいずれかに属するデータについて、決定木が作られていく過程を追っていく。

データとしては、scikit-learnのmake_moons()で得られる以下のデータを用いる。

このデータセットについて、2つの特徴量のいずれかを調節して順次領域を分割していく。このとき、どのように分割するのが最も妥当かということについては、決定木の分割の考え方を参照。また、以下の実行例のコードについてはDecisionTreeClassifierに関するテストプログラムを参照。

まず最初の分割は以下の通りで、特徴量1の値0.272が境界となり、それ以下がクラス0、それより大きいとクラス1が卓越していると判定される。

第2ステップは、第1ステップで分けられたそれぞれの領域を分割する。どちらの領域も特徴量0が境界となっていて、それぞれの領域/ノードの分布状態に応じた特性量によって左右に分けられている。

第3ステップでは、左上の領域は特徴量1で上下に、右下の領域は特徴量0で左右に分割されるが、いずれについても分割後の領域のクラスが同じになっている。利得が最も高くなるように分割しても、領域の中の擾乱クラスのデータが少ない場合はこのようになる。

なお、右上の領域はクラス1のデータが2個のみ、左下の領域クラス0のデータが2個のみと単一のクラスのデータしかないため、これ以上分割されない葉(leaf)となっている。

第4ステップでは、左上と右下の領域がいずれも特徴量0で左右に分割され、今度は分割後のノードが異なるクラスになっている。左上と右下の領域(ノード)がクラス1の葉となっている。

第5ステップでは下方右から2番目の領域を特徴量1で分割している。

分割はここで終了。全てのノードが単一のクラスで構成された純粋な状態となっている。

全体としてはクラス0が左側の上に凸な分布、クラス1が左側の下に凸な分布と判定されていて、make_moons()が意図した分布と合っている。ただし左上に一つクラス1のデータがあるために、本来意図しない領域がクラス1と分割されている。

過学習の抑制

過学習の視覚的な例

決定木の構築過程で、1つのデータの影響で予想と異なるノード区分が発生するのが見られた。別のパターンのデータの例を以下に示す。少ないデータの影響で領域分割が複雑になっていることがわかる。

このような場合、特定のパターンの教師データに対しては適合度が高く、純粋な葉のみで構成される場合には適合度が100%になるが、他のデータに対しての適合度を下げてしまう。いわゆる過学習となってしまう。

決定木の過学習を防ぐための方法として、決定木の階層をあるレベルまでに留める、いわゆる枝刈り/剪定(pruning)という考え方や、葉の切り分けを純粋なレベルより前に留める(ノード内のデータ数が2つ以上複数のデータ数になったら分割を止める)という考え方がある。

これらはscikit-learnのDecisionTreeClassifierでは、枝刈りのうち事前剪定(pre-pruning)のためのパラメーターとしてmax_depth、ノードの最小データ数のパラメーターとしてmin_samples_leafが設定できる。

max_depth~剪定

以下の例は、上記のデータに対してmax_depthを変化させたときの領域分割の様子。このパラメーターはデフォルトではNoneで可能な限り分割を行っていくが、正の整数値を指定すると、その深さまでで分割を止める。分割の深さが少なくなるにしたがってモデルが単純化されていく様子がわかる。

min_samples_leaf~葉の純度の制限

min_samples_leafはデフォルトでは1で、ノードが完全に純粋でない限りデータ数が1個になるまで分割を試みる。この値を変化させて、異なるクラスのデータを含んでいても分割を行わないようにした場合の領域分割の状況を見てみる。

枝刈りに比べてモデルの複雑さは回避しながら、それらしい領域分割になっている。これは、事前枝刈りが各ノードの不純度に関わらず同じレベルで計算を止めるのに対して、葉の純度を個別にコントロールしているために柔軟に分割が進められているためと考えられる。

cancerデータによる確認

剪定による過剰適合の抑制

breast_cancerデータセットに対してDecisionTreeClassifierを適用してクラス分類し、訓練セットとテストセットのスコアを計算する。リーフノードが純粋になるまで木を成長させた場合と、深さ4で事前剪定をした場合のスコアを比較してみる。

出力結果は以下の通り。完全な木は7層で、訓練セットに対しては全データに適合しており、テストセットに対しては93.7%の適合率。一方、深さ4で枝刈りをした場合は、訓練セットに対する適合率は下がるがテストセットの適合率は上がり、過剰適合が抑制されている。

剪定の深さに対するスコアの変化

事前剪定の深さレベルを変化させたときの、訓練セットとテストセットに対するスコアの変化を確認する。train_test_split()の乱数系列によって結果のパターンが異なるが、概ねmax_depth=4で適合不足と過剰適合のバランスが最もとれているようであり、スコアは0.95程度。

ただし、そもそも決定木の深さがそれほど深くなく、max_depthのバリエーションが数個となるので、線形モデルにおけるハイパーパラメーターのような連続的な曲線は描き難い。

リーフの最小サンプル数に対するスコアの変化

葉ノードの最小サンプル数を一定値以上とするmin_samples_leafを変化させたときの、訓練セットとテストセットの変化を見てみたが、乱数系列によってけっこうパターンがばらついている。この傾向は、max_depthを変化させても変わらなかった。

特徴量重要度

特徴量重要度の特性

breast_cancerデータセットを深さ4で剪定した決定木によってクラス分類した場合の特徴量重要度は以下のようになる。このグラフを表示するコードや特徴量重要度の計算方法についてはこちらを参照。

このグラフと以下の決定木を比べ、重要度が大きい順に調べてみると以下のようなことがわかる。

  • worst radius(0.726)、第0層の不純度が高いノードを2つのクラスに分割し、分割後のノードの純度が高い
  • worst concave points(0.122)、第1層の左側のノード、259個のクラス1のデータの大部分を左側の子ノードに切り出しつつ、クラス0のデータも25を4と21と切り分けている
  • texture error(0.048)、第1層の右側のノード、クラス0のデータを完全に右の子ノードに切り分け、左の子ノードは不純度はゼロ
  • worst textureは(0.045)、第2層の左から2番目のノード、データ数は少ないが、左の子ノードにクラス1のデータを9/11、右の子ノードにクラス0のデータを18/21と子ノードの純度が高い
  • radius error(0.010)、第2層の左のノード、クラス1のデータを完全に左の子ノードに切り分け、右の子ノードの不純度はゼロ

特徴量重要度の計算方法や上記の特徴から、その性質は以下のように整理できる。

  • 重要度は0~1の間の値をとる
  • 浅いノードの対象特徴量ほど重要度が高い傾向(ただし、データの分布による可能性あり)
  • 分割後の子ノードの純度が高いほど重要度が高い傾向
  • 重要度の値は、どのクラスの切り分けに効いているかとは無関係

特徴量重要度の大きさは、枝を分離するときに重要な特徴量を示唆するが、その特徴量の大小とクラス分類の関係までは知ることができない。

線形モデルの特徴量係数との対比

特徴量重要度をLogistic回帰における特徴量の係数と比較してみる。以下はL2正則化によるLogistic回帰モデルをbreast_cancerデータに適用した場合の特徴量係数。

worst radiusについては、決定木での重要度が最も高いが、Logistic回帰でも比較的特徴量の重みは大きい。Logistic回帰の場合はこの特徴量がターゲット1(malignant:良性)であることを示唆しているが、決定木の場合にはそのような情報は得られない。

単調でないクラス分類

以下の例では、2つのクラス分類の境界が必ずしも1つではない。

Logistic回帰モデルの場合、一つの直線で分離しようとした結果、境界は赤い線のようになり、スコアも0.66625とかなり低い。

決定木はこのような場合でも分類可能だが、そもそもこのようなケースでは(それが決定木の性質ではなく本質的に)決定境界に対する大小だけでクラス分類を論ずることができない。

max_features~特徴量選択

DecisionTreeClassifierのコンストラクターのパラメーターの1つ、max_featuresについて。このパラメーターがデフォルトのNoneの場合やautoを指定した場合、n_featuresすなわちすべての特徴量が比較され、最も分離の性能がいいものが選ばれる。一方、このパラメーターに整数を指定すると、分離の際にランダムにその数だけ特徴量が選ばれ、その中で分離の性能がいいものが選ばれる。max_features=1とすると、ランダムに選ばれた特徴量が、その分離性能に関わらず用いられる。

以下は、make_moonsで生成された特徴量数2のデータセットについて、max_featuresを1、2と変えた時の実行結果。max_features=1の場合は、各深さにおいて特徴量1、2がランダムに選ばれる。これは必ずしも最適な分割とならないため、ノイズのように分割が滑らかになっていない。

決定木の特徴量重要度の計算方法

概要

scikit-learnのDecisionTreeClassificationモデルにfeature_importances_というパラメーターがある。このパラメーターは1次元配列で、特徴量番号に対する重要度が実数で格納されている。

このfeature_importances_について、公式ドキュメントでは以下のように書かれている。

The importance of a feature is computed as the (normalized) total reduction of the criterion brought by that feature. It is also known as the Gini importance.

~特徴量の重要度は、対象とする特徴量から得られた基準値の減少分の(正規化された)合計値。ジニ重要度としても知られている。~

と書かれているが、ちょっと曖昧で定義がはっきりしない。ジニ重要度というのは日本語サイトではなかなかヒットしないが、英語では結構取り上げられている。たとえばこちらのサイトでは以下のように引用説明されている。

It is sometimes called “gini importance” or “mean decrease impurity” and is defined as the total decrease in node impurity (weighted by the probability of reaching that node (which is approximated by the proportion of samples reaching that node)) averaged over all trees of the ensemble.

これを読むと、それはジニ重要度/平均不純度減少量と呼ばれ、ノードの不純度の減少分の重み付き和(重みはそのノードにたどり着いたサンプル数の比率)を決定木全体にわたって平均した値、となる。

定式化

あるノードの不純度をI(tP)、その左右の子ノードの不純度I(tL), (tR)とし、それぞれのノードのサンプル数をnP, nL, nRとする(nP = nL + nR)。このとき、ノードtPの不純度の減少分の重み付き和は以下のようになる。

(1)    \begin{equation*} \Delta I(t_P) = \frac{n_p}{N} I(t_P) - \frac{n_L}{N} I(t_L) - \frac{n_R}{N} I(t_R) \end{equation*}

ここでNは全サンプル数。この値を決定木全体にわたって平均したものが特徴量重要度となるので、これをM(tP)とすると、以下のようになる。

(2)    \begin{equation*} M(t_P) = \frac{\Delta I(t_P)}{\displaystyle \sum_P^{all~nodes} \Delta I(t_P)} \end{equation*}

なお分母分子でNが共通なので、式(1)においてNで割らずに計算しても結果は同じになる。

以上から、特徴量重要度の計算は以下の手順となる。

  1. 決定木の葉ノードを除く各ノードについて以下を計算
    1. ノードの不純度とサンプル数を掛けた値(wI)を計算
    2. ノードのwIから、左右の子ノードのwIを減じた値gを計算
  2. 決定木全体のgの合計でこれを除した値を、そのノードの分割基準となった特徴量の特徴量重要度とする

feature_importances_の内容

この流れを、breast_cancerデータセットに対して以下のコードで確認してみる。パラメーターの設定はO’Reillyの”Pythonではじめる機械学習”の例に合わせていて、深さ4で事前刈込をしている。

まず、この決定木のfeature_importances_パラメーターそのものの内容は以下の通り。すべての値を合計すると1.0となる。

また、pandas.DataFrameで特徴量名と併せて表示すると以下の通り。

特徴量重要度の計算過程

特徴量重要度の計算過程を視覚的に追ってみる。まず深さ4までの決定木をgraphvizで視覚化すると以下の通り。

この決定木の葉以外のノードについて、gini不純度とサンプル数を掛け合わせた値をwIとして決定木を描きなおすと以下のとおり。また、gは着目するノードとその左右の子ノードのwIの差で、総サンプル数で無次元化しない重みによる情報利得と等価。

(3)    \begin{equation*} g = n_p I(t_P) - n_L I(t_L) - n_R I(t_R) \end{equation*}

まずmax_depth=1のとき、worst radiusについてg = 138.6130となり、特徴量重要度はこの1つに対して1となる。

次にmax_depth=2とすると、3つのノードについて特徴量とgの値、重要度は以下の通り。

特徴量 g 重要度
worst radius 138.6130 0.809982
wors concave points 23.28798 0.136083
texture error 9.229907 0.053935
171.13089 1

実際の計算結果は以下の通りで符合している。

同じように計算していき、max_depth=4の時は以下の通り。ただしここで、worst textureが2回登場していることに注意。1つ目は深さ2の左から2番目、もう1つは深さ3の右から2番目で、それぞれのノードが分割されたときのクラスは異なっている。worst textureの重要度を計算する際には、この2つのgを加えている。

特徴量 g 重要度
worst radius 138.6130 0.726829
worst concave points 23.28798 0.122113
texture error 9.229907 0.048398
radius error 1.944615 0.010197
worst texture 8.737504 0.045816
worst concavity 3.168669 0.018188
smoothness error 0.460808 0.002416
worst smoothness 2.7 0.001416
worst symmetry 2.266668 0.011885
190.7091 1

実際の計算結果は以下の通りで符合している。

 

matplotlib.pyplot.barh – 横棒グラフ

概要

barh()は横棒グラフを描く。主要なパラメーターは以下の通り。
barh(y, width, height, left, align, fc, ec, linewidth, xerr, capsize, log)

y, width, height
yは縦方向の座標で棒グラフのラベルをリスト等で指定するのが一般的。widthは棒の長さでこれもリスト等で指定。heightは棒の太さでデフォルトは0.8だが数値/リスト等で指定可。
align
alignはデフォルトで'center'だが、'edge'を指定すると棒の下側がラベルに合わせられる。上を合わせるにはheightに負の値を指定する。
fc, ec, linewidth
fc/colorは棒の塗りつぶし色、ec/edgecolorは縁の色、linewidthは縁の太さ
xerr, capsize
xerrは誤差の範囲でリスト等で指定。capsizeは誤差範囲の両端の直交線の長さ。
log
log=Trueを指定すると横軸が対数スケールになる。

実行例

基本形

基本的な使い方で、第1引数yに縦軸のラベル、第2引数widthに各棒の長さをそれぞれリストで与える。

色・枠線の指定

棒の塗りつぶし色と枠線の色・太さを指定。

高さ・位置

棒の高さ、開始位置を指定し、ラベルに対して棒の下端を合わせている。

誤差

棒の端に誤差範囲を表示。

対数軸

横軸を対数軸としている。

 

pyplot – グラフエリアが切れる・はみ出る

グラフのラベルがはみ出てしまう場合がある。

このようなときは、pyplotfigureに対してsubplots_adjust()leftbottomなどの引数でマージンを指定する。