从入门到反对 – 代替提示词工程的编程自我改进方案

直到几个月前,提示词工程才被大肆宣传。整个就业市场都充斥着提示词工程师的角色,但现在情况已不再如此。

提示词工程不是任何艺术或科学,它只是一种聪明的汉斯现象,人类为系统提供必要的背景信息,以便以更好的方式回答。

人们甚至写了书/博客,比如《前 50 个提示,以充分利用 GPT》,等等。但大规模实验清楚地表明,没有一种单一的提示或策略可以解决各种问题,只是有些提示单独看起来更好,但综合分析后就会出现问题。

所以,今天我们要讨论DSPY

官方地址:https://github.com/stanfordnlp/dspy

学习入口:https://dspy.ai/learn/

DSPy 是用于编程(而非提示)语言模型的开源框架。它允许您快速迭代构建模块化 AI 系统,并提供用于优化其提示和权重的算法,无论您是构建简单的分类器、复杂的 RAG 管道还是代理循环。

DSPy 代表声明式自我改进 Python。您无需使用脆弱的提示,而是编写组合式Python 代码并使用 DSPy 的工具来教您的 LM 提供高质量的输出

从入门到反对 - 代替提示词工程的编程自我改进方案

介绍

正如我上面提到的,互联网上充斥着各种推销书籍和博客。其中大多数只是在向你推销一堆垃圾。现在,正如我所说,其中一些可能确实有效,但这不是构建应用程序的好方法。不知道什么时候某些东西不起作用很重要,我们需要定义一个安全的假设空间,系统在其中可以工作,也可以不工作。

从入门到反对 - 代替提示词工程的编程自我改进方案

在 Google 上搜索“快速工程”书籍的前几个结果。这些书中写有文字提示,而不是使用 CoT 或 ReAct 等技术

甚至有论文表明,通过某些情感提示,大语言模型的表现会提高。

对我来说,我仍然对这种论文的真实性持保留态度。它能持续多久?对每个主题都是如此吗?是否有主题进行这种情感提示可能会导致更糟糕的结果?

有很多这样的论文,它们无意中发表了不成熟的研究。另一篇这样的论文是《自回归的余烬》,很多东西后来被证明是错误的。

理解提示问题

例如,当我说“使用 RAG 添加 5 次 CoT,使用困难的负面示例”时,从概念上讲非常清楚,但在实践中很难实现。LLM 对提示非常敏感,因此将这种结构放在提示中在大多数情况下都不起作用。

LLM 的行为对提示的编写方式非常敏感,这使得操纵它们变得非常困难。

因此,当我们构建管道时,不仅仅是我试图说服 LLM 以某种方式给出输出,而且更多的输出应该受到限制,以便它可以作为更大管道中其他模块的输入。

为了解决这个问题,已经有很多研究,但它们在很多方面都很有限。他们中的大多数人都在努力使用字符串模板,这些模板很脆弱不可扩展。语言模型会随着时间的推移而改变,提示会中断。如果我们想将模块插入不同的管道,这是行不通的。我们希望它与更新的工具、新的数据库或检索器进行交互,这是行不通的。

这正是 DSPy 旨在解决的问题,将 LLM 作为一个模块,根据它与管道中其他组件的交互方式自动调整其行为。

DSPy 范式

让我们编程 — 而不是提示 — LM

因此,DSPy 的目标是将重点从调整 LLM 转移到良好的总体系统设计。

但该怎么做呢?

为了从心理层面思考这个问题,我们可以将 LLM 视为:设备:执行指令并通过类似于 DNN 的抽象进行操作。

例如,我们在 PyTorch 中定义了一个卷积层,它可以对来自其他层的一组输入进行操作。从概念上讲,我们可以堆叠这些层并在原始输入上实现所需的抽象级别,我们不需要定义任何 CUDA 核心和许多其他指令。所有这些都已在卷积层的定义中抽象化。这就是我们希望用 LLM 做的事情,其中 LLM 是抽象模块,以不同的组合堆叠以实现某种类型的行为,无论是 CoT、ReAct 还是其他什么。

为了获得期望的行为,我们需要改变一些事情:—>代表需要演变到右侧的内容

  1. Hand written Prompts —-> SIgnatures
  2. Prompting Techniques and Prompt Chains —–>Moduels
  3. Manual Prompt ENgineering —->Optimizers

NLP 签名

这些只是我们希望 LLM 实现的行为的声明。这仅定义了需要实现什么,而不是如何实现的规范。

规范告诉 DSPy转换的作用,而不是如何提示 LLM 执行转换。

签名示例

  • 签名处理结构化格式和解析逻辑。
  • 签名可以被编译成自我改进和管道自适应的提示或微调。

DSPY 使用以下方式推断字段的作用:

  • 他们的名字,例如 DSPy 将使用上下文学习来以不同于答案的方式解释问题
  • 它们的踪迹(输入/输出示例)

