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 使用这类的排名算法从存储文档的索引中返回相关文档,也可以使用排名算法构建自己的搜索引擎!

猜你喜欢

转载自www.cnblogs.com/dalton/p/11354021.html
今日推荐