前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >解密prompt系列35. 标准化Prompt进行时! DSPy论文串烧和代码示例

解密prompt系列35. 标准化Prompt进行时! DSPy论文串烧和代码示例

原创
作者头像
风雨中的小七
发布2024-08-01 07:44:44
740
发布2024-08-01 07:44:44
举报
文章被收录于专栏:小七的各种胡思乱想

一晃24年已经过了一半,我们来重新看下大模型应用中最脆弱的一环Prompt Engineering有了哪些新的解决方案。这一章我们先看看大火的DSPy框架,会先梳理DSPy相关的几篇核心论文了解下框架背后的设计思想和原理,然后以FinEval的单选题作为任务,从简单指令,COT指令,到采样Few-shot和优化指令给出代码示例和效果评估。

论文串烧

DEMONSTRATE–SEARCH–PREDICT:Composing retrieval and language models for knowledge-intensive NLP DSPy: Compiling Declarative Language Model Calls into Self-Improving Pipelines In-Context Learning for Extreme Multi-Label Classification Optimizing Instructions and Demonstrations for Multi-Stage Language Model Programs DSPy Assertions: Computational Constraints for Self-Refining Language Model Pipelines

DSPY这个prompt框架着实火了一阵了,项目也发了上面的许多论文,每篇论文都对应了项目中的一个或几个模块。我们来串一遍以上论文的核心思想。

DEMONSTRATE–SEARCH–PREDICT是DSPy的第一篇,核心思想和现在coze这类流程控制软件很相似(哈哈现在大模型领域的名词异常的多,这个workflow其实和agent,chain,pipeline,bot啥的意思也很相似)。核心在固定流程,模块化推理过程和指令(few-shot)生成过程。

虽然是在RAG任务上提出的DSP框架,但我们抛开RAG的search,predict的流程,论文的核心其实是把任务拆分成多个原子节点,每个原子节点是一个不可再分割的function,通过整体的control flow来串联原子节点。同时这种流程化的框架,使得每个节点都可以基于训练数据生成demonstation,并且可以通过不同的召回逻辑,在推理时进行few-shot的动态选择,来优化每一个节点的效果。

以下是OpenQA任务上的一个workflow的示例,整个流程有三个部分,生成Demonstration,并基于示例,进行检索和推理。

其中示例的生成,会使用训练数据在相同的流程上,取运行过程中每一步的结果作为实例,这里选择了k=3的3-shot示例

在上面DSP的基础上,DSPy: Compiling Declarative Language Model Calls into Self-Improving Pipelines对流程进一步做了抽象和重构,提出了以三个核心模块为基础的prompt生成和优化框架

  • Signature: 继承了pydantic的BaseModel,定义任务的范式,例如Context,Question -> Answer
  • Module:类似pytorch对模型定义的callable function,定义了任务的workflow,例如上面retrieve-then-read的RAG流程
  • Teleprompter: 基于任务指标对流程进行优化的编译器,这里的优化集中在few-shot的选择和动态流程选择,这里流程选择主要是Ensemble,而非直接对workflow进行修改。论文说还提供了微调能力,其实也是基于流程生成的Demonstration作为样本来微调。

其中Telepropmter提供了BootstrapFewShot, BootstrapFewShotWithRandomSearch,Ensemble等prompt优化器。这篇论文的重心是放在prompt中few-shot选择和优化

以BootstrapFewShotWithRandomSearch为例,Teleprompter会先基于训练集生成一批Demonstration,再基于指定任务的metric,从中采样得到在验证集上效果表现最优的few-shot来构建最终的prompt。

In-Context Learning for Extreme Multi-Label Classification其实是利用了DSPy的框架,在标签超大(≥10,000)的分类任务上构建了一个新的workflow,叫做Infer-Retrieve-Rank。也就是想让模型猜测N个可能的分类标签,然后召回这些标签。

