はじめに
前回、BOW形式での単語ベクトルを使ってk-means法でクラスタリングしたところ、散々だったという報告をしました。
続いて、TF-IDFでの単語ベクトルで実験を行いました。実はこれでもあまり精度がよくないものの、BOWの時ほど悪いことはないだろうと考えていたのですが、実験結果はさらにひどくなっていて、ショックを受けていました。
その後、Clustering.jlのドキュメントを読んでいたところ、k-meansで使われている距離関数がユークリッド距離であることに気が付きました。一般的な属性ベクトル(特徴ベクトル)の場合にはユークリッド距離で事足りるのですが、単語のベクトルの場合は、その特性上から通常cos類似度が用いられます。
ところが、Clustering.jlのドキュメントを読む限りでは、距離関数を指定する方法は見当たりません。やむなく、k-meansを自作しようとしていてClusterling.jlのソース(kmeans.jl)を見ていたところ、どうやら距離関数を指定できるようになっていることがわかりました。
kmeansの呼び出し
前回、Clustering.jlのkmeansは次のように関数呼び出していました。
using Clustering
# mat = 文書数分のベクトルを一つの行列で表現したもの
n_clusters = 8 #the number of clusters
result = kmeans(mat, n_clusters; maxiter=200, display=:none)
これに、「distance=<距離関数>」の記述を追加することで、kmeans内部で使用する距離関数を指定することができます。Clustering.jlのソースを読むと、距離関数はSemiMetric型で定義する必要があります。
距離関数の定義
距離関数は、Distance.jlで定義されているものを使うことができます。自作する必要はありませんでした。
単語ベクトルの場合に使用するcos距離は、Distance.jlには次が用意されています。
type name | convenient syntax | math definition |
CosineDist | cosine_dist(x, y) | 1 – dot(x, y) / (norm(x) * norm(y)) |
kmeansを再実行
それでは、BOW形式の単語ベクトルをcos距離を使ったkmeans法でクラスタリングしてみましょう。
前回のプログラムで、kmeans呼び出しの引数に「distance=CosineDist()」を追加するだけです。
using Distances
using Clustering
# K-meansを使って、8個(カテゴリー数)のクラスタに分類する
n_clusters = 8 #the number of clusters
result = kmeans(mat, n_clusters; maxiter=200, display=:none, distance=CosineDist())
clust_numbers = assignments(result) # get the assignments of points to clusters
次のような結果が得られました。
カテゴリー名 | Cluster1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
“local” | 26 | 18 | 16 | 66 | 16 | 45 | 90 | 24 |
“domestic” | 14 | 62 | 4 | 63 | 59 | 57 | 36 | 22 |
“sports” | 48 | 78 | 2 | 17 | 0 | 2 | 21 | 36 |
“entertainment” | 15 | 8 | 30 | 20 | 2 | 2 | 40 | 59 |
“science” | 6 | 7 | 3 | 23 | 0 | 0 | 33 | 7 |
“it” | 5 | 7 | 3 | 46 | 2 | 1 | 22 | 14 |
“world” | 9 | 4 | 2 | 141 | 0 | 19 | 11 | 6 |
“business” | 26 | 14 | 3 | 46 | 2 | 2 | 40 | 60 |
比較のために、前回ユークリッド距離を用いたkmeanでの結果を掲載します。
カテゴリー名 | Cluster1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
“local” | 192 | 1 | 0 | 89 | 1 | 2 | 15 | 1 |
“domestic” | 220 | 1 | 2 | 80 | 2 | 3 | 11 | 0 |
“sports” | 96 | 0 | 0 | 43 | 45 | 16 | 4 | 0 |
“entertainment” | 81 | 3 | 1 | 60 | 2 | 4 | 25 | 0 |
“science” | 53 | 0 | 0 | 24 | 0 | 0 | 2 | 0 |
“it” | 59 | 0 | 0 | 30 | 1 | 1 | 9 | 0 |
“world” | 121 | 0 | 0 | 65 | 0 | 0 | 6 | 0 |
“business” | 122 | 0 | 0 | 51 | 2 | 5 | 13 | 0 |
今回の結果を見ても、ニュースカテゴリーに対して十分なクラスターが構成されているわけではありません(これは想定通りです)が、前回に比べると、極端な偏りは解消されているのが見て取れます。
さて、次こそは、TF-IDFを使ったk-meansの結果を確認してみたいと思います。
まとめ
上記をまとめたコードを下記に公開しています。ipynb形式です。
なお、この記事で使用したニュース記事データは本サイトでは公開しておりません。必要に応じて、自分で用意してご確認ください。
コメント
ご存じでしたら申し訳ありません,
また,本筋と全く関係ないことで申し訳ありません.
# カレントフォルダにある拡張子が「txt」のファイル名の一覧
filter(f -> isfile(f)&&occursin(r”.txt$”, f), readdir(“.”))
の部分は,
filter(endswith(“.txt”), readdir())
と書くことができるのではないかと思いました.