原文:towardsdatascience.com/how-to-cut-rag-costs-by-80-using-prompt-compression-877a07c6bedb

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/b700815cc6ffd03e8c238f5e921b523d.png

图片由作者提供。AI 生成。

推理过程是大大增加使用大型语言模型金钱和时间成本的因素之一。对于较长的输入,这个问题会显著增加。下面,你可以看到模型性能与推理时间之间的关系。

<…/Images/d23e1f118c00b021e62e46f25a13ad45.png>

性能分数与推理吞吐量 [1]

每秒生成更多标记的快速模型在 Open LLM 排行榜上的得分往往较低。扩大模型规模可以提升性能,但会以降低推理吞吐量为代价。这使得它们难以在实际应用中部署 [1]。

提高 LLM 的速度并减少资源需求将使它们能够被个人或小型组织更广泛地使用。

为了提高 LLM 的效率,提出了不同的解决方案;一些关注模型架构或系统。然而,像 ChatGPT 或 Claude 这样的专有模型只能通过 API 访问,因此我们无法改变它们的内部算法。

我们将讨论一种简单且经济的方法,它仅依赖于改变提供给模型的输入——提示压缩。

首先,让我们明确 LLM 是如何理解语言的。理解自然语言文本的第一步是将它分成片段。这个过程称为分词。一个标记可以是一个完整的单词、一个音节或当前口语中常用的一串字符。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/42cb90d8e79f3ffa486cf54ac593bea1.png

分词示例。图片由作者提供。

按照惯例,标记的数量比单词数量多 33%。因此,1000 个单词大约对应 1333 个标记。

让我们具体看看 OpenAI 为 gpt-3.5-turbo 模型提供的定价,因为这是我们接下来将要使用的模型。

<…/Images/ccd4980fee3ee767f69ca2f0185c8cf4.png>

OpenAI 定价 [3]

我们可以看到,推理过程对输入标记(对应于发送给模型的提示)和输出标记(模型生成的文本)都有成本。

在输入标记消耗最多资源的应用中,检索增强生成(RAG)就是一个例子。输入甚至可以达到数千个标记。在 RAG 中,用户查询被发送到一个向量数据库,在那里检索到最相似的信息,并将其与查询一起发送给 LLM。在向量数据库中,我们可以添加模型在初始训练期间未看到的个人文档。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/317f8139bb37f9f1c379b1a993d36663.png

RAG 流程图。图片由作者提供。

发送到 LLM 的标记数量可以根据从数据库中检索到的文本片段的数量而显著变化。

提示压缩

<…/Images/0b430f554de4c375ef3519fd630131c7.png>

提示压缩的示例 [1]

提示压缩缩短了原始提示,同时保留了最重要的信息。它还加快了语言模型处理输入的速度,以帮助它快速准确地生成答案。

这种技术利用了语言往往包含不必要的重复这一事实。研究表明,在段落或章节长度的文本中,英语有大约 75%的冗余 [2]。这意味着大部分单词都可以从上下文中的前一个单词预测出来。

自动压缩器

我们将要讨论的第一种压缩方法是 AutoCompressors。它通过将长文本总结成称为摘要向量的短向量表示来实现。这些压缩的摘要向量随后作为模型的软提示 [4]。

<…/Images/6a5895ae3493dd6af97580a3e746f961.png>

自动压缩器流程 [4]

在软提示期间,预训练模型保持冻结状态,并为每个特定任务在输入文本的开头添加少量可训练的标记。这些标记不是固定的,而是通过训练学习。它们在整个模型上下文中进行端到端优化,以最好地适应特定任务。

<…/Images/e7e5b45305575f253149562dc76412e5.png>

RAG 与 AutoCompressor [4]

对于 RAG,索引的文档可以预先处理以转换为摘要向量。在检索阶段,检索到的片段被融合并发送到 LLM。融合过程意味着它们的向量表示被端到端连接,形成一个单一的、更长的向量。这些向量基本上是堆叠在一起的。

