搜索引擎

simHash 文档指纹去重算法

2018-04-18  本文已影响89人  SHAN某人

1.simHash算法过程:

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

完整的算指纹的算法:

    // 传入分词与词频
    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 可以是无序的

    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分段示意

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

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

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

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

上一篇下一篇

猜你喜欢

热点阅读