自己組織化マップ PythonとNumPyによる自己組織化マップの理論と実装

このガイドでは、教師なし学習モデルである自己組織化マップ(SOM)と、そのPythonでの実装について見ていきます。

RGB Colorのサンプルを使ってSOMを学習させ、その性能と典型的な使用方法を示します。

自己組織化マップ(Self-Organizing Maps)。一般的な紹介

自己組織化マップは1982年にTeuvo Kohonenによって初めて紹介され、Kohonenマップとも呼ばれることがある。

自己組織化マップは人工ニューラルネットワークの一種であり、学習データからマップを構築する。

マップは一般に2Dの長方形グリッドの重みを持つが、3Dや高次元のモデルにも拡張できる。

また、六角形のような他のグリッド構造も可能である。

SOMは主にデータの可視化に用いられ、学習インスタンスの概要を素早く視覚的に表示することができる。

2次元の矩形格子では、各セルは重みベクトルによって表現される。

学習されたSOMでは、各セルの重みは数個の学習例の要約を表している。

近傍にあるセルは似たような重みを持ち、似たような例は互いに小さな近傍にあるセルにマッピングすることができる。

下図はSOMの構造を大まかに示したものである。

SOMは競合学習により学習される。

>
競合学習は教師なし学習の一種であり、構成要素が満足のいく結果を出すために競争し、1つだけが競争に勝てるようにする。

グリッドに学習例が入力されると、BMU(Best Matching Unit)が決定される(競争勝者)。

BMUとは、重みが学習例と最も近いセルのことである。

次に、BMUの重みとBMUに隣接するセルの重みは、入力された訓練事例に近づくように調整される。

SOM の学習方法には他にも有効なものがあるが、本ガイドでは最も一般的で広く使われている SOM の実装を紹介する。

SOMを学習するためのPythonルーチンをいくつか紹介しますので、使用するライブラリのいくつかをインポートしましょう。

import numpy as np
import matplotlib.pyplot as plt


SOM GRIDの初期化

SOM グリッドの重みはすべてランダムに初期化することができる.また、SOMグリッドの重みは、学習データセットからランダムに選ばれた例によって初期化することもできる。

>
> どちらを選ぶべきですか?
>
>
>

SOMはマップの初期重みに敏感なので、この選択はモデル全体に影響を及ぼします。

レスター大学・シベリア連邦大学のAyodejiとEvgenyが行った事例によると。

>
RI (Random Initialization) が PCI (Principal Component Initialization) よりも優れていることを、同じ条件下で比較したところ、RI は非線形データセットに対して非常に良い性能を示すことが確認されました。

>
>
しかし、準線形データセットでは、結果はまだ結論に至っていない。

一般に、本質的に非線形なデータセットでは、PCIの優位性に関する仮説は間違いであると結論付けることができる。

非線形データセットでは、ランダム初期化が非ランダム初期化より優れている。

準線形データセットでは、どのようなアプローチが一貫して勝利するのかがよくわからない。

これらの結果から、我々はランダムな初期化にこだわることにします。

ベストマッチングユニット(BMU)の発見

前述したように、ベストマッチング・ユニットとは、学習例xxxに最も近いSOM格子のセルのことである。

このユニットを見つける一つの方法は、グリッドの各セルの重みからxxxのユークリッド距離を計算することである。

>
> 距離が最小となるセルをBMUとして選択することができる。

ここで重要なのは、BMUを選択する方法はユークリッド距離だけではないことである。

別の距離測定や類似性メトリックを使用してBMUを決定することもでき、これを選択することは、主にあなたが特に構築しているデータやモデルに依存します。

BMUと隣接セルのウェイトベクトルの更新

学習例 xxx は SOM グリッドの様々なセルの重みを引き寄せることにより影響を与える。

BMUで最大の変化が起こり、SOMグリッドのBMUから離れるにつれてxxxの影響は小さくなる。

座標が (i,j)(i,j)(i,j) のセルに対して、その重み wijwijw_{ij} はエポック t+1t+1t+1 において、次のように更新される。

w(t+1)ij←w(t)ij+Δw(t)ijwij(t+1)←wij(t)+Δwij(t)
w_{ij}^{(t+1)} leftarrow w_{ij}^{(t)} + Delta w_{ij}^{(t)} ここで、Δw(t)ij←→Δw(t)ij+Δw(t)ij

