AI知识库

53AI知识库

学习大模型的前沿技术与行业应用场景


DSPy(声明式自改进语言程序),面向大模型编程的新方法,克服当前LLM应用开发的诸多缺点
发布日期:2024-06-04 15:28:38 浏览次数: 3192 来源:AI工程化


Prompt(提示),实质上是一段自然语言文本,它极大降低了操纵模型的复杂度,对于AI应用普及带来了极大的推动。然而,当下以langchain、llamaindex为代表的过程式的LLM应用开发框架却也存在着一些问题。

首先,LLM 对不同的Prompt反应非常敏感,再加上应用Pipeline或者Chain围绕prompt构建,因此提示间微小的差异都会带来很大的影响(比如加个尊称),十分脆弱,开发过程中不得不小心进行调试验证。其次,由于自然语言的模式导致代码繁琐,维护困难,当然基于此也出现了prompt模板这样的缓解措施,但仍然不够优雅。再次,随着模型及提示技术发展,原来的提示需要同步迭代优化,而原来的方式想要完成这样的工作,却非常困难。因为,往往LLM应用的业务流程与Prompt本身耦合在一起,每次更改Pipeline、LLM 或数据时,所有Prompt,甚至微调流程都需要更改,牵一发而动全身。LangChain等更是引入了langsmith这样的系统来监控每一个步骤,来确保到底问题出现在哪里。

基于上面的问题,受到pytorch的声明式、模块化的设计思想启发,斯坦福研究团队提出了DSPy(https://github.com/stanfordnlp/dspy),它引入了一种声明式的方法来开发和优化 LM(语言模型)应用。它将 LLM 视为一个模块,根据它与流水线中其他组件的交互方式自动调整行为。其理念是将围绕如何设计Prompt转变为如何设计好一个系统,而无需关注细节,每当修改代码、数据、断言或指标时,都可以重新编译程序,DSPy 会生成新的有效提示来适应变更。

官方指出它的三大特点:

  • 系统优化
    从一系列优化器中进行选择,以增强程序表现。无论是生成完善的Prompt,还是微调权重,DSPy 的优化器都能最大限度地提高效率和效果。
  • 模块化方法
    使用 DSPy,可以使用预定义模块构建系统,用简单有效的解决方案取代复杂的提示技术。
  • 跨 LM 兼容性
    无论使用的是 GPT-3.5 或 GPT-4 等强大机型,还是 T5-base 或 Llama2-13b 等本地机型,DSPy 都能在系统中无缝集成并增强它们的性能。

Dspy应用的开发过程如下:

它相较于langchain等围绕Prompt构建应用chain/Pipeline的过程式开发,DSPy 引入了签名(用于抽象Prompt)、模块(用于抽象Prompt技巧)和优化器(可以调整模块的提示(或LM权重))将应用结构和流程与模型权重及Prompt调整分离。

下面是它三个重要的领域概念的详细介绍:

  • 签名(Signature)是对 DSPy 模块的输入/输出行为的声明式规范。签名使得只需要告诉语言模型它需要做什么,而不是描述语言模型如何去做。引入签名是避免直接手写Prompt,而是通过框架自动优化生成高质量Prompt。这样有助于编写模块化且干净的代码,其中对于LM 调用可以被优化成高质量的Prompt(或自动微调)。

  • 模块(Module)是使用LM的程序的Block,类似于Pytorch中的Convolution等。每个内置模块都抽象化了一种提示技术(例如COT或 ReAct)。最重要的是,它们被泛化以处理任何 DSPy 签名。DSPy 模块具有可学习的参数(即构造提示和 LM可微调的少量权重),可以被调用来处理输入并返回输出。多个模块可以组合成更大的模块(程序)。常见的Module有dspy.Predict、dspy.Retrieve、dspy.ChainOfThought、dspy.ReAct等。

  • 优化器(Optimizer,原名:Teleprompter)是一种优化算法,类似于Pytorch中的优化器,如SGD或Adam,可以调整 DSPy 程序的参数(例如提示或语言模型权重),以最大限度地提高指定的指标(Metric),例如准确性。DSPy 有很多内置的优化器,它们采用不同的策略,如LabeledFewShot、BootstrapFewShotWithRandomSearch、KNNFewShot、COPRO、BootstrapFinetune等。

    一个典型的 DSPy 优化器编译执行需要三个输入:

  1. DSPy 程序。由一个或者多个模块(例如,dspy.Predict)组成的LM应用(模块)。

  2. 指标(Metric)。这是一个评估程序输出并为其分配分数(分数越高越好)的函数,类似于定义loss function。如:

    def validate_answer(example, pred, trace=None):return example.answer.lower() == pred.answer.lower(
  3. 训练样本数据。可以很少(例如,只有 5 或 10 个例子)甚至没有标签(如果有大量数据,DSPy 也可以利用它。但推荐从小一点点迭代,进而取得更好的效果。)

from dspy.teleprompt import BootstrapFewShotWithRandomSearch
# Set up the optimizer: we want to "bootstrap" (i.e., self-generate) 8-shot examples of your program's steps.# The optimizer will repeat this 10 times (plus some initial attempts) before selecting its best attempt on the devset.config = dict(max_bootstrapped_demos=4, max_labeled_demos=4, num_candidate_programs=10, num_threads=4)
teleprompter = BootstrapFewShotWithRandomSearch(metric=YOUR_METRIC_HERE, **config)optimized_program = teleprompter.compile(YOUR_PROGRAM_HERE, trainset=YOUR_TRAINSET_HERE)

下面是一个利用DSPy实现RAG的官方例子:

1)配置LM和RM(Retrieve Model)。

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)
本例子里使用 GPT-3.5(gpt-3.5-turbo)和 ColBERTv2 检索器(一个免费服务器,托管着 2017 年维基百科“摘要”搜索索引,其中包含 2017 年转储中每篇文章的第一段)。在 DSPy 中配置 LM 和 RM,使 DSPy 在需要生成或检索时能够内部调用相应的模块。

