图像相似度搜索的 5 大技术组件
在本系列博文的第一部分,我们介绍了图像相似度搜索并回顾了可降低复杂性并协助实施的整体架构。本篇博文针对实施图像相似度搜索应用程序所需的每个组件,解释了底层概念和技术考量。深入了解:
- 嵌入模型:Machine Learning 模型,能够为您需要应用矢量搜索的数据生成数字表示
- 推理终端:API,能够将嵌入模型应用到您在 Elastic 内的数据
- 矢量搜索:相似度搜索如何与最近邻搜索搭配使用
- 生成图像嵌入:将数字表示的生成工作扩展至大型数据集
- 应用程序逻辑:交互式前端如何与位于后端的矢量搜索引擎进行通信
深入了解这五大组件后,您脑中就会有一幅蓝图,知道如何通过在 Elastic 中应用矢量搜索来打造更加直观的搜索体验。
1. 嵌入模型
如要对自然语言或图像数据应用相似度搜索,您需要 Machine Learning 模型来将您的数据转换为数据的数字表示,也称为矢量嵌入。在本示例中:
- NLP“转换器”模型将自然语言转换成矢量。
- OpenAI CLIP(对比语言-图像预训练)模型能够对图像进行矢量化。
转换器模型是经过训练的 Machine Learning 模型,能够通过多种方式(例如语言翻译、文本分类,或者命名实体识别 (NER))处理自然语言数据。这些模型会使用由已添加注释的数据构成的极大型数据集进行训练,以了解人类语言的模式和结构。
图像相似度应用程序会寻找匹配给定文本型自然语言描述的图像。如要实施这一类型的相似度搜索,您需要一个符合下列条件的模型:同时使用文本和图像进行过训练,而且能够将文本查询转换为矢量。然后便可使用矢量来寻找相似图像了。
详细了解如何在 Elasticsearch 中上传和使用 NLP 模型 >>
CLIP 是一个由 OpenAI 开发的大型语言模型,可用来处理文本和图像。此模型经过训练,能够基于输入的一小段文本预测图像的文本表示。这涉及学习将图像的视觉和文本表示统一起来,而且所使用的方法要能够允许模型做出准确预测。
CLIP 的另一重要特点是它是一个“零次”模型,该模型可允许它完成未经过具体训练的任务。例如,它可以将训练时未见到过的内容翻译成另一种语言,或将数据划分至训练时未见过的类别。这使得 CLIP 成为一个十分灵活的多用途模型。
您将会使用 CLIP 模型和 Elastic 中的推理终端(如下一部分所述)对大型图像集执行推理(如下面第 3 部分所述),从而完成您的图像的矢量化。
2. 推理终端
POST _ml/trained_models/sentence-transformers__clip-vit-b-32-multilingual-v1/deployment/_infer
{
"docs" : [
{"text_field": "A mountain covered in snow"}
]
}
3. 矢量(相似度)搜索
使用矢量嵌入对查询和文档都完成索引后,相似文档是您的查询在嵌入空间内的最近邻。实现这一结果的一个热门算法是 k 最近邻 (kNN),该算法会寻找与查询矢量最接近的 k 个最近邻。然而,对于您在图像搜索应用程序中通常会处理的大型数据集,kNN 需要特别高的计算资源,而且可能会导致执行时间过长。近似最近邻 (ANN) 搜索这一解决方案应运而生,该方案会牺牲完美准确性,以在高维度嵌入空间中实现大规模高效运行。
在 Elastic 中,_search 终端同时支持准确和近似最近邻搜索。使用下面的代码进行 kNN 搜索。它的假设前提是 your-image-index 中所有图像的嵌入在 image_embedding 字段中均可用。下一部分会讨论您可以如何创建嵌入。
# Run kNN search against <query-embedding> obtained above
POST <your-image-index>/_search
{
"fields": [...],
"knn": {
"field": "image_embedding",
"k": 5,
"num_candidates": 10,
"query_vector": <query-embedding>
}
}
如需详细了解 Elastic 中的 kNN,请参见我们的文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html。
4. 生成图像嵌入
要想在执行图像相似度搜索时实现良好效果,上面所提到的图像嵌入至为关键。应该将它们存储在可容纳图像嵌入的单独索引中,在上面的代码中此索引被称为 you-image-index。此索引包括针对每个图像的一个文档,还包括针对上下文和图像密集矢量(图像嵌入)诠释的字段。图像嵌入代表较低维度空间内的一个图像。相似图像会映射至此空间内的临近点。原始图像可能会有几兆大,具体要取决于分辨率。
生成这些嵌入的具体细节会有所不同。通常而言,这一过程涉及从图像中提取特征,然后使用数学函数将这些特征映射至较低维度空间。通常会使用大型图片数据集来训练这一函数,以了解在较低维度空间内表示这些特征的最佳方式。生成嵌入是一次性任务。
在本篇博文中,我们将使用 CLIP 模型来实现此目的。此模型由 OpenAI 分发,可以作为一个很好的起始点。对于特殊用例,您可能需要训练一个定制嵌入模型来实现预期效果,具体取决于您想分类的图片类型在用于训练 CLIP 模型的面向公众的数据中代表性如何。
Elastic 中的嵌入生成需要在采集时完成,因此需要在搜索之外的进程中完成,步骤如下:
- 加载 CLIP 模型。
- 对于每张图片:
- 加载图片。
- 使用模型评估图片。
- 将生成的嵌入保存到文档中。
- 将文档保存到数据存储/Elasticsearch 中。
这里的伪代码能让这些步骤更具体,您可以在这个示例存储库中获取完整代码。
...
img_model = SentenceTransformer('clip-ViT-B-32')
...
for filename in glob.glob(PATH_TO_IMAGES, recursive=True):
doc = {}
image = Image.open(filename)
embedding = img_model.encode(image)
doc['image_name'] = os.path.basename(filename)
doc['image_embedding'] = embedding.tolist()
lst.append(doc)
...
或者可以参考下图帮助理解:
处理后的文档可能看起来就像下面这样。关键部分是字段 "image_embedding",因为密集矢量表示就存储在这里。
{
"_index": "my-image-embeddings",
"_id": "_g9ACIUBMEjlQge4tztV",
"_score": 6.703597,
"_source": {
"image_id": "IMG_4032",
"image_name": "IMG_4032.jpeg",
"image_embedding": [
-0.3415695130825043,
0.1906963288784027,
.....
-0.10289803147315979,
-0.15871885418891907
],
"relative_path": "phone/IMG_4032.jpeg"
}
}
5. 应用程序逻辑
构建完这些基本组件之后,您最终可以将所有这些都组合到一起,并完成逻辑步骤以实施交互式图像相似度搜索。我们先从概念上开始,看看当您想交互式检索匹配给定描述的图像时,您需要怎么做。
对于文本查询,输入可以简单到只是一个词(例如玫瑰),或者更长的描述(例如“覆盖着雪的山峰”)。 或者您也可以提供一个图像,并要求查找与您的图像相似的图像。
尽管您使用不同的模式来构建查询,但两个模式在底层的矢量搜索方面,都会使用相同的步骤序列来执行,亦即对通过嵌入(作为“密集”矢量)表示的文档使用查询 (kNN)。我们在之前的部分已描述过这一机制,该机制支持 Elasticsearch 对大型图像数据集执行十分迅速且可扩展的必要矢量搜索。请参阅此文档详细了解如何在 Elastic 中微调 kNN 搜索以提高效率。
- 那么您可以如何实施上述逻辑呢?在下面的流程图中,你可以看到信息是如何流动的:用户发出的查询(作为文本或图片)由嵌入模型进行矢量化;使用什么模型取决于输入类型:对于文本描述会使用 NLP 模型,对于图像则为 CLIP 模型。
- 两个模型都会将输入查询转换为其数字表示并将结果存储到 Elasticsearch 中的密集矢量类型中([number, number, number...])。
- 然后便会在 kNN 搜索中使用这一矢量表示来寻找相似矢量(图像),并返回图像作为结果。
推理:对用户查询进行矢量化
后台应用程序会向 Elasticsearch 中的推理 API 发送一个请求。对于文本输入,就像下面这样:
POST _ml/trained_models/sentence-transformers__clip-vit-b-32-multilingual-v1/deployment/_infer
{
"docs" : [
{"text_field": "A mountain covered in snow"}
]
}
对于图像,您可以使用下面的简化代码来通过 CLIP 模型(您需要将此模型提前加载到您的 Elastic Machine Learning 节点中)处理单张图像:
model = SentenceTransformer('clip-ViT-B-32')
image = Image.open(file_path)
embedding = model.encode(image)
您将会收到一个由 Float32 值组成的 512 字节长度的数组,就像下面这样:
{
"predicted_value" : [
-0.26385045051574707,
0.14752596616744995,
0.4033305048942566,
0.22902603447437286,
-0.15598160028457642,
...
]
}
搜索:针对相似图像
两类输入的搜索原理是一样的。发送查询以及 kNN 搜索定义到带图像嵌入的索引 my-image-embeddings。放入来自之前查询的密集矢量 ("query_vector": [ ... ]) 并执行搜索。
GET my-image-embeddings/_search
{
"knn": {
"field": "image_embedding",
"k": 5,
"num_candidates": 10,
"query_vector": [
-0.19898493587970734,
0.1074572503566742,
-0.05087625980377197,
...
0.08200495690107346,
-0.07852292060852051
]
},
"fields": [
"image_id", "image_name", "relative_path"
],
"_source": false
}
来自 Elasticsearch 的响应将会基于我们的 kNN 搜索查询为您提供(作为文档存储在 Elastic 中的)最匹配的图像。
下面的流程图总结了在处理用户查询时您的交互式应用程序所历经的步骤:
- 加载交互式应用程序,它的前端。
- 用户选择一张他们感兴趣的图像。
- 您的应用程序会通过应用 CLIP 模型对图像进行矢量化,并将生成的嵌入作为密集矢量存储起来。
- 应用程序会在 Elasticsearch 中启动一个 kNN 查询,该查询会获取嵌入并返回它的最近邻。
- 您的应用程序会处理响应并呈现一个(或多个)匹配的图像。
现在您已经了解实施交互式图像相似度搜索所需的主要组件和信息流动,您可以查看本系列最后一篇来了解如何成功实现了。您将会获得有关下列内容的分步指导:如何创建应用程序环境,如何导入 NLP 模型,以及如何最终完成图像嵌入的生成。然后您便能使用自然语言搜索图像了 — 无需使用关键字。