DecisionTreeClassifier – Treeオブジェクト・再帰表示など

概要

Scikit-learnの決定木モデル、DecisionTreeClassifierについていろいろ試した際のコードをストック。

Treeオブジェクト内容確認

DecisionTreeClassifierオブジェクトのプロパティーtree_はデータセットに対して生成された決定木の構造が保存されている。以下はその内容を確認するためのコード。

Treeクラスはツリー内の各ノードの情報を1次元の配列でもっていて、子ノードを参照するにはノード番号に対応する配列のインデックスを参照する。Treeクラスが持っている主なプロパティーは以下の通り。

node_count
ツリーが持つ全ノード数。
children_left, children_right
各ノードの左/右の子ノードの番号を格納した1次元配列。
feature
各ノードを分割する際に使われる特徴量の番号を格納した1次元配列。
threshold
各ノードをfeatureで示された特性量で分割する際の閾値を格納した1次元配列。
value
各ノードにおける、各クラスのデータ数。クラス数分のデータを格納した1次元配列1つだけを要素とする2次元配列を、ノード数分だけ集めた3次元配列。

コードの実行結果は以下の通り。

親ノードと子ノードの関係は、たとえばノード0の左右の子ノードはchildren_leftchildren_rightの0番目の要素からノード1とノード4、ノード1の左右の子ノードはノード2とノード3、という風に追っていくことができる。

valueがややこしい。この配列は各ノードにおけるクラスごとのデータ数を格納している。全体配列の中にこのケースだとノード数に対応する7個の配列が要素として格納されているが、その配列が2次元配列になっていて、その要素の配列がクラスごとのデータを格納した配列になっている。例えば3番目の要素のクラス1の要素を取り出す場合にはvalue[3, 0, 1]と言う風に指定することになる。

Treeのコンソール表示

Treeオブジェクトのツリー構造を確認し、決定境界の描画などの準備とするために書いたコード。決定木の構造をコンソールに表示させる。2つの再帰関数を定義していて、本体は決定木学習後にそれらの関数を呼び出すのみ。

関数print_node1()は、ツリー構造をルートノードから階層が下がるごとに段下げして表示していく。このため、まず親ノードを表示してから左右の子ノードを引数として再帰呼び出しをしている。

終了条件はノードが子ノードを持たない葉(leaf)であることを利用するが、リーフの時のパラメータは以下の通りで、ここでは左子ノードの番号が−1となることを利用している。

  • 子ノードの番号が−1
  • 特性量の番号が−2
  • 特性量の閾値が−2.0

関数print_node2は、決定木の構造を枝分かれした木の形で表示する。左側のノードから右側に移るのを、コンソール上で上から下に表示していく。手順としては、

  1. リーフノードならノードの内容を出力してリターン
  2. リーフノードでなければ、
    1. 左子ノードの処理を呼び出す
    2. それが戻ってきたら(左側の全子孫ノードが出力されたら)自身の内容を出力
    3. 右子ノードの処理を呼び出す
    4. それが戻ってきたら(右側の全子孫ノードが出力されたら)リターン

引数に現在のノードの階層を保持する変数があり、その階層に応じた数のスペースでインデントすることで木の構造を表す。

出力は以下の通り。

決定木の構築過程の表示

make_monns()による2特性量のデータについて、順次ノードを分割する過程を図で描画するためのコード。

draw_tree_boundary()関数は再帰関数で、もしそのノードがリーフノードか指定された終了階層の場合はクラスに応じた色で領域を塗りつぶす。リーフノードでなければ、閾値が特性量0の場合と1の場合で境界線の縦横や開始終了位置を変化させて再帰的に関数を呼び出す。引数stop_levelに正の整数を指定することで、その階層までの描画に留めることができる。関数の内容についてはこちらを参照。

本体はデータをクラスごとの色で散布図として描き、ルートノードについてdraw_tree_boundary()を呼び出している。

