写在前面:本博客只是非常非常粗糙的介绍以下tf-idf算法,倒排检索等信息检索知识,并借助这些知识来实现12万篇文档内容的查询,如想要深入了解请相关知识,请自行百度。
文件:en.txt 共计125298篇文档,要求为每个文件建立一个向量空间
要求:使用向量空间125298篇文档中查找相关文档,按照向量相似度进行排序模型,分别进行以上查询,取排名前20的文档doc id 和其文档内容。
在我们实现之前,我们先了解几个铺垫知识

  1. 倒排索引:
    对于一个文档集中每个词(记作t),将其包含这个词的doc id记下。形成一张倒排记录表。
    例如:
    图片说明
    每个词后面的数字表示这个在对应编号的文档中包含了这个词。这样一来,当文本集数量极大时,想要查询一些词就可以通过倒排索引表获得包含它们的文档的编号。进而减少有关文档数量,更加便于进行具体的查询(比如and or not等)
    借助倒排索引我们可以在接下来的12W篇文档的查询中减少查询量以提升速度。

2.tf-idf算法
在上面的倒排索引中,对于每个查询,我们返回的答案,它都是同一个地位的,它不存在一个评分机制,但事实是,对于每个查询,我们都希望返回的内容是排好序的,相关度高的放在上面。于是我们要给每个查询加一个权值。
我们先来看tf:tf是指单词出现的频率,称为词项频率。 称为单词t在文档d中出现的次数。但是两篇文档的相关度并不是与成线性关系的。例:词A在文章1中出现了10次,在文章2中出现1次,你觉得它们的相关度差距真是10倍吗?肯定不是,于是我们对其进行取log运算,以降低它的权值。那么新的值的计算公式就是
图片说明
这个 被称为对数词频 。
接下来我们再看以下稀有度df 记为某个词在文档集中多少篇文档中出现,对于一个词,如果它在每篇文档中都出现,你说它重要吗?烂大街的N卡,肯定不重要!但是如果一个词只在那么一两篇文档中出现,你说这个词重要吗?肯定重要啊,馋哭萌新的SSR/SP卡,稀有的不得了,当然重要啊。那么还是那个问题,词A在1篇文档中出现了,词B在1000篇文档中出现了,你说它们的稀有度差距有1000倍吗,肯定不是。也是一个道理,我们还是对其进行取log运算,以降低它的权值。新的计算公式就是
idf_t
注意这里的N是文档集中文档的数目。
一个词的tf-idf就是这两个词的乘积。
tf-idf公式
这里的 是tf-idf(别误会):

至此书写代码前的预备知识已经全部知道了。

思路:很明显,如果我们想平常那样把所有文章的所有词的tf-idf表做出来不仅时间消耗巨大,而且会做出一个超级多0的矩阵,这对空间的消耗也是巨大的,这肯定不是我们想看到的,那么有什么方法来优化呢?我们看到一篇文档与查询的相似度如果不为0,那么它里面一定包含了查询词,那么我们就可以借助倒排索引表,将包含查询词的文档全部返回,再在这些文档中计算相似度,这样计算的数量就大大减少了。至此代码也就可以写出来了。

import re
import math
import time
import sys
st = time.time()
'''下面是变量区'''
dict_letter = {'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4, 'f': 5, 'g': 6, 'h': 7, 'i': 8, 'j': 9, 'k': 10, 'l': 11,
               'm': 12,
               'n': 13, 'o': 14, 'p': 15, 'q': 16, 'r': 17, 's': 18, 't': 19, 'u': 20, 'v': 21, 'w': 22, 'x': 23,
               'y': 24, 'z': 25}
start_list = []  # 这个是每篇doc 的起始行
end_list = []  # 这是终止行
list_everydoc = []  # 这个是每篇文章的内容  这个肯定做好了
list_everydoc_set = []  # 存储着每篇文章中所有(不重复)单词
list_words_in_everydoc = []#每篇文章中的所有不重复单词
daopai = []#这也就开着玩玩,根本没用到,用到的是它的那种思想
set_jiao=set()#就是所有包含任一查询词的集合


list_haveword = [[] for i in range(0, 26)]  # 这个是已经有了的单词,总词数是15554(这是包含了不是字母单词的)真正的总数更少。
list_index = [[] for j in range(0, 26)]  # 这是对应词在倒排索引表中的位置

query=sys.argv[2]

list_query=query.split()
tot_num=len(list_query)
#下面是对查询词进行以下处理,首先要去除重复元素(用于查找),再就是计算查询词的tf-idf
set_query=set(list_query)
list_set_query=list(set_query)
list_set_query.sort()
query_vec=[list_query.count(list_set_query[i]) for i in  range(0,len(list_set_query))]

