実装関連事項

各種プログラミング言語の基本的な書き方やソフトウェア等の使用方法について.

決定木 (decision tree) は最も簡単な論理体系からなる機械学習法のひとつ.サポートベクターマシンをはじめとするアドホックな解析をしなければ変数の重要度を出せないような手法,すなわち,ブラックボックス的な学習法とは異なり,計算の過程において重要な特徴量が自然に明らかにされる.決定木で構築した予測器の性能は多くの場合,その他のファンシーな手法による予測器に劣るが,予測の根拠を論理的に解釈できるという点に利点がある.

学習させるデータの構造

以下のようなデータセットを考える.このデータセットでは,60次元 (アトリビュート) のインプットベクトルに対して0または1のスカラーの値が対応している.インプットベクトルの各アトリビュートはコンマ (,) によって分割されており,インプットベクトルとターゲットベクトル (スカラー) はタブ (\t) によって分割されている.

0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0	0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1	1
0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0	0
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0	1
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0	1
0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1	1
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0	1
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0	1
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0	0
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0	0
.
.
.

データセット

このような構造のインスタンスが400個含まれるデータをラーニングデータセット (classification_01_learning.txt),411個含まれるデータをテストデータセット (classification_01_test.txt) とする.以下からダウンロードできる.

classification_01_learning.txt

classification_01_test.txt

学習による予測器の生成

このラーニングデータセットを学習して予測器を作るには以下のようにする.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sklearn
import pydotplus
from sklearn.tree import DecisionTreeClassifier
import numpy as np
np.random.seed(0)

def main():
	# 1. reading data
	xtrain,ttrain=[],[]
	fin=open("classification_01_learning.txt","r")
	for i,line in enumerate(fin):
		line=line.rstrip()
		if line:
			tmp=line.split("\t")
			tmpx=tmp[0].split(",")
			tmpx=[float(j) for j in tmpx]
			tmpt=int(tmp[1])
			xtrain.append(tmpx)
			ttrain.append(tmpt)
	fin.close()
	xtrain=np.asarray(xtrain,dtype=np.float32)
	ttrain=np.asarray(ttrain,dtype=np.int32)
	
	# 2. learning, cross-validation
	diparameter={"criterion":["gini","entropy"],"max_depth":[i for i in range(1,6,1)],"random_state":[123],}
	licv=sklearn.model_selection.GridSearchCV(DecisionTreeClassifier(),param_grid=diparameter,cv=5,n_jobs=5)
	licv.fit(xtrain,ttrain)
	predictor=licv.best_estimator_
	sklearn.externals.joblib.dump(predictor,"predictor_dt.pkl",compress=True)
	
	# 3. evaluating the performance of the predictor
	liprediction=predictor.predict(xtrain)
	table=sklearn.metrics.confusion_matrix(ttrain,liprediction)
	tn,fp,fn,tp=table[0][0],table[0][1],table[1][0],table[1][1]
	print("TPR\t{0:.3f}".format(tp/(tp+fn)))
	print("SPC\t{0:.3f}".format(tn/(tn+fp)))
	print("PPV\t{0:.3f}".format(tp/(tp+fp)))
	print("ACC\t{0:.3f}".format((tp+tn)/(tp+fp+fn+tn)))
	print("MCC\t{0:.3f}".format((tp*tn-fp*fn)/((tp+fp)*(tp+fn)*(tn+fp)*(tn+fn))**(1/2)))
	print("F1\t{0:.3f}".format((2*tp)/(2*tp+fp+fn)))
	
	# 4. printing parameters of the predictor
	print(sorted(predictor.get_params(True).items()))
	
	# 5. printing importances of the predictor
	print(predictor.feature_importances_)
	
	# 6. drawing the decision tree
	dot_data=sklearn.externals.six.StringIO()
	sklearn.tree.export_graphviz(predictor,out_file=dot_data)
	graph=pydotplus.graph_from_dot_data(dot_data.getvalue())
	graph.write_svg("iml_sklearn_decision_tree_01.svg")

if __name__ == '__main__':
	main()

