決定木の分割の考え方

決定木の分割の考え方

決定木のデータを特性量によって分割するには、分割後のノードの状態ができるだけうまく分かれていることが必要となる。この「うまく分かれている」状態は、言い換えれば分割後のノード内のデータができるだけ「揃っている」ともいえる。たとえば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つのデータがクラスゼロと多いため、不純度は比較的低い。

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

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

 

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

概要

この項は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つの決定関数から導かれた意思決定境界であり、その線上で決定関数の値が等しくなっている。

 

scikit-learn – Ridge/Lasso

概要

scikit-learnRidge/Lassoは、それぞれRidge回帰、Lasso回帰のモデルを提供する。それぞれのモデルは、LinearRegression回帰に対してL2ノルム、L1ノルムによる正則化を付加する(Ridge回帰とLasso回帰を参照)。

モデルの利用方法の概要は以下の手順でLinearRegressionとほぼ同じだが、モデルインスタンス生成時に正則化に関するハイパーパラメーターalphaを与える。

  1. Ridge/Lassoのクラスをインポートする
  2. ハイパーパラメーターalphasolver(収束計算方法)などを指定し、モデルのインスタンスを生成する
  3. fit()メソッドに訓練データを与えて学習させる

学習済みのモデルの利用方法は以下の通り。

  • score()メソッドにテストデータを与えて適合度を計算する
  • predict()メソッドに説明変数を与えてターゲットを予測
  • モデルインスタンスのプロパティーからモデルのパラメーターを利用
    • 切片はintercept_、重み係数はcoef_(末尾のアンダースコアに注意)

利用例

以下はscikit-learnのBoston hose pricesデータのうち、2つの特徴量RM(1戸あたり部屋数)とLSTAT(下位層の人口比率)を取り出して、Ridge回帰/Lasso回帰のモデルを適用している。ハイパーパラメーターはalpha=1.0で設定している(ここではpandasのDataFrameを利用しているが、配列による操作についてはLinearRegressionを参照)。

利用方法

Ridge/Lassoの利用方法はLineaRegressionとほとんど同じで、以下はそれぞれに特有の設定についてまとめる。

モデルクラスのインポート

scikit-learn.linear_modelパッケージからRidgeクラスをインポートする。

モデルのインスタンスの生成

Ridge/Lassoでは、ハイパーパラメーターalphaによって正則化の強さを指定する。

以下、RidgeLassoに特有のパラメーターのみ説明。LinearRegressionと共通のパラメーターはLinearRegressionを参照。

alpha
正則化の強さを実数で指定する。値が大きいほど正則化が強く効き、小さいほど弱くなる。alpha=0で正則化の効果はゼロとなり、通常線形回帰と同じになる。デフォルトは1.0。
max_iter
共役勾配法による収束計算の制限回数を指定する。’sparse_cg’と’lsqr’の場合はデフォルト値はscipy.sparse.linalgで規定され、’sag’の場合はデフォルト値は1000。
tol
収束計算の解の精度で、デフォルトは1e-3。
solver
'auto''svd''cholesky''lsqr''sparse_cg''sag''saga'のうちから選択される。デフォルトは'auto'
random_state
データをシャッフルする際のランダム・シードで、solver=’sag’の際に用いる。

 モデルの学習

fit()メソッドに特徴量とターゲットの訓練データを与えてモデルに学習させる(回帰係数を決定する)。

X
特徴量の配列。2次元配列で、各列が各々の説明変数に対応し、行数はデータ数を想定している。変数が1つで1次元配列の時はreshape(-1, 1)かスライス([:, n:n+1])を使って1列の列ベクトルに変換する必要がある。
y
ターゲットの配列で、通常は1変数で1次元配列。

3つ目の引数sample_weightは省略。

適合度の計算

score()メソッドに特徴量とターゲットを与えて適合度を計算する。

戻り値は適合度を示す実数で、回帰計算の決定係数R2で計算される。

