搜索
您的当前位置:首页正文

java算法之余弦相似度计算字符串相似率

来源:爱够旅游网
java算法之余弦相似度计算字符串相似率

⽬录

概述

⼀、理论知识

1、说重点2、案例理论知识⼆、实际开发案例1、pom.xml2、main⽅法3、Tokenizer(分词⼯具类)4、Word(封装分词结果)5、CosineSimilarity(相似率具体实现⼯具类)6、AtomicFloat原⼦类三、总结概述

功能需求:最近在做通过爬⾍技术去爬取各⼤相关⽹站的新闻,储存到公司数据中。这⾥⾯就有⼀个技术点,就是如何保证你已爬取的新闻,再有相似的新闻

或者⼀样的新闻,那就不存储到数据库中。(因为有⽹站会去引⽤其它⽹站新闻,或者把其它⽹站新闻拿过来稍微改下内容就发布到⾃⼰⽹站中)。

解析⽅案:最终就是采⽤余弦相似度算法,来计算两个新闻正⽂的相似度。现在⾃⼰写⼀篇博客总结下。

⼀、理论知识

先推荐⼀篇博客,对于余弦相似度算法的理论讲的⽐较清晰,我们也是按照这个⽅式来计算相似度的。⽹址:相似度算法之余弦相似度。

1、说重点

我这边先把计算两个字符串的相似度理论知识再梳理⼀遍。(1)⾸先是要明⽩通过向量来计算相识度公式。

(2)明⽩:余弦值越接近1,也就是两个向量越相似,这就叫\"余弦相似性\",余弦值越接近0,也就是两个向量越不相似,也就是这两个字符串越不相似。

2、案例理论知识

举⼀个例⼦来说明,⽤上述理论计算⽂本的相似性。为了简单起见,先从句⼦着⼿。句⼦A:这只⽪靴号码⼤了。那只号码合适。句⼦B:这只⽪靴号码不⼩,那只更合适。怎样计算上⾯两句话的相似程度?

基本思路是:如果这两句话的⽤词越相似,它们的内容就应该越相似。因此,可以从词频⼊⼿,计算它们的相似程度。第⼀步,分词。

句⼦A:这只/⽪靴/号码/⼤了。那只/号码/合适。句⼦B:这只/⽪靴/号码/不/⼩,那只/更/合适。第⼆步,计算词频。(也就是每个词语出现的频率)

句⼦A:这只1,⽪靴1,号码2,⼤了1。那只1,合适1,不0,⼩0,更0句⼦B:这只1,⽪靴1,号码1,⼤了0。那只1,合适1,不1,⼩1,更1第三步,写出词频向量。

句⼦A:(1,1,2,1,1,1,0,0,0)句⼦B:(1,1,1,0,1,1,1,1,1)第四步:运⽤上⾯的公式:计算如下:

计算结果中夹⾓的余弦值为0.81⾮常接近于1,所以,上⾯的句⼦A和句⼦B是基本相似的

⼆、实际开发案例

我把我们实际开发过程中字符串相似率计算代码分享出来。

1、pom.xml

展⽰⼀些主要jar包

org.apache.commons commons-lang3 3.5

org.projectlombok lombok

com.hankcs hanlp

portable-1.6.5

2、main⽅法

/**

* 计算两个字符串的相识度 */

