期末大作业的其中一部分是要求对文档进行相似度计算,并提示可以用文档词向量的方法来做。于是查了一些资料。
然后引出了 空间向量模型(VSM) 这个概念。
向量空间模型(VSM:Vector Space Model)由Salton等人于20世纪70年代提出,并成功地应用于著名的SMART文本检索系统。 VSM概念简单,把对文本内容的处理简化为向量空间中的向量运算,并且它以空间上的相似度表达语义的相似度,直观易懂。当文档被表示为文档空间的向量,就可以通过计算向量之间的相似性来度量文档间的相似性。
看完这段,顿悟了!不得不佩服向量的强大力量!
既然知道了空间向量模型的存在,计算相似度就非常简单了!无非就是计算余弦值。
关键的地方是怎样构建文档向量?
原理&思路:
要构建文档向量,应该选取最能代表这个文档的元素(特征值),很显然这个元素是文档中的关键词。(获取关键词的主要方法是分词)
关键词 就是 特征值
分词,计算出文档中关键词的词频。
然后,文档特征值应该有两个维度,一个是关键词本身,另一个是关键词出现的频数。(慢慢有了向量的感觉了)
当然,一个文档不止一个关键词,把所有n个关键词堆到一起,就得到了一个n * 2的矩阵。
最后,计算相似度的时候只用到频数,为了方便表示,取第二列转置得到向量b(v1,v2,…,vn)。这个n维向量就是表示该文档的向量,每一维度表示一个特征值,维度的长度表示特征值的频数。
步骤:
因为不同文档的特征值唯独可能不一样,而且相似性计算是相对于双方来说的,所以这里还要对上面的文档向量进一步构建(归一化,让维度相同)。
对要比较的文档的文档向量的关键词求全集。
分别将文档向量跟得到的全集比较,将对应关键词的频数填到全集的频数上,这样就得到了要拿来比较的两个文档的文档向量。每个向量都包含了对方的特征值维度,两个向量在相同的向量空间中。
根据向量的夹角余弦公式计算,夹角余弦值就是相似度。
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%.
这个结果基本可以用来做文档分类了,要想得到更好的结果,应该优化分词的词典。
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 keywords;
private ArrayList values;
public int getDim() {
return dim;
}
public void setDim(int dim) {
this.dim = dim;
}
public ArrayList getKeywords() {
return keywords;
}
public void setKeywords(ArrayList keywords) {
this.keywords = keywords;
}
public ArrayList getValues() {
return values;
}
public void setValues(ArrayList 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 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 list1 = built1.getValues();
ArrayList 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 autoBuild(DocVector merged, DocVector[] dv){
List 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 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 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 k;
ArrayList v = new ArrayList<>();
k = dv[0].getKeywords();
// 将dv中所有向量的keyword累加到k中
for(int i = 1; i keywords = new ArrayList<>();
ArrayList 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 ArrayList arrayListUnion(
ArrayList List1, ArrayList List2) {
ArrayList unionList = new ArrayList();
unionList.addAll(List1);
unionList.addAll(List2);
unionList = new ArrayList(new HashSet(unionList));
return unionList;
}
/**
* 思路:
* 1. read data
* 2. parse to vector
* 3. 根据阈值选择截断vector的前 N 个特征
* 4. 求出两个vector的全集
* 5. 在全集中加入相应vector的频数value(构建文档向量)
* 6. 计算cosine
*/
}