文本相似度算法之 minHash

前言

在数据挖掘中,一个最基本的问题就是比较两个集合的相似度

通常通过遍历这两个集合中的所有元素,统计这两个集合中相同元素的个数,来表示集合的相似度;这一步也可以看成特征向量间相似度的计算(欧氏距离,余弦相似度)。

当这两个集合里的元素数量异常大(特征空间维数很大),同时又有很多个集合需要判断两两间的相似度时,传统方法会变得十分耗时,最小哈希(minHash)可以用来解决该问题。

Jaccard 相似度

在本例中,我们仅探讨集合的相似度,先来看 Jaccard 相似度。假设有两个集合A,B,则

Jaccard(A, B)= |A ∩ B| / |A ∪ B|

我们举一个例子:

集合相似度

在上述例子中,sim(A, B) = 2/7

minHash最小哈希

举例 AB 两个集合:

A = {s1, s3, s6, s8, s9}
B = {s3, s4, s7, s8, s10}

根据 Jaccard 公式,A, B 的相似度 S(A,B) = |A ∩ B| / |A ∪ B| = 2 / 8 = 0.25

当然直接计算两个集合的交集与并集,是很耗计算资源的,特别是在海量数据场景下不可行。

假如,我们随机从两个集合中各挑选一个元素 s(A)s(B),刚好这两个元素相同的概率是多少呢?
这个概率其实等同于,在 A ∪ B 这个大的随机域里,选中的元素落在 A ∩ B 这个区域的概率,这个概率就等于 Jaccard 的相似度!这就是 MinHash 的基本原理。

基于这一原理,我们找一个随机的哈希函数 h,对集合的每一个元素作哈希运算。
比如集合 A,可以算出 5 个 hash 值,因为是随机的,这 5 个 hash 值里值最小的那个元素,对于 A 集合中所有元素概率都是均等的。同样方法从 B 中取最小 hash 值2 个 minHash 相等的概率就是集合的相似度了。

我们只需要找到 N 个哈希函数,对集合生成一组 minHash,算两个集合的相似度,也就是这 2 组 minHash中,交集/并集了。
这个计算相对容易了,因为每个集合的元素数变成了常数 N,也就是说,minHash 其实是一种降维技术。

MinHash 其实是一种降维技术

代码演示

  1. 添加 Maven 依赖:
<dependency>
  <groupId>org.codelibs</groupId>
  <artifactId>minhash</artifactId>
  <version>0.2.0</version>
</dependency>
  1. 计算 minHash
import org.apache.commons.lang3.StringUtils;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.Tokenizer;
import org.apache.lucene.analysis.core.WhitespaceTokenizer;
import org.codelibs.minhash.MinHash;

import java.io.IOException;

public class MinHashDemo {
    public static void main(String[] args) throws IOException {
        // Lucene's tokenizer parses a text.
        Tokenizer tokenizer = new WhitespaceTokenizer();
        // The number of bits for each hash value.
        int hashBit = 1;
        // A base seed for hash functions.
        int seed = 0;
        // The number of hash functions.
        int num = 128;
        // Analyzer for 1-bit 32 hash with custom Tokenizer.
        Analyzer analyzer = MinHash.createAnalyzer(tokenizer, hashBit, seed, num);

        String text = "Fess is very powerful and easily deployable Enterprise Search Server.";

        // Calculate a minhash value. The size is hashBit*num.
        byte[] minhash = MinHash.calculate(analyzer, text);

        System.out.println(minhash.length); // 128 / 8 = 16
        System.out.println(StringUtils.join(minhash, ','));
    }
}

tips:1 byte = 8 bits

  1. 比较文本相似度

compare 方法返回文本之间的相似性。值从 01,但是值小于 0.5 表示不同的文本。

import lombok.SneakyThrows;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.Tokenizer;
import org.apache.lucene.analysis.core.WhitespaceTokenizer;
import org.codelibs.minhash.MinHash;

import java.io.IOException;

public class MinHashDemo {
    public static void main(String[] args) throws IOException {
        String text = "Fess is very powerful and easily deployable Enterprise Search Server.";
        byte[] minhash = calculateMinHash(text);

        String text1 = "Fess is very powerful and easily deployable Search Server.";
        byte[] minhash1 = calculateMinHash(text1);
        float score1 = MinHash.compare(minhash, minhash1);
        System.out.println(score1); // 0.953125

        // Compare a different text.
        String text2 = "Solr is the popular, blazing fast open source enterprise search platform";
        byte[] minhash2 = calculateMinHash(text2);
        float score2 = MinHash.compare(minhash, minhash2);
        System.out.println(score2); // 0.453125
    }
    
    @SneakyThrows
    private static byte[] calculateMinHash(String text) {
        // Lucene's tokenizer parses a text.
        Tokenizer tokenizer = new WhitespaceTokenizer();
        // The number of bits for each hash value.
        int hashBit = 1;
        // A base seed for hash functions.
        int seed = 0;
        // The number of hash functions.
        int num = 128;
        // Analyzer for 1-bit 32 hash with custom Tokenizer.
        Analyzer analyzer = MinHash.createAnalyzer(tokenizer, hashBit, seed, num);

        // Calculate a minhash value. The size is hashBit*num.
        return MinHash.calculate(analyzer, text);
    }
}

Lucene 分词器(拓展)

WhitespaceAnalyzer 演示代码:

import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.core.WhitespaceAnalyzer;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;

import java.io.IOException;

public class AnalyzerDemo {
    public static void main(String[] args) throws IOException {
        WhitespaceAnalyzer analyzer = new WhitespaceAnalyzer();

        String text = "The Lucene PMC is pleased to announce the release of the Apache Solr Reference Guide for Solr 4.4.";

        TokenStream tokenStream = analyzer.tokenStream("field", text);
        CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
        tokenStream.reset();
        while (tokenStream.incrementToken()) {
            System.out.println(charTermAttribute.toString());
        }
        tokenStream.end();
        tokenStream.close();
    }
}

输出:

The
Lucene
PMC
is
pleased
to
announce
the
release
of
the
Apache
Solr
Reference
Guide
for
Solr
4.4.

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容