在本章中,您将学习到:
- 不同的生成性AI模型是如何工作的
- 如何将生成模型集成并提供服务到FastAPI
- 如何处理文本、图像、音频、视频和3D模型
- 如何快速构建原型的用户界面
- FastAPI中的几种模型服务策略
- 如何利用中间件进行服务监控
在本章中,您将学习各种生成性AI模型的机制以及如何在FastAPI应用程序中为它们提供服务。此外,使用Streamlit UI包,您将创建一个简单的浏览器客户端,用于与模型服务端点交互。我们将探讨不同的模型服务策略,如何预加载模型以提高效率,以及如何使用FastAPI功能进行服务监控。
为了巩固您在本章的学习,我们将逐步构建一个FastAPI服务,使用开源生成性AI模型生成文本、图像、音频和3D几何图形,全部从零开始。在后续章节中,您将为您的生成性AI服务构建解析文档和网页内容的功能,以便您可以使用语言模型与它们对话。
注意: 在上一章中,您已经学习了如何在Python中设置一个全新的FastAPI项目。在继续阅读本章内容之前,请确保您已经准备好了全新的安装环境。或者,您可以克隆或下载本书的GitHub仓库。然后在克隆完成后,切换到ch03-start分支,准备好进行接下来的步骤。
到本章结束时,您将拥有一个FastAPI服务,该服务为各种开源生成性AI模型提供服务,您可以在Streamlit UI中进行测试。此外,您的服务还能够通过中间件将使用数据记录到磁盘。
生成模型服务
在您的应用程序中提供预训练的生成性模型之前,值得了解这些模型是如何训练的,以及它们是如何生成数据的。通过理解这一点,您可以定制应用程序的内部机制,以增强您提供给用户的输出。
在本章中,我将向您展示如何为多种模态提供模型服务,包括:
- 基于变换器神经网络架构的语言模型
- 基于变换器架构的激进文本转语音和文本转音频服务的音频模型
- 基于Stable Diffusion和视觉变换器架构的文本转图像和文本转视频服务的视觉模型
- 基于条件隐式函数编码器和扩散解码器架构的文本转3D服务的3D模型
这个列表并不详尽,仅涵盖了一些生成性AI模型。要探索其他模型,请访问Hugging Face模型库。
语言模型
在本节中,我们将讨论语言模型,包括变换器和递归神经网络(RNNs)。
变换器与递归神经网络
随着"Attention Is All You Need"这篇标志性论文的发布,AI领域发生了巨大的震动。论文中,作者提出了一种完全不同的自然语言处理(NLP)和序列建模方法,这与现有的递归神经网络(RNN)架构有所不同。
图3-1展示了原始论文中提出的变换器架构的简化版本。

历史上,文本生成任务依赖于RNN模型来学习顺序数据中的模式,例如自由文本。为了处理文本,这些模型将文本拆分成小块,如单词或字符,这些小块被称为"标记"(token),可以按顺序处理。
RNNs保持一个叫做状态向量的记忆存储,它在整个文本序列中传递信息,从一个标记到下一个标记,直到序列的结尾。这意味着,随着你到达文本序列的末尾,早期标记对状态向量的影响比最新的标记要小得多。
理想情况下,每个标记在任何文本中都应该与其他标记一样重要。然而,由于RNNs只能通过查看之前出现的项目来预测序列中的下一个项目,它们在捕捉长距离依赖关系和建模大量文本中的模式时存在困难。因此,它们实际上无法记住或理解大文档中的关键信息或上下文。
随着变换器(transformers)的发明,递归或卷积建模现在可以被更高效的方法所取代。由于变换器不保持隐藏的状态记忆,而是利用了一种叫做自注意力(self-attention)的新能力,它们能够建模单词之间的关系,无论它们在句子中相隔多远。这个自注意力组件使模型能够"聚焦"句子中语境相关的单词。
当RNNs建模句子中相邻单词之间的关系时,变换器则映射文本中每个单词之间的成对关系。
图3-2展示了RNNs与变换器在处理句子时的对比。

自注意力系统的驱动力是被称为注意力头(attention heads)的专门模块,它们捕捉单词之间的成对模式,并将其表示为注意力图。
图3-3可视化了一个注意力头的注意力图。连接可以是双向的,连接的粗细表示句子中单词之间关系的强度。

变换器模型包含多个注意力头,这些注意力头分布在其神经网络层中。每个注意力头独立计算自己的注意力图,以捕捉单词之间的关系,专注于输入中的特定模式。通过使用多个注意力头,模型可以同时从不同的角度和上下文分析输入,以理解数据中的复杂模式和依赖关系。
图3-4展示了模型每一层中每个注意力头(即独立的注意力权重集)的注意力图。

RNNs还需要大量的计算力来训练,因为由于其训练算法的顺序性,训练过程无法在多个GPU上并行化。另一方面,变换器模型以非顺序的方式处理单词,因此可以在GPU上并行运行注意力机制。
变换器架构的高效性意味着,只要有足够的数据、计算能力和内存,这些模型就能更具可扩展性。你可以使用跨越人类图书馆的语料库来构建语言模型。你所需要的只是充足的计算能力和数据来训练一个大语言模型(LLM)。这正是OpenAI所做的,这家公司开发了著名的ChatGPT应用程序,背后支持着多个他们自有的大语言模型,包括GPT-4o。
截至本书写作时,OpenAI大语言模型的实现细节仍然是商业机密。尽管许多研究人员对OpenAI的方法有大致了解,但他们可能没有足够的资源去复制这些方法。然而,随着时间的推移,多个开源替代品已被发布,供研究和商业用途,包括Llama(Facebook)、Gemma(Google)、Mistral和Falcon等。截止本书写作时,模型的大小介于0.05B和480B参数之间(即模型的权重和偏差),可以根据需求进行选择。
开源大语言模型(LLM)的硬件要求
截至本书写作时,最大的开源大语言模型是多语言的480B参数Snowflake Arctic。运行这个庞大模型的推荐硬件是单个AWS/Azure 8xH100实例,其中包含八个H100数据中心GPU卡,每个卡提供80GB的VRAM。其他旗舰开源大语言模型,如多语言405B参数的Llama 3.1,也需要类似的硬件。
截至2024年1月,最适合AI工作负载的消费级GPU卡之一是NVIDIA 4090 RTX,它仅配备24GB的VRAM。由于内存限制,单个消费级GPU(如4090 RTX)可能无法运行超过30B的模型,除非该模型经过量化(即压缩)。
如果你想运行量化的70B-Llama模型,你可能需要一块64GB VRAM的GPU,或者多个较小的卡。除了配置多GPU家庭服务器的电源和散热挑战外,在运行如此大的模型时,你仍然可能会遇到较慢的预测速度。
关于量化过程和使用量化大语言模型的内容,你将在第10章中了解更多。
由于高内存需求,提供大语言模型(LLM)的服务仍然是一个挑战,如果需要在自己的数据集上训练和微调这些模型,内存需求可能会翻倍。这是因为训练过程需要在训练批次之间缓存和重用模型参数。因此,大多数组织可能会依赖轻量级(最多3B参数)模型,或者依赖OpenAI、Anthropic、Cohere、Mistral等LLM提供商的API。
随着大语言模型的流行,了解它们如何训练以及如何处理数据变得更加重要,因此接下来我们将讨论它们的基本机制。
标记化与嵌入
神经网络无法直接处理单词,因为它们是基于数字运作的大型统计模型。为了弥合语言与数字之间的差距,必须使用标记化技术。通过标记化,你可以将文本分解为模型可以处理的小块。
任何一段文本必须首先被切分为一个标记列表,这些标记代表了单词、音节、符号和标点符号。然后,这些标记会被映射到唯一的数字,以便模型能够以数字的形式建模这些模式。
通过向训练好的变换器提供一组输入标记,网络就能预测下一个最佳标记来生成文本,每次生成一个单词。
图3-5展示了OpenAI的标记器如何将文本转换为标记序列,并为每个标记分配唯一的标记标识符。

那么,在你对一些文本进行标记化之后,你能做什么呢?这些标记需要进一步处理,才能被语言模型处理。
在标记化之后,你需要使用嵌入器(embedder)将这些标记转换为实数的稠密向量,称为嵌入(embeddings),它们在连续的向量空间中捕捉语义信息(即每个标记的意义)。图3-6展示了这些嵌入的示例。

提示
这些嵌入向量使用小的浮点数(而非整数)来捕捉标记之间微妙的关系,具有更大的灵活性和精确度。它们通常呈正态分布,因此语言模型的训练和推理可以更加稳定和一致。
在嵌入过程之后,每个标记都会被分配一个嵌入向量,向量中填充了n个数字。嵌入向量中的每个数字专注于表示标记含义的特定维度。
训练变换器
一旦你拥有了一组嵌入向量,你就可以在你的文档上训练模型,以更新每个嵌入中的值。在模型训练过程中,训练算法会更新嵌入层的参数,使得嵌入向量尽可能准确地描述输入文本中每个标记的含义。
理解嵌入向量如何工作可能是具有挑战性的,因此我们可以尝试一种可视化方法。
假设你使用的是二维嵌入向量,这意味着向量只包含两个数字。然后,如果你绘制这些向量,比较模型训练前后的变化,你会看到类似图3-7的图形。具有相似含义的标记或单词的嵌入向量将彼此更接近。

