Rubyでb-Bit MinHashを実装してみた。
昨日に引き続き、SmartNews開発者ブログの「b-Bit MinHashによる高速かつ省スペースな類似度判定」を参考に、Rubyでb-Bit MinHashを実装してみた。
ビット数は1、ハッシュ関数の個数は128で計算させている。
ただし、1ビット値は最初から50%の確率で衝突するので、単に一致した回数をkで割っただけでは、真のJaccard係数が0の場合でも0.5くらいの推定値が出てきてしまいます。
とのことなので、一致数をハッシュ関数の個数で割った値から0.5を引いて2倍したものを表示させてみた。(calc_jaccardのコメントを外すと表示される)
だいたい0.4に近い値が出るので多分合っているはず。
ベンチマーク的にはMinHashと大差がなかったけれど、popcountの処理をきちんと実装すれば速くなるかもしれない。
ソースコード
require 'murmurhash3' require 'benchmark' def init_seeds(num) seeds = Array.new num.times do seeds << Random.rand(999999999) end seeds end def calc_bbitminhash(targets, seed) hash_values = Array.new targets.each do |str| hash_values << MurmurHash3::V32.str_hash(str, seed) end hash_values.sort[0] & 0b00000001 end def calc_jaccard(str_a, str_b) k = 128 seeds = init_seeds(k) bits_a = 0 bits_b = 0 seeds.each do |seed| bits_a <<= 1; bits_a |= calc_bbitminhash(str_a, seed) bits_b <<= 1; bits_b |= calc_bbitminhash(str_b, seed) end k - (bits_a ^ bits_b).to_s(2).count("1") #2.0 * ((k - (bits_a ^ bits_b).to_s(2).count("1")) / k.to_f - 0.5) end puts Benchmark.measure { str_a = %w(巨人 中井 左膝 靭帯 損傷 登録 抹消) str_b = %w(中井 左膝 登録 抹消 歩行 問題) i = 10 sum = 0.0 i.times do jaccard = calc_jaccard(str_a, str_b) sum += jaccard puts jaccard end puts "avg = #{(sum / i.to_f)}" }
実行結果
% bundle exec ruby bbitminhash.rb 84 94 97 101 90 92 100 85 95 91 avg = 92.9 0.010000 0.000000 0.010000 ( 0.005646) # 補正後 0.46875 0.484375 0.4375 0.484375 0.515625 0.546875 0.328125 0.296875 0.578125 0.40625 avg = 0.4546875 0.000000 0.000000 0.000000 ( 0.005636)
まとめ
MinHashとb-Bit MinHashをRubyで実装してみた。
前にLSHを触ったことがあるからか、あまり違和感なく実装することができた。 (ちょっと調べてみたところ、ベクトルの近さの指標にjaccard距離を用いたLSH=MinHashであるらしい。)
SmartNews開発者ブログにも書いてあるけれど、本文抽出や特徴語抽出などの前処理の結果が類似度の計算には結構響いてくると思う。
そういう部分の実装に各サービスのノウハウがありそう。
内部の仕組みを想像しながら勉強すると理解が深まりそうでよさげな感じがする。