TASK 2 学习笔记打卡 Datawhale

1.配置环境,打开anaconda终端安装第三方库

!pip install scipy openai tiktoken retry dashscope loguru

2.按常规导入库(巴拉巴拉,没什么说的),重要的是移除默认的控制台输出,因为后续要自定义输出日志,模型选择qwen2-7b-instruct,目前在阿里云上是免费的

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'
# 注意:这里需要填入你的key~ 
dashscope.api_key="sk-"

这段代码定义了一个名为 call_qwen_api 的函数,该函数用于调用一个名为 dashscope.Generation 的 API 来生成文本。以下是该代码的功能、用途和特点的详细介绍:

功能

调用 API 生成文本:该函数通过传递一个模型名称 (MODEL_NAME) 和一个查询 (query) 来调用 dashscope.Generation.call 方法,生成相应的文本。

处理 API 响应:函数会检查 API 的响应状态码,如果状态码为 HTTPStatus.OK,则提取并返回生成的文本内容。如果状态码不是 HTTPStatus.OK,则打印错误信息并抛出异常。

用途

文本生成:该函数主要用于通过调用外部 API 来生成文本,适用于需要动态生成内容的场景,如聊天机器人、内容创作辅助等。

错误处理:通过检查 API 响应状态码并处理错误情况,确保在调用失败时能够及时发现并处理问题。

特点

重试机制:函数使用了 @retry(delay=3, tries=3) 装饰器,这意味着在调用 API 失败时,函数会自动重试最多 3 次,每次重试间隔 3 秒。

消息格式:在调用 API 时,设置了 result_format='message',表示期望的响应格式是消息格式。

错误处理:在 API 调用失败时,函数会打印详细的错误信息,包括请求 ID、状态码、错误代码和错误消息,并抛出异常,以便上层调用者能够捕获并处理这些错误。

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()

3.在调用API时可能会出现因网络传输问题导致无法使用,api_retry函数多次尝试调用call_qwen_api来调用MODEL_NAME,最多重试5次每次延迟为60s,如果失败一次则弹出一条失败日志

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

 4.定义了一个prompt,但是最后output文件里出现了很多控制,我怀疑是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}
"""
    # print(prompt)
    return prompt

5.大模型一般有自己的使用方式,都写在官网上(比如huggingface.com、dashscope.console.aliyun.com)。最后output出现空值有可能是调用的模型是base款,容易不稳定,所以需要微调。

# 这里使用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]

6.数据处理主过程,创建一个多线程池ThreadPoolExecutor来并发执行任务(最多同时执行16个,如果运行不了可以改成8或6,单位时间耗算力少点),控制0.5s提交一个任务,将每一条结果写入result。

我只能说一下大概理解,首先for遍历datasets提取到problem,data是当前遍历到的dataseta中的json元素,再遍历data中的id,question,再用get_prompt得到我们之前构建的prompt并把problem,question的值填充进去,这样就构建好一个完整的prompt,然后把api_retry, MODEL_NAME, prompt提交给多线程池执行,最后就是从执行好的结果future中提取我们需要的输出和题目编号

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

7.这是主函数,其实只有两个作用,1.读取数据形成总的数据集再分割成若干个,2.调用process_datas

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

8.评估函数evaluate,计算result的正确率,遍历ofn中的每一个json元素,如果有MODEL_NAME正确数pse++

def evaluate(ofn):
    data = []
    with open(ofn) as reader:
        for line in reader:
            sample = json.loads(line)
            data.append(sample)

    pse = 0
    cnt = 0
    tot = 0
    for task in data:
        for question in task['questions']:
            
            if MODEL_NAME in question:
                tot += 1
                cnt += question[MODEL_NAME] == question['answer']
            else:
                pse += 1

    print(cnt, tot, cnt/tot, pse)

9.大模型很容易产生幻觉,就像人一样会猜答案,可这样大多数时候是错的,所以prompt工程很重要。如果给高中生一道特别难的数学题但给他解题的时间很短,规定时间一到他是不是就要猜一个答案,大模型也是这样,所以我们在prompt的时候要提醒他在某些种类的问题上多花一点时间多使用算力,可以提高输出的准确率。

就我们这次赛事,要求提交结果是ABCD四个选项,直接让模型输出它会猜答案然后。。(你不会想看到比赛成绩的),所以我们要引导它进行具体推理,就如下代码框所示:

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')

10.调用filter_problems对return_list中的函数进行一次清洗,然后按id后三位对return_list中的元素进行排序

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)

12.大模型输出的一大特点就是不稳定,对于round1中的某些问题它输出的格式可能不符合预期,甚至没有输出

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)

13.缺失的部分填充上A作为答案,毕竟是比赛有答案总比没答案好

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:]))
        

14.保存到upload.jsonl中,就可以在大赛官网提交并查看成绩啦

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

  • 18
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值