为了确定两个单词之间的相似度,可以使用一种叫做余弦相似度的计算方法来计算向量之间的角度。较小的角度意味着更高的相似度,表示具有相似的上下文和含义。训练之后,两个具有相似含义的嵌入向量的余弦相似度计算将验证这些向量彼此接近。
图3-8展示了完整的标记化、嵌入和训练过程。

一旦你有了训练好的嵌入层,就可以使用它来将任何新的输入文本嵌入到变换器模型中,如图3-1所示。
位置编码
在将嵌入向量传递到变换器网络中的注意力层之前,最后一步是实现位置编码。位置编码过程生成位置嵌入向量,然后与标记嵌入向量相加。
由于变换器同时处理单词,而不是按顺序处理,因此需要位置嵌入来记录顺序数据中的单词顺序和上下文,比如句子。生成的嵌入向量在传递到变换器的注意力机制之前,既捕捉了单词的语义信息,又包含了位置相关的信息。这个过程确保了注意力头拥有所有必要的信息,从而有效地学习模式。
图3-9展示了位置编码过程,其中位置嵌入与标记嵌入相加。

自回归预测
变换器是一个自回归(即顺序)模型,因为未来的预测是基于过去的值,如图3-10所示。

模型接收输入标记,然后将这些标记嵌入并传递通过网络,以预测下一个最佳标记。这个过程会重复,直到生成一个 <stop>
或句子结束的 <eos>
标记。
然而,模型能够存储用于生成下一个标记的标记数量是有限的。这个标记限制被称为模型的上下文窗口,这是在为你的生成性AI服务选择模型时需要考虑的重要因素。
如果上下文窗口限制被达到,模型将简单地丢弃最久未使用的标记。这意味着它可能会忘记文档中或对话中最久未使用的句子。
注意
截至本书写作时,最便宜的OpenAI GPT-4o-mini模型的上下文约为~128,000个标记,相当于超过300页的文本。
截至2025年3月,最大的上下文窗口属于Magic.Dev LTM-2-mini,具有1亿个标记,相当于1000万行代码或750本小说。
其他模型的上下文窗口大小大致在数十万到百万个标记之间。
较短的上下文窗口会导致信息丢失,难以维持对话,并减少与用户查询的连贯性。
另一方面,较长的上下文窗口有更大的内存需求,在扩展到成千上万的并发用户时,可能会导致性能问题或服务变慢。此外,由于增加了计算和内存需求,依赖具有更大上下文窗口的模型通常会更昂贵。因此,正确的选择将取决于你的预算和用户需求。
将语言模型集成到应用程序中
你可以通过几行代码下载并在应用程序中使用语言模型。在示例3-1中,你将下载一个TinyLlama模型,具有11亿个参数,并在3万亿个标记上进行预训练。
安装TinyLlama依赖项
要将TinyLlama集成到你的应用程序中,你可以使用Hugging Face的transformers库。你还需要通过安装torch包来安装Pytorch深度学习框架。可以通过pip安装这两个包。
在Windows上,当你安装为CUDA启用的GPU编译的torch时,需要在pip命令中提供
--index-url
标志。
go# 在Windows上安装具有CUDA 12.4支持的`torch`和`transformers`包 $ pip install transformers torch \ --index-url https://download.pytorch.org/whl/cu124
TinyLlama一次生成的句子数量有限。你还需要大约3 GB的磁盘空间和RAM来将此模型加载到内存中进行推理。我建议在启用了CUDA的NVIDIA GPU上运行该模型(并使用为CUDA编译的torch wheel),因为CPU推理可能会很慢。请参考NVIDIA的CUDA安装说明,适用于Windows或Linux。
此外,要在Windows上运行示例3-1,你可能需要安装Visual Studio Build Tools 2022,并包括C++和.NET开发工具,以解决缺少DLL库和依赖项的问题。
示例 3-1. 从Hugging Face仓库下载并加载语言模型
ini
# models.py
import torch
from transformers import Pipeline, pipeline
prompt = "如何设置FastAPI项目?"
system_prompt = """
你的名字是FastAPI机器人,你是一个有帮助的聊天机器人,负责教授FastAPI给用户。
始终以markdown格式回答。
"""
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
def load_text_model():
pipe = pipeline(
"text-generation",
model="TinyLlama/TinyLlama-1.1B-Chat-v1.0",
torch_dtype=torch.bfloat16,
device=device
)
return pipe
def generate_text(pipe: Pipeline, prompt: str, temperature: float = 0.7) -> str:
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt},
]
prompt = pipe.tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
predictions = pipe(
prompt,
temperature=temperature,
max_new_tokens=256,
do_sample=True,
top_k=50,
top_p=0.95,
)
output = predictions[0]["generated_text"].split("</s>\n<|assistant|>\n")[-1]
return output
解释:
-
检查是否有NVIDIA GPU可用,如果可用,则将设备设置为当前启用CUDA的GPU。否则,继续使用CPU。
-
下载并将TinyLlama模型加载到内存中,使用float16张量精度数据类型。
-
在首次加载时,将整个管道移动到GPU上。
-
准备消息列表,列表由包含角色和内容键值对的字典组成。字典的顺序决定了对话中消息的顺序,通常第一个消息是系统提示,用来引导模型的输出。
-
将聊天消息的列表转换为模型的整数标记列表。然后,模型被要求以文本格式生成输出,而不是整数标记(
tokenize=False
)。还会在聊天消息的末尾添加生成提示(add_generation_prompt=True
),以便鼓励模型基于聊天历史生成响应。 -
将准备好的提示传递给模型,并设置多个推理参数,以优化文本生成性能。关键推理参数包括:
max_new_tokens
: 指定在输出中生成的最大新标记数。do_sample
: 确定在生成输出时,是否从适合的标记列表中随机选择一个标记(True
)还是直接选择每个步骤中最可能的标记(False
)。temperature
: 控制输出生成的随机性。较低的值使模型的输出更精确,较高的值允许更具创造性的响应。top_k
: 限制模型的标记预测为前K个选项。top_k=50
表示在当前标记预测步骤中,从前50个最适合的标记中选择。top_p
: 在创建最适合的标记列表时实现核采样。top_p=0.95
表示创建一个前几个标记的列表,直到你确信你的列表中有95%的最适合标记可供选择。
-
最终的输出从预测对象中获得。TinyLlama生成的文本包括完整的对话历史,生成的响应附加在末尾。
</s>
停止标记后跟着\n<|assistant|>\n
标记,用来选择对话中的最后一条消息内容,也就是模型的响应。
示例 3-1是一个很好的起点;你仍然可以在CPU上加载这个模型并在合理的时间内获得响应。然而,TinyLlama的表现可能不如其较大的版本。对于生产工作负载,你将希望使用更大的模型,以获得更好的输出质量和性能。
你现在可以在控制器函数中使用load_model
和predict
函数,然后添加一个路由处理装饰器,通过端点提供模型服务,如示例3-2所示。
示例 3-2. 通过FastAPI端点提供语言模型服务
python
# main.py
from fastapi import FastAPI
from models import load_text_model, generate_text
app = FastAPI()
@app.get("/generate/text")
def serve_language_model_controller(prompt: str) -> str:
pipe = load_text_model()
output = generate_text(pipe, prompt)
return output
解释:
- 创建一个FastAPI服务器,并为提供模型服务添加
/generate
路由处理器。 serve_language_model_controller
负责从请求查询参数中获取提示词。- 模型被加载到内存中。
- 控制器将查询传递给模型进行预测。
- FastAPI服务器将输出作为HTTP响应发送给客户端。
一旦FastAPI服务启动并运行,你可以访问Swagger文档页面(位于http://localhost:8000/docs
)来测试你的新端点:
http://localhost:8000/generate/text?prompt="什么是FastAPI?"
如果你在CPU上运行代码示例,模型返回响应的时间大约为一分钟,如图3-11所示。

对于一个运行在自己电脑CPU上的小型语言模型(SLM)来说,这个响应还不错,除了TinyLlama产生了一个错误的结论,认为FastAPI使用的是Flask。事实上,这个说法是不正确的;FastAPI使用的是Starlette作为底层Web框架,而不是Flask。
幻觉指的是那些没有基于训练数据或现实的输出。尽管像TinyLlama这样的开源小型语言模型在3万亿个标记上进行了训练,但较少的模型参数可能限制了它们学习数据中真实信息的能力。此外,也可能使用了一些未经过滤的训练数据,这两者都可能导致更多幻觉的产生。
警告
在提供语言模型服务时,始终告知用户需要通过外部来源核实输出的正确性,因为语言模型可能会产生幻觉并输出错误的陈述。
你现在可以使用Python中的Web浏览器客户端,比使用命令行客户端更具交互性地测试你的服务。
一个很好的Python包,用于快速开发用户界面的是Streamlit,它可以让你以很少的努力为AI服务创建美观且可定制的UI。
将FastAPI与Streamlit UI生成器连接
Streamlit允许你轻松创建聊天用户界面,以便测试和原型设计。你可以使用pip安装streamlit包:
ruby
$ pip install streamlit
示例 3-3 展示了如何开发一个简单的UI来连接到你的服务。
示例 3-3. 使用Streamlit聊天UI消费FastAPI /generate端点
csharp
# client.py
import requests
import streamlit as st
st.title("FastAPI ChatBot")
if "messages" not in st.session_state:
st.session_state.messages = []
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
if prompt := st.chat_input("Write your prompt in this input field"):
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.text(prompt)
response = requests.get(
f"http://localhost:8000/generate/text", params={"prompt": prompt}
)
response.raise_for_status()
with st.chat_message("assistant"):
st.markdown(response.text)
解释:
- 为应用程序添加一个标题,将显示到UI中。
- 初始化聊天并跟踪聊天历史。
- 在应用程序重新运行时显示聊天历史中的消息。
- 等待用户通过聊天输入字段提交提示。
- 将用户或助手的消息添加到聊天历史。
- 在聊天消息容器中显示用户消息。
- 发送GET请求,将提示作为查询参数发送到FastAPI端点,从TinyLlama生成响应。
- 验证响应是否正常。
- 在聊天消息容器中显示助手消息。
你现在可以启动你的Streamlit客户端应用程序:
arduino
$ streamlit run client.py
现在你应该能够在Streamlit中与TinyLlama进行交互,如图3-12所示。所有这一切都通过几行简短的Python脚本实现。

