ruby で短い文章の cos類似度を計算してみる

cos

RSSリーダーを開発しています(嘘)

色々な記事を収集すると、必ず同じ内容だけど配信元が違う という問題にあたります。
例えば、
①:羽生 練習中に村上と接触し互いに転倒/スポーツ/ABCDスポーツ online”
②:羽生が村上と衝突し転倒、笑顔で練習再開も一時騒然 – フィギュア : XXスポーツ

という2つのtitleは普通の人なら同じ内容のコンテンツだという事を推定できますが、それを今回ruby先輩にやってもらいたいという内容です。

文章の類似度を計算するにはCos類似度の計算がこの世の主流だそうですので早速試すことに。
ちなみにだいたいの大枠は秀逸な後輩である@sonoshou先生に教えいただきました。 ありがとうございます。

概要

A:今夜が、山田。
B:山田の、山だ。

↓ 形態素解析(MeCab)

A:[ 今夜 が 、 山田 ]
B:[ 山田 の 、 山 だ ]

↓ ユニークな成分を全て抽出

[ 今夜 が 、 山田 の 山 だ ]

↓ 各成分があるか、無いかでベクトル化

A:[ 1 1 1 1 0 0 0 ]
B:[ 0 0 1 1 1 1 1 ]

で、このベクトルの内積を計算することとなります。
b2643de4-abf7-11e5-9f28-236f98342882

今回の類似度は
0.5477225575051661
でした。

現時点では、そもそも「今夜が山田」という文章の意味が全然わからないので類似してると言われても
「・・・」
ですね。

コサイン類似度計算をする前に成分の重み付けをするTF-IDFという手法がある事も初めてしりましたがコンテンツのタイトルだけで判定するという事を考えると、同じ単語が複数回タイトルに出現するのはまず無いだろうと思い使いませんでした。

書いたソース

今回はほぼ計算内容として下記のブログに書いてあるものと同じ事をやっています。
http://qiita.com/tabachain/items/77a9b0a049ed5b8e5650

これをちょっとシンプルにしてみた。
(MeCabのラッパーはnattyに変更)

require 'natto'
require 'matrix'

module CosSimilarity

  def calculate(text1, text2)
    a1 = break_up(text1)
    a2 = break_up(text2)
    uniq_words = (a1 + a2).uniq

    f1 = make_flags(uniq_words, a1)
    f2 = make_flags(uniq_words, a2)

    v1 = Vector.elements(f1, copy = true)
    v2 = Vector.elements(f2, copy = true)

    return v2.inner_product(v1)/(v1.norm() * v2.norm())
  end

  def break_up(text)
    arr = Array.new
    nm = Natto::MeCab.new
    nm.parse(text) do |n|
      arr.push(n.surface)
    end
    arr
  end

  def make_flags(uniq_words, elements)
    frags = []
    uniq_words.each do |word|
      flag = elements.include?(word) == true ? 1 : 0
      frags.push(flag)
    end
    frags
  end

end

結果①

include CosSimilarity
text1 = “羽生 練習中に村上と接触し互いに転倒/スポーツ/ABCDポーツ online”
text2 = “安仁屋氏臨時コーチ!緒方監督直々要請/野球/ABCDスポーツ online”
text3 = “羽生が村上と衝突し転倒、笑顔で練習再開も一時騒然 – フィギュア : XXスポーツ”
text4 = “久保建英、飛び級で東京U18へ 中3Jデビューも – サッカー : XXスポーツ”

puts CosSimilarity.calculate(text1, text2) # => 0.29411764705882354
puts CosSimilarity.calculate(text1, text3) # => 0.4234048992199705
puts CosSimilarity.calculate(text1, text4) # => 0.15512630699850574
puts CosSimilarity.calculate(text3, text4) # => 0.37219368415938836

1と3が類似記事だという事が判定したいのが目標なので、まぁまぁ結果出てますね。
けど、3と4の記事もかなりスコアが高めに出てしまうのが気になります。

もっと高い精度で判定できないか試してみます。

品詞に分解したあとに、採用する文字列をフィルタしてみる

Natto::MeCabクラスのインスタンスメソッドの parseで回しているところで品詞をみてみた
nm.parse(text) do |n|
puts “#{n.surface}\t#{n.feature}
end