以下は、実行例。

以下は、stop_levelを順次増やしていって、領域が分割される過程を描いた例。

決定木のツリー表示

DecisionTreeClassificationオブジェクトを可視化する環境によって、決定木を表示する例。

  1. 環境構築
    1. Pythonでpydotplusパッケージを導入
    2. Graphviz環境を構築
  2. 実行
    1. sklearn.tree.export_graphviz()で決定木のdotデータを得る
    2. pydotplus.graph_from_dot_data()Dotオブジェクトを生成
    3. write_png()などのメソッドでグラフを画像として書き出す

このコードはAtom上でコードを実行したため、Atomのディレクトリーに画像ファイルが書き出される。

 

決定木の分割の考え方

決定木の分割の考え方

決定木のデータを特性量によって分割するには、分割後のノードの状態ができるだけうまく分かれていることが必要となる。この「うまく分かれている」状態は、言い換えれば分割後のノード内のデータができるだけ「揃っている」ともいえる。たとえば2クラスの分類をする場合、ノード内に1つのクラスしか含まれていない場合は最も「純度が高い」状態であり、2つのクラスのデータが半分ずつ含まれている場合に最も「純度が低い」状態となる。

このような状態を定量的に表すのにエントロピーとジニ不純度という2つの考え方があるが、ここではそれらを確認する。

エントロピー(平均情報量)とジニ不純度

定義

クラスc = 1~Cに属するデータがノードtに属しており、各クラスごとのデータ数をnc、データの総数をNとする。この場合、このノードの純度/不純度を表すのに、エントロピー(entropy、平均情報量)とジニ不純度(Gini impurity)という2つの考え方がある。ノードtのエントロピーをIH(t)、ジニ不純度をIG(t)と表すと、それぞれの定義は以下の通り。

(1)    \begin{align*} I_H(t) &= - \sum_{c=1}^C p_c(t) \log p_c(t) = - \sum_{c=1}^C \frac{n_c}{N} \log \frac{n_c}{N} \\ I_G(t) &= 1 - \sum_{c=1}^C p_c^2 = 1 - \sum_{c=1}^C \left( \frac{n_c}{N} \right)^2 \end{align*}

エントロピーの対数の底は何でもいいが、分類するクラス数にすると最も高いエントロピーが1になって都合がよいようだ)。

ジニ不純度については次の表現の方が直感的にわかりやすい。

(2)    \begin{equation*} I_G(t) = \sum_{c=1}^C p_c (1 - p_c) \end{equation*}

これを展開すると先のIGと同じ形になるが、この形だと関数形が上に凸でpc = 0, 1でIG = 0となることがわかる。

計算例

クラス数Cのデータについて、あるノード内のデータが全て同じクラスの場合、純度が高い/不純度が低い。

(3)    \begin{align*} I_H(t) &= - \frac{N}{N} \log_C \frac{N}{N} = 0 \\ I_G(t) &= 1 - \left( \frac{N}{N} \right)^2 = 0 \end{align*}

ノード内で全てのクラスのデータが同じ数ずつある場合、純度が低い/不純度が高い。

(4)    \begin{align*} I_H(t) &= - C \cdot \frac{N/C}{N} \log_C \frac{N/C}{N} = - C \cdot \frac{1}{C} \log_C \frac{1}{C} = 1 \\ I_G(t) &= 1 - C \left( \frac{N/C}{N} \right)^2 = 1 - \frac{1}{C} \end{align*}

分布

あるノード内にN個の2クラスデータがあり、クラス1のデータ数をnとする。このとき、クラス1のデータの発生率pに対するエントロピー、ジニ不純度の分布は以下のようになる。

(5)    \begin{align*} I_H(p; t) &= - \frac{n}{N} \log_2 \frac{n}{N} - \frac{N-n}{N} \log_2 \frac{N-n}{N} \\ &= -p \log_2 p - (1-p) \log_2 (1-p) \\ I_G(p; t) &= 1 - \left( \frac{n}{N} \right)^2 - \left( \frac{N-n}{N} \right)^2 \\ &= 1 - p^2 - (1-p)^2 \end{align*}