图3-13展示了我们到目前为止开发的解决方案的整体系统架构。

警告
虽然示例3-3中的解决方案非常适合原型设计和测试模型,但它不适用于生产工作负载,其中多个用户需要同时访问模型。这是因为在当前的设置下,每次处理请求时,模型都会被加载到内存中并卸载。将大模型加载/卸载到内存中是缓慢的,并且会阻塞I/O操作。
你刚刚构建的TinyLlama服务使用了一个解码器变换器,优化了对话和聊天的使用场景。然而,变换器的原始论文提出的架构包含了编码器和解码器两部分。
变换器变体
在使用语言模型时,有三种类型的变换器是你应该了解的,如图3-14所示。

每种变换器变体都有其独特的能力,并专门用于某些任务。
编码器-解码器变换器
用于将一个信息序列转换为另一个信息序列
擅长翻译、文本摘要、问答任务
仅编码器变换器
用于理解和表示输入序列的含义
专门用于情感分析、实体提取和文本分类任务
仅解码器变换器
用于预测序列中的下一个标记
在文本生成、对话和语言建模任务中表现优于其他变换器
在实践中,你应该根据变换器的专长和能力,选择适合你用例的变换器。
现在,你应该对语言模型的内部工作原理以及如何将它们打包到FastAPI Web服务器中有了更深入的了解。
语言模型仅代表所有生成模型的一小部分。接下来的章节将扩展你的知识,涵盖生成音频、图像和视频的模型的功能和服务。
我们可以先从音频模型开始。
音频模型
在生成性AI服务中,音频模型对于创建互动和真实的声音非常重要。与现在你已经熟悉的文本模型不同,文本模型专注于处理和生成文本,而音频模型可以处理音频信号。通过它们,你可以合成语音、生成音乐,甚至为虚拟助手、自动配音、游戏开发和沉浸式音频环境等应用创建声音效果。
最强大的文本转语音和文本转音频模型之一是由Suno AI创建的Bark模型。这个基于变换器的模型可以生成逼真的多语言语音和音频,包括音乐、背景噪音和声音效果。
Bark模型由四个模型组成,这些模型通过管道串联在一起,从文本提示中合成音频波形,如图3-15所示。

1. 语义文本模型
一个因果(顺序)自回归变换器模型接受标记化的输入文本,并通过语义标记捕捉其含义。自回归模型通过重用自身先前的输出,预测序列中的未来值。
2. 粗略音频模型
一个因果自回归变换器接收语义模型的输出并生成初步的音频特征,但缺少更精细的细节。每个预测基于语义标记序列中的过去和当前信息。
3. 精细音频模型
一个非因果自动编码器变换器通过生成剩余的音频特征来精细化音频表示。由于粗略音频模型已经生成了整个音频序列,精细模型不需要是因果的。
4. Encodec音频编解码模型
该模型解码所有先前生成的音频编码,输出音频。
Bark通过解码精细化的音频特征,将其转换为最终的音频输出,以口语、音乐或简单的音效形式合成音频波形。
示例 3-4 展示了如何使用小型Bark模型。
示例 3-4. 从Hugging Face仓库下载并加载小型Bark模型
python
# schemas.py
from typing import Literal
VoicePresets = Literal["v2/en_speaker_1", "v2/en_speaker_9"]
# models.py
import torch
import numpy as np
from transformers import AutoProcessor, AutoModel, BarkProcessor, BarkModel
from schemas import VoicePresets
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
def load_audio_model() -> tuple[BarkProcessor, BarkModel]:
processor = AutoProcessor.from_pretrained("suno/bark-small", device=device)
model = AutoModel.from_pretrained("suno/bark-small", device=device)
return processor, model
def generate_audio(
processor: BarkProcessor,
model: BarkModel,
prompt: str,
preset: VoicePresets,
) -> tuple[np.array, int]:
inputs = processor(text=[prompt], return_tensors="pt",voice_preset=preset)
output = model.generate(**inputs, do_sample=True).cpu().numpy().squeeze()
sample_rate = model.generation_config.sample_rate
return output, sample_rate
步骤说明:
- 指定支持的语音预设选项 :使用
Literal
类型来指定语音预设选项。 - 下载小型Bark处理器:该处理器为核心模型准备输入文本提示。
- 下载Bark模型:该模型将用于生成输出音频。两个对象将在后续的音频生成中使用。
- 预处理文本提示 :为文本提示添加语音预设嵌入,并使用
return_tensors="pt"
返回一个PyTorch张量数组的标记化输入。 - 生成音频数组:生成包含合成音频信号幅度值的音频数组。
- 获取采样率:从模型生成配置中获取采样率,供后续音频生成使用。
当你使用模型生成音频时,输出是表示音频信号在每个时间点幅度(或强度)的浮动数值序列。
为了播放这些音频,需要将其转换为可以传送到扬声器的数字格式。这涉及以固定的采样率对音频信号进行采样,并将幅度值量化为固定的位数。soundfile
库可以帮助你通过采样率生成音频文件。采样率越高,采样点越多,音频质量越好,但文件大小也会增加。
你可以使用pip安装音频文件
库:
ruby
$ pip install soundfile
示例 3-5 展示了如何将音频内容流式传输给客户端。
示例 3-5. 返回生成音频的FastAPI端点
python
# utils.py
from io import BytesIO
import soundfile
import numpy as np
def audio_array_to_buffer(audio_array: np.array, sample_rate: int) -> BytesIO:
buffer = BytesIO()
soundfile.write(buffer, audio_array, sample_rate, format="wav")
buffer.seek(0)
return buffer
# main.py
from fastapi import FastAPI, status
from fastapi.responses import StreamingResponse
from models import load_audio_model, generate_audio
from schemas import VoicePresets
from utils import audio_array_to_buffer
@app.get(
"/generate/audio",
responses={status.HTTP_200_OK: {"content": {"audio/wav": {}}}},
response_class=StreamingResponse,
)
def serve_text_to_audio_model_controller(
prompt: str,
preset: VoicePresets = "v2/en_speaker_1",
):
processor, model = load_audio_model()
output, sample_rate = generate_audio(processor, model, prompt, preset)
return StreamingResponse(
audio_array_to_buffer(output, sample_rate), media_type="audio/wav"
)
步骤说明:
- 安装soundfile库:用于将音频数组写入内存缓冲区并使用采样率。
- 重置缓冲区指针:将缓冲区游标重置到缓冲区开始位置,并返回可迭代的缓冲区。
- 创建新的音频端点 :该端点返回
audio/wav
内容类型,作为StreamingResponse
返回。StreamingResponse
通常用于当你想要流式传输响应数据时,例如返回大文件或生成响应数据。它允许你返回一个生成器函数,该函数将数据分块发送给客户端。 - 将生成的音频数组转换为可传递给流响应的可迭代缓冲区。
在示例 3-5中,你使用小型Bark模型生成了音频数组,并将音频内容的内存缓冲区流式传输给客户端。流式传输对于大文件更为高效,因为客户端可以在数据正在提供时消费内容。在前面的示例中,我们没有使用流式响应,因为生成的图像或文本通常较小,而与音频或视频内容相比。
提示
直接从内存缓冲区流式传输音频内容比将音频数组写入文件并从硬盘流式传输内容更快、更高效。
如果你需要将内存腾出来用于其他任务,可以先将音频数组写入文件,然后使用文件读取生成器从文件中流式传输。这样做会在延迟和内存之间进行权衡。
现在你已经有了一个音频生成端点,你可以更新你的Streamlit UI客户端代码来呈现音频消息。更新后的Streamlit客户端代码如下所示,见示例3-6。
示例 3-6. 使用Streamlit音频UI消费FastAPI /audio生成端点
css
# client.py
for message in st.session_state.messages:
with st.chat_message(message["role"]):
content = message["content"]
if isinstance(content, bytes):
st.audio(content)
else:
st.markdown(content)
if prompt := st.chat_input("Write your prompt in this input field"):
response = requests.get(
f"http://localhost:8000/generate/audio", params={"prompt": prompt}
)
response.raise_for_status()
with st.chat_message("assistant"):
st.text("Here is your generated audio")
st.audio(response.content)
步骤说明:
- 更新Streamlit客户端代码以渲染音频内容。
- 使用Streamlit,你可以替换组件来渲染任何类型的内容,包括图像、音频和视频。
你现在应该能够在更新后的Streamlit UI中生成非常逼真的语音音频,如图3-16所示。