为了创建这些摘要向量,你可以选择自己训练一个压缩器,或者使用一个预训练的压缩器。以下是从论文[5]的 GitHub 页面中摘取的如何使用预训练压缩器的 API 示例。

<…/Images/beac48bf858fa91fb6dceaab9ecc22a0.png>

API 与预训练 AutoCompressor 模型的示例使用 [5]

AutoCompressor-Llama-2–7b-6k 是 LLama-2–7B 模型的微调版本。它在一个 NVIDIA A100 80GB GPU 上进行了微调。训练数据包括来自 RedPajama 的 150 亿个标记,分为每个 6,144 个标记的序列。LLama-2 模型在训练期间保持冻结状态。只有摘要标记嵌入和注意力权重使用了 LoRA 进行优化。

选择性上下文

<…/Images/ee56d3bf24df3a9d5b32c508acb13327.png>

基于自信息分数的上下文过滤示例 [6]

在信息理论中,熵衡量信息片段的不确定性或不确定性。在语言模型的上下文中,它表示预测序列中下一个标记的不确定性水平。更高的熵表示更大的不可预测性。

当 LLM 以高确定性预测标记时,这些标记对模型的整体熵的贡献较小。这促使引入了一种基于从数据中移除可预测标记的提示压缩方法。

理念是,如果移除低困惑度的标记,对 LLM 对上下文的理解影响较小,因为这些标记一开始就没有添加太多新信息。高困惑度的标记被认为具有高自信息值。

为了压缩提示,像 Llama 或 GPT-2 这样的基础语言模型会给每个词汇单元分配一个自信息值(基本上是看到它的惊讶程度)。词汇单元可以是短语、句子或标记,取决于我们的选择。然后,基础模型按降序排列这些单元,并仅保留来自前 p 百分位的单元,其中 p 是我们可以设置的变量。作者选择基于百分比的而不是绝对值的方法,因为它更灵活。

让我们看看一个在不同词汇单元压缩的文本示例。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/5c29db748d43d978e232e023bbcc4d87.png

使用不同词汇单元和比例的选区上下文进行文本压缩。图由作者提供。

在这三个词汇单元之间,句子级压缩保持了原始句子的完整性。此外,较低的压缩比会更多地压缩文本。

LongLLMLingua

<…/Images/df07901e481a96c2fc9eaf4943fc37e9.png>

LongLLMLingua 框架 [7]

我们接下来要讨论的最后一种压缩方法是 LongLLMLingua。LongLLMLingua 建立在 LLMLingua 的基础上,它使用基础 LLM 如 Llama 评估提示中每个标记的困惑度,丢弃那些低困惑度的标记。这种方法基于信息熵,类似于选区上下文。

然而,LLMLingua 并非直接删除标记,而是使用预算控制器、标记级提示压缩算法和分布对齐机制。我们不会过多深入细节,但你可以通过原始论文[8]了解更多信息。

LLMLingua 的问题在于它没有在压缩过程中考虑用户问题,这可能导致保留不相关的信息。LongLLMLingua 声称通过将用户问题纳入压缩过程来改进这一不足。

他们带来的四个新组件是一个问题感知的粗粒度到细粒度压缩方法、文档重新排序机制、压缩比以及一个后压缩子序列恢复策略,以改善 LLMs 对关键信息的感知。

问题感知粗粒度压缩意味着不是单独查看每个文档,新的方法检查每个文档与问题的关系。如果一个文档使问题对模型来说看起来更可预期或“不那么令人惊讶”,那么它被视为更重要。

问题感知细粒度压缩

<…/Images/1cd23f14cd05bfc2880d21763b2cd1b6.png>

对比困惑度方程式 [7]

首先,我们测量一个词通常有多大的惊讶程度(不考虑问题)。这是 _perplexity(xi | x<i),意味着在看到所有之前的词之后看到词 x_i 的困惑度或惊讶度。然后,我们再次测量困惑度,但这次包括问题在上下文中。这是 _perplexity(xi | x^que, x<i),意味着在问题以及所有之前的词之后看到词 x_i 的惊讶度。

想法是找出问题如何改变每个词的惊讶程度。如果一个词在包含问题后变得少了很多惊讶,那么它可能非常相关于问题。

