超平面多维近似向量查找工具annoy使用总结

发布日期:2019-05-18

需求:我有800万的中文词向量,我想要查询其中任意一个向量对应的最邻近的向量是哪些。通常情况下如果向量集比较小的话,几十万个向量(几个G这种),我们都可以用gensim的word2vec来查找,但是880万有16个G,加到内存中就爆炸了,而且gensim中的查找属于暴力搜索,即全都遍历比较余弦相似度来进行查找,因此几百万的词向量查找起来就很慢了。这里我需要用更快速的工具来进行查找,找到了两个工具,一个是facebook的faiss包另一个是annoy包。Faiss只能部署在linux,而且看着好复杂,各种索引类型啥的,估计够我研究一阵,索性使用了annoy包;

关于annoy包的使用方法参考这两个网址足够:

1、https://github.com/spotify/annoy 

2、https://markroxor.github.io/gensim/static/notebooks/annoytutorial.html

1是官方文档,写的非常简单,但是我刚开始没有认真看,所以走了很多弯路;2是一个notebook实践案例,基于gensim的,我就是被这个版本给坑了。。。这里面有很多说道,首先我先说一下代码逻辑,其实很简单,首先是读取你的带 word和vec的txt向量文件作为model:

1 model = gensim.models.KeyedVectors.load_word2vec_format("D:describedicsynonyms_vector.txt"binary=Falseunicode_errors="ignore")2 annoy_index = AnnoyIndexer(model 100)      # 生成索引3 fname = "synonyms_txt_index"4 annoy_index.save(fname)               # 将索引文件保存到硬盘

代码说明

这四行代码目的是

1、加载model

2、对model进行聚类计算

3、建立一个二叉树集合的索引(树的数量为100),

4、将索引保存到硬盘

接下来我们就可以根据建立的这个索引来查找近似向量了:

1 annoy_index2 = AnnoyIndexer()    # 初始化索引2 annoy_index2.load(fname)3 annoy_index2.model = model

这三行就是来加载索引,值得注意的是这里的model就是之前最开始加载的txt文件对应的model

接下来问题来了,执行

1 word = ‘人民’2 vector1 = model[word]3 approximate_neighbors = model.most_similar([vector1] topn=30 indexer=annoy_index2)

这里想要查询“人民”对应的前30个相近词,通过加载索引来查询,可是最终的查询速度跟我没建立索引之前的暴力搜索(即word2vec自带搜索)是一样的,但是如果我在这个加载索引之前先进行一个词的暴力搜索,然后再对其他的词进行加载索引搜索,速度就会快出很多倍,这让我百思不得其解,最后没办法我就先用暴力搜索先搜索一个词,然后剩下的词都用annoy搜索,这样速度还是很快的;

但是我还是想弄明白到底怎么回事,于是我去官网问作者,作者就说了一句,你需要进行整数映射,(而且应该是非负整数),靠!!!其实官网写的明明白白:

a.add_item(i v) adds item i (any nonnegative integer) with vector v. Note that it will allocate memory for max(i)+1 items.

也就是说我的txt文件格式需要如下这种格式:

1 vecor2 vecor

而不是开头是汉语单词以及对应的vector,最后再做一个integer到word的映射字典即可;

接下来我对作者给出的github上的版本进行了验证,代码如下:

from annoy import AnnoyIndeximport randomf = 100t = AnnoyIndex(f)dict = {}with open("C:UsersAdministratorDesktopsynonymssynonyms_vector.txt""r"encoding="utf-8") as f: count = 0 for line in f: result = line.split() if len(result)>10: count+=1 word = result[0] dict[count] = word vector = list(map(eval result[1:])) # 需要将txt中的str格式vec转化为float格式 t.add_item(count vector)t.build(10)t.save("C:UsersAdministratorDesktopsynonymstest.ann")u = AnnoyIndex(100)u.load("C:UsersAdministratorDesktopsynonymstest.ann")simi_id = u.get_nns_by_item(880 20include_distances=True)id = simi_id[0]score = simi_id[1]# print(simi_id)# for ij in zip(idscore): # print(dict[i]) # print(0.5*(abs(1-j))+0.5)result =[(dict[i]0.5*(abs(1-j))+0.5) for ij in zip(idscore)]输出结果(result):[("投资" 1.0) ("融资" 0.6934992074966431) ("投资者" 0.6180032193660736) ("投资额" 0.6166684031486511) ("房地产" 0.6127455532550812) ("外资" 0.6104367673397064)]

这里面我需要指出几点需要注意的地方:

1、需要将txt中的str格式vec转化为float格式,否则会报错;

2、我建立了一个字典映射,这样能够最后从查询到的近似向量对应的id值查询到映射的词;

3、最后那个0.5*(abs(1-j))+0.5是余弦相似度归一化的计算公式,作者程序中建立的距离索引以及最后查找返回值都是默认angular模式(即j的值),也即是余弦相似度,即angular=1-cosin,且其值域为[02](因为cosin值域为[-11])。但是我们通常只需要求取cosin的绝对值,即其值域应该位于[01]。所以我先用1-angular 获取cosin 然后再求取绝对值,最后再进行归一化((1+余弦相似度)/2)即可。

4、对于上面代码求取结果我和word2vec的most_similar对比了一下,近似度基本一致,前三位精度完全一致,说明最后的余弦相似度求取向量相似度就是按照我上面说的方法来进行的;

最后附上余弦相似度计算方法参考网址:余弦相似度python实现

PS:关于annoy加载索引还需要注意一点,索引文件路径必须是英文路径,否则程序就会提示查找不到文件,望注意!!!