613. AI Mathematical Olympiad - Progress Prize 1 | ai-mathematical-olympiad-prize
首先,我们要感谢 Kaggle 和 XTX Markets 为我们带来了这场激动人心的比赛。通过这次比赛,我们学到了很多。我们也要感谢每一位团队成员的辛勤工作。他们真的很棒,提出了许多优秀的想法并展现了出色的工程技能。
我们的解决方案基于 AbdurRafae 的伟大工作:Improved Code Interpretation (kaggle.com)。他获得的早期分享奖是实至名归的。同样,Anren 对此代码的重组也为我们提供了巨大的帮助。
下面简要介绍我们解决方案的整体流程和一些独特的技巧。我们的解决方案代码在此处:
我们使用了这个 71k 的数据集:AIMO-24: Processor (Art Of Problem Solving) (kaggle.com)。从所有 AMC_12A 问题中,我们选择了最新的 50 个问题用于本地验证(排除了文本中包含 [asy] 字符串的问题,稍后将解释原因)。这与公共 leaderboard 显示出良好的相关性。
我们使用了 deepseek-math-7b-rl,参数设置为:temperature 0.9, top_p 1.0, max tokens 2048。配合代码工具,该模型在 MATH 基准测试上可达到 58.8% 的成绩。
我们在两个 T4 GPU 上并行部署和运行此模型,以最大化自一致性从而提高最终预测准确率。双 GPU 推理将每个问题的总重复次数从 21 次增加到 30-40 次。
TimeManager 类
我们创建了一个 TimeManager 类来管理时间分配。它跟踪每个问题花费的时间,并动态调整后续问题的尝试次数,以确保在给定时间内解决尽可能多的问题。
class TimeManager:
def __init__(self, total_problems, total_time, default_attempts):
self.total_problems = total_problems # 问题总数
self.total_time = total_time # 可用总时间
self.default_attempts = default_attempts # 默认尝试次数
self.time_spent = 0 # 目前已花费时间
self.current_problem = 0 # 当前问题索引
self.last_problem_time = 0 # 上一个问题花费的时间
def update_time_spent(self, time_for_problem):
self.time_spent += time_for_problem # 将问题花费时间加到总花费时间中
self.current_problem += 1 # 增加问题索引
self.last_problem_time = time_for_problem # 记录当前问题花费的时间
def get_next_attempts(self):
if self.current_problem >= self.total_problems:
return 0
remaining_time = self.total_time - self.time_spent # 剩余时间
remaining_problems = self.total_problems - self.current_problem # 剩余问题数
average_time_per_problem = remaining_time / remaining_problems # 平均每个问题的时间
if self.current_problem == 0 or self.time_spent / self.current_problem <= average_time_per_problem:
next_attempts = self.default_attempts
else:
next_attempts = max(1, int(self.default_attempts * (average_time_per_problem / (self.time_spent / self.current_problem)) * 0.8))
return next_attempts
def print_status(self):
remaining_time = self.total_time - self.time_spent # 剩余时间
print(f"Time spent: {self.time_spent} seconds") # 打印总花费时间
print(f"Remaining time: {remaining_time} seconds") # 打印剩余时间
print(f"Current problem: {self.current_problem}/{self.total_problems}") # 打印当前问题进度
print(f"Time spent on current problem: {self.last_problem_time} seconds") # 打印当前问题花费时间
减少困难问题的尝试次数
我们手动减少了我们认为模型无法解决的问题的尝试次数,为剩余问题提供更多尝试机会。具体来说,对于包含 [asy] 符号的问题,我们只允许 3 次重复。该符号表示包含图形的几何问题,在 LaTeX 中被 [asy] 包围。在我们的本地测试中,模型很少能正确回答这些问题,即使回答了,我们也怀疑该问题可能存在于模型的训练集中。
减少双 GPU 运行之间的等待时间
当使用 ThreadPoolExecutor 进行并行推理时,一个 GPU 通常完成得更快。如果我们看到第一个完成的 GPU 已经有足够的候选答案,我们将提前终止较慢 GPU 的尝试。然后,合并两个 GPU 生成的候选答案。这确保了更高效地使用双 GPU,并增加了所有问题的平均重复次数。
DeepSeekMath 初始化自 DeepSeek-Coder-v1.5 7B,它可以通过编写程序有效地解决和证明数学问题。我们遵循 Anren 的方法,使用两个提示,让模型使用 COT(思维链)和编写代码的方法解决问题。
我们做了一些改进:
限制模型的代码修改机会
我们将每次重复的模型代码修改机会限制为仅 3 次,因为输入文本过长会显著降低 LLM 的输出质量。此功能由参数 while_limit 控制,我们将其设置为 6(因为每个代码输入和输出处理需要两个 while 循环)。
候选答案的加权计数排序
我们发现模型在回答错误时倾向于输出小整数,如 0, 1, 2, 3, 4, 5。我们将这些数字的权重降低到 0.25,而其他数字保留正常的权重 1。
修复一些不正确行为
例如,当代码输出不是有效答案(例如错误、小数、复数)时,原始方法仍尝试解析文本答案,这几乎总是错误的。在这种情况下,我们跳过记录文本答案。
我们的日志记录了每次实验的详细数据,包括候选答案、程序代码、代码准确率、文本准确率等。这些详细日志极大地促进了我们的快速实验和结果观察。我们的本地实验得分约为 21 分,pass1@ 达到 32 分,pass2@ 达到 23 分。
当然,比赛中可能需要一些运气。我们的解决方案在公共 leaderboard 上,使用不同的种子,可能会导致 3 分的分数差异。
详细本地日志(参数略有不同)
