返回列表

1st Place Detailed Solution

632. Eedi - Mining Misconceptions in Mathematics | eedi-mining-misconceptions-in-mathematics

开始: 2024-09-12 结束: 2024-12-12 学习效果预测 数据算法赛
第一名详细解决方案

第一名详细解决方案

作者: Raja Biswas (Grandmaster)
发布时间: 2024-12-14
竞赛: EEDI - Mining Misconceptions in Mathematics

我要感谢 Kaggle 和 EEDI 举办了这场有趣的竞赛!这很有挑战性,并提供了许多学习/应用新技术的机会。一如既往,我感谢 Kaggle 社区提供的创新思想和引人入胜的讨论。我很兴奋能分享我的详细解决方案,希望其他人会觉得它有用!

1. 任务

给定一道诊断性数学选择题(MCQ),以及正确答案和一个错误答案,任务是从 2500+ 个误解(misconceptions)池中推荐前 25 个误解(按与错误答案的亲和力排序)。

尽管标准的检索 - 重排序(retrieve-rerank)框架是一个自然的选择,但它带来了一些挑战:

  • 当前的 LLM 往往难以模仿 (1) 初学者 (Novice Learner),即生成源于特定误解的错误答案,或 (2) 专家导师 (Expert Tutor),即识别解释给定错误答案的误解。LLM 擅长解决数学问题,但不擅长反事实推理。
  • 误解池将紧密相关的概念性和计算性错误联系在一起,需要高度的辨别力才能精确地识别细微差别。这在竞赛概述中强调过:

使用预训练语言模型的初步努力并未成功,可能是由于问题中数学内容的复杂性。

  • 任务设置要求模型不仅在已知误解上表现良好,还要能泛化到新的误解。

2. 元思考 (Meta Thoughts)

在竞赛开始时,我对良好解决方案的重要性有以下假设:

  • 合成数据应该有助于分布外(out-of-distribution)泛化
  • 高质量合成数据生成应该是可行的,因为数学领域可以客观地验证和策划
  • 来自好老师(模型)的蒸馏应该非常强大,因为大型 LLM 的推理能力显著更强
  • 从顶级 LLM(如 Claude 3.5 Sonnet)蒸馏思维链(CoT)应该有助于解决困难的例子
  • 量化方法和校准数据集可能很重要。我们可能需要向量化模型添加恢复适配器(recovery adapters)以维持 Top 1 准确率。
  • 全量训练可能比 LoRA 更好

这些假设指导了我整个竞赛期间的实验设计,并帮助我坚持自己的想法,即使最初的尝试并不成功。

3. 流程 (Pipeline)

我实现了一个检索和重排序系统,涉及四个步骤:

  • 检索 (Retrieval):为每个 MCQ-错误答案组合识别顶级误解候选者。具体来说,我保留了前 32 个检索到的候选者,并根据动态阈值包含最多 32 个额外的候选者。动态阈值计算为 顶级候选相似度分数 - 常数。相似度分数高于动态阈值的候选者被保留。
  • 14B 重排序器 (Ranker):使用微调的 Qwen/Qwen2.5-14B 重排序器处理所有检索到的候选者,并选出前 8 名。
  • 32B 重排序器 (Ranker):使用微调的 Qwen/Qwen2.5-32B 重排序器处理前 8 名候选者,并将其缩小到前 5 名。
  • 72B 重排序器 (Ranker):使用微调的 Qwen/Qwen2.5-72B 重排序器最终确定前 5 名候选者的排名。

流程示意图及各阶段的排行榜分数如下所示:

推理流程示意图

最终的 25 个误解预测由以下部分组成:

  • 来自 72B 重排序器的第 1 至 5 名候选者
  • 来自 32B 重排序器的第 6 至 8 名候选者
  • 来自 14B 重排序器的第 9 至 25 名候选者。

4. 数据

检索器和重排序器模型的训练数据混合包括竞赛数据(1.8k 示例)和合成数据(10k 示例)。合成数据在提高原始性能和针对未见误解的泛化能力方面发挥了至关重要的作用。

4.1 合成数据

在理想情况下,合成 MCQ 应具有以下属性:

  • 指定的正确答案确实是正确答案(高准确性)。
  • 错误答案直接源于目标误解(高诊断能力)。
  • 在误解池(2.5k+)中的所有误解中,指出的误解应为给定的错误答案提供最精确的解释(高分辨率)。
  • 生成的 MCQ 应涵盖广泛的主题和结构(高多样性)。