これらをグラフ化したのが以下の図。p = 0.5で双方最大値をとり、エントロピーは1 、ジニ不純度は0.5。グラフの形状を比較するため、ジニ不純度を2倍した線も入れている。

ノード分割の考え方~利得

親ノードを子ノードに分割するのに最も妥当な分割とするには、分割後の子ノードの純度ができるだけ高くなるような特徴量を探すことになる。ある特徴量について、親ノードから子ノードに分割したときにどれだけ純度が高くなったかを比較する量として、利得(情報利得、gain)が用いられる。

親ノードtPc = 1~Cのクラスのデータがそれぞれncずつあるとする。このときの親ノードのエントロピーIH(tP)、ジニ不純度IG(tP)は式(1)で計算される。

まずエントロピーについて考える。ある特徴量fを定めると左右のノードのデータ分布が決まり、その時の左ノードtL、右ノードtLのエントロピーが以下のように計算される。

(6)    \begin{align*} & I_H(t_L) = -p_c(t_L) \sum_{c=1}^C \log p_c(t_L) = - \sum_{c=1}^C \frac{n_c(t_L)}{N(t_L)} \log \frac{n_c(t_L)}{N(t_L)} \\ & I_H(t_R) = -p_c(t_R) \sum_{c=1}^C \log p_c(t_R) = - \sum_{c=1}^C \frac{n_c(t_R)}{N(t_R)} \log \frac{n_c(t_R)}{N(t_R)} \end{align*}

このとき、それぞれのノードのエントロピーにノードの重みwL, wRを掛け、これを親ノードのエントロピーから引いた量を利得(gain、情報利得)という。重みを各ノードのデータ数の比率とすると、利得は以下のように計算される。

(7)    \begin{align*} G_H(t_P, f) &= I_H(t_P) - w_L I_H(t_L) - w_R I_H(t_R) \\ &= I_H(t_P) - \frac{n_L}{N} I_H(t_L) - \frac{n_R}{N} I_H(t_R) \end{align*}

ジニ不純度についても同様の計算ができる。

(8)    \begin{align*} G_G(t_P, f) &= I_G(t_P) - w_L I_G(t_L) - w_R I_G(t_R) \\ &= I_G(t_P) - \frac{n_L}{N} I_G(t_L) - \frac{n_R}{N} I_G(t_R) \end{align*}

利得は、ある特徴量の値によって分割した後の状態が、分割前の状態に対してどれだけ純度が高くなったかを表す。

決定木のノードを分割するにあたっては、子ノードの純度ができるだけ高くなるように(エントロピー/ジニ不純度が小さくなるように)fを選ぶことになる。

簡単な例

特徴量が1つでデータ数が少ないケースで、利得の計算を確認してみる。

000111と並んでいる場合

クラス0、1がこのように並んでいるとき、左から境界を動かしていったときの左右のノードの不純度、利得について計算した結果は以下の通り。

この場合、当然のことながら真ん中でノードを分割することで2つのクラスがきれいに分かれ、利得もこれを表している。

00100111と並んでいる場合

今度は一部に他のクラスが紛れ込んでいる場合。左のクラス0の集団に1つだけクラス1のデータが含まれているときの挙動を確認する。

左から1つ目2つ目と境界を動かしていくと少しずつ利得が上昇するが、左側のノードにクラス1のデータが入ってきたところでその不純度が跳ね上がり、利得が下がる(8~10行目)。その後再び利得は上昇し、右側のデータがクラス1のみ3つとなった時に利得が最大となっている。このとき左側のノードにクラス1のデータが1つ含まれているが、他の4つのデータがクラスゼロと多いため、不純度は比較的低い。

