全文共9000余字,预计阅读时间约1~2小时 | 满满干货(附论文复现代码),建议收藏!
本文目标:复现论文:《LEAST-TO-MOST PROMPTING ENABLES COMPLEX REASONING IN LARGE LANGUAGE MODELS》, 并提出改进策略,采用LtM提示方法在text-davinci-003模型上完成对SCAN数据集准确率高达99.7%的预测,实现一个基于大模型的完整建模流程。
一、介绍
对于熟悉机器学习和深度学习的人来说,一个模型的组合泛化能力是衡量其优劣的关键指标。大语言模型(LLM)不例外,它也强调组合泛化作为其特定领域涌现能力的基准。然而,与传统模型相比,大语言模型(LLM)能够更深入地解读输入信息的复杂语义,并做出准确的推理,这是它所固有的底层能力,一个大模型越优秀,它的这种底层能力就越强,提示工程作为一种激发大模型潜在能力的技术,其效果的最佳验证方法是看它如何增强模型的组合泛化表现。
在OpenAI开发系列(八):基于思维链(CoT)的进阶提示工程这篇文章中,通过一系列的实践和结合相关论文的结论,已经印证了:EAST-TO-MOST PROMPTING(LtM提示法)是截至目前围绕模型推理能力提升的最为有效的提示学习方法。
本文对《LEAST-TO-MOST PROMPTING ENABLES COMPLEX REASONING IN LARGE LANGUAGE MODELS》进行论文复现。采用SCAN数据集,并选用text-davinci-003作为模型,同时结合LtM提示方法。在此基础上,融入一些改进策略,构建一个完整的基于大模型的建模流程。通过这种方法,成功地在text-davinci-003模型上对SCAN数据集实现了高达99.7%的预测准确率,希望通过这种方式给大家提供一种使用大模型建模的落地思路。
下面开始复现过程。
二、数据背景
先了解一下数据。SCAN数据集,全称是Sequential Compositionality Abstraction and Generalization,译为:顺序组合性、抽象化和泛化,它被设计的初衷是为了评估神经网络模型在组合泛化(compositional generalization)方面的能力。
组合泛化是指利用既有的知识去理解或生成从未见过的组合。
这份数据集是由纽约大学和Facebook于2018年提出,包含2万条基于隐藏语义关系的指令及对应的翻译动作序列,简单理解就是将简单的指令翻译为一系列动作,例如指令 “walk twice” 应该被翻译为 “WALK WALK”。它之所以能被用于评估模型的组合泛化能力,是因为这个数据集包含了各种不同结构的句子,其中有一些句子结构在训练集中是未出现的,因此模型需要具备组合泛化的能力,才能在这些未见过的结构上取得好的表现。
很多传统的深度学习模型在某些测试任务上表现并不理想,尤其是那些要求模型展现出组合泛化能力的任务,很多研究者们为了这个数据集提出了许多新方法和模型架构,试图提高在SCAN数据集上的表现,所以可见围绕SCAN数据集建模,难度是非常大的。
2.1 数据形式
SCAN数据集由一系列指令(commands
)和相应的动作序列(actions
)组成。这些指令通常由一系列动词和修饰词构成,代表了一连串的动作。如:
对于第一条数据:**Command:“look thrice after jump” ,Action Sequence:“JUMP LOOK LOOK LOOK”**来说:
这个指令可以分解为两部分来理解:
- “jump”: 这是一个单一的动作指令,表示执行跳跃动作。动作序列中的“JUMP”正是对这个动作的响应。
- “look thrice:” 这表示执行"look"(看)这个动作三次。动作序列中的三个连续的“LOOK”是对这部分指令的响应。
关键词 “after” 确定了动作的顺序。这意味着首先要执行"jump",然后再执行三次"look"。所以,当指令是“look thrice after jump”,动作序列是按照指令中给出的顺序和次数,首先跳跃,然后看三次。
所以如果要针对这份数据来进行建模的话,可以把Command理解为特征,Action Sequence理解为标签。
2.2 数据难度
这个数据集存在一定的“黑箱”性质,比如“look thrice after jump”可以理解指代[”JUMP", "LOOK ", "LOOK ", "LOOK "],但有些命令和行为之间的关系却不是很好理解,就像“run opposite left after walk opposite right”,给出的行为是[”I_TURN_RIGHT”, ”I_TURN_RIGHT”, ”I_WALK”, ”I_TURN_LEFT”, ”I_TURN_LEFT”, ”I_RUN”],直接懵了,这并不能很好的通过自然语言的语法规则进行理解。
根据官方的解释是:这套数据集是根据一套隐藏的语法规则创建的指令和行为数据集,指令和动作在现实中并没有特殊的含义,但它们模拟了一种常见的问题:理解和执行一系列指令。所以针对这个数据集建模的模型,能很好的测试出其能否找到这个隐藏的语法规则,并根据Command准确的预测Action Sequence,预测准确率越高,说明模型对这个隐藏语法规则学习的越准确,模型性能越强。
2.3 建模难度
SCAN数据集是目前建模难度非常大的数据集,主要原因由以下两点:
-
SCAN数据集数据集的主要特点是其指令和相应的行动序列的生成,是完全基于某种预定义的、结构化的语法规则。所以预测模型不能仅依赖于表面层次的统计规律或者局部的模式匹配来进行预测,必须理解和掌握数据的底层结构和规则。
-
传统模型通常依赖大量的数据和计算资源来学习和泛化复杂的模式,而SCAN数据集则需要模型具备强大的组合泛化能力,即只通过少量数据集的训练,就需要理解没有直接见过的组合指令并执行正确的行动序列。例如,虽然模型可能在训练中见过"look"和"thrice"这两个单词,但是如果没有直接见过"look thrice"这个组合,那么模型可能就无法正确地理解和执行这个指令。
因此截至目前,传统的深度学习方法、哪怕是最先进的深度学习模型也难以在该数据集上达到高水平的性能。但这样的一个业内难题,却能够被大语言模型“轻易”的解决。根据《LEAST-TO-MOST PROMPTING ENABLES COMPLEX REASONING IN LARGE LANGUAGE MODELS》论文中的描述,根据基于合理的LtM提示学习,"code-davinci-002"模型只需要输入14条训练样本,就能够对SCAN数据集做到几乎100%的准确预测(准确率为99.7%),可见大模型能力有多夸张。
三、复现针对SCAN数据预测的LtM提示流程
3.1 SCAN数据集下载
Hugging Face的Datasets库提供了一个方便的方式来访问和使用各种数据集,SCAN数据集也托管在Hugging Face上。
- Step 1:安装必要的库
要从Hugging Face读取SCAN数据集,首先需要安装datasets
库。可以使用以下命令进行安装:
pip install datasets
- Step 2:读取SCAN数据集
from datasets import load_dataset
# 加载数据集
scan_dataset = load_dataset("scan", "simple")
- Step 3:查看数据情况
此时得到的scan_dataset是DatasetDict结构,提供了一种组织和处理多个数据集分割的高效方式。对于SCAN来说,它已经被划分为训练集和测试集两个数据集,其中训练集总共包含16728条数据,而测试集总共包含4182条数据。代码如下:
DatasetDict
是 Hugging Face 的datasets
库中的一个结构,它是一个特殊的字典,用于容纳不同分割(例如训练、验证和测试)的数据集。这个结构使得处理不同的数据集分割变得非常方便。
# 打印所有的分割和其对应的数据量
for split_name, split_dataset in scan_dataset.items():
print(f"{split_name}: {len(split_dataset)} samples")
看下返回结果:
- Step 4:重命名训练集和测试集
# 重新命名现有的分割
training_data = scan_dataset['train']
testing_data = scan_dataset['test']
- Step 5:查看数据
可以直接通过索引来查看数据
training_data[0]
看下返回结果:
也可以使用to_pandas()方法,它可以将Hugging Face的datasets.Dataset
对象直接转换为pandas的DataFrame
对象。
training_data.to_pandas()
看下返回结果:
3.2 使用Zero-shot提示法推理
鉴于这个数据集的复杂性,建议不要一上来就选择最复杂的提示方法和最困难的数据进行建模。遵循“简洁高效”的原则,应优先考虑简单提示,毕竟Token也是花钱的呀。在对数据集了解不深的情况下,首先使用Zero-shot这种简单的提示方法和简单的指令集进行测试,也许能获得出乎意料的好效果。
- Step 1:抽取数据
从数据中提取简单的数据,很明显就两个策略:指令长度和动作序列长度呗,简单的指令相对较短,因为它们包含的动作和修饰词数量较少。简单的动作序列较短,因为它们包含的实际动作较少,所以代码如下:
# 定义最大长度阈值
max_command_length = 20 # 表示指令的最大长度为20个字符
max_actions_length = 5 #表示动作序列的最大长度为5个动作
# 使用列表推导式提取简单的数据
simple_data = [example for example in training_data if len(example['commands']) <= max_command_length and len(example['actions'].split()) <= max_actions_length]
# 打印前几条简单的数据
for example in simple_data[:5]:
print(example['commands'], ":", example['actions'])
看下数据抽取结果:
没问题,足够简单!
- Step 2:提取3对指令和动作单独保存为独立的变量
其内容是这样的:
command1 = 'jump thrice and look'
action1 = 'I_JUMP I_JUMP I_JUMP I_LOOK'
command2 = 'look twice and look'
action2 = 'I_LOOK I_LOOK I_LOOK'
command3 = 'run right after jump'
action3 = 'I_JUMP I_TURN_RIGHT I_RUN'
- Step 3:使用Zero-shot提示法尝试预测
简单理解Zero-shot就是什么都不告诉大模型,直接问,所以传入command1看看大模型返回的action1是什么,代码如下:
# 使用text-davinci-003模型预测
response_Zero_shot = openai.Completion.create(
model="text-davinci-003",
prompt=Command1,
max_tokens=1000,
)
response_Zero_shot["choices"][0]["text"].strip()
结果如下:
从结果上,模型完全无法给出答案,甚至是毫无关系。
- Step 4:优化Prompt
将简单的直接输入command1 改为 “The action of this command:‘%s’ should be:” % command1作为Prompt传给大模型看下效果
“The action of this command:‘%s’ should be:” % command1 --> “The action of this command:‘jump thrice and look’ should be:”
response_Zero_shot = openai.Completion.create(
model="text-davinci-003",
prompt= "%s should be translated as" % command1,
max_tokens=1000,
)
response_Zero_shot["choices"][0]["text"].strip()
看下大模型的推理结果:
直接返回了法语,译为跳三次看,一样没什么效果。既然Zero-shot不行,就进一步尝试使用Few-shot提示法。
3.3 使用Few-shot提示法推理
以第一、二条command对应的action作为提示构建Few-shot Prompt,然后围绕第三条数据进行预测,看下效果如何。
- Step 1:拼接Few-shot Prompt
few_shot_prompt = 'Q: "%s", A: "%s", Q: "%s", A: "%s", Q: "%s", A: ' % (command1, action1, command2, action2, command3)
看下最终的prompt:
- Step 2:使用Few-shot提示法尝试预测
response_Few_shot = openai.Completion.create(
model="text-davinci-003",
prompt=few_shot_prompt,
max_tokens=1000,
)
看下模型推理结果:
从结果可以明显看出,简单的以问题+答案组成的few-shot,比Zero-shot靠谱很多,已经接近标准指令的形式,但是推理是不正确的。根据原论文的描述,few-shot提示方法在SCAN数据集上的准确率不到17%,依旧很低。
3.4 使用Zer-shot-LtM提示法推理
论文中给出的最佳方法就是进行一种基于Few-shot的LtM提示工程,在OpenAI开发系列(八):基于思维链(CoT)的进阶提示工程中提到的LtM是一种Zer-shot-LtM方法。其基本流程是这样的:
根据"to solve…,we need to first solve"提示模板来进行问题拆解,代码如下:
zero_shot_ltm_prompt = "In order to translate '%s', we need to first solve" % command1
response_zero_shot_ltm_prompt = openai.Completion.create(
model="text-davinci-003",
prompt=zero_shot_ltm_prompt,
max_tokens=1000,
)
response_zero_shot_ltm_prompt["choices"][0]["text"].strip()
看下模型推理结果:
从结果上看,在Zero-shot-LtM提示下模型仍然难以准确地解读指令。其实仔细想想这样的结果符合预期的。因为对于SCAN数据集中的指令翻译,其背后的规则与自然语言的结构不完全一致。模型之前并未接触过此类特定规则,在Zero-shot条件下,其拆解的问题可以断定对整体的翻译任务没有帮助,拆解出来的子问题很大概率是毫无逻辑性的。
3.5 few-shot-LtM提示法推理
3.5.1 思路
在原论文中提出了一种Few-shot-LtM提示工程流程来解决该问题,这种Few-shot-LtM提示流程在原有流程上进行了如下两方面改进:
-
引入Few-shot引导模型对原始问题进行拆解
-
引入Few-shot引导模型对子问题再次进行多步分解
通过以上两点优化达到提升模型准确率的目的。这样说可能很难理解,我画了一个流程图,如下:
要知道,LtM提示流程是:先拆解子问题,然后去回答子问题,把子问题的答案作为Few-shot去解决原始问题。但是根据Zer-shot-LtM的测试结果来看,执行LtM流程的第一步拆解子问题就卡住了,因为模型根本不知道该怎么拆解子问题,所以第一个优化:引入Few-shot引导模型对原始问题进行拆解就是要先给模型提供一些例子,告诉模型要如何拆解子问题,让模型按照提供的插接示例拆解原始问题。当拆解完子问题后,模型需要去回答子问题,这个过程就是第二个优化:引入Few-shot引导模型对子问题再次进行多步分解,通过多步拆分多个子问题回答,即可以尝试围绕某个拆解之后的子问题进一步拆分子问题,进而提升子问题的回答准确率,上图流程就演示的将一个问题拆分成三个子问题时LtM提示回答的基本过程。
所以根据上述流程,Few-shot实际上是两个部分,第一个部分要利用Few-shot告诉模型应该如何拆解问题,第二个部分要通过Few-shot来告诉模型应该如何进行问题的回答。
在论文中,将Few-shot-LtM提示的两个阶段进行重新命名,第一阶段Decompose Questions into Subquestions被重新命名为Command decomposition,即指令拆解,也就是问题拆解,第二个阶段Sequentially Solve Subquestion则重新命名为Command mapping,即指令翻译。其本质就是依次翻译拆解的指令以及原始指令。
了解上述思想后,就可以尝试复现论文是如何在SCAN数据集上进行Few-shot-LtM提示工程的。
3.5.2 指令拆解(Command decomposition)
第一步,使用Few-shot的方法围绕命令进行拆解,从而达到引导模型对Commands进行问题拆解的目的。
在这个优化后的LtM流程中,Few-shot的设计就需要建模人员大量的工作经验+大量的尝试,来寻找到最佳的Few-shot拆分示例,并没有什么捷径。
总体思路:就是为了解读长的指令,要首先解读那些与之相关的短指令。在原论文中是这样描述的:
- Step 1:构建Command decomposition阶段指令拆解的Few-shot
按照论文给出的提示示例,选择2、3、5这三个示例做测试(选了三个复杂一点的),所以构建的Few-Shot Prompt如下:
cd_few_shot = 'Q: “jump opposite right thrice and walk” \
A: “jump opposite right thrice” can be solved by: “jump opposite right”, “jump opposite right thrice”. \
“walk” can be solved by: “walk”. So, “jump opposite right thrice and walk” can be solved by: “jump \
opposite right”, “jump opposite right thrice”, “walk”. \
Q: “run left twice and run right” \
A: “run left twice” can be solved by: “run left”, “run left twice”. “run right” can be solved by “run right”. \
So, “run left twice and run right” can.be solved by: “run left”, “run left twice”, “run right”. \
Q: “look opposite right thrice after walk” \
A: “look opposite right thrice” can be solved by: “look opposite right”, “look opposite right thrice”. \
“walk” can be solved by “walk”. So, “look opposite right thrice after walk” can be solved by: “look \
opposite right”, “look opposite right thrice”, “walk”. \
'
通过这样的提示示例明确的告诉模型指令拆解的基本规范,如“run left twice and run right”需要被拆解为“run left”, “run left twice”, “run right”这三个短的指令,而“run left twice”,需要被拆解为“run left”, “run left twice”
- Step 2:拼接Command decomposition阶段的完整提示
LtM提示流程的厉害之处在于设置了多个阶段进行分段提示,其中Step1过程构造了第一个阶段的提示,目的是为了创建中间结果——即分解子问题(命令拆解),因此Few-shot-LtM第一个阶段的提示示例也就是展示如何进行指令拆解。
可以把Few-shot就可以看成是训练集,大语言模型的建模过程和机器学习类似,都是在训练集上进行训练,然后在新的数据集上验证效果。
此处选取command:“jump thrice and look”这条指令作为特征x,其对应的action:“I_JUMP I_JUMP I_JUMP I_LOOK”作为标签y,带入LtM提示流程,如下:
为什么选择简单的?
因为此处只带入了3个提示做流程测试,如果选择复杂指令,很大概率无法覆盖到复杂指令的拆解逻辑,无法复现,所以再测试阶段,以跑通流程为主。
cd_final_prompt = cd_few_shot + 'Q:"jump thrice and look" A:' %
看下输入指令拆解提示后的完整Prompt:
- Step 4:做模型推理
response_cd = openai.Completion.create(
model="text-davinci-003",
prompt=cd_final_prompt,
temperature=0.5,
max_tokens=1000
)
response_cd["choices"][0]["text"].strip()
看下模型的推理结果:
从结果上看:原始指令’jump thrice and look’被拆分成了“jump”, “jump thrice”, “look”三个短指令。
所以对于指令拆解阶段,通过Few-shot的提示过程可以让大模型学习到底层拆解command的逻辑,也就是说,在优化后的LtM流程中,至此完成了第一个Few-shot,即:将原始问题’jump thrice and look拆分成了三个子问题,分别是:“jump”, “jump thrice”, “look”。
此处有一点需要注意:原问题拆分的子问题,是存在先后顺序的,也就是说:jump是最底层的问题,jump thrice是倒数第二层,look是最上层。之所以强调拆分子问题的顺序,是因为在下一个指令翻译(Command mapping)阶段,是按照由下往上的顺序依次进行解决,并且每一个子问题的解决都需要将下一层子问题的问题+答案作为Few-shot,如不理解请再次看上面的流程图。
3.5.3 指令翻译(Command mapping)
经过指令拆解(Command decomposition)阶段,已经拆分出了多个子问题(短命令)之后,接下来就需要依次解答(翻译)这一系列的子问题。
这个阶段会有两个过程:
- 通过Few-shot给模型短命令的翻译方法的提示
- 在翻译原始指令时,先翻译短指令,然后从底层依次向上传递,将每个解决的短指令的问题和答案作为上层prompt的一部分,带入到原指令的翻译过程中。
在原论文的第29~30页中给出的部分短指令翻译:
- Step 1:随便选择三个指令翻译做提示示例
cm_few_shot = 'Q: “jump right thrice” \
A: The output of “jump right thrice” concatenates: the output of “jump right”, the output of “jump \
right”, the output of “jump right”. “jump right” outputs “TURN RIGHT” + “JUMP”. So repeating the \
output of “jump right” three times leads to (“TURN RIGHT” + “JUMP”) * 3. So the output of “jump \
right thrice” is (“TURN RIGHT” + “JUMP”) * 3. \
Q: “turn opposite left” \
A: The output of “turn opposite left” concatenates: the output of “turn left”, the output of “turn left”. \
“turn left” outputs “TURN LEFT”. So repeating the output of “turn left” twice leads to “TURN LEFT” * \
2. So the output of “turn opposite left” is “TURN LEFT” * 2. \
Q: “turn opposite right” \
A: The output of “turn opposite right” concatenates: the output of “turn right”, the output of “turn \
right”. “turn right” outputs “TURN RIGHT”. So repeating the output of “turn right” twice leads to \
“TURN RIGHT” * 2. So the output of “turn opposite right” is “TURN RIGHT” * 2.'
- Step 2:翻译第一个子命令jump
先拼接Prompt,再送入大模型推理
cm1_prompt = cm_few_shot + 'Q:“jump” A:'
看下拼接的Prompt:
传入大模型:
response_cm1 = openai.Completion.create(
model="text-davinci-003",
prompt=cm1_prompt,
temperature=0.5,
max_tokens=1000
)
response_cm1["choices"][0]["text"].strip()
看下结果:
该结果就是第一个子命令的翻译得到结果。
- Step 3:翻译第二个子命令jump thrice
在翻译第二个子命令时,将第一个子命令时的问答结果作为Few-shot的一个示例:
cm2_prompt = cm_few_shot + response_cm1["choices"][0]["text"].strip() + 'Q:“jump thrice” A:'
response_cm2 = openai.Completion.create(
model="text-davinci-003",
prompt=cm2_prompt,
temperature=0.5,
max_tokens=1000
)
response_cm2["choices"][0]["text"].strip()
看下模型推理结果如下:
- Step 4:翻译第三个子命令look
第三个子命令的翻译,则需要同时将此前两个子命令的问答都加入Few-shot中,然后再进行提问,代码如下:
cm3_prompt = cm2_prompt + response_cm2["choices"][0]["text"].strip() + 'Q:“look” A:'
response_cm3 = openai.Completion.create(
model="text-davinci-003",
prompt=cm3_prompt,
temperature=0.5,
max_tokens=1000
)
response_cm3["choices"][0]["text"].strip()
模型推理结果如下:
- Step 5:提问原始问题,流程如下
在获得了每个子命令的问答结果之后,接下来,将每个子命令的问答结果都拼接到Few-shot中,并对模型提问原始问题,流程如下:
cm4_prompt = cm3_prompt + response_cm3["choices"][0]["text"].strip() + 'Q:“jump thrice and look” A:'
response_CM = openai.Completion.create(
model="text-davinci-003",
prompt=prompt_CM,
temperature=0.5,
max_tokens=1000
)
response_CM["choices"][0]["text"].strip()
结果如下:
模型最终获得原始问题的答案为JUMP” + “JUMP” + “JUMP” + “LOOK”,其实很明显可以看出,和正确答案是完全匹配的。
- Step 5:格式化输出
为了更清晰的比对模型推理结果和真实结果,可以使用如下函数格式化大模型的推理输出。
import re
def format_model_output(command_output):
"""
将大模型的输出格式化并标准化为 SCAN 数据集的标准动作。
此函数对 command_output 执行以下转换:
1. 处理乘法运算。
2. 用下划线替换引号内的空格。
3. 在引号内添加 'I_' 前缀。
4. 删除引号。
5. 删除 '+'。
参数:
- command_output (str): 大模型的输出字符串。
返回:
- str: 转换后的动作序列。
"""
# 提取命令输出的相关部分。
matched_section = re.search(r'is “.*', command_output)
if not matched_section:
return None
standardized_output = matched_section.group()[3: -1].replace('“', '"').replace('”', '"')
# 处理乘法: 用 "action" * N 替换为 N 次重复的 "action"。
for action, repetition_count in re.findall(r'"([^"]+)" \* (\d+)', standardized_output):
repeated_action = ' '.join([f'"{action}"'] * int(repetition_count))
standardized_output = standardized_output.replace(f'"{action}" * {repetition_count}', repeated_action)
# 用下划线替换引号内的空格。
for action in re.findall(r'"([^"]+)"', standardized_output):
underscored_action = action.replace(' ', '_')
standardized_output = standardized_output.replace(f'"{action}"', f'"{underscored_action}"')
# 在引号内添加 'I_' 前缀。
for action in re.findall(r'"([^"]+)"', standardized_output):
prefixed_action = 'I_' + action
standardized_output = standardized_output.replace(f'"{action}"', f'"{prefixed_action}"')
# 删除引号和 '+'。
final_action_sequence = standardized_output.replace('"', '').replace(' +', '')
return final_action_sequence
看下格式化后的结果:
上述完成了一次完整的基于复杂语义问题的LtM提示工程流程,并顺利获得准确答案。Few-shot-LtM提示流程非常复杂,流程图如下:
四、SCAN数据集完整预测流程
经过以上实验,验证了Few-shot-LtM在SCAN数据集上是有效的,下面就可以基于这个流程,构建一个大模型的预测建模流程。所以可以思考一下,上述流程是对一条数据做的预测,如果围绕着类似机器学习建模在全量数据上,应该怎么做比较合适呢?下面给出一种解决方案。
- Step 1:提取数据并划分训练集和测试集
from datasets import load_dataset
# 加载数据集
scan_dataset = load_dataset("scan", "simple")
# 重新命名现有的分割
training_data = scan_dataset['train']
testing_data = scan_dataset['test']
- Step 2:构建两个阶段的Few-shot示例
根据原论文的描述,为了更好的完成完整数据集的预测,第一阶段命令拆解需要设置8组问答示例,第二阶段命令映射则需要设置14组问答示例
cd阶段和cm阶段的这两组提示示例,均为原论文提供。
cd_few_shot = 'Q: “look right after look twice” \
A: “look right after look twice” can be solved by: “look right”, “look twice”. \
Q: “jump opposite right thrice and walk” \
A: “jump opposite right thrice” can be solved by: “jump opposite right”, “jump opposite right thrice”. \
“walk” can be solved by: “walk”. So, “jump opposite right thrice and walk” can be solved by: “jump \
opposite right”, “jump opposite right thrice”, “walk”. \
Q: “run left twice and run right” \
A: “run left twice” can be solved by: “run left”, “run left twice”. “run right” can be solved by “run right”. \
So, “run left twice and run right” can.be solved by: “run left”, “run left twice”, “run right”. \
Q: “run opposite right” \
A: “run opposite right” can be solved by “run opposite right”. \
Q: “look opposite right thrice after walk” \
A: “look opposite right thrice” can be solved by: “look opposite right”, “look opposite right thrice”. \
“walk” can be solved by “walk”. So, “look opposite right thrice after walk” can be solved by: “look \
opposite right”, “look opposite right thrice”, “walk”. \
Q: “jump around right” \
A: “jump around right” can be solved by: “jump right”, “jump around right”. So, “jump around right” \
can be solved by: “jump right”, “jump around right”. \
Q: “look around right thrice and walk” \
A: “look around right thrice” can be solved by: “look right”, “look around right”, “look around right \
thrice”. “walk” can be solved by “walk”. So, “look around right thrice and walk” can be solved by: \
“look right”, “look around right”, “look around right thrice”, “walk”. \
Q: “turn right after run right thrice” \
A: “turn right” can be solved by: “turn right”. “run right thrice” can be solved by: “run right”, “run \
right thrice”. So, “turn right after run right thrice” can be solved by: “turn right”, “run right”, “run right \
thrice”. \
'
cm_few_shot = 'Q: “turn left” \
A: “turn left” outputs “TURN LEFT”. \
Q: “turn right” \
A: “turn right” outputs “TURN RIGHT”. \
Q: “jump left” \
A: The output of “jump left” concatenates: the output of “turn left”, the output of “jump”. “turn left” \
outputs “TURN LEFT”. “jump” outputs “JUMP”. So concatenating the output of “turn left” and the output of “jump” leads to “TURN LEFT” + “JUMP”. So the output of “jump left” is “TURN LEFT” + “JUMP”. \
Q: “run right” \
A: The output of “run right” concatenates: the output of “turn right”, the output of “run”. “turn right” \
outputs “TURN RIGHT”. “run” outputs “RUN”. So concatenating the output of “turn right” and the \
output of “run” leads to “TURN RIGHT” + “RUN”. So the output of “run right” is “TURN RIGHT” + \
“RUN”. \
Q: “look twice” \
A: The output of “look twice” concatenates: the output of “look”, the output of “look”. “look” outputs \
“LOOK”. So repeating the output of “look” two times leads to “LOOK” * 2. So the output of “look \
twice” is “LOOK” * 2. \
Q: “run and look twice” \
A: The output of “run and look twice” concatenates: the output of “run”, the output of “look twice”. \
“run” outputs “RUN”. “look twice” outputs “LOOK” * 2. So concatenating the output of “run” and the \
output of “look twice” leads to “RUN” + “LOOK” * 2. So the output of “run and look twice” is “RUN” + \
“LOOK” * 2. \
Q: “jump right thrice” \
A: The output of “jump right thrice” concatenates: the output of “jump right”, the output of “jump \
right”, the output of “jump right”. “jump right” outputs “TURN RIGHT” + “JUMP”. So repeating the \
output of “jump right” three times leads to (“TURN RIGHT” + “JUMP”) * 3. So the output of “jump \
right thrice” is (“TURN RIGHT” + “JUMP”) * 3. \
Q: “walk after run” \
A: The output of “walk after run” concatenates: the output of “run”, the output of “walk”. “run” outputs \
“RUN”. “walk” outputs “WALK”. So concatenating the output of “run” and the output of “walk” leads to \
“RUN” + “WALK”. So the output of “walk after run” is “RUN” + “WALK”. \
Q: “turn opposite left” \
A: The output of “turn opposite left” concatenates: the output of “turn left”, the output of “turn left”. \
“turn left” outputs “TURN LEFT”. So repeating the output of “turn left” twice leads to “TURN LEFT” * \
2. So the output of “turn opposite left” is “TURN LEFT” * 2. \
Q: “turn around left” \
A: The output of “turn around left” concatenates: the output of “turn left”, the output of “turn left”, the \
output of “turn left”, the output of “turn left”. “turn left” outputs “TURN LEFT”. So repeating the output \
of “turn left” four times leads to “TURN LEFT” * 4. So the output of “turn around left” is “TURN LEFT” \
* 4. \
Q: “turn opposite right” \
A: The output of “turn opposite right” concatenates: the output of “turn right”, the output of “turn \
right”. “turn right” outputs “TURN RIGHT”. So repeating the output of “turn right” twice leads to \
“TURN RIGHT” * 2. So the output of “turn opposite right” is “TURN RIGHT” * 2. \
Q: “turn around right” \
A: The output of “turn around right” concatenates: the output of “turn right”, the output of “turn right”, \
the output of “turn right”, the output of “turn right”. “turn right” outputs “TURN RIGHT”. So repeating \
the output of “turn right” four times leads to “TURN RIGHT” * 4. So the output of “turn around right” \
is “TURN RIGHT” * 4. \
Q: “walk opposite left” \
A: The output of “walk opposite left” concatenates: the output of “turn opposite left”, the output of \
“walk”. “turn opposite left” outputs “TURN LEFT” * 2. “walk” outputs “WALK”. So concatenating the \
output of “turn opposite left” and the output of “walk” leads to “TURN LEFT” * 2 + “WALK”. So the \
output of “walk opposite left” is “TURN LEFT” * 2 + “WALK”. \
Q: “walk around left” \
A: The output of “walk around left” concatenates: the output of “walk left”, the output of “walk left”, \
the output of “walk left”, the output of “walk left”. “walk left” outputs “TURN LEFT” + “WALK”. So \
repeating the output of “walk around left” four times leads to (“TURN LEFT” + “WALK”) * 4. So the \
output of “walk around left” is (“TURN LEFT” + “WALK”) * 4. \
'
- Step 3:将command拼接到提示词的词库中去,组成一个新的提示
这里就以训练集的第一条数据为例,将它的command与设置的8组拆分子指令的提示拼接到一起。
cd_prompt = cd_few_shot + 'Q:“%s” A:' % training_data[0]['commands']
看下拼接的prompt
通过这种方式,来给每一条特征x,也就是每一个command做提示,让大模型拆分每个command的子指令。
- Step 4:送入大模型做指令拆解
response_cd = openai.Completion.create(
model="text-davinci-003",
prompt=cd_prompt,
temperature=0.5,
max_tokens=1000
)
- Step 5:提取拆解出来的指令
看下执行完Step 4模型的推理结果:
其中can be solved by: 后面的内容,就是要提取出来的拆解指令,可使用以下代码:
import re
def extract_subcommands(text):
"""
从给定的文本中提取拆分后的子指令。
函数搜索文本中最后一个 "solved by:" 部分,并从其后面提取子指令。
参数:
- text (str): 包含子指令解析的文本。
返回:
- list of str: 提取的子指令列表。
"""
# 提取 "solved by:" 后面拆解出来的全部子指令
last_solution_section = text.rsplit("solved by:", 1)[-1]
# 使用正则表达式提取引号中的子指令
subcommands = re.findall(r'“([^”]*)”', last_solution_section)
return subcommands
测试一下函数:
extract_subcommands(response_cd["choices"][0]["text"].strip())
看下函数输出:
ok,没问题!
- Step 6: 循环完成每一个子问题的回答
每一个循环的内容都是上一层的问题+上一层的答案+原始的Few-shot提示词不断的累积拼接,代码如下:
from tqdm import tqdm
cm_few_shot_temp = cm_few_shot
sub_qa = extract_subcommands(response_cd["choices"][0]["text"].strip())
for qs in tqdm(sub_qa):
cm_few_shot_temp += 'Q:“%s” A:' % qs
response_cm = openai.Completion.create(
model="text-davinci-003",
prompt=cm_few_shot_temp,
temperature=0.5,
max_tokens=1000,
)
cm_few_shot_temp += response_cm["choices"][0]["text"].strip()
cm_few_shot_temp
看下函数运行结果:
- Step 7:对原始问题提问并获取最终结果
prompt_cm = cm_few_shot_temp + 'Q:“%s” A:' % training_data[0]['commands']
response_cm = openai.Completion.create(
model="text-davinci-003",
prompt=prompt_cm,
temperature=0.5,
max_tokens=1000,
)
format_model_output(response_cm["choices"][0]["text"].strip())
看下最终结果:
- Step 8:将上述流程封装完整函数
def llm_predict(scan_data, model="text-davinci-003", cd_few_shot, cm_few_shot):
"""
使用OpenAI的大模型对SCAN数据集进行预测。
此函数的工作流程:
1. 拆解命令(Command Decomposition)。
2. 使用拆解后的短命令进行翻译。
3. 对原始问题提问。
参数:
- scan_data: 待预测的SCAN数据集。
- model (str): 使用的OpenAI模型名称,默认为"text-davinci-003"。
- cd_few_shot: 命令拆解的提示示例。
- cm_few_shot: 命令映射的提示示例。
返回:
- pandas.DataFrame: 包含预测结果的数据框。
"""
# 转化为dataframe
data_frame = data.to_pandas()
# 初始化预测列
data_frame['actions_predict'] = 'unkown'
for i, data in enumerate(scan_data):
# 阶段一:拆解子命令
prompt_cd = cd_few_shot + 'Q:“%s” A:' % data['commands']
response_cd = openai.Completion.create(
model=model,
prompt=prompt_cd,
temperature=0.8,
max_tokens=1000
)
# 拆解命令结果
cd_result = extract_subcommands(response_cd["choices"][0]["text"].strip())
# 阶段二:短命令翻译
cm_few_shot_temp = cm_few_shot
for qs in cd_result:
cm_few_shot_temp += 'Q:“%s” A:' % qs
response_cm = openai.Completion.create(
model=model,
prompt=cm_few_shot_temp,
temperature=0.8,
max_tokens=1000
)
cm_few_shot_temp += response_cm["choices"][0]["text"].strip()
# 对原始问题提问
prompt_cm = cm_few_shot_temp + 'Q:“%s” A:' % data['commands']
response_c, = openai.Completion.create(
model=model,
prompt=prompt_cm,
temperature=0.8,
max_tokens=1000
)
# 将结果保存在dataframe的对应位置
data_frame['actions_predict'][i] = format_model_output(response_cm["choices"][0]["text"].strip())
return data_frame
# 验证实际预测效果
data_frame = llm_predict(scan_data=testing_data)
输出如下:
(data_frame['actions'] == data_frame['actions_predict']).sum() / data_frame.shape[0]
输出为1.0,在completion模型text-davinci-003预测下,SCAN数据集的预测准确率能够达到100%。
五、总结
总结一下Few-shot-LtM提示流程,还是这张图:
Zero-shot对于复杂指令的提示是非常有限的,很多时候不得不要人工依据经验去编写和测试一系列的高效提示示例,然后通过Few-shot提示法去尝试解决,和机器学习直接在完整数据集上随机划分训练集和测试集不同的是,Few-shot往往不需要太多数据(受限于模型的上下文限制,也无法输入太多数据)。
另一方面,从这个复现过程也强力的验证了大语言模型(LLMs)具备非常强的“迁移学习”能力,像SCAN数据集这种学习难度非常大的任务,模型仍然可以在只带入非常少的数据的情况下完成底层推理规则的学习,由此也确实能看出大语言模型恐怖的涌现能力。
最后,感谢您阅读这篇文章!如果您觉得有所收获,别忘了点赞、收藏并关注我,这是我持续创作的动力。您有任何问题或建议,都可以在评论区留言,我会尽力回答并接受您的反馈。如果您希望了解某个特定主题,也欢迎告诉我,我会乐于创作与之相关的文章。谢谢您的支持,期待与您共同成长!