6.文檔相似度分析


6.文檔相似度分析

將嘗試分析文檔之間的相似度指出。到目前為止,相比已經知道了文檔的定義是可以由句子或文本段落組成的文本體。為了分析文檔相似度,將使用 utils 模塊的 build_feature_matrix() 函數從文檔中提取特征。將使用文檔的 TF-IDF 相似度對文檔進行向量化,在之前的分類文本文檔和歸納整個文檔時曾使用過該方法。有了各種文檔的向量表示之后,將使用幾個距離或相似度度量來計算文檔之間的相似度。如下度量:

  • 余弦相似度。
  • 海靈格-巴塔恰亞(Helinger-Bhattacharya)距離。
  • Okapi BM25 排名。

像以往一樣,將介紹每個度量背后的概念,其屬性表達形式和定義,然后使用 Python 實現它們。還將使用一個包含九個文檔的小語料庫和一個包含三個文檔(也是查詢文檔)的獨立語料庫進行測試。對於這三個文檔中的每一個,都將嘗試從包含九個文檔的語料庫中找出最相似的文檔,小語料庫將作為查詢索引。可以把這一過程想象成對搜索過程的一個微型模型——當你使用句子進行搜索時,最相關的結果將從搜索引擎的網頁索引中返回給你。在用例中,待索引的內容是三個文檔,將根據相似度度量返回九個文檔中相關文檔的索引。

接下來,從加載必要的依賴關系和用來測試各種度量指標的文檔語料庫開始着手,如下面的代碼段所示:

normalization.py  折疊源碼
# -*- coding: utf-8 -*-
"""
Created on Fri Aug 26 20:45:10 2016
@author: DIP
"""
 
from  contractions  import  CONTRACTION_MAP
import  re
import  nltk
import  string
from  nltk.stem  import  WordNetLemmatizer
from  html.parser  import  HTMLParser
import  unicodedata
 
stopword_list  =  nltk.corpus.stopwords.words( 'english' )
stopword_list  =  stopword_list  +  [ 'mr' 'mrs' 'come' 'go' 'get' ,
                                  'tell' 'listen' 'one' 'two' 'three' ,
                                  'four' 'five' 'six' 'seven' 'eight' ,
                                  'nine' 'zero' 'join' 'find' 'make' ,
                                  'say' 'ask' 'tell' 'see' 'try' 'back' ,
                                  'also' ]
wnl  =  WordNetLemmatizer()
html_parser  =  HTMLParser()
 
def  tokenize_text(text):
     tokens  =  nltk.word_tokenize(text)
     tokens  =  [token.strip()  for  token  in  tokens]
     return  tokens
 
def  expand_contractions(text, contraction_mapping):
     
     contractions_pattern  =  re. compile ( '({})' . format ( '|' .join(contraction_mapping.keys())),
                                       flags = re.IGNORECASE|re.DOTALL)
     def  expand_match(contraction):
         match  =  contraction.group( 0 )
         first_char  =  match[ 0 ]
         expanded_contraction  =  contraction_mapping.get(match)\
                                 if  contraction_mapping.get(match)\
                                 else  contraction_mapping.get(match.lower())                      
         expanded_contraction  =  first_char + expanded_contraction[ 1 :]
         return  expanded_contraction
         
     expanded_text  =  contractions_pattern.sub(expand_match, text)
     expanded_text  =  re.sub( "'" , "", expanded_text)
     return  expanded_text
     
     
from  pattern.en  import  tag
from  nltk.corpus  import  wordnet as wn
 
# Annotate text tokens with POS tags
def  pos_tag_text(text):
     
     def  penn_to_wn_tags(pos_tag):
         if  pos_tag.startswith( 'J' ):
             return  wn.ADJ
         elif  pos_tag.startswith( 'V' ):
             return  wn.VERB
         elif  pos_tag.startswith( 'N' ):
             return  wn.NOUN
         elif  pos_tag.startswith( 'R' ):
             return  wn.ADV
         else :
             return  None
     
     tagged_text  =  tag(text)
     tagged_lower_text  =  [(word.lower(), penn_to_wn_tags(pos_tag))
                          for  word, pos_tag  in
                          tagged_text]
     return  tagged_lower_text
     
