# 可视化数据集浏览器

现代数据集通常包含数百万个非结构化数据,如图像、音频片段甚至视频。在这样的数据集中查询最近邻是具有挑战性的:1. 对非结构化数据进行距离度量是模糊的;2. 对数十亿个距离进行排序也需要额外的努力。幸运的是,最近的研究(如CLIP (opens new window))已经消除了第一个障碍,而先进的向量搜索算法可以提高第二个障碍。MyScale提供了一个统一的数据库解决方案,用于DB+AI应用程序,实现在大型数据集中的高性能搜索。在本示例中,我们将演示如何使用硬负采样技术训练一个细粒度分类器来构建一个DB+AI应用程序。

在这个演示中,我们采用了unsplash-25k数据集 (opens new window),这是一个包含大约2.5万张图像的数据集,其中的照片涵盖了复杂的场景和物体。

# 为什么我们要使用数据库?

对于那些问数据库的作用的人,我需要深入一些AI的内容。我们都知道,传统的分类器在现实生活中要求大量的数据、注释和训练技巧才能获得高准确性。这些是足够的,但实际上并不是必需的来获得准确的分类器。更接近的猜测将帮助我们更快地达到最优解。由于CLIP的出现,我们现在可以获得一个良好的分类器的起点。我们只需要关注那些相似但不完全相同的例子,这在AI术语中称为硬负采样技术。现在是一个向量数据库(如MyScale)发光的时候了。

MyScale是一个支持在数十亿个向量中进行高性能搜索的向量数据库。像硬负采样这样的昂贵操作将不会成为使用MyScale进行AI应用和研究的障碍。找到硬负采样只需要几毫秒的时间。因此,整个微调过程只需要在网页上点击几下即可完成。

# 如何使用演示?

为什么不试试我们的在线演示 (opens new window)

# 安装先决条件

  • transformers:运行CLIP模型
  • tqdm:为人类提供美观的进度条
  • clickhouse-connect:MyScale数据库客户端
  • streamlit:用于运行应用程序的Python Web服务器
python3 -m pip install transformers tqdm clickhouse-connect streamlit pandas lmdb torch

如果您想构建自己的数据库,可以下载元数据。

# 下载Unsplash 25K数据集
wget https://unsplash-datasets.s3.amazonaws.com/lite/latest/unsplash-research-dataset-lite-latest.zip
# 解压缩...
unzip unsplash-research-dataset-lite-latest.zip
# 您将在当前工作目录中找到一个名为`photos.tsv000`的文件
# 然后您可以从数据集中提取CLIP特征

# 使用向量构建数据库

# 进入数据

首先,让我们看一下Unsplash-25k数据集的结构。文件photos.tsv000包含数据集中所有图像的元数据和注释。它的一行如下所示:

photo_id photo_url photo_image_url ...
xapxF7PcOzU https://unsplash.com/photos/xapxF7PcOzU https://images.unsplash.com/photo-1421992617193-7ce245f5cb08 ...

第一列是该图像的唯一标识符。下一列是指向其描述页面的URL,其中包含其作者和其他元信息。第三列包含图像的URL。图像URL可以直接用于使用unsplash API (opens new window)检索图像。这是上面提到的photo_image_url列的示例:

特别感谢和照片作者Timothy Kolczak

因此,我们使用以下代码加载数据:

import pandas as pd
from tqdm import tqdm
images = pd.read_csv(args.dataset, delimiter='\t')

# 创建MyScale数据库表

# 使用数据库

您需要与数据库后端建立连接,以在MyScale中创建表。您可以在此页面上查看有关Python客户端的详细指南。

如果您熟悉SQL(结构化查询语言),那么在MyScale中使用它将更加容易。MyScale将结构化查询与向量搜索相结合,这意味着创建向量数据库与创建传统数据库完全相同。下面是我们在SQL中创建向量数据库的方法:

CREATE TABLE IF NOT EXISTS unsplash_25k(
        id String,
        url String,
        vector Array(Float32),
        CONSTRAINT vec_len CHECK length(vector) = 512
        ) ENGINE = MergeTree ORDER BY id;

我们将图像的id定义为字符串,url定义为字符串,特征向量vector定义为具有32位浮点数数据类型和512维的固定长度数组。换句话说,图像的特征向量包含512个32位浮点数。我们可以使用刚刚创建的连接执行此SQL:

