不會真有人覺得聊天機器人難吧(二)

引言
本文介紹傳統(tǒng)的檢索排名技術實現(xiàn)。教你如何實現(xiàn)輸入一個問句,快速查詢到最相關的文檔。
單詞權重
假設你有很多文檔(稱為語料庫,corpus),你想實現(xiàn)輸入一個問句,快速查詢到最相關的文檔。
首先是要讓計算器理解文檔和問句,這里需要的是進行分詞,得到問句或每個文檔的單詞列表,然后去掉重復的單詞就可以得到單詞詞典。
根據(jù)單詞詞典利用下面介紹的TF-IDF算法可以得到每個問句或文檔的向量表示,接著可以利用余弦相似度計算問句和所有文檔的相似度,根據(jù)相似度排名可以找到最相似的文檔。 該方法可以參考TF-IDF文本表示。但是本文介紹的是另一種方法,通過計算問句和每篇文檔的相關度得分,根據(jù)得分就可以得到相似文檔排名。
不管是哪種方法都需要計算單詞的權重,所謂權重可以認為是某個單詞對該篇文章主題的貢獻度。比如談論體育的文章里面,多次出現(xiàn)了“籃球”和“的”?!暗摹背霈F(xiàn)的次數(shù)很有可能是超過“籃球”出現(xiàn)的次數(shù),我們不能說“的”比“籃球”對于該主題來說更重要。因此除了頻次還有其他指標需要考慮。
常用方法的是和它的變體BM25。
TF-IDF
詞頻(term frequence) 反映了單詞的頻率,單詞在某篇文章中出現(xiàn)的越多,越可能由它反映文章的內容。通常會對詞頻取對數(shù),因為某個單詞出現(xiàn)了100次,并不見得它對文章的貢獻有100倍那么重要。因為是無意義的,因此我們加了一個:
詞頻有很多種計算方法,還有一種方法是用單詞在文章中出現(xiàn)的次數(shù),除以文章中單詞的總數(shù),這種方法其實更符合詞頻的字面意思。但是上面的方法也不錯。
文檔頻率(document frequence) 反映了出現(xiàn)單詞的文檔的數(shù)量?;诔@?,我們知道只在一小部分文檔中出現(xiàn)的單詞通常更助于這些文檔的區(qū)分度。而在整個集合中的文檔都出現(xiàn)的單詞通常是沒什么幫助的,比如“的”、“呢”這些詞。
逆文檔頻率(inverse document frequency) 單詞權重被定義為:
其中是集合中文檔的總數(shù);是單詞出現(xiàn)的文檔數(shù)量;
從公式可以看出,某個單詞出現(xiàn)的文檔數(shù)量越小,它的權重就越大;如果它在所有文檔中都出現(xiàn),那么,表示權重最低。
就是用詞頻乘以逆文檔頻率,表示單詞在文檔中的權重:
下面我們重點介紹它的變體——BM25。
BM25
一個的變體是BM25,它增加了兩個參數(shù):和。其中用來平衡和;控制文檔長度的重要性。給定計算文檔的BM25得分公式如下:

其中是文檔長度;是文檔集合中平均文檔長度。
假設,那么加權的就是,其中是用來控制的增速的,假設某篇文章單詞出現(xiàn)200次,它能帶來的相關性是出現(xiàn)100次的兩倍嗎?
假設出現(xiàn)了100次,那么該篇文章肯定是與有關的,如果再出現(xiàn)更多的次數(shù)帶來權重也不應該增加這種相關性了。使用就能避免它的無限增大,相當于達到某個閾值就飽和了。

上圖是不同的值得到的曲線,越大,曲線越緩和。當時,BM25就沒有計算,只是使用了。

引入的目的是考慮文檔長度,如果某篇文章很短,出現(xiàn)了某個單詞1次,和某篇文章特別長,但是也只出現(xiàn)了2次。我們就想讓文章的長度來控制的取值,如果文章很長,那么就取大一點,得到的就小一點;反之文章很短,那么就設小一點。如何才能知道文章是短是長呢?這里的做法就是用它的長度和平均長度作比較,如果大于平均,那么就是相對較長。
而引入就是為了控制文章長度的重要性,的取值范圍是。若,那么完全不考慮文章長度;若,如果文章較長,那么就增大;如果文章較短,那么就減少。下圖是和相關的圖形:

從中可以看出,越大,對文章長度的懲罰也越大。
常用的取值。
關于也有一種變體叫作概率IDF(probabilistic IDF),它的公式為
這樣得到的對出現(xiàn)在較多文檔中的單詞來說是急劇下降的,但是它可能得到負值,Lucene的實現(xiàn)是在最后加,防止出現(xiàn)負值:

