Datawhale-从零入门 AI 逻辑推理Task1笔记

Task 1-跑通baseline

1.环境搭建

1.1.下载必要的库

pip install scipy openai tiktoken retry dashscope loguru

scipy: 基于Python的开源库,主要用于科学计算。包括最优化、积分、插值、特征值问题、代数方程、微分方程和统计等。
openai: 提供了一个名为 OpenAI API 的库,用于与他们的人工智能模型进行交互。
tiktoken: 用于OpenAI模型的快速BPE标记器。
retry: 提供了一个简单的重试机制。它通过在调用失败时自动重试一定次数来帮助您处理错误。
dashscope: 阿里云的一款模型服务产品,简化了AI模型的应用与部署。
loguru: 提供了丰富的日志处理功能,包括灵活的日志处理器配置、上下文信息绑定、搜索、存储、导出和导入等

1.2.导入需要的包

from multiprocessing import Process, Manager
import json
import os
from pprint import pprint
import re
from tqdm import tqdm
import random

import uuid
import openai
import tiktoken
import json
import numpy as np
import requests
from retry import retry
from scipy import sparse
#from rank_bm25 import BM25Okapi
#import jieba
from http import HTTPStatus
import dashscope


from concurrent.futures import ThreadPoolExecutor, as_completed
from loguru import logger
import json
import time
from tqdm import tqdm

logger.remove()  # 移除默认的控制台输出
logger.add("logs/app_{time:YYYY-MM-DD}.log", level="INFO", rotation="00:00", retention="10 days", compression="zip")

MODEL_NAME = 'qwen2-7b-instruct'

tqdm: 快速、可扩展的Python进度条,可以在Python长循环中添加一个进度提示信息。

1.2.大模型API

填入申请的api,本次默认为千问,其他大模型也可以。

# 注意:这里需要填入你的key~ 咱们在第二步申请的。
dashscope.api_key="sk-"

2.模型构建

2.1.反复尝试

调用模型出错,可以反复尝试调用。默认最多调用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

2.2.调用模型

使用dashscope库调用大模型回答问题。

def call_qwen_api(MODEL_NAME, query):
    # 这里采用dashscope的api调用模型推理,通过http传输的json封装返回结果
    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:
        # print(response)
        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()

2.3.Prompt模板

构建提示词模板,将其输入大语言模型,会获得相应的回复。我认为这部分是整个baseline可调整性最多的部分,不同的提示词搜索出来的结果不同,最终选择的答案“answer"也会不同,影响整个baseline的最终得分。

# 这里定义了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}
"""
    # print(prompt)
    return prompt

2.4.抽取结果

定义extract函数,获得大语言模型输出的答案。如果没有输出答案,默认为A。默认答案为A这一点感觉不太合理,会极大降低答案的正确率。个人感觉应该修改提示词再次输入问题,直至得到一个明确的答案。

# 这里使用extract抽取模获得抽取的结果

def extract(input_text):
    ans_pattern = re.compile(r"答案是:(.)", re.S)

    problems = ans_pattern.findall(input_text)
    # print(problems)
    if(problems == ''):
        return 'A'
    return problems[0]

2.5.多线程处理

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"):
            # print('data',data)
            data = future_data[future][0]
            problem_id = future_data[future][1]
            try:
                res  = future.result()
                extract_response = extract(res)
                # print('res',extract_response)
                data['questions'][problem_id]['answer'] = extract_response
                results.append(data)
                # print('data',data)
                
            except Exception as e:
                logger.error(f"Failed to process text: {data}. Error: {e}")
    
    return results

max_workers为线程数量,ThreadPoolExecutor(max_workers=16)即使用16线程的线程池,同时处理多个问题,提高运算速度。

2.6.模型封装

def main(ifn, ofn):
    if os.path.exists(ofn):
        pass
    data = []
    # 按行读取数据
    with open(ifn) as reader:
        for line in reader:
            sample = json.loads(line)
            data.append(sample)
    datas = data
    # print(data)
    # 均匀地分成多个数据集
    return_list = process_datas(datas,MODEL_NAME)
    print(len(return_list))
    print("All tasks finished!")
    return return_list

将前面的函数封装进一个main函数,ifn代表输入文件,ofn代表输出文件。封装函数的具体步骤如下:
1)文件ifn输入后,通过函数process_datas()同时处理16个子问题,
2)对每个子问题所在文本使用get_prompt()函数变为固定模板的提示词,
3)再通过call_qwen_api()函数将提示词输入大模型回答问题。
4)如果回答时出现问题,调用api_retry()函数重复尝试。
5)如果大模型正确输出答案,那就通过extract()函数提取出答案;如果没有答案,那就默认答案为A
具体细节部分还需进一步理解。

2.7.模型示例启动

if __name__ == '__main__':

    a = extract("""根据欧几里得算法,逐步解析计算两个数6和7的最大公约数(gcd)的步骤如下:

