목차
개요
이전 Qwen2.5에 적용된 TIR(Tool-Integrated Reasoning)에 대한 내용을 설명하다보니, 내용이 너무 길어졌다. 정작, Qwen2.5 모델에 대한 내용은 설명도 못하고 글을 마무리 지어서, 이번 글에는 문제를 해결할 때 Qwen2.5-Math Instruction 모델에 대한 소개와, 모델을 사용한 방법에 대해서 적어보려고 한다.
Qwen2.5-Math
본 내용은 [Qwen2.5-Math 아티클, Qwen Blog] 글의 내용을 기반으로 설명한다. 이전 글에, Qwen2.5-Math 모델의 핵심은 TIR 방법론을 적용하여, 학습을 진행하여 성능을 크게 끌어올렸다고 설명했다.
따라서, 이전 세대의 모델인 Qwen2-Math 모델은 CoT(Chain-of-Thought)만 사용 가능한것에 반해, 이번 Qwen2.5-Math 모델은 CoT와 TIR을 모두 지원한다.
Qwen2.5-Math : Base Models Training

Qwen2.5-Math 모델은 Qwen2-Math 베이스 모델을 학습한 후에, 주요 방법 3가지를 통해 업그레이드를 진행했다.
- Qwen2-Math-72B-Instruct 모델로 고품질의 수학 사전 학습 데이터를 생성
- 수차례 웹소스, 서적, 코드에서 고품질의 수학 데이터를 더 수집
- Qwen2.5의 Base Model을 활용해서, 더욱 강력한 언어 이해능력, 코드 생성, 텍스트 추론 능력을 높임
마지막으로, Qwen2.5-Math 모델의 Pre Training을 위해, 'Qwen Math Corpus V2'를 구성하여, 컨텍스트 길이를 4K로 유지했다. 'Math Corpus V1'이 700B tokens인것에 비해, 1T tokens 이상으로 구성되어, 이는 기존 대비 훨씬 증가한 수치이다.
아래는 영어와 중국어의 유명한 수학 벤치마크들의 결과이다.

기존 Qwen2-Math의 모델 대비, 동일 사이즈의 각 모델이 더 상당히 좋은 결과를 보인 것을 알 수 있다.
그럼에도 호기심이 드는 부분이 몇가지 있는데,
- GSM8K데이터셋에서는 7B모델이 72B모델보다 좋은 결과를 냈다. 이전 모델에서의 둘 사이의 차이가 상당한데, 신기할정도로 10배나 차이나는 파라미터 수로 어떻게 이런 결과가 나왔는지 궁금하다.
- MMLU STEM데이터셋에서는 72B을 제외하고는, Math 모델의 세대간의 차이(50.4 vs. 51.3 / 65.7 vs. 67.8)가 크지않다. 더 나아가선, 이전 세대의 General Model과도 큰 차이(44.8 vs. 51.3 / 67.6 vs. 67.8 / 79.9 vs. 82.8)가 나지않아보인다. MMLU는 다지선다 형식으로, STEM은 과학,기술, 공학, 수학 분야의 관한 내용이다. 그럼, 수학을 제외한 내용도 들어있어서, 파라미터 수가 적은 모델에선 능력의 한계가 있는 것일까?
벤치마크는 이 정도로 정리하고, 이제 Instruct Tuning 모델의 학습과정을 확인하자.
Qwen2.5-Math-Instruct : Instruction Tuning
Qwen2-Math-Instruct 모델 학습과정과 유사하게, Qwen2.5-Math-72B를 기반으로, 수학에 특화된 Reward Model(RM)을 학습시킨다. 이 RM을 Rejection Sampling을 통한, SFT데이터를 구성하고, 또한 SFT 후 GRPO(Group Relative Policy Optimization)을 사용한 강화 학습에도 사용한다. Qwen2-Math-Instruct 모델과 다르게, Post-Training 과정에 중국어와 영어로 된 TIR데이터와 SFT데이터를 추가로 도입했다.
아래는 5-shot설정의 벤치마크 데이터셋(MMLU STEM, GaoKao의 다지선다문제, CN Middle School24)를 제외한, 벤치마크 데이터셋들의 0-shot Greedy, Maj@8, RM@8 성능이다.

