Skip to content

9. 教師なし学習

本章では、ラベルなしデータから構造やパターンを発見する 教師なし学習 について学びます。クラスタリングと次元削減という 2 つの代表的な手法を扱います。

教師なし学習(Unsupervised Learning) は、正解ラベルのないデータからパターンや構造を見つける手法です。

graph TB
    A("機械学習") --> B("教師あり学習")
    A --> C("教師なし学習")

    B --> B1("正解ラベルあり<br/>分類・回帰")
    C --> C1("正解ラベルなし<br/>クラスタリング・次元削減")

    classDef data fill:#D9EAF3,stroke:#0072B2,stroke-width:2px,color:#000
    classDef proc fill:#D9F0EA,stroke:#009E73,stroke-width:2px,color:#000
    classDef neu fill:#F2F2F2,stroke:#8C8C8C,stroke-width:2px,color:#000
    class A neu
    class B,B1 data
    class C,C1 proc
項目 教師あり学習 教師なし学習
正解ラベル あり なし
目的 予測 構造の発見
代表的手法 分類、回帰 クラスタリング、次元削減
応用例 スパム判定、価格予測 顧客セグメント、異常検知

教師なし学習の応用例

  • 顧客セグメンテーション: 購買行動が似た顧客をグループ化
  • 異常検知: 通常と異なるパターンを検出
  • 次元削減: データの可視化、特徴量圧縮
  • トピックモデリング: 文書のテーマを自動分類

クラスタリング(Clustering) は、データを類似したグループ(クラスタ)に分割する手法です。

graph LR
    A("データ") --> B("クラスタリング")
    B --> C("クラスタ 1")
    B --> D("クラスタ 2")
    B --> E("クラスタ 3")

    classDef data fill:#D9EAF3,stroke:#0072B2,stroke-width:2px,color:#000
    classDef proc fill:#D9F0EA,stroke:#009E73,stroke-width:2px,color:#000
    classDef hl fill:#F9E7D9,stroke:#D55E00,stroke-width:2px,color:#000
    classDef model fill:#F7EBF2,stroke:#CC79A7,stroke-width:2px,color:#000
    classDef out fill:#F5ECE2,stroke:#BF8040,stroke-width:2px,color:#000
    class A data
    class B proc
    class C hl
    class D model
    class E out

クラスタリングの特徴

  • 正解ラベルが不要
  • データの構造を探索的に発見
  • クラスタ数は事前に決める必要がある場合が多い

なお、クラスタリングは 第4章 でも簡単に概観しましたが、本章では各手法のアルゴリズムの仕組みから詳しく学んでいきます。

9.1 k-means法

9.1.1 k-means法とは

k-means法 は、最も基本的なクラスタリング手法です。データを k 個のクラスタに分割します。

9.1.2 k-means法の仕組み

graph TB
    A("1. 初期重心をランダムに配置") --> B("2. 各点を最も近い重心に割り当て")
    B --> C("3. 各クラスタの重心を再計算")
    C --> D{重心が変化した?}
    D -->|Yes| B
    D -->|No| E("完了")

    classDef proc fill:#D9F0EA,stroke:#009E73,stroke-width:2px,color:#000
    classDef hl fill:#F9E7D9,stroke:#D55E00,stroke-width:2px,color:#000
    classDef out fill:#F5ECE2,stroke:#BF8040,stroke-width:2px,color:#000
    class A,B,C proc
    class D hl
    class E out

アルゴリズムの手順

  1. k 個の初期重心(セントロイド)をランダムに配置
  2. 各データ点を最も近い重心のクラスタに割り当て
  3. 各クラスタの重心を再計算(平均位置)
  4. 重心が変化しなくなるまで 2-3 を繰り返す

9.1.3 k-means法の実装

import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs

# サンプルデータの生成
X, y_true = make_blobs(n_samples=300, centers=4, cluster_std=0.6, random_state=42)

# k-means法によるクラスタリング
kmeans = KMeans(n_clusters=4, random_state=42, n_init=10)
y_pred = kmeans.fit_predict(X)

# 可視化
plt.figure(figsize=(10, 4))

# クラスタリング前
plt.subplot(1, 2, 1)
plt.scatter(X[:, 0], X[:, 1], s=50, alpha=0.6)
plt.title('クラスタリング前')

