simHash 文档指纹去重算法

1.simHash算法过程:

参考论文来源 《Similarity estimation techniques from rounding algorithms》 。 介绍下这个算法主要原理,为了便于理解尽量不使用数学公式,分为这几步(标准做法):

  • 1、分词,把需要判断文本分词形成这个文章的特征单词。最后形成去掉噪音词的单词序列并为每个词加上权重,我们假设权重分为5个级别(1~5)。比如:“ 美国“51区”雇员称内部有9架飞碟,曾看见灰色外星人 ” ==> 分词后为 “ 美国(4) 51区(5) 雇员(3) 称(1) 内部(2) 有(1) 9架(3) 飞碟(5) 曾(1) 看见(3) 灰色(4) 外星人(5)”,括号里是代表单词在整个句子里重要程度,数字越大越重要。

  • 2、hash,通过hash算法把每个词变成hash值,比如“美国”通过hash算法计算为 100101,“51区”通过hash算法计算为 101011。这样我们的字符串就变成了一串串数字,还记得文章开头说过的吗,要把文章变为数字计算才能提高相似度计算性能,现在是降维过程进行时。

  • 3、加权,通过 2步骤的hash生成结果,需要按照单词的权重形成加权数字串,比如“美国”的hash值为“100101”,通过加权计算为“4 -4 -4 4 -4 4”;“51区”的hash值为“101011”,通过加权计算为 “ 5 -5 5 -5 5 5”。

  • 4、合并,把上面各个单词算出来的序列值累加,变成只有一个序列串。比如 “美国”的 “4 -4 -4 4 -4 4”,“51区”的 “ 5 -5 5 -5 5 5”, 把每一位进行累加, “4+5 -4+-5 -4+5 4+-5 -4+5 4+5” ==》 “9 -9 1 -1 1 9”。这里作为示例只算了两个单词的,真实计算需要把所有单词的序列串累加。

  • 5、降维,把4步算出来的 “9 -9 1 -1 1 9” 变成 0 1 串,形成我们最终的simhash签名。 如果每一位大于0 记为 1,小于0 记为 0。最后算出结果为:“1 0 1 0 1 1”。

完整的算指纹的算法:

  • 按照网上资料或者书本上常见的做法,权重为词频
    // 传入分词与词频
    public long simHash(Map<String, Integer> tokenWeightMap) {
        int[] bits = new int[64];
        tokenWeightMap.forEach((token, weight) -> {
            long v = MurmurHash.hash64(token);
            for (int c = 0; c < 64; c++) {
                bits[c] += (v & (1l << c)) == 0 ? -weight : weight;
            }
        });
        long simHash = 0;
        for (int c = 0; c < 64; c++) {
            long t = ((bits[c] >= 0) ? 1 : 0);
            t <<= c;
            simHash |= t;
        }
        return simHash;
    }

按照这种市面上的通用做法,传入的map 可以是无序的

  • nlp-cn 的做法
    nlp-cn 默认采用murmur64的hash算法
    private static final int BYTE_LEN = 64;

    private static final long[] BITS = new long[BYTE_LEN];

    static {
        BITS[0] = 1;
        for (int i = 1; i < BITS.length; i++) {
            BITS[i] = BITS[i - 1] * 2;
        }
    }
    public long fingerprint(String content) {
        int[] values = new int[BYTE_LEN];
        for (String word : analysis(content)) {
            long hashCode = hash(word);
            for (int i = 0; i < BYTE_LEN; i++) {
                if ((hashCode & BITS[i]) != 0) {
                    values[BYTE_LEN - 1 - i]++;
                } else {
                    values[BYTE_LEN - 1 - i]--;
                }
            }
        }

        long result = 0;

        for (int i = 0; i < BYTE_LEN; i++) {
            if (values[i] > 0) {
                result = result | BITS[BYTE_LEN - 1 - i];
            }
        }

        return result;
    }

有一个小问题提请注意
直接用 1<<n 得到 2 的n 次方到31次方以上就会超出int 的取值范围,
运算时直接用 (long)1<<n 或者 1l << n 就行了
所以 nlp-cn 的算法可以简化如下:

    public long fingerprint(String content) {
        int[] values = new int[64];
        for (String word : analysis(content)) {
            long hashCode = MurmurHash.hash64(word);
            for (int i = 0; i < 64; i++) {
                if ((hashCode & 1l << i) != 0) {
                    values[64 - 1 - i]++;
                } else {
                    values[64 - 1 - i]--;
                }
            }
        }
        long simHash = 0;
        for (int i = 0; i < 64; i++) {
            if (values[i] > 0) {
                simHash = simHash | (1l << (64 - 1 - i));
            }
        }
        return simHash;
    }
  • 比较海明距离
public int hmDistance(long a, long b) {
        return Long.bitCount(a ^ b);
    }

两种方式的比较:

2.simHash 算法去重时时间复杂度问题:

2.1 分桶