Optimizing Instructions and Demonstrations for Multi-Stage Language Model Programs把重心放在了端到端的对任务prompt进行整体优化,除了few-shot之外,还包括对任务指令和任务条件的优化。这里有两个技术难点,一个是prompt空间的搜索范围极大,另一个是整体优化时指标的归因问题。

为解决prompt搜索空间问题,论文借鉴APE,也就是大模型基于input-output样本来分析任务本质,生成任务指令的思路。并且在描述任务时除了必须的训练样本,还提供了多个可选择的任务信息,包括

  • 是否使用大模型生成的2~3句话的任务描述
  • 是否使用任务的流程代码
  • 是否使用历史已经尝试过的指令
  • 是否使用不同的指令风格(tips), 例如更有创意,更简洁

模型生成指令的代码如下

代码语言:python
代码运行次数:0
复制
TIPS = {
        "none": "",
        "creative": "Don't be afraid to be creative when creating the new instruction!",
        "simple": "Keep the instruction clear and concise.",
        "description": "Make sure your instruction is very informative and descriptive.",
        "high_stakes": "The instruction should include a high stakes scenario in which the LM must solve the task!",
        "persona": 'Include a persona that is relevant to the task in the instruction (ie. "You are a ...")',
    }
class GenerateSingleModuleInstruction(dspy.Signature):
    (
        """Use the information below to learn about a task that we are trying to solve using calls to an LM, then generate a new instruction that will be used to prompt a Language Model to better solve the task."""
    )
    if use_dataset_summary:
        dataset_description = dspy.InputField(
            desc="A description of the dataset that we are using.",
            prefix="DATASET SUMMARY:",
        )
    if program_aware:
        program_code = dspy.InputField(
            format=str,
            desc="Language model program designed to solve a particular task.",
            prefix="PROGRAM CODE:",
        )
        program_description = dspy.InputField(
            desc="Summary of the task the program is designed to solve, and how it goes about solving it.",
            prefix="PROGRAM DESCRIPTION:",
        )
        module = dspy.InputField(
            desc="The module to create an instruction for.", prefix="MODULE:",
        )
    task_demos = dspy.InputField(
        format=str,
        desc="Example inputs/outputs of our module.",
        prefix="TASK DEMO(S):",
    )
    if use_instruct_history:
        previous_instructions = dspy.InputField(
            format=str,
            desc="Previous instructions we've attempted, along with their associated scores.",
            prefix="PREVIOUS INSTRUCTIONS:",
        )
    basic_instruction = dspy.InputField(
        format=str, desc="Basic instruction.", prefix="BASIC INSTRUCTION:",
    )
    if use_tip:
        tip = dspy.InputField(
            format=str,
            desc="A suggestion for how to go about generating the new instruction.",
            prefix="TIP:",
        )
    proposed_instruction = dspy.OutputField(
        desc="Propose an instruction that will be used to prompt a Language Model to perform this task.",
        prefix="PROPOSED INSTRUCTION:",
    )

基于以上大模型生成的指令候选,以及前面BootstrapFewShot生成的众多示例,下一步就是基于训练集选择最优的指令。这里论文构建了包括randomSearch等多个优化器,论文主推的MIPRO(Multi-prompt Instruction Proposal Optimizer)使用常见的超参优化的TPE算法来拟合以上指令中的多个超参对最终prompt效果的正负面影响,基于训练集上的评估指标最终选出最优的指令。所以是生成指令的超参(包括采样生成的指令)选择,并非直接去迭代更新指令本身。

其实这里也可以使用各类模型可解释算法。核心难点是整个任务中有多个节点,每个节点prompt有多个超参,彼此间互相影响,并最终影响任务的完成效果,但我们只能拿到最终任务完成的标签,无法获得中间节点的输出反馈。和决策树之类的归因算法难点相似。

DSPy: Compiling Declarative Language Model Calls into Self-Improving Pipelines是DSPy新出的一个子功能-条件判断。考虑常见的prompt构成基本就包括三个部分,任务描述,few-shot示例,还有针对任务完成细节的requirement,前面两篇论文分别给出了生成优化任务描述和采样筛选few-shot的方案,那Assert就是面向requirements的优化方案。

