期末大作业的其中一部分是要求对文档进行相似度计算,并提示可以用文档词向量的方法来做。于是查了一些资料。
然后引出了 空间向量模型(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
*/
}