文本相似性計算總結(余弦定理,simhash)及代碼


  最近在工作中要處理好多文本文檔,要求找出和每個文檔的相識的文檔。通過查找資料總結如下幾個計算方法:

  1、余弦相似性

    我舉一個例子來說明,什么是"余弦相似性"。

    為了簡單起見,我們先從句子着手。


    

    請問怎樣才能計算上面兩句話的相似程度?

    基本思路是:如果這兩句話的用詞越相似,它們的內容就應該越相似。因此,可以從詞頻入手,計算它們的相似程度。

    第一步,分詞。

    

    第二步,列出所有的詞。

    

    第三步,計算詞頻。

    

    第四步,寫出詞頻向量。

    

    

    到這里,問題就變成了如何計算這兩個向量的相似程度。

    我們可以把它們想象成空間中的兩條線段,都是從原點([0, 0, ...])出發,指向不同的方向。兩條線段之間形成一個夾角,如果夾角為0度,意味着方向相同、線段重合;如果夾角為90度,

    意味着形成直角,方向完全不相似;如果夾角為180度,意味着方向正好相反。因此,我們可以通過夾角的大小,來判斷向量的相似程度。夾角越小,就代表越相似。

    

    以二維空間為例,上圖的a和b是兩個向量,我們要計算它們的夾角θ。余弦定理告訴我們,可以用下面的公式求得:

    

    

    假定a向量是[x1, y1],b向量是[x2, y2],那么可以將余弦定理改寫成下面的形式:

    

  

    數學家已經證明,余弦的這種計算方法對n維向量也成立。假定A和B是兩個n維向量,A是 [A1, A2, ..., An] ,B是 [B1, B2, ..., Bn] ,則A與B的夾角θ的余弦等於:

    

    使用這個公式,我們就可以得到,句子A與句子B的夾角的余弦。

    

 

     

    余弦值越接近1,就表明夾角越接近0度,也就是兩個向量越相似,這就叫"余弦相似性"。所以,上面的句子A和句子B是很相似的,事實上它們的夾角大約為20.3度。

    由此,我們就得到了"找出相似文章"的一種算法:

    1、使用TF-IDF算法,找出兩篇文章的關鍵詞;

    2、每篇文章各取出若干個關鍵詞(比如20個),合並成一個集合,計算每篇文章對於這個集合中的詞的詞頻(為了避免文章長度的差異,可以使用相對詞頻);

    3、生成兩篇文章各自的詞頻向量;

    4、計算兩個向量的余弦相似度,值越大就表示越相似。

 

     "余弦相似度"是一種非常有用的算法,只要是計算兩個向量的相似程度,都可以采用它。

    

    應用場景及優缺點

          本文目前將該算法應用於網頁標題合並和標題聚類中,目前仍在嘗試應用於其它場景中。

          優點:計算結果准確,適合對短文本進行處理。

          缺點:需要逐個進行向量化,並進行余弦計算,比較消耗CPU處理時間,因此不適合長文本,如網頁正文、文檔等。

     

   java實現代碼

package main.java;
import com.hankcs.hanlp.HanLP;
import com.hankcs.hanlp.corpus.tag.Nature;
import com.hankcs.hanlp.seg.common.Term;

import java.util.*;
/**
 * @Author:sks
 * @Description:
 * @Date:Created in 16:04 2018/6/1
 * @Modified by:
 **/
public class CosineSimilarity {

    public static void main(String[] args) {

        ComputeTxtSimilar();

    }

    private static void ComputeTxtSimilar(){
        String txtLeft = CommonClass.getDocFileText("D:/work/Solr/ImportData-1/160926 進擊的直播:細數各路媒體的入場“姿勢”(完整版).doc");
        String txtRight = CommonClass.getDocFileText("D:/work/Solr/ImportData-1/160926 劉慶振 直播經濟:全民網紅時代的內容創業與流量變現.doc");
        //所有文檔的總詞庫
        List<String> totalWordList = new ArrayList<String>();
        //計算文檔的詞頻
        Map<String, Integer> leftWordCountMap = getWordCountMap(txtLeft, totalWordList);
        Map<String, Float> leftWordTfMap = calculateWordTf(leftWordCountMap);

        Map<String, Integer> rightWordCountMap = getWordCountMap(txtRight, totalWordList);
        Map<String, Float> rightWordTfMap = calculateWordTf(rightWordCountMap);


        //獲取文檔的特征值
        List<Float> leftFeature = getTxtFeature(totalWordList,leftWordTfMap);
        List<Float> rightFeature = getTxtFeature(totalWordList,rightWordTfMap);

        //計算文檔對應特征值的平方和的平方根
        float leftVectorSqrt = calculateVectorSqrt(leftWordTfMap);
        float rightVectorSqrt = calculateVectorSqrt(rightWordTfMap);

        //根據余弦定理公司,計算余弦公式中的分子
        float fenzi = getCosValue(leftFeature,rightFeature);

        //根據余弦定理計算兩個文檔的余弦值
        double cosValue = 0;
        if (fenzi > 0) {
            cosValue = fenzi / (leftVectorSqrt * rightVectorSqrt);
        }
        cosValue = Double.parseDouble(String.format("%.4f",cosValue));
        System.out.println(cosValue);

    }

