python根據(jù)BM25實現(xiàn)文本檢索

目的

給定一個或多個搜索詞,如“高血壓 患者”,從已有的若干篇文本中找出最相關(guān)的(n篇)文本。

理論知識

文本檢索(text retrieve)的常用策略是:用一個ranking function根據(jù)搜索詞對所有文本進行排序,選取前n個,就像百度搜索一樣。
顯然,ranking function是決定檢索效果最重要的因素,本文選用了在實際應(yīng)用中效果很好的BM25。BM25其實只用到了一些基礎(chǔ)的統(tǒng)計和文本處理的方法,沒有很高深的算法。

BM25

上圖是BM25的公式,對于一個搜索q和所有文本,計算每一篇文本d的權(quán)重。整個公式其實是TF-IDF的改進:

  • 第一項c(w,q)就是搜索q中詞w的詞頻
  • 第三項是詞w的逆文檔頻率,M是所有文本的個數(shù),df(w)是出現(xiàn)詞w的文本個數(shù)
  • 中間的第二項是關(guān)鍵,實質(zhì)是詞w的TF值的變換,c(w,d)是詞w在文本d中的詞頻。首先是一個TF Transformation,目的是防止某個詞的詞頻過大,經(jīng)過下圖中公式的約束,詞頻的上限為k+1,不會無限制的增長。例如,一個詞在文本中的詞頻無論是50還是100,都說明文本與這個詞有關(guān),但相關(guān)度不可能是兩倍關(guān)系。
    TF Transformation
  • 上圖的公式分母中的k還乘了一個系數(shù),目的是歸一化文本長度。歸一化公式中,b是[0,1]之間的常數(shù),avdl是平均文本長度,d是文本d的長度。顯然,d比平均值大,則normalizer大于1,代入BM25最終的權(quán)重變小,反之亦然。
length normalization

python實現(xiàn)

下面通過一個例子來實現(xiàn)根據(jù)BM25來進行文本檢索?,F(xiàn)在從網(wǎng)上爬下來了幾十篇健康相關(guān)的文章,部分如下圖所示。模擬輸入搜索詞,如“高血壓 患者 藥物”,搜素最相關(guān)的文章。

文本列表

python的實現(xiàn)用到了gensim庫,其中的BM25實現(xiàn)的源碼如下:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Licensed under the GNU LGPL v2.1 - http://www.gnu.org/licenses/lgpl.html

import math
from six import iteritems
from six.moves import xrange


# BM25 parameters.
PARAM_K1 = 1.5
PARAM_B = 0.75
EPSILON = 0.25


class BM25(object):

    def __init__(self, corpus):
        self.corpus_size = len(corpus)
        self.avgdl = sum(map(lambda x: float(len(x)), corpus)) / self.corpus_size
        self.corpus = corpus
        self.f = []
        self.df = {}
        self.idf = {}
        self.initialize()

    def initialize(self):
        for document in self.corpus:
            frequencies = {}
            for word in document:
                if word not in frequencies:
                    frequencies[word] = 0
                frequencies[word] += 1
            self.f.append(frequencies)

            for word, freq in iteritems(frequencies):
                if word not in self.df:
                    self.df[word] = 0
                self.df[word] += 1

        for word, freq in iteritems(self.df):
            self.idf[word] = math.log(self.corpus_size - freq + 0.5) - math.log(freq + 0.5)

    def get_score(self, document, index, average_idf):
        score = 0
        for word in document:
            if word not in self.f[index]:
                continue
            idf = self.idf[word] if self.idf[word] >= 0 else EPSILON * average_idf
            score += (idf * self.f[index][word] * (PARAM_K1 + 1)
                      / (self.f[index][word] + PARAM_K1 * (1 - PARAM_B + PARAM_B * self.corpus_size / self.avgdl)))
        return score

    def get_scores(self, document, average_idf):
        scores = []
        for index in xrange(self.corpus_size):
            score = self.get_score(document, index, average_idf)
            scores.append(score)
        return scores


def get_bm25_weights(corpus):
    bm25 = BM25(corpus)
    average_idf = sum(map(lambda k: float(bm25.idf[k]), bm25.idf.keys())) / len(bm25.idf.keys())

    weights = []
    for doc in corpus:
        scores = bm25.get_scores(doc, average_idf)
        weights.append(scores)

    return weights

gensim中代碼寫得很清楚,我們可以直接利用。

import jieba.posseg as pseg
import codecs
from gensim import corpora
from gensim.summarization import bm25
import os
import re

構(gòu)建停用詞表

stop_words = '/Users/yiiyuanliu/Desktop/nlp/demo/stop_words.txt'
stopwords = codecs.open(stop_words,'r',encoding='utf8').readlines()
stopwords = [ w.strip() for w in stopwords ]

結(jié)巴分詞后的停用詞性 [標(biāo)點符號、連詞、助詞、副詞、介詞、時語素、‘的’、數(shù)詞、方位詞、代詞]

stop_flag = ['x', 'c', 'u','d', 'p', 't', 'uj', 'm', 'f', 'r']

對一篇文章分詞、去停用詞

def tokenization(filename):
    result = []
    with open(filename, 'r') as f:
        text = f.read()
        words = pseg.cut(text)
    for word, flag in words:
        if flag not in stop_flag and word not in stopwords:
            result.append(word)
    return result

對目錄下的所有文本進行預(yù)處理,構(gòu)建字典