注意:所有这些都不是硬编码的,而是系统在编译过程中解决的。

模块

在这里,我们使用签名来构建模块,例如,如果我们想构建一个 CoT 模块,我们会使用这些签名来构建它。这会自动生成高质量的提示,以实现某些提示技术的行为。

更技术性的定义:模块是通过抽象提示技术来表达签名的参数化层。

模块类型

声明之后,模块的行为就像一个可调用函数。

参数:为了表达特定的签名,任何 LLM 调用都需要指定:

  • 要调用的特定 LLM
  • 提示说明
  • 各签名字段的字符串前缀
  • 演示使用了少量的镜头提示和/或微调数据

优化器

为了使这个系统发挥作用,优化器基本上会采用整个管道并根据某个指标对其进行优化,并在此过程中自动提出最佳提示,甚至语言模型的权重也会在此过程中更新。

高层的想法是,我们将使用优化器来编译我们的代码,从而进行语言模型调用,以便我们管道中的每个模块都被优化为为我们自动生成的提示,或者为我们的语言模型生成一组新的经过微调的权重,以适合我们正在尝试解决的任务。

实例

单一的搜索查询通常不足以完成复杂的 QA 任务。

例如,其中的一个例子HotPotQA包括有关“Right Back At It Again”作者出生城市的问题。

搜索查询通常正确地将作者标识为“Jeremy McKinnon”,但缺乏确定作者出生时间的预期答案的能力。

在检索增强型 NLP 文献中,应对这一挑战的标准方法是构建多跳搜索系统,例如 GoldEn(Qi 等人,2019 年)和 Baleen(Khattab 等人,2021 年)。这些系统读取检索到的结果,然后生成其他查询以在必要时收集其他信息,然后得出最终答案。使用 DSPy,我们只需几行代码就可以轻松模拟此类系统。

目前,为了实现这一点,我们需要编写非常复杂的提示,并以非常混乱的方式构建它们。但不好的是,一旦我改变问题类型,我可能需要完全改变系统设计,但使用 DSPy 则不需要。

配置语言模型和检索模型

Import dspy 

turbo  = dspy.OpenAI(model = 'gpt-3.5-turbo')
colbertv2_wiki17_abstracts = dspy.ColBERTv2(url= 'http://20.102.90.50:2017/wiki17_abstracts')

dspy.settings.configure(lm=turbo,rm=colbertv2_wiki17_abstracts)

加载数据集

我们利用上述HotPotQA数据集,这是一组通常以多跳方式回答的复杂问答对。我们可以通过类加载 DSPy 提供的此数据集HotPotQA

Import dspy.datasets from HotPotQA 

# 加载数据集。
 dataset = HotPotQA(train_seed= 1 , train_size= 20 , eval_seed= 2023 , dev_size= 50 , test_size= 0 ) 

# 告诉 DSPy,“question”字段是输入。任何其他字段都是标签和/或元数据。
 trainset = [x.with_inputs( 'question' ) for x in dataset.train] 
devset = [x.with_inputs( 'question' ) for x in dataset.dev] 

len (trainset), len (devset) 

# 输出
( 20 , 50 )

建筑特色

现在我们已经加载了数据,让我们开始定义 Baleen 管道子任务的签名。

我们将首先创建以作为输入并作为输出的GenerateAnswer签名。contextquestionanswer

class  GenerateAnswer (dspy.Signature):
    """用简短的事实答案回答问题。"""

     context = dspy.InputField(desc= "可能包含相关事实" )
    question = dspy.InputField()
    answer = dspy.OutputField(desc= "通常在 1 到 5 个字之间" ) 


class  GenerateSearchQuery (dspy.Signature):
    """编写一个简单的搜索查询,帮助回答一个复杂的问题。"""

     context = dspy.InputField(desc= "可能包含相关事实" )
    question = dspy.InputField()
    query = dspy.OutputField()

构建管道

那么,让我们来定义程序本身SimplifiedBaleen。有很多可能的方法来实现这一点,但我们将把这个版本保留为关键元素。

import dsp.utils from deduplicate

class SimplifiedBaleen(dspy.Module):
    def  __init__(self,passages_per_hop= 3,max_hops= 2):
        super()。__init__()

        self.generate_query = [dspy.ChainOfThought(GenerateSearchQuery)for _ in  range(max_hops)]
        self.retrieve = dspy.Retrieve(k=passages_per_hop)
        self.generate_answer = dspy.ChainOfThought(GenerateAnswer)
        self.max_hops = max_hops 
    def  forward(self,question):
        context = [] 
        for hop in  range(self.max_hops):
            query = self.generate_query[hop](context=context,question=question)。query
            passages = self.retrieve(query)。passages
            context = deduplicate(context + passages)

        pred = self.generate_answer(context=context,question=question)
        return dspy.Prediction(context=context,answer=pred.answer)

