微信扫码
与创始人交个朋友
我要投稿
近日,DSPy的研究团队联合其它几家机构的研究者,发布了一项LM程序中Prompt优化的最大规模的联合研究《优化多阶段语言模型程序的说明和演示》(arXiv:2406.11695v1),DSPy的研究团队基于prompt优化用六个解决方案解决了两个难题,提出五条优化时的教训。这对于LM程序开发者具有非常重要的指导意义,接下来,让我们一起看看研究者是如何深入剖析DSPy的核心思想和关键技术的以,文末我抛砖引玉谈了些这项技术对NLP发展的一些影响。
Prompt优化问题的定义和算法
给定一个由m个LM模块组成的程序Φ,每个模块都有一组开放变量v,如指令、示例等,需要用特定的字符串s来填充。优化的目标是,找到一组最优的变量赋值V7→S,使得在训练集D上该程序的性能指标μ最大化:
再看他们提出的优化算法,这也是一个通用的、可扩展的LM程序自动优化框架。通过不断迭代生成、评估、更新参数配置,该算法可以在给定训练数据和评估指标的情况下,持续提升LM程序在目标任务上的性能,减少手工调参的成本。
这是一个用于优化LM(Language Model)程序的算法,其输入包括:
1.优化器M(Optimizer)
2.初始的LM程序Φ(Initial Program)
3.评估指标μ(Metric)
4.最大迭代次数I(Max Iterations)
5.训练数据D(Training Data)
6.小批量数据大小B(Minibatch size)
7.Proposer超参数θ(Proposer Hyperparameters)
用中文翻译后再解释一下:
算法 1:使用优化器 M 优化 Φ
输入:
1. 优化器 M,初始程序 Φ,度量 μ
2. 最大迭代次数 I,训练数据 D
3. 小批量大小 B,提议者超参数 θ
输出: 优化后的 Φ
步骤:
1. M.Initialize(D, θ) ⟵ 使用数据初始化优化器
2. 对于 k 从 1 到 I,重复以下步骤:
1. (V ⟶ S_k) ⟵ M.Propose(θ) ⟵ 生成提案
2. D_k ⟵ {(x_j, x'_j) ∼ D}^B_{j=1} ⟵ 采样大小为 B 的批次
3. σ ⟵ 1/B ∑_{(x, x') ∈ D_k} μ(Φ_{V ⟶ S_k}(x), x') ⟵ 验证更新后的程序
4. M.Update(V ⟶ S_k, σ) ⟵ 基于观察到的验证得分更新优化器
3. (V ⟶ S) ⟵ M.ExtractOptimizedSets()
4. 返回 Φ_{V ⟶ S}
优化过程如下:
1.使用训练数据D初始化优化器M
2.进入优化迭代,最多执行I次:(1) 优化器M根据超参数θ生成一个新的参数配置建议(V 7→ Sk) (2) 从训练数据D中随机采样一个大小为B的小批量数据Dk (3) 将新的参数配置应用于LM程序Φ,在小批量数据Dk上计算该配置的性能指标σ (4) 将评估结果(V 7→ Sk,σ)反馈给优化器M,用于更新其内部状态,指导后续的参数建议
3.迭代结束后,从优化器M中提取性能最优的参数配置(V 7→ Sk)
4.将最优参数配置应用于原LM程序Φ,得到优化后的LM程序
该算法的特点包括:
1.可以优化任意的LM程序,只要其由一组参数(如prompt、示例等)定义
2.采用了小批量采样和验证的方式,在降低计算开销的同时保证了泛化性
3.可以使用不同的优化器,如贝叶斯优化、强化学习等,根据具体任务选择
4.Proposer可以利用先验知识(如训练数据特征)辅助参数生成,提高优化效率
由此,我再简要介绍一遍DSPy的三个基本模块加深你对本篇文章的理解或者也可以移步,或者在我的公众号文章中搜索DSPy
往期推荐
ICLR2024重磅 | DSPy或将手写Prompt推进历史,悄悄学会DSPy,一线技术圈很缺你这类人才
DSPy引入了三个关键概念:
图片由xiumaodalle生成
1. 自然语言签名(Natural Language Signatures)
与其规定模型应如何被Prompt,不如直接说明它应该做什么。自然语言签名就是这样一种声明性描述,它只关注输入和期望输出,而不涉及具体实现。
例如,您可以声明一个签名为"输入为文档,输出为摘要"。根据这个签名,DSPy会自动生成合适的Prompt,无需您手动构造。这种抽象化的方式大大提高了灵活性和可重用性。
2. 模块(Modules)
模块是对Prompt技术(如Chain of Thought、Program of Thought等)的抽象和封装。每个模块都关联一个自然语言签名,并内部实现了相应的Prompt流程。
您可以直接调用这些模块,而不必了解它们的具体实现细节。这为模块的复用和组合提供了基础,使构建复杂pipeline系统成为可能。
3. 优化器(Optimizers)
优化器的作用是将您的程序(由多个模块组成)自动编译为高质量的Prompt或微调后的模型权重,以最大化您所关注的指标。
在编译过程中,优化器会考虑多种因素,如Prompt指令、示例数据等,并通过离散优化寻找最优解。这确保了您的pipeline能够高效地产出所需的结果。
通过自然语言签名、模块和优化器的有机结合,DSPy实现了一种全新的"声明式"编程范式。您只需关注任务本身,而不必纠结于Prompt工程的具体细节。这不仅极大地提高了开发效率,也为复杂系统的构建和可解释性奠定了基础。
这个优化问题非常具有挑战性,因为:
1.每个字符串s的取值空间非常大
2.只有整体任务有监督信号,中间模块都是隐变量
3.无法访问LM的梯度、概率或嵌入
4.训练集D通常很小
5.LM调用的预算有限
六个方案解决两个问题
面对这些挑战,研究者们提出了两个关键问题:
1.如何高效地从海量空间中采样出优质的Prompt?
2.如何在缺乏中间监督的情况下,判断每个变量的贡献?
这两个问题可谓是Prompt领域的绝杀,相信很多读者和Prompt开发者都会有这样痛彻心扉的疑问。
一、优质Prompt的高效采样
从海量的Prompt空间中找到最优的候选项无疑是大海捞针。为了高效采样,研究者们提出了三个方案:
1.自动生成few-shot示例。借助拒绝采样策略,他们先用给定的instruction和demonstration运行LM程序,如果输出的结果质量较高,就将中间过程作为潜在的few-shot示例。这种无监督的示例生成方法可以自动获得大量优质的in-context learning数据,避免了手工设计的繁琐。作者在开篇就用了一个图来说明这个问题, 对比了一个 LM (Language Model) 程序在优化前后的变化:
图片上方是优化前的 LM 程序。它由两个部分组成:
A.给定 context 和 question,生成一个 search query
B.给定 context 和 question,直接生成 answer 该程序在训练集上(Train Set:Question/Answer Pairs)的 Exact Match 准确率只有 21%。
经过优化后,该 LM 程序的结构并没有发生变化,但我们修改了每个部分的 prompt:
A.在第一部分加入了一个few-shot 示例,告诉模型应该如何根据context 和question 生成一个简洁的search query
B.在第二部分也加入了一个few-shot 示例,告诉模型应该如何根据完整的context 正确回答question
在相同的训练集上,优化后的 LM 程序将 Exact Match 准确率提升到了 40%,接近翻倍。
这个对比生动地展示了 prompt 优化对于提升 LM 程序性能的重要意义。通过精心设计 few-shot 示例等方式,我们可以更好地发挥语言模型的能力,显著提升其在下游任务上的表现,而无需修改模型本身。这也是当前prompt engineering 的一个重要方向。
2.多维度grounding。光有任务示例还不够,还要用好上下文信息。一个好的instruction需要全面考虑任务的数据分布、推理逻辑、执行流程等多方面信息。为此,研究者们专门设计了一套zero-shot程序来总结训练集的特征、梳理程序的控制流、复盘成功的执行轨迹、回顾历史尝试过的instruction和评分。把这些信息都作为先验知识提供给LM,可以极大地引导其朝着贴合任务本质的方向生成instruction。
3.元学习Proposal过程。不同的任务对Proposal过程的超参数要求不尽相同,比如有的任务数据summary很重要,有的则需要更多样化的采样temperatue。为了自适应地调整这些超参数,研究者们将它们也作为一个优化问题,通过贝叶斯优化来学习最佳的few-shot示例选择、grounding组合、temperatue设置等决策。经过多轮迭代,Proposal过程本身也在不断进化。通过不断试错,学习一个自适应的参数生成器,提高采样效率。
二、变量贡献度的合理判断
传统的优化往往依赖每个变量的梯度信息,但LM程序的诸多限制令其无法直接获得。这是论文在多模块设置下面临的另一个挑战:如何在只有任务级别监督的情况下,判断每个模块对整体性能的贡献。面对这一难题,研究者们另辟蹊径, 论文提出了几种创新的学分分配方法:
1.贪心归因。最简单的思路是假设各模块对整体性能的贡献是独立的,于是可以单独评估每个模块的instruction变化,择优更新。这种贪心策略虽然忽略了模块间的耦合,但胜在分而治之,实现简单,但效率低下。
2.代理模型。更高明的做法是训练一个surrogate模型来拟合变量组合与程序性能之间的函数关系。通过向模型喂入历史上评估过的各种配置及其得分,就可以学到一个性能预测器。在优化过程中,新的配置首先经由该预测器评分,再选择那些有潜力的组合实际运行程序求真实性能,从而大大提高了搜索效率。
代理模型的基本思想是,用一个轻量级的机器学习模型来近似LM程序的参数-性能映射关系。具体来说,就是训练一个预测器,输入为一组参数配置,输出为该配置对应的期望任务性能。这个预测器通常选择训练和推理开销较小的模型,如神经网络、决策树等。
在优化过程中,每当产生一组新的参数配置,我们首先将其输入到代理模型中,由代理模型预测其对应的性能得分。然后,我们选择得分最高的一些配置,在实际的LM程序上进行评估,获得真实的性能反馈。接着,我们将这些新的配置及其真实性能作为额外的训练数据,去更新代理模型,提高其预测准确性。
通过这种迭代的训练-预测-评估-更新过程,代理模型可以逐步学习到LM程序的参数-性能映射关系。一个准确的代理模型可以帮助我们从众多候选配置中快速筛选出性能优异的配置,从而避免在劣质配置上浪费过多的评估预算。同时,由于代理模型的训练和预测开销远小于LM程序的实际运行开销,因此可以显著提高优化效率。
以MIPRO算法为例,其主要流程如下:
随机生成一组初始的参数配置,在LM程序上实际评估它们的性能
用这些初始配置和性能训练一个代理模型
用代理模型从候选配置中采样一组新的配置
在LM程序上评估这些新配置的真实性能
将新配置和性能加入到代理模型的训练集中,重复步骤2-4
在这个过程中,代理模型扮演了一个"参数评估器"的角色。它以较低的成本对不同参数配置的优劣进行预估,引导优化算法朝着更有希望的方向搜索。同时,由于代理模型本身是一个灵活的学习器,它可以逐步适应不同任务和数据分布,学习到参数间的相关性和约束关系。这使得基于代理模型的优化通常比随机搜索、网格搜索等传统方法更加高效和鲁棒。
3.历史归因。与surrogate模型类似,历史归因法也充分利用了过去的优化信息,但它是直接让LM从历史执行轨迹及其性能中总结规律、判断趋势。具体而言,就是在Proposal阶段将历史数据也作为prompt的一部分,请LM分析哪些instruction的变化会对哪个模块产生积极影响。LM强大的语言理解和推理能力使其有望学会对不同变量的权重动态调整,自适应地指导后续采样。
研究者还简要介绍了几种优化算法:
Bootstrap算法:
通过在训练数据上运行LM程序,自动筛选高质量的few-shot示例
使用随机搜索从示例池中采样出最优的示例组合:该算法的特点是简单高效,不需要训练额外的生成器或评估器。但其探索能力有限,容易陷入局部最优。
单模块OPRO算法:
模块粒度的历史分析,每个模块维护一个独立的prompt生成器
使用元学习不断优化单个模块的prompt生成策略:该算法更关注局部最优,将复杂的学分分配问题简化为多个单模块优化问题。这在一定程度上限制了模块间的协同优化。
程序级OPRO算法:
程序粒度的历史分析,使用一个统一的prompt生成器
将完整的历史执行轨迹作为元信息,端到端优化整个程序:该算法从全局的角度考虑模块间的交互和学分分配,但也增加了优化难度,对prompt生成器提出了更高的要求。
代理模型算法(MIPRO):
使用代理模型对参数-性能映射进行建模,用于指导采样和学分分配
在Prompt生成和学分分配之间达到平衡,同时兼顾局部优化和全局协同:该算法引入了额外的代理模型,可以显式地建模参数间的相关性,平衡探索和利用。但其性能也受制于代理模型的准确性。
通过以上种种优化策略的巧妙组合,研究者们构建了一系列强大的DSPy程序优化器,在采样效率和归因准确性上实现了重大突破。
五个教训
好的,让我们来深入剖析DSPy程序优化中的五大教训,看看它们各自蕴含着怎样的深刻洞见。
教训一:自动生成few-shot示例是性能提升的利器
传统的few-shot learning依赖人工设计的任务示例,这不仅成本高昂,而且难以覆盖任务的所有重要变体。DSPy优化器MIPRO通过rejection sampling自动生成高质量的few-shot示例,在大多数任务上取得了优于只优化instruction的效果。这表明:
从数据中自动提取的示例往往比手工设计的更能反映任务的真实分布和多样性。
高质量的示例蕴含着丰富的推理逻辑、常识知识和问题解决策略,单纯的instruction无法完全覆盖。
即使是为instruction优化的算法,添加few-shot示例也能起到事半功倍的增效作用。
因此,Few-shot prompting已成为语言模型的标配。工程师们应重视示例的自动化生成,最大限度地开发其在任务上的先验知识,为语言模型插上腾飞的翅膀。
教训二:使用MIPRO优化指令和少量示例会产生最佳的整体性能
尽管few-shot示例的重要性不言而喻,但instruction优化的意义同样不容忽视。MIPRO算法通过surrogate模型实现了指令和示例的联合优化,在多数任务上取得了最佳效果,充分说明:
高质量的指令可以弥补示例的盲区,为语言模型提供更全面、更细粒度的执行指南。
对那些规则清晰、与预训练数据接近的简单任务,示例的作用可能大于指令。但对多数需要复杂推理的任务而言,两者缺一不可。
Instruction tuning和few-shot learning在机制上是互补的,前者重在对模型施加自上而下的任务约束,后者善于自下而上地萃取数据特征,两者协同发力才能实现1+1>2的效果。
可见,即使拥有再强大的few-shot学习能力,语言模型也离不开精心设计的instruction。工程师们应在两个方向上并行发力,利用surrogate模型学习两者的最佳平衡,方能收获事半功倍之效。
教训三:指令优化对复杂任务而言至关重要
DSPy的实验还发现,instruction优化对那些规则难以从少量示例中总结,且需要复杂逻辑表达的任务尤为关键。在Iris分类和HotpotQA条件问答等任务上,zero-shot的指令优化就胜过了few-shot示例。这启示我们:
对于简单、常见的任务,语言模型可以从少量示例中归纳出任务的一般规律。但对于那些规则晦涩、变化多端的复杂任务,单靠示例是远远不够的。
精心设计的instruction可以用自然语言明确表达任务的约束条件、特殊情况、处理步骤等,减轻语言模型推理的负担,指引其朝正确方向搜索。
即便缺少示例,一个详尽的zero-shot instruction也可能胜过泛泛而谈的few-shot学习,尤其是在训练数据稀缺的情况下。
由此可见,即使few-shot示例通常更有效,我们也不能忽视instruction优化的独特价值。对于复杂、小样本的任务,工程师们更应下好instruction这盘大棋,用人类的智慧来弥补数据的先天不足。
教训四:grounding要因任务施策,切忌生搬硬套
为了提高instruction的质量,DSPy优化器利用数据分布、程序流程、成功示例等多源信息对LM进行grounding。目前DSPy有两个优化器来实现这一点:COPRO和MIPRO。文章还介绍了其它OPRO的变体,CA-OPRO,相信很快就会发布MIPROv2。MIPRO++进一步通过元学习来调整不同信息的权重,发现最佳策略因任务而异:
对ScoNe这样的逻辑推理任务,数据集总结和prompt engineering提示至关重要,因为它们能揭示语料的特殊构造,提醒注意double negation等易混淆的表达。
对HotpotQA这样的知识密集型任务,数据总结的作用有限,因为模型需要的知识大多已在预训练阶段获得。相比之下,clarification问题对缩小解空间更有帮助。
而对于Iris这样的结构化任务,执行流程的先验反而不可或缺,因为它暗示了可能的判别规则。
可见,grounding虽有助于提升指令质量,但切忌生搬硬套、不分场合。工程师们应结合任务本身的特点,甄选出最匹配的grounding信息,进而因任务施策,因地制宜。唯有对症下药,grounding才能真正发挥减少探索、助力优化的功效。
教训五:优化器的性能取决于优化预算
最后,DSPy还告诉我们,在有限的优化预算下,不同算法的表现因任务而异。MIPRO等采用mini-batch采样的方法更擅长在低预算下快速收敛,而meta-learn的优势要到高预算时才能显现。这提醒我们:
没有一种通用的优化策略可以笼统所有场景。合适的算法取决于任务难度、训练数据规模、inference成本、优化预算等多重因素。
Mini-batch采样虽然有偏,但在预算紧张时却是一种经济有效的权宜之计。尤其是对训练周期长、样本量大的任务,mini-batch可以实现快速迭代,避免过拟合。
Meta-learn算法虽然能学到更细致入微的优化策略,但前期的试错成本不可忽视。只有当优化预算较为宽裕,又需要精细调控时,才值得投入这样的学习成本。
由此可见,面对纷繁复杂的现实任务,优化器的选择绝非一蹴而就。唯有因地制宜、随机应变,方能在NLP上一路畅行。
NLP的未来不是修改Prompt,而是构建更高抽象级别的模块
DSPy的研究表明,单纯地优化Prompt已经无法满足日益复杂的NLP应用需求。原因有三:
首先,优质Prompt的设计需要大量的人力和经验,且很难覆盖任务的所有重要变体。即使采用自动优化的方法,其探索空间也随任务复杂度指数级增长。
其次,单一的Prompt缺乏模块化和可重用性。每个任务都需要从零开始优化,难以复用已有的知识和逻辑。这导致了大量重复工作,也限制了模型的组合能力。
最后,Prompt只是对语言模型的浅层利用,未能充分挖掘其理解、推理、归纳等高阶认知能力。单纯的输入输出映射无法应对需要多轮交互、逻辑推理的复杂任务。
基于以上局限,学界和业界已有初步共识:NLP的未来在于构建更高抽象级别、更加模块化的语言模型程序。这一范式有望从以下几个方面颠覆传统的Prompt工程,以下我提到的每个方面都有不止一篇研究论文作为佐证:
如果您兴趣浓厚可以移步
往期推荐
重磅 | 最新最系统的Prompt调查,谁再打着提示词大师的旗号蒙你,把这篇甩给他
1. 从浅层映射到深层建模。与Prompt的浅层IO映射不同,高阶模块可以建模任务的内在逻辑、知识依赖、执行流程等深层信息。通过将复杂任务分解为多个可复用的原子步骤,每个模块都可以封装特定的语言理解、常识推理、因果分析等认知技能。这有助于语言模型学习高阶的问题解决策略,而不仅仅是模式匹配。
2. 从静态映射到动态推理。传统的Prompt将任务限定为单轮的静态映射,忽视了许多任务固有的动态性和交互性。高阶模块则可以通过定义清晰的输入输出接口,实现灵活的数据流和控制流。不同的模块可以动态地连接、嵌套、递归,形成复杂的推理图。这使得语言模型可以进行多轮对话、迭代优化、递归计算等动态推理。
3. 从特定任务到通用技能。当前的Prompt工程往往针对特定任务量身定制,缺乏通用性和迁移力。而高阶模块则提倡将语言理解、常识推理、因果分析等通用技能封装为标准化的组件。不同的任务只需通过灵活组合、参数调优来实例化这些通用模块,而不必重复设计Prompt。这极大地提高了知识和逻辑的复用性,使得语言模型可以在更广泛的任务上展现迁移和泛化能力。
4. 从黑盒优化到白盒编程。Prompt优化往往将语言模型视为黑盒,通过端到端的梯度下降来间接调整模型行为。比如
往期推荐
教你用TextGrad在网页优化MedPrompt,一定要摒弃高级代码只能用程序运行的想法
而高阶模块范式则提倡从白盒的视角显式地编程语言模型。每个模块的输入输出行为都有明确的语义和类型定义,可以通过外部逻辑显式地控制。这种白盒性使得复杂系统的开发、调试、维护变得更加透明和可控。
5.从单一模型到异构组合。传统的NLP系统往往依赖单一的大模型,缺乏灵活性和可解释性。而高阶模块范式则提倡将不同尺度、不同类型的模型组合起来,发挥各自的特长。小模型负责具体的语言理解和信息抽取,中等模型负责语境编码和上下文交互,大模型负责高层的逻辑推理和决策生成。通过模块化组合,不同模型可以协同工作,取长补短,既降低了整体系统的训练成本,又提高了性能和可解释性。
由此可见,构建高阶模块化的语言模型程序代表了NLP未来的必由之路。它从知识表示、推理逻辑、任务分解、模型组合等多个维度,颠覆了原有的Prompt范式。如果再执着于修改Prompt,那么可以再浅显一点:可能事实过于残忍,但事实就是下面这张图上你看到的这样:手写Prompt已经被Signatures(签名)代替,各种CoT等牛X的Prompt技术被Modules封装,让你引以为傲的Prompt优化已经可以自动完成,而你却一无所知。
未来已来,变革在即。当你的同事用几行代码干了你几天才做完的事情,你或许会记得我这篇文章。
上面的Prompt调查报告后面也用DSPy和一位专家级人类提示工程师连续20小时写出的提示在识别Suicide风险信号的研究上做了比较,DSPy的性能明显超越人类20小时手写Prompt的努力。
这一变革不仅能大幅提升语言模型的性能和泛化力,也将极大地降低NLP系统的开发、维护、迭代成本。可以预见,随着编程语言、软件工程、认知科学等领域知识的引入,NLP技术将从浅层模式匹配向深层语言理解、从特定任务适配向通用认知建模、从端到端黑盒向模块化白盒全面升级。Prompt工程师们应当立足当下,放眼未来,拥抱语言模型编程这一必然趋势。惟有掌握高阶模块的设计、实现、优化、组合技艺,方能在NLP从 Narrow AI向 General AI演进的浪潮中立于不败之地。让我们携手并进,共同开创NLP的新纪元!
我相信,以上洞见对于正在开发AI产品的Prompt工程师们具有重要的启示意义。衷心希望这些思考能为大家提供一些新的视角,助力大家更好地把握NLP技术的发展脉搏,一起砥砺前行!
53AI,企业落地应用大模型首选服务商
产品:大模型应用平台+智能体定制开发+落地咨询服务
承诺:先做场景POC验证,看到效果再签署服务协议。零风险落地应用大模型,已交付160+中大型企业
2024-05-28
2024-04-26
2024-08-21
2024-04-11
2024-07-09
2024-08-13
2024-07-18
2024-10-25
2024-07-01
2024-06-17