public class Similarity {

public static final String content1=\"今天⼩⼩和爸爸⼀起去摘草莓,⼩⼩说今天的草莓特别的酸,⽽且特别的⼩,关键价格还贵\"; public static final String content2=\"今天⼩⼩和妈妈⼀起去草原⾥采草莓,今天的草莓味道特别好,⽽且价格还挺实惠的\";

public static void main(String[] args) {

double score=CosineSimilarity.getSimilarity(content1,content2); System.out.println(\"相似度:\"+score);

score=CosineSimilarity.getSimilarity(content1,content1); System.out.println(\"相似度:\"+score); } }

先看运⾏结果:

通过运⾏结果得出:

(1)第⼀次⽐较相似率为:0.772853 (说明这两条句⼦还是挺相似的),第⼆次⽐较相似率为:1.0 (说明⼀模⼀样)。(2)我们可以看到这个句⼦的分词效果,后⾯是词性。

3、Tokenizer(分词⼯具类)

import com.hankcs.hanlp.HanLP;

import com.hankcs.hanlp.seg.common.Term;import java.util.List;

import java.util.stream.Collectors;

/**

* 中⽂分词⼯具类*/public class Tokenizer {

/**

* 分词*/

public static List segment(String sentence) { //1、 采⽤HanLP中⽂⾃然语⾔处理中标准分词进⾏分词 List termList = HanLP.segment(sentence); //上⾯控制台打印信息就是这⾥输出的 System.out.println(termList.toString());

//2、重新封装到Word对象中(term.word代表分词后的词语,term.nature代表改词的词性)

return termList.stream().map(term -> new Word(term.word, term.nature.toString())).collect(Collectors.toList()); }}

4、Word(封装分词结果)

这⾥⾯真正⽤到的其实就词名和权重。

import lombok.Data;import java.util.Objects;

/**

* 封装分词结果*/@Data

public class Word implements Comparable { // 词名

private String name; // 词性

private String pos; // 权重,⽤于词向量分析 private Float weight;

public Word(String name, String pos) { this.name = name; this.pos = pos; }

@Override

public int hashCode() {

return Objects.hashCode(this.name); }

@Override

public boolean equals(Object obj) { if (obj == null) { return false; }

if (getClass() != obj.getClass()) { return false; }

final Word other = (Word) obj;

return Objects.equals(this.name, other.name); }

@Override

public String toString() {

StringBuilder str = new StringBuilder(); if (name != null) { str.append(name); }

if (pos != null) {

str.append(\"/\").append(pos); }

return str.toString(); }

@Override

public int compareTo(Object o) { if (this == o) { return 0; }

if (this.name == null) { return -1; }

if (o == null) { return 1; }

if (!(o instanceof Word)) { return 1; }

String t = ((Word) o).getName(); if (t == null) { return 1; }

return this.name.compareTo(t); }}

5、CosineSimilarity(相似率具体实现⼯具类)

import com.jincou.algorithm.tokenizer.Tokenizer;import com.jincou.algorithm.tokenizer.Word;import org.apache.commons.lang3.StringUtils;import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.util.CollectionUtils;import java.math.BigDecimal;

import java.util.*;

import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.atomic.AtomicInteger;

/**

* 判定⽅式:余弦相似度,通过计算两个向量的夹⾓余弦值来评估他们的相似度 余弦夹⾓原理: 向量a=(x1,y1),向量b=(x2,y2) similarity=a.b/|a|*|b| a.b=x1x2+y1y2 * |a|=根号[(x1)^2+(y1)^2],|b|=根号[(x2)^2+(y2)^2]*/public class CosineSimilarity {

protected static final Logger LOGGER = LoggerFactory.getLogger(CosineSimilarity.class); /**

* 1、计算两个字符串的相似度 */

public static double getSimilarity(String text1, String text2) {

//如果wei空,或者字符长度为0,则代表完全相同

if (StringUtils.isBlank(text1) && StringUtils.isBlank(text2)) { return 1.0; }

//如果⼀个为0或者空,⼀个不为,那说明完全不相似

if (StringUtils.isBlank(text1) || StringUtils.isBlank(text2)) { return 0.0; }

//这个代表如果两个字符串相等那当然返回1了(这个我为了让它也分词计算⼀下,所以注释掉了)// if (text1.equalsIgnoreCase(text2)) {// return 1.0;// }

//第⼀步:进⾏分词

List words1 = Tokenizer.segment(text1); List words2 = Tokenizer.segment(text2); return getSimilarity(words1, words2); }

/**

* 2、对于计算出的相似度保留⼩数点后六位 */

public static double getSimilarity(List words1, List words2) { double score = getSimilarityImpl(words1, words2);

//(int) (score * 1000000 + 0.5)其实代表保留⼩数点后六位 ,因为1034234.213强制转换不就是1034234。对于强制转换添加0.5就等于四舍五⼊ score = (int) (score * 1000000 + 0.5) / (double) 1000000; return score; }

/**

* ⽂本相似度计算 判定⽅式:余弦相似度,通过计算两个向量的夹⾓余弦值来评估他们的相似度 余弦夹⾓原理: 向量a=(x1,y1),向量b=(x2,y2) similarity=a.b/|a|*|b| a.b=x1x2+y1y2 * |a|=根号[(x1)^2+(y1)^2],|b|=根号[(x2)^2+(y2)^2] */

public static double getSimilarityImpl(List words1, List words2) { // 向每⼀个Word对象的属性都注⼊weight(权重)属性值 taggingWeightByFrequency(words1, words2);

//第⼆步:计算词频

//通过上⼀步让每个Word对象都有权重值,那么在封装到map中(key是词,value是该词出现的次数(即权重)) Map weightMap1 = getFastSearchMap(words1); Map weightMap2 = getFastSearchMap(words2); //将所有词都装⼊set容器中

Set words = new HashSet<>(); words.addAll(words1); words.addAll(words2);

AtomicFloat ab = new AtomicFloat();// a.b

AtomicFloat aa = new AtomicFloat();// |a|的平⽅ AtomicFloat bb = new AtomicFloat();// |b|的平⽅ // 第三步:写出词频向量,后进⾏计算 words.parallelStream().forEach(word -> { //看同⼀词在a、b两个集合出现的此次

Float x1 = weightMap1.get(word.getName()); Float x2 = weightMap2.get(word.getName()); if (x1 != null && x2 != null) { //x1x2

float oneOfTheDimension = x1 * x2; //+

ab.addAndGet(oneOfTheDimension); }

if (x1 != null) { //(x1)^2

float oneOfTheDimension = x1 * x1; //+

aa.addAndGet(oneOfTheDimension); }

if (x2 != null) { //(x2)^2

float oneOfTheDimension = x2 * x2;

//+

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; }

/**

* 向每⼀个Word对象的属性都注⼊weight(权重)属性值 */

protected static void taggingWeightByFrequency(List words1, List words2) { if (words1.get(0).getWeight() != null && words2.get(0).getWeight() != null) { return; }

//词频统计(key是词,value是该词在这段句⼦中出现的次数) Map frequency1 = getFrequency(words1); Map frequency2 = getFrequency(words2);

//如果是DEBUG模式输出词频统计信息// if (LOGGER.isDebugEnabled()) {

// LOGGER.debug(\"词频统计1:\\n{}\// LOGGER.debug(\"词频统计2:\\n{}\// }

// 标注权重(该词出现的次数)

words1.parallelStream().forEach(word -> word.setWeight(frequency1.get(word.getName()).floatValue())); words2.parallelStream().forEach(word -> word.setWeight(frequency2.get(word.getName()).floatValue())); }

/**

* 统计词频

* @return 词频统计图 */

private static Map getFrequency(List words) {

Map freq = new HashMap<>(); //这步很帅哦

words.forEach(i -> freq.computeIfAbsent(i.getName(), k -> new AtomicInteger()).incrementAndGet()); return freq; }

/**

* 输出:词频统计信息 */

private static String getWordsFrequencyString(Map frequency) { StringBuilder str = new StringBuilder();

if (frequency != null && !frequency.isEmpty()) { AtomicInteger integer = new AtomicInteger();

frequency.entrySet().stream().sorted((a, b) -> b.getValue().get() - a.getValue().get()).forEach(

i -> str.append(\"\\").append(integer.incrementAndGet()).append(\"、\").append(i.getKey()).append(\"=\") .append(i.getValue()).append(\"\\n\")); }

str.setLength(str.length() - 1); return str.toString(); }

/**

* 构造权重快速搜索容器 */

protected static Map getFastSearchMap(List words) { if (CollectionUtils.isEmpty(words)) { return Collections.emptyMap(); }

Map weightMap = new ConcurrentHashMap<>(words.size()); words.parallelStream().forEach(i -> { if (i.getWeight() != null) {

weightMap.put(i.getName(), i.getWeight()); } else {

LOGGER.error(\"no word weight info:\" + i.getName()); } });

return weightMap; }}

这个具体实现代码因为思维很紧密所以有些地⽅写的⽐较绕,同时还⼿写了AtomicFloat原⼦类。

6、AtomicFloat原⼦类

import java.util.concurrent.atomic.AtomicInteger;/**

* jdk没有AtomicFloat,写⼀个 */

public class AtomicFloat extends Number { private AtomicInteger bits; public AtomicFloat() { this(0f); }

public AtomicFloat(float initialValue) {

bits = new AtomicInteger(Float.floatToIntBits(initialValue)); }

//叠加

public final float addAndGet(float delta) { float expect; float update; do {

expect = get();

update = expect + delta;

} while (!this.compareAndSet(expect, update)); return update; }

public final float getAndAdd(float delta) { float expect; float update; do {

expect = get();

update = expect + delta;

} while (!this.compareAndSet(expect, update)); return expect; }

public final float getAndDecrement() { return getAndAdd(-1); }

public final float decrementAndGet() { return addAndGet(-1); }

public final float getAndIncrement() { return getAndAdd(1); }

public final float incrementAndGet() { return addAndGet(1); }

public final float getAndSet(float newValue) { float expect; do {

expect = get();

} while (!this.compareAndSet(expect, newValue)); return expect; }

public final boolean compareAndSet(float expect, float update) {

return bits.compareAndSet(Float.floatToIntBits(expect), Float.floatToIntBits(update)); }

public final void set(float newValue) {

bits.set(Float.floatToIntBits(newValue)); }

public final float get() {

return Float.intBitsToFloat(bits.get()); }

@Override

public float floatValue() { return get(); }

@Override

public double doubleValue() { return (double) floatValue(); }

@Override

public int intValue() { return (int) get(); }

@Override

public long longValue() { return (long) get(); }

@Override

public String toString() {

return Float.toString(get()); }}

三、总结

把⼤致思路再捋⼀下:

(1)先分词:分词当然要按⼀定规则,不然随便分那也没有意义,那这⾥通过采⽤HanLP中⽂⾃然语⾔处理中标准分词进⾏分词。(2)统计词频:就统计上⾯词出现的次数。

(3)通过每⼀个词出现的次数,变成⼀个向量,通过向量公式计算相似率。

以上就是java算法之余弦相似度计算字符串相似率的详细内容,更多关于java算法的资料请关注其它相关⽂章!

因篇幅问题不能全部显示,请点此查看更多更全内容

Top