SimHash算法(转载)

1  SimHash简介

  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 -4 4 -4 4”;“51区”的hash值为“101011”,通过加权计算为 “ 5 -5 5 -5 5 5”。
  4. 合并,把上面各个单词算出来的序列值累加,变成只有一个序列串。比如 “美国”的 “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”。这里作为示例只算了两个单词的,真实计算需要把所有单词的序列串累加。
  5. 降维,把4步算出来的 “9 -9 1 -1 1 9” 变成 0 1 串,形成我们最终的simhash签名。 如果每一位大于0 记为 1,小于0 记为 0。最后算出结果为:“1 0 1 0 1 1”。

过程图为:

2  算法几何意义及原理

2.1  几何意义

这个算法的几何意义非常明了。它首先将每一个特征映射为f维空间的一个向量,这个映射规则具体是怎样并不重要,只要对很多不同的特征来说,它们对所对应的向量是均匀随机分布的,并且对相同的特征来说对应的向量是唯一的就行。比如一个特征的4位hash签名的二进制表示为1010,那么这个特征对应的 4维向量就是(1, -1, 1, -1)T,即hash签名的某一位为1,映射到的向量的对应位就为1,否则为-1。然后,将一个文档中所包含的各个特征对应的向量加权求和,加权的系数等于该特征的权重。得到的和向量即表征了这个文档,我们可以用向量之间的夹角来衡量对应文档之间的相似度。最后,为了得到一个f位的签名,需要进一步将其压缩,如果和向量的某一维大于0,则最终签名的对应位为1,否则为0。这样的压缩相当于只留下了和向量所在的象限这个信息,而64位的签名可以表示多达264个象限,因此只保存所在象限的信息也足够表征一个文档了。

2.2  原理

明确了算法了几何意义,使这个算法直观上看来是合理的。但是,为何最终得到的签名相近的程度,可以衡量原始文档的相似程度呢?这需要一个清晰的思路和证明。在simhash的发明人Charikar的论文中并没有给出具体的simhash算法和证明,以下列出我自己得出的证明思路。

Simhash是由随机超平面hash算法演变而来的,随机超平面hash算法非常简单,对于一个n维向量v,要得到一个f位的签名(f<<n),算法如下:

  1. 随机产生f个n维的向量r1,…rf;
  2. 对每一个向量ri,如果v与ri的点积大于0,则最终签名的第i位为1,否则为0。

这个算法相当于随机产生了f个n维超平面,每个超平面将向量v所在的空间一分为二,v在这个超平面上方则得到一个1,否则得到一个0,然后将得到的 f个0或1组合起来成为一个f维的签名。如果两个向量u, v的夹角为θ,则一个随机超平面将它们分开的概率为θ/π,因此u, v的签名的对应位不同的概率等于θ/π。所以,我们可以用两个向量的签名的不同的对应位的数量,即汉明距离,来衡量这两个向量的差异程度。

Simhash算法与随机超平面hash是怎么联系起来的呢?在simhash算法中,并没有直接产生用于分割空间的随机向量,而是间接产生的:第 k个特征的hash签名的第i位拿出来,如果为0,则改为-1,如果为1则不变,作为第i个随机向量的第k维。由于hash签名是f位的,因此这样能产生 f个随机向量,对应f个随机超平面。下面举个例子:

假设用5个特征w1,…,w5来表示所有文档,现要得到任意文档的一个3维签名。假设这5个特征对应的3维向量分别为:

h(w1) = (1, -1, 1)T

h(w2) = (-1, 1, 1)T

h(w3) = (1, -1, -1)T

h(w4) = (-1, -1, 1)T

h(w5) = (1, 1, -1)T

按simhash算法,要得到一个文档向量d=(w1=1, w2=2, w3=0, w4=3, w5=0) T的签名,

先要计算向量m = 1*h(w1) + 2*h(w2) + 0*h(w3) + 3*h(w4) + 0*h(w5) = (-4, -2, 6) T,然后根据simhash算法的步骤3,得到最终的签名s=001。上面的计算步骤其实相当于,先得到3个5维的向量,第1个向量由h(w1),…,h(w5)的第1维组成:r1=(1,-1,1,-1,1) T;第2个5维向量由h(w1),…,h(w5)的第2维组成:r2=(-1,1,-1,-1,1) T;同理,第3个5维向量为:r3=(1,1,-1,1,-1) T.按随机超平面算法的步骤2,分别求向量d与r1,r2,r3的点积:

d T r1=-4 < 0,所以s1=0;
d T r2=-2 < 0,所以s2=0;
d T r3=6 > 0,所以s3=1.

故最终的签名s=001,与simhash算法产生的结果是一致的。

从上面的计算过程可以看出,simhash算法其实与随机超平面hash算法是相同的,simhash算法得到的两个签名的汉明距离,可以用来衡量原始向量的夹角。这其实是一种降维技术,将高维的向量用较低维度的签名来表征。衡量两个内容相似度,需要计算汉明距离,这对给定签名查找相似内容的应用来说带来了一些计算上的困难。

