Never Too Late

いつだってゼロからやり直そう

scikit-learn/scipyを使い、コサイン類似度ベースで階層型クラスタリングをするときのメモ

scikit-learn, scipyを使い、文章をコサイン類似度を用いて階層型クラスタリングを行おうとしたときに少し詰まったのでメモです。

まずは以下のような文章を用意します。Chat GPTに作らせたそれぞれバスケットボール、野球、交通渋滞に関する50単語のニュースです。

doc1 = "Last night, the City Hawks clinched a nail-biting victory against the Mountain Lions, 102-99. Star player, Jordan Mitchell, secured the win with a last-second three-pointer. Fans are eagerly anticipating next week's match, as playoff implications heat up. Basketball enthusiasts, mark your calendars!"
doc2 = "Yesterday, the Bay Breeze clinched a 5-4 win over the Sunset Sluggers. Ace pitcher, Liam Rodriguez, delivered 8 strong innings with 10 strikeouts. The highlight was shortstop Alex Torres' game-winning home run in the 9th. The league title race intensifies as the season's end approaches."
doc3 = "Heavy gridlock paralyzed California's I-5 highway yesterday, with delays stretching for miles. A combination of roadwork and multiple minor accidents exacerbated the rush-hour congestion. Commuters are urged to seek alternative routes or use public transport today as authorities work to clear the backlog and ensure smoother traffic flow."

scikit-learnを用い、それぞれの文章のTF-IDFベクトルをを求めます。なお本当なら文章にステミングを施したり、ストップワードを除去したり等の作業が必要ですが、ここでは省略します。

import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer

tfidf = TfidfVectorizer()
docs = np.array([doc1, doc2, doc3])
vector_array = tfidf.fit_transform(docs).toarray()
print(vector_array)

配列の中身は以下のようになっています。どうやら全体で113個の単語が検出されたようです。

[[0.   0.15 0.15 0.   0.   0.   0.15 0.   0.   0.   0.15 0.   0.12 0.09
  0.   0.   0.15 0.   0.15 0.   0.15 0.   0.15 0.   0.12 0.   0.   0.
  0.   0.   0.15 0.   0.   0.15 0.   0.15 0.   0.   0.   0.   0.15 0.15
  0.   0.   0.   0.   0.   0.15 0.   0.   0.   0.15 0.3  0.   0.   0.15
  0.15 0.15 0.   0.   0.15 0.15 0.   0.15 0.15 0.15 0.   0.   0.   0.
  0.   0.15 0.15 0.15 0.   0.   0.   0.   0.   0.   0.   0.   0.15 0.15
  0.   0.   0.   0.   0.15 0.   0.   0.   0.   0.27 0.15 0.   0.   0.
  0.   0.   0.   0.15 0.   0.   0.15 0.   0.15 0.12 0.   0.09 0.   0.
  0.15]
 [0.15 0.   0.   0.15 0.   0.15 0.   0.15 0.   0.   0.   0.15 0.   0.09
  0.   0.   0.   0.15 0.   0.15 0.   0.   0.   0.   0.11 0.   0.   0.
  0.   0.15 0.   0.15 0.   0.   0.   0.   0.   0.   0.15 0.   0.   0.
  0.   0.15 0.   0.15 0.   0.   0.15 0.15 0.15 0.   0.   0.15 0.15 0.
  0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.15 0.
  0.15 0.   0.   0.   0.   0.15 0.   0.15 0.   0.15 0.   0.15 0.   0.
  0.   0.15 0.15 0.   0.   0.   0.15 0.15 0.15 0.52 0.   0.15 0.   0.
  0.15 0.   0.   0.   0.   0.   0.   0.15 0.   0.11 0.15 0.09 0.   0.11
  0.  ]
 [0.   0.   0.   0.   0.14 0.   0.   0.   0.14 0.29 0.   0.   0.11 0.09
  0.14 0.14 0.   0.   0.   0.   0.   0.14 0.   0.14 0.   0.14 0.14 0.14
  0.14 0.   0.   0.   0.14 0.   0.14 0.   0.14 0.14 0.   0.14 0.   0.
  0.14 0.   0.14 0.   0.14 0.   0.   0.   0.   0.   0.   0.   0.   0.
  0.   0.   0.14 0.14 0.   0.   0.14 0.   0.   0.   0.14 0.14 0.   0.14
  0.   0.   0.   0.   0.14 0.   0.14 0.   0.14 0.   0.14 0.   0.   0.
  0.14 0.   0.   0.14 0.   0.14 0.   0.   0.   0.17 0.   0.   0.29 0.14
  0.   0.14 0.14 0.   0.14 0.14 0.   0.   0.   0.   0.   0.09 0.14 0.11
  0.  ]]

次にscipyのlinkageを用いてベクトル間のコサイン類似度と、その値に応じた階層構造を求めます。

from scipy.cluster.hierarchy import linkage

hierarchy = linkage(vector_array, metric='cosine')
print(hierarchy)

出力結果は以下のようになりました。

[[0.   1.   0.82 2.  ]
 [2.   3.   0.89 3.  ]]

まずですが、TF-IDFのベクトルは負数を返さないはずなので、コサイン類似度は0から1に収まるはずです。またscipyの実装では、コサイン類似度の値は1から引いた値が返されるので0と1の関係が逆転しており、0が最も遠く、1が最も近いという意味になりますpdistのドキュメントにそう書いてありました。

よって上記の出力の意味は、doc1とdoc2(出力の中では0と1)が距離0.82で最も近く、その2つとdoc3の距離(出力の中では2と3)は0.89ということになります。なおこの(doc1/doc2)とdoc3のように、複数の要素を含みだしたクラスターと別のクラスターの距離を測るアルゴリズムというのはいくつかあり、linkageのmethodパラメータで指定できます。ユークリッド距離を用いる場合は色々な選択肢があるのですが、コサイン類似度を用いる場合にはそのサブセットを使用することになります。methodを指定しない場合は単連結法というアルゴリズムが利用されるのですが、それを確認してみましょう。

以下のようにすると、各文章間のコサイン類似度を確認することが出来ます。

from scipy.spatial.distance import pdist, squareform

pd.DataFrame(squareform(pdist(vector_array, metric='cosine')),index=['doc1','doc2','doc3'], columns=['doc1','doc2','doc3'])
doc1 doc2 doc3
doc1 0.00 0.82 0.93
doc2 0.82 0.00 0.89
doc3 0.93 0.89 0.00

というわけで、先のコサイン類似度0.89というのはdoc2とdoc3の距離だったことが分かります。doc1とdoc3の距離が0.93とこれより遠い為、0.89の方が採用と思われます。method='complete', 'average', 'weighted'といったアルゴリズムも指定可能です。詳しくはlinkageのドキュメントを参照ください。 '