    /**
     * @Author:sks
     * @Description:獲取詞及詞頻鍵值對,並將詞保存到詞庫中
     * @Date:
     */
    public static  Map<String,Integer> getWordCountMap(String text,List<String> totalWordList){
        Map<String,Integer> wordCountMap = new HashMap<String,Integer>();
        List<Term> words= HanLP.segment(text);
        int count = 0;
        for(Term tm:words){
            //取字數為兩個字或兩個字以上名詞或動名詞作為關鍵詞
            if(tm.word.length()>1 && (tm.nature== Nature.n||tm.nature== Nature.vn)){
                count = 1;
                if(wordCountMap.containsKey(tm.word))
                {
                    count = wordCountMap.get(tm.word) + 1;
                    wordCountMap.remove(tm.word);
                }
                wordCountMap.put(tm.word,count);
                if(!totalWordList.contains(tm.word)){
                    totalWordList.add(tm.word);
                }
            }
        }
        return wordCountMap;
    }



    //計算關鍵詞詞頻
    private static Map<String, Float> calculateWordTf(Map<String, Integer> wordCountMap) {
        Map<String, Float> wordTfMap =new HashMap<String, Float>();
        int totalWordsCount = 0;
        Collection<Integer> cv = wordCountMap.values();
        for (Integer count : cv) {
            totalWordsCount += count;
        }

        wordTfMap = new HashMap<String, Float>();
        Set<String> keys = wordCountMap.keySet();
        for (String key : keys) {
            wordTfMap.put(key, wordCountMap.get(key) / (float) totalWordsCount);
        }
        return wordTfMap;
    }

    //計算文檔對應特征值的平方和的平方根
    private static float calculateVectorSqrt(Map<String, Float> wordTfMap) {
        float result = 0;
        Collection<Float> cols =  wordTfMap.values();
        for(Float temp : cols){
            if (temp > 0) {
                result += temp * temp;
            }
        }
        return (float) Math.sqrt(result);
    }



    private static List<Float> getTxtFeature(List<String> totalWordList,Map<String, Float> wordCountMap){
        List<Float> list =new ArrayList<Float>();
        for(String word :totalWordList){
            float tf = 0;
            if(wordCountMap.containsKey(word)){
                tf = wordCountMap.get(word);
            }
            list.add(tf);
        }
        return list;
    }