# lemmatize text based on POS tags   
def  lemmatize_text(text):
     
     pos_tagged_text  =  pos_tag_text(text)
     lemmatized_tokens  =  [wnl.lemmatize(word, pos_tag)  if  pos_tag
                          else  word                    
                          for  word, pos_tag  in  pos_tagged_text]
     lemmatized_text  =  ' ' .join(lemmatized_tokens)
     return  lemmatized_text
     
 
def  remove_special_characters(text):
     tokens  =  tokenize_text(text)
     pattern  =  re. compile ( '[{}]' . format (re.escape(string.punctuation)))
     filtered_tokens  =  filter ( None , [pattern.sub( ' ' , token)  for  token  in  tokens])
     filtered_text  =  ' ' .join(filtered_tokens)
     return  filtered_text
     
     
def  remove_stopwords(text):
     tokens  =  tokenize_text(text)
     filtered_tokens  =  [token  for  token  in  tokens  if  token  not  in  stopword_list]
     filtered_text  =  ' ' .join(filtered_tokens)   
     return  filtered_text
 
def  keep_text_characters(text):
     filtered_tokens  =  []
     tokens  =  tokenize_text(text)
     for  token  in  tokens:
         if  re.search( '[a-zA-Z]' , token):
             filtered_tokens.append(token)
     filtered_text  =  ' ' .join(filtered_tokens)
     return  filtered_text
 
def  unescape_html(parser, text):
     
     return  parser.unescape(text)
 
def  normalize_corpus(corpus, lemmatize = True ,
                      only_text_chars = False ,
                      tokenize = False ):
     
     normalized_corpus  =  []   
     for  text  in  corpus:
         text  =  html_parser.unescape(text)
         text  =  expand_contractions(text, CONTRACTION_MAP)
         if  lemmatize:
             text  =  lemmatize_text(text)
         else :
             text  =  text.lower()
         text  =  remove_special_characters(text)
         text  =  remove_stopwords(text)
         if  only_text_chars:
             text  =  keep_text_characters(text)
         
         if  tokenize:
             text  =  tokenize_text(text)
             normalized_corpus.append(text)
         else :
             normalized_corpus.append(text)
             
     return  normalized_corpus
 
 
def  parse_document(document):
     document  =  re.sub( '\n' ' ' , document)
     if  isinstance (document,  str ):
         document  =  document
     elif  isinstance (document,  unicode ):
         return  unicodedata.normalize( 'NFKD' , document).encode( 'ascii' 'ignore' )
     else :
         raise  ValueError( 'Document is not string or unicode!' )
     document  =  document.strip()
     sentences  =  nltk.sent_tokenize(document)
     sentences  =  [sentence.strip()  for  sentence  in  sentences]
     return  sentences
utils.py  折疊源碼
# -*- coding: utf-8 -*-
"""
Created on Sun Sep 11 23:06:06 2016
@author: DIP
"""
 
from  sklearn.feature_extraction.text  import  CountVectorizer, TfidfVectorizer
 
def  build_feature_matrix(documents, feature_type = 'frequency' ,
                          ngram_range = ( 1 1 ), min_df = 0.0 , max_df = 1.0 ):
 
     feature_type  =  feature_type.lower().strip() 
     
     if  feature_type  = =  'binary' :
         vectorizer  =  CountVectorizer(binary = True , min_df = min_df,
                                      max_df = max_df, ngram_range = ngram_range)
     elif  feature_type  = =  'frequency' :
         vectorizer  =  CountVectorizer(binary = False , min_df = min_df,
                                      max_df = max_df, ngram_range = ngram_range)
     elif  feature_type  = =  'tfidf' :
         vectorizer  =  TfidfVectorizer(min_df = min_df, max_df = max_df,
                                      ngram_range = ngram_range)
     else :
         raise  Exception( "Wrong feature type entered. Possible values: 'binary', 'frequency', 'tfidf'" )
 
     feature_matrix  =  vectorizer.fit_transform(documents).astype( float )
     
     return  vectorizer, feature_matrix
