見出し画像

scikit-learn機械学習㉜DBSCAN実践編

前回は、DBSCANの仕組みを解説しました。

今回は、scikit-learn の DBSCAN を使って、以下のデータでクラスタリングの実験を行います。

また、エルボー法やシルエット係数によるパラメータの最適化を実演します。

どんな結果になるでしょうか。さっそく始めましょう。


Python環境の準備

Pythonの仮想環境を作ってscikit-learnとJupyterなど必要なライブラリをインストールします。

# 作業フォルダを作って、そこへ移動する
mkdir dbscan_test
cd dbscan_test

# 仮想環境を作り、アクティベートする
python3 -m venv venv
source venv/bin/activate

# pip をアップグレードしておく
pip install --upgrade pip

# 必要なライブラリをインストール
pip install numpy scikit-learn jupyter matplotlib 

Jupyterノートブックを立ち上げてPython3のノートブックを作成してください。Jupyterノートブックに関しては、こちらを参照してください。

なお、ノートブックの閲覧編集にはVSCode(Visual Studio Code)も使えます。私は、どちらかというとVSCodeをよく使います。これについてもこちらで簡単に解説しています。

DBSCAN実験1

データの準備

まずは、クラスタリングをするためのデータを生成します。

from sklearn.datasets import make_moons

# 月形のデータセットを生成
X, y = make_moons(n_samples=200, noise=0.08, random_state=42)

これはこれまで何度も使っている手法ですが、月型の2Dデータセットを生成します。以下のファンクションを使って表示します。

import matplotlib.pyplot as plt

x1 = X[:, 0]
x2 = X[:, 1]

plt.plot(x1, x2, 'o', markerfacecolor='none', markeredgecolor='k', markersize=6)
plt.title("Initial Data Points (No Clustering)")
plt.xlabel("Feature 1")
plt.ylabel("Feature 2")
plt.show()

パラメータ設定1

from sklearn.cluster import DBSCAN

# DBSCANモデルを作成
dbscan = DBSCAN(min_samples=5, eps=0.3)

k-meansとは違ってクラスタ数を指定しません。

  • min_samples:コアポイントの近傍内の必要サンプル数

  • eps:2つのデータポイントが近傍にあるとみなされる最大距離

DBSCANは、epsで指定された距離を基に、コアポイントやボーダーポイントを判定します。ただし、クラスタ内の全ての距離がeps以下になるわけではありません。

上図の青い破線で示された円が eps の範囲を表し、この範囲内に min_samples 以上のデータポイントがあれば、⊕とマークされた赤い点はコアポイントとして判定されます。この図では、⊕はコアポイントではないとしています。

ただし、⊕の近傍に1つのコアポイントが含まれており、⊕はボーダーポイントとして認識されます。⊕の近傍にある◯は、その近くに他のクラスタのコアポイントがないならばノイズとして認識されます。

よって、eps は k-means におけるセントロイドからの距離とは異なる概念です。

クラスタリングの実行

クラスタリングを実行します。

# データをクラスタリング
labels = dbscan.fit_predict(X)

dbscan.fit_predict(X) は、DBSCANのロジックを使ってデータをクラスタに分け、それぞれのデータポイントのクラスタ番号(ラベル)をリスト(NumPyの配列)にして返します。

labels
2つのクラスタに分類されている

結果として2つのクラスタが判別されているのがわかります。

なお、dbscan.fit(X)とすると、クラスタの計算だけ行います。その場合、クラスタ番号のリストを取得するには、dbscan.labels_ を参照します。

dbscan.labels_

クラスターを色分け表示

クラスタリングの結果に従ってデータポイントを色分けします。次のファンクションを準備しました。

import numpy as np
import matplotlib.pyplot as plt