然后,我们根据第一步获得的重要性分数重新排序文档,按降序排列。这样,最重要的文档将排在最前面。

子序列恢复

<…/Images/df484131d396a99aafb10d9f3c0986f8.png>

子序列恢复的示例,红色文本表示原始文本,蓝色文本是使用 LLaMA 2–7B 分词器[7]后的结果

压缩后,可能会发生关键实体如日期或名字被改变的情况。例如,“2009”可能变成“209”,或者“Wilhelm Conrad Rontgen”可能变成“Wilhelmgen”。为了避免这个问题,我们首先识别 LLM 响应中最长的子串,该子串与压缩提示的一部分匹配。这个子串被认为是关键实体。接下来,我们找到与压缩实体对应的原始、未压缩的子序列。然后,我们将压缩实体替换为原始实体。

使用 LlamaIndex 和提示压缩的 RAG

我们将使用尼古拉斯·凯奇的维基百科页面来进行一个实际的 RAG 应用。可能,模型在训练数据中已经看到了关于这位演员的信息,所以我们指定我们期望的答案仅基于检索到的上下文。我们使用*WikipediaReader()*加载器加载维基百科页面。

from llama_index import (
    VectorStoreIndex,
    download_loader,
    load_index_from_storage,
    StorageContext,
)
WikipediaReader = download_loader("WikipediaReader")
loader = WikipediaReader()
documents = loader.load_data(pages=['Nicolas Cage'])

现在,我们正在构建一个简单的向量存储索引。对文档进行分块、嵌入和索引只需要一行代码。

检索器将被用来根据用户查询返回最相关的文档。它是通过计算查询与嵌入空间中各种文档块之间的相似性来做到这一点的。我们希望检索最相似的 2 个块。

index = VectorStoreIndex.from_documents(documents)

retriever = index.as_retriever(similarity_top_k=2)

现在我们将数据存储在索引中,我们启动用户查询。retriever.retrieve(question) 函数搜索索引以找到与查询最相似的 2 个数据块。

question = "Where did Nicolas Cage go to school?"

contexts = retriever.retrieve(question)

# Expected answer:  Beverly Hills High School

contexts 列包含带有元数据和与其他节点关系信息的 NodeWithScore 数据实体。目前,我们只对内容感兴趣。

context_list = [n.get_content() for n in contexts]
context_list

这是检索到的上下文。即使我们选择只获取前两个文档,我们仍然需要处理大量的文本。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/67ed3bb929e0b5f381f5fd417227e718.png

基于用户查询检索的文本。图片由作者提供。

我们将这些相关的块与原始查询结合起来创建提示。我们将使用提示模板而不是简单的 f-string,因为我们希望稍后重用它。

我们然后将这个提示输入到 gpt-3.5-turbo-16k 中以生成响应。

# The response from original prompt
from llama_index.llms import OpenAI
from llama_index.prompts import PromptTemplate

llm = OpenAI(model="gpt-3.5-turbo-16k")

template = (
    "We have provided context information below. n"
    "---------------------n"
    "{context_str}"
    "n---------------------n"
    "Given this information, please answer the question: {query_str}n"
)

qa_template = PromptTemplate(template)

# you can create text prompt (for completion API)
prompt = qa_template.format(context_str="nn".join(context_list), query_str=question)

response = llm.complete(prompt)
print(str(response))

输出:

尼古拉斯·凯奇就读于比佛利山高中,后来就读于加州大学洛杉矶分校戏剧、电影与电视学院。

现在,让我们使用不同的提示压缩技术来衡量 RAG 的性能。

选择性上下文

我们将使用 0.5 的 **reduceratio 来看看模型的表现。如果压缩保留了我们感兴趣的信息,我们将降低该值以压缩更多文本。

from selective_context import SelectiveContext
sc = SelectiveContext(model_type='gpt2', lang='en')
context_string = "nn".join(context_list)
context, reduced_content = sc(context_string, reduce_ratio = 0.5,reduce_level="sent")
prompt = qa_template.format(context_str="nn".join(reduced_content), query_str=question)
response = llm.complete(prompt)
print(str(response))

