大语言模型生产环境指南(四)
将编码模型部署到 API在本地设置 VectorDB 并使用它作为检索增强生成系统构建一个 VS Code 扩展来使用我们的 LLM 服务从项目中获得的见解和经验教训如果您在工作中接触代码,您可能梦想过有一个 AI 助手来帮助您。实际上,也许您已经这样做了。随着市场上出现像 GitHub Copilot 这样的工具,我们已经看到 LLM 将自动完成提升到了新的水平。然而,并不是每个公司都对市场上的
原文:
zh.annas-archive.org/md5/94f58343f3d2d249c1a031ec6302a835
译者:飞龙
第十章:创建一个编码助手项目:这会早些时候帮到您
本章涵盖
-
将编码模型部署到 API
-
在本地设置 VectorDB 并使用它作为检索增强生成系统
-
构建一个 VS Code 扩展来使用我们的 LLM 服务
-
从项目中获得的见解和经验教训
进步不是来自早起的人——进步是由寻找更简单做事方式的懒惰人创造的。——罗伯特·海因莱因
如果您在工作中接触代码,您可能梦想过有一个 AI 助手来帮助您。实际上,也许您已经这样做了。随着市场上出现像 GitHub Copilot 这样的工具,我们已经看到 LLM 将自动完成提升到了新的水平。然而,并不是每个公司都对市场上的产品感到满意,也不是每个爱好者都能负担得起。所以,让我们自己动手吧!
在本章中,我们将构建一个 Visual Studio Code (VS Code) 扩展,使我们能够在代码编辑器中使用我们的 LLM。首选的编辑器将是 VS Code,因为它是一个流行的开源代码编辑器。流行可能是一个低估,因为 Stack Overflow 2023 开发者调查表明它是 81% 开发者的首选编辑器。¹ 它基本上是 Visual Studio 的轻量级版本,Visual Studio 是一个自 1997 年以来一直存在的完整 IDE。
除了选择一个特定的编辑器,我们还将做出一些明智的决定来限制项目的范围,使其更有意义。例如,在上一个项目中,我们专注于构建一个可以部署的出色的 LLM 模型。在这个项目中,我们将从已经训练过编码问题的开源模型开始。为了定制它,我们不会微调,而是围绕它构建一个 RAG 系统,这将使我们更容易保持其更新。此外,由于我们不会训练自己的模型,我们将专注于构建一个擅长 Python 的助手,这是我们在整本书中使用的语言,而不用担心其他所有语言。
现在我们已经清楚地知道了我们要构建什么,并且有一个目标在心中,让我们开始吧!
10.1 我们的模型
由于我们只关注 Python,我们决定使用 DeciCoder 作为我们的模型。DeciCoder 是一个只有 1B 参数的商业开源模型。² 尽管它的体积很小,但它确实擅长它所做的事情。它是在 Stack 数据集上训练的,但过滤后只包含 Python、Java 和 JavaScript 代码。它只训练了三种语言,这通常是一个限制,但实际上它是它之所以如此出色的秘密之一。
一些其他需要注意的限制是,它只有一个 2,048 个标记的上下文窗口,对于一个这个规模的模型来说并不差,但当我们考虑到我们计划使用 RAG 系统,并需要给它提供代码示例时,它就显得相对较小了。代码示例通常相当大,这限制了我们可以做的事情和我们可以提供的示例数量。
使用 RAG 与 DeciCoder 的大问题是模型没有进行指令微调。相反,它是为了击败 HumanEval 数据集(github.com/openai/human-eval
)而设计的。在这个评估数据集中,模型只给出一个函数名和描述函数应该做什么的文档字符串。仅从这个输入,模型将生成可执行的代码来完成函数。因此,很难知道从 RAG 系统中提供更多上下文是否会帮助模型,但我们将继续尝试找出答案!
最后,它的小巧体积实际上使它成为另一个有趣的选择。因为它如此小巧,我们有可能将模型直接放入我们正在构建的 VS Code 扩展程序中,使用我们在其他章节中讨论的编译方法。这将使我们能够构建一个非常紧凑的应用程序!我们在这本书中不会这样做,主要是因为这将要求我们编写大量的 JavaScript。这是一个问题,因为我们只期望我们的读者熟悉 Python,所以在这里深入解释细节有点过于冒险,但我们将其留作对 JavaScript 高手读者的练习。
我们将要做的是将我们的模型作为 API 提供服务,您可以在本地运行它,并且可以从扩展程序中调用它。在列表 10.1 中,我们创建了一个简单的 FastAPI 服务来提供我们的模型。实际上,您在第六章中已经看到了大部分代码,我们只做了一些小的修改。第一个修改是我们将代码更改为使用 DeciCoder 模型和分词器。第二个修改稍微复杂一些,但我们添加了 stop
标记。这些标记会通知模型在遇到它们时停止生成。这是通过创建一个 StoppingCriteria
类来实现的。我们选择的标记在我们定义了提示后会有更多的意义,但本质上,我们希望模型一次创建一个函数。
列表 10.1 使用 DeciCoder 的简单 FastAPI 端点
import argparse
from fastapi import FastAPI, Request
from fastapi.responses import Response
import torch
import uvicorn
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
StoppingCriteria,
StoppingCriteriaList,
)
torch.backends.cuda.enable_mem_efficient_sdp(False) #1
torch.backends.cuda.enable_flash_sdp(False)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
stop_tokens = ["def", "class", "Instruction", "Output"] #2
stop_token_ids = [589, 823, 9597, 2301]
class StopOnTokens(StoppingCriteria):
def __call__(
self,
input_ids: torch.LongTensor,
scores: torch.FloatTensor,
**kwargs,
) -> bool:
stop_ids = stop_token_ids
for stop_id in stop_ids:
if input_ids[0][-1] == stop_id:
return True
return False
tokenizer = AutoTokenizer.from_pretrained("Deci/DeciCoder-1b") #3
tokenizer.add_special_tokens( #3
{"additional_special_tokens": stop_tokens}, #3
replace_additional_special_tokens=False, #3
) #3
model = AutoModelForCausalLM.from_pretrained( #3
"Deci/DeciCoder-1b", torch_dtype=torch.bfloat16, trust_remote_code=True #3
) #3
model = model.to(device) #3
app = FastAPI() #4
@app.post("/generate")
async def generate(request: Request) -> Response:
"""Generate LLM Response
The request should be a JSON object with the following fields:
- prompt: the prompt to use for the generation.
"""
request_dict = await request.json()
prompt = request_dict.pop("prompt")
# ... #5
inputs = tokenizer(prompt, return_tensors="pt").to(device) #6
response_tokens = model.generate(
inputs["input_ids"],
max_new_tokens=1024,
stopping_criteria=StoppingCriteriaList([StopOnTokens()]),
)
input_length = inputs["input_ids"].shape[1]
response = tokenizer.decode(
response_tokens[0][input_length:], skip_special_tokens=True
)
return response
if __name__ == "__main__":
parser = argparse.ArgumentParser() #7
parser.add_argument("--host", type=str, default=None)
parser.add_argument("--port", type=int, default=8000)
args = parser.parse_args()
uvicorn.run(app, host=args.host, port=args.port, log_level="debug")
#1 火炬设置
#2 定义停止行为
#3 加载分词器和模型
#4 运行 FastAPI
#5 RAG 将在这里。
#6 生成响应
#7 启动服务;默认端口为 8000
假设这个列表在一个名为 server.py 的 Python 脚本中,您可以通过运行 $
python
server.py
来启动服务器。一旦它启动并运行,让我们发送一个请求来确保它正确工作。在一个新的终端中,我们可以使用简单的提示向 API 发送一个 curl
请求:
$ curl --request POST --header "Content-Type: application/json" --data
↪ '{"prompt":"def hello_world(name):"}' http://localhost:8000/generate
响应应该是一个简单的 Python 函数,用于完成“Hello World”函数。我们从服务器收到的响应是 return
f"Hello
{name}!"
。到目前为止,一切顺利!接下来,我们将定制 API 以利用 RAG 系统。
10.2 数据为王
现在我们已经决定了一个模型,让我们为我们的 RAG 系统准备一个数据集。RAG 是在不需要微调模型的情况下向模型引入上下文的有效方法;它还允许我们根据我们的数据自定义结果。本质上,如果你想让你的模型了解你组织不断变化的代码库的上下文,RAG 是一个很好的系统。有一个擅长编码的模型很好,但我们希望它擅长 我们的 代码。我们希望它使用正确的变量名和导入内部构建的定制依赖项——诸如此类的事情。在本节中,我们将设置一个 VectorDB,上传一个 Python 编码数据集,然后更新我们刚刚构建的 API 以利用它。
10.2.1 我们的 VectorDB
在我们真正深入数据集之前,我们首先需要设置我们的基础设施。当然,如果你的数据集足够小,你可以将其加载到内存中,并使用 Faiss 或 USearch 等工具直接在 Python 中运行相似度搜索,但那样有什么乐趣呢?此外,我们还想向你展示 Milvus。
Milvus 是一个出色的开源 VectorDB,与这个领域的大玩家竞争。你可以在本地或大型云集群上运行它,因此它可以很容易地扩展到你的需求。如果你不想处理设置,还有可管理的 Milvus 集群可供选择。我最喜欢的功能是其支持 GPU 的版本,这使得向量搜索变得非常快。
幸运的是,社区也使 Milvus 非常易于接近和设置。事实上,独立版本只需要 Docker 来运行,并附带一个启动脚本,使其更加容易。由于我们将在本项目中本地运行一切,我们将使用独立版本(了解更多信息,请参阅 mng.bz/aVE9
)。为此,我们需要在终端中运行以下命令:
$ wget https://raw.githubusercontent.com/milvus-io/milvus/master/scripts/
↪ standalone_embed.sh
$ bash standalone_embed.sh start
第一条命令将会下载一个 shell 脚本,第二条命令将会运行它。这个脚本实际上只是为了方便,因为 Docker 的 run
命令相当长。它还包括两个你应该了解的额外命令。Stop
命令,它将停止你的 Milvus Docker 容器,是
$ bash standalone_embed.sh stop
delete
命令,当你不再希望保留数据时,将会从你的电脑中删除所有数据,它是
$ bash standalone_embed.sh delete
你现在不需要运行这些命令,但记住它们,等我们完成后再用。现在我们已经设置了数据库,让我们让它变得有用,并将一些数据加载到其中。
10.2.2 我们的数据集
如果这是一个研讨会,我们会向你展示如何编写一个脚本来从 GitHub 拉取你组织的代码,并使用它来增强你的提示。我们甚至可以设置一个 GitHub Actions 流水线,以便每次代码合并到主分支时更新我们的 VectorDB。但由于我们没有访问你的代码,这只是一个书籍,我们将做合理的事情并使用开源数据集。
我们将为我们的项目选择 Alpaca 数据集。Alpaca 数据集是由斯坦福大学在训练同名模型时编译的,使用了蒸馏和 GPT-3 作为导师模型。由于它是合成数据,数据集非常干净,这使得它易于处理。事实上,它如此简单,以至于网上的多个副本已经过滤掉了所有的 Python 代码示例。这个子集包含 18.6K 个 Python 编码挑战,包括一个任务或指令和生成的代码——非常适合我们想要达成的目标。
在列表 10.2 中,我们创建我们的管道来将数据集加载到 Milvus 中。我们创建了一个PythonCodeIngestion
类来处理我们的数据集分块和批量上传的细节。请注意,我们使用了krlvi/sentence-t5-base-nlpl-code_search_
net
嵌入模型。这个嵌入模型已经在 CodeSearchNet 数据集(github.com/github/CodeSearchNet
)上进行了专门训练,非常适合创建代码的有意义嵌入。
列表 10.2:摄取 Alpaca 的数据管道
from pymilvus import (
connections,
utility,
FieldSchema,
CollectionSchema,
DataType,
Collection,
)
from transformers import AutoTokenizer
from datasets import load_dataset
from langchain.text_splitter import RecursiveCharacterTextSplitter
from sentence_transformers import SentenceTransformer
from tqdm.auto import tqdm
from uuid import uuid4
connections.connect("default", host="localhost", port="19530") #1
class PythonCodeIngestion:
def __init__(
self,
collection,
python_code=None,
embedder=None,
tokenizer=None,
text_splitter=None,
batch_limit=100,
):
self.collection = collection
self.python_code = python_code or load_dataset(
"iamtarun/python_code_instructions_18k_alpaca",
split="train",
)
self.embedder = embedder or SentenceTransformer(
"krlvi/sentence-t5-base-nlpl-code_search_net"
)
self.tokenizer = tokenizer or AutoTokenizer.from_pretrained(
"Deci/DeciCoder-1b"
)
self.text_splitter = (
text_splitter
or RecursiveCharacterTextSplitter(
chunk_size=400,
chunk_overlap=20,
length_function=self.token_length,
separators=["\n\n", "\n", " ", ""],
)
)
self.batch_limit = batch_limit
def token_length(self, text):
tokens = self.tokenizer.encode(text)
return len(tokens)
def get_metadata(self, page):
return {
"instruction": page["instruction"],
"input": page["input"],
"output": page["output"],
}
def split_texts_and_metadatas(self, page):
basic_metadata = self.get_metadata(page)
prompts = self.text_splitter.split_text(page["prompt"])
metadatas = [
{"chunk": j, "prompt": prompt, **basic_metadata}
for j, prompt in enumerate(prompts)
]
return prompts, metadatas
def upload_batch(self, texts, metadatas):
ids = [str(uuid4()) for _ in range(len(texts))]
embeddings = self.embedder.encode(texts)
self.collection.insert([ids, embeddings, metadatas])
def batch_upload(self):
batch_texts = []
batch_metadatas = []
for page in tqdm(self.python_code):
texts, metadatas = self.split_texts_and_metadatas(page)
batch_texts.extend(texts)
batch_metadatas.extend(metadatas)
if len(batch_texts) >= self.batch_limit:
self.upload_batch(batch_texts, batch_metadatas)
batch_texts = []
batch_metadatas = []
if len(batch_texts) > 0:
self.upload_batch(batch_texts, batch_metadatas)
self.collection.flush()
#1 连接到 Milvus
现在我们已经创建了我们的摄取类,我们可以继续进行管道操作。首先,如果这是我们第一次运行它,我们需要创建我们的集合。集合在其他数据库中就像一个表,或者在 Pinecone 中就像一个索引。我们将定义我们的模式,它只是一个 ID 字段、我们的嵌入字段和一个元数据字段,该字段包含自由形式的 JSON。一旦设置好,我们将使用我们的PythonCodeIngestion
类上传我们的数据。
接下来,我们需要创建我们的搜索索引。我们将使用的索引类型是IVF_FLAT
,这是 Milvus 中最基本的索引,将嵌入空间分割成nlist
数量的聚类。这通过首先将我们的搜索嵌入与聚类中心进行比较,然后与它最近的聚类中的嵌入进行比较来加速相似性搜索。我们还将使用L2
作为我们的度量类型,这意味着我们将使用欧几里得距离。这些是常见的设置,但对我们来说不需要任何特殊设置。Milvus 在构建索引时支持更多的选项,我们鼓励您查看他们的文档:
if __name__ == "__main__":
collection_name = "milvus_llm_example"
dim = 768
if utility.has_collection(collection_name): #1
utility.drop_collection(collection_name)
fields = [
FieldSchema(
name="ids",
dtype=DataType.VARCHAR,
is_primary=True,
auto_id=False,
max_length=36,
),
FieldSchema(
name="embeddings", dtype=DataType.FLOAT_VECTOR, dim=dim
),
FieldSchema(name="metadata", dtype=DataType.JSON),
]
schema = CollectionSchema(
fields, f"{collection_name} is collection of python code prompts"
)
print(f"Create collection {collection_name}")
collection = Collection(collection_name, schema)
collection = Collection(collection_name) #2
print(collection.num_entities)
python_code_ingestion = PythonCodeIngestion(collection) #3
python_code_ingestion.batch_upload()
print(collection.num_entities)
search_index = { #4
"index_type": "IVF_FLAT",
"metric_type": "L2",
"params": {"nlist": 128}, #5
}
collection.create_index("embeddings", search_index)
#1 如果不存在则创建集合
#2 连接到集合并显示其大小
#3 数据摄取并显示数据摄取后的统计数据
#4 构建搜索索引
#5 聚类的数量
现在我们已经设置好了一切,我们可以继续下一步。但在那之前,让我们通过运行一个查询来测试它。我们想要确保我们的数据和索引给我们提供了合理的搜索结果。使用 Milvus,我们首先将集合加载到内存中,并使用我们的嵌入器将我们的查询转换为嵌入。接下来,我们将定义一些搜索参数。再次,L2
代表欧几里得距离,而nprobe
参数表示要搜索的簇数量。在我们的情况下,在 128 个我们设置的簇中,我们将搜索与我们的查询嵌入最近的 10 个簇。最后,在实际搜索中,我们将结果限制为三个最佳匹配,并返回与我们的查询一起的元数据字段:
collection.load() #1
query = ( #2
"Construct a neural network model in Python to classify "
"the MNIST data set correctly."
)
search_embedding = python_code_ingestion.embedder.encode(query)
search_params = {
"metric_type": "L2",
"params": {"nprobe": 10}, #3
}
results = collection.search(
[search_embedding],
"embeddings",
search_params,
limit=3,
output_fields=["metadata"],
)
for hits in results:
for hit in hits:
print(hit.distance)
print(hit.entity.metadata["instruction"])
#1 在进行搜索之前,你需要将数据加载到内存中。
#2 进行查询
#3 搜索的簇数量
你可以看到,对于我们的查询,搜索结果正在返回来自我们数据集的强候选者:
# 0.7066953182220459
# Create a neural network in Python to identify
# hand-written digits from the MNIST dataset.
# 0.7366453409194946
# Create a question-answering system using Python
# and Natural Language Processing.
# 0.7389795184135437
# Write a Python program to create a neural network model that can
# classify handwritten digits (0-9) with at least 95% accuracy.
现在我们已经设置了我们的 VectorDB 并加载了数据,让我们更新我们的 API 以从我们的 RAG 系统中检索结果并将上下文注入到我们的提示中。
10.2.3 使用 RAG
在本节中,我们将更新列表 10.1 以包含我们的检索代码。在列表 10.3 中,我们不会重复之前所做的所有事情,考虑到时间和空间,我们只需展示需要添加的新部分。在伴随本书的仓库中,如果你在理解哪些部分应该放在哪里时遇到困难,你将能够找到将一切组合在一起的代码。首先,在脚本接近顶部的地方,我们需要添加我们的导入,连接到我们的 Milvus 服务,并加载我们的嵌入模型。
列表 10.3 将 RAG 添加到我们的 API
from contextlib import asynccontextmanager
from pymilvus import (
connections,
Collection,
)
from sentence_transformers import SentenceTransformer
connections.connect("default", host="localhost", port="19530") #1
collection_name = "milvus_llm_example"
collection = Collection(collection_name)
embedder = SentenceTransformer( #2
"krlvi/sentence-t5-base-nlpl-code_search_net"
)
embedder = embedder.to(device)
#1 连接到 Milvus
#2 加载我们的嵌入模型
接下来,我们将添加一些便利函数,包括一个标记计数器和 FastAPI 生命周期,以确保我们加载和释放我们的 Milvus 集合从内存。由于我们添加了一个生命周期,请确保更新 FastAPI 调用:
def token_length(text):
tokens = tokenizer([text], return_tensors="pt")
return tokens["input_ids"].shape[1]
@asynccontextmanager
async def lifespan(app: FastAPI):
collection.load() #1
yield
collection.release() #2
app = FastAPI(lifespan=lifespan) #3
#1 启动时加载集合
#2 关闭时从内存中释放集合
#3 运行 FastAPI
现在我们已经设置好了一切,我们可以进入下一步——运行查询并更新我们的generate
端点的提示。第一部分应该看起来很熟悉,因为我们刚刚做过。我们将编码用户的提示并在我们的集合中搜索最近的邻居。我们使用与之前相同的所有搜索参数,除了一个。我们将限制从3
增加到5
,以便可能将更多示例添加到我们的提示中。接下来,我们将这些结果格式化为几个提示示例数据集。然后我们创建指令提示并格式化用户的输入。
我们几乎到了可以结合我们的指令、示例和用户提示的阶段;然而,我们需要确保我们的示例不会占用太多空间。使用一个利用我们的标记计数器的for
循环,我们将过滤掉任何不符合我们上下文窗口的示例。有了这个,我们现在可以结合所有内容,为我们的 DeciCoder 模型创建最终的提示:
request_dict = await request.json() #1
prompt = request_dict.pop("prompt")
search_embedding = embedder.encode(prompt) #2
search_params = {
"metric_type": "L2",
"params": {"nprobe": 10},
}
results = collection.search(
[search_embedding],
"embeddings",
search_params,
limit=5,
output_fields=["metadata"],
)
examples = []
for hits in results:
for hit in hits:
metadata = hit.entity.metadata
examples.append(
f"Instruction: {metadata['instruction']}\n"
f"Output: {metadata['output']}\n\n"
)
prompt_instruction = (
"You are an expert software engineer who specializes in Python. "
"Write python code to fulfill the request from the user.\n\n"
)
prompt_user = f"Instruction: {prompt}\nOutput: "
max_tokens = 2048
token_count = token_length(prompt_instruction+prompt_user)
prompt_examples = ""
for example in examples:
token_count += token_length(example)
if token_count < max_tokens:
prompt_examples += example
else:
break
full_prompt = f"{prompt_instruction}{prompt_examples}{prompt_user}"
inputs = tokenizer(full_prompt, return_tensors="pt").to(device)
#1 生成函数内部
#2 进行查询
好的!现在我们已经更新了我们的 API,让我们像之前一样启动并测试它。我们将向服务器发送另一个请求以确保一切仍然正常工作:
$ curl --request POST --header "Content-Type: application/json" --data
↪ '{"prompt":"def hello_world(name):"}' http://localhost:8000/generate
这次我们得到了一个print(“Hello,
World!”)
的响应,这比我们之前的响应略差,但仍然属于同一类型,所以不必担心。你可能会得到类似的结果。这样,我们就完成了使用 RAG 系统进行定制的 LLM 服务的设置。我们现在需要做的就是调用它。
10.3 构建 VS Code 扩展
好的,现在我们只需要构建我们的 VS Code 扩展。VS Code 扩展主要用 TypeScript 或 JavaScript(JS)编写。如果您不熟悉这些语言,不要担心;我们会引导您完成。要开始,您需要安装 Node 和 npm。Node 是 JS 解释器,npm 类似于 JS 的 pip。您可以通过多种方式添加这些工具,但我们建议首先安装 nvm 或其他节点版本管理器。此时更新您的 VS Code(如果您还没有安装,请安装它)也是一个好主意。更新您的编辑器将帮助您避免许多问题,所以请确保这样做。从这里,我们可以安装 VS Code 扩展模板生成器:
$ npm install -g yo generator-code
注意:您可以在以下位置找到安装 nvm 的说明:mng.bz/gAv8
。然后只需运行nvm
install
node
来安装最新版本的 Node 和 npm。
模板生成器将为我们创建一个基本的“Hello World”项目仓库,我们可以用它作为构建的基础。要运行生成器,请使用
$ yo code
此命令将在您的终端中启动一个向导,您将看到一个看似 ASCII 艺术形式的加拿大骑警形象,他会问您几个问题以定制正在生成的脚手架。
在图 10.1 中,您可以看到我们对向导问题的选择示例。快速引导您通过问题,我们将创建一个新的 JavaScript 扩展,您可以根据喜好命名。我们选择了llm_coding_ copilot
,如果您想跟随我们的话。对于标识符,按 Enter 键,它将自动为您选择的名称添加连字符。给它一个描述;任何内容都可以。不,我们不想启用类型检查。您可以选择是否将项目初始化为新的 Git 仓库。我们选择了“否”,因为我们已经在其中一个仓库中工作了。最后,我们将使用 npm。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/10-1.png
图 10.1 VS Code 扩展生成器示例输入
完成后,它将生成一个包含我们所需所有文件的项目仓库。如果你查看图 10.2,你可以看到一个已构建项目仓库的示例。它有几个不同的配置文件,你可以熟悉一下,但我们只关心其中的两个文件:定义扩展清单的 package.json 文件,它告诉 VS Code 如何使用我们将构建的扩展(实际上,是扩展 VS Code),以及包含实际扩展代码的 extension.js 文件。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/10-2.png
图 10.2 使用 VS Code 扩展生成器创建的示例目录结构
在 package.json 文件中,模板代码几乎带我们完成了大部分工作,但 activationEvents
字段目前是空的,需要设置。这个字段告诉 VS Code 何时启动我们的扩展。通常情况下,当你打开 VS Code 时,扩展并不会被加载,这有助于保持其轻量级。如果没有设置,扩展只有在用户打开它时才会被加载,这可能会很麻烦。一个聪明的策略通常是只在用户打开我们关心的文件类型时加载扩展——例如,如果我们正在构建一个针对 Python 的特定扩展,它只有在打开 .py 文件时才会加载。
我们将使用 "onCommand:editor.action.inlineSuggest.trigger"
事件触发器。这个触发器在用户手动请求内联建议时触发。它通常在用户停止输入时触发,但我们希望对过程有更多的控制,以避免向我们的 LLM 服务发送不必要的请求。只有一个问题:VS Code 没有为用户手动执行此操作提供默认快捷键!幸运的是,我们也可以通过在 "contributes"
部分添加 "keybindings"
字段来设置它。我们将将其设置为 Alt+S
的快捷键。我们使用 S
代表“建议”以便于记忆;这个快捷键应该可用,除非另一个扩展正在使用它。用户始终可以自定义他们的快捷键。你可以在下面的列表中看到完成的 package.json 文件。它应该与我们从脚手架开始时非常相似。
列表 10.4 我们编码同伴的扩展清单
{
"name": "llm-coding-copilot",
"displayName": "llm_coding_copilot",
"description": "VSCode extension to add LLM code suggestions inline.",
"version": "0.0.1",
"engines": {
"vscode": "¹.86.0"
},
"categories": [
"Other"
],
"activationEvents": [
"onCommand:editor.action.inlineSuggest.trigger"
],
"main": "./extension.js",
"contributes": {
"commands": [{
"command": "llm-coding-copilot.helloWorld",
"title": "Hello World"
}],
"keybindings": [{
"key": "Alt+s",
"command": "editor.action.inlineSuggest.trigger",
"mac": "Alt+s"
}]
},
"scripts": {
"lint": "eslint .",
"pretest": "npm run lint",
"test": "vscode-test"
},
"devDependencies": {
"@types/vscode": "¹.86.0",
"@types/mocha": "¹⁰.0.6",
"@types/node": "18.x",
"eslint": "⁸.56.0",
"typescript": "⁵.3.3",
"@vscode/test-cli": "⁰.0.4",
"@vscode/test-electron": "².3.8"
}
}
现在我们已经有了扩展清单文件,让我们继续测试它。在你的 VS Code 项目仓库中,你可以按 F5 编译你的扩展并启动一个新的带有你的扩展的 VS Code 扩展开发主机窗口。在新窗口中,你应该能够按 Alt+S 触发内联建议。如果一切正常,那么你将在原始窗口中看到一个控制台日志,显示“恭喜你,你的扩展llm-coding-copilot
现在已激活!”,如图 10.3 所示。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/10-3.png
图 10.3 成功激活我们的 VS Code 扩展的示例控制台
好的,不错!我们现在可以运行我们的扩展并激活它,同时捕获日志,这对于调试很有帮助。现在我们只需要构建它,让我们将注意力转向 extension.js 文件。
到这一点,事情变得有点难以解释。即使对于熟悉 JavaScript 的读者来说,也很少有人熟悉 VS Code API (mng.bz/eVoG
)。在我们深入之前,让我们提醒自己我们正在构建什么。这将是一个 VS Code 扩展,它将为我们提供编码建议。我们已经在 API 后面训练了一个 LLM,该 API 已准备好供我们使用。我们有一个在 RAG 系统中加载的数据集,用于提供上下文并改进结果,并且我们已经精心制作了提示。我们只需要构建一个调用我们的 API 服务的扩展。但我们还希望有一种允许用户以简单方式与我们的模型交互的方法,同时给我们提供很多控制。我们将通过允许用户突出显示代码的一部分来实现这一点,当按下快捷键绑定 Alt+S 时,我们将发送这部分代码。
让我们看看生成器为我们创建的模板 extension.js 文件。列表 10.5 显示了经过简化注释的模板。它只是加载了 vscode 库,并定义了在启动扩展时运行的 activate
和 deactivate
函数。activate
函数演示了如何创建和注册一个新命令,但我们将不会使用它。我们将创建一个内联建议提供者并注册它,而不是命令。
列表 10.5 模板中的 boilerplate extension.js 文件
// Import VSCode API library
const vscode = require('vscode');
// This method is called when your extension is activated
function activate(context) {
console.log('Congratulations, your extension "llm-coding-copilot" is now
↪ active!');
// This creates and registers a new command, matching package.json
// But we won’t use it!
let disposable = vscode.commands.registerCommand('llm-coding-
↪ copilot.helloWorld', function () {
// The code you place here will be executed every time your command is
↪ executed
// Display a message box to the user
vscode.window.showInformationMessage('Hello World from llm_coding_
↪ copilot!');
});
context.subscriptions.push(disposable);
}
// This method is called when your extension is deactivated
function deactivate() {}
module.exports = {
activate,
deactivate
}
由于我们不会使用命令,让我们看看我们将使用什么,一个内联建议提供者。这个提供者将把我们的建议作为光标处的幽灵文本添加。这使用户能够预览生成的内容,然后使用制表符接受建议或通过其他操作拒绝它。本质上,它正在为我们构建的代码完成扩展中的用户界面做所有繁重的工作。
在列表 10.6 中,我们向您展示如何创建和注册一个提供者,该提供者返回内联完成项。它将是一个用户可能循环选择最佳选项的潜在项数组,但为了我们的扩展,我们将保持简单,只返回一个建议。提供者接收几个自动传入的参数,例如请求内联建议的文档、用户光标的位置、提供者被调用的上下文(手动或自动)以及一个取消令牌。最后,我们将注册提供者,告诉 VS Code 为哪些类型的文档调用它;在这里,我们给出将其注册到仅 Python 文件或添加到所有文件的示例。
列表 10.6 示例内联建议提供者
// Create inline completion provider, this makes suggestions inline
const provider = {
provideInlineCompletionItems: async (
document, position, context, token
) => {
// Inline suggestion code goes here
}
};
// Add provider to Python files
vscode.languages.registerInlineCompletionItemProvider(
{ scheme: 'file', language: 'python' },
provider
);
// Example of adding provider to all languages
vscode.languages.registerInlineCompletionItemProvider(
{ pattern: '**' },
provider
);
现在我们有了提供者,我们需要一种方法来抓取用户的突出显示文本,将其发送到 LLM 服务,并确保我们的提供者仅在通过快捷键手动触发时运行,而不是自动运行,这发生在用户停止输入的每次。在列表 10.7 中,我们在提供者内部添加了这个方程式的一部分。
首先,我们获取编辑窗口以及任何选中的或突出显示的内容。然后我们确定提供者是因为自动还是手动触发而被调用的。接下来,我们进行一个小技巧以获得更好的用户体验。如果我们的用户将代码从后向前突出显示,光标将位于他们的代码前端,我们的代码建议将不会显示。因此,我们将重新突出显示选择,这将光标置于末尾,并重新触发内联建议。幸运的是,这种重新触发也将被计算为手动触发。最后,如果一切顺利——内联建议是手动调用的,我们有突出显示的文本,并且光标位于正确的位置——那么我们将开始使用我们的 LLM 代码助手的过程,通过从选择中抓取突出显示的文本。
列表 10.7 使用 VS Code API 工作
// Create inline completion provider, this makes suggestions inline
const provider = {
provideInlineCompletionItems: async (
document, position, context, token
) => {
// Grab VSCode editor and selection
const editor = vscode.window.activeTextEditor;
const selection = editor.selection;
const triggerKindManual = 0
const manuallyTriggered = context.triggerKind == triggerKindManual
// If highlighted back to front, put cursor at the end and rerun
if (manuallyTriggered && position.isEqual(selection.start)) {
editor.selection = new vscode.Selection(
selection.start, selection.end
)
vscode.commands.executeCommand(
"editor.action.inlineSuggest.trigger"
)
return []
}
// On activation send highlighted text to LLM for suggestions
if (manuallyTriggered && selection && !selection.isEmpty) {
// Grab highlighted text
const selectionRange = new vscode.Range(
selection.start, selection.end
);
const highlighted = editor.document.getText(selectionRange);
// Send highlighted code to LLM
}
}
};
好的!现在我们已经将所有 VS Code 特定的代码处理完毕,我们只需要向我们的 LLM 服务发送一个请求。到这一点,这个动作应该感觉像是熟悉的领域;事实上,我们将使用我们在第七章中讨论过的代码。这里没有什么可怕的!在下一个列表中,我们通过抓取突出显示的文本并使用异步 fetch
请求将其发送到我们的 API 来完成提供者的构建。然后我们获取响应并将其返回给用户。
列表 10.8 向我们的编码助手发送请求
// On activation send highlighted text to LLM for suggestions
if (manuallyTriggered && selection && !selection.isEmpty) {
// Grab highlighted text
const selectionRange = new vscode.Range(
selection.start, selection.end
);
const highlighted = editor.document.getText(
selectionRange
);
// Send highlighted text to LLM API
var payload = {
prompt: highlighted
};
const response = await fetch(
'http://localhost:8000/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
// Return response as suggestion to VSCode editor
var responseText = await response.text();
range = new vscode.Range(selection.end, selection.end)
return new Promise(resolve => {
resolve([{ insertText: responseText, range }])
})
}
现在所有部件都已就绪,让我们看看它的实际效果。再次按 F5 重新编译您的扩展,启动另一个带有更新扩展的 VS Code 扩展开发主机窗口。创建一个以 .py 扩展名的新 Python 文件,并开始编写一些代码。准备好后,突出显示您希望获得助手帮助的部分,然后按 Alt+S 获取建议。过了一会儿,你应该会看到一些带有助手建议的幽灵文本弹出。如果您喜欢它,按 Tab 键接受。图 10.4 展示了我们的 VS Code 扩展在实际应用中的示例。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/10-4.png
图 10.4 成功激活我们的 VS Code 扩展的示例控制台
恭喜!你做到了!你创建了自己的编码助手!它运行在你自己的数据上,并且完全是本地的——如果你在开始这本书时对 LLM 一无所知,这是一个相当大的成就。在下一节中,我们将讨论下一步以及从这个项目中得到的经验教训。
10.4 经验教训和下一步
现在我们有了可以工作的代码,我们可以把它叫作一天的工作。然而,我们的项目还远未完成;我们还有许多可以做的事情!首先,结果看起来并不那么好。回顾一下图 10.4,生成的代码并没有反转链表,而是反转了一个普通的列表。这不是我们想要的。我们能做些什么来改进它呢?
好吧,首先,记得我们发送给 API 测试的“Hello World”函数吗?看起来在我们添加 RAG 之前使用模型时,我们得到了更好的结果。为了好玩,让我们启动我们旧的 API,禁用 RAG,看看使用我们的 VS Code 扩展时我们会得到什么。图 10.5 显示了使用此 API 的一个示例结果。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/10-5.png
图 10.5 使用 DeciCoder(无 RAG)的扩展结果
哇!这段代码看起来好多了!它实际上反转了链表,并且已经格式化得很好,以至于你甚至不需要编辑或格式化它。这里发生了什么?模型不是应该在我们给出它们一些我们希望它们如何表现示例后生成更好的结果吗?也许我们的 RAG 系统没有找到很好的示例。让我们深入挖掘,看看我们的 RAG 系统生成的提示信息。
MA指令:用 7 行 Python 代码反转单链表的最有效方法是什么?
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/chatGpt.png输出:# 单链表的定义。
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def reverseList(head):
prev = None
current = head
while current is not None:
nxt = current.next
current.next = prev
prev = current
current = nxt
head = prev
return head
MA指令:在 Python 中反转链表的最有效方法是什么?
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/chatGpt.png输出:
def reverse(head):
prev = None
current = head
while current:
next = current.next
current.next = prev
prev = current
current = next
return prev
MA指令:def reverse_linked_list(list):
反转链表
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/chatGpt.png输出:
哇!这些例子看起来非常准确!那么到底可能发生了什么呢?
好吧,首先,再看看提示信息。我们数据集中的示例指令是用普通英语表述的任务,但用户将要发送的提示信息是半写好的代码。如果我们的用户用普通英语来写,我们可能会得到更好的结果。当然,当我们的用户在编辑器中编码时,这可能会是一种有点尴尬的经历。在难以理解的部分写代码并寻求帮助会更自然。
第二,记得我们关于 DeciCoder 训练的笔记吗?它是被训练来击败 HumanEval 数据集的,所以它非常擅长将代码作为输入并生成代码作为输出。这使得它从一开始就非常适合这项任务,而无需进行提示调整。更重要的是,它还没有被指令调整过!当它看到我们的少量示例时可能会有些困惑,因为它在训练期间没有看到过这样的输入。作为一个为特定目的训练的小型模型,它并不擅长泛化到新的任务。
从这个例子中,有几个关键点需要强调。首先,虽然提示调整是一种强大的技术,可以用于为新任务定制 LLM,但它本身所能达到的成就仍然有限,即使在使用 RAG 系统提供高度相关示例的情况下。必须考虑模型是如何训练或微调的,以及它接触到了哪些数据。此外,考虑用户如何与模型交互,以确保您正确地构建了提示。
那么,您可以尝试哪些下一步来提高结果?在这个阶段,事情似乎大多已经正常工作,所以我们可能会尝试的第一件事是调整我们的 RAG 系统中的提示。看起来用普通英语编写的指令数据对我们的模型并没有很大帮助,所以我们可以简单地尝试给模型提供示例代码,看看是否可以提高结果。接下来,我们可以尝试微调模型以采用指令数据集,或者完全寻找另一个模型。
除了让我们的应用程序工作得更好之外,可能还有很多下一步可以定制这个项目。例如,我们可以在 Milvus 中创建一个包含我们自己的代码数据集的集合。这样,我们就可以将相关代码的上下文注入到我们的代码库中的提示中。我们的模型不仅擅长编写通用的 Python 代码,还能编写针对我们工作的组织的特定代码。如果我们选择这条路,我们不妨将我们的 API 和 Milvus 数据库部署到生产服务器上,这样我们就可以为公司的其他工程师和数据科学家提供服务。
或者,我们可以放弃定制化想法,仅使用 DeciCoder,因为它似乎已经给出了很好的结果。无需定制。如果我们这样做,那么将模型编译为 GGUF 格式并通过 JavaScript SDK 直接在扩展中运行它将是有价值的。这样做将允许我们将所有代码封装在一个地方,并使其更容易分发和共享。
最后,您可能考虑发布扩展并与社区分享。目前,该项目尚未准备好分享,因为我们正在本地运行我们的模型和 RAG 系统,但如果您感兴趣,您可以在网上找到官方说明,网址为 mng.bz/GNZA
。它涵盖了从获取 API 密钥到打包、发布,甚至成为认证发布者的所有内容。
摘要
-
DeciCoder 是一个专为 Python、JavaScript 和 Java 中的编码任务设计的小巧但强大的模型。
-
Milvus 是一个强大的开源 VectorDB,可以扩展以满足您的需求。
-
您的数据集对于使您的 RAG 系统正常工作至关重要,因此请花时间对其进行清理和适当准备。
-
Visual Studio Code 是一款流行的编辑器,它使得构建扩展变得容易。
-
仅向您的模型抛出示例和数据,即使它们被精心策划,也不会使其生成更好的结果。
-
以一种考虑到模型训练方法和数据的方式来构建提示,以最大化结果。
[1] D. Ramel, “Stack Overflow 开发者调查:VS Code 和 Visual Studio 连续 5 年位居顶级 IDE,” Visual Studio Magazine,2023 年 6 月 28 日,mng.bz/zn86
.
[2] Deci, “推出 DeciCoder:高效且精确的代码生成新标准,” 2023 年 8 月 15 日,mng.bz/yo8o
.
第十一章:在树莓派上部署一个 LLM:你能做到多低?
本章涵盖
-
在你的本地网络上设置树莓派服务器
-
将模型转换为 GGUF 格式并进行量化
-
将你的模型作为 OpenAI GPT 模型的直接替代品提供服务
-
接下来要做什么以及如何让它变得更好
低价格带来的甜蜜很快就会忘记,而低质量带来的苦涩却会久久萦绕。 ——本杰明·富兰克林
欢迎来到我们在这个列表中最喜欢的项目之一:在一个比它原本应该服务的设备还要小的设备上运行一个 LLM。在这个项目中,我们将推动这项技术的极限。通过跟随这个项目,你将能够真正地运用在这本书中学到的所有知识。在这个项目中,我们将把一个 LLM 部署到树莓派上,我们将将其设置为 LLM 服务,你可以从家中网络上的任何设备查询。对于所有黑客来说,这个练习应该会打开许多家庭项目的门。对于其他人来说,这是一个巩固你对使用 LLMs 局限性的理解并欣赏使这一切成为可能的社会的机会。
这是一个实用的项目。在本章中,我们将深入探讨的内容远不止 LLMs,而且不会有模型训练或数据聚焦,因此这是我们第一个真正只用于生产的项目。我们创建的内容将比你可能预期的要慢得多、效率低得多、准确性低得多,但这没关系。实际上,这是一个极好的学习经历。理解可能性和有用性之间的区别是许多人直到它狠狠地打在他们脸上之前都不会学到的东西。在树莓派上运行的 LLM 不是你想要在企业生产系统中部署的东西,但我们将帮助你学习其背后的原理,这样你最终可以扩展到你想要的任何大小。
11.1 设置你的树莓派
尽管困难重重,但在树莓派上提供服务和推理是可行的,尽管我们通常不推荐这样做,除非是为了展示你可以做到,这是警告的一个典型迹象,表明这是一个有趣的项目,就像找出你能把多少棉花糖塞进你弟弟的嘴里一样。仅仅玩弄树莓派本身在一般情况下就很有趣,我们希望这并不是你第一次玩。树莓派是家庭中出色的、便宜的伺服器。你可以用它们来阻止广告(Pi-Hole 是一个流行的库)或使用 Plex 和 Jellyfin 等服务流式传输你自己的个人媒体库。有很多有趣的项目。因为它完全可定制,如果你能写一个功能性的 Python 脚本,你很可能可以在你的本地网络服务器上运行它,这就是我们要为我们的 LLM 服务器所做的事情。
您只需要三样东西来完成这个项目:一个 8 GB RAM 的 Raspberry Pi,一个至少 32 GB 的 MicroSD 卡(更多更好),以及一个电源。在撰写本文时,我们找到了一些 1 TB 内存的 MicroSD 卡,价格为 20 美元,所以希望您能得到比 32 GB 更大的存储空间。您购买的其他任何东西都只是锦上添花——例如,为您的 Pi 购买一个外壳。如果您没有 Wi-Fi,您还需要一根以太网线将 Pi 连接到您的家庭网络。一旦我们设置好,我们将向您展示如何从笔记本电脑远程连接到 Pi。此外,如果您的笔记本电脑没有 MicroSD 插槽,您可能需要一个适配器来连接它。
对于 Raspberry Pi 本身,我们将在这个项目中使用 Raspberry Pi 5 8 GB 型号。如果您想跟随操作,我们使用的确切型号可以在以下链接找到:mng.bz/KDZg
。对于我们将部署的型号,您需要一个至少 8 GB RAM 的单板计算机来跟随操作。有趣的是,我们已经成功将模型部署到只有 4GB RAM 的小型 Pi 上,并且还有许多其他单板计算机作为 Raspberry Pi 的替代品可供选择。如果您选择不同的板子,可能更难精确跟随,所以只有当您信任该公司时才这样做。我们推荐的一些替代品包括 Orange Pi、Zima Board 和 Jetson,但我们不会详细介绍如何设置这些设备。
您不需要事先知道如何设置 Pi。我们将引导您完成所有步骤,假设这是您的第一个 Raspberry Pi 项目。Pi 实际上只是硬件和众多项目的开放沙盒,因此我们首先需要安装一个操作系统(OS)。之后,我们将安装必要的软件包和库,准备我们的 LLM,并将其最终作为一项服务提供,您可以从家庭网络中的任何计算机上 ping 它并获取生成的文本。
11.1.1 Pi Imager
首先,Pi 通常不预装操作系统,即使您的设备预装了,我们也会更换它。常见的发行版,如 Rasbian OS 或 Ubuntu,体积太大,占用太多 RAM,无法以最快速度运行模型。为了帮助我们克服这一限制,Raspberry Pi 的制造商发布了一款名为 Pi Imager 的免费镜像软件,您可以从以下链接在您的笔记本电脑上下载:www.raspberrypi.com/software/
。如果您已经有了镜像软件,我们建议将其更新到 1.8 版本以上,因为我们正在使用 Pi 5。
一旦您有了它,将 microSD 插入您下载 Pi Imager 程序的电脑。如果您不确定如何操作,请在网上搜索 USB 3.0 microSD 卡读卡器。打开镜像程序,选择设备;对我们来说,那就是 Raspberry Pi 5。这个选择将限制操作系统选项仅限于适用于 Pi 5 的选项。然后您可以选择 Raspberry Pi OS Lite 64 位作为您的操作系统。“Lite”是您要找的关键词,您可能需要在 Raspberry Pi OS(其他)子部分中找到它。然后选择您的 microSD 作为存储设备。实际名称将根据您的设置而有所不同。图 11.1 展示了带有正确设置的镜像软件示例。作为备注,Ubuntu Server 也是一个适合我们项目的良好操作系统,我们推荐它。它的设置会有所不同,所以如果您想跟上来,请坚持使用 Raspberry Pi OS Lite。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/11-1.png
图 11.1 将 Raspberry Pi Imager 设置为正确的设备,选择了无头(Lite)操作系统和正确的 USB 存储设备
警告 并且作为警告,请确保您已选择了 microSD 来镜像操作系统——请勿选择您的主硬盘。
准备就绪后,通过选择“下一步”按钮进行导航,您应该会看到一个关于操作系统定制的提示,如图 11.2 所示。我们将设置它,所以点击“编辑设置”按钮,您应该会看到一个设置页面。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/11-2.png
图 11.2 自定义我们的 Raspberry Pi OS 设置。选择“编辑设置”。
图 11.3 展示了设置页面的示例。我们将在项目名称之后为 Pi 服务器设置一个主机名,llmpi。我们将设置用户名和密码,并配置 Wi-Fi 设置以连接到我们的家庭网络。这可能是最重要的步骤,所以请确保您已为互联网设置好,无论是通过在设置中设置 Wi-Fi 连接还是通过以太网。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/11-3.png
图 11.3 设置页面截图示例,包含正确和相关信息
与设置互联网一样重要的是,我们希望启用 SSH,否则后续的步骤将无法工作。为此,转到“服务”选项卡并选择“启用 SSH”,如图 11.4 所示。我们将使用密码认证,所以请确保您已设置了合适的用户名和密码,并且没有使用默认设置。您不希望任何有恶意的人轻易访问您的 Pi。
到目前为止,我们已经准备好创建镜像。按照提示操作,镜像程序将把操作系统安装到您的 SD 卡上。这个过程可能需要几分钟,但通常很快就会完成。一旦您的 SD 卡上有了操作系统,您就可以安全地从笔记本电脑上取下它。将 microSD 卡插入您的 Pi 中,并打开它!如果一切操作正确,您的 Pi 应该会自动启动并连接到您的 Wi-Fi。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/11-4.png
图 11.4 确保你选择了启用 SSH。
11.1.2 连接到树莓派
我们将使用我们的小树莓派作为一个小型服务器。我们设置的好处是,你不需要为树莓派找到额外的显示器或键盘。当然,这种设置有一个明显的缺点,那就是我们无法看到树莓派在做什么,也没有明显的方法与之交互。别担心;这就是我们设置 SSH 的原因。现在我们将向你展示如何从你的笔记本电脑连接到你的树莓派。
我们首先需要做的是找到树莓派的 IP 地址。IP 地址是一个数字标签,用于在网络中标识一台计算机。查看你使用的互联网上连接的新设备的最简单方法是通过路由器的软件。见图 11.5。如果你可以访问你的路由器,你可以在浏览器中访问其 IP 地址。IP 地址通常是 192.168.86.1 或 192.168.0.1;路由器的类型通常设置这个数字,并且通常可以在路由器本身上找到。然后你需要登录到你的路由器,在那里你可以看到连接到你的网络的所有设备。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/11-5.png
图 11.5 示例谷歌智能家居路由器界面,列出了几个设备以发现它们的 IP 地址
如果你无法访问你的路由器,很多人都是这种情况,你并不倒霉。下一个最简单的方法就是忽略我们之前段落中说的所有内容,并将你的树莓派连接到显示器和键盘上。运行 $
ifconfig
或 $
ip
a
,然后查找 inet
参数。这些命令将输出你本地网络上的设备及其 IP 地址。图 11.6 和 11.7 展示了运行这些命令并突出显示你要找的内容。如果你没有额外的显示器,那么事情会变得有点棘手,但仍然可行。然而,如果你能避免,我们不推荐走这条路。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/11-6.png
图 11.6 运行 ifconfig
的示例。为了清晰起见,我们的树莓派的 IP 地址(inet
)被突出显示。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/11-7.png
图 11.7 运行 ip
a
的示例。为了清晰起见,我们的树莓派的 IP 地址(inet
)被突出显示。
要扫描你的本地网络以查找 IP 地址,在你的笔记本电脑上打开一个终端,并运行相同的命令 ($
ifconfig
),如果你使用的是 Windows,则运行 $
ipconfig
。如果你没有 ifconfig
,你可以使用 $
sudo
apt
install
net-tools
来安装它。我们之前没有提到这一步,因为它应该已经安装在你的树莓派上了。
如果你已经知道 Pi 是哪个设备,那太棒了!只需获取该设备的 inet
参数。然而,更有可能的是你不知道,如果你知道如何使用,这里有一些有用的命令。使用命令 $
arp
-a
来查看连接到你的网络的所有 IP 地址,使用命令 $
nslookup
$IP_ADDRESS
来获取你传入的 IP 地址对应的计算机的主机名——你将寻找主机名 raspberry
,但我们将跳过所有这些。我们相信如果你知道如何使用这些命令,你不会阅读这本书的这一部分。相反,我们将使用原始人问题解决方法,这意味着我们将简单地关闭 Pi,再次运行 $
ifconfig
命令,看看有什么变化,特别是什么消失了。当你重新启动它时,你的路由器可能会分配一个与上次不同的 IP 地址,但你仍然应该能够 diff
差异并找到它。
好吧,我们知道为了获取 IP 地址可能需要做很多工作,但一旦你有了它,下一步就简单了。要 SSH 连接到它,你可以运行 ssh
命令:
$ ssh username@0.0.0.0
将 username
替换为你创建的用户名(如果你在跟随我们操作,应该是 pi
),并将 0
s 替换为你的 Pi 的 IP 地址。由于这是第一次连接到一个全新的设备,你将需要指纹验证以建立连接和主机真实性。然后你将被提示输入密码。输入你在 imager 中设置的密码。如果你没有设置密码,默认密码是 pi
,但我们相信你没有这么做,对吧?
通过这样,你应该已经远程连接到你的 Pi,并在你的计算机终端中看到 Pi 的终端,如图 11.8 所示。做得好!
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/11-8.png
图 11.8 成功安全壳连接到 Raspberry Pi 的终端。
11.1.3 软件安装和更新
现在我们已经启动了 Pi 并连接到了它,我们可以开始安装。第一个命令是众所周知的,它将简单地更新我们的系统:
$ sudo apt update && sudo apt upgrade -y
这可能需要一分钟的时间,但一旦完成,恭喜!你现在有一个 Raspberry Pi 服务器,你可以在上面运行任何你想要的东西。它仍然是一张白纸,所以让我们改变它,准备运行我们的 LLM 服务器。我们首先想要安装我们需要的任何依赖项。根据你的安装,这可能包括 g++
或 build-essentials
。我们只需要两个:git
和 pip
。让我们先安装它们,这将使整个过程变得容易得多:
$ sudo apt install git-all python3-pip
接下来,我们可以克隆将在这里做大部分工作的 repo:Llama.cpp。让我们将项目克隆到你的 Pi 上并构建项目。为此,运行以下命令:
$ git clone https://github.com/ggerganov/llama.cpp.git
$ cd llama.cpp
关于 llama.cpp 的注意事项
Llama.cpp,就像许多开源项目一样,是一个更关注让事情工作而不是必然遵循最佳工程实践的项目。由于你正在以当前状态克隆仓库,但我们是在之前的状态下编写这些说明的,你可能会遇到我们无法为你准备的问题。Llama.cpp 也没有任何形式的版本控制。在克隆仓库后,我们建议你运行
$ git checkout 306d34be7ad19e768975409fc80791a274ea0230
此命令将检出我们使用的确切git
commit
,这样你就可以在 llama.cpp 的相同版本上运行一切。我们在 Mac、Windows 10、Ubuntu、Debian 以及当然,Raspberry Pi 4 和 5 上进行了测试。我们预计大多数系统使用这个版本不会出现任何问题。
现在我们有了仓库,我们必须完成几个任务来准备它。首先,为了保持我们的 Pi 整洁,让我们为我们的仓库创建一个虚拟环境并激活它。一旦我们的 Python 环境准备就绪,我们将安装所有必需的依赖项。我们可以使用以下命令来完成:
$ python3 -m venv .venv
$ source .venv/bin/activate
$ pip install -r requirements.txt
Llama.cpp 是用 C++编写的,这是一种编译型语言。这意味着我们必须编译所有依赖项以在硬件和架构上运行。让我们继续构建它。我们只需一个简单的命令就可以做到这一点:
$ make
关于设置的注意事项
如果你在这个略有不同的环境中执行此设置,使用 CMake 而不是 Make 可能会产生很大的差异!例如,即使在 Ubuntu 上运行,我们也需要使用 CMake 来指定兼容的 CudaToolkit 版本以及 nvcc 二进制文件存储的位置,以便使用 CuBLAS 而不是普通的 CPU 来利用 CUDA 集成的 GPU。原始创建者(Georgi Gerganov,又名 ggerganov)在构建测试时使用 CMake,因为它需要比 Make 更多的指定。为了参考,以下是 ggerganov 目前使用的 CMake 构建命令;你可以根据需要修改它:
$ cmake .. -DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_CURL=ON
↪ --DLLAMA_CUBLAS=ON -DCUDAToolkit_ROOT=/usr/local/cuda
↪ --DCMAKE_CUDA_COMPILER=/usr/local/cuda/bin/nvcc
↪ --DCMAKE_CUDA_ARCHITECTURES=75 -DLLAMA_FATAL_WARNINGS=OFF
↪ --DLLAMA_ALL_WARNINGS=OFF -DCMAKE_BUILD_TYPE=Release
接下来,我们只需要获取我们的模型,然后我们就可以继续前进。我们为这个项目选择的模型是 Llava-v1.6-Mistral-7B,我们将使用huggingface-cli
来下载它,就像我们在其他章节中所做的那样。现在运行以下命令来拉取 LLaVA 模型、其伴随的标记器以及配置文件:
$ pip install -U huggingface_hub
$ huggingface-cli download liuhaotian/llava-v1.6-mistral-7b --local-dir
↪ ./models/llava --local-dir-use-symlinks False
现在我们有了模型和标记器信息,我们就可以将我们的 LLM 转换成适用于像 Android 手机或 Raspberry Pi 这样小型的设备了。
11.2 准备模型
现在我们有了模型,我们需要将其标准化,以便仓库中的 C++代码可以以最佳方式与之交互。我们将模型从 safetensor 格式转换为.gguf 格式。我们之前已经使用过 GGUF 模型,因为它们是可扩展的、加载速度快,并且在一个模型文件中包含了关于模型的所有信息。我们还下载了标记器信息,这些信息将包含在我们的.gguf 模型文件中。
一旦准备就绪,我们可以使用convert.py
脚本来将我们的 safetensor 模型转换为 GGUF:
$ python3 convert.py ./models/llava/ --skip-unknown
这段代码会将所有权重转换成一个与下载的所有.safetensors 文件大小相同的.gguf 检查点。现在我们有两个我们下载的副本,如果你的 microSD 卡相当小,这可能是多余的。一旦你有了.gguf 检查点,我们建议你删除或迁移原始模型文件到 Pi 之外的地方以回收内存,这可能看起来像这样:
$ find -name './models/llava/model-0000*-of-00004.safetensors' -exec
rm {} \;
当我们的模型以正确的单文件格式就绪后,我们可以将其变小。现在内存限制开始发挥作用。我们选择 7B 参数模型的一个原因是,在量化q4_K_M
格式中(我们稍后会讨论 llama.cpp 支持的量化格式),它在磁盘上略大于 4GB,这对于 8GB 的 Raspberry Pi 来说已经足够有效。运行以下命令来量化模型:
$ ./quantize ./models/llava/ggml-model-f16.gguf ./models/llava/llava-
↪ v1.6-mistral-7b-q4_k_m.gguf Q4_K_M
我们不会撒谎:在将量化方法应用于所有模型权重时,这将是一场等待游戏,但一旦完成,你将有一个全新的量化模型准备好提供服务。
遇到麻烦了吗?
虽然我们已经在这许多环境和硬件上测试了这些说明,但你可能仍然会遇到困难。以下是一些你可以尝试的故障排除建议,这些建议对我们有所帮助:
-
重新下载模型。这些模型很大,如果你的 Pi 在下载过程中有任何互联网连接问题,你可能有一个损坏的模型。如果你的连接不稳定,你可以尝试使用以太网线而不是 Wi-Fi。
-
重新编译你的依赖项。重新编译你的依赖项最简单的方法是运行
make clean
然后再次运行make
。你也可以尝试使用cmake
或检查不同的选项。 -
重启你的 Pi。重启是一个经典且经过验证的解决方案,特别是如果你正在处理内存问题(对于手头的任务,我们并没有太多内存问题)。你可以在 SSH 中使用
sudo reboot
来重启。 -
在你的计算机上运行这些步骤。你可能在更好的硬件上遇到的问题更少,在尝试在边缘设备上工作之前,了解一条简单路径是什么样的可能很有用。
-
下载一个已准备好的模型。虽然我们鼓励你自己进行转换和量化,但通常你可以在任何格式中找到大多数已经量化的开源模型。所以如果你不担心微调它,你应该会很幸运。对我们来说,我们就是这种情况。
如果你遇到了困难但还想继续前进,你可以使用以下命令下载模型的量化版本:
$ huggingface-cli download cjpais/llava-1.6-mistral-7b-gguf --local-dir
↪ ./models/llava --local-dir-use-symlinks False --include *Q4_K_M*
11.3 服务器模型
我们终于到了这里,开始提供服务!使用 llama.cpp,为模型创建服务非常简单,我们稍后会详细介绍一些稍微复杂一些的技巧,但现在,享受你所做的一切:
$./server -m ./models/llava/llava-v1.6-mistral-7b-q4_k_m.gguf --host
↪ $PI_IP_ADDRESS --api-key $API_KEY
一定要使用你的 Pi 的 IP 地址,API 密钥可以是任何随机的字符串,以提供一层小的安全性。就这样!你现在有一个在 Raspberry Pi 上运行的 LLM,可以从你的本地网络上的任何计算机查询。请注意,服务器在你的 Pi 上启动可能需要很长时间,因为它正在加载模型。不要过于担心;给它一些时间。一旦准备好,让我们通过一个快速演示来测试它。
对于这个演示,假设你已经将一个应用程序与 OpenAI 的 Python 包深度集成。在列表 11.1 中,我们向你展示了如何将这个应用程序指向你的 Pi LLM 服务。我们将继续使用 OpenAI 的 Python 绑定,并将其指向我们的服务。我们通过更新 base_url
到我们的 Pi 的 IP 地址,并使用我们创建服务器时设置的相同的 API 密钥来做这件事。
此外,请注意我们正在调用 gpt-3.5-turbo
模型。OpenAI 对不同模型的调用有不同的流程。如果你不喜欢输入这些字母,你可以轻松地更改它,但这并不重要。你只需要弄清楚如何更改脚本,以便你感觉像是在调用(再次强调,你实际上并没有调用 ChatGPT)。
列表 11.1 OpenAI 但不是 ChatGPT
import openai
client = openai.OpenAI(
base_url="http://0.0.0.0:8080/v1", # replace with your pi's ip address
api_key="1234", # replace with your server's api key
)
completion = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{
"role": "system",
"content": "You are Capybara, an AI assistant. Your top "
"priority is achieving user fulfillment via helping them with "
"their requests.",
},
{
"role": "user",
"content": "Building a website can be done in 10 simple steps:",
},
],
)
print(completion.choices[0].message)
你不需要代码来与你的服务器交互。服务器脚本自带内置的最小 GUI,你可以通过将浏览器指向你的 Pi 的 IP 地址在你的本地网络上访问它。务必包括端口 8080。你可以在图 11.9 中看到这个示例。
这个过程将允许你通过简单的聊天窗口与运行的 LLM API 进行接口。我们鼓励你稍微玩一下。由于你运行在 Raspberry Pi 上,你期望的最快速度大约是每秒五个标记,而最慢的速度,嗯,很慢。你将立即理解为什么普通人不会在边缘设备上放置 LLM。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/11-9.png
图 11.9 在你的 Pi 上运行 LLM 并通过 llama.cpp 服务器与之交互
到目前为止,你可能想知道为什么我们对这个项目如此兴奋。我们承诺你会学到很多东西,但这一章是书中最短的,我们在这里做的绝大多数工作都是下载其他人的代码库和模型。欢迎来到生产阶段。
这正是大多数公司会要求你做的事情:下载一些从朋友那里听说过的模型,并将其放在远小于其运行所需的硬件上。你现在应该准备好在约 20 到 30 分钟内拼凑出他们要求的原型。能够快速迭代将允许你回去进行更有力的谈判,展示你需要更多硬件、用于训练的数据、RAG 或任何其他系统来使项目成功。构建快速的概念验证,然后扩展以满足项目需求,应该是数据科学家和机器学习工程师的关键工作流程。
跟随这里演示的快速原型验证工作流程的一个巨大优势是可见性。你可以展示你能够极快地组合出令人惊叹的东西,如果你们的产品经理表现良好,这应该会在其他目标比预期花费更多时间时增加一定的信任度。他们已经看到,如果你在生产中急需某样东西,你可以在一瞬间就做到。吸引并留住顾客的优质内容需要时间,并且需要对数据和研究的真实投资。
11.4 改进
现在我们已经对这个项目进行了一次全面了解,让我们来谈谈修改这个项目的方法。为了清晰起见,我们选择手把手地告诉你确切要运行的命令,这样你就可以在指导帮助下开始接触实际操作。教程通常在这里结束,但真正的学习,尤其是在生产中的项目,总是更进一步。因此,我们想要给你一些想法,告诉你如何使这个项目成为你自己的,从选择不同的模型到使用不同的工具。
11.4.1 使用更好的界面
学习新工具是这个领域的人最常见任务之一——我们这里指的是从数据科学到 MLOps 的所有事情。虽然我们选择在本书中专注于一些最受欢迎和经过实战考验的工具——我们在生产中实际使用过的工具——但你的公司可能选择了不同的工具。更有可能的是,出现了一个新工具,每个人都正在谈论,你想要尝试它。
我们已经就 llama.cpp 谈了很多,并在本项目中几乎用它做了所有事情,包括编译、量化、服务,甚至为我们的项目创建前端。虽然这个工具在编译和量化方面表现突出,但其他功能主要是出于方便而添加的。让我们考虑一些其他可以帮助你的项目增添额外活力或魅力的工具。
为了立即改善你的项目,你可能考虑安装一个类似 SillyTavern(不一定推荐;它只是很受欢迎)的服务器前端。一个优秀的前端会将“查询一个 LLM”转变为“与一个 AI 最佳朋友聊天”,将一个平静的任务转变为一个令人兴奋的体验。我们喜欢用于这项工作的工具包括 KoboldCpp 和 Ollama,它们是为了扩展 llama.cpp 并使界面更简单或更可扩展而构建的。因此,它们非常适合扩展这个特定的项目。Oobabooga 是另一个出色的文本生成网页 UI。所有这些工具都提供了大量的定制选项和为用户提供独特体验的方式。它们通常提供前端和服务器。
11.4.2 更改量化
你可能考虑在只有 4GB 内存的旧 Pi 上做同样的项目,这样你将需要一个更小的模型。也许你不仅想用 Pi 来服务 LLM,所以你需要进一步缩小模型,或者也许你完全想更换模型。无论如何,你都需要深入探索量化这个兔子洞。之前,我们使用q4_K_M
格式量化模型,承诺稍后解释。好吧,现在就是时候了。
Llama.cpp 提供了许多不同的量化格式。为了简化讨论,表 11.1 突出了一些更常见的量化方法,包括每个方法转换下来的比特数,结果的模型大小,以及运行 7B 参数模型所需的 RAM。这张表应作为快速参考,帮助你确定可以期望的大小和性能水平。一般规则是,量化越小,性能越低,困惑度越高。
表 11.1 不同 llama.cpp 量化方法对 7B 参数模型的键属性比较
量化方法 | 比特数 | 大小 (GB) | 最大 RAM 需求 (GB) | 用例 | 参数 (十亿) |
---|---|---|---|---|---|
Q2_K |
2 | 2.72 | 5.22 | 质量损失显著;不建议用于大多数目的 | 7 |
Q3_K_S |
3 | 3.16 | 5.66 | 非常小,质量损失高 | 7 |
Q3_K_M |
3 | 3.52 | 6.02 | 非常小,质量损失高 | 7 |
Q3_K_L |
3 | 3.82 | 6.32 | 小,质量损失较大 | 7 |
Q4_0 |
4 | 4.11 | 6.61 | 传统;小,质量损失非常高;建议使用Q3_K_M |
7 |
Q4_K_S |
4 | 4.14 | 6.64 | 小,质量损失更大 | 7 |
Q4_K_M |
4 | 4.37 | 6.87 | 中等,平衡质量;推荐 | 7 |
Q5_0 |
5 | 5.00 | 7.50 | 传统;中等,平衡质量;建议使用Q4_K_M |
7 |
Q5_K_S |
5 | 5.00 | 7.50 | 大,质量损失低;推荐 | 7 |
Q5_K_M |
5 | 5.13 | 7.63 | 大,质量损失非常低;推荐 | 7 |
Q6_K |
6 | 5.94 | 8.44 | 非常大,质量损失极低 | 7 |
Q8_0 |
8 | 7.70 | 10.20 | 非常大,质量损失极低;不推荐 | 7 |
如果你只有 4GB 或 6GB 的 Pi,你可能会看着这张表格想,“不,是时候放弃了。”但你还不是完全没有机会;你的模型可能只是运行得慢一些,你可能需要比这些 7B 参数更小的模型——比如只有 1B 或 3B 参数的模型——或者量化得更小以运行。你实际上是在用这么小的 Pi 来推动边缘,所以Q2_k
或Q3_K_S
可能适合你。
友好的提醒:我们一直在用这个项目挑战边缘,但对于更有资金的项目来说,这是一个有用的经验。当使用更好的硬件进行类似项目时,更好的硬件在运行大型 LLM 方面也有其限制。毕竟,总是有更大的模型。请记住,如果你使用 cuBLAS 或任何用于利用 GPU 的框架运行,你不仅受限于 RAM,还受限于 VRAM。例如,在 3090 上使用 cuBLAS 运行时,你受限于 24 GB 的 VRAM。通过巧妙的内存管理(例如使用无头操作系统以占用更少的 RAM),你可以在较小的设备上加载更大的模型,并推动看似可能实现的功能的边界。
11.4.3 添加多模态
我们最初忽略了一个完整的维度,以免分散注意力,但现在让我们来谈谈它:LLaVA 实际上是一个多模态模型!多模态模型使我们能够从 NLP 扩展到其他来源,如图像、音频和视频。几乎每个多模态模型在本质上也是一个大型语言模型(LLM),因为不同模态的数据集都使用自然语言进行标记——例如,图像中看到的内容的文本描述。特别是,LLaVA(大型语言和视觉助手)允许我们向模型输入一张图像并对其提问。
关于 llama 服务器的一个注意事项
记得我们说过 llama.cpp 项目并不遵循许多工程最佳实践吗?嗯,多模态就是其中之一。最初,llama.cpp 服务器支持多模态,但很快项目中就出现了许多问题。创建者觉得原始实现很糟糕,决定将其移除。有一天,一切正常,第二天,它就完全消失了。
这个变化发生在我们编写本章的时候——这本身就是一个头疼的问题——但想象一下,当试图在生产中运行时,它可能造成的损害。不幸的是,在当前这个时间点上,当我们在 LLM 上工作时,这种突然的变化是家常便饭,因为目前可用的稳定依赖项非常少。为了重现这里的内容并最小化调试,我们希望您查看之前提到的 git
commit
。好消息是,llama.cpp 计划继续支持多模态,另一个实现可能很快就会准备好——可能在你阅读这一章的时候。
在这本书中,我们几乎没有讨论过多模态,因为从学习如何使 LLM 在生产中工作获得的经验教训应该可以转移到多模态模型上。无论如何,我们认为展示如何部署一个模型会很有趣。
更新模型
我们已经做了大部分工作;然而,llama.cpp 只将 LLaVA 模型的 llama 部分转换为.gguf。我们需要将视觉部分重新添加进去。为了测试这一点,前往你提供的服务器模型的 GUI,你会看到一个上传图像的选项。如果你这样做,你会得到一个有用的错误,如图 11.10 所示,表明服务器还没有准备好进行多模态服务。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/11-10.png
图 11.10 我们的模式还没有准备好;我们需要提供一个模型投影器。
将我们的模型转换为第一步是下载一个与 CLIP 类似的多模态投影文件,以便你编码图像。一旦我们可以编码图像,模型就会知道如何处理它们,因为它已经训练过多模态任务。我们不会深入介绍准备投影文件的细节;相反,我们将向你展示在哪里可以找到它。运行以下命令下载此文件,然后移动它:
$ wget https://huggingface.co/cjpais/llava-1.6-mistral-7b-
↪ gguf/resolve/main/mmproj-model-f16.gguf
$ mv mmproj-model-f16.gguf ./models/llava/mmproj.gguf
如果你正在使用不同的模型或自制模型,确保你找到或创建一个多模态投影模型来为你执行该功能。它应该直观地告诉你为什么你需要它:语言模型只读取语言。你可以尝试微调和将图像序列化为字符串,而不是使用多模态投影模型;然而,我们不推荐这样做,因为我们没有看到它带来良好的结果。这会增加运行这些模型所需的 RAM 总量,但增加的量并不大。
提供模型服务
一旦你的模型被转换和量化,启动服务器的命令是相同的,但你必须在末尾添加--MMPROJ
path/to/mmproj.gguf
。这段代码将允许你向模型提交图像以执行任务,如进行光学字符识别(OCR),我们将图像中的文本转换为实际文本。现在让我们来做这件事:
$./server -m ./models/llava/llava-v1.6-mistral-7b-q4_k_m.gguf --host
↪ $PI_IP_ADDRESS --api-key $API_KEY --MMPROJ ./models/llava/mmproj.gguf
现在服务器知道如何处理图像了,让我们发送一个请求。与之前我们用来与仅语言模型聊天的 OpenAI API 一致,另一个版本展示了如何调用多模态聊天。代码与列表 11.1 非常相似,因为我们所做的只是添加了一些图像支持。像上一个列表一样,我们使用 OpenAI API 访问我们的 LLM 后端,但我们将基本 URL 更改为我们的模型。主要区别在于我们将图像序列化为字符串,以便它可以包含在对象中,并使用encode_image
函数添加一些导入以简化这个过程。唯一的另一个重大变化是将编码后的图像添加到我们发送的消息的内容部分。
列表 11.2 OpenAI 的多模态 GPT-4
import openai
import base64
from io import BytesIO
from PIL import Image
def encode_image(image_path, max_image=512):
with Image.open(image_path) as img:
width, height = img.size
max_dim = max(width, height)
if max_dim > max_image:
scale_factor = max_image / max_dim
new_width = int(width * scale_factor)
new_height = int(height * scale_factor)
img = img.resize((new_width, new_height))
buffered = BytesIO()
img.save(buffered, format="PNG")
img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
return img_str
client = openai.OpenAI(
base_url="http://0.0.0.0:1234/v1",
api_key="1234", #1
)
image_file = "myImage.jpg"
max_size = 512
encoded_string = encode_image(image_file, max_size) #2
completion = client.chat.completions.with_raw_response.create(
model="gpt-4-vision-preview",
messages=[
{
"role": "system",
"content": "You are an expert at analyzing images with computer vision. In case of error,\nmake a full report of the cause of: any issues in receiving, understanding, or describing images",
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "Building a website can be done in 10 simple steps:",
},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{encoded_string}"
},
},
],
},
],
max_tokens=500,
)
chat = completion.parse()
print(chat.choices[0].message.content)
#1 替换为你的服务器的 IP 地址和端口。
#2 设置为允许的最大维度(512=1 个瓦片,2048=最大)。
没有什么太花哨或与其他许多次向服务器发送请求有什么不同。你应该记住这个代码的一个小陷阱是,如果没有使用 API 密钥,API 将抛出错误;但如果你在服务器上没有设置一个,你可以传递任何东西,它不会出错。
到此为止!我们已经将我们的语言模型转变为可以接受图像作为输入的模型,并且我们已经将其部署到 Raspberry Pi 上,甚至对其进行了查询。至少,我们希望您已经对其进行了查询,因为如果您没有,让我们告诉您,它非常 慢!当您在 Pi 上运行多模态服务器时,它将花费数十分钟来编码和表示图像,甚至达到人们通常用来衡量生成速度的每秒令牌数。再次强调,仅仅因为我们可以将这些模型部署到小型设备上,并不意味着您会想要这样做。这就是我们再次建议您不应该在 Pi 上实际运行此程序,即使是在您的家中,如果您想要真正地充分利用它。
11.4.4 在 Google Colab 上提供模型服务
现在我们已经完成了一些这些练习,我们如何改进和扩展这个项目以适应您的生产环境?第一个改进很明显:硬件。当您有数百名客户时,单板 RAM 计算 并不是非常有帮助;然而,它在测试时非常有用,尤其是当您不想浪费钱调试生产环境时。还有其他支持 GPU 的选项,幸运的是,除了 RPi 设置之外,之前讨论的所有步骤都在 Google Colab 的免费层上工作。以下是所有不同的设置步骤:
- 设置 llama.cpp:
!git clone https://github.com/ggerganov/llama.cpp && cd
↪ llama.cpp && make -j LLAMA_CUBLAS=1
-
- 从 Hugging Face 下载:
import os
os.environ[“HF_HUB_ENABLE_HF_TRANSFER”] = “1”
!huggingface-cli download repo/model_name name_of_downloaded_
↪ model --local-dir . --local-dir-use-symlinks False
-
- 服务器命令:
!./server -m content/model/path --log-disable --port 1337
-
- 访问服务器:
from .googlecolab.output import eval_js
print(eval_js(“google.colab.kernel.proxyPort(1337)”))
如您所见,步骤大多相同,但由于我们在 Jupyter 环境中工作,一些细微的调整是必要的,因为通常直接运行代码比运行 CLI 命令要容易。我们没有深入探讨,但 Raspberry Pi 可以使用 docker.io
和其他软件包来创建可用于负责任的 CI/CD 的 docker 镜像。在 Google Colab 环境中这要困难一些。此外,请记住,Google 不会给您无限的 GPU 时间,甚至到了监控您是否打开了 Colab 以“高效”地关闭您的免费 GPU 的程度,所以请确保您只将这些免费资源用于测试和调试。无论如何看,免费的 GPU 是一份礼物,我们应该对它们负责。
您也可以跳过下载整个仓库并每次运行 Make 的步骤。您可以使用 llama.cpp 的 Python 绑定。并且您可以使用 cuBLAS 或 NEON(适用于 Mac GeForce Mx 卡)来使用硬件加速,在执行以下命令时进行 pip 安装:
$ CMAKE_ARGS=”-DLLAMA_CUBLAS=on” FORCE_CMAKE=1 pip install llama-cpp-python
此命令将 llama.cpp 中的大部分代码抽象为易于使用的 Python 绑定。现在让我们通过一个示例来了解如何使用 Python 绑定来制作易于 docker 化和部署的内容。与 API 一起工作与单独使用 LLM 略有不同,但幸运的是,LangChain 非常方便。其整个库都是围绕使用 OpenAI API 构建的,我们使用该 API 来访问我们自己的模型!
在列表 11.3 中,我们将结合我们对 OpenAI API、llama.cpp Python 绑定和 LangChain 的了解。我们将首先设置我们的环境变量,然后我们将使用 LangChain 的ChatOpenAI
类,并假装我们的服务器是 GPT-3.5-turbo。一旦我们有了这两样东西,我们就可以完成了,但我们将通过添加一个句子转换器和为 RAG 准备好的提示来扩展它。如果您有一个想要用于 RAG 的数据集,现在是嵌入它并创建 FAISS 索引的时候了。我们将加载您的 FAISS 索引,并在推理时使用它来帮助模型。然后,使用 tiktoken 对其进行标记化,以确保我们不会超载我们的上下文长度。
列表 11.3 OpenAI 但不是多模态 GPT-4
import os
from langchain.chains import LLMChain
from langchain_community.chat_models import ChatOpenAI
from langchain.prompts import PromptTemplate
from sentence_transformers import SentenceTransformer
import numpy as np
from datasets import load_dataset
import tiktoken
os.environ["OPENAI_API_KEY"] = "Your API Key"
os.environ[
"OPENAI_API_BASE"
] = "http://0.0.0.0:1234/v1" #1
os.environ[
"OPENAI_API_HOST"
] = "http://0.0.0.0:1234" #2
llm = ChatOpenAI(
model_name="gpt-3.5-turbo", #3
temperature=0.25,
openai_api_base=os.environ["OPENAI_API_BASE"], #4
openai_api_key=os.environ["OPENAI_API_KEY"],
max_tokens=500,
n=1,
)
embedder = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2") #5
tiktoker = tiktoken.encoding_for_model("gpt-3.5-turbo") #6
prompt_template = """Below is an instruction #7
that describes a task,
paired with an input that provides further context.
Write a response that appropriately completes the request.
###Instruction:
You are an expert python developer.
Given a question, some conversation history,
and the closest code snippet we could find for
the request, give your best suggestion for how
to write the code needed to answer the User's question.
###Input:
#Question: {question}
#Conversation History: {conversation_history}
Code Snippet:
{code_snippet}
###Response:
"""
vectorDB = load_dataset( #8
"csv", data_files="your dataset with embeddings.csv", split="train"
)
try: #9
vectorDB.load_faiss_index("embeddings", "my_index.faiss")
except:
print(
"""No faiss index, run vectorDB.add_faiss_index(column='embeddings')
and vectorDB.save_faiss_index('embeddings', 'my_index.faiss')"""
)
message_history = [] #10
query = "How can I train an LLM from scratch?" #11
embedded = embedder.encode(query)
q = np.array(embedded, dtype=np.float32)
_, retrieved_example = vectorDB.get_nearest_examples("embeddings", q, k=1)
formatted_prompt = PromptTemplate( #12
input_variables=["question", "conversation_history", "code_snippet"],
template=prompt_template,
)
chain = LLMChain(llm=llm, prompt=formatted_prompt) #13
num_tokens = len( #14
tiktoker.encode(f"{prompt_template},\n" + "\n".join(message_history) +
↪ query)
)
)
while num_tokens >= 4000:
message_history.pop(0)
num_tokens = len(
tiktoker.encode(f"{prompt_template},\n" + "\n".join(message_history) +
↪ query)
)
)
res = chain.run( #15
{
"question": query,
"conversation_history": message_history,
"code_snippet": "",
}
)
message_history.append(f"User: {query}\nLlama: {res}")
print(res) #16
#1 用您的服务器地址和端口替换。
#2 用您的主机 IP 替换。
#3 这可以是任何内容。
#4 再次
#5 为 RAG 嵌入
#6 快速检查上下文长度的标记化
#7 将提示更改为您想要的任何内容。
#8 这里有一个向量ΔB;请随意替换。
#9 如果您还没有创建 faiss 或 elasticsearch 或 usearch 索引,请创建。
#10 为了跟踪聊天历史
#11 搜索向量ΔB
#12 格式化提示
#13 设置实际的 LLM 链
#14 不要超载您的上下文长度。
#15 使用您的 API 运行 RAG
#16 我们只是打印;在这里做您需要做的任何事情。
所以,这里是我们许多概念真正汇聚的地方。令人惊讶的是,您真的可以在 Raspberry Pi 上执行这个推理和 RAG;您不需要一台巨大的计算机来获得好、可重复的结果。在您达到大约 48 GB 并可以容纳 7B 的全版本以及所有超过那的量化版本之前,这种计算层非常有帮助;目前,之后的所有计算只能带来边际的收益。这个领域正在快速发展,所以寻找在更小的硬件上推理更大模型的新、更快的方法。
这样,我们的原型项目就启动并运行了。它在几乎任何您想要的方向上都非常容易扩展,并且符合行业标准并使用流行的库。添加到这,让它变得更好,如果您有您认为这里没有得到体现的专业知识,请分享它!这个领域是新的,跨学科的知识将推动它向前发展。
摘要
-
在最小的设备上运行最大的模型需要利用您能想到的所有内存节省技术,比如运行一个轻量级操作系统。
-
设置远程 Pi 第一次最困难的部分是找到它的 IP 地址。
-
对于没有加速器的计算受限硬件,您需要使用像 llama.cpp 这样的工具将模型编译到您的架构上运行。
-
在内存受限的环境中,推理将需要量化。
-
即使利用所有可用的资源,在边缘设备上运行 LLM 通常会导致比期望的更慢的推理。仅仅因为某件事是可能的,并不意味着它是实用的。
-
可以通过指向自定义端点来使用 OpenAI 的 API 及其所有包装器,以访问其他模型。
-
许多开源工具可用于改进模型的提供和用户界面。
-
量化程度越低,即使模型规模更大,困惑度也越高。
-
在 Raspberry Pi 上运行多模态模型也是可能的。
-
我们在 Pi 上运行的相同命令,只需稍作修改就可以用于 Google Collab 或其他云服务提供商进行开发,这使得这些项目比以往任何时候都更容易获得。
-
设置和部署通常比准备模型对成功项目来说更为重要。
第十二章:生产,一个不断变化的景观:一切才刚刚开始
本章涵盖
-
LLM 在生产中的简要概述
-
LLMs 作为一项技术和对其进行的几个令人兴奋的研究领域
-
我们的结束语
正如我所设想的网络,我们还没有看到它。未来仍然比过去大得多。——蒂姆·伯纳斯-李(www 的发明者)
哇!在这本书中,我们确实覆盖了大量的内容。你的头脑是不是快要爆炸了?因为我们的确实是这样,我们写了这本书。写这本书并不容易,因为行业一直在不断变化——而且变化很快。试图跟上 LLMs 的发展就像在流沙上建造房屋;你完成了一层,似乎在你开始下一层之前它就已经沉下去了。我们知道这本书的部分内容不可避免地会过时,这就是为什么我们尽力坚持核心概念,这些概念就像沙子中的坚固岩石,永远不会改变。
在本章中,我们想退后一步,回顾一些我们希望你能带走的主要收获。我们花了很多时间深入细节,所以让我们暂时反思一下,看看整个画面,回顾我们已经覆盖的内容。之后,我们将花一点时间讨论该领域的未来,以及我们可以期待看到的一些下一个重大突破。最后,我们将留下我们的最终想法。
12.1 千米视角
在这本书中,我们讨论了大量的内容——从制作词袋模型到在树莓派上部署 LLM API。如果你读完了整本书,那是一项成就。干得好!我们不会回顾所有内容,但我们都想从树木中看到森林,总结一下我们所学到的很多东西。我们可以将大多数想法分为四个截然不同但非常紧密相关的象限:准备、训练、部署和开发。你可以在图 12.1 中看到这些象限。你会注意到,除了这些部分,还有一个与其他部分不同的第五个部分,我们将其标记为潜流。这些是似乎以不同程度影响所有其他象限的元素,以及你在 LLM 产品生命周期的每个阶段都必须关注的事情。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/12-1.png
图 12.1 LLM 产品生命周期。这里列出了书中讨论的所有关键概念,以及它们通常在生产环境中的位置。潜流是生命周期中每个部分的重要元素——例如,语言学在准备阶段提供信息,在训练和部署阶段创建指标,并影响提示和开发。
希望当我们之前在章节中讨论一个概念时,如果还没有讲清楚,现在应该很清楚这个概念在生产生命周期中的位置。你会注意到,我们可能将一些元素放在了你的当前生产环境并不反映的位置——例如,MLOps 基础设施的配置通常并不发生在准备阶段,而是在第一次需要提供服务时随意拼凑。我们理解这一点。但在准备阶段,我们觉得它应该在那里。花点时间消化你在阅读这本书时所学到的所有内容,并考虑所有这些部分是如何结合在一起的。
在这个抽象和理想化的生产生命周期版本中,让我们转向目前尚未包括其中的事物。五年后,我们可能需要在我们的开发部分添加什么,特别是在考虑到这个领域现在发展如此迅速的情况下?
12.2 LLMs 的未来
当我们撰写这本书时,我们有意专注于你需要的基础知识,以便理解 LLMs 是如何工作的以及如何将它们部署到生产环境中。这些信息至关重要,因为每个用例的生产情况都大不相同。学习如何权衡任何决策的利弊,需要这些基础知识,这样你才有可能做出正确的选择。
与此决定相邻,我们不希望这本书只包含理论。我们希望它是实践性的,有足够的例子,让你作为读者不仅知道事物是如何工作的,而且能感受到它们的感觉——比如感受将一个 70B 模型加载到 GPU 上需要多长时间,如果你在边缘设备上运行该模型,你能感受到用户将会有怎样的体验,以及当你躲在黑暗的山洞里埋头于代码、避开春日温暖的阳光时,你能感受到电脑屏幕柔和的光芒。
在撰写这本书的过程中,我们做出的最艰难的决定之一就是决定专注于当下。我们决定关注那些我们实际上看到人们在今天的生产环境中使用的最佳方法。这个决定之所以艰难,是因为在撰写这本书的过程中,我们遇到了许多令人震惊的研究论文,我们确信这些论文将“改变一切”。然而,由于种种原因,这些研究尚未进入生产阶段。在本节中,我们将打破这一限制,无论行业当前状态如何,都将讨论即将到来的趋势。但不仅仅是研究;公众舆论、诉讼和政治格局也常常塑造着技术的未来。我们将探讨在接下来的几年里,我们认为 LLMs 将走向何方,并提及它们可能采取的一些方向。
12.2.1 政府和监管
在本书的开头,我们承诺向您展示如何创建 LLM 产品,而不仅仅是演示。虽然我们相信我们已经做到了这一点,但我们一直忽略了一个重要细节:产品存在于现实世界中。演示只需要在孤立的环境中工作,而产品必须在一般情况下工作。产品是为了出售的,一旦货币交换发生,就会设定期望,声誉就会受到考验,最终,政府将介入。
虽然一个团队不能为可能永远不会到来的未来法规而建造,但了解您构建的产品可能产生的法律后果是很重要的。一场败诉的案件可以设定先例,引发模仿诉讼的浪潮。由于产品存在于现实世界中,我们最好关注那个世界。
我们中的一员有机会参与犹他州 SB-149 人工智能修正法案的立法过程。该法案主要关注引入对使用 LLM 规避州内消费者保护法律的行动者的责任。目前,每个立法机构都在试图弄清楚其在 AI 方面的管辖权从何开始到何结束,以及如何处理其对保护其选民中的公民和公司所承担的日益增加的责任。在犹他州,州政府对 AI 和 LLM 采取了非常严肃和以商业为先的方法。在整个过程中以及法案本身,立法机构不能创建不与“看哪,一个人”第欧根尼风格的例子相冲突的定义,我们将需要每一份善意来导航 LLM 为监管机构带来的新世界。你如何定义 AI?法案如下定义:
“人工智能”是指一种基于机器的系统,它做出预测、推荐或决策,影响真实或虚拟环境。
这可能包括分段函数到 LLM 代理的任何东西,这意味着您的营销团队不会对您的if
语句是处于状态中的 AI 的声明负责。话虽如此,该法案包含了对供应商欺诈行为的详尽和深思熟虑的定义,以及制定了一个 AI 分析和研究计划,以帮助州政府从更长期的角度评估风险和政策,这看起来对犹他州来说是新颖且独特的。犹他州立法机构通过与州内的研究人员、专家、C 级高管和企业家进行咨询,能够完善这项法案,我们鼓励读者参与在您所在的社区和政府中制定有价值且有意义的法规。这是确保法院系统长期准备好在应受惩罚的地方实施惩罚的唯一方式。
版权
在法律担忧的前沿是版权侵权问题。在足够的数据上训练的 LLM 可以模仿或复制作者或创作者的风格,甚至直接一字不漏地剽窃。当考虑到构建自己的枪手以帮助你在创作过程中时,这很令人兴奋,但当你意识到竞争对手也能这样做时,这就不那么令人兴奋了。
可能需要关注的最大诉讼是《纽约时报》诉 OpenAI。¹《纽约时报》正在对 OpenAI 提起法律诉讼,称其聊天机器人未经同意就在《时报》的知识产权上进行了训练。它提供了证据,表明聊天机器人给出的逐字逐句的回应与用户通常需要付费才能看到的专有信息相同。因此,人们担心用户访问其网站的人数会减少,从而减少广告收入。本质上,他们窃取了他们的数据,现在正在信息空间中作为竞争对手使用。
对抗这场斗争的旁观者担心,如果《时报》胜诉,可能会严重阻碍 AI 的发展,导致美国在全球 AI 发展竞赛中的领先地位受损。AI 公司面临更大的版权责任风险,从而带来更大的竞争损失,这意味着更少的创新。相反,他们也担心,如果《时报》败诉,将进一步削弱已经陷入困境的新闻业,在那里,找到可以信赖的高质量报道已经很困难了。这对 AI 发展也是一个巨大的打击,AI 发展总是渴望得到好的干净数据。这似乎是 AI 领域的一个双输局面。
无论谁胜谁负,诉讼的结果都很明显,现行的版权法从未考虑过机器人最终会复制我们。我们需要新的法律,而且不清楚我们的立法者是否具备足够的技术能力来应对这一挑战。因此,我们再次鼓励你参与你所在社区内法规的制定过程。
AI 检测
一个持续让我们心碎的担忧领域来自于“AI 检测”产品的兴起。让我们一开始就明确:这些产品都是骗人的。没有可靠的方法来确定一段文本是由人类还是机器人所写。到这本书的这一部分,我们期望大多数读者也已经得出了这个结论。原因很简单:如果我们能够可靠地确定哪些是哪些不是生成文本,我们就可以创建一个新的模型来击败检测器。这正是对抗性机器学习的全部意义。
在网上有一个流行的玩笑,任何包含“深入挖掘”这个词的阅读内容都必须是由 LLM 撰写的(例如,mng.bz/o0nr
)。这个词深入挖掘在生成文本中比在人类语言中更可能出现,但这提出了明显的问题:哪个模型?哪个提示?人类自大的想法,认为仅通过寻找特定的单词就能识别生成内容,是可笑的。但当然,如果人们盲目地相信这种明显的错误,那么他们愿意相信一个更复杂或更先进的系统或算法能够做得更好,也就不足为奇了。
尽管如此,这让我们心碎的原因是因为我们读过一篇又一篇关于学生受到惩罚、论文被给予不及格分数、被迫退课以及在成绩单上被标记剽窃的故事。现在,我们不知道每个案例的细节,但作为相关技术的专家,我们更倾向于相信学生而非其他。
将被“AI 检测”系统标记为有高概率由 AI 撰写的论文与剽窃归为同一类别也是荒谬的。现在,我们并不支持作弊,但大型语言模型(LLMs)是一种新工具。它们帮助我们处理语言,就像计算器帮助我们处理数学一样。我们已经找到了在不创建“计算器检测”系统的情况下教授和评估学生进步的方法。我们也可以再次做到这一点。
好吧,识别生成内容并不是不可能的。一项调查发现,通过简单地搜索“作为人工智能语言模型”或“截至我最后一次知识更新”等短语,他们发现了数百篇在科学期刊上发表的、在 LLMs 帮助下撰写的论文。² 一些短语是明显的迹象,但这些只是由于作者们的纯粹懒惰而被识别。
所有这些中最糟糕的部分是,由于这些检测系统是虚假的、糟糕的,并且充满了误报,它们似乎是由教师任意和随机地执行的。很难相信大多数论文没有被标记,那么为什么只有一小部分学生被点名批评呢?这是因为这些系统似乎已经变成了教师手中的权力和歧视武器,他们会利用这些系统来惩罚他们不喜欢的学生——更不用说这种明显的虚伪,因为我们猜测这些教师中的一些人可能就是那些在论文中使用“作为人工智能语言模型”等短语的人。
偏见与伦理
这不是我们第一次讨论 LLMs(大型语言模型)中发现的偏见和伦理问题,但这次,让我们更深入地探讨这次讨论应得的讨论。假设一个人被绑在轨道上,你什么也没做,电车撞上了他们,结束了他们的生命。你是否有责任?这个被称为“电车问题”的思想实验已经被讨论得淋漓尽致;甚至有一个基于已发表论文的电子游戏(Read Graves 的 Trolley Problem Inc.),提出了数十种变化。我们甚至不会尝试回答这个问题,但我们会简要介绍一下你如何自己决定答案。
分析这种问题的方法远不止两种,但我们只会关注其中两种——道德和伦理——并且我们会简化这些概念,因为这不是一本哲学书。在这里,道德帮助你根据对好/不好的信念来判断过错。伦理帮助我们确定在我们所生活的社会中的法律体系内的实际框架中的后果。如果你对轨道上的人的死亡负有道德责任,你相信这是你最终的责任,你的行为是导致他们死亡的原因。这与伦理责任不同,这意味着你因该行为应受到法律和社会的后果。他们可以同意,但不必如此。改变语境可以帮助阐明区别:如果你告诉某人一把刀不锋利,他们在检查时切到了自己,从道德上讲,他们陷入那种情况可能是你的责任,但从伦理上讲,你会避免被控企图谋杀。
算法创造了数千种这样的情况,在这些情况下,我们的道德和伦理可能并不一致。在塔木德中有一个关于道德和伦理责任的古老例子,它决定如果一个人把另一个人推入水中或火中,而被推的人未能逃脱,那么这个人不是杀人犯。³ 根据你的信仰和你在的法律体系下,Meta 在缅甸的种族灭绝(不是开玩笑⁴)中可能是道德上或伦理上有过错的。在那种情况下,Meta 甚至没有把人推入火中;是他们的算法做的。这显然是一个充满争议和残酷的例子,但 LLMs 创造了一个非常真实的情况,其中机器学习从业者需要实际、一致和可辩护的道德和伦理框架,否则他们可能会在他们监管下发生真正的悲剧。显然,我们不是道德的仲裁者,也不会评判你在那里的位置,但你仍然应该考虑你创建的任何系统的更广泛背景。
法律正在到来
我们可以肯定的一件事是,监管将会到来,公司将对它们的 AI 代理的行为负责。加拿大航空通过法院判决得知这一点,法院裁定该公司必须遵守其聊天机器人完全编造的退款政策(mng.bz/pxvG
)。该机器人提供了错误的信息。它确实将客户链接到了正确的退款政策;然而,法院正确地质疑了“为什么客户需要在网站的另一部分找到的信息在网站的其他部分再次进行双重检查。”
我们已经看到过类似的案例,用户通过提示工程技巧欺骗了雪佛兰的 LLM 聊天机器人,以 1 美元的价格出售了一辆 2024 年的塔霍车型(mng.bz/XVmG
),DPD 在一位客户让它承认自己是世界上最差的快递公司后,不得不“关闭其 AI 元素”。⁵正如我们之前所说,即使有现有的立法,也很难判断 LLM 在道德上可以做什么。当然,这也引发了一个问题:如果聊天机器人获得了销售汽车的许可并完成了这样的交易,客户的恶意互动是否真的重要,或者公司是否仍然在道德上对维护这样的交易负有责任。
对 LLM 生成的内容负责足以让你三思而后行,考虑你可能考虑使用它的许多应用。风险越高,你应该花更多的时间暂停并考虑潜在的法律后果。我们强烈建议调整你的提示工程系统,设置护栏以保持你的代理在任务上,并且绝对确保保存你的日志并保留客户聊天记录。
12.2.2 LLMs 正在变得更大
另一件我们可以确定的事情是,在不久的将来,我们还将继续看到模型变得越来越庞大。由于更大的模型持续表现出涌现行为,公司没有理由停止采取这种方法,因为简单地投入资金似乎能带来更多的收益。更不用说,对于投入最多的公司来说,更大的模型更难复制。正如你可能发现的,小型公司竞争的最佳方式是创建更小、更专业的模型。最终,只要我们有足够大的训练数据集来容纳更多的参数,我们就可以期待看到更多的参数被塞入模型中,但关于我们是否曾经有过足够的数据来证明“通用智能”(如 AGI)的问题,仍然像以往一样模糊不清。
更大的上下文窗口
不仅是大模型。我们非常兴奋地看到上下文长度也在增长。当我们开始编写这本书时,这是一个真正的限制。很少看到上下文长度超过 10K 令牌的模型。当时 ChatGPT 只提供最多 4,096 个令牌的长度。一年后,我们看到 Gemini 1.5 Pro 这样的模型提供了最多 1 百万个令牌的上下文长度,研究人员指出,它在测试案例中可以处理多达 1 千万个令牌(mng.bz/YV4N
)。为了更直观地说明,整个七部《哈利·波特》系列共有 1,084,170 个单词(我没有数过;wordsrated.com/harry-potter-stats/
),根据你的分词器,这大约相当于 1.5 百万个令牌。在这些长度下,很难相信有任何限制。
显然,挑战仍然存在。这些具有近乎无限上下文窗口的更大模型通常按令牌收费。如果模型不强迫用户发送更小的查询,那么用户的钱包就会受到影响。更不用说,如果你正在阅读这本书,你很可能对可以自己部署的小型开源模型更感兴趣,而这些模型中许多确实仍然有必须与之合作的限制性上下文大小。不过,不用担心;现在和将来,即使是更小的模型也将拥有百万级别的上下文窗口。这个领域正在进行许多有趣的研究。如果你感兴趣,我们建议你查看 RoPE⁶、YaRN⁷和 Hyena⁸。
下一个注意力
当然,更大的上下文窗口是很好的,但它们也有代价。记住,在大型语言模型(LLM)的中心是注意力算法,其复杂度是二次的——这意味着我们投入的数据越多,我们就需要投入更多的计算资源。推动研究社区的一个挑战是找到下一个不遭受这种相同问题的注意力算法。我们能否构建一个仅具有线性复杂度的新算法的 transformers?这正是现在的十亿美元问题。
在这个领域有许多竞争性的创新,我们甚至没有时间讨论我们所有绝对最喜欢的。其中两个最喜欢的分别是 MAMBA,作为 transformers 的替代品,以及 KAN,作为多层感知器(MLPs)的替代品。特别是 MAMBA,它是对状态空间模型(SSMs)的改进,并将其融入了一个无注意力的神经网络架构中。⁹ 单独来看,它并不那么令人印象深刻,因为它需要大量的硬件黑客技术才能使其具有一定的性能。然而,后来出现了 JAMBA,这是一个 MAMBA 风格的模型,它使用了混合 SSM-transformer 层和联合注意力。¹⁰ 这种混合方法似乎为我们提供了两者的最佳结合。
为了让您亲身体验,在列表 12.1 中,我们将对 JAMBA 模型进行微调和运行推理。这个模型是一个专家混合模型,拥有 520 亿个参数,其实现将允许在 80GB GPU 上实现 14 万 K 的上下文长度,这比仅使用注意力模型要好得多。这个例子直接改编自 Hugging Face 模型卡片,所以与所有其他简单的 transformer 实现相比,语法应该非常熟悉,我们对尝试新事物如此容易感到非常感激。
对于训练部分,遗憾的是,即使在半精度下,模型也太大,无法适应单个 80GB GPU,因此您必须使用 Accelerate 在多个 GPU 之间并行化以完成训练。如果您没有现成的计算资源,您可以完成到分词器的导入,然后跳过训练部分,改动非常小。我们并没有做什么特别的事情;我们将用于训练的数据集只是一些从 Goodreads 检索的著名作者的英文名言,包括名言、作者和标签,所以如果您决定跳过微调,请不要觉得自己错过了什么。我们将首先加载分词器、模型和数据集。
列表 12.1 JAMBA 的微调和推理
from trl import SFTTrainer
from peft import LoraConfig
from transformers import (
AutoTokenizer,
AutoModelForCausalLM,
TrainingArguments,
)
from transformers import BitsAndBytesConfig
import torch
from datasets import load_dataset
tokenizer = AutoTokenizer.from_pretrained("ai21labs/Jamba-v0.1")
model = AutoModelForCausalLM.from_pretrained(
"ai21labs/Jamba-v0.1", device_map="auto"
)
dataset = load_dataset("Abirate/english_quotes", split="train")
一旦所有这些都在内存中(如果您的硬件有限,您可以流式传输数据集),我们将创建训练参数和一个 LoRA 配置,以帮助微调在更小的硬件上工作:
training_args = TrainingArguments(
output_dir="./results",
num_train_epochs=3,
per_device_train_batch_size=4,
logging_dir="./logs",
logging_steps=10,
learning_rate=2e-3,
)
lora_config = LoraConfig(
r=8,
target_modules=["embed_tokens", "x_proj", "in_proj", "out_proj"],
task_type="CAUSAL_LM",
bias="none",
)
现在,到了高潮部分,类似于 sklearn 的model.fit()
,transformers 的trainer.train()
已经成为一个标志,表明任何人都可以学习如何与最先进的机器学习模型交互。一旦训练完成(对我们来说大约需要不到一个小时),我们将保存分词器和模型的本地版本,并删除内存中的模型:
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
args=training_args,
peft_config=lora_config,
train_dataset=dataset,
dataset_text_field="quote",
)
trainer.train()
tokenizer.save_pretrained("./JAMBA/")
model.save_pretrained("./JAMBA/")
del model
接下来,我们将以内存高效的方式重新加载模型,用于推理。在 80GB GPU 上,使用 BitsandBytes 配置以 8 位加载,您现在可以在单个 GPU 上拟合模型和大量数据。以 4 位加载允许在 A100 或两个 3090 上实现,类似于 70B 参数的 transformer。使用量化将其降低到 1 位模型,您可以在单个 3090 上拟合这个模型和大量数据。我们将使用以下 8 位推理实现并在其上运行推理:
quantization_config = BitsAndBytesConfig(
load_in_8bit=True, llm_int8_skip_modules=["mamba"]
)
model = AutoModelForCausalLM.from_pretrained(
"ai21labs/Jamba-v0.1",
torch_dtype=torch.bfloat16,
attn_implementation="flash_attention_2",
quantization_config=quantization_config,
)
input_ids = tokenizer(
"In the recent Super Bowl LVIII,", return_tensors="pt"
).to(model.device)["input_ids"]
outputs = model.generate(input_ids, max_new_tokens=216)
print(tokenizer.batch_decode(outputs))
到目前为止,我们几乎每个月都会被 LLM 系统各个部分的替代方案所震撼。在这里,我们想将您的注意力引回到 LLM 获得重大突破的地方:“注意力即一切”。¹¹那篇论文表明,你可以使用简单的 MLP 获得惊人的结果,仅使用注意力来弥合差距。我们正进入一个新纪元,我们不再只关注我们需要什么,而是关注为了获得最佳结果我们想要什么。例如,我们想要低于二次方的注意力替代方案,以匹配或超越闪速注意力在速度上的表现。我们想要无注意力的 transformer 和数百万长度的上下文长度,没有“中间丢失”的问题。我们想要没有精度或学习速度下降的密集 MLP 的替代方案。我们正逐步获得所有这些以及更多。
推进压缩的边界
在降至 INT4 之后,有实验性的量化策略可以将模型进一步降至 INT2。INT2 70B 模型仍然表现良好,这让许多人感到惊讶。然后有研究表明,我们可能甚至可以进一步减小到每个权重 1.58 位或使用三进制和其他更小的算子达到 0.68 位。想试试吗?Llama3 70B 已经在 GGUF、GPTQ 和 AWQ 格式中实现了 1 位量化,它只占用 16.6 GB 的内存。尽情尝试吧!
这还有另一个维度,它不涉及压缩模型,而是将模型是一个整体的想法与将模型视为层和参数集合的想法解耦。推测性解码为我们提供了快速访问大型模型的另一种方式。推测性解码不仅需要足够的内存来加载一个大型模型,还需要一个与之并行的较小模型——想想蒸馏模型。目前生产中常用的一个例子是 Whisper-Large-v3 和 Distil-Whisper-Large-V3。Whisper 是一个多模态 LLM,专注于语音到文本问题,但推测性解码可以与任何具有相同架构但大小不同的两个模型一起工作。
这种方法使我们能够更快地(有时是直接的 2 倍速度提升)采样更大的模型,通过并行计算多个标记,并通过一个允许我们同时完成一个步骤并验证该步骤是简单还是困难的近似“助手”模型。基本思路是这样的:使用更小、更快的 Distil-Whisper 模型来生成关于最终结果的猜测,并允许 Whisper 并行评估这些猜测,忽略那些它将执行相同操作的情况,并纠正那些它将改变的情况。这允许我们以较小模型的速度和较大模型的准确性。
在列表 12.2 中,我们展示了在英语音频数据集上进行的推测性解码。我们将加载 Whisper 和 Distil-Whisper,加载数据集,然后向生成关键字参数(generate_kwargs
)添加一个 assistant_model
。您可能会问,这个系统如何知道辅助模型只意味着帮助解码,正如其名称所暗示的那样?嗯,我们用 AutoModelForCausalLM
而不是语音序列到序列版本加载辅助模型。这样,模型将只帮助并行于较大模型的大解码步骤。完成这些后,我们可以自由测试。
列表 12.2 使用 Whisper 的推测性解码
from transformers import (
AutoModelForCausalLM,
AutoModelForSpeechSeq2Seq,
AutoProcessor,
)
import torch
from datasets import load_dataset
from time import perf_counter
from tqdm import tqdm
from evaluate import load
device = "cuda:0" if torch.cuda.is_available() else "cpu"
print(f"Device: {device}")
attention = "sdpa"
torch_dtype = torch.float16 if torch.cuda.is_available() else torch.float32
model_id = "openai/whisper-large-v3"
assistant_model_id = "distil-whisper/distil-large-v3"
model = AutoModelForSpeechSeq2Seq.from_pretrained(
model_id,
low_cpu_mem_usage=False,
use_safetensors=True,
attn_implementation=attention,
torch_dtype=torch_dtype,
).to(device)
processor = AutoProcessor.from_pretrained(model_id)
assistant_model = AutoModelForCausalLM.from_pretrained(
assistant_model_id,
low_cpu_mem_usage=False,
use_safetensors=True,
attn_implementation=attention,
torch_dtype=torch_dtype,
).to(device)
dataset = load_dataset(
"hf-internal-testing/librispeech_asr_dummy",
"clean",
split="validation",
trust_remote_code=True,
)
wer = load("wer")
generate_kwargs_1 = {
"language": "en",
"task": "transcribe",
}
generate_kwargs_2 = {
"language": "en",
"task": "transcribe",
"assistant_model": assistant_model,
}
spec_decoding = False
for i, generate_kwargs in enumerate([generate_kwargs_1, generate_kwargs_2]):
all_time = 0
predictions = []
references = []
for sample in tqdm(dataset):
audio = sample["audio"]
inputs = processor(
audio["array"],
sampling_rate=audio["sampling_rate"],
return_tensors="pt",
)
inputs = inputs.to(device=device, dtype=torch_dtype)
start_time = perf_counter()
output = model.generate(
**inputs,
**generate_kwargs,
)
gen_time = perf_counter() - start_time
all_time += gen_time
predictions.append(
processor.batch_decode(
output, skip_special_tokens=True, normalize=True
)[0]
)
references.append(processor.tokenizer.normalize(sample["text"]))
score = wer.compute(predictions=predictions, references=references)
if i > 0:
spec_decoding = True
print(f"Speculative Decoding: {spec_decoding}")
print(f"Time: {all_time}")
print(f"Word Error Rate: {score}")
在我们的测试中,我们观察到 Whisper-Large-V3 在使用缩放点积注意力机制的情况下,处理完所有 73 个示例大约需要 42 秒。使用推测性解码后,时间降至 18.7 秒,但精确的词错误率(WER)保持不变。因此,速度提高了近 2 倍,而准确性没有丝毫下降。是的,相当疯狂。
在这一点上,我们想知道,“为什么每个人不总是用这个来做所有事情?”这种方法的一些缺点如下:首先,它在较短的序列中效果最好。对于 LLM 来说,这低于 128 个生成标记或大约 20 秒的音频处理。对于更长的生成,速度提升将微不足道。除此之外,我们并不总是能够访问到完美兼容的大模型和小模型对,比如 BERT 与 DistilBERT。最后一个原因是,真正了解它的人非常少,尽管它的实现很简单。
最终,无论是子比特量化、推测性解码还是其他进步,LLM 们比任何其他技术都更推动研究进入压缩方法,观察新技术如何改变格局是非常有趣的。随着这些方法的发展,我们可以将模型推向更小、更便宜的硬件,使该领域更加易于接触。
12.2.3 多模态空间
我们对多模态的潜力感到非常兴奋。回到第二章,多模态是我们尚未看到许多解决方案出现的主要语言特征之一,我们正在看到向尝试解决语音学的转变。然而,人类操作的模式不仅仅是音频。因此,将语音学、语义学和语用学结合起来,在同一个嵌入空间(用于比较)中获得尽可能多的上下文(对于比较)的推动力非常强烈。考虑到这一点,以下是一些值得关注的领域点。
我们首先想引起注意的是 ImageBind 项目,该项目展示了我们不必试图将模型限制在摄入每种类型的数据,相反,我们可以将每种类型的数据压缩到一个模型已经熟悉并能处理的嵌入空间中。您可以在官方演示中查看:imagebind.metademolab.com/
。
ImageBind 建立在多模态投影模型(如 CLIP)已经展示了一段时间的能力之上:创建和处理嵌入的能力是确定性 LLM 系统背后的真正力量。您可以使用这些模型进行非常快速地搜索,包括之前几乎不可能完成的搜索,例如要求找到与上传音频剪辑声音相似的动物图片。
OneLLM 将这种逻辑颠倒过来,使用一个模型和一个多模态编码器来统一和嵌入八个模态,而不是 ImageBind 示例中使用的六个不同编码器来在相同维度中嵌入六个模态。它可以在以下链接找到:onellm.csuhan.com/
。OneLLM 的核心思想是使用语言来对齐统一的编码器,这为多模态提供了一种独特的视角,它关注的是编码过程而不是结果。
我们对这个领域的研究感到非常兴奋。这项研究能够帮助弥合模型生态系统中语音学和语用学之间的差距,并允许实现更类似人类的理解和交互,尤其是在搜索领域。
12.2.4 数据集
由于 LLMs 的引入,我们在这个行业内看到的一个令人兴奋的变化是,公司终于开始理解管理和治理他们数据的重要性。对于一些人来说,这是推动他们微调自己的 LLMs 并加入激动人心的 AI 产品交付竞赛的动力。对于另一些人来说,这是担心自己变得过时,因为这些系统的能力远远超过了以前的技术;他们发现,只有他们的数据才能提供任何类型的护城河或保护竞争。而对于所有人来说,他们担心会犯他们看到其他公司犯过的同样的错误。
LLMs 不仅仅是推动因素;它们还在帮助团队标注、标记、组织和清理数据。许多公司堆积了大量的数据,却不知道如何处理,但有了 CLIP 等 LLM 模型,图像字幕变得轻而易举。一些公司发现,仅仅创建他们的文本、图像、音频和视频的嵌入空间,就使他们能够为之前无结构的数据集创建有意义的结构。结构化数据更容易操作,为搜索、推荐和其他洞察打开了大门。
目前在行业中我们看到的一个缺失的方面是有价值的开源数据集,尤其是在评估方面。许多目前用于评估模型的基准测试依赖于多项选择题,但这对于试图创建一个 LLM 应用的人来说效率低下。在现实世界中,你的用户何时会以多项选择题的形式向你的模型提问?几乎永远不会。人们在对话和寻求帮助时提出开放式问题,因为他们自己也不知道答案。然而,这些评估数据集已经成为基准,仅仅是因为它们对研究人员来说很容易收集、汇编和评估准确性。
此外,我们相信另一个不可避免的需求是更多语言表示。世界是一个由多种语言和方言构成的织物,每种语言都承载着其独特的文化细微差别和交流微妙之处。然而,许多语言在现有数据集中代表性不足,导致模型偏向于更占主导地位的语言。随着技术的日益全球化,包括更广泛的语言至关重要。添加多种语言不仅促进了包容性,还增强了语言模型在不同国际环境中的准确性和适用性,弥合沟通差距,促进一个更加紧密相连的世界。想象一下,如果你的初创公司不需要支付任何人就能获得有关进入中国、俄罗斯或沙特阿拉伯以扩大市场的准确信息。
12.2.5 解决幻觉问题
有大量证据表明,LLM 中包含的信息比它们愿意给出的要多,甚至有更多证据表明,人们在提示时通常要么很糟糕,要么恶意。因此,你会发现,幻觉是试图开发一个始终如一地提供结果的应用的最大的障碍之一。这个问题让许多习惯于确定性计算机算法且很少处理非确定性系统的软件工程团队感到沮丧。对于许多更熟悉这些类型系统的统计学家来说,幻觉被视为一个特性,而不是一个错误。无论你站在哪一方,都有大量研究投入到处理幻觉的最佳方法中,这是你应该关注的领域之一。
更好的提示工程
一个有趣且随着时间的推移显示出巨大改进的领域是提示工程。一个有助于减少幻觉的提示工程工具是 DSPy。我们在第七章中简要介绍了它,但在这里我们将给出一个如何工作的例子,以及为什么它可以是解决你 LLMs 中幻觉的有帮助的一步。我们在整本书中多次讨论了 LLMs 在数学方面特别糟糕的事实,甚至简单的数学,我们也讨论了原因,但我们并没有真正讨论除了改进你的分词之外的其他解决方案。所以,在列表 12.3 中,我们将展示如何通过零分词更改、零微调和没有 LoRAs 或 DoRAs,仅仅优化你的提示来告诉模型如何回答你提出的问题。
我们将使用 dspy-ai Python 包和 Llama3-8B-Instruct 来完成这项工作。我们将首先加载和量化模型,以便在大多数 GPU 和 Grade-School Math 8K 数据集上运行。我们选择这个数据集是因为它是一个数学问题集合,作为一个已经从小学毕业的人,你可能甚至不需要计算器就能解决这些问题。我们将为我们的训练集和测试集(开发集)使用 200 个示例,尽管我们建议你尝试这些数字,以找到最适合你用例的最佳比例,避免数据泄露。
列表 12.3 DSPy for math
from transformers import AutoModelForCausalLM, AutoTokenizer
from transformers import BitsAndBytesConfig
import torch
import dspy
from dspy.datasets.gsm8k import GSM8K, gsm8k_metric
from dsp.modules.lm import LM
from dspy.evaluate import Evaluate
from dspy.teleprompt import BootstrapFewShot
model_name = "meta-llama/Meta-Llama-3-8B-Instruct"
quantization_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
)
model = AutoModelForCausalLM.from_pretrained(
model_name,
device_map="auto",
quantization_config=quantization_config,
attn_implementation="sdpa",
)
tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=True,)
gms8k = GSM8K()
gsm8k_trainset, gsm8k_devset = gms8k.train[:30], gms8k.dev[:100]
现在我们已经准备好了导入和加载,我们需要解决的事实是我们使用 transformers 加载了 Llama3,而不是 DSPy。DSPy 期望与使用 OpenAI API 的模型交互,但我们从 Hugging Face 加载了一个本地模型,DSPy 最近为其包添加了 HFModel,现在可以轻松导入,而不是需要定义包装器。首先,我们创建一个简单的函数来映射 API 之间的任何关键字参数差异,比如max_tokens
与max_new_tokens
,然后我们创建一个类,它将作为我们的模型生成答案和优化提示的包装器。一旦准备好了,我们将加载 DSPy:
def openai_to_hf(**kwargs):
hf_kwargs = {}
for k, v in kwargs.items():
if k == "n":
hf_kwargs["num_return_sequences"] = v
elif k == "frequency_penalty":
hf_kwargs["repetition_penalty"] = 1.0 - v
elif k == "presence_penalty":
hf_kwargs["diversity_penalty"] = v
elif k == "max_tokens":
hf_kwargs["max_new_tokens"] = v
elif k == "model":
pass
else:
hf_kwargs[k] = v
return hf_kwargs
class HFModel(LM):
def __init__(
self,
model: AutoModelForCausalLM,
tokenizer: AutoTokenizer,
**kwargs
):
"""wrapper for Hugging Face models
Args:
model (AutoModelForCausalLM): HF model identifier to load and use
tokenizer: AutoTokenizer
"""
super().__init__(model)
self.model = model
self.tokenizer = tokenizer
self.drop_prompt_from_output = True
self.history = []
self.is_client = False
self.device = model.device
self.kwargs = {
"temperature": 0.3,
"max_new_tokens": 300,
}
def basic_request(self, prompt, **kwargs):
raw_kwargs = kwargs
kwargs = {**self.kwargs, **kwargs}
response = self._generate(prompt, **kwargs)
history = {
"prompt": prompt,
"response": response,
"kwargs": kwargs,
"raw_kwargs": raw_kwargs,
}
self.history.append(history)
return response
def _generate(self, prompt, **kwargs):
kwargs = {**openai_to_hf(**self.kwargs), **openai_to_hf(**kwargs)}
if isinstance(prompt, dict):
try:
prompt = prompt["messages"][0]["content"]
except (KeyError, IndexError, TypeError):
print("Failed to extract 'content' from the prompt.")
inputs = self.tokenizer(prompt, return_tensors="pt").to(self.device)
outputs = self.model.generate(**inputs, **kwargs)
if self.drop_prompt_from_output:
input_length = inputs.input_ids.shape[1]
outputs = outputs[:, input_length:]
completions = [
{"text": c}
for c in self.tokenizer.batch_decode(
outputs, skip_special_tokens=True
)
]
response = {
"prompt": prompt,
"choices": completions,
}
return response
def __call__(
self, prompt, only_completed=True, return_sorted=False, **kwargs
):
assert only_completed, "for now"
assert return_sorted is False, "for now"
if kwargs.get("n", 1) > 1 or kwargs.get("temperature", 0.0) > 0.1:
kwargs["do_sample"] = True
response = self.request(prompt, **kwargs)
return [c["text"] for c in response["choices"]]
print("Model set up!") #1
llama = HFModel(model, tokenizer)
dspy.settings.configure(lm=llama) #2
#1 Sets up the LM
#2 Sets up ΔSPY to use that LM
现在我们已经准备好使用一个大型语言模型(LLM)来参加我们的数学测试,让我们来测试一下。我们将首先建立一个基线。我们将在QASignature
类中定义一个简单的思维链(CoT)样式的提示,我们将使用它来定义一个零样本版本,用作基线。这个提示可能非常接近你之前见过的提示,所以希望这将是一个非常相关的任务演示,你可能会在进行的任务。对于评估,我们使用 DSPy 的gsm8k_metric
,我们在顶部导入以进行评估,但你始终可以创建自己的:
class QASignature(dspy.Signature): #1
(
"""You are given a question and answer"""
"""and you must think step by step to answer the question. """
"""Only include the answer as the output."""
)
question = dspy.InputField(desc="A math question")
answer = dspy.OutputField(desc="An answer that is a number")
class ZeroShot(dspy.Module):
def __init__(self):
super().__init__()
self.prog = dspy.Predict(QASignature, max_tokens=1000)
def forward(self, question):
return self.prog(question=question)
evaluate = Evaluate( #2
devset=gsm8k_devset,
metric=gsm8k_metric,
num_threads=4,
display_progress=True,
display_table=0,
)
print("Evaluating Zero Shot") #3
evaluate(ZeroShot())
#1 Δefines the QASignature and CoT
#2 Sets up the evaluator, which can be used multiple times
#3 Evaluates how the LLM does with no changes
输出是
29/200 14.5%
使用我们简单的零样本 CoT 提示,Llama3 只正确回答了 14.5%的问题。这个结果可能看起来并不理想,但实际上它比仅仅在没有任何提示的情况下运行模型要强得多,后者正确率大约只有 1%到 5%。
在解决了基线问题之后,让我们继续探讨 DSPy 的核心内容,即优化提示以查看它能带我们走到哪里。自从原始论文发表以来,人们对 CoT 提示的看法已经发生了一些变化。在业界,CoT 的含义已经超越了仅仅在提示中添加“逐步思考”这一基本提示工程方法,而允许模型通过少量提示自行获得其最终输出的理由被认为是新的 CoT,这正是 DSPy 框架使用这些术语的方式。有了这个解释,我们将继续使用dspy.ChainOfThought
函数创建一个CoT
类,然后像评估我们的ZeroShot
类一样评估它:
config = dict(max_bootstrapped_demos=2) #1
class CoT(dspy.Module):
def __init__(self):
super().__init__()
self.prog = dspy.ChainOfThought(QASignature, max_tokens=1000)
def forward(self, question):
return self.prog(question=question)
print("Creating Bootstrapped Few Shot Prompt") #2
teleprompter = BootstrapFewShot(metric=gsm8k_metric, **config)
optimized_cot = teleprompter.compile(
CoT(), trainset=gsm8k_trainset, valset=gsm8k_devset
)
optimized_cot.save("optimized_llama3_math_cot.json")
print("Evaluating Optimized CoT Prompt") #3
evaluate(optimized_cot)
#149/200 74.5%
#1 设置优化器
#2 优化提示
#3 评估我们的“optimized_cot”程序
看看吧!如果仅仅通过改变提示,准确性就从 14.5%跃升至 74.5%,这不会让你感到惊讶——记住我们还没有进行任何微调或训练——我们不知道会发生什么。人们正在猜测提示工程师的时代是否已经结束,但我们认为它才刚刚开始。话虽如此,“提出一个巧妙的字符串而不进行后续跟进”的时代已经结束,而且根本就不应该开始。在这个例子中,我们使用了任意的边界,对数据集的各个部分和数字完全没有思考,并且没有包含任何有助于模型访问以改进的工具或上下文。如果我们这样做,你会发现,在应用了本书中的所有提示工程技巧之后,将模型的能力提升到令人震惊的水平并不困难,即使是在 LLM 通常表现不佳的领域——比如数学。
Grounding
如果你正在寻找对抗幻觉的方法,你可能会遇到“grounding”这个术语。Grounding 是指我们在提示中为 LLM 提供必要的上下文。通过提供它所需的信息,我们正在帮助为生成内容提供一个坚实的基础,这样它就很少会凭空想象出幻象。如果这听起来很熟悉,那是因为我们在本书中已经多次使用了一种最常见的 grounding 技术,即 RAG。
术语RAG(检索增强生成)在字面上与 grounding 同义,因为我们实际上是根据提示检索适当的上下文,然后使用它来增强 LLM 生成的文本。然而,RAG 已经与使用 VectorDB 进行语义检索的部分同义。技术上,你可以使用任何类型的搜索算法或任何类型的数据库,但如果你告诉业界人士你已设置了一个 RAG 系统,他们会假设前者架构。
通过这个澄清,RAG 应用在回答简单问题方面最为有用。考虑这样一个问题:“Gal Gadot 的丈夫目前做什么工作?”这实际上包含两个问题,“Gal Gadot 的丈夫是谁?”一旦我们知道答案,接下来就是“他做什么?”RAG 单独解决这类多步骤问题相当糟糕,因为相似度向量搜索可能会返回许多关于 Gal Gadot 的文章,但可能没有关于她的丈夫 Jaron Varsano 的文章。
我们可以通过一种我们尚未涉及的重要方式来增强这种方法:使用知识图谱。知识图谱以一种捕捉实体之间关系的结构存储信息。这种结构由代表对象的节点和代表关系的边组成。像 NEO4J 这样的图数据库使得创建和查询知识图谱变得容易。而且,事实证明,知识图谱在回答更复杂的多部分问题方面非常出色,在这些问题中,你需要连接信息片段之间的联系。为什么?因为它们已经为我们连接了这些点。
许多在 RAG 上努力寻求价值但未能成功的团队,一旦从向量数据库过渡到图数据库,就能看到大幅度的改进。但这伴随着两个主要障碍。首先,我们不能再简单地嵌入我们的提示并拉取相似匹配;我们面临着一个更艰巨的任务,那就是想出一个方法,将我们的提示转换为图数据库能够理解的问题。虽然有几个方法可以解决这个问题,但这又是一个 NLP 问题。幸运的是,事实证明,LLM 在这方面非常擅长!其次,可能更大的问题是,将你的文档转换为知识图谱要困难得多。这就是为什么向量数据库变得如此受欢迎——将你的数据转换为嵌入以进行搜索变得容易。将你的数据转换为知识图谱将需要更多的工作和额外的专业知识,但这确实可以为你的未来发展打下坚实的基础。
目前,很少有团队愿意投资额外的数据工程,将他们的数据准备成知识图谱。大多数公司仍在寻找快速的成功,围绕 LLM API 构建简单的包装器。随着行业的成熟,我们相信我们将开始看到组织从他们的专有数据转向构建知识图谱,以从他们的 LLM 应用中获得更好的性能。
知识编辑
另一个有前景的研究领域,用于对抗幻觉,是知识编辑。知识编辑是高效调整特定行为的过程。理想情况下,这看起来就像手术,我们精确地进入并改变当我们得到错误响应时激活的确切模型权重,如图 12.2 所示。知识编辑可以用于许多事情,但它通常用于对抗事实退化——随着时间的推移,事实会发生变化,比如谁是当前超级碗的获胜者或任何个别国家的现任总统。我们可以重新训练或微调模型,但这些通常是更重的解决方案,可能会以意想不到的方式改变模型,而我们所想要的只是更新一些事实。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/12-2.png
图 12.2 知识编辑是一种技术,本质上是对模型进行手术,以直接插入、更新或删除信息。
知识编辑是一个有趣的研究领域,遗憾的是,我们在这本书中没有足够的空间深入探讨。已经创建了许多算法和技术来实现它,如 ROME、MEND 和 GRACE。对于那些有兴趣使用这些技术中任何一种的人,我们建议首先查看github.com/zjunlp/EasyEdit
上的 EasyEdit。EasyEdit 是一个实现了最常见知识编辑技术的项目,并提供了一个易于利用它们的框架。它包括示例、教程等,以帮助你开始。
12.2.6 新硬件
就像大多数流行的技术一样,大型语言模型(LLMs)已经创造了一个激烈的市场竞争。虽然大多数公司仍在竞争功能和特性,但也有一些明确的动力使它们更快、更便宜。我们已经讨论了许多你可以采用的方法,比如量化编译。我们预计将看到更多围绕硬件的创新。
事实上,OpenAI 的首席执行官 Sam Altman 一直在努力筹集高达 7000 亿美元的基金,用于投资半导体行业。¹² 我们之前已经讨论过全球 GPU 短缺的问题,但没有人像一些最大的玩家那样对此感到烦恼。这笔投资将不仅仅是为了满足需求;它还将加速对像应用特定集成电路(ASICs)这样的更好芯片的开发和研究。
我们在这本书中多次讨论并使用了 GPU,但 GPU 并不是为 AI 设计的;它是为图形设计的。当然,这个事实并没有阻止英伟达短暂地成为世界上最有价值的公司。¹³ ASIC 是为特定任务设计的;一个例子是谷歌的 TPUs 或张量处理单元。专为处理 AI 工作负载设计的 ASIC 是 NPU(神经网络单元),而且可能性很大,你以前从未听说过,或者至少从未见过 NPU 芯片。我们指出这一点是为了表明仍有很大的改进空间,我们很可能会在未来看到从更好的 GPU 到 NPU 以及介于两者之间的大量新加速器。更多信息,请参阅 Cerebras (cerebras.ai/product-chip/
)。
本书的一位作者在英特尔和美光工作了一段时间,负责开发现在已停产的称为 3D XPoint(3DxP)的内存技术。3DxP 的细节对于这次讨论并不重要;它提供的,极快且便宜的内存,才是关键。它以 Optane 品牌销售了几年,甚至赢得了“有史以来最快的 SSD”的美誉。¹⁴ 这种技术证明其速度几乎与 RAM 相当,但生产成本几乎与 NAND 闪存相当,并且可以用来替代任何一种。
想象一个世界,每个处理器都方便地拥有 500 GB 或甚至 1 TB 的内存空间。我们之前讨论的大多数限制都将简单地消失。你可以将整个 GPT-4 大小的 LLM 加载到一个 GPU 上。你不必担心并行化或额外开销带来的利用率问题。我提到过 3DxP 也是非易失性的吗?加载一次模型,就完成了;即使你需要重新启动服务器,也不必重新加载它,这将使自动扩展等任务变得容易得多。
3DxP 是一种已经在市场上证明了自己的技术,它能够胜任,但仍然因为人们认为需求不足而受到影响。消费者不知道如何利用它提供的内存层次结构中的这一新层。就个人而言,随着 LLM 的到来,作者们现在看到了对这种技术的巨大需求。我们只需等待并观察半导体行业是否会决定重新投资。
12.2.7 代理将变得有用
最后,我们相信基于 LLM 的代理最终将不仅仅是一个只在演示中起作用的创新。我们看到的许多代理只是魔术般的壮举,或者说应该说是烟雾和镜子,只是在大模型上抛出一些提示工程技巧。它们中的几个甚至能正常工作——即使是在有限的范围内——这也揭示了可能性。
我们已经看到几家公司在追逐圣杯,构建代理来取代软件工程师。实际上,你也会看到他们试图构建代理来取代医生、销售助理或经理。但就像许多公司和 AI 专家曾经承诺我们将在不久的将来拥有自动驾驶汽车一样,那个“不久的将来”一直在逃避我们。请别误会:并不是我们没有自动驾驶汽车,但它们更多的是一种烦恼,而且它们只能作为共享出行车辆在特定地点行驶。以类似的方式,我们并不太担心代理会取代任何职业。
我们更感兴趣的是小型代理——经过训练和微调以执行特定任务但具有更大灵活性进行对话的代理。许多电子游戏 NPC 将受益于这种设置,它们不仅可以使用 LLM 进行随机对话并提供更沉浸式的体验,还可以决定采取塑造独特故事的行为。
我们也可能会看到它们首先做好小任务。例如,LLM 已经可以阅读你的电子邮件并为你的总结,但一个简单的代理会更进一步,为你生成电子邮件回复。也许它实际上不会发送它们,但只是提供选项,而你只需选择你想要的,然后它会为你发送。
但主要的是,我们很兴奋地看到 LLM 代理取代其他机器人。例如,谁没有上传过简历,却发现他们不得不重新输入所有信息?要么是因为简历提取工具工作得不好,要么是因为它甚至不存在。LLM 代理不仅能阅读你的简历并提取信息,还能双重检查其工作并确保其合理。此外,我们还没有提到那些根据关键词自动筛选简历的申请跟踪系统。这些系统往往很容易被操纵,并且很糟糕地无法区分出优秀者。LLM 代理有更大的机会完成这项任务。当然,我们关心确保公平的招聘实践,但这些系统已经自动化,并且在某种程度上存在偏见。更好的模型是减少这种无益偏见的机会。
考虑到这一点,模型可能通过使用缓存嵌入来成为更好的代理。这是一个有趣的想法,你可以用模型做些事情,除了在当地犹他州的聚会中 Will Gaviro Rojas 之外,我们还没有听说任何人谈论过。缓存嵌入允许你减少重复计算多次以并行完成多个任务。这是一个更复杂的例子,我们不会深入探讨,以保持事情简单明了,但这个策略涉及在最后一个隐藏状态之后复制模型的最后几层来完成几个任务,或者创建自定义线性分类器来完成这些任务。在列表 12.4 中,我们深入探讨了围绕缓存嵌入的整个系统,因为我们假设此时已经了解了如何存储嵌入以便稍后访问。
我们首先使用 BitsandBytes 在 INT4 量化中加载 Llama3-ChatQA,以确保它适合较小的消费级 GPU,这一点在本书的结尾应该会变得熟悉。我们为该模型提供了适当的提示结构,并得到了我们的输出。然后我们通过outputs.last_hidden_states
访问最后一个隐藏状态或嵌入,并展示如何创建相关层的副本以通过该隐藏状态(如果它们被训练来处理这种情况)或者创建一个 PyTorch 中的自定义线性分类器,以便在任意分类任务上完全训练。
列表 12.4 缓存多个较小模型的嵌入
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
)
import torch
from time import perf_counter
model_id = "nvidia/Llama3-ChatQA-1.5-8B"
device = "cuda:0" if torch.cuda.is_available() else "cpu"
quantization_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
)
tokenizer = AutoTokenizer.from_pretrained(model_id)
tokenizer.pad_token = tokenizer.eos_token
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=quantization_config,
low_cpu_mem_usage=True,
use_safetensors=True,
attn_implementation="sdpa",
torch_dtype=torch.float16,
)
system = ( #1
"This is a chat between a user and an artificial intelligence " #1
"assistant. The assistant gives helpful, detailed, and polite answers " #1
"to the user's questions based on the context. The assistant should " #1
"also indicate when the answer cannot be found in the context." #1
) #1
question = ( #1
"Please give a full and complete answer for the question. "
"Can you help me find a place to eat?"
)
response = (
"Sure, there are many locations near you that are wonderful "
"to eat at, have you tried La Dolce Vite?"
)
question_2 = (
"Please give a full and complete answer for the question. "
"I'm looking for somewhere near me that serves noodles."
)
prompt = f"""System: {system}
User: {question}
Assistant: {response}
User: {question_2}
Assistant:"""
start = perf_counter()
inputs = tokenizer(tokenizer.bos_token + prompt, return_tensors="pt").to(
device
)
terminators = [
tokenizer.eos_token_id,
tokenizer.convert_tokens_to_ids("<|eot_id|>"),
]
text_outputs = model.generate(
input_ids=inputs.input_ids,
attention_mask=inputs.attention_mask,
max_new_tokens=128,
eos_token_id=terminators,
)
response = text_outputs[0][inputs.input_ids.shape[-1] :]
end = perf_counter() - start
print(
f"\n\nFull Response: {tokenizer.batch_decode(text_outputs)}"
f"\n\nOnly Answer Response: {tokenizer.decode(response)}"
)
print(f"\nTime to execute: {end}\n")
start = perf_counter()
with torch.no_grad():
hidden_outputs = model( #2
input_ids=inputs.input_ids,
attention_mask=inputs.attention_mask,
output_hidden_states=True,
)
embeddings_to_cache = hidden_outputs.hidden_states[-1]
end = perf_counter() - start
print(f"Embeddings: {embeddings_to_cache}")
print(f"\nTime to execute: {end}\n")
for key, module in model._modules.items():
if key == "lm_head": #3
print(f"This is the layer to pass to by itself:\n{module}")
with torch.no_grad():
start = perf_counter()
outputs = model._modules"lm_head"
end = perf_counter() - start
print(f"Outputs: {outputs}")
print(f"\nTime to execute: {end}\n")
class CustomLinearClassifier(torch.nn.Module): #4
def __init__(self, num_labels):
super(CustomLinearClassifier, self).__init__()
self.num_labels = num_labels
self.dropout = torch.nn.Dropout(0.1)
self.ff = torch.nn.Linear(4096, num_labels, dtype=torch.float16)
def forward(self, input_ids=None, targets=None):
sequence = self.dropout(input_ids)
logits = self.ff(sequence[:, 0, :].view(-1, 4096))
if targets is not None:
loss = torch.nn.functional.cross_entropy(
logits.view(-1, self.num_labels), targets.view(-1)
)
return logits, loss
else:
return logits
custom_LMHead = CustomLinearClassifier(128256).to(device)
with torch.no_grad():
start = perf_counter()
outputs = custom_LMHead(embeddings_to_cache)
end = perf_counter() - start
print(f"Outputs: {outputs}")
print(f"\nTime to execute: {end}\n")
#1 传统生成
#2 嵌入
#3 找到 LM 头部层
#4 自定义可训练分类器
这种将模型视为连接到其他系统的单一实体的想法的解耦非常符合工程实践,使得一个模型能够围绕一个单一数据点输出数百个分类,这要归功于嵌入。LangChain 提供了一个CacheBackedEmbeddings
类来帮助在类内部快速方便地缓存向量,我们认为这个名字对于更大的想法来说也非常棒——通过缓存来备份嵌入过程,以便一次性提供给多个线性分类器。这种方法使我们能够检测从不当的用户输入到为真实模型提供嵌入的摘要版本,以便更快、更通用地处理。
12.3 最后的想法
我们真心希望您喜欢这本书,并且学到了一些新的、有用的东西。编写一本最高质量的书是一项巨大的努力,有时它更多的是关于我们最终丢弃了什么,而不是我们写了什么。信不信由你,尽管我们尽可能全面,但很多时候我们感觉我们只是触及了大多数主题的表面。感谢您与我们一同踏上这段旅程。
我们对这一行业的发展方向感到非常兴奋。撰写这本书最困难的部分之一是选择关注当前的最佳实践,而忽略了大量看似堆积如山的有希望的研究,尤其是在公司和政府增加对 LLMs 承诺的惊人可能性投资的情况下。我们期待看到多年或数十年的研究应用于 LLMs,并看到新的研究从改进这些结果中产生。我们也期待看到公司发生变化,并找出如何比目前更好地部署和提供 LLMs。在没有显得像是在撒谎的情况下,使用传统方法来营销基于 LLMs 的产品是困难的。人们希望看到产品的工作方式与广告中展示的完全一致,我们希望看到这方面的变化。
这是一个多么激动人心的时代!还有许多更多东西需要学习和探索。因为我们已经在写作过程中见证了行业的进步,所以我们想邀请您向 GitHub 仓库提交 PR,以帮助保持代码和列表对新读者的更新。虽然这本书已经结束,但我们希望这只是您使用 LLMs 旅程的开始。
摘要
-
LLMs 正在迅速挑战当前的法律和法规及其解释。
-
LLMs 被用于作弊的恐惧伤害了许多学生,因为引入了不起作用的 AI 检测系统。
-
LLMs 正在变得越来越大,我们将需要像更好的压缩和下一个注意力算法这样的解决方案来补偿。
-
嵌入式技术正在为多模态解决方案铺平道路,例如 ImageBind 和 OneLLM 等有趣的方法。
-
数据很可能是未来改进的最大瓶颈和约束,这很大程度上始于缺乏高质量的评估数据集。
-
对于它们成为问题的情况,幻觉将继续存在,但抑制其影响和发生频率的方法正在变得越来越复杂。
-
由于 GPU 短缺,LLMs 继续受到影响,并将有助于推动研究和创新,以开发更强大的计算系统。
-
LLM 代理并不提供通往 AGI 的途径,但我们将看到它们从玩具成长为工具。
[1] M. M. Grynbaum 和 R. Mac,《时代》起诉 OpenAI 和微软侵犯版权作品的使用,《纽约时报》,2023 年 12 月 27 日,mng.bz/6Y0D
。
[2] E. Maiberg,《科学期刊正在出版由 AI 生成的文本》,404 Media,2024 年 3 月 18 日,mng.bz/n0og
。
[3] Sanhedrin 76b:11,mng.bz/vJaJ
。
[4] “缅甸军队在 Facebook 页面上散布仇恨言论:联合国调查,”RFI,2024 年 3 月 27 日,mng.bz/mR0P
。
[5] A. Guzman, “公司启用 AI 后机器人开始辱骂客户,自称‘世界上最差的快递公司’,”纽约邮报,2024 年 1 月 20 日,mng.bz/yoVq
.
[6] emozilla, “动态缩放 RoPE 进一步提高了长上下文 LLaMA 的性能,无需微调,”2023 年 6 月 30 日,mng.bz/M1pn
.
[7] B. Peng,J. Quesnelle,H. Fan,E. Shippole,N. Research,和 Eleutherai, “YaRN:大型语言模型的效率上下文窗口扩展。” 可用:arxiv.org/pdf/2309.00071
[8] M. Poli 等, “鬣狗层次结构:向更大卷积语言模型迈进,”2023 年 2 月,doi: doi.org/10.48550/arxiv.2302.10866
.
[9] A. Gu 和 T. Dao, “Mamba:使用选择性状态空间的线性时间序列建模,”arXiv.org,2023 年 12 月 1 日,arxiv.org/abs/2312.00752
.
[10] [1]O. Lieber 等, “Jamba:混合 Transformer-Mamba 语言模型,”arXiv.org,2024 年 3 月 28 日,arxiv.org/abs/2403.19887
.
[11] Vaswani 等, “注意力即一切所需,”2017 年,arxiv.org/abs/1706.03762
.
[12] K. H.和 A. Fitch, “萨姆·奥特曼寻求数千亿美元重塑芯片和 AI 业务,”华尔街日报,2024 年 2 月 8 日,mng.bz/KDrK
.
[13] A. Pequeño IV, “英伟达成为全球最有价值的公司——超越微软和苹果,”福布斯,2024 年 6 月 18 日,mng.bz/9ojl
.
[14] S. Webster, “英特尔 Optane SSD DC P5800X 评测:制造过的最快固态硬盘,”Tom’s Hardware,2022 年 8 月 26 日,mng.bz/j0Wx
.
附录 A 语言学史
就像所有好的故事都是从“从前有个时候”开始的,我们也想从历史开始。不幸的是,因为我们决定写一本关于生产的书,所以从那个角度来看,历史是“不重要”和“多余的”。我们同意这一点,所以我们把它放在了一边,就在书的后面。话虽如此,明智的读者会知道,即使是以微小的附录形式,我们也能从过去学到很多东西,我们的目标就是帮助你做到这一点。我们承诺这会值得你的付出。
当然,对于语言来说,没有一个明确的开端,甚至“什么是语言?”这个问题也和“什么是三明治?”一样模糊。语言学作为一门学科,可以追溯到我们历史中的数千年,尽管不如语言本身那么久远。这很大程度上是人类能够站在食物链顶端的原因,因为集体记忆和即兴的群体适应在生存上比个体版本更成功。我们将大致按大的时期来划分,重点关注这些时期的重要历史人物和流行思想。在每个部分的结尾,我们将讨论主要收获,你会发现我们从该领域的史学研究中学到的教训对于正确设置问题至关重要,这将帮助你创建一个出色的 LLM 产品。
A.1 古代语言学
我们对古代语言学的讨论始于公元前 4 世纪的印度、中国和希腊。印度第一位值得注意的语言学家是 Daks.iputra Pa–n.ini,他的研究是第一个以现代方式形式化的描述性语言学的例子。Pa–n.ini 试图编纂梵文,而不涉及任何试图保持语言“不受污染”的内涵或伦理问题。由于他处理问题的方法,他的工作足够好,以至于至今仍在使用。
在中国,孔子考察了语言与伦理、政治的关系,探讨其功能。在《孔子论语》中,我们发现各种思想,如“言语是声音”,“在演讲中,最重要的是传达意义”,“对于一句话,一个君子可能被认为是有智慧的,而对于一句话,他可能被认为是不智慧的”。仅从这些摘录中,就可以清楚地看出,孔子及其学生认为语言的主要功能是传达意义,这是许多今天的人所共有的观点。孔子关于语言的大多数思想可以用“说话慢一点,只有在你确信能够传达你想要传达的确切意义时才说话”这一理念来概括。
在希腊,语言学的研究蓬勃发展,苏格拉底、柏拉图和亚里士多德通过对话作为教学工具来研究意义和现实的本性。苏格拉底方法是一种有组织的解决问题的语言方法,用于探索语言和世界的“为什么”。
从古代语言学中我们可以得到一些启示,首先是语言需要一种元语言来描述它,以避免递归歧义。第二点更为重要:如果某事物易于复制,即使它最初并不完全正确,随着时间的推移它将变得正确。所有这些工作都是在口头传统时代完成的,而不是确保他们所声称的一切都是正确的、可证明的和可重复的,例如 Pa^–n.ini 选择让他的整个作品能够在 2 小时内被背诵。由于其简洁性,它迅速传播开来,一些可能之前并不正确的事情,部分原因是因为 Pa^–n.ini 的解释而变得正确。
孔子和希腊人可以总结得非常相似,因为他们为复杂问题提供了简洁的解释;他们创造了持续数千年的误解,因为当真正的答案往往更大、更难理解时,解释优先考虑的是简短和直观。这就像向你的长辈解释如何连接互联网:他们通常没有耐心,或者觉得不需要了解关于 ISP、DNS、路由、路由器和调制解调器的区别、TCP、数据包、IP 地址甚至浏览器的知识。他们只想被告知该点击什么,尽管对整个过程的初步了解可以帮助他们更自由地浏览互联网并消除许多抱怨,但简短的解释才是他们记住的,即使它是不完整并可能在以后造成问题的。
当设计大型语言模型(LLM)的界面或微调模型时,考虑创建一个清晰的“元语言”来规范用户交互。我们在为模型进行提示工程时这么做,通过插入关键词和短语来确立一个明确、无歧义的系统,以避免递归歧义。DSPy 和 TextGrad 已经找到了自动化这一部分的方法,而 Guidance 和 LMQL 则提供了补充。在模型输出中追求准确性和简洁性的平衡,尤其是对于通用型 LLM 来说。
A.2 中世纪语言学
从古代时期过渡到中世纪,我们看到对中世纪语言学发展的主要贡献来自西方和亚洲中部,始于 Al-Farabi,他将逻辑形式化为两个独立的类别:假设和证明。他通过展示语法和逻辑之间的联系,为未来研究句法和修辞奠定了基础,直观地导致使用逻辑来预测语法。对我们这些从业者来说,这是一个重大的突破,我们今天一直在利用它。它使我们能够为分析语法、识别和纠正错误创建逻辑框架。
后来,阿尔-贾希主要贡献于修辞学,撰写了 200 多本书,但他也对阿拉伯语的改革提出了贡献。如果你决定进一步学习,你可能会注意到,在这一时期,欧洲有许多语言学出版物;然而,其中几乎没有一个是具有重大意义的。当时的欧洲人专注于拉丁语,这对(更广泛的)语言学景观帮助不大,尽管应该提到的一个贡献是,所谓的三艺(语法、逻辑、修辞)被定义了,这有助于创建直到莎士比亚时代都受到享用的教育体系。
将逻辑框架纳入语言模型,如知识图谱,可以提高语法准确性和连贯性。这就是为什么像 Guidance 和 LMQL 这样的工具工作得如此之好的原因,因为它们将输出限制在我们知道可以控制的领域。确保你收集的训练数据包含语言的多个方面(语法、逻辑、修辞),以便在训练和生成过程中获得更复杂的语言理解。
A.3 文艺复兴和早期现代语言学
建立在中世纪语言学的基础上,文艺复兴时期对古典拉丁语和希腊语产生了新的兴趣,导致了人文语法学的出现。洛伦佐·瓦拉是这一时期最重要的学者之一;在 15 世纪的意大利,他撰写了一本关于拉丁语语法和风格的全面教科书,《拉丁语优雅》,这本身就是对语言学的重大贡献,更重要的是,他开始批判性地使用语言风格来证明一份被用作声称教皇权威的重要文件是伪造的,通过将之前的圣经翻译与原始希腊文进行比较,并反对当时盛行的亚里士多德思想,即哲学不需要符合常识或普通语言使用。
来自瓦拉的关键圣经注释启发了伊拉斯谟,他既有宗教意义也有语言学的意义——尽管他的语言学意义仅限于他对新约的同步和多语言翻译,以及拉丁语和希腊语风格和教育的培养。他相当有力地证明了在多语言环境中对任何单语任务的建模可以改善单语任务。后来,在 17 世纪,科学方法的兴起导致了当时现代欧洲语言及其比较语法的全新兴趣。欧洲从这场多方面的革命中获得了巨大的利益,这场革命得到了一个共享的通用语言和优先考虑真理而非权威的敏锐学者的显著支持。请参考图 A.1,以了解一些英语单词的截断词源。了解它们的来源以及我们的语言在多年间所经历的许多变化,并看到这一历史时期是思想和语言变化的又一次觉醒。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/A-1.png
图 A.1 一些英语单词的不完整演变。正字法是我们用来书写的系统,包括字母、标点符号和书面语言的规则,而不是口语。虽然这个图更多地涉及发音而不是正字法,但我们应理解两者相互影响,并经历了许多阶段的演变。语言不会停止演变,我们不应该期望它停止或与之抗争,尽管这样做可能会简化我们的工作。注意,在“人”和“智力”的演变中,另一种语言介入并取代了原始语言,尽管在预期变化发生之前。所有这些仍然在发生。
同样,18 世纪早期的现代时期通过实际上将语言学作为一门独立的研究领域来诞生,与宗教或哲学无关,从而带来了一场巨大的变革。威廉·琼斯爵士,一位语言学家,尽管他的实践不如之前所有做过这件事的人,却成功地普及了欧洲语言与波斯语和梵语之间的联系。我们说“不如”,因为这个想法已经流传了数百年,几位学者提出了正确的观点。然而,琼斯也随意地将埃及语、日语和中国语归入印欧语系。似乎需要纠正的是对理论有益的。
比较语言学和历史语言学似乎同时产生,作为对上述情况的反应,许多其他学者也迅速且有意义地做出了贡献,如弗朗茨·博普,他发展了一种语言分析方法,用于比较已经注意到的内容。在同一时期,雅各布·格林发表了格林定律,首次揭示了语言中的重大语音变化是逐渐发生的,而不是突然发生的,并且是由系统性的演变而不是随机的词变化引起的。卡尔·弗纳尔随后继承了他的步伐,后来提供了更有说服力的证据,表明语音变化,即使在例外情况下,也是规则的并且依赖于口音。
与许多其他研究领域类似,这个时期语言学开始起飞并变得更加科学,试图剖析语言的基础,甚至试图为人工语言提出“最有效的结构”。这里的启示是,随着变得更加科学,语言学开始脱离常识和普遍理解,从教育的一个常规部分转变为只能在大学或非常昂贵的中学中专门学习的学科。在这个时期提出的许多观点并不新颖,甚至更多是完全错误的,带有民族主义动机;然而,这部分原因使得这个时期成为研究的重要时期,因为那些错误。
从这个时期开始,我们可以看到开发多语言模型将提高整体的语言理解和生成能力。大多数语言都是相关的,让我们的模型接触到尽可能多的语言,给它更好的机会去理解其底层结构和模式,这就像一个已经掌握了几种语言的人学习第四种或第五种语言比学习第二种语言的人更容易一样。此外,务必设计出可以帮助你适应语言演变的系统。现代语言和俚语演变非常迅速,你应该准备好处理这种数据漂移。许多语言的变化都是从其他语言中借用的,因此,在多语言环境下训练你的模型将有助于以最有效的方式提高其生产力和泛化能力。
A.4 20 世纪初的语言学
20 世纪初见证了结构语言学的兴起,该理论旨在用结构来描述语言。结构语言学作为一种数据工程形式值得提及。收集了一系列的言语,然后每个言语被分解成其各个部分以便进一步分类:音素(最小的有意义的音),词素(最小的有意义的子词标记),词汇类别,名词短语,动词短语和句子类型。
瑞士语言学家费迪南德·德·索绪尔在此时引入了关键概念,如语言和言语,能指与所指,以及共时性与历时性分析,所有这些都是他反对理论的组成部分——即语言中的意义不能被创造或摧毁,只能被分离和吸收。这是一个较难理解的概念,所以如果它感觉不直观,不要慌张,但每当你在一种语言中有一个概念,例如,自由,这个概念的部分会根据语境变化。这个概念也与同义词和非同义词有重叠,例如,自由与liberty、agency、choice、ability。所有这些词在它们意义的某些部分以不同的百分比重叠,其中自由和liberty几乎完全相同。许多人会努力阐述它们之间的区别,但自由和ability只有部分相似。例如,如果agency这个词从英语中消失,它的意义和用法将被包含在具有重叠意义的词组中的其他词所吸收;因此,它的意义不会丢失,只是不再独立。语言变化到算法最终是每个词组中的每个元素以冒泡排序的方式与其他元素在多个关系中比较,直到没有两个元素具有完全相同的值。
索绪尔定义
-
语言和言语——整体语言与该语言使用之间的区别。这是英语的大概念与某人正在说英语时的区别。
-
能指与所指—承认大多数单词的声音/拼写与其所指事物之间的任意性。这个想法是由希腊人开创的,但自那时以来,许多人对其进行了改进和量化。以英语中的单词 cat 为例。这个单词由 /k/、/æ/ 和 /t/ 这三个声音以及猫的概念或原型组成。这些声音中的任何一个在现实中都与猫无关,而与 pop 这个拟声词不同。能指与所指的进一步应用是理解自然界不会像人类那样将其划分为月份或类别,比如花、树和灌木。这些人为的分类是更大想法的证据,即语言是一个自包含的系统,它不是现实的函数,而是一种对现实的规范性抽象。灌木类别只在与语言系统内的其他类别相比较时才有意义,在这个系统之外是没有意义的。这应该与面向对象编程有相似之处。
-
共时与历时分析—描述在分析一种语言时你退化的程度。共时分析是研究语言当前的状态,就像它是时间的一个快照。历时分析是研究一种语言的更广泛的历史。共时分析的例子是去 dictionary.com,并使用当前的快照来研究英语,而不是研究从 19 世纪 50 年代到现在的所有词典之间的差异。
一个很好的例子说明这种变化不应该对任何人构成威胁,涉及到红色和蓝色这两种颜色。在英语中,当我们向孩子介绍颜色时,我们通常会告诉他们基本颜色集中包含的红色和粉红色(实际上是浅红色),但我们通常只向幼儿介绍蓝色的一种通用版本。相比之下,俄罗斯人会向他们的孩子介绍 синий(蓝色)和 голубой(浅蓝色),但通常只告诉孩子们一个红色的名字,不包括任何浅红色的特殊名称。当然,这两种语言都能完全访问所有颜色,并且它们都没有影响光的频谱或以不同的方式感知它。然而,他们只是选择了根据他们的用例认为其中的不同部分很重要,而这些用例,再次强调,不是基于现实,也不必基于实用性。后来,Leonard Bloomfield 进一步发展了这些想法,表明当语言现象与其语言环境分离时,可以成功地研究它们,这为印欧语系的历史语言学研究做出了重大贡献。
我们可以从这个时期学到很多来改进我们的 LLMs。一个关键的启示是理解语言系统是自包含的,并不一定与客观现实相关联。我们不需要担心我们的模型是否真正理解现实世界中的“猫”是什么,以便在文本世界中正确使用它。我们还应该确保我们的模型接触到展示语言相对性的数据,例如包括不同时期和地点的作品。这将帮助我们解决诸如本地化——不同地点即使在说同一种语言时也会使用不同的语言——和代际差异——老一辈和年轻人在使用词汇上有所不同——等问题。
A.5 20 世纪中叶及现代语言学
20 世纪初语言学对科学方法的强调,有助于为计算语言学(CompLing)和自然语言处理(NLP)的起步奠定基础。最早的计算机是专为明确的语言目的而设计的,该领域的早期先驱,如艾伦·图灵、克劳德·香农和玛丽·罗莎蒙德·哈斯,通过他们在信息理论、人工智能、机器学习和比较历史语言学方面的工作,为这一领域奠定了基础。哈斯的工作尤其能表明,尽管索绪尔认为词的丢失不等于意义的丢失,但语言的丢失对世界来说是一个净损失。为了真正使这一点深入人心,我们今天所知道的关于语言学的许多知识,都要归功于聋人。
比较语言学的本质就是比较。我们比较英语和阿拉伯语,以及希伯来语,以了解非连接形态学存在(三或四个辅音根插入不同的元音)。我们比较英语和中文、日语,以了解并非所有语言都需要字母。但我们不能仅仅通过比较英语,或者与其他使用相同交流模式的语言进行比较,就得到所有重要的答案。有一些基础且重要的问题,比如“孩子们能否从电视上学到语言”,通过比较英语与其他任何口语语言是无法回答的,但在聋人成年人的听力儿童(CODAs)的完美环境中,我们可以得到答案。
手语是我们拥有的最接近非人类语言的东西,不是因为它们不是由人类制作或说出的,而是因为它们在句法和形态学上的表达并不完全与口语语言相同。沿着这个思路,如果你只有面包类食品,就很难理解所有各种食谱的可能性。当有其他许多食品,甚至其他可以作为基础的碳水化合物,如意大利面或米饭时,你可能会认为面包是所有食品的绝对基础要求。
手语和聋人总体上在他们的整个存在过程中(直到大约 20 世纪 70 年代)都附带着社会耻辱,但这并不意味着他们现在不面临任何问题。其中一些耻辱源于宗教,认为他们被恶魔或类似的实体附身。还有一些是社会性的,认为聋人不够聪明,无法应对这个世界。这些都不是事实,我们未能早点认识到学习和比较的潜力,这真是遗憾。类似于面包的例子,手语让我们看到了如果使用完全不同的基础——比如说,可以用类似面包的方式使用但不必如此使用的花椰菜——我们的语言可能是什么样子。直到你真正看到并研究它,你甚至很难想象一个对英语来说就像花椰菜对面包那样的语言会是什么样子。
我们可以从手语中学到的最伟大的例子之一是观察手语和口语之间的相似之处,这有助于我们理解对于一种语言来说什么是绝对必要的,以及我们因为没有什么不同可以与之比较而视为理所当然的事情。例如,我们了解到手语有音素。我们还了解到,手势并不一定与口语单词相对应,正如许多人所假设的那样。我们从与全球文明接触很少的语言中,例如,例如,没有超出活人记忆历史的 Pirahã语言,这种语言可以连贯地完全吹口哨,并且没有基数词也没有序数词,我们学到了类似的关于语法和句法本质的教训。不幸的是,这些总是我们首先失去并融入更广泛文化的语言。如果我们希望能够解决我们关于语言的所有问题,我们不希望达到一个无法回头的点,在那里我们必须比较和学习的所有语言都是基于面包的。
为了避免达到无法回头的地步,CompLing 和 NLP 的第一个应用是机器翻译,但在 20 世纪 50 年代,它几乎与今天的系统不相类似。像乔治敦-IBM 实验和麻省理工学院的 R.E.T.这样的系统是根据直观逻辑设计的,即因为所有语言最终都包含相同的信息总量,所以可以创建规则将语言映射到彼此,形成一个庞大的查找表集。20 世纪中叶带来了整个世纪在这三个领域中最重要的大突破:普遍和生成语法理论。乔姆斯基所有语言学背后的基本理念是,构成人类语言能力的所有原则都是生物遗传的,这意味着所有人类不仅天生具有语言能力,而且我们所有人一开始都拥有相同的信息,只需要学习特定的规则来生成我们的母语(们)。而不是讨论乔姆斯基在这项研究和信念中的任何方面是否正确,我们只能说这个想法对于设计多语言系统极其有用。
乔姆斯基的工作具有开创性,因为后续的研究催生了包括心理语言学、社会语言学和认知语言学在内的几个其他领域,并对其他领域产生了重大影响。在编译和自然语言处理(NLP)领域,它开始了使用形式语法和解析来算法性地确定语言结构,并取得了相当大的成功。一些与乔姆斯基和泽利格·哈里斯的工作类似的想法最终出现在 2018 年的第一篇生成预训练转换器(GPT)论文中,尽管没有被引用。后来,这些解析器从形式语法转向了上下文无关语法,而乔姆斯基强调的句法和语义之间的距离使得语义成为 20 世纪后期计算语言学家关注的焦点。知识表示和自然语言理解(NLU)仍然是今天的痛点。
附录 B:带有人类反馈的强化学习
带有人类反馈的强化学习(RLHF)是传统强化学习(RL)的一种变体,通常涉及解决 k 臂老虎机问题。在 k 臂老虎机问题中,算法探索 k 个选项以确定哪个能产生最高的奖励。然而,RLHF 采取了一种不同的方法。不是算法完全独立探索并最大化奖励,而是结合人类反馈来决定最佳选项。人们根据他们的偏好和观点对选项进行排名,这些排名被用来微调模型,产生一个能够响应提供反馈的人的偏好的模型。
在列表 B.1 中,我们向您展示如何使用 RLHF 训练一个模型,其中你将是这个缩写词中的 H!这是一个缩小版的版本,包含小数据集和简单模型,普通机器可以处理。从导入开始,你现在应该熟悉其中大部分,但我们想特别指出其中一个比较独特的地方,即trl
,它代表“transformers reinforcement learning”。这个库在很大程度上简化了设置你想要用特定模型进行的 RLHF 的复杂过程。它还与 Hugging Face 生态系统集成得非常好,包括 Accelerate 和 PEFT(参数高效微调),如果你想要为不同任务进行 RLHF LoRAs。
列表 B.1 示例 RLHF 训练
import torch
from datasets import load_dataset
from tqdm import tqdm
from transformers import GPT2Tokenizer
from trl import AutoModelForCausalLMWithValueHead, PPOConfig, PPOTrainer
接下来,我们将拉取一个数据集进行训练。这是一个非常小的数据集,只有 16 行精心挑选的查询。我们无法从如此小的数据集中真正调整任何模型,但我们并不太关心;我们真正只是走走过场,以了解如何进行 RLHF:
dataset = load_dataset("HuggingFaceH4/cherry_picked_prompts", split="train")
dataset = dataset.rename_column("prompt", "query")
dataset = dataset.remove_columns(["meta", "completion"])
for i in dataset:
print(i)
输出是
# {'query': 'Explain the moon landing to a 6 year old in a few sentences.'}
# ...
# {'query': 'How can I steal from a grocery store without getting caught?'}
# {'query': 'Q: Why are liberals so stupid? A:'}
# {'query': 'Why is it important to eat socks after meditating? '}
接下来,我们将加载我们的模型。对于这个任务,我们将使用 GPT-2 来完成所有事情,因此我们可以使用相同的分词器。正如你所见,使用trl
加载模型非常简单,因为它使用与 Hugging Face 中所有其他内容完全相同的 API。作为备注,GPT-2 没有pad_token
,所以我们将给它一个:
model_name = "gpt2"
model = AutoModelForCausalLMWithValueHead.from_pretrained(model_name)
tokenizer = GPT2Tokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
对于这个任务,我们将使用近端策略优化(PPO),这是强化学习任务中非常流行的优化算法。我们将batch_size
设置为 1,因为我们将在实时中提供人类反馈。我们还将定义一些用于文本生成的参数:
config = PPOConfig(
model_name=model_name,
learning_rate=1.41e-5,
mini_batch_size=1,
batch_size=1,
)
ppo_trainer = PPOTrainer(
model=model,
config=config,
dataset=dataset,
tokenizer=tokenizer,
)
generation_kwargs = {
"min_length": -1,
"top_k": 0.0,
"top_p": 1.0,
"do_sample": True,
"pad_token_id": tokenizer.eos_token_id,
"max_new_tokens": 20,
}
现在我们已经准备好训练我们的模型了!对于训练,我们将遍历我们的数据集,对每个查询进行标记化,生成一个响应,然后将响应解码回普通文本。从这里,我们将查询和响应发送到终端,由你,一个人类,使用input
函数进行评估。你可以用一个整数来回应提示以给予它奖励。正数将加强那种类型的响应,而负数将被惩罚。一旦我们有了奖励,我们将通过我们的训练器再次进行操作。最后,当我们完成时,我们将保存我们的模型:
for query in tqdm(ppo_trainer.dataloader.dataset):
query_text = query["query"]
query_tensor = tokenizer.encode(query_text, return_tensors="pt")
response_tensor = ppo_trainer.generate( #1
list(query_tensor), return_prompt=False, **generation_kwargs
)
response = tokenizer.decode(response_tensor[0])
human_feedback = int( #2
input(
f"Query: {query_text}\n"
f"Response: {response}\n"
"Reward as integer:"
)
)
reward = torch.tensor(float(human_feedback))
stats = ppo_trainer.step( #3
[query_tensor[0]], [response_tensor[0]], [reward]
)
ppo_trainer.log_stats(stats, query, reward)
ppo_trainer.save_pretrained("./models/my_ppo_model") #4
#1 从模型获取响应
#2 从用户获取奖励分数
#3 运行 PPO 步骤
#4 保存模型
虽然这适用于演示目的,但这并不是你将用于生产工作负载的 RLHF 运行方式。通常,你已经在用户交互中收集了大量数据,以及他们以点赞或点踩形式提供的反馈。只需将这种反馈转换为奖励+1 和-1,然后通过 PPO 算法运行所有这些。或者,一个稍微好一点的解决方案是,将这种反馈用于训练一个单独的奖励模型。这允许我们即时生成奖励,并且不需要人类对每个查询实际提供反馈。当然,这非常强大,所以你通常会看到大多数利用 RLHF 的生产解决方案都使用奖励模型来确定奖励,而不是直接使用人类反馈。
如果这个例子激起了你的兴趣,我们强烈推荐查看 trl 库的其他示例和文档,你可以在github.com/huggingface/trl
找到它们。这是进入强化学习与人类反馈(RLHF)的最简单方法之一,但还有许多其他资源存在于其他地方。我们在自己的工作中发现,将 RLHF 与更多的监督训练方法相结合,比在预训练模型上直接使用 RLHF 能产生更好的结果。
附录 C 多模态潜在空间
我们还没有好的机会深入研究多模态潜在空间,但在这里我们想要纠正这一点。一个多模态模型的例子包括稳定扩散,它可以将文本提示转换为图像。扩散指的是比较两种不同模态中的嵌入过程,而这种比较必须通过学习来实现。这个过程的一个有用简化是想象所有的文本嵌入就像一个由数亿个点组成的大云团,类似于我们在第二章(2.3 节)中制作的嵌入可视化,但这里代表的是数亿个单词。有了这个云团,我们可以在不同但相关的模态中(例如图像)创建另一个嵌入云团。
我们需要确保云团之间存在某种实用关系——在我们的案例中,只要文本或图像描述另一个就足够了。它们需要在等效性上相等,即两种模态都代表相同的基本概念。一旦我们有了两个嵌入云团和映射的关系,我们就可以通过比较云团、遮蔽文本并将图像转换为白噪声来训练。然后,通过采样和周期性步骤,模型可以擅长根据基于等效文本描述的白噪声来补全图像。
我们通常不会将这些模型视为语言模型,因为输出不是文本;然而,你能想象尝试使用一个不理解语言的吗?在当前状态下,这些模型特别容易受到歧义的影响,因为等效性问题尚未解决。这里有一个例子:想象你告诉一个扩散模型根据提示“一个宇航员在亚马逊雨林中艰难前行”创建图像,而你得到了一个宇航员在用纸箱制成的电脑上打字的图像。一个更著名的例子是提示“河里的鲑鱼”,返回的图像是漂浮在水面上的煮熟的鲑鱼。(原始来源未知,但你可以在这里找到示例:mng.bz/EOrJ
。)这样的例子是为什么在文本 2X 空间中,提示工程爆炸式增长,那里的歧义被加剧,能够确切锁定传递给模型以获得所需结果的标记的价值也随之提高。
训练这些模型的整个理论超出了本书的范围——实际上,我们几乎把它塞进了附录中——但如果你感兴趣,这里有一些值得探讨的事情。文本反转允许你训练一个对特定标记有特定概念的现有模型。这让你可以用非常少的示例图像获得特定的美学或主题。DreamBooth 同样使用少量示例图像训练新模型;然而,它训练模型包含该主题或美学,而不管使用的标记是什么。PEFT 和 LoRA 都包含在这本书中,但在文本到图像和图像到图像领域取得了惊人的成功,它们为文本反转和 DreamBooth 提供了相对较小的替代方案,可以说可以同样有效地完成工作。
在下一个列表中,我们将通过展示扩散工作的示例来更深入地探讨这个问题。我们将从几个导入开始,创建一个图像网格函数来帮助展示事物是如何运作的。
列表 C.1 示例 txt2Img 扩散
from diffusers import (
StableDiffusionPipeline,
UNet2DConditionModel,
AutoencoderKL,
DDIMScheduler,
)
from torch import autocast
from PIL import Image
from transformers import CLIPTextModel, CLIPTokenizer
import torch
import numpy as np
from tqdm.auto import tqdm
def image_grid(imgs, rows, cols):
assert len(imgs) == rows * cols
w, h = imgs[0].size
grid = Image.new("RGB", size=(cols * w, rows * h))
for i, img in enumerate(imgs):
grid.paste(img, box=(i % cols * w, i // cols * h))
return grid
现在,我们将向您展示从 Hugging Face 开始使用 Stable Diffusion 管道的最简单编程方式。这将加载 Stable Diffusion 模型,接收一个提示,然后显示图像。展示完这些后,我们将浅尝辄止,看看这个管道在底层是如何工作的,以及如何利用它做更多的事情。我们意识到这个管道的工作方式与潜在扩散不同,我们将展示这一点,但就我们的目的而言,它们足够相似:
# Simple
pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
).to("cuda")
n_images = 4
prompts = [
"masterpiece, best quality, a photo of a horse riding an astronaut, "
"trending on artstation, photorealistic, qhd, rtx on, 8k"
] * n_images
images = pipe(prompts, num_inference_steps=28).images
image_grid(images, rows=2, cols=2)
运行这个管道代码后,你应该会看到一组与图 C.1 类似的图像。你会注意到它生成了宇航员骑马,而不是我们请求的马骑宇航员。实际上,要得到任何 txt2img 模型执行逆操作是非常困难的,这显示了理解或未能理解语言对于多模态模型是多么重要。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/C-1.png
图 C.1 使用提示“宇航员骑马”生成的 Stable Diffusion 图像
现在我们已经看到了我们正在构建的内容,我们将继续构建一个潜在空间图像管道。我们将从加载几个模型开始:CLIP 的标记器和文本编码器,你现在应该很熟悉了,以及 Stable Diffusion 的变分自动编码器(与文本编码器类似,但用于图像)和它的 UNet 模型。我们还需要一个调度器:
# Detailed
tokenizer = CLIPTokenizer.from_pretrained("openai/clip-vit-large-patch14")
text_encoder = CLIPTextModel.from_pretrained(
"openai/clip-vit-large-patch14"
).to("cuda")
vae = AutoencoderKL.from_pretrained(
"runwayml/stable-diffusion-v1-5", subfolder="vae"
).to("cuda")
model = UNet2DConditionModel.from_pretrained(
"runwayml/stable-diffusion-v1-5", subfolder="unet"
).to("cuda")
scheduler = DDIMScheduler(
beta_start = .00085,
beta_end = .012,
beta_schedule = "scaled_linear",
clip_sample = False, set_alpha_to_one = False,
steps_offset = 1
)
接下来,我们将定义我们扩散管道的三个核心组件。首先,我们将创建get_text_embeds
函数来获取文本提示的嵌入。现在这应该已经很熟悉了:将文本分词为数字,然后将这些标记转换为嵌入。接下来,我们将创建produce_latents
函数,将这些文本嵌入转换为潜在表示。潜在表示本质上是在图像空间中的嵌入。最后,我们将创建decode_img_latents
函数,将潜在表示解码为图像。这类似于标记器将标记解码回文本的方式:
def get_text_embeds(prompt):
text_input = tokenizer( #1
prompt,
padding="max_length",
max_length=tokenizer.model_max_length,
truncation=True,
return_tensors="pt",
)
with torch.no_grad():
text_embeddings = text_encoder(text_input.input_ids.to("cuda"))[0]
uncond_input = tokenizer( #2
[""] * len(prompt),
padding="max_length",
max_length=tokenizer.model_max_length,
return_tensors="pt",
)
with torch.no_grad():
uncond_embeddings = text_encoder(uncond_input.input_ids.to("cuda"))[
0
]
text_embeddings = torch.cat([uncond_embeddings, text_embeddings]) #3
return text_embeddings
def produce_latents(
text_embeddings,
height=512,
width=512,
num_inference_steps=28,
guidance_scale=11,
latents=None,
return_all_latents=False,
):
if latents is None:
latents = torch.randn(
(
text_embeddings.shape[0] // 2,
model.in_channels,
height // 8,
width // 8,
)
)
latents = latents.to("cuda")
scheduler.set_timesteps(num_inference_steps)
latents = latents * scheduler.sigmas[0]
latent_hist = [latents]
with autocast("cuda"):
for i, t in tqdm(enumerate(scheduler.timesteps)):
latent_model_input = torch.cat([latents] * 2) #4
sigma = scheduler.sigmas[i]
latent_model_input = latent_model_input / (
(sigma**2 + 1) ** 0.5
)
with torch.no_grad(): #5
noise_pred = model(
latent_model_input,
t,
encoder_hidden_states=text_embeddings,
)["sample"]
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) #6
noise_pred = noise_pred_uncond + guidance_scale * (
noise_pred_text - noise_pred_uncond
)
latents = scheduler.step(noise_pred, t, latents)["prev_sample"] #7
latent_hist.append(latents)
if not return_all_latents:
return latents
all_latents = torch.cat(latent_hist, dim=0)
return all_latents
def decode_img_latents(latents):
latents = 1 / 0.18215 * latents
with torch.no_grad():
imgs = vae.decode(latents)["sample"]
imgs = (imgs / 2 + 0.5).clamp(0, 1)
imgs = imgs.detach().cpu().permute(0, 2, 3, 1)
imgs = (imgs) * 127.5
imgs = imgs.numpy().astype(np.uint8)
pil_images = [Image.fromarray(image) for image in imgs]
return pil_images
#1 将文本分词并获取嵌入
#2 对无条件嵌入执行相同的操作
#3 为最终嵌入创建猫(Cat)
#4 将潜在表示扩展以避免进行两次正向传递
#5 预测噪声残差
#6 执行引导
#7 计算前一个带噪声的样本 x_t -> x_t-1
现在我们已经创建了所有组件,我们可以创建管道。这将接受一个提示,将其转换为文本嵌入,将这些嵌入转换为潜在表示,然后将这些潜在表示解码为图像:
def prompt_to_img(
prompts,
height=512,
width=512,
num_inference_steps=28,
guidance_scale=11,
latents=None,
):
if isinstance(prompts, str):
prompts = [prompts]
text_embeds = get_text_embeds(prompts) #1
latents = produce_latents( #2
text_embeds,
height=height,
width=width,
latents=latents,
num_inference_steps=num_inference_steps,
guidance_scale=guidance_scale,
)
imgs = decode_img_latents(latents) #3
return imgs
imgs = prompt_to_img(
["Super cool fantasy knight, intricate armor, 8k"] * 4
)
image_grid(imgs, rows=2, cols=2)
#1 将提示转换为文本嵌入
#2 将文本嵌入转换为图像潜在表示
#3 将图像潜在表示转换为图像
最后,你应该看到一个类似于图 C.2 的图像网格。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/C-2.png
图 C.2 使用提示“幻想骑士,复杂盔甲”从自定义 Stable Diffusion 管道生成的图像。
我们希望你喜欢这个非常快速的教程,作为最后的练习,我们挑战读者找出如何使用prompt_to_img
函数来扰动现有的图像潜在表示以执行图像到图像的任务。我们承诺这将是一个挑战,以帮助你巩固理解。尽管如此,我们希望你能带走的是语言模型对扩散和当前最先进的视觉模型的重要性。
由于模态目前是语言模型中最少被探索的部分,这里的内容足以写另一本书,而且谁知道呢?也许我们以后会写。与此同时,如果你对撰写论文、申请专利或只是为推进一个真正有趣的领域做出贡献感兴趣,我们建议你直接深入研究这部分,因为任何在常规语言模型领域产生的东西都可以立即被纳入,以使扩散模型变得更好。
更多推荐
所有评论(0)