请记住,你使用的是Bark模型的压缩版本,但使用轻量版时,即使在单个CPU上,你也可以相当快速地生成语音和音乐音频。这是以牺牲一定的音频生成质量为代价的。
现在,你应该对通过流式响应为用户提供更大内容以及与音频模型的协作感到更加自如。
到目前为止,你已经构建了对话和文本转语音服务。接下来,让我们看看如何与视觉模型互动,构建一个图像生成服务。
视觉模型
通过使用视觉模型,你可以根据提示生成、增强和理解视觉信息。
由于这些模型能够比任何人都更快地生成非常逼真的输出,并且能够理解和操控现有的视觉内容,它们在图像生成器和编辑器、物体检测、图像分类和标注、增强现实等应用中非常有用。
训练图像模型最常用的架构之一叫做Stable Diffusion(SD)。
SD模型被训练用于将输入图像编码到潜在空间。这个潜在空间是模型从训练数据中学习到的模式的数学表示。如果你尝试可视化一个编码后的图像,你看到的将只是一个白噪声图像,类似于你在电视信号丢失时屏幕上看到的黑白点。
图3-17展示了训练和推理的完整过程,并可视化了图像是如何通过前向和反向扩散过程进行编码和解码的。使用文本、图像和语义图的文本编码器通过反向扩散过程来控制输出。

这些模型的神奇之处在于它们能够将噪声图像解码回原始输入图像。实际上,SD(稳定扩散)模型还学会了从编码图像中去除白噪声,以重现原始图像。该模型通过多次迭代执行这一去噪过程。
然而,你并不希望重新创建你已经拥有的图像。你希望模型生成全新的、前所未见的图像。那么,SD模型如何实现这一点呢?答案在于潜在空间,其中包含了编码后的噪声图像。你可以改变这些图像中的噪声,使得当模型去噪并将其解码回来时,得到一幅模型从未见过的新图像。
仍然存在一个挑战:如何控制图像生成过程,以确保模型不会生成随机的图像?解决方案是同时编码图像描述。潜在空间中的模式随后会映射到每个输入图像中看到的文本描述。现在,你使用文本提示来采样噪声潜在空间,使得去噪过程后的生成图像正是你想要的。
这就是SD模型如何生成它们在训练数据中从未见过的新图像的原理。本质上,这些模型在潜在空间中进行导航,潜在空间包含了各种模式和意义的编码表示。通过去噪过程,模型迭代地优化噪声,从而生成在其训练数据集中不存在的新图像。
要下载SD模型,你需要安装Hugging Face的diffusers库:
ruby
$ pip install diffusers
示例3-7展示了如何将SD模型加载到内存中。
示例3-7:从Hugging Face仓库下载并加载SD模型
python
# models.py
import torch
from diffusers import DiffusionPipeline, StableDiffusionInpaintPipelineLegacy
from PIL import Image
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
def load_image_model() -> StableDiffusionInpaintPipelineLegacy:
pipe = DiffusionPipeline.from_pretrained(
"segmind/tiny-sd", torch_dtype=torch.float32,
device=device
)
return pipe
def generate_image(
pipe: StableDiffusionInpaintPipelineLegacy, prompt: str
) -> Image.Image:
output = pipe(prompt, num_inference_steps=10).images[0]
return output
将TinySD模型下载并加载到内存中,使用float32数据类型进行内存加载,尽管这会占用更多内存。使用float16数据类型(该类型对大模型精度有限)会导致数值不稳定和精度丢失。由于硬件对float16的支持有限,因此在CPU上运行SD模型时,可能无法使用float16。
将文本提示传递给模型,生成一组图像并选择第一张。有些模型允许在一个推理步骤中生成多张图像。
num_inference_steps=10
指定了推理过程中执行的扩散步骤数。在每个扩散步骤中,从前一个步骤产生更强的噪声图像。通过多次扩散步骤,模型可以更好地理解输入数据中的噪声模式,并学习更有效地去除它们。推理步骤越多,结果越好,但需要更多的计算资源和更长的处理时间。
生成的图像将是Python的Pillow图像类型,因此你可以访问Pillow的各种图像方法进行后处理和存储。例如,你可以调用image.save()
方法将图像存储在文件系统中。
注意 视觉模型非常资源密集。要在CPU上加载并使用像TinySD这样的较小视觉模型,你需要大约5GB的磁盘空间和内存。然而,你可以通过使用pip install accelerate
安装加速器来优化所需的资源,使得模型管道使用更少的CPU内存。
当提供视频模型时,你将需要使用GPU。稍后在本章中,我将展示如何利用GPU为视频模型提供支持。
你现在可以将这个模型打包成另一个端点,类似于示例3-2,不同之处在于返回的响应将是图像二进制数据(而不是文本)。参考示例3-8。
示例3-8:返回生成图像的FastAPI端点
python
# utils.py
from typing import Literal
from PIL import Image
from io import BytesIO
def img_to_bytes(
image: Image.Image, img_format: Literal["PNG", "JPEG"] = "PNG"
) -> bytes:
buffer = BytesIO()
image.save(buffer, format=img_format)
return buffer.getvalue()
# main.py
from fastapi import FastAPI, Response, status
from models import load_image_model, generate_image
from utils import img_to_bytes
@app.get("/generate/image",
responses={status.HTTP_200_OK: {"content": {"image/png": {}}}},
response_class=Response)
def serve_text_to_image_model_controller(prompt: str):
pipe = load_image_model()
output = generate_image(pipe, prompt)
return Response(content=img_to_bytes(output), media_type="image/png")
创建一个内存缓冲区,将图像保存到该缓冲区中,然后返回缓冲区中的原始字节数据。
指定媒体内容类型和状态码,以便自动生成Swagger UI文档页面。
指定响应类,以防止FastAPI将application/json
作为额外的可接受响应媒体类型。
从模型返回的响应将是Pillow图像格式。
我们将需要使用FastAPI的Response
类发送一个特殊响应,携带图像字节数据,媒体类型为PNG。
图3-18显示了通过FastAPI Swagger文档测试新/generate/image
端点的结果,使用文本提示:"一间有树的温馨客厅"。

现在,将你的端点连接到Streamlit UI以进行原型设计,如示例3-9所示。
示例3-9:Streamlit视觉UI使用FastAPI /image生成端点
css
# client.py
...
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.image(message["content"])
...
if prompt := st.chat_input("在此输入框中写下你的提示词"):
...
response = requests.get(
f"http://localhost:8000/generate/image", params={"prompt": prompt}
)
response.raise_for_status()
with st.chat_message("assistant"):
st.text("这是你生成的图像")
st.image(response.content)
...
通过HTTP协议传输的图像将是二进制格式。因此,我们更新显示函数以渲染二进制图像内容。你可以使用st.image
方法将图像显示到UI上。
更新GET请求,以访问/generate/image
端点。然后,将文本消息和图像消息渲染给用户。
图3-19显示了用户与模型的最终体验结果。