我们可以看到,该init方法定义了几个关键的子模块:

  • generate_query:对于每一跳,我们将有一个带有签名dspy.ChainOfThought的预测器GenerateSearchQuery
  • 检索:该模块将通过模块使用生成的查询对我们定义的 ColBERT RM 搜索索引进行搜索dspy.Retrieve
  • generate_answer:该dspy.Predict模块将与签名一起使用GenerateAnswer以产生最终答案。

forward方法在简单的控制流中使用这些子模块。

  1. 首先,我们将循环至self.max_hops时间。
  2. 在每次迭代中,我们将使用 的预测器生成一个搜索查询self.generate_query[hop]
  3. 我们将使用该查询检索前 k 个段落。
  4. 我们将把(去重复的)段落添加到我们的context累加器中。
  5. 循环结束后,我们将用它self.generate_answer来产生答案。
  6. context我们将返回已检索到的和预测的预测answer

执行管道

让我们在零样本(未编译)设置下执行该程序。

这并不一定意味着性能会很差,而是意味着我们直接受到底层 LM 的可靠性限制,无法从最少的指令理解我们的子任务。通常,当在最简单和最标准的任务(例如,回答有关流行实体的简单问题)上使用最昂贵/最强大的模型(例如,GPT-4)时,这是完全没问题的。

优化管道

然而,零样本方法很快就无法满足更专业的任务、新颖的领域/设置以及更高效(或开放)的模型的要求。

为了解决这个问题,DSPy 提供了编译功能。让我们来编译我们的多跳(SimplifiedBaleen)程序。

让我们首先定义编译的验证逻辑:

  • 预测答案与黄金答案相符。
  • 检索到的上下文包含黄金答案。
  • 所生成的查询都不是杂乱无章的(即,长度均不超过 100 个字符)。
  • 生成的查询均未粗略重复(即,没有一个查询的 F1 分数在早期查询的 0.8 或更高范围内)。

我们将使用 DSPy 中最基本的提词器之一,即BootstrapFewShot使用少量示例优化管道中的预测器。

import dspy.teleprompt from BootstrapFewShot

prompt = BootstrapFewShot(metric=validate_context_and_answer_and_hops) compilation_baleen
 = teleprompter.compile (SimplifiedBaleen(),teacher=SimplifiedBaleen(passages_per_hop= 2 ),trainset=trainset)

评估管道

现在让我们定义评估函数并比较未编译和已编译的 Baleen 管道的性能。虽然此 devset 不能作为完全可靠的基准,但对于本教程来说还是很有启发的。

import dspy.evaluate.evaluate from Evaluate 

# 定义指标来检查我们是否检索到了正确的文档
def  gold_passages_retrieved ( example, pred, trace= None ):
    gold_titles = set ( map (dspy.evaluate.normalize_text, example[ "gold_titles" ]))
    found_titles = set (
        map (dspy.evaluate.normalize_text, [c.split( " | " )[ 0 ] for c in pred.context])
    )
    return gold_titles.issubset(found_titles) 

# 设置 `evaluate_on_hotpotqa` 函数。我们将在下面多次使用它。
 evaluation_on_hotpotqa = Evaluate(devset=devset, num_threads= 1 , display_progress= True , display_table= 5 ) 

uncompiled_baleen_retrieval_score = evaluation_on_hotpotqa(uncompiled_baleen, metric=gold_passages_retrieved, display= False ) 

compilation_baleen_retrieval_score = evaluation_on_hotpotqa(compiled_baleen, metric=gold_passages_retrieved) 

print ( f"## 未编译的 Baleen 的检索分数:{uncompiled_baleen_retrieval_score} " ) 
print ( f"## 已编译的 Baleen 的检索分数:{compiled_baleen_retrieval_score} " ) 


#Output 
## 未编译的 Baleen 的检索分数:36.0 
## 已编译的 Baleen 的检索分数:60.0

结论

从入门到反对 - 代替提示词工程的编程自我改进方案

这些结果表明,在 DSPy 中结合多跳设置甚至可以超越人类反馈。他们甚至表明,即使是像 T5 这样小得多的模型,在 DSPy 设置中使用时也可以与 GPT 进行比较。

DSPy 是我在 lang chain 发布后遇到的最酷的系统之一,这表明在制作一个更好、系统设计的系统方面有很大的希望,而不是在大型 LLM 管道中疯狂地放置零件。

最后介绍大家一个挺好的学习站点

RA/SD 衍生者AI训练营。发布者:chris,转载请注明出处:https://www.shxcj.com/archives/7450

Like (0)
Previous 2024-11-25 3:55 下午
Next 2024-11-26 2:24 下午

相关推荐

发表回复

Please Login to Comment
本文授权以下站点有原版访问授权 https://www.shxcj.com https://www.2img.ai https://www.2video.cn