算法簡介
SimHash也即相似hash,是一類特殊的信息指紋,常用來比較文章的相似度,與傳統hash相比,傳統hash只負責將原始內容盡量隨機的映射為一個特征值,並保證相同的內容一定具有相同的特征值。而且如果兩個hash值是相等的,則說明原始數據在一定概率下也是相等的。但通過傳統hash來判斷文章的內容是否相似是非常困難的,原因在於傳統hash只唯一標明了其特殊性,並不能作為相似度比較的依據。
SimHash最初是由Google使用,其值不但提供了原始值是否相等這一信息,還能通過該值計算出內容的差異程度。
算法原理
simhash是由 Charikar 在2002年提出來的,參考 《Similarity estimation techniques from rounding algorithms》 。 介紹下這個算法主要原理,為了便於理解盡量不使用數學公式,分為這幾步:
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”。
原理圖:
我們可以來做個測試,兩個相差只有一個字符的文本串,“你媽媽喊你回家吃飯哦,回家羅回家羅” 和 “你媽媽叫你回家吃飯啦,回家羅回家羅”。
通過simhash計算結果為:
1000010010101101111111100000101011010001001111100001001011001011
1000010010101101011111100000101011010001001111100001101010001011
通過比較差異的位數就可以得到兩串文本的差異,差異的位數,稱之為“海明距離”,通常認為海明距離<3的是高度相似的文本。
算法實現
這里的代碼引用自博客:http://my.oschina.net/leejun2005/blog/150086 ,這里表示感謝。
代碼實現中使用Hanlp代替了原有的分詞器。
package com.emcc.changedig.extractengine.util; /** * Function: simHash 判斷文本相似度,該示例程支持中文<br/> * date: 2013-8-6 上午1:11:48 <br/> * @author june * @version 0.1 */ import java.io.IOException; import java.math.BigInteger; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import com.hankcs.hanlp.seg.common.Term; public class SimHash { private String tokens; private BigInteger intSimHash; private String strSimHash; private int hashbits = 64; public SimHash(String tokens) throws IOException { this.tokens = tokens; this.intSimHash = this.simHash(); } public SimHash(String tokens, int hashbits) throws IOException { this.tokens = tokens; this.hashbits = hashbits; this.intSimHash = this.simHash(); } HashMap<String, Integer> wordMap = new HashMap<String, Integer>(); public BigInteger simHash() throws IOException { // 定義特征向量/數組 int[] v = new int[this.hashbits]; String word = null; List<Term> terms = SegmentationUtil.ppl(this.tokens); for (Term term : terms) { word = term.word; // 將每一個分詞hash為一組固定長度的數列.比如 64bit 的一個整數. BigInteger t = this.hash(word); for (int i = 0; i < this.hashbits; i++) { BigInteger bitmask = new BigInteger("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; } } } BigInteger fingerprint = new BigInteger("0"); StringBuffer simHashBuffer = new StringBuffer(); for (int i = 0; i < this.hashbits; i++) { // 4、最后對數組進行判斷,大於0的記為1,小於等於0的記為0,得到一個 64bit 的數字指紋/簽名. if (v[i] >= 0) { fingerprint = fingerprint.add(new BigInteger("1").shiftLeft(i)); simHashBuffer.append("1"); } else { simHashBuffer.append("0"); } } this.strSimHash = simHashBuffer.toString(); return fingerprint; } private BigInteger hash(String source) { if (source == null || source.length() == 0) { return new BigInteger("0"); } else { 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 海明距離 */ public int hammingDistance(SimHash other) { BigInteger x = this.intSimHash.xor(other.intSimHash); int tot = 0; while (x.signum() != 0) { tot += 1; x = x.and(x.subtract(new BigInteger("1"))); } return tot; } public int getDistance(String str1, String str2) { int distance; if (str1.length() != str2.length()) { distance = -1; } else { distance = 0; for (int i = 0; i < str1.length(); i++) { if (str1.charAt(i) != str2.charAt(i)) { distance++; } } } return distance; } public List<BigInteger> subByDistance(SimHash simHash, int distance) { // 分成幾組來檢查 int numEach = this.hashbits / (distance + 1); List<BigInteger> characters = new ArrayList<BigInteger>(); StringBuffer buffer = new StringBuffer(); for (int i = 0; i < this.intSimHash.bitLength(); i++) { // 當且僅當設置了指定的位時,返回 true boolean sr = simHash.intSimHash.testBit(i); if (sr) { buffer.append("1"); } else { buffer.append("0"); } if ((i + 1) % numEach == 0) { // 將二進制轉為BigInteger BigInteger eachValue = new BigInteger(buffer.toString(), 2); buffer.delete(0, buffer.length()); characters.add(eachValue); } } return characters; } public static void main(String[] args) throws IOException { String s = "傳統的 hash 算法只負責將原始內容盡量均勻隨機地映射為一個簽名值," + "原理上相當於偽隨機數產生算法。產生的兩個簽名,如果相等,說明原始內容在一定概 率 下是相等的;" + "如果不相等,除了說明原始內容不相等外,不再提供任何信息,因為即使原始內容只相差一個字節," + "所產生的簽名也很可能差別極大。從這個意義 上來 說,要設計一個 hash 算法," + "對相似的內容產生的簽名也相近,是更為艱難的任務,因為它的簽名值除了提供原始內容是否相等的信息外," + "還能額外提供不相等的 原始內容的差異程度的信息。"; SimHash hash1 = new SimHash(s, 64); // 刪除首句話,並加入兩個干擾串 s = "原理上相當於偽隨機數產生算法。產生的兩個簽名,如果相等,說明原始內容在一定概 率 下是相等的;" + "如果不相等,除了說明原始內容不相等外,不再提供任何信息,因為即使原始內容只相差一個字節," + "所產生的簽名也很可能差別極大。從這個意義 上來 說,要設計一個 hash 算法," + "對相似的內容產生的簽名也相近,是更為艱難的任務,因為它的簽名值除了提供原始內容是否相等的信息外," + "干擾1還能額外提供不相等的 原始內容的差異程度的信息。"; SimHash hash2 = new SimHash(s, 64); // 首句前添加一句話,並加入四個干擾串 s = "imhash算法的輸入是一個向量,輸出是一個 f 位的簽名值。為了陳述方便," + "假設輸入的是一個文檔的特征集合,每個特征有一定的權重。" + "傳統干擾4的 hash 算法只負責將原始內容盡量均勻隨機地映射為一個簽名值," + "原理上這次差異有多大呢3相當於偽隨機數產生算法。產生的兩個簽名,如果相等," + "說明原始內容在一定概 率 下是相等的;如果不相等,除了說明原始內容不相等外,不再提供任何信息," + "因為即使原始內容只相差一個字節,所產生的簽名也很可能差別極大。從這個意義 上來 說," + "要設計一個 hash 算法,對相似的內容產生的簽名也相近,是更為艱難的任務,因為它的簽名值除了提供原始" + "內容是否相等的信息外,干擾1還能額外提供不相等的 原始再來干擾2內容的差異程度的信息。"; SimHash hash3 = new SimHash(s, 64); int dis12 = hash1.getDistance(hash1.strSimHash, hash2.strSimHash); System.out.println(hash1.strSimHash); System.out.println(hash2.strSimHash); System.out.println(dis12); System.out.println("============================================"); int dis13 = hash1.getDistance(hash1.strSimHash, hash3.strSimHash); System.out.println(hash1.strSimHash); System.out.println(hash3.strSimHash); System.out.println(dis13); } }