运行XL模型
你现在已经了解了如何使用FastAPI和Streamlit实现模型服务端点来生成文本或图像。我们使用了这些模型的精简版本,以便你可以在CPU上运行示例。然而,如果你需要使用XL版本以获得更好的质量,硬件要求会显著增加。举个例子,要运行SDXL模型,你需要16 GB的CPU内存和16 GB的GPU显存来生成一张图像。这是因为你首先需要将模型从磁盘加载到CPU上,然后再将其移动到GPU上进行推理。我们将在讨论模型服务策略时更详细地讲解这一过程。
我们看到即使使用一个精简版的SD模型,也能生成看起来合理的图像。XL版本能够生成更加逼真的图像,但仍然有其自身的局限性。
在写作时,目前开源的SD模型确实存在一些限制:
一致性
这些模型无法生成提示词中描述的每个细节和复杂的组合。
输出大小
输出图像只能是预定义的大小,比如512 × 512或1024 × 1024像素。
可组合性
你无法完全控制生成的图像,也无法在图像中定义组合。
照片现实主义
生成的输出确实显示了一些细节,这些细节表明它们是由AI生成的。
可读文本
一些模型无法生成可读的文本。
你所使用的tiny-sd模型是一个早期阶段的模型,它已经经过了从更大的V1.5 SD模型进行蒸馏(即压缩)的过程。因此,生成的输出可能无法满足生产标准,或者不完全连贯,可能未能完全包含提示词中提到的所有概念。然而,如果你使用低秩适配(LoRA)对特定概念/风格进行微调,蒸馏模型可能表现得很好。
低秩适配在生成模型微调中的应用
LoRA是一种训练策略,它为模型的每一层引入了最小数量的可训练参数。大多数原始模型的参数保持不变。
通过限制需要训练的参数数量,LoRA大大减少了训练所需的GPU内存。这在微调或训练大规模模型时非常有用,因为内存限制通常是定制化的一大挑战。
你现在可以构建基于文本和图像的生成AI服务。然而,你可能会想知道如何基于视频模型构建文本到视频的服务。接下来,我们将了解更多关于视频模型的内容,了解它们是如何工作的,以及如何使用它们构建图像动画服务。
视频模型
视频模型是最为资源密集的生成模型之一,通常需要GPU才能生成短小的高质量视频片段。这些模型必须生成几十帧来生成一秒钟的视频,即便没有任何音频内容。
Stability AI发布了几个基于SD架构的开源视频模型,存放在Hugging Face上。我们将使用它们的压缩版图像到视频模型,以便实现更快速的图像动画服务。
为了开始,我们可以使用示例3-10运行一个小型的图像到视频模型。
注意
要运行示例3-10,可能需要访问一台支持CUDA的NVIDIA GPU。
此外,对于stable-video-diffusion-img2vid模型的商业使用,请参阅其模型卡。
示例3-10:从Hugging Face仓库下载并加载Stability AI的img2vid模型
ini
# models.py
import torch
from diffusers import StableVideoDiffusionPipeline
from PIL import Image
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
def load_video_model() -> StableVideoDiffusionPipeline:
pipe = StableVideoDiffusionPipeline.from_pretrained(
"stabilityai/stable-video-diffusion-img2vid",
torch_dtype=torch.float16,
variant="fp16",
device=device,
)
return pipe
def generate_video(
pipe: StableVideoDiffusionPipeline, image: Image.Image, num_frames: int = 25
) -> list[Image.Image]:
image = image.resize((1024, 576))
generator = torch.manual_seed(42)
frames = pipe(
image, decode_chunk_size=8, generator=generator, num_frames=num_frames
).frames[0]
return frames
- 将输入图像调整为模型输入所期望的标准尺寸。调整大小也可以避免处理过大的输入。
- 创建一个随机张量生成器,将种子设置为42,以确保视频帧生成的可重现性。
- 运行帧生成管道一次性生成所有视频帧。获取生成的第一批帧。这个步骤需要显著的显存。
num_frames
指定了生成的帧数,而decode_chunk_size
指定了每次生成多少帧。
有了模型加载功能后,你现在可以构建视频服务端点。
然而,在声明路由处理程序之前,你需要一个工具函数来处理视频模型输出,将帧转换为可流式传输的视频,使用I/O缓冲区。
为了将一系列帧导出为视频,你需要使用像av
这样的库将它们编码成视频容器,该库实现了流行的ffmpeg视频处理库的Python绑定。
你可以通过以下命令安装av库:
ruby
$ pip install av
现在,你可以使用示例3-11创建可流式传输的视频缓冲区。
示例3-11:使用av库将视频模型输出从帧导出为可流式传输的视频缓冲区
ini
# utils.py
from io import BytesIO
from PIL import Image
import av
def export_to_video_buffer(images: list[Image.Image]) -> BytesIO:
buffer = BytesIO()
output = av.open(buffer, "w", format="mp4")
stream = output.add_stream("h264", 30)
stream.width = images[0].width
stream.height = images[0].height
stream.pix_fmt = "yuv444p"
stream.options = {"crf": "17"}
for image in images:
frame = av.VideoFrame.from_image(image)
packet = stream.encode(frame)
output.mux(packet)
packet = stream.encode(None)
output.mux(packet)
return buffer
- 打开一个缓冲区以写入MP4文件,然后使用AV的视频复用器配置视频流。
- 设置视频编码为h264,帧率为30帧每秒,并确保帧的尺寸与提供给函数的图像匹配。
- 设置视频流的像素格式为yuv444p,以确保每个像素具有完整的y(亮度或亮度)和u、v(色度或颜色)分量的分辨率。
- 配置流的常数速率因子(CRF),以控制视频质量和压缩。将CRF设置为17,以输出无损高质量视频,压缩最小。
- 使用配置的视频流复用器将输入帧编码成编码数据包。
- 将编码的帧添加到已打开的视频容器缓冲区中。
- 刷新编码器中的所有剩余帧,并将结果数据包合并到输出文件中,然后返回包含编码视频的缓冲区。
要将图像提示与服务一起使用作为文件上传,你需要安装python-multipart
库:
ruby
$ pip install python-multipart
安装后,你可以使用示例3-12设置新的端点。
示例3-12:从图像到视频模型服务生成视频
python
# main.py
from fastapi import status, FastAPI, File
from fastapi.responses import StreamingResponse
from io import BytesIO
from PIL import Image
from models import load_video_model, generate_video
from utils import export_to_video_buffer
@app.post(
"/generate/video",
responses={status.HTTP_200_OK: {"content": {"video/mp4": {}}}},
response_class=StreamingResponse,
)
def serve_image_to_video_model_controller(
image: bytes = File(...), num_frames: int = 25
):
image = Image.open(BytesIO(image))
model = load_video_model()
frames = generate_video(model, image, num_frames)
return StreamingResponse(
export_to_video_buffer(frames), media_type="video/mp4"
)
- 使用
File
对象指定图像作为表单文件上传。 - 通过传递图像字节流创建一个Pillow图像对象。模型管道期望输入为Pillow图像格式。
- 将生成的帧导出为MP4视频,并通过可迭代的视频缓冲区流式传输给客户端。
设置好视频端点后,你现在可以将图像上传到FastAPI服务,并将其作为视频进行动画处理。
Hub上还有其他视频模型,允许你生成GIF和动画。作为额外的练习,你可以尝试使用它们构建生成AI服务。虽然开源视频模型能够生成高质量的视频,但OpenAI发布的一个新大型视觉模型(LVM)叫做Sora,已经震动了视频生成行业。
OpenAI Sora
文本到视频的模型在生成能力上存在一定限制。除了需要巨大的计算能力来顺序生成连贯的视频帧外,训练这些模型还面临挑战,主要原因包括:
- 在帧之间保持时间和空间一致性,以实现真实且无失真的视频输出。
- 缺乏高质量的标题和元数据训练数据,这些数据对于训练视频模型至关重要。
- 标注视频内容的挑战,清晰且描述性强的标注非常耗时,且远远超出了简短文本的草拟。标注必须描述每个序列的叙事和场景,模型才能学习并将视频中包含的丰富模式映射到文本。
由于这些原因,视频生成模型一直没有取得突破,直到OpenAI宣布了Sora模型。
Sora是一个通用的大型视觉扩散变换器模型,能够生成涵盖多种持续时间、纵横比和分辨率的视频和图像,最长可生成一分钟的高清晰度视频。其架构基于通常用于LLM的变换器和扩散过程。与LLM使用文本令牌不同,Sora使用视觉补丁。
提示
Sora模型结合了变换器和SD架构的元素和原理,而在示例3-10中,你使用了Stability AI的SD模型来生成视频。
那么,Sora有什么不同之处呢?
变换器在语言模型、计算机视觉和图像生成方面展现出了显著的可扩展性,因此Sora架构基于变换器来处理多种输入(如文本、图像或视频帧)是合乎逻辑的。而且,由于变换器能够理解序列数据中的复杂模式和长程依赖关系,作为视觉变换器的Sora也能够捕捉视频帧之间的精细时间和空间关系,从而生成连贯的帧,并在帧与帧之间平滑过渡(即展现时间一致性)。
此外,Sora借鉴了SD模型的能力,使用迭代噪声去除过程生成高质量且视觉上连贯的视频帧,并通过精确控制生成过程。使用扩散过程可以让Sora生成具有细节和理想属性的图像。
通过结合变换器的顺序推理和SD的迭代优化,Sora可以从文本和图像等多模态输入中生成高分辨率、连贯且平滑的视频,这些输入包含抽象概念。
Sora的网络架构也通过U形网络减少维度,其中高维的视觉数据被压缩并编码成潜在的噪声空间。然后,Sora可以通过去噪扩散过程从潜在空间生成补丁。
扩散过程与基于图像的SD模型相似。OpenAI没有使用通常用于图像的2D U-Net,而是训练了一个3D U-Net,其中第三个维度是跨时间的视频帧序列(即生成视频),如图3-20所示。

OpenAI已经证明,通过将视频压缩为补丁,如图3-21所示,该模型可以在训练不同分辨率、持续时间和纵横比的视频和图像时,实现学习高维表示的可扩展性。

通过扩散过程,Sora将输入的噪声补丁进行处理,生成清晰的视频和图像,支持任何纵横比、大小和分辨率,直接适应设备的原生屏幕尺寸。
当文本变换器预测文本序列中的下一个令牌时,Sora的视觉变换器则预测下一个补丁,以生成图像或视频,如图3-22所示。

通过在各种数据集上进行训练,OpenAI克服了之前提到的训练视觉模型的挑战,如缺乏高质量的标题、视频数据的高维度等问题。
Sora以及潜在的其他大型视觉模型(LVMs)令人着迷的一点是它们展现出的新兴能力:
3D一致性
生成场景中的物体保持一致,并且即使相机在场景中移动和旋转,物体也能调整为正确的透视。
物体持久性和大范围一致性
被遮挡或离开画面的物体和人物在重新出现在视野中时会持续存在。在某些情况下,模型能够有效地记住如何在环境中保持它们的一致性。这也被称为时间一致性,是大多数视频模型所难以解决的问题。
世界互动
生成视频中模拟的动作会真实地影响环境。例如,Sora理解吃汉堡的动作会在汉堡上留下咬痕。
模拟环境
Sora还可以模拟世界------无论是现实世界还是游戏中的虚构环境------并遵守这些环境中交互的规则,比如在Minecraft关卡中扮演一个角色。换句话说,Sora已经学会了成为一个数据驱动的物理引擎。
图3-23展示了这些能力。