(1)    \begin{equation*} R^2 = 1 - \frac{RSS}{TSS} = 1 - \frac{\sum (y_i - \hat{y}_i)^2}{\sum (y_i - \overline{y})^2} \end{equation*}

モデルによる予測

predict()メソッドに特徴量を与えて、ターゲットの予測結果を得る。

ここで特徴量Xは複数のデータセットの2次元配列を想定しており、1組のデータの場合でも2次元配列とする必要がある。

また、結果は複数のデータセットに対する1次元配列で返されるため、ターゲットが1つの場合でも要素数1の1次元配列となる。

切片・係数の利用

fit()メソッドによる学習後、モデルの学習結果として切片と特徴量に対する重み係数を得ることができる。

各々モデル・インスタンスのプロパティーとして保持されており、切片はintercept_で1つの実数、重み係数はcoeff_で特徴量の数と同じ要素数の1次元配列となる(特徴量が1つの場合も要素数1の1次元配列)。

末尾のアンダースコアに注意。

Python3 – range関数

概要

Python3のrange関数は引数で指定した範囲の整数列をrangeオブジェクトで返す。rangeオブジェクトを表示するには、list関数でリスト化する必要がある。

書式

range(end)
0から始まり、1ずつ増加してendに達しない範囲の数列
range(start, end)
startから始まり、1ずつ増加してendに達しない範囲の数列
range(start, end, step)
startから始まり、stepずつ増加してendに達しない範囲の数列

引数は整数である必要があり、そうでなければエラーとなる。

共通なのは、開始値からステップ値ずつ変化させ、終了値に達する直前までの数列を返す、というもので、ステップ値が正の場合にはstart ≤ 値 < endを、ステップ値が負の場合にはstart ≥ 値 > endを要素とする数列となる。

実行例

引数を1つだけ指定した場合は、0から引数未満の整数列となる。

引数を2つ指定すると、第1引数以上、第2引数未満の整数列となる。

引数を3つ指定すると、第1引数から第3引数ずつ増加して第2引数を超える手前までの整数列となる。

第3引数を負の数とすると、降順の数列になる。このとき、第2引数の終了値以下になる前までの数列となっていることに注意。

なお、2つだけ引数を指定して、第1引数>第2引数としても、降順の数列は得られず、空となる。

 

囲碁 – 手筋:ゲタ

 

ゲタの基本形

カケ

 

 

ケイマガケ

 

 

実戦例

シチョウが悪くても

 

 

使い分け

断点が多い時、下手にゲタにかけると大変。

 

 

ケイマガケでも方向を間違えると・・・

 

 

それでも取れるときは取れる。

 

 

応用例

 

 

 

 

 

囲碁 – 隅の曲がり四目

基本形

隅の曲がり四目は、日本棋院囲碁規約(平成元年四月)の第七条-2において「死」とされている。

第九条の「対局の停止」後での、死活確認の際における同一の劫での取り返しは、行うことができない。ただし劫を取られた方が取り返す劫のそれぞれにつき着手放棄を行った後は、新たにその劫を取ることができる。

 

 

隅の曲がり四目になる形

第1形

 

 

第2系

 

 

第3系

 

 

隅の曲がり四目にならない形

 

 

 

 

囲碁 – 定石:単独の三々入り

閉じ込め

基本形

単独で三々に入った場合、布石によって厚みが期待できるなら押さえる方向を選択。外勢を張るが後手になる。

  • 三々に入って生きた方が先手
  • 押さえて外勢を張った方が後手

入った方が生きてから先手で他へ回る。

 

 

定石はずれはこちら

三々入り後の死活はこちら

デギリ対策

デギリは無理。「グルグルマワシ」が痛快。

 

 

二段バネ

基本形

黒が伸びずに二段にハネる定石で、外勢を張りながら先手を取る方法と、後手になるが外勢と実利をある程度とる方法がある。

 

 

先手で外勢を張る場合

先手で外勢を張りたいときの打ち方。

 

 

後手で外勢と実利を取る場合

外勢と実利の両方を取りたい場合。後手になる。

 

 

 変化~分かれ