而现实任务中requirement往往是最琐碎的部分,例如像Query改写任务,我们可能需要要求改写query和原始query相似度不能太高,但又不能丢失核心主体,不能丢失或者改写时间实体,query不能太长,不能对query中无关细节等等

论文给出了硬性要求(Assert)和软性建议(Suggest)两种方法,直接加入到前面编写的任务module中,这样任务推理的过程会根据Assert和Suggest直接生成建议,而使用也有两种,一种是模型在推理时命中assert和suggest后,对应建议会直接加入到prompt中用于模型self-refine,另一种是assert可以直接打断模型施法进行重试。

论文就简单说这么多,下面我们以金融单选题任务为示例尝试对prompt进行优化。

代码示例

https://github.com/stanfordnlp/dspy https://dspy-docs.vercel.app/

下面我们以FinEval的单选题作为任务,尝试使用DSPy进行Prompt生成,Prompt优化,和效果评估。考虑成本这里只使用了100条样本。

基础Prompt

首先定义LLM模型,这里我使用的Azure的GPT4

代码语言:python
代码运行次数:0
复制
import dspy
model = dspy.AzureOpenAI(**kwargs)
dspy.settings.configure(lm=model)

使用DSPy定义prompt,就是定义一个Pydantic模型,包括任务描述,输入描述和输出描述,称之为Signature,如下

代码语言:python
代码运行次数:0
复制
class SingleChoiceQA(dspy.Signature):
    """单项选择题,给定题目和ABCD四个选项,输出正确的选项"""
    question = dspy.InputField(desc='问题和选项')
    answer = dspy.OutputField(desc="[ABCD]之一的正确选项")

然后我们把数据导入并转化成DSPy规定的样本格式,同样只要注明输入,Example会自动把剩余字段都当做输出

代码语言:python
代码运行次数:0
复制
def format_example(line):
    input = line['question']
    for choice in ['A', 'B', 'C', 'D']:
        input += f'\n{choice}. {line[f"{choice}"]}'
    output = line["answer"]
    example = dspy.Example(question=input, answer=output).with_inputs('question')
    return example

下一步我们来定义任务,DSPy称之为Module,也就是定义任务流,像RAG就是retrieve-read,multihop QA就是多轮的QA,而这里因为是简单的单选题,因此Module定义非常简单。

代码语言:python
代码运行次数:0
复制
class SQA(dspy.Module):
    def __init__(self):
        self.generate_answer = dspy.Predict(SingleChoiceQA)

    def forward(self, question):
        return self.generate_answer(question=question)
singlechoice_qa = SQA()
pred = singlechoice_qa(question=examples[0]['question'])
print('以下为DSPy生成Prompt')
print(model.inspect_history(n=1))

同时我们可以使用inspect_history很方便的查看,DSPy生成的具体Prompt,和对应模型的推理效果,如下

COT优化和评估

在上面基础任务定义的基础上,下一步我们看看能否简单使用COT就能优化任务效果。加入COT定义的Module如下,DSPy提供了常见的ReACT,POT等思考Prompt。

代码语言:python
代码运行次数:0
复制
class SQACOT(dspy.Module):
    def __init__(self):
        self.generate_answer = dspy.ChainOfThought(SingleChoiceQA)

    def forward(self, question):
        return self.generate_answer(question=question)
singlechoice_qa_cot = SQACOT()

加入COT后的prompt和推理效果如下

接下来我们来定义下任务的评估指标,然后批量评估下使用基础Prompt和COT Prompt的效果差异。

DSPy提供了一些Exact Match,Passage Match之类的指标,但其实自己定义指标最方便。只要和DSPy Metric的输入输出对齐即可。这里我们简单抽取答案中的ABCD和标准答案计算Accuracy。评估代码如下,DSPy支持在返回打分的同时,返回每一条预测的具体结果