假設,代表的取值,上圖是原始的與它兩種變體的圖形??梢钥吹?,Lucene的實現(xiàn)的圖形和傳統(tǒng)的圖形類似。
代碼實現(xiàn)
def get_score(self, word, doc_id, doc_freq):
'''
計算bm25
:param word:
:param doc_id: 文檔在語料庫中的id
:param doc_freq: 出現(xiàn)單詞word的文檔數(shù)量
:return:
'''
tf = self.idx[word][doc_id]
if not tf:
return 0
tf = self._compute_weighted_tf(tf, self.dl[doc_id], self.dl.get_avg_len())
idf = self._compute_probabilistic_idf(doc_freq)
return tf * idf
def _compute_weighted_tf(self, tf, doc_len, avg_doc_len):
return tf * (self.k + 1) / (self.k * (1 - self.b + self.b * doc_len / avg_doc_len) + tf)
def _compute_probabilistic_idf(self, df):
return math.log((len(self.dl) + 1) / (df + 0.5))
def get_scores(self, query):
query = self.tokenize(query) if self.tokenize else query
scores = np.zeros(len(self.dl))
for word in query:
# 利用倒排索引,提升查詢效率,我們可以不需要計算不出現(xiàn)查詢單詞的文檔
if word in self.idx:
for doc_id in self.idx[word].keys():
scores[doc_id] += self.get_score(word, doc_id, len(self.idx[word]))
return scores
倒排索引
為了計算相似度,我們需要高效的找到包含查詢中單詞的文檔集合。
通常解決這個問題的方法是使用倒排索引(inverted index)。
在倒排索引中,給定一個查詢詞,可以很快找到包含該查詢詞的文檔列表。
倒排索引包含兩部分,一個字典和ID列表。字典中包含了所有的單詞,每個單詞指向一個出現(xiàn)該單詞的文檔ID列表。其中還可以詞頻或甚至是單詞在文檔中出現(xiàn)的位置信息。

比如上面就是一個簡單的倒排索引,基于下面這4個簡單的文檔集合實現(xiàn)。其中包含了單詞的總數(shù),以及每個文檔中該單詞出現(xiàn)的次數(shù)。這樣可以很方便地計算。

代碼實現(xiàn)
import collections
class Dictionary(dict):
'''
倒排索引用到的數(shù)據(jù)結構
word -> postings(doc_id->word_count)
'''
def __missing__(self, key):
# Python中如果字典找不到key這個鍵,那么會調用該方法,我們在該方法中返回一個defaultdict
# 可以避免很多if else
postings = collections.defaultdict(int)
self[key] = postings
return postings
class InvertedIndex:
def __init__(self):
self.dictionary = Dictionary()
def add(self, doc_id, doc):
for word in doc:
postings = self.dictionary[word]
postings[doc_id] += 1
def __contains__(self, word):
return word in self.dictionary
def __getitem__(self, word):
return self.dictionary[word]
def __len__(self):
'''
:return: 語料庫中的單詞數(shù)量
'''
return len(self.dictionary)
def get_doc_frequency(self, word):
'''
得到單詞出現(xiàn)的文檔數(shù)量
:param word:
:return:
'''
return len(self.dictionary[word])
評估IR系統(tǒng)
我們評估排序的IR系統(tǒng)表現(xiàn)通過精確率(precision)和召回率(recall)指標。
精確率表示返回的文檔中屬于相關文檔的比例;
召回率表示返回相關的文檔占總相關文檔數(shù)的比例;
是返回的文檔數(shù);
是返回的文檔中相關文檔的數(shù)量;
是返回的文檔中不相關文檔的數(shù)量;
是所有文檔中相關文檔的數(shù)據(jù);
假設有50篇相關文檔,你的查詢算法返回了10篇,其中9篇是相關的,1篇是不相關的。
那么準確率就是,召回率是
測試
數(shù)據(jù)使用的是網(wǎng)上找到的醫(yī)療問答數(shù)據(jù),它是長這個樣子的:

比如,我們查詢“醫(yī)生,我肛門處非常癢怎么辦”相關的相似問題。
def main():
data_path = './data/questions.csv'
df = pd.read_csv(data_path, usecols=['content'])
corpus = df['content'].tolist()
# 傳入語料庫,和分詞方法
bm25 = OkapiBM25(corpus, tokenize=jieba.lcut)
query = "醫(yī)生,我肛門處非常癢怎么辦"
result = bm25.get_top_k(query, corpus=corpus, k=5)
for q in result:
print(q)
打印的結果如下:
肛門處有濕疹,非常癢,怎么辦
痔瘡,肛門處很癢,大便有點疼痔瘡,肛門處很癢,大便有點疼
肛門非常癢怎么辦肛門很癢之前只是有意無意的感覺,可是現(xiàn)在特別癢有的地方用手碰還刺痛,嚴重癢時像被蚊子哎,特別是晚上,我該怎么辦?。?br>我有輕微的內痔,肛門處總是很癢怎么辦?。?br>我肛門癢,該怎么辦
這是返回最相似的5個問句,是不是像那么回事。
完整代碼
完整代碼:https://github.com/nlp-greyfoss/nlp-algorithms/tree/main/bm25
最后一句:BUG,走你!


沒有人比我更懂Redis(一)
沒有人比我更懂Redis(二)
不會真有人角色聊天機器人難吧(一)
自然語言處理入門之分詞
入門人工智能必備的線性代數(shù)基礎
1.看到這里了就點個在看支持下吧,你的在看是我創(chuàng)作的動力。
2.關注公眾號,每天為您分享原創(chuàng)或精選文章!
3.特殊階段,帶好口罩,做好個人防護。