큰 특징은 TIR의 성능이 CoT의 성능보다 다 높게 나온다는 것이다. 이 것이 Qwen2.5-Math 모델의 가장 큰 발전이지 않을까싶다.
이제 가장 주의 깊게 봐야하는 벤치마크인 AIME24, AMC23 데이터셋이다. 이 데이터셋들이 지금 하고있는 AIMO2와 가장 유사한 성적을 보이지 않을까싶다.

AIME24 문제에서는 Qwen2.5-Math-72B-Instruct 모델은 CoT Greedy 디코딩으로 9/30 의 점수를 달성했다. 이는 Closed모델보다도 뛰어난 수치고, 다른 어떤 모델들과 비교해도 가장 좋은 성적이다. 또한 RM@64, RM@256으로 생성한다면, 13개까지 늘어난다.
TIR로 넘어가면 훨씬 더 많은 문제를 맞출 수 있게되는데, 신기한건 여기서도 Qwen2.5-Math-7B-Instruct모델의 RM@64방식으로 생성한 것이 21/30으로, 72B모델보다 더 좋고, 가장 높은 결과를 보여준다.
Qwen2.5에 대한 정리는 여기까지 하고, 이제 실제 적용으로 넘어가보자.
Generation Setting
vLLM
기본적으로 Model의 Generation에는 vLLM을 사용한다. 런타임의 5시간의 제한이 있기 때문에, 공개된 테스트셋 50문제에 대해서 시간 내에 효율적으로 생성해야하기 때문이다. 모델로딩 시간을 고려해도, 한 문제당 평균적으로 5~6분이내에 생성해야하는 셈이다.
또한, API를 활용해서 제출하기 때문에, Reference 10문제에 대해서 생성하는 시간을 보면 대충 생성 시간이 짐작할 수 있다.
from vllm import LLM, SamplingParams
llm = LLM(
[MODEL_PATH],
dtype="half",
max_num_seqs=8,
max_model_len=4096,
tensor_parallel_size=1,
gpu_memory_utilization=0.8
)
sampling_params = SamplingParams(
temperature=0.7,
min_p=0.01,
top_p=0.8,
max_tokens=2400,
)
vLLM의 사용법은 한번 정리할 필요가 있지만, 일단은 여기선 사용한 주요한 파라미터들만 위에 적어두었다. 간단하게 보면, LLM
은 모델에 해당이 되고, SamplingParams
는 Generation Config와 같은 역할을 한다. 위에 적힌 SamplingParams은 Qwen의 Evaluation과 동일하게 설정했다.
LLM의 파라미터의 의미부터 보면,[EngineArgs]
- [MODEL_PATH] : 모델 경로(Transformers, AWQ, GGUF 등 거의 모든 모델 형태를 다 지원한다.)
- dtype : 데이터 타입
- max_num_seqs : 배치처리할 최대 시퀀스의 수
- max_model_len : 모델 지원 최대 컨텍스트 길이
- tensor_parallel_size : 병렬로 처리할 GPU의 수
- gpu_memory_utilization : GPU에 할당할 메모리 비율
사용할 SamplingParams는 다음과 같다.[SamplingParams]
- temperature : 생성에 사용할 temperature
- min_p : min-p
- top_p : Top-p(Nucleus) 샘플링에 사용할, Top-p 값
- max_tokens : 생성할 최대 토큰의 수
Qwen2.5모델의 TIR, CoT
Qwen2.5모델은 CoT와 TIR을 둘 다 지원하기 때문에, 두 생성 방법에 대한 System prompt가 다르다.
# CoT
messages = [
{"role": "system", "content": "Please reason step by step, and put your final answer within \\boxed{}."},
{"role": "user", "content": prompt}
]
# TIR
messages = [
{"role": "system", "content": "Please integrate natural language reasoning with programs to solve the problem above, and put your final answer within \\boxed{}."},
{"role": "user", "content": prompt}
]
따라서, System Prompt를 변경하여, CoT로 생성할지, TIR로 생성할지 결정해야한다.
Python코드의 실행
TIR로 생성한 결과에는 Python 코드가 포함되기 때문에, 코드부분을 파싱해서 실행해야 한다.
Python 코드는 '''python
내부에 생성되기 때문에, 해당 부분 사이의 텍스트를 파싱하면 된다. 또한, 파싱된 Python 코드를 실행하는 Python 실행 모듈이 필요하다.
Python 실행 모듈은 주어진 Python코드로 임시 파일을 만들어서, subprocess로 실행하여, 리턴된 결과를 이용한다.
import os
import tempfile
import subprocess
class PythonREPL:
def __init__(self, timeout=5):
self.timeout = timeout
def __call__(self, query):
with tempfile.TemporaryDirectory() as temp_dir:
temp_file_path = os.path.join(temp_dir, "tmp.py")
with open(temp_file_path, "w", encoding="utf-8") as f:
f.write(query)
try:
result = subprocess.run(
["python3", temp_file_path],
capture_output=True,
check=False,
text=True,
timeout=self.timeout,
)
except subprocess.TimeoutExpired:
return False, f"Execution timed out after {self.timeout} seconds."
stdout = result.stdout.strip()
stderr = result.stderr.strip()
if result.returncode == 0:
return True, stdout
else:
# Process the error message to remove the temporary file path
# This makes the error message cleaner and more user-friendly
error_lines = stderr.split("\n")
cleaned_errors = []
for line in error_lines:
if temp_file_path in line:
# Remove the path from the error line
line = line.replace(temp_file_path, "<temporary_file>")
cleaned_errors.append(line)
cleaned_error_msg = "\n".join(cleaned_errors)
# Include stdout in the error case
combined_output = f"{stdout}\n{cleaned_error_msg}" if stdout else cleaned_error_msg
return False, combined_output
문제풀이 결과
각 프롬프팅의 전략(CoT, TIR, CoT+TIR)을 시험하기 위해, Reference 10문제에 대해서 성능 평가를 간단하게 진행해봤다.
Qwen2.5-Math-1.5B-Instruct
CoT - maj@64
64개 생성결과 중 majority를 정답으로, 1문제 맞췄다(10%).
맞춘 문제는 피보나치 수열에 대한 문제였다.
TIR - maj@64, call 1
64개의 생성결과 중 majority를 정답으로, 0문제 맞췄다(0%).
TIR - maj@64, call 4
TIR로 생성된, 코드 결과를 다시 검증하는 지시문을 추가해서, 여러번 코드의 결과에 대한 검증을 수행한다.
64개의 생성결과 중 majority를 정답으로, 0문제 맞췄다(0%).
CoT(32) + TIR(32) - maj@64, call 1
CoT 32개, TIR 32개로 생성하고, majority를 정답으로, 1문제 맞췄다.(10%)
맞춘 문제는 역시.. 피보나치 수열에 대한 문제였다.
CoT(32) + TIR(32) - maj@64, call 4
CoT 32개, TIR 32개로 생성하고, 코드의 결과를 다시 검증하는 지시문을 추가하여 다시 생성한다.
majority를 정답으로, 0문제 맞췄다.(0%)
역시, 1.5B 모델은 결과가 좋지않다. AIME 문제와 비교해도, 이 대회의 문제는 새롭게, 더 어렵게 문제를 출제했다고 하니,, 좋지 않은 결과가 예상됐다. 또한, TIR의 성능이 더 좋을 것으로 예상했지만, 너무 사이즈가 작은 모델이어서 그런지 성능이 더 안좋거나, 떨어졌다.
Qwen2.5-Math-7B-Instruct
CoT - maj@64
64개 생성결과 중 majority를 정답으로, 1문제 맞췄다(10%).
맞춘 문제는 동일하게, 피보나치 수열에 관한 문제였다.
TIR - maj@64, call 1
64개의 생성결과 중 majority를 정답으로, 0문제 맞췄다(0%).
맞춘 문제는 동일하게, 피보나치 수열에 관한 문제였다.
TIR - maj@64, call 4
TIR로 생성된, 코드 결과를 다시 검증하는 지시문을 추가해서, 여러번 코드의 결과에 대한 검증을 수행한다.
64개의 생성결과 중 majority를 정답으로, 0문제 맞췄다(0%).
CoT(32) + TIR(32) - maj@64, call 1
CoT 32개, TIR 32개로 생성하고, majority를 정답으로, 2문제 맞췄다.(20%)
하나는 역시 피보나치와, 삼각형 외심과 빗변의 길이에 대한 문제이다.
CoT(32) + TIR(32) - maj@64, call 4
CoT 32개, TIR 32개로 생성하고, 코드의 결과를 다시 검증하는 지시문을 추가하여 다시 생성한다.
majority를 정답으로, 피보나치 1문제 맞췄다.(10%)
Qwen2.5-Math-Instruct | CoT, maj@64 | TIR, maj@64, call 1 | TIR, maj@64, call 4 | CoT(32) + TIR(32), maj@64, call 1 | CoT(32) + TIR(32), maj@64, call 4 |
1.5B | 1/10 | 0/10 | 0/10 | 1/10 | 0/10 |
7B | 1/10 | 0/10 | 0/10 | 2/10 | 1/10 |
결과를 보면 알겠지만, 일관적으로 좋은 성능을 내지는 못한다. 생성된 답을 확인해봐도, 다양한 답이 나온다. 샘플링 파라미터를 조절해서 Greedy 하게 생성할 수도 있겠지만, Qwen의 평가 과정에서 사용한 값과 동일하게 다양하게 생성해서, 그 중 majority를 사용하는 방식으로 평가했기 때문에 일단은 이 방법을 따랐다.
생각을 덧붙이자면, 어려운 문제에서는 생성된 결과가 당연하게 일관적일 수 없는 것 같다. 시험 문제로 생각하면, 학생들이 문제를 풀 때, 어려운 문제가 오답률이 높고, 다양한 접근법을 생각하는 것처럼 모델도 그런 결과를 내는 것 같다.
또한, 쉬운 문제에 대한 접근법은 한정적이고 정말 이상한 답을 하지않는 이상 간단한 절차로 해결할 수 있는 것이다.
이는 이후 시도한 가장 좋은 성능을 냈던 모델인 Qwen/QwQ-32B-Preview
모델에서도 비슷한 결과가 나왔는데, 그건 새로운 글에서 실험했던 내용을 기록해보려 한다.
잠정적인 결론
추론 과정은 CoT, TIR 모두 \boxed{}
가 나타나면, 답을 파싱하고 답이 나타나지 않는 것에 대해서, Python 코드를 실행한 결과를 이용한다.
TIR로 생성된 결과는 아래와 같은 형태로 나오기에, 코드 실행의 결과인 output을 이용할 수도 있고, boxed의 결과를 이용해도 된다. 둘의 값이 대부분은 같게 생성되지만, 사이즈가 작은 1.5B의 경우 종종 같지 않게 나올 때도 있어서, TIR코드는 Python코드를 실행한 결과를 이용하도록 실험을 해볼 필요가 있을 것 같다. 추가적으로, 문제는 생성한 코드의 출력이 딱 정답만 정확하게 나오는게 아니라서, 후처리 하는 과정도 포함되어야할 것 같다.
```python
...
```
```output
...
```
...
\boxed{}
https://github.com/dbsrlskfdk/Kaggle/blob/AIMO2/AIMO2/notebooks/Version1-Qwen2.5_TIR.ipynb
Kaggle/AIMO2/notebooks/Version1-Qwen2.5_TIR.ipynb at AIMO2 · dbsrlskfdk/Kaggle
Contribute to dbsrlskfdk/Kaggle development by creating an account on GitHub.
github.com