按:本文浅谈信息检索是什么,为何,怎么作等问题,主要内容是Manning等人著的《信息检索导论》前八张的读书笔记php
答曰:html
根据《信息检索导论》(Manning, Raghavan & Schütze, 2008)第一章:java
Information retrieval (IR) is finding material (usually documents) of an unstructured nature (usually text) that satisfies an information need from within large collections (usually stored on computers).python
翻译过来的大白话,“信息检索”是在一大堆非结构化的信息里面(一般是文本),找到符合需求的信息。web
或问曰: 信息检索这门技术用在什么地方?算法
或问曰: 信息检索的起源与演变过程是怎样的?typescript
答曰:json
为何须要信息检索?最本源的缘由就是,信息太多,找不过来,须要有相应的技术和工具辅助。信息越多,咱们的需求越细致,就越要使用高级的技术和工具。ubuntu
信息检索技术起源于图书馆书籍管理。这不难理解,在计算机和互联网出现之前,书籍就是信息的基本载体,图书馆就是信息的集中地。要在一本书籍中找到某个信息,好比“某个朝代发生的某件事情”,能够人工把书本翻个遍,找出来这个信息,这并非难难事。但要在整个图书馆找到符合“某个朝代发生的某件事情”这个需求的信息,显然是不可能一本书一本书地去翻。所以,对书籍进行分类,就是必须的。分类之后,咱们能够到“历史”这个类别里找到这个朝代的书籍,再翻书查找。“分类”就是最简单的信息管理手段,对信息分类,而后根据需求到特定的类别里去找找信息,就是最简单的信息检索方法。数组
进入互联网时代,计算机里的文档成为了信息的主要载体,互联网成为了新时代的图书馆(互联网本质上就是一个分布式文档系统)。天然而然地,分类方法在一开始也被应用到了互联网文档(主要是HTML文档)的检索上。而Yahoo!的成功案例也证实了分类方法在互联网初期也是很是有用的。但随着互联网内容的爆炸性增加,分类方法也逐渐失效了。没办法,即使设置几百个类别,每一个类别内的内容也太多了。因此Yahoo!逐渐没落,而Google逐渐兴起。毕竟Google表明的全文搜索方法不管在有效性仍是快捷性都比分类方法有优点。
至此,信息检索的发展主要脉络就是从图书馆书籍管理开始产生分类方法,而后在互联网时代分类方法再也不适用,由此催生了全文检索方法,更准确地说,是创建索引,用索引来作检索的方法。
索引,顾名思义,就是搜索的指引。任何可以指引咱们尽快搜索到咱们所求之物的东西,都是索引。若咱们把每一本书的书名、做者等信息记录在案,找书时只要提供书名、做者等部分或所有信息,就能找到一本书。这种状况下,书名、做者信息的记录表就是索引。Google的全文检索把每一篇文档的每个词记录在案,造成了一个全面而庞大的索引,因此能比分类法更准确的找到信息。顺带一提,这种角度看,分类法实际上是索引法的特殊形式,由于类别也是一种索引。如此一来,用全文每一个词作索引,和根据书本要义用类别作索引,孰优孰劣,就不难分别了。
答曰:
信息检索的目标,或者说基本的任务,就是从一大堆信息中找到咱们须要的某部分信息。进一步,咱们缩小范围,使之更加具体:信息检索的目标是在一大堆文档等非结构化信息中根据咱们的需求挑选出咱们须要的部分文档。这其实就体如今Manning等人对信息检索的定义中。
那么,进行信息检索的基本流程有哪些?或者说为了达成信息检索的任务,咱们要作哪些子任务?首先咱们总得先对咱们眼前的一大堆数据(在这里特指文档集),有一个清晰的认识。起码要知道这个文档集的规模如何,文档由哪些语言写成,若是是电子文档,还要检查一下编码之类的,这样咱们就能够大概知道须要用什么方案。而后就能够创建索引,不管是类别,仍是全文词项,又或是其余的辅助指引工具,都是索引。最后还要实现检索机制,好比制定一些图书馆借阅规定,或者开发一套计算机系统。
答曰(这一部分假定读者掌握线性代数的基础知识):
目前最经常使用最典型的信息检索任务,恐怕就是对网联网上的文档作检索了。而最典型的信息检索方案,即是web搜索引擎。那么web搜索引擎的工做原理又是怎样的呢?
Google和百度等大公司都会有web采集器,不断地、动态地从网上获取web文档(基本就是HTML文档)。采集回来的文档就是信息集,要在这么一大堆文档里(一般是百亿级规模)找出用户须要的文档,就须要索引了。
不过搜索引擎用到的索引到底长什么样?
咱们要根据关键词把文档找出来,也就是说要针对文档中出现的每个词给问你当创建索引。因此全文检索用到的索引就是一个以词为行,文档为列的“词-文档表格”,确切的说是“词-文档矩阵”。
上图就是一个“词-文档矩阵”,图中的行就是文档中出现的词,列就是文档的名字(都是莎士比亚的做品)。图中某行某列中的0表明该行的词未曾出如今该列的文档中,反之。所以“Antony”这个词出如今了“Julius Caesar”这篇文档中,而未曾出如今“Hamlet”中。 从这个矩阵能够清晰看出哪些词存在于哪些文档中,这个就是全文搜索会用到的索引,到信息检索里的术语称为“倒排索引”。
要知道,互联网上的文档数量是百亿级的,而其中包含的词可能也要数十万甚至上百万(瞎猜的),总之比任何一步词典收录的词都要多。这么大的矩阵,实在是太过占用空间了,哪怕是今天,计算机的内存都是宝贵资源啊。考虑到大规模文档集和大规模词表造成的“词-文档矩阵”中会有不少0存在,咱们就不难想到采用稀疏表示的方式来存储这个矩阵。而上图的索引也会变成下面的样子。
图中左边是词项,右边则是文档(编号)列表。“Brutus”对应的列表里有一、二、4等号码,意味着文档一、文档二、文档4里包含着“Brutus”这个词。
那么这个索引是怎么构建出来的呢?
在建索引前确定要作一些预处理。常见的预处理可能会有识别文档编码(UTF-8?GBK?ASKII?),选用正确的解码方式。而后就是分词。英文等拉丁语系的文字词与词之间有间隔,因此还不算难,但也要注意不能把“United Kingdom”这样的专有名词切开。而对于汉语等东亚的语言文字,就须要特殊的分词手段(好比条件随机场、马尔科夫链等)。分完词后还没完,通常还会把一些的都好、句号之类的符号去掉。最后可能还会考虑一下要不要把全部词换成小写,甚至进行词根还原(把词的动词、名词等各类形式统一映射到词根)等词项归一化,使得用户的查询可以匹配到更多可能相关结果。
通过了编码识别、分词、大小写转换、词项归一化等一系列预处理,咱们把一个文档转换成词项流(能够当作由词组成的数组),所以文档集也就转换成了词项数组的集合。接下来咱们就能够创建索引了。咱们能够扫描一遍获得的词项流,获得一个“词-文档ID”流,而后把词项相同的元组合并,就获得了下图右面的倒排索引
Talk is cheap, show me your code!
—— Torvalds · Linus
下面是一段很简单的python代码,为一个简单的文档集构建倒排索引
""" inverted_index.py Build a basic (naive) inverted index for a simple (naive) documents set Please Run this script using Python3.x Tested under Python3.6, Win7 and Python3.5 ubuntu16.04 Author: Richy Zhu Email: rickyzhu@foxmail.com """ import json import re from pprint import pprint def clear_symbols(text): """remove symbols like commas, semi-commas """ simbols = re.compile("[\s+\.\!\/_,$%^*()+\"\']+|[+——!,。?、~@#¥%……&*():]+") if type(text) is str: processed_text = re.sub(simbols, ' ', text) return processed_text elif type(text) is list: return [re.sub(simbols, ' ', item) for item in text] else: raise TypeError("This function only accept str or list as argument") def lowercase(text): """turn all the characters to be lowercase """ if type(text) is str: return text.lower() elif type(text) is list: return [item.lower() for item in text] else: raise TypeError("This function only accept str or list as argument") def tokenize(docs): token_stream = [] for doc in docs: token_stream.append(doc.split()) return token_stream def preprocess(docs): """clear symbols, lowercase, tokenize, get clean tokenized docs """ normalized_docs = lowercase(clear_symbols(docs)) tokenized_docs = tokenize(normalized_docs) return tokenized_docs def get_token_stream(tokenized_docs, docs_dict): """get (term-doc_id) stream """ token_stream = [] for doc_id in docs_dict: for term in tokenized_docs[doc_id]: token_stream.append((term, doc_id)) return token_stream def build_indices(tokenized_docs, docs_dict): """main function -- build invertex index assume that the documents set is small enough to be loaded into Memory """ token_stream = get_token_stream(tokenized_docs, docs_dict) # pprint(token_stream) indices = {} for pair in token_stream: if pair[0] in indices: if pair[1] not in indices[pair[0]]: indices[pair[0]].append(pair[1]) else: indices[pair[0]] = [pair[1]] return indices if __name__ == "__main__": docs = [ "hello world", "hello python", "I love C, Java, Python, Typescript, and PHP", "use python to build inverted indices", "you and me are in one world" ] docs_dict = { 0: "docs[0]", 1: "docs[1]", 2: "docs[3]", 3: "docs[4]", 4: "docs[5]" } tokenized_docs = preprocess(docs) # pprint(tokenized_docs) indices = build_indices(tokenized_docs, docs_dict) pprint(indices)
运行文件能够获得以下结果:
$ python3 inverted_index.py {'and': [4], 'are': [4], 'build': [3], 'hello': [0, 1], 'i': [2], 'in': [4], 'indices': [3], 'inverted': [3], 'love': [2], 'me': [4], 'one': [4], 'python': [1, 2, 3], 'to': [3], 'use': [3], 'world': [0, 4], 'you': [4]}
固然,上面的索引构建程序有一个假设:文档规模不大,整个文档集索引的构建过程均可以在内存里完成。若是文档集过大,就要将其分割成较小的块,对每块作构建索引的操做,而后把每个块操做的结果(这一个块的索引文件)合并起来获得整个文档集的索引。典型的算法有BSBI算法、SPIMI算法等。
如今咱们已经有一个索引了,但咱们怎么去使用呢?
有了索引,就能够很方便地找出咱们须要的文档,最典型的的应用是布尔查询。布尔查询使用布尔运算符对查询 关键词进行链接,好比:
“信息 AND 检索 AND 导论”, 这个查询就是查找文中既包含“信息”,又包含“检索”,还包含“导论”这三个关键词的文档。
“(python OR Java) NOT Ruby”,这个查询就是查找文中包含“python”或包含“Java”,但不包含“Ruby”的文档。
对于AND操做,只需在索引中找到每一个关键词对应的的文档ID列表,而后对文档ID列表求交集,也就是找出全部列表中共有的文档ID,那么这些找出来的文档就是用户想要的文档。OR的NOT操做再也不赘述。
直到如今,不少图书馆的检索系统都还支持布尔查询的操做(大多放在“高级查询”功能里面)。
可是下一个问题来了,布尔查询使用到布尔操做符,并且其理论基础是布尔代数。虽然说他们已经足够简单,但仍是要求用户学习一点知识。并且布尔查询的查询表达式能够至关复杂。并且查找出来的文档虽然都与用户的查询相关,可是并不能按照类似程度来排序,只能根据发表日期等指标来排序。
那么有没有更加简单有效的方法让用户输入天然语言查询(而非布尔表达式等有必定规则的查询),获得根据相关程度排序的文档结果呢?
答案是向量空间模型。
咱们的目标是把全部文档和用户查询一块儿转化成向量(词袋模型、if-idf权重),而后使用线性代数的方法来求得用户查询向量与全部文档向量之间的类似度(余弦类似度),进而获得用户查询与全部文档之间的相关程度。
要把向量空间模型应用于信息检索,要关注三个重要概念:词袋模型、TF-IDF、余弦类似度。
关于向量空间模型的文章整个互联网满大街都是,随便百度一下都能找到许多好的入门文章。在此我摘引一篇文章的部分来介绍词袋模型,资料来自bag-of-words模型入门:
Bag-of-words模型是信息检索领域经常使用的文档表示方法。
在信息检索中,BOW模型假定对于一个文档,忽略它的单词顺序和语法、句法等要素,将其仅仅看做是若干个词汇的集合,文档中每一个单词的出现都是独立的,不依赖于其它单词是否出现。(是不关顺序的)
也就是说,文档中任意一个位置出现的任何单词,都不受该文档语意影响而独立选择的。那么究竟是什么意思呢?那么给出具体的例子说明:
例子
Wikipedia[1]上给出了以下例子:
John likes to watch movies. Mary likes too.
John also likes to watch football games.根据上述两句话中出现的单词, 咱们能构建出一个字典 (dictionary):
{ "John": 1, "likes": 2, "to": 3, "watch": 4, "movies": 5, "also": 6, "football": 7, "games": 8, "Mary": 9, "too": 10 }该字典中包含10个单词, 每一个单词有惟一索引,注意它们的顺序和出如今句子中的顺序没有关联. 根据这个字典, 咱们能将上述两句话从新表达为下述两个向量:
[1, 2, 1, 1, 1, 0, 0, 0, 1, 1] [1, 1, 1, 1, 0, 1, 1, 1, 0, 0]这两个向量共包含10个元素, 其中第i个元素表示字典中第i个单词在句子中出现的次数. 所以BoW模型可认为是一种统计直方图 (histogram). 在文本检索和处理应用中, 能够经过该模型很方便的计算词频.
可是从上面咱们也可以看出,在构造文档向量的过程当中能够看到,咱们并无表达单词在原来句子中出现的次序(这也是bag of words的一个缺点,可是听师兄说,不少状况简单的用bow特征产生的结果就比较好了)
根据上面介绍到的词袋模型,咱们能够把一个文档转换成一个向量,向量的长度是词典的大小,也就是文档集里词项的数量,而向量中的每一个元素都是都是对应词项的词频。同理,用户的查询也能转换为一个向量,好比在上述的情境下,若是用户查询是“football games”,那么对应的向量就是[0,0,0,0,0,0,1,1,0,0]
。如今,咱们就可使用线性代数的方法来求得两个向量的类似度,从而获得用户查询和全部文档对应的类似度。典型的方法就是求得条用户查询向量与文档向量之间的余弦夹角,夹角越小,类似度越大。具体的求法以下:
假定A和B是两个n维向量,A是 [A1, A2, ..., An] ,B是 [B1, B2, ..., Bn] ,则A与B的夹角θ的余弦等于:
这种方法虽然用到了形式化数学的方法,可是本质上的思想很简单:包含用户查询中关键词的文档才与用户的查询相关。一篇文档包含的关键词越多,关键词出现的频率越高,这篇文档与用户查询的相关度就越高。
可是如今问题又来了,有一些词好比“a”,“the”, “of”,或者“啊”, “的”, “了”,它们基本上在每一篇文档都出现,并且在每一篇文档中的出现频率都很高,用户查询中也惊颤会包含这些词。
显然这些词是没什么价值的,不能帮咱们找出真正与用户查询相关的文档的。那咱们要怎么作来消除这些词的影响呢?
一个简单粗暴的方法是维护一个停用词表,也就是把一些没有价值的词,也就是在绝大多数文档都出现的词记录成一张表,在建索引和解析用户查询时忽略这些词。
一个更有技术含量的方法是,在向量化文档和用户查询时,给每一个词赋予权重,使得重要的词权重高,“a”,“啊”之类的词权重低。那么文档向量和用户查询向量里的元素就再也不是词频这样简单的指标,而是词在文档中的权重这样的指标。这种权重指标中最经常使用的方法就是TF-IDF。其核心思想是,在绝大多数文档的中都出现的词重要性最低,只在少数文档中出现的词重要性较高,这些词的词频越高,重要性越高。所以,IF-iDF的计算方法以下:
一些符号: t--某个词项, d--某篇文档, n--文档集包含的文档总数
第一步,计算词频。**
tf = 词项t在文章d中的出现次数 / 文章d的总词项数
第二步,计算逆文档频率。
idf = lg(文档集规模n / 包含词项t的文档数 + 1)
第三步,计算TF-IDF。
tf-idf = tf * idf
Again, talk is cheap。下面是一段简单的python代码,演示使用VSM来计算用户查询和文档类似度。
""" vsm.py Simple implementation of Vector Space Model Note: Depend on Numpy, please install it ahead (`pip install numpy`) Please Run this script using Python3.x Tested under Python3.6, Win7 and Python3.5 ubuntu16.04 Author: Richy Zhu Email: rickyzhu@foxmail.com """ from math import log10 from pprint import pprint import numpy as np def _tf(tokenized_doc): """calculate term frequency for each term in each document""" term_tf = {} for term in tokenized_doc: if term not in term_tf: term_tf[term]=1.0 else: term_tf[term]+=1.0 # pprint(term_tf) return term_tf def _idf(indices, docs_num): """calculate inverse document frequency for every term""" term_df = {} for term in indices: # 一个term的df就是倒排索引中这个term的倒排记录表(对应文档列表)的长度 term_df.setdefault(term, len(indices[term])) term_idf = term_df for term in term_df: term_idf[term] = log10(docs_num /term_df[term]) # pprint(term_idf) return term_idf def tfidf(tokenized_docs, indices): """calcalate tfidf for each term in each document""" term_idf = _idf(indices, len(tokenized_docs)) term_tfidf={} doc_id=0 for tokenized_doc in tokenized_docs: term_tfidf[doc_id] = {} term_tf = _tf(tokenized_doc) doc_len=len(tokenized_doc) for term in tokenized_doc: tfidf = term_tf[term]/doc_len * term_idf[term] term_tfidf[doc_id][term] =tfidf doc_id+=1 # pprint(term_tfidf) return term_tfidf def build_terms_dictionary(tokenized_docs): """assign an ID for each term in the vocabulary""" vocabulary = set() for doc in tokenized_docs: for term in doc: vocabulary.add(term) vocabulary = list(vocabulary) dictionary = {} for i in range(len(vocabulary)): dictionary.setdefault(i, vocabulary[i]) return dictionary def vectorize_docs(docs_dict, terms_dict, tf_idf): """ transform documents to vectors using bag-of-words model and if-idf """ docs_vectors = np.zeros([len(docs_dict), len(terms_dict)]) for doc_id in docs_dict: for term_id in terms_dict: if terms_dict[term_id] in tf_idf[doc_id]: docs_vectors[doc_id][term_id] = tf_idf[doc_id][terms_dict[term_id]] return docs_vectors def vectorize_query(tokenized_query, terms_dict): """ transform user query to vectors using bag-of-words model and vector normalization """ query_vector = np.zeros(len(terms_dict)) for term_id in terms_dict: if terms_dict[term_id] in tokenized_query: query_vector[term_id] += 1 return query_vector / np.linalg.norm(query_vector) def cos_similarity(vector1, vector2): """compute cosine similarity of two vectors""" return np.dot(vector1,vector2)/(np.linalg.norm(vector1)*(np.linalg.norm(vector2))) def compute_simmilarity(docs_vectors, query_vector, docs_dict): """compute all similarites between user query and all documents""" similarities = {} for doc_id in docs_dict: similarities[doc_id] = cos_similarity(docs_vectors[doc_id], query_vector) return similarities if __name__ == '__main__': tokenized_docs = [ ['hello', 'world'], ['hello', 'python'], ['i', 'love', 'c', 'java', 'python', 'typescript', 'and', 'php'], ['use', 'python', 'to', 'build', 'inverted', 'indices'], ['you', 'and', 'me', 'are', 'in', 'one', 'world'] ] tokenized_query = ["python", "indices"] docs_dict = { 0: "docs[0]", 1: "docs[1]", 2: "docs[2]", 3: "docs[3]", 4: "docs[4]" } indices = {'and': [2, 4], 'are': [4], 'build': [3], 'c': [2], 'hello': [0, 1], 'i': [2], 'in': [4], 'indices': [3], 'inverted': [3], 'java': [2], 'love': [2], 'me': [4], 'one': [4], 'php': [2], 'python': [1, 2, 3], 'to': [3], 'typescript': [2], 'use' : [3], 'world': [0, 4], 'you': [4]} tf_idf = tfidf(tokenized_docs, indices) terms_dict = build_terms_dictionary(tokenized_docs); docs_vectors = vectorize_docs(docs_dict, terms_dict, tf_idf) query_vector = vectorize_query(tokenized_query, terms_dict) # pprint(docs_vectors) pprint(compute_simmilarity(docs_vectors, query_vector, docs_dict))
运行以上脚本能够获得下面的结果,字典作点是文档id,右边是对应的用户查询的类似度。可见文档3与用户查询最为相关。
$ python3 vsm.py {0: 0.0, 1: 0.34431538823149532, 2: 0.088542411007409116, 3: 0.41246212572975449, 4: 0.0}
关于向量空间模型的资料,有不少更加详细,更加通俗易懂的文章,好比吴军博士《数学之美》的第11章: 肯定网页和查询的相关性:TF-IDF,Manning等人《信息检索导论》的第6章: Scoring, term weighting, and the vector space model。
小结
因此向量空间模型的要点是把文档转换成向量,把用户的查询也转换成向量,而后求查询向量和全部文档向量的余弦夹角等类似度指标,根据类似度来排序。
可是对大规模文档集,不可能把用户查询跟全部文档向量的类似度都计算一遍,这样太耗费时间了。咱们能够牺牲一点点搜索质量,来获取时间性能上的大幅提升。首先咱们能够结合倒排索引,先根据用户查询的所有或部分关键词,在索引中找出相关文档列表,而后只对搜索出来的文档进行类似度计算。若是这一步后找出来的文档依然太多,咱们能够更进一步,先根据每一个词在每一篇文档中TF-IDF值对文档进行排序,排序高的放在这个词对应的文档列表前面。这样,咱们能够每次只选出关键词对应的文档列表的前K个文档作类似度计算,便彻底能够控制时间成本和搜索质量之间的平衡了。
至此,一个基于倒排索引和向量空间模型的全文搜索引擎的核心工做机制就完备了。
答曰:
前文详细介绍了一种主要使用倒排索引和向量空间模型的信息检索方案,主要用于检索计算机里的文档。早期的Google等互联网搜索引擎主要采用的都是这种方案。甚至如今,这种方案可能也仍是Google的主要框架。这种搜索引擎也称为“ad hoc search engine”, “ad hoc”在拉丁文中是“特殊的、临时的、针对特定目的”的意思。也就是说咱们经常使用的搜索引擎针对获得用户信息需求都是“特殊的、临时的、针对特定目的”。相应的,也有长期的,稳定的信息需求,好比一个长期关注信息检索技术的人,就想要天天阅读一些信息检索主题的文章。更加常见的长期、稳定的信息需求,多是“在一大堆邮件中找出垃圾邮件(而后丢掉)”。针对这些信息需求,咱们须要文档集进行分类等操做,找出文档集里“信息检索技术”主题的文章,或者对邮件集操做,找出“垃圾邮件”类别的邮件(而后丢掉)。典型的方案就是贝叶斯文本分类、支持向量机文本分类,K临近文本聚类之类额机器学习习方法。Manning等人的《信息检索导论》后面好几章都是介绍这些机器学习方法在文本分类和聚类中的应用。
当下,搜索引擎是信息检索的主要工具和研究分支。可是让咱们从新检视信息检索的定义:
Information retrieval (IR) is finding material (usually documents) of an unstructured nature (usually text) that satisfies an information need from within large collections (usually stored on computers).
信息检索的本质,应该是“给用户找到他想要的信息”。那么若是用户提出一个问题,而后咱们不是返回一堆文档,而是直接告诉他问题的答案,岂不是更好?是的,人们把自动问答系统看做是下一代的搜索引擎,而Google、百度、搜狗等人工智能和信息检索巨头也正在大力开发自动问答系统。
让咱们更激进一点,若是信息检索的本质是“给用户找到他想要的信息”,那么更加高端的信息检索形式多是在用户提出问题以前就给他提供他想要的信息,好比用户刚想换一部手机,就把全部手机相关的商品信息呈现给用户;或者说用户刚想了解一下娱乐圈某明星最近发生的一件事,系统就他推送相关新闻给这位用户。是的,推荐系统可能才是信息检索最高级的形态,这可能也是不少人看好今日头条的缘故吧。
一些参考代码:https://coding.net/u/qige96/p/IR-exe
本文直接或简介地使用了如下著做的内容:
本做品首发于简书平台,采用知识共享署名 4.0 国际许可协议进行许可。