K-meansで遊んでみた

研究室の輪講でK-meansを実装する課題を出すために、自分でも実装してみました。

from copy import deepcopy
from random import shuffle

def kmeans(rows, k=4, distance=None):
    if not distance:
        distance = euclidean

    dim = len(rows[0])  # 特徴空間の次元数

    # データの中からcentroidをk個ランダムに選択
    shuffle(rows)
    centroids = rows[:k]

    result = []

    prev = None
    while True:
        clusters = [[] for i in xrange(k)]
        
        # それぞれのデータに対し、最も近い重心を探す
        for j, row in enumerate(rows):
            ds = map(lambda x: distance(row, x), centroids)
            m = min(ds)
            clusters[ds.index(m)].append(j)

        # 結果が前回と同じ場合は終了
        if clusters == prev: break
        prev = clusters

        # 重心をメンバーの平均に移動する
        for j, member in enumerate(clusters):
            centroid = [0.0 for i in xrange(dim)]
            for m in member:
                row = rows[m]
                for n, val in enumerate(row):
                    centroid[n] += val
            for m in xrange(dim):
                centroid[m] /= len(member)
            centroids[j] = centroid

        # 可視化するために途中経過も結果に含める
        result.append((clusters, deepcopy(centroids)))

    return result

def euclidean(v1, v2):
    'ユークリッド距離'
    dim = len(v1)  # 特徴ベクトルの次元数
    assert len(v1) == len(v2)

    sum_sq = 0.0
    for i in xrange(dim):
        sum_sq += pow(v1[i] - v2[i], 2)

    return 1 - 1 / max(pow(sum_sq, 0.5), 0.00001)

これに対して、次のようなコードを実行してみると

from random import randint

def generate_data(size_x=10, size_y=10, t=4):
    data = []
    for i in xrange(size_y):
        line = ""
        for j in xrange(size_x):
            if randint(0, 10) < t:
                line += "*"
            else:
                line += " "
        data.append(line)
    print 'data set'
    print "\n".join(data)

    rows = []
    for y_, line in enumerate(data):
        y = len(data) - y_ - 1
        for x, v in enumerate(line):
            if v == "*":
                rows.append((x, y))
    return rows

def print_result(size_x, size_y, cluster, centroid):
    rounded_centroid = [(int(round(x1)), int(round(x2))) for x1, x2 in centroid]

    print rounded_centroid
    for y in xrange(size_y-1, -1, -1):
        line = ""
        for x in xrange(size_x):
            for i, member in enumerate(cluster):
                if (x, y) in rounded_centroid:
                    line += "*"
                    break
                elif (x, y) in rows and rows.index((x, y)) in member:
                    line += str(i)
                    break
            else:
                line += " "
        print line

# 20*10 の大きさの平面に2割程度データを設置する
size_x = 20
size_y = 10
t = 2

# データセットの生成
rows = generate_data(size_x, size_y, t)

# kmeansの実行。今回は4クラスタに分類する
results = kmeans(rows, k=4)

print 'result'
for i, (cluster, centroid) in enumerate(results):
    print ''
    print i, 
    print_result(size_x, size_y, cluster, centroid)

例えばこういう感じに表示されます。
結果の出力の中で*と表示されているのが、クラスタの重心で、数字は分類されているクラスタの番号を表しています。
反復するたびに少しずつ移動しているのが分かりますね。

# 入力されるデータ
data set
  * ***    *     * *
   * *  *          *
        *     *    *
   *   **  *      * 
                   *
* * *     *         
          *  * *   *
* *           *     
              *     
   * *            * 

# 1回目の反復での結果
0 [(13, 3), (2, 4), (14, 7), (3, 6)]
  3 333    2     2 2
   3 3  3          2
        3     *    2
   *   33  2      2 
                   2
1 * 1     0         
          0  * 0   0
1 1           0     
              0     
   1 1            0 

# 2回目の反復での結果
1 [(14, 2), (2, 2), (16, 7), (5, 8)]
  3 333    2     2 2
   3 *  3          2
        3     2 *  2
   3   33  0      2 
                   2
1 1 1     0         
          0  0 0   0
1 *           *     
              0     
   1 1            0 

# 3回目の反復での結果
2 [(14, 3), (2, 2), (17, 8), (5, 8)]
  3 333    3     2 2
   3 *  3        * 2
        3     2    2
   3   33  0      2 
                   2
1 1 1     0         
          0  0*0   2
1 *           0     
              0     
   1 1            0