您的位置:首页 > 其它

使用simhash以及海明距离判断内容相似程度

2015-09-29 11:57 796 查看

算法简介

SimHash也即相似hash,是一类特殊的信息指纹,常用来比较文章的相似度,与传统hash相比,传统hash只负责将原始内容尽量随机的映射为一个特征值,并保证相同的内容一定具有相同的特征值。而且如果两个hash值是相等的,则说明原始数据在一定概率下也是相等的。但通过传统hash来判断文章的内容是否相似是非常困难的,原因在于传统hash只唯一标明了其特殊性,并不能作为相似度比较的依据。

SimHash最初是由Google使用,其值不但提供了原始值是否相等这一信息,还能通过该值计算出内容的差异程度。

算法原理

simhash是由Charikar在2002年提出来的,参考《Similarityestimationtechniquesfromroundingalgorithms》。介绍下这个算法主要原理,为了便于理解尽量不使用数学公式,分为这几步:

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-44-44”;“51区”的hash值为“101011”,通过加权计算为“5-55-555”。

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

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

原理图:



我们可以来做个测试,两个相差只有一个字符的文本串,“你妈妈喊你回家吃饭哦,回家罗回家罗”和“你妈妈叫你回家吃饭啦,回家罗回家罗”。

通过simhash计算结果为:

1000010010101101111111100000101011010001001111100001001011001011

1000010010101101011111100000101011010001001111100001101010001011

通过比较差异的位数就可以得到两串文本的差异,差异的位数,称之为“海明距离”,通常认为海明距离<3的是高度相似的文本。

算法实现

这里的代码引用自博客:http://my.oschina.net/leejun2005/blog/150086,这里表示感谢。

代码实现中使用Hanlp代替了原有的分词器。

packagecom.emcc.changedig.extractengine.util;

/**
*Function:simHash判断文本相似度,该示例程支持中文<br/>
*date:2013-8-6上午1:11:48<br/>
*@authorjune
*@version0.1
*/
importjava.io.IOException;
importjava.math.BigInteger;
importjava.util.ArrayList;
importjava.util.HashMap;
importjava.util.List;

importcom.hankcs.hanlp.seg.common.Term;

