赞
踩
论文: https://arxiv.org/abs/2310.03714
Github: https://github.com/stanfordnlp/dspy
DSPy中文文档: http://www.aidoczh.com/docs/dspy/docs/intro/
机器学习社区正在快速探索促使语言模型(LMs)和将它们堆叠成解决复杂任务的管道的技术。不幸的是,现有的 LM 管道通常使用硬编码的“提示模板”实现,即通过试错发现的冗长字符串。为了更系统地开发和优化 LM 管道,我们引入了 DSPy,这是一个将 LM 管道抽象为文本转换图的编程模型,即通过声明性模块调用 LM 的命令式计算图。DSPy 模块是参数化的,意味着它们可以学习(通过创建和收集演示)如何应用提示、微调、增强和推理技术的组合。我们设计了一个编译器,可以优化任何 DSPy 管道以最大化给定的度量标准。我们进行了两个案例研究,展示简洁的 DSPy 程序可以表达和优化复杂的 LM 管道,这些管道可以处理数学问题、处理多跳检索、回答复杂问题,并控制代理循环。在编译几分钟后,几行 DSPy 代码就可以让 GPT-3.5 和 llama2-13b-chat 自我启动管道,其性能优于标准的少样本提示(通常分别提高了 25 % 25 \% 25% 和 65 % 65 \% 65%)以及专家创建演示的管道(分别提高了 5 − 46 % 5-46 \% 5−46% 和 16 − 40 % 16-40 \% 16−40%)。此外,编译为开放和相对较小的 LM(如 770M 参数的 T5 和 llama2-13b-chat)的 DSPy 程序与依赖于专家编写的提示链的专有 GPT-3.5 方法竞争力相当。
语言模型(LMs)使研究人员能够以比以往更高的抽象级别和更低的数据需求构建 NLP 系统(Bommasani 等,2021)。这推动了“提示”技术和轻量级微调技术的爆炸性增长,用于使 LM 适应新任务(Kojima 等,2022),从中引发系统性推理(Wei 等,2022;Wang 等,2022b),并用检索到的来源(Guu 等,2020;Lazaridou 等,2022;Khattab 等,2022)或工具(Yao 等,2022;Schick 等,2023)增强它们。这些技术大多是孤立探讨的,但人们越来越感兴趣于构建多阶段管道和代理,将复杂任务分解为更易处理的 LM 调用,以提高性能(Qi 等,2019;Khattab 等,2021a;Karpas 等,2022;Dohan 等,2022;Khot 等,2022;Khattab 等,2022;Chen 等,2022;Pourreza & Rafiei,2023;Shinn 等,2023)。
不幸的是,众所周知,LM 对于如何提示每个任务都很敏感,在多个 LM 调用需要有效交互的管道中,这一点被加剧。因此,现有 LM 管道和流行的开发者框架中的 LM 调用通常使用硬编码的“提示模板”实现,即通过手工试错制作的一长串指令和演示。我们认为,尽管这种方法普遍存在,但可能是脆弱且不可扩展的——在概念上类似于手动调整分类器的权重。给定的字符串提示可能无法推广到不同的管道或不同的 LM、数据领域,甚至是输入。
为了更系统地设计 AI 管道,我们引入了 DSPy 编程模型。
1
{ }^{1}
1 DSPy 将构建新的 LM 管道从操纵自由形式字符串转向编程(组合模块化运算符以构建文本转换图),其中编译器会自动生成优化的 LM 调用策略和提示。我们从围绕神经网络抽象达成的共识中汲取灵感(Bergstra 等,2013),在这里(1)许多通用层可以模块化地组成任何复杂的架构,(2)模型权重可以使用优化器进行训练,而不是手动调整。
为此,我们提出了 DSPy 编程模型(第 3 节)。我们首先将基于字符串的提示技术进行转换,包括复杂的、任务相关的技术,如 Chain of Thought(Wei 等,2022)和 ReAct(Yao 等,2022),转换成带有自然语言类型签名的声明式模块。DSPy 模块是任务自适应组件,类似于神经网络层,抽象出任何特定的文本转换,比如回答问题或总结论文。然后,我们对每个模块进行参数化,使其能够通过在管道内迭代引导有用的演示来学习其期望行为。受 PyTorch 抽象(Paszke 等,2019)的直接启发,DSPy 模块通过表达式定义运行计算图来使用。管道通过(1)声明所需的模块和(2)在任何逻辑控制流中使用这些模块(例如 if 语句、for 循环、异常处理等)来逻辑连接这些模块来表达。
然后,我们开发了 DSPy 编译器(第 4 节),用于优化任何 DSPy 程序以提高质量或成本。编译器的输入是程序、少量训练输入以及可选标签,以及验证指标。编译器在输入上模拟程序的版本,并引导每个模块的示例跟踪以自我改进,利用它们构建有效的少样本提示或微调管道步骤的小 LM。DSPy 中的优化是高度模块化的:它由电子提示器(teleprompters)进行, 2 { }^{2} 2 这些是确定模块应如何从数据中学习的通用优化策略。通过这种方式,编译器自动将声明式模块映射到高质量的提示、微调、推理和增强组合。
像 DSPy 这样的编程模型可以从许多维度进行评估,但我们关注专家设计提示在塑造系统性能方面的作用。我们希望通过 DSPy 模块(例如 Chain of Thought 的版本)和电子提示器来减少甚至消除它们的作用。我们报告了两个广泛的案例研究:数学文字问题(GMS8K;Cobbe 等,2021)和多跳问题回答(HotPotQA;Yang 等,2018),探讨了思维链、多链反思、多跳检索、检索增强问答和代理循环。我们的评估有效地使用了许多不同的编译策略,并显示简单的 DSPy 程序胜过使用手工设计提示的系统,同时也使我们的程序能够有效地使用规模更小、因此更高效的 LM。
总的来说,这项工作提出了第一个将提示技术转换为参数化声明式模块的编程模型,并引入了具有通用优化策略(电子提示器)的有效编译器,以优化这些模块的任意管道。我们的主要贡献是经验性的和算法性的:通过 DSPy,我们发现我们可以实现非常简短的程序,可以使用小型 LM(如 llama2-13b-chat 和 T5-Large(770M 参数))引导自我改进的多阶段 NLP 系统。在没有手工设计提示的情况下,在几分钟到几十分钟的编译时间内,DSPy 模块的组合可以将简单程序的质量从 33 % 33 \% 33% 提高到 82 % 82 \% 82%(第 6 节),从 32 % 32 \% 32% 提高到 46 % 46 \% 46%(第 7 节)对于 GPT-3.5,同样,从 9 % 9 \% 9% 提高到 47 % 47 \% 47%(第 6 节),从 22 % 22 \% 22% 提高到 41 % 41 \% 41%(第 7 节)对于 llama2-13b-chat。[^0]
这项工作受到 Torch(Collobert 等,2002)、Theano(Bergstra 等,2010;2011;Al-Rfou 等,2016)、Chainer(Tokui 等,2015)等在深度学习发展中发挥作用的启发。类似的转变正在出现,高级 LM 管道的发展,我们试图为我们所称的基础模型编程提供坚实的概念框架和编程抽象。我们借鉴了可微分编程(Wang 等,2018),但应用于 LM 调用而不是神经网络,并从 PyTorch(Paszke 等,2019)借鉴了语法元素。
上下文学习(McCann 等,2018;Radford 等,2018;Brown 等,2020)是基础模型编程的关键机制。越来越多的工作表明,特别是通过指导调整(Ouyang 等,2022),我们可以通过提示(Wei 等,2022;Wang 等,2022b;Press 等,2022;Yao 等,2022;Khot 等,2022;Madaan 等,2023)引发复杂的行为。类似地,通常需要特定任务(Khattab 等,2021a;b)或手工构建(Ratner 等,2016;Hancock 等,2018)启发式的弱监督形式现在由 LM 完成(Wang 等,2022b;Zelikman 等,2022;Zhang 等,2022;Shao 等,2023)。
在上下文学习方法中,现在常常使用工具,这导致语言模型(LM)管道使用检索模型(Chen et al., 2017; Lewis et al., 2020; Guu et al., 2020; Lazaridou et al., 2022; Izacard et al., 2022)、多模态基础模型,以及诸如 API(Nakano et al., 2021)和计算器等更传统的工具。已经开发了许多工具包来促进这一点,包括 LangChain(Chase, 2022)、Semantic Kernel(Microsoft, 2023)、LlamaIndex(Liu, 2022)以及许多其他的检索和代理库。这些工具包提供了预打包的链和代理,将LM与许多可访问的工具连接起来。然而,它们受到了我们在DSPy中解决的普遍提示工程挑战的影响:它们通过手写提示模板表达特定于任务的行为(有关详细讨论,请参见附录B)。
研究人员开始应用离散优化和强化学习来寻找有效的提示,通常是针对单个逻辑LM调用(Guo et al., 2023; Pryzant et al., 2023; Huang et al., 2022; Yang et al., 2023)。DSPy旨在推广这一领域:它提供了一个丰富的框架,用于从高级声明性签名中优化任意管道,通过使用约束引导高质量的多阶段演示。在这个框架中,DSPy提示器可以应用模型选择技术,如交叉验证,或者原则上使用涉及强化学习和LM反馈(Hu et al., 2023; Zhao et al., 2023a; Shinn et al., 2023)或学习或贝叶斯超参数优化方法(Bergstra et al., 2013; Akiba et al., 2019)等复杂技术进行优化。
本文旨在将DSPy作为一种编程模型,并报告应用DSPy编译器的新的实证发现。这受到了Bergstra等人(2010; 2013)、Paszke等人(2019)和Wolf等人(2020)的启发,他们通过一系列基准数据和一些定性指标支持他们各自的编程模型。对于本文,我们专注于展示,DSPy及其编译器使我们能够构建出色的LM系统,而无需手工制作提示字符串,而是使用真正的模块化单元,这为系统地探索高度抽象的丰富设计空间打开了大门。
我们介绍了DSPy,它将LM视为文本生成的抽象设备, 3 { }^{3} 3 并优化它们在任意计算图中的使用。DSPy程序使用Python表达:每个程序都接受任务输入(例如,要回答的问题或要总结的论文)并在一系列步骤之后返回输出(例如,答案或摘要)。DSPy对自动优化做出了三种抽象贡献:签名、模块和提示器。签名抽象了模块的输入/输出行为;模块取代了现有的手动提示技术,并可以组合成任意管道;提示器优化管道中的所有模块,以最大化指标。[^1]
DSPy程序使用自然语言签名来分配工作给LM,而不是自由格式的字符串提示。DSPy签名是函数的自然语言类型声明:一个简短的声明规范,告诉DSPy文本转换需要做什么(例如,“接受问题并返回答案”),而不是如何提示特定的LM来实现该行为。更正式地说,DSPy签名是输入字段和输出字段的元组(以及可选指令)。字段由字段名和可选元数据组成。 4 { }^{4} 4 在典型的用法中,字段的角色由DSPy根据字段名的函数推断出来。例如,DSPy编译器将使用上下文学习来解释问题与答案的不同,并将迭代地优化这些字段的使用。
签名相对于提示有两个优点:它们可以被编译成自我改进和管道自适应的提示或微调。这主要是通过为每个签名引导(第4节)有用的示例来完成的。此外,它们处理结构化格式和解析逻辑,以减少(或理想情况下避免)用户程序中脆弱的字符串操作。
在实践中,DSPy签名可以用类似问题 − > -> −> 答案的简写表示,因此以下第1行是一个基本问答系统的完整DSPy程序(第2行说明了使用情况,第3行是GPT-3.5是LM时的响应):
qa = dspy.Predict("question -> answer")
2 qa(question="Where is Guaraní spoken?")
# Out: Prediction(answer='Guaraní is spoken mainly in South America.')
在简写符号中,每个字段的名称表示输入(或输出)字段在转换中扮演的语义角色。DSPy将解析这些符号,并将字段名称扩展为LM的有意义指令,以便英文文档 -> 法语翻译会提示进行英语到法语的翻译。在需要时,DSPy提供更高级的编程接口,用于表达对签名更明确约束的方式(附录A)。
类似于编程语言中的类型签名,DSPy签名简单地定义了一个接口,并为预期行为提供类似类型的提示。要使用签名,我们必须声明一个具有该签名的模块,就像我们上面实例化了一个Predict模块一样。这样的模块声明将返回一个具有该签名的函数。
预测模块 DSPy中用于处理签名的核心模块是Predict(附录D.1中的简化伪代码)。在内部,Predict存储提供的签名、要使用的可选LM(最初为None,但否则会覆盖此模块的默认LM),以及提示的演示列表(最初为空)。类似于PyTorch中的层,实例化的模块表现为可调用函数:它接受与签名输入字段相对应的关键字参数(例如,问题),格式化提示以实现签名并包含适当的演示,调用LM,并解析输出字段。当Predict检测到它正在编译模式中使用时,它还会在内部跟踪输入/输出跟踪,以帮助电视提示器在引导演示时进行引导。
其他内置模块 DSPy模块将提示技术转换为支持任何签名的模块化函数,与使用任务特定细节(例如,手写的少量示例)提示LM的标准方法形成对比。为此,DSPy包括许多更复杂的模块,如ChainOfThought、ProgramOfThought、MultiChainComparison和ReAct。[5] 这些都可以互换使用来实现DSPy签名。例如,仅需将上述程序中的Predict更改为ChainOfThought,即可导致系统在提交输出字段之前逐步思考。
重要的是,所有这些模块都是通过扩展用户定义的签名并根据需要一次或多次在新签名上调用Predict来实现的几行代码。例如,我们在下面展示了内置ChainOfThought的简化实现。
class ChainofThought(dspy.Module):
def __init__(self, signature):
\# 将'*inputs -> *outputs'的签名修改为'*inputs -> rationale, *outputs'
rationale_field = dspy.OutputField(prefix="推理:让我们逐步思考。")
signature = dspy.Signature(signature).prepend_output_field(rationale_field)
\# 使用修改后的签名声明一个子模块
self.predict $=$ dspy.Predict(signature)
def forward(self, **kwargs):
\# 只需将输入转发给子模块。
return self.predict(**kwargs)
这是一个完全成熟的模块,能够为任何LM或任务学习有效的少量提示。我们将其与附录C中的内容进行对比,后者复制了从最近的研究到流行提示库的手写长推理提示。
参数化 独特地,DSPy对这些提示技术进行了参数化。要理解这种参数化,观察到寻求实现特定签名的任何LM调用需要指定参数,包括:(1)要调用的特定LM(Chen等,2023年),(2)提示说明(Yang等,2023年)和每个签名字段的字符串前缀,最重要的是,(3)用作少量提示(对于冻结LM)或训练数据(用于微调)的演示。我们主要关注自动生成和选择有用演示。在我们的案例研究中,我们发现引导良好的演示为我们系统地教授LM的新行为提供了强大的方法。
工具 DSPy程序可以使用工具,这些工具是执行计算的模块。我们通过dspy.Retrieve模块支持检索模型。在撰写本文时,DSPy已内置支持ColBERTv2、Pyserini和Pinecone检索器,我们还尝试了用于执行SQL查询的实验性dspy.SQL和用于在沙盒中执行Python代码的dspy.PythonInterpreter。
程序 DSPy模块可以在定义运行接口中以任意管道组合。受PyTorch和Chainer直接启发,首先在初始化时声明所需的模块,允许DSPy跟踪它们以进行优化,然后通过调用forward方法的任意代码表达管道。作为一个简单的例子,我们提供以下简单但完整的检索增强生成(RAG)系统。
class RAG(dspy.Module):
def __init__(self, num_passages=3) :
# 'Retrieve' will use the user's default retrieval settings unless overriden.
self.retrieve = dspy.Retrieve(k=num_passages)
# 'ChainofThought' with signature that generates answers given retrieval & question
self.generate_answer = dspy.ChainofThought("context, question $->$ answer")
def forward(self, question):
context = self.retrieve(question).passages
return self.generate_answer(context=context, question=question)
为了突出模块化,我们使用 ChainOfThought 作为基本 Predict 的即插即用替代品。现在,我们只需简单地编写 RAG()(“瓜拉尼语是在哪里说的?”)就可以使用它。请注意,如果我们使用一个签名为“context, question -> search_query”的系统,我们将得到一个生成搜索查询而不是答案的系统。
在编译 DSPy 程序时,通常会调用电视提示器,这是一个优化器,它接受程序、训练集和度量,并返回一个新的优化程序。不同的电视提示器(第 4 节)采用不同的优化策略。
在 DSPy 中,训练集可能很小,可能只有少数示例,尽管更大的数据可以实现更强大的优化。训练示例可能是不完整的,即只需要输入值。管道步骤的标签不是必需的,除非它们需要在度量中使用。在实践中,我们通常只为程序的最终输出(最多)假设标签,而不是中间步骤。这种标签效率对于模块化至关重要:在 DSPy 中构建新管道只需要重新编译新管道的代码,而不需要注释特定于新管道的数据。
度量可以是简单的概念,比如精确匹配(EM)或 F1,但它们可以是平衡多个关注点的整个 DSPy 程序。例如,我们可以针对问题-答案对 qa_trainset 数据集编译上面的 RAG 模块,并使用度量 EM。这里的优化目标是有效地引导少量示例。以下代码实现了这一点:
# 只有问题和最终答案的小训练集。
qa_trainset = [dspy.Example(question="法国的首都是哪里?", answer="巴黎")]
# 电视提示器将引导缺失的标签:推理链和检索上下文
teleprompter = dspy.BootstrapFewShot(metric=dspy.evaluate.answer_exact_match)
compiled_rag = teleprompter.compile(RAG(), trainset=qa_trainset)
在这个例子中,BootstrapFewShot 电视提示器(第 4 节,附录 E.1)模拟了训练示例上的 RAG。它将收集每个模块的演示(即其输入-输出行为的示例),这些演示共同导致有效的输出(即尊重签名和度量)。
如果想要推动编译后的程序成为提取式,可以定义一个自定义度量来替代 dspy.evaluate.answer_exact_match:
def answer_and_context_match(example, pred, trace=None):
answer_match = dspy.evaluate.answer_exact_match(example, pred)
# 预测是否是某个段落的子串?
context_match = any((pred.answer.lower() in c) for c in pred.context)
return answer_match and context_match
请注意,这样的行为可能更准确地由另一个检查答案忠实接地的 DSPy 程序来检查。DSPy 充分支持并鼓励这样的度量。
可以通过指定教师程序来组合电视提示器。DSPy 将从该程序中抽取演示以进行提示优化。这种组合可以实现非常丰富的管道,其中昂贵的程序(例如,使用大型 LM 的复杂昂贵集成)监督廉价的程序(例如,使用较小 LM 的简单管道)。可以从上面的 compiled_rag 开始(比如,编译为使用大型 Llama2-13b-chat LM),但现在可以微调 Flan-T5-large 来创建一个高效的程序:
# 没有标签的更多问题。所有步骤的标签将被引导
unlabeled_questions = [dspy. Example(question="德国的首都是哪里?"), ...]
# 由于我们假设没有答案,我们使用 'answer_passage_match' 来过滤未接地的答案
finetuning_teleprompter = BootstrapFinetune(metric=dspy.evaluate.answer_passage_match)
# 我们设置 'teacher=compiled_rag' 来组合。现在引导将使用 'compiled_rag'
compiled_rag_via_finetune = finetuning_teleprompter.compile(RAG(), teacher=compiled_rag
trainset=unlabeled_questions, target='google/flan-t5-large')
DSPy 表达能力的一个关键来源是它能够编译 - 或自动优化 - 这个编程模型中的任何程序。编译依赖于电视提示器,这是 DSPy 程序的优化器,通过提示或微调改进模块的质量(或成本),这在 DSPy 中是统一的。虽然在创建新的电视提示器时 DSPy 不强制执行这一点,但典型的电视提示器通常经历三个阶段。
阶段 1:候选生成 编译器首先(递归地)找到程序中所有唯一的 Predict 模块(预测器),包括那些嵌套在其他模块下面的模块。对于每个唯一的预测器
p
p
p,电视提示器可以为
p
p
p 的参数生成候选值:指令、字段描述或者最重要的是演示(即示例输入-输出对)。在这个迭代中
在 DSPy 的演示中,我们专注于发现简单的拒绝抽样方法可以帮助引导高效的多阶段系统。
考虑 DSPy 中最简单的非平凡的电视提示器 BootstrapFewShot(附录 E.1 中的简化伪代码)。这个电视提示器将在一些训练输入上模拟一个教师程序(或者,如果未设置,则编译中的程序的零射击版本),可能一次或多次以较高的温度运行。在编译模式下运行时,多阶段跟踪会透明地并且线程安全地贯穿执行。程序的度量标准用于过滤出一起帮助管道通过度量标准的多阶段跟踪。因此,我们通过丢弃不良示例并使用良好示例作为潜在演示来获得程序中所有签名的潜在标签,尽管这些设计决策受用户控制。
虽然语言模型可能非常不可靠,但我们发现它们在搜索多阶段设计解空间时可能相当高效。一个良好分解的程序通常可以找到至少几个训练示例,其中语言模型可以通过签名和度量强制执行的约束。这样,如果需要,我们可以进行迭代引导。
第二阶段:参数优化 现在每个参数都有一组候选值:演示,指令等。许多超参数调整算法(例如,随机搜索或 HyperOpt 中的 Treestructured Parzen Estimators(Bergstra 等,2013)和 Optuna(Akiba 等,2019))可以用于在候选值之间进行选择。我们在附录 E.2 和附录 E.3 中报告了 DSPy 的 BootstrapFewShotWithRandomSearch 和 BootstrapFewShotWithOptuna 的简化实现。
另一种优化类型是使用 BootstrapFinetune 进行微调,其中演示用于更新每个预测器的语言模型权重。当应用此方法时,每个模块的语言模型参数将更新为新的语言模型权重。通常,我们通过在训练集或验证集上进行交叉验证来优化平均质量。这在任何阶段都适用,取决于度量的性质。
第三阶段:高阶程序优化 DSPy 编译器支持的另一种优化类型是修改程序的控制流。其中最简单形式之一是集成,我们在本文的案例研究中使用了集成。集成将引导多个副本的相同程序,然后用一个新程序替换这些程序,该程序并行运行它们所有,并将它们的预测减少为一个具有自定义函数(例如,多数投票)的预测。在未来的工作中,这个阶段可以轻松地容纳更动态的(即测试时)引导技术以及类似于自动回溯的逻辑。
编程框架可以从许多维度进行评估:计算效率,开发者效率,代码和概念的直观性等等。在本文中,我们专注于当前语言模型管道中最紧迫的问题之一:手工编写的、特定任务的提示在实现高性能系统中的作用。我们的评估旨在测试以下假设:
H1 使用 DSPy,我们可以用简洁明确定义的模块替换手工制作的提示字符串,而不降低质量或表达能力。
H2 对模块进行参数化并将提示视为优化问题使得 DSPy 更擅长适应不同的语言模型,并且可能优于专家编写的提示。
H3 结果的模块化使得更彻底地探索具有有用性能特征或符合微妙度量标准的复杂管道成为可能。
我们的评估将使用不同的任务-程序对来探索这些假设。我们希望这将开始从“不明确的问题如何比较不同的语言模型在 GSM8K 上”转变为“在使用策略 S 编译程序 P 时,它们在 GSM8K 上如何比较”,这是一个明确定义且可重现的运行。最终,我们的目标是减少现代人工智能中艺术性提示构造的作用,而是发展新的模块化、可组合的程序和优化器。
表 1:在 GSM8K 数学问题中的上下文学习结果。每行代表一个单独的管道:Program 列中的模块针对 Training 集中的示例进行编译。程序、编译器和(小)训练集在第 6 节中定义。具有集成的行建立在前一行之上。值得注意的是,表中的所有程序都是通过组合两到四个 DSPy 模块和电视提示器来表达的。编译正确的模块,而不是字符串提示,将不同的语言模型的准确率从 4-20% 提高到 49-88%。
我们在流行的 GSM8K 数据集上进行评估,该数据集包含小学数学问题(Cobbe 等,2021)。我们从官方训练集中分别采样了 200 和 300 个问题-答案对用于训练和开发。我们最终的评估使用了 1.3k 个官方测试集示例。我们在开发集上进行了广泛的比较,以避免在测试集上过拟合。与之前在 GSM8K 上的工作一样,我们评估了 LM 输出中出现的最终数值的准确性。
考虑的程序 对于这个任务,我们考虑了三个简单的 DSPy 程序:一个一步预测模块(vanilla),一个两步的思维链模块(COT),最后是一个多阶段的思维比较模块(ThoughtReflection)。它们的定义如下代码所示:
vanilla = dspy.Predict("question -> answer") # GSM8K 程序 'vanilla'
CoT = dspy.ChainOfThought("question -> answer") # GSM8K 程序 'CoT'
class ThoughtReflection(dspy.Module)
def __init__(self, num_attempts)
self.predict = dspy.ChainOfThought("question -> answer", n=num_attempts)
self.compare = dspy.MultiChainComparison('question -> answer', M= num_attempts)
def forward(self, question):
completions = self.predict(question=question).completions
return self.compare(question=question, completions=completions)
reflection = ThoughtReflection(num_attempts=5) # GSM8K 程序 'reflection'
在 reflection 中,从 LM 中随机抽取了五个推理链(以及它们的答案),并通过内置的 MultiChainComparison 模块进行并行比较,这是对 Yoran 等人(2023)的推广。这生成一个新的答案,考虑了这五次尝试中的模式。关键是,使用的模块都是通用的,没有针对特定数学问题或特定 LM 的。
编译 正如我们在第 4 节中讨论的,DSPy 程序可以编译成新的优化程序。在我们的实验中,我们评估了零-shot 程序(无编译)以及一些编译策略。我们最简单的编译器是 LabeledFewShot:
fewshot = dspy.LabeledFewShot(k=8).compile(program, trainset=trainset)
这里,program 可以是任何 DSPy 模块。这简单地从 trainset 中为训练示例和签名字段(在本例中为问题和答案,但不包括推理)随机抽取
k
=
8
\mathrm{k}=8
k=8 个演示。当应用这种随机抽样时,我们报告 3-5 次运行的平均值(取决于设置)。
接下来,我们还考虑了使用随机搜索对少量示例进行引导:
tp = BootstrapFewShotWithRandomSearch(metric=gsm8k_accuracy)
bootstrap = tp.compile(program, trainset=trainset, valset=devset)
这将为训练集中的示例生成演示链,并优化演示的选择(从该集合中)以自我改进程序的模块。顾名思义,这是通过随机搜索完成的,将演示的选择视为要优化的参数。
接下来,如果需要,这个引导过程可以在 DSPy 中嵌套。特别是,我们可以使用优化后的引导程序本身来进一步引导另一个程序。例如,每当原始的零-shot 程序表现相对较差时,这是相关的。
bootstrap2 = tp.compile(program, teacher=bootstrap, trainset=trainset, valset=devset)
最后,我们考虑对这些引导程序进行集成:
# 一个从引导编译器运行中集成前 7 个候选程序(特别是 'bootstrap' 或在适用时是 'bootstrap2'),采用多数投票。
ensemble = Ensemble(reduce_fn=dspy.majority).compile(bootstrap.programs[:7])
GSM8K 包含人类推理链。在上述代码中,trainset 不包括这些推理链。我们还使用 trainset_human_CoT 进行评估,该数据集通过人类推理字符串扩展了 trainset 中的示例。这两个数据集可以互换地作为上述 trainset 参数的值。我们在这里指出,编译通常需要几分钟(或几十分钟),因为即使在更昂贵的设置中,也只需要运行程序几千次(例如,在 150-300 个验证示例上进行 10-20 次试验),而且它们可以并行进行。
结果 我们的结果总结在表1中,其中包括开发结果以及我们对每种方法中有潜力的代表在测试集上的评估。首先,普通程序的结果显示,当它们需要直接预测答案时,GPT-3.5和llama2-13b-chat在解数学文字问题时遇到困难,即在没有使用推理链的情况下。这在缺乏良好演示的情况下最为明显,可以在无编译设置(即零短指令)和少样本设置(即随机抽取问题-答案对)中看到。然而有趣的是,通过使用引导编译以及将这个过程迭代两次(即bootstrap
×
2
\times 2
×2),普通程序得到了相当大的帮助。检查引导的提示(附录F)时,我们发现提示允许语言模型首先利用答案字段进行推理,这是允许的,因为度量指标提取最终的数值值以进行评估。
接下来,我们考虑CoT程序。虽然专家人类推理链(+human_CoT)在可用时提供了很大的提升,但我们可以通过引导来达到或超过这一水平,从而证实了我们的假设,即DSPy可以减少手工制作提示的需求。除此之外,我们发现反思程序虽然只比其他程序多了几行代码,但却是明显的赢家,尽管CoT在集成方面非常有效。总的来说,引导编译程序为每个程序带来了巨大的收益,对于两种语言模型都是如此。事实上,表中的所有程序都是通过组合两到四个DSPy模块和电子提示器来表达的,总体上表明,在DSPy规定的新范式中,组合正确的通用模块,而不是操作字符串提示,将不同的语言模型的准确率从4-20%提高到49-88%。
我们可以与以下内容进行非正式比较。Zhang等人(2022年)报告了text-davinci-002的准确率为48%,这与我们的llama2-13b-chat结果非常接近,并且在采用手动CoT方法时,codex的准确率为59.4%,采用自动CoT方法时为62.8%。Wang等人(2022b)报告了CoT提示与PaLM 540-B的准确率为57%,在添加自一致性后为74%。Llama2的作者(Touvron等人,2023年)展示了llama2-13b的准确率为28.7%,llama2-34b的准确率为42.2%,llama2-70b的准确率为56.8%。有趣的是,我们的使用该模型13b变体的程序与他们基于34b的结果竞争,尽管我们的程序中没有使用人类推理链。Zhao等人(2023b)报告了使用来自2023年4月的gpt-3.5-turbo的CoT的准确率为80.8%。GPT-4的作者(OpenAI,2023年)报告称,GPT-3.5的准确率为57.1%,而GPT-4将其提高到92%,但他们指出GPT-4实际上是在GSM8K的训练集的子集上进行了预训练。
在这个案例研究中,我们使用HotPotQA(Yang等人,2018年)数据集在开放域“fullwiki”设置中探索多跳问题回答任务。对于检索,我们使用HotPotQA的官方维基百科2017年“摘要”转储的搜索索引。搜索由ColBERTv2(Santhanam等人,2021年)检索器进行。HotPotQA的测试集是隐藏的,因此我们保留官方验证集用于测试,并从中抽取1000个示例。我们将训练集分成
70
%
/
30
%
70\%/30\%
70%/30%的训练/验证拆分。在训练集(因此在验证集)中,我们只保留原始数据集中标记为“困难”的示例,这与官方验证和测试集的指定相匹配。对于训练和报告开发结果,我们分别抽取了200个和300个示例。
考虑的程序 我们最简单的基准是在之前的GSM8K案例研究中使用的普通程序(第6节);“问题 -> 答案”的签名足够通用,当适当编译时,它将适用于这个任务(以及许多其他任务)。
我们的基准RAG程序是在第3.2节中给出的一个简单的RAG程序,其中包含一个dspy.ChainOfThought层。我们将看到这个程序在HotPotQA上表现不佳,这促使我们评估两个多跳程序。
为此,我们首先测试ReAct(Yao等人,2022年),这是一个用于工具使用的多步代理,它作为DSPy中的内置模块实现。在最简单的情况下,特定签名的ReAct模块可以在DSPy中声明如下:
react = dspy.ReAct("问题 -> 答案", tools=[dspy.Retrieve(k=1)], max_iters=5)
我们还测试了以下自定义程序,该程序模拟了Baleen(Khattab等人,2021a)和IRRR(Qi等人,2020)中的信息流,并与IRCoT(Trivedi等人,2022年)有相似之处。
class BasicMultiHop(dspy.Module):
def __init__(self, passages_per_hop):
self.retrieve = dspy.Retrieve(k=passages_per_hop)
self.generate_query = dspy.ChainOfThought("context, question search_query")
self.generate_answer = dspy.ChainofThought("context, question answer")
def forward(self, question):
context = []
for hop in range(2):
query = self.generate_query(context=context, question=question).search_query
context += self.retrieve(query).passages
return self.generate_answer(context=context, question=question)
multihop = BasicMultiHop(passages_per_hop=3)
对于编译器,我们继续使用 GSM8K 中使用的编译器(见第 6 节)。我们还考虑了我们的两个话筒的组合。对于 ReAct,我们考虑使用 BootstrapFewShotWithRandomSearch 进行引导启动,从 ReAct 程序的早期引导启动开始。对于简单的多跳程序,我们还考虑使用 T5-Large 进行微调,从该程序的早期引导启动开始。
multihop_t5 = dspy.BootstrapFinetune(metric=answer_exact_match).compile(program,
teacher=bootstrap, trainset=trainset, target='t5-large')
表 2 总结了我们的结果。与普通的少样本提示相比,链式思维和检索增强生成(COT_RAG)程序可以在 DSPy 中自我引导,显著提高答案 EM。然而,这完全依赖于 ColBERTv2 检索器直接从原始问题中找到相关段落,限制了其段落回忆。这在 react 和 multihop 程序中得到了解决,它们将在多次迭代的“跳跃”中为检索器生成查询。事实上,总体而言,一个简单的多跳程序表现最佳,而引导启动再次被证明对于提高其质量相对于其 LM 的少样本变体非常有效。
特别是,我们可以看到引导启动(和/或引导启动 × 2 \times 2 ×2)可以胜过少样本提示(对于 multihop)和专家人类推理(对于 react;从 Yao 等人 (2022) 稍微调整到我们的检索设置)。也许最重要的是,我们可以通过简单编译我们的程序使 llama2-13b-chat 与 GPT-3.5 竞争。
为了评估 DSPy 的微调能力,我们还评估了上面定义的编译器 multihop_t5,它生成了一个 T5-Large(770M 参数)模型。该程序在开发集上得分为 39.3 % 39.3 \% 39.3% 答案 EM 和 46.0 % 46.0 \% 46.0% 段落准确率,仅使用 200 个标记的输入和 800 个未标记的问题。对于编译,我们使用一个由两个 multihop 和 llama2-13b-chat 的集成(并集)组成的教师程序。考虑到其极小的大小和本地可用性,与专有 LM 如 GPT-3.5 相比,这个使用 T5-Large 的编译程序在推理方面会降低数个数量级的成本。
表 2:在 HotPotQA 多跳检索问答中的上下文学习结果。我们报告答案精确匹配(Ans)和对检索对的准确性(Psg)。每行代表一个单独的流水线:程序列中的模块针对训练集中的示例进行编译。程序、编译器和(小)训练集在正文中定义。对于 HotPotQA,我们直接使用训练集(而不是开发集)进行交叉验证。*由于成本原因,标记的结果在我们的测试集的 50 % 50 \% 50% 上进行评估。
我们的结果可以与最近一些论文中在 HotPotQA 上的评估进行比较,尽管在这个领域的研究中,评估方法和测试集样本存在显著变化。使用 CoT 提示,Si 等人 (2022) 实现了 25.2 % 25.2 \% 25.2% 的 EM。通过使用“背诵和回答”技术,该技术利用 PaLM-62B(Chowdhery 等人,2022)背诵证据段落,Sun 等人 (2022) 实现了 26.5 % 26.5 \% 26.5% 的 EM。Wang 等人 (2022a) 在应用 PaLM-540B 的自一致性时实现了 33.8 % 33.8 \% 33.8% 的 EM 和 44.6 % 44.6 \% 44.6% 的 F1。Yao 等人 (2022) 使用 PaLM-540B 实现了 27.4% 的 EM,并使用 text-davinci-002 实现了 30.8,使用维基百科 API 进行搜索。他们通过应用额外的 CoT 步骤和自一致性将他们的 PaLM 结果推到了 35.1 % 35.1 \% 35.1% 的 EM,这可能类似于我们的集成方法,即聚合多个答案。Trivedi 等人 (2022) 报告了在 500 个 HotPotQA 问题样本上使用 code-davinci-002 LM 的流水线达到了 49 % 49 \% 49%。
本文介绍了 DSPy,这是一种用于设计人工智能系统的新编程模型,使用预训练语言模型和其他工具的流水线。我们介绍了在这种抽象中引入的三个新概念(DSPy 签名、模块和提示器),并在两个非常不同的案例研究中展示了它支持使用相对较小的语言模型快速开发高效系统。我们已经维护了这个框架的开源版本接近一年。在这段时间里,我们看到并创建了大量程序,通过 DSPy 编译成高质量系统,涵盖了从信息提取到低资源合成数据生成等各种任务。出于篇幅考虑,并为了保持本文的合理范围,我们将对这些任务在受控实验条件下的报告留给未来的工作。虽然在过去 2 − 3 2-3 2−3 年的语言模型研究中,上下文学习已被证明具有变革性,但我们认为,这种新兴范式的真正表现力在于构建复杂的文本转换图,其中可组合的模块和优化器(提示器)汇集在一起,以更系统和可靠的方式利用语言模型。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。