自己組織化マップ (SOM) 入門: 高次元データ可視化とクラスタリング

アルゴリズム概要

自己組織化マップ (Self-Organizing Maps, SOM) は、フィンランドの学者テウヴォ・コホネンが1982年に提唱した教師なし学習型の人工ニューラルネットワークです。SOMは、高次元の入力データを低次元(一般的には2次元)空間にマッピングしつつ、元のデータの位相構造を維持する能力を持ちます。この特性により、データ可視化やクラスタリング分析における強力なツールとなっています。

SOMネットワークの主な目的は、データ間の位相関係を保ちながら次元削減を実現することです。これは、元の空間で隣接していたデータポイントが、マッピングされた低次元空間でも隣接関係を維持することを意味します。この特徴により、SOMは複雑なデータセットの内部構造を探索し、理解するための理想的な手法となります。

アルゴリズムの主な特徴

  • 次元削減機能:
    • 高次元データを低次元空間へ効果的にマッピングします。
    • データの主要な特徴と構造を保持します。
    • データ可視化や分析を容易にします。
    • データ処理の複雑さを軽減します。
  • 位相構造の維持:
    • 入力データの空間的関係性を保ちます。
    • 類似した入力データは、ネットワーク上の隣接するニューロンにマッピングされます。
    • データの内在的な構造を発見するのに役立ちます。
    • データの局所的な特性を維持します。
  • 自己組織化学習:
    • 競合学習メカニズムを通じて自動的に構造を形成します。
    • ネットワークが自動的に重みを調整します。
    • 人間の介入を必要としない学習プロセスです。
    • 優れた自己適応能力を持っています。
  • 教師なし学習:
    • ラベル付きデータを必要としません。
    • データそのものから特徴を学習できます。
    • 探索的データ分析に適しています。
    • データ内に隠されたパターンを発見できます。
  • 高い可視化能力:
    • 直感的なデータ分布の可視化を提供します。
    • データ構造の理解を促進します。
    • インタラクティブなデータ探索をサポートします。
    • データ分析と意思決定に貢献します。

自己組織化マップの数理

SOMネットワークの数理的原理は、以下の主要な公式を通じて理解できます。

  1. 距離の計算:
    入力ベクトル x と重みベクトル w 間の距離をユークリッド距離で計算します。
    D(x, w) = √(Σ(xᵢ - wᵢ)²)
    ここで:
    • x は入力ベクトル
    • w は重みベクトル
    • i はベクトルの次元インデックス
  2. 最良一致ユニット (BMU) の選択:
    距離が最小となるニューロンをBMUとして選択します。
    BMU = argmin(D(x, w))
  3. 近傍関数:
    ガウス近傍関数を用いて、各ニューロンへの影響度を計算します。
    h(t) = exp(-||rᵢ - rₖ||² / (2σ(t)²))
    ここで:
    • rᵢ は現在のニューロンの位置
    • rₖ はBMUの位置
    • σ(t) は時刻 t における近傍半径
  4. 学習率の減衰:
    学習率は時間と共に指数関数的に減衰します。
    α(t) = α₀ * exp(-t/T)
    ここで:
    • α₀ は初期学習率
    • t は現在のイテレーション回数
    • T は総イテレーション回数
  5. 重みの更新規則:
    BMUとその近傍ニューロンの重みを更新します。
    w(t+1) = w(t) + α(t) * h(t) * (x - w(t))

アルゴリズムの動作原理

SOMアルゴリズムの核となる原理は、以下のステップで構成されます。

  1. 初期化:
    • 各ノードがニューロンを表す2次元グリッドを作成します。
    • 各ニューロンの重みベクトルをランダムに初期化します。
    • 初期学習率と初期近傍半径を設定します。
  2. 競合プロセス:
    • 各入力サンプルに対し、全てのニューロンの重みベクトルとの距離を計算します。
    • 距離が最小のニューロンを特定し、これを最良一致ユニット (BMU) とします。
    • このプロセスにより、ネットワークの自己組織化特性が実現されます。
  3. 協力プロセス:
    • BMUの近傍範囲を決定します。
    • 近傍内の全てのニューロンについて近傍関数値を計算します。
    • 近傍範囲は訓練の進行と共に徐々に縮小します。
  4. 適応プロセス:
    • BMUおよびその近傍内のニューロンの重みを更新します。
    • 重み更新式は以下で与えられます:
      W(t+1) = W(t) + α(t) * h(t) * (X - W(t))
      ここで:
      • W(t) は時刻 t における重み
      • α(t) は学習率
      • h(t) は近傍関数
      • X は入力サンプル

