bm25 是一种用来评价搜索词和文档之间相关性的算法,它是一种基于概率检索模型提出的算法。
它的出现主要是解决TF-IDF算法中 TF的影响可无限增大的不足,本质上 BM25是基于TF-IDF并做了改进的算法。
如图所示,传统的TF-IDF算法中 词频的影响程度是无限增大的,换而言之就是关键词出现的越频繁,TF-IDF相关度就越高。稍微有点简单粗暴;而BM25算法就是让词频的影响到达一定程度后趋于收敛,相比而已更加符合自然语言相关程度的实际逻辑。
参考:
《搜索中的权重度量利器: TF-IDF和BM25》:https://mp.weixin.qq.com/s/T9lFJDCCEwd_PfpPbjklDA
《NLP之TF-IDF与BM25原理探究》:https://www.cnblogs.com/johnnyzen/p/11298273.html
已经非常详细了,我这儿写了一个JAVA的实现,IDF字典是直接使用的 jieba分词项目中的文件。
公式: Similarity(word|documents)=IDFScore∗(k+1)∗tf / k∗(1.0−b+b∗|D|avgDl)+tf
package com.zjf.seo.core.algorithm;
import java.util.List;
import java.util.Map;
import com.zjf.seo.core.utils.StringUtils;
/**
* BM25 相关度算法实现
* @author zhaojunfu
*
* 公式: Similarity(word|documents)=IDFScore∗(k+1)∗tf / k∗(1.0−b+b∗|D|avgDl)+tf
*
*
*/
public class BM25 {
//常量k,用来限制TF值的增长极限 默认1.2
private double k = 1.2;
//b是一个常数,它的作用是规定L对评分的影响有多大
private double b;
/**
* BM25 相关度算法实现
* @param k 常量k,用来限制TF值的增长极限 默认1.2
* @param b b是一个常数,它的作用是规定L对评分的影响有多大
*/
public BM25(double k, double b) {
super();
this.k = k;
this.b = b;
}
/**
* BM25 相关度算法实现 k=1.2
* @param b b是一个常数,它的作用是规定L对评分的影响有多大
*/
public BM25(double b) {
super();
this.b = b;
}
/**
* 计算 关键词 在文档中的相关值
* @param idf 关键词的逆文档频率
* @param tf 关键词在文档中的词频
* @param L 当前文档长度/平均文档长度
* @return
*/
public double cal(double idf,double tf,double L){
double v = 0;
v= (idf*(k+1)*tf) /(k * (1.0-b+b*L)+tf);
return v;
}
/**
* 计算 关键词 在文档中的相关值
* @param keywords 关键词
* @param doc 关键词所在文档的全文
* @param docs 所有文档集
* @param idfMap idf字典表
* @return
*/
public double cal(String keywords,String doc,Listdocs,MapidfMap){
Double idf = idfMap.get(keywords);
if(null==idf) idf=1.0d;
Double tf = (double)StringUtils.count(doc, keywords)*keywords.length() / doc.length();
double avgLength = calAvgLength(docs);
Double L = (double)doc.length()/avgLength;
return cal(idf,tf,L);
}
private double calAvgLength(Listdocs) {
if(docs==null || docs.size()<=0) {
throw new RuntimeException("给定文档集不能为空");
}
int s = 0;
for(String d:docs){
s+=d.length();
}
return (double)s/(double)docs.size();
}
}
测试效果:
package com.zjf.seo.core;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.zjf.seo.core.algorithm.BM25;
import junit.framework.TestCase;
public class BM25Test extends TestCase{
public void test(){
BM25 bm25= new BM25(1.2,0.75);
String keywords = "租房";
Listdocs = new ArrayList();
docs.add("深圳保障房计划给出最新公租房、安居房消息!没房的赶紧来看!");
docs.add("在深圳,有多少人每个月最大的一笔支出就是房租。所以大家都挺关心公租房消息的,毕竟公租房能让房租这笔支出少一些又或者是期待安居房能让自己有点买房的机会。");
docs.add("深圳的保障房工作近几年进展就很不错。从最初的廉租房、公租房、经济适用住房,发展到今天的公租房、安居房和人才住房。");
docs.add("保障群体从最初的户籍低收入家庭,扩展到现在的户籍中低收入家庭、人才家庭,以及为城市提供基本公共服务的公交司机、环卫工人和先进制造业职工等群体");
docs.add("好消息,新版租房合同来袭,在深圳租房的你有福了!");
MapidfMap = new HashMap();
loadIDFMap(idfMap, this.getClass().getResourceAsStream("idf_dict.txt"));
for(String doc:docs){
System.out.println(keywords+"-bm25计算:"+bm25.cal(keywords, doc, docs, idfMap));
}
}
private static void loadIDFMap(Mapmap, InputStream in ){
BufferedReader bufr;
try
{
bufr = new BufferedReader(new InputStreamReader(in));
String line=null;
while((line=bufr.readLine())!=null) {
String[] kv=line.trim().split(" ");
map.put(kv[0],Double.parseDouble(kv[1]));
}
try
{
bufr.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
测试结果: 使用参数 k=1.2 b=0.75
可以看出文档5的“租房”相关度最高,而文档4因为根本没出现过“租房”相关度为0。
参数b的作用是调节文档长度的影响,值越大则最终值对于文档长度越敏感。
1、深圳保障房计划给出最新公租房、安居房消息!没房的赶紧来看!
租房-bm25计算:1.5994173445298407
2、在深圳,有多少人每个月最大的一笔支出就是房租。所以大家都挺关心公租房消息的,毕竟公租房能让房租这笔支出少一些又或者是期待安居房能让自己有点买房的机会。
租房-bm25计算:0.6491108274274898
3、深圳的保障房工作近几年进展就很不错。从最初的廉租房、公租房、经济适用住房,发展到今天的公租房、安居房和人才住房。
租房-bm25计算:1.5675707523417919
4、保障群体从最初的户籍低收入家庭,扩展到现在的户籍中低收入家庭、人才家庭,以及为城市提供基本公共服务的公交司机、环卫工人和先进制造业职工等群体
租房-bm25计算:0.0
5、好消息,新版租房合同来袭,在深圳租房的你有福了!
租房-bm25计算:3.8257399202973743
收敛效果测试:
docs.add("好消息,租房新版租房合同租房来袭,在深圳租房租房的你有福了租房!");
docs.add("好消息,租房新版租房合同租房来袭,在深圳租房租房的你有福了租房!租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租");
docs.add("好消息,租房新版租房合同租房来袭,在深圳租房租房的你有福了租房!租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租房租");
当这三个文档去计算 “租房”相关度时,结果如下:(此时 k=1.2 , b=0 忽略文档长度的影响)
租房-bm25计算:4.87433980096381
租房-bm25计算:8.507938561682286
租房-bm25计算:8.831724589865122
可以发现 词频到了一定程度就趋于收敛了,再使劲重复 租房 ,相关度提升地就不明显了。
如果将b 改回0.75 ,即考虑篇幅的因素,结果如下:
租房-bm25计算:7.659676830085987
租房-bm25计算:6.500512115234726
租房-bm25计算:4.717086835182325
可以发现 复读机式的重复关键词,相关度反而低了,这是因为后面两个文档篇幅远大于整个搜索集的平均篇幅,同时因为词频的影响收敛,所以整体值就降低了。