使用 Nomic Atlas 进行数据探索和过滤

社区文章 发布于 2024 年 3 月 22 日

摘要

在这篇文章中,我将展示如何轻松地在 Nomic Atlas 中可视化多模态数据集。之后,我将探讨如何组合多个投影以在同一张地图上显示它们。最后,我将研究如何跨多个字段(如图片和问题)检测重复项。包含代码的笔记本可在此处获取。

任务

假设您想对某些新数据进行 VLM(例如,MC-LLaVA)的微调。但如果是全新的数据,首先需要分析数据并找出其中可能存在的问题——不正确的数据、重复项等。要做到这一点,您需要能够手动和程序化地处理数据。您可以编写自己的处理管道和可视化工具。根据我的经验,从头开始构建它既困难又非常耗时。前段时间,我发现了一个非常适合此类任务的平台——Nomic Atlas。今天,我将展示如何轻松地在 Atlas 中探索和处理大型多模态数据集,使其更适合训练。

数据集

图片

作为图像来源,我们将使用Unsplash 数据集的精简版,其中包含近 25,000 张可用于商业用途的高分辨率图像。这些图像非常适合我们的需求,因为它们的平均宽度为 4400 像素,平均高度为 4200 像素。

文本

没有与图像相关的文本,我们就无法训练 VLM。不幸的是,数据集本身没有包含太多有用的文本信息。但我们生活在 2024 年,一个强大封闭模型的时代!是的,如果您在 2025 年阅读本文,或者在 2024 年末,或者在发布本文的几个月后阅读,您可以嘲笑我所说的“强大”。无论如何,我们将使用 Gemini Vision 1.0 API 为数据集中的每张图像生成标题和问答对。原始的、未经处理的结果数据集可在此处获取。

处理

文本处理

首先,将数据加载到 Atlas 中。这部分会非常简单,因为 Hugging Face 数据集和 Nomic 库都与 Pandas 兼容。

将数据集转换为数据帧

from datasets import load_dataset

dataset = load_dataset("visheratin/unsplash-caption-questions-init")
df = dataset["train"].to_pandas()

将数据帧加载到 Atlas

from nomic import atlas

nomic_dataset = atlas.map_data(
    data=df,
    id_field='id',
    indexed_field='question',
    identifier="unsplash-synthetic-data-init",
    topic_model=False, # we don't need topics
    duplicate_detection=True, # it is True by default, but we still set it for visibility
)

之后,我们需要等待几分钟,直到所有内容都已索引和映射。然后,我们就可以开始探索数据了!您可以看到地图上有很多小簇和一些奇怪的环。

image/png

颜色表示重复类别。如果您按重复类别进行过滤,您会发现根据生成的问题,有近 17,000 个删除候选!发生了什么?

手动检查单个聚类后,我们发现,在许多情况下,问题确实是相同的。然而,答案并不总是相同,因为图片内容不同。并且标题总是唯一的。我们需要从不同的角度调查我们的数据。

到目前为止,我们只根据问题为数据创建了一个投影。让我们为答案和标题添加两个更多的索引。

nomic_dataset.create_index(
    indexed_field='caption',
    topic_model=False,
    duplicate_detection=True,
)
nomic_dataset.create_index(
    indexed_field='answer',
    topic_model=False,
    duplicate_detection=True,
)

我们处理了所有的文本数据,但如果你打开地图,你只能在用户界面中看到最新生成的投影。我们将很快解决这个问题。在此之前,我们需要图片的嵌入。

关于环境的说明

我们正在进行的这种数据处理有两种标准开发环境。第一种是您自己的计算机。这使您可以完全控制过程,并对数据具有绝对的持久性(除非您的硬盘损坏)。但是,要执行复杂任务(处理大型数据集、运行大型模型),您可能需要比本地可用更多的处理能力。这就是人们开始使用 Google Colab 等基于云的解决方案的原因。只需点击几下,您就可以获得一台具有大量内存和 GPU 的机器,以满足您的所有需求。但是这些环境不是持久的,如果您长时间闲置,它将断开连接,您将不得不重做所有事情。是的,您可以连接 Google Drive 来存储您的数据,但这仍然不是最佳选择。最近,Lightning 发布了其云环境来解决这个特定问题。当您创建工作室时,您将获得一个默认在 CPU 上运行的持久环境。但是,如果您需要 GPU 或更多内存,您可以快速切换环境并继续工作,几乎不会中断!这就是我为这篇文章使用 Lightning Studio 进行数据处理的原因。我花了好几天(更像是几个晚上)的时间进行实验和摸索。我真的很喜欢拥有一个持久的环境,它不会弄乱我的机器,而且我只在需要 GPU 时才使用它。

我个人主要使用 Lightning Studio 进行数据处理和训练实验。与专用 GPU 云提供商(例如 Lambda)相比,完整的多 GPU 或多节点训练对我来说太昂贵了,但我相信 Lightning 在不久的将来会解决这个问题。