# データセットをプロット
def plot_dbscan(dbscan, X):
    # DBSCANでクラスタリングしたラベルを取得
    labels = dbscan.labels_
    unique_labels = set(labels)

    # 各ポイントの種類を判定
    core_samples_mask = np.zeros_like(labels, dtype=bool)
    core_samples_mask[dbscan.core_sample_indices_] = True  # コアポイント

    # プロット
    plt.figure(figsize=(8, 6))

    # カラーマップで色分け
    colors = [plt.cm.Spectral(each) 
              for each in np.linspace(0, 1, len(unique_labels))]

    for k, col in zip(unique_labels, colors):
        # クラスタのメンバーを選ぶ
        class_member_mask = (labels == k)

        if k >= 0:
            # コアポイントをプロット
            xy = X[class_member_mask & core_samples_mask]
            if len(xy) > 0:
                plt.plot(xy[:, 0], xy[:, 1], 'o', markerfacecolor=tuple(col),
                         markeredgecolor='k', markersize=6, 
                         label=f'Cluster {k} (Core)')

            # ボーダーポイントをプロット
            xy = X[class_member_mask & ~core_samples_mask]
            if len(xy) > 0:
                plt.plot(xy[:, 0], xy[:, 1], 'o', markerfacecolor=tuple(col),
                         markeredgecolor='k', markersize=6, alpha=0.5, 
                         label=f'Cluster {k} (Border)')
        else:
            # ノイズをプロット
            xy = X[class_member_mask]
            plt.plot(xy[:, 0], xy[:, 1], 'o', markerfacecolor='none',
                    markeredgecolor='k', markersize=6, 
                    label=f'Noise')

    # タイトルを設定
    title = f"DBSCAN: min_samples={dbscan.min_samples}, eps={dbscan.eps}"
    plt.title(title)
    plt.xlabel("Feature 1")
    plt.ylabel("Feature 2")
    plt.legend(loc='upper right', fontsize='small', markerscale=0.8)
    plt.show()

これは、センターポイント、ボーダーポイント、ノイズを色分けして表示します。実際にやってみましょう。

# データセットをプロット
plot_dbscan(dbscan, X)

色が薄くなっているのがボーダーポイントです。今回は1つだけあるのがわかります。

パラメータ設定2

DBSCANの設定を変えて結果を見てみましょう。min_samples=10 にします。

# DBSCANモデルを作成
dbscan = DBSCAN(eps=0.3, min_samples=10)

# データをクラスタリング
labels = dbscan.fit_predict(X)

# データセットをプロット
plot_dbscan(dbscan, X)

クラスタの数は2と同じですが、ボーダーポイントがはじの方に増えてたのが確認できます。これらのポイントの近傍にはコアポイントとして認識されるために必要な数のデータポイントがないというわけです。

パラメータ設定3

さらに、esp=0.2 にしてみます。

# DBSCANモデルを作成
dbscan = DBSCAN(eps=0.2, min_samples=10)

# データをクラスタリング
labels = dbscan.fit_predict(X)

# データセットをプロット
plot_dbscan(dbscan, X)

クラスタの数が3つに増えました。上部の月形が微妙に2つ分かれているのがわかります。また、ボーダーポイントも増えています。

さらに、ノイズ(◯)も確認できます。

以上、3つの設定でDBSCANを実践しましたが、パラメータにより大きく結果が異なるのがわかります。「DBSCANは k-means よりも使い方が難しい」と感じさせます。

ただし、DBSCANは k-means では正確に処理できなかった月形のクラスタリングを(設定によっては)正しく行えることも事実です。

しかし、より複雑なデータだとそうも簡単にはいきません。

DBSCAN実験2

今度はいくつかのデータセットを混合させてみます。

from sklearn.datasets import make_blobs, make_circles, make_moons

# ランダムなデータセット
X_blobs, _ = make_blobs(n_samples=100, centers=[[-1, -1], [1, 1]], 
                        cluster_std=0.2, random_state=42)