client.command(
"CREATE TABLE IF NOT EXISTS unsplash_25k (\
        id String,\
        url String,\
        vector Array(Float32),\
        CONSTRAINT vec_len CHECK length(vector) = 512\
) ENGINE = MergeTree ORDER BY id")

# 提取特征并填充数据库

CLIP (opens new window)是一种流行的方法,可以将不同形式(或我们采用的学术术语“模态”)的数据匹配到一个统一的空间中,实现高性能的跨模态检索。例如,您可以使用短语“一张湖边的房子的照片”的特征向量来搜索相似的照片,反之亦然。

通过几个硬负采样步骤,可以使用零样本分类器作为初始化来训练一个准确的分类器。我们可以将从文本中生成的CLIP向量作为分类器的初始参数。然后,我们可以继续进行硬负采样部分:搜索所有相似的样本并排除所有负样本。下面是一个代码示例,演示如何从单个图像中提取特征:

from torch.utils.data import DataLoader
from transformers import CLIPProcessor, CLIPModel
model_name = "openai/clip-vit-base-patch32"
# 您可能需要几分钟来下载CLIP模型
model = CLIPModel.from_pretrained(model_name).to(device)
# 处理器将预处理图像
processor = CLIPProcessor.from_pretrained(model_name)
# 使用刚刚加载的数据
row = images.iloc[0]
# 获取图像的URL和唯一标识符
url = row['photo_image_url']
_id = row['photo_id']
import requests
from io import BytesIO
# 下载图像并加载它
response = requests.get(url)
img = Image.open(BytesIO(response.content))
# 预处理图像并返回一个PyTorch张量
ret = self.processor(text=None, images=img, return_tensor='pt')
# 获取图像值
img = ret['pixel_values']
# 获取特征向量(float32,512d)
out = model.get_image_features(pixel_values=img)
# 在插入到数据库之前对向量进行归一化
out = out / torch.norm(out, dim=-1, keepdims=True)

到目前为止,我们已经收集到了构建表所需的所有数据。这个谜题中只剩下一块拼图:将数据插入到MyScale中。有关详细的INSERT子句用法,请参阅SQL参考

# 将单行插入表的示例
# 您需要将特征向量转换为Python列表
transac = [_id, url, out.cpu().numpy().squeeze().tolist()]
# 将向量插入数据库
client.insert("unsplash_25k", transac)

# 少样本学习分类器

# 初始化分类器参数

如上所述,我们可以使用文本特征来初始化我们的分类器。

from transformers import CLIPTokenizerFast, CLIPModel
# 初始化分词器
tokenizer = CLIPTokenizerFast.from_pretrained(model_name)
# 输入任何您想要搜索的内容
prompt = 'a house by the lake'
# 获取分词后的提示和其特征
inputs = tokenizer(prompt, return_tensors='pt')
out = model.get_text_features(**inputs)
xq = out.squeeze(0).cpu().detach().numpy().tolist()

有了文本特征向量,我们可以获得我们所需图像的近似质心,这将是分类器的初始参数。因此,可以定义一个分类器类如下:

DIMS = 512
class Classifier:
    def __init__(self, xq: list):
        # 使用DIMS输入大小和1个输出初始化模型
        # 注意,偏置被忽略,因为我们只关注内积结果
        self.model = torch.nn.Linear(DIMS, 1, bias=False)
        # 将初始查询`xq`转换为张量参数以初始化权重
        init_weight = torch.Tensor(xq).reshape(1, -1)
        self.model.weight = torch.nn.Parameter(init_weight)
        # 初始化损失和优化器
        self.loss = torch.nn.BCEWithLogitsLoss()
        self.optimizer = torch.optim.SGD(self.model.parameters(), lr=0.1)

回顾一下, for , and ,带有激活函数的线性分类器与CLIP的相似度度量完全相同——进行一个映射的简单内积。因此,您可以将输出视为给定输入向量和决策向量的相似度分数。当然,您可以使用与查询图像接近的文本特征作为分类器的初始参数。此外,BCEwithLogitsLoss(二元交叉熵损失)用于将负样本推开并将决策向量拉向正样本。这将给您对AI部分发生的情况有直观的了解。

# 使用数据库获取相似样本

最后,我们使用MyScale的相似性搜索功能对分类器进行硬负采样。但首先,我们需要告诉数据库使用哪种度量来衡量特征的相似性。