from  normalization  import  normalize_corpus
from  utils  import  build_feature_matrix
import  numpy as np
 
 
toy_corpus  =  [ 'The sky is blue' ,
'The sky is blue and beautiful' ,
'Look at the bright blue sky!' ,
'Python is a great Programming language' ,
'Python and Java are popular Programming languages' ,
'Among Programming languages, both Python and Java are the most used in Analytics' ,
'The fox is quicker than the lazy dog' ,
'The dog is smarter than the fox' ,
'The dog, fox and cat are good friends' ]
 
query_docs  =  [ 'The fox is definitely smarter than the dog' ,
             'Java is a static typed programming language unlike Python' ,
             'I love to relax under the beautiful blue sky!' ]

從該代碼段可以看出,在語料庫索引中有各種各樣的文檔,設計天空、程序語言和動物。此外,還有三個查詢文檔,希望根據相似度計算從 toy_corpus 索引中獲取與其最相關的文檔。在開始介紹度量之前,首先要規范化文檔並通過提取 TF-IDF 特征將其向量化,如下代碼段所示:

# normalize and extract features from the toy corpus
norm_corpus  =  normalize_corpus(toy_corpus, lemmatize = False )
tfidf_vectorizer, tfidf_features  =  build_feature_matrix(norm_corpus,
                                                         feature_type = 'tfidf' ,
                                                         ngram_range = ( 1 1 ),
                                                         min_df = 0.0 , max_df = 1.0 )
                                                         
# normalize and extract features from the query corpus
norm_query_docs  =   normalize_corpus(query_docs, lemmatize = True )
query_docs_tfidf  =  tfidf_vectorizer.transform(norm_query_docs)

現在,已經完成了文檔規范化並使用基於 TF-IDF 的向量表示方式實現了文檔向量化,接下來將研究如何計算每個向量的相似度值。

余弦相似度

繼續使用相同的概念來計算文檔的余弦相似度得分,采用基於詞袋模型的文檔向量,並用 TF-IDF 數值替換詞頻。在這里,同樣只采用一元分詞形式,但是也可以在向量化過程中嘗試采用二元分詞等方式,並將其作為文檔特征。對於三個查詢文檔中的每一個,都將使用 toy_corpus 中的糾葛文檔計算其相似度,並返回 n 個最相似的文檔,其中 n 為用戶輸入參數。

將定義一個函數,它的輸入是向量化的語料庫和需要計算相似度的文檔語料庫。使用點積運算獲得相似度得分,並以倒序的方式對文檔進行排序,以獲得相似度最高的 n 個文檔。下面的函數實現了上述功能:

def  compute_cosine_similarity(doc_features, corpus_features,
                               top_n = 3 ):
     # get document vectors
     doc_features  =  doc_features.toarray()[ 0 ]
     corpus_features  =  corpus_features.toarray()
     # compute similarities
     similarity  =  np.dot(doc_features,
                         corpus_features.T)
     # get docs with highest similarity scores
     top_docs  =  similarity.argsort()[:: - 1 ][:top_n]
     top_docs_with_score  =  [(index,  round (similarity[index],  3 ))
                             for  index  in  top_docs]
     return  top_docs_with_score

在該函數中,corpus_features 是位於 toy_corpus 索引中的向量化文檔。這些文件將根據與 doc_features 的相似度得分進行抓取,doc_features 代表了屬於每個 query_doc 的向量化文檔,如下代碼段所示:

print  ( 'Document Similarity Analysis using Cosine Similarity' )
print  ( '=' * 60 )
for  index, doc  in  enumerate (query_docs):
     
     doc_tfidf  =  query_docs_tfidf[index]
     top_similar_docs  =  compute_cosine_similarity(doc_tfidf,
                                              tfidf_features,
                                              top_n = 2 )
     print  ( 'Document' ,index + 1  , ':' , doc)
     print  ( 'Top' len (top_similar_docs),  'similar docs:' )
     print  ( '-' * 40 )
     for  doc_index, sim_score  in  top_similar_docs:
         print  ( 'Doc num: {} Similarity Score: {}\nDoc: {}' . format (doc_index + 1 ,
                                                                  sim_score,
                                                                  toy_corpus[doc_index]))
         print  ( '-' * 40 )
     print ("")

結果:

