立即开始使用 ClickHouse Cloud,并获得 300 美元信用额度。要了解有关我们基于用量的折扣的更多信息,请联系我们或访问我们的定价页面。
目录
简介
本篇博文继续我们的向量搜索系列,以上一篇博文为基础,我们在上一篇博文中概述了什么是向量搜索、它与历史的基于倒排索引的方法的关系、它目前交付价值的可能用例以及一些高层次的实现方法。在这篇博文中,我们将通过实际示例详细探讨与 ClickHouse 相关的向量搜索,并回答“我应该在何时使用 ClickHouse 进行向量搜索?”这个问题。
对于我们的示例,我们使用了 ClickHouse Cloud 集群,每个节点总共有 60 个内核和 240GB 的 RAM。但是,这些示例应该可以在同等大小的自管理集群上重现。或者,立即启动您的 ClickHouse Cloud 集群,并获得 300 美元的信用额度。让我们来担心基础设施,您来查询!
我应该在何时使用 ClickHouse 进行向量搜索?
ClickHouse 是一个实时 OLAP 数据库,具有完整的 SQL 支持和广泛的功能,可帮助用户编写分析查询。其中一些函数和数据结构执行向量之间的距离运算,使 ClickHouse 可以用作向量数据库。
由于完全并行化的查询管道,ClickHouse 可以非常快速地处理向量搜索操作,尤其是在通过线性扫描所有行来执行精确匹配时,其处理速度与专用向量数据库相当。
高压缩级别(可通过自定义压缩编解码器进行调整)使存储和查询非常大的数据集成为可能。ClickHouse 不受内存限制,允许查询包含嵌入的多 TB 数据集。
用于计算两个向量之间距离的功能只是另一个 SQL 函数,可以有效地与更传统的 SQL 过滤和聚合功能相结合。这允许向量与元数据甚至富文本一起存储和查询,从而实现广泛的用例和应用。
最后,实验性的 ClickHouse 功能,如近似最近邻 (ANN) 索引,支持更快地近似匹配向量,并提供了一个有希望的发展方向,旨在进一步增强 ClickHouse 的向量匹配能力。
总而言之,当以下任何一项为真时,ClickHouse 是一个有效的向量搜索平台
- 您希望将向量匹配与元数据过滤和/或聚合或连接功能相结合
- 您需要对非常大的向量数据集执行线性距离匹配,并希望在许多 CPU 内核上并行化和分发这项工作,而无需额外的工作或配置
- 您需要匹配的向量数据集的大小使得仅依赖内存索引在成本或硬件可用性方面不可行
- 在查询向量时,您将受益于完整的 SQL 支持
- 您有一个现有的嵌入生成管道,可以生成您的向量,并且不需要此功能成为您的存储引擎的原生功能
- 您已经在 ClickHouse 中拥有相关数据,并且不希望为几百万个向量承担学习另一个工具的开销和成本
- 您主要需要对向量进行快速并行化的精确匹配,并且尚不需要 ANN 的生产实现(目前!)
- 您是一位经验丰富或好奇的 ClickHouse 用户,并且相信我们会改进我们的向量匹配功能,并希望成为这段旅程的一部分
虽然这涵盖了广泛的用例,但在某些情况下,ClickHouse 可能不太适合作为向量存储引擎,您可能希望考虑诸如 Faiss 或专用向量数据库之类的替代方案。如果出现以下情况,那么今天 ClickHouse 作为向量搜索引擎可能提供的优势较少
- 您的向量数据集很小,并且可以轻松放入内存。虽然 ClickHouse 可以轻松完成小型数据集的向量搜索,但在这种情况下,它可能过于强大而没有必要。
- 您的向量没有其他元数据,只需要距离匹配和排序。如果将向量搜索结果与其他元数据连接起来没有用处,并且您的数据集很小,那么如上所述,ClickHouse 可能比您真正需要的更强大。
- 您有非常高的 QPS,大于每秒几千次。通常,对于这些用例,数据集将适合内存,并且需要几毫秒的匹配时间。虽然 ClickHouse 可以服务于这些用例,但一个简单的内存索引可能就足够了。
- 您需要一个解决方案,该解决方案开箱即用地包含嵌入生成功能,其中模型在插入和查询时集成。向量数据库(如 Weaviate)是专门为这种用例设计的,可能更适合这些需求。
考虑到这一点,让我们探索 ClickHouse 的向量功能。
设置示例
LAION 数据集
正如我们在之前的博文中所讨论的那样,向量搜索在嵌入上运行 - 嵌入是表示上下文含义的向量。嵌入是通过将原始内容(如图像或文本)传递到预训练的机器学习模型来生成的。
对于这篇博文,我们使用了预先准备好的一组嵌入,可公开下载,称为 LAION 50 亿测试集。我们选择这个数据集是因为我们认为在撰写本文时,这是可用于测试的最大的预计算嵌入数据集。它由维度为 768 的嵌入组成,用于互联网上的数十亿张公共图像及其标题,通过对互联网的公开抓取生成。它明确地用于大规模测试向量搜索,它还包括元数据,这些元数据反过来可用于说明如何将 ClickHouse 的通用分析功能与向量搜索相结合。
在 LAION 数据集中,已为每个图像及其关联的标题生成了嵌入 - 为每个对象提供了两个嵌入。对于这篇博文,我们仅关注英语子集,该子集由减少后的 22 亿个对象组成。尽管这些对象中的每一个都有两个嵌入,一个用于其图像,一个用于标题,但我们将每对作为 ClickHouse 中的单行存储,总共给了我们近 22 亿行和 44 亿个向量。对于每一行,我们都包含元数据作为列,这些列捕获诸如图像尺寸、图像的相似度和标题嵌入之类的信息。这种相似性(余弦距离)使我们能够识别标题和图像在概念上不一致的对象,从而可能在查询中过滤掉这些对象。
我们要感谢原始作者为整理此数据集并生成用于公共用途的嵌入所做的努力。我们建议阅读完整的生成过程此数据集,该过程克服了许多具有挑战性的数据工程难题,例如在合理的时间内以可接受的成本有效地下载和调整数十亿张图像的大小。
使用 CLIP 模型生成嵌入
这些 LAION 嵌入是使用 ViT-L/14 模型生成的,该模型由 LAION 使用 openCLIP 训练,openCLIP 是 OpenAI 开发的 CLIP 模型的开源实现。这不是一个廉价的过程!在 4 亿张图像上,这花费了大约 30 天,需要 592 个 V100 GPU(在 AWS 按需实例上大约 100 万美元)。
CLIP(对比语言-图像预训练)是一种多模态模型,这意味着它旨在训练多种相关类型的数据,例如图像和关联文本。CLIP 已被证明在学习文本的视觉表示方面非常有效,并在 OCR、地理定位和动作识别方面取得了可喜的成果。对于图像的编码,CLIP 的作者使用了 Resnet50 和 Vision Transformer (ViT),对于文本的编码,使用了类似于 GPT-2 的 Transformer。生成的嵌入表示为两组独立的向量。
训练过程的关键结果是两种数据类型的嵌入具有可比性——如果图像和标题的向量接近,那么它们可以被认为是概念上相似的。像 CLIP 这样的优秀模型将导致图像向量及其关联的标题向量在距离方面具有接近的嵌入,或者对于余弦相似度,值接近 1。下图说明了这一点,其中 T1 是第 1 张图像的标题的嵌入表示,而 I1 是图像本身的编码。这意味着我们希望在训练过程中最大化此矩阵的对角线,其中我们的图像和文本重合。
作为后处理步骤,作者丢弃了与文本标题的余弦相似度小于 0.28 的图像,从而过滤掉了标题和图像不一致的潜在低质量结果。通过图像大小、标题长度、可能的非法性和删除重复项进行进一步过滤,将数据集总数从超过 50 亿减少到 22 亿。
来源:https://openai.com/research/clip
准备要加载的数据
LAION 数据集可以从多个来源下载。选择英语子集后,我们使用了 Hugging Face 托管的版本。此服务依赖于 Git Large File Storage (LFS),这需要安装客户端才能下载文件。安装后,下载数据只需要一个命令。为此,请确保您至少有 20TB 的可用磁盘空间。
git lfs install
git clone https://huggingface.co/datasets/laion/laion2b-en-vit-l-14-embeddings
下载包括三个文件夹;其中两个文件夹包含图像和标题的 npy
格式(实际上是多维数组格式)的嵌入,第三个目录包含 Parquet 文件,其中包含每个图像和标题对的元数据。
ubuntu@ip-172-31-2-70:/data$ ls -l ./laion2b-en-vit-l-14-embeddings
total 456
drwxrwxr-x 2 ubuntu ubuntu 77824 May 16 12:28 img_emb
drwxrwxr-x 2 ubuntu ubuntu 110592 May 16 12:27 metadata
drwxrwxr-x 2 ubuntu ubuntu 270336 May 16 12:28 text_emb
为了将此数据加载到 ClickHouse 中,我们希望为每个嵌入对生成一行,并使用元数据进行丰富。这将需要一个过程,该过程合并每个对象的相应嵌入和元数据。考虑到 ClickHouse 中的向量可以表示为 Float 数组,因此此过程生成的 JSON 行可能如下所示
{
"key": "196060024",
"url": "https://cdn.shopify.com/s/files/1/1194/1070/products/[email protected]?v=1477414012",
"caption": "MERCEDES BENZ G65 RIDE-ON TOY CAR WITH PARENTAL REMOTE | CHERRY",
"similarity": 0.33110910654067993,
"width": "220",
"height": "147",
"original_width": "220",
"original_height": "147",
"status": "success",
"NSFW": "UNLIKELY",
"exif": {
"Image Orientation": "Horizontal (normal)",
"Image XResolution": "72",
"Image YResolution": "72",
"Image ResolutionUnit": "Pixels/Inch",
"Image YCbCrPositioning": "Centered",
"Image ExifOffset": "102",
"EXIF ExifVersion": "0210",
"EXIF ComponentsConfiguration": "YCbCr",
"EXIF FlashPixVersion": "0100",
"EXIF ColorSpace": "Uncalibrated",
"EXIF ExifImageWidth": "220",
"EXIF ExifImageLength": "147"
},
"text_embedding": [
0.025299072265625,
...
-0.031829833984375
],
"image_embedding": [
0.0302276611328125,
...
-0.00667572021484375
]
}
预处理数据集的完整代码可以在此处找到。最终生成的 2313 个 Parquet 文件大约消耗 5.9TB 的磁盘空间。我们已将这些文件合并以生成一个 6TB 的 Parquet 数据集,我们的用户可以s3://datasets-documentation/laion/ 并使用它来重现示例。
在 ClickHouse 中存储向量
将生成的 Parquet 文件加载到 ClickHouse 中需要几个简单的步骤。
架构和加载过程
以下显示了我们的表架构,其中嵌入存储为 Array(Float32)
列。
CREATE TABLE laion
(
`_file` LowCardinality(String),
`key` String,
`url` String,
`caption` String,
`similarity` Float64,
`width` Int64,
`height` Int64,
`original_width` Int64,
`original_height` Int64,
`status` LowCardinality(String),
`NSFW` LowCardinality(String),
`exif` Map(String, String),
`text_embedding` Array(Float32),
`image_embedding` Array(Float32),
`orientation` String DEFAULT exif['Image Orientation'],
`software` String DEFAULT exif['Image Software'],
`copyright` String DEFAULT exif['Image Copyright'],
`image_make` String DEFAULT exif['Image Make'],
`image_model` String DEFAULT exif['Image Model']
)
ENGINE = MergeTree
ORDER BY (height, width, similarity)
exif
列包含我们可以稍后用于过滤和聚合的元数据。为了灵活性和架构简洁性,我们已将其映射为 Map(String,String)
。此列包含超过 100,000 个唯一的元标签。访问子键需要从列中加载所有键,这可能会减慢某些查询的速度,因此我们已使用 DEFAULT
语法将五个感兴趣的属性提取到根目录以供以后分析。对于对可用元属性的完整列表感兴趣的用户,可以使用以下查询来识别可用的 Map 键及其频率
SELECT
arrayJoin(mapKeys(exif)) AS keys,
count() AS c
FROM laion
GROUP BY keys
ORDER BY c DESC
LIMIT 10
我们的架构还包括一个 _file
列,表示生成此数据的原始 Parquet 文件。这使我们能够在插入到 ClickHouse 期间特定文件加载失败时重新启动该加载。
为了将来使用,我们将此数据加载到公共 S3 存储桶中。要将此数据插入到 ClickHouse 中,用户可以执行以下查询
INSERT INTO laion SELECT * FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/laion/*.parquet')
这是一个相当大的数据量要加载,未优化的加载需要几个小时。我们建议用户批量加载过程,以避免网络连接问题等中断。用户可以使用 glob 模式来定位特定子集,例如,s3(https://datasets-documentation.s3.eu-west-3.amazonaws.com/laion/00*.parquet)
。_file
列可用于通过确认 ClickHouse 中的计数与原始 Parquet 文件中的计数来协调任何加载问题。
对于下面的示例,我们创建了各种大小的表,后缀表示行数;例如,laion_100m 包含 1 亿行。这些是使用适当的 glob 模式创建的。
INSERT INTO laion_sample (_file, key, url, caption, similarity, width, height, original_width, original_height, status, NSFW, exif, text_embedding, image_embedding) SELECT
_file,
key,
url,
caption,
similarity,
width,
height,
original_width,
original_height,
status,
NSFW,
exif,
text_embedding,
image_embedding
FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/laion/*.parquet')
压缩的存储性能
ClickHouse 的面向列的结构意味着列的值按顺序排序和写入。磁盘上相同和相似值的聚类通常会导致高压缩率。ClickHouse 甚至提供多种架构和编解码器,允许用户根据其数据的属性调整其配置。对于浮点数数组,由于嵌入的值没有可利用的领域无关属性,因此很难实现高压缩率。利用了完整的 32 位范围,并且对于大多数编解码器,嵌入中相邻值之间的关系是随机的。因此,我们建议使用 ZSTD 编解码器来压缩嵌入。下面我们展示了四个大小递增的表(1m、10m、100m 和 2b 行)中向量列的压缩率。
SELECT
table,
name,
formatReadableSize(sum(data_compressed_bytes)) AS compressed_size,
formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed_size,
round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 2) AS ratio
FROM system.columns
WHERE (table IN ('laion_100m', 'laion_1m', 'laion_10m', 'laion_2b')) AND (name IN ('text_embedding', 'image_embedding'))
GROUP BY
table,
name
ORDER BY table DESC
┌─table──────┬─name────────────┬─compressed_size─┬─uncompressed_size─┬─ratio─┐
│ laion_1m │ text_embedding │ 1.60 GiB │ 2.50 GiB │ 1.56 │
│ laion_1m │ image_embedding │ 1.61 GiB │ 2.50 GiB │ 1.55 │
│ laion_10m │ text_embedding │ 18.36 GiB │ 28.59 GiB │ 1.56 │
│ laion_10m │ image_embedding │ 18.36 GiB │ 28.59 GiB │ 1.56 │
│ laion_100m │ text_embedding │ 181.64 GiB │ 286.43 GiB │ 1.58 │
│ laion_100m │ image_embedding │ 182.29 GiB │ 286.43 GiB │ 1.57 │
│ laion_1b │ image_embedding │ 1.81 TiB │ 2.81 TiB │ 1.55 │
│ laion_1b │ text_embedding │ 1.81 TiB │ 2.81 TiB │ 1.55 │
└────────────┴─────────────────┴─────────────────┴───────────────────┴───────┘
6 rows in set. Elapsed: 0.006 sec.
虽然压缩率通常可以受到主键选择的影响,但 1.56 的恒定压缩率不太可能受到数据排序方式的影响。ZSTD 编解码器的压缩级别可以从其在 ClickHouse Cloud 中的默认值 1 提高。这提供了大约 10% 的改进,在 1000 万行样本中将我们的数据压缩了 1.71
SELECT
table,
name,
formatReadableSize(sum(data_compressed_bytes)) AS compressed_size,
formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed_size,
round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 2) AS ratio
FROM system.columns
WHERE (table IN ('laion_10m_zstd_3')) AND (name IN ('text_embedding', 'image_embedding'))
GROUP BY
table,
name
ORDER BY table DESC
┌─table────────────┬─name────────────┬─compressed_size─┬─uncompressed_size─┬─ratio─┐
│ laion_10m_zstd_3 │ text_embedding │ 16.68 GiB │ 28.56 GiB │ 1.71 │
│ laion_10m_zstd_3 │ image_embedding │ 16.72 GiB │ 28.56 GiB │ 1.71 │
└──────────────────┴─────────────────┴─────────────────┴───────────────────┴───────┘
2 rows in set. Elapsed: 0.026 sec.
请注意,较高的 ZSTD 值会减慢压缩和数据插入速度,但解压缩速度应保持相当恒定(约为 20% 的差异)。
浮点数的压缩是一个研究领域,基于量化的几种有损候选项(例如 SZ 算法)可能是 ClickHouse 的可能补充。其他选项包括将浮点数的精度降低到 16 位。我们在下面的“改进压缩”部分中讨论了这一点。
在 ClickHouse 中搜索向量
正如我们在本系列第 1 部分中所介绍的那样,执行向量搜索意味着将输入向量与向量存储库进行比较,以找到最接近的匹配项。
输入向量表示感兴趣的概念。在我们的例子中,它是编码的图像或标题。向量存储库表示我们希望与之比较的其他图像及其标题。
执行搜索时,将比较向量的邻近度或距离。距离接近的两个向量表示相似的概念。距离最接近的两个向量是集合中最相似的。
选择距离函数
鉴于向量的高维度,有很多方法可以比较距离。这些不同的机制称为距离函数。
ClickHouse 支持广泛的距离函数 - 您可以根据您的用例选择最合适的函数。对于这篇博文,我们重点介绍向量搜索中非常常用的两个函数
- 余弦距离 -
cosineDistance(vector1, vector2)
- 这为我们提供了 2 个向量之间的余弦距离(1 - 余弦相似度)。更具体地说,这测量了两个向量之间角度的余弦,即点积除以长度。这产生一个介于 -1 和 1 之间的值,其中 1 表示两个嵌入是成比例的,因此在概念上是相同的。可以解析列名和输入嵌入以进行向量搜索。如果向量尚未归一化,并且提供了可用于过滤的有界范围,则此函数尤其相关。 - L2 距离-
L2Distance(vector1, vector2)
- 这测量了 2 个点之间的 L2 距离。实际上,这是两个输入向量之间的欧几里得距离,即向量表示的点之间线的长度。距离越小,源对象在概念上就越相似。
这两个函数都计算一个分数,该分数用于比较向量嵌入。对于我们预训练的 CLIP 模型,L2 距离表示基于 官方示例中使用的内部评分的最合适的距离函数。
有关可用距离和向量归一化函数的完整列表,请参见此处。我们很乐意听到您如何利用这些功能来搜索您的嵌入!
生成输入向量
现在我们已经确定了我们将要使用的距离函数,我们需要将输入(我们要搜索的图像或标题)转换为向量嵌入。
这需要我们调用 CLIP 模型。这可以通过一个简单的 Python 脚本轻松实现。有关安装此脚本的依赖项的说明,请参见此处。我们在下面显示了这个脚本
#!/usr/bin/python3
import argparse
from PIL import Image
import clip
import torch
if __name__ == '__main__':
parser = argparse.ArgumentParser(
prog='generate',
description='Generate CLIP embeddings for images or text')
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('--text', required=False)
group.add_argument('--image', required=False)
parser.add_argument('--limit', default=1)
parser.add_argument('--table', default='laion_1m')
args = parser.parse_args()
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"using {device}")
device = torch.device(device)
model, preprocess = clip.load("ViT-L/14")
model.to(device)
images = []
if args.text:
inputs = clip.tokenize(args.text)
with torch.no_grad():
print(model.encode_text(inputs)[0].tolist())
elif args.image:
image = preprocess(Image.open(args.image)).unsqueeze(0).to(device)
with torch.no_grad():
print(model.encode_image(image)[0].tolist())
此版本的脚本接受文本或图像路径作为输入,并将嵌入输出到命令行。请注意,如果存在 CUDA 启用的 GPU,这将利用它们。这可以极大地改变生成时间 - 在 Mac M1 2021 上进行测试时,生成 100 个标题的时间约为 6 秒,而在具有 1 个 GPU 内核的 p3.2xlarge 上则为 1 秒。
例如,让我们将文本“a sleepy ridgeback dog”转换为嵌入。为了简洁起见,我们裁剪了完整的嵌入结果,可以在此处找到。
python generate.py --text "a sleepy ridgeback dog"
[0.5736801028251648, 0.2516217529773712, ..., -0.6825592517852783]
我们现在有一个向量嵌入,它表示文本“a sleepy ridgeback dog”。这是我们的搜索输入向量。我们现在可以将此输入向量与我们的向量嵌入存储库进行比较,以查找在概念上表示相似事物的图像及其标题。
整合在一起
下面的查询搜索概念上相似的嵌入,并按距离对它们进行排序。嵌入存储在 image_embedding
列中。距离存储为 similarity。
我们过滤掉任何大于 0.2 的距离以减少噪声。
SELECT
url,
caption,
L2Distance(image_embedding, [0.5736801028251648, 0.2516217529773712, ..., -0.6825592517852783]) AS score
FROM laion_10m WHERE similarity >= 0.2
ORDER BY score ASC
LIMIT 2
FORMAT Vertical
Row 1:
──────
url: https://thumb9.shutterstock.com/image-photo/stock-photo-front-view-of-a-cute-little-young-thoroughbred-african-rhodesian-ridgeback-hound-dog-puppy-lying-in-450w-62136922.jpg
caption: Front view of a cute little young thoroughbred African Rhodesian Ridgeback hound dog puppy lying in the woods outdoors and staring.
score: 12.262665434714496
Row 2:
──────
url: https://m.psecn.photoshelter.com/img-get2/I0000_1Vigovbi4o/fit=180x180/fill=/g=G0000x325fvoXUls/I0000_1Vigovbi4o.jpg
caption: SHOT 1/1/08 3:15:27 PM - Images of Tanner a three year-old male Vizsla sleeping in the sun on the couch in his home in Denver, Co. The Hungarian Vizsla, is a dog breed originating in Hungary. Vizslas are known as excellent hunting dogs, and also have a level personality making them suited for families. The Vizsla is a medium-sized hunting dog of distinguished appearance and bearing. Robust but rather lightly built, they are lean dogs, have defined muscles, and are similar to a Weimaraner but smaller in size. The breed standard calls for the tail to be docked to two-thirds of its original length in smooth Vizslas and to three-fourths in Wirehaired Vizslas..(Photo by Marc Piscotty/ (c) 2007)
score: 12.265194306913513
2 rows in set. Elapsed: 1.595 sec. Processed 9.92 million rows, 32.52 GB (6.22 million rows/s., 20.38 GB/s.)
结果表明,我们的输入向量“a sleepy ridgeback dog”在概念上与非洲罗德西亚脊背犬的照片最相似,并且在概念上也非常相似于一张正在睡觉的猎犬的图像。
我的狗 Kibo
为了进一步证明这些模型的实用性,作为使用文本搜索的替代方法,我们可以从一张正在睡觉的狗的图像开始,并以这种方式搜索相似的图像。我们生成一个表示这张照片的输入向量,并搜索概念上相似的结果。
为此,我们使用 text_embedding
列重复上面的查询。完整的嵌入可以在此处找到。
python generate.py --image images/ridgeback.jpg
[0.17179889976978302, 0.6171532273292542, ..., -0.21313616633415222]
SELECT
url,
caption,
L2Distance(text_embedding, [0.17179889976978302, ..., -0.21313616633415222]
) AS score
FROM laion_10m WHERE similarity >= 0.2
ORDER BY score ASC
LIMIT 2
FORMAT Vertical
Row 1:
──────
url: https://i.pinimg.com/236x/ab/85/4c/ab854cca81a3e19ae231c63f57ed6cfe--submissive--year-olds.jpg
caption: Lenny is a 2 to 3 year old male hound cross, about 25 pounds and much too thin. He has either been neglected or on his own for a while. He is very friendly if a little submissive, he ducked his head and tucked his tail a couple of times when I...
score: 17.903361349936052
Row 2:
──────
url: https://d1n3ar4lqtlydb.cloudfront.net/c/a/4/2246967.jpg
caption: American Pit Bull Terrier/Rhodesian Ridgeback Mix Dog for adoption in San Clemente, California - MARCUS = Quite A Friendly Guy!
score: 17.90681696075351
2 rows in set. Elapsed: 1.516 sec. Processed 9.92 million rows, 32.52 GB (6.54 million rows/s., 21.45 GB/s.)
为了方便起见,我们提供了一个简单的结果生成器 search.py,它对传递的图像或文本进行编码并执行查询,将查询结果呈现为本地 html 文件。然后,该文件会自动在本地浏览器中打开。下面显示了上述查询的结果文件
python search.py search --image images/ridgeback.jpg --table laion_10m
在这两个示例中,我们都匹配了不同模态的嵌入,即来自图像输入的嵌入与 text_embedding
列匹配,反之亦然。这与前面描述的原始模型训练一致,并且是预期的应用。虽然已经探索了针对相同类型匹配输入嵌入,但之前的尝试导致了好坏参半的结果。
SQL 的优势
通常,在向量搜索的实践中,我们不仅仅跨嵌入进行搜索。通常,将搜索与元数据的过滤或聚合相结合还有额外的实用性。
使用元数据进行过滤
例如,假设我们希望对非版权图像执行向量搜索。这种查询会将向量搜索与基于版权元数据的过滤相结合。
再举一个例子,假设我们只想将搜索限制为大图像 - 至少 300px*500px,并且标题相似度满足更高的 0.3 余弦相似度分数。对于此示例,让我们从搜索“great animal migrations”开始。幸运的是,将其制定为 SQL 查询很简单。下面,我们对 1 亿张图像执行此查询。
SELECT
url,
caption,
L2Distance(image_embedding, [<embedding>]) AS score
FROM laion_100m
WHERE (width >= 300) AND (height >= 500) AND (copyright = '') AND similarity > 0.3
ORDER BY score ASC
LIMIT 10
FORMAT Vertical
Row 1:
──────
url: https://aentcdn.azureedge.net/graphics/items/sdimages/a/500/3/6/5/4/1744563.jpg
caption: Great Migrations
width: 366
height: 500
score: 16.242750635008512
Row 2:
──────
url: https://naturefamiliesdotorg.files.wordpress.com/2017/01/on-the-move.jpg?w=418&h=557
caption: on-the-move
width: 384
height: 512
score: 16.26983713529263
10 rows in set. Elapsed: 2.010 sec. Processed 6.82 million rows, 22.52 GB (3.39 million rows/s., 11.20 GB/s.)
这说明了使用 SQL 和元数据将向量比较限制为子集的好处。在这种特定情况下,我们查询了超过 1 亿个向量,但由于我们的元数据,将实际距离匹配减少到不到 700 万个。
为了方便起见,我们还在 search.py 中添加了传递附加过滤器的功能,使我们能够验证上述匹配的质量
python search.py search --filter "(width >= 300) AND (height >= 500) AND (copyright = '') AND simularity > 0.3" --text "great animal migrations"
使用元数据进行聚合
除了过滤之外,我们还可以对元数据执行聚合。作为面向列的数据库,ClickHouse 非常适合此任务。
例如,假设我们想确定用于“safari pictures”的主要相机型号。我们在此处执行该搜索
WITH results AS
(
SELECT
image_make,
image_model,
L2Distance(image_embedding, [<embedding>]) AS score
FROM laion_100m
WHERE (image_make != '') AND (image_model != '')
ORDER BY score ASC
LIMIT 1000
)
SELECT
image_make,
image_model,
count() AS c
FROM results
GROUP BY
image_make,
image_model
ORDER BY c DESC
LIMIT 10
┌─image_make────────┬─image_model───────────┬──c─┐
│ Canon │ Canon EOS 7D │ 64 │
│ Canon │ Canon EOS-1D X │ 51 │
│ Canon │ Canon EOS 5D Mark III │ 49 │
│ NIKON CORPORATION │ NIKON D700 │ 26 │
│ NIKON CORPORATION │ NIKON D800 │ 24 │
│ Canon │ Canon EOS 5D Mark II │ 23 │
│ NIKON CORPORATION │ NIKON D810 │ 23 │
│ NIKON CORPORATION │ NIKON D7000 │ 21 │
│ Canon │ Canon EOS 40D │ 18 │
│ Canon │ Canon EOS 60D │ 17 │
└───────────────────┴───────────────────────┴────┘
10 rows in set. Elapsed: 23.897 sec. Processed 100.00 million rows, 286.70 GB (4.18 million rows/s., 12.00 GB/s.)
显然,佳能应该是您下次旅行的首选相机。请注意,在这里,我们仅使用前 1000 个结果。与无界的余弦距离不同,欧几里得距离没有上限 - 使施加阈值具有挑战性。
使用倒排索引
注意:倒排索引是 ClickHouse 中的一项实验性功能。
ClickHouse 的实验性二级索引功能也可以证明在向量处理中很有用。
例如,我们可能希望强制执行一个过滤器,将我们的 safari 图片限制为包含狮子的图片。为此,我们可以施加令牌限制 - 要求 caption
列包含字符串 lions
。
如果没有倒排索引,我们的搜索可能如下所示。在这里,我们使用 embedding 用于 以下图片 并针对 1 亿个向量进行搜索。
SELECT url, caption, L2Distance(text_embedding, [<embedding>]) AS score FROM laion_10m WHERE SELECT
url,
caption,
L2Distance(text_embedding, [-0.17659325897693634, …, 0.05511629953980446]) AS score
FROM laion_100m
WHERE hasToken(lower(caption), 'lions')
ORDER BY score ASC
LIMIT 10
FORMAT Vertical
Row 1:
──────
url: https://static.wixstatic.com/media/c571fa_25ec3694e6e04a39a395d07d63ae58fc~mv2.jpg/v1/fill/w_420,h_280,al_c,q_80,usm_0.66_1.00_0.01/Mont%20Blanc.jpg
caption: Travel on a safari to Tanzania, to the rolling plains of the Serengeti, the wildlife-filled caldera of the Ngorongoro Crater and the lions and baobabs of Tarangire; Tanzania will impress you like few other countries will. This tailor-made luxury safari will take you to three very different parks in northern Tanzania, each with their own scenery and resident wildlife. As with all our private tours, this sample itinerary can be completely tailored to create the perfect journey of discovery for you.
score: 18.960329963316692
Row 2:
──────
url: https://thumbs.dreamstime.com/t/jeepsafari-ngorongoro-tourists-photographers-watching-wild-lions-who-walk-jeeps-79635001.jpg
caption: Jeep safari in Ngorongoro3. Tourists and photographers are watching wild lions, who walk between the jeeps Stock Image
score: 18.988379350742093
hasToken(lower(caption), 'lions') ORDER BY score ASC LIMIT 10 FORMAT Vertical
10 rows in set. Elapsed: 6.194 sec. Processed 93.82 million rows, 79.00 GB (15.15 million rows/s., 12.75 GB/s.)
为了加速这种元数据查询,我们可以利用倒排索引,并为 caption
列添加倒排索引。
SET allow_experimental_inverted_index=1
ALTER TABLE laion_100m ADD INDEX caption_idx(lower(caption)) TYPE inverted;
ALTER TABLE laion_100m MATERIALIZE INDEX caption_idx;
重复我们之前的查询,我们可以看到这显着提高了查询时间。倒排索引可用于将距离比较的行数限制为 3000 万行,从而将时间从 6 秒减少到 3 秒。
SELECT url, caption, L2Distance(text_embedding, [<embedding>]) AS score FROM laion_10m WHERE SELECT
url,
caption,
L2Distance(text_embedding, [-0.17659325897693634, ..., 0.05511629953980446]) AS score
FROM laion_100m
WHERE hasToken(lower(caption), 'lions')
ORDER BY score ASC
LIMIT 10
FORMAT Vertical
Row 1:
──────
url: https://static.wixstatic.com/media/c571fa_25ec3694e6e04a39a395d07d63ae58fc~mv2.jpg/v1/fill/w_420,h_280,al_c,q_80,usm_0.66_1.00_0.01/Mont%20Blanc.jpg
caption: Travel on a safari to Tanzania, to the rolling plains of the Serengeti, the wildlife-filled caldera of the Ngorongoro Crater and the lions and baobabs of Tarangire; Tanzania will impress you like few other countries will. This tailor-made luxury safari will take you to three very different parks in northern Tanzania, each with their own scenery and resident wildlife. As with all our private tours, this sample itinerary can be completely tailored to create the perfect journey of discovery for you.
score: 18.960329963316692
Row 2:
──────
url: https://thumbs.dreamstime.com/t/jeepsafari-ngorongoro-tourists-photographers-watching-wild-lions-who-walk-jeeps-79635001.jpg
caption: Jeep safari in Ngorongoro3. Tourists and photographers are watching wild lions, who walk between the jeeps Stock Image
score: 18.988379350742093
10 rows in set. Elapsed: 3.554 sec. Processed 32.96 million rows, 74.11 GB (9.27 million rows/s., 20.85 GB/s.)
此查询的结果如下
python search.py search --image ./images/safari.jpg --table laion_100m --filter "hasToken(lower(caption), 'lions')"
高级功能
近似最近邻 (Annoy)
注意:Annoy 索引在 ClickHouse 中是高度实验性的。
Annoy 索引旨在提高大规模最近邻向量搜索的效率。它在准确性和计算效率之间进行了权衡。
具体来说,Annoy 索引是一种数据结构,用于在高维空间中查找近似最近邻。Annoy 通过将向量组织成树结构来工作。它使用随机超平面(2d 空间中的线,3d 空间中的平面等)将高维空间划分为分区。这些超平面将空间划分为更小的区域,每个区域仅包含数据点的子集。这些分区又用于构建树结构(通常是二叉树),其中每个节点代表一个超平面,子节点代表分割平面的区域。树的叶节点包含实际的数据点。平衡和优化技术(例如,随机化插入和使用启发式方法来确定用于分区的最佳超平面)确保树高效且平衡良好。
一旦构建了 Annoy 索引,就可以将其用于搜索。在提供向量时,可以通过将每个向量与每个内部节点的超平面进行比较来遍历树。在树的每个级别,Annoy 估计查询向量与子节点表示的区域之间的距离。距离度量确定要进一步探索哪个子节点。到达根节点或指定节点后,将返回它遇到的节点集。结果是一组近似结果,其搜索时间可能比线性扫描快得多。
Annoy 分割的超平面的图像
在为 ClickHouse 创建 Annoy 索引时,我们可以同时指定 NumTree 和 DistanceName。后者表示使用的距离函数,默认为 L2Distance
,并且适合我们的 LAION 数据集。前者表示算法将创建的树的数量。树越大,其工作速度就越慢(在 CREATE 和 SELECT 请求中),但您获得的准确性就越高(针对随机性进行调整)。默认情况下,NumTree 设置为 100。
下面,我们显示了 LAION 数据集的架构,其中每个嵌入字段都有一个 Annoy 索引。我们使用索引的默认值,并用 1 亿行填充表。
SET allow_experimental_annoy_index = 1
CREATE TABLE default.laion_100m_annoy
(
`_file` LowCardinality(String),
`key` String,
`url` String,
`caption` String,
`similarity` Float64,
`width` Int64,
`height` Int64,
`original_width` Int64,
`original_height` Int64,
`status` LowCardinality(String),
`NSFW` LowCardinality(String),
`exif` Map(String, String),
`text_embedding` Array(Float32),
`image_embedding` Array(Float32),
`orientation` String DEFAULT exif['Image Orientation'],
`software` String DEFAULT exif['Image Software'],
`copyright` String DEFAULT exif['Image Copyright'],
`image_make` String DEFAULT exif['Image Make'],
`image_model` String DEFAULT exif['Image Model'],
INDEX annoy_image image_embedding TYPE annoy(1000) GRANULARITY 1000,
INDEX annoy_text text_embedding TYPE annoy(1000) GRANULARITY 1000
)
ENGINE = MergeTree
ORDER BY (height, width, similarity)
INSERT INTO laion_100m_annoy SELECT * FROM laion_100m
0 rows in set. Elapsed: 1596.941 sec. Processed 100.00 million rows, 663.68 GB (62.62 thousand rows/s., 415.59 MB/s.)
如图所示,Annoy 索引在插入时的开销很大,上面的插入 1 亿行大约需要 27 分钟。相比之下,没有这些索引的表需要 10 分钟。下面,我们重复了我们的早期查询,该查询大约花费了 24 秒(热)。
SELECT
url,
caption,
L2Distance(image_embedding, [embedding]) AS score
FROM laion_100m_annoy
ORDER BY score ASC
LIMIT 10 FORMAT Vertical
Row 1:
──────
url: https://i.dailymail.co.uk/i/pix/2012/04/26/article-2135380-12C5ADBC000005DC-90_634x213.jpg
caption: Pampered pets: This hammock-style dog bed offers equal levels of pet comfort
score: 12.313203570174357
Row 2:
──────
url: https://i.pinimg.com/originals/15/c2/11/15c2118a862fcd0c4f9f6c960d2638a0.jpg
caption: rhodesian ridgeback lab mix puppy
score: 12.333195649580162
10 rows in set. Elapsed: 1.456 sec. Processed 115.88 thousand rows, 379.06 MB (79.56 thousand rows/s., 260.27 MB/s.)
Annoy 索引在查询性能方面提供了显着改进,此查询花费了 1 到 2 秒,但牺牲了一些搜索质量。
此处的测试嵌入表示我们的“a sleepy ridgeback dog”文本。我们可以在下面看到图像结果。
python search.py search --text "a sleepy ridgeback dog" --table laion_100m_annoy
在 ClickHouse 中,需要注意的是 Annoy 索引可以用于加速查询,这些查询利用了 ORDER BY DistanceFunction(Column, vector)
或 WHERE DistanceFunction(Column, Point) < MaxDistance,
,但不能同时使用两者。查询必须施加 LIMIT,以返回前 N 个匹配项。为了返回最匹配的项,一个基于优先级队列的缓冲区被用于收集匹配的向量。一旦缓冲区满了,收集就会停止,并且缓冲区会被排序。这个缓冲区的大小受设置 max_limit_for_ann_queries
(默认为 1000000) 限制。
用户自定义函数 (UDF)
ClickHouse 的用户自定义函数,或 UDF,允许用户通过创建可以利用 SQL 构造和函数的 lambda 表达式来扩展 ClickHouse 的行为。这些函数可以像查询中的任何内置函数一样使用。
到目前为止,我们一直依赖于在 ClickHouse 之外执行向量生成,并在查询时从我们的 search.py
脚本传递生成的嵌入。虽然这已经足够了,但如果我们能够直接在 SQL 查询中简单地传递文本或图像路径(甚至 url!),那就更好了。
我们可以使用 UDF 来完成这项任务。下面定义的 UDF 分别称为 embedText
和 embedImage
。
SELECT
url,
caption,
L2Distance(image_embedding, embedText('a sleepy ridgeback dog')) AS score
FROM laion_10m
ORDER BY score ASC
LIMIT 10
SELECT
url,
caption,
L2Distance(text_embedding, embedImage("https://dogpictures.com/ridgeback.jpg")) as score
FROM laion_100m
ORDER BY score ASC
LIMIT 10
为了定义 embedText
UDF,我们首先改编了我们早期的 generate.py
,用于生成嵌入,改编成下面的 embed_text.py。
注意:这应该保存在 ClickHouse 的 user_scripts
文件夹中。
#!/usr/bin/python3
import clip
import torch
import sys
device = "cuda" if torch.cuda.is_available() else "cpu"
model, preprocess = clip.load("ViT-L/14", device=device)
if __name__ == '__main__':
for text in sys.stdin:
inputs = clip.tokenize(text)
with torch.no_grad():
text_features = []
text_features = model.encode_text(inputs)[0].tolist()
print(text_features)
sys.stdout.flush()
然后,这个 embed_text.py 脚本可以通过自定义函数 embedText
公开。以下配置可以放置在 ClickHouse 配置目录(默认为 /etc/clickhouse-server/
)下,名称为 embed_text__function.xml
。
注意:用户应确保已为 clickhouse
用户安装此脚本的依赖项 - 请参阅 此处 了解步骤。
<functions>
<function>
<type>executable</type>
<name>embedText</name>
<return_type>Array(Float32)</return_type>
<argument>
<type>String</type>
<name>text</name>
</argument>
<format>TabSeparated</format>
<command>embed_text.py</command>
<command_read_timeout>1000000</command_read_timeout>
</function>
</functions>
注册函数后,我们现在可以像之前的示例中所示的那样使用它
SELECT
url,
caption,
L2Distance(image_embedding, embedText('a sleepy ridgeback dog')) AS score
FROM laion_10m
ORDER BY score ASC
LIMIT 10
对于我们类似的 embedImage
函数,我们基于以下 python 脚本 embed_image.py 添加另一个 UDF。
#!/usr/bin/python3
from io import BytesIO
from PIL import Image
import requests
import clip
import torch
import sys
device = "cuda" if torch.cuda.is_available() else "cpu"
model, preprocess = clip.load("ViT-L/14", device=device)
if __name__ == '__main__':
for url in sys.stdin:
response = requests.get(url.strip())
response.raise_for_status()
image = preprocess(Image.open(BytesIO(response.content))).unsqueeze(0).to(device)
with torch.no_grad():
print(model.encode_image(image)[0].tolist())
sys.stdout.flush()
<functions>
<function>
<type>executable_pool</type>
<name>embedImage</name>
<return_type>Array(Float32)</return_type>
<argument>
<type>String</type>
</argument>
<format>TabSeparated</format>
<command>embed_image.py</command>
<command_read_timeout>1000000</command_read_timeout>
</function>
</functions>
当 UDF 设置为 executable_pool
类型时,ClickHouse 会维护一个预加载的 python 实例池,随时准备接收输入。对于我们的函数,这很有益,因为它减少了首次执行后的模型加载时间。这使得后续调用速度更快。有关如何控制池大小和其他配置参数的更多详细信息,请参阅 此处。
现在两个 UDF 都已配置,我们可以按如下方式查询
SELECT embedImage('https://cdn.britannica.com/12/236912-050-B39F82AF/Rhodesian-Ridgeback-dog.jpg')
...
1 row in set. Elapsed: 13.421 sec.
SELECT embedImage('https://cdn.britannica.com/12/236912-050-B39F82AF/Rhodesian-Ridgeback-dog.jpg')
...
1 row in set. Elapsed: 0.317 sec.
SELECT
url,
caption,
L2Distance(image_embedding, embedImage('https://cdn.britannica.com/12/236912-050-B39F82AF/Rhodesian-Ridgeback-dog.jpg')) AS score
FROM laion_10m
ORDER BY score ASC
LIMIT 10
完成此操作后,我们可以使用 embed_concept.py 脚本和函数 embedConcept
公开我们早期的概念数学能力。
select embedConcept('(berlin - germany) + (uk + bridge)')
SELECT
url,
caption,
L2Distance(image_embedding, embedConcept('(berlin - germany) + (uk + bridge)')) AS score
FROM laion_10m
ORDER BY score ASC
LIMIT 10
请注意,以上示例不包括错误处理和输入验证。我们将其作为读者的练习。希望这些示例为结合用户自定义函数、嵌入模型和向量搜索提供了一些灵感!
改进压缩
增强的压缩技术可能有助于整体数据大小和存储需求。例如,我们之前的模式和生成的压缩统计信息是基于将向量存储为 Array(Float32)
类型。但是,对于某些模型,不需要 32 位浮点精度,并且可以通过将其降低到 16 位来实现类似的匹配质量。
虽然 ClickHouse 没有原生的 16 位浮点类型,但我们仍然可以将精度降低到 16 位并重用 Float32
类型,每个值都简单地用零填充。这些零将通过 ZSTD 编解码器(ClickHouse Cloud 中的标准)有效地压缩,从而减少我们的压缩存储需求。
为了实现这一点,我们需要确保正确完成 16 位浮点值的编码。幸运的是,Google 的 bloat16 类型 用于机器学习用例效果良好,并且只需要截断 32 位浮点数的最后 16 位,假设后者使用 IEE-754 编码。
来源:https://cloud.google.com/tpu/docs/bfloat16
虽然 bfloat16 目前不是 ClickHouse 原生的,但它可以很容易地用其他函数复制。我们对 image_embedding
和 text_embedding
列执行了以下操作。
为此,从表 laion_100m
(包含 1 亿行)中选择所有行,并使用 INSERT INTO SELECT
子句插入到表 laion_100m_bfloat16
中。在 SELECT
期间,我们将嵌入中的值转换为 BFloat16 表示形式。
此 bfloat16 转换是使用 arrayMap
函数实现的,即 arrayMap(x -> reinterpretAsFloat32(bitAnd(reinterpretAsUInt32(x), 4294901760)), image_embedding)
。
这会迭代向量嵌入中的每个值 x
,执行转换 reinterpretAsFloat32(bitAnd(reinterpretAsUInt32(x), 4294901760))
- 这使用函数 reinterpretAsUInt32
将二进制序列解释为 Int32,并使用值 4294901760
执行 bitAnd
。后一个值是二进制序列 000000000000000001111111111111111
。因此,此操作将尾随的 16 位清零,从而执行有效的截断。然后将生成的二进制值重新解释为 float32。
我们在下面说明这个过程
INSERT INTO default.laion_1m_bfloat16 SELECT
_file,
key,
url,
caption,
similarity,
width,
height,
original_width,
original_height,
status,
NSFW,
exif,
arrayMap(x -> reinterpretAsFloat32(bitAnd(reinterpretAsUInt32(x), 4294901760)), text_embedding) AS text_embedding,
arrayMap(x -> reinterpretAsFloat32(bitAnd(reinterpretAsUInt32(x), 4294901760)), image_embedding) AS image_embedding,
orientation,
software,
copyright,
image_make,
image_model
FROM laion_1m
如下所示,这具有将我们的压缩数据减少超过 35% 的效果 - 0s 压缩效果非常好。
SELECT
table,
name,
formatReadableSize(sum(data_compressed_bytes)) AS compressed_size,
formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed_size,
round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 2) AS ratio
FROM system.columns
WHERE (table IN ('laion_100m', 'laion_100m_bfloat16', 'laion_10m', 'laion_10m_bfloat16')) AND (name IN ('text_embedding', 'image_embedding'))
GROUP BY
table,
name
ORDER BY table DESC
┌─table───────────────┬─name────────────┬─compressed_size─┬─uncompressed_size─┬─ratio─┐
│ laion_10m_bfloat16 │ text_embedding │ 13.51 GiB │ 28.46 GiB │ 2.11 │
│ laion_10m_bfloat16 │ image_embedding │ 13.47 GiB │ 28.46 GiB │ 2.11 │
│ laion_10m │ text_embedding │ 18.36 GiB │ 28.59 GiB │ 1.56 │
│ laion_10m │ image_embedding │ 18.36 GiB │ 28.59 GiB │ 1.56 │
│ laion_100m_bfloat16 │ image_embedding │ 134.02 GiB │ 286.75 GiB │ 2.14 │
│ laion_100m_bfloat16 │ text_embedding │ 134.82 GiB │ 286.75 GiB │ 2.13 │
│ laion_100m │ text_embedding │ 181.64 GiB │ 286.43 GiB │ 1.58 │
│ laion_100m │ image_embedding │ 182.29 GiB │ 286.43 GiB │ 1.57 │
└─────────────────────┴─────────────────┴─────────────────┴───────────────────┴───────┘
8 rows in set. Elapsed: 0.009 sec.
由于我们的精度降低到 16 位,因此 ZSTD 压缩级别的进一步提高对我们的压缩 bfloat16 的影响将小于对我们的 32 位表示的影响。如下所示,ZSTD(3) 对我们的压缩 bfloat16 几乎没有影响。
SELECT
table,
name,
formatReadableSize(sum(data_compressed_bytes)) AS compressed_size,
formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed_size,
round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 2) AS ratio
FROM system.columns
WHERE (table IN ('laion_100m_bfloat16', 'laion_100m_bfloat16_zstd_3')) AND (name IN ('text_embedding', 'image_embedding'))
GROUP BY
table,
name
ORDER BY table DESC
┌─table──────────────────────┬─name────────────┬─compressed_size─┬─uncompressed_size─┬─ratio─┐
│ laion_100m_bfloat16_zstd_3 │ text_embedding │ 128.12 GiB │ 286.85 GiB │ 2.24 │
│ laion_100m_bfloat16_zstd_3 │ image_embedding │ 127.28 GiB │ 286.85 GiB │ 2.25 │
│ laion_100m_bfloat16 │ image_embedding │ 133.80 GiB │ 286.75 GiB │ 2.14 │
│ laion_100m_bfloat16 │ text_embedding │ 134.59 GiB │ 286.75 GiB │ 2.13 │
└────────────────────────────┴─────────────────┴─────────────────┴───────────────────┴───────┘
除了减少磁盘空间外,压缩的增加还具有其他潜在的好处。我们通过包含 1000 万行和 1 亿行的表的查询性能来证明这些好处,使用编码为 float32 和 bfloat16 的嵌入。这些结果基于之前使用的 相同查询。
表 | 编码 | 冷启动 (秒) | 热启动 (秒) |
---|---|---|---|
laion_10m | Float32 | 12.851秒 | 2.406秒 |
laion_10m | bloat16 | 7.285秒 | 1.554秒 |
laion_100m | Float32 | 111.857秒 | 24.444秒 |
laion_100m | bloat16 | 71.362秒 | 16.271秒 |
我们在线性扫描速度方面获得的收益是显着的,对于 1 亿行数据集的冷启动查询,bfloat16 变体将我们的性能从 111 秒提高到 71 秒。
一个显而易见的问题可能是,这种精度降低如何影响我们在向量中表示概念的能力,以及它是否会导致搜索质量下降。毕竟,我们减少了多维空间中编码的信息,并有效地将我们的向量“更紧密地”压缩在一起。下面我们展示了使用我们新的 laion_100m_v2
表和我们的 search.py
脚本,对早期的“一只昏昏欲睡的脊背犬”查询的结果。
python search.py search --text "a sleepy ridgeback dog" --table laion_100m_bfloat16
虽然对于此搜索,搜索质量没有明显的下降,但这可能需要在更广泛的查询样本中进行相关性测试。用户需要在他们特定的模型和数据集上测试这种精度降低技术,结果可能会因情况而异。
额外的向量乐趣
在阅读了一篇关于如何使用向量数学在高维空间中移动的 有趣博客文章 后,我们认为看看相同的概念是否可以应用于我们 CLIP 生成的嵌入可能会很有趣。
例如,假设我们有单词 Berlin
、Germany
、United Kingdom
和 Bridge
的嵌入。可以在它们各自的向量上执行以下数学运算。
(berlin - germany) + ('united kingdom' + bridge)
如果我们从逻辑上减去和加上上述概念,我们可以假设结果将代表伦敦的一座桥。
为了测试这个想法,我们增强了我们简单的 search.py
脚本,以支持一个基本的解析器,它可以接受类似于上述的输入。此解析器支持 +
、-
、*
和 /
运算,以及 '
来表示多词输入,并通过 concept_math
命令公开。
感谢出色的 pyparsing
库,为这种语法构建解析器非常简单。总而言之,上述短语将被解析为以下语法树
反过来,我们可以递归地计算上述树中文本术语(叶子)的向量。然后可以使用 ClickHouse 中指定的数学运算符的等效向量函数组合分支。此过程以深度优先的方式执行,将整个树解析为单个查询(应代表等效概念)。
最后,此函数使用与标准搜索相同的过程在 image_embedding
列上匹配。因此,上述内容将解析为以下查询
SELECT url, caption,
L2Distance(image_embedding,
arrayMap((x,y) -> x+y,
arrayMap((x,y) -> x-y, [berlin embedding], [germany embedding]),
arrayMap((x,y) -> x+y, ['united kingdom' embedding], [bridge embedding])
)
) AS score FROM laion_10m ORDER BY score ASC LIMIT 10
请注意,我们使用 arrayMap 函数来下推我们的逐点加法和减法(对作为逐点运算的 +
和 -
运算符的支持 正在考虑中)。
我们在下面显示了此结果,在 1000 万行样本上进行匹配
python search.py concept_math —-text "(berlin - germany) + ('united kingdom' + bridge)"
太酷了!它奏效了!请注意,文本中没有提及伦敦桥 - 第一张图片是 克劳德·莫奈滑铁卢桥 系列画作的一部分。
最后,我们认为增强语法解析器以支持整数常量可能很有用。具体来说,我们想看看两个对比概念之间的中点是否会产生一些有趣的东西。例如,cubism
和 surrealism
概念之间的艺术可能代表什么?这可以用数学方式表示为 (cubism+surrealism)/2
。执行此搜索实际上产生了一些有趣的东西
我们将其留给读者中的艺术家来评论此处的关联性和准确性。
这展示了组合向量的另一种有趣的可能。毫无疑问,在其他情况下,这种基本的向量数学可能很有用。我们很乐意听到任何例子!
结论
在这篇博文中,我们展示了如何将包含 20 亿行的向量数据集转换为 Parquet 格式并加载到 ClickHouse 中。我们已经证明,这压缩效果良好,并且线性搜索可以通过 CPU 进行扩展,并辅以基于完整 SQL 的元数据分析。最后,我们展示了 ClickHouse 的一些较新的 ANN 功能,并探讨了如何使用 UDF 来提供用于生成嵌入的优雅函数。
立即开始使用 ClickHouse Cloud,并获得 300 美元的信用额度。在 30 天试用期结束时,继续使用按需付费计划,或联系我们以了解有关我们基于用量的折扣的更多信息。访问我们的定价页面了解详情。