期末大作業的其中一部分是要求對文檔進行相似度計算,並提示可以用文檔詞向量的方法來做。於是查了一些資料。
然後引出了 空間向量模型(VSM) 這個概念。
-
空間向量模型
向量空間模型(VSM:Vector Space Model)由Salton等人於20世紀70年代提出,併成功地應用於著名的SMART文本檢索系統。 VSM概念簡單,把對文本內容的處理簡化爲向量空間中的向量運算,並且它以空間上的相似度表達語義的相似度,直觀易懂。當文檔被表示爲文檔空間的向量,就可以通過計算向量之間的相似性來度量文檔間的相似性。
看完這段,頓悟了!不得不佩服向量的強大力量!
既然知道了空間向量模型的存在,計算相似度就非常簡單了!無非就是計算餘弦值。
關鍵的地方是怎樣構建文檔向量?
-
構建文檔向量
原理&思路:
-
要構建文檔向量,應該選取最能代表這個文檔的元素(特徵值),很顯然這個元素是文檔中的關鍵詞。(獲取關鍵詞的主要方法是分詞)
關鍵詞 就是 特徵值
-
分詞,計算出文檔中關鍵詞的詞頻。
-
然後,文檔特徵值應該有兩個維度,一個是關鍵詞本身,另一個是關鍵詞出現的頻數。(慢慢有了向量的感覺了)
-
當然,一個文檔不止一個關鍵詞,把所有n個關鍵詞堆到一起,就得到了一個n * 2的矩陣。
-
最後,計算相似度的時候只用到頻數,爲了方便表示,取第二列轉置得到向量b(v1,v2,…,vn)。這個n維向量就是表示該文檔的向量,每一維度表示一個特徵值,維度的長度表示特徵值的頻數。
-
-
相似性計算
步驟:
-
因爲不同文檔的特徵值唯獨可能不一樣,而且相似性計算是相對於雙方來說的,所以這裏還要對上面的文檔向量進一步構建(歸一化,讓維度相同)。
-
對要比較的文檔的文檔向量的關鍵詞求全集。
-
分別將文檔向量跟得到的全集比較,將對應關鍵詞的頻數填到全集的頻數上,這樣就得到了要拿來比較的兩個文檔的文檔向量。每個向量都包含了對方的特徵值維度,兩個向量在相同的向量空間中。
-
根據向量的夾角餘弦公式計算,夾角餘弦值就是相似度。
-
-
計算結果
說明:
- 第一組計算對象爲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
*/
}