java算法之余弦相似度计算字符串相似率
⽬录
概述
⼀、理论知识
1、说重点
2、案例理论知识
⼆、实际开发案例
1、l
2、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、l
展⽰⼀些主要jar包
<!--结合操作⼯具包-->
<dependency>
<groupId>org.apachemons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.5</version>
</dependency>
<!--bean实体注解⼯具包-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--汉语⾔包,主要⽤于分词-->
<dependency>
<groupId>com.hankcs</groupId>
<artifactId>hanlp</artifactId>
<version>portable-1.6.5</version>
</dependency>
2、main⽅法
/
**
* 计算两个字符串的相识度
*/
public class Similarity {
public static final String content1="今天⼩⼩和爸爸⼀起去摘草莓,⼩⼩说今天的草莓特别的酸,⽽且特别的⼩,关键价格还贵";
public static final String content2="今天⼩⼩和妈妈⼀起去草原⾥采草莓,今天的草莓味道特别好,⽽且价格还挺实惠的";
public static void main(String[] args) {
double Similarity(content1,content2);
System.out.println("相似度:"+score);
Similarity(content1,content1);
System.out.println("相似度:"+score);
}
}
先看运⾏结果:
通过运⾏结果得出:
(1)第⼀次⽐较相似率为:0.772853 (说明这两条句⼦还是挺相似的),第⼆次⽐较相似率为:1.0 (说明⼀模⼀样)。(2)我们可以看到这个句⼦的分词效果,后⾯是词性。
3、Tokenizer(分词⼯具类)
import com.hankcs.hanlp.HanLP;
import com.hankcs.hanlp.segmon.Term;
import java.util.List;
import java.util.stream.Collectors;
/**
* 中⽂分词⼯具类*/
public class Tokenizer {
/**
* 分词*/
public static List<Word> segment(String sentence) {
//1、采⽤HanLP中⽂⾃然语⾔处理中标准分词进⾏分词
List<Term> termList = HanLP.segment(sentence);
//上⾯控制台打印信息就是这⾥输出的
System.out.String());
//2、重新封装到Word对象中(term.word代表分词后的词语,term.nature代表改词的词性)
return termList.stream().map(term -> new Word(term.word, String())).List());
}
}
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() != Class()) {
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);
}
String();
}
@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.namepareTo(t);
}
}
5、CosineSimilarity(相似率具体实现⼯具类)import com.kenizer.Tokenizer;
import com.kenizer.Word;
import org.apachemons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import urrent.ConcurrentHashMap;
import urrent.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 = Logger(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<Word> words1 = Tokenizer.segment(text1);
List<Word> words2 = Tokenizer.segment(text2);
return getSimilarity(words1, words2);
}
/**
* 2、对于计算出的相似度保留⼩数点后六位
*/
public static double getSimilarity(List<Word> words1, List<Word> 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<Word> words1, List<Word> words2) {
// 向每⼀个Word对象的属性都注⼊weight(权重)属性值
taggingWeightByFrequency(words1, words2);
//第⼆步:计算词频
//通过上⼀步让每个Word对象都有权重值,那么在封装到map中(key是词,value是该词出现的次数(
即权重))
Map<String, Float> weightMap1 = getFastSearchMap(words1);
Map<String, Float> weightMap2 = getFastSearchMap(words2);
//将所有词都装⼊set容器中
Set<Word> 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 = (Name());
Float x2 = (Name());
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.()).divide(aabb, 9, BigDecimal.ROUND_HALF_UP).doubleValue();
return cos;
}
/**
* 向每⼀个Word对象的属性都注⼊weight(权重)属性值
*/
protected static void taggingWeightByFrequency(List<Word> words1, List<Word> words2) {
if ((0).getWeight() != null && (0).getWeight() != null) {
return;
}
//词频统计(key是词,value是该词在这段句⼦中出现的次数)
Map<String, AtomicInteger> frequency1 = getFrequency(words1);
Map<String, AtomicInteger> frequency2 = getFrequency(words2);
/
/如果是DEBUG模式输出词频统计信息
// if (LOGGER.isDebugEnabled()) {
// LOGGER.debug("词频统计1:\n{}", getWordsFrequencyString(frequency1));
// LOGGER.debug("词频统计2:\n{}", getWordsFrequencyString(frequency2));
// }
// 标注权重(该词出现的次数)
words1.parallelStream().forEach(word -> word.(Name()).floatValue()));
words2.parallelStream().forEach(word -> word.(Name()).floatValue()));
}
/**
* 统计词频
* @return 词频统计图
*/
private static Map<String, AtomicInteger> getFrequency(List<Word> words) {
Map<String, AtomicInteger> freq = new HashMap<>();
//这步很帅哦
words.forEach(i -> Name(), k -> new AtomicInteger()).incrementAndGet());
return freq;
}
/**
* 输出:词频统计信息
*/
private static String getWordsFrequencyString(Map<String, AtomicInteger> frequency) {
StringBuilder str = new StringBuilder();
if (frequency != null && !frequency.isEmpty()) {
AtomicInteger integer = new AtomicInteger();
i -> str.append("\t").append(integer.incrementAndGet()).append("、").Key()).append("=")
.Value()).append("\n"));
}
str.setLength(str.length() - 1);
String();
}
/**
* 构造权重快速搜索容器
*/
protected static Map<String, Float> getFastSearchMap(List<Word> words) {
if (CollectionUtils.isEmpty(words)) {
ptyMap();
}
Map<String, Float> weightMap = new ConcurrentHashMap<>(words.size());
words.parallelStream().forEach(i -> {
if (i.getWeight() != null) {
weightMap.Name(), i.getWeight());
} else {
<("no word weight info:" + i.getName());
}
});
return weightMap;
}
}
这个具体实现代码因为思维很紧密所以有些地⽅写的⽐较绕,同时还⼿写了AtomicFloat原⼦类。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论