「コサイン類似度」で文書がどれだけ似ているかを調べてみた

SlackばかりやっているからSlack上のみんなの口癖がうつりがち
2022.12.21

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

今年のマックフルーリー「ストロベリー ココアクッキー」が個人的大ブームになっています。暖かい部屋で猫と触れ合いながらアイスを食べる至福の時間を楽しんでいるのですが、実はお腹が弱いので色々なものとトレードオフでアイスを食べて家で仕事を頑張る日々です。


▲ ラムレーズンのアイスも好きです、オススメがあったら教えてください

こんにちは。データアナリティクス事業本部 インテグレーション部 機械学習チームのShirotaです。
これは「 クラスメソッド 機械学習チーム アドベントカレンダー 2022 」12/21(水)の記事となっております。
前日 12/20(火)の記事は以下よりご覧ください。自然言語処理においてTransformersなどで有名なHugging FaceのモデルをVertex AIにデプロイするという、Google CloudとHugging Faceの個人的には嬉しい二つを活用したブログになっております。

自然言語処理強化月間 と銘打って私は今回のアドベントカレンダーを執筆しております。私のアドベントカレンダーは今回で最終回なので、最後はやっぱり自然言語処理関連のお話をしたいと思います。

ベクトル化した文書の活用方法は?

以前、アドベントカレンダーで自然言語処理の概要とその際に必要になってくるベクトル化の話をさせていただきました。

今回は、ベクトル化した後どうするの?の話をしたいと思います。

さて、文書にしても画像にしても、コンピュータが理解し処理できるようにするためにベクトル化する必要があることや様々な手法についてはお伝えしました。
次に考えることは、「そのベクトル化した文書で何ができるの?」ということなのではないでしょうか。

例えば、以下に三つの文書があります。

  • 猫はこの世で一番可愛い生き物なので猫と一緒に眠ると幸せになれる
  • ホットケーキに挟まれて眠りたい
  • 台湾カステラを敷き布団にして眠りたい

直感的に、見た感じで似たようなことを言っている文書はどれでしょうか?
多分、2番目と3番目の文書が似たようなことを言っていると分かるのではないでしょうか。
しかし、それ以外の文書についてはどうでしょうか。
一応、見たところどれも睡眠についての話をしているように見えます。
この「一応どの文書も似たような話をしてはいそう」と言うことや、「2番目と3番目の文書はこの中だと一番似たような話をしている」と言うことをコンピュータに理解させたいと思った時、どうすればいいでしょうか。
まずはコンピュータが理解できるように文書をベクトル化します。次に、文書の類似性をベクトルを用いて評価したい、と考えたのではないでしょうか。
そこで、 コサイン類似度 と言う指標が存在します。

コサイン類似度の定義

コサイン類似度とは、自然言語処理においては文書の類似性を評価するものです。
この値を用いると「似ているかどうか」という観点が分かるため、「Aというものが好きならこれによく似たBも好きだろう」といった似たものをレコメンドする際の一手法であるコンテンツベースフィルタリングに用いることができます。
また上記に関連して類似性から未知のものを分類する手法としてk近傍法(k-nearest neighbors algorithm)という手法がありますが、この手法における距離関数としてコサイン類似度を用いることもできます。

k近傍法については以下ブログが分かりやすくまとめてありオススメです。よかったら参考にしてください。

また余談ですが、身近なところで言うと全文検索エンジンである ElasiticSearch でもk近傍法による探索を用いることができます。

本題に戻りましょう。
例えば、ある二つの文書がどれくらい似ているのかを評価したいと思ったとします。
ここで文書がベクトルになっていることを利用して、そのベクトルが成す角度で、二つの文書の類似性を評価することができます。
二次元のベクトルを頭に思い描いてもらえると分かりやすいと思います。

二つのベクトルが成す角度が0度に近い時、二つのベクトルは重なるように存在します。つまり、この二つは似ているということになります。
反対に、180度に近い時は互いがほぼ正反対の方向を向いている状態になります。つまり、この二つは似ていないということになります。
特殊な例で90度が存在します。90度はベクトルが直交している状態で、これは相関関係が無い状態と見なします。似ている・似ていないと尺度とは別な特別な状態です。

