余弦相似性

余弦的概念对我们来说并不陌生,中学数学就开始接触余弦的概念了,在三角形中,余弦的公式是:

java 智能问答系统 余弦相似度 余弦相似度文本_java



cosα=b2+c2−a22bc(式1−1)

在向量表示的三角形中,假设向量 a⃗ =(x1,y1) , b⃗ =(x2,y2) 则向量a⃗ ,和向量b⃗ 的夹角的余弦为:


cos(a⃗ ,b⃗ )=a⃗ ⋅b⃗ |a||b|(式1−2)


余弦有这样的性质:余弦值越接近1,就表明夹角越接近0度,也就是两个向量越相似,夹角等于0,即两个向量相等,这就叫”余弦相似性”。


那么该怎么利用余弦来计算文本的相似性了?首先来看一个例子。

文本1:我们喜欢足球
文本2:我喜欢踢足球

大致思路是:我们认为问用词越相似,则文本的相似度越高,所以计算文本相似性主要按照以下步骤进行:

  1. 分词
    此处利用 ansj 分词工具进行分词,文本1和文本2的分词结果为:
    文本1:我/喜欢/足球
    文本2:我们/喜欢/踢/足球
  2. 列出所有的词
    我, 喜欢, 踢, 我们, 足球
  3. 计算词频
    文本1: 我:1,喜欢:1,踢:0,我们:0,足球:1
    文本2: 我:0,喜欢:1,踢:1,我们:1,足球:1
  4. 转化为向量
    文本1:[1, 1, 0, 0, 1]
    文本2:[0, 1, 1, 1, 1]

代码示例

package com.myapp.ml.nlp;

import lombok.Data;

/**
 * Created by lionel on 16/12/21.
 */
@Data
public class WordValue {
    private String word;
    private Integer frequecy;

    public WordValue() {
    }

    public WordValue(String word, Integer frequecy) {
        this.word = word;
        this.frequecy = frequecy;
    }

}
package com.myapp.ml.nlp;

import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;
import org.ansj.util.FilterModifWord;
import org.apache.commons.lang3.StringUtils;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Created by lionel on 16/12/21.
 */
public class CosineSimilarity {
    /**
     * 向量余弦
     *
     * @param text1 文本1
     * @param text2 文本2
     * @return 向量余弦
     */
    public double cos(String text1, String text2) {
        if (StringUtils.isBlank(text2 + text1)) {
            return 0.0;
        }
        List<String> words = parse(text1);
        List<String> words1 = parse(text2);
        Set<String> set = mergeList(words, words1);
        List<WordValue> wordValueList = computeWordCount(words);
        List<WordValue> wordValueList1 = computeWordCount(words1);
        int[] vector = wordToVector(set, wordValueList);
        int[] vector1 = wordToVector(set, wordValueList1);
        int moduleA = 0;
        int moduleB = 0;
        int AMutiplyB = 0;
        for (int i = 0; i < vector.length; i++) {
            moduleA += vector[i] * vector[i];
            moduleB += vector1[i] * vector1[i];
            AMutiplyB += vector[i] * vector1[i];
        }
        return (double) AMutiplyB / (Math.sqrt(moduleA) * Math.sqrt(moduleB));

    }

    /**
     * 转化为向量
     *
     * @param set  所有的词表
     * @param list 词信息
     * @return 向量
     */

    private int[] wordToVector(Set<String> set, List<WordValue> list) {
        if (set == null || set.size() == 0 || list == null || list.size() == 0) {
            return null;
        }
        List<String> mergeList = new ArrayList<String>(set);
        int[] vector = new int[mergeList.size()];

        for (int i = 0; i < mergeList.size(); i++) {
            for (WordValue wordValue : list) {
                if (wordValue.getWord().equals(mergeList.get(i))) {
                    vector[i] = wordValue.getFrequecy();
                    break;
                } else {
                    vector[i] = 0;
                }
            }
        }
        return vector;
    }

    /**
     * 分词
     *
     * @param text 文本
     * @return 分词结果
     */

    private List<String> parse(String text) {
        if (StringUtils.isBlank(text)) {
            return null;
        }

        List<Term> terms = FilterModifWord.modifResult(ToAnalysis.parse(text));
        if (terms == null || terms.size() == 0) {
            return null;
        }
        List<String> words = new ArrayList<String>();
        for (Term term : terms) {
            if (StringUtils.isNotBlank(term.getName())) {
                words.add(term.getName());
            }
        }
        return words;
    }

    /**
     * 获取所有的词
     *
     * @param str1 文本1分词结果
     * @param str2 文本2分词结果
     * @return 返回所有的词,不重复
     */

    private Set<String> mergeList(List<String> str1, List<String> str2) {
        if (str1 == null || str1.size() == 0
                || str2 == null || str2.size() == 0) {
            return null;
        }
        Set<String> set = new HashSet<String>();
        for (String ele : str1) {
            set.add(ele);
        }
        for (String ele : str2) {
            set.add(ele);
        }
        return set;
    }

    /**
     * 计算词频
     *
     * @param list 分词结果
     * @return 词频
     */

    private List<WordValue> computeWordCount(List<String> list) {
        List<WordValue> wordValueList = new ArrayList<WordValue>();
        for (String element : list) {
            if (isInWordValueList(element, wordValueList)) {
                WordValue wordValue = wordValueList.get(getElementIndex(element, wordValueList));
                wordValue.setFrequecy(wordValue.getFrequecy() + 1);
            } else {
                WordValue wordValue = new WordValue(element, 1);
                wordValueList.add(wordValue);
            }
        }
        return wordValueList;
    }

    private int getElementIndex(String word, List<WordValue> wordValueList) {
        if (StringUtils.isBlank(word) || wordValueList == null || wordValueList.size() == 0) {
            return -1;
        }
        for (int i = 0; i < wordValueList.size(); i++) {
            if (wordValueList.get(i).getWord().equals(word)) {
                return i;
            }
        }
        return -1;
    }

    private boolean isInWordValueList(String str, List<WordValue> wordValueList) {
        if (StringUtils.isBlank(str) || wordValueList == null || wordValueList.size() == 0) {
            return false;
        }
        for (WordValue wordValue : wordValueList) {
            if (wordValue.getWord().equals(str)) {
                return true;
            }
        }
        return false;
    }
}

测试代码

package com.myapp.ml.nlp;

import org.junit.Test;

/**
 * Created by lionel on 16/12/21.
 */
public class CosineSimilarityTest {
    @Test
    public void test() {
        String text = "我喜欢足球";
        String text1 = "我们喜欢踢足球";
        CosineSimilarity cosineSimilarity = new CosineSimilarity();
        System.out.println(cosineSimilarity.cos(text, text1));//0.5773502691896258
    }
}

以上程序结果是0.5773502691896258,经验证结果是正确的。
注意:以上程序只符合简单的应用情况,不适用复杂的情况。另外,余弦相似性还可以用来判别文档的相似性,大致思路是:首先求出文档的关键词(可以利用 TF-IDF 方法),以后可以按照以上2,3,4步骤进行计算。