二段バネに対して反対側をハネて、守らずに切ってくるケース。

 

 

 

WordPress – functions.phpの場所

functions.phpは各テーマのディレクトリ下に存在する。

functions.phpは各テーマごとに生成されるため、意図をもってその内容を変更した後にテーマをアップデートすると、変更内容が失われる。

テーマをアップデートしても変更内容が失われないようにするには、元のテーマに対する「子テーマ」を作り、そのfunctions.phpに付加する内容を書くとよい。

 

Python3 – property – getter/setter

一般的な書き方

クラス定義でgetter/setterなどのアクセサを一般的に書くと次のようになる。

Pythonでは、メンバ変数(インスタンス変数)はパブリックアクセスが可能だが、メンバ変数への代入や参照を行った際に、裏でセッターやゲッターを呼び出すようにすることができる。また同じ手順で、インスタンスのdelete時の処理もプロパティとして登録できる。

このように見かけ上の参照・代入・削除処理から各アクセサを呼び出すために、Pythonでは組み込み関数のproperty()を使う方法と、@マークに続けたデコレータを使う方法がある。

property()関数

property関数を使うことで、getterなどをプロパティとして登録できる。次の例では、インスタンス変数_xに関するgetter、setter、deleterをxという名前のプロパティとして登録している。

property()関数の引数は以下の通り。

property(fget=None, fset=None, fdel=None, doc=None)

  • fget getterのメソッド名
  • fset setterのメソッド名
  • fdel deleterのメソッド名
  • doc プロパティの説明文(ドキュメント文字列)

propertyデコレータ

使い方

以下のように@マークに続けて書くデコレータでアクセサを定義できる。ただし以下の点に注意。

  • デコレータの最初は、@property
  • @propertyで定義できるのはgetterのみ

つまり、必ず最初に@propertyで、かつgetterを定義しなければならない。

@propertyは最初でなければならない

たとえば次のように@propertyがsetterよりも後にあると、”setterでxが定義されていませんよ”と怒られる。

@propertyはgetterしか定義できない

また@propertyでsetterを定義し、@*.getterでgetterを定義しようとすると、getterは実行されてsetterの方で”引数をセットできない”とエラーになる。

@propertyでダミーのアクセサを定義する方法

@propertyでダミーのアクセサを定義して、その後にgetterのデコレータで定義をするのは通る。

property()でドキュメント文字列だけを定義する方法

また、property()関数でメンバ変数のドキュメント文字列だけを定義したのちに、getter/setter/deleterのデコレータでそれぞれのメソッドを定義する方法もあるらしい。

 

 

 

Python3 – オブジェクトの文字列表現

__str__/__repr__

__str__メソッドはprint文やformat文の引数にオブジェクトが指定された場合に呼び出され、オブジェクトの”非公式の”文字列表現を返す。

__repr__メソッドはより正式なオブジェクトの内容を文字列で返し、2つのオブジェクトの同値性をインタープリタがチェックするときに使われる。

print文やformat文での__str____repr__の使われ方は次の通り。

  • __str__のみが定義されていれば__str__が使われる
  • __repr__のみが定義されていれば__repr__が使われる
  • __str____repr__の両方が定義されていれば__str__が使われる

エラー~str()関数による明示的な変換

ただし、以下のように文字列として直接操作しようとするとエラーになる。

これはprint文で扱われるかどうか以前の問題で、文字列にオブジェクトを結合しようとしたときに起こる。先行のオペランドが文字列で、その次に結合演算子が出てきたのでオブジェクトを文字列に変換しようとして、”オブジェクトを暗黙で文字列に変換できませんよ”とエラーになる。

また演算の順序を入れ替えると、次のようなエラーになる。

今度はオブジェクトが先行しているので、これに対して+演算子を読んだ瞬間に、インタプリタは”このオブジェクトと文字列の演算子として+はサポートされていない”と言ってくる。

これを解決するには、str()関数でオブジェクトを明示的に文字列に変換してやればよい。