# クラスタリング後
plt.subplot(1, 2, 2)
plt.scatter(X[:, 0], X[:, 1], c=y_pred, cmap='viridis', s=50, alpha=0.6)
plt.scatter(kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1],
            c='red', marker='x', s=200, linewidths=3, label='重心')
plt.title('クラスタリング後')
plt.legend()

plt.tight_layout()
plt.show()

9.1.4 最適なクラスタ数の決定

クラスタ数 k をどう決めるかは重要な問題です。エルボー法 がよく使われます。

from sklearn.cluster import KMeans
import matplotlib.pyplot as plt

# 異なる k での慣性(inertia)を計算
inertias = []
k_range = range(1, 11)

for k in k_range:
    kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
    kmeans.fit(X)
    inertias.append(kmeans.inertia_)

# エルボー法の可視化
plt.figure(figsize=(8, 5))
plt.plot(k_range, inertias, 'bo-')
plt.xlabel('クラスタ数 k')
plt.ylabel('慣性(Inertia)')
plt.title('エルボー法')
plt.grid(alpha=0.3)
plt.show()

エルボー法

慣性(各点から重心までの距離の二乗和)を k ごとにプロットし、グラフが「肘」のように曲がる点を最適な k とします。

Playground:k-means法

クラスタリング(k-means) の Playground では、「1ステップ進める」を押すたびに「各点を一番近い重心へ割り当て → 重心をグループの平均位置へ動かす」が 1 回ずつ実行されます。繰り返すうちに重心が動かなくなって収束すること、SSE が下がっていくこと、そして同じデータでも最初の重心の位置によって結果が変わることを確かめてみましょう。

9.2 階層的クラスタリング

9.2.1 階層的クラスタリングとは

階層的クラスタリング(Hierarchical Clustering) は、データをツリー構造(デンドログラム)で表現するクラスタリング手法です。

graph TB
    A("全データ") --> B("クラスタ A")
    A --> C("クラスタ B")
    B --> D("サブクラスタ A1")
    B --> E("サブクラスタ A2")
    C --> F("サブクラスタ B1")
    C --> G("サブクラスタ B2")

    classDef data fill:#D9EAF3,stroke:#0072B2,stroke-width:2px,color:#000
    classDef hl fill:#F9E7D9,stroke:#D55E00,stroke-width:2px,color:#000
    classDef neu fill:#F2F2F2,stroke:#8C8C8C,stroke-width:2px,color:#000
    class A neu
    class B,D,E hl
    class C,F,G data

特徴

  • デンドログラムを見てからクラスタ数または距離のしきい値を決められる
  • デンドログラムでクラスタの関係を可視化できる
  • 計算コストが高い(大規模データには不向き)

9.2.2 階層的クラスタリングの実装

from scipy.cluster.hierarchy import dendrogram, linkage
from sklearn.cluster import AgglomerativeClustering
import matplotlib.pyplot as plt

# デンドログラムの作成
linkage_matrix = linkage(X, method='ward')

plt.figure(figsize=(12, 5))
dendrogram(linkage_matrix)
plt.title('デンドログラム')
plt.xlabel('サンプルインデックス')
plt.ylabel('距離')
plt.show()

# クラスタリングの実行
clustering = AgglomerativeClustering(n_clusters=4)
y_pred = clustering.fit_predict(X)

Playground:階層的クラスタリング

階層的クラスタリング の Playground では、スライダーでデンドログラムを切る高さを変えると、カットラインが上下して散布図の色分けが連動します。樹形図の縦線が大きく伸びているところのすぐ下で切ると自然なクラスタ数になること、連結法(単連結・完全連結・平均連結)を切り替えると樹形図の形そのものが変わることを試してみましょう。

9.3 シルエット分析

9.3.1 シルエット係数とは

シルエット係数 は、クラスタリングの品質を評価する指標です。

\[ s(i) = \frac{b(i) - a(i)}{\max(a(i), b(i))} \]
  • \(a(i)\): 同じクラスタ内の他の点との平均距離
  • \(b(i)\): 他の各クラスタの点までの平均距離をそれぞれ求めたときの最小値

シルエット係数の解釈

  • 1 に近い: 適切なクラスタに割り当てられている
  • 0 に近い: クラスタの境界にある
  • 負の値: 誤ったクラスタに割り当てられている可能性

9.3.2 シルエット分析の実装