我开始使用一个简单的提示生成诊断性 MCQ,指定了预期格式和感兴趣的误解。然而,这导致生成的数据集在不同程度上违反了几乎所有属性。这是意料之中的,因为:

  • 简单的提示只包含关于单个误解的信息。因此,数据生成 LLM 完全不知道误解池中紧密相关的误解。
  • 缺乏少样本(few shot)示例意味着缺乏对预期质量、难度级别、语气和语言的参考。
  • 即使是先进的 LLM 也难以有效地处理误解和反事实推理。
  • 未执行策划(curation)。

我尝试了几种策略来解决这些问题,并通过 分组合成数据生成 (grouped synthetic data generation) 方法和结合基于 LLM 的过滤,成功创建了高质量的合成数据集。具体来说,我首先利用验证数据上检索器/重排序器预测中误解的共现统计信息,创建了紧密相关误解的聚类。这个 Notebook 展示了聚类过程。以下是一个误解聚类的示例:

- 认为 x^2 - a^2x 是平方差的有效形式
- 当因式分解没有非变量项的二次方程时,尝试双括号因式分解
- 错误地因式分解二次方程
- 认为展开的二次方程中的常数来自括号中两个数字的相加
- 认为展开的二次方程中 x 的系数来自括号中两个数字的相乘
- 不认识平方差
- 没意识到二次方程必须是 ax^2+bx+c=0 的形式才能因式分解
- 不知道如何从代数项中识别公因式
- 认为因式分解的平方差方法也适用于平方和
- 因式分解时,认为可以为每一项选择不同的因式
- 认为可以通过将常数放在两个括号中而不进行开方来因式分解平方差

接下来,我使用 Claude 3.5 Sonnet 为每个聚类生成 5 个新的 MCQ,并使用包含聚类中误解的 4-5 个参考示例的优化提示。我使用了 Anthropic cookbook 中的 metaprompt 工具进行提示工程。以下是用于数据生成的最终提示:

你将生成诊断特定数学误解的多项选择题 (MCQ)。这是你应该关注的误解:

<misconceptions>
{cluster_misconceptions}
</misconceptions>

以下是展示如何有效诊断这些误解的参考 MCQ:

<reference_mcqs>
{reference_mcqs}
</reference_mcqs>

你的任务是生成 {num_mcqs} 个新的 MCQ,以诊断参考 MCQ 尚未涵盖的误解。

首先,仔细分析参考 MCQ:
1. 对于每个参考 MCQ,在你的 <analysis> 标签中识别:
   - 它针对哪个误解
   - 错误答案如何映射到特定误解
   - 是什么使问题在诊断误解方面有效
2. 注意风格、难度级别和语言的精确性

然后,在你的 <planning> 标签中:
- 列出哪些误解仍需涵盖
- 对于每个需要的误解,头脑风暴它通常出现的数学背景
- 设计误解自然导致特定错误答案的问题
- 记录如何制作新的 MCQ 以符合参考 MCQ 的风格、难度级别和语言精确性

最后,遵循以下重要指南生成新的 MCQ:
- 确保每个错误答案清晰地映射到正好一个误解
- 使用与参考 MCQ 风格匹配的精确数学语言
- 使问题具有足够的挑战性,学生必须展示真正的理解
- 确保错误答案是似是而非的,源于真正的误解,而不是粗心错误
- 使用误解列表中给出的确切措辞
- 注意误解之间的细微差别,并观察哪一个最适合给定的错误答案
- 尽可能保持结构名称和主题名称简短,隐藏误解的细节
- 问题的难度级别应高于参考 MCQ

4.1.1 策划 (Curation)

我使用 LLM-as-a-judge 来过滤掉低质量的合成 MCQ。我提示 GPT-4o 按以下方式对合成 MCQ 的质量进行评分:

你将分析错误答案在多大程度上反映了数学问题中的疑似误解。你的目标是确定误解与错误答案之间是否存在清晰、逻辑的连接。

以下是包含正确答案和错误答案的问题。疑似误解也已提供:
<problem>
{PROBLEM_DATA}
</problem>

首先,在你的 scratchpad 中分析问题:
<scratchpad>
1. 独立解决问题以验证正确答案
2. 检查持有疑似误解的人会如何处理问题
3. 追踪从误解到错误答案的逻辑路径
4. 识别此连接中的任何差距或不一致之处
</scratchpad>

