我们日常使用的各种 APP 中的许多功能,都离不开相似度检索技术。比如一个接一个的新闻和视频推荐、各种常见的对话机器人、保护我们日常账号安全的风控系统、能够用哼唱来找到歌曲的听歌识曲,甚至就连外卖配送的最佳路线选择也都有着它的身影。
相信很多同学是第一次听说它,或者只知道它的大名,而不知该如何使用它。本篇文章,我们就来聊聊 faiss,分享这个“黑科技”是如何发挥神奇的“魔法”的。
写在前面
faiss 是相似度检索方案中的佼佼者,是来自 Meta AI(原 Facebook Research)的开源项目[1],也是目前最流行的、效率比较高的相似度检索方案之一。虽然它和相似度检索这门技术颇受欢迎,在出现在了各种我们所熟知的“大厂”应用的功能中,但毕竟属于小众场景,有着不低的掌握门槛和复杂性。
所以,不要想着一口气就完全掌握它,咱们一步一步来。
当然,如果你实在懒得了解,希望能够和写简单的 Web 项目一样,写几行 CRUD 就能够完成高效的向量检索功能,可以试试启动一个 Milvus 实例[2]。或者更懒一些的话,可以试着使用 Milvus 的 Cloud 服务[3],来完成高性能的向量检索。
了解 Faiss 的工作机制和适用场景
在正式使用 faiss 之前,我们需要先了解它的工作机制。
当我们把通过模型或者 AI 应用处理好的数据喂给它之后(“一堆特征向量”),它会根据一些固定的套路,例如像传统数据库进行查询优化加速那样,为这些数据建立索引。避免我们进行数据查询的时候,需要笨拙的在海量数据中进行一一比对,这就是它能够实现“高性能向量检索”的秘密。
我们熟知的互联网企业中比较赚钱的“搜广推”(搜索、广告、推荐)业务中,会使用它解决这些场景下的向量召回工作。在这些场景下,系统需要根据多个维度进行数据关联计算,因为实际业务场景中数据量非常大,很容易形成类似“笛卡尔积”这种变态的结果,即使减少维度数量,进行循环遍历,来获取某几个向量的相似度计算,在海量数据的场景下也是不现实的。
而 Faiss 就是解决这类海量数据场景下,想要快速得到和查询内容相似结果(Top K 个相似结果),为数不多的靠谱方案之一。
和我们在常见数据库里指定字段类型一样, Faiss 也能够指定数据类型,比如 IndexFlatL2、IndexHNSW、IndexIVF 等二十来种类型,虽然类型名称看起来比较怪,和传统的字符串、数字、日期等数据看起来不大一样,但这些场景将能够帮助我们在不同的数据规模、业务场景下,带来出乎意料的高性能数据检索能力。反过来说,在不同的业务场景、不同数据量级、不同索引类型和参数大小的情况下,我们的应用性能指标也会存在非常大的差异,如何选择合适的索引,也是一门学问。(下文会提到)
除了支持丰富的索引类型之外,faiss 还能够运行在 CPU 和 GPU 两种环境中,同时可以使用 C++ 或者 Python 进行调用,也有开发者做了 Go-Faiss ,来满足 Golang 场景下的 faiss 使用。
对 Faiss 有了初步认识之后,我们来进行 Faiss 使用的前置准备。
环境准备
为了尽可能减少不必要的问题,本篇文章中,我们使用 Linux 操作系统作为 faiss 的基础环境,同时使用 Python 作为和 faiss 交互的方式。
在之前的文章中,我介绍过如何准备 Linux 环境 和 Python 环境,如果你是 Linux 系统新手,可以阅读这篇文章,从零到一完成系统环境的准备:《在笔记本上搭建高性价比的 Linux 学习环境:基础篇》[4];如果你不熟悉 Python 的环境配置,建议阅读这篇文章《用让新海诚本人惊讶的 AI 模型制作属于你的动漫视频》[5],参考“准备工作”部分,完成 “Conda” 的安装配置。
在一切准备就绪之后,我们可以根据自己的设备状况,选择使用 CPU 版本的 faiss 还是 GPU 版本的 faiss,以及选择是否要指定搭配固定 CUDA 版本使用:
# 创建一个干净的环境
conda create -n faiss -y
# 激活这个环境
conda activate faiss
# 安装 CPU 版本
conda install -c pytorch python=3.8 faiss-cpu -y
# 或者,安装 GPU 版本
conda install -c pytorch python=3.8 faiss-gpu -y
# 或者,搭配指定 CUDA 版本使用
conda install -c pytorch python=3.8 faiss-gpu cudatoolkit=10.2 -y
在配置安装的时候,推荐使用 3.8 版本的 Python,避免不必要的兼容性问题。在准备好环境之后,我们就能够正式进入神奇的向量数据世界啦。
构建向量数据
前文提到了,适合 faiss 施展拳脚的地方是向量数据的世界,所以,需要先进行向量数据的构建准备。
本文作为入门篇,就先不聊如何对声音(音频)、电影(视频)、指纹和人脸(图片)等数据进行向量数据构建啦。我们从最简单的文本数据上手,实现一个“基于向量检索技术的文本搜索功能”。接下来,我将以我比较喜欢的小说 “哈利波特”为例,你可以根据自己的喜好调整要使用的文本数据。从网络上下载好要处理为向量的文本数据(txt 文档)。
简单针对数据进行 ETL
我这里的原始 TXT 文档尺寸是 3 MB 大小,为了减少不必要的向量转化计算量,我们先对内容进行必要的预处理(数据的 ETL 过程),去掉不必要的重复内容,空行等:
cat /Users/soulteary/《哈利波特》.txt | tr -d ' ' | sed '/^[[:space:]]*$/d' > data.txt
打开文本仔细观察,数据中有一些行中的文本数据格外长,是由好多个句子组成的,会对我们的向量特征计算、以及精准定位检索结果造成影响的。所以,我们还需要进行进一步的内容调整,将多个长句拆成每行一个的短句子。
为了更好的解决句子换行的问题,以及避免将一段人物对话中的多个句子拆散到多行,我们可以使用一段简单的 Node.js 脚本来处理数据:
const { readFileSync, writeFileSync } = require("fs");
const raw = readFileSync("./hp.txt", "utf-8")
.split("\n")
.map((line) => line.replace(/。/g, "。\n").split("\n"))
.flat()
.join("\n")
.replace(/“([\S]+?)”/g, (match) => match.replace(/\n/g, ""))
.replace(/“([\S\r\n]+?)”/g, (match) => match.replace(/[\r\n]/g, ""))
.split("\n")
.map((line) => line.replace(/s/g, "").trim().replace(/s/g, "—"))
.filter((line) => line)
.join("\n");
writeFileSync("./ready.txt", raw);
我们执行 node .
将文本处理完毕之后,当前文件夹中将出现一个名为 ready.txt
的文本文件。
为了方便后文中,我们更具象地了解向量数据库的资源占用,我们顺手查看下整理好的文本文件占磁盘空间是多少:
du -hs ready.txt
5.5M ready.txt
使用模型将文本转换为向量
为了将文本转换为向量数据,我们需要使用能够处理文本嵌入的模型。我这里选择的模型是来自人大、腾讯 AI Lab、北大(按论文作者顺序)联合推出的《UER: An Open-Source Toolkit for Pre-training Models》预训练模型。
关于这个预训练模型的相关资料:
-
HuggingFace https://huggingface.co/uer/sbert-base-chinese-nli -
训练数据 https://github.com/liuhuanyong/ChineseTextualInference/
想要使用模型,我们需要先安装一些 Python 的基础软件包:
pip install sentence_transformers pandas
在依赖安装完毕之后,我们可以在终端中输入 python
来进入 Python 交互式终端,首先将我们准备好的文本文件使用 pandas
解析为 DataFrames 。
import pandas as pd
df = pd.read_csv("ready.txt", sep="#",header=None, names=["sentence"])
print(df)
在执行之后,我们将看到类似下面的结果:
sentence
0 《哈利波特》J.K罗琳
1 第一部 第一章 幸存的男孩
2 住在四号普里怀特街的杜斯利先生及夫人非常骄傲地宣称自己是十分正常的人。
3 但是他们最不希望见到的就是任何奇怪或神秘故事中的人物因为他们对此总是嗤之以鼻。
4 杜斯利先生是一家叫作格朗宁斯的钻机工厂的老板。
... ...
60023 哈利看着她茫然地低下头摸了摸额头上闪电形的伤疤。
60024 “我知道他会的。”
60025 十九年来哈利的伤疤再也没有疼过。
60026 一切都很好。
60027 (全书完)
[60028 rows x 1 columns]
接下来,我们对载入内存的文本进行向量计算,对每一行数据进行“特征向量抽取”:
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('uer/sbert-base-chinese-nli')
sentences = df['sentence'].tolist()
sentence_embeddings = model.encode(sentences)
这个过程会比较久,消耗时间将会和你的电脑性能相关,我这边使用一台 Zen2 的普通笔记本,大概需要运行接近半个小时,所以这个时间不妨站起来动一动,缓解一天的疲劳。
当数据向量完毕之后,我们可以先执行 sentence_embeddings.shape
,看看数据的状况:
(60028, 768)
执行完毕,我们将看到类似上面的结果,有六万条文本被向量化为了 768 维的向量数据。
最后
我们已经搞定了“向量数据”,下一篇内容中,我们将一起了解如何使用 Faiss 来实现向量相似度检索功能。
相关链接
[1]https://github.com/facebookresearch/faiss
[2]https://github.com/milvus-io/milvus
[3]https://zilliz.com/cloud
[4]https://soulteary.com/2022/06/21/building-a-cost-effective-linux-learning-environment-on-a-laptop-the-basics.html
[5]https://soulteary.com/2022/06/04/create-your-own-anime-video-with-an-ai-model-that-surprised-makoto-shinkai.html
Zilliz 是向量数据库系统领域的开拓者和全球领先者,研发面向 AI 生产系统的向量数据库系统。Zilliz 以发掘非结构化数据价值为使命,致力于打造面向 AI 应用的新一代数据库技术,帮助企业便捷地开发 AI 应用。Zilliz 的产品能显著降低管理 AI 数据基础设施的成本,帮助 AI 技术赋能更多的企业、组织和个人。