    /**
     * @Author:sks
     * @Description:根據兩個向量計算余弦值
     * @Date:
     */
    private static float getCosValue(List<Float> leftFeature, List<Float> rightFeature) {
        float fenzi = 0;
        float tempX = 0;
        float tempY = 0;
        for (int i = 0; i < leftFeature.size(); i++) {
            tempX = leftFeature.get(i);
            tempY = rightFeature.get(i);
            if (tempX > 0 && tempY > 0) {
                fenzi += tempX * tempY;
            }
        }
        return fenzi;
    }




}
View Code

 

  

  

  2、SimHash

          SimHash為Google處理海量網頁的采用的文本相似判定方法。該方法的主要目的是降維,即將高維的特征向量映射成f-bit的指紋,通過比較兩篇文檔指紋的漢明距離來表征文檔重復或相似性。

    備注:此處f-bit指紋,可以根據應用需求,定制為16位、32位、64位或者128位數等。

    simhash算法分為5個步驟:分詞、hash、加權、合並、降維,具體過程如下所述:

    

    1、分詞

      這一步和上面的余弦相似性介紹的一到四步驟類似。

    2、hash

      通過hash函數計算各個特征向量的hash值,hash值為二進制數01組成的n-bit簽名。比如比如句子 A:我 1,喜歡 2,看 2,電視 1,電影 1,不 1,也 0中的hash值Hash(喜歡)為100101,

      “電視”的hash值Hash(電視)為“101011”(當然實際的值要比這個復雜,hash串比這要長)就這樣,字符串就變成了一系列數字。

 

    3、加權

      通過 2步驟的hash生成結果,需要按照單詞的權重形成加權數字串,比如“喜歡”的hash值為“100101”,把hash值從左到右與權重相乘,如果為1則乘以1 ,如果是0則曾以-1 ,喜歡的權重是2 計算為“2 -2 -2 2 -2 2”;

      “電視”的hash值為“101011”,通過加權計算為 “ 1 -1 1 -1 1 1”,其余特征向量類似此般操作。

    4、合並

      把上面所有各個單詞算出來的序列值累加,變成只有一個序列串。比如 “喜歡”的“2 -2 -2 2 -2 2”,“電視”的 “ 1 -1 1 -1 1 1”, 把每一位進行累加, (2+1)+(-2-1)+(-2+1)+(2-1)+(-2+1)+(2+1)=3 -3 -1 1 -1 3

      這里作為示例只算了兩個單詞的,真實計算需要把所有單詞的序列串累加。

    5、降維

      把4步算出來的 “3 -3 -1 1 -1 3” 變成 0 1 串,形成我們最終的simhash簽名。 如果每一位大於0 記為 1,小於0 記為 0。最后算出結果為:“1 0 0 1 0 1”。這樣就得到了每篇文檔的SimHash簽名值

    6、海明距離

      海明距離的求法:異或時,只有在兩個比較的位不同時其結果是1 ,否則結果為0,兩個二進制“異或”后得到1的個數即為海明距離的大小。

      根據經驗值,對64位的 SimHash值,海明距離在3以內的可認為相似度比較高。

      比如:

        A:我喜歡看電視,不喜歡看電影 其hash值為 1 0 0 1 0 1

        B:我也很喜歡看電視,但不喜歡看電影 其hash值為 1 0 1 1 0 1

        兩者 hash 異或運算結果 001000,統計結果中1的個數是1,那么兩者的海明距離就是1,說明兩者比較相似。

    7、 python代碼和java實現代碼

    

# coding=utf-8
class simhash:
    # 構造函數
    def __init__(self, tokens='', hashbits=128):
        self.hashbits = hashbits
        self.hash = self.simhash(tokens)


    # 生成simhash值
    def simhash(self, tokens):
        # v是長度128的列表
        v = [0] * self.hashbits
        tokens_hash = [self.string_hash(x) for x in tokens]
        for t in tokens_hash:  # t為token的普通hash值
            for i in range(self.hashbits):
                bitmask = 1 << i
                if t & bitmask:
                    v[i] += 1  # 查看當前bit位是否為1,是的話將該位+1
                else:
                    v[i] -= 1  # 否則的話,該位-1
        fingerprint = 0
        for i in range(self.hashbits):
            if v[i] >= 0:
                fingerprint += 1 << i
        return fingerprint  # 整個文檔的fingerprint為最終各個位>=0的和

    # 求海明距離
    def hamming_distance(self, other):
        # 異或結果
        xorResult = (self.hash ^ other.hash)
        # 128個1的二進制串
        hashbit128 = ((1 << self.hashbits) - 1)
        x = xorResult & hashbit128
        count = 0
        while x:
            count += 1
            x &= x - 1
        return count

    # 求相似度
    def similarity(self, other):
        a = float(self.hash)
        b = float(other.hash)
        if a > b:
            return b / a
        else:
            return a / b

    # 針對source生成hash值
    def string_hash(self, source):
        if source == "":
            return 0
        else:
            result = ord(source[0]) << 7
            m = 1000003
            hashbit128 = ((1 << self.hashbits) - 1)

            for c in source:
                temp = (result * m) ^ ord(c)
                result = temp & hashbit128

            result ^= len(source)
            if result == -1:
                result = -2

            return result




