NLP(九十七)大模型数学解题能力的初步探索

本文将会介绍如何对大模型进行微调(SFT),使其具备数学解题能力,这是笔者在探索大模型的数学解题能力方向的初步探索,后续将持续跟进。

在文章NLP(九十六)使用LLaMA-Factory实现function calling中,笔者介绍了如何使用LLaMa-Factory微调框架,对Qwen1.5-4B模型进行微调,实现function calling功能,使得大模型具有工具调用能力。

对大模型微调使其具备调用调用能力,这是通过function calling的方式实现的,这也是说,我们事先需要提供对应工具API的调用函数及其函数参数。那么,有没有一种工具是大模型天然具备的呢?

当然有,这就是我们开发大模型时要用到的代码啊!代码(包括Python)就是一种最基本的工具。受此启发,我们可以对大模型进行微调,使其具备生成Python代码的能力,从而解决数学题目。

数据合成

对大模型微调,使其具备数学解题能力,高质量的数据集是最重要的保障之一。

业界、数学界关于大模型数学解题能力方面的研究,已经有不少研究成果和数据集。在LLaMa-Factory中,内置了两类数学方面的数据集,它们为BelleGroup/school_math_0.25MTIGER-Lab/MathInstruct,前者是包含约25万条由BELLE项目生成的中文数学题数据,包含解题过程,但题目偏简单,多为小学数学题;后者由13个具有中间原理的数学数据集编译而成,其中6个为新数据集,混合了思想链(CoT)和思想程序(PoT),确保覆盖了广泛的数学领域。

但是,这两个数据集并没有调用Python代码去解题,仅仅是利用了大模型本身的世界知识和推理能力(如COT)。

我们希望在用大模型解决数学题目时,能够按照我们读书时的过程,先给出思考过程,再调用合适的知识点(这里是Python)代码,最后给出结果。下面是一个数学题目的解答过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[
{
"from": "human",
"value": "计算 345 * 321 = ?"
},
{
"from": "gpt",
"value": "首先给出思考过程:这是一道简单的数学运算题目,考察的是两个整数的乘法。直接调用Python代码计算即可。"
},
{
"from": "function_call",
"value": "```python\ndef multiply(num1, num2):\n return num1 * num2\n\nnum1 = 345\nnum2 = 123\n\nprint(f'{num1} * {num2} = {multiply(num1, num2)}')\n```\n"
},
{
"from": "observation",
"value": "现在将上面的代码复制到Python环境中运行,运行以上代码,输出会是: ```345 * 123 = 42435```"
},
{
"from": "gpt",
"value": "因此,345 * 123 = 42435"
}
]

基于上述的这种格式,我们借助GPT-4-turbo模型进行数据合成,其中一个模板(即Prompt)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
现在,请你开始编写一个给初中生做的数学运算题目。
数学运算题目的出题方式要丰富多样,可以是加减乘除,也可以是分数、小数等,也可以出现复杂的运算,比如开方、对数、三角函数等。
在回答过程中,你可以调用Python代码,使用math或sympy模块,并给出Python代码的运行结果,要求输出格式与上述例子的输出相同,以```json开头,以```结尾,中间是JSON字符串,第一步一定要给出思考过程。

以下是一个示例的JSON格式的输出结果:

[
{
"from": "human",
"value": "计算 345 * 321 = ?"
},
{
"from": "gpt",
"value": "首先给出思考过程:这是一道简单的数学运算题目,考察的是两个整数的乘法。直接调用Python代码计算即可。"
},
{
"from": "function_call",
"value": "```python\ndef multiply(num1, num2):\n return num1 * num2\n\nnum1 = 345\nnum2 = 123\n\nprint(f'{num1} * {num2} = {multiply(num1, num2)}')\n```\n"
},
{
"from": "observation",
"value": "现在将上面的代码复制到Python环境中运行,运行以上代码,输出会是: ```345 * 123 = 42435```"
},
{
"from": "gpt",
"value": "因此,345 * 123 = 42435"
}
]

出题要求:
1. 题目要求随机生成,不能是固定的题目。
2. 题目要求有一定的难度,不能太简单。
3. 题目的描述要清晰明了,且表达形式要丰富多样,有时候可以用中文来描述。
4. 计算题中可以包含一些特殊的数学符号,比如分数、小数、开方、对数、三角函数等。

如何写好合适的Prompt,使得GPT-4-turbo能够遵循指令,给出高质量的数学解题过程,也是值得探索的,笔者这里只是给出了初步尝试的结果,这中间的过程还是有待提升的。

使用GPT-4-turbo模型生成数据,再经过大量时间的数据清洗与合成,最终我们得到547个样本(包含重复样本),它们都满足sharegpt格式,格式样例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[
{
"conversations": [
{
"from": "human",
"value": "题目:苹果的价格是每公斤3元,小明买了2.5公斤的苹果,他一共需要支付多少钱?"
},
{
"from": "gpt",
"value": "首先给出思考过程:这是一个简单的乘法问题,其中涉及到的是单价和数量的乘积计算。我们需要用苹果的单价乘以小明购买的苹果的重量来找出总金额。\n生成的Python代码如下:\n计算总金额的公式是: \\[ 总金额 = 单价 \\times 数量 \\] 在这个具体例子中,苹果的单价为3元每公斤,小明购买了2.5公斤。\n生成的Python代码如下:\n```python\ndef total_cost(price_per_kg, weight):\n return price_per_kg * weight\n\n# 苹果的单价(每公斤)\nprice_per_kg = 3\n\n# 小明购买的苹果重量(公斤)\nweight = 2.5\n\n# 计算总金额\ncost = total_cost(price_per_kg, weight)\nprint('小明需要支付的总金额为:', cost)\n```\n"
},
{
"from": "human",
"value": "现在将上面的代码复制到Python环境中运行,我们应得到小明需要支付的总金额。运行结果将告诉我们具体的金额数值。 运行以上代码,输出会是: ```小明需要支付的总金额为: 7.5```"
},
{
"from": "gpt",
"value": "因此,小明购买2.5公斤苹果,总共需要支付7.5元。"
}
]
}
...
]

这就是我们最终送入LLaMa-Factory的数据格式了。

模型微调

使用LLaMa-Factory微调框架对大模型进行SFT,笔者之前在很多文章中已经介绍多次了,并没有太多新意,这里也不再给出详细的微调过程。

我们使用Qwen/Qwen1.5-32B模型,对上述合成的少量数据集进行SFT,训练完毕后导出模型为Qwen1.5-32B-math,再使用下面的脚本启动训练后模型的推理服务:

1
CUDA_VISIBLE_DEVICES=0 python -m llmtuner.api.app --model_name_or_path /models/Qwen1.5-32B-math --template qwen

模型预测

接下来,就是快乐的模型预测时光了,此时,我们就能见证大模型在经过数学题数据集微调后,是否具备了数学解题能力。

在此之前,我们先看看Qwen/Qwen1.5-32B模型本身的数学解题能力。

  • 题目:小明有98元,他要买一本书和一支笔,已知书的价格是25元,笔的价格比书的价格多15元,请问小明还剩余多少元钱?
model_before_1.png
  • 题目:抛物线y=x^2﹣4x+3的顶点坐标为多少?
model_before_2.png
  • 题目:计算123456789与987654321的乘积
model_before_3.png

由此可见,模型在微调前已经具备一定的数学解题与分析能力,但无法调用Python代码,并且存在幻觉(第三题的结果是不正确的!),另外,模型还会生成额外的不相关文本,这与模型是生成模型而并非Chat模型有关,这点可使用Chat模型来解决。

让我们来看看微调后模型Qwen1.5-32B-math的表现吧!

这里先给出模型预测的Python代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# -*- coding: utf-8 -*-
import gradio as gr
import os
import re
import subprocess
from openai import OpenAI

os.environ["OPENAI_BASE_URL"] = "http://localhost:8000/v1"
os.environ["OPENAI_API_KEY"] = "0"
client = OpenAI()

def question_answer(query):
messages = []
messages.append({"role": "user", "content": f"题目:{query}"})
result = client.chat.completions.create(messages=messages,
model="Qwen1.5-32B-math",
temperature=0.0,
stream=True)
reply_message = ""
for chunk in result:
if hasattr(chunk, "choices") and chunk.choices[0].delta.content:
reply_message += chunk.choices[0].delta.content
yield reply_message

# find python code and execute the code
if '```python' in reply_message:
messages.append({"role": "assistant", "content": reply_message})
python_code_string = re.findall(r'```python\n(.*?)\n```', reply_message, re.S)[0]
python_file_path = 'temp.py'
with open(python_file_path, 'w') as f:
f.write(python_code_string)
python_code_execution = subprocess.run(['python3', python_file_path], stdout=subprocess.PIPE).stdout.decode('utf-8')
os.remove(python_file_path)
code_reply = f"\n运行以上代码,输出会是: ```{python_code_execution.strip()}```\n"
reply_message += code_reply
yield reply_message
messages.append({"role": "user", "content": code_reply})
result = client.chat.completions.create(messages=messages,
model="Qwen1.5-32B-math",
temperature=0.0,
stream=True)

for chunk in result:
if hasattr(chunk, "choices") and chunk.choices[0].delta.content:
reply_message += chunk.choices[0].delta.content
yield reply_message


demo = gr.Interface(
fn=question_answer,
inputs=gr.Textbox(lines=3, placeholder="题目", label="数学题目"),
outputs=gr.Markdown(),
)

demo.launch(server_name="0.0.0.0", server_port=8001, share=True)

在这里,我们将生成后的Python代码存入temp.py脚本中,并使用subprocess模块运行该脚本并获取Python代码的执行结果,然后将其加工成prompt",输出会是: {python_code_execution.strip()}"送入大模型中继续回答。

我们来看看模型微调后的结果(下面会给出几个表现好的case):

首先是上面展示过的三道题目。

  • 题目1:小明有98元,他要买一本书和一支笔,已知书的价格是25元,笔的价格比书的价格多15元,请问小明还剩余多少元钱?
model_after_1.png
  • 题目2:抛物线y=x^2﹣4x+3的顶点坐标为多少?
model_after_2.png
  • 题目3:计算123456789与987654321的乘积
model_after_3.png

接着是更多题目的展示。

  • 题目4:若(ax-b)(3x+4)=bx^2+cx+72,则a+b+c的值为多少?
model_after_4.png
  • 题目5:123456789 + 987654321 = ?
model_after_5.png
  • 题目6:已知ΔABC为正三角形,则tan(A+)的值等于多少?
model_after_6.png
  • 题目7:各项均为正数的等比数列{a_{n}}的前n项和为S_{n},已知S_{3}=10, S_{6}=30,则S_{12}等于多少?
model_after_7.png
  • 题目8:复数 3+4i 的模是多少?
model_after_8.png
  • 题目9:(1+2x)^6 的展开式中 x^3 项的系数为多少?
model_after_9.png
  • 题目10:学校图书馆有故事书、科技书和连环画共1200本,其中故事书占60%,科技书和连环画的数量比是2:3,图书馆有多少本连环画?
model_after_10.png

接着,我们来看下最开始提到的数据题数据集BelleGroup/school_math_0.25MTIGER-Lab/MathInstruct中的两道题目。

  • 题目11:某天,小明去菜市场买了5个苹果和3个梨,花了20元。第二天,他又去同一个菜市场买了3个苹果和4个梨,花了15元。问:小明每个苹果和每个梨的价格是多少元?
model_after_11.png
  • 题目12:Suppose you have a system of linear equations: 2x + 3y + z = 7 x + 2y + 4z = 12 3x + y + 2z = 8 Using matrix methods, find the values of x, y, and z that satisfy the system of equations.
model_after_12.png

最后,我们来看看大学数学中的微积分方面的数学题目的表现。(题目来自网站:https://univs-news-1256833609.cos.ap-beijing.myqcloud.com/123/upload/resources/file/7526039.pdf 中的 第七题 )

model_after_13.png

再使用定积分计算器(https://zs.symbolab.com/solver/definite-integral-calculator)进行验证,答案正确。

总结

ok,经过上述漫长的模型预测的测试,我们可以发现,经过SFT后的大模型遵循了指令,并且按照我们的要求给出了思考过程,分析过程,调用Python代码,执行Python代码,最后给出答案。上述的测试的答案(除了精度上的偏差)都是正确的,这样的表现无疑是让人惊喜的。

大模型之所以有这样的数学解题能力,个人理解是:

大模型本身就具有一定的数学题目分析能力,借助Python代码(实际上就是code intepreter)能力,可以得到正确结果,这样既避免了幻觉,又能借助Python中的数学相关模块的帮助,因此,才使得它拥有了数学解题能力。

当然,上面的模型预测题目只是给出了一些好的测试样例,实际上,现在微调后的大模型在数学解题能力方面还有很大的进步空间。

基于笔者这段时间的尝试和思考,关于大模型数学解题能力方面的提升如下:

  • 高质量数学题的数据集获取
  • 更大规模、更高质量、形式更丰富的数学题的数据合成
  • 数据集的清洗,包括公式整理、去重等
  • 加入 高等数学题、与其它专业学科融合的题目等方面的数据集
  • 多模态数据集的获取与合成,使得大模型能结合图片进行解题
  • 多次思考过程,类似于COT,现在的方案只有一次思考,生成一次代码
  • 可靠性:生成的Python代码更可靠,现在生成的Python代码存在多种问题,如运行报错,无法执行,进入死循环等等
  • 准确性:Python代码运行后的数字精度问题,是否可以用分数或根式等其它形式表达,现在的执行结果有时候返回小数,与正确答案存在精度偏差,其实返回分数或根式更为合理(比如上面的题目11、12等)
  • 稳定性:大模型的生成文本或代码不稳定,变动较大,导致答案有时正确,有时不正确
  • 更优雅地数学解题过程与代码生成,有时候一道题目有更好的解法(比如上面的题目7)
  • 其它...

上述的问题,每一个都很有难度,希望在后面的尝试与探索能有所突破~

本项目已开源至Github,网址为:https://github.com/percent4/llm_math_solver ,后续将持续更新~

参考文献

  1. NLP(九十六)使用LLaMA-Factory实现function calling
  2. 上交开源MathPile专业数学语料库,95亿tokens,含可商用版:https://mp.weixin.qq.com/s/Qe_wMMsvdsNSJGv_oI24fw
  3. BelleGroup/school_math_0.25M: https://huggingface.co/datasets/BelleGroup/school_math_0.25M
  4. TIGER-Lab/MathInstruct: https://huggingface.co/datasets/TIGER-Lab/MathInstruct?row=0
  5. 单卡 3 小时训练专属大模型 Agent:基于 LLaMA Factory 实战: https://zhuanlan.zhihu.com/p/678989191
  6. 题海网: http://www.7249.cn/

欢迎关注我的公众号NLP奇幻之旅,原创技术文章第一时间推送。

欢迎关注我的知识星球“自然语言处理奇幻之旅”,笔者正在努力构建自己的技术社区。


NLP(九十七)大模型数学解题能力的初步探索
https://percent4.github.io/NLP(九十七)大模型数学解题能力的初步探索/
作者
Jclian91
发布于
2024年5月3日
许可协议