ここで、Δw(t)ijΔwij(t) Delta w_{ij}^{(t)} は w(t)ijwij(t)w_{ij}^{(t)} に追加する変化量です。

として計算することができる。

Δw(t)ij=η(t)fi,j(g,h,σt)(x-w(t)ij) Δwij(t)=η(t)fi,j(g,h,σt)(x-wij(t))
Delta w_{ij}^{(t)} = eta^{(t)} f_{i,j}(g,h,sigma_t) (x-w_{ij}^{(t)})

この式では

  • ttt はエポック数
  • (g,h)(g,h)(g,h) はBMUの座標である。
  • ηηΘ は学習率
  • σtσt σσ_t is the radius
  • fij(g,h,σt)fij(g,h,σt)FT_{ij}(g,h, sigma_t) は近傍距離関数の値です。

以下では、このウェイトトレーニング式の詳細を紹介する。

学習率

学習率ηηΘは[0,1]の範囲の定数で、入力学習例に対するウエイトベクトルのステップサイズを決定する。

η=0η=0θ=0では重みに変化はなく、η=1η=1θ=1では重みベクトルwijwijw_{ij}はxxxの値をとる。

ηη Θは開始時に高く保たれ、エポックが進むにつれて減衰する。

学習段階での学習率を下げる方策として、指数関数的に減衰させる方法がある。

η(t)=η0e-t∗λη(t)=η0e-t∗λ
η(t)=η0e-t∗λ(t)=η0e-t∗λ.

ここで、λ<0λ<0 γlambda<0は減衰率である。

学習率が減衰率によってどのように変化するかを理解するために、初期の学習率を1として、様々なエポックに対して学習率をプロットしてみよう。

epochs = np.arange(0, 50)
lr_decay = [0.001, 0.1, 0.5, 0.99]
fig,ax = plt.subplots(nrows=1, ncols=4, figsize=(15,4))
plt_ind = np.arange(4) + 141
for decay, ind in zip(lr_decay, plt_ind):
    plt.subplot(ind)
    learn_rate = np.exp(-epochs * decay)
    plt.plot(epochs, learn_rate, c='cyan')
    plt.title('decay rate: ' + str(decay))
    plt.xlabel('epochs $t$')
    plt.ylabel('$eta^(t)$')
fig.subplots_adjust(hspace=0.5, wspace=0.3)
plt.show()


近傍距離関数

近傍距離関数は次式で与えられる。

fij(g,h,σt)=e-d((i,j),(g,h))22σ2tfij(g,h,σt)=e-d((i,j),(g,h))22σt2
f_{ij}(g,h,sigma_t) = e^frac{-d((i,j),(g,h))^2}{2sigma_t^2}

ここで、d((i,j),(g,h))はBMUの座標(g,h)(g,h)からあるセルの座標(i,j)の距離、σtσtはエポックtttでの半径である。

距離の計算には通常ユークリッド距離を用いるが、他の距離や類似度指標を用いてもよい。

BMUとそれ自身との距離は0であるため、BMUの重み変化は以下のようになる。

Δwgh=η(x-wgh)Δwgh=η(x-wgh)
Δwgh=η(x-wgh) Δwgh=η(x-wgh) Δdelta w_{gh} = eta (x-w___{gh})

BMUからの距離が大きいユニット(i,j)(i,j)(i,j)については、近傍距離関数がほぼゼロになり、ΔwijΔwijDelta w_{ij}の大きさが非常に小さくなる。

したがって、このようなユニットは、学習例xxxの影響を受けない。

したがって、1つの学習例は、BMUとBMUの近傍のセルにのみ影響を与える。

BMU から遠ざかるにつれて、重みの変化は小さくなり、無視できるまでになる。

半径は、学習例 xxx の影響領域を決定する。

このため、このような場合、「BMU」 のみを対象とすることになります。

一般的な戦略は、大きな半径で開始し、エポックが進むにつれて半径を小さくすることである。

σt=σ0e-t∗βσt=σ0e-t∗βとするのが一般的です。

ここで、β<0β<0 Θbeta<0 は減衰率である。

半径に対応する減衰率は、学習率に対応する減衰率と同じ効果を持つ。

近傍関数の振る舞いをより深く理解するために、半径の値を変えて距離に対する近傍関数をプロットしてみよう。

このグラフで注目すべき点は、σ2≦10σ2≦10σsigma^2 leq 10において、距離が10を超えると距離関数がほぼゼロに近づく点である。