2)加载数据集

from dspy.datasets import HotPotQA
# Load the dataset.dataset = HotPotQA(train_seed=1, train_size=20, eval_seed=2023, dev_size=50, test_size=0)
# Tell DSPy that the 'question' field is the input. Any other fields are labels and/or metadata.trainset = [x.with_inputs('question') for x in dataset.train]devset = [x.with_inputs('question') for x in dataset.dev]
len(trainset), len(devset)
本例子中使用 HotPotQA 数据集,它包含一系列复杂的问答对,通常需要多跳方式才能回答。可以通过 HotPotQA 类加载 DSPy 提供的这个数据集。

3)构造签名

因为是构建 RAG Pipeline,故定义签名为:context,question --> answer。

class GenerateAnswer(dspy.Signature):"""Answer questions with short factoid answers."""
context = dspy.InputField(desc="may contain relevant facts")question = dspy.InputField()answer = dspy.OutputField(desc="often between 1 and 5 words")

4)构建RAG Pipeline。

为了构建RAG模块,需要集成dspy.Module类并实现下面方法,
1)在init 方法中声明所需要的子模块:dspy.Retrieve 和 dspy.ChainOfThought。
2)在forward 方法将描述控制流程:给定一个问题,搜索排名前三的相关段落,然后将它们作为上下文进行答案生成。
class RAG(dspy.Module):def __init__(self, num_passages=3):super().__init__()
self.retrieve = dspy.Retrieve(k=num_passages)self.generate_answer = dspy.ChainOfThought(GenerateAnswer)
def forward(self, question):context = self.retrieve(question).passagesprediction = self.generate_answer(context=context, question=question)return dspy.Prediction(context=context, answer=prediction.answer)

5)编译优化该Pipeline。

在已经定义了这个程序,现在让我们编译它。编译程序会更新存储在每个模块中的参数。编译程序时还依赖下面三点:

  1. 一个训练集。使用上面训练集中 20 个问答示例。 

  2. 一个用于验证的指标。定义一个简单的Metric(validate_context_and_answer),它检查预测的答案是否正确,以及检索的上下文是否实际包含答案。 

  3. 一个特定的优化器。

