需求: 我有 800 万的中文词向量, 我想要查询其中任意一个向量对应的最邻近的向量是哪些. 通常情况下如果向量集比较小的话, 几十万个向量(几个 G 这种), 我们都可以用 gensim 的 word2vec 来查找, 但是 880 万有 16 个 G, 加到内存中就爆炸了, 而且 gensim 中的查找属于暴力搜索, 即全都遍历比较余弦相似度来进行查找, 因此几百万的词向量查找起来就很慢了. 这里我需要用更快速的工具来进行查找, 找到了两个工具, 一个是 Facebook 的 faiss 包另一个是 annoy 包. Faiss 只能部署在 Linux, 而且看着好复杂, 各种索引类型啥的, 估计够我研究一阵, 索性使用了 annoy 包;
关于 annoy 包的使用方法参考这两个网址足够:
- ,https://github.com/spotify/annoy
- ,https://markroxor.github.io/gensim/static/notebooks/annoytutorial.html
1 是官方文档, 写的非常简单, 但是我刚开始没有认真看, 所以走了很多弯路; 2 是一个 notebook 实践案例, 基于 gensim 的, 我就是被这个版本给坑了... 这里面有很多说道, 首先我先说一下代码逻辑, 其实很简单, 首先是读取你的带 Word 和 vec 的 txt 向量文件作为 model:
- model = gensim.models.KeyedVectors.load_word2vec_format('D:\\describe\\dic\\synonyms_vector.txt',binary=False,unicode_errors='ignore')
- annoy_index = AnnoyIndexer(model, 100) # 生成索引
- fname = 'synonyms_txt_index'
- annoy_index.save(fname) # 将索引文件保存到硬盘
代码说明
这四行代码目的是
1, 加载 model
2, 对 model 进行聚类计算
3, 建立一个二叉树集合的索引(树的数量为 100),
4, 将索引保存到硬盘
接下来我们就可以根据建立的这个索引来查找近似向量了:
- annoy_index2 = AnnoyIndexer() # 初始化索引
- annoy_index2.load(fname)
- annoy_index2.model = model
这三行就是来加载索引, 值得注意的是这里的 model 就是之前最开始加载的 txt 文件对应的 model
接下来问题来了, 执行
- Word = '人民'
- vector1 = model[Word]
- 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 vecor 2 vecor
而不是开头是汉语单词以及对应的 vector, 最后再做一个 integer 到 Word 的映射字典即可;
接下来我对作者给出的 GitHub 上的版本进行了验证, 代码如下:
- from annoy import AnnoyIndex
- import random
- f = 100
- t = AnnoyIndex(f)
- dict = {}
- with open('C:\\Users\Administrator\Desktop\synonyms\\synonyms_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:\\Users\Administrator\Desktop\synonyms\\test.ann')
- u = AnnoyIndex(100)
- u.load('C:\\Users\Administrator\Desktop\synonyms\\test.ann')
- simi_id = u.get_nns_by_item(880, 20,include_distances=True)
- id = simi_id[0]
- score = simi_id[1]
- # print(simi_id)
- # for i,j in zip(id,score):
- # print(dict[i])
- # print(0.5*(abs(1-j))+0.5)
- result =[(dict[i],0.5*(abs(1-j))+0.5) for i,j in zip(id,score)]
输出结果(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, 且其值域为[0,2](因为 cosin 值域为[-1,1]). 但是我们通常只需要求取 cosin 的绝对值, 即其值域应该位于[0,1]. 所以我先用 1-angular 获取 cosin 然后再求取绝对值, 最后再进行归一化((1 + 余弦相似度)/2) 即可.
4, 对于上面代码求取结果我和 word2vec 的 most_similar 对比了一下, 近似度基本一致, 前三位精度完全一致, 说明最后的余弦相似度求取向量相似度就是按照我上面说的方法来进行的;
最后附上余弦相似度计算方法参考网址: 余弦相似度 python 实现
PS: 关于 annoy 加载索引还需要注意一点, 索引文件路径必须是英文路径, 否则程序就会提示查找不到文件, 望注意!!!
来源: https://www.cnblogs.com/zxyza/p/10061330.html