然后使用以下格式提供你的评估:
<evaluation>
1. 简要解释误解如何导致错误答案
2. 根据以下标准评分 0-10:
   - 10: 完美对齐 - 错误答案是误解的直接结果
   - 8-9: 强对齐 - 从误解到答案的清晰逻辑路径
   - 5-7: 中等对齐 - 连接存在但有一些差距
   - 1-4: 弱对齐 - 连接不清晰或需要假设
   - 0: 无对齐 - 误解不能解释错误答案
</evaluation>

重要指南:
- 仅关注误解与错误答案之间的逻辑连接
- 不要推测其他可能的误解
- 具体说明误解如何导致错误
- 如果连接误解与答案需要任何假设,请标记并扣分
- 考虑持有此误解的学生是否一致地得出此错误答案

我选择 GPT-4o 作为法官是基于感觉测试,因为它对评分方案感觉更敏感,并且与 Claude 3.5 Sonnet 一样一致。

4.1.2 纳入额外的误解

在创建合成数据时,LLM 产生了许多(4k+)不属于主办方提供的误解池的误解。我决定保留外部误解以提高检索器和重排序器模型的泛化能力。

天真地将外部误解与现有误解合并会引入冲突和噪音,因为它们中的许多只是现有误解的改写。我采取了以下步骤来小心处理:

  • 尝试使用字符串规范化(小写、删除尾随标点符号等)与现有误解匹配
  • 尝试使用嵌入模型 (thenlper/gte-base) 的相似度分数与现有误解匹配,阈值非常高 (0.995)
  • 删除与任何现有误解具有相当高相似度(在 0.95 和 0.995 之间)的新误解
  • 保留其余的外部误解

4.1.3 数据集

最终数据集上传至此:Eedi - Mining Misconceptions in Mathematics。它包含:

  • 1.8k 竞赛 MCQ + 10.6k 生成的 MCQ
  • 4791 个误解

并遵循与原始竞赛数据集相同的格式。

参考:我强烈建议阅读 answer.ai 的合成数据生成帖子:How To Synthesize Your Data

4.2 思维链 (CoT) 生成

为了在我的解决方案流程中蒸馏先进 LLM 的推理能力,我首先创建了一个包含由 Claude 3.5 Sonnet 生成的思维过程的数据集。我向 Claude 提供了:(i) 问题陈述,(ii) 正确答案,(iii) 错误答案,(iv) 基本事实误解,以及 (v) 几个紧密相关的误解。利用这些信息,我提示 Claude 生成导致学生选择错误答案的可能思维过程。提示如下所示:

你将分析学生的错误答案,以识别导致其错误的具体推理缺陷。
你的目标是精确解释他们的误解如何导致他们得出错误答案。

以下是问题信息:
<problem_data>
# 问题:简化表达式:\[x \cdot y \cdot x\]
# 正确答案:\(x^2y\)
# 错误答案:\(x^2\)
# 主要误解:乘以时忽略没有显式系数的变量
</problem_data>

以下是相关但并不能像主要误解那样精确解释此特定错误的误解:
<related_misconceptions>
- 认为只有同类项可以相乘
- 未能合并同一变量的所有实例
- 错误地识别不完整的变量因子
- 不理解如何乘代数项
</related_misconceptions>

首先,仔细检查问题的所有组成部分:
1. 问题陈述和所问问题
2. 正确答案和解决方法
3. 学生的错误答案
4. 给出的主要误解
5. 应与主要误解区分开的相关误解

然后,重构学生可能的思维过程:
- 确定他们的推理偏离正确解决路径的确切点
- 注意他们误用了哪些具体的数学运算或概念
- 将他们的错误直接与陈述的主要误解联系起来
- 验证此解释比相关误解更适合该错误

在 <evaluation> 标签中写下你的分析,遵循以下结构:
- 首先显示正确的计算
- 显示演示错误的错误计算
- 解释学生推理中的具体缺陷
- 演示误解如何导致此特定错误
- 与相关误解区分开
- 将你的解释保持在 5-6 句清晰、不重复的句子中
- 仅关注产生此特定错误的推理

编写解释的指南:
- 不要重述问题或命名误解
- 精确涉及所涉及的数学概念
- 确切展示误解如何导致错误
- 与相关误解区分开
- 避免重复
- 专注于此特定错误

使用此数据集,我微调了三个 Qwen 2.5 系列模型 (Qwen/Qwen2.5-Math-7B, Qwen/Qwen2.5-14B, 和 Qwen/Qwen2.5-32B) 作为推理器。值得注意的是,我在微调时从输入中省略了真实和相关误解。在推理期间,这些模型将看到:(i) 问题陈述,(ii) 正确答案,和 (iii) 错误答案。然后它们将生成学生选择错误答案背后的可能思维过程。我在推理期间使用 vllm 进行 CoT 生成,参数为 temperature=0.7, top_p=0.8, repetition_penalty=1.0, max_tokens=256

CoT 数据集上传至此:Eedi - CoT Dataset

5. 训练 - 验证集划分

我最初基于 QuestionId 字段将竞赛数据集划分为 5 折。然而,这产生了过于乐观的估计,验证和排行榜分数之间存在很大差距。接下来,我尝试在 SubjectId 上使用 GroupKFold,但这导致过于悲观的估计。我最终确定了在 ConstructId 上使用 GroupKFold,这在 ~0.62 公共 LB 分数范围内产生了狭窄的验证/LB 差距和良好的相关性。

在开发期间,我总是在折 1-4 上训练模型,并在折 0 上验证。合成数据被标记为折 99 并包含在训练集中。最终模型使用开发阶段的相同超参数在整个数据集(全量拟合)上进行训练。

6. 检索器 (Retrievers)

我使用标准的 MultipleNegativesRankingLoss 来微调检索器,同时参考 FlagEmbedding 代码库来开发训练脚本。在此阶段,我跟踪了 map@25recall@32 指标,并优先优化 recall@32,因为召回率对整个流程性能更重要。

我微调了几个检索器,分数如下:

检索器分数

将硬负例(hard negatives)纳入训练批次并蒸馏重排序器分数一致地提高了 map@25 性能,但没有对 recall@32 产生积极影响。对于我的最终提交选择,我选择了高召回率的 Qwen/Qwen2.5-14B 编码器(模型 3),而不是最佳的 map@25 编码器(模型 4)。遵循相同的逻辑,我也排除了 BGE 编码器(模型 2)。值得注意的是,我最好的仅编码器提交(模型 1 + 模型 2 + 模型 4)在公共 LB 上得分为 0.524,在私有 LB 上得分为 0.475。

检索器使用 LoRA 微调,参数为:r=64, alpha=128, learning_rate_lora_a=1e-5, learning_rate_lora_b=5e-5, LoRA on all linear layers, batch_size=128, and epochs=12。提高召回率性能的几个关键因素是:

  • 将温度设置为 0.01,而不是 LLM 基编码器中通常使用的 0.02
  • 确保每个训练批次中每个误解只出现一次演示,因为多个演示会通过批次内负例引入标签噪音
  • 在策划步骤(4.1.1 节)之前,使用所有可用的合成数据预训练 Qwen/Qwen2.5-14B 编码器。

无效的方法:

  • 迭代硬负例挖掘
  • 通过跨设备负例增加批次大小
  • 自定义批处理策略,例如在同一批次中拥有来自同一 SubjectId 的 (query, misconception) 正例对
  • 我尝试将 LLM 检索器转换为双向编码器,类似于 nvidia/NV-Embed-v2 中使用的策略。

7. 重排序器 (Re-rankers)

Qwen 14B 重排序器承担了大部分繁重的工作,处理所有检索到的误解(32-64 个候选者)并识别前 8 个候选者。该模型使用点对点(pointwise)方法进行训练,它在上下文窗口中一次看到一个误解。模型输入结构如下:

点对点重排序器

Logits 是通过取 'Yes' 和 'No' 标记分数的差值计算的,如下所示:

outputs = self.model(input_ids=input_ids, attention_mask=attention_mask, output_hidden_states=True)
logits_yes = outputs.logits[:, -1, self.yes_loc]  # 最后一个位置的 Yes 标记 logit [bs]
logits_no = outputs.logits[:, -1, self.no_loc]  # 最后一个位置的 No 标记 logit [bs]
logits = logits_yes - logits_no  # [bs]

logits = logits.reshape(-1, self.group_size)
labels = labels.to(logits.device).reshape(-1) 
ce_loss = self.loss_fn(logits, labels)

每个批次包含给定问题 - 错误答案组合的 1 个正例和 batch_size-1 个负例误解。标签设置为正例误解的索引,并使用交叉熵损失进行训练。模型使用 LoRA 训练,参数为 r=64, alpha=128, LoRA on all linear layers。LLM (Qwen/Qwen2.5-14B) 的语言建模头 (lm_head) 在微调期间被冻结。

7.1 消融实验 (Ablations)

我发现以下 4 种策略对于提升重排序器性能及其泛化能力特别重要:

7.1.1 少样本示例 (Few Shot Examples)

为了保留和利用 LLM 的上下文学习能力,我选择在模型输入上下文中包含几个参考误解示例(0 到 2 个)(如上图所示)。并非所有训练示例都包含这些少样本演示。目标是鼓励模型在可用时使用参考示例,否则依赖其内部推理。在推理期间,我使用整个训练集中的 1 或 2 个示例作为演示。这种方法对 14B 重排序器的性能特别有效,将私有 LB 分数从 0.495 提高到 0.531 (+0.036)。此时,我仅使用竞赛数据进行训练。

7.1.2 蒸馏 / 伪标签 (Distillation / Pseudo Labelling)

接下来,我通过伪标签将合成示例(策划后)纳入训练流程。我首先仅使用竞赛数据集微调了两个点对点 72B 模型 (Qwen/Qwen2.5-Math-72BQwen/Qwen2.5-72B)。然后使用这些模型为合成示例生成伪标签。最后,我使用竞赛 + 伪标记数据微调下一代的 14B 重排序器。此策略将 14B 重排序器的私有 LB 分数从 0.531 提高到 0.575 (+0.044)

7.1.3 负例比例 (Negative Ratio)

接下来,我尝试增加每个正例的负例样本数量。在训练期间向重排序器展示大量负例有助于提高性能。对于每个正例,我将负例数量从之前的 16 个增加到 24 个。此时,我还添加了 2 倍数量的合成示例。这两个修改将 14B 重排序器的私有 LB 分数从 0.575 提高到 0.596 (+0.021)

7.1.4 思维链 (CoT)

我最后将微调的 CoT 推理器(4.2 节)添加到训练和推理流程中。在训练期间,来自微调的 Qwen/Qwen2.5-14B 推理器产生的 CoT 被选择性地添加到模型输入中。50% 的训练数据使用 CoT,50% 不使用。这鼓励模型在可用时使用外部推理,否则依赖其内部推理。CoT 将 14B 重排序器的私有 LB 分数从 0.596 提高到 0.615 (+0.019)

这些策略的影响总结如下表:

消融实验结果

微调期间的实时评估显示 14B 重排序器的性能持续提高。

实时评估

7.2 点对点重排序器 (Qwen/Qwen2.5-32B)

我以与 14B 重排序器完全相同的方式(使用不同的种子)微调了 Qwen/Qwen2.5-32B 模型。我最好的 32B 模型验证分数为 0.663,而 14B 重排序器为 0.646。在推理期间,32B 重排序器用于处理来自 14B 重排序器的前 8 个候选者,并将其缩小到前 5 个候选者。纳入 32B 重排序器将私有 LB 分数从 0.615 提高到 0.625 (+0.010)

7.3 列表式重排序器 (Qwen/Qwen2.5-72B)

前 5 个候选者最终使用微调的 Qwen/Qwen2.5-72B 模型以列表式(listwise)方式进行排名。在这里,模型一次看到所有前 5 个候选者。模型输入示例如下所示:

列表式模型输入

该模型比点对点重排序器拥有更多的信息,即:

  • 它在推理期间看到 3 个思维链 (CoT) - 分别来自 7B、14B 和 32B 推理器
  • 它看到来自训练集的最多 5 个参考示例 - 5 个候选误解中每个对应 1 个
  • 它在上下文窗口中一次看到所有候选者。

该模型在所有重排序器中具有最佳的 MAP@5(验证 MAP@5 为 0.661)。基于 LLM 的列表式重排序器通常会受到候选位置偏差的影响。缓解这种情况的一种方法是通过打乱候选误解的位置运行多次推理运行(类似测试时增强 (TTA) 的影响)。由于时间限制,我无法在推理期间运行 TTA。纳入 72B 重排序器将私有 LB 分数从 0.625 提高到 0.638 (+0.013)

7.4 量化 (Quantization)

我使用 AutoAWQ 对微调的重排序器进行了量化。我为每个模型使用特定任务的校准数据集来减轻性能下降。我用其中一个 32B 重排序器测试了校准数据集的影响。结果如下所示:

校准更新

7.5 无效的方法

同比赛其他方案