利得が最大となる時でも、完全にクラスが分かれた時に比べて半分近くの利得だが、これはデータ数の多い左側に異なるクラスのデータが含まれているからと考えられる。

ここまでの計算に使ったコードは以下の通り。

 

pyplot – グラフの端が枠線で切れる

pyplotでグラフを描画したとき、軸の端の方でグラフが見切れてしまう。軸の外側も使って線や点をクリップせずに表示させるには、各グラフ描画の引数でclip_on=Falseを指定する。

 

DecisionTreeClassifierの可視化環境

概要

Pythonのscikit-learnで提供される決定木のクラス分類モデルDecisionTreeClassifierの実行結果を可視化する環境について。

Graphvizとgraphvizパッケージ

この方法は、決定木の画像がPDFとして生成され、デフォルトのPDFリーダーが自動的に起動して確認できる。画像ファイルを利用する場合、PDFから切り出すか、以下のpydotplusパッケージを利用する。

 

Graphvizとpydotplosパッケージ

この方法は、決定木の画像がファイルとして生成・保存される。画像を確認するためにファイルが保存されたディレクトリでファイルを開く手順が必要になるが、得られたファイルをそのまま活用することができる。

pydotplusのインストール

pydotplusをインストールする。

これだけでは次のようなエラーが出る。

Graphvizのインストール

Graphvizのサイトから実行ファイル(msiファイル)をダウンロード、インストールする。

実行方法1:Graphvizの実行位置を指定

以下のコード例13行目のように、Graphvizの実行プログラムの位置を指定。

実行方法2:Graphvizへのパスを環境変数に登録

環境変数に上記のGraphvizのパスを指定する。

  1. デスクトップのPCアイコンを右クリック→プロパティ
  2. システム・ウィンドウ→システムの詳細設定
  3. システムのプロパティダイアログ→環境変数ボタン
  4. 環境変数ダイアログのシステム環境変数→Pathを指定して編集ボタン
  5. 環境変数名の編集ダイアログ→新規ボタン
  6. Graphvizへのパス(例えばC:\Program Files (x86)\Graphviz2.38\bin\)を入力してOK
  7. 以下、各ダイアログでOK

環境変数を設定しておくと、毎回パスを指定しなくてよい。

dtreeviz

dtreevizのインストール

dtreevizをインストールする。

実行方法

Graphvizの実行方法2で環境変数を追加。

 

 

scikit-learn – make_moons

概要

sklearn.datasets.make_moons()はクラス分類のためのデータを生成する。上向き、下向きの弧が相互にかみ合う形で生成され、単純な直線では分離できないデータセットを提供する。クラス数は常に2クラス。

得られるデータの形式

2つの配列X, yが返され、配列Xは列が特徴量、行がレコードの2次元配列。ターゲットyはレコード数分のクラス属性値の整数。

利用例

以下の例では、noiseパラメーターを変化させている。

 

パラメーターの指定

n_samples

1つの数値で与えた場合は全データ数、2要素のタプルで与えた場合はそれぞれのクラスのデータ数。デフォルトは100。
shuffle
データをシャッフルするかどうか。デフォルトはTrue。
noise
データに加えられるノイズの標準偏差。デフォルトはノイズなし。
random_state
データ生成の乱数系列。

 

線形モデルによる多クラス分類

概要

この項はO’REILLYの「Pythonではじめる機械学習」の「2.3.3.6 線形モデルによる多クラス分類」を自分なりに理解しやすいようにトレースしたもの。扱いやすい仮想のデータセットを生成し、LinearSVCモデルでこれらを分類する流れを例示している。

例えば特徴量x1xnのデータxC1, C2の2クラスに分類する線形モデルは以下とおり。