Document Similarity Analysis using Cosine Similarity
= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
Document  1  : The fox  is  definitely smarter than the dog
Top  2  similar docs:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Doc num:  8  Similarity Score:  1.0
Doc: The dog  is  smarter than the fox
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Doc num:  7  Similarity Score:  0.426
Doc: The fox  is  quicker than the lazy dog
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
Document  2  : Java  is  a static typed programming language unlike Python
Top  2  similar docs:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Doc num:  4  Similarity Score:  0.709
Doc: Python  is  a great Programming language
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Doc num:  5  Similarity Score:  0.573
Doc: Python  and  Java are popular Programming languages
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
Document  3  : I love to relax under the beautiful blue sky!
Top  2  similar docs:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Doc num:  2  Similarity Score:  1.0
Doc: The sky  is  blue  and  beautiful
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Doc num:  1  Similarity Score:  0.72
Doc: The sky  is  blue
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

由於余弦相似度得分,上面的輸出給出了與每個查詢文檔最相關的兩個文檔,可以看到輸出是符合預期的。關於動物的文檔與提及狐狸與狗的文檔相似;關於 Python 和 Java 的文檔與乞討這兩種程序語言的查詢文檔相似;美麗的藍天也確實類似於談論填空是藍色而美麗的文檔!

還要注意前面輸出中的余弦相似度得分,其中 1.0 表示完全相似,0.0 表示不相似,它們之間的分數表示不同的相似度水平(基於得分多少)。例如,最后一個例子中,主要文檔向量是['sky','blue','beautiful'], 因為它們都與語料庫中的第一個文檔匹配,所以獲得了 1.0 或 100% 的相似度得分,只有 ['sky', 'blue'] 與第二個最相似的文檔匹配,因為得到了 0.70 或 70% 的相似度得分。應該還記得前面的內容里,簡單的提及了余弦相似度使用基於詞袋的向量,僅僅考慮標識的權重,而不考慮詞項的順序。這在大型文檔中是非常可取的,因為相同的內容可能會以不同的方式描繪,所以捕獲詞語序列可能會導致信息丟失,從而導致不希望看到的誤匹配。

建議使用 cikit-learn 的 cosine_similarity() 函數,可以在 sklearn.metrics.pairwise 模塊中找到它。它使用類似的邏輯實現以上功能,但是相比之下性能更優,並在大型文檔上表現良好。還可以直接使用 gensim.matutils 模塊中提供的 gensim 相似度(similarities) 模塊或 cossim() 函數。

海靈格-巴塔恰亞距離

海靈格-巴塔恰亞(Hellinger-Bhattacharya)距離(HB距離)也稱為海靈格距離或巴塔恰亞距離。巴塔恰亞距離有巴塔恰亞(A. Bhattacharya)提取,用於測量兩個離散或連續概率分布之間的相似度。海靈格(E. Hellinger)在 1909 年提出了海靈格積分,用於計算海靈格距離。總的來說,海靈格-巴塔恰亞距離是一個 f 散度(f-divergence),f 散度在概率論中定義為函數 Dƒ(P||D),可用於測量 P 和 Q 概率分布之間的差異。有多種 f 散度的實例,包括 KL 散度和 HB 距離。請記住,KL 散度不是一個距離度量,因為它不符合將距離測量值作為度量所需的四個條件。

對於連續和離散的概率分布,均可以計算 HB 距離。在例子中,將會使用基於 TF-IDF 的向量作為文檔的概率分布。該分布為離散分布,因為對於特定的特征項有特定的 TF-IDF 值,即數值不連續。海靈格-巴塔恰亞距離的數學定義為:

其中 hdb(u,v) 表示文檔向量 u 和 v 之間的海靈格-巴塔恰亞距離,並且它等於向量的平方根差的歐幾里得或 L2 范數除以 2 的平方根。考慮到文檔向量 u 和 v 是具有 n 個特征的離散量,可以進一步擴展上式為:

其中 u = (u1,u2,...,un) 和 v = (v1,v2,...,vn) 的長度為 n 的文檔向量,n 表示有 n 個特征,它們是文檔中各類詞項的 TF-IDF 權重。與前面的余弦相似度計算類似,以相同的原理建立函數;會將文檔向量語料庫和單個文檔向量作為輸入,這些單個文檔向量正是我們希望基於 HB 距離從語料庫獲取 n 個最相似文檔的文檔向量。如下函數使用 Python 語言實現了上述概念:

