コサイン類似度を利用し、集団の類似性を測ってみる

  • 11
    Like
  • 2
    Comment

Gaiax Advent Calendar 2016、19日目の記事です。
私は普段、Railsでアプリケーション開発をしています。
しかし、今入っているプロジェクトで、ある解析をしたら面白いんじゃないかということでやってみました。
今回はそのお話です。

何を解析するのか

例えば、本が好きな人達がいるとしましょう。彼らは様々な本が好きで、好きな本の作者が紐付いています。
Aさんは伊坂幸太郎、東野圭吾、村上春樹が好き。Bさんは伊坂幸太郎、J.K.ローリングが好き。CさんはJ.K.ローリングだけ好き。などなど。
(ちなみに私は挙げた人たちの本は全然知りません。唯一、ハリーポッターは好きでした。)

各人が好きな作家テーブルを作る

まず、好きな作家に対して[0,1]のフラグを立てます。

伊坂幸太郎 東野圭吾 村上春樹 J.K.ローリング
Aさん 1 1 1 0
Bさん 1 0 0 1
Cさん 0 0 0 1
Dさん 0 0 1 1
Eさん 1 1 0 0

各作家が好きな人を抽出・合算したテーブルを作る

伊坂幸太郎を好きな人だけに注目すると、下記のようになります。

伊坂幸太郎 東野圭吾 村上春樹 J.K.ローリング
Aさん 1 1 1 0
Bさん 1 0 0 1
Eさん 1 1 0 0

このAさん、Bさん、Eさんのフラグを合算することで、伊坂幸太郎好きの集団ベクトルを作り出します。

伊坂幸太郎 東野圭吾 村上春樹 J.K.ローリング
伊坂幸太郎
好きの集団
3 2 1 1

この合算方法を、全ての作家に対して行います。

伊坂幸太郎 東野圭吾 村上春樹 J.K.ローリング
伊坂幸太郎
好きの集団
3 2 1 1
東野圭吾
好きの集団
2 2 1 0
村上春樹
好きの集団
1 1 2 1
J.K.ローリング
好きの集団
1 0 1 3

この各作家好きの集団ベクトル同士を解析していきます。

どのように解析するのか

コサイン類似度

今回はコサイン類似度というものを利用します。
下記の数式で表され、ベクトルのなす角のコサインを表します。2つのベクトルの方向が近いほど、そのなす角は小さくなり、向きが異なるほど大きくなる。今回の場合、成分が非負の数ベクトルなので、なす角は最小値 0 度、最大値 90 度。角度のコサインを計算すれば 0 度のとき 1 で、90 度のときに 0 と、うまい具合に「類似度」と呼ぶにふさわしい値が得られます。

\cos( \vec{q}, \vec{d} ) = \frac{ \vec{q} \cdot \vec{d}}{  |\vec{q}| |\vec{d}|} =  \frac{ \vec{q} }{  |\vec{q}| }\cdot\frac{  \vec{d}}{  |\vec{d}| } = \frac{\sum_{i=1}^{|V|} q_i d_i}{\sqrt{\sum_{i=1}^{|V|} q_i^2} \cdot \sqrt{\sum_{i=1}^{|V|} d_i^2}}

正規化された単位ベクトルについてはベクトルの内積を求めるだけでよくなります。

\cos( \vec{q}, \vec{d} ) = \vec{q} \cdot \vec{d} = \sum_{i=1}^{|V|} q_i d_i

参考:http://www.cse.kyoto-su.ac.jp/~g0846020/keywords/cosinSimilarity.html

通常は文章を形態素解析し、各単語の出現回数をベクトルとして格納し、TF-IDFで一般的な文脈の影響を避けた上で、文章同士の類似度を測るのに使われます。
しかし今回は単語の代わりに作家を使い、文章の代わりに各作家好きの集団どうしの類似度を測っていきます。

実際に解析する

コード

cosine_similarity.rb
require 'matrix'

module CosineSimilarity

  module_function

  def combination_calc(group)
    # 集団のデータをベクトルに変換し、正規化
    normalized_vectors = {}
    group.each do |gk, gv|
      normalized_vectors[gk] = Vector.elements(gv).normalize
    end

    # 組み合わせごとのコサイン類似度を出力
    normalized_vectors.keys.combination(2) do |v1k, v2k|
      puts "#{v1k} - #{v2k} = #{normalized_vectors[v1k].inner_product(normalized_vectors[v2k])}"
    end
  end
end

writers = {
  :kotaro_isaka => [3, 2, 1, 1],
  :keigo_higashino => [2, 2, 1, 0],
  :haruki_murakami => [1, 1, 2, 1],
  :j_k_rowling => [1, 0, 1, 3]
}

CosineSimilarity.combination_calc(writers)

結果

~/w/cosine_similarity ❯❯❯ ruby cosine_similarity.rb
kotaro_isaka - keigo_higashino = 0.9467292624062573
kotaro_isaka - haruki_murakami = 0.7807200583588264
kotaro_isaka - j_k_rowling = 0.5449492609130661
keigo_higashino - haruki_murakami = 0.7559289460184544
keigo_higashino - j_k_rowling = 0.30151134457776363
haruki_murakami - j_k_rowling = 0.6837634587578276
伊坂幸太郎
好きの集団
東野圭吾
好きの集団
村上春樹
好きの集団
J.K.ローリング
好きの集団
伊坂幸太郎
好きの集団
1 0.95 0.78 0.54
東野圭吾
好きの集団
- 1 0.76 0.30
村上春樹
好きの集団
- - 1 0.68
J.K.ローリング
好きの集団
- - - 1

同じ集団の結果は、類似度1になります。
伊坂幸太郎と東野圭吾が0.95と高い類似性を示し、東野圭吾とJ.K.ローリングが0.30という低い類似性を示しています。

何がうれしいのか

例えば伊坂幸太郎好きの集団に対し、彼に関するおすすめ情報を送りたい際、類似度が高い東野圭吾好きの集団にも送るとします。
その場合、同じ人を含んでいますが、伊坂幸太郎を好きになるかもしれない・好きと宣言し忘れている「東野圭吾好きの集団」の潜在層にも、アプローチしやすいのではないかと考えました。
また、意外な作家同士の類似性も見えてきたり、何かしらのコラボレーションを考えたりするのも楽しいです。

おわりに

一旦Rubyで全てまかなってしまいましたが、かなりの量のデータをテーブル整形から行ったので、解析に計6時間かかりました。今のところスピードを求められることは無いのですが、今後は静的言語でバッチを回す等の対策が必要かもしれません。
何がうれしいのか、については確信を持っては言えないのでご意見があればお願い致します。
このように、普段のアプリケーションの開発だけではなく、新しいことを提案して開発し、ビジネスに繋がる(かも)ことが面白いですね。