(1)    \begin{gather*} y = b + w_1 x_1 + \cdots + w_n x_n \\ \left\{ \begin{array}{ll} y \ge 0 \quad \rightarrow \quad \boldsymbol{x} \in C_1 \\ y < 0 \quad \rightarrow \quad \boldsymbol{x} \in C_2 \end{array} \right. \end{gather*}

yの符号によってどちらのクラスに分類されるかを決定するが、1つの式で3つ以上のクラスを分類することはできない(ただし一般化線形モデル(GLM)であるLogistic回帰は多クラス分類が可能)。

このような2クラス分類を多クラス分類に拡張する方法の一つが1対その他(one-vs-rest, one-vs-the-rest, 1vR)という考え方で、1つの式によって、あるクラスとその他すべてのクラスを分けようというもの。この式の形は(1)と同じで、yの値は与えられたデータがそのクラスに属する確信度(confidence)を表す。クラスの数だけこの分類器(one-vs-the-rest-classifier)を準備し、あるデータが与えられたとき、最も確信度が高いクラスに属すると考える。たとえばn個の特徴量を持つデータの3クラス分類の場合、次のように3つの分類器を準備し、与えられたデータxycの値が最も大きいクラスに属する。

(2)    \begin{gather*} y_0 = b_0 + w_{01} x_1 + \cdots + w_{0n} x_n \\ y_1 = b_1 + w_{11} x_1 + \cdots + w_{1n} x_n \\ y_2 = b_2 + w_{21} x_1 + \cdots + w_{2n} x_n \end{gather*}

LinearSVCによる多クラス分類の例

データの準備

準備として、shikit-leran.datasetsmake_blobs()で、2つの特徴量と3つのクラスのデータセットを生成する。

LinearSVCによる学習

学習とモデルの形

scikit-learn.linear_modelLinearSVC(Linear Support Vector Classification)は多クラス分類のモデルを提供する。このモデルをmake_blobs()で生成したデータで学習させると、3行2列の係数(LinearSVC.coef_)と3要素の切片(LinearSVC.intercept_)を得る。

これらの係数の行と切片の要素は分類されるべきクラス、係数の列は特徴量に対応している。クラスに対するインデックスをc = 0, 1, 2、特徴量f0, f1に対するインデックスをf= 0, 1とすると、上記の結果は以下のような意味になる。

(3)    \begin{align*} w_{cf} &= \left[ \begin{array}{rrr} -0.17492222 & 0.23140089 \\ 0.4762125 & -0.06936704 \\ -0.18914556 & -0.20399715 \end{array} \right] \\ b_c &= [-1.07745632 \quad 0.13140349 \quad -0.08604899] \end{align*}

これらの係数、切片を用いたクラス分類の予測式は以下の通りで、LinearSVCではdecision function(決定関数)とされている。

(4)    \begin{equation*} y_c = b_c + w_{c0} \times f_0 + w_{c1} \times f_1 \end{equation*}

あるデータの特徴量f0, f1に対して上記のycが正の時にはそのデータはクラスc、負の時にはクラスc以外であると判定される。

coef_intercept_の値は、実行ごとにわずかに異なる(10−6くらいのオーダー)。LinearSVCのコンストラクターの引数にrandom_stateが含まれていて、ドキュメントに以下のような記述があった。

The underlying C implementation uses a random number generator to select features when fitting the model. It is thus not uncommon to have slightly different results for the same input data. If that happens, try with a smaller tol parameter.The underlying implementation, liblinear, uses a sparse internal representation for the data that will incur a memory copy.

Predict output may not match that of standalone liblinear in certain cases. See differences from liblinear in the narrative documentation.

訓練データに対する決定関数・確信度

データセットの100個の各データに対してyc (c = 0, 1, 2)を計算した結果は以下の通り。

たとえばNo.0のデータはクラス2に属するので確信度はy2が正となり、他の2つのクラスに対しては負の値になっている。

上の計算ではintercept_coef_を使ってもともとの決定関数の式から確信度を計算したが、LinearSVCのdecition_function()メソッドで同じ結果を得ることができる。たとえばNo.0~2のデータで計算してみると以下の通りで同じ結果。

テストデータに対する予測

3つのテストデータを用意してクラス分類をしてみる。

各データとも分類されたクラスに対応する確信度が最も高い。ただし2つ目のデータについては全てのクラスに対する確信度が負の値で、その中で最も値が大きいクラス2に分類されている。

これらを図示すると以下のようになり、クラス2に分類された▼のデータは確かにどのデータにも属していそうな位置にある。

以上のコードをまとめておく。

LinearSVCの決定境界

クラスごとのone-vs-restの決定境界

blobsデータは明確に分かれた3つのクラスに分類され、それぞれに対する決定関数の切片と係数が得られた。そこで、各決定関数の決定関数の意思決定境界(decision boundary)を描いてみる。意思決定境界は決定関数の値がゼロとなる線なので、以下の式で表される。

(5)    \begin{gather*} b_c + w_0 f_0 + w_1 f_1 = 0 \quad \rightarrow \quad f_1 = \frac{-(b_c + w_{c0} f_0)}{w_{c1}} \end{gather*}

3つの決定関数について決定境界を描いたのが以下の結果。

たとえばClass 0の実線は、Class 0の塊とその他(Class1, Class 2)の塊を1対その他で分けている。この線の上側では確信度はプラスで、下側ではマイナスとなっている。

全体を融合した決定境界

先の図の中で、各クラスの塊の近くでは、そのクラスの決定関数の値はプラスで他はマイナスとなっているが、真ん中の三角形の中や、その対角にある三角形の領域では、複数の確信度がマイナスあるいはプラスとなる。このような場合には、全クラスに対して着目するデータの決定関数値を計算し、その確信度が最も大きいクラスをそのデータのクラスラベルとして与える。

以下の図は、領域内の点について全て確信度を計算し、各点において最も確信度が大きいクラスをその点のクラスとして表現した図である。

各領域の境界が3つの決定関数から導かれた意思決定境界であり、その線上で決定関数の値が等しくなっている。

 

ndarray.reshape()の使い方

reshape()の考え方

a.reshape(d1, ..., dn)として変形する場合

  • n次元の配列になる
  • d1 + ... + dn = a.sizeでなければならない

要素が1つの場合

ndarrayの引数に1つの数値を指定するとndarrayクラスだが数値のように表示される。

これをreshape(1)とすると、1要素の1次元配列になる。

reshape(1, 1)とすると、1要素の2次元配列になる。reshape(1, 1, 1)なら3次元配列。

2次元化、3次元化された配列をreshape(1)とすると、1要素の1次元配列になる。

1次元配列の変形

2次元1行の配列への変形

1次元配列をreshape(1, -1)とすると、その配列を要素とする2次元1行の配列になる。

2次元1列の配列への変形

1次元配列をreshape(-1, 1)とすると、その配列を要素とする2次元1列の配列となる。

任意の次元の配列への変形

1次元配列をreshape(m, n)とすると、m行n列の2次元配列になる。m×nが配列のサイズと等しくないとエラーになる(いずれかを−1として自動設定させることは可能)。

3次元以上の配列へも変形可能。

1次元配列への変換

任意の形状の配列aについてreshape(a.size)とすることで、1次元の配列に変換できる。

 

Python – itertools

概要

itertoolsは高速でメモリー効率のよいイテレーターを生成するツールを提供する。

主となる引数にはコレクション(リスト、タプル)を与える。

文字列を渡すと文字列中の1文字ずつを要素としたリストと同じ効果。

range()関数などコレクションを生成する対象も使える。

無限イテレーター(infinite iterators)

無限イテレーターは、コレクションの要素を繰り返し取り出し続ける。ループ処理に使う場合、break文などの終了処理が必要。

count()

itertools.count(start, [step])
startに与えた数値から初めてstepずつ増加させて取り出す。stepを省略した場合は1ずつ増やす。

cycle()

itertools.cycle(p)
コレクションpを与えて、その要素p0, p1, …, plastを取り出し、その後p0へ戻って繰り返す。

repeat()

itertools.repeat(elem [, n])
elemで与えた要素を第2引数で与えた数値の回数分繰り返す。第2引数を省略すると無限回繰り返す。

組み合わせイテレーター(combinatoric iterator)

組み合わせイテレーターは、コレクションの要素から指定した数を取り出し、それらの直積、順列、組み合わせを結果とする。

product()

itertools.product(p [, repeat=n])
コレクションpの要素について、repeatで指定した数の直積の結果をタプルで返す。同一の要素、順番の異なる同じ組み合わせの要素を持つ結果を許す。
第2引数repeatを省略すると要素数1のタプルを返す。

permutations

itertools.permutations(p [, r=n])
コレクションpの要素について、rで指定した数の順列の結果をタプルで返す。統一要素の組はなく、同じ組み合わせの要素の順番が異なる結果は許す。
第2引数はrepeatではなくrである点に注意。rを省略すると、全ての要素に対する組み合わせを返す。

combinations

itertools.combinations(p, repeat=n)
コレクションpの要素について、repeatで指定した数の組み合わせの結果をタプルで返す。同一要素の組はなく、同じ組み合わせで順番が異なるものは同じ結果となる。
第2引数rは省略できない。省略するとそれ以降の実行がされないなど動作が不定になる。

combinations_with_replacement

itertools.combinations_with_replacement
組み合わせに、同一要素の重複を許す。
第2引数rは省略できない。省略するとそれ以降の実行がされないなど動作が不定になる。

 

scikit-learn – make_blobs

概要

sklearn.datasets.make_blobls()は、クラス分類のためのデータを生成する。blobとはインクの染みなどを指し、散布図の点の様子からつけられてるようだ。

標準では、データの総数、特徴量の数、クラスターの数などを指定して実行し、特徴量配列X、ターゲットとなるクラスデータyのタプルが返される(引数の指定によってはもう1つ戻り値が追加される)。

得られるデータの形式

特徴量配列Xは列が特徴量、行がレコードの2次元配列。ターゲットyはレコード数分のクラス属性値の整数。

利用例

そのままscikit-learnのモデルの入力とする。

クラスごとに色やマークを変えて散布図を描く。

パラメーターの指定

主なもの。

n_samples
整数で指定した場合、生成されるサンプルの総数で戻り値Xの行数になる。配列で指定した場合、その要素数がクラスターの数となり、各要素はクラスターのデータ数となる。デフォルトは100。
n_features
特徴量の数で、戻り値Xの列数になる。デフォルトは2
centers
クラスター中心の数。n_samplesを整数で指定してcentersを指定しない場合(デフォルトのNoneの場合)、centers=3となる。n_samplesを配列で指定した場合はNoneか[n_centers, n_features]の配列。
center_std
クラスターの標準偏差。

 

Logistic回帰~cancer~Pythonではじめる機械学習より

モデルの精度

breast_cancerデータセットに対してLogistic回帰モデル、scikit-learnLogisticRegression適用し、訓練データとテストデータのスコアを計算してみる。

(注)solverに関する警告と計算結果

上のコードを実行したとき、結果は書籍と整合しているが、警告表示が出た

この時点でscikit-learnのバージョンが古く(0.21.3)、将来のデフォルトが変更されるとのこと。そこでインスタンス生成時にデフォルトのソルバーを明示的にsolver='liblinear'と指定して実行すると、警告は出ず値もそのまま。

なお、solver='lbfgs'としてみたところ、計算が収束しない旨の警告が出た。

そこで収束回数を増やしていったところ、最大回数2000では収束せず、3000で収束し、警告は出なくなった。

その後、scikit-learnのバージョンを0.23.0にアップグレードしたところ、デフォルトで警告は表示されず、収束回数に関する警告が同じように出て、結果も再現された。以下、ソルバーとしてliblinearを明示的に指定し、random_stateの値も書籍と同じ値として確認する。

学習精度の向上

先のC=1.0liblinearによるスコアは、訓練データに対して0.953、テストデータに対して0.958と両方に対して高い値となっている。ここで、訓練データとテストデータのスコアが近いということは、適合不足の可能性がある。そこでC=100と値を大きくして、より柔軟なモデルにしてみる(柔軟なモデルとは、正則化を弱めて訓練データによりフィットしやすくしたモデル)。

訓練データ、テストデータともそれぞれ若干向上している。なお、Cの値を1000、10000ともっと大きくしてもスコアはほとんど変わらない。

今度は逆に、Cの値を1.0より小さくして正則化を強めてみると、訓練データ、テストデータ両方に対するスコアが下がってしまう。

Cを変化させたときの学習率曲線は以下の通り。Cが10より小さいところでは正則化が強く学習不足、そこを超えると学習率が頭打ちで、学習率の改善はそれほど顕著ではない。Logistic回帰モデルの学習率曲線のバリエーションについては、こちらでまとめている。

特徴量の係数

L2正則化の場合

breast_cancerデータセットに対してLogisticRegressionを学習させた場合の、30個の特徴量に対する係数をプロットする。liblinearソルバーで、デフォルトでL2正則化を行っている。Cの値が大きいほど正則化の効果が弱く、係数の絶対値が大きくなっている。

書籍で注意喚起しているのは3番目の特徴量mean perimeterで、モデルによって正負が入れ替わることから、クラス分類に対する信頼性を問題にしている。

ここで書籍について以下の点が気になった。

  • logreg001のインスタンス生成時にC=0.01としているが、凡例で”C=0.001″としている(グラフの結果はあまり変わらない)
  • logreg100C=100とすると、書籍にあるような結果にならない(worst concave pointsが-8以下になるなど、分布が大幅に変わってくる)
  • C=20とすると、概ね書籍と同じ分布になる(若干異なる部分は残る)

いずれにしても”Pythonではじめる機械学習”は、入門者にとってとてもありがたいきっかけを提供してくれる良著であることに変わりはない。

L1正則化の場合

ソルバーを同じliblinearとして、penalty='l1'と明示的に指定する。今回はL2正則化の時と違って、C=0.001はコード中に明示され、C=100としてスコアの計算結果まで合う。ただしset_ylim()によって表示範囲を制限しており、C=100に対するいくつかの点が枠外にある。

L1正則化によって、多くの係数がゼロとなり、少ない特徴量によるシンプルなモデルでそれなりのスコアを出している。

係数の符号と選択確率について

ターゲットのクラスは、malignant(悪性)が0、benign(良性)が1で、係数が正の場合は良性となる確率を上げる方向に、負の場合は悪性となる確率を上げる方向に効くことになる。

ここでL2正則化のworst concavityを見てみると、負~0の値をとっているが、元のデータを俯瞰すると良性の集団の方が全体的に高い値を示していて矛盾している。一方、L1正則化の場合は、C=0.001で全ての係数がゼロとなっていて、結果に影響していないことを示唆している。

L1正則化で正則化の程度を弱めて、C=1, 0.5, 0.1としてみると、worst concavityは結局ゼロとなるが、worst textureは一貫して負の値を維持している。この傾向はarea errorにも僅かだが見られる。

cancerデータを俯瞰してみると、worst textureは良性・悪性の分布がかなり重なっていて、悪性のデータのボリュームが大きい。area errorも両クラスのデータが近く、値が小さく、良性のデータ量が卓越している。

ヒストグラムを見る限りほとんどの特性量の値が大きいときに良性を示唆しているようみ見えるが、Logistic回帰の結果からは、多くの特性量が効いておらず、中には分布からの推測と逆の傾向を示す。