環境準備

本チュートリアルでは、以下のPythonライブラリが必要です。

import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler
import seaborn as sns
# from matplotlib.font_manager import FontProperties # 日本語表示用のフォント設定はここでは省略します

コード実装

SOMクラスの定義

まず、SOMネットワークの主要なクラスを実装します。

class KohonenSOM:
    def __init__(self, grid_dimensions=(10, 10), feature_count=2, initial_learning_rate=0.1, initial_radius=1.0, total_epochs=1000):
        """
        自己組織化マップ (SOM) を初期化します。
        
        Args:
            grid_dimensions (tuple): 出力グリッドのサイズ (例: (行数, 列数))。
            feature_count (int): 入力データの次元数。
            initial_learning_rate (float): 初期学習率。
            initial_radius (float): 初期近傍半径(シグマ)。
            total_epochs (int): 全訓練エポック数。
        """
        self.grid_dimensions = grid_dimensions
        self.feature_count = feature_count
        self.learning_rate_start = initial_learning_rate
        self.neighborhood_radius_start = initial_radius
        self.total_epochs = total_epochs
        
        # 重みベクトルをランダムに初期化
        self.weight_vectors = np.random.randn(grid_dimensions[0], grid_dimensions[1], feature_count)
        
        # グリッド上の各ニューロンの論理座標
        self.neuron_coordinates = np.array([(row, col) for row in range(grid_dimensions[0]) 
                                            for col in range(grid_dimensions[1])]).reshape(grid_dimensions[0], grid_dimensions[1], 2)

    def find_bmu(self, input_vector):
        """最良一致ユニット (BMU) を見つけます。"""
        squared_distances = np.sum((self.weight_vectors - input_vector) ** 2, axis=2)
        return np.unravel_index(np.argmin(squared_distances), self.grid_dimensions)

    def neighborhood_influence(self, bmu_coordinates, current_epoch):
        """近傍関数を計算し、近傍ニューロンへの影響度を決定します。"""
        current_radius = self.neighborhood_radius_start * np.exp(-current_epoch / self.total_epochs)
        grid_distances = np.sum((self.neuron_coordinates - np.array(bmu_coordinates)) ** 2, axis=2)
        return np.exp(-grid_distances / (2 * current_radius ** 2))

    def visualize_map(self, input_data, current_epoch):
        """
        SOMの現在の状態を可視化します。(ここでは簡略化された表示のみ)
        実際の使用では、例えば重みベクトルの分布やBMUマッピングを表示します。
        """
        if current_epoch == 0:
            print(f"エポック {current_epoch}: SOMの初期状態を可視化しています...")
        elif current_epoch % (self.total_epochs // 10) == 0: # 例として10%ごとに表示
            print(f"エポック {current_epoch}: SOMの学習途中状態を可視化しています...")
        # 実際の可視化ロジックはここに追加されます (例: matplotlibで重みベクトルをプロット)
        # plt.figure(figsize=(6, 6))
        # plt.pcolormesh(np.linalg.norm(self.weight_vectors, axis=2)) # 重みベクトルのノルムで色付け
        # plt.title(f'SOM Weight Map at Epoch {current_epoch}')
        # plt.colorbar()
        # plt.show()
    
    def train(self, training_data):
        """SOMネットワークを訓練します。"""
        quantization_error_log = []
        
        for epoch_step in range(self.total_epochs):
            current_error_sum = 0
            current_learning_rate = self.learning_rate_start * np.exp(-epoch_step / self.total_epochs)
            
            # ランダムに1つのサンプルを選択
            sample_vector = training_data[np.random.randint(len(training_data))]
            
            # BMUを見つける
            bmu_coords = self.find_bmu(sample_vector)
            
            # 近傍関数を計算
            influence_values = self.neighborhood_influence(bmu_coords, epoch_step)
            
            # 重みを更新
            for r_idx in range(self.grid_dimensions[0]):
                for c_idx in range(self.grid_dimensions[1]):
                    weight_update_vector = current_learning_rate * influence_values[r_idx, c_idx] * (sample_vector - self.weight_vectors[r_idx, c_idx])
                    self.weight_vectors[r_idx, c_idx] += weight_update_vector
                    current_error_sum += np.sum(weight_update_vector ** 2)
            
            quantization_error_log.append(current_error_sum)
            
            # 特定のエポックで可視化結果を保存
            if epoch_step % 100 == 0 or epoch_step == self.total_epochs - 1:
                self.visualize_map(training_data, epoch_step)
        
        return quantization_error_log

サンプルデータの生成と前処理

SOMの動作を実演するために、二つの環状分布を持つサンプルデータを生成します。

def generate_ring_data(num_samples=1000):
    """二つの環状分布を持つサンプルデータを生成します。"""
    # 最初の環
    ring1_radius = np.random.normal(2, 0.2, num_samples // 2)
    ring1_angle = np.random.uniform(0, 2 * np.pi, num_samples // 2)
    ring1_x = ring1_radius * np.cos(ring1_angle)
    ring1_y = ring1_radius * np.sin(ring1_angle)
    
    # 二番目の環
    ring2_radius = np.random.normal(4, 0.2, num_samples // 2)
    ring2_angle = np.random.uniform(0, 2 * np.pi, num_samples // 2)
    ring2_x = ring2_radius * np.cos(ring2_angle)
    ring2_y = ring2_radius * np.sin(ring2_angle)
    
    # データを結合
    raw_data = np.vstack([np.column_stack([ring1_x, ring1_y]), np.column_stack([ring2_x, ring2_y])])
    
    # データを標準化
    scaler = StandardScaler()
    normalized_data = scaler.fit_transform(raw_data)
    
    return normalized_data

# 使用例
if __name__ == "__main__":
    sample_data = generate_ring_data(num_samples=1000)
    
    som_instance = KohonenSOM(grid_dimensions=(10, 10), feature_count=2, 
                            initial_learning_rate=0.1, initial_radius=1.0, total_epochs=1000)
    
    error_history = som_instance.train(sample_data)

    # 誤差収束の可視化
    plt.figure(figsize=(10, 6))
    plt.plot(error_history)
    plt.title('SOM Training Error Convergence')
    plt.xlabel('Epoch')
    plt.ylabel('Quantization Error')
    plt.grid(True)
    plt.show()

    # 最終的な重みベクトルの分布を可視化 (簡略版)
    # ここでは、重みベクトルがどのように入力データに適合したかを示すために、
    # 各ニューロンの重みベクトルの位置をプロットします。
    plt.figure(figsize=(8, 8))
    plt.scatter(sample_data[:, 0], sample_data[:, 1], alpha=0.3, label='Original Data')
    plt.scatter(som_instance.weight_vectors[:, :, 0].flatten(), 
                som_instance.weight_vectors[:, :, 1].flatten(), 
                color='red', marker='x', s=100, label='SOM Weights')
    
    # ニューロン間の接続を可視化(グリッド構造)
    for r in range(som_instance.grid_dimensions[0]):
        plt.plot(som_instance.weight_vectors[r, :, 0], som_instance.weight_vectors[r, :, 1], 'k-', linewidth=0.5)
    for c in range(som_instance.grid_dimensions[1]):
        plt.plot(som_instance.weight_vectors[:, c, 0], som_instance.weight_vectors[:, c, 1], 'k-', linewidth=0.5)

    plt.title('Final SOM Weight Distribution vs. Original Data')
    plt.xlabel('Feature 1 (Standardized)')
    plt.ylabel('Feature 2 (Standardized)')
    plt.legend()
    plt.grid(True)
    plt.show()

実行結果の分析

訓練過程の可視化

SOMネットワークの訓練過程は、ネットワークがデータ分布にどのように段階的に適応していくかを示します。上記のコードでは簡略化された可視化ですが、実際の訓練では以下の段階でマップが形成されます。

  1. 初期段階(0エポック):
    • ネットワークの重みはランダムに分布しています。
    • グリッド構造はまだ形成されていません。
    • データ分布とは無関係な状態です。
  2. 中期段階(例: 500エポック):
    • グリッドがデータ分布に適応し始めます。
    • 重みベクトルはデータが密集する領域へと移動します。
    • 大まかなグリッド構造は維持されます。
  3. 最終段階(例: 900エポック):
    • グリッドはデータ分布に完全に適応します。
    • データの環状構造などが明確に表現されます。
    • 良好な位相関係が保たれます。

誤差収束の分析

誤差収束曲線からは、以下の特徴が観察されます。

  • 初期には誤差が大きく、学習速度が速いです。
  • 中期には誤差が徐々に減少し、学習が安定してきます。
  • 後期には誤差が平坦になり、ネットワークが収束したことを示します。

よくある課題と解決策

  1. グリッドサイズ選択の問題
    • 問題: グリッドが大きすぎると過学習に、小さすぎると過少適合につながる可能性があります。
    • 解決策:
      • データ量と次元数に基づいて適切なグリッドサイズを選択します。
      • 交差検定を用いて最適なグリッドサイズを選択することも可能です。
      • 一般的には、グリッドノード数をサンプル数の5〜10%とすることが推奨されます。
  2. 学習率設定の問題
    • 問題: 学習率が大きすぎると不安定になり、小さすぎると収束が遅くなります。
    • 解決策:
      • 初期学習率は0.1〜0.5の範囲で設定します。
      • 指数関数的に減衰する学習率調整戦略を採用します。
      • 自己適応型学習率を使用することも検討します。
  3. 訓練時間の問題
    • 問題: 大規模データセットでは訓練に時間がかかります。
    • 解決策:
      • ミニバッチ訓練戦略を導入します。
      • 並列計算を活用します。
      • 距離計算方法の最適化を検討します。
  4. 初期化への感度
    • 問題: 異なる初期化が異なる結果を招くことがあります。
    • 解決策:
      • PCA(主成分分析)を用いた初期化を試みます。
      • 複数回実行し、最も良い結果を採用します。
      • 決定論的な初期化方法を検討します。

主要な応用分野

SOMネットワークは、様々な実際の応用において広く利用されています。

  1. データ可視化:
    • 高次元データの2次元表現。
    • 複雑なデータ関係の直感的な表現。
    • データ分布の特徴探索。
  2. パターン認識:
    • 画像セグメンテーション。
    • 音声認識。
    • テキスト分類。
  3. データマイニング:
    • 顧客セグメンテーション。
    • 異常検出。
    • 特徴抽出。
  4. バイオインフォマティクス:
    • 遺伝子発現データ分析。
    • タンパク質配列分類。
    • 疾患診断。
  5. 金融分析:
    • 市場セグメンテーション。
    • リスク評価。
    • ポートフォリオ分析。

まとめ

自己組織化マップは、データの次元削減と可視化を効果的に行うことができる強力な教師なし学習ツールです。本チュートリアルで示した例を通じて、SOMネットワークがランダムな初期状態からどのようにデータ分布の構造を段階的に学習し、適応していくかを実演しました。この自己組織化の特性により、SOMは様々な実世界の問題で優れた性能を発揮します。

実際のアプリケーションでは、以下の点に注意が必要です。

  1. グリッドサイズの選択は、具体的な問題とデータ規模に基づいて決定する必要があります。
  2. 学習率や近傍関数のパラメータは、最適な結果を得るために適切に調整する必要があります。
  3. データの前処理(例: 標準化)は、最終結果に重要な影響を与えます。
  4. ネットワークがデータの特徴を十分に学習できるよう、十分な訓練エポック数を確保することが重要です。

最適化の推奨事項

  1. データ前処理:
    • 特徴量の標準化または正規化。
    • 欠損値や外れ値の処理。
    • 効果的な特徴選択の検討。
  2. パラメータチューニング:
    • グリッドサーチやランダムサーチを用いた最適パラメータの探索。
    • 交差検定によるモデル性能の評価。
    • 特定の問題に応じてネットワーク構造を調整。
  3. 結果の評価:
    • 定量的な指標(例: 量子化誤差、トポロジカル誤差)を用いた評価。
    • 可視化による結果の分析。
    • 他のクラスタリングや次元削減手法との比較。
  4. 実応用への考慮事項:
    • 計算リソースの制約を考慮。
    • モデルの解釈可能性に注意。
    • 結果の実用性への着目。

タグ: 自己組織化マップ SOM コホネンネットワーク 教師なし学習 データ可視化

8月8日 13:15 投稿