機械学習の定番であるscikit-learnを, 機械学習可視化パッケージ yellowbrick を用いて解説する。

機械学習とは

機械学習(machine learning)は,Googleの検索エンジン,Facebookの友人紹介,Amazonのおすすめ商品の紹介,スパムメイルの分別, ドローンの宙返り,自然言語処理,手書き文字の認識,データマイニングなど様々な応用に使われている. 機械学習は,コンピュータに新しい能力を与えるための基本学問体系であり, コンピュータに知能を与えるための総合学問である人工知能の一分野と位置づけられる. 機械学習の定義には様々なものがあるが,チェッカーゲームに対する初めての学習で有名な Arthur Samuelによれば「コンピュータに明示的なプログラムを書くことなしに学習する能力を与える学問分野」 ("the field of study that gives computers the ability to learn without being explicitly programmed") と定義される.

教師あり学習

機械学習は,大きく分けて教師あり学習(supervised learning)と 教師なし学習(unsupervised learning)に分類される. 教師あり学習は,入力と出力の組から成るデータを与えて, その関係を学習するものであり,教師なし学習は, 入力だけから成るデータから学習を行うものである.

教師あり学習は,大きく回帰(regression)と分類(classification)に分けられる. 回帰は出力が連続な値をもつ場合であり, 分類は離散的な値をもつ場合に対応する. 日にちを入力として各日の商品の需要量を出力としたデータが与えられたとき, 需要量を予測するためには回帰を用いる.需要量は連続な値をもつからだ.一方,顧客の過去の購入履歴から, ある商品を購入するか否かを判定するためには分類を用いる.購入するか否かは $0,1$ の離散的な値で表されるからだ. 回帰や分類を行うための入力を特徴(feature)とよぶ.特徴は1つでも複数でも(場合によっては)無限個でもよい.

教師なし学習

教師なし学習は,出力(正解)が与えられていないデータを用いる機械学習であり, 代表的なものとしてクラスタリング(clustering)や次元削減(dimension reduction)があげられる. クラスタリングは,入力されたデータ(変数)間の関係をもとにデータのグループ化を行う際に用いられ, 次元削減は,高次元データを低次元に落とす際に用いられる. これらの手法は,教師あり学習の前処理として有用である.

線形回帰

線形回帰の仮説関数(hypothesis function)は,

$$ h_w (x)=w_0 +w_1 x_1 + w_2 x_2 + \cdots + w_n x_n $$

と表すことができる.これは,入力 $x$ と出力 $y$ が重み $w$ を用いて $h_w(x)$ の ように表すことができるという仮説を表す.

学習アルゴリズムの目的は,トレーニング集合を用いて, 最も「良い」重みベクトル $w$ を求めることである. それでは,どのような重み $w$ が「良い」のであろうか? 線形回帰では,各データ $i~(=1,2,\ldots,m)$ に対して,仮説関数によって「予測」された値 $\hat{y}^{(i)} = h_{w}(x^{(i)})$ と本当の値 $y^{(i)}$ の 誤差の自乗(2乗)が小さい方が「良い」と考える.これを損出関数 (loss function)とよぶ. すべてのデータに対する損出関数の平均値(を $2$で割ったもの.これは微分したときに打ち消し合うためである)を費用関数 (cost function) $J(w)$ とよぶ.

$$ J(w)= \frac{1}{2m} \displaystyle\sum_{i=1}^m \left( h_{w}(x^{(i)}) -y^{(i)} \right)^2 $$

費用関数 $J(w)$ を最小にする重みベクトル $w$ を求めることが線形回帰の目的となる.

実は,費用関数 $J(w)$ は凸関数になるので, これを最小にする $w$ は凸関数に対する非線形最適化の手法を用いて簡単に求めることができる.

ベクトルと行列を用いて最適な $w$ を求める公式を導いておこう. トレーニングデータをベクトル $y$ と行列 $X$ で表しておく.

$$ y = \left( \begin{array}{c} y^{(1)} \\ y^{(2)} \\ \vdots \\ y^{(m)} \end{array} \right) $$$$ X = \left( \begin{array}{cccc} 1 & x^{(1)}_{1} & \ldots & x^{(1)}_{n} \\ 1 & x^{(2)}_{1} & \ldots & x^{(2)}_{n} \\ \vdots & \vdots & \ddots & \vdots \\ 1 & x^{(m)}_{1} & \ldots & x^{(m)}_{n} \end{array} \right) $$

行列 $X$ に対しては最初の列にすべて $1$ のベクトルを追加していることに注意されたい.

重みベクトル $w$ は $n+1$次元のベクトルとして以下のように表す. $$ w = \left( \begin{array}{c} w_0 \\ w_1 \\ \vdots \\ w_n \end{array} \right) $$

このとき費用関数は,上で定義したベクトルと行列で記述すると $$ J(w)= \frac{1}{2m} (y-Xw)^T (y-Xw) $$ となる.これを $w$ で偏微分したものを $0$ とおくと方程式 $$ X^T (y-X w) =0 $$ が得られるので,$w$ について解くことによって, $$ w =(X^T X)^{-1} X^T y $$ を得る.これが線形回帰の解ベクトルとなる.

線形回帰がどれだけ良いかを表す尺度として決定係数(coefficient of determination) $R^2$ がある. $R^2$ は予測値とトレーニングデータの誤差の自乗和から計算される.

まず,最適な重み $w$ の下での誤差の自乗和(SSE: Sum of Square Error)を以下のように計算する. $$ SSE = \displaystyle\sum_{i=1}^m \left( h_{w}(x^{(i)}) -y^{(i)} \right)^2 $$ 次に,基準となる誤差の自乗和として, 平均値 $\bar{y}$ とトレーニングデータ $y^{(i)}$ の差の 自乗和(SST: Total Sum of Square)を計算する. $$ SST = \displaystyle\sum_{i=1}^m \left( \bar{y} -y^{(i)} \right)^2 $$

決定係数 $R^2$ はこれらの自乗和の比 $SSE/SST$ を $1$ から減じた値と定義される. $$ R^2 = 1 - SSE/SST $$

定義から分かるように $R^2$ は $1$以下の値をもち, $1$に近いほど誤差が少ない予測であることが言える.

例題:広告による売上の予測

広告のデータ http://logopt.com/data/Advertising.csv を用いる.

テレビ(TV),ラジオ(Radio),新聞(Newspaper)への広告から売上(Sales)を予測する.

import pandas as pd  # まずはpandasモジュールを準備する.

# csvファイルからデータ読み込み
df = pd.read_csv(
    "http://logopt.com/data/Advertising.csv", index_col=0
)  # 0行目をインデックスにする.
df.tail()
TV Radio Newspaper Sales
196 38.2 3.7 13.8 7.6
197 94.2 4.9 8.1 9.7
198 177.0 9.3 6.4 12.8
199 283.6 42.0 66.2 25.5
200 232.1 8.6 8.7 13.4

独立変数(特徴ベクトル)$X$ は TV, Radio, Newspaperの列,従属変数(ターゲット) $y$ は Salesの列である.

y = df["Sales"]
X = df[["TV", "Radio", "Newspaper"]]  # df.drop("Sales",axis=1) でも同じ
X.head()
TV Radio Newspaper
1 230.1 37.8 69.2
2 44.5 39.3 45.1
3 17.2 45.9 69.3
4 151.5 41.3 58.5
5 180.8 10.8 58.4

scikit-learnの基本手順

  • 手順1:クラスをインポートして,インスタンスを生成する.

  • 手順2:fitメソッドを用いて,データから訓練する.

  • 手順3:predictメソッドを用いて予測を行う.

線形回帰クラス LinearRegression を用いて線形回帰を行う.

from sklearn.linear_model import LinearRegression

reg = LinearRegression()  # 線形回帰クラスのインスタンス reg を生成
reg.fit(X, y)  # fitによる訓練
yhat = reg.predict(X)  # predictによる予測
print("y-切片= ", reg.intercept_)
print("係数 = ", reg.coef_)
y-切片=  2.938889369459412
係数 =  [ 0.04576465  0.18853002 -0.00103749]
SSE = ((yhat - y) ** 2).sum()  # Sum of Square Error
SST = ((y.mean() - y) ** 2).sum()  # Total Sum of Square
print("R2 =", 1 - SSE / SST)  # 決定係数 R^2
R2 = 0.8972106381789522
print(reg.score(X, y))  # 決定係数の別計算
0.8972106381789522

可視化(回帰)

yellowbrickパッケージを用いて,結果の可視化を行う.

可視化の基本手順

  • 手順1:クラスをインポートして,可視化インスタンスを生成する.

  • 手順2:fitメソッドで,データを用いて訓練する.

  • 手順3:scoreメソッドを用いて評価尺度を計算する.

  • 手順4:showメソッドを用いて図を表示する.

    回帰に対しては,以下の2種類がある.

    • 予測誤差 (PredictionError)
    • 残差プロット (ResidualPlot)
from yellowbrick.regressor import PredictionError

visualizer = PredictionError(reg)

visualizer.fit(X, y)
visualizer.score(X, y)
visualizer.show()
from yellowbrick.regressor import ResidualsPlot

visualizer = ResidualsPlot(reg)

visualizer.fit(X, y)
visualizer.score(X, y)
visualizer.show()

問題 (SAT,GPA)

http://logopt.com/data/SATGPA.csv データを用いて,2種類のSATの成績からGPAを予測せよ. さらに結果の可視化を行え.

問題(住宅価格)

http://logopt.com/data/Boston.csv のBostonの住宅データを用いて回帰分析を行え. さらに結果の可視化を行え.

medvが住宅の価格で,他のデータ(犯罪率や人口など)から予測する.

問題(車の燃費)

http://logopt.com/data/Auto.csv の車の燃費のデータを用いて回帰分析を行え. さらに結果の可視化を行え.

データの詳細については,

https://vincentarelbundock.github.io/Rdatasets/doc/ISLR/Auto.html

を参照せよ.

最初の列が燃費(mpg: Mile Per Gallon)であり,これを他の列の情報を用いて予測する.最後の列は車名なので無視して良い.

car = pd.read_csv("http://logopt.com/data/Auto.csv", index_col=0)
car.head()
mpg cylinders displacement horsepower weight acceleration year origin name
0 18.0 8 307.0 130 3504 12.0 70 1 chevrolet chevelle malibu
1 15.0 8 350.0 165 3693 11.5 70 1 buick skylark 320
2 18.0 8 318.0 150 3436 11.0 70 1 plymouth satellite
3 16.0 8 304.0 150 3433 12.0 70 1 amc rebel sst
4 17.0 8 302.0 140 3449 10.5 70 1 ford torino

