使用simhash计算文本相似度
Java RESTful API — HanLP Documentation
demo

import com.hankcs.hanlp.seg.common.Term; import com.hankcs.hanlp.tokenizer.StandardTokenizer; import org.apache.commons.lang3.StringUtils; import org.jsoup.Jsoup; import org.jsoup.safety.Whitelist; import java.math.BigInteger; import java.util.HashMap; import java.util.List; import java.util.Map; public class MySimHash { private String tokens; //字符串 private BigInteger strSimHash;//字符产的hash值 private int hashbits = 64; // 分词后的hash数; public MySimHash(String tokens) { this.tokens = tokens; this.strSimHash = this.simHash(); } private MySimHash(String tokens, int hashbits) { this.tokens = tokens; this.hashbits = hashbits; this.strSimHash = this.simHash(); } /** * 清除html标签 * * @param content * @return */ private String cleanResume(String content) { // 若输入为HTML,下面会过滤掉所有的HTML的tag content = Jsoup.clean(content, Whitelist.none()); content = StringUtils.lowerCase(content); String[] strings = {" ", "\n", "\r", "\t", "\r", "\n", "\t", " "}; for (String s : strings) { content = content.replaceAll(s, ""); } return content; } /** * 这个是对整个字符串进行hash计算 * * @return */ private BigInteger simHash() { tokens = cleanResume(tokens); // cleanResume 删除一些特殊字符 int[] v = new int[this.hashbits]; List<Term> termList = StandardTokenizer.segment(this.tokens); // 对字符串进行分词 //对分词的一些特殊处理 : 比如: 根据词性添加权重 , 过滤掉标点符号 , 过滤超频词汇等; Map<String, Integer> weightOfNature = new HashMap<String, Integer>(); // 词性的权重 weightOfNature.put("n", 2); //给名词的权重是2; Map<String, String> stopNatures = new HashMap<String, String>();//停用的词性 如一些标点符号之类的; stopNatures.put("w", ""); // int overCount = 5; //设定超频词汇的界限 ; Map<String, Integer> wordCount = new HashMap<String, Integer>(); for (Term term : termList) { String word = term.word; //分词字符串 String nature = term.nature.toString(); // 分词属性; // 过滤超频词 if (wordCount.containsKey(word)) { int count = wordCount.get(word); if (count > overCount) { continue; } wordCount.put(word, count + 1); } else { wordCount.put(word, 1); } // 过滤停用词性 if (stopNatures.containsKey(nature)) { continue; } // 2、将每一个分词hash为一组固定长度的数列.比如 64bit 的一个整数. BigInteger t = this.hash(word); for (int i = 0; i < this.hashbits; i++) { BigInteger bitmask = new BigInteger("1").shiftLeft(i); // 3、建立一个长度为64的整数数组(假设要生成64位的数字指纹,也可以是其它数字), // 对每一个分词hash后的数列进行判断,如果是1000...1,那么数组的第一位和末尾一位加1, // 中间的62位减一,也就是说,逢1加1,逢0减1.一直到把所有的分词hash数列全部判断完毕. int weight = 1; //添加权重 if (weightOfNature.containsKey(nature)) { weight = weightOfNature.get(nature); } if (t.and(bitmask).signum() != 0) { // 这里是计算整个文档的所有特征的向量和 v[i] += weight; } else { v[i] -= weight; } } } BigInteger fingerprint = new BigInteger("0"); for (int i = 0; i < this.hashbits; i++) { if (v[i] >= 0) { fingerprint = fingerprint.add(new BigInteger("1").shiftLeft(i)); } } return fingerprint; } /** * 对单个的分词进行hash计算; * * @param source * @return */ private BigInteger hash(String source) { if (source == null || source.length() == 0) { return new BigInteger("0"); } else { /** * 当sourece 的长度过短,会导致hash算法失效,因此需要对过短的词补偿 */ while (source.length() < 3) { source = source + source.charAt(0); } char[] sourceArray = source.toCharArray(); BigInteger x = BigInteger.valueOf(((long) sourceArray[0]) << 7); BigInteger m = new BigInteger("1000003"); BigInteger mask = new BigInteger("2").pow(this.hashbits).subtract(new BigInteger("1")); for (char item : sourceArray) { BigInteger temp = BigInteger.valueOf((long) item); x = x.multiply(m).xor(temp).and(mask); } x = x.xor(new BigInteger(String.valueOf(source.length()))); if (x.equals(new BigInteger("-1"))) { x = new BigInteger("-2"); } return x; } } /** * 计算海明距离,海明距离越小说明越相似; * * @param other * @return */ private int hammingDistance(MySimHash other) { BigInteger m = new BigInteger("1").shiftLeft(this.hashbits).subtract( new BigInteger("1")); BigInteger x = this.strSimHash.xor(other.strSimHash).and(m); int tot = 0; while (x.signum() != 0) { tot += 1; x = x.and(x.subtract(new BigInteger("1"))); } return tot; } public double getSemblance(MySimHash s2) { double i = (double) this.hammingDistance(s2); return 1 - i / this.hashbits; } public static void main(String[] args) { String s1 = "张继群,23岁,临沂大学。"; String s2 = "我叫张继群,今年23岁,在临沂大学上学。"; String s3 = "我在临沂大学上学是张继群,今年23岁。"; String s4 = "11111111111111。"; MySimHash hash1 = new MySimHash(s1, 64); MySimHash hash2 = new MySimHash(s2, 64); System.out.println("======================================"); System.out.println(hash1.hammingDistance(hash2)); System.out.println(hash1.getSemblance(hash2)); System.out.println("======================================"); } }

汉明距离计算
主要的6个步骤为:分词、hash、加权、合并、降维、计算汉明距离,前5个步骤本质上是simhash算法的流程,思路很简单易懂。
分词
分词工具有很多,例如Ansj、JieBa、HanLP等等,本文使用的 是HanLP。
为了减小无关词的影响(降噪),可以将标点等过滤掉,并移除停用词,只留下有意义的词语。
hash(64位)
使用MurmurHash3.hash64(byte[] data)来得到词语的hash值。需要注意的是它返回的是Long类型,如果转换为二进制表示后不足64位,需要在补齐64位(高位补0)
加权
词频是衡量一个词语在句子中的常用方法之一,除此之外还有TF-IDF、词语的情感色彩值等等,本文使用词频作为词语的权重。
合并
对每个词语计算到的加权后的hash值按位求和
降维
对求和后的数据进行降维,即对于每一位,大于0的位变为1,小于0的位变为0,得到一个二进制数(或者字符串),即为最终的simhash值。
计算汉明距离
对以上步骤得到的两个simhash值计算其汉明距离,即统计两个64位的二进制数中对应位不同的个数(异或后1的个数),最终得到汉明距离。一般根据经验值,汉明距离小于等于3的即可认为相似。
汉明距离为一个整数,似乎不能很直观的反应两个文本的相似度(0 ~ 1),所以这里通过实验的方法找了条类似正态分布的函数来将汉明距离转化为一个0~1之间的数来表示相似度,更直观一些,方程如下:

在IDEA里gradle配置和使用
搜索对应的jar名称进行加入
建议:转用maven的项目使用pom.xml文件

在IDEA里gradle配置和使用,一般别使用同步gradle
在gradle 配置中dependencies 是不管用的,sync 的话之前添加的依赖都没有了

Gradle和maven都是构建工具
.gradle和pom.xml 是对应的配置文件



