zoukankan      html  css  js  c++  java
  • 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字),对于小文本而言,误差较大!!!

  • 相关阅读:
    Hexo命令无法找到 -问题修复
    技术的本质
    java 多线程总结篇4——锁机制
    java 多线程总结篇3之——生命周期和线程同步
    java 多线程总结篇2之——Thread类及常用函数
    java 多线程总结篇1之——基本概念
    java IO流的继承体系和装饰类应用
    java IO流知识点总结
    java IO流之——File类知识总结和面试
    为什么需要学UML建模
  • 原文地址:https://www.cnblogs.com/qijunhui/p/8445475.html
Copyright © 2011-2022 走看看