这里先引入一个概念: 抽屉原理

桌上有十个苹果,要把这十个苹果放到九个抽屉里,无论怎样放,我们会发现至少会有一个抽屉里面至少放两个苹果。这一现象就是我们所说的“抽屉原理”。 抽屉原理的一般含义为:“如果每个抽屉代表一个集合,每一个苹果就可以代表一个元素,假如有n+1个元素放到n个集合中去,其中必定有一个集合里至少有两个元素。” 抽屉原理有时也被称为鸽巢原理。它是组合数学中一个重要的原理。

假设我们要寻找海明距离3以内的数值,根据抽屉原理,只要我们将整个64位的二进制串划分为4块,无论如何,匹配的两个simhash code之间至少有一块区域是完全相同的,如下图所示:

不同的比特位至多分布在三个区域

由于我们无法事先得知完全相同的是哪一块区域,因此我们必须采用存储多份table的方式。在本例的情况下,我们需要存储4份table,并将64位的simhash code等分成4份;对于每一个输入的code,我们通过精确匹配的方式,查找前16位相同的记录作为候选记录,如下图所示:


分桶哈希

让我们来总结一下上述算法的实质:
假定我们最大判重海明距离为MAX_HD
1、将64位的二进制串等分成MAX_HD+1块
2、PUT操作:调整上述64位二进制,将任意一块作为前16位,总共有MAX_HD+1种组合,生成MAX_HD+1份table
3、GET操作:采用精确匹配的方式在MAX_HD+1份table中查找前16位,若找不到,说明不重复,做PUT操作;若找到了,则对剩余链做海明距离计算。
4、如果样本库中存有2^34 (差不多10亿)的哈希指纹,则每个table返回2^(34-16)=262144个候选结果,大大减少了海明距离的计算成本

为何要分桶?
两个字符串通过SimHash码和海明距离比较好判断是否相似,假设计算海明距离的时间为基本操作时间。如果有海量的数据,一一比较计算的次数为 1 + 2 + 3 + ......+ n ,时间复杂度为 O(n^2) 级别。这样的时间复杂度肯定是不能接受的。

2.1 索引

构建索引

List<Long>[] lists = new List[2048];

将SimHashCode添加到索引

    public void addHashCode(long hash) {
            int[] indexs = makeCodeIndex(hash);

            for (int i = 0; i < indexs.length; i++) {
                int idx = indexs[i];
                if (lists[idx] == null) {
                    lists[idx] = new ArrayList<Long>();
                }
                lists[idx].add(hash);
            }

        }

        private int[] makeCodeIndex(long hashCode) {
            return new int[]{
                    (int) (hashCode & (BITS[8] - 1)),
                    (int) ((hashCode >>> 8) & (BITS[8] - 1) + 256), 
                    (int) ((hashCode >>> 16) & (BITS[8] - 1) + 512),
                    (int) ((hashCode >>> 24) & (BITS[8] - 1) + 768), 
                    (int) ((hashCode >>> 32) & (BITS[8] - 1) + 1024), 
                    (int) ((hashCode >>> 40) & (BITS[8] - 1) + 1280),
                    (int) ((hashCode >>> 48) & (BITS[8] - 1) + 1536), 
                    (int) ((hashCode >>> 56) & (BITS[8] - 1) + 1792)};
        }

查询与索引库中比较的最近的海明距离

public int nearest(long hashCode) {
            int[] indexs = makeCodeIndex(hashCode);

            int hmDistance = 64;
            for (int i = 0; i < indexs.length; i++) {
                List<Long> list = lists[indexs[i]];
                if (list != null) {
                    for (Long hc : list) {
                        hmDistance = Math.min(hmDistance(hashCode, hc), hmDistance);
                    }
                    if (hmDistance == 0) {
                        return hmDistance;
                    }
                }
            }
            return hmDistance;
        }

其中 bit[n] = 2^n ,索引降低比较时算法时间复杂度的方法是
将SimHashCode 比特位分成8段


SimHash分段示意
  • 先将第一段与上 2^8 -1 即 0111 1111,得出低七位的值作为第一个索引,将SimHash值放置到索引list中。

  • 将SimHash逻辑右移8位,拿低2段的值与上 2^8 -1 即 1111 1111,再补上第八位1,作为第二个索引值,将SimHash值放置到索引list中。

  • 以此类推,将SimHash 的分布作为特征建立了八个索引,查询的时候先查索引,再去索引里比较海明距离。

其实这里也是用上了抽屉原理的,各位看官自己思考下吧。

3.simHash 算法去重的基础:分词

分词 -->另写一篇博客说明

需要说明的一点: 分词的时候需要去掉停用词等噪音词,分词是该算法特征抽取的关键一步。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,744评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,505评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,105评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,242评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,269评论 6 389
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,215评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,096评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,939评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,354评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,573评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,745评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,448评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,048评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,683评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,838评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,776评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,652评论 2 354

推荐阅读更多精彩内容