MyScale提供了许多不同的算法来加速各种度量的搜索。支持常见的度量如L2cosineIP。在本示例中,我们遵循CLIP的设置,选择余弦距离作为我们的度量来搜索最近邻,并使用一种名为MSTG的近似最近邻搜索算法来索引我们的特征。

-- 我们在向量列上创建一个名为vindex的向量索引
-- 使用`metric`和`ncentroids`参数
ALTER TABLE unsplash_25k ADD VECTOR INDEX vindex vector TYPE MSTG('metric_type=Cosine')

一旦构建了向量索引,我们现在可以使用运算符distance执行最近邻检索。

-- 请注意,在执行之前,查询向量应转换为字符串
SELECT id, url, vector, distance(vector, <query-vector>) AS dist FROM unsplash_25k ORDER BY dist LIMIT 9

请注意:对于返回值的任何SQL动词(如SELECT),您需要使用client.query()来检索结果。

您还可以执行一个混合查询,过滤掉不需要的行:

SELECT id, url, vector, distance(vector, <query-vector>) AS dist
        FROM unsplash_25k WHERE id NOT IN ('U5pTkZL8JI4','UvdzJDxcJg4','22o6p17bCtQ', 'cyPqQXNJsG8')
        ORDER BY dist LIMIT 9

假设我们将上述SQL语句命名为字符串qstr,那么在Python中可以这样进行查询:

q = client.query(qstr).named_results()

返回的q具有多个类似字典的对象。在这种情况下,我们有9个返回对象,因为我们请求了前9个最近邻。我们可以使用列名从q的每个元素中检索值。例如,如果我们想要获取所有id及其与查询向量的距离,我们可以在Python中这样编写代码:

id_dist = [(_q["id"], _q["dist"]) for _q in q]

# 对分类器进行微调

有了MyScale的强大功能,我们现在可以在眨眼之间检索数据库中的最近邻。这个应用程序的最后一步将是根据用户的监督对分类器进行微调。

我将省略UI设计步骤,因为在这篇博客中写太多叙述性的内容会很冗长 😛 当模型训练发生时,我将直接进入重点。

# 注意:请将此代码添加到前面的分类器中
def fit(self, X: list, y: list, iters: int = 5):
# 将X和y转换为张量
X = torch.Tensor(X)
y = torch.Tensor(y).reshape(-1, 1)
for i in range(iters):
    # 清除梯度
    self.optimizer.zero_grad()
    # 在推理之前对权重进行归一化
    # 这将约束梯度,否则查询向量会爆炸
    self.model.weight.data = self.model.weight.data / torch.norm(self.model.weight.data, p=2, dim=-1)
    # 前向传播
    out = self.model(X)
    # 计算损失
    loss = self.loss(out, y)
    # 反向传播
    loss.backward()
    # 更新权重
    self.optimizer.step()

上面的代码为您提供了一个少样本学习的流水线,用于训练现有的分类器。只需对少数图像进行注释,分类器就可以收敛并给出令人印象深刻的准确性,以符合您心中的概念。

训练过程是微不足道的。首先,我们回顾一下权重向量通常是衡量查询和所需图像之间相似性的指标。您可以将其视为一个锥体的质心,其中分类器参数是其方向向量,分数阈值是其半径。锥体内的所有内容都将被视为正样本,而外部的内容则为负样本。训练步骤将使向量尽可能多地覆盖正样本,并远离负样本。继续锥体向量理论,我们只需要一个归一化的向量来描述锥体的质心。因此,我们需要在每次迭代后对学习到的参数进行归一化。我们还可以以另一种方式思考:正样本是未归一化的向量,它们将质心拉向它们的位置,我们可能最终得到一个在幅度上非常长但在正样本之间描述方向较差的向量。这将降低相似性搜索的性能。将向量归一化将仅保留梯度的垂直分量。这将稳定我们演示中的视觉结果。

# 最后

在这个演示中,我们回顾了如何使用MyScale构建一个训练少样本学习分类器的演示。更重要的是,我们还介绍了如何使用MyScale存储、索引和使用扩展SQL进行搜索的高级向量搜索引擎。希望您喜欢这篇博客!

参考资料:

  1. 多语言CLIP: https://huggingface.co/M-CLIP/XLM-Roberta-Large-Vit-B-32 (opens new window)
  2. CLIP: https://huggingface.co/openai/clip-vit-base-patch32 (opens new window)
  3. Unsplash 25K数据集: https://github.com/unsplash/datasets (opens new window)