在写作时,Sora尚未作为API发布,但开源替代品已经出现。一种有前景的大型视觉模型叫做"Latte",允许你在自己的视觉数据上微调LVM。
注意
目前,像Latte这样的某些开源模型尚不可商业化。写作时请始终检查模型卡和许可证,以确保任何商业使用是被允许的。
将变换器与扩散器结合起来创建LVMs是生成复杂输出(如视频)的一项有前景的研究领域。然而,我认为相同的过程也可以应用于生成其他类型的高维数据,这些数据可以表示为多维数组。
现在,你应该能更自信地使用文本、音频、视觉和视频模型来构建服务了。接下来,让我们看看另一类能够生成复杂数据(例如通过构建3D资产生成服务生成3D几何体)的模型。
3D模型
现在,你已经理解了前面提到的模型如何使用变换器和扩散器来生成任何形式的文本、音频或视觉数据。生成3D几何体需要与图像、音频和文本生成不同的方法,因为你必须考虑空间关系、深度信息和几何一致性,这些都增加了其他数据类型中没有的复杂性。
对于3D几何体,网格(meshes)用于定义物体的形状。像Autodesk 3ds Max、Maya和SolidWorks等软件包可以用来生成、编辑和渲染这些网格。
网格实际上是由位于3D虚拟空间中的顶点、边和面组成的集合。顶点是空间中的点,连接起来形成边。边形成面(多边形),当它们围成一个平面时,通常是三角形或四边形的形状。图3-24展示了顶点、边和面之间的区别。

你可以通过它们在3D空间中的坐标来定义顶点,通常由笛卡尔坐标系(x, y, z)决定。实际上,顶点的排列和连接形成了一个3D网格的表面,从而定义了几何形状。
图3-25展示了这些特征如何结合起来定义一个3D几何体的网格,例如猴子头部的网格。

你可以训练并使用变换器模型来预测序列中的下一个令牌,其中序列是3D网格表面上顶点的坐标。这种生成模型可以通过预测在3D空间中形成所需几何形状的下一个顶点和面的集合来生成3D几何体。然而,为了获得平滑的表面,几何体可能需要成千上万的顶点和面。
这意味着对于每个3D物体,你需要等待很长时间才能完成生成,且结果可能仍然是低保真度的。因此,最强大的模型(例如OpenAI的Shap-E)通过训练函数(具有大量参数)来隐式定义3D空间中的表面和体积,从而生成3D几何体。
隐式函数对于创建平滑的表面或处理网格等离散表示难以处理的复杂细节非常有用。一个训练好的模型可以由一个编码器组成,该编码器将模式映射到隐式函数。条件3D模型不需要显式生成网格的顶点和面序列,而是可以在连续的3D空间中评估训练好的隐式函数。因此,生成过程具有很高的自由度、控制力和灵活性,能够生成高保真度的输出,适用于需要详细和复杂3D几何体的应用。
一旦模型的编码器训练好以生成隐式函数,它会利用神经辐射场(NeRF)渲染技术,作为解码器的一部分来构建3D场景。NeRF将一对输入------3D空间坐标和3D视角方向------映射到通过隐式函数输出的物体密度和RGB颜色。为了在3D场景中合成新的视图,NeRF方法将视口视为射线矩阵。每个像素对应一条射线,射线从相机位置开始,然后沿着视角方向延伸。每条射线及其对应的像素的颜色通过沿射线评估隐式函数并整合结果来计算RGB颜色。
一旦3D场景被计算出来,符号距离函数(SDFs)用于通过计算任何点到3D物体最近表面的距离和颜色来生成网格或3D物体的线框。可以将SDFs看作一种通过告诉你空间中每个点离物体表面的距离来描述3D物体的方式。这个函数为每个点提供一个数值:如果点在物体内部,数值为负;如果点在物体表面,数值为零;如果点在物体外部,数值为正。物体的表面是所有点的数值为零的地方。SDFs帮助将这些信息转化为3D网格。
尽管使用了隐式函数,输出质量仍然低于人工创建的3D资产,可能看起来像是卡通风格。然而,使用3D生成AI模型,你可以快速生成初始的3D几何体,迭代概念并精炼3D资产。
OpenAI Shap-E
Shap-E(由OpenAI开发)是一个开源模型,"以"输入的3D数据(描述、参数、部分几何体、颜色等)为条件,生成特定的3D形状。你可以使用Shap-E创建图像或文本到3D的服务。
如常规操作,你首先从Hugging Face下载并加载模型,如示例3-13所示。
示例3-13:下载并加载OpenAI的Shap-E模型
ini
# models.py
import torch
from diffusers import ShapEPipeline
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
def load_3d_model() -> ShapEPipeline:
pipe = ShapEPipeline.from_pretrained("openai/shap-e", device=device)
return pipe
def generate_3d_geometry(
pipe: ShapEPipeline, prompt: str, num_inference_steps: int
):
images = pipe(
prompt,
guidance_scale=15.0,
num_inference_steps=num_inference_steps,
output_type="mesh",
).images[0]
return images
这个特定的Shap-E管道接受文本提示,但如果你想传递图像提示,你需要加载一个不同的管道。
- 使用
guidance_scale
参数来微调生成过程,以更好地匹配提示。 - 使用
num_inference_steps
参数来控制输出的分辨率,并需要额外的计算。请求更多的推理步骤或增加引导比例可以延长渲染时间,以换取更高质量的输出,更好地遵循用户的请求。 - 将
output_type
参数设置为生成网格张量输出。
默认情况下,Shap-E管道将生成一系列图像,这些图像可以组合成一个旋转的GIF动画。你可以将这个输出导出为GIF、视频或OBJ文件,后者可以在Blender等3D建模工具中加载。
现在,你已经有了模型加载和3D网格生成的功能,接下来让我们使用示例3-14将网格导出到缓冲区。
提示
open3d是一个开源库,用于处理3D数据,如点云、网格和带深度信息的彩色图像(即RGB-D图像)。你需要安装open3d才能运行示例3-14:
ruby
$ pip install open3d
示例3-14:将3D张量网格导出到Wavefront OBJ缓冲区
python
# utils.py
import os
import tempfile
from io import BytesIO
from pathlib import Path
import open3d as o3d
import torch
from diffusers.pipelines.shap_e.renderer import MeshDecoderOutput
def mesh_to_obj_buffer(mesh: MeshDecoderOutput) -> BytesIO:
mesh_o3d = o3d.geometry.TriangleMesh()
mesh_o3d.vertices = o3d.utility.Vector3dVector(
mesh.verts.cpu().detach().numpy()
)
mesh_o3d.triangles = o3d.utility.Vector3iVector(
mesh.faces.cpu().detach().numpy()
)
if len(mesh.vertex_channels) == 3: # You have color channels
vert_color = torch.stack(
[mesh.vertex_channels[channel] for channel in "RGB"], dim=1
)
mesh_o3d.vertex_colors = o3d.utility.Vector3dVector(
vert_color.cpu().detach().numpy()
)
with tempfile.NamedTemporaryFile(delete=False, suffix=".obj") as tmp:
o3d.io.write_triangle_mesh(tmp.name, mesh_o3d, write_ascii=True)
with open(tmp.name, "rb") as f:
buffer = BytesIO(f.read())
os.remove(tmp.name)
return buffer
- 创建一个Open3D三角网格对象。
- 将模型生成的网格转换为Open3D三角网格对象。为此,通过将网格的顶点和面张量移动到CPU,并转换为numpy数组,来获取生成的3D网格的顶点和三角形。
- 检查网格是否有三个顶点颜色通道(表示RGB颜色数据),并将这些通道堆叠成一个张量。
- 将网格颜色张量转换为Open3D兼容的格式,以设置网格的顶点颜色。
- 使用临时文件创建并返回数据缓冲区。
- Windows不支持
NamedTemporaryFile
的delete=True
选项。相反,手动在返回内存缓冲区之前删除创建的临时文件。
最后,你可以创建端点,如示例3-15所示。
示例3-15:创建3D模型服务端点
python
# main.py
from fastapi import FastAPI, status
from fastapi.responses import StreamingResponse
from models import load_3d_model, generate_3d_geometry
from utils import mesh_to_obj_buffer
...
@app.get(
"/generate/3d",
responses={status.HTTP_200_OK: {"content": {"model/obj": {}}}},
response_class=StreamingResponse,
)
def serve_text_to_3d_model_controller(
prompt: str, num_inference_steps: int = 25
):
model = load_3d_model()
mesh = generate_3d_geometry(model, prompt, num_inference_steps)
response = StreamingResponse(
mesh_to_obj_buffer(mesh), media_type="model/obj"
)
response.headers["Content-Disposition"] = (
f"attachment; filename={prompt}.obj"
)
return response
- 指定OpenAPI规范,以使成功响应包括
model/obj
作为媒体内容类型。 - 告知客户端,流式响应的内容应作为附件处理。
如果你发送请求到/generate/3d
端点,3D物体将作为Wavefront OBJ文件进行下载,一旦生成完成。
你可以将OBJ文件导入任何3D建模软件(如Blender)中,以查看3D几何形状。使用如"apple"、"car"、"phone"和"donut"等提示词,你可以生成图3-26所示的3D几何体。

如果你将物体(比如苹果)隔离出来并启用线框视图,你可以看到构成苹果网格的所有顶点和边,这些顶点和边被表示为三角形多边形,如图3-27所示。

