はじめに
今回私は最近はやりのchatGPTに興味を持ち、深層学習について学んでみたいと思い立ちました!
深層学習といえばPythonということなので、最終的にはPythonを使って深層学習ができるとこまでコツコツと学習していくことにしました。
ただ、勉強するだけではなく少しでもアウトプットをしようということで、備忘録として学習した内容をまとめていこうと思います。
この記事が少しでも誰かの糧になることを願っております!
※投稿主の環境はWindowsなのでMacの方は多少違う部分が出てくると思いますが、ご了承ください。
最初の記事:Python初心者の備忘録 #01
前の記事:Python初心者の備忘録 #20 ~機械学習入門編06~
次の記事:まだ
今回はクラスタリング、決定木、SVMについてまとめております。
■学習に使用している資料
Udemy:【後編】米国データサイエンティストがやさしく教える機械学習超入門【Pythonで実践】
■クラスタリング(Clustering)
▶クラスタリングとは
- 教師データを使わない分類タスクの一種
データをカテゴリーやクラス、ラベルと呼ばれる「種類」に分別・グループ化していく
分類/識別(classification)との違い
クラスタリングは教師なし学習
教師あり/なし学習のおさらい
教師あり学習(Supervised learning)
- 正解の情報(ラベルや値)から損失を計算し、損失が最小となるようにモデルを構築
※回帰や分類タスクは教師あり学習 - ◎ 正解があるのでモデルの結果に対して解釈しやすい
- ✕ 正解データを集める必要がある
教師なし学習(Unsuperviced learning)
- 類似しているデータをグループ(クラスタ)にしてデータを分類
※クラスタリングは教師なし学習 - ✕ 正解データがないのでやり方によって結果が色々と異なり解釈が難しい
- ◎ 正解データを集める必要がないので簡単に行える
クラスタリングの実用例
- Fake Newsやスパムメールなどの判定:その時に正解がわからないことが多い
- 顧客のセグメンテーション(マーケティング):対象によって正解が異なる
- 画像処理:似た画像でも光の加減でRGB値が異なるので正解を決めにくい
▶k-means
- 最も基本的なクラスタリングアルゴリズム
- データをk個のクラスタに分類する
→ 各クラスタ内の分散の合計が全体で最小となるようにする
k-meansの損失関数
- 全クラスタ内の各データ同士の差の平方和(クラスタ内分散)の合計を損失関数とする
※あるアルゴリズムに従って局所解を見つける
クラスタ内分散:$min(\sum_{k=1}^K\sum_{i,i'\in C_{k}}(x_i-x_{i'})^2)$
k-meansのアルゴリズム
- ランダムにそれぞれのデータに対して1~Kのクラスタに振り分ける
- クラスタが変更しなくなるまで以下を繰り返す
- 各クラスタの中心を求める
- 最も近いクラスタの中心をそのデータのクラスタとする
このアルゴリズムで行きつく答えは最適解(Gloval Optima)にならず、極小解(Local Optima)となることがあることに注意する。
→ そのためk-meansを複数回行い、解が最小となるものを選択するのが一般的
▶K(クラスタ数)の決め方
- (当然)データ数よりも少ない必要がある
- 明確な正解はなく、ドメイン知識やデータの背景から仮説を立てる必要がある
→ いろいろなKを試して考察するのが一番
★とは言いつつも方法がないわけではなく、方法の一つにElbow methodがある。
Pythonでk-means
-
sklearn.cluster.KMeans
ライブラリを使用する-
KMeans(n_clusters)
でインスタンス生成(n_cluster
:クラスタ数の指定) -
.fit(X)
で学習 → クラスタリング実行-
.predict(X)
で各データのクラスタリングの結果を参照 -
.score(X)
で損失を計算
-
-
# データ準備
import seaborn as sns
df = sns.load_dataset('iris')
X = df.drop(columns=['species'])
# 標準化
from sklearn.preprocessing import StandardScaler
X_scaled = StandardScaler().fit_transform(X)
# k-means
from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters=3, random_state=0)
labels = kmeans.fit_predict(X_scaled) # .fit(X)と.predict(X)を同時に行う
# ilisデータのクラスタリング結果が出ている
array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 2, 2, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 2,
0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 2, 2, 2, 0, 2, 2, 2,
2, 2, 2, 0, 0, 2, 2, 2, 2, 0, 2, 0, 2, 0, 2, 2, 0, 2, 2, 2, 2, 2,
2, 0, 0, 2, 2, 2, 0, 2, 2, 2, 0, 2, 2, 2, 0, 2, 2, 0], dtype=int32)
k-meansの描画
## データの分布を描画
import pandas as pd
result_df = pd.concat([X, pd.DataFrame(labels, columns=['kmean_result'])], axis=1)
sns.pairplot(result_df, hue='kmean_result')
# 教師付きのデータ分布
sns.pairplot(df, hue='species')
教師なし学習であるクラスタリングの描画と教師あり学習の分類の描画では大体あっているが多少の違いがあることがわかる。
→ 教師なし学習のクラスタリングでは正解を出すことは難しいが、ある程度の判断材料には使用できる。
k-meansの損失の推移を描画
import matplotlib.pyplot as plt
# Kが1~9の時の損失関数を計算
losses = []
for K in range(1, 10):
kmeans = KMeans(n_clusters=K, random_state=0).fit(X_scaled)
losses.append(-kmeans.score(X_scaled))
# Elbow methodを描画
plt.plot(range(1, 10), losses)
plt.xlabel('K')
plt.ylabel('loss')
★Elbow methodを見る限り、Kは2か3がよさそうと分かる
▶k-meansの注意点
- 特徴量の数が多い(高次元空間)場合は2組のデータ間の距離に差がなくなってくるので、うまくクラスタリングできなくなる:次元の呪い(curse of dimensionality)
→ 特徴量選択やPCAで次元削減する
- 質的変数との相性が悪い
- ダミー変数化して距離を計算しても距離の意味を持たない
$\sqrt{(0-1)^2+(1-1)^2+(1-0)^2}$:0か1でしか計算できない - 高次元化し、やはりデータ間の距離に差がなくなっていく
→ 量的変数のみでk-meansをしたり、k-modesなどの別のアルゴリズムを使用する
- ダミー変数化して距離を計算しても距離の意味を持たない
▶階層クラスタリング(Hierarchical Clustering)
- k-meansと双璧を成す有名なクラスタリング手法
- 事前にクラスタ数(K)を指定する必要がないのことが特徴で、dendrogram(樹状図)を作り"階層的に"クラスタリングする
階層クラスタリングのアルゴリズムの流れ
※データは事前に標準化しておく
- (初期設定)各データを各クラスタとする
- 距離が最も近いクラスタ同士を融合し、1つのクラスタにする
- 2.を繰り返す
- 最終的にクラスが1つになり終了
▶クラスタ間の距離の測り方
single
- クラスタ間で最も近いデータの距離
※たまたま1つでも近いデータがあれば融合してしまうのでバランスが悪くなりがち
complete
- クラスタ間で最も遠いデータの距離
average
- クラスタ間のすべての2組のデータの距離の平均
centroid
- 各クラスタの中心間の距離
ward(ウォード法)
- 全データの中心から各データの距離の二乗和(分散)から、それぞれのクラスタの中心とそのクラスタのデータの距離の二乗和を引いた値
- 計算量は多いが分類の感度が良いのでよく使われる
▶dengrogramの使い方と注意点
- 縦軸の任意の箇所で切ることで、好きなクラスタ数でクラスタリングができる
注意点
- ネストできない様なデータに対してもネスト型の階層を作ってしまう
→ Kの値によって分類の軸を分けることができない
▶Pythonで階層クラスタリング
階層クラスタリング
-
scipy.cluster.hierarchy.linkage
ライブラリを使用 -
linkage(X, method)
でクラスタリングを実施-
method
には'single', 'average', 'complete', 'centroid', 'ward'
等を指定
-
- dendrogramを構成する情報をNumpy Arrayで返す
-
[クラスタ1のindex, クラスタ2のindex, 距離, データの数]
- index < データ数:学習データのインデックス(クラスタ = データ)
- index >= データ数:linkageの戻り値のindex(結合されたクラスタに割り振られたindex)
-
from scipy.cluster.hierarchy import linkage, dendrogram
Z = linkage(X_scaled, method='ward')
# [クラスタ1のidx, クラスタ2のidx, 距離, データ数]のリスト
# idxは,idx<len(X)ならX[idx] idx>=len(X)ならZ[idx-len(X)]でクラスタを取得
Z[:10]
# n回目のクラスタリングで使用したデータの情報[クラスタ1のidx, クラスタ2のidx, 距離, データ数]のリスト
array([[1.01000000e+02, 1.42000000e+02, 0.00000000e+00, 2.00000000e+00],
[7.00000000e+00, 3.90000000e+01, 1.21167870e-01, 2.00000000e+00],
[1.00000000e+01, 4.80000000e+01, 1.21167870e-01, 2.00000000e+00],
[9.00000000e+00, 3.40000000e+01, 1.31632184e-01, 2.00000000e+00],
[0.00000000e+00, 1.70000000e+01, 1.31632184e-01, 2.00000000e+00],
[1.28000000e+02, 1.32000000e+02, 1.31632184e-01, 2.00000000e+00],
[1.27000000e+02, 1.38000000e+02, 1.33836265e-01, 2.00000000e+00],
[2.00000000e+00, 4.70000000e+01, 1.33836265e-01, 2.00000000e+00],
[8.00000000e+01, 8.10000000e+01, 1.43378956e-01, 2.00000000e+00],
[1.90000000e+01, 4.60000000e+01, 1.43378956e-01, 2.00000000e+00]])
dendrogram
-
scipy.cluster.hierarchy.dendrogram
ライブラリを使用 -
dendrogram(Z)
で描画-
Z
にはlinkage
の戻り値を入れる
-
-
trancate_mode
引数およびp
引数で任意の高さでdendrogramを切ることができる-
trancate_mode='lastp', p=K
:指定したK個のクラスタになるように切る -
trancate_mode='lastp', p=段数
:p段になるように切る
-
d = dendrogram(Z, truncate_mode='level', p=2)
▶実際にクラスタリングしてみる
各データのラベル付け
-
scipy.cluster.hierarchy.fcluster
ライブラリを使用-
Z
:linkage関数の戻り値 -
criterion
引数:どの段階のクラス手分けを使うかを指定-
'maxclust'
:t
引数に指定したクラスタ数でラベル付けを行う
-
-
t
引数:criterion
引数によって異なる値を指定する
-
from scipy.cluster.hierarchy import fcluster
# ラベル付け
clusters = fcluster(Z, criterion='maxclust', t=3)
hc_result_df = pd.concat([X, pd.DataFrame({'cluster': clusters})], axis=1)
sns.pairplot(hc_result_df, hue='cluster')
sns.pairplot(df, hue='species')
■決定木
▶決定木とは
- 回帰にも分類にも使用できるアルゴリズムで、人間の意思決定のロジックに近いので非常に解釈しやすい
- 単体での制度は低く、複数の決定木を組み合わせることで非常に高い精度モデルを構築できる
▶決定木のアルゴリズム概要
- データを2つの領域に分割していく
回帰の場合:領域の学習データの平均値を予測値をする
分類の場合:多数決
▶領域の分割の仕方
回帰
- 分割後の2つの領域の残差(偏差)の平方和の合計が最小になるように二分していく
分類
-
ジニ不純度(gini impurity) を使用
- どれだけ綺麗に分けられるかを表す指標
⇒ どれだけ「不純なもの」が含まれるか
$G=\sum_{k=1}^Kp(k)(1-p(k))$($K$:クラス数、$p(k)$:その領域でのクラスkの割合)
- どれだけ綺麗に分けられるかを表す指標
- 分割後の2つの領域のジニ不純度の合計が最小になるように二分していく
理想はすべての領域における偏差の平方和の合計が最小になることだが、それだと計算量が高くなるので分割毎に最小になるようにする。
そのため、最終的にすべての領域の平方和の合計が最小になるとは限らない...
⇒ 途中の分割が後々悪い分割になる可能性もある。(最適解ではなく、極小解を求めている)
▶決定木の過学習
- 分割を進めていくとモデルが複雑になり、過学習で汎化性能が下がる
⇒ 精度の低下、解釈性の低下、学習時間の増大が発生する
決定木の分割を途中で終了する
- 主に使用されるのは以下の2パターン
- 深さを指定
- 葉の最小データ数を指定
決定木の剪定(pruning)
- 葉の数だけペナルティを与える(cost complexity pruning)
最も小さい$\alpha$を持つ枝から選定していくとは?
$\alpha$が小さくても分割前と分割後の損失の差がほとんどないということは、分割しなくても結果に影響しないということになる。
⇒ つまり、分割の必要性が低い
▶Pythonで決定木
回帰
-
sklean.tree.DecisionTreeRegressor
ライブラリを使用-
max_depth
:木の深さを指定 -
min_samples_split
:最低限分割に必要なデータ数を指定 -
ccp_alpha
:cost complexity pruningの$\alpha$の値を指定
-
- 使い方は他のsklearnのモデルと同じで、パラメータをしていないと最後まで学習して過学習を起こすので注意
分類
-
sklean.tree.DecisionTreeClassifier
ライブラリを使用
※使い方は回帰と同じ
import seaborn as sns
import pandas as pd
from sklearn.model_selection import train_test_split
# データ準備
df = sns.load_dataset('tips')
df = pd.get_dummies(df, drop_first=True)
y_col = 'tip'
X = df.drop(columns=[y_col])
y = df[y_col]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)
# 決定木モデル(回帰)
from sklearn import tree
model = tree.DecisionTreeRegressor(max_depth=4)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
# MSE
from sklearn.metrics import mean_squared_error
mean_squared_error(y_test, y_pred) # -> 1.1060246260428703
# R^2(R-squared)
model.score(X_test, y_test) # -> 0.3590618660303134
▶決定木の可視化
描画
-
sklearn.tree.plot_tree
ライブラリを使用-
decision_tree
:sklearnのtreeモデルを引数にする -
fontsize
:フォントサイズを指定 -
feature_name
:特徴量の名前をリストで渡す(model.feature_names_in_
が便利)
-
- 図の内容が戻り値で返ってくる
# figureサイズの調整
import matplotlib.pyplot as plt
plt.figure(figsize=(40, 20))
# 決定木の描画
_ = tree.plot_tree(model, fontsize=10, feature_names=model.feature_names_in_)
テキスト
-
sklearn.tree.export_text
ライブラリを使用
※使い方はplot_tree
と同じ -
print()
関数に渡すことできれいに表示される
print(tree.export_text(model, feature_names=list(model.feature_names_in_)))
▶決定木の特徴量の重要性
- 予測に重要な特徴量ほど決定木の上部で分岐される
Pythonで重量度の可視化
-
.feature_importances_
属性でモデル内の特徴量の重要度を取得できる
⇒ 分岐の際にどれだけ不純度を下げることができたかで計算する
# 各特徴量の重要度
model.feature_importances_ # -> array([0.87823592, 0.07825569, 0., 0.03169922, 0., 0.01180916, 0., 0.])
# 描画
plt.barh(model.feature_names_in_, model.feature_importances_)
▶Minimal cost complexity pruning(剪定)
-
.cost_complecity_pruning_path(X, y)
で、$\alpha$と不純度をリストで取得できる - それぞれの$\alpha$でのモデルの精度を計測し、最良の$\alpha$を使用する
# model作成
model = tree.DecisionTreeRegressor(random_state=0)
# 決定木を剪定する閾値となるαと不純度のリスト
path = model.cost_complexity_pruning_path(X_train, y_train)
eff_alphas = path.ccp_alphas
# 各αでのモデルを構築
models = []
for eff_alpha in eff_alphas:
model = tree.DecisionTreeRegressor(random_state=0, ccp_alpha=eff_alpha)
model.fit(X_train, y_train)
models.append(model)
# それぞれのモデルの精度指標を記録(上記のfor文の中で実行した方が効率がいいが,ここでは別ループにて実行する)
from sklearn.metrics import mean_squared_error
train_scores = [mean_squared_error(y_train, model.predict(X_train)) for model in models]
test_scores = [mean_squared_error(y_test, model.predict(X_test)) for model in models]
# 各αで剪定したときのモデルの精度の推移を描画
plt.plot(eff_alphas, train_scores, marker='o', label='train', drawstyle='steps-post')
plt.plot(eff_alphas, test_scores, marker='o', label='test', drawstyle='steps-post')
plt.legend()
plt.xlabel('alpha')
plt.ylabel('MSE')
※大体$\alpha$が1の時のモデルがちょうどよさそうというのが見てとれる
最後まで剪定したモデルを見てみる
print(tree.export_text(models[-1], feature_names=list(models[-1].feature_names_in_)))
models[-1].predict(X_test)
# 全て2.96...と予測される
array([2.96429412, 2.96429412, 2.96429412, 2.96429412, 2.96429412,
2.96429412, 2.96429412, 2.96429412, 2.96429412, 2.96429412,
2.96429412, 2.96429412, 2.96429412, 2.96429412, 2.96429412,
2.96429412, 2.96429412, 2.96429412, 2.96429412, 2.96429412,
2.96429412, 2.96429412, 2.96429412, 2.96429412, 2.96429412,
2.96429412, 2.96429412, 2.96429412, 2.96429412, 2.96429412,
2.96429412, 2.96429412, 2.96429412, 2.96429412, 2.96429412,
2.96429412, 2.96429412, 2.96429412, 2.96429412, 2.96429412,
2.96429412, 2.96429412, 2.96429412, 2.96429412, 2.96429412,
2.96429412, 2.96429412, 2.96429412, 2.96429412, 2.96429412,
2.96429412, 2.96429412, 2.96429412, 2.96429412, 2.96429412,
2.96429412, 2.96429412, 2.96429412, 2.96429412, 2.96429412,
2.96429412, 2.96429412, 2.96429412, 2.96429412, 2.96429412,
2.96429412, 2.96429412, 2.96429412, 2.96429412, 2.96429412,
2.96429412, 2.96429412, 2.96429412, 2.96429412])
■SVM(Support Vector Machine)
▶SVMとは
- 分類と回帰のどちらにも使用可能で、単体のモデルとしては精度が高いことで有名
※深層学習を除いて... - 決定境界を引くことで分類を行う
- この決定境界があまりにギリギリだと未知のデータを誤分類してしまう可能性がある
- 決定境界線はある程度の余裕(マージン)を取って引いた方がいい
▶ソフトマージンとハードマージン
ハードマージン
- 誤分類を許容しないので、外れ値があると大きく結果が変わってしまう
⇒ high variance、過学習、汎化性能が低いモデルになる
ソフトマージン
- 誤分類を許容するので、外れ値があった場合でも大きく結果は変わらない
⇒ モデルのロバスト性が高くなる - マージンの中にデータが入ることも許容する(ハードマージンでは許容しない)
※境界の直近のデータがサポートベクターになるとは限らない
▶SVMの損失関数
- マージンを最大化しつつ誤分類を減らすような関数
$min(\frac{1}{M}+C\sum_{i=1}^m\xi_i)$ ⇒ パラメータCにより誤分類の許容を調整する
ヒンジ損失(hinge loss)
- 交差エントロピー(log loss)を近似した損失で、近似により計算量が削減できる
- 1>や-1<で損失するようにすることで、マージンを考慮した損失となる
上図において、$\theta^Tx$が0の時にp(x)=0.5になり、0より大きいか小さいかで正解/不正解の識別は可能のように見えるが、それぞれ1や-1までいかなければ十分なマージンではないということを表している。
ハードマージンにおける制約
- 誤分類が許されないので、以下のような制約で最適化問題を解くこととなる
ソフトマージンにおける制約
- 誤分類を許容するので、制約は以下のように緩める
▶非線形の分類
- SVMを非線形分離可能のアルゴリズムとする
- 特徴量空間を非線形変換することで非線形分離を実現する
カーネルトリック(kernel trick)
- 特徴量を写像した際に膨大となる各データ間の内積の計算を解決する方法
カーネル関数:$K(x_i, x_j)=\phi(x_i)^T\phi(x_j)$
様々なカーネル関数
- 多項式カーネル
$K(x_i, x_j)=(x_i^Tx_j+c)^d$ - ガウスカーネル
$K(x_i, x_j)=exp(-\frac{||x_i-x_j||^2}{2\sigma^2})$ - シグモイドカーネル
$K(x_i, x_j)=\frac{1}{1+exp(-\gamma x_i^Tx_j)}$
▶PythonでSVM
-
sklearn.svm.SVC
を使用する-
C
:エラーの正則化項の係数(誤分類の許容値と考えればいい) -
kernel
:使用するカーネル-
linear
,poly
,rdf
,sigmoid
など
-
-
degree
:'poly'のd(デフォルトだと3) -
gamma
:'poly'、'rdf'、'sigmoid'の係数$\gamma$- 'scale':$\frac{1}{n×Var(X)}$(デフォルト)
- 'auto':$\frac{1}{n}$
-
.support_vectors_
でサポートベクトルのリストを取得
-
- 回帰は
sklearn.svm.SVR
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
# データ準備
df = sns.load_dataset('iris')
y_col = 'species'
X = df.drop(columns=[y_col])
y = df[y_col]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)
# 標準化
scaler = StandardScaler().fit(X_train)
X_train = scaler.transform(X_train)
X_test = scaler.transform(X_test)
# PCA
pca = PCA(n_components=2)
X_train_pc = pca.fit_transform(X_train)
X_test_pc = pca.transform(X_test)
# 学習(SVM)
from sklearn.svm import SVC
model = SVC(kernel='poly', C=100)
model.fit(X_train_pc, y_train)
# 予測
y_pred = model.predict(X_test_pc)
# 評価
from sklearn.metrics import accuracy_score
accuracy_score(y_test, y_pred) # -> 0.9333333333333333
# サポートベクトルのリスト取得
# model.support_vectors_
▶決定境界とサポートベクトルの可視化
-
sklearn.inspectopn.DecisionBoundaryDisplay
を使用
※version 1.1から使用可能(python>3.8)-
.from_estimator(model, X)
で描画
-
from sklearn.inspection import DecisionBoundaryDisplay
import matplotlib.pyplot as plt
import numpy as np
# 決定境界描画
DecisionBoundaryDisplay.from_estimator(model,
X_train_pc,
plot_method='contour',
cmap=plt.cm.Paired,
xlabel='first principal component',
ylabel='second principal component')
# (PCA後の)学習データ描画
for class_, color in zip(model.classes_, 'bry'):
idx = np.where(y_train == class_)
plt.scatter(X_train_pc[idx, 0],
X_train_pc[idx, 1],
c=color,
label=class_,
edgecolor='black',
s=20)
# サポートベクトル描画
plt.scatter(model.support_vectors_[:, 0],
model.support_vectors_[:, 1],
s=100,
facecolor='none',
linewidth=1,
edgecolor='black')
plt.legend()
次の記事
まだ