下校時刻

メモ、読んだ本のまとめメモ、考えたことのメモ、その他のメモなどが書いてあるブログです(twitter: @hoture6)

「Python Machine Learning」Ch. 3(Scikit-learnいろいろ)まとめ

Ch. 3はPythonから動かせる機械学習ライブラリであるscikit-learnを使ってみようという話です。いろいろな機械学習アルゴリズムの考え方と、scikit-learnでの実装方法が示されます。scikit-learnのAPIがどの手法に対してもほぼ同じなので単に使うだけなら覚えることは少なく、アルゴリズムの原理・得意不得意とチューニングパラメータの意味を理解することが大切っぽいです。


アルゴリズムの選択

分類のための機械学習アルゴリズムはいろいろあるので、解きたい問題に対して適切なものを選ぶ必要がある。No Free Lunch theorem(どの種類の問題に対しても他のどの手法より優秀なアルゴリズムは存在しない(こういうの、後世の人間から見ると適当に当たり前のことをキャッチーに言ったもの勝ちという感じに見える))は正しくて、絶対にこのアルゴリズムを使っておけばOKということはありえない。

アルゴリズムを試す手順は以下。

  1. 使う特徴量を選び出す
  2. performance metricを決める
  3. アルゴリズムを決めて実行
  4. パフォーマンスを評価
  5. パラメータをチューニング

Perceptron

Ch. 2で実装したPerceptronは、実はscikit-learnにすでにいい感じのやつがある。

from sklearn.linear_model import Perceptron

ppn = Perceptron(n_iter=40, eta0=0.1, random_state=0)
ppn.fit(X_train, Y_train)

Y_pred = ppn.predict(X_test)

## 正解率 (accuracy)
print(sum(Y_pred == Y_test) / len(Y_test))

Logistic regression

PerceptronからAdalineへの変化は、活性化関数をステップ関数から恒等関数に変化させたことであった。さらに、活性化関数をロジスティック関数

$$\phi(z) = \frac{1}{1+\exp(-z)}$$

にしたアルゴリズムをロジスティック回帰という。基本的な枠組みはAdalineとまったく一緒であり、$\phi(z)$の関数系と、それに伴って損失関数$J$の形と重みベクトルの更新方法$\Delta \boldsymbol{w} = - \xi \nabla \boldsymbol{w}$が異なるのみである。

ロジスティック回帰では、活性化関数の値とサンプルがあるクラスに属する確率とが対応している。例えば$\phi(z(\boldsymbol{x})) = 0.8$となるような入力$\boldsymbol{x}$がクラス1に属する確率は0.8, クラス0に属する確率は0.2である。ただし、最終的な予測結果は確率でなく一つの値として出さなければならないので、$\phi(z) = 0.5$を閾値としてそれより大きければクラス1、小さければクラス0とする。

from sklearn.linear_model import LogisticRegression

lr = LogisticRegression(C=1000, random_state=0)
lr.fit(X_train, Y_train)

Y_pred = lr.predict(X_test)

## 正解率 (accuracy)
print(sum(Y_pred == Y_test) / len(Y_test))
## テストセットの1つ目のサンプルが各クラスに属する確率
print(lr.predict_proba(X_test[0, :]))

Support Vector Machine (SVM)

SVMでは、クラス間のマージンを最大化することを目的とする。線形SVMは、ロジスティック回帰とほとんど同じ結果を与える上に、ロジスティック回帰の方が重みベクトルの更新が簡単である。SVMのいいところはカーネルSVM非線形な問題を解けることである。

これまでのアルゴリズムは線形な問題(クラスが直線や平面で分類できる問題)しか解けなかった。しかし、SVMではカーネルを使うことで非線形な問題を解ける。元となる考え方は、高次元空間へのマップである。

例えば、2次元の分類問題で、クラス0のサンプルは原点付近に、クラス1のサンプルは曲線$x^{2} + y^{2} = 1$付近に散らばっていたとする。直線でこの2つのクラスを分類することは不可能であるが、3次元空間へのマップ

$$\phi(x, y) = (x, y, x^{2} + y^{2})$$

をしてあげると、この場合$z = 0.5$の平面できれいに分類できる(!)。

ということでシンプルで素晴らしいアイデアだけど、計算量的にいちいち高次元空間を考えていられないので、カーネルを導入する。実際にやることは、2サンプルの内積$\boldsymbol{x}^{(i)T} \boldsymbol{x}^{(j)}$を$\phi(\boldsymbol{x}^{(i)T}) \phi(\boldsymbol{x}^{(j)})$と置き換えてあげることである(なるほど?)。そして、この置き換えた内積カーネル関数