Shap-E取代了另一种较旧的模型------Point-E,该模型生成3D物体的点云。这是因为与Point-E相比,Shap-E收敛速度更快,并且尽管建模的是一个高维度、多表示的输出空间,Shap-E仍然能够达到相当或更好的生成形状质量。
点云(通常在建筑行业中使用)是大量点坐标的集合,这些点紧密地代表了一个3D物体(例如建筑结构)在现实世界中的空间。包括LiDAR激光扫描仪在内的环境扫描设备产生点云,以表示3D空间中的物体,并提供与现实世界环境接近的近似测量。
随着3D模型的不断改进,未来可能能够生成与其真实物体高度相似的对象。
生成AI模型服务策略
现在,你应该更有信心构建自己的端点,提供来自Hugging Face模型库的各种模型。我们讨论了几种不同的模型,包括那些生成文本、图像、视频、音频和3D形状的模型。
你使用的模型较小,因此可以在CPU上加载并生成合理的输出。然而,在生产环境中,你可能希望使用更大的模型来生成更高质量的结果,这些模型可能仅能在GPU上运行,并且需要大量的视频随机存取内存(VRAM)。
除了利用GPU,你还需要从以下几种选项中选择一种模型服务策略:
- 模型无关
在每次请求时加载模型并生成输出(适用于模型交换)。 - 计算高效
使用FastAPI生命周期来预加载可以在每次请求中重复使用的模型。 - 精简
外部提供模型,无需框架,或者使用第三方模型API并通过FastAPI与它们交互。
接下来,让我们详细了解每种策略。
模型无关:在每次请求时交换模型
在之前的代码示例中,你定义了模型加载和生成函数,然后在路由处理程序中使用它们。使用这种服务策略时,FastAPI将模型加载到RAM(如果使用GPU,则加载到VRAM)中,并运行生成过程。一旦FastAPI返回结果,模型就会从RAM中卸载。这个过程在下一个请求时重复进行。
由于模型在使用后被卸载,内存会被释放,供其他进程或模型使用。采用这种方法,如果处理时间不是问题,你可以在单个请求中动态交换各种模型。这意味着其他并发请求必须等待,直到服务器响应它们。
在处理请求时,FastAPI会将传入的请求排队,并按先进先出(FIFO)顺序处理它们。由于每次都需要加载和卸载模型,这种行为会导致较长的等待时间。在大多数情况下,不推荐使用这种策略,但如果你需要在多个大模型之间进行切换,并且内存不足以同时加载它们,那么你可以采用这种策略进行原型设计。然而,在生产环境中,显而易见的原因是绝对不应该使用这种策略------你的用户会希望避免长时间的等待。
图3-28展示了这种模型服务策略。

如果你需要在每个请求中使用不同的模型,并且内存有限,这种方法在处理少数用户并在较弱的机器上快速尝试时可以很好地工作。权衡是,由于模型切换,处理时间会显著变慢。然而,在生产环境中,最好增加更大的RAM,并使用FastAPI应用生命周期的模型预加载策略。
计算高效:使用FastAPI生命周期预加载模型
在FastAPI中,加载模型最计算高效的策略是使用应用生命周期。采用这种方法,你可以在应用启动时加载模型,在应用关闭时卸载模型。在关闭过程中,你还可以执行任何所需的清理步骤,例如文件系统清理或日志记录。
与前面提到的第一种策略相比,这种策略的主要优点是,你可以避免在每个请求中重新加载大型模型。你可以加载一个大型模型,并在每个请求中使用预加载的模型进行生成。因此,你可以节省数分钟的处理时间,以换取大量的RAM(如果使用GPU,则是VRAM)。然而,由于响应时间更短,你的应用用户体验将显著改善。
图3-29展示了使用应用生命周期的模型服务策略。