代码语言:python
代码运行次数:0
复制
from dspy.evaluate.evaluate import Evaluate
def choice_match(example, pred, trace=None):
    def extract_choice(gen_ans):
        m = re.findall(r'[ABCD]', gen_ans, re.M)
        if len(m) >= 1:
            answer = m[0]
            return answer
        return random.choice('ABCD')

    return extract_choice(pred.answer) == example.answer

evaluate_on_qa = Evaluate(devset=test, num_threads=1,
                          display_progress=True, display_table=True)
output1 = evaluate_on_qa(singlechoice_qa, metric=choice_match,return_outputs=True,return_all_scores=True)

output2 = evaluate_on_qa(singlechoice_qa_cot, metric=choice_match,return_outputs=True,return_all_scores=True)

以上评估,基础Prompt的准确率在50%,而COT Prompt的准确率在60%

FewShot优化

再进一步我们使用DSPy最重点打造的FewShot采样优化,看下能否进一步提升效果。这里DSPy提供了多种优化器,适配不同的样本量级,这里考虑成本我们使用了基础的BootstrapFewShot,样本更多可以尝试BootstrapFewShotWithRandomSearch,或者BootstrapFewShotWithOptuna。

BootstrapFewShot可以指定few-shot中包含几个模型预测生成的样本(max_bootstrapped_demos),和几个真实标签的训练样本(max_labeld_demos)

代码语言:python
代码运行次数:0
复制
teleprompter = BootstrapFewShot(metric=choice_match,
                                max_bootstrapped_demos=4,
                                max_labeled_demos=16)
compiled_qa = teleprompter.compile(singlechoice_qa_cot, trainset=train)

score3, results3, output3, df3 = evaluate_on_qa(compiled_qa, metric=choice_match,
                                                return_outputs=True,
                                                return_all_scores=True)

加入采样Few-Shot后的推理准确率提升到了75% ,使用2个推理Demos,和4个真实标签的Demos拼接而成的模型Prompt如下

指令优化

再看下指令优化,这里使用的是COPRO优化器,也就是使用prompt让大模型基于原有prompt优化生成新的prompt,优化指令和第一轮优化生成的prompt如下

第二轮之后指令优化会给出之前尝试过的所有指令,和每个指令在验证集上的打分,并让模型进行有针对性的优化,称之为GenerateInstructionGivenAttempts,指令如下

在0.7的temperature下,每一轮模型会基于上一轮的最优prompt生成breadth=5个新的prompt,并重复以上过程depth=3轮,最后选取在验证集上效果最优的prompt。

代码语言:python
代码运行次数:0
复制
COPRO_teleprompter = COPRO(prompt_model=model ,
                          metric=choice_match,
                          breadth=5,
                          depth=3,
                          init_temperature=0.7,
                          track_stats=True)
kwargs = dict(num_threads=1, display_progress=True, display_table=5)
COPRO_compiled_qa = COPRO_teleprompter.compile(singlechoice_qa_cot,
                                                trainset=train,
                                                eval_kwargs=kwargs)
print(model.inspect_history(n=1))

最终优化后的最优的prompt指令如下

但在测试集上最终并没有提升准确率还是60%,其中一个主要原因也是单选QA任务本身比较基础,在指令上可以优化的空间不算太大,指令优化再更复杂,非常规任务上的效果会更显著些。

整体上DSPy确实提供了模块化,标准化设计Prompt的方案,但是在任务描述上的优化方案现在还比较有限,上面GenerateInstructionGivenAttempts虽然提供了历史尝试过的指令和验证集打分,但缺少了模型从历史prompt的回答中总结模型当前指令存在什么问题的步骤,后面可能可以考虑更多reflection相关的指令优化方案像TextGrad,这个等我玩过再来总结吧~

想看更全的大模型相关论文梳理·微调及预训练数据和框架·AIGC应用,移步Github >> DecryPrompt

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 论文串烧
  • 代码示例
    • 基础Prompt
      • COT优化和评估
        • FewShot优化
          • 指令优化
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档