分类优化#
分类是自然语言处理(NLP)中最广泛使用的任务之一。 优化基于GenAI的分类可以帮助开发者快速创建一个性能良好的模型。 从长远来看,这个模型还可以帮助引导训练一个更具成本效益的分类模型。
学习曲线在训练系统任务指令和一次性演示上。#
分类任务的优化提示。#
以下是您将从本教程中学到的内容:
如何构建一个具有结构化输出的分类任务管道。
在我们探索
TextOptimizer和DemoOptimizer以优化分类任务时,mixed和sequential训练的概念。如何处理验证数据集不能很好地指示测试准确性的情况。
注意
你可以在我们的GitHub仓库中找到我们所有的代码:use_cases/classification,以及Dspy的实现位于benchmarks/trec_classification。
带有结构化输出的任务管道#
我们将使用以下整体模板,包含system_prompt、output_format_str和few_shot_demos变量。
task_desc_template将用于从类名和每个标签的描述中渲染最终的分类任务描述。
TRECExtendedData是一个数据类,它通过一个理由字段扩展了TrecData。这将确保我们的生成器在预测最终的类名之前首先利用‘链式思维’推理。
template = r"""<START_OF_SYSTEM_MESSAGE>
{{system_prompt}}
{% if output_format_str is not none %}
{{output_format_str}}
{% endif %}
{% if few_shot_demos is not none %}
Here are some examples:
{{few_shot_demos}}
{% endif %}
<END_OF_SYSTEM_MESSAGE>
<START_OF_USER_MESSAGE>
{{input_str}}
<END_OF_USER_MESSAGE>
"""
task_desc_template = r"""You are a classifier. Given a question, you need to classify it into one of the following classes:
Format: class_index. class_name, class_description
{% if classes %}
{% for class in classes %}
{{loop.index-1}}. {{class.label}}, {{class.desc}}
{% endfor %}
{% endif %}
- Do not try to answer the question:
"""
@dataclass
class TRECExtendedData(TrecData):
rationale: str = field(
metadata={
"desc": "Your step-by-step reasoning to classify the question to class_name"
},
default=None,
)
__input_fields__ = ["question"]
__output_fields__ = ["rationale", "class_name"] # it is important to have the rationale before the class_name
我们将从Component子类化来完成我们的最终任务管道。
我们使用DataClassParser来简化输出格式化和解析的过程。
class TRECClassifierStructuredOutput(adal.Component):
def __init__(self, model_client: adal.ModelClient, model_kwargs: Dict):
super().__init__()
label_desc = [
{"label": label, "desc": desc}
for label, desc in zip(_COARSE_LABELS, _COARSE_LABELS_DESC)
]
task_desc_str = adal.Prompt(
template=task_desc_template, prompt_kwargs={"classes": label_desc}
)()
self.data_class = TRECExtendedData
self.data_class.set_task_desc(task_desc_str)
self.parser = adal.DataClassParser(
data_class=self.data_class, return_data_class=True, format_type="yaml"
)
prompt_kwargs = {
"system_prompt": adal.Parameter(
data=self.parser.get_task_desc_str(),
role_desc="Task description",
requires_opt=True,
param_type=adal.ParameterType.PROMPT,
),
"output_format_str": adal.Parameter(
data=self.parser.get_output_format_str(),
role_desc="Output format requirements",
requires_opt=False,
param_type=adal.ParameterType.PROMPT,
),
"few_shot_demos": adal.Parameter(
data=None,
requires_opt=True,
role_desc="Few shot examples to help the model",
param_type=adal.ParameterType.DEMOS,
),
}
self.llm = adal.Generator(
model_client=model_client,
model_kwargs=model_kwargs,
prompt_kwargs=prompt_kwargs,
template=template,
output_processors=self.parser,
use_cache=True,
)
def _prepare_input(self, question: str):
input_data = self.data_class(question=question)
input_str = self.parser.get_input_str(input_data)
prompt_kwargs = {
"input_str": adal.Parameter(
data=input_str, requires_opt=False, role_desc="input to the LLM"
)
}
return prompt_kwargs
def call(
self, question: str, id: Optional[str] = None
) -> Union[adal.GeneratorOutput, adal.Parameter]:
prompt_kwargs = self._prepare_input(question)
output = self.llm(prompt_kwargs=prompt_kwargs, id=id)
return output
在这个任务管道中,我们准备了两个可训练的参数:system_prompt 和 few_shot_demos,每个参数的类型分别为 adal.ParameterType.PROMPT 和 adal.ParameterType.DEMOS。
我们将需要 TGDOptimizer 来优化 system_prompt 和 BootstrapOptimizer
来优化 few_shot_demos。
定义AdalComponent#
现在,我们将定义一个AdalComponent的子类来准备训练管道。
我们已经设置了eval_fn、loss_fn,以及为文本优化器配置反向引擎的方法,
还有一个为演示优化器配置教师生成器的方法。
class TrecClassifierAdal(adal.AdalComponent):
def __init__(
self,
model_client: adal.ModelClient,
model_kwargs: Dict,
teacher_model_config: Dict,
backward_engine_model_config: Dict,
text_optimizer_model_config: Dict,
):
task = TRECClassifierStructuredOutput(model_client, model_kwargs)
eval_fn = AnswerMatchAcc(type="exact_match").compute_single_item
loss_fn = adal.EvalFnToTextLoss(
eval_fn=eval_fn,
eval_fn_desc="exact_match: 1 if str(y) == str(y_gt) else 0",
)
super().__init__(
task=task,
eval_fn=eval_fn,
loss_fn=loss_fn,
backward_engine_model_config=backward_engine_model_config,
text_optimizer_model_config=text_optimizer_model_config,
teacher_model_config=teacher_model_config,
)
def prepare_task(self, sample: TRECExtendedData):
return self.task.call, {"question": sample.question, "id": sample.id}
def prepare_eval(
self, sample: TRECExtendedData, y_pred: adal.GeneratorOutput
) -> float:
y_label = -1
if y_pred and y_pred.data is not None and y_pred.data.class_name is not None:
y_label = y_pred.data.class_name
return self.eval_fn, {"y": y_label, "y_gt": sample.class_name}
def prepare_loss(
self, sample: TRECExtendedData, y_pred: adal.Parameter, *args, **kwargs
) -> Tuple[Callable[..., Any], Dict]:
full_response = y_pred.full_response
y_label = -1
if (
full_response
and full_response.data is not None
and full_response.data.class_name is not None
):
y_label = full_response.data.class_name
y_pred.eval_input = y_label
y_gt = adal.Parameter(
name="y_gt",
data=sample.class_name,
eval_input=sample.class_name,
requires_opt=False,
)
return self.loss_fn, {"kwargs": {"y": y_pred, "y_gt": y_gt}}
训练器和训练策略#
训练策略
以下代码展示了我们的默认训练配置。我们使用批量大小为4,12个步骤,以及4个工作者来并行调用LLMs。
optimize_order设置为sequential,以首先训练文本优化器,然后训练演示优化器。
这种训练策略一直表现良好。通过优化文本,可能会提升教师模型的性能。
借助教师模型的推理,演示优化器即使仅从教师那里获得一次演示,也能学会更好地推理。
当我们处于sequential优化顺序时,最终将训练24个步骤。
此外,您可以尝试mixed作为优化顺序,其中在每一步中,它将同时更新文本优化器和演示优化器。
def train(
model_client: adal.ModelClient,
model_kwargs: Dict,
train_batch_size=4, # larger batch size is not that effective, probably because of llm's lost in the middle
raw_shots: int = 0,
bootstrap_shots: int = 1,
max_steps=12,
num_workers=4,
strategy="constrained",
optimization_order="sequential",
debug=False,
):
# TODO: ensure the teacher prompt gets updated with the new model
adal_component = TrecClassifierAdal(
model_client=model_client,
model_kwargs=model_kwargs,
text_optimizer_model_config=gpt_4o_model,
backward_engine_model_config=gpt_4o_model,
teacher_model_config=gpt_4o_model,
)
print(adal_component)
trainer = adal.Trainer(
train_batch_size=train_batch_size,
adaltask=adal_component,
strategy=strategy,
max_steps=max_steps,
num_workers=num_workers,
raw_shots=raw_shots,
bootstrap_shots=bootstrap_shots,
debug=debug,
weighted_sampling=True,
optimization_order=optimization_order,
exclude_input_fields_from_bootstrap_demos=True,
)
print(trainer)
train_dataset, val_dataset, test_dataset = load_datasets()
trainer.fit(
train_dataset=train_dataset,
val_dataset=test_dataset,
debug=debug,
)
在这种情况下,我们没有使用val_dataset,因为我们进行了诊断,并且如表1所示,验证数据集并不是测试准确性的良好指标。
因此,我们最终的训练策略是直接在测试数据集上进行验证。
训练检查点:
在训练结束时,我们将打印出ckpt路径,您可以在其中查找有关训练提示的所有详细信息。 这是我们上面的训练:
Loading Data: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 144/144 [00:00<00:00, 51011.81it/s]
Evaluating step(24): 0.8426 across 108 samples, Max potential: 0.8819: 75%|█████████████████████████████████████████████████████████████████████▊ | 108/144 [00:00<00:00, 1855.48it/s]
Fail validation: 0.8348623853211009 <= 0.8819444444444444, revert
Training Step: 24: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 12/12 [03:05<00:00, 15.46s/it]
Saved ckpt to /Users/liyin/.adalflow/ckpt/TrecClassifierAdal/constrained_max_steps_12_848d2_run_7.json
Training time: 823.8977522850037s
我们可以看到训练仅需14分钟。 我们使用了12个步骤,学习曲线如图1所示。 这是我们训练的系统提示和演示提示:
system_prompt = "You are a classifier. Given a question, you need to classify it into one of the following classes:\nFormat: class_index. class_name, class_description\n0. ABBR, Abbreviation or acronym\n1. ENTY, Entity, including specific terms, brand names, or other distinct entities\n2. DESC, Description and abstract concept, including explanations, characteristics, and meanings\n3. HUM, Human being\n4. LOC, Location, including spatial information, geographical places\n5. NUM, Numeric value, including measurable figures, quantities, distances, and time\n- Focus on correctly identifying the class based on the question's main inquiry:"
few_shot_demos = "rationale: The question is asking for a specific term used to describe the sum of\n all genetic material in an organism.\nclass_name: ENTY"
我们可以看到,与我们的初始提示相比,它为每个类添加了一些简洁的解释。 演示提示也很简短,直接来自教师模型,教导学生模型进行推理以达到最终的类名。
性能与基准测试#
我们实现了带有随机搜索的Dspy Bootstrap少样本。
这里是 DsPy 的签名(类似于提示),其任务描述直接复制了我们 AdalFlow 的起始提示:
class GenerateAnswer(dspy.Signature):
"""You are a classifier. Given a question, you need to classify it into one of the following classes:
Format: class_index. class_name, class_description
1. ABBR, Abbreviation
2. ENTY, Entity
3. DESC, Description and abstract concept
4. HUM, Human being
5. LOC, Location
6. NUM, Numeric value
- Do not try to answer the question:"""
question: str = dspy.InputField(desc="Question to be classified")
answer: str = dspy.OutputField(
desc="Select one from ABBR, ENTY, DESC, HUM, LOC, NUM"
)
这是性能结果
方法 |
训练 |
验证 |
测试 |
|---|---|---|---|
开始(手动提示) |
67.5% (20*6 样本) |
69.4% (6*6 样本) |
82.64% (144 样本) |
开始 (GPT-4o/教师) |
77.5% |
77.78% |
86.11% |
DsPy (开始) |
57.5% |
61.1% |
60.42% |
DsPy (bootstrap 4-shots + raw 36-shots) |
N/A |
86.1% |
82.6% |
AdalFlow(优化的零样本) |
N/A |
77.78%, 80.5% (+8.4%) |
86.81%, 89.6% (+4.2%) |
AdalFlow(优化的零样本 + 引导1样本) |
N/A |
N/A |
88.19% |
AdalFlow(优化的零样本 + 引导1样本 + 40原始样本) |
N/A |
86.1% |
90.28% |
AdalFlow(在GPT-4o上优化的零样本) |
77.8% |
77.78% |
84.03% |
在这种情况下,我们的文本优化器–Text-Grad 2.0能够缩小与教师模型的差距,为DemoOptimizer在学习从教师模型的推理中提升其推理能力时留下的改进空间很小。 尽管多次尝试(多达40次)仍然可以稍微提高性能,但它会增加更多的标记。
我们可以看到,能够灵活控制提示而不是委托给固定的Signature是有优势的。
在这种情况下,我们使用yaml格式进行输出,并且能够使用模板来控制我们想要训练的部分。
我们尝试训练一个结合了系统提示和输出格式的Parameter,发现仅训练系统提示更为有效。
结论:
我们的SOTA性能归功于以下因素的结合
我们对优化器的研究:每个单独的优化器,实现我们研究的文本优化器 Text-grad 2.0 和实现我们研究的演示优化器
Learn-to-reason Few-shot In-context Learning我们对训练范式的研究:顺序训练,即首先训练文本优化器,然后训练演示优化器,已被证明在优化性能方面是有效的,而不会在提示中添加过多的标记。
库的灵活性和可定制性:通过库为开发者提供对提示的直接控制,并允许灵活和细粒度地定义参数,这是我们能够大幅超越其他方法的第二个原因。