以上のプログラムにおいて学習は以下の5ステップからなる.

  1. データの読み込み.
  2. クロスバリデーションによる学習.
  3. 構築した予測器のラーニングデータセットにおける性能の評価結果の出力.
  4. 予測器のパラメーターの出力.
  5. フィーチャーの重要度の出力.
  6. 決定木の画像の出力.

26行目からの以下の部分では評価基準 (ジニ係数またはエントロピー) と決定木の深さ (1から5まで1刻み) についてグリッドサーチを行いながら,5フォールドのクロスバリデーションで予測器を複数個作り,その中で最も良い性能の予測器を最終的な予測器としている.クロスバリデーションを用いたグリッドサーチは過学習を抑制するための重要な方法.特別な事情がない限り絶対に行うべき.ここで探索すべきパラメーターはページ最下部の表にあるものとなる.グリッドサーチにおいて使用するスレッドの数は n_jobs= に指定する.31行目では構築した予測器を保存している.

	# 2. learning, cross-validation
	diparameter={"criterion":["gini","entropy"],"max_depth":[i for i in range(1,6,1)],"random_state":[123],}
	licv=sklearn.model_selection.GridSearchCV(DecisionTreeClassifier(),param_grid=diparameter,cv=5,n_jobs=5)
	licv.fit(xtrain,ttrain)
	predictor=licv.best_estimator_
	sklearn.externals.joblib.dump(predictor,"predictor_dt.pkl",compress=True)

予測器の構築の際にクロスバリデーションをしない場合,すなわち最適なパラメーターが判っている場合は,27-30行目を以下のように書き換える.

predictor=DecisionTreeClassifier(criterion="gini",max_depth=4)
predictor.fit(xtrain,ttrain)

以上のプログラムを実行した結果は以下のようになる.この場合,正確度は0.848,MCC は0.700,F1スコアは0.856となった.また,最終的な予測器の評価基準はジニ係数であり,決定木の深さは最大で4というように決定したことがわかる.重要な変数は数値でその下に表示される.また,これによって出力されるファイルは予測器を再現するためのファイル predictor_dt.pkl と決定木の画像ファイル (.svg) である.

TPR     0.910
SPC     0.785
PPV     0.809
ACC     0.848
MCC     0.700
F1      0.856
[('class_weight', None), ('criterion', 'gini'), ('max_depth', 4), ('max_features', None), ('max_leaf_nodes', None), ('min_samples_leaf', 1), ('min_samples_split', 2), ('min_weight_fraction_leaf', 0.0), ('presort', False), ('random_state', 123), ('splitter', 'best')]
[ 0.38246486  0.          0.          0.          0.          0.          0.
  0.          0.          0.          0.          0.          0.          0.
  0.          0.24815199  0.06547114  0.          0.          0.01278733
  0.          0.07236545  0.          0.08758116  0.          0.          0.
  0.          0.01253118  0.          0.          0.          0.          0.
  0.10617543  0.          0.          0.          0.          0.          0.
  0.          0.          0.          0.01247144  0.          0.          0.
  0.          0.          0.          0.          0.          0.          0.
  0.          0.          0.          0.          0.        ]

出力される画像は以下のようになる.これによると,入力ベクトルの16番目の要素 (X[15]) の値が0.5以下またはそれ以外の条件によって400個のインスタンスは293個と107個に分けられ,293個中には179個の正例と114個の負例が,107個中には21個の正例と86個の負例が存在することとなる.このように決定木による予測器はどの変数が予測結果に影響を及ぼすのかを一目瞭然で知ることができる.

iml_sklearn_decision_tree_01.svg

構築した予測器を用いたテスト

以上で構築した予測器を読み込んで,ラーニングデータセットとは完全に独立なデータセットであるテストデータセットにおいて予測器の性能を評価するには以下のように書く.構築した予測器は,25行目のように書くことで読み込むことができる.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sklearn
import numpy as np
np.random.seed(0)