問題(コンクリートの強度)

以下のコンクリートの強度の例題に対して,strength列の強度を他の列の情報から,線形回帰を用いて推定せよ. さらに結果の可視化を行え.

concrete = pd.read_csv("http://logopt.com/data/concrete.csv")
concrete.head()
cement slag ash water splast coarse fine age strength
0 540.0 0.0 0.0 162.0 2.5 1040.0 676.0 28 79.986111
1 540.0 0.0 0.0 162.0 2.5 1055.0 676.0 28 61.887366
2 332.5 142.5 0.0 228.0 0.0 932.0 594.0 270 40.269535
3 332.5 142.5 0.0 228.0 0.0 932.0 594.0 365 41.052780
4 198.6 132.4 0.0 192.0 0.0 978.4 825.5 360 44.296075

問題(シェアバイク)

以下のシェアバイクのデータに対して,riders列の利用者数を線形回帰を用いて推定せよ.ただし,date列とcasual列は除いてから回帰を行え. さらに結果の可視化を行え.

また,なぜcasual列を含めて推定をしないのか考察せよ.

bikeshare = pd.read_csv("http://logopt.com/data/bikeshare.csv")
bikeshare.head()
date season year month hour holiday weekday workingday weather temp feelslike humidity windspeed casual registered riders
0 2011-01-01 1 0 1 0 0 6 0 1 0.24 0.2879 0.81 0.0 3 13 16
1 2011-01-01 1 0 1 1 0 6 0 1 0.22 0.2727 0.80 0.0 8 32 40
2 2011-01-01 1 0 1 2 0 6 0 1 0.22 0.2727 0.80 0.0 5 27 32
3 2011-01-01 1 0 1 3 0 6 0 1 0.24 0.2879 0.75 0.0 3 10 13
4 2011-01-01 1 0 1 4 0 6 0 1 0.24 0.2879 0.75 0.0 0 1 1

ダミー変数を用いたダイアモンドの価格の予測

http://logopt.com/data/Diamond.csv からダイアモンドの価格データを読み込み,線形回帰による予測を行う.

列は ["carat","colour","clarity","certification","price"] であり,他の情報から価格(price)の予測を行え.

カラット(carat)以外の列は情報が文字列として保管されている.

これはカテゴリー変数とよばれ,sciki-learnで扱うには,数値に変換する必要がある.

pandasのget_dummies関数で数値情報(ダミー変数)に変換してから,線形回帰を行う.

たとえば,色を表すcolour列は D,E,F,G,H,Iの文字列が入っている.これを各値が入っているとき1,それ以外のとき0の数値に変換したものがダミー変数になる.

色はいずれかの値をとるので,ダミー変数は独立でない(1つが1になると,他のすべては0になる).

最初のダミー変数を除くには,get_dummies関数の引数のdrop_firstをTrueに設定すれば良い.

diamond = pd.read_csv("http://logopt.com/data/Diamond.csv", index_col=0)
diamond.head()
carat colour clarity certification price
1 0.30 D VS2 GIA 1302
2 0.30 E VS1 GIA 1510
3 0.30 G VVS1 GIA 1510
4 0.30 G VS1 GIA 1260
5 0.31 D VS1 GIA 1641
diamond = pd.get_dummies(diamond, drop_first=True)  # ダミー変数の最初のものを除く
# diamond = pd.get_dummies(diamond) # 除かなくても結果は同じ
diamond.head()
carat price colour_E colour_F colour_G colour_H colour_I clarity_VS1 clarity_VS2 clarity_VVS1 clarity_VVS2 certification_HRD certification_IGI
1 0.30 1302 False False False False False False True False False False False
2 0.30 1510 True False False False False True False False False False False
3 0.30 1510 False False True False False False False True False False False
4 0.30 1260 False False True False False True False False False False False
5 0.31 1641 False False False False False True False False False False False
y = diamond.price  # 従属変数(price)の抽出
X = diamond.drop("price", axis=1)  # 独立変数(特徴ベクトル)をpriceの列を除くことによって生成
X.head()
carat colour_E colour_F colour_G colour_H colour_I clarity_VS1 clarity_VS2 clarity_VVS1 clarity_VVS2 certification_HRD certification_IGI
1 0.30 False False False False False False True False False False False
2 0.30 True False False False False True False False False False False
3 0.30 False False True False False False False True False False False
4 0.30 False False True False False True False False False False False
5 0.31 False False False False False True False False False False False
from sklearn.linear_model import LinearRegression  # 線形回帰クラスのインポート

reg = LinearRegression()  # 線形回帰クラスのインスタンス生成
reg.fit(X, y)  # 訓練
yhat = reg.predict(X)  # 予測
print("y-切片= ", reg.intercept_)
print("係数 = ", reg.coef_)
print("決定変数= ", reg.score(X, y))  # 決定係数の別計算
y-切片=  169.17604383492107
係数 =  [12766.39597047 -1439.0853427  -1841.69054716 -2176.67218633
 -2747.14998002 -3313.1023993  -1474.56614749 -1792.01092358
  -689.29043537 -1191.16426364    15.22672874   141.2624469 ]
決定変数=  0.9581280577870392
visualizer = PredictionError(reg)

visualizer.fit(X, y)
visualizer.score(X, y)
visualizer.show()
visualizer = ResidualsPlot(reg)

visualizer.fit(X, y)
visualizer.score(X, y)
visualizer.show()

問題(車の価格)

http://logopt.com/data/carprice.csv から車の価格データを読み込み,線形回帰による予測を行え. また,結果を可視化せよ.

データの詳細は https://vincentarelbundock.github.io/Rdatasets/doc/DAAG/carprice.html にある.

車種(Type),100マイル走る際のガロン数(gpm100),都市部での1ガロンあたりの走行距離(MPGcity),高速道路での1ガロンあたりの走行距離(MPGhighway)から,価格(Price)を予測せよ.

carprice = pd.read_csv("http://logopt.com/data/carprice.csv", index_col=0)
carprice.head()
Type MinPrice Price MaxPrice RangePrice RoughRange gpm100 MPGcity MPGhighway
6 Midsize 14.2 15.7 17.3 3.1 3.09 3.8 22 31
7 Large 19.9 20.8 21.7 1.8 1.79 4.2 19 28
8 Large 22.6 23.7 24.9 2.3 2.31 4.9 16 25
9 Midsize 26.3 26.3 26.3 0.0 -0.01 4.3 19 27
10 Large 33.0 34.7 36.3 3.3 3.30 4.9 16 25

問題(チップ)

チップ(tips)データに対して線形回帰を用いてもらえるチップの額 (tip) を予測せよ. また,結果を可視化せよ.

import seaborn as sns

tips = sns.load_dataset("tips")
tips.head()
total_bill tip sex smoker day time size
0 16.99 1.01 Female No Sun Dinner 2
1 10.34 1.66 Male No Sun Dinner 3
2 21.01 3.50 Male No Sun Dinner 3
3 23.68 3.31 Male No Sun Dinner 2
4 24.59 3.61 Female No Sun Dinner 4

ロジスティック回帰による分類

ロジスティック回帰(logistic regression)は,その名前とは裏腹に分類のための手法である.

ここでは出力が $0$ もしくは $1$ の値をもつ分類問題を考える. ロジスティック回帰の仮説関数 $h_w (x)$ は,分類が $0$ になるのか $1$ になるのかを表す確率を表す. ロジスティック回帰の名前の由来は, 条件 $0 \leq h_w (x) \leq 1$ を満たすために, 線形回帰の仮説関数にロジスティック関数(logistic function; シグモイド関数)を用いることによる. ロジスティック関数 $g(z)$ は,以下のように定義される (グラフを見たい場合には,Googleで 1/(1+e^-x)と検索する).

$$ g(z)= \frac{1}{1+e^{-z}} $$

ここで $e$ は自然対数の底である.$z=0$ のときに $g(z)=0.5$ になり,$z \rightarrow \infty$ になると $g \rightarrow 1$,$z \rightarrow -\infty$ になると $g \rightarrow 0$ になることに注意されたい.

この関数を用いると,ロジスティック回帰の仮説関数は, $$ h_w (x)= g(w_0 +w_1 x_1 + w_2 x_2 + \cdots + w_n x_n ) $$ と表すことができる.

仮説関数を用いて実際の分類を行うためには,$h_w (x) \geq 0.5$ のときには $y=1$ と予測し, $h_w (x) < 0.5$ のときには $y=0$ と予測すれば良い. すなわち,$h_w (x) = 0.5$ を境界として, 領域を $y=1$ と $y=0$ の2つに分けるのである. このことから,$h_w (x) = 0.5$ は決定境界(decision boundary)とよばれる.

ロジスティック回帰における費用関数 は以下のように定義される.

