はじめに
これまで、BOW形式のベクトルを使ってk-meansを試してきました。
しかし、BOW形式では、クラスタリングできているとは言えない状況でした。
単語の出現頻度のみならず、どの記事に出現しているかの分布をみて、広く多くの文書に出現している単語には小さな重みを、少ない文書にしか出現していない単語には大きな重みを与えることで、それぞれの記事の特徴を計算し、クラスタリングしてみます。
この重みづけとして TF-IDF を用います。
BOW(Bag of Word)
まずは、BOWですが、文書中の単語の出現回数(出現頻度、TF=Term Frequency)を要素とするベクトルで表現されます。
例)「長い髪の黒い少女と、黒い眼の大きな少女」
あの | この | … | 黒い | 大きな | 長い | … | 髪 | 神 | 眼 | … | 少女 |
0 | 0 | 2 | 1 | 1 | 1 | 0 | 1 | 2 |
TF-IDF
これに対して、TF-IDFでは、単語の出現頻度に重み(重要度)を付けます。重みは、検索対象であるすべての文書のうち、その単語が出現している文書の数から計算されます。
TF | Term Frequency | 文書中の単語の出現頻度 |
IDF | Inverse Document Frequency | 単語が出現する文書数の逆数 |
計算式は次のようになります
この計算結果により、それぞれの文書中の個々の単語に数値が与えられます。そしてその値を要素としたベクトルを作成することで、文書をベクトル化することができます。
BOWよりはTF-IDFの方が、より特徴的な単語の値を大きくしていると言えます。特徴的な単語とは、その文書を代表するような単語のことです。もちろん、必ずしも正確に表しているとは限りませんが、単純な計算式の割には、それなりの効果が出ていると言えます。
特徴ベクトル(TF-IDF)の作成
BOWのプログラムを基に少しの書き換えでTF-IDFに置き換えます。したがって、本来ならもっと効率のいい書き方があるのですが、少し冗長な記述になっています。
# TF=単語頻度
function mergeword(list_word_counts)
all_word_counts = Dict{String, Int}()
for wc in list_word_counts
mergewith!(+, all_word_counts, wc) # Dictの合成、値は+演算
end
all_word_counts
end
# DF=文書頻度(単語がいくつの文書に出現したかの数)
function mergedf(list_doc_counts)
all_doc_counts = Dict{String, Int}()
for dc in list_doc_counts
mergewith!(+, all_doc_counts, dc) # Dictの合成、値は+演算
end
all_doc_counts
end
# tf-idf 作成
function make_tfidf_vector(label_pos, list_word_counts, all_doc_counts)
n = length(list_word_counts) # 文書数=記事数
println(n)
list_tfidf_vector = []
for tfs in list_word_counts
# 1記事分
vec = zeros(Float64, length(label_pos))
for (w, tf) in tfs
df = get(all_doc_counts, w, 0)
if df != 0
pos = get(label_pos, w, 0)
if pos != 0
idf = log(n / df) + 1
vec[pos] = tf * idf
end
end
end
push!(list_tfidf_vector, vec)
end
list_tfidf_vector
end
# 特徴ベクトル(TF-IDF)の作成
## 記事ごとの単語と頻度の一覧
list_word_counts = []
list_doc_counts = []
for article in articles
text = article["detail"]
lines = getlines(text)
(wcs, dcs) = countword(tokenizer, lines)
push!(list_word_counts, wcs)
push!(list_doc_counts, dcs)
article["word_count"] = wcs
end
## 全体の単語と頻度の一覧を作成
all_word_counts = mergeword(list_word_counts)
all_doc_counts = mergedf(list_doc_counts)
## 全体の単語一覧
labels = sort(collect(keys(all_word_counts)))
label_pos = Dict([(w, pos) for (pos, w) in enumerate(labels)])
## 各記事ごとの単語ベクトル(Bag of Words)作成
list_vector = make_tfidf_vector(label_pos, list_word_counts, all_doc_counts)
# 行列に変換する. juliaはcolumn-major order
mat = hcat(list_vector...)
k-means実行
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
# 元のカテゴリーと、クラスタリングの結果を比較する
# カテゴリごとに、各クラスタに含まれる記事数を求める
check_table = Dict([(name, zeros(Int, n_clusters)) for name in keys(categories)])
for (clust_no, article) in zip(clust_numbers, articles)
category = article["category"]
check_table[category][clust_no] += 1
end
check_table
今回の実験では、次のようになりました。属しているカテゴリーに対して、クラスターにどう割り振られたかを示しています。クラスタに割り振られた記事の多い順に赤文字、青文字、緑文字としています。
カテゴリー名 | Cluster1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
“local” | 3 | 41 | 3 | 68 | 18 | 33 | 131 | 4 |
“domestic” | 10 | 92 | 17 | 120 | 19 | 16 | 25 | 18 |
“sports” | 4 | 2 | 21 | 2 | 94 | 54 | 27 | 0 |
“entertainment” | 0 | 2 | 7 | 3 | 10 | 19 | 135 | 0 |
“science” | 0 | 21 | 0 | 0 | 11 | 10 | 37 | 0 |
“it” | 10 | 3 | 4 | 2 | 4 | 6 | 71 | 0 |
“world” | 7 | 2 | 0 | 34 | 6 | 12 | 21 | 110 |
“business” | 59 | 19 | 1 | 4 | 10 | 27 | 67 | 6 |
比較のために、BOWでの結果を示します。
カテゴリー名 | 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 |
TF-IDFの方が、緑<青<赤の開きが大きくなっているのがわかると思います。各カテゴリーの文書の特徴が先鋭化されたのではないかと思います。
とはいえ、適切なクラスタリングができたとはいいがたい状況です。
原因は「適切なベクトルが設定されていない」ということになると思います。現在文書数が1562件(2022年6月21日~7月13日)です。例えば文書数10倍にすると性能の違いが見えてくるかもしれません。しかし、ニュース記事をその数集めるのは大変です。
適切なベクトルとして、すでに外部で計算されているWord2vecを用いようと思います。これによって記事を表現するベクトルがより適切なものになり、クラスタリングの精度が上がることが期待できます。
まとめ
上記をまとめたコードを下記に公開しています。ipynb形式です。
なお、この記事で使用したニュース記事データは本サイトでは公開しておりません。必要に応じて、自分で用意してご確認ください。
コメント