from dspy.teleprompt import BootstrapFewShot
# Validation logic: check that the predicted answer is correct.# Also check that the retrieved context does actually contain that answer.def validate_context_and_answer(example, pred, trace=None):answer_EM = dspy.evaluate.answer_exact_match(example, pred)answer_PM = dspy.evaluate.answer_passage_match(example, pred)return answer_EM and answer_PM
# Set up a basic teleprompter, which will compile our RAG program.teleprompter = BootstrapFewShot(metric=validate_context_and_answer)
# Compile!compiled_rag = teleprompter.compile(RAG(), trainset=trainset)

编译好的程序可以保存在本地:

compiled_rag.save(YOUR_SAVE_PATH)

在使用时可按如下方式加载:

loaded_program = YOUR_PROGRAM_CLASS()loaded_program.load(path=YOUR_SAVE_PATH)

6)执行应用Pipeline

# Ask any question you like to this simple RAG program.my_question = "What castle did David Gregory inherit?"
# Get the prediction. This contains `pred.context` and `pred.answer`.pred = compiled_rag(my_question)
# Print the contexts and the answer.print(f"Question: {my_question}")print(f"Predicted Answer: {pred.answer}")print(f"Retrieved Contexts (truncated): {[c[:200] + '...' for c in pred.context]}")

可利用下面命令查看最近的Prompt。

turbo.inspect_history(n=1)

输出:

Answer questions with short factoid answers.
---
Question: At My Window was released by which American singer-songwriter?Answer: John Townes Van Zandt
Question: "Everything Has Changed" is a song from an album released under which record label ?Answer: Big Machine Records...(truncated)

可以看出,DSPy自动生成了带有COT的示例(shot)的Prompt。

7)评估Pipeline

可以用开发集(devset)来评估我们的compiled_rag程序。

from dspy.evaluate.evaluate import Evaluate
# Set up the `evaluate_on_hotpotqa` function. We'll use this many times below.evaluate_on_hotpotqa = Evaluate(devset=devset, num_threads=1, display_progress=False, display_table=5)
# Evaluate the `compiled_rag` program with the `answer_exact_match` metric.metric = dspy.evaluate.answer_exact_matchevaluate_on_hotpotqa(compiled_rag, metric=metric)

输出:

Average Metric: 22 / 50(44.0): 100%|██████████| 50/50 [00:00<00:00, 116.45it/s]Average Metric: 22 / 50(44.0%)
44.0

8)评估检索结果

同时,也可以评估检索结果,以判定检索的结果中是否包含答案。

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)
compiled_rag_retrieval_score = evaluate_on_hotpotqa(compiled_rag, metric=gold_passages_retrieved)

输出:

Average Metric: 13 / 50(26.0): 100%|██████████| 50/50 [00:00<00:00, 671.76it/s]Average Metric: 13 / 50(26.0%)
通过这个评估结果不难看出,检索质量并不高,虽然最终答案的准确率有44%,但是检索的上下文包含正确答案的仅为26%,故而可以得出大模型能够得到正确答案很大程度来自于模型记忆而非提供给其的上下文。因此,为了提升该RAG应用效果,应该在检索上下功夫。

可在colab上体验:https://colab.research.google.com/github/stanfordnlp/dspy/blob/main/intro.ipynb

小结

本文介绍新的大模型应用开发的新方式DSPy,通过介绍和案例,可以看出,开发者希望通过类似于使用Pytorch开发NN应用的方式开发大模型应用。其在设计思想及开发流程步骤上都有Pytorch应用的影子,这对于熟悉Pytorch模型应用开发的开发者来讲是容易上手的。另一方面,就如同开发NN模型程序一样,我们可以专注于流程结构,而无需过度关注模型权重及Prompt具体内容,这对于开发和后期优化来讲都是非常优雅的设计。在我们之前介绍的LLM应用成熟度(Google总监提出生成式AI应用架构和成熟度模型,一步步指导进阶)中也提到,DSPy是L6最高级的技术。随着LLM应用不断深入,它也将被越来越多的开发者所熟知。



53AI,企业落地应用大模型首选服务商

产品:大模型应用平台+智能体定制开发+落地咨询服务

承诺:先做场景POC验证,看到效果再签署服务协议。零风险落地应用大模型,已交付160+中大型企业

联系我们

售前咨询
186 6662 7370
预约演示
185 8882 0121

微信扫码

与创始人交个朋友

回到顶部

 
扫码咨询