def  compute_hellinger_bhattacharya_distance(doc_features, corpus_features,
                                             top_n = 3 ):
     # get document vectors                                           
     doc_features  =  doc_features.toarray()[ 0 ]
     corpus_features  =  corpus_features.toarray()
     # compute hb distances
     distance  =  np.hstack(
                     np.sqrt( 0.5  *
                             np. sum (
                                 np.square(np.sqrt(doc_features)  -
                                           np.sqrt(corpus_features)),
                                 axis = 1 )))
     # get docs with lowest distance scores                           
     top_docs  =  distance.argsort()[:top_n]
     top_docs_with_score  =  [(index,  round (distance[index],  3 ))
                             for  index  in  top_docs]
     return  top_docs_with_score

從上述實現過程中可以看出,按照得分對文檔進行升序排列,因為與余弦相似度不同(其中 1.0 表示完全相似),這里是分布之間的距離度量,得分為 0 表示完全相似,而較高的數值則表示存在一些不相似之處。現在可以將此函數應用於示例語料庫計算 HB 距離,可以在如下代碼段中看到結果:

print  ( 'Document Similarity Analysis using Hellinger-Bhattacharya distance' )
print  ( '=' * 60 )
for  index, doc  in  enumerate (query_docs):
     
     doc_tfidf  =  query_docs_tfidf[index]
     top_similar_docs  =  compute_hellinger_bhattacharya_distance(doc_tfidf,
                                              tfidf_features,
                                              top_n = 2 )
     print  ( 'Document' ,index + 1  , ':' , doc)
     print  ( 'Top' len (top_similar_docs),  'similar docs:' )
     print  ( '-' * 40 )
     for  doc_index, sim_score  in  top_similar_docs:
         print  ( 'Doc num: {} Distance Score: {}\nDoc: {}' . format (doc_index + 1 ,
                                                                  sim_score,
                                                                  toy_corpus[doc_index]))
         print  ( '-' * 40 )
     print  ("")

結果:

Document Similarity Analysis using Hellinger - Bhattacharya distance
= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
Document  1  : The fox  is  definitely smarter than the dog
Top  2  similar docs:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Doc num:  8  Distance Score:  0.0
Doc: The dog  is  smarter than the fox
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Doc num:  7  Distance Score:  0.96
Doc: The fox  is  quicker than the lazy dog
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
Document  2  : Java  is  a static typed programming language unlike Python
Top  2  similar docs:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Doc num:  4  Distance Score:  0.734
Doc: Python  is  a great Programming language
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Doc num:  5  Distance Score:  0.891
Doc: Python  and  Java are popular Programming languages
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
Document  3  : I love to relax under the beautiful blue sky!
Top  2  similar docs:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Doc num:  2  Distance Score:  0.0
Doc: The sky  is  blue  and  beautiful
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Doc num:  1  Distance Score:  0.602
Doc: The sky  is  blue
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

上述輸出可以看出,具有較低 HB 距離得分的文檔與查詢文檔更為相似,輸出文檔結果與使用余弦相似度獲得的輸出非常相似。請比較結果,並使用更大的語料庫驗證這些函數!在構建大型的相似度分析系統時,推薦使用在 gensim.matutils 模塊(它的邏輯與前述函數相同)中的 hellinger() 函數。

Okapi BM25 排名

目前,在信息索引和搜索引擎領域中,有幾種非常受歡迎的技術,包括 pageRank 和 Okapi BM25 ,縮寫詞 BM 代表 最佳匹配。這種技術也稱為 BM25,但是為了完整起見,將其稱為 Okapi BM25,因為最初 BM25 函數的概念只存在於理論上,倫敦城市大學在 20 世紀 80 年代至 90 年代建立了 Okapi 信息檢索系統,才真正實現了這種技術,並用來監測現實世界里真實的文件數據。這種技術也稱為基於概率相關性的框架或模型,並由 20 世紀 70 年代至 80 年代由記為科學家提出,包括計算機科學家 S • 羅伯森(S. Robertson) 和 K • 瓊斯(K. Jones)。有一些函數可以根據不同的因素對文檔進行排名,BM25 是其中之一,其較新的變體是 BM25F,其他變體包括 BM15 和 BM25+。