def main():
	# 1. reading data
	xtest,ttest=[],[]
	fin=open("classification_01_test.txt","r")
	for i,line in enumerate(fin):
		line=line.rstrip()
		if line:
			tmp=line.split("\t")
			tmpx=tmp[0].split(",")
			tmpx=[float(j) for j in tmpx]
			tmpt=int(tmp[1])
			xtest.append(tmpx)
			ttest.append(tmpt)
	fin.close()
	xtest=np.asarray(xtest,dtype=np.float32)
	ttest=np.asarray(ttest,dtype=np.int32)
	
	# 2. reading predictor
	predictor=sklearn.externals.joblib.load("predictor_dt.pkl")
	
	# 3. evaluating the performance of the predictor on the test dataset
	liprediction=predictor.predict(xtest)
	table=sklearn.metrics.confusion_matrix(ttest,liprediction)
	tn,fp,fn,tp=table[0][0],table[0][1],table[1][0],table[1][1]
	print("TPR\t{0:.3f}".format(tp/(tp+fn)))
	print("SPC\t{0:.3f}".format(tn/(tn+fp)))
	print("PPV\t{0:.3f}".format(tp/(tp+fp)))
	print("ACC\t{0:.3f}".format((tp+tn)/(tp+fp+fn+tn)))
	print("MCC\t{0:.3f}".format((tp*tn-fp*fn)/((tp+fp)*(tp+fn)*(tn+fp)*(tn+fn))**(1/2)))
	print("F1\t{0:.3f}".format((2*tp)/(2*tp+fp+fn)))

if __name__ == '__main__':
	main()

これを実行した結果は以下のようになる.独立なデータセットであるにも関わらずまずまずの性能が出ていることがわかる.

TPR     0.896
SPC     0.810
PPV     0.833
ACC     0.854
MCC     0.709
F1      0.863

学習パラメーター一覧

決定木でコントロールすべきパラメーターには以下のものがある.これを上述のクロスバリデーションを用いたグリッドサーチの部分で変化させることで最も良い予測器を構築する.

パラメーター詳細
criterion文字列を指定.デフォルトは gini.その他に entropy を指定できる.決定木はこの指標を基準にデータを分割する.
splitter文字列で指定.デフォルトは best.その他に random を指定できる.各ノードを分割する際の方法を指定.best を指定すると常に最良の分割が,random を指定するといくつかの最良の分割候補からランダムに分割が決定される.
max_features整数,小数,文字列または None を指定.デフォルトは None.最適な分割をするために考慮するフィーチャーの数を指定.整数を指定した場合,その個数,小数の場合全フィーチャーに対する割合個,auto を指定した場合,フィーチャー数のルート個,log2 を指定した場合,log2(フィーチャー数) 個.基本は None を使うべき.
max_depth整数または None を指定.決定木の深さの最大値を指定.過学習を避けるためにはこれを調節するのが最も重要.
min_samples_split整数または小数を指定.デフォルトは None.ノードを分割するために必要な最小サンプルサイズ.整数を指定した場合,その数,小数を指定した場合,全サンプルサイズに対する割合個.
min_samples_leaf整数か小数で指定.デフォルトは1.葉を構成するのに必要な最小限のサンプルの数.整数を指定した場合,その数.小数を指定した場合,元々のサンプルサイズに占める割合と解釈される.
min_weight_fraction_leaf数値で指定.デフォルトは0.葉における重みの総和の最小加重率を指定.何のためのパラメーターなのか不明瞭.
max_leaf_nodes整数または None を指定.デフォルトは None.生成される決定木における最大の葉の数を指定.
class_weightディクショナリ,ディクショナリのリスト,balanced または None を指定.デフォルトは None.ディクショナリを指定する場合,{class_label:weight} の形式で,各クラスに重みを設定できる.指定しない場合は全てのクラスに1が設定されている.balanced を指定すると,y の値により n_samples / (n_classes * np.bincount(y)) を計算することで自動的に重みを調整する.
random_state乱数のタネの指定.何かしらの整数を指定すべき.
min_impurity_split数値を指定.デフォルトは 1e-7.決定木の成長の早期停止の閾値.不純度がこの値より大きいとき,ノードは分割される.
presortTrue または False で指定.デフォルトは False.データを事前に並び替えることで計算の高速化を図る.データサイズが大きい場合はトレーニングが遅くなる可能性がある.データサイズが小さい場合や決定木の深さが制限されている場合は高速化が期待される.
Hatena Google+