from sklearn.metrics import silhouette_score, silhouette_samples
import matplotlib.pyplot as plt
import numpy as np

# 異なる k でシルエットスコアを計算
silhouette_scores = []
k_range = range(2, 11)

for k in k_range:
    kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
    y_pred = kmeans.fit_predict(X)
    score = silhouette_score(X, y_pred)
    silhouette_scores.append(score)
    print(f"k={k}: シルエットスコア = {score:.4f}")

# 可視化
plt.figure(figsize=(8, 5))
plt.plot(k_range, silhouette_scores, 'bo-')
plt.xlabel('クラスタ数 k')
plt.ylabel('シルエットスコア')
plt.title('シルエット分析')
plt.grid(alpha=0.3)
plt.show()

シルエット分析の活用

シルエットスコアが最も高い k が最適なクラスタ数の候補となります。エルボー法と併用することで、より信頼性の高い判断ができます。

9.4 次元削減

9.4.1 次元削減とは

次元削減(Dimensionality Reduction) は、データの特徴量を減らしながら重要な情報を保持する手法です。

次元削減の目的

  • 可視化: 高次元データを2〜3次元に投影して可視化
  • 計算効率: 特徴量を減らして計算コストを削減
  • ノイズ除去: 不要な次元を削除してモデルの性能向上

9.4.2 主成分分析(PCA)

主成分分析(Principal Component Analysis, PCA) は、最も一般的な次元削減手法です。データの分散が最大になる方向を第1主成分とし、以降は、それまでの主成分に直交する方向のうち分散が最大の方向を順に求めます。分散の大きい順に上位の主成分を残して次元を削減します。寄与率は元のデータの分散のうち各主成分が表す割合で、予測に必要な情報の保存率ではありません。

graph LR
    A("高次元データ<br/>n次元") --> B("PCA")
    B --> C("低次元データ<br/>k次元")

    B --> D("第1主成分<br/>最大分散方向")
    B --> E("第2主成分<br/>第1主成分に直交する<br/>最大分散方向")

    classDef data fill:#D9EAF3,stroke:#0072B2,stroke-width:2px,color:#000
    classDef proc fill:#D9F0EA,stroke:#009E73,stroke-width:2px,color:#000
    classDef model fill:#F7EBF2,stroke:#CC79A7,stroke-width:2px,color:#000
    classDef out fill:#F5ECE2,stroke:#BF8040,stroke-width:2px,color:#000
    class A data
    class B proc
    class C out
    class D,E model

9.4.3 PCA の実装

from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
import seaborn as sns
import matplotlib.pyplot as plt

# アヤメデータセットの読み込み
iris = sns.load_dataset('iris')
X = iris.drop('species', axis=1)
y = iris['species']

# 標準化(PCA の前に推奨)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# PCA で2次元に削減
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_scaled)

# 寄与率の確認
print(f"各主成分の寄与率: {pca.explained_variance_ratio_}")
print(f"累積寄与率: {pca.explained_variance_ratio_.sum():.4f}")

# 可視化
plt.figure(figsize=(8, 6))
for species in y.unique():
    mask = y == species
    plt.scatter(X_pca[mask, 0], X_pca[mask, 1], label=species, alpha=0.7, s=50)

plt.xlabel(f'PC1 ({pca.explained_variance_ratio_[0]:.2%})')
plt.ylabel(f'PC2 ({pca.explained_variance_ratio_[1]:.2%})')
plt.title('PCA による次元削減(Iris データセット)')
plt.legend()
plt.grid(alpha=0.3)
plt.show()

Playground:主成分分析(PCA)

PCA(主成分分析) の Playground では、相関の強さを動かすと第1主成分(PC1)の向きが追いかけてきて、相関が強いほど PC1 の寄与率が大きくなります。「第1主成分に射影」を押すと点が PC1 上に潰れます。これが次元削減です。少ない軸で情報をなるべく残すという狙いを目で確かめてみましょう。

9.4.4 次元数の決定

累積寄与率をもとに、適切な次元数を決定します。

# 全主成分を計算
pca_full = PCA()
pca_full.fit(X_scaled)

# 累積寄与率の計算
cumulative_variance = np.cumsum(pca_full.explained_variance_ratio_)

