余弦相似性
余弦的概念对我们来说并不陌生,中学数学就开始接触余弦的概念了,在三角形中,余弦的公式是:
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:我喜欢踢足球
大致思路是:我们认为问用词越相似,则文本的相似度越高,所以计算文本相似性主要按照以下步骤进行:
- 分词
此处利用 ansj 分词工具进行分词,文本1和文本2的分词结果为:
文本1:我/喜欢/足球
文本2:我们/喜欢/踢/足球 - 列出所有的词
我, 喜欢, 踢, 我们, 足球 - 计算词频
文本1: 我:1,喜欢:1,踢:0,我们:0,足球:1
文本2: 我:0,喜欢:1,踢:1,我们:1,足球:1 - 转化为向量
文本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步骤进行计算。