图像处理

回到数据处理!2023 年,Google 发布了 SigLIP 模型——一个 CLIP 模型家族,它们体积小但能生成出色的文本和图像嵌入。我们将使用其中一个模型 ViT-B-16-SigLIP 的视觉编码器来嵌入我们的图像。我使用了 OpenCLIP 库中的模型。但如果您愿意,也可以使用 Transformers 中的实现。您可以在笔记本中查看代码。

我们生成了嵌入,为了在地图上显示它们,我们需要再做一步——将维度从 768 减少到 2。我们将分两步完成。首先,我们应用主成分分析将嵌入维度从 768 减少到 20。其次,我们应用 t-SNE 将 30 维转换为 2 维。

from sklearn.decomposition import PCA
from sklearn.manifold import TSNE

pca_embeddings = PCA(n_components=20).fit_transform(embeddings)
tsne_embeddings = TSNE(n_components=2, perplexity=30).fit_transform(pca_embeddings)

关于哪种降维方法及其组合效果最佳,目前没有明确的共识。我发现这种两步法非常适合生成可视化效果。

在继续之前,我们可以检查我们的嵌入效果如何。让我们找到彼此非常相似的图像对。为此,我们首先计算余弦相似度矩阵,然后找到得分高于阈值(在本例中为 0.95)的索引对。

import numpy as np
from sklearn.metrics.pairwise import cosine_similarity

similarity_matrix = cosine_similarity(embeddings)
threshold = 0.95

similar_pairs = np.column_stack(np.where(similarity_matrix > threshold))
unique_pairs = set(tuple(sorted(pair)) for pair in similar_pairs)
filtered_pairs = np.array([list(pair) for pair in unique_pairs if pair[0] != pair[1]])

之后,我们可以可视化一些结果图像对。

image/png

image/png

是的,它们不完全相同,但棕榈树就是棕榈树,云就是云。在没有其他显著细节的情况下,这些图像的信息密度不高。

图像处理的最后一部分是重复项检测。这更多是出于显示目的,因为我们稍后将更具创造性地处理重复项。我们可以使用标准的 DBSCAN 对 t-SNE 嵌入进行分组,然后将最接近聚类中心的点视为“最佳”点(在 Atlas 术语中为“保留候选”),将聚类中的其余点视为“删除候选”,将未聚类的点视为“单例”。

from sklearn.cluster import DBSCAN
import numpy as np
from scipy.spatial import distance

dbscan = DBSCAN(eps=0.5, min_samples=3).fit(tsne_embeddings)

closest_point_indices = set()
for cluster in set(dbscan.labels_):
    if cluster == -1:
        continue
    indices_in_cluster = np.where(dbscan.labels_ == cluster)[0]
    points_in_cluster = tsne_embeddings[indices_in_cluster]
    centroid = points_in_cluster.mean(axis=0)
    distances = distance.cdist([centroid], points_in_cluster, 'euclidean')[0]
    closest_point_index = np.argmin(distances)
    global_index = indices_in_cluster[closest_point_index]
    closest_point_indices.add(global_index)
 
image_classes = []

for i in range(len(tsne_embeddings)):
    if i in closest_point_indices:
        image_classes.append("retention candidate")
    elif dbscan.labels_[i] == -1:
        image_classes.append("singleton")
    else:
        image_classes.append("deletion candidate")

为了以后更方便地处理数据,我们将嵌入和类别编译到数据框中。

image_items = []
for i in range(len(image_ids)):
    image_items.append([image_ids[i], tsne_embeddings[i,0], tsne_embeddings[i,1], image_classes[image_ids[i]]])

image_df = pd.DataFrame.from_records(image_items, columns=["id", "x_image", "y_image", "duplicate_class_image"])

嵌入显示

好的,我们已经有了所有嵌入。现在是时候把所有东西放在一张地图上!Atlas 的一个优点是您可以访问所有生成的坐标和嵌入。以下是如何将所有文本投影的坐标提取到 Pandas 数据帧中。

answer_embeddings_df = nomic_dataset.maps[0].embeddings.df
caption_embeddings_df = nomic_dataset.maps[1].embeddings.df
question_embeddings_df = nomic_dataset.maps[2].embeddings.df

之后,我们将所有数据帧合并在一起。

import pandas as pd

merged_embeddings_df = answer_embeddings_df.merge(caption_embeddings_df, on='id', suffixes=('_answer', '_caption'))
merged_embeddings_df = merged_embeddings_df.merge(question_embeddings_df, on='id')
merged_embeddings_df.rename(columns={'x': 'x_question', 'y': 'y_question'}, inplace=True)

然后,我们从每个文本投影中提取重复信息。

answer_duplicates_df = nomic_dataset.maps[0].duplicates.df
caption_duplicates_df = nomic_dataset.maps[1].duplicates.df
question_duplicates_df = nomic_dataset.maps[2].duplicates.df

重命名列以使其更具可读性