# 可視化
plt.figure(figsize=(8, 5))
plt.plot(range(1, len(cumulative_variance) + 1), cumulative_variance, 'bo-')
plt.axhline(y=0.95, color='r', linestyle='--', label='95% の寄与率')
plt.xlabel('主成分の数')
plt.ylabel('累積寄与率')
plt.title('累積寄与率')
plt.legend()
plt.grid(alpha=0.3)
plt.show()

寄与率とは

寄与率は、各主成分がデータの分散(情報量)をどれだけ説明しているかを示します。累積寄与率が 80〜95% 以上になる次元数を選ぶことが多いです。

9.4.5 t-SNE

PCAは線形な次元削減手法ですが、データの非線形な構造を捉えたい場合は t-SNE(t-distributed Stochastic Neighbor Embedding)が有効です。 t-SNEは、高次元空間で近いデータ点を低次元空間でも近くに配置するように変換する手法で、特に可視化に優れています。 たとえば、同心円状の2つのグループからなるデータは、線形変換であるPCAでは分離できませんが、t-SNEでは近傍関係をもとにまとまりが見える場合があります。ただし、結果はパラメータや乱数によって変わり、必ず2つに分かれるとは限りません。図上のクラスタ間の距離や大きさは、元の空間での距離や大きさをそのまま表すわけではありません。

それでは、PCAとt-SNEを同じデータに適用して、結果を比較してみましょう。 ここまで使ってきたIrisデータセットは4次元で、品種もほぼ線形に分かれるため、PCAでも十分きれいな図が得られ、両者の差が出にくいという事情があります。 そこでここでは、より高次元な Digitsデータセット を使います。Digitsはscikit-learnに付属する手書き数字のデータセットで、0〜9の数字を書いた8×8画素の画像1,797枚を、1枚あたり64次元のベクトルとして収録しています。 コード中の perplexity は、各データ点の近傍とみなすデータ点数の目安を決めるパラメータで、データ数より十分小さい値を指定します。

import matplotlib.pyplot as plt
from sklearn.datasets import load_digits
from sklearn.decomposition import PCA
from sklearn.manifold import TSNE

# 手書き数字データセット(64次元・10クラス)を読み込む
digits = load_digits()
X_digits, y_digits = digits.data, digits.target

# PCAとt-SNEでそれぞれ2次元に削減
X_pca_digits = PCA(n_components=2).fit_transform(X_digits)
tsne = TSNE(n_components=2, random_state=0, perplexity=30)
X_tsne_digits = tsne.fit_transform(X_digits)

# 色を数字の種類(0〜9)に対応させて描画
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

axes[0].scatter(X_pca_digits[:, 0], X_pca_digits[:, 1],
                c=y_digits, cmap="tab10", s=15)
axes[0].set_title("PCA")
axes[0].set_xlabel("第1主成分")
axes[0].set_ylabel("第2主成分")

axes[1].scatter(X_tsne_digits[:, 0], X_tsne_digits[:, 1],
                c=y_digits, cmap="tab10", s=15)
axes[1].set_title("t-SNE")
axes[1].set_xlabel("成分1")
axes[1].set_ylabel("成分2")

plt.tight_layout()
plt.show()

散布図の色は数字の種類(0〜9)を表します。PCAの結果では多くの数字が重なり合っているのに対し、t-SNEの結果では数字ごとのまとまりがはっきりと分かれています。 ただし、t-SNEにはいくつかの注意点があります。

  • 計算コストがPCAよりも高い
  • perplexity パラメータの設定によって結果が変わる
  • 実行するたびに結果が変わる(random_state で固定可能)
  • 主に可視化に用いるもので、新しいデータの変換には適さない

PCAとt-SNEの特徴を以下の表にまとめます。

PCA t-SNE
変換の種類 線形 非線形
計算速度 高速 低速
用途 次元削減・可視化 主に可視化
新しいデータへの適用 可能(transform()) 困難
結果の解釈性 高い(寄与率が計算可能) 低い

9.5 演習問題

演習 9-1: k-means法と最適クラスタ数

演習 9-1

iris データセット(アヤメの花の計測データ、150 件・4 つの数値特徴量・3 品種ラベル)を使って、以下のタスクを実行してください。

タスク:

  1. 4 つの数値特徴量で k-means法を実行する(k=3)
  2. エルボー法で最適なクラスタ数を探索する(k=1〜10)
  3. シルエットスコアで評価する(k=2〜10)
  4. 実際の品種ラベル(species)とクラスタリング結果をクロス集計で比較する
  5. エルボー法とシルエット分析の結果が一致するか確認し、その理由を考察する
