嵌入比模型的最大上下文长度长的文本
OpenAI 的嵌入模型无法嵌入超过最大长度的文本。 最大长度因模型而异,并且由标记而不是字符串长度来衡量。 如果您不熟悉令牌化,请查看如何使用 tiktoken 计算令牌。
此笔记本展示了如何处理比模型的最大上下文长度更长的文本。 我们将演示使用来自 text-embedding-ada-002
的嵌入,但同样的想法可以应用于其他模型和任务。 要了解有关嵌入的更多信息,请查看 OpenAI 嵌入指南。
1.模型上下文长度
首先,我们选择模型并定义一个函数以从 API 获取嵌入。
import openai from tenacity import retry, wait_random_exponential, stop_after_attempt, retry_if_not_exception_type EMBEDDING_MODEL = 'text-embedding-ada-002' EMBEDDING_CTX_LENGTH = 8191 EMBEDDING_ENCODING = 'cl100k_base' # 让我们确保不要重试无效请求,因为这就是我们想要展示的 @retry(wait=wait_random_exponential(min=1, max=20), stop=stop_after_attempt(6), retry=retry_if_not_exception_type(openai.InvalidRequestError)) def get_embedding(text_or_tokens, model=EMBEDDING_MODEL): return openai.Embedding.create(input=text_or_tokens, model=model)["data"][0]["embedding"]
text-embedding-ada-002
模型的上下文长度为 8191 个标记,编码为 cl100k_base
,我们可以看到超过该限制会导致错误。
long_text = 'AGI ' * 5000 try: get_embedding(long_text) except openai.InvalidRequestError as e: print(e)
此模型的最大上下文长度为 8191 个标记,但您请求了 10001 个标记(提示中为 10001;0 表示完成)。 请减少您的提示; 或完成长度。
显然,我们希望避免这些错误,尤其是在以编程方式处理大量嵌入时。 然而,我们仍然可能面临比最大上下文长度更长的文本。 下面我们描述并提供处理这些较长文本的主要方法的方法:(1) 简单地将文本截断到允许的最大长度,以及 (2) 将文本分块并单独嵌入每个块。
1.截断输入文本
最简单的解决方案是将输入文本截断到允许的最大长度。 因为上下文长度是以标记来衡量的,所以我们必须在截断文本之前首先对文本进行标记化。 API 接受文本或标记形式的输入,因此只要您注意使用适当的编码,就无需将标记转换回字符串形式。 以下是此类截断函数的示例。
import tiktoken def truncate_text_tokens(text, encoding_name=EMBEDDING_ENCODING, max_tokens=EMBEDDING_CTX_LENGTH): """根据给定的编码截断字符串以具有"max_tokens"。""" encoding = tiktoken.get_encoding(encoding_name) return encoding.encode(text)[:max_tokens]
我们之前的例子现在可以正常工作了。
truncated = truncate_text_tokens(long_text) len(get_embedding(truncated))
1536
2.分块输入文本
尽管截断有效,但丢弃可能相关的文本是一个明显的缺点。 另一种方法是将输入文本分成块,然后单独嵌入每个块。 然后,我们可以单独使用块嵌入,或者以某种方式组合它们,例如平均(按每个块的大小加权)。
我们将从 Python’s own cookbook 获取一个函数,该函数将序列分解为块。
from itertools import islice def batched(iterable, n): """将数据批处理成长度为 n 的元组。 最后一批可能更短。""" # batched('ABCDEFG', 3) --> ABC DEF G if n < 1: raise ValueError('n must be at least one') it = iter(iterable) while (batch := tuple(islice(it, n))): yield batch
现在我们定义一个函数,将字符串编码为标记,然后将其分解为块。
def chunked_tokens(text, encoding_name, chunk_length): encoding = tiktoken.get_encoding(encoding_name) tokens = encoding.encode(text) chunks_iterator = batched(tokens, chunk_length) yield from chunks_iterator
最后,我们可以编写一个安全处理嵌入请求的函数,即使输入文本长于最大上下文长度,也可以通过将输入标记分块并单独嵌入每个块来实现。 可以将 average
标志设置为 True
以返回块嵌入的加权平均值,或者设置为 False
以简单地返回未修改的块嵌入列表。
import numpy as np def len_safe_get_embedding(text, model=EMBEDDING_MODEL, max_tokens=EMBEDDING_CTX_LENGTH, encoding_name=EMBEDDING_ENCODING, average=True): chunk_embeddings = [] chunk_lens = [] for chunk in chunked_tokens(text, encoding_name=encoding_name, chunk_length=max_tokens): chunk_embeddings.append(get_embedding(chunk, model=model)) chunk_lens.append(len(chunk)) if average: chunk_embeddings = np.average(chunk_embeddings, axis=0, weights=chunk_lens) chunk_embeddings = chunk_embeddings / np.linalg.norm(chunk_embeddings) # normalizes length to 1 chunk_embeddings = chunk_embeddings.tolist() return chunk_embeddings
再一次,我们现在可以处理长输入文本。
average_embedding_vector = len_safe_get_embedding(long_text, average=True) chunks_embedding_vectors = len_safe_get_embedding(long_text, average=False) print(f"Setting average=True gives us a single {len(average_embedding_vector)}-dimensional embedding vector for our long text.") print(f"Setting average=False gives us {len(chunks_embedding_vectors)} embedding vectors, one for each of the chunks.")
设置 average=True 为我们的长文本提供了一个 1536 维的嵌入向量。
设置 average=False 给我们 2 个嵌入向量,一个对应每个块。
在某些情况下,在段落边界或句子边界上拆分块可能是有意义的,以帮助保留文本的含义。
评论 (0)