Okapi BM25 的正式定義是采用一個基於詞袋的模型,根據用戶輸入檢索相關文檔的文檔排名和檢索函數。該查詢本身可以是包含句子或句子集合的文檔,也可以只是幾個單詞。實際上,Okapi BM25 不僅僅是一個函數,而是由一整套評分功能組合在一起構成的一個框架。假設有一個查詢文檔 QD,其中 QD=(q1,q2,...,qn) 包含 n 個詞項或關鍵字,同時在文檔語料庫中有一個語料庫文檔 CD,希望使用相似度得分從中獲取與查詢文檔最相關的文檔,正如我們之前所做的那樣。那么,可以在數學上定義這兩個文檔直接的 DM25 得分:

上試中函數 bm25(CD,QD)基於查詢文檔 QD 計算文檔 CD 的 DM 25 排名或得分。函數 idf(qi) 給出了在包含 CD 的語料庫(希望從其中檢索相關文檔的語料庫)中的詞項 qi 的逆文檔頻率(IDF)。在前面實現 TF-IDF 特征提取器時計算過 IDF,它的表達式如下:

其中 idf(t) 表示詞項 t 的 idf,C 表示語料庫中的總文檔數,df(t) 表示包含詞項 t 的文檔數量的頻率。實現 idf 還有其他各種各樣的方法,但是在這里將使用這種方法,需要說明的是,不同實現方法最終得出的結果是非常相似的。函數 f(qi,CD) 給出了語料庫文檔 CD 中詞頻 qi 的頻率。|CD| 表示通過字數測量得到的文檔 CD 的總長度,avgdl 表示待檢索文檔的語料庫中文檔的平均長度。此外,還會觀察到有兩個自由參數 k1 和 b,k1的取值范圍通常為 [1.2, 2.0],b 則通常取 0.75。將在實際執行中將 k1 的值設為 1.5。

通過以下幾個步驟來計算文檔的 BM25 得分:

  1. 建立一個函數以獲得語料庫中詞項的逆文檔頻率(IDF)值。
  2. 構建一個計算查詢文檔和語料庫文檔的 BM25 得分的函數。
  3. 為語料庫文檔和查詢文檔獲取基於詞袋的特征。
  4. 使用第 1 步中的函數計算語料庫文檔的平均長度和語料庫文檔中詞項的 IDF 值。
  5. 使用第 2 步中的函數計算 BM25 得分、為相關文檔排序為每個查詢文檔去前 n 個最相關的文檔。

從實現一個提取和計算文檔語料庫中所有詞項的逆文檔頻率的函數開始着手,該函數使用包含詞項的詞袋特征,然后使用前述公式將其轉換為 IDF。如下函數所示:

import  scipy.sparse as sp
 
def  compute_corpus_term_idfs(corpus_features, norm_corpus):
     
     dfs  =  np.diff(sp.csc_matrix(corpus_features, copy = True ).indptr)
     dfs  =  1  +  dfs  # to smoothen idf later
     total_docs  =  1  +  len (norm_corpus)
     idfs  =  1.0  +  np.log( float (total_docs)  /  dfs)
     return  idfs

現在,要實現基於查詢文件的、對語料庫中所有文檔的 BM25 得分進行計算的主要函數,並根據文檔的 BM25 得分從語料庫中檢索前 n 個最相關的文檔。以下函數實現了 BM25 評分框架:

