Github13K!相似搜索百宝箱,文本匹配入门必备!

NLP情报局

共 4501字,需浏览 10分钟

 · 2021-03-29

每个人都有网购的经历,当你打开淘宝、京东app进入店铺,是怎样找到自己心仪商品的呢?
最直白的方法,拉到“全部商品”页,从头一件件看。但这样做明显效率不高,如果有上千件商品,可能想买的没买到,还加购了一堆自己原本不想买的东西。

所以,你一般会直接在已有分类栏中选择感兴趣的类别,比如“男鞋”,“女大衣”;或者在搜索框中输入关键词。总之,目的只有一个:缩小搜素范围,避免无关商品刷屏。

在自然语言处理基础任务文本匹配中,也存在搜索问题。例如,现在有一个包含了1000个常见高频问题(即FAQ)的问答知识库,用户提出一个新问题,工程师们希望以最快速定位知识库中和它最相似的问题,并返回答案。
当数据规模在1K或1W时,线性搜索(暴力法)是一种省事儿的好方法。但是当数据增加到百万、千万级别时,O(n)的时间开销可能会让用户们抓狂,内存中也无法存放所有的数据。当数据涨到亿级别,问题更加棘手了
莫慌,今天小徐我来推荐一个牛X的开源项目。通过简单的讲解,只用不到10行代码,就能实现任意规模数据集的相似搜索。
  0.Faiss是什么?
  1.Faiss如何使用?
  2.项目小结
上车出发。

0. 一个神奇的相似搜索工具


Faiss:集众多算法于一身的相似搜索工具箱
Github Star:12.8K
特点:算法丰富,安全可靠,底层C++实现,提供完整Python接口;部分算法支持GPU加速
项目连接
https://github.com/facebookresearch/faiss
使用文档
https://faiss.ai/
FaceBook在推荐新闻和用户时,使用了自家研究院开发的Faiss库。Faiss是一个高效的相似性搜索(KNN)和密集向量聚类库,包含的算法可以在任意大小的向量集合中进行搜索
官方推荐使用anaconda包和conda指令安装Faiss:
# CPU-only version
$ conda install -c pytorch faiss-cpu

# GPU(+CPU) version
$ conda install -c pytorch faiss-gpu
测试后发现国内会有网络不通畅的问题,易安装失败。这里我推荐另一种方法,使用pip和国内镜像,约1分钟搞定。

# CPU-only version
$ pip install faiss-cpu -i https://pypi.douban.com/simple

# GPU(+CPU) version
$ pip install faiss-gpu -i https://pypi.douban.com/simple

1. Faiss使用教程

Faiss构建索引一般分为基于原始数据构建全量索引,以及加入新的数据构建增量索引。"Train"在某些算法中可以省略,“Add”是必须的。

下面,我以线性搜索和乘积量化算法为例,介绍Faiss的上手方法。

IndexFlatl2:暴力搜索

最简单的方法是基于欧式距离度量的暴力搜索法。
首先,指定特征向量的维度d,通过词向量或预训练语言模型获取原始文本的语义特征。随后,构建 IndexFlatl2 索引对象,并将文本特征加入到索引。属性“ntotal”,可以查看当前索引中到数据规模。
import faiss
d = 768 # embedding 维度
# 构建 IndexFlatL2 索引,它是最简单的索引类型,只执行强力L2距离搜索
faiss_l2_index = faiss.IndexFlatL2(d)
print(faiss_l2_index.is_trained) # True

