多模态数据的结构化工具
注意
您当前查看的 README 适用于 DocArray>0.30,与 DocArray 0.21 相比引入了重大更改。如果您希望继续使用旧版 DocArray <=0.21,请通过pip install docarray==0.21安装。有关更多信息,请参阅其代码库、文档以及其热修复分支。
DocArray 是一个专为多模态数据的表示、传输、存储和检索而设计的 Python 库。它为多模态 AI 应用的开发量身定制,其设计确保了与广泛的 Python 和机器学习生态系统的无缝集成。自2022年1月起,DocArray 在 Apache License 2.0 下公开发布,并目前是 LF AI & Data Foundation 的沙盒项目。
通过命令行安装 DocArray,请运行以下命令:
pip install -U docarray
注意
要使用 DocArray <=0.21,请确保通过pip install docarray==0.21安装,并查看其代码库、文档和其热修复分支。
DocArray 新手?根据您的使用场景和背景,有多种方式了解 DocArray:
DocArray 使您能够以与机器学习天然契合的方式表示数据。
这在多种场景下尤其有益:
:bulb: 熟悉 Pydantic? 您会很高兴地了解到 DocArray 不仅构建在 Pydantic 之上,而且与它完全兼容!此外,我们还有专门针对您需求的章节!
本质上,DocArray 以类似于 Python 数据类的方式促进数据表示,机器学习是其核心组成部分:
from docarray import BaseDoc
from docarray.typing import TorchTensor, ImageUrl
import torch
# 定义您的数据模型
class MyDocument(BaseDoc):
description: str
image_url: ImageUrl # 也可以是 VideoUrl、AudioUrl 等
image_tensor: TorchTensor[1704, 2272, 3] # 您可以表达张量形状!
# 在 DocVec 中堆叠多个文档
from docarray import DocVec
vec = DocVec[MyDocument](
[
MyDocument(
description="A cat",
image_url="https://example.com/cat.jpg",
image_tensor=torch.rand(1704, 2272, 3),
),
]
* 10
)
print(vec.image_tensor.shape) # (10, 1704, 2272, 3)
让我们仔细看看如何使用 DocArray 表示数据:
from docarray import BaseDoc
from docarray.typing import TorchTensor, ImageUrl
from typing import Optional
import torch
# 定义您的数据模型
class MyDocument(BaseDoc):
description: str
image_url: ImageUrl # 也可以是 VideoUrl、AudioUrl 等
image_tensor: Optional[
TorchTensor[1704, 2272, 3]
] = None # 也可以是 NdArray 或 TensorflowTensor
embedding: Optional[TorchTensor] = None
因此,您不仅可以定义数据的类型,甚至可以指定张量的形状!
# 创建一个文档
doc = MyDocument(
description="This is a photo of a mountain",
image_url="https://upload.wikimedia.org/wikipedia/commons/2/2f/Alpamayo.jpg",
)
# 从 URL 加载图像张量
doc.image_tensor = doc.image_url.load()
# 使用您选择的任何模型计算嵌入
def clip_image_encoder(image_tensor: TorchTensor) -> TorchTensor: # 虚拟函数
return torch.rand(512)
doc.embedding = clip_image_encoder(doc.image_tensor)
print(doc.embedding.shape) # torch.Size([512])
当然,您可以将文档组合成嵌套结构:
from docarray import BaseDoc
from docarray.documents import ImageDoc, TextDoc
import numpy as np
class MultiModalDocument(BaseDoc):
image_doc: ImageDoc
text_doc: TextDoc
doc = MultiModalDocument(
image_doc=ImageDoc(tensor=np.zeros((3, 224, 224))), text_doc=TextDoc(text='hi!')
)
您很少一次只处理单个数据点,尤其是在机器学习应用中。因此,您可以轻松地收集多个 Document:
Document在构建或与 ML 系统交互时,通常希望一次处理多个 Document(数据点)。
DocArray 为此提供了两种数据结构:
DocVec:一个 Document 的向量。文档中的所有张量被堆叠为单个张量。非常适合批量处理和在 ML 模型内部使用。DocList:一个 Document 的列表。文档中的所有张量都保持原样。非常适合数据流、重排序和洗牌。让我们从 DocVec 开始看:
from docarray import DocVec, BaseDoc
from docarray.typing import AnyTensor, ImageUrl
import numpy as np
class Image(BaseDoc):
url: ImageUrl
tensor: AnyTensor # 允许 torch、numpy 和 tensor flow 张量
vec = DocVec[Image]( # DocVec 由您的个人模式参数化!
[
Image(
url="https://upload.wikimedia.org/wikipedia/commons/2/2f/Alpamayo.jpg",
tensor=np.zeros((3, 224, 224)),
)
for _ in range(100)
]
)
在上面的代码片段中,DocVec 由您希望与其一起使用的文档类型参数化:DocVec[Image]。
这可能起初看起来有些奇怪,但我们相信您会很快习惯!此外,它让我们可以做很酷的事情,比如对您在文档中定义的字段进行批量访问:
tensor = vec.tensor # 获取 DocVec 中的所有张量
print(tensor.shape) # 它们被堆叠成一个张量!
print(vec.url) # 您也可以批量访问任何其他字段
第二种数据结构 DocList 的工作方式类似:
from docarray import DocList
dl = DocList[Image]( # DocList 由您的个人模式参数化!
[
Image(
url="https://upload.wikimedia.org/wikipedia/commons/2/2f/Alpamayo.jpg",
tensor=np.zeros((3, 224, 224)),
)
for _ in range(100)
]
)
您仍然可以批量访问文档的字段:
tensors = dl.tensor # 获取 DocList 中的所有张量
print(type(tensors)) # 作为张量列表
print(dl.url) # 您也可以批量访问任何其他字段
您可以向 DocList 中插入、删除和追加文档:
# 追加
dl.append(
Image(
url="https://upload.wikimedia.org/wikipedia/commons/2/2f/Alpamayo.jpg",
tensor=np.zeros((3, 224, 224)),
)
)
# 删除
del dl[0]
# 插入
dl.insert(
0,
Image(
url="https://upload.wikimedia.org/wikipedia/commons/2/2f/Alpamayo.jpg",
tensor=np.zeros((3, 224, 224)),
),
)
您可以在 DocVec 和 DocList 之间无缝切换:
vec_2 = dl.to_doc_vec()
assert isinstance(vec_2, DocVec)
dl_2 = vec_2.to_doc_list()
assert isinstance(dl_2, DocList)
DocArray 促进数据传输,并天然兼容机器学习。
这包括对 Protobuf 和 gRPC 以及 HTTP 和序列化为 JSON、JSONSchema、Base64 和 Bytes 的原生支持。
这个功能对多种场景有益:
:bulb: 熟悉 FastAPI? 您会很高兴学到 DocArray 与 FastAPI 完全兼容!此外,我们有专门针对您的章节!
数据传输中,序列化是关键的步骤。让我们深入了解 DocArray 如何简化这个过程:
from docarray import BaseDoc
from docarray.typing import ImageTorchTensor
import torch
# 建模您的数据
class MyDocument(BaseDoc):
description: str
image: ImageTorchTensor[3, 224, 224]
# 创建一个文档
doc = MyDocument(
description="This is a description",
image=torch.zeros((3, 224, 224)),
)
# 序列化!
proto = doc.to_protobuf()
bytes_ = doc.to_bytes()
json = doc.json()
# 反序列化!
doc_2 = MyDocument.from_protobuf(proto)
doc_4 = MyDocument.from_bytes(bytes_)
doc_5 = MyDocument.parse_raw(json)
当然,序列化并不是全部。因此,请查看 DocArray 如何与 Jina 和 FastAPI 集成。
在建模并可能分发数据后,您通常希望将其存储在某处。这正是 DocArray 的用武之地!
Document Stores 提供了一种无缝的方式来存储您的 Document。无论是本地还是远程,您都可以通过相同的用户界面完成:
Document Store 接口允许您通过相同的用户界面将 Document 推送到多个数据源并从这些数据源拉取。
例如,让我们看看本地磁盘存储是如何工作的:
from docarray import BaseDoc, DocList
class SimpleDoc(BaseDoc):
text: str
docs = DocList[SimpleDoc]([SimpleDoc(text=f'doc {i}') for i in range(8)])
docs.push('file://simple_docs')
docs_pull = DocList[SimpleDoc].pull('file://simple_docs')
Document Indexes 允许您将 Document 索引到向量数据库中,以进行高效的基于相似性的检索。
这对于以下用途很有用:
目前,Document Indexes 支持 Weaviate、Qdrant、ElasticSearch、Redis、Mongo Atlas 和 HNSWLib,更多功能即将推出!
Document Index 接口允许您通过相同的用户界面索引和从多个向量数据库检索 Document。
它支持 ANN 向量搜索、文本搜索、过滤和混合搜索。
from docarray import DocList, BaseDoc
from docarray.index import HnswDocumentIndex
import numpy as np
from docarray.typing import ImageUrl, ImageTensor, NdArray
class ImageDoc(BaseDoc):
url: ImageUrl
tensor: ImageTensor
embedding: NdArray[128]
# 创建一些数据
dl = DocList[ImageDoc](
[
ImageDoc(
url="https://upload.wikimedia.org/wikipedia/commons/2/2f/Alpamayo.jpg",
tensor=np.zeros((3, 224, 224)),
embedding=np.random.random((128,)),
)
for _ in range(100)
]
)
# 创建一个文档索引
index = HnswDocumentIndex[ImageDoc](work_dir='/tmp/test_index')
# 索引您的数据
index.index(dl)
# 查找相似的文档
query = dl[0]
results, scores = index.find(query, limit=10, search_field='embedding')
根据您的背景和使用场景,有不同的方式来理解 DocArray。
如果您使用的是 DocArray 0.30.0 或更低版本,您会熟悉其 dataclass API。
DocArray >=0.30 是那个理念的正式体现。 每个文档都通过类似 dataclass 的接口创建,这得益于 Pydantic。
这带来了以下优势:
- 灵活性: 无需遵守一组固定的字段——您的数据定义模式
- 多模态性: 文档本质上是字典。这使得从任何语言(不仅仅是 Python)创建和发送它们都变得简单。
您可能还熟悉我们旧的用于向量数据库集成的 Document Stores。它们现在被称为 Document Indexes,并提供以下改进(有关新 API,请参阅此处):
目前,Document Indexes 支持 Weaviate、Qdrant、ElasticSearch、Redis、Mongo Atlas、精确最近邻搜索和 HNSWLib,更多功能即将推出。
如果您从 Pydantic 转来,可以将 DocArray 文档视为增强版的 Pydantic 模型,而 DocArray 则是围绕它们的好用工具的集合。
具体来说,我们致力于让 Pydantic 适合 ML 世界——不是通过替换它,而是在其基础上构建!
这意味着您将获得以下好处:
ImageUrl 可以 .load() 一个 URL 到图像张量,TextUrl 可以加载和分词文本文档等。这里最明显的优势是对 ML 中心数据(如 {Torch、TF,...}Tensor、Embedding 等)的一流支持。
这包括张量形状验证等便捷功能:
from docarray import BaseDoc
from docarray.typing import TorchTensor
import torch
class MyDoc(BaseDoc):
tensor: TorchTensor[3, 224, 224]
doc = MyDoc(tensor=torch.zeros(3, 224, 224)) # 有效
doc = MyDoc(tensor=torch.zeros(224, 224, 3)) # 通过重塑有效
try:
doc = MyDoc(tensor=torch.zeros(224)) # 验证失败
except Exception as e:
print(e)
# tensor
# Cannot reshape tensor of shape (224,) to shape (3, 224, 224) (type=value_error)
class Image(BaseDoc):
tensor: TorchTensor[3, 'x', 'x']
Image(tensor=torch.zeros(3, 224, 224)) # 有效
try:
Image(
tensor=torch.zeros(3, 64, 128)
) # 验证失败,因为第二维与第三维不匹配
except Exception as e:
print()
try:
Image(
tensor=torch.zeros(4, 224, 224)
) # 验证失败,因为第一维
except Exception as e:
print(e)
# Tensor shape mismatch. Expected(3, 'x', 'x'), got(4, 224, 224)(type=value_error)
try:
Image(
tensor=torch.zeros(3, 64)
) # 验证失败,因为维度不足
except Exception as e:
print(e)
# Tensor shape mismatch. Expected (3, 'x', 'x'), got (3, 64) (type=value_error)
如果您从 PyTorch 转来,可以将 DocArray 主要视为在数据流经模型时组织数据的一种方式。
它为您提供了几个优势:
DocArray 可以直接在 ML 模型内部使用,以处理和表示多模态数据。这使您能够使用 DocArray 在 nn.Module 内部对数据进行抽象,并提供与 FastAPI 兼容的架构,从而简化从模型训练到模型服务的过渡。
为了了解其效果,首先看一个简单的三模态 ML 模型的纯 PyTorch 实现:
import torch
from torch import nn
def encoder(x):
return torch.rand(512)
class MyMultiModalModel(nn.Module):
def __init__(self):
super().__init__()
self.audio_encoder = encoder()
self.image_encoder = encoder()
self.text_encoder = encoder()
def forward(self, text_1, text_2, image_1, image_2, audio_1, audio_2):
embedding_text_1 = self.text_encoder(text_1)
embedding_text_2 = self.text_encoder(text_2)
embedding_image_1 = self.image_encoder(image_1)
embedding_image_2 = self.image_encoder(image_2)
embedding_audio_1 = self.image_encoder(audio_1)
embedding_audio_2 = self.image_encoder(audio_2)
return (
embedding_text_1,
embedding_text_2,
embedding_image_1,
embedding_image_2,
embedding_audio_1,
embedding_audio_2,
)
这不像我们说的那么易读。更糟糕的是,如果您需要添加一种模态,您必须触及代码库的每个部分,更改 forward() 的返回类型并在下游进行大量更改。
现在,让我们看看使用 DocArray 的相同代码:
from docarray import DocList, BaseDoc
from docarray.documents import ImageDoc, TextDoc, AudioDoc
from docarray.typing import TorchTensor
from torch import nn
import torch
def encoder(x):
return torch.rand(512)
class Podcast(BaseDoc):
text: TextDoc
image: ImageDoc
audio: AudioDoc
class PairPodcast(BaseDoc):
left: Podcast
right: Podcast
class MyPodcastModel(nn.Module):
def __init__(self):
super().__init__()
self.audio_encoder = encoder()
self.image_encoder = encoder()
self.text_encoder = encoder()
def forward_podcast(self, docs: DocList[Podcast]) -> DocList[Podcast]:
docs.audio.embedding = self.audio_encoder(docs.audio.tensor)
docs.text.embedding = self.text_encoder(docs.text.tensor)
docs.image.embedding = self.image_encoder(docs.image.tensor)
return docs
def forward(self, docs: DocList[PairPodcast]) -> DocList[PairPodcast]:
docs.left = self.forward_podcast(docs.left)
docs.right = self.forward_podcast(docs.right)
return docs
看起来好多了,对吧?您在代码可读性和可维护性上立即获胜。同时,您可以将 PyTorch 模型变成 FastAPI 应用,并重用您的 Document模式定义(参见下方)。一切均以 Pythonic 的方式通过类型提示处理。
像 PyTorch 方法一样,您也可以将 DocArray 与 TensorFlow 结合使用,以处理和表示 ML 模型中的多模态数据。
首先,要使用 DocArray 与 TensorFlow,我们需要按如下方式安装它:
pip install tensorflow==2.12.0
pip install protobuf==3.19.0
与使用 PyTorch 相比,在使用 TensorFlow 时有一个主要区别:虽然 DocArray 的 TorchTensor 是 torch.Tensor 的子类,但 TensorFlowTensor 并非如此:由于 tf.Tensor 的一些技术限制,DocArray 的 TensorFlowTensor 不是 tf.Tensor 的子类,而是在其 .tensor 属性中存储了一个 tf.Tensor。
这对您有何影响?每当您想要访问张量数据以进行操作或将其传递给 ML 模型时,您需要访问其 .tensor 属性,而不是直接传递 TensorFlowTensor 实例。
看起来如下:
from typing import Optional
from docarray import DocList, BaseDoc
import tensorflow as tf
class Podcast(BaseDoc):
audio_tensor: Optional[AudioTensorFlowTensor] = None
embedding: Optional[AudioTensorFlowTensor] = None
class MyPodcastModel(tf.keras.Model):
def __init__(self):
super().__init__()
self.audio_encoder = AudioEncoder()
def call(self, inputs: DocList[Podcast]) -> DocList[Podcast]:
inputs.audio_tensor.embedding = self.audio_encoder(
inputs.audio_tensor.tensor
) # 访问 audio_tensor 的 .tensor 属性
return inputs
Document 是 Pydantic 模型(略有不同),因此它们与 FastAPI 完全兼容!
但为什么要使用它们,而不是您已经熟悉和喜爱的 Pydantic 模型呢?好问题!
接下来,让我们展示 Document 如何轻松地融入您的 FastAPI 应用:
import numpy as np
from fastapi import FastAPI
from docarray.base_doc import DocArrayResponse
from docarray import BaseDoc
from docarray.documents import ImageDoc
from docarray.typing import NdArray, ImageTensor
class InputDoc(BaseDoc):
img: ImageDoc
text: str
class OutputDoc(BaseDoc):
embedding_clip: NdArray
embedding_bert: NdArray
app = FastAPI()
def model_img(img: ImageTensor) -> NdArray:
return np.zeros((100, 1))
def model_text(text: str) -> NdArray:
return np.zeros((100, 1))
@app.post("/embed/", response_model=OutputDoc, response_class=DocArrayResponse)
async def create_item(doc: InputDoc) -> OutputDoc:
doc = OutputDoc(
embedding_clip=model_img(doc.img.tensor), embedding_bert=model_text(doc.text)
)
return doc
input_doc = InputDoc(text='', img=ImageDoc(tensor=np.random.random((3, 224, 224))))
async with AsyncClient(app=app, base_url="http://test") as ac:
response = await ac.post("/embed/", data=input_doc.json())
就像一个普通的 Pydantic 模型一样!
Jina 已采用 docarray 作为其表示和序列化 Document 的库。
Jina 允许您服务使用 DocArray 构建的模型和服务,让您可以充分利用 DocArray 的序列化能力来部署和扩展这些应用。
import numpy as np
from jina import Deployment, Executor, requests
from docarray import BaseDoc, DocList
from docarray.documents import ImageDoc
from docarray.typing import NdArray, ImageTensor
class InputDoc(BaseDoc):
img: ImageDoc
text: str
class OutputDoc(BaseDoc):
embedding_clip: NdArray
embedding_bert: NdArray
def model_img(img: ImageTensor) -> NdArray:
return np.zeros((100, 1))
def model_text(text: str) -> NdArray:
return np.zeros((100, 1))
class MyEmbeddingExecutor(Executor):
@requests(on='/embed')
def encode(self, docs: DocList[InputDoc], **kwargs) -> DocList[OutputDoc]:
ret = DocList[OutputDoc]()
for doc in docs:
output = OutputDoc(
embedding_clip=model_img(doc.img.tensor),
embedding_bert=model_text(doc.text),
)
ret.append(output)
return ret
with Deployment(
protocols=['grpc', 'http'], ports=[12345, 12346], uses=MyEmbeddingExecutor
) as dep:
resp = dep.post(
on='/embed',
inputs=DocList[InputDoc](
[InputDoc(text='', img=ImageDoc(tensor=np.random.random((3, 224, 224))))]
),
return_type=DocList[OutputDoc],
)
print(resp)
如果您是通过 DocArray 作为通用向量数据库客户端了解的,可以最好地将其视为向量数据库的一种新型 ORM。DocArray 的工作是将多模态、嵌套和领域特定的数据映射到向量数据库,存储在其中,从而使其可搜索:
from docarray import DocList, BaseDoc
from docarray.index import HnswDocumentIndex
import numpy as np
from docarray.typing import ImageUrl, ImageTensor, NdArray
class ImageDoc(BaseDoc):
url: ImageUrl
tensor: ImageTensor
embedding: NdArray[128]
# 创建一些数据
dl = DocList[ImageDoc](
[
ImageDoc(
url="https://upload.wikimedia.org/wikipedia/commons/2/2f/Alpamayo.jpg",
tensor=np.zeros((3, 224, 224)),
embedding=np.random.random((128,)),
)
for _ in range(100)
]
)
# 创建一个文档索引
index = HnswDocumentIndex[ImageDoc](work_dir='/tmp/test_index2')
# 索引您的数据
index.index(dl)
# 查找相似的文档
query = dl[0]
results, scores = index.find(query, limit=10, search_field='embedding')
目前,DocArray 支持以下向量数据库:
OpenSearch 的集成正在开发中。
当然,这只是 DocArray 能做的其中一件事,所以我们鼓励您查看本说明的其余部分!
使用 DocArray,您可以通过 Langchain 将外部数据连接到 LLM。DocArray 让您可以自由地建立灵活的文模式架构,并从不同的后端选择存储文档。创建文档索引后,您可以使用 DocArrayRetriever 将其连接到您的 Langchain 应用。
通过以下命令安装 Langchain:
pip install langchain
from docarray import BaseDoc, DocList
from docarray.typing import NdArray
from langchain.embeddings.openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings()
# 定义文档模式
class MovieDoc(BaseDoc):
title: str
description: str
year: int
embedding: NdArray[1536]
movies = [
{"title": "#1 title", "description": "#1 description", "year": 1999},
{"title": "#2 title", "description": "#2 description", "year": 2001},
]
# 嵌入 `description` 并创建文档
docs = DocList[MovieDoc](
MovieDoc(embedding=embeddings.embed_query(movie["description"]), **movie)
for movie in movies
)
from docarray.index import (
InMemoryExactNNIndex,
HnswDocumentIndex,
WeaviateDocumentIndex,
QdrantDocumentIndex,
ElasticDocIndex,
RedisDocumentIndex,
MongoDBAtlasDocumentIndex,
)
# 选择合适的后端并初始化数据
db = InMemoryExactNNIndex[MovieDoc](docs)
from langchain.chat_models import ChatOpenAI
from langchain.chains import ConversationalRetrievalChain
from langchain.retrievers import DocArrayRetriever
# 创建检索器
retriever = DocArrayRetriever(
index=db,
embeddings=embeddings,
search_field="embedding",
content_field="description",
)
# 在您的链中使用该检索器
model = ChatOpenAI()
qa = ConversationalRetrievalChain.from_llm(model, retriever=retriever)
另外,您可以使用内置的向量存储。Langchain 支持两种向量存储: DocArrayInMemorySearch 和 DocArrayHnswSearch。两者都用户友好,最适合中小型数据集。