# 月形のデータセット
X_moons, _ = make_moons(n_samples=100, noise=0.05, 
                        random_state=42)

# 円形のデータセット
X_circles, _ = make_circles(n_samples=100, factor=0.5, 
                            noise=0.05, random_state=42)

# Combine all shapes into one dataset
X = np.vstack((X_blobs, X_moons, X_circles))

この複雑なデータセットを表示してみます。

# 2つの特徴量に分ける
x1 = X[:, 0]
x2 = X[:, 1]

# データセットをプロット
plt.figure(figsize=(8, 6))
plt.scatter(x1, x2, s=10, color='none', edgecolor='k')
plt.title("Complex Shapes Dataset for DBSCAN")
plt.xlabel("Feature 1")
plt.ylabel("Feature 2")
plt.show()

これは、DBSCANでクラスタリングしたらどうなるでしょうか。

パラメータ設定1

まずはこの設定で試してみます。

# DBSCANモデルを作成
dbscan = DBSCAN(eps=0.3, min_samples=5)

# データをクラスタリング
labels = dbscan.fit_predict(X)

# データセットをプロット
plot_dbscan(dbscan, X)

たった一つのクラスタに全てが収まってしまいました。

パラメータ設定2

eps を少し小さくしてクラスタが分かれるように仕向けてみます。

# DBSCANモデルを作成
dbscan = DBSCAN(eps=0.25, min_samples=5)

# データをクラスタリング
labels = dbscan.fit_predict(X)

# データセットをプロット
plot_dbscan(dbscan, X)

2つのクラスタに分割されましたが、ノイズとしてどうなのかという判別も増えました。

パラメータ設定3

もっと、eps を少し小さくしてクラスタがさらに分かれるように仕向けてみます。おそらくノイズは増えるでしょう。

# DBSCANモデルを作成
dbscan = DBSCAN(eps=0.2, min_samples=5)

# データをクラスタリング
labels = dbscan.fit_predict(X)

# データセットをプロット
plot_dbscan(dbscan, X)

3つのクラスタに分かれましたが、予想通りノイズも増えました。DBSCANは密度(繋がり)をみているので致し方ないですが。

最適なパラメータの探索

k-距離プロットによるエルボー法

以前に教師あり学習モデルのk-近傍アルゴリズムを紹介しましたが、その教師なしバージョンとも言える NearestNeighbors を使って最適な eps を決めてみます。

# k近傍法モデルを作成
neighbors = NearestNeighbors(n_neighbors=5)

# データをフィット
neighbors.fit(X)

# k近傍距離とインデックスを取得
distances, indices = neighbors.kneighbors(X)

これは各データポイントに対して最も近い5つのデータポイントの距離とそのインデックスをリストにして返しています。これもラベルを必要としない教師なし学習モデルです。

distances を表示します。

distances

NumPyの2次元配列になっています。

distances.shape

これは300個ある各データポイントに対して5つの近傍のデータポイントの距離を意味します。最初のデータポイントは自分自身であり、その距離は0です。

また、indices からはデータポイントのインデックスがわかります。

indices

では、この各データポイントの最後の近傍データポイントまでの距離を値が上昇する順番でソートし表示してみます。

import matplotlib.pyplot as plt
import numpy as np

# 各点のk番目の距離をソートしてプロット
def plot_k_distance(distances):
    # k番目(最後)の近傍データポイントの距離を取得しソート
    distances = np.sort(distances[:, -1])

    # プロット
    plt.figure(figsize=(8, 6))
    plt.plot(distances)
    plt.xlabel("Points sorted by distance")
    plt.ylabel(f"{len(distances)}-th Nearest Neighbor Distance")
    plt.title("k-Distance Plot")
    plt.show()

# k番目の距離をプロット
plot_k_distance(distances)

この図の左の方は距離が緩やかに増加しています。これは、密度の高いエリアではポイントが近接しているためです。そして、右へ進んでいくと、あるデータポイントを境に距離が急激に増加します。これは、密度の低いエリアやノイズに対応した反応です。