answer_duplicates_df = answer_duplicates_df.rename(
    columns={
        "duplicate_class_at_0,100": "duplicate_class",
        "cluster_id_at_0,100": "cluster_id"
    }
)
caption_duplicates_df = caption_duplicates_df.rename(
    columns={
        "duplicate_class_at_0,100": "duplicate_class",
        "cluster_id_at_0,100": "cluster_id"
    }
)
question_duplicates_df = question_duplicates_df.rename(
    columns={
        "duplicate_class_at_0,100": "duplicate_class",
        "cluster_id_at_0,100": "cluster_id"
    }
)

合并重复数据帧

merged_duplicates_df = answer_duplicates_df.merge(caption_duplicates_df, on='id', suffixes=('_answer', '_caption'))
merged_duplicates_df = merged_duplicates_df.merge(question_duplicates_df, on='id')
merged_duplicates_df.rename(columns={'duplicate_class': 'duplicate_class_question', 'cluster_id': 'cluster_id_question'}, inplace=True)

现在,将所有数据帧合并到一个最终数据帧中。

final_df = pd.merge(df, merged_embeddings_df, on='id')
final_df = pd.merge(final_df, merged_duplicates_df, on='id')
final_df = pd.merge(final_df, image_df, on='id')

我们终于可以提取出显示所需的一切了。

objects_list = final_df.apply(lambda row: {
    "id": row["id"],
    "answer.x": row["x_answer"],
    "answer.y": row["y_answer"],
    "caption.x": row["x_caption"],
    "caption.y": row["y_caption"],
    "question.x": row["x_question"],
    "question.y": row["y_question"],
    "image.x": row["x_image"],
    "image.y": row["y_image"],
    ".url": row["url"], # dots are needed to make sure these fields are displayed at the top
    ".question": row["question"],
    ".answer": row["answer"],
    ".caption": row["caption"],
    "answer_duplicate_class": row["duplicate_class_answer"],
    "caption_duplicate_class": row["duplicate_class_caption"],
    "question_duplicate_class": row["duplicate_class_question"],
    "image_duplicate_class": row["duplicate_class_image"]
}, axis=1).tolist()

并将数据加载到新数据集

combined_dataset = AtlasDataset(
    "unsplash-data-combined",
    unique_id_field="id",
)
combined_dataset.add_data(data=objects_list)
combined_dataset.create_index(
    indexed_field='.question',
    topic_model=False,
    duplicate_detection=True,
)

我们可以前往 Web UI 查看我们的工作成果。要能够更改可视化坐标,您首先需要启用 Beta 功能,然后在地图 Web 界面中启用“自定义映射”。

<video controls autoplay src="

">

很美,不是吗?用户界面让我们能够轻松地浏览数据,找到一些模式,并查看重复项。很明显,数据集中存在重复的问题和非常相似的图像。然而,如果相似的图像伴随着不同的标题或问答对,那并不是一个大问题。但是我们如何搜索这类情况呢?

联合嵌入

为了根据多个属性查找重复项,我们可以简单地连接这些属性的嵌入并对其进行分析。例如,要获取问题和图像的联合嵌入,我们可以使用以下代码。

import numpy as np

question_image_embeddings = final_df.apply(lambda row: np.concatenate((row["embeddings_question"], row["embeddings_image"]), axis=0), axis=1).tolist()

现在我们来看看最新的 Atlas 功能之一——嵌入显示和重复检测。我们不需要自己进行降维和重复检测。我们可以简单地将嵌入加载到新数据集中,并以与原始文本相同的方式进行分析!

from nomic import AtlasDataset

combined_dataset = AtlasDataset(
    "unsplash-data-joint-embeddings",
    unique_id_field="id",
)
combined_dataset.add_data(data=objects_list, embeddings=np.array(image_question_embeddings))
combined_dataset.create_index(
    indexed_field='embeddings',
    topic_model=False,
    duplicate_detection=True,
)

就是这样!现在我们只需要等待几分钟让数据处理完毕,然后我们就可以访问新地图了。

image/png

很明显,重复项比以前少得多,精确地说,是 735 个。这些样本绝对值得删除,因为它们不会为模型贡献任何新内容。您可以尝试不同的字段组合(例如,图像+标题)或重复检测参数。

之后,删除重复项就很容易了。

duplicates_df = combined_dataset.maps[0].duplicates.df
filter_df = duplicates_df[duplicates_df['duplicate_class_at_0,100'] == "deletion candidate"]
mask = final_df['id'].isin(filter_df['id'])
filtered_df = final_df[~mask]

结论

Nomic Atlas 提供了出色的数据探索和分析工具。它能够以编程方式和通过快速响应的 UI 访问数据,这真是太棒了。我没有涵盖其他功能,例如标记,它可以帮助您只需点击几下即可删除大量不良数据。但我希望这篇帖子能帮助您开始使用 Atlas,并且您能够使您的数据集变得更好。

社区

注册登录 发表评论