機械学習」タグアーカイブ

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

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

まずは以下のような3つの文章を用意します。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'])
doc1doc2doc3
doc10.000.820.93
doc20.820.000.89
doc30.930.890.00

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

Google BigQuery MLを使ってみたけど、これが便利に使えるシーンが分からない

野暮用でGoogle BigQueryを勉強し始めました。そしてBigQueryは機械学習をサポートしているというので早速少し使ってみました。「SQLと機械学習」と聞くとどこか奇妙な組み合わせのような響きですが、機械学習は大抵データフレームを使ってモデルを作り、そのモデルを使ってデータフレームを変更する(大抵は列を足す)という処理になので、そう考えればSQLで使うのは自然かもしれません。

ちょうどこちらも野暮用でバスケットボール選手の成績を使ったクラスタリングをしていたので、そのCSVファイルをBigQueryにアップロードしデータセットを作成、そのデータセットを使ってk-means法によるクラスタリングを試してみました。トレーニングはこんな感じで書けます。ちなみに簡略化の為にパラメータは2つのみ使用しています。

CREATE OR REPLACE MODEL
`mydataset1.bleague_player_clustering` OPTIONS(model_type='kmeans', num_clusters=10) AS
SELECT
(PPG - PPG_AVG) / PPG_STD AS PPG_STDED,
(RPG - RPG_AVG) / RPG_STD AS RPG_STDED
FROM
(
SELECT
PPG,
AVG(PPG) OVER() AS PPG_AVG,
STDDEV(PPG) OVER() AS PPG_STD,
RPG,
AVG(RPG) OVER() RPG_AVG,
STDDEV(RPG) OVER() RPG_STD
FROM
`mydataset1.bleague_players`
)

下部のSELECT文で抽出されたデータがモデルのトレーニングに使われるという構図です。CREATE OR REPLACE MODELのOPTIONSでは、モデルのハイパーパラメータ等が指定し、ここでは分類するクラスター数を10と指定しました。mydataset1.bleague_player_clusteringというのがモデルの名前です。

Big Query MLの文法的はそれなりにシンプルだと思います。ですがSQLには大抵データの前処理に適した関数などが少なく、それをする為に複雑な副問い合わせなどが必要になりそうです。例えばk-means法だと各パラメータに標準化を施す必要があったりしますが、それだけの為に上述のような副問い合わせが必要になってしまいます。

作成したモデルを使い、実際にクラスタリングを行うためには以下のように記述します。

SELECT
PLAYER,
TEAM,
CENTROID_ID
FROM
ML.PREDICT
(
MODEL `myproject1-401904.mydataset1.bleague_player_clustering`,
(
SELECT
*,
(PPG - PPG_AVG) / PPG_STD AS PPG_STDED,
(RPG - RPG_AVG) / RPG_STD AS RPG_STDED
FROM
(
SELECT
*,
AVG(PPG) OVER() AS PPG_AVG,
STDDEV(PPG) OVER() AS PPG_STD,
AVG(RPG) OVER() RPG_AVG,
STDDEV(RPG) OVER() RPG_STD
FROM
`mydataset1.bleague_players`
)
)
)

下部が再びモデルに与えるデータで(今回の場合はトレーニングのデータと同じ)、それに伴いML.PREDICTが新たなカラムを追加し(例えばCENTROID_ID)、最上部でそれをSELECTしています。こちらも文法だけ言えばとてもシンプルです。

Googleのインフラを用いて動くこともあり、BigQuery MLにはそれなりの速度的なメリットはあるのではないかと推測します。ただし上述のように前処理をデータに施すときにも面倒が起きそうですし、トレーニングデータとテストデータを分けるといった、機械学習の前に行うあれこれがいちいち面倒になりそうです。トレーニングデータとテストデータを分ける場合、乱数の入ったひとつのカラムを用意し、その値でどの行を取得するのかを決定する感じでしょうか。

タイトルにもつけたのですが、Google BigQueryが便利に使えるシーンが今のところ分かりません。前処理の終わったデータをアップロードしてから使用してもよいかもしれませんが、速度的メリットを享受したい程の大量データだとそれも大変ですし、かといってバックエンドで何処かから流してきたデータを使うのであればやはり前処理は必須でしょうし、その辺の丁度いいシチュエーションが何なのか、頭の片隅に入れておいて考えたいと思います。