この事実を利用して、後ほど実装部分でより効率的な学習を行う予定である。

distance = np.arange(0, 30)
sigma_sq = [0.1, 1, 10, 100]
fig,ax = plt.subplots(nrows=1, ncols=4, figsize=(15,4))
plt_ind = np.arange(4) + 141
for s, ind in zip(sigma_sq, plt_ind):
    plt.subplot(ind)
    f = np.exp(-distance ** 2 / 2 / s)
    plt.plot(distance, f, c='cyan')
    plt.title('$sigma^2$ = ' + str(s))
    plt.xlabel('Distance')
    plt.ylabel('Neighborhood function $f$')
fig.subplots_adjust(hspace=0.5, wspace=0.3)
plt.show()


NumPyを用いたPythonによる自己組織化地図の実装

デファクトスタンダードの機械学習ライブラリであるScikit-LearnにはSOMの組み込みルーチンがないため、NumPyを使って手動で簡単に実装することにします。

教師なし機械学習モデルは非常にわかりやすく、簡単に実装することができます。

SOMを2Dの mxn グリッドとして実装するので、3Dの NumPy 配列が必要になります。

3次元目は、各セルの重みを格納するために必要です。

# Return the (g,h) index of the BMU in the grid
def find_BMU(SOM,x):
    distSq = (np.square(SOM - x)).sum(axis=2)
    return np.unravel_index(np.argmin(distSq, axis=None), distSq.shape)

# Update the weights of the SOM cells when given a single training example
# and the model parameters along with BMU coordinates as a tuple
def update_weights(SOM, train_ex, learn_rate, radius_sq, 
                   BMU_coord, step=3):
    g, h = BMU_coord
    #if radius is close to zero then only BMU is changed
    if radius_sq &lt; 1e-3:
        SOM[g,h,:] += learn_rate * (train_ex - SOM[g,h,:])
        return SOM
    # Change all cells in a small neighborhood of BMU
    for i in range(max(0, g-step), min(SOM.shape[0], g+step)):
        for j in range(max(0, h-step), min(SOM.shape[1], h+step)):
            dist_sq = np.square(i - g) + np.square(j - h)
            dist_func = np.exp(-dist_sq / 2 / radius_sq)
            SOM[i,j,:] += learn_rate * dist_func * (train_ex - SOM[i,j,:])   
    return SOM


# Main routine for training an SOM. It requires an initialized SOM grid
# or a partially trained grid as parameter
def train_SOM(SOM, train_data, learn_rate = .1, radius_sq = 1, 
             lr_decay = .1, radius_decay = .1, epochs = 10):    
    learn_rate_0 = learn_rate
    radius_0 = radius_sq
    for epoch in np.arange(0, epochs):
        rand.shuffle(train_data)      
        for train_ex in train_data:
            g, h = find_BMU(SOM, train_ex)
            SOM = update_weights(SOM, train_ex, 
                                 learn_rate, radius_sq, (g,h))
        # Update learning rate and radius
        learn_rate = learn_rate_0 * np.exp(-epoch * lr_decay)
        radius_sq = radius_0 * np.exp(-epoch * radius_decay)            
    return SOM


自己組織化マップを実装するために使用される主要な関数を分解してみましょう。

find_BMU()は、SOMのグリッドと学習例xが与えられたときに、ベストマッチングするユニットのグリッドセル座標を返します。

各セルの重みとxとのユークリッド距離の二乗を計算し、(g,h)` 、つまり距離が最小となるセル座標を返す。

update_weights()関数は、SOM グリッド、学習用サンプルx、パラメータlearn_rateradius_sq、ベストマッチングユニットの座標、そしてstepパラメータを必要とします。

理論的には、SOMの全セルは次の学習例で更新される。

しかし、BMUから遠いセルでは変化が無視できることを先に示した。

したがって、BMU のごく近傍のセルだけを変更することで、コードをより効率的にすることができる。

ステップパラメータは、重みを更新する際に変更する左右上下のセルの最大数を指定する。

最後に、train_SOM() 関数は、SOM の主な学習手順を実装しています。

この関数は、初期化済みあるいは部分的に学習された SOM グリッドと train_data をパラメータとして必要とします。

この関数の利点は、以前の学習済みステージから SOM を学習できることです。

さらに、 learn_rateradius_sq パラメータ、およびそれらに対応する減衰率 lr_decayradius_decay が必要となります。

epochs` パラメータはデフォルトで 10 に設定されていますが、必要に応じて変更することができます。

