1 BaseLine分析
baseline的大致逻辑如下:
- 处理数据:从test中提取问题并组成prompt作为大模型的输入
- 调用模型:重复调用
qwen1.5-1.8b-chat
模型API进行推理,并提取答案 - 处理答案:过滤掉没有完整答案的问题,将有完整答案的问题去重并排序。查找缺失的问题并从原始数据中补充,默认答案为’A’。
- 输出结果:将最终结果写入输出文件。
1.1 关键函数分析
1.1.1 def call_qwen_api(MODEL_NAME, query)
调用Qwen模型的API并返回推理结果。如果请求成功,返回推理结果的内容;否则,打印错误信息并抛出异常。
def call_qwen_api(MODEL_NAME, query):
messages = [
{'role': 'user', 'content': query}]
response = dashscope.Generation.call(
MODEL_NAME,
messages=messages,
result_format='message', # set the result is message format.
)
if response.status_code == HTTPStatus.OK:
return response['output']['choices'][0]['message']['content']
else:
print('Request id: %s, Status code: %s, error code: %s, error message: %s' % (
response.request_id, response.status_code,
response.code, response.message
))
raise Exception()
1.1.2 def api_retry(MODEL_NAME, query)
带有重试机制的API调用函数。该函数最多重试5次,每次失败后等待60秒。如果所有重试均失败,记录错误日志并抛出异常。
def api_retry(MODEL_NAME, query):
max_retries = 5
retry_delay = 60 # in seconds
attempts = 0
while attempts < max_retries:
try:
return call_qwen_api(MODEL_NAME, query)
except Exception as e:
attempts += 1
if attempts < max_retries:
logger.warning(f"Attempt {attempts} failed for text: {query}. Retrying in {retry_delay} seconds...")
time.sleep(retry_delay)
else:
logger.error(f"All {max_retries} attempts failed for text: {query}. Error: {e}")
raise
1.1.3 def get_prompt(problem, question, options)
根据给定的题目、问题和选项生成用于模型推理的Prompt文本。后期可以尝试修改prompt以提高分数
def get_prompt(problem, question, options):
options = '\n'.join(f"{'ABCDEFG'[i]}. {o}" for i, o in enumerate(options))
prompt = f"""你是一个逻辑推理专家,擅长解决逻辑推理问题。以下是一个逻辑推理的题目,形式为单项选择题。所有的问题都是(close-world assumption)闭世界假设,即未观测事实都为假。请逐步分析问题并在最后一行输出答案,最后一行的格式为"答案是:A"。题目如下:
### 题目:
{problem}
### 问题:
{question}
{options}
"""
return prompt
1.1.4 def extract(input_text)
从模型的推理结果中提取出答案。使用正则表达式查找格式为“答案是:X”的答案。
def extract(input_text):
ans_pattern = re.compile(r"答案是:(.)", re.S)
problems = ans_pattern.findall(input_text)
if(problems == ''):
return 'A'
return problems[0]
1.1.5 def process_datas(datas,MODEL_NAME)
使用多线程并发处理数据集中的所有问题。对于每个问题,生成Prompt并调用API进行推理,提取答案后存储在结果列表中。
def process_datas(datas,MODEL_NAME):
results = []
with ThreadPoolExecutor(max_workers=16) as executor:
future_data = {}
lasttask = ''
lastmark = 0
lens = 0
for data in tqdm(datas, desc="Submitting tasks", total=len(datas)):
problem = data['problem']
for id,question in enumerate(data['questions']):
prompt = get_prompt(problem, question['question'], question['options'])
future = executor.submit(api_retry, MODEL_NAME, prompt)
future_data[future] = (data,id)
time.sleep(0.6) # 控制每0.5秒提交一个任务
lens += 1
for future in tqdm(as_completed(future_data), total=lens, desc="Processing tasks"):
data = future_data[future][0]
problem_id = future_data[future][1]
try:
res = future.result()
extract_response = extract(res)
data['questions'][problem_id]['answer'] = extract_response
results.append(data)
except Exception as e:
logger.error(f"Failed to process text: {data}. Error: {e}")
return results
1.2 BaseLine结果
baseline 整体跑下来需要30分钟左右,由于是调用的 1.8b 的模型且没有进行微调,所以最终评分只有0.3494。
2 尝试微调大模型
2.1 Unsloth
Unsolth是一个免费开源的大模型微调工具。Unsloth能使 Llama-3、Mistral、Phi-3 和 Gemma 等大型语言模型的微调
速度提高 2 倍
,内存使用量减少 70%
,并且准确度不会降低! 官方Github,官网
目前支持Llama3.1、Mistral NeMo、Qwen2等主流的开源大模型。我们可以在自己的数据集上进行微调得到一个专属于自己的AI大模型。
2.2 什么是微调?为什么要微调?
微调(Fine-tuning)是机器学习和深度学习中的一个过程,指的是在一个已经训练好的模型的基础上,进一步在特定任务或特定数据集上进行训练,以提高模型在该任务或数据集上的性能。这种方法通常用于迁移学习(Transfer Learning),其主要目的是利用已经在大规模数据集(如ImageNet等)上训练的模型的预训练权重,减少在目标任务上的训练时间,并且通常可以获得更好的性能。
微调有以下主要优点:
- 提高性能:预训练模型已经学习了大量通用特征,微调能使模型更好地适应特定任务,从而提升性能。
- 减少数据需求:微调只需较少的特定任务数据,比从零开始训练要节省数据和时间。
- 节省计算资源:相比从头训练,微调能显著减少计算资源的消耗。
2.3 在逻辑推理数据集上微调
由于Unsloth需要从 Github 和 HuggingFace 上加载预训练模型,可以在代码开头导入os并使用镜像避免网络错误。os.environ['GIT_URL'] = 'https://githubfast.com'
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
使用Unsloth对预训练的 Qwen2-7B
模型在官方训练集上进行Lroa微调步骤如下:
2.3.1 加载模型
在这一步,我们使用 FastLanguageModel 来加载预训练的模型和 tokenizer。我们指定了模型的名称、最大序列长度、数据类型和是否以 4-bit 加载模型。
- model_name: 预训练模型的名称,这里是 “unsloth/Qwen2-7B”。
- max_seq_length: 模型输入的最大序列长度。
- dtype: 模型的参数数据类型。
- load_in_4bit: 指定是否以 4-bit量化的形式加载模型,以节省显存。
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="unsloth/Qwen2-7B",
max_seq_length=max_seq_length,
dtype=dtype,
load_in_4bit=load_in_4bit,
)
2.3.2 配置 PEFT(参数高效微调)
在这一步,我们使用 PEFT(参数高效微调)技术来优化模型的训练。我们指定了模型中需要微调的参数、LoRA(低秩适配器)相关的参数、是否使用梯度检查点等配置。
- r: LoRA 的秩(rank)。
- target_modules: 需要微调的目标模块。
- lora_alpha: LoRA 的缩放因子。
- lora_dropout: LoRA 的 dropout 率。
- bias: 指定是否训练偏置项。
- use_gradient_checkpointing: 是否使用梯度检查点。
- random_state: 随机种子,确保实验的可重复性。
- use_rslora: 是否使用 RSLora。
- loftq_config: LoftQ 配置,默认为 None。
model = FastLanguageModel.get_peft_model(
model,
r=16,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
lora_alpha=16,
lora_dropout=0,
bias="none",
use_gradient_checkpointing="unsloth",
random_state=3407,
use_rslora=False,
loftq_config=None,
)
2.3.3 配置训练参数
在这一步,我们设置训练参数,包括批量大小、学习率、优化器类型等。这些参数用于控制训练过程。
- per_device_train_batch_size: 每个设备上的训练批量大小。
- gradient_accumulation_steps: 梯度累积步数。
- warmup_steps: 学习率预热步数。
- max_steps: 最大训练步数。
- learning_rate: 学习率。
- fp16: 是否使用 16-bit 浮点数训练。
- bf16: 是否使用 bfloat16 训练。
- logging_steps: 记录日志的步数。
- optim: 使用的优化器类型,这里是 adamw_8bit。
- weight_decay: 权重衰减率。
- lr_scheduler_type: 学习率调度器类型,这里是线性调度器。
- seed: 随机种子。
- output_dir: 输出目录。
training_args = TrainingArguments(
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
warmup_steps=5,
max_steps=60,
learning_rate=2e-4,
fp16 = not is_bfloat16_supported(),
bf16 = is_bfloat16_supported(),
logging_steps=1,
optim="adamw_8bit",
weight_decay=0.01,
lr_scheduler_type="linear",
seed=3407,
output_dir="outputs",
)
2.3.4 加载数据
在这一步,我们加载数据并格式化成模型训练所需的形式。首先从 JSONL 文件中加载数据,然后创建包含问题、选项和答案的提示语句。
def load_dataset(file_path):
data = []
with open(file_path) as reader:
for line in reader:
data.append(json.loads(line))
return data
data = load_dataset('round1_train_data.jsonl')
def create_prompts(data):
prompts = []
for item in data:
problem = item['problem']
for question in item['questions']:
prompt = get_prompt(problem, question['question'], question['options'])
answer = question['answer']
text = prompt + "答案是:" + answer + EOS_TOKEN
prompts.append({"text": text})
return prompts
formatted_data = create_prompts(data)
dataset = Dataset.from_list(formatted_data)
2.3.5 训练模型
在这一步,我们使用 SFTTrainer 进行模型训练。我们将模型、tokenizer、训练数据集和训练参数传递给训练器,然后开始训练并记录训练统计信息。
- model: 需要训练的模型。
- tokenizer: 用于分词的 tokenizer。
- train_dataset: 训练数据集。
- dataset_text_field: 数据集中包含文本的字段。
- max_seq_length: 最大序列长度。
- dataset_num_proc: 数据集处理的进程数。
- args: 训练参数。
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
train_dataset=dataset,
dataset_text_field="text",
max_seq_length=max_seq_length,
dataset_num_proc=2,
args=training_args,
)
trainer_stats = trainer.train()
通过以上步骤,我们成功加载了模型,配置了 PEFT,设置了训练参数,加载了数据并格式化,最后进行模型训练。
2.4 完整微调代码
import os
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
os.environ['GIT_URL'] = 'https://kkgithub.com'
# os.environ['GIT_URL'] = 'https://bgithub.xyz'
os.environ['GIT_URL'] = 'https://githubfast.com'
from unsloth import FastLanguageModel
import torch
from concurrent.futures import ThreadPoolExecutor, as_completed
from transformers import TrainingArguments
from trl import SFTTrainer
from datasets import Dataset
import json
from http import HTTPStatus
import time
import dashscope
from tqdm import tqdm
import re
from unsloth import is_bfloat16_supported
# 设置模型参数
max_seq_length = 2048
dtype = None
load_in_4bit = True
# 加载预训练模型
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="unsloth/Qwen2-7B",
max_seq_length=max_seq_length,
dtype=dtype,
load_in_4bit=load_in_4bit,
)
# 训练配置
model = FastLanguageModel.get_peft_model(
model,
r=16,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
lora_alpha=16,
lora_dropout=0,
bias="none",
use_gradient_checkpointing="unsloth",
random_state=3407,
use_rslora=False,
loftq_config=None,
)
def get_prompt(problem, question, options):
options = '\n'.join(f"{'ABCDEFG'[i]}. {o}" for i, o in enumerate(options))
prompt = f"""你是一个逻辑推理专家,擅长解决逻辑推理问题。以下是一个逻辑推理的题目,形式为单项选择题。所有的问题都是(close-world assumption)闭世界假设,即未观测事实都为假。请逐步分析问题并在最后一行输出答案,最后一行的格式为"答案是:A"。题目如下:
### 题目:
{problem}
### 问题:
{question}
{options}
"""
# print(prompt)
return prompt
EOS_TOKEN = tokenizer.eos_token
# 创建prompt
def create_prompts(data):
prompts = []
for item in data:
problem = item['problem']
for question in item['questions']:
prompt = get_prompt(problem, question['question'], question['options'])
answer = question['answer']
text = prompt + "答案是:" + answer + EOS_TOKEN
prompts.append({"text": text})
return prompts
# Load and process the dataset
def load_dataset(file_path):
data = []
with open(file_path) as reader:
for line in reader:
data.append(json.loads(line))
return data
# Helper function to create prompts
def create_prompts_test(data):
prompts = []
for item in data:
problem = item['problem']
for question in item['questions']:
prompt = get_prompt(problem, question['question'], question['options'])
text = prompt + EOS_TOKEN
prompts.append({"text": text})
return prompts
# 加载训练数据集
data = load_dataset('round1_train_data.jsonl')
# 创建训练数据集
formatted_data = create_prompts(data)
dataset = Dataset.from_list(formatted_data)
# 训练参数
training_args = TrainingArguments(
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
warmup_steps=5,
max_steps=100,
learning_rate=2e-4,
fp16 = not is_bfloat16_supported(),
bf16 = is_bfloat16_supported(),
logging_steps=1,
optim="adamw_8bit",
weight_decay=0.01,
lr_scheduler_type="linear",
seed=3407,
output_dir="outputs",
)
# 定义训练器
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
train_dataset=dataset,
dataset_text_field="text",
max_seq_length=max_seq_length,
dataset_num_proc=2,
args=training_args,
)
# 训练模型
trainer_stats = trainer.train()
# 保存模型
model.save_pretrained("lora_model")
tokenizer.save_pretrained("lora_model")
- 完善推理代码
- 尝试更多参数的模型