你可以使用应用生命周期实现模型预加载,如示例3-16所示。
示例3-16:使用应用生命周期预加载模型
less
# main.py
from contextlib import asynccontextmanager
from typing import AsyncIterator
from fastapi import FastAPI, Response, status
from models import load_image_model, generate_image
from utils import img_to_bytes
models = {}
@asynccontextmanager
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
models["text2image"] = load_image_model()
yield
... # 在这里运行清理代码
models.clear()
app = FastAPI(lifespan=lifespan)
@app.get(
"/generate/image",
responses={status.HTTP_200_OK: {"content": {"image/png": {}}}},
response_class=Response,
)
def serve_text_to_image_model_controller(prompt: str):
output = generate_image(models["text2image"], prompt)
return Response(content=img_to_bytes(output), media_type="image/png")
-
在全局应用范围内初始化一个空的可变字典,用于存放一个或多个模型。
-
使用
asynccontextmanager
装饰器处理启动和关闭事件,作为异步上下文管理器的一部分:- 上下文管理器会在
yield
关键字前后运行代码。 - 装饰后的生命周期函数中的
yield
关键字分隔了启动和关闭阶段。 - 在
yield
关键字之前的代码会在应用启动时运行,处理任何请求之前。 - 当你想终止应用时,FastAPI会在关闭阶段运行
yield
关键字之后的代码。
- 上下文管理器会在
-
在启动时预加载模型到
models
字典中。 -
启动阶段完成后开始处理请求。
-
在应用关闭时清理模型。
-
创建FastAPI服务器并传入生命周期函数。
-
将全局预加载的模型实例传递给生成函数。
现在,如果你启动应用,你应该立即看到模型管道被加载到内存中。在应用这些更改之前,模型管道只有在你发出第一个请求时才会加载。
警告
你可以使用生命周期模型服务策略将多个模型预加载到内存中,但对于大型生成AI模型来说,这并不实用。生成模型可能非常消耗资源,并且在大多数情况下,你需要GPU来加速生成过程。最强大的消费级GPU仅配备24GB的VRAM。某些模型需要18GB的内存才能执行推理,因此建议将模型部署在单独的应用实例和GPU上。
启动和关闭事件
在FastAPI 0.93.0引入生命周期异步上下文管理器之前,通常使用单独的启动和关闭事件处理函数来处理应用生命周期。示例3-17展示了这种用法。
示例3-17:启动和关闭事件
python# main.py from models import load_image_model models = {} app = FastAPI() @app.on_event("startup") def startup_event(): models["text2image"] = load_image_model() @app.on_event("shutdown") def shutdown_event(): with open("log.txt", mode="a") as logfile: logfile.write("Application shutdown")
在网络上的一些资源中,仍然可能使用这种替代和遗留的方法,因此了解这种方法是有价值的。
精简:外部提供模型服务
另一种提供生成AI模型的策略是通过其他工具将其打包为外部服务。然后,你可以使用FastAPI应用作为客户端和外部模型服务器之间的逻辑层。在这个逻辑层中,你可以处理模型之间的协调、与API的通信、用户管理、安全措施、监控活动、内容过滤、增强提示词或任何其他所需的逻辑。
云服务提供商
云服务提供商不断创新无服务器和专用计算解决方案,你可以用它们将模型外部化。例如,Azure机器学习工作室现在提供了一个PromptFlow工具,你可以用它来部署和定制OpenAI或开源语言模型。部署后,你将获得一个在Azure计算上运行的模型端点,准备好使用。然而,使用PromptFlow或类似工具时,可能需要特定的依赖项并遵循非传统步骤,因此其学习曲线较陡。
BentoML
另一个很好的选择是将模型外部化到FastAPI的是BentoML。BentoML受FastAPI启发,但实现了不同的服务策略,专为AI模型设计。
相比FastAPI,BentoML在处理并发模型请求时有显著的改进,它能够在不同的工作进程中运行不同的请求。它可以并行化CPU密集型请求,无需你直接处理Python多进程。更重要的是,BentoML还可以批量处理模型推理,使多个用户的生成过程通过单次模型调用完成。
我在第2章详细介绍了BentoML。
提示
要运行BentoML,你需要先安装一些依赖项:
ruby
$ pip install bentoml
你可以查看如何启动BentoML服务器,在示例3-18中展示了具体步骤。
示例3-18:使用BentoML提供图像模型服务
python
# bento.py
import bentoml
from models import load_image_model
@bentoml.service(
resources={"cpu": "4"}, traffic={"timeout": 120}, http={"port": 5000}
)
class Generate:
def __init__(self) -> None:
self.pipe = load_image_model()
@bentoml.api(route="/generate/image")
def generate(self, prompt: str) -> str:
output = self.pipe(prompt, num_inference_steps=10).images[0]
return output
- 声明一个BentoML服务,分配4个CPU。如果模型未及时生成,服务将在120秒后超时,且服务将在5000端口运行。
- 声明一个API控制器来处理核心模型生成过程。该控制器将连接到BentoML的API路由处理程序。
你可以在本地运行BentoML服务:
ruby
$ bentoml serve service:Generate
现在,你的FastAPI服务器可以成为客户端,外部提供的模型进行服务。你可以从FastAPI内部发出HTTP POST请求以获取响应,如示例3-19所示。
示例3-19:通过FastAPI调用BentoML端点
python
# main.py
import httpx
from fastapi import FastAPI, Response
app = FastAPI()
@app.get(
"/generate/bentoml/image",
responses={status.HTTP_200_OK: {"content": {"image/png": {}}}},
response_class=Response,
)
async def serve_bentoml_text_to_image_controller(prompt: str):
async with httpx.AsyncClient() as client:
response = await client.post(
"http://localhost:5000/generate", json={"prompt": prompt}
)
return Response(content=response.content, media_type="image/png")
- 使用
httpx
库创建一个异步HTTP客户端。 - 向BentoML图像生成模型端点发送POST请求。
模型提供商
除了BentoML和云服务提供商,你还可以使用外部模型服务提供商,如OpenAI。在这种情况下,你的FastAPI应用将作为OpenAI API的服务包装器。
幸运的是,与OpenAI等模型提供商的API集成非常简单,如示例3-20所示。
提示
要运行示例3-20,你必须获取API密钥并将OPENAI_API_KEY
环境变量设置为该密钥,正如OpenAI所推荐的那样。
示例3-20:与OpenAI服务集成
python
# main.py
from fastapi import FastAPI
from openai import OpenAI
app = FastAPI()
openai_client = OpenAI()
system_prompt = "You are a helpful assistant."
@app.get("/generate/openai/text")
def serve_openai_language_model_controller(prompt: str) -> str | None:
response = openai_client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": f"{system_prompt}"},
{"role": "user", "content": prompt},
],
)
return response.choices[0].message.content
- 使用
gpt-4o
模型,通过OpenAI API与模型进行对话。
现在,你应该能够通过外部调用OpenAI服务来获取输出。
LANGCHAIN
你可以使用
langchain
库来切换与任何LLM(大语言模型)提供商的集成。这个库还提供了与LLM协作的优秀工具,我们将在本书后续部分讲解。首先,安装该库:
ruby$ pip install langchain langchain-openai
安装完
langchain
后,按照示例3-21与外部模型API(如OpenAI)进行集成。示例3-21:使用LangChain与外部提供商API集成
python# main.py from fastapi import FastAPI from langchain.chains.llm import LLMChain from langchain_core.prompts import PromptTemplate from langchain_openai import OpenAI llm = OpenAI(openai_organization="YOUR_ORGANIZATION_ID") template = """ Your name is FastAPI bot and you are a helpful chatbot responsible for teaching FastAPI to your users. Here is the user query: {query} """ prompt = PromptTemplate.from_template(template) llm_chain = LLMChain(prompt=prompt, llm=llm) app = FastAPI() @app.get("/generate/text") def generate_text_controller(query: str): return llm_chain.run(query)
- 使用组织ID创建OpenAI客户端。
- 构建一个提示模板。
- 从提示模板构建OpenAI的
llm_chain
对象。- 通过传入用户查询来运行文本生成过程。
在使用外部服务时,请注意数据将与第三方服务提供商共享。在这种情况下,如果您重视数据隐私和安全,您可能会倾向于选择自托管解决方案。自托管的权衡在于需要增加部署和管理自己模型服务器的复杂性。如果您确实不想自己托管大模型,云服务提供商可以提供托管解决方案,在这种情况下,您的数据永远不会与第三方共享。一个例子是Azure OpenAI,在撰写时提供了OpenAI最好的LLM和图像生成器的快照。
现在,您有几个模型服务选项。在本章结束之前,您还需要实现一个服务的日志记录和监控系统。
中间件在服务监控中的作用 您可以实现一个简单的监控工具,在其中记录提示和响应,并记录它们的请求和响应令牌使用情况。为了实现日志记录系统,您可以在模型服务控制器中编写一些日志记录函数。然而,如果您有多个模型和端点,您可能会从利用FastAPI中间件机制中受益。
中间件是运行在请求由您的控制器处理之前和之后的代码块。您可以定义自定义中间件,并将其附加到任何API路由处理程序。一旦请求到达路由处理程序,中间件作为中介,处理客户端和服务器控制器之间的请求和响应。
中间件的优秀使用场景包括日志记录和监控、限速、内容过滤和跨域资源共享(CORS)实现。
示例3-22展示了如何监控您的模型服务处理程序。
生产环境中的使用日志记录通过自定义中间件 请不要在生产中使用示例3-22,因为如果您从Docker容器或没有挂载持久卷或没有日志数据库的主机上运行应用程序,监控日志可能会消失。
在第7章中,您将与数据库集成监控系统,以便在应用程序环境之外持久化日志。
示例3-22. 使用中间件机制捕获服务使用日志
python
# main.py
import csv
import time
from datetime import datetime, timezone
from uuid import uuid4
from typing import Awaitable, Callable
from fastapi import FastAPI, Request, Response
# preload model with a lifespan
...
app = FastAPI(lifespan=lifespan)
csv_header = [
"Request ID", "Datetime", "Endpoint Triggered", "Client IP Address",
"Response Time", "Status Code", "Successful"
]
@app.middleware("http")
async def monitor_service(
req: Request, call_next: Callable[[Request], Awaitable[Response]]
) -> Response:
request_id = uuid4().hex
request_datetime = datetime.now(timezone.utc).isoformat()
start_time = time.perf_counter()
response: Response = await call_next(req)
response_time = round(time.perf_counter() - start_time, 4)
response.headers["X-Response-Time"] = str(response_time)
response.headers["X-API-Request-ID"] = request_id
with open("usage.csv", "a", newline="") as file:
writer = csv.writer(file)
if file.tell() == 0:
writer.writerow(csv_header)
writer.writerow(
[
request_id,
request_datetime,
req.url,
req.client.host,
response_time,
response.status_code,
response.status_code < 400,
]
)
return response
# Usage Log Example
""""
Request ID: 3d15d3d9b7124cc9be7eb690fc4c9bd5
Datetime: 2024-03-07T16:41:58.895091
Endpoint triggered: http://localhost:8000/generate/text
Client IP Address: 127.0.0.1
Processing time: 26.7210 seconds
Status Code: 200
Successful: True
"""
# model-serving handlers
...
声明一个由FastAPI HTTP中间件机制装饰的函数。该函数必须接收Request对象,并调用call_next
回调函数才能视为有效的HTTP中间件。
将请求传递给路由处理程序以处理响应。
生成一个请求ID,以跟踪所有传入请求,即使在请求处理过程中call_next
出现错误。
计算响应的持续时间,精确到四位小数。
为处理时间和请求ID设置自定义响应头。
将触发的端点URL、请求日期时间和ID、客户端IP地址、响应处理时间和状态码记录到磁盘上的CSV文件中(以附加模式)。
在本节中,您捕获了关于端点使用的信息,包括处理时间、状态码、端点路径和客户端IP。
中间件是一个强大的系统,可以在请求传递给路由处理程序之前和响应发送给用户之前执行代码块。您看到了如何使用中间件来记录任何模型服务端点的模型使用情况。
在中间件中访问请求和响应体 如果您需要跟踪与模型的交互,包括提示和它们生成的内容,使用中间件进行日志记录比在每个处理程序中添加单独的记录器更高效。然而,您应当考虑到数据隐私和性能问题,因为用户可能会向您的服务提交敏感或大数据,这需要小心处理。
总结
在本章中,我们讨论了许多概念,下面让我们快速回顾一下所讨论的内容。 你了解了如何使用Streamlit包,在几行代码内下载、集成并通过简单的UI提供各种开源GenAI模型,这些模型来自Hugging Face仓库。你还回顾了几种类型的模型以及如何通过FastAPI端点为它们提供服务。你实验的模型包括文本、图像、音频、视频和基于3D的模型,并了解了它们如何处理数据。你还学习了这些模型的架构和支撑它们的底层机制。
接着,你回顾了几种不同的模型服务策略,包括按需切换模型、预加载模型,最后是使用BentoML等其他框架或第三方API在FastAPI应用程序外提供模型服务。
然后,你注意到较大的模型可能需要一些时间来生成响应。最后,你为模型实现了一个服务监控机制,利用FastAPI中间件系统对每个模型服务端点进行监控。你随后将日志写入磁盘以供未来分析。
现在,你应该更加自信地构建自己的GenAI服务,并能够使用多种开源模型。
在下一章中,你将了解更多关于类型安全的内容,以及它在消除应用程序错误和减少与外部API及服务交互时的不确定性方面的作用。你还将学习如何验证请求和响应的模式,以使你的服务更加可靠。
附加参考文献
- "Bark",在"Transformers"文档中,Hugging Face,访问于2024年3月26日。
- Borsos, Z. 等(2022)。"AudioLM:一种用于音频生成的语言建模方法"。arXiv预印本 arXiv:2209.03143。
- Brooks, T. 等(2024)。"视频生成模型作为世界模拟器"。OpenAI。
- Défossez, A. 等(2022)。"高保真神经音频压缩"。arXiv预印本 arXiv:2210.13438。
- Jun, H. & Nichol, A.(2023)。"Shap-E:生成条件3D隐式函数"。arXiv预印本 arXiv:2305.02463。
- Kim, B.-K. 等(2023)。"BK-SDM:一个轻量级、快速且便宜的稳定扩散版本"。arXiv预印本 arXiv:2305.15798。
- Liu, Y. 等(2024)。"Sora:关于大型视觉模型的背景、技术、局限性和机会的综述"。arXiv预印本 arXiv:2402.17177。
- Mildenhall, B. 等(2020)。"NeRF:将场景表示为神经辐射场以进行视图合成"。arXiv预印本 arXiv:2003.08934。
- Nichol, A. 等(2022)。"Point-E:一个从复杂提示生成3D点云的系统"。arXiv预印本 arXiv:2212.08751。
- Vaswani, A. 等(2017)。"Attention Is All You Need"。arXiv预印本 arXiv:1706.03762。
- Wang, C. 等(2023)。"神经编解码语言模型是零-shot文本到语音合成器"。arXiv预印本 arXiv:2301.02111。
- Zhang, P. 等(2024)。"TinyLlama:一个开源的小型语言模型"。arXiv预印本 arXiv:2401.02385。