simhash 性能测试

2018-07-06  本文已影响44人  Pope怯懦懦地

随便找了 4 篇同一主题的新闻:

序号 标题 备注
0 习近平在全国组织部长会议上的讲话 2011 年
1 习近平出席全国组织工作会议并发表重要讲话 2018 年
2 习近平出席全国组织工作会议并讲话 2018 年
3 习近平:切实贯彻落实新时代党的组织路线 全党努力把党建设得更加坚强有力 2018 年

注:文档 0 是 2011 年的新闻,而文档 1、2、3 都是 2018 年同一通稿的不同转载。

我们可以用「最长公共子串」(LCS)所占比例来估计两篇文档的相似程度:

min{lcs.size / document_1.size, lcs.size / document_2.size}

如果我们分别以「子句」(即以标点符号为切分)「单个汉字」和「词」(做分词处理)为单位计算 simhash :

比较文档 LCS 占比 simhash 值距离(子句) simhash 值距离(字) simhash 值距离(词)
0 v.s. 1 0.0966 32 8 8
0 v.s. 2 0.0966 32 8 7
0 v.s. 3 0.0991 32 8 7
1 v.s. 2 0.9756 6 0 1
1 v.s. 3 0.9552 6 0 1
2 v.s. 3 0.9612 2 0 0

可见,更小的粒度(单个汉字)并没有换回更显著的差异。可能是因为以小粒度计算 simhash 时,相同的维度会大幅增加,从而拉低了敏感性。

这也是 simhash 算法的一个缺陷:它并不是映射为一个 0 到 1 之间的「相似概率」,在判定是比较不好把握。虽然好像原论文是希望做成一个这样的概率映射。


下面我们对以上两种指标做敏感性分析。我们在同一样本中分别删去 1、2、4、8、16、32、64 个汉字,然后计算其指标:

比较文档 LCS 占比 simhash 值距离
0 v.s. 1 0.9874581939799331 2
0 v.s. 2 0.9871794871794872 2
0 v.s. 4 0.9866220735785953 3
0 v.s. 8 0.9855072463768116 3
0 v.s. 16 0.9832775919732442 3
0 v.s. 32 0.9788182831661093 1
0 v.s. 64 0.9698996655518395 3
1 v.s. 2 0.987454697518818 0
1 v.s. 4 0.9868971285196543 1
1 v.s. 8 0.985781990521327 1
1 v.s. 16 0.9835517145246724 1
1 v.s. 32 0.9790911625313633 3
1 v.s. 64 0.9701700585447449 3
2 v.s. 4 0.9871723368655884 1
2 v.s. 8 0.9860568878973787 1
2 v.s. 16 0.9838259899609593 1
2 v.s. 32 0.9793641940881205 3
2 v.s. 64 0.9704406023424428 3
4 v.s. 8 0.9866071428571429 2
4 v.s. 16 0.984375 2
4 v.s. 32 0.9799107142857143 4
4 v.s. 64 0.9709821428571429 4
8 v.s. 16 0.9854748603351955 0
8 v.s. 32 0.9810055865921787 2
8 v.s. 64 0.9720670391061452 4
16 v.s. 32 0.9832026875699889 2
16 v.s. 64 0.9742441209406495 4
32 v.s. 64 0.9786276715410573 4
可见 simhash 对微小差异并不敏感

注:以上是手工删除的结果,删除的是多处连续的小段文字。

下面我们试试随机删除的效果:

比较文档 LCS 占比 simhash 值距离
0 v.s. 1 0.9874581939799331 3
0 v.s. 2 0.9871794871794872 2
0 v.s. 4 0.9866220735785953 3
0 v.s. 8 0.9855072463768116 5
0 v.s. 16 0.9832775919732442 8
0 v.s. 32 0.9788182831661093 9
0 v.s. 64 0.9701783723522854 7
0 v.s. 128 0.9531772575250836 18
0 v.s. 256 0.9186176142697882 21
0 v.s. 512 0.85479375696767 33
随机删除测试

注:样本文档为 3550 字。

重复测试:

测试 1 测试 2 测试 3 测试 4 测试 5

由上测试可以得到以下经验法则:


大规模随机测试(从样本文档(3588 字)中随机删除任意个字符,而后计算其与原文档的 simhash 距离):


横轴是「删除的字符数」,纵轴是「simhash 距离」(为避免作图点重合,y 值都做了漂移)

由上图可以观察到如下经验规律:

如果我们把差异量控制在小范围内,再计算其相关系数,得到如下曲线:

图为:差异在 x 以内的样本的相关系数。x-轴单位为百

可以看到:当我们把差异量控制在 400 字以内(差异小于 11%)时,能得到 80% 以上的相关度。


但实际中,并不会随机删除,而是连续的小段文字删减。下面我们测试一下,以「子句」为单位作少量(20 句以内)、随机删除的效果:

横轴是「删除的字符数」,纵轴是「simhash 距离」(为避免作图点重合,y 值都做了漂移)

我不想说啥了,和「 LCS 占比」的相关系数为 -0.62 。

然后我还不甘心,想着算 simhash 时是以标点符号为分隔切分的。如果用上分词算法会不会好一点呢?接着试了下以「词」为单位的效果:

以「词」为单位的效果,还不如以「子句」为单位。和「 LCS 占比」的相关系数为 -0.34

心塞,这次真的不想说啥了。

实际情况也不会全是块删除,而是点删除与块删除的混合。可以想见,效果应该介于上述两种情况之间。

但反过来看,这个算法对判定相似文本是相当成功的(删掉快 300 个字依旧能保证距离在 4 以内)。好吧,simhash 本来就是为相似文本而生的,你却硬要她去做 MD5 的事情。


以下是垃圾代码,存个档。

require "diff/lcs"
require "simhash"
require 'rmmseg'

class String
    RMMSeg::Dictionary.load_dictionaries

    def strip_heredoc
        self.gsub(/[\t\n\p{Z}]/, '')
    end

    def random_delete(n)
        ary = self.split(/\p{P}/)
        n.times do
            ary.delete_at(rand(ary.size))
        end
        ary.delete('')
        ary.join("。")
    end

    def segments()
        algor = RMMSeg::Algorithm.new(self)
        seg = []
        loop do
            tok = algor.next_token
            break if tok.nil?
            seg << tok.text.force_encoding('utf-8')
        end
        seg
    end
end

def lcs_index(seq1, seq2)
    return 0 if seq1.empty? or seq2.empty?
    lcs = Diff::LCS.LCS(seq1.strip_heredoc, seq2.strip_heredoc)
    [lcs.size.to_f / seq1.size, lcs.size.to_f / seq2.size].min
end

def calc(doc, n)
    del_doc = doc.random_delete(n)
    sim_0 = Simhash.hash(doc.segments) # .split(/[\n\p{P}]/)
    sim_n = Simhash.hash(del_doc.segments)
    {
        count: doc.size - del_doc.size,
        lcs: lcs_index(doc, del_doc),
        simhash: sim_0.hamming_distance_to(sim_n)
    }
end

data = ""
doc = File.open("./sample.txt", "r").read.force_encoding('utf-8')
1000.times do
    h = calc(doc, rand(20))
    data += "#{h[:count]}\t#{h[:lcs]}\t#{h[:simhash] + rand / 8}\n"
end

File.open("./random_test.txt", "w") { |f| f << data }
上一篇下一篇

猜你喜欢

热点阅读