1. 程式人生 > 實用技巧 >BM25演算法語義相似度計算

BM25演算法語義相似度計算

原理

BM25演算法,通常用來作搜尋相關性平分。一句話概況其主要思想:對Query進行語素解析,生成語素qi;然後,對於每個搜尋結果D,計算每個語素qi與D的相關性得分,最後,將qi相對於D的相關性得分進行加權求和,從而得到Query與D的相關性得分。
BM25演算法的一般性公式如下:


其中,Q表示Query,qi表示Q解析之後的一個語素(對中文而言,我們可以把對Query的分詞作為語素分析,每個詞看成語素qi。);d表示一個搜尋結果文件;Wi表示語素qi的權重;R(qi,d)表示語素qi與文件d的相關性得分。
下面我們來看如何定義Wi。判斷一個詞與一個文件的相關性的權重,方法有多種,較常用的是IDF。這裡以IDF為例,公式如下:



其中,N為索引中的全部文件數,n(qi)為包含了qi的文件數。
根據IDF的定義可以看出,對於給定的文件集合,包含了qi的文件數越多,qi的權重則越低。也就是說,當很多文件都包含了qi時,qi的區分度就不高,因此使用qi來判斷相關性時的重要度就較低。
我們再來看語素qi與文件d的相關性得分R(qi,d)。首先來看BM25中相關性得分的一般形式:


其中,k1,k2,b為調節因子,通常根據經驗設定,一般k1=2,b=0.75;fi為qi在d中的出現頻率,qfi為qi在Query中的出現頻率。dl為文件d的長度,avgdl為所有文件的平均長度。由於絕大部分情況下,qi在Query中只會出現一次,即qfi=1,因此公式可以簡化為:


從K的定義中可以看到,引數b的作用是調整文件長度對相關性影響的大小。b越大,文件長度的對相關性得分的影響越大,反之越小。而文件的相對長度越長,K值將越大,則相關性得分會越小。這可以理解為,當文件較長時,包含qi的機會越大,因此,同等fi的情況下,長文件與qi的相關性應該比短文件與qi的相關性弱。
綜上,BM25演算法的相關性得分公式可總結為:


從BM25的公式可以看到,通過使用不同的語素分析方法、語素權重判定方法,以及語素與文件的相關性判定方法,我們可以衍生出不同的搜尋相關性得分計算方法,這就為我們設計演算法提供了較大的靈活性。


參考來源:https://www.jianshu.com/p/1e498888f505


CODING

import math
import jieba
# from utils import utils

STOPWORDS = open('resources/stopwords.txt', 'r', encoding='utf8').readlines()
STOPWORDS = {each.strip():1 for each in STOPWORDS}

# 測試文字
text = '''
自然語言處理是電腦科學領域與人工智慧領域中的一個重要方向。
它研究能實現人與計算機之間用自然語言進行有效通訊的各種理論和方法。
自然語言處理是一門融語言學、電腦科學、數學於一體的科學。
因此,這一領域的研究將涉及自然語言,即人們日常使用的語言,
所以它與語言學的研究有著密切的聯絡,但又有重要的區別。
自然語言處理並不是一般地研究自然語言,
而在於研製能有效地實現自然語言通訊的計算機系統,
特別是其中的軟體系統。因而它是電腦科學的一部分。
'''

class BM25(object):

    def __init__(self, docs):
        self.D = len(docs)
        self.avgdl = sum([len(doc)+0.0 for doc in docs]) / self.D
        self.docs = docs
        self.f = []  # 列表的每一個元素是一個dict,dict儲存著一個文件中每個詞的出現次數
        self.df = {} # 儲存每個詞及出現了該詞的文件數量
        self.idf = {} # 儲存每個詞的idf值
        self.k1 = 1.5
        self.b = 0.75
        self.init()

    def init(self):
        for doc in self.docs:
            tmp = {}
            for word in doc:
                tmp[word] = tmp.get(word, 0) + 1  # 儲存每個文件中每個詞的出現次數
            self.f.append(tmp)
            for k in tmp.keys():
                self.df[k] = self.df.get(k, 0) + 1
        for k, v in self.df.items():
            self.idf[k] = math.log(self.D-v+0.5)-math.log(v+0.5)

    def sim(self, doc, index):
        score = 0
        for word in doc:
            if word not in self.f[index]:
                continue
            d = len(self.docs[index])
            score += (self.idf[word]*self.f[index][word]*(self.k1+1)
                      / (self.f[index][word]+self.k1*(1-self.b+self.b*d
                                                      / self.avgdl)))
        return score

    def simall(self, doc):
        scores = []
        for index in range(self.D):
            score = self.sim(doc, index)
            scores.append(score)
        return scores

if __name__ == '__main__':
    # sents = utils.get_sentences(text)
    sents = [each.strip() for each in text.split('')]
    doc = []
    for sent in sents:
        words = list(jieba.cut(sent))
        words = [each for each in words if each not in STOPWORDS]
        doc.append(words)
    print(doc)
    s = BM25(doc)
    print('freq of sentences:{}'.format(s.f))
    print('idf of sentences:{}'.format(s.idf))
    # print(s.simall(['自然語言', '電腦科學', '領域', '人工智慧', '領域']))
    print(s.simall(['自然語言', '電腦科學', '科學']))