ここまでベクトルの角度の話をしてきましたが、「ベクトルそれぞれの大きさはどう考慮するのだろうか」という疑問が湧いている方もいらっしゃるのではないでしょうか。
そこでコサイン値を取る、という行為が響いてきます。
コサイン類似度は以下の式で求められ、これはベクトルの内積の定義を変形したもの( ベクトルの内積/各ベクトルのノルム )となっておりベクトルの大きさは無視できるものになります。


▲ 内積の定義、懐かしいですね

このようにコサイン値を求めることで文書の類似性が-1〜1の範囲で見られるようになります。
このコサイン値のことを コサイン類似度 と呼びます。

「次元の呪い」に注意

コサイン類似度をこのように定義しているため、n次元のベクトルに拡張されても計算は容易にできます。


▲ 2次元でずっと例えていましたが、これなら容易に高次元にも適応できます

しかし、この際に意識しておく必要があることが2つあります。
1つは、高次元になればなるほど計算量が指数関数的に増えていくことです。

もう1つは、高次元になればなるほど前述した90度(ベクトルが直交している)の値を取る確率が上がり、相関関係を調べるための値が取りづらくなっていくことです。
これは、次元ごとに軸が増えるため直交の組み合わせが増えることからも分かると思います。より詳細を知りたい方は 球面集中現象サクサクメロンパン問題 で検索してみてください。
このような、高次元になるにつれてコサイン類似度の有用性が落ち、計算そのものの処理時間も大きくなっていく問題を 次元の呪い と言います。
次元の呪いを防ぐために、本当に必要な特徴量を選択したり次元を減らす処理を追加することが有効になっていきます。

Pythonでコサイン類似度を求めたい

コサイン類似度の解説が終わったところで、冒頭にお話しした文書のコサイン類似度を求めていきましょう。
Pythonでコサイン類似度を求めたい時にできる方法についてまとめてみました。

NumPyやSciPyをのような行列演算に長けた数値計算ライブラリ使って計算する

ベクトル化ができていれば、Pythonには行列演算に長けた数値計算ライブラリがあるのでそれを使って計算することができます。
Google Colaboratory(以下 Colab)などではデフォルトで上記のパッケージが入っているのでインストールの手間も省けて楽です。

機械学習ライブラリのscikit-learnを用いる

こちらもColabだとデフォルトでインストールされていて導入の手間が省けて楽です。
scikit-learnには cosine_similarity という組み込み関数が用意されているので上記の手法よりより手軽にコサイン類似度を求めることができます。

日本語自然言語処理ライブラリのGiNZAと自然言語処理ライブラリのspaCyを用いる

以前紹介したGiNZAとspaCyを用いてコサイン類似度を求めることもできます。
それぞれのライブラリについてや導入については、以前書いたブログを参考にしてください。

spaCyには Doc.similarity というメソッドがあります。
GiNZAをインストールする作業は必要になりますが、このメソッドを使うだけでコサイン類似度を簡単に求められるので手軽に実施できます。
今回はGiNZAとspaCyを用いてコサイン類似度を求めてみることにしました。

GiNZAとspaCyでコサイン類似度を求める

実施するにあたって、GiNZAのライブラリによってテキストをベクトル化する処理に用いているものが違うので先にこちらの説明を簡単にしておきます。

  • ja_ginza:chiVe を使用している。chiVe自身はWord2Vecの一種であるSkip-gramアルゴリズムを使用して単語分散表現を構築している

今回はja_ginzaで文書をベクトル化してコサイン類似度を求めてみます。

GiNZAで文書をベクトル化する

GiNZAで文書をベクトル化するには以下のコードを実行します。

import spacy
nlp = spacy.load('ja_ginza')
doc = nlp('文書の形態素解析をしてみたよ')
for sent in doc.sents:
    for token in sent:
        print(
            token.i,
            token.text,
            token.vector,
            token.vector.shape,
        )

実行すると、以下のように形態素解析したもののベクトルが出力されます。