publicclassSimHash
{
privateStringtokens;

privateBigIntegerintSimHash;

privateStringstrSimHash;

privateinthashbits=64;

publicSimHash(Stringtokens)throwsIOException
{
this.tokens=tokens;
this.intSimHash=this.simHash();
}

publicSimHash(Stringtokens,inthashbits)throwsIOException
{
this.tokens=tokens;
this.hashbits=hashbits;
this.intSimHash=this.simHash();
}

HashMap<String,Integer>wordMap=newHashMap<String,Integer>();

publicBigIntegersimHash()throwsIOException
{
//定义特征向量/数组
int[]v=newint[this.hashbits];

Stringword=null;
List<Term>terms=SegmentationUtil.ppl(this.tokens);
for(Termterm:terms)
{
word=term.word;

//将每一个分词hash为一组固定长度的数列.比如64bit的一个整数.
BigIntegert=this.hash(word);
for(inti=0;i<this.hashbits;i++)
{
BigIntegerbitmask=newBigInteger("1").shiftLeft(i);

//建立一个长度为64的整数数组(假设要生成64位的数字指纹,也可以是其它数字),
//对每一个分词hash后的数列进行判断,如果是1000...1,那么数组的第一位和末尾一位加1,
//中间的62位减一,也就是说,逢1加1,逢0减1.一直到把所有的分词hash数列全部判断完毕.
if(t.and(bitmask).signum()!=0)
{
//这里是计算整个文档的所有特征的向量和
//这里实际使用中需要+-权重,比如词频,而不是简单的+1/-1,
v[i]+=1;
}
else
{
v[i]-=1;
}
}
}

BigIntegerfingerprint=newBigInteger("0");
StringBuffersimHashBuffer=newStringBuffer();
for(inti=0;i<this.hashbits;i++)
{
//4、最后对数组进行判断,大于0的记为1,小于等于0的记为0,得到一个64bit的数字指纹/签名.
if(v[i]>=0)
{
fingerprint=fingerprint.add(newBigInteger("1").shiftLeft(i));
simHashBuffer.append("1");
}
else
{
simHashBuffer.append("0");
}
}
this.strSimHash=simHashBuffer.toString();
returnfingerprint;
}

privateBigIntegerhash(Stringsource)
{
if(source==null||source.length()==0)
{
returnnewBigInteger("0");
}
else
{
char[]sourceArray=source.toCharArray();
BigIntegerx=BigInteger.valueOf(((long)sourceArray[0])<<7);
BigIntegerm=newBigInteger("1000003");
BigIntegermask=newBigInteger("2").pow(this.hashbits).subtract(
newBigInteger("1"));
for(charitem:sourceArray)
{
BigIntegertemp=BigInteger.valueOf((long)item);
x=x.multiply(m).xor(temp).and(mask);
}
x=x.xor(newBigInteger(String.valueOf(source.length())));
if(x.equals(newBigInteger("-1")))
{
x=newBigInteger("-2");
}
returnx;
}
}

/**
*计算海明距离
*
*@paramother
*被比较值
*@return海明距离
*/
publicinthammingDistance(SimHashother)
{

BigIntegerx=this.intSimHash.xor(other.intSimHash);
inttot=0;

while(x.signum()!=0)
{
tot+=1;
x=x.and(x.subtract(newBigInteger("1")));
}
returntot;
}

publicintgetDistance(Stringstr1,Stringstr2)
{
intdistance;
if(str1.length()!=str2.length())
{
distance=-1;
}
else
{
distance=0;
for(inti=0;i<str1.length();i++)
{
if(str1.charAt(i)!=str2.charAt(i))
{
distance++;
}
}
}
returndistance;
}

publicList<BigInteger>subByDistance(SimHashsimHash,intdistance)
{
//分成几组来检查
intnumEach=this.hashbits/(distance+1);
List<BigInteger>characters=newArrayList<BigInteger>();

StringBufferbuffer=newStringBuffer();

for(inti=0;i<this.intSimHash.bitLength();i++)
{
//当且仅当设置了指定的位时,返回true
booleansr=simHash.intSimHash.testBit(i);

if(sr)
{
buffer.append("1");
}
else
{
buffer.append("0");
}

if((i+1)%numEach==0)
{
//将二进制转为BigInteger
BigIntegereachValue=newBigInteger(buffer.toString(),2);
buffer.delete(0,buffer.length());
characters.add(eachValue);
}
}

returncharacters;
}

publicstaticvoidmain(String[]args)throwsIOException
{
Strings="传统的hash算法只负责将原始内容尽量均匀随机地映射为一个签名值,"
+"原理上相当于伪随机数产生算法。产生的两个签名,如果相等,说明原始内容在一定概率下是相等的;"
+"如果不相等,除了说明原始内容不相等外,不再提供任何信息,因为即使原始内容只相差一个字节,"
+"所产生的签名也很可能差别极大。从这个意义上来说,要设计一个hash算法,"
+"对相似的内容产生的签名也相近,是更为艰难的任务,因为它的签名值除了提供原始内容是否相等的信息外,"
+"还能额外提供不相等的原始内容的差异程度的信息。";
SimHashhash1=newSimHash(s,64);

//删除首句话,并加入两个干扰串
s="原理上相当于伪随机数产生算法。产生的两个签名,如果相等,说明原始内容在一定概率下是相等的;"
+"如果不相等,除了说明原始内容不相等外,不再提供任何信息,因为即使原始内容只相差一个字节,"
+"所产生的签名也很可能差别极大。从这个意义上来说,要设计一个hash算法,"
+"对相似的内容产生的签名也相近,是更为艰难的任务,因为它的签名值除了提供原始内容是否相等的信息外,"
+"干扰1还能额外提供不相等的原始内容的差异程度的信息。";
SimHashhash2=newSimHash(s,64);

//首句前添加一句话,并加入四个干扰串
s="imhash算法的输入是一个向量,输出是一个f位的签名值。为了陈述方便,"
+"假设输入的是一个文档的特征集合,每个特征有一定的权重。"
+"传统干扰4的hash算法只负责将原始内容尽量均匀随机地映射为一个签名值,"
+"原理上这次差异有多大呢3相当于伪随机数产生算法。产生的两个签名,如果相等,"
+"说明原始内容在一定概率下是相等的;如果不相等,除了说明原始内容不相等外,不再提供任何信息,"
+"因为即使原始内容只相差一个字节,所产生的签名也很可能差别极大。从这个意义上来说,"
+"要设计一个hash算法,对相似的内容产生的签名也相近,是更为艰难的任务,因为它的签名值除了提供原始"
+"内容是否相等的信息外,干扰1还能额外提供不相等的原始再来干扰2内容的差异程度的信息。";
SimHashhash3=newSimHash(s,64);

intdis12=hash1.getDistance(hash1.strSimHash,hash2.strSimHash);
System.out.println(hash1.strSimHash);
System.out.println(hash2.strSimHash);
System.out.println(dis12);

System.out.println("============================================");

intdis13=hash1.getDistance(hash1.strSimHash,hash3.strSimHash);

System.out.println(hash1.strSimHash);
System.out.println(hash3.strSimHash);
System.out.println(dis13);
}
}



                                            
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: