文章目錄
- 前言
- 一、理論知識
- 1.分詞
- 2.列出所有的詞
- 3.計算詞頻
- 4.寫出詞頻向量
- 5.計算相似度
- 二、java開發樣例
- 1.pom.xml
- 2.相似度計算代碼
- 結尾
前言
計算文章/字串的相似度有多種演算法,本文將采用java+jieba/hanlp分詞進行余弦相似性計算,一、理論知識
余弦距離,也稱為余弦相似度,是用向量空間中兩個向量夾角的余弦值作為衡量兩個個體間差異的大小的度量,
余弦值越接近1,就表明夾角越接近0度,也就是兩個向量越相似,這就叫"余弦相似性",
為了簡單起見,我們先從句子著手,
句子A:我喜歡看電視,不喜歡看電影,
句子B:我不喜歡看電視,也不喜歡看電影,
請問怎樣才能計算上面兩句話的相似程度?
基本思路是:如果這兩句話的用詞越相似,它們的內容就應該越相似,因此,可以從詞頻入手,計算它們的相似程度,
1.分詞
句子A:我/喜歡/看/電視,不/喜歡/看/電影,
句子B:我/不/喜歡/看/電視,也/不/喜歡/看/電影,
2.列出所有的詞
我,喜歡,看,電視,電影,不,也,
3.計算詞頻
句子A:我 1,喜歡 2,看 2,電視 1,電影 1,不 1,也 0,
句子B:我 1,喜歡 2,看 2,電視 1,電影 1,不 2,也 1,
4.寫出詞頻向量
句子A:[1, 2, 2, 1, 1, 1, 0]
句子B:[1, 2, 2, 1, 1, 2, 1]
到這里,問題就變成了如何計算這兩個向量的相似程度,
5.計算相似度
我們可以把它們想象成空間中的兩條線段,都是從原點([0, 0, …])出發,指向不同的方向,兩條線段之間形成一個夾角,如果夾角為0度,意味著方向相同、線段重合;如果夾角為90度,意味著形成直角,方向完全不相似;如果夾角為180度,意味著方向正好相反,因此,我們可以通過夾角的大小,來判斷向量的相似程度,夾角越小,就代表越相似,

以二維空間為例,上圖的a和b是兩個向量,我們要計算它們的夾角θ,余弦定理告訴我們,可以用下面的公式求得:


假定a向量是[x1, y1],b向量是[x2, y2],那么可以將余弦定理改寫成下面的形式:


數學家已經證明,余弦的這種計算方法對n維向量也成立,假定A和B是兩個n維向量,A是 [A1, A2, …, An] ,B是 [B1, B2, …, Bn] ,則A與B的夾角θ的余弦等于:

使用這個公式,我們就可以得到,句子A與句子B的夾角的余弦,

余弦值越接近1,就表明夾角越接近0度,也就是兩個向量越相似,所以,上面的句子A和句子B是很相似的,
二、java開發樣例
1.pom.xml
引入分詞jar包,樣例中使用了jieba和hanlp,實際選擇一個即可
<!-- jieba分詞 -->
<dependency>
<groupId>com.huaban</groupId>
<artifactId>jieba-analysis</artifactId>
<version>1.0.2</version>
</dependency>
<!-- hanlp分詞 -->
<dependency>
<groupId>com.hankcs</groupId>
<artifactId>hanlp</artifactId>
<version>portable-1.8.2</version>
</dependency>
2.相似度計算代碼
package com.neu.his.domain.control;
import com.google.common.util.concurrent.AtomicDouble;
import com.hankcs.hanlp.HanLP;
import com.hankcs.hanlp.seg.common.Term;
import com.huaban.analysis.jieba.JiebaSegmenter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@Service
public class QcSimilarDocDomain {
private static final Logger log = LoggerFactory.getLogger(QcSimilarDocDomain.class);
private static Pattern pattern = Pattern.compile("[\\pP‘’“”]");
private final double COS_DOUBLE = 0.8;
/**
* 文本相似度對比處理
*
* @param firstText
* @param secondText
* @return
*/
public boolean process(String firstText, String secondText) {
List<String> firstList = segmentByJieba(firstText);
Map<String, AtomicInteger> firstMap = getFrequency(firstList);
log.debug("第一個句子分詞結果:{}", firstList);
log.debug("第一個句子詞頻結果:{}", firstMap);
List<String> secondList = segmentByJieba(secondText);
Map<String, AtomicInteger> secondMap = getFrequency(secondList);
log.debug("第二個句子分詞結果:{}", secondList);
log.debug("第二個句子詞頻結果:{}", secondMap);
Set<String> allWordsSet = new TreeSet<>();
allWordsSet.addAll(firstList);
allWordsSet.addAll(secondList);
log.debug("所有詞:{}", allWordsSet);
double cos = cos(firstMap, secondMap, allWordsSet);
log.info("余弦相似度:{}", cos);
return cos >= COS_DOUBLE ? true : false;
}
/**
* jieba分詞
**/
public List<String> segmentByJieba(String words) {
JiebaSegmenter segmenter = new JiebaSegmenter();
List<String> resultList = segmenter.sentenceProcess(words);
//去除分詞標點符號
return resultList.stream().filter(s -> {
Matcher matcher = pattern.matcher(s);
return !matcher.find();
}).collect(Collectors.toList());
}
/**
* hanlp分詞
*
* @param text
* @return
*/
public List<String> segmentByHanLP(String text) {
List<Term> termList = HanLP.segment(text);
return termList.stream().map(s -> s.word).collect(Collectors.toList());
}
/**
* 計算詞頻
*
* @param words
* @return
*/
private static Map<String, AtomicInteger> getFrequency(List<String> words) {
Map<String, AtomicInteger> freq = new HashMap<>();
words.forEach(i -> freq.computeIfAbsent(i, k -> new AtomicInteger()).incrementAndGet());
return freq;
}
/**
* 計算余弦相似度
*
* @param firstMap
* @param secondMap
* @param allWordsSet
* @return
*/
public double cos(Map<String, AtomicInteger> firstMap, Map<String, AtomicInteger> secondMap, Set<String> allWordsSet) {
AtomicDouble ab = new AtomicDouble();// a.b
AtomicDouble aa = new AtomicDouble();// |a|的平方
AtomicDouble bb = new AtomicDouble();// |b|的平方
allWordsSet.parallelStream().forEach(word -> {
//看同一詞在a、b兩個集合出現的此次
AtomicInteger x1 = firstMap.get(word);
AtomicInteger x2 = secondMap.get(word);
if (x1 != null && x2 != null) {
//x1x2
double oneOfTheDimension = x1.doubleValue() * x2.doubleValue();
//+
ab.addAndGet(oneOfTheDimension);
}
if (x1 != null) {
//(x1)^2
double oneOfTheDimension = x1.doubleValue() * x1.doubleValue();
//+
aa.addAndGet(oneOfTheDimension);
}
if (x2 != null) {
//(x2)^2
double oneOfTheDimension = x2.doubleValue() * x2.doubleValue();
//+
bb.addAndGet(oneOfTheDimension);
}
});
//|a| 對aa開方
double aaa = Math.sqrt(aa.doubleValue());
//|b| 對bb開方
double bbb = Math.sqrt(bb.doubleValue());
//使用BigDecimal保證精確計算浮點數
//double aabb = aaa * bbb;
BigDecimal aabb = BigDecimal.valueOf(aaa).multiply(BigDecimal.valueOf(bbb));
//similarity=a.b/|a|*|b|
//divide引數說明:aabb被除數,9表示小數點后保留9位,最后一個表示用標準的四舍五入法
double cos = BigDecimal.valueOf(ab.get()).divide(aabb, 9, BigDecimal.ROUND_HALF_UP).doubleValue();
return cos;
}
public static void main(String[] args) {
QcSimilarDocDomain qcSimilarDocDomain = new QcSimilarDocDomain();
String firstText = "我喜歡看電視,不喜歡看電影,";
String secondText = "我不喜歡看電視,也不喜歡看電影,";
qcSimilarDocDomain.process(firstText, secondText);
}
}
結尾
- 感謝大家的耐心閱讀,如有建議請私信或評論留言,
- 如有識訓,勞煩支持,關注、點贊、評論、收藏均可,博主會經常更新,與大家共同進步
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/423688.html
標籤:AI
