静かなる名辞

pythonとプログラミングのこと


【python】クラスタリング結果をエントロピーで評価する

はじめに

 クラスタリング結果の良し悪しを評価したいことがあります。

 正解ラベルがないデータに対してクラスタリングを行った場合(つまり本当に教師なし学習でやる場合)、基本的にクラスタ内距離二乗和やクラスタ中心間の距離などを使ってやる以外の方法はありません*1

 しかし、正解ラベルがあるデータに対してクラスタリングをかける、というシチュエーションもあります。たとえばクラスタリング手法の性能評価だとか、相手にするデータセットが教師なしでやってどれくらいまとまる性質があるか調べるとか、そんな感じのときです。

 こういうとき、評価指標がないと困ります。そこで、評価指標を考えます。何通りかあるようですが、簡単に計算できるのはエントロピーです。

説明

 エントロピー(厳密にはシャノン情報量)は次式で定義されます。

  H(P) = - \sum_{A\in\Omega} P(A) \log_2 P(A)
  \Omegaは対象とする事象を表します。

 厳密な理論や定義などはwikipediaでも読んでいただくとして、エントロピーは乱雑さの指標になります。scipyで計算できますからやってみましょう。

>>> from scipy.stats import entropy
>>> entropy([0.5, 0.5],base=2)
1.0
>>> entropy([1, 0],base=2)
0.0
>>> entropy([0.25, 0.25, 0.25, 0.25],base=2)
2.0
>>> entropy([1, 0, 0, 0],base=2)
0.0

 渡しているリストの乱雑さを測っていると見ることができます。中身が一様(=乱雑)なら取り得る値の \log_2となり、乱雑でない(=どれかが1で他が0)なら0となります。

 ということは、クラスタごとにエントロピーを計算してやると、クラスタの中身が特定の正解ラベルに偏っているならば値は0、まんべんなくすべての正解ラベルを含んでいれば \log_2 正解ラベル数となり、0に近いほど良いという性質の評価指標になります。エントロピー自体はクラスタごとに計算できますが、クラスタに割り振られたデータ数で重み付き平均を取ることで全体の結果としてまとめます。

実装

 ライブラリがありそうだけど見つけられなかったので、結局自前実装することに。sklearn.metricsにあると思ったら、0~1のレンジに収まるような改良されまくった指標ばっかりでそのものズバリのエントロピー加重平均はありませんでした(と、思う。見落としてたら恥ずかしい)。

 2.3. Clustering — scikit-learn 0.20.1 documentation

def entropy_score(true_y, cl_y):
    df = pd.crosstab(cl_y, true_y)
    arr = df.as_matrix()

    weight = arr.sum(axis=1)
    weight = weight/(weight.sum())

    cl_entropy = np.array([entropy(cl, base=2) for cl in arr])
    return (cl_entropy*weight).sum()

 もうちょっと綺麗な書き方は色々あると思いますが、こんなので動きました。scipyのentropyは引数の和が1になるように勝手に揃えてくれるので、それに頼り切ってコードを書いています。

実験

 irisでKMeansとGMM(混合ガウスモデル)によるクラスタリングを比較してみましょう。GMMの方が良さそうなのは以前の記事で確認しているので、その定量的評価を今回はするということです。

# coding: UTF-8

import numpy as np
import pandas as pd

from scipy.stats import entropy

from sklearn.datasets import load_iris
from sklearn.cluster import KMeans
from sklearn.mixture import GaussianMixture as GMM

def entropy_score(true_y, cl_y):
    df = pd.crosstab(cl_y, true_y)
    arr = df.as_matrix()

    weight = arr.sum(axis=1)
    weight = weight/weight.sum()

    cl_entropy = np.array([entropy(cl, base=2) for cl in arr])
    return (cl_entropy*weight).sum()

def main():
    iris = load_iris()
    km = KMeans(n_clusters=3, n_init=30, max_iter=1000)
    cluster_labels = km.fit_predict(iris.data)
    print("km")
    print(entropy_score(iris.target, cluster_labels))
 
    gmm = GMM(n_components=3)
    cluster_labels = gmm.fit(iris.data).predict(iris.data)

    print("gmm")
    print(entropy_score(iris.target, cluster_labels))

if __name__ == "__main__":
    main()

 結果は、

km
0.39388631839664884
gmm
0.1611488952045549

 やっぱりGMMが良かったです。

蛇足

 「それにしてもsklearnにないのは変だなぁ」と思い、上記資料を読んだ感じだとhomogeneity_scoreが今回やったことに近いようですが、

  • 条件付きエントロピーできっちり定義
  • 0~1になるように元データのデータ比率を log_2したもので割り、1から引く

 という感じでした。そのうちちゃんと調べます。あと、実務的に使う場合はsklearnのhomogeneity_score、completeness_score(homogeneityのクロス集計を逆転させて計算しようという発想に近い)、v_measure_score(homogeneityとcompletenessの調和平均。要するにF1値みたいなもん)でやれば良いので、今回書いた方法の出番はないと思います。まあ、何かの参考にしてください。

*1:こちらの記事が参考になります。クラスタリング結果の評価の尺度基準 - froglog