英文原文地址:How to Cut RAG Costs by 80% Using Prompt Compression
通过即时压缩加速推理
2024 年 1 月 5 日
推理过程是使用大型语言模型时消耗资金和时间成本的因素之一,对于较长的输入,这个问题会更加凸显。下面,您可以看到模型性能与推理时间之间的关系。
性能得分与推理吞吐量[1]
小型模型每秒生成更多的tokens,往往在Open LLM排行榜上得分较低。增加模型参数大小可以提高性能,但会降低推理吞吐量。这使得在实际应用中部署它们变得困难[1]。
提高LLMs的速度并减少资源需求将使其能够更广泛地被个人或小型组织使用。
对于提高LLM的效率,目前大家提出了不同的解决方案来,有些人会关注模型架构或系统。然而,像ChatGPT或Claude这样的专有模型只能通过API访问,因此我们无法改变它们的内部算法。
本文我们将讨论一种简单且廉价的方法,该方法仅依赖于改变输入给模型的方式——即提示压缩。
首先,让我们澄清LLMs如何理解语言。理解自然语言文本的第一步是将其分割成片段。这个过程被称为tokenization(标记化)。一个token可以是一个完整的单词,一个音节,或者是当前口语中经常使用的字符序列。
分词示例。图片由作者提供。
作为一个经验法则,tokens的数量通常比单词的数量高出33%。因此,1000个单词大约对应1333个tokens。让我们具体看一下OpenAI gpt-3.5-turbo模型的定价,因为这是我们将来要使用的模型。
OpenAI定价[3]
我们可以看到,推理过程对输入tokens(对应发送给模型的提示)和输出tokens(模型生成的文本)都有成本。
输入tokens消耗最多资源的应用之一是检索增强生成。输入甚至可以达到数千个tokens。在检索增强生成中,用户查询被发送到一个向量数据库,从中检索出最相似的信息,并与查询一起发送到LLM。在向量数据库中,我们可以添加模型在初始训练中未见过的个人文档。
RAG流程图。作者提供的图片。
发送到LLM的tokens数量可能会很大,这取决于从数据库中检索到多少文本块。
提示压缩的插图[1]
Prompt压缩可以缩短原始提示,同时保留最重要的信息。它还可以加快语言模型处理输入的速度,帮助生成快速准确的答案。
这种技术利用了语言中常常包含不必要的重复。研究表明,英语在段落或章节长度的文本中有很多冗余,大约占75% [2]。这意味着大部分单词可以从它们前面的单词中预测出来。
我们将要讨论的第一种压缩方法是AutoCompressors。它通过将长文本总结为称为摘要向量的短向量表示来工作。这些压缩的摘要向量然后作为模型的软提示。
自动压缩机流量[4]
在软提示期间,预训练模型保持冻结状态,并在每个特定任务的输入文本开头添加了少量可训练的标记。这些标记不是固定的,而是通过训练学习得到的。它们在整个模型的上下文中进行端到端优化,以最适合特定任务。
RAG与自动压缩机[4]
对于RAG,索引文档可以进行预处理,以转化为摘要向量。在检索阶段,检索到的块被融合并发送到LLM。融合过程意味着它们的向量表示被连接在一起,形成一个单一、更长的向量。这些向量基本上是堆叠在一起的。
为了创建这些摘要向量,您可以选择自己训练一个压缩器,或者使用一个预训练的压缩器。下面是一个使用来自论文[5]的GitHub页面的预训练压缩器的API示例。
使用预训练的AutoCompressor模型[5]的API示例
AutoCompressor-Llama-2–7b-6k是LLama-2–7B模型的优化版本。它在一块NVIDIA A100 80GB GPU上进行了优化。训练数据包括来自RedPajama的150亿个tokens,每个序列包含6144个tokens。在训练过程中,LLama-2模型本身保持冻结状态。只有摘要tokens embedding和注意力权重使用LoRA进行了优化。
基于自信息分数[6]的上下文过滤示例
在信息论中,熵衡量了一段信息的不可预测性或不确定性。在语言模型的背景下,它代表了在预测序列中下一个token的不确定程度。熵越高,不可预测性越大。
当一个LLM以高确定性预测token时,这些token对模型的总熵贡献较小。这促使引入一种基于从数据中删除可预测token的prompt压缩方法。
这个想法是,如果移除低困惑度的token,对LLM对上下文的理解影响较小,因为这些token本身并没有提供太多新信息。高困惑度的标记被认为具有较高的自信息值。
为了压缩prompt,像Llama或GPT-2这样的基础语言模型为每个词汇单元分配一个自信息值(基本上表示对其出现感到惊讶的程度)。词汇单元可以是短语、句子或标记,具体取决于我们的选择。然后,基础模型按降序对单元进行排名,并仅保留来自第p个百分位数的单元,其中p是我们可以设置的变量。作者选择了基于百分位数的方法而不是绝对值方法,因为它更加灵活。
让我们看一个以不同词汇单位压缩的文本示例。
使用不同的词汇单位和比率进行选择性上下文的文本压缩。图片由作者提供。
在这三个词汇单元之间,句子级压缩保持原始句子完整。此外,较低的压缩比可以更大程度地压缩文本。
LongLLMLingua的框架[7]
最后一个我们要讨论的压缩方法是LongLLMLingua。LongLLMLingua基于LLMLingua,它使用类似Llama的基础LLM来评估提示中每个token的困惑度,并丢弃那些困惑度较低的token。这种方法基于信息熵,类似于选择性上下文。
然而,LLMLingua并非直接删除tokens,而是使用了一个预算控制器、一个token级别的prompt压缩算法和一个分布对齐机制。我们不会详细介绍,但你可以在原始论文[8]中阅读更多相关内容。
LLMLingua的问题在于在压缩过程中没有考虑用户的问题,可能会保留与问题无关的信息。LongLLMLingua声称通过将用户的问题纳入压缩过程来改善这个缺点。
他们带来的四个新组件是一个问题感知的粗细压缩方法,一个文档重新排序机制,压缩比率,以及一个压缩后子序列恢复策略,以提高LLMs对关键信息的感知。
Question-Aware Coarse-Grained Compression 问题感知的粗粒度压缩
意味着不再单独查看每个文档,而是检查每个文档与问题的关联性。如果一个文档使问题对模型来说更加预期或“不那么令人惊讶”,那么它被视为更重要。
Question-Aware Fine-Grained Compression 问题感知的细粒度压缩
对比困惑度方程式[7]
首先,我们测量一个词通常的惊讶程度(不考虑问题)。这就是*perplexity(x_i | x,它表示在看到单词x_i之前的所有单词时感到困惑或惊讶。然后,我们再次测量困惑,但这一次包括上下文中的问题。这就是perplexity(x_i | x^que, x
这个想法是找出每个词对问题的惊讶程度产生了多大的改变。如果一个词在包含问题时变得不那么令人惊讶,那么它可能与问题非常相关。
然后,我们根据第一步得到的重要性分数,按降序重新排列文档。这样,最重要的文档会排在前面。
Subsequence recovery 子序列恢复
子序列恢复的示例,红色文本表示原始文本,蓝色文本是使用LLaMA 2-7B分词器[7]后的结果
压缩后,关键实体如日期或姓名可能会发生变化。例如,“2009”可能变成“209”,或者“威廉·康拉德·伦琴”可能变成“威廉根”。为了避免这个问题,我们首先识别LLM响应中与压缩提示的一部分匹配的最长子字符串。这个子字符串被视为关键实体。接下来,我们找到对应于压缩实体的原始、未压缩的子序列。然后,我们用原始实体替换压缩实体。
我们将使用尼古拉斯·凯奇的维基百科页面进行实际的RAG应用。可能,模型已经在其训练数据中看到了关于这位演员的信息,所以我们正在指定我们只期望基于检索到的上下文给出答案。我们正在使用WikipediaReader()加载器加载维基百科页面。
1 2 3 4 5 6 7 8 9 |
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']) |
现在,我们正在构建一个简单的向量存储索引。只需一行代码即可对文档进行分块、嵌入和索引。
检索器将用于返回与用户查询最相关的文档。它通过计算查询与嵌入空间中的各个文档块之间的相似度来实现。我们希望检索出两个最相似的文档块。
1 2 3 |
index = VectorStoreIndex.from_documents(documents) retriever = index.as_retriever(similarity_top_k=2) |
现在我们的数据存储在索引中,我们发起一个用户查询。retriever.retrieve(question)函数在索引中搜索与查询最相似的两个数据块。
1 2 3 4 5 |
question = "Where did Nicolas Cage go to school?" contexts = retriever.retrieve(question) # Expected answer: Beverly Hills High School |
上下文列表包含具有元数据和与其他节点的关系信息的NodeWithScore数据实体。目前,我们只对内容感兴趣。
1 2 |
context_list = [n.get_content() for n in contexts] context_list |
这是检索到的上下文。即使我们选择只获取前两个文档,我们仍然需要处理大量的文本。
根据用户查询检索到的文本。作者提供的图片。
我们将这些相关的片段与原始查询结合起来创建提示。我们将使用提示模板而不仅仅是f-string,因为我们希望在后续中能够重复使用它。
然后我们将这个提示输入到gpt-3.5-turbo-16k中生成一个回应。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# 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="\n\n".join(context_list), query_str=question) response = llm.complete(prompt) print(str(response)) |
输出:
尼古拉斯·凯奇曾就读于贝弗利山高中,后来进入了加州大学洛杉矶分校戏剧、电影和电视学院。
现在,让我们在使用不同的提示压缩技术后测量RAG性能。
我们将使用0.5的reduce_ratio,并观察模型的表现。如果压缩能够保留我们感兴趣的信息,我们将降低该值以压缩更多的文本。
1 2 3 4 5 6 7 |
from selective_context import SelectiveContext sc = SelectiveContext(model_type='gpt2', lang='en') context_string = "\n\n".join(context_list) context, reduced_content = sc(context_string, reduce_ratio = 0.5,reduce_level="sent") prompt = qa_template.format(context_str="\n\n".join(reduced_content), query_str=question) response = llm.complete(prompt) print(str(response)) |
这是简化后的内容。
使用选择性上下文减少内容。作者提供的图片。
很遗憾,尽管在句子层面上进行了压缩,但关于尼古拉斯·凯奇上学的信息丢失了。我们还尝试了标记和短语级别的压缩,但信息仍然缺失。
输出:
提供的信息没有提到尼古拉斯·凯奇上过哪所学校。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# 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() |
postprocess_nodes函数是我们最关心的函数,因为它可以根据查询缩短节点文本。
1 2 3 4 5 |
from llama_index.indices.query.schema import QueryBundle new_retrieved_nodes = node_postprocessor.postprocess_nodes( retrieved_nodes, query_bundle=QueryBundle(query_str=question) ) |
现在让我们看看结果。
1 2 3 4 5 6 7 8 9 |
original_contexts = "\n\n".join([n.get_content() for n in retrieved_nodes]) compressed_contexts = "\n\n".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") |
Original Tokens: 2362
Compressed Tokens: 344
Compressed Ratio: 6.87x
压缩的上下文:
使用LLMLingua进行压缩上下文。作者提供的图片。
让我们看看模型是否理解了压缩的上下文。
1 2 |
response = synthesizer.synthesize(question, new_retrieved_nodes) print(str(response)) |
输出:
尼古拉斯·凯奇曾就读于贝弗利山高中。
从使用longllmlingua压缩的上下文中,可以清楚地看出演员上学的地方。我们还几乎减少了7倍的输入标记!这意味着节省了0.00202美元。想象一下对于10亿个标记的成本减少。通常情况下,它们将花费1000美元,但通过提示压缩,我们只需支付约150美元。
压缩和原始提示的成本比较。图片由作者提供。
在讨论的方法中,LongLLMLingua似乎是在RAG应用中进行快速压缩最有前景的方法。它将提示压缩了6-7倍,同时仍保留了生成准确回答所需的关键信息。
Efficient Large Language Models: A Survey
Prediction and entropy of printed English
Pricing
Adapting Language Models to Compress Contexts
https://github.com/princeton-nlp/AutoCompressors
Unlocking Context Constraints of LLMs: Enhancing Context Efficiency of LLMs with Self-Information-Based Content Filtering
LLMLingua: Compressing Prompts for Accelerated Inference of Large Language Models
LongLLMLingua: ACCELERATING AND ENHANCING LLMS IN LONG CONTEXT SCENARIOS VIA PROMPT COMPRESSION