このようにグラフの曲線が急に変化し、傾きが大きくなる点はエルボー(ひじ)と呼ばれます。エルボーの位置は、密度の高いクラスタとそれ以外の領域を分ける境界として機能します。この例では、エルボーの位置は0.2付近にあると推測されます。

クラスタにはノイズを多く含めたくないのでエルボーの前後で eps の値を調節すると良いのがわかります。実はこの設定をもとに前述の「DBSCAN実験2、パラメータ設定3」を決めていました。

なお、このエルボー法は k-means の実践編でも使いました。

シルエット係数(スコア)を使う

シルエット係数は、クラスタリングの質を測る指標です。同じクラスタ内のデータポイント間の凝集度と異なるクラスタとの分離度を比較し、クラスタリングの質を測定する指標です。

つまり、クラスタ内のデータがより密に集まっていて、他のクラスタからより離れているほとクラスタリングがうまくいったと捉えます。その値の範囲は$${−1}$$から$${+1}$$で、大きいほど質が良いことになります。

なお、シルエット係数も k-means の実践編で使ったものと同じ概念です。

次のファンクションは、指定された eps の範囲と min_samples の範囲から全てのシルエット係数を計算して上位(n_top)のものを表示します。

from sklearn.metrics import silhouette_score
from sklearn.cluster import DBSCAN

def find_best_dbscan_params(X, eps_range, min_samples_range, top_n=5):
    results = []
    
    for eps in eps_range:
        for min_samples in min_samples_range:
            # DBSCANモデルを作成
            dbscan = DBSCAN(eps=eps, min_samples=min_samples)
            
            # データをクラスタリング
            labels = dbscan.fit_predict(X)
            
            # クラスタの数とノイズポイントの数を計算
            unique_labels = set(labels)
            num_noise = list(labels).count(-1)
            
            # クラスタが2つ以上の場合にのみスコアを計算
            if len(unique_labels) > 1:
                score = silhouette_score(X, labels)

                # 結果を保存
                results.append((score, eps, min_samples, num_noise))
    
    # 上位 top_n の結果を取得(スコアが高い順)
    top_results = sorted(results, key=lambda x: x[0], reverse=True)[:top_n]
    
    # 結果を表示
    print(f"Top {top_n} DBSCAN Results:")
    for rank, (score, eps, min_samples, num_noise) in enumerate(top_results, 1):
        print(f"{rank}. Silhouette Score: {score:.4f}, eps: {eps:.2f}, min_samples: {min_samples}, noise points: {num_noise}")
    
    return top_results

これを試すと以下の結果が出ました。

# パラメータ範囲を指定して実行
eps_range = np.arange(0.05, 0.35, 0.01)
min_samples_range = range(3, 10)

top_results = find_best_dbscan_params(X, eps_range, min_samples_range, top_n=7)

この結果ではスコア、eps、min_samples、ノイズの数を表示しています。上位4つは同じスコアとノイズの数でした。その中から eps=0.25、min_samples=5 を選んだのが「DBSCAN実験2、パラメータ設定2」です。


以上、2つのパラメータ探索手法を試してみました。

ある程度は効果があると言えますが、DBSCANの密度に基づくクラスタリングにも限界があるのがわかります。複雑な形状のクラスタを扱えますが、データが重なり合う場合や類似度が複雑なケースには弱いことが確認できました。

次回予告

今回の結果を見ると「複雑なデータセットをクラスタリングする場合でもクラスタ数を指定できたら」と考えてしまいます。もちろん、k-means ではうまくいかないことはわかっています。さて、どうしたものか。

そこで次回は、クラスタ数を指定できるのに複雑な形状にも対応できるSpectral Clustering(スペクトラル・クラスタリング)を紹介します。

お楽しみに!

いいなと思ったら応援しよう!