標籤:

python根據BM25實現文本檢索

作者:劉藝原,現浙江大學 生物醫學工程研究生。

原文鏈接:jianshu.com/p/ff28004ef

正文共5788個字,5張圖,預計閱讀時間:15分鐘。

目的

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

理論知識

文本檢索(text retrieve)的常用策略是:用一個ranking function根據搜索詞對所有文本進行排序,選取前n個,就像百度搜索一樣。

顯然,ranking function是決定檢索效果最重要的因素,本文選用了在實際應用中效果很好的BM25。BM25其實只用到了一些基礎的統計和文本處理的方法,沒有很高深的演算法。

BM25

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

  • 第一項c(w,q)就是搜索q中詞w的詞頻
  • 第三項是詞w的逆文檔頻率,M是所有文本的個數,df(w)是出現詞w的文本個數
  • 中間的第二項是關鍵,實質是詞w的TF值的變換,c(w,d)是詞w在文本d中的詞頻。首先是一個TF Transformation,目的是防止某個詞的詞頻過大,經過下圖中公式的約束,詞頻的上限為k+1,不會無限制的增長。例如,一個詞在文本中的詞頻無論是50還是100,都說明文本與這個詞有關,但相關度不可能是兩倍關係。

TF Transformation

  • 上圖的公式分母中的k還乘了一個係數,目的是歸一化文本長度。歸一化公式中,b是[0,1]之間的常數,avdl是平均文本長度,d是文本d的長度。顯然,d比平均值大,則normalizer大於1,代入BM25最終的權重變小,反之亦然。

length normalization

Python實現

下面通過一個例子來實現根據BM25來進行文本檢索。現在從網上爬下來了幾十篇健康相關的文章,部分如下圖所示。模擬輸入搜索詞,如「高血壓 患者 藥物」,搜素最相關的文章。

文本列表

python的實現用到了gensim庫,其中的BM25實現的源碼如下:

#!/usr/bin/env pythonn# -*- coding: utf-8 -*-n#n# Licensed under the GNU LGPL v2.1 - http://www.gnu.org/licenses/lgpl.htmln import mathnfrom six import iteritemsnfrom six.moves import xrangen # BM25 parameters. nPARAM_K1 = 1.5nPARAM_B = 0.75nEPSILON = 0.25n class BM25(object): ndef __init__(self, corpus): nself.corpus_size = len(corpus) nself.avgdl = sum(map(lambda x: float(len(x)), corpus)) / self.corpus_size self.corpus = corpus nself.f = [] nself.df = {} nself.idf = {} nself.initialize() ndef initialize(self): n for document in self.corpus: nfrequencies = {} nfor word in document: nif word not in frequencies: nfrequencies[word] = 0 nfrequencies[word] += 1 nself.f.append(frequencies) n for word, freq in iteritems(frequencies): nif word not in self.df: nself.df[word] = 0 nself.df[word] += 1 nfor word, freq in iteritems(self.df): nself.idf[word] = math.log(self.corpus_size - freq + 0.5) - math.log(freq + 0.5) ndef get_score(self, document, index, average_idf): nscore = 0 nfor word in document: nif word not in self.f[index]: ncontinue nidf = self.idf[word] if self.idf[word] >= 0 else EPSILON * average_idf nscore += (idf * self.f[index][word] * (PARAM_K1 + 1) n/ (self.f[index][word] + PARAM_K1 * (1 - PARAM_B + PARAM_B * self.corpus_size / self.avgdl))) nreturn score ndef get_scores(self, document, average_idf): nscores = [] nfor index in xrange(self.corpus_size): nscore = self.get_score(document, index, average_idf) nscores.append(score) n return scoresn def get_bm25_weights(corpus): nbm25 = BM25(corpus) naverage_idf = sum(map(lambda k: float(bm25.idf[k]), bm25.idf.keys())) / len(bm25.idf.keys()) nweights = [] nfor doc in corpus: nscores = bm25.get_scores(doc, average_idf)n weights.append(scores) nreturn weightsn

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

import jieba.posseg as psegimport codecsfrom gensim import corporafrom gensim.summarization import bm25import osimport re

構建停用詞表

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

結巴分詞後的停用詞性 [標點符號、連詞、助詞、副詞、介詞、時語素、『的』、數詞、方位詞、代詞]

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

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

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

對目錄下的所有文本進行預處理,構建字典

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

建立詞袋模型

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

doc_vectors = [dictionary.doc2bow(text) for text in corpus]nvec1 = doc_vectors[0]nvec1_sorted = sorted(vec1, key=lambda (x,y) : y, reverse=True)print len(vec1_sorted)for term, freq in vec1_sorted[:5]: print dictionary[term]n

76n高血壓n患者n藥物n血壓n治療n

用gensim建立BM25模型

bm25Model = bm25.BM25(corpus)

根據gensim源碼,計算平均逆文檔頻率

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

假設用戶輸入了搜索詞「高血壓 患者 藥物」,利用BM25模型計算所有文本與搜索詞的相關性

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]n

idx = scores.index(max(scores))print idxn

5n

找到最相關的文本

fname = filenames[idx]nprint fnamen關於降壓藥的五個問題.txtnwith open(fname,r) as f: nprint f.read() n

高血壓的重要治療方式之一,就是口服降壓藥。對於不少剛剛被確診的「高血壓新手」來說,下面這些關於用藥的事情,是必須知道的。

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

事實上,降壓藥物的化學機構和作用機制不一樣。每一種降壓藥,其降壓機理和適應人群都不一樣。只要適合自己的病情和身體狀況,就是好葯。因此,不存在「貴重降壓藥一定比便宜的降壓藥好使」這一說法。

2. 能不吃藥就不吃藥,靠身體調節血壓,這種想法對嗎?

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

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

對於這個問題,中醫和西醫有著不同的認識。西醫認為,降壓藥服用之後不應該停用,以免血壓形成波動,造成對身體的進一步傷害。中醫則認為,通過適當的運動和飲食調節,早期的部分高血壓患者,可以在服藥之後的某段時間裡停葯。總之,處理這一問題的時候,我們還是要根據自己的情況而定。對於高血壓前期,或者輕度高血壓的人來說,在生活方式調節能夠讓血壓穩定的情況下,可以考慮停葯,採取非藥物療法。對於中度或者重度的高血壓患者來說,就不能這麼做了。

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

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

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

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

參考資料

Coursera: Text Mining and Analytics(coursera.org/learn/text

推薦閱讀:

深入描述符
pandas複習總結(二)
Python數據分析之鎖具裝箱問題

TAG:Python |