使用simhash以及海明距離判斷內容相似程度


算法簡介

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);
    }
}
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM