文檔向量模型及其實踐-計算文檔的相似度

期末大作業的其中一部分是要求對文檔進行相似度計算,並提示可以用文檔詞向量的方法來做。於是查了一些資料。

然後引出了 空間向量模型(VSM) 這個概念。

  • 空間向量模型

    向量空間模型(VSM:Vector Space Model)由Salton等人於20世紀70年代提出,併成功地應用於著名的SMART文本檢索系統。 VSM概念簡單,把對文本內容的處理簡化爲向量空間中的向量運算,並且它以空間上的相似度表達語義的相似度,直觀易懂。當文檔被表示爲文檔空間的向量,就可以通過計算向量之間的相似性來度量文檔間的相似性。

    看完這段,頓悟了!不得不佩服向量的強大力量!

    既然知道了空間向量模型的存在,計算相似度就非常簡單了!無非就是計算餘弦值。

    關鍵的地方是怎樣構建文檔向量?

  • 構建文檔向量

    原理&思路:

    1. 要構建文檔向量,應該選取最能代表這個文檔的元素(特徵值),很顯然這個元素是文檔中的關鍵詞。(獲取關鍵詞的主要方法是分詞)

      關鍵詞 就是 特徵值

    2. 分詞,計算出文檔中關鍵詞的詞頻。

    3. 然後,文檔特徵值應該有兩個維度,一個是關鍵詞本身,另一個是關鍵詞出現的頻數。(慢慢有了向量的感覺了)

    4. 當然,一個文檔不止一個關鍵詞,把所有n個關鍵詞堆到一起,就得到了一個n * 2的矩陣。

    5. 最後,計算相似度的時候只用到頻數,爲了方便表示,取第二列轉置得到向量b(v1,v2,…,vn)。這個n維向量就是表示該文檔的向量,每一維度表示一個特徵值,維度的長度表示特徵值的頻數。

  • 相似性計算

    步驟:

    1. 因爲不同文檔的特徵值唯獨可能不一樣,而且相似性計算是相對於雙方來說的,所以這裏還要對上面的文檔向量進一步構建(歸一化,讓維度相同)。

    2. 對要比較的文檔的文檔向量的關鍵詞求全集。

    3. 分別將文檔向量跟得到的全集比較,將對應關鍵詞的頻數填到全集的頻數上,這樣就得到了要拿來比較的兩個文檔的文檔向量。每個向量都包含了對方的特徵值維度,兩個向量在相同的向量空間中。

    4. 根據向量的夾角餘弦公式計算,夾角餘弦值就是相似度。

  • 計算結果

    說明:

    • 第一組計算對象爲2003年政府工作報告和2016年政府工作報告。
    • 第二組計算對象爲2003年政府工作報告和三體 I
    • 閾值控制爲10,詞頻低於閾值的將被忽略
    • 這裏事先計算好了兩篇文章的詞頻。

    計算結果:

      Report(2003) & Report(2016)  Similarity: 0.740816488615756
    
      Process finished with exit code 0
      
      Report(2003) & TreeBody  Similarity: 0.07395613415240768
    
      Process finished with exit code 0
    

    兩篇政府工作報告的相似度是74%.

    政府工作報告和三體的相似度是7%.

    這個結果基本可以用來做文檔分類了,要想得到更好的結果,應該優化分詞的詞典。

    附上前10詞頻

    2016政府工作報告:

      發展	92
      推進	65
      建設	63
      創新	59
      經濟	48
      改革	46
      加快	44
      加強	41
      促進	40
      實施	38
    

    2003政府工作報告:

      建設	78
      發展	76
      加強	63
      堅持	54
      對	48
      實施	44
      改革	43
      積極	41
      支持	38
      我們	38
    

    三體 I:

      汪淼	623
      中	521
      葉文潔	433
      三體	401
      對	356
      地	334
      上	299
      太陽	273
      自己	271
      文明	255
    

    三體第一部出場最多的竟然不是葉文潔??


附上向量模型的源碼:

DocVector.java

package com.syang;

import java.util.ArrayList;

/**
 * Created by Answer on 2017/6/24.
 */
public class DocVector {
    private int dim;
    private ArrayList<String> keywords;
    private ArrayList<Integer> values;


    public int getDim() {
        return dim;
    }

    public void setDim(int dim) {
        this.dim = dim;
    }

    public ArrayList<String> getKeywords() {
        return keywords;
    }

    public void setKeywords(ArrayList<String> keywords) {
        this.keywords = keywords;
    }

    public ArrayList<Integer> getValues() {
        return values;
    }

    public void setValues(ArrayList<Integer> values) {
        this.values = values;
    }
}


DocVecManager.class

package com.syang;

import java.io.*;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;

/**
 * Created by Answer on 2017/6/24.
 */
public class DocVecManager {
    private int THRESHOLD = 5; // 關鍵詞閾值,頻數低於這個值的關鍵詞將被忽略

    public void test(){
        try {
            DocVector wukong = parseFile2Vector("f:///分詞詞頻-悟空傳");
            DocVector report03 = parseFile2Vector("f:///分詞詞頻-2003工作報告");
            DocVector report16 = parseFile2Vector("f:///分詞詞頻-2016工作報告");
            DocVector santi = parseFile2Vector("f:///分詞詞頻-三體I");

            DocVector[] dv = {report16,report03}; // 需要比較的的向量數組
            DocVector merged = merge(dv); // 求向量的並集
            List<DocVector> list = autoBuild(merged, dv); // 批量構建

            System.out.println("Similarity: "
                    + calSimilarity(list.get(0),list.get(1)));

        }catch (IOException e){
            e.printStackTrace();
        }
    }

    /**
     * 計算兩個向量的夾角餘弦
     * @param built1
     * @param built2
     * @return
     */
    public double calSimilarity(DocVector built1, DocVector built2){
        double multi = 0; //向量點乘
        double temp1 = 0, temp2 = 0; // 兩個向量的模
        ArrayList<Integer> list1 = built1.getValues();
        ArrayList<Integer> list2 = built2.getValues();
        for (int i = 0; i < built1.getDim(); i++){
            multi += list1.get(i) * list2.get(i);
            temp1 += list1.get(i) * list1.get(i);
            temp2 += list2.get(i) * list2.get(i);
        }
        return multi / (Math.sqrt(temp1) * Math.sqrt(temp2));
    }

    /**
     * 自動構建多個文檔向量
     * @param merged
     * @param dv
     * @return
     */
    public List<DocVector> autoBuild(DocVector merged, DocVector[] dv){
        List<DocVector> built = new ArrayList<>();
        // 將dv中的每個向量跟並集build
        for(int i = 0; i < dv.length; i++){
            built.add(buildVector(merged,dv[i]));
        }
        return built;
    }

    /**
     * 構建最終的文檔向量模型
     * 其結構爲:
     *      keywords爲兩個比較向量的keyword的全集,value爲相應的value,如果不包含在全集中則爲0.
     * @param merged  合併後的全集
     * @param vector  需要構建的向量
     * @return
     */
    public DocVector buildVector(DocVector merged, DocVector vector){
        ArrayList<String> fullSet = merged.getKeywords(); // 獲取合併後的keyword全集

        Integer[] values = new Integer[merged.getDim()];// 將要賦給並集的value數組
        Arrays.fill(values, 0); // 清零,確保不包含在全集中的keyword的value爲0,避免出現null
        // 向全集中對應的keyword賦value
        for(int i = 0; i < vector.getDim(); i++){
            int index = fullSet.indexOf(vector.getKeywords().get(i));// 在並集中查找vector的相應項
            // 只要index有效,就代表存在,那麼就把相應項的value寫到並集中
            if(index != -1){
                values[index] = vector.getValues().get(i);
            }
        }
        DocVector built = new DocVector();
        built.setKeywords(fullSet);
        ArrayList<Integer> list = new ArrayList<>(Arrays.asList(values)); // Integer[] values轉換爲ArrayList 並 set
        built.setValues(list);
        built.setDim(fullSet.size());
        return built;
    }


    /**
     * 求輸入的多個DocVector的並集
     * 合併多個DocVector的keywords,其values暫時爲null。
     * @param dv
     * @return
     */
    public DocVector merge(DocVector[] dv) throws IllegalArgumentException{
        DocVector merged = new DocVector();
        int total = 0;
        ArrayList<String> k;
        ArrayList<Integer> v = new ArrayList<>();
        k = dv[0].getKeywords();
        // 將dv中所有向量的keyword累加到k中
        for(int i = 1; i<dv.length; i++){
            k = arrayListUnion(k,dv[i].getKeywords());
        }
        merged.setDim(k.size());
        merged.setKeywords(k);
        merged.setValues(v);
        return merged;
    }


    /**
     * 功能:
     * 根據path按行讀取文件,並將每行分割爲keyword和value,再根據閾值決定是否裝入DocVector
     * @param path
     * @return DocVector
     * @throws IOException
     */
    public DocVector parseFile2Vector(String path) throws IOException{
        ArrayList<String> keywords = new ArrayList<>();
        ArrayList<Integer> values = new ArrayList<>();
        File file = new File(path); //  獲取文件句柄
        InputStreamReader read = new InputStreamReader(new FileInputStream(file),"utf-8");
        BufferedReader bufferedReader = new BufferedReader(read);
        String lineTxt = null;
        while((lineTxt = bufferedReader.readLine()) != null){
            String[] temp = lineTxt.split("\t| "); // 正則表達式匹配空格,分隔一行
            int t = Integer.parseInt(temp[1]);
            // 根據定義的閾值決定是否納入
            if(t > THRESHOLD) {
                keywords.add(temp[0]);
                values.add(t);
            }
        }
        read.close();
        DocVector vector = new DocVector();
        vector.setKeywords(keywords);
        vector.setValues(values);
        vector.setDim(keywords.size());
        return vector;
    }

    /**
     * 兩個整數集求並集
     * @param List1
     * @param List2
     * @return
     */
    public <T>ArrayList<T> arrayListUnion(
            ArrayList<T> List1, ArrayList<T> List2) {
        ArrayList<T> unionList = new ArrayList<T>();
        unionList.addAll(List1);
        unionList.addAll(List2);
        unionList = new ArrayList<T>(new HashSet<T>(unionList));
        return unionList;
    }

    /**
     * 思路:
     * 1. read data
     * 2. parse to vector
     * 3. 根據閾值選擇截斷vector的前 N 個特徵
     * 4. 求出兩個vector的全集
     * 5. 在全集中加入相應vector的頻數value(構建文檔向量)
     * 6. 計算cosine
     */
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章