1. 判断6和7是否相等:不相等。
2. 判断6和7大小关系,7 > 6,所以用更大的数7减去较小的数6得到结果1。
3. 现在计算6和1的最大公约数。
4. 6 > 1,根据算法用更大的数6减去较小的数1得到结果5。
5. 再计算5和1的最大公约数。
6. 5 > 1,用5减去1得到结果4。
7. 再计算4和1的最大公约数。
8. 4 > 1,用4减去1得到结果3。
9. 再计算3和1的最大公约数。
10. 3 > 1,用3减去1得到结果2。
11. 再计算2和1的最大公约数。
12. 2 > 1,用2减去1得到结果1。
13. 最后计算1和1的最大公约数,两数相等,gcd即为这两个数,也就是1。

因此,6和7的最大公约数是1。

答案是:C.""")

    print(a)
    return_list = main('round1_test_data.jsonl', 'upload.jsonl')

这里先给了一个示例,会从大模型输出的结果里提取这道题的答案C。后面运行main函数将测试文件’round1_test_data.jsonl’输入模型,并输出一个包含问题答案的文件’upload.jsonl’。

3.答案优化

3.1.问题整合

函数has_complete_answer()检测每一个问题是否有完整答案,filter_problems()函数将相同背景的问题整合到一个问题集里。

def has_complete_answer(questions):
    # 这里假设完整答案的判断逻辑是:每个question都有一个'answer'键
    for question in questions:
        if 'answer' not in question:
            return False
    return True

def filter_problems(data):
    result = []
    problem_set = set()

    for item in data:
        # print('处理的item' ,item)
        problem = item['problem']
        if problem in problem_set:
            # 找到已存在的字典
            for existing_item in result:
                if existing_item['problem'] == problem:
                    # 如果当前字典有完整答案,替换已存在的字典
                    if has_complete_answer(item['questions']):
                        existing_item['questions'] = item['questions']
                        existing_item['id'] = item['id']
                    break
        else:
            # 如果当前字典有完整答案,添加到结果列表
            if has_complete_answer(item['questions']):
                result.append(item)
                problem_set.add(problem)

    return result

按编号队问题集进行排序。

return_list
return_list = filter_problems(return_list)
sorted_data = sorted(return_list, key=lambda x: int(str(x['id'])[-3:]))
print(sorted_data)

将json格式化,方便阅读。

sorted_data

3.2.找到缺失序号

find_missing_ids()找出所有缺失答案的序号。

def find_missing_ids(dict_list):
    # 提取所有序号
    extracted_ids = {int(d['id'][-3:]) for d in dict_list}
    
    # 创建0-500的序号集合
    all_ids = set(range(500))
    
    # 找出缺失的序号
    missing_ids = all_ids - extracted_ids
    
    return sorted(missing_ids)

# 示例字典列表
dict_list = sorted_data

# 找出缺失的序号
missing_ids = find_missing_ids(dict_list)
print("缺失的序号:", missing_ids)
len(missing_ids)

3.3.补充缺失答案

将所有缺失的答案都填为A。与extract类似,感觉对最终结果的得分会有很大影响。

data  = []
with open('round1_test_data.jsonl') as reader:
    for id,line in enumerate(reader):
        if(id in missing_ids):
            sample = json.loads(line)
            for question in sample['questions']:
                question['answer'] = 'A'
            sorted_data.append(sample)
sorted_data = sorted(sorted_data, key=lambda x: int(str(x['id'])[-3:]))

4.输出文本

输出’upload.jsonl’,里面包含“answer”字段,回答所有的问题。

with open('upload.jsonl', 'w') as writer:
    for sample in sorted_data:
        writer.write(json.dumps(sample, ensure_ascii=False))
        writer.write('\n')

5.改进优化

通读代码,感觉可以从三方面入手优化:
1.提示词模板优化,可以通过优化提示词,使得大模型输出的答案更准确。
2.对于缺失答案的问题,可以通过改变提示词再输入大模型,进而获得准确答案。
3.可以增加数据集、多次训练,提高模型对此类数据的回答效果。

  • 9
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值