if __name__ == '__main__':


    s = '你 知道 里約 奧運會,媒體 玩出了 哪些 新花樣?'
    hash1 = simhash(s.split())
    print hash1.__str__()
    s = '我不知道 里約 奧運會,媒體 玩出了 哪些 新花樣'
    hash2 = simhash(s.split())
    print hash2.__str__()
    s = '視頻 直播 全球 知名 媒體 的 戰略 轉移'
    hash3 = simhash(s.split())

    print(hash1.hamming_distance(hash2), "   ", hash1.similarity(hash2))
    print(hash1.hamming_distance(hash3), "   ", hash1.similarity(hash3))
    print(hash2.hamming_distance(hash3), "   ", hash2.similarity(hash3))
View Code

    

package main.java;
import java.math.BigInteger;
import java.util.StringTokenizer;
public class SimHash
{
    private String tokens;
    private BigInteger strSimHash;
    private int hashbits = 128;

    /**
    * @Author:sks
    * @Description 構造函數:
    * @tokens:  特征值字符串,括號內的數值是權重,媒體(56),發展(31),技術(15),傳播(12),新聞(11),用戶(10),信息(10),生產(9)
    */
    public SimHash(String tokens)
    {
        this.tokens = tokens;
        this.strSimHash = this.simHash();
    }
    public SimHash(String tokens, int hashbits)
    {
        this.tokens = tokens;
        this.hashbits = hashbits;
        this.strSimHash = this.simHash();
    }

    public String getSimHash(){
        return this.strSimHash.toString();
    }
    private BigInteger simHash() {
        int[] v = new int[this.hashbits];
        StringTokenizer stringTokens = new StringTokenizer(this.tokens,",");
        while (stringTokens.hasMoreTokens()) {
            //媒體(56)
            String temp = stringTokens.nextToken();
            String token = temp.substring(0,temp.indexOf("("));
            int weight = Integer.parseInt(temp.substring(temp.indexOf("(")+1,temp.indexOf(")")));
            BigInteger t = this.hash(token);
            for (int i = 0; i < this.hashbits; i++){
                BigInteger bitmask = new BigInteger("1").shiftLeft(i);
                if (t.and(bitmask).signum() != 0) {
                    v[i] += 1 * weight;//加權
                }
                else {
                    v[i] -= 1 * weight;
                }
            }
        }
        BigInteger fingerprint = new BigInteger("0");
        for (int i = 0; i < this.hashbits; i++) {
            if (v[i] >= 0) {
                fingerprint = fingerprint.add(new BigInteger("1").shiftLeft(i));
            }
        }
        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;
        }
    }
    /**
    * @Author:sks
    * @Description:計算海明距離
    * @leftSimHash,rightSimHash:要比較的信息指紋
     * @hashbits:128
    */
    public static int hammingDistance(String leftSimHash,String rightSimHash,int hashbits){
        BigInteger m = new BigInteger("1").shiftLeft(hashbits).subtract(
                new BigInteger("1"));
        BigInteger left = new BigInteger(leftSimHash);
        BigInteger right = new BigInteger(rightSimHash);
        BigInteger x = left.xor(right).and(m);
        int count = 0;
        while (x.signum() != 0) {
            count += 1;
            x = x.and(x.subtract(new BigInteger("1")));
        }
        return count;
    }


    public static void main(String[] args)
    {

        String s = "你,知道,里約,奧運會,媒體,玩出,了,哪些,新,花樣";
        SimHash hash1 = new SimHash(s, 128);
        System.out.println(hash1.getSimHash() + "  " + hash1.getSimHash().bitCount());
        s = "我,不,知道,里約,奧運會,媒體,玩出,了,哪些,新,花樣";
        SimHash hash2 = new SimHash(s, 128);
        System.out.println(hash2.getSimHash() + "  " + hash2.getSimHash().bitCount());
        s = "視頻,直播,全球,知名,媒體,的,戰略,轉移";
        SimHash hash3 = new SimHash(s, 128);
        System.out.println(hash3.getSimHash() + "  " + hash3.getSimHash().bitCount());
        System.out.println("============================");
        System.out.println(SimHash.hammingDistance(hash1.getSimHash(),hash2.getSimHash(),128));

        System.out.println(SimHash.hammingDistance(hash1.getSimHash(),hash3.getSimHash(),128));

        System.out.println(SimHash.hammingDistance(hash2.getSimHash(),hash3.getSimHash(),128));

    }
}
View Code

 

    

  參考資料:

    1、http://www.ruanyifeng.com/blog/2013/03/cosine_similarity.html

    2、數學之美

    3、https://blog.csdn.net/u011630575/article/details/52164688

    4、https://blog.csdn.net/chenguolinblog/article/details/50830948

 


免責聲明!

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



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