$$k(\boldsymbol{x}^{(i)}, \boldsymbol{x}^{(j)}) = \phi(\boldsymbol{x}^{(i)T}) \phi(\boldsymbol{x}^{(j)})$$

で計算すると考えることで、計算量が減る。よく使われるのはRBFカーネル

$$k(\boldsymbol{x}^{(i)}, \boldsymbol{x}^{(j)}) = \exp \left( - \frac{|\boldsymbol{x}^{(i)} - \boldsymbol{x}^{(j)}|}{2 s^{2}} \right)$$

である。

from sklearn.svm import SVC

svc = SVC(kernel='pdf', random_state=0, gamma=0.1, C=10)
svc.fit(X_train, Y_train)

Y_pred = svc.predict(X_test)

## 正解率 (accuracy)
print(sum(Y_pred == Y_test) / len(Y_test))

Decision Tree

決定木は、とくに分類の解釈をしたいときに有用なアルゴリズムである。データに対して次々と質問を投げかけていき、YES/NOで分類していくことで木を作っていく。すべての葉がpure(すべて1つのクラスに属する)になったら終了だが、そこまで行くと明らかに過学習になるので木の深さを制限することが多い。

さて、どういう質問をしてYES/NOの基準をどこに置くかを決めるのが重要であるが、Information gain(IG)が大きくなるような質問をする。IGは、質問前と後のimpurityの差分で定義される。$f$が質問の内容、添え字$p$と$j$がそれぞれ親と子を表すとすると、

$$IG(D_{p}, f) = I(D_{p}) - \sum_{j} \frac{N_{j}}{N_{p}} I(D_{j})$$

となる。子のimpurityが小さくなるほど(pureになるほど)、IGは大きくなる。

impurityの測り方は主に3種類ある。

  • Gini impurity ($I_{G}$)
  • Entropy ($I_{H}$)
  • Classification error ($I_{E}$)
from sklearn.tree import DecisionTreeClassifier

tree = DecisionTreeClassifier(criterion='entropy', max_depth=3, random_state=0)
tree.fit(X_train, Y_train)

Y_pred = tree.predict(X_test)

## 正解率 (accuracy)
print(sum(Y_pred == Y_test) / len(Y_test))

Random Forest

ランダムフォレストはいくつかの決定木の予測結果で多数決をとるアルゴリズムで、アンサンブル学習の一種である。アンサンブル学習の考え方は、弱い学習器を組み合わせて強い学習器を作ることでよりロバストなモデルを作るというものである。

ランダムフォレストのアルゴリズムは以下。特徴量をあえて間引くことで一つ一つの決定木の予測を少しずつ変えている。

  1. トレーニングセットとしてランダムにn個のサンプルを選ぶ
  2. m個の特徴量からランダムにd個選び、決定木を作っていく(典型的には$d = \sqrt{m}$)
  3. 上記を繰り返してk本の決定木を作る
  4. k本の決定木でそれぞれ予測をし、その多数決を取ったものを最終的な予測結果とする

ランダムフォレストの利点は、ハイパーパラメータをあまり気にしなくても良いことである。チューニングするべき主なパラメータは木の本数kのみである。

from sklearn.ensenble import RandomForestClassifier

forest = RandomForestClassifier(criterion='entropy', n_estimators=10, random_state=0, n_jobs=2)
forest.fit(X_train, Y_train)

Y_pred = forest.predict(X_test)

## 正解率 (accuracy)
print(sum(Y_pred == Y_test) / len(Y_test))

K-nearest neighbors (KNN)

KNNは怠惰学習の一種である。怠惰学習とは、トレーニングデータから学ぶのではなく、トレーニングデータを単に覚えてしまう学習法である。

KNNのアルゴリズムは以下。

  1. 距離のmetricとkを決める
  2. 分類したいサンプルのk-nearest-neighborsを見つける
  3. k個のサンプルの多数決で一番多かったクラスに分類する

kの値がunder/over fittingのバランスを取る上で重要である。また、データのすべての特徴量を標準化しておくことが必須である(そうしないとある特徴量だけ重要視されてしまうなど不公平になる)。

KNNの長所は、分類器が新たなトレーニングデータの入力に対してすぐに適用できるということである。一方で、計算量がサンプルサイズに比例して増えてしまうという短所がある。また、次元の呪いに注意。