@evilking
2018-04-30T10:56:02.000000Z
字数 4803
阅读 1292
SimHash
NLP
SimHash本篇介绍一个文本去重算法,叫SimHash,包括算法原理及java源码分析;最后再简要介绍一下其他相关去重方法.SimHash原理简介simhash 是 google 用来处理海量文本去重的算法。simhash最牛逼的一点就是将一个文档,最后转换成一个64位的字节,暂且称之为特征字,然后判断重复只需要判断他们的特征字的距离是不是 < n(根据经验这个n一般取值为3),就可以判断两个文档是否相似。simhash 本质上是一种LSH,它是局部敏感的;所谓局部敏感是指非常相似的文本,即便只差一个字符,MD5 后的序列也可能非常不同,但是simhash之后的序列可能只是某几位不同;这样对于不同的文本来说,可能就几个字不同,使用simhash就能判断文本是否相似,而传统 MD5或者 hashcode就不行了。算法描述这里直接给出算法过程的 5 步描述 : 分词,hash,加权,合并,转换.分词分词是把需要去重的两个文本先通过分词工具分词,然后去掉噪音词,并给每个词加上权重(此数字越大代表这个词对文本越重要,一般可以设置为词频,或者TF-IDF也行).例如:"美国"51区"雇员称内部有9架飞碟,曾看见灰色外星人",分词之后为 "美国(4) 51区(5) 雇员(3) 称(1) 内部(2) 有(1) 9架(3) 飞碟(5) 曾(1) 看见(3) 灰色(4) 外星人(5)",括号里的数字表示权重.hash此过程是把上一步分词后的每个单词,通过hash算法转换成 hash值,这里指定hashcode的长度,用来控制hash散列的精度.比如"美国"通过hash算法计算为"100101","51区"通过hash算法计算为"101011";这样我们的字符串就变成了一串串固定长度的数字,提高了相似度的计算性能.加权通过上两步的操作结果,需要按照单词的对应位加权,形成加权数字串.比如 "美国"的hash值为"100101",通过加权计算为"4 -4 -4 4 -4 4";"51区"的hash值为"101011",通过加权计算为"5 -5 5 -5 5 5";合并把上步各个单词算出来的序列值按对应位进行累加,合并成一个序列串.比如"美国"的"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";
这里作为示例只算了两个单词的,实际情况下要算所有单词的序列串累加
转换把上步算出来的序列值"9 -9 1 -1 1 9"转换成 0 1 串,形成我们最终的simhash签名;其中每一位大于0 就记为 1,小于 0 记为 0;比如上述例子中,"9 -9 1 -1 1 9"就转换为了"1 0 1 0 1 1"汉明距离是计算两个等长字符串对应位不同的数量.
比如"101011"与"100101"的汉明距离为 3
public static void main(String[] args) {
String test1 = "aaaaaaaaaaaaaaaaaaaa";
String test2 = "aaaaaaaaaajfljaslfjaslddsjflasdjaaaaaz";
//初始化Simhash工具
//这里主要是为了初始化其中的分词工具
Simhash simHash = new Simhash(new BinaryWordSeg());
//计算 64位的simhash
long docHash = simHash.simhash64(test1);
long docHash2 = simHash.simhash64(test2);
//计算两 simhash的汉明距离
int dist = simHash.hammingDistance(docHash, docHash2);
System.out.println(dist);
}
2
public Simhash(IWordSeg wordSeg) {
this.wordSeg = wordSeg;
}
public long simhash64(String doc) {
//指定使用64位的hashcode
int bitLen = 64;
int[] bits = new int[bitLen];
//分词
List<String> tokens = wordSeg.tokens(doc);
for (String t : tokens) {
//对每个词计算 hashcode
long v = MurmurHash.hash64(t);
//这里是为了将整型数按位拆分成 0 1 串
//并以词频为权重,按位累加
for (int i = bitLen; i >= 1; --i) {
if (((v >> (bitLen - i)) & 1) == 1)
++bits[i - 1];
else
--bits[i - 1];
}
}
long hash = 0x0000000000000000;
long one = 0x0000000000000001;
for (int i = bitLen; i >= 1; --i) {
//每位大于0 的就设置为 1
if (bits[i - 1] > 1) {
hash |= one;
}
//
one = one << 1;
}
return hash;
}
这种是使用了倒排索引的方式进行了优化
笔者在想是否可以使用图数据库索引的方式来加快速度,simhash分隔的每一小块就是图数据库中的一个实体
Shingle算法也可作为句子相似度的计算方法
若想了解更多Shingle算法细节,请自行搜索.
Rabin指纹去重假设 X = ([b_1,b_2,\cdots,b_n]) 包含了 n 个二进制符号串,并且 b_1 = 1,那么构造一个 (n-1)阶的多项式 X(f)为:具体可参考《Rabin指纹去重算法在搜索引擎中的应用》这篇论文.
# 创建 worker启动器,计算simhash,取前两个关键词
> simhasher = worker("simhash",topn = 2)
> simhasher <= "你妈妈叫你回家吃饭啊"
$simhash</span></code></li><li class="L5"><code class="language-R"><span class="pun">[</span><span class="lit">1</span><span class="pun">]</span><span class="pln"> </span><span class="str">"5255740375710833686"</span></code></li><li class="L6"><code class="language-R"></code></li><li class="L7"><code class="language-R"><span class="pln">$keyword
6.55531 6.44422
"妈妈" "吃饭"
> simhasher <= "你妈妈喊你回家吃饭,回家喽"
$simhash</span></code></li><li class="L3"><code class="language-R"><span class="pun">[</span><span class="lit">1</span><span class="pun">]</span><span class="pln"> </span><span class="str">"9184284471008831268"</span></code></li><li class="L4"><code class="language-R"></code></li><li class="L5"><code class="language-R"><span class="pln">$keyword
12.3845 6.55531
"回家" "妈妈"
>
# 计算hamming distance
> distance("你妈妈喊你回家吃饭","你妈妈叫你回家吃饭啊",simhasher)
$distance</span></code></li><li class="L3"><code class="language-R"><span class="pun">[</span><span class="lit">1</span><span class="pun">]</span><span class="pln"> </span><span class="lit">0</span></code></li><li class="L4"><code class="language-R"></code></li><li class="L5"><code class="language-R"><span class="pln">$lhs
6.55531 6.44422
"妈妈" "吃饭"
$rhs
6.55531 6.44422
"妈妈" "吃饭"
>