3  Java算法实现

  1 import java.math.BigInteger;
  2 import java.util.ArrayList;
  3 import java.util.HashMap;
  4 import java.util.List;
  5 import java.util.StringTokenizer;
  6 
  7 public class SimHash {
  8 
  9     private String tokens;
 10 
 11     private BigInteger intSimHash;
 12 
 13     private String strSimHash;
 14 
 15     private int hashbits = 64;
 16 
 17     public SimHash(String tokens) {
 18         this.tokens = tokens;
 19         this.intSimHash = this.simHash();
 20     }
 21 
 22     public SimHash(String tokens, int hashbits) {
 23         this.tokens = tokens;
 24         this.hashbits = hashbits;
 25         this.intSimHash = this.simHash();
 26     }
 27 
 28     HashMap wordMap = new HashMap();
 29 
 30     public BigInteger simHash() {
 31         // 定义特征向量/数组
 32         int[] v = new int[this.hashbits];
 33         // 1、将文本去掉格式后, 分词.
 34         StringTokenizer stringTokens = new StringTokenizer(this.tokens);
 35         while (stringTokens.hasMoreTokens()) {
 36             String temp = stringTokens.nextToken();
 37             // 2、将每一个分词hash为一组固定长度的数列.比如 64bit 的一个整数.
 38             BigInteger t = this.hash(temp);
 39             for (int i = 0; i < this.hashbits; i++) {
 40                 BigInteger bitmask = new BigInteger("1").shiftLeft(i);
 41                 // 3、建立一个长度为64的整数数组(假设要生成64位的数字指纹,也可以是其它数字),
 42                 // 对每一个分词hash后的数列进行判断,如果是1000...1,那么数组的第一位和末尾一位加1,
 43                 // 中间的62位减一,也就是说,逢1加1,逢0减1.一直到把所有的分词hash数列全部判断完毕.
 44                 if (t.and(bitmask).signum() != 0) {
 45                     // 这里是计算整个文档的所有特征的向量和
 46                     // 这里实际使用中需要 +- 权重,而不是简单的 +1/-1,
 47                     v[i] += 1;
 48                 } else {
 49                     v[i] -= 1;
 50                 }
 51             }
 52         }
 53         BigInteger fingerprint = new BigInteger("0");
 54         StringBuffer simHashBuffer = new StringBuffer();
 55         for (int i = 0; i < this.hashbits; i++) {
 56             // 4、最后对数组进行判断,大于0的记为1,小于等于0的记为0,得到一个 64bit 的数字指纹/签名.
 57             if (v[i] >= 0) {
 58                 fingerprint = fingerprint.add(new BigInteger("1").shiftLeft(i));
 59                 simHashBuffer.append("1");
 60             } else {
 61                 simHashBuffer.append("0");
 62             }
 63         }
 64         this.strSimHash = simHashBuffer.toString();
 65         System.out.println(this.strSimHash + " length " + this.strSimHash.length());
 66         return fingerprint;
 67     }
 68 
 69     private BigInteger hash(String source) {
 70         if (source == null || source.length() == 0) {
 71             return new BigInteger("0");
 72         } else {
 73             char[] sourceArray = source.toCharArray();
 74             BigInteger x = BigInteger.valueOf(((long) sourceArray[0]) << 7);
 75             BigInteger m = new BigInteger("1000003");
 76             BigInteger mask = new BigInteger("2").pow(this.hashbits).subtract(new BigInteger("1"));
 77             for (char item : sourceArray) {
 78                 BigInteger temp = BigInteger.valueOf((long) item);
 79                 x = x.multiply(m).xor(temp).and(mask);
 80             }
 81             x = x.xor(new BigInteger(String.valueOf(source.length())));
 82             if (x.equals(new BigInteger("-1"))) {
 83                 x = new BigInteger("-2");
 84             }
 85             return x;
 86         }
 87     }
 88 
 89     public int hammingDistance(SimHash other) {
 90 
 91         BigInteger x = this.intSimHash.xor(other.intSimHash);
 92         int tot = 0;
 93 
 94         // 统计x中二进制位数为1的个数
 95         // 我们想想,一个二进制数减去1,
 96         //那么,从最后那个1(包括那个1)后面的数字全都反了,
 97         //对吧,然后,n&(n-1)就相当于把后面的数字清0,
 98         // 我们看n能做多少次这样的操作就OK了。
 99 
100         while (x.signum() != 0) {
101             tot += 1;
102             x = x.and(x.subtract(new BigInteger("1")));
103         }
104         return tot;
105     }
106 
107     public int getDistance(String str1, String str2) {
108         int distance;
109         if (str1.length() != str2.length()) {
110             distance = -1;
111         } else {
112             distance = 0;
113             for (int i = 0; i < str1.length(); i++) {
114                 if (str1.charAt(i) != str2.charAt(i)) {
115                     distance++;
116                 }
117             }
118         }
119         return distance;
120     }
121 
122     public List subByDistance(SimHash simHash, int distance) {
123         // 分成几组来检查
124         int numEach = this.hashbits / (distance + 1);
125         List characters = new ArrayList();
126 
127         StringBuffer buffer = new StringBuffer();
128 
129         int k = 0;
130         for (int i = 0; i < this.intSimHash.bitLength(); i++) {
131             // 当且仅当设置了指定的位时,返回 true
132             boolean sr = simHash.intSimHash.testBit(i);
133 
134             if (sr) {
135                 buffer.append("1");
136             } else {
137                 buffer.append("0");
138             }
139 
140             if ((i + 1) % numEach == 0) {
141                 // 将二进制转为BigInteger
142                 BigInteger eachValue = new BigInteger(buffer.toString(), 2);
143                 System.out.println("----" + eachValue);
144                 buffer.delete(0, buffer.length());
145                 characters.add(eachValue);
146             }
147         }
148 
149         return characters;
150     }
151 
152     public static void main(String[] args) {
153         String s = "小明叫她妈妈吃饭";
154         SimHash hash1 = new SimHash(s, 64);
155         System.out.println(hash1.intSimHash + "  " + hash1.intSimHash.bitLength());
156         //hash1.subByDistance(hash1, 3);
157 
158         s = "小明叫她妈妈吃饭";
159         SimHash hash2 = new SimHash(s, 64);
160         System.out.println(hash2.intSimHash + "  " + hash2.intSimHash.bitCount());
161         //hash1.subByDistance(hash2, 3);
162         
163         s = "小明叫她妈妈";
164         SimHash hash3 = new SimHash(s, 64);
165         System.out.println(hash3.intSimHash + "  " + hash3.intSimHash.bitCount());
166         //hash1.subByDistance(hash3, 4);
167         
168         System.out.println("============================");
169         
170         int dis = hash1.getDistance(hash1.strSimHash, hash2.strSimHash);
171         System.out.println(hash1.hammingDistance(hash2) + " " + dis);
172 
173         int dis2 = hash1.getDistance(hash1.strSimHash, hash3.strSimHash);
174         System.out.println(hash1.hammingDistance(hash3) + " " + dis2);
175         
176         //通过Unicode编码来判断中文
177         /*String str = "中国chinese";
178         for (int i = 0; i < str.length(); i++) {
179             System.out.println(str.substring(i, i + 1).matches("[\\u4e00-\\u9fbb]+"));
180         }*/
181 
182     }
183 }