def  compute_bm25_similarity(doc_features, corpus_features,
                             corpus_doc_lengths, avg_doc_length,
                             term_idfs, k1 = 1.5 , b = 0.75 , top_n = 3 ):
     # get corpus bag of words features
     corpus_features  =  corpus_features.toarray()
     # convert query document features to binary features
     # this is to keep a note of which terms exist per document
     doc_features  =  doc_features.toarray()[ 0 ]
     doc_features[doc_features > =  1 =  1
     
     # compute the document idf scores for present terms
     doc_idfs  =  doc_features  *  term_idfs
     # compute numerator expression in BM25 equation
     numerator_coeff  =  corpus_features  *  (k1  +  1 )
     numerator  =  np.multiply(doc_idfs, numerator_coeff)
     # compute denominator expression in BM25 equation
     denominator_coeff  =   k1  *  ( 1  -  +
                                 (b  *  (corpus_doc_lengths  /
                                         avg_doc_length)))
     denominator_coeff  =  np.vstack(denominator_coeff)
     denominator  =  corpus_features  +  denominator_coeff
     # compute the BM25 score combining the above equations
     bm25_scores  =  np. sum (np.divide(numerator,
                                    denominator),
                          axis = 1 )
     # get top n relevant docs with highest BM25 score                    
     top_docs  =  bm25_scores.argsort()[:: - 1 ][:top_n]
     top_docs_with_score  =  [(index,  round (bm25_scores[index],  3 ))
                             for  index  in  top_docs]
     return  top_docs_with_score

函數里的注釋十分簡單明了,它們解釋了函數如何實現 BM25 的評分功能。簡單來說,首先計算 BM25 數學表達式中的分子,然后計算其分母。最后,將分子除以分母,獲得所有語料庫文檔的 BM25 得分。最后按降序順序,並返回前 n 個具有最高 BM25 的分的相關文檔。在下面的代碼中,將在示例語料庫中對函數進行測試,並查看它們對每個查詢文檔的執行情況:

vectorizer, corpus_features  =  build_feature_matrix(norm_corpus,
                                                    feature_type = 'frequency' )
query_docs_features  =  vectorizer.transform(norm_query_docs)
 
doc_lengths  =  [ len (doc.split())  for  doc  in  norm_corpus]  
avg_dl  =  np.average(doc_lengths)
corpus_term_idfs  =  compute_corpus_term_idfs(corpus_features,
                                             norm_corpus)
print  ( 'Document Similarity Analysis using BM25' )
print  ( '=' * 60 )
for  index, doc  in  enumerate (query_docs):
     
     doc_features  =  query_docs_features[index]
     top_similar_docs  =  compute_bm25_similarity(doc_features,
                                                corpus_features,
                                                doc_lengths,
                                                avg_dl,
                                                corpus_term_idfs,
                                                k1 = 1.5 , b = 0.75 ,
                                                top_n = 2 )
     print  ( 'Document' ,index + 1  , ':' , doc)
     print  ( 'Top' len (top_similar_docs),  'similar docs:' )
     print  ( '-' * 40 )
     for  doc_index, sim_score  in  top_similar_docs:
         print  ( 'Doc num: {} BM25 Score: {}\nDoc: {}' . format (doc_index + 1 ,
                                                                  sim_score,
                                                                  toy_corpus[doc_index]))
         print  ( '-' * 40 )
     print ("")

結果:

Document Similarity Analysis using BM25
= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
Document  1  : The fox  is  definitely smarter than the dog
Top  2  similar docs:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Doc num:  8  BM25 Score:  7.334
Doc: The dog  is  smarter than the fox
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Doc num:  7  BM25 Score:  3.88
Doc: The fox  is  quicker than the lazy dog
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
Document  2  : Java  is  a static typed programming language unlike Python
Top  2  similar docs:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Doc num:  4  BM25 Score:  6.521
Doc: Python  is  a great Programming language
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Doc num:  5  BM25 Score:  5.501
Doc: Python  and  Java are popular Programming languages
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
Document  3  : I love to relax under the beautiful blue sky!
Top  2  similar docs:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Doc num:  2  BM25 Score:  7.334
Doc: The sky  is  blue  and  beautiful
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Doc num:  1  BM25 Score:  4.984
Doc: The sky  is  blue
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

現在可以看出,對於每個查詢文檔,是如何獲得與查詢文檔內容相似的文檔的。可以看到該結果與前面測試結果非常相似,當然了,因為它們都是相似度和排名指標,並且預期就會返回類似的結果。請注意,相關文件的 BM25 得分越高,文檔越相關。不幸的是,無法再 nltk 或 scikit-learn 中找到任何成熟的、可擴展的  BM25 排名框架實現方法。但是,在 gensim.summarization 包下,gensim 似乎有一個 bm25 模塊,如果有興趣的話,可以嘗試一下。

可以嘗試加載更大的文檔語料庫,並在一些示例查詢字符串和示例文檔上測試這些函數。事實上,諸如 Solr 和 Elasticsearch 這樣的信息檢索框架是建立在 Lucene 之上的,Lucene 使用這類的排名算法從存儲文檔的索引中返回相關文檔,也可以使用排名算法構建自己的搜索引擎!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM