返回列表

3rd place solution

603. LLM Prompt Recovery | llm-prompt-recovery

开始: 2024-02-27 结束: 2024-04-16 AIGC与多模态 数据算法赛
```html 第三名解决方案 - LLM提示词恢复

第三名解决方案

作者:Eduardo Rocha de Andrade

团队成员:matheus, pedromb

排名:第三名

首先,我要感谢我的队友和朋友 @tomirol@pedromb —— 这些家伙太棒了。

我还要感谢Kaggle和Google举办这次比赛。尽管有些"平均提示词"的争议,但这次比赛非常有趣,也是一次很棒的学习体验,因为这是我第一次微调LLM。

像许多其他团队一样,我们的解决方案是平均提示词和模型预测的混合方案。它包含5个主要组成部分:

  1. 一个平均提示词模板(我们用模型预测来格式化字符串)
  2. 一个微调的 MistralForCausalLM 用于预测完整提示词
  3. 一个训练用于过滤明显错误提示词预测的 MistralForSequenceClassification(类似于门控机制)
  4. 一个预测样本标签的 MistralForCausalLM,如"船歌"、"总结"、"正式语气"等
  5. 两个聚类模型,对测试样本进行聚类并为其选择最佳平均提示词模板

仅使用组件1、2、3和4,我们就达到了0.71分。聚类策略将0.71分提高了,但不足以达到0.72(在本地验证中提高了约0.005分):(

最终解决方案是平均提示词 + 标签 + 完整提示词(如果通过门控)。对于标签和完整提示词,我们只添加不重复的单词(意味着如果它们已经在平均提示词中,就不会再次添加)。我们还将标签 + 完整提示词添加到平均提示词的第三个单词之后——我们尝试找到最佳位置,这个位置在本地验证中取得了最佳效果。使用的平均提示词是基于聚类选择的。

平均提示词

为了获得平均提示词,我们做了3个步骤:

  1. 生成数据。首先,我们生成了潜在的重写提示词数据集(请参阅下面的"预测完整提示词的模型"部分,了解数据集是如何准备的)。
  2. 获得这个数据集后,我们运行了一个程序来选择一个子样本,使其遵循公开LB上数据集的分布。我们通过将使用单个提示词预测的LB结果与选择的子样本进行匹配来实现这一点。执行此操作的脚本见这里:https://github.com/pedromb/llm-prompt-recovery/blob/main/src/data_generation/prompt_selection.ipynb
  3. 然后,我们运行了一个简单的束搜索,使用几千个单词来找到能够优化所选数据集分数的单词组合:https://github.com/matheuspf/llm-prompt-recovery/blob/main/src/optimization/mean_prompt_tokens.py

我们注意到在子采样数据集上获得的分数与使用优化平均提示词的LB分数之间存在非常强的相关性,这表明我们确实获得了一些有用的东西。这是我们最终的(全局)平均提示词:

"improve phrasing text {这里我们格式化完整提示词预测 + 标签} lucrarea tone lucrarea rewrite this creatively formalize discours involving lucrarea anyone emulate lucrarea description send casual perspective information alter it lucrarea ss plotline speaker recommend doing if elegy tone lucrarea more com n paraphrase ss forward this st text redesign poem above etc possible llm clear lucrarea"

预测完整提示词的模型

像许多人一样,我们努力微调一个能够预测完整提示词并在此比赛中取得好结果的模型。一旦我们找到了神奇的平均提示词,就在这方面花了较少精力。在比赛初期,我们得到了一个得分为0.61的零样本模型,但我们微调的大多数模型都无法超过0.6。最终,我们得到了一个单独得分为0.62的模型,并与之迭代以找到与平均提示词配合使用的最佳版本。

最终使用的版本是一个LoRA微调的 mistralai/Mistral-7B-Instruct-v0.2。这个mistral模型在所有我们的测试中都远远优于其他模型,比v0.1好很多,也远远领先于Gemma。多亏了 unsloth,我们还能够非常高效地微调它,这可能是这次比赛最好的发现。训练本身非常标准,没有什么秘诀(脚本见这里:https://github.com/pedromb/llm-prompt-recovery/blob/main/src/train/lora_mistral_7b_unsloth.py),收益来自于数据生成过程本身。以下是最终帮助提高结果的策略:

  1. 使用一些LLM生成提示词候选(主要使用Gemini和gpt3.5 turbo)。重要的是要以一种也能获得输入文本预期特征的方式来提示。这有助于在选择/生成原始文本以提示Gemma进行转换时进行匹配。这是我们使用的脚本:https://github.com/pedromb/llm-prompt-recovery/blob/main/src/data_generation/prompt_generation.py
  2. 之后,通过指示LLM以某种方式改变它来生成原始提示词的一些变体。这有助于增加多样性,更重要的是,它创建了元组 (original_text, rewrite_prompt) 的变体,其中原始文本相同,重写意图相同但表达方式不同。使用的脚本是:https://github.com/pedromb/llm-prompt-recovery/blob/main/src/data_generation/prompt_variations_generation.py
  3. 生成输入文本。我们更倾向于使用LLM生成原始文本,以保证输入文本特征与提示词匹配。同样,主要使用gemini和gpt3.5 turbo来完成此任务。这是使用的脚本:https://github.com/pedromb/llm-prompt-recovery/blob/main/src/data_generation/text_generation.py
  4. 生成Gemma版本的改写文本。起初我们使用Gemma 7b-it-quant版本,但非常慢,所以最终改为使用Gemma 2b-it-quant版本与 unsloth 配合使用。我们仍然想知道这是否影响了最终结果,可能会,但有了平均提示词后它变得不那么重要了。使用的脚本是:https://github.com/pedromb/llm-prompt-recovery/blob/main/src/data_generation/gemma_2b_rewritten_text_generation_unsloth.py
  5. 对提示词进行聚类并平衡训练数据集。为此,我们获取所有聚类的平均大小,并为每个聚类选择最多该大小的样本(我认为我错过了这一步的脚本,稍后会尝试发布——但聚类是使用T5嵌入作为特征,通过 sklearn 中的 HDBSCAN 生成的,如果一个提示词没有被聚类,我会将其添加到与它最相似的聚类中——使用每个聚类嵌入的平均值来计算相似度——如果最大相似度 < 0.9,则该提示词将成为单独的提示词聚类)。
  6. 后处理文本,删除所有"Sure here is..."等内容,以提高整体数据集质量。脚本见这里:https://github.com/pedromb/llm-prompt-recovery/blob/main/src/data_generation/process_data.ipynb

值得注意的是,我们还将比赛中分享的所有公共数据集添加到最终数据集,并运行步骤2来生成提示词的变体。步骤2和步骤5的结合是突破0.62分的关键。我相信,强制模型看到原始文本相同且重写指令在语义上相似但词汇不同的例子,会迫使模型学习重写提示词的变化如何转化为改写的文本。也许使用实际的7b版本生成改写版本会带来更好的结果,但在达到这里并发现平均提示词后,我们只是尝试了不同的训练设置和不同的聚类策略,但与平均提示词结合使用时没有带来进一步改进。

门控模型

在某种程度上,我们意识到大多数时候,当预测完整提示词的模型犯错误时,通常是非常明显的,即与真实 rewrite_prompt 完全无关。因此,我们认为有可能让另一个模型来"门控"或过滤掉我们的错误预测。

我们通过简单地将Mistral实例化为带有单个类的 MistralForSequenceClassification 来训练门控模型,并使用以下提示词:

prompt = (
        "[INST]给定原始文本和候选的重写提示词,你需要评估改写后的文本是否合理地应用了候选提示词。你将获得正面示例,其中改写后的文本确实是通过应用所提出的提示词创建的,以及负面示例,其中改写后的文本是通过不同的提示词创建的。"
        f'\n\nOriginal text: \n"""{original_text}"""\n\nCandidate Prompt: \n"""{candidate_prompt}"""\n\nRewritten text: \n"""{rewritten_text}"""[/INST]'
    )

在训练时,40%的时间我们使用正确的三元组(正面样本),另外20%的时间我们只是从数据集中随机选择另一个 rewrite_prompt(简单负样本),40%的时间我们选择在T5嵌入空间中与正确提示词更接近的另一个候选提示词(困难负样本)。

标签模型

我们意识到预测 rewrite_prompt 的某些方面是相当困难的,甚至是不可能的。例如主要动词,如"rewrite"、"rephrase"、"change"、"transform",或主语(即使 original_text 是一首诗,rewrite_prompt 可能是"Make the text happier",预测"poem"不会有太大帮助)。因此,我们训练了第二个MistralForCausalLM,但只预测样本的标签,我们注意到只要简单删除完整提示词预测中已经提到的标签,这个模型就能与完整提示词模型很好地配合使用。

聚类

好吧,如果1个平均提示词很好,那么2个、3个或12个会不会更好呢?😂

说真的,当我们发现平均提示词后,我们在本地验证集的T5嵌入空间上直接运行LBFGS优化,发现最佳解决方案得分为72。当然,嵌入空间是连续的,而通过T5使用单词时只能离散地接近,这限制了我们的分数约为0.7 CV(0.69 LB)。

然而,有一种方法可以进一步远离0.7。如果我们认为测试分布实际上由许多聚类组成,我们可以尝试为每个聚类找到平均提示词。在聚类数量等于样本数量的极限情况下,我们就有完整的提示词预测。我们基本上是从两个极端解决方案(平均提示词 vs. 单个样本预测)来尝试完成这个任务,但实际上我们可以在这条最大分数 vs. 任务难度的权衡曲线上的任何点进行操作。

带着这个想法,我们在本地验证集的T5嵌入上拟合了一个12聚类的KMeans,并运行了相同的LBFGS优化。对于12个聚类,理论最大分数为:

[(0, 0.7262467741966248),
 (1, 0.7863955497741699),
 (2, 0.8009814620018005),
 (3, 0.7827126383781433),
 (4, 0.8333203792572021),
 (5, 0.7971279621124268),
 (6, 0.8103494048118591),
 (7, 0.7536653876304626),
 (8, 0.7482798099517822),
 (9, 0.7608581781387329),
 (10, 0.7638632655143738),
 (11, 0.7885770201683044)]

按每个聚类的样本数量加权的平均分数约为76 CV。当然,这种方法的问题在于,我们需要为测试样本分配正确的聚类,因为错误分配会导致很高的惩罚。

因此,我们训练了一个 MistralForSequenceClassification 来将每个(original_text, rewritten_text)对分类到12个聚类之一(我们使用基于真实 rewrite_promptKMeans 预测作为标签进行训练)。在推理时,我们会在完整提示词模型预测的 rewrite_prompt 上同时运行我们的分类器和 KMeans,只有在两者一致时才选择该聚类。否则,我们会保守地使用全局平均提示词模板。

附加挑战:上面的文本是由LLM从原始书面文本重写的,试着猜猜重写提示词。提示词不是 improve phrasing text lucrarea tone lucrarea rewrite this creatively formalize discours involving lucrarea anyone emulate lucrarea description send casual perspective information alter it lucrarea ss plotline speaker recommend doing if elegy tone lucrarea more com n paraphrase ss forward this st text redesign poem above etc possible llm clear lucrarea 🤣

```