结果:

4  Python算法实现

 1 #!/usr/bin/python  
 2 # coding=utf-8  
 3 class simhash:
 4     # 构造函数
 5     def __init__(self, tokens='', hashbits=128):
 6         self.hashbits = hashbits
 7         self.hash = self.simhash(tokens);
 8 
 9         # toString函数
10 
11     def __str__(self):
12         return str(self.hash)
13 
14         # 生成simhash值
15 
16     def simhash(self, tokens):
17         v = [0] * self.hashbits
18         for t in [self._string_hash(x) for x in tokens]:  # t为token的普通hash值
19             for i in range(self.hashbits):
20                 bitmask = 1 << i
21                 if t & bitmask:
22                     v[i] += 1  # 查看当前bit位是否为1,是的话将该位+1
23                 else:
24                     v[i] -= 1  # 否则的话,该位-1
25         fingerprint = 0
26         for i in range(self.hashbits):
27             if v[i] >= 0:
28                 fingerprint += 1 << i
29         return fingerprint  # 整个文档的fingerprint为最终各个位>=0的和
30 
31     # 求海明距离
32     def hamming_distance(self, other):
33         x = (self.hash ^ other.hash) & ((1 << self.hashbits) - 1)
34         tot = 0;
35         while x:
36             tot += 1
37             x &= x - 1
38         return tot
39 
40         # 求相似度
41 
42     def similarity(self, other):
43         a = float(self.hash)
44         b = float(other.hash)
45         if a > b:
46             return b / a
47         else:
48             return a / b
49 
50         # 针对source生成hash值   (一个可变长度版本的Python的内置散列)
51 
52     def _string_hash(self, source):
53         if source == "":
54             return 0
55         else:
56             x = ord(source[0]) << 7
57             m = 1000003
58             mask = 2 ** self.hashbits - 1
59             for c in source:
60                 x = ((x * m) ^ ord(c)) & mask
61             x ^= len(source)
62             if x == -1:
63                 x = -2
64             return x
65 
66 
67 if __name__ == '__main__':
68     s = '小明叫她妈妈吃饭'
69     hash1 = simhash(s.split())
70 
71     s = '小明叫她妈妈吃饭'
72     hash2 = simhash(s.split())
73 
74     s = '小明叫她妈妈'
75     hash3 = simhash(s.split())
76 
77     print(hash1.hamming_distance(hash2), "   ", hash1.similarity(hash2))
78     print(hash1.hamming_distance(hash3), "   ", hash1.similarity(hash3))

结果:

注:SimHash适用于大文本处理(>400字),对于小文本而言,误差较大!!!

作者:祁俊辉
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
posted @ 2018-02-12 20:33  祁俊辉  阅读(720)  评论(0编辑  收藏  举报