print("查询词处理完毕,查询词矩阵建立完毕")
print("耗时:",time.time()-st)
#你要明白, tf-idf 里面有查询词就好了
tf = [[list_set_query[i],0] for i in range(0,len(list_set_query))]
list_ans=[]

'''变量开完了'''



pattern_start = re.compile(r'<doc id=\d+>')
pattern_end = re.compile(r'<\\\\doc>')
with open(sys.argv[1], "rb") as f:
    f.seek(0)
    w = f.readlines()
print("读完了")
print("耗时:", time.time() - st)
len_w = len(w)  # 这个就是总行数
cnt = 0#文章编号从0开始
for i in range(0, len_w):
    f=0
    a = pattern_start.search(str(w[i]))
    if a:
        start_list.append(i)
        continue
    b = pattern_end.search(str(w[i]))
    if b:
        str_temp = str()
        str_temp_split = list()
        str_temp_split_temp = list()
        end_list.append(i)
        cnt += 1  # 文章篇数++
        list_everydoc.append([])
        for k in range(start_list[-1] + 1, end_list[-1]):  # 当前文章的合成
            str_temp += str(w[k]).strip()
        str_temp.strip()

        list_everydoc[-1].append(str_temp)  # 把文章放入每篇文章内容中

        str_temp_split_temp = str_temp.split()
        for k in range(0, len(str_temp_split_temp)):  # 一个憨批至极的分词
            if (len(str_temp_split_temp[k].split('\\n\'b\'')) == 2):
                str_temp_split.extend(str_temp_split_temp[k].split('\\n\'b\''))
            elif (len(str_temp_split_temp[k].split('\\n\'b\"')) == 2):
                str_temp_split.extend(str_temp_split_temp[k].split('\\n\'b\"'))
            else:
                str_temp_split.append(str_temp_split_temp[k])
        '''这里的str_temp_split 就是本篇文章中 出现的单词'''
        str_temp_split.sort()

        for k in range(0,len(list_set_query)):
            if(str_temp_split.count(list_set_query[k])!=0):#说明这个词在这里面出现过
                set_jiao.add(cnt)
                f=1
        if(f):#如果这篇文章里面没有任何一个查询词,它就一点意义都没有
            for k in range(0, len(list_set_query)):
                if(str_temp_split.count(list_set_query[k])!=0):
                    tf[k].append(str_temp_split.count(list_set_query[k]))
                    tf[k][1]+=1
                else:
                    tf[k].append(0)
            list_everydoc_set.extend(str_temp_split)
            list_words_in_everydoc.append(str_temp_split)


        '''到这里,所有doc已经放入list_everydoc里面'''
        #print(cnt,end=" ")

print('共有文章',cnt,"篇")
set_jiao=sorted(list(set_jiao))
# print(set_jiao)
# print(tf)
# print(query_vec)
print("已经获得交集")
print("耗时:",time.time()-st)
for i in range(0,len(set_jiao)):
    list_ans.append([set_jiao[i]])
    temp=0
    for j in range(0,len(query_vec)):#j 对应的是词的位置
        if(tf[j][i+2]!=0):
            temp+=math.log10(cnt/tf[j][1])*(1+math.log10(tf[j][i+2]))*query_vec[j]
    list_ans[-1].append(temp)
list_ans.sort(key=lambda x:-x[1])
print("相似度最高的",min(20,len(list_ans)),"篇文本编号是:")
for i in range(0,min(20,len(list_ans))):
    print(list_ans[i][0])
    print(str(list_everydoc[i-1][0]))
print("耗时:",time.time() - st,"秒")





写在最后:这个大作业应该是这个学期所有知识的一个大混合,前面的作业除了网页处理那次其他基本都是暴力的(文章篇数太少了),这次一看到12W的文档就知道肯定要优化,但真没想到这么难写。写了将近一个月,每周两节课(共计4H)一直在写。写了有四五个版本吧,都因为速度问题被淘汰了(其实就是自己傻了,非要去先算所有文档的tf-idf值,再求交集,再求相似度)。后来经过高人点拨,先求交集,直接飞起来。
不知道为什么,虽然别的同学都叫我用jieba库,但自己就是很莽撞的要自己手码,可能是因为ACM上菜的像狗(本来就是菜狗),希望有那么一点成就感吧。至此我也就完成了本学期的信息检索课的所有内容,嗯,很棒!抓紧时间,期末考试冲鸭!