文档向量模型及其实践-计算文档的相似度

期末大作业的其中一部分是要求对文档进行相似度计算,并提示可以用文档词向量的方法来做。于是查了一些资料。

然后引出了 空间向量模型(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.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
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
*/
}

坚持原创技术分享,您的支持将鼓励我继续创作!