# 在索引中添加原始特征
# faq_texts_emb 是特征向量,维度 [6417, 768]
faiss_l2_index.add(faq_texts_emb)
print(faiss_l2_index.ntotal) # 查看数据规模:6417
下面开始KNN搜索流程。指定需返回的最相似数据量k,假设现在用户输入了新问题q,通过编码器获得特征向量后,直接调用faiss索引,可以得到原始数据中和q最相似的k个问题,以及相应的空间距离。
# 查找 k 近邻
k = 3
q = '我咋没收到交费的通知呢?'
q_emb = bc.encode([q]) # 获得特征
# dis_res:距离矩阵,由小到大排序
# idx_res:数据索引id
dis_res, idx_res = faiss_l2_index.search(q_emb, k)
print(idx_res.shape, dis_res.shape) # [1,3], [1,3]
为了检验结果是否正确,可以打印变量的值看看:
print(idx_res) # [[107, 111, 75]]
print(dis_res) # [[18.602684 20.919521 21.184694]]
# labels[i] 先映射到训练集文本对应的真实意图 label_id,再转换为 label 标签
print(list(id2name[labels[i]] for i in idx_res[0]))
"""['未收到催费短信', '未收到催费短信', '未收到催费短信']"""
查找的topk标准问题都是“未收到催费短信”,和真实提问“我咋没收到交费的通知呢”一致。

IndexPQ:乘积量化

Faiss调用的套路基本是类似的。再来看另一种更高效的搜索方法,乘积量化。
所谓量化,就是将连续的空间离散化,目的是为了优化距离计算的速度。对原理感兴趣对小伙伴,可以阅读参考文献[1],之后小徐会单独开一篇文章介绍,欢迎关注!
这里先介绍调用方法。乘积量化有一个聚类过程,所以需要显式调用索引对象的“train”方法,再添加向量文件。预测过程,和“暴力搜索”类似。
# m: 特征拆分子空间的数量
m, d = 8768
# 每一个子空间分配的bit大小
n_bits = 8
# 创建乘积量化索引
faiss_pq_index = faiss.IndexPQ(d, m, n_bits)        
# train 阶段
faiss_pq_index.train(faq_texts_emb)
# add 阶段
faiss_pq_index.add(faq_texts_emb)

k = 3
q = '我妹夫家的租户不交电费,这样子会影响我妹夫的信用不?'
q_emb = bc.encode([q])
dis_res, idx_res = faiss_pq_index.search(q_emb, k)
print(idx_res.shape, dis_res.shape) # [1,3], [1,3]
print(idx_res) # [[2563 2575 2547]]
print(dis_res) # [[11.660806 11.660806 11.660806]]
print(list(id2name[labels[i]] for i in idx_res[0]))
"""['租户不交电费是否影响户主信用', '租户不交电费是否影响户主信用', '租户不交电费是否影响户主信用']"""
最终结果和预期一致。
Faiss中还有非常多好用高效的相似搜索方法,例如点积计算IndexFlatIP、改进版乘积量化、加入倒排索引的量化搜索“IndexIVFPQ等等,更多🌰可以查看Github上的tutorial文件夹。
对源码感兴趣又有C++基础的同学,可以从 AutoTone.cpp 文件开始,阅读源码。

2. Faiss 项目总结

回顾Faiss框架,这是一个用于相似检索的算法集大成者。检索目标不仅仅是文本,还包括图片、音频、视频等等多模态形式,只要能将目标转换为特征向量。
除了开源项目,作者还提供了完整的文档体系[2],方便大家快速上手。

如果你的实验或项目中涉及相似匹配任务,不妨尝试一下Faiss框架,毕竟FaceBook自家产品都在用,质量还是有保障滴。如果通过调用接口,顺便弄懂了算法原理,恭喜你,在检索匹配领域你离成为巨佬又近了一步!


深度学习资源下载

在NLP情报局公众号后台回复“三件套”,即可获取深度学习三件套:
《PyTorch深度学习》,《Hands-on Machine Learning》,《Python深度学习》
推 荐 阅 读

参 考 文 献

[1] Faiss-PQ/IVF介绍:https://blog.csdn.net/u013066730/article/details/106252573
[2] Faiss文档:https://faiss.ai/

原创不易,有收获的话请帮忙点击点赞分享在看吧🙏

浏览 28
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报