テンプレートコード
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")

# 1. k=3 で k-means法を実行

# 2. エルボー法(k=1〜10 のイナーシャをプロット)

# 3. シルエットスコア(k=2〜10)

# 4. 実ラベルとの比較(クロス集計)
解答例
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: 人工データのクラスタリング

演習 9-2

make_blobs() 関数で以下のように 5 つのクラスタを持つデータを生成し、k-means法でクラスタリングしてください。エルボー法で最適なクラスタ数が 5 と判定できるか確認してください。

X, y = make_blobs(n_samples=500, centers=5,
                  cluster_std=1.0, random_state=42)
解答例
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: 階層的クラスタリングとデンドログラム

演習 9-3

iris データセットに対して階層的クラスタリング(Ward法)を適用し、デンドログラムを描いてください。デンドログラムから適切なクラスタ数を読み取り、その結果が真の品種数と一致するか確認してください。

解答例
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))
各クラスタのデータ数: [50 36 64]
species  setosa  versicolor  virginica
row_0
1            50           0          0
2             0           1         35
3             0          49         15

実行すると、データ点が距離の近い順に結合されていく様子を示すデンドログラムが表示されます。

考察:デンドログラムは、結合距離が最も大きいところで2つの枝に分かれ、次に片方の枝が2つに分かれます。ここから2〜3クラスタが妥当と読めます。3クラスタに分けると、setosa の50件は1つのクラスタにまとまります。versicolor と virginica は分布が近く、計16件(1件と15件)が相手のクラスタに入ります。境界付近がうまく分かれないのは、演習 9-1 のk-means法と同じです。

演習 9-4: PCA による可視化

演習 9-4

penguins データセット(パルマー諸島のペンギンの計測データ、3 種類のペンギンに対する数値特徴量)を PCA で 2 次元に削減し、可視化してください。

タスク:

  1. penguins データを読み込み、欠損値を削除する
  2. 数値特徴量を標準化する(StandardScaler クラス)
  3. PCA で 2 次元に削減する
  4. 種類(species)ごとに色分けして散布図を作成する
  5. 各主成分の寄与率を確認する
テンプレートコード
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()

# 2. 数値特徴量を標準化

# 3. PCA で 2 次元に削減

# 4. 種類ごとに色分けして散布図

# 5. 各主成分の寄与率を表示
解答例
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}")
寄与率: [0.6863 0.1945]
累積寄与率: 0.8809

実行すると、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 の比較

演習 9-5

make_blobs() 関数で 10 次元のデータを生成し(n_features=10 を指定)、PCA と t-SNE それぞれで 2 次元に削減して可視化してください。結果の違いを考察してください。

解答例
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の寄与率: [0.6628 0.1366]
累積寄与率: 0.7994

実行すると、PCA と t-SNE で2次元化した散布図が並んで表示されます。

考察:どちらの手法でも5つのクラスタが見えますが、見え方が違います。PCA は元の10次元での距離関係をなるべく保つので、クラスタ同士の遠近は元データを反映します。ただし2次元で説明できる分散は約 80% で、近くに表示されるクラスタもあります。t-SNE は近くの点どうしの関係を重視するので、各クラスタがはっきり離れたかたまりになります。その代わり、クラスタ間の距離や配置にはあまり意味がなく、random_state を変えると配置も変わります。

まとめ

本章では、教師なし学習について学びました。

第9章のまとめ

教師なし学習

  • 正解ラベルなしでデータの構造を発見
  • クラスタリングと次元削減が代表的

k-means法

  • 基本的なクラスタリング手法
  • クラスタ数 k を事前に指定
  • エルボー法で最適な k を探索

階層的クラスタリング

  • デンドログラムでクラスタ関係を可視化
  • デンドログラムを確認してから、クラスタ数または距離のしきい値を決められる

シルエット分析

  • クラスタリングの品質を評価
  • -1〜1 の範囲で、1 に近いほど良い

PCA(主成分分析)

  • 代表的な次元削減手法
  • 分散が最大になる方向を抽出
  • 累積寄与率で次元数を決定

t-SNE

  • 非線形な次元削減手法
  • 高次元データの可視化に特に有効