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

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

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

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值