$$ J(w)= \frac{1}{m} \displaystyle\sum_{i=1}^m Cost ( h_{w}(x^{(i)}),y^{(i)} ) $$$$ Cost (h_w(x),y)= \left\{ \begin{array}{l l } -\log (h_{w}(x) ) & y=1 \\ -\log (1- h_{w}(x) ) & y=0 \end{array} \right. $$

$y=1$ の場合には,この費用関数は $h_w(x)=1$ のとき $0$ になり, $h_w(x) \rightarrow 0$ になると無限大に近づく. $y=0$ の場合には,この費用関数は $h_w(x)=0$ のとき $0$ になり, $h_w(x) \rightarrow 1$ になると無限大に近づく. つまり,予測が当たる場合には費用が $0$ に,外れるにしたがって大きくなるように設定されている訳である. さらに,この費用関数は凸であることが示せるので,線形回帰と同様に非線形最適化の手法を用いることによって, 最適な重み $w$ を容易に求めることができるのである.

2値分類 (スパム判定)

メイルがスパム(spam;迷惑メイル)か否かを判定する例題を用いる.

https://archive.ics.uci.edu/ml/datasets/spambase

様々な数値情報から,is_spam列が $1$ (スパム)か, $0$ (スパムでない)かを判定する.

spam = pd.read_csv("http://logopt.com/data/spam.csv")
spam.head().T
0 1 2 3 4
word_freq_make 0.210 0.060 0.000 0.000 0.000
word_freq_address 0.280 0.000 0.000 0.000 0.000
word_freq_all 0.500 0.710 0.000 0.000 0.000
word_freq_3d 0.000 0.000 0.000 0.000 0.000
word_freq_our 0.140 1.230 0.630 0.630 1.850
word_freq_over 0.280 0.190 0.000 0.000 0.000
word_freq_remove 0.210 0.190 0.310 0.310 0.000
word_freq_internet 0.070 0.120 0.630 0.630 1.850
word_freq_order 0.000 0.640 0.310 0.310 0.000
word_freq_mail 0.940 0.250 0.630 0.630 0.000
word_freq_receive 0.210 0.380 0.310 0.310 0.000
word_freq_will 0.790 0.450 0.310 0.310 0.000
word_freq_people 0.650 0.120 0.310 0.310 0.000
word_freq_report 0.210 0.000 0.000 0.000 0.000
word_freq_addresses 0.140 1.750 0.000 0.000 0.000
word_freq_free 0.140 0.060 0.310 0.310 0.000
word_freq_business 0.070 0.060 0.000 0.000 0.000
word_freq_email 0.280 1.030 0.000 0.000 0.000
word_freq_you 3.470 1.360 3.180 3.180 0.000
word_freq_credit 0.000 0.320 0.000 0.000 0.000
word_freq_your 1.590 0.510 0.310 0.310 0.000
word_freq_font 0.000 0.000 0.000 0.000 0.000
word_freq_000 0.430 1.160 0.000 0.000 0.000
word_freq_money 0.430 0.060 0.000 0.000 0.000
word_freq_hp 0.000 0.000 0.000 0.000 0.000
word_freq_hpl 0.000 0.000 0.000 0.000 0.000
word_freq_george 0.000 0.000 0.000 0.000 0.000
word_freq_650 0.000 0.000 0.000 0.000 0.000
word_freq_lab 0.000 0.000 0.000 0.000 0.000
word_freq_labs 0.000 0.000 0.000 0.000 0.000
word_freq_telnet 0.000 0.000 0.000 0.000 0.000
word_freq_857 0.000 0.000 0.000 0.000 0.000
word_freq_data 0.000 0.000 0.000 0.000 0.000
word_freq_415 0.000 0.000 0.000 0.000 0.000
word_freq_85 0.000 0.000 0.000 0.000 0.000
word_freq_technology 0.000 0.000 0.000 0.000 0.000
word_freq_1999 0.070 0.000 0.000 0.000 0.000
word_freq_parts 0.000 0.000 0.000 0.000 0.000
word_freq_pm 0.000 0.000 0.000 0.000 0.000
word_freq_direct 0.000 0.060 0.000 0.000 0.000
word_freq_cs 0.000 0.000 0.000 0.000 0.000
word_freq_meeting 0.000 0.000 0.000 0.000 0.000
word_freq_original 0.000 0.120 0.000 0.000 0.000
word_freq_project 0.000 0.000 0.000 0.000 0.000
word_freq_re 0.000 0.060 0.000 0.000 0.000
word_freq_edu 0.000 0.060 0.000 0.000 0.000
word_freq_table 0.000 0.000 0.000 0.000 0.000
word_freq_conference 0.000 0.000 0.000 0.000 0.000
char_freq_; 0.000 0.010 0.000 0.000 0.000
char_freq_( 0.132 0.143 0.137 0.135 0.223
char_freq_[ 0.000 0.000 0.000 0.000 0.000
char_freq_! 0.372 0.276 0.137 0.135 0.000
char_freq_$ 0.180 0.184 0.000 0.000 0.000
char_freq_# 0.048 0.010 0.000 0.000 0.000
capital_run_length_average 5.114 9.821 3.537 3.537 3.000
capital_run_length_longest 101.000 485.000 40.000 40.000 15.000
capital_run_length_total 1028.000 2259.000 191.000 191.000 54.000
is_spam 1.000 1.000 1.000 1.000 1.000

is_spam列が従属変数(ターゲット)$y$ になり,それ以外の列が独立変数(特徴ベクトル)$X$ になる.

ロジスティック回帰を用いる。

X = spam.drop("is_spam", axis=1)
y = spam.is_spam

LogisticRegressionクラスの引数

最適化をするための手法をsolver引数で変えることができる. 以下のものが準備されている.

  • "newton-cg": Newton共役方向法
  • "lbfgs": 記憶制限付きBFGS(Broyden–Fletcher–Goldfarb–Shanno)法 (既定値)
  • "liblinear": 座標降下法(小さなデータのとき有効)
  • "sag" : 確率的平均勾配法 (Stochastic Average Gradient); 大規模なデータで高速
  • "saga" : 確率的平均勾配法の変形; 大規模なデータで高速
  • "newton-cholesky": データ(サンプル)数が特徴の数と比較して非常に大きいときに有効
from sklearn.linear_model import LogisticRegression  # ロジスティック回帰クラスの読み込み

logreg = LogisticRegression(solver="liblinear")  # インスタンスの生成
logreg.fit(X, y)
# 訓練

可視化(分類)

yellowbrickパッケージを用いて,結果の可視化を行う. 分類に対しては以下が準備されている.

  • 混同行列 (ConfusionMatrix)
  • 分類レポート (ClassificationReport)
  • 2値分類に対する閾値変化図 (DiscriminationThreshold)
  • ROC曲線 (ROCAUC)

混同行列

混同行列 (confusion matrix)とは, 行を正解,列を予測とし,あたり(true)が対角線になるようにした行列である. 普通は $1$ を左上にするが,yellowbrickだと $0,1$ の順に並ぶので注意する必要がある.

True\Prediction not spam is spam
not spam (0) TN FP
is spam (1) FN TP
  • positive(陽性): 予測(prediction)が $1$ (スパム);(注意)positiveか否かは相対的なものであるので,どちらでも良い. $0$ (スパムでない)をpositiveとすることも可能である.
  • negative(陰性): 予測が $0$ (スパムでない)

  • true (真): あたり

  • false(偽): はずれ

    正解率 (accuracy) は以下のように定義される評価尺度である.

$$ \mathrm{accuracy} = \frac{\mathrm{TP}+\mathrm{TN}}{\mathrm{TP}+\mathrm{FN}+\mathrm{FP}+\mathrm{TN}} $$
from yellowbrick.classifier import ConfusionMatrix

cm = ConfusionMatrix(logreg, classes=["not spam", "is spam"])

cm.fit(X, y)
cm.score(X, y)
cm.show()

評価尺度(メトリクス)

spamでないメイルをspamと判断する(false positive)のは,大事なメイルがスパムフォルダに入ってしまうので困る. spamをspamでないと判断するのは(false negative),スパムを消せば良いのであまり困らない.

したがって,他の評価尺度 (recall, precision, f1 scoreなど) が準備されている.

$$ \mathrm{true~positive~rate} = \mathrm{recall} = \frac{\mathrm{TP}}{\mathrm{TP}+\mathrm{FN}} $$$$ \mathrm{precision} = \frac{\mathrm{TP}}{\mathrm{TP}+\mathrm{FP}} $$$$ \mathrm{false~positive~rate} = \frac{\mathrm{FP}}{\mathrm{TN}+\mathrm{FP}} $$$$ \mathrm{f1~score} = \frac{2 (\mathrm{precision} \cdot \mathrm{recall}) }{\mathrm{precision} + \mathrm{recall}} $$
TP, FN, FP, TN = 2665, 123, 190, 1622
precision = TP / (TP + FP)
recall = TP / (FN + TP)
print("precision=", precision)
print("recall=", recall)
print("F1 score=", 2 * (precision * recall) / (precision + recall))
precision= 0.9334500875656743
recall= 0.9558823529411765
F1 score= 0.9445330497962078
from yellowbrick.classifier import ClassificationReport

visualizer = ClassificationReport(logreg, classes=["not spam", "is spam"])

visualizer.fit(X, y)
visualizer.score(X, y)
visualizer.show()

閾値 (threshold)を変えてみる

ロジスティック回帰は,spamである確率を推定し,それが閾値より大きいとspam,それ以外のときspamでないと判定する.

  • 通常のロジスティック回帰の閾値は $0.5$

  • 閾値を大きくすると,全部spamでない(negative)と判定(false positiveが減り,precisionが上がる)

  • 閾値を小さくすると,全部spam(positive)と判定(false negativeが減り,recall が上がる)

  • f1 scoreはバランスをとる

from yellowbrick.classifier import DiscriminationThreshold

visualizer = DiscriminationThreshold(logreg)

visualizer.fit(X, y)
visualizer.score(X, y)
visualizer.show()

ROC曲線とAUC

閾値を $1$ から $0$ に変えると,recall (true positive rate) が増加し,precision が減少する. $x$ 軸に false positive rate, $y$ 軸にtrue positive rateをとると,両方とも $1$ に近づく曲線になる. これをROC (receiver operating characteristic;受信者操作特性) 曲線 とよぶ.

曲線の下の面積が AUC (area under the curve) という評価尺度になる. これは大きいほど良い.

from yellowbrick.classifier import ROCAUC

visualizer = ROCAUC(logreg, size=(600, 400))

visualizer.fit(X, y)
visualizer.score(X, y)
visualizer.show()

問題(クレジットカード)

以下のクレジットカードのデフォルトの判定データに対してロジスティック回帰を行い, 混同行列を描画せよ.

default列にデフォルトか否かの情報があり,他の列の情報を用いて分類せよ.

credit = pd.read_csv("http://logopt.com/data/credit.csv")
credit.head()
limit sex edu married age apr_delay may_delay jun_delay jul_delay aug_delay ... jul_bill aug_bill sep_bill apr_pay may_pay jun_pay jul_pay aug_pay sep_pay default
0 20000 2 2 1 24 2 2 -1 -1 -2 ... 0 0 0 0 689 0 0 0 0 1
1 120000 2 2 2 26 -1 2 0 0 0 ... 3272 3455 3261 0 1000 1000 1000 0 2000 1
2 90000 2 2 2 34 0 0 0 0 0 ... 14331 14948 15549 1518 1500 1000 1000 1000 5000 0
3 50000 2 2 1 37 0 0 0 0 0 ... 28314 28959 29547 2000 2019 1200 1100 1069 1000 0
4 50000 1 2 1 57 -1 0 -1 0 0 ... 20940 19146 19131 2000 36681 10000 9000 689 679 0

5 rows × 24 columns

問題(部屋)

以下の部屋が使われているか否かを判定するデータに対してロジスティック回帰を行い,混同行列とROC曲線を描画せよ.

occupancy列が部屋が使われているか否かを表す情報であり,これをdatetime列以外の情報から分類せよ.

occupancy = pd.read_csv("http://logopt.com/data/occupancy.csv")
occupancy.head()
datetime temperature relative humidity light CO2 humidity occupancy
0 2015-02-04 17:51:00 23.18 27.2720 426.0 721.25 0.004793 1
1 2015-02-04 17:51:59 23.15 27.2675 429.5 714.00 0.004783 1
2 2015-02-04 17:53:00 23.15 27.2450 426.0 713.50 0.004779 1
3 2015-02-04 17:54:00 23.15 27.2000 426.0 708.25 0.004772 1
4 2015-02-04 17:55:00 23.10 27.2000 426.0 704.50 0.004757 1

多クラス分類(アヤメ)

上ではロジスティック回帰を用いた2値分類について考えた. ここでは,iris(アヤメ)のデータを用いて,3種類以上の分類(これを多クラス分類とよぶ)を解説する.

アヤメの例題では,4つの花の特徴を用いて3つの種類に分類する.

入力は長さ4のベクトル $x$ であり,$4 \times 3$ の重み行列 $W$ に $x$ を乗じることによって, ベクトル $z =[z_1,z_2,z_3]$ を得る.重み $W$ は分類がうまくいくように決めるパラメータである. 

$z_1, z_2, z_3$ の各値を用いて, 各クラス(アヤメの種類)が選択される確率を計算する.これには,以下のソフトマックス関数を用いる. $$ p_j = \frac{e^{z_j}}{\sum_{k=1}^3 e^{z_k}} \ \ \ j=1,2,3 $$ ここで $e$ は自然対数の底である.

分類は $p_j$ が最も大きい $j$ を選択することによって行われる. これをソフトマックス回帰とよぶ.ただしscikit-learnでは,ロジスティック回帰と同じクラスを用いる. ソフトマックス回帰における費用関数は,以下のように定義される.

$$ J(w)= \frac{1}{m} \displaystyle\sum_{i=1}^m Cost ( h_{w}(x^{(i)}),y^{(i)} ) $$$$ Cost (h_w(x),y)= - \sum_{j=1}^3 [y=j] \log \frac{e^{z_j}}{\sum_{k=1}^3 e^{z_k}} $$

ここで,$[y=j]$ はデータ $y$ の正解が $j$ のとき $1$,そうでないとき $0$ の記号である.

以下ではアヤメのデータを用いてロジスティック(ソフトマックス)回帰を行い,3種類のアヤメを分類する.

import plotly.express as px

iris = px.data.iris()
iris.head()
sepal_length sepal_width petal_length petal_width species species_id
0 5.1 3.5 1.4 0.2 setosa 1
1 4.9 3.0 1.4 0.2 setosa 1
2 4.7 3.2 1.3 0.2 setosa 1
3 4.6 3.1 1.5 0.2 setosa 1
4 5.0 3.6 1.4 0.2 setosa 1
X = iris[["sepal_length", "sepal_width", "petal_length", "petal_width"]]
# 従属変数 y
y = iris["species_id"]
y.head()
0    1
1    1
2    1
3    1
4    1
Name: species_id, dtype: int64
  • 手順1:分類するためのクラスをインポートして,インスタンスを生成する.

  • 手順2:fitメソッドを用いて,訓練する.

  • 手順3:predictメソッドを用いて予測を行う.

from sklearn.linear_model import LogisticRegression  # ロジスティック回帰クラスの読み込み

logreg = LogisticRegression()  # インスタンスの生成
logreg.fit(X, y)  # 訓練
LogisticRegression()
logreg.predict([[3, 5, 4, 2]])  # 試しに予測
array([1])

予測と実際の誤差を検証

元データ $X$ を入れたときの予測y_predと本当の値 $y$ を比較する.

metricsにある正解率を計算する関数 accuracy_score を利用すると簡単だ.

y_pred = logreg.predict(X)
from sklearn import metrics

print(metrics.accuracy_score(y, y_pred))
0.9733333333333334

可視化

以下の2つの可視化を追加する.

  • どの特徴が重要かを可視化するRadViz

  • 重要と思われる2つの特徴に対して,ロジスティック回帰の分類がどのように行われたかを示すDecisionViz(決定の境界を示す)

2値分類(スパムの例)と同じ可視化も行う.

  • 混同行列 (ConfusionMatrix)
  • 分類レポート (ClassificationReport)
  • ROC曲線 (ROCAUC)

3値以上の分類の場合には,閾値を変化させる可視化はできない.

from yellowbrick.features import RadViz

visualizer = RadViz(
    classes=["Iris-setosa", "Iris-versicolor", "Iris-virginica"], size=(600, 400)
)

visualizer.fit(X, y)
visualizer.transform(X)
visualizer.show()
from sklearn.preprocessing import StandardScaler
from yellowbrick.contrib.classifier import DecisionViz

X = iris[["sepal_width", "petal_width"]]  # 2次元を切り出す
X = StandardScaler().fit_transform(X)  # 可視化のためにスケーリングしておく

viz = DecisionViz(
    logreg,
    features=["sepal_width", "petal_width"],
    classes=["Iris-setosa", "Iris-versicolor", "Iris-virginica"],
)
viz.fit(X, y)
viz.draw(X, y)
viz.show()
from yellowbrick.classifier import ClassificationReport

visualizer = ClassificationReport(logreg)

visualizer.fit(X, y)
visualizer.score(X, y)
visualizer.show()
from yellowbrick.classifier import ConfusionMatrix

cm = ConfusionMatrix(logreg)

cm.fit(X, y)
cm.score(X, y)
cm.show()
from yellowbrick.classifier import ROCAUC

visualizer = ROCAUC(logreg, size=(600, 400))

visualizer.fit(X, y)
visualizer.score(X, y)
visualizer.show()

例題:毒キノコの判定

ここでは,データから毒キノコか否かを判定する.

target列がターゲット(従属変数)であり,edibleが食用,poisonousが毒である.

他の列のデータもすべて数値ではない.ここでは,scikit-learnのprocessingを用いて,数値に変換してからロジスティック回帰により分類を行う.

mashroom = pd.read_csv("http://logopt.com/data/mashroom.csv")
mashroom.head()
target shape surface color
0 edible convex smooth yellow
1 edible bell smooth white
2 poisonous convex scaly white
3 edible convex smooth gray
4 edible convex scaly yellow
mashroom.color.unique()
array(['yellow', 'white', 'gray', 'brown', 'red', 'pink', 'buff',
       'purple', 'cinnamon', 'green'], dtype=object)
X = mashroom.drop("target", axis=1)
y = mashroom.target
X
shape surface color
0 convex smooth yellow
1 bell smooth white
2 convex scaly white
3 convex smooth gray
4 convex scaly yellow
... ... ... ...
8118 knobbed smooth brown
8119 convex smooth brown
8120 flat smooth brown
8121 knobbed scaly brown
8122 convex smooth brown

8123 rows × 3 columns

前処理(preprocessing)

sklearn.preprocessingモジュールにあるLabelEncoderクラスのfit_transformメソッドを用いてターゲット列を数値化する.

edibleは0に,poisonousは1に変換されていることが分かる.

独立変数 $X$ に対しては,OrdinalEncoderを用いる.これによって文字列データが数値に変換されていることが分かる.

なお,OneHotEncoderを使うと,ダイアモンドの例題でダミー変数に変換したのと同じことができる.

from sklearn.preprocessing import LabelEncoder

y = LabelEncoder().fit_transform(y)
y
array([0, 0, 1, ..., 0, 1, 0])
from sklearn.preprocessing import OrdinalEncoder, OneHotEncoder

# X = OneHotEncoder().fit_transform(X)
X = OrdinalEncoder().fit_transform(X)
print(X)
[[2. 3. 9.]
 [0. 3. 8.]
 [2. 2. 8.]
 ...
 [3. 3. 0.]
 [4. 2. 0.]
 [2. 3. 0.]]
from sklearn.linear_model import LogisticRegression

logreg = LogisticRegression()
logreg.fit(X, y)
LogisticRegression()
from yellowbrick.classifier import ConfusionMatrix

cm = ConfusionMatrix(logreg, classes=["edible", "poisonous"])

cm.fit(X, y)
cm.score(X, y)
cm.show()
from yellowbrick.classifier import ClassificationReport

visualizer = ClassificationReport(logreg)

visualizer.fit(X, y)
visualizer.score(X, y)
visualizer.show()
from yellowbrick.classifier import DiscriminationThreshold

visualizer = DiscriminationThreshold(logreg)

visualizer.fit(X, y)
visualizer.score(X, y)
visualizer.show()
from yellowbrick.classifier import ROCAUC

visualizer = ROCAUC(logreg, size=(600, 400))

visualizer.fit(X, y)
visualizer.score(X, y)
visualizer.show()

問題(タイタニック)

titanicデータに対してロジスティック回帰を行い,死亡確率の推定を行え. また,混同行列とROC曲線を描画せよ.

ヒント:このデータは欠損値を含んでいる.pandasのところで学んだ欠損値処理を参照せよ.

非数値の(カテゴリー)データをどのように数値化するかには色々な方法がある.自分で色々工夫せよ.

titanic = pd.read_csv("http://logopt.com/data/titanic.csv")
titanic.head()
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th... female 38.0 1 0 PC 17599 71.2833 C85 C
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S

問題(胸部癌)

http://logopt.com/data/cancer.csv にある胸部癌か否かを判定するデータセットを用いて分類を行え.

最初の列diagnosisが癌か否かを表すものであり,"M"が悪性(malignant),"B"が良性(benign)を表す.

cancer = pd.read_csv("http://logopt.com/data/cancer.csv", index_col=0)
cancer.head().T  # 横長なので転置して表示する.
id 842302 842517 84300903 84348301 84358402
diagnosis M M M M M
radius_mean 17.99 20.57 19.69 11.42 20.29
texture_mean 10.38 17.77 21.25 20.38 14.34
perimeter_mean 122.8 132.9 130.0 77.58 135.1
area_mean 1001.0 1326.0 1203.0 386.1 1297.0
smoothness_mean 0.1184 0.08474 0.1096 0.1425 0.1003
compactness_mean 0.2776 0.07864 0.1599 0.2839 0.1328
concavity_mean 0.3001 0.0869 0.1974 0.2414 0.198
concave points_mean 0.1471 0.07017 0.1279 0.1052 0.1043
symmetry_mean 0.2419 0.1812 0.2069 0.2597 0.1809
fractal_dimension_mean 0.07871 0.05667 0.05999 0.09744 0.05883
radius_se 1.095 0.5435 0.7456 0.4956 0.7572
texture_se 0.9053 0.7339 0.7869 1.156 0.7813
perimeter_se 8.589 3.398 4.585 3.445 5.438
area_se 153.4 74.08 94.03 27.23 94.44
smoothness_se 0.006399 0.005225 0.00615 0.00911 0.01149
compactness_se 0.04904 0.01308 0.04006 0.07458 0.02461
concavity_se 0.05373 0.0186 0.03832 0.05661 0.05688
concave points_se 0.01587 0.0134 0.02058 0.01867 0.01885
symmetry_se 0.03003 0.01389 0.0225 0.05963 0.01756
fractal_dimension_se 0.006193 0.003532 0.004571 0.009208 0.005115
radius_worst 25.38 24.99 23.57 14.91 22.54
texture_worst 17.33 23.41 25.53 26.5 16.67
perimeter_worst 184.6 158.8 152.5 98.87 152.2
area_worst 2019.0 1956.0 1709.0 567.7 1575.0
smoothness_worst 0.1622 0.1238 0.1444 0.2098 0.1374
compactness_worst 0.6656 0.1866 0.4245 0.8663 0.205
concavity_worst 0.7119 0.2416 0.4504 0.6869 0.4
concave points_worst 0.2654 0.186 0.243 0.2575 0.1625
symmetry_worst 0.4601 0.275 0.3613 0.6638 0.2364
fractal_dimension_worst 0.1189 0.08902 0.08758 0.173 0.07678

$K$ 近傍法による分類と可視化

4種類のフルーツを重量と色で分類する.

フルーツは数値だが,順に "apple", "mandarin","orange","lemon" を表す.

$K$ 近傍法とは,($X$の空間で)データに近い $K$ 個のデータの値($y$)の多数決で分類を行う最も簡単な分類手法である.

以下のようにしてクラスをインポートできる.

from sklearn.neighbors import KNeighborsClassifier

クラス KNeighborsClassifier は引数 n_neighbors でパラメータ $K$ を設定できる.

$K$ を色々変えて実験せよ(以下では$K=3$の結果を示す).

fruit = pd.read_csv("http://logopt.com/data/fruit_simple.csv")
fruit.head()
fruit_label mass color_score
0 1 192 0.55
1 1 180 0.59
2 1 176 0.60
3 2 86 0.80
4 2 84 0.79
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
from yellowbrick.contrib.classifier import DecisionViz

X = fruit.drop("fruit_label", axis=1)
y = fruit.fruit_label
X = StandardScaler().fit_transform(X)  # 可視化のためにスケーリングしておく

knn = KNeighborsClassifier(3)
knn.fit(X, y)  # 訓練
y_pred = knn.predict(X)

print("正解率=", metrics.accuracy_score(y, y_pred))

viz = DecisionViz(
    knn,
    title="Nearest Neighbors",
    features=["mass", "color score"],
    classes=["apple", "mandarin", "orange", "lemon"],
)
viz.fit(X, y)
viz.draw(X, y)
viz.show()
正解率= 0.9322033898305084

問題(アヤメ)

irisデータに対して,$K$ 近傍法で元データを予測したしたときの正解率を計算せよ.パラメータ $K$(近傍の数)が $5$ のときはどうか? また,パラメータ $K$ が $1$ のときはどうか? また,DecisionVizを用いた可視化も行え.

注意:DecisionVizによる可視化は2次元データではないので、そのままではできない. 適当な特徴ベクトルを切り出してから可視化を行え.

多項式回帰

回帰や分類を行う際に予測誤差が小さい方が望ましいことは言うまでもない. しかし,予測誤差を小さくするために変数(特性)を増やしすぎると, トレーニングデータに適合しすぎて,実際のデータに対して良い結果を出さない 過剰適合(overfitting; 過学習)が発生してしまう.

たとえば,線形回帰の仮説関数として以下のような $x$ に対する多項式関数を考える. $$ h_w (x)= w_0 +w_1 x_1 + w_2 x_2 + \cdots + w_n x_n + w_{n+1} x_1^2 + w_{n+2} x_2^2 + \cdots + w_{2n} x_n^2 + w_{2n+1} x_1^3 + w_{2n+2} x_2^3 + \cdots + w_{3n} x_n^3 $$ この関数は重み $w$ に対する線形関数であるので,(特性の数は $3$倍になるが) 線形回帰と同じように最適な重みを計算することができる. ちなみに,このような回帰を多項式回帰(polynomial regression)とよぶ. 一般に高次の多項式回帰でトレーニングデータの数が少ない場合には,過剰適合が発生する.

宣伝の効果のデータを用いて多項式回帰を行う.

pandasで2次の項(たとえばテレビとラジオの相乗効果の列なら TV*Radio)を生成してから,線形回帰を行う.

data = pd.read_csv("http://logopt.com/data/Advertising.csv", index_col=0)
data["TV*Radio"] = data.TV * data.Radio
data["TV*Newspaper"] = data.TV * data.Newspaper
data["Radio*Newspaper"] = data.Radio * data.Newspaper
data.head()
X = data[["TV", "Radio", "Newspaper", "TV*Radio", "TV*Newspaper", "Radio*Newspaper"]]
y = data["Sales"]
from sklearn.linear_model import LinearRegression  # クラスのインポート

lin_reg = LinearRegression()  # 線形回帰クラスのインスタンス生成
lin_reg.fit(X, y)  # 訓練
yhat = lin_reg.predict(X)  # 予測
print(lin_reg.score(X, y))  # 決定係数 (線形回帰だと R^2は 0.897210638179
print("y-切片= ", lin_reg.intercept_)
print("係数 = ", lin_reg.coef_)

トレーニングデータとテスト(検証)データ

元データ(トレーニングデータ)を入れてテストをすることは推奨されない. $K(=1)$近傍法を適用したときのように,過剰適合してしまうからである.

これを避けるための一番簡単な方法は,トレーニングデータとテストデータを分けることである. train_test_split関数を使うと簡単にできる.

例としてBostonの住宅データの予測を行う.

medvが住宅の価格で,他のデータ(犯罪率や人口など)から予測する.

boston = pd.read_csv("http://logopt.com/data/Boston.csv", index_col=0)
boston.head()
X = boston.drop("medv", axis=1)  # 最後の列以外のデータを独立変数(特徴ベクトル)として抽出
y = boston.medv  # 最後の列(medv)を従属変数として抽出
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3
)  # 30%のデータをテスト用,それ以外をトレーニング用に分離
from sklearn.linear_model import LinearRegression  # クラスのインポート

reg = LinearRegression()  # 線形回帰クラスのインスタンス生成
reg.fit(X_train, y_train)  # 訓練
yhat = reg.predict(X_test)  # 予測
print(reg.score(X_test, y_test))  # 決定係数

残差プロットを描画すると,トレーニングデータとテストデータに色分けして表示される.

from yellowbrick.regressor import ResidualsPlot

visualizer = ResidualsPlot(reg)

visualizer.fit(X_train, y_train)
visualizer.score(X_test, y_test)
visualizer.show();

リッジ回帰

過剰適合を避けるための方法として正則化 (regulalization) がある。

正則化を追加した線形回帰は,リッジ回帰(ridge regression)とよばれる.

正則化のアイディアは単純であり,重み $w$ を小さくするような項を費用関数に追加するだけである.

線形回帰の場合には,以下の費用関数を用いる. $$ J(w)= \frac{1}{2m} \left[ \sum_{i=1}^m \left( h_{w}(x^{(i)}) -y^{(i)} \right)^2 + \lambda \sum_{j=1}^n w_j^2 \right] $$

追加された2項目が重みを小さくし,これによって過剰適合が軽減される. パラメータ $\lambda$ は正則化パラメータ(regularization parameter)とよばれ, 誤差と過剰適合のトレードオフをとるために用いられる. $\lambda$ が小さいときには,誤差は少なくなるが過剰適合の危険性が増大する. 逆に $\lambda$ を大きくすると,誤差は大きくなり,過剰適合は回避される傾向になる.

パラメータ $\lambda$ はリッジ回帰クラス Ridge では引数 alpha で与える.

from sklearn.linear_model import Ridge  # クラスのインポート

reg = Ridge(alpha=10.0)  # リッジ回帰クラスのインスタンス生成
reg.fit(X_train, y_train)  # 訓練
yhat = reg.predict(X_test)  # 予測
print(reg.score(X_test, y_test))  # 決定係数

最も良い正則化パラメータalphaを求める.

後述する交差検証を使い,yellowbrickのAlphaSelectionで可視化する.

from sklearn.linear_model import RidgeCV
from yellowbrick.regressor import AlphaSelection
import numpy as np

alphas = np.logspace(-100, -1, 400)
model = RidgeCV(alphas=alphas)
visualizer = AlphaSelection(model)
visualizer.fit(X, y)
visualizer.show();
reg = Ridge(alpha=0.032)  # リッジ回帰クラスのインスタンス生成
reg.fit(X_train, y_train)  # 訓練
yhat = reg.predict(X_test)  # 予測
print(reg.score(X_test, y_test))  # 決定係数

問題(アヤメ,タイタニック,ダイヤモンド)

  1. irisデータをトレーニング用とテスト用に分けてから,ロジスティック回帰による分類を行い,正解率を計算せよ.
  2. titanicデータトレーニング用とテスト用に分けてから,ロジスティック回帰による分類を行い,正解率を計算せよ.
  3. 例題2で用いた ダイアモンドの価格データhttp://logopt.com/data/Diamond.csv に対して線形回帰とリッジ回帰による予測を行え.また,トレーニング用とテスト用に分けて決定係数 $R^2$ を計算し,評価せよ. ただしリッジ回帰の正則化パラメータは $0.03$ と設定せよ.

カーネルとSVM (Support Vector Machine)

分類のための手法の1つである SVM(Support Vector Machine;サポートベクトルマシン) について述べる.

SVMはロジスティック回帰を計算しやすいように近似したものと考えることができる.

ロジスティック回帰における費用関数 $$ Cost (h_w(x),y)= \left\{ \begin{array}{l l } -\log (h_{w}(x) ) & y=1 \\ -\log (1- h_{w}(x) ) & y=0 \end{array} \right. $$ は $$ Cost (h_w(x),y)= -y \log (h_{w}(x) ) - (1-y) \log (1- h_{w}(x) ) $$ と書き直すことができる.

ロジスティック回帰の仮説関数 $h_w (x)$ は, $z=w_0 +w_1 x_1 + w_2 x_2 + \cdots + w_n x_n$ とおいたときに $$ h_w (x)= g(z) =\frac{1}{1+e^{-z}} $$ であったことを思い起こすと,費用関数は $$ Cost (h_w(x),y)= -y \log \frac{1}{1+e^{-z}} - (1-y) \log (1- \frac{1}{1+e^{-z}} ) $$ となる.

$y=1$ のときの費用関数は第1項目だけになるので, $$ -\log \frac{1}{1+e^{-z}} $$ となる.これは $z$ が大きくなると $0$ に近づく曲線である (グラフを見たい場合には,Googleで-log(1/(1+e^-x))と検索する). これを $z \geq 1$ のとき $0$ となる区分的線形関数 $cost_1(z)$ で近似する.

$y=0$ のときも同様に第2項 $$ -\log (1- \frac{1}{1+e^{-z}} ) $$ は,$z$ が小さくなると $0$ に近づく曲線となるので, $z \leq -1$ のときに $0$ になる区分的線形関数 $cost_0(z)$ で近似する. これに正則化パラメータを加えたものがSVMの費用関数 $J(w)$ になる. $$ J(w)= C \displaystyle\sum_{i=1}^m \left[ y^{(i)} cost_1(w^T x^{(i)}) + (1-y^{(i)}) cost_0(w^T x^{(i)}) \right] +\frac{1}{2} \displaystyle\sum_{j=1}^m w_j^2 $$ ここで $C$ は正則化パラメータを $\lambda$ としたとき $1/2 \lambda$ に相当するパラメータである. $\lambda$ のかわりに $C$ を用いるのは歴史的な理由に基づく慣例であり,特に意味はない.

パラメータ $C$ を大きく設定することは正則化パラメータ $\lambda$ を小さく設定することに相当するので, 誤差は小さくなるが過剰適合の危険性が高まり,$C$ を小さくするとその逆になる.

仮説関数を用いて実際の分類を行うためには, $w_0 +w_1 x_1 + w_2 x_2 + \cdots + w_n x_n \geq 0$ のときには $y=1$ と予測し, それ以外のときには $y=0$ と予測すれば良い.

費用関数 $J(w)$ の第1項が $0$ の場合には,$y^{(i)}=1$ なら $w^T x^{(i)} \geq 1$ かつ $y^{(i)}=0$ なら $w^T x^{(i)} \leq -1$ になる必要がある. このことから,データが直線で2つのクラスに分類可能な場合には, SVMの決定境界は,$1,-1$ の幅をもたせた境界になることが言える.

次に,カーネル(kernel)の概念を用いたSVMを解説する. これは,決定境界が非線形になる場合にも適用可能な手法である. もちろん,$x$ に対する多項式を作成してからロジスティック回帰 を適用しても良いのだが,非線形性が強い場合には高次の多項式が必要になり, それは計算量の増大をもたらす. 以下では,カーネルの中でも非線形な場合に適用可能で計算量的にも優秀な Gaussカーネル関数(Gaussian kernel function)を紹介する.

トレーニングデータの空間にランドマークとよばれる点列 $\ell^{(1)}, \ell^{(2)},\ldots,\ell^{(k)}$ が与えられているものとする(設定法については後述する). このとき特性ベクトル $x=(x_1, x_2, \ldots, x_n)$ とランドマーク $\ell^{(i)}$ の「近さ」を表す尺度として 以下のGaussカーネル関数を用いる. $$ f_i = \exp \left( - \frac{ \sum_{j=1}^n (x_j-\ell_j^{(i)})^2 }{2 \sigma^2} \right) = \exp \left( - \frac{ \| x-\ell^{(i)} \|}{2 \sigma^2} \right) $$ ここで $\| \cdot \|$ はベクトルのノルムを表す. この関数は $x$ がランドマーク $\ell^{(i)}$ に近づくと $1$ になり, 遠ざかると $0$ になる多次元正規(Gauss)分布のような形状をとる. これがGaussカーネルとよばれる所以である. $\sigma$ は正規分布の標準偏差に対応するパラメータであり, $\sigma$ が小さくなると分布が $\ell^{(i)}$ の周りに集中し, 逆に$\sigma$ が大きくなると裾野が広がる.

仮説関数は,カーネルを用いて計算された $f_i$ と重み $w$ の線形関数として,以下のように定義する. $$ h_w (x)= w_0 +w_1 f_1 + w_2 f_2 + \cdots + w_k f_k $$ つまり,元の変数 $x$ をそのまま特性ベクトルとして用いるのではなく, $f= (f_1,f_2,\ldots,f_k)$ を特性ベクトルとして予測を行う訳である. これは多次元正規分布を重ね合わせたような分布になるので,非線形な決定境界を うまく表現できることが期待できる.

ランドマーク $\ell^{(i)}$ としては,トレーニング集合の要素 $x^{(i)}$ を用いる. すなわち,トレーニングデータ $x^{(i)},y^{(i)} (i=1,2,\ldots,m)$ に対して $\ell^{(i)} =x^{(i)}$ と設定するのである. このとき,トレーニングデータ $x^{(i)}$ に対する特性ベクトル $f^{(i)} =(f_0^{(i)}, f_1^{(i)}, \ldots, f_m^{(i)})^T$ は, 各ランドマークへの近さを表すベクトルとして, $$ \begin{array}{c} f_0^{(i)} =1 \\ f_1^{(i)} = \exp \left( - \frac{ \| x^{(i)}-\ell^{(1)} \|}{2 \sigma^2} \right) \\ \vdots \\ f_i^{(i)} = \exp \left( - \frac{ \| x^{(i)}-\ell^{(i)} \|}{2 \sigma^2} \right) = \exp (0) = 1 \\ \vdots \\ f_m^{(i)} = \exp \left( - \frac{ \| x^{(i)}-\ell^{(m)} \|}{2 \sigma^2} \right) \end{array} $$ と計算される $m+1$次元ベクトルである.ここで $f_0^{(i)}$ はランドマークに依存しない定数項を表す (線形回帰における $y$-切片を表す)パラメータであり,常に $1$ に設定しておく. これは元のトレーニングデータである $n$次元ベクトル $x^{(i)}$ の空間から $m$次元の特性ベクトル $f^{(i)}$ の空間に射影したものと考えられる.

費用関数 $J(w)$ は, ランドマークに対する重みベクトル $w=(w_0, w_1, \ldots,w_m)$ の関数として, $$ J(w)= C \displaystyle\sum_{i=1}^m \left[ y^{(i)} cost_1(w^T f^{(i)}) + (1-y^{(i)}) cost_0(w^T f^{(i)}) \right] +\frac{1}{2} \displaystyle\sum_{j=0}^m w_j^2 $$ となる. 2項目の$\sum_{j=0}^m w_j^2$ は正則化項を表し, ベクトル表記すると $w^T w$ となる. これはカーネルによって定まる行列 $M$ を用いて $w^T M w$ とする場合もある. 仮説関数を用いて実際の分類を行うためには, $w^T f \geq 0$ のときには $y=1$ と予測し,それ以外のときには $y=0$ と予測すれば良い.

前節と同様に, パラメータ $C$ を大きく設定すると誤差は小さくなるが過剰適合の危険性が高まり, $C$ を小さくするとその逆になる. パラメータ $\sigma$ を小さくするとカーネルはランドマークの周辺に集中するので, 誤差は小さくなるが,一方では過剰適合の危険性が高まり,$\sigma$ を大きくするとその逆になる.

元の特性ベクトルの数 $n$ がトレーニングデータの数 $m$ に対して小さいときには, 決定境界の非線形性が強いことが予想されるので, Gaussカーネルを用いたSVMが有効になる.ただし計算量はそこそこかかるので, $m$ が非常に大きいときには,特性ベクトルの数 $n$ を増やしてから比較的高速な ロジスティクス回帰や線形カーネルを用いたSVMを用いた方が良い.

カーネルはクラス SVC (Support Vector Classifier)の引数 kernel で与える.

Gaussカーネル(引数はrbf)を用いると非線形な境界をもつ問題に対しても、分類が可能になる.

irisデータで試してみよう.

import plotly.express as px

iris = px.data.iris()
#  独立変数(特徴ベクトル) X
X = iris[["sepal_length", "sepal_width", "petal_length", "petal_width"]]
# 従属変数 y
y = iris["species_id"]
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)
from sklearn.svm import SVC  # クラスのインポート

svc = SVC(kernel="rbf", gamma="scale")  # インスタンス生成
svc.fit(X_train, y_train)  # 訓練
yhat = svc.predict(X_test)  # 予測
from sklearn import metrics

print(metrics.accuracy_score(y_test, yhat))
from yellowbrick.classifier import ClassificationReport

visualizer = ClassificationReport(svc)

visualizer.fit(X_train, y_train)
visualizer.score(X_test, y_test)
visualizer.show();
from yellowbrick.classifier import ConfusionMatrix

cm = ConfusionMatrix(svc)

cm.fit(X_train, y_train)
cm.score(X_test, y_test)
cm.show();
from yellowbrick.classifier import ROCAUC

visualizer = ROCAUC(svc, size=(600, 400))

visualizer.fit(X_train, y_train)
visualizer.score(X_test, y_test)
visualizer.show();
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
from yellowbrick.contrib.classifier import DecisionViz

X = iris[["sepal_width", "petal_width"]]  # 2次元を切り出す
X = StandardScaler().fit_transform(X)  # 可視化のためにスケーリングしておく

viz = DecisionViz(
    svc,
    features=["sepal_width", "petal_width"],
    classes=["Iris-setosa", "Iris-versicolor", "Iris-virginica"],
)
viz.fit(X, y)
viz.draw(X, y)
viz.show();

仮説の評価(交差検証)

トレーニングデータだけを用いて訓練すると,他のデータに対して役に立たない危険性がある. このような過剰適合を避けるためには,特徴の数を減らしたり,正則化パラメータを調整したりする方法があるが, ここではどのモデルを採用すれば良いかを評価するための方法論について述べる.

学習した仮説関数が正しいかどうかを検証するための基本は,データを分けることである. 最も簡単なのは,トレーニングデータとテストデータの2つに分け,訓練(費用関数を最小化する重み $w$ の決定)はトレーニングデータで行い, 評価(正解データとの誤差を計算)はテストデータで行う方法である.

データの数が足りない場合には,トレーニングデータとテストデータを入れ替えて行う方法が行われる. これを 交差検証 (cross validation)とよぶ.

上で用いたサポートベクトル分類 svcのインスタンスで試してみる. 引数の cv は交差検証の分割数であり, 引数の scoring="accuracy" は評価尺度を正解率に設定している (回帰分析のときには,引数を決定変数 "r2" や最大誤差 "max_error" などに設定する).

以下ではデータを $10$ に分割し,そのうちの1つをテストデータ(残りをトレーニングデータ)として $10$ 個の正解率を計算し,その平均値を出している.

from sklearn.model_selection import cross_val_score

scores = cross_val_score(svc, X, y, cv=10, scoring="accuracy")
print(scores)
print(scores.mean())

ニューラルネット

ここでは,古典的なニューラルネットを機械学習の観点から解説する.

ニューラルネットワークは脳の動きを模した極めて単純な構造から構成される. 基本となるのは, ロジスティック回帰の仮説関数 $h_w(x)$ である. これは,入力ベクトル $x=(x_1,x_2,\ldots,x_n)$ を出力 $h_w(x)$ に変換する. $$ h_w(x)= g(w_0 +w_1 x_1 + w_2 x_2 + \cdots + w_n x_n ) $$ これは,入力ベクトルに行列を乗じた後に,非線形変換 $g(z)$ をしているだけである. ここで $g(z)$ は,活性化関数 (activation function)と呼ばれる関数であり,通常は非線形関数を用いる.

代表的な活性化関数を以下に示す.

  • ロジスティック(シグモイド)関数 $\sigma$: 値域は $(0,1)$ $$ \frac{1}{1+e^{-z}} $$
  • ReLU(rectified linear unit) $(x)^+$: 値域は $[0,\infty]$ $$ \max(0,z) $$
  • $\tanh$ (hyperbolic tangent): 値域は $(-1,1)$ $$ \frac{e^z- e^{-z}}{e^z + e^{-z}} $$

ニューラルネットワークでは,上の機構で入力を出力に変換する点を,複数の層に並べることによって学習を行う. 線形関数の重み $w$ は枝上に定義されており, 左端の1層目に入力 $x$ を与えることで計算された出力が2層目の入力となり, 2層目の出力が3層目の入力になる. 最後の(右端の)層の出力が $y$ となる.このように計算を行う関数が, ニューラルネットワークの仮説関数となる.

分類を例としてニューラルネットワークの学習(重みの最適化)を考える. トレーニングデータの集合を $x^{(i)}, y^{(i)} (i=1,2,\ldots,m)$ とする. 各層 $\ell~(=1,2,\ldots,L)$ における点の数を $s_{\ell}$ と記す.

$y=0,1$ に分類する場合には最後の層 $L$ は1つの点を含む(すなわち $s_{L}=1$ になる). $K$個のクラスに分類する場合には,最後の層 $L$ は $K$個の点を含む(すなわち $s_{L}=K$ になる).

2つのクラスへの分類($y=0,1$)のときの費用関数は,ロジスティック回帰と同様に, $$ J(w)= \frac{1}{m} [ \sum_{i=1}^m y^{(i)} \log h_{w}(x^{(i)}) + (1-y^{(i)})\log (1- h_{w}(x^{(i)}) )]+ \frac{\lambda}{m} \sum_{\ell=1}^{L-1} \sum_{i=1}^{s_{\ell}} \sum_{j=1}^{s_{\ell+1}} (w_{ji}^{(\ell)})^2 $$ と書くことができる. ここで $h_{w}(x^{(i)})$ はトレーニングデータ $x^{(i)}$ に対する最後の層の出力であり, $w_{ji}^{(\ell)}$ は第 $\ell$ 層の点 $i$ から 第 $\ell+1$ 層の点 $j$ への枝上の重みである. 上の費用関数では,正則化のために,重みの自乗平均に 正則化パラメータ $\lambda$ を乗じたものを加えている.

費用関数 $J(w)$ を最小化するためには,費用関数の勾配ベクトルを計算する必要がある. ニューラルネットワークでは,逆伝播(backpropagation;バックプロパゲーション)とよばれる方法を用いて勾配を計算する. 勾配を用いて費用関数の最小化を行うことができるが, ニューラルネットワークの場合には費用関数 $J(w)$ は非凸であり, 局所的最適解に停留する可能性があることに注意する必要がある.

最近では,ニューラルネットは深層学習と進化し, ロジスティック関数以外の活性化関数が使われるようになり,実用化が進んでいる. 深層学習は, http://playground.tensorflow.org/ で体験することができる.

from sklearn.neural_network import MLPClassifier  # ニューラルネット

neural = MLPClassifier()
neural.fit(X_train, y_train)  # 訓練
yhat = neural.predict(X_test)  # 予測
print(metrics.accuracy_score(y_test, yhat))
scores = cross_val_score(neural, X, y, cv=10, scoring="accuracy")
print(scores)
print(scores.mean())

単純Bayes

単純Bayes(naive Bayes)は,名前の通り,高校で習うBayesの定理(規則)(Bayes' theorem; Bayes' rule)を用いた手法である. $P(X)$ を事象 $X$ が起きる確率,$P(Y|X)$ を事象 $X$ が起きたもとで事象 $Y$ が起きる確率 としたとき,Bayesの定理は以下の式で表される. $$ P(X) P(Y|X) =P(Y)P(X|Y) $$

分類の目的は,特性ベクトル $x$ に対して出力が $y$ になる確率 $P(y|x)$ を推定することであった. これはBayesの定理を用いると, $$ P(y|x) = \frac{ P(y) P(x|y) }{P(x)} $$ と計算できるので,我々の目的は $P(x|y)$ を推定することに置き換えられる. つまり,$y=0,1$ の各々に対して, トレーニングデータのしたがう分布 $p(x|y)$ を推定すれば良い.

いま,データは $n$次元実数ベクトルであり,各成分 $j=1,2,\ldots,n$ は独立な 正規(Gauss)分布にしたがっているものとする「単純な」仮定をする (これが単純Bayesの名前の由来である).

平均 $\mu$,標準偏差 $\sigma$ の正規分布 $N(\mu, \sigma^2)$ の密度関数を $$ p(x; \mu, \sigma^2)= \frac{1}{\sqrt{2\pi\sigma^{2}}} \exp\!\left(-\frac{(x-\mu)^2}{2\sigma^2} \right) $$ と記す.

$y$ が $1$ であるトレーニング集合を $\{ x^{(1)},x^{(2)}, \ldots, x^{(m)} \}$ とし, 各$j=1,2,\ldots,n$ に対して平均と分散を推定する. $$ \mu_j=\frac{1}{m} \displaystyle\sum_{i=1}^{m} x_j^{(i)} $$ $$ \sigma_j^2 = \frac{1}{m} \displaystyle\sum_{i=1}^{m} (x_j^{(i)}-\mu_j)^2 $$ 1式目は平均,2式目は分散を表す. 統計学者は不偏分散($1/m$ のかわりに $1/(m-1)$)を用いる場合が多いが, 機械学習では上の定義を用いる場合が多い.

成分ごとに独立な正規分布という仮定の下では,$y=1$ に分類されるデータの密度関数は, 正規分布の密度関数の積として $$ P(x|y=1) = \displaystyle\prod_{j=1}^n p(x_j; \mu_j, \sigma_j^2) $$ と書くことができる. 同様に $y=0$ に対しても $P(x|y=0)$ が計算できる.

これらの情報を用いることによって, (Bayesの定理を用いると)与えられたデータ $x$ が $y=1$ に分類される確率は, $$ P(y=1|x) = \frac{ P(y=1) P(x|y=1) }{P(x)} $$ と計算できる.ここで分母の $P(x)$ も $$ P(x)= P(x|y=1) P(y=1) + P(x|y=0) P(y=0) $$ と計算できることに注意されたい.

単純Bayes法はデータがしたがう確率分布についての情報をもつ場合に有効であり, 分類だけでなく不適合値の検出などの応用にも用いることができる.

from sklearn.naive_bayes import GaussianNB  # 単純Bayes

bayes = GaussianNB()
bayes.fit(X_train, y_train)  # 訓練
yhat = bayes.predict(X_test)  # 予測
print(metrics.accuracy_score(y_test, yhat))
scores = cross_val_score(bayes, X, y, cv=10, scoring="accuracy")
print(scores)
print(scores.mean())

決定木

決定木(decision tree)は,回帰にも分類にも使えるが,ここでは分類を用いて解説する. 決定木では,1つの特性に対する分岐によってトレーニング集合を分類していく. 分岐の情報は,根付き木で表現される. 決定木における仮説関数は,木の葉に対する出力の値の多数決をとったものである. どの特性を優先して分岐するかは,情報量(エントロピー)の増加が最大になるものを選択していく方法が一般的である. この手法は,結果の解釈が可能な点が利点であるが,過剰適合に陥りやすいのが弱点である.

from sklearn import tree

tree_class = tree.DecisionTreeClassifier()  # 決定木
tree_class.fit(X_train, y_train)  # 訓練
yhat = tree_class.predict(X_test)  # 予測
print(metrics.accuracy_score(y_test, yhat))
scores = cross_val_score(tree_class, X, y, cv=10, scoring="accuracy")
print(scores)
print(scores.mean())

アンサンブル法

アンサンブル法(ensemble method)とは,複数のアルゴリズムを合わせることによって, より高精度の予測もしくは分類を得るためのメタアルゴリズムである. その基本原理は「ばらつきをもったデータを集約するとばらつきが減少する」という 統計の公理であり,様々なデータや手法で得られた結果を, 平均したり多数決したりすることによって,よりばらつきの少ない結果を得ようというものである.

バギング(bagging; bootstrap aggregatingの略)は,ブートストラップ(bootstrap)とよばれるリサンプリング法によって複数のトレーニング集合を作成し, それらのデータを用いて作成したモデルを平均(回帰の場合)もしくは多数決(分類の場合)によって統合する方法である.

ブースティング(boosting)は,複数の手法の重み付けを学習によって調整する方法である.

ランダム森(random forest)は,決定木ベースのバギングである.

from sklearn.ensemble import RandomForestClassifier  # ランダム森

forest = RandomForestClassifier()
forest.fit(X_train, y_train)  # 訓練
yhat = forest.predict(X_test)  # 予測
print(metrics.accuracy_score(y_test, yhat))
scores = cross_val_score(forest, X, y, cv=10, scoring="accuracy")
print(scores)
print(scores.mean())

問題(タイタニック)

titanicデータをトレーニング用とテスト用に分けてから,ニューラルネット,単純Bayes,決定木,ランダム森による分類を行い,交差検証を行え.

クラスタリング

UCI機械学習レポジトリのワインに関するデータセットを用いてクラスタリングを解説する.

使用するのはKMeansクラスで実装されている $k$-平均法である.

$k$-平均法($k$-means method)は,与えられたデータを $k$個の部分集合 (クラスター)に分けるための手法である. $k$-平均法は, クラスター数 $k$ と$n$次元実数ベクトル $x^{(i)} \in \mathbf{R}^n$ を要素とする トレーニング集合 $\{ x^{(1)}, x^{(2)}, \ldots, x^{(m)} \}$ を 与えたとき,$k$個のクラスター(データの部分集合)の 重心(centroid) $\mu_1,\mu_2, \ldots ,\mu_k$ を(近似的に)求めることを目的とする. 重心はクラスター内のデータの平均値をとることによって得られるので,この名前がついた.

アルゴリズムは単純である.まずランダムに重心 $\mu_1, \mu_2, \ldots, \mu_k \in \mathbf{R}^n$ を決める. 次に,トレーニング集合内のデータを最も近い重心に割り当てることによってクラスターを決める. そして,各クラスターの重心を新しい重心として,重心が移動しなくなるまで,この操作を繰り返す.

$k$-平均法で対象とする問題は$NP$-困難であるので,このアルゴリズムは局所的最適解を 求めるだけである.初期重心をランダムに変更して繰り返すことによって, より良い解を求めることができるが,大域的最適解を得られる保証はない.

データは http://logopt.com/data/wine.data にある.

列名は https://archive.ics.uci.edu/ml/datasets/Wine で解説されている.

以下では,$k$-平均法で4つのクラスターに分類する.

L = [
    "Alcohol",
    "Malic",
    "Ash",
    "Alcalinity",
    "Magnesium",
    "Phenols",
    "Flavanoids",
    "Nonflavanoid",
    "Proanthocyanins",
    "Color",
    "Hue",
    "OD280",
    "OD315",
    "Proline"
]
wine = pd.read_csv("http://logopt.com/data/wine.data", names=L)
wine.head()
from sklearn.cluster import KMeans  # クラスをインポート

kmeans = KMeans(n_clusters=4)  # インスタンス生成
kmeans.fit(wine);  # 訓練
wine["label"] = kmeans.labels_
wine.head()

パラメータ $k$ の適正化 (エルボー法)

上では4つのクラスターに分けたが, 幾つに分けるかはデータに依存する. 目安になる方法としてエルボー法がある. yellowbrickパッケージのKElbowVisualizerクラスを用いて可視化する. クラスの引数のkで調べる $k$ の範囲を指定する.以下では,$2$から$12$ の範囲で $k$-平均法を適用し,結果をプロットしている. 評価尺度(重心への距離の和)が $k$ の増加とともに減少するが, それが減少が緩やかになった $k$ が適正なクラスター数であると考える.

この図が肘(エルボー)を曲げたように見えることからエルボー法とよばれ.

from yellowbrick.cluster import KElbowVisualizer

visualizer = KElbowVisualizer(kmeans, k=(2, 12))
visualizer.fit(wine)
visualizer.show();

クラスター間距離の可視化

4つのクラスターに分けたときのクラスターの大きさとクラスター間の距離をyellowbrickパッケージのInterclusterDistanceクラスを用いて可視化する. 以下の図から,大きなクラスター2つは近い関係にあり,最小のクラスターは他から遠いことが分かる.

from yellowbrick.cluster import InterclusterDistance

kmeans = KMeans(n_clusters=4, random_state=123)
visualizer = InterclusterDistance(kmeans)
visualizer.fit(wine)
visualizer.show();

問題(アヤメ)

irisのデータセットの各データを $k$-平均法を用いて3つのクラスターに分けよ.

次元削減

ここでは、高次元のデータを低次元に変換するための方法について学ぶ。

多くの機械学習の実際問題は多次元データである。これを人間が見てわかる程度(通常は2次元か3次元)に(本質を失うことなしに)変換できれば、 問題に対する洞察を得ることができ、大変便利である。また、低次元に変換してから、回帰や分類を行う方が、良い結果を出す場合もある。

主成分分析

最初に紹介するのは、古典的な主成分分析 (principal component analysis; PCA)である。 これは、低次元に射影変換したときに、なるべくデータがばらつくような軸(主成分)を選択するものである。

主成分分析は, $n$次元実数ベクトル $x^{(i)} \in \mathbf{R}^n$ を要素とする トレーニング集合 $\{ x^{(1)}, x^{(2)}, \ldots, x^{(m)} \}$ を, $k(<n)$次元平面上に射影したときの距離の自乗和を最小にするような $k$次元平面を求める ことによって次元削減を行う.

行列を用いると,トレーニング集合を表す $m \times n$ 行列 $X$ は, $$ X = \left( \begin{array}{cccc} x^{(1)}_{1} & \ldots & x^{(1)}_{n} \\ x^{(2)}_{1} & \ldots & x^{(2)}_{n} \\ \vdots & \ddots & \vdots \\ x^{(m)}_{1} & \ldots & x^{(m)}_{n} \end{array} \right) $$ となる.これを $m \times k$ 行列 $Z$ に,$n \times k$ の行列 $P$ で $$ Z = X P $$ と射影することによって次元削減が行われる.

アルゴリズムを適用する前にデータの正規化を行っておく. つまり,各特性の平均値 $\mu_j (j=1,2,\ldots,n)$ を $$ \mu_j =\frac{1}{m} \displaystyle\sum_{i=1}^m x_j^{(i)} $$ と計算し,新たな $x_j^{(i)}$ を $x_j^{(i)}-\mu_j$ もしくは平均値で正規化して $$ \frac{x_j^{(i)}-\mu_j}{\mu_j} $$ と設定する. これはデータの平均を原点に移動させたことに相当し, 特性スケーリング(feature scaling)もしくは平均正規化(mean normalization)とよばれる.

次に,データの共分散行列 $$ \Sigma =\frac{1}{m} \displaystyle\sum_{i=1}^m (x^{(i)}) (x^{(i)})^T $$ の固有値分解 $\Sigma = U S U^T$ を求める.ここで $U$ は直交行列であり, $S$ は固有値を対角成分にもつ対角行列である. ここで,正方行列 $\Sigma$ の固有値と固有ベクトルとは, $$ \Sigma u = \lambda u $$ を満たすスカラー $\lambda$ と(長さが $1$ で $u^T u=0$ を満たす)正規直交ベクトル $u$ である.

以下では,$k=1$ の場合のみを解説する.このとき,$Z$ と $P$ はベクトルになるので, $$ z = X p $$ が射影変換になる.$z$ の分散が最大になる射影 $p$ が,射影したときの距離の和を最小にする. $$ z^T z = (Xp)^T (Xp) =p^T X^T X p = m p^T \Sigma p $$ であり,$\Sigma$ の最大固有値を $\lambda$,そのときの固有ベクトルを $u$ とすると, $$ u^T \Sigma u = u^T \lambda u =\lambda $$ であるので,分散を最大にするには,$p=u$ にとれば良いことが分かる. $k>1$ の場合も同様に計算ができ, $n \times n$ 行列 $U$ の(固有値が大きい順に並べた)最初の $k$個の列が, 求めたい平面への射影を表すベクトルになる.

irisデータセットを用いて主成分分析と可視化の方法を説明する.

iris = px.data.iris()
X = iris[["sepal_length", "sepal_width", "petal_length", "petal_width"]]
from sklearn.decomposition import PCA

pca = PCA(n_components=2)  # 2次元に射影
pca.fit(X)
pca.components_  # 射影行列
Z = pca.transform(X)  # 射影したデータ
# 射影した2次元データを元のデータフレームに追加
iris["X"] = Z[:, 0]
iris["Y"] = Z[:, 1]
iris.head()
import seaborn as sns
sns.lmplot(x="X", y="Y", hue="species", fit_reg=False, data=iris);

$t$-SNE

もう1つの次元削減の手法である t-SNE (t-distributed Stochastic Neighbor Embedding) を紹介する。 直訳すると、$t$-分布確率的近傍埋め込みであるが、英語で呼ばれることが多く、ティー・スニーと発音される。

簡単に言うと、低次元空間での類似度を元の次元でのデータごとの類似度と(分布の意味で)近くなるような、位置(2次元の場合には $X,Y$ 座標)を求める手法である。

通常は、こちらの方が(主成分分析よりは)低次元に射影したデータを可視化したとき綺麗に分類される。

from sklearn.manifold import TSNE

Z = TSNE(n_components=2, random_state=0).fit_transform(X)
# 射影した2次元データを元のデータフレームに追加
iris["X"] = Z[:, 0]
iris["Y"] = Z[:, 1]
sns.lmplot(x="X", y="Y", hue="class", fit_reg=False, data=iris);

問題(アルコール摂取量)

http://logopt.com/data/drinks.csv にある国別のアルコール摂取量データを用いて主成分分析を行え.

4次元の数値データを2次元に射影し,2次元座標で表示せよ.その際,色調としては,大陸(continent)列を用いよ.

問題(ワイン)

クラスタリングの例で用いたワインのデータを2次元に射影して,クラスタリングされたラベルを色調として描画せよ.

特徴量の重要度

sklearn.inspection にある permutation_importance 関数に, 回帰もしくは分類インスタンスと 特徴ベクトル $X$, ターゲット $y$ を入れると,特徴の重要度を計算してくれる.

例として,最初に示した広告による売上をランダム森によって予測を行い,各特徴の重要度を計算する.

from sklearn.ensemble import RandomForestRegressor

data = pd.read_csv(
    "http://logopt.com/data/Advertising.csv", index_col=0
)  # 0行目をインデックスにする.
X = data[["TV", "Radio", "Newspaper"]]
y = data["Sales"]

rf = RandomForestRegressor()
rf.fit(X, y);
from sklearn.inspection import permutation_importance

result = permutation_importance(rf, X, y)
result.importances_mean

結果から, TVが最も重要で,次いで Radio であり, Newspaperは広告効果がほとんどないことが分かる.

yellowbrickパッケージのFeatureImportancesクラスを用いて可視化する.

from yellowbrick.model_selection import FeatureImportances

viz = FeatureImportances(rf)
viz.fit(X, y)
viz.show();

問題(アヤメ)

irisデータセットにおける特徴ベクトルの重要度を計算し,可視化せよ.

import plotly.express as px

iris = px.data.iris()
X = iris[["sepal_length", "sepal_width", "petal_length", "petal_width"]]
y = iris["species_id"]

問題(胸部癌)

http://logopt.com/data/cancer.csv にある胸部癌か否かを判定するデータセットに対してランダム森で分類し, 特徴重要度を計算し,可視化せよ.

最初の列diagnosisが癌か否かを表すものであり,Mが悪性(malignant),Bが良性(benign)を表す.