"BOKU"のITな日常

還暦越えの文系システムエンジニアの”BOKU”は新しいことが大好きです。

手書き数字画像を教師データなしでグループ分けする/クラスタリングをやってみる

手書き数字画像(Scikit-learn付属のdigits)を、教師なしで分類(クラスタリング)してみて、どの程度の精度がでるものなのかを見てみようと思います。

f:id:arakan_no_boku:20190720142708p:plain

 

クラスタリングとは

 

簡単に言えば、データをグループ分けすることです。

詳しい説明は、自分が理解でするより、こちらのサイトの説明を読むほうがいいと思うので、リンクをのせておきます(笑)。

www.albert2005.co.jp

www.albert2005.co.jp

www.albert2005.co.jp

通常のクラス分類だと、教師データを用意して事前学習する必要があります。

でも、クラスタリングはその必要がありません。

なので。

とりあえずデータのグルーピング(分類)のあたりをつけるとかにも使えます。

今回はScikit-learnに付属の手書き画像データ「digits」を使って、クラスタリングをやってみて、それが正解の分類とどの程度差があるのか?を確認してみます。

 

まずは代表的な「k-means」をやってみる

 

クラスタリングには色々な手法があります。

データを階層的に分類していく「階層的クラスタリング」の代表的な手法。

  • 最短距離法
  • 最長距離法
  • 群平均法
  • ウォード法

階層を意識せずに評価関数が最適になるように分類していく「非階層的クラスタリング」の代用的な手法。

  • k-means
  • Affinity propagation

なんかです。

でも、圧倒的に露出が多く代表的と言われているのが「k-means」です。

それを使って、以下のような手書き数字画像をグルーピングします。

f:id:arakan_no_boku:20190720142708p:plain

まずは、ソースコードから。

sk_kmean_digits.py

from sklearn import datasets
from sklearn import cluster
from matplotlib import pyplot as plt

# digitsデータのロード
digits = datasets.load_digits(n_class=10)
# KMeansクラスの生成
model = cluster.KMeans(n_clusters=10)
# 学習・クラスタリング実行
model.fit(digits.data)
# クラスタリング結果カウント用のリスト初期化
results = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# グラフ表示ラベル用のリスト
labels = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
# グルーピング結果をカウントする
for v in model.labels_:
    results[int(v)] += 1
# 比較用に正解ラベルをカウントするリストの初期化
targets = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# 比較用に正解ラベルをグループごとにカウントする
for v in digits.target:
    targets[int(v)] += 1

# グラフ表示のための設定(クラスタリング結果用)
plt.subplot(1, 2, 1)
plt.xlabel('results')
plt.pie(x=results, labels=labels, autopct='%.2f%%')
# グラフ表示設定(正解ラベルカウント用)
plt.subplot(1, 2, 2)
plt.xlabel('targets')
plt.pie(x=targets, labels=labels, autopct='%.2f%%')
# 結果表示
plt.show()

補足します。 

10グループにクラスタリングしてます。

結果は「model.labels_」に以下のようにはいります。

[1 4 4 0 2 3 7 8 9 4 6 6・・・・]

 別に手書き数字を読み取っているわけではなく、0番目のグループ、1番目のグループ・・ってな感じでしょうが、「digits」データの場合は、それっぽくはまります。

なので、今回は散布図とかでなく、結果のラベルをカウントしたものを円グラフにして、正解をカウントしたものと比べてみたら面白いかな・・と考えて、そうしてます。

その実行した結果はこんな感じです。

上(results)がクラスタリングでグループ分け結果で、下(targets)が正解ラベルです。

f:id:arakan_no_boku:20190720183133p:plain

f:id:arakan_no_boku:20190720214046p:plain

正直、0から9の数字がちゃんと対応づいていることが保証されないので、感覚的な比較にしかならないのですが、結構良い感じでグループ分類できているように見えます。

k-means法は、なかなかのものですね。

 

他の手法もためしてみる

 

k-means法では、なかなか良い感じでグルーピングできました。

じゃあ、以外の手法でやったらどうなるのか?

興味があるのでやってみます。

Scikit-learnだと、簡単に試せますしね。

なんせ。

手法の切り替えは上記のソースを一か所おきかえるだけですから。

k-means法はこうでした。

model = cluster.KMeans(n_clusters=10)

ウォード法(階層的クラスタリング)だとこう。

model = cluster.AgglomerativeClustering(n_clusters=10, linkage='ward')

群平均法(階層的クラスタリング)だとこう。

model = cluster.AgglomerativeClustering(n_clusters=10, linkage='average')

最長距離法(階層的クラスタリング)だとこう。

model = cluster.AgglomerativeClustering(n_clusters=10, linkage='complete')

 最短距離法(階層的クラスタリング)だとこう。

model = cluster.AgglomerativeClustering(n_clusters=10, linkage='single')

Affinity propagation(非階層的クラスタリング)だとこう。

model = cluster.AffinityPropagation()

です。

ただ、最後の「Affinity propagation」は、k-meansよりも誤差が少ないと言われてますが、クラスタ数を指定せずアルゴリズムが自動的に決定する手法であるため、今回のように10分類に収まらないといけないケースでは使いづらいです。

なので、今回はAffinity propagation法は、試行から除外してます。

 

他の手法の実行結果(参考情報)

 

それぞれの手法で実行した結果グラフだけのせておきます。

2つ横並びにしています。

左側がグルーピング結果、右側が正解です。

 

ウォード法(階層的クラスタリング

sk_agg_ward_digits.py

f:id:arakan_no_boku:20190720222959p:plain

これはまずまず・・。


群平均法(階層的クラスタリング

sk_agg_average_digits.py

f:id:arakan_no_boku:20190720223141p:plain

ちょっと、ブレが大きいかな。



最長距離法(階層的クラスタリング

sk_agg_complete_digits.py

f:id:arakan_no_boku:20190720223301p:plain

これも結構ブレますね。


 最短距離法(階層的クラスタリング

sk_agg_single_digits.py f:id:arakan_no_boku:20190720235113p:plain


うーーーん。

少なくとも、「digits」データに関しては、k-means法を上回る結果をだせる手法はなさそうです。

特に、最後の「最短距離法(single)」は壊滅的ですね(笑)

データと手法があわないと、こうなるんだということは、よーーくわかりました。

いずれにしても、

クラスタリングでは0に見える数字を0に分類する・・みたいな芸当はできません。

しょせん、アルゴリズムが検出した共通性によってグルーピングしたにすぎなくて、その各グループに対する意味付けは人間がしなければならないものですから。

でもまあ。

だからこそ、データの探索だったり、分析だったりの手法としても使えるわけで・・。

使いどころがいろいろあって、覚えておいて損はないことだけは、よくわかりました。

と・・いうことで。

ではでは。