実例で自己組織化マップを動かしてみる

SOM を学習する例としてよく挙げられるのは、ランダムな色の例である。

SOMを学習させることで、様々な類似色が隣接するセルにどのように配置されるかを簡単に可視化することができる。

となります。

遠くのセルが異なる色になる。

RGB のランダムな色で満たされた学習データ行列に対して train_SOM() 関数を実行しましょう。

以下のコードでは、学習データ行列と SOM グリッドを RGB のランダムな色で初期化します。

また,訓練データとランダムに初期化された SOM グリッドを表示します.なお、学習行列は3000×3の行列ですが、可視化のために50x60x3の行列に整形しています。

# Dimensions of the SOM grid
m = 10
n = 10
# Number of training examples
n_x = 3000
rand = np.random.RandomState(0)
# Initialize the training data
train_data = rand.randint(0, 255, (n_x, 3))
# Initialize the SOM randomly
SOM = rand.randint(0, 255, (m, n, 3)).astype(float)
# Display both the training matrix and the SOM grid
fig, ax = plt.subplots(
    nrows=1, ncols=2, figsize=(12, 3.5), 
    subplot_kw=dict(xticks=[], yticks=[]))
ax[0].imshow(train_data.reshape(50, 60, 3))
ax[0].title.set_text('Training Data')
ax[1].imshow(SOM.astype(int))
ax[1].title.set_text('Randomly Initialized SOM Grid')


それでは、SOMを学習させ、5エポックごとにその進捗を確認しましょう。

fig, ax = plt.subplots(
    nrows=1, ncols=4, figsize=(15, 3.5), 
    subplot_kw=dict(xticks=[], yticks=[]))
total_epochs = 0
for epochs, i in zip([1, 4, 5, 10], range(0,4)):
    total_epochs += epochs
    SOM = train_SOM(SOM, train_data, epochs=epochs)
    ax[i].imshow(SOM.astype(int))
    ax[i].title.set_text('Epochs = ' + str(total_epochs))


上の例は、SOMのグリッドにおいて、RGBの色が自動的に配置され、同じ色の濃淡が近接する様子を示しており、非常に興味深いものである。

この配置は最初のエポックと同じくらい早く行われるが、理想的なものではない。

SOMは10エポック程度で収束し、その後のエポックでは変化が少なくなっていることが分かります。

学習速度と半径の効果

学習率と半径の違いによる学習率の変化を見るために、同じ初期グリッドからスタートした場合のSOMを10エポック実行することができる。

以下のコードでは、3種類の学習率と3種類の半径でSOMを学習させる。

SOMは各シミュレーションで5エポック後にレンダリングされます。

fig, ax = plt.subplots(
    nrows=3, ncols=3, figsize=(15, 15), 
    subplot_kw=dict(xticks=[], yticks=[]))


# Initialize the SOM randomly to the same state


for learn_rate, i in zip([0.001, 0.5, 0.99], [0, 1, 2]):
    for radius_sq, j in zip([0.01, 1, 10], [0, 1, 2]):
        rand = np.random.RandomState(0)
        SOM = rand.randint(0, 255, (m, n, 3)).astype(float)        
        SOM = train_SOM(SOM, train_data, epochs = 5,
                        learn_rate = learn_rate, 
                        radius_sq = radius_sq)
        ax[i][j].imshow(SOM.astype(int))
        ax[i][j].title.set_text('$eta$ = ' + str(learn_rate) + 
                                ', $sigma^2$ = ' + str(radius_sq))


上の例から、半径が0に近い場合(最初の列)、SOMは個々のセルだけを変化させ、隣接するセルは変化させないことがわかる。

したがって、学習速度に関わらず、適切なマップは作成されない。

また、学習率が小さい場合(1行目、2列目)にも同様のケースが発生する。

他の機械学習アルゴリズムと同様、理想的な学習を行うためには、パラメータのバランスが必要である。

結論

このガイドでは、SOM の理論モデルとその詳細な実装について説明した。

RGBカラーでSOMのデモを行い、同じ色の異なる色調が2Dグリッド上でどのように組織化されるかを示した。

SOMは機械学習界ではもはやあまり人気がないが、データの要約や可視化には良いモデルであることに変わりはない。

タイトルとURLをコピーしました