Jina AI 始终致力于构建简单、易用、全托管的最佳工具,来帮助开发者快速搭建多模态、跨模态应用。而作为工程师,我们一直在努力开发新的功能和 API,以满足用户对多模态数据处理的诸多场景需要。
Jina 现支持的 Dataclass 新特性提供了更丰富的默认方法支持,大大简化了定义类对象的代码量,代码简洁明晰。本文我将向你介绍 Dataclass 所带来的便利性,为什么要使用它,以及演示如何使用它。
Jina AI 机器学习工程师 Johannes Messner
Dataclass 是一个数据类,顾名思义,数据类只需要关心数据,而和具体行为解耦。Dataclass 是对 Document 更高层次的封装,可以更好地表示一个多模态文档。如图所示,你可以利用装饰器 @dataclass,将左边的多模态文档的信息表示为右边代码片段。
dataclass-example
总的来说,这个新特性会更好地提升开发者的体验,让开发者近乎于使用自然语言般封装自己的数据,并拓展使其成公共可用的服务。减少了对 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 用于保存数据。整体结构如下图所示:
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 结构。
新的 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 等实际数据。
在上面的代码里你可能会奇怪, 为什么我需要调用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 生态系统中重复使用。
process
Dataclass 为处理多模态数据的数据科学家和机器学习工程师提供了极好的表达能力,使他们能够以非常直观的方式表示图像、文本、视频、网格、表格数据。本文只介绍了它的部分功能,还有更多强大的功能等你来探索!
• 自定义数据类型: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
开源:http://Github.com/Jina-ai
私信后台加入vx群