一文读懂Jina生态的Dataclass
Jina AI 始终致力于构建简单、易用、全托管的最佳工具,来帮助开发者快速搭建多模态、跨模态应用。而作为工程师,我们一直在努力开发新的功能和 API,以满足用户对多模态数据处理的诸多场景需要。
Jina 现支持的 Dataclass 新特性提供了更丰富的默认方法支持,大大简化了定义类对象的代码量,代码简洁明晰。本文我将向你介绍 Dataclass 所带来的便利性,为什么要使用它,以及演示如何使用它。作者介绍
Jina AI 机器学习工程师 Johannes Messner
Dataclass 是一个数据类,顾名思义,数据类只需要关心数据,而和具体行为解耦。Dataclass 是对 Document 更高层次的封装,可以更好地表示一个多模态文档。如图所示,你可以利用装饰器 @dataclass,将左边的多模态文档的信息表示为右边代码片段。
总的来说,这个新特性会更好地提升开发者的体验,让开发者近乎于使用自然语言般封装自己的数据,并拓展使其成公共可用的服务。减少了对 DocArray 及其特性的考虑,更多地考虑自己的数据和任务:你可以根据自己的数据来自定义 Dataclass,快速地表示自己的数据,实现自己的任务。此外最重要的是,DocArray 和 Jina 都支持了这个新特性。
在这个新特性的开发过程中,为了保证用户的最佳体验,我们不得不做出一些细微的设计和改进,以提高其效率、可用性和便携性。因此,让我们借此机会回顾一下这些决定,我们做出这些决定的原因,以及我们认为你会喜欢这个新特性的原因。
过去是怎么做的
Document[1] 和 DocumentArray[2] 一直以来都是极其灵活的数据结构,基本上可以容纳任何类型的数据。但是在过去,我们使用的是和其他软件相同的方式,提供开发者需要的所有工具,并告诉开发者如何与这些工具交互,如何让数据去适配这些工具。
举个例子,按照之前的方法,当你想要表示一篇包含多种模态信息的论文,里面包含正文文本、图片、该图片的描述文本、许多参考文献的超链接、以及一些元数据。在之前,我们需要这样建模:
from docarray import Document
# modelling your data as a nested Document
image = Document(uri="myimage.jpg").load_uri_to_image_tensor()
description = Document(text="this is my awesome image")
references = [
Document(uri="https://arxiv.org/abs/2109.08970"),
Document(uri="https://arxiv.org/abs/1706.03762"),
]
reference_doc = Document(chunks=references)
article = Document(
text="this is the main text of the article",
tags={"author": "you", "release": "today"},
chunks=[image, description, reference_doc],
)
我们需要将论文建模为嵌套的 Document,主要数据(如正文和标签)位于顶层,其他数据放在 chunk(块) 级别,每个块都有分别的 Document 用于保存数据。整体结构如下图所示:
author_str = article.tags["author"]
image_tensor = article.chunks[0].tensor
first_reference_uri = article.chunks[2].chunks[0].uri
哦豁😅 突然你不得不考虑块的问题,还有块的块,块的索引,而你想做的只是访问作者、图像和参考文献。显然,这里需要做出改变。
现在:以你的数据为中心
Jina 经典的 Document、DocumentArray 以及它们提供的 API 依然非常好用,但之前存在的问题是:只有开发者才最了解自己的数据,而像块这样的概念可能无法自然地映射到手头的任务里来。
于是,我们提供了 Dataclass!
口说无凭,让我们用 Dataclass 重新建模上文提到的多模态文档:首先定义数据的结构,然后填充数据。
from docarray import Document, DocumentArray, dataclass
from typing import List
from docarray.typing import Image, Text, JSON
# 首先,我们定义数据的结构
@dataclass
class Article:
image: Image
image_description: Text
main_text: Text
metadata: JSON
references: List[Text]
# 接着,我们填充数据进去
article_dataclass = Article(
image="myimage.jpg",
image_description="this is my awesome image",
main_text="this is the main text of the article",
metadata={"author": "you", "release": "today"},
references=["https://arxiv.org/abs/2109.08970", "https://arxiv.org/abs/1706.03762"],
)
article = Document(article_dataclass)
实际上这和人类对世界的看法的方式很相似了,几乎等价于用自然语言去描述这篇多模态文档。换句话说,Dataclase 充当了从现实世界到 DocArray 世界的映射。你几乎可以把它看作是一个非常漂亮的__init__()方法。
与此同时,我们也获得了更清晰的 Document 结构。
以上这些看起来已经相当不错了,但接下来才是真正酷的部分,也是我们最新功能所带来的东西。当我们想在article
访问数据时,我们可以:
image_doc = article.image # returns a Document
image_tensor = article.image.tensor # returns the image tensor
author_str = article.metadata.tags["author"]
first_reference_uri = article.references[0].text
这里是在 DocumentArray 级别访问自定义 Dataclass 的语法:
da = DocumentArray([Document(article_dataclass) for _ in range(3)])
image_docs = da["@.[image]"]
image_and_description_docs = da["@.[image, image_description]"]
image_tensors = image_docs[:, "tensor"]
如图所示,即使将 Dataclass 转换为 Document 或 DocumentArray 之后,你仍然可以根据你自定义的 Dataclass 来推理数据及嵌套数据。此外,现在”块”已经一去不复返了,你可以直接访问 Image、reference 等实际数据。
Document Everywhere
在上面的代码里你可能会奇怪,🤔 为什么我需要调用article.image.tensor
来获取图像向量,调用article.image
不就够了吗?为什么要通过Document
这个中间步骤?
有如下 3 个重要因素,使得必须返回一个完整的 Document,而不只是存储在 Document 里的数据。
1. 灵活性:DocArray 是适用于任何类型数据的数据结构,所以灵活性始终是我们的首要任务之一。因此,我们不是返回特定的数据类型,而是返回一个 Document,因为这是最灵活的数据表示,你可以用它做任何你想做的复杂的任务。
2. Documents Everywhere:DocArray 和 Jina 中的几乎每个操作都将 Document(或 DocumentArray)作为输入,并返回一个作为输出。
3. 更好地融入 Jina 生态 :一旦进入到 Jina 生态,将 Document 作为返回类型就变得至关重要,下文我将详细介绍原因。
从本地到云端
一直以来,DocArray 专注于本地和单片机开发者的体验,Jina 将 DocArray 扩展到云端。到目前为止,我们只讨论了使用 DocArray 进行本地开发,现在让我们把注意力转移到 Jina 的微服务世界。
首先请放心,使用 Document 的 Dataclass 并不受限于运行环境。它可以在任意 Executor 中实现,不管是在你的电脑上,还是分布全球的 Kubernetes 集群里,又或是在 JCloud。
from docarray import Document, dataclass
from docarray.typing import Image, Text
from jina import Executor, Flow, requests
import numpy as np
@dataclass
class Article:
image: Image
description: Text
article_dataclass = Article(image="myimage.jpg", description="this is an awesome image")
article = Document(article_dataclass)
class ImageEncoder(Executor):
@requests(on="/index")
def encode_image(self, docs, *args, **kwargs):
image_tensor = docs[0].image.tensor
docs[0].embedding = np.zeros(image_tensor.shape) # dummy embding
with Flow().add(uses=ImagEncoder) as f:
f.index(inputs=article)
接下来,让我们看看 Dataclass 是如何在一个真正实践的 demo 中发挥作用的。
实际示例
假设我们现在有一些非常简单的文章,由图像和描述组成,我们想要创建这些文章的 embedding。为此,我们先对图像和描述文本分别进行编码,以便将这些向量表示合并成整篇文章的最终 embedding。
from docarray import Document, dataclass
from docarray.typing import Image, Text
from jina import Executor, Flow, requests
import numpy as np
@dataclass
class Article:
image: Image
description: Text
article_dataclass = Article(image="myimage.jpg", description="this is my cool image")
article = Document(article_dataclass)
class ImageEncoder(Executor):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.model = lambda t: np.random.rand(
128
) # initialize dummy image embedding model
@requests(on="/encode")
def encode_image(self, docs, **kwargs):
for d in docs:
image = d.image
image.embedding = self.model(image.tensor)
class TextEncoder(Executor):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.model = lambda t: np.random.rand(
128
) # initialize dummy text embedding model
@requests(on="/encode")
def encode_text(self, docs, **kwargs):
for d in docs:
description = d.description
description.embedding = self.model(description.text)
class EmbeddingCombiner(Executor):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.model = lambda emb1, emb2: np.concatenate(
[emb1, emb2]
) # initialize dummy model to combine embeddings
@requests(on="/encode")
def encode_text(self, docs, **kwargs):
for d in docs:
d.embedding = self.model(d.image.embedding, d.description.embedding)
f = (
Flow()
.add(uses=ImageEncoder, name="ImageEncoder")
.add(uses=TextEncoder, name="TextEncoder", needs="gateway")
.add(uses=EmbeddingCombiner, name="Combiner", needs=["ImageEncoder", "TextEncoder"])
)
with f:
da = f.post(inputs=article, on="/encode")
如代码所示,图片和文本对应的 embedding 存储到了对应的 Document 中。统一的 Document 的格式规范,使得我们在寻找和使用相应 Embedding 时可以说是顺手拈来,开发起来极其舒适。
通用性
看到这里,希望你和我们一样为 Documents 交互的新方式而兴奋。可能你会担心:既然这些 Dataclass 是完全个性化的,那我要怎么确保自己上传到 Jina Hub[3]的 Executor 能供其他社区用户复用,又如何确保能使用由其他社区用户共享的 Executor 呢?
无需担心,DocArray 解决这种问题只是洒洒水。
虽然现在我们依然支持文档级选择器语法 ( d.image
),但更加推荐你使用 DocumentArray 级语法 ( da['@.[image]']
) 来保持最大的互操作性。
代码胜于雄辩!让我们重构上面的 Executor 代码:
class ImageEncoder(Executor):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.model = lambda t: np.random.rand(
len(t), 128
) # initialize dummy image embedding model
@requests(on="/encode")
def encode_image(self, docs, parameters, **kwargs):
path = parameters.get("access_path", "@r")
image_docs = docs[path]
embeddings = self.model(image_docs[:, "tensor"])
image_docs.embeddings = embeddings
class TextEncoder(Executor):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.model = lambda t: np.random.rand(
len(t), 128
) # initialize dummy text embedding model
@requests(on="/encode")
def encode_text(self, docs, parameters, **kwargs):
path = parameters.get("access_path", "@r")
text_docs = docs[path]
embeddings = self.model(text_docs[:, "text"])
text_docs.embeddings = embeddings
class EmbeddingCombiner(Executor):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.model = lambda emb1, emb2: np.concatenate(
[emb1, emb2], axis=1
) # initialize dummy model to combine embeddings
@requests(on="/encode")
def combine(self, docs, parameters, **kwargs):
image_path = parameters.get("image_access_path", "@r")
text_path = parameters.get("text_access_path", "@r")
image_docs = docs[image_path]
text_docs = docs[text_path]
combined_embeddings = self.model(image_docs.embeddings, text_docs.embeddings)
docs.embeddings = combined_embeddings
重写之后,每个连接到这些 Executor 的客户端都可以提供自己的参数,并将access_path
匹配其自定义数据类:
f = (
Flow()
.add(uses=ImageEncoder, name="ImageEncoder")
.add(uses=TextEncoder, name="TextEncoder", needs="gateway")
.add(uses=EmbeddingCombiner, name="Combiner", needs=["ImageEncoder", "TextEncoder"])
)
@dataclass
class Article:
image: Image
description: Text
article_dataclass = Article(image="myimage.jpg", description="this is my cool image")
article = Document(article_dataclass)
with f:
da = f.post(
inputs=article,
on="/encode",
parameters={
"ImageEncoder__access_path": "@.[image]",
"TextEncoder__access_path": "@.[description]",
"Combiner__image_access_path": "@.[image]",
"Combiner__text_access_path": "@.[description]",
},
)
print(da[0].embedding.shape)
这样一来,每个用户都可以定义他们自己的 Dataclass,使用自定义的数据类完成开发任务。并且这些数据类不光在 Executor 是通用的,还可以在整个 Jina 生态系统中重复使用。
下一步等你来探索
• 自定义数据类型:DocArray 为数据类接口提供了许多常见的类型,如 Text、Image、JSON 等。但你也可以定义和使用你自己的类型,包括定义从数据类到 Document ,以及返回的自定义映射。 • 嵌套数据类: 一些复杂的领域需要更复杂的建模。例如,一篇文章实际上可能由多个段落组成,每个段落包含一个图片、一个描述和一个正文文本。你可以通过在文章的 Dataclass 中嵌套一个段落的 Dataclass 的列表来轻松地表示。 • 子索引:在上面的例子中,我们使用 EmbeddingCombiner 为每个 Document 生成顶层 embedding,以用于神经搜索任务。但是对于某些任务,你可能希望不在顶层进行搜索,而是在模式层进行搜索。比如你不希望找到整体上相似的文章,而是希望找到那些有相似图片的文章,这就是子索引能够做到的事情。
引用链接
[1]
Document: https://docarray.jina.ai/fundamentals/document[2]
DocumentArray: https://docarray.jina.ai/fundamentals/documentarray[3]
Jina Hub: https://hub.jina.ai/
加入 J-Tech 交流群
官网:Jina.ai
社区:Slack.jina.ai
开源:Github.com/Jina-ai
扫码加入讨论组