第 9 章 演習解答¶
第 9 章「教師なし学習」の演習問題の解答例です。問題は 第 9 章 演習問題 を参照してください。
演習 9-1: k-means と最適クラスタ数¶
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
# irisデータセットの読み込み
iris = sns.load_dataset("iris")
X = iris[["sepal_length", "sepal_width",
"petal_length", "petal_width"]].values
# 1. k=3 でk-means法を実行
km3 = KMeans(n_clusters=3, random_state=0, n_init=10)
labels3 = km3.fit_predict(X)
# 2. エルボー法(k=1〜10)と 3. シルエットスコア(k=2〜10)
inertias, sils = [], []
for k in range(1, 11):
km = KMeans(n_clusters=k, random_state=0, n_init=10)
labels = km.fit_predict(X)
inertias.append(km.inertia_)
if k == 1:
print(f"k={k:2d}: イナーシャ {km.inertia_:8.2f}")
else:
sil = silhouette_score(X, labels)
sils.append(sil)
print(f"k={k:2d}: イナーシャ {km.inertia_:8.2f}, "
f"シルエットスコア {sil:.4f}")
# 2つの指標を並べてプロット
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].plot(range(1, 11), inertias, marker="o")
axes[0].set_xlabel("クラスタ数 k")
axes[0].set_ylabel("イナーシャ")
axes[0].set_title("エルボー法")
axes[0].grid(True)
axes[1].plot(range(2, 11), sils, marker="o")
axes[1].set_xlabel("クラスタ数 k")
axes[1].set_ylabel("シルエットスコア")
axes[1].set_title("シルエット分析")
axes[1].grid(True)
plt.tight_layout()
plt.show()
# 4. 実際の品種ラベルとのクロス集計
print()
print(pd.crosstab(labels3, iris["species"]))
k= 1: イナーシャ 681.37
k= 2: イナーシャ 152.35, シルエットスコア 0.6810
k= 3: イナーシャ 78.85, シルエットスコア 0.5528
k= 4: イナーシャ 57.23, シルエットスコア 0.4981
k= 5: イナーシャ 46.45, シルエットスコア 0.4887
k= 6: イナーシャ 39.04, シルエットスコア 0.3648
k= 7: イナーシャ 34.42, シルエットスコア 0.3569
k= 8: イナーシャ 30.06, シルエットスコア 0.3597
k= 9: イナーシャ 28.33, シルエットスコア 0.3373
k=10: イナーシャ 25.97, シルエットスコア 0.3238
species setosa versicolor virginica
row_0
0 0 48 14
1 50 0 0
2 0 2 36
実行すると、左にエルボー法(イナーシャ)、右にシルエット分析のグラフが並んで表示されます。
考察:イナーシャは \(k=3\) を境に減り方が緩やかになるので、エルボー法ではクラスタ数3と読めます。一方、シルエットスコアは \(k=2\)(0.6810)で最大です。versicolor と virginica は分布が近く、この2品種をまとめた2クラスタのほうがきれいに分かれるためです。クロス集計でも、setosa の50件は1つのクラスタにまとまりますが、versicolor と virginica は計16件(14件と2件)が相手のクラスタに入っています。指標によって答えが変わるときは、品種が3つという知識なども合わせて判断します。
演習 9-2: 人工データのクラスタリング¶
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
# 5つのクラスタを持つデータを生成
X, y = make_blobs(n_samples=500, centers=5,
cluster_std=1.0, random_state=42)
# エルボー法
inertias = []
K_range = range(1, 11)
for k in K_range:
km = KMeans(n_clusters=k, random_state=0, n_init=10)
km.fit(X)
inertias.append(km.inertia_)
print(f"k={k:2d}: イナーシャ {km.inertia_:9.2f}")
plt.figure(figsize=(8, 5))
plt.plot(K_range, inertias, marker="o")
plt.xlabel("クラスタ数 k")
plt.ylabel("イナーシャ")
plt.xticks(K_range)
plt.grid(True)
plt.show()
# k=5 でクラスタリングして可視化
km5 = KMeans(n_clusters=5, random_state=0, n_init=10)
labels = km5.fit_predict(X)
plt.figure(figsize=(8, 5))
plt.scatter(X[:, 0], X[:, 1], c=labels, cmap="viridis", s=30)
plt.scatter(km5.cluster_centers_[:, 0], km5.cluster_centers_[:, 1],
c="red", marker="X", s=200, edgecolors="black")
plt.xlabel("特徴量1")
plt.ylabel("特徴量2")
plt.show()
k= 1: イナーシャ 30210.77
k= 2: イナーシャ 14726.12
k= 3: イナーシャ 3648.76
k= 4: イナーシャ 1543.39
k= 5: イナーシャ 923.99
k= 6: イナーシャ 836.13
k= 7: イナーシャ 766.13
k= 8: イナーシャ 691.77
k= 9: イナーシャ 629.19
k=10: イナーシャ 580.64
実行すると、エルボー法のグラフと、\(k=5\) でクラスタリングした散布図(重心を赤い×印で表示)が表示されます。
考察:イナーシャは \(k=5\) まで大きく減り(1543.39 → 923.99)、\(k=6\) ではわずかしか減りません(923.99 → 836.13)。グラフでも \(k=5\) に肘がはっきり出ており、データを作ったときのクラスタ数5を当てています。ここまで明瞭なのはクラスタ同士がよく離れたデータだからで、実データでは肘があいまいなことも多くあります。
演習 9-3: 階層的クラスタリングとデンドログラム¶
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from scipy.cluster.hierarchy import linkage, dendrogram, fcluster
# Irisデータセットの読み込み
df_iris = sns.load_dataset("iris")
X_iris = df_iris[["sepal_length", "sepal_width",
"petal_length", "petal_width"]].values
y_iris = df_iris["species"]
# Ward法による階層的クラスタリング
Z = linkage(X_iris, method="ward")
# デンドログラムの描画
plt.figure(figsize=(12, 5))
dendrogram(Z, no_labels=True)
plt.xlabel("データ点")
plt.ylabel("結合距離")
plt.show()
# 3クラスタに分割して真の品種と比較
labels3 = fcluster(Z, t=3, criterion="maxclust")
print("各クラスタのデータ数:", np.bincount(labels3)[1:])
print(pd.crosstab(labels3, y_iris))
実行すると、データ点が距離の近い順に結合されていく様子を示すデンドログラムが表示されます。
考察:デンドログラムは、結合距離が最も大きいところで2つの枝に分かれ、次に片方の枝が2つに分かれます。ここから2〜3クラスタが妥当と読めます。3クラスタに分けると、setosa の50件は1つのクラスタにまとまります。versicolor と virginica は分布が近く、計16件(1件と15件)が相手のクラスタに入ります。境界付近がうまく分かれないのは、演習 9-1 のk-means法と同じです。
演習 9-4: PCA による可視化¶
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
# 1. penguinsデータの読み込みと欠損値の削除
penguins = sns.load_dataset("penguins").dropna()
feature_cols = ["bill_length_mm", "bill_depth_mm",
"flipper_length_mm", "body_mass_g"]
X = penguins[feature_cols].values
# 2. 数値特徴量を標準化
X_scaled = StandardScaler().fit_transform(X)
# 3. PCAで2次元に削減
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_scaled)
# 4. 種類ごとに色分けして散布図を作成
plt.figure(figsize=(8, 6))
for species in ["Adelie", "Chinstrap", "Gentoo"]:
mask = (penguins["species"] == species).values
plt.scatter(X_pca[mask, 0], X_pca[mask, 1], label=species, s=30)
plt.xlabel("第1主成分")
plt.ylabel("第2主成分")
plt.legend()
plt.grid(True)
plt.show()
# 5. 各主成分の寄与率を表示
print("寄与率:", pca.explained_variance_ratio_.round(4))
print(f"累積寄与率: {pca.explained_variance_ratio_.sum():.4f}")
実行すると、3種のペンギンが色分けされた2次元の散布図が表示されます。
考察:第1・第2主成分で分散の約 88% を説明でき、散布図でも 3 種のまとまりが見えます。Gentoo は第1主成分の正の側に分かれ、Adelie と Chinstrap は主に第2主成分で分かれます(境界付近は一部重なります)。pca.components_ 属性を見ると、第1主成分は flipper_length_mm と body_mass_g の重みが大きく、体の大きさを表す軸と読めます。第2主成分は bill_depth_mm と bill_length_mm の重みが大きい、くちばしの形の軸です。なお、body_mass_g は数千、bill_depth_mm は十数と桁が違うため、標準化せずに PCA をかけると、第1主成分はほぼ体重だけで決まってしまいます。
演習 9-5: PCA と t-SNE の比較¶
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs
from sklearn.decomposition import PCA
from sklearn.manifold import TSNE
# 10次元のデータを生成
X, y = make_blobs(n_samples=500, centers=5, n_features=10,
cluster_std=1.0, random_state=42)
# PCAで2次元に削減
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X)
print(f"PCAの寄与率: {pca.explained_variance_ratio_.round(4)}")
print(f"累積寄与率: {pca.explained_variance_ratio_.sum():.4f}")
# t-SNEで2次元に削減
tsne = TSNE(n_components=2, random_state=42)
X_tsne = tsne.fit_transform(X)
# 並べて可視化
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
axes[0].scatter(X_pca[:, 0], X_pca[:, 1], c=y, cmap="viridis", s=20)
axes[0].set_title("PCA")
axes[1].scatter(X_tsne[:, 0], X_tsne[:, 1], c=y, cmap="viridis", s=20)
axes[1].set_title("t-SNE")
plt.tight_layout()
plt.show()
実行すると、PCA と t-SNE で2次元化した散布図が並んで表示されます。
考察:どちらの手法でも5つのクラスタが見えますが、見え方が違います。PCA は元の10次元での距離関係をなるべく保つので、クラスタ同士の遠近は元データを反映します。ただし2次元で説明できる分散は約 80% で、近くに表示されるクラスタもあります。t-SNE は近くの点どうしの関係を重視するので、各クラスタがはっきり離れたかたまりになります。その代わり、クラスタ間の距離や配置にはあまり意味がなく、random_state を変えると配置も変わります。