Juliaでk-mean法(3) TF-IDF

技術
この記事は約8分で読めます。

はじめに

 これまで、BOW形式のベクトルを使ってk-meansを試してきました。

 しかし、BOW形式では、クラスタリングできているとは言えない状況でした。
 単語の出現頻度のみならず、どの記事に出現しているかの分布をみて、広く多くの文書に出現している単語には小さな重みを、少ない文書にしか出現していない単語には大きな重みを与えることで、それぞれの記事の特徴を計算し、クラスタリングしてみます。
 この重みづけとして TF-IDF を用います。

BOW(Bag of Word)

 まずは、BOWですが、文書中の単語の出現回数(出現頻度、TF=Term Frequency)を要素とするベクトルで表現されます。

例)「長い髪の黒い少女と、黒い眼の大きな少女」

あのこの黒い大きな長い少女
002111012

TF-IDF

 これに対して、TF-IDFでは、単語の出現頻度に重み(重要度)を付けます。重みは、検索対象であるすべての文書のうち、その単語が出現している文書の数から計算されます。

TFTerm Frequency文書中の単語の出現頻度
IDFInverse Document Frequency単語が出現する文書数の逆数

 計算式は次のようになります

計算式

$tfidf_{i,j}=tf_{i,j}・idf_{i}$
$idf_{i}=log\frac{\left|D\right|}{\left|df_{i}\right|}+1$

$tf_{i,j}$文書 $d_{j}$ における単語 $t_{i}$ の出現回数
$\left|D\right|$総文書数
$\left|df_{i}\right|$単語 $t_{i}$ を含む文書数

 この計算結果により、それぞれの文書中の個々の単語に数値が与えられます。そしてその値を要素としたベクトルを作成することで、文書をベクトル化することができます。
 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

 今回の実験では、次のようになりました。属しているカテゴリーに対して、クラスターにどう割り振られたかを示しています。クラスタに割り振られた記事の多い順に赤文字青文字緑文字としています。

カテゴリー名Cluster12345678
“local”34136818331314
“domestic”10921712019162518
“sports”422129454270
“entertainment”027310191350
“science”021001110370
“it”1034246710
“world”7203461221110
“business”5919141027676
TF-IDF、cos距離

 比較のために、BOWでの結果を示します。

カテゴリー名Cluster12345678
“local”2618166616459024
“domestic”146246359573622
“sports”4878217022136
“entertainment”1583020224059
“science”6732300337
“it”57346212214
“world”942141019116
“business”2614346224060
BOW、cos距離

 TF-IDFの方が、の開きが大きくなっているのがわかると思います。各カテゴリーの文書の特徴が先鋭化されたのではないかと思います。
 とはいえ、適切なクラスタリングができたとはいいがたい状況です。
 原因は「適切なベクトルが設定されていない」ということになると思います。現在文書数が1562件(2022年6月21日~7月13日)です。例えば文書数10倍にすると性能の違いが見えてくるかもしれません。しかし、ニュース記事をその数集めるのは大変です。
 適切なベクトルとして、すでに外部で計算されているWord2vecを用いようと思います。これによって記事を表現するベクトルがより適切なものになり、クラスタリングの精度が上がることが期待できます。

まとめ

 上記をまとめたコードを下記に公開しています。ipynb形式です。

 なお、この記事で使用したニュース記事は公開しておりません。必要に応じて、自分で用意してご確認ください。

コメント

タイトルとURLをコピーしました