这是减少后的内容。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/14340ceea32c19fa8f50a19b6da13cff.png

使用选择性上下文的减少内容。图片由作者提供。

它在句子级别进行了压缩,但不幸的是,关于尼古拉斯·凯奇上过哪所学校的消息丢失了。我们还尝试了标记和短语级别的压缩,但信息仍然缺失。

输出:

提供的信息没有提到尼古拉斯·凯奇上过哪所学校。

LongLLMLingua

# Setup LLMLingua
from llama_index.query_engine import RetrieverQueryEngine
from llama_index.response_synthesizers import CompactAndRefine
from llama_index.indices.postprocessor import LongLLMLinguaPostprocessor
node_postprocessor = LongLLMLinguaPostprocessor(
    instruction_str="Given the context, please answer the final question",
    target_token=300,
    rank_method="longllmlingua",
    additional_compress_kwargs={
        "condition_compare": True,
        "condition_in_question": "after",
        "context_budget": "+100",
        "reorder_context": "sort",  # enable document reorder,
        "dynamic_context_compression_ratio": 0.3,
    },
)
retrieved_nodes = retriever.retrieve(question)
synthesizer = CompactAndRefine()

我们最关心的 postprocessnodes 函数是它根据查询缩短节点文本。

from llama_index.indices.query.schema import QueryBundle

new_retrieved_nodes = node_postprocessor.postprocess_nodes(
    retrieved_nodes, query_bundle=QueryBundle(query_str=question)
)

现在我们来看看结果。

original_contexts = "nn".join([n.get_content() for n in retrieved_nodes])
compressed_contexts = "nn".join([n.get_content() for n in new_retrieved_nodes])
original_tokens = node_postprocessor._llm_lingua.get_token_length(original_contexts)
compressed_tokens = node_postprocessor._llm_lingua.get_token_length(compressed_contexts)
print(compressed_contexts)
print()
print("Original Tokens:", original_tokens)
print("Compressed Tokens:", compressed_tokens)
print("Compressed Ratio:", f"{original_tokens/(compressed_tokens + 1e-5):.2f}x")

原始标记:2362 压缩标记:344 压缩比率:6.87x

压缩后的上下文:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/49a3c028898bd4590a91401ac02bf9bc.png

使用 LLMLingua 压缩的上下文。图片由作者提供。

让我们看看模型是否理解了压缩后的上下文。

response = synthesizer.synthesize(question, new_retrieved_nodes)
print(str(response))

输出:

尼古拉斯·凯奇就读于比佛利山高中。

从使用 longllmlingua 压缩的上下文中,我们可以清楚地看到演员上过哪所学校。我们还实现了输入标记的几乎 7 倍 减少!这相当于节省了 $0.00202。想象一下 1B 个标记的成本减少。通常,它们会花费 1000 美元,但通过提示压缩,我们只需支付大约 150 美元。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/0b2a360d0333c07540a7481776c7f8fc.png

压缩提示与原始提示的成本比较。图片由作者提供。

结论

在讨论的方法中,LongLLMLingua 似乎在 RAG 应用中的提示压缩方面最有前景。它通过压缩提示将信息量减少 6-7 倍,同时仍然保留了 LLM 生成准确响应所需的关键信息。

. . .

如果您喜欢这篇文章,请加入*文本生成** – 我们的时事通讯每周有两篇关于生成式 AI 和大型语言模型的最新见解的文章。*

您也可以在LinkedIn上找到我。

. . .

参考文献

  1. 高效大型语言模型:综述

  2. 打印英语的预测和熵

  3. openai.com/pricing

  4. 将语言模型调整为压缩上下文

  5. github.com/princeton-nlp/AutoCompressors

  6. 解锁 LLMs 的上下文约束:通过基于自信息的内容过滤增强 LLMs 的上下文效率

  7. LLMLingua:压缩提示以加速大型语言模型的推理

  8. LongLLMLingua:通过提示压缩加速和增强 LLMs 在长上下文场景中的应用

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