概要
前回、自分のツイートの TFIDF を算出してツイート内から特徴語を算出してみました
今回はさらに算出した TFIDF を使って各ツイートの類似度をコサイン類似度をクラスタリングしてみたいと思います
環境
- macOS 10.14.3
- Ruby 2.5.1p57
コサイン類似度を算出するスクリプト
前回、算出した各ツイートの単語ごとの TFIDF の結果を読み込んで算出します
bundle init
vim Gemfile
gem "natto"
bundle install --path vendor
vim cosine_similarity.rb
require 'json'
class CosSim
attr_accessor :all_tweet_count
def initialize
file = 'results.json'
@data = JSON.parse(File.read(file))
@all_tweet_count = @data.size
@results = []
end
def calc(index: 0)
@source = @data['results'][index]
@data['results'].each_with_index { |target, i|
r = Result.new(target['tweet'], cos(target))
@results.push(r)
}
end
def clear
@results.clear
end
def show_result(top = 10)
ret = {}
ret.store(:source, @source['tweet'])
rr = []
sorted = @results.sort { |a, b| b.cos <=> a.cos }.first(top)[1..-1]
sorted.each { |result|
r = {}
r.store(:target, result.tweet)
r.store(:cos, result.cos)
rr.push(r)
}
ret.store(:result, rr)
puts ret.to_json
end
def cos(target)
term_dimensions = []
@source['terms'].each { |t|
term_dimensions.push(t['term']) unless term_dimensions.include?(t['term'])
}
target['terms'].each { |t|
term_dimensions.push(t['term']) unless term_dimensions.include?(t['term'])
}
source_tfidf_map = []
target_tfidf_map = []
term_dimensions.each { |dim|
sterm = @source['terms'].select { |t| t['term'] == dim }.first
tterm = target['terms'].select { |t| t['term'] == dim }.first
if sterm.nil?
source_tfidf_map.push(0.0)
else
source_tfidf_map.push(sterm['tfidf'].to_f)
end
if tterm.nil?
target_tfidf_map.push(0.0)
else
target_tfidf_map.push(tterm['tfidf'].to_f)
end
}
a = Math.sqrt(source_tfidf_map.inject(0){ |sum, tfidf| sum + tfidf * tfidf })
b = Math.sqrt(target_tfidf_map.inject(0){ |sum, tfidf| sum + tfidf * tfidf })
ab = (target_tfidf_map.zip(source_tfidf_map).map { |t, s| t * s }).sum
cos = ab / (a * b)
end
class Result
attr_accessor :tweet, :cos
def initialize(tweet, cos)
@tweet = tweet
@cos = cos
end
end
end
cs = CosSim.new
10.times do
i = Random.rand(0..10015)
cs.calc(index: i)
cs.show_result(4)
cs.clear
end
少し解説
そもそもコサイン類似度は「ある文書 (ここではツイート) とある文書が似ている文書なのか」を算出することができる計算方法です
ツイートは全部で 10,000 ほどあるので単純にすべてのツイートに対してコサイン類似度を出そうとすると 10,000 * 10,000 = 1億回
の処理が必要になります
時間があるのであれば放置しておけばいいですが、そうもいきません
上記のスクリプトは cs.calc(index: 100)
で対象のツイートのインデックスを指定することできるのであるツイートに対して類似するツイートを 10,000 件の中から探すようになっています
もし全件やりたい場合は all_tweet_count
分メイン処理でループさせれば OK です
処理の流れとしてまず類似度を比較する文書 2 つの中に出現する単語のベクトルを作成します
ベクトルの大きさは TFIDF を採用します
イメージ的には以下の通りです
文書 | ギガ | の | 使用 | 量 | … |
---|---|---|---|---|---|
ツイート1 | 0.38 | 0 | 0.18 | 0.13 | … |
ツイート2 | 0.22 | 0 | 0.01 | 0 | … |
そのためにまず文書間で出現する単語のユーニク (term_dimensions
) を算出します
そしてその単語ごとに前回の結果から TFIDF を算出し source_tfidf_map
と target_tfidf_map
に追加します
単語が出現しない場合は 0.0 を追加します
あとは作成したそれぞれの map からコサイン類似度の計算に必要な値を算出します
Math.sqrt(source_tfidf_map.inject(0){ |sum, tfidf| sum + tfidf * tfidf })
は各単語の tfidf を二乗して、それらの sum を計算しています
更に計算された合計に対して平方根をとります
それを同様に target_tfidf_map
でも行います
(target_tfidf_map.zip(source_tfidf_map).map { |t, s| t * s }).sum
はそれぞれのマップの各要素の tfidf の積を計算しそれを sum しています
あとはこれらの値からコサイン類似度を求めます (cos = ab / (a * b)
)
ここで算出された値は -1 から 1 の間を取ります
1 に近ければ近いほど類似度が高くなります
結果を少し見てみる
先程も述べたようにすべてのツイートの類似度を見るのは時間がかかるので適当にサンプリングして見てみました
例えば以下の結果は「元気」というワードがどのツイートにも出てきており「似ていると言えば似ているな、、、」という感じなのがわかります
cos の数値を見ても 0.36 くらいなので 1 には遠いと考えるとニュアンス的にはそんな感じなのかなと思います
{
"source": "大学生、、、元気だなw",
"result": [
{
"target": "arduino ってまだまだ元気なのか、、なつかしい、、",
"cos": 0.36860955705104625
},
{
"target": "フィッツすっかり元気になっているじゃないか",
"cos": 0.32779605613840673
},
{
"target": "エシャロットって体に良いとかあるのかな。なんか食べた翌日は元気な気がする。たまたまかなw",
"cos": 0.2422238044523076
}
]
}
cos が 0.5 を超える類似度だと以下の通りです
確かに似ているような気がします、、、
{
"source": "storyboard 落ちるわー、、、",
"result": [
{
"target": "気持ち落ちるでぇ、、、",
"cos": 0.5008608639512765
},
{
"target": "なんか、storyboard もめっちゃ変わってるな",
"cos": 0.40529191248135504
},
{
"target": "立ち上がりはいつも落ちるなー。",
"cos": 0.39170065104194474
}
]
}
今回は 10 個しかサンプルリングしなかったので何とも言えませんが cos が 0.75 以上くらいで絞り込めばかなり似ているツイートを探せる印象です
ただ感じ的にはほとんどが 0.1 から 0.2 くらいだったので高スコアなペアを探せるのは結構レアなケースかなと思います
最後に
各ツイートごとに TFIDF を算出したのでコサイン類似度を使ってツイートのクラスタリングも行ってみました
この方法でも確かに似ているツイートは探せると思います
ただ今回の手法ではあくまでも「似ている」ツイートを探せているだけであり意味やコンテキストが似ているかどうかまでは判断できていません
例えばサンプリングの結果が見せた「storyboard 落ちるわー、、、」と「気持ち落ちるでぇ、、、」は確かにツイート的には似ていますがコンテキストとしては前者が Xcode に関するツイートで後者は体調や気持ちに関するツイートです
なので似てはいますが内容は全く違います
更に類似しているツイートを探したいのであれば今回クラスタリングした結果から更に TFIDF -> コサイン類似度を算出すれば良いと思います
TFIDF を算出した元データ (文書集合) がツイート全体だったので、文書集合をクラスタリング単位にすれば結果も変わってくると思います
そもそも Twitter 自体全く同じツイートはできないのはなかなか難しいですが、おもしろいアプローチかなと思います (定石かもしれませんが)
0 件のコメント:
コメントを投稿