0 文書 [-3.39792520e-01 -3.65710974e-01  1.68725878e-01 -1.21724837e-01
 -1.19769022e-01  3.46934795e-02  6.16223216e-02 -8.42880756e-02
 -7.65963271e-02 -2.44371071e-01  2.82809716e-02 -3.12742777e-02
  6.30355626e-02  1.22547727e-02  1.23147272e-01 -1.87365696e-01
…(300,)
1 の [-8.27400386e-02 -9.10336450e-02 -8.74446332e-02 -1.43936828e-01
……

今回は長いので省略しましたが、300次元のベクトルになって出力されました。

コサイン類似度を求める

ベクトル化を試したところで、コサイン類似度を求めていきましょう。
spaCyで用いるDoc.similarityメソッドでは単語ベクトルの平均値を用いてコサイン類似度の計算を行います。

Doc.similarity METHODNEEDS MODEL
Make a semantic similarity estimate. The default estimate is cosine similarity using an average of word vectors.

コサイン類似度は以下のコードを実行して求めます。
冒頭に挙げた3つの文書のコサイン類似度を求めてみました。

import spacy
nlp = spacy.load('ja_ginza')
doc1 = nlp('猫はこの世で一番可愛い生き物なので猫と一緒に眠ると幸せになれる')
doc2 = nlp('ホットケーキに挟まれて眠りたい')
doc3 = nlp('台湾カステラを敷き布団にして眠りたい')

print(doc1.similarity(doc2))
print(doc1.similarity(doc3))
print(doc2.similarity(doc3))

結果、以下のコサイン類似度が得られました。

0.7744854256641114
0.7562894735207788
0.8472876158152333

はじめに予想した通り、2番目の文書と3番目の文書のコサイン類似度が一番高くなりました。

同じ小説内の文書を類似度で見抜くことはできるか?

ちょっと気になったので、今回 青空文庫 から太宰治の『走れメロス』の文書を2つ(冒頭と最後の方)持ってきました。これともう一つ、私のブログの冒頭の文書を用意しました。
これらのコサイン類似度を調べてみます。

import spacy
nlp = spacy.load('ja_ginza')
doc1 = nlp('メロスは激怒した。必ず、かの邪智暴虐の王を除かなければならぬと決意した。メロスには政治がわからぬ。メロスは、村の牧人である。笛を吹き、羊と遊んで暮して来た。けれども邪悪に対しては、人一倍に敏感であった。')
doc2 = nlp('「セリヌンティウス。」メロスは眼に涙を浮べて言った。「私を殴れ。ちから一ぱいに頬を殴れ。私は、途中で一度、悪い夢を見た。君が若し私を殴ってくれなかったら、私は君と抱擁する資格さえ無いのだ。殴れ。」セリヌンティウスは、すべてを察した様子で首肯き、刑場一ぱいに鳴り響くほど音高くメロスの右頬を殴った。')
doc3 = nlp('地域限定のTipsをお話しようと思います。栗きんとんと言えば数個まとめて箱に入っているものが売られているものですが、岐阜県のアンテナショップでは単品の栗きんとんが買えます。単品の栗きんとんも美味しいのですが、栗きんとんを作る過程で生まれる「栗きんとんのおこげ」というものがこの世には存在します。それがかなり美味しいので、見かける機会があったら是非買ってみてください。')
print(doc1.similarity(doc2))
print(doc1.similarity(doc3))
print(doc2.similarity(doc3))

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

0.9660452862035832
0.9217821909846253
0.9294249972771635

『走れメロス』の文書同士が一番類似度が高くなりました。
この類似度に匹敵する文書が書けたら、私のブログの書き出しが太宰治風になれたと評価することができるようになるでしょう。精進します。

直感的な理解を数値的な理解に落とし込むということ

今回は、ベクトル化した文書のコサイン類似度を計算することで文書が似ているかどうかを調べてみました。
似た文書を調べることができると、例えば興味がある記事をレコメンドすることやレポートがコピペで作成されたものでないか調べることなど様々なことに活用できます。
今回は比較的次元も少なめなものでベクトル類似度を計算してみましたが、このくらいのものでも直感的に「あれ似ているな」と思ったものが「XXの値が出ているからこれは似ている」と表現できるようになったことが分かりました。

私のアドベントカレンダーの担当は今回が最後となりますが、今後も自然言語処理に関する様々なブログを書いていきたいと思います。とても楽しい企画でした。

明日の機械学習チームのアドベントカレンダーもどうぞお楽しみに!