corpus = [];
dirname = '/Users/yiiyuanliu/Desktop/nlp/demo/articles'
filenames = []
for root,dirs,files in os.walk(dirname):
    for f in files:
        if re.match(ur'[\u4e00-\u9fa5]*.txt', f.decode('utf-8')):
            corpus.append(tokenization(f))
            filenames.append(f)
    
dictionary = corpora.Dictionary(corpus)
print len(dictionary)
Building prefix dict from the default dictionary ...
Loading model from cache /var/folders/1q/5404x10d3k76q2wqys68pzkh0000gn/T/jieba.cache
Loading model cost 0.328 seconds.
Prefix dict has been built succesfully.


2552

建立詞袋模型

打印了第一篇文本按詞頻排序的前5個詞

doc_vectors = [dictionary.doc2bow(text) for text in corpus]
vec1 = doc_vectors[0]
vec1_sorted = sorted(vec1, key=lambda (x,y) : y, reverse=True)
print len(vec1_sorted)
for term, freq in vec1_sorted[:5]:
    print dictionary[term]
76
高血壓
患者
藥物
血壓
治療

用gensim建立BM25模型

bm25Model = bm25.BM25(corpus)

根據(jù)gensim源碼,計算平均逆文檔頻率

average_idf = sum(map(lambda k: float(bm25Model.idf[k]), bm25Model.idf.keys())) / len(bm25Model.idf.keys())

假設(shè)用戶輸入了搜索詞“高血壓 患者 藥物”,利用BM25模型計算所有文本與搜索詞的相關(guān)性

query_str = '高血壓 患者 藥物'
query = []
for word in query_str.strip().split():
    query.append(word.decode('utf-8'))
scores = bm25Model.get_scores(query,average_idf)
# scores.sort(reverse=True)
print scores
[4.722034069722618, 4.5579610648148625, 2.859958016641194, 3.388613898734133, 4.6281563584251995, 4.730042214103296, 1.447106736835707, 2.595169814422283, 2.894213473671414, 2.952010252059601, 3.987044912721877, 2.426869660460219, 1.1583806884161147, 0, 3.242214688067997, 3.6729065940310752, 3.025338037306947, 1.57823130047124, 2.6054874252518214, 3.4606547596124635, 1.1583806884161147, 2.412854586446401, 1.7354863870557247, 1.447106736835707, 3.571235274862516, 2.6054874252518214, 2.695780408029825, 2.3167613768322295, 4.0309963518837595, 0, 2.894213473671414, 3.306255023356817, 3.587349029341776, 3.4401107112269824, 3.983307351035947, 0, 4.508767501123564, 3.6289862140766926, 3.6253442838304633, 4.248297326100691, 3.025338037306947, 3.602635199166345, 3.4960329155028464, 3.3547048399034876, 1.57823130047124, 4.148340973502125, 1.1583806884161147]
idx = scores.index(max(scores))
print idx
5

找到最相關(guān)的文本

fname = filenames[idx]
print fname
關(guān)于降壓藥的五個問題.txt
with open(fname,'r') as f:
    print f.read()
      高血壓的重要治療方式之一,就是口服降壓藥。對于不少剛剛被確診的“高血壓新手”來說,下面這些關(guān)于用藥的事情,是必須知道的。

1. 貴的藥,真的比便宜的藥好用嗎?

      事實上,降壓藥物的化學(xué)機構(gòu)和作用機制不一樣。每一種降壓藥,其降壓機理和適應(yīng)人群都不一樣。只要適合自己的病情和身體狀況,就是好藥。因此,不存在“貴重降壓藥一定比便宜的降壓藥好使”這一說法。

2. 能不吃藥就不吃藥,靠身體調(diào)節(jié)血壓,這種想法對嗎?

     這種想法很幼稚。其實,高血壓是典型的,應(yīng)該盡早服藥的疾病。如果服藥太遲,高血壓對重要臟器持續(xù)形成傷害,會讓我們的身體受很大打擊。所以,高血壓患者盡早服藥,是對身體的最好的保護。

3. 降壓藥是不是得吃一輩子?

      對于這個問題,中醫(yī)和西醫(yī)有著不同的認(rèn)識。西醫(yī)認(rèn)為,降壓藥服用之后不應(yīng)該停用,以免血壓形成波動,造成對身體的進一步傷害。中醫(yī)則認(rèn)為,通過適當(dāng)?shù)倪\動和飲食調(diào)節(jié),早期的部分高血壓患者,可以在服藥之后的某段時間里停藥。總之,處理這一問題的時候,我們還是要根據(jù)自己的情況而定。對于高血壓前期,或者輕度高血壓的人來說,在生活方式調(diào)節(jié)能夠讓血壓穩(wěn)定的情況下,可以考慮停藥,采取非藥物療法。對于中度或者重度的高血壓患者來說,就不能這么做了。

4. 降壓藥是不是要早晨服用?

      一般來說,長效的降壓藥物,都是在早晨服用的。但是,我們也可以根據(jù)高血壓患者的波動情況,適當(dāng)改變服藥時間。

5. 降壓藥是不是一旦服用就不能輕易更換?

      高血壓病人一旦服用了某種降壓藥物,就不要輕易更換。只要能維持血壓在正常范圍而且穩(wěn)定,就不用換藥。只有在原有藥物不能起到控制作用的時候,再考慮更換服藥方案。對于新發(fā)高血壓病人來說,長效降壓藥若要達(dá)到穩(wěn)定的降壓效果,往往需要4到6周或者更長時間。如果經(jīng)過4到6周也實現(xiàn)不了好的控制效果,可以考慮加第二種藥物來聯(lián)合降壓,不必盲目換藥。

參考資料:

Coursera: Text Mining and Analytics

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容