羽生 名詞,固有名詞,人名,姓,*,*,羽生,ハブ,ハブ
  記号,空白,*,*,*,*, , ,
練習 名詞,サ変接続,*,*,*,*,練習,レンシュウ,レンシュー
中 名詞,接尾,副詞可能,*,*,*,中,チュウ,チュー
に 助詞,格助詞,一般,*,*,*,に,ニ,ニ
村上 名詞,固有名詞,人名,姓,*,*,村上,ムラカミ,ムラカミ
と 助詞,格助詞,一般,*,*,*,と,ト,ト
接触 名詞,サ変接続,*,*,*,*,接触,セッショク,セッショク
し 動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
互いに 副詞,一般,*,*,*,*,互いに,タガイニ,タガイニ
転倒 名詞,サ変接続,*,*,*,*,転倒,テントウ,テントー
/ 名詞,サ変接続,*,*,*,*,*
スポーツ 名詞,一般,*,*,*,*,スポーツ,スポーツ,スポーツ
/ 名詞,サ変接続,*,*,*,*,*
ABCD 名詞,一般,*,*,*,*,*
ポーツ 名詞,一般,*,*,*,*,*
online 名詞,固有名詞,組織,*,*,*,*
BOS/EOS,*,*,*,*,*,*,*,*

これを見ても、まず動詞や副詞は不要なんじゃないか? と思って除外してみる。
あと今回の場合、’:’ や ‘-‘ 空白の ‘ ’ など記号情報が邪魔だなと思いつつ、弾きたいけど
これも名詞として解析されとるので、 最後の読みが * になっていると記号と判断し判定の対象から除外してみました。

正確に言うと本当は「読点」とかが「記号」と判定される模様。
逆にアルファベットも ‘*’ とよみが出力されるので判定対象から外れてしまうので注意。

最終的にこのようにしてみた↓

require 'natto'
require 'matrix'

module CosSimilarity

  def calculate(text1, text2)
    a1 = break_up(text1)
    a2 = break_up(text2)
    uniq_words = (a1 + a2).uniq

    f1 = make_flags(uniq_words, a1)
    f2 = make_flags(uniq_words, a2)

    v1 = Vector.elements(f1, copy = true)
    v2 = Vector.elements(f2, copy = true)

    return v2.inner_product(v1)/(v1.norm() * v2.norm())
  end

  def break_up(text)
    arr = Array.new
    nm = Natto::MeCab.new
    nm.parse(text) do |n|
      surface = n.surface
      feature = n.feature.split(',')

      # 品詞が名刺、かつ記号っぽくなければ採用
      if feature.first == "名詞" && feature.last != '*'
        arr.push(surface)
      end
    end
    arr
  end

  def make_flags(uniq_words, elements)
    frags = []
    uniq_words.each do |word|
      flag = elements.include?(word) == true ? 1 : 0
      frags.push(flag)
    end
    frags
  end

end

結果②

text1 = “羽生 練習中に村上と接触し互いに転倒/スポーツ/ABCDポーツ online”
text2 = “安仁屋氏臨時コーチ!緒方監督直々要請/野球/ABCDスポーツ online”
text3 = “羽生が村上と衝突し転倒、笑顔で練習再開も一時騒然 – フィギュア : XXスポーツ”
text4 = “久保建英、飛び級で東京U18へ 中3Jデビューも – サッカー : XXスポーツ”

puts CosSimilarity.calculate(text1, text2) # => 0.11396057645963795
puts CosSimilarity.calculate(text1, text3) # => 0.5698028822981898
puts CosSimilarity.calculate(text1, text4) # => 0.1259881576697424
puts CosSimilarity.calculate(text3, text4) # => 0.10050378152592121

え、めっちゃ改善されてるんじゃないのこれ。
そして text1 と text3の類似度も向上してますね。 ナイス。
ただし そこそこ文章の長さがないと抽出された成分が1個でも合ってたら急激に類似度が増えるので注意ですね。

2015-12-30 | Posted in Ruby1 Comment » 


関連記事

コメント1件

 Lina | 2016.07.11 17:33

Evernoye would benefit from reading this post

Comment





Comment



*