Datawhale AI 夏令营(逻辑推理方向)2024 3期 笔记2

目录

一、baseline代码详解

 1. 环境配置

(1)库安装

(2)API配置 

(3)库导入 

(4)日志配置 

(5)定义模型 

2. 答案生成

(1)调用模型

(2)网络错误处理

(3)提示工程

(4)结果抽取

(5)多线程处理

(6)主函数

(7)启动函数

3. 纠错与结果文件生成

(1)去重与排序

(2)纠错

(3)补全

(4)储存


 

一、baseline代码详解

下面将逐块对本期给出的baseline01代码进行解析,以确保对基础代码有较完善的理解。

 1. 环境配置

(1)库安装

!pip install scipy openai tiktoken retry dashscope loguru

该Jupyter命令行代码段用于安装一系列Python库,具体功能如下:

  1. scipy:

    • 用途: 科学计算工具箱,提供数学、科学和工程领域的高级函数。
    • 功能: 包括信号处理、优化、统计、线性代数、傅里叶变换等功能。
  2. openai:

    • 用途: 访问OpenAI API,特别是其语言模型服务。
    • 功能: 可以生成文本、翻译、总结、问答等自然语言处理任务。
  3. tiktoken:

    • 用途: 与OpenAI的GPT系列模型兼容的编码器。
    • 功能: 提供了将文本转换为模型可理解的token序列的能力,用于预处理输入数据。
  4. retry:

    • 用途: 异常处理和重试机制。
    • 功能: 允许用户定义在遇到错误时自动重试函数调用的次数和间隔,提高程序的健壮性。
  5. dashscope:

    • 用途: 达摩院的API客户端,用于访问其人工智能服务。
    • 功能: 包括但不限于图像识别、语音识别、自然语言处理等AI能力。
  6. loguru:

    • 用途: 高效的日志记录库。
    • 功能: 简化日志记录配置,支持异步日志记录、日志文件管理及多级日志输出

(2)API配置 

dashscope.api_key="sk-"

填入申请到的模型key

(3)库导入 

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
  1. json

    用于处理JSON格式的数据,包括解析和生成JSON。
  2. os

    提供了与操作系统交互的功能,如文件路径操作、环境变量管理等。
  3. pprint

    用于以更易读的格式打印Python数据结构。
  4. re

    支持正则表达式操作,用于字符串搜索、替换和分割等。
  5. tqdm

    用于显示进度条,常用于循环中显示处理进度。
  6. random

    提供随机数生成器,用于生成伪随机数。
  7. uuid

    用于生成全局唯一标识符(UUID),常用于分布式系统中确保ID的唯一性。
  8. openai

    提供了访问OpenAI API的接口,用于调用人工智能模型,如GPT系列。
  9. tiktoken

    用于处理文本编码,特别是与OpenAI模型兼容的编码。
  10. numpy

    提供了强大的数值计算能力,支持大型多维数组和矩阵运算。
  11. requests

    用于发送HTTP请求,获取网页内容或API数据。
  12. retry

    提供了函数重试机制,当函数失败时自动重试。
  13. scipy.sparse

    提供了稀疏矩阵的存储和运算功能,适用于大数据集的高效存储和处理。
  14. dashscope

    用于访问阿里云DashScope平台的各种AI服务。
  15. ThreadPoolExecutor, as_completed

    用于并行执行任务,提高程序运行效率。
  16. loguru

    提供了高级的日志记录功能,简化日志配置和输出。
  17. time

    提供了时间相关功能,如时间戳、计时器等。
  18. HTTPStatus

    定义了HTTP状态码,用于理解HTTP响应状态。
  19. jieba

    用于中文分词,是中文自然语言处理的重要工具。

(4)日志配置 

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

logger.remove()

这一行代码是在操作日志系统。在Python中,logger通常指的是logging模块的一个实例,但实际上,logger.remove()更常见于第三方日志处理库如loguru。当调用logger.remove()时,它会移除所有已添加的处理器(handlers),这通常包括默认的控制台输出处理器。这意味着之后的日志信息将不再被输出到控制台,除非重新添加处理器。

logger.add("logs/app_{time:YYYY-MM-DD}.log", level="INFO", rotation="00:00", retention="10 days", compression="zip")

此行代码向logger对象添加了一个新的处理器,用于将日志信息写入文件。参数解析如下:

  • "logs/app_{time:YYYY-MM-DD}.log":指定了日志文件的保存位置及命名规则。{time:YYYY-MM-DD}是一个格式化字符串,它会被替换为当前日期,确保每天的日志被存储在不同的文件中。

  • level="INFO":设置了日志记录的最低等级为INFO。这意味着只有等级为INFO或更高级别的日志消息(如WARNING, ERROR, CRITICAL)会被记录下来。

  • rotation="00:00":配置了日志文件的自动轮换机制,设置为每天午夜(00:00)时创建新的日志文件并开始记录新一天的日志。

  • retention="10 days":规定了日志文件的保留期限。在这个例子中,日志文件将被保留10天,超过这个期限的旧日志文件将被自动删除。

  • compression="zip":指定了日志文件的压缩方式。在这里,一旦日志文件被轮换,它们将被压缩成.zip格式,有助于节省磁盘空间。

(5)定义模型 

MODEL_NAME = 'qwen-'

填入要调用的模型名称 

2. 答案生成

(1)调用模型

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

此函数用于调用阿里云DashScope平台上的Qwen模型,执行自然语言处理任务。以下是函数的详细解析:

  1. 参数说明

    • MODEL_NAME: 指定调用的模型名称。
    • query: 用户输入的查询或指令。
  2. 构建请求体

    • 函数首先创建一个messages列表,其中包含一个字典,字典中的role字段设为usercontent字段设为query。这表示用户向模型发出的请求。
  3. 调用DashScope API

    • 使用dashscope.Generation.call方法发起API请求,传入MODEL_NAMEmessages作为参数。
    • 设置result_formatmessage,意味着期望从模型得到的结果将以消息格式返回。
  4. 处理响应

    • 成功情况:如果API响应的状态码是HTTPStatus.OK,函数将解析响应中的output字段,获取choices列表的第一个元素,进一步提取message字段中的content,最后返回这个内容作为模型的回复。
    • 失败情况:如果API调用失败,函数会捕获响应中的请求ID、状态码、错误代码和错误消息,并将其格式化输出到控制台,随后抛出异常,中断程序执行。

(2)网络错误处理

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

此函数 api_retry 的设计目的是为了处理与特定模型 MODEL_NAME 进行交互时可能出现的网络或服务端错误。它通过多次尝试调用 call_qwen_api 函数来确保请求的成功执行。

参数说明

  • MODEL_NAME: 指定要调用的模型名称。
  • query: 发送给模型的查询或输入文本。

内部逻辑

  1. 重试机制:

    • 定义最大重试次数 max_retries 为5次。
    • 每次重试之间等待的时间 retry_delay 为60秒。
  2. 循环重试:

    • 使用 while 循环来尝试调用 call_qwen_api 函数。
    • 如果调用成功,立即返回结果。
    • 如果调用失败(捕获到任何类型的异常),增加尝试计数 attempts 并检查是否达到最大重试次数。
      • 如果未达到最大重试次数,记录警告信息,说明当前尝试失败,并将在指定延迟后再次尝试。
      • 如果达到最大重试次数,记录错误信息,说明所有尝试均失败,并将具体的异常信息一并记录,最后抛出该异常。
  3. 异常处理:

    • 通过 try-except 结构捕捉 call_qwen_api 调用中可能发生的任何异常。
    • 在所有重试失败后,通过 raise 抛出最后一次捕获的异常,确保调用方能够感知到最终的失败状态。

(3)提示工程

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

 这个函数的主要目的是构造一个逻辑推理题目的prompt模版,用于指导模型或人如何理解和解答此类题目。

  • 参数说明:

    • problem: 字符串类型,代表逻辑推理题目的背景描述或情境设定。
    • question: 字符串类型,代表基于problem提出的具体问题。
    • options: 列表类型,包含多个字符串元素,每个元素代表一个可能的答案选项。
  • 内部处理流程:

    1. 选项格式化: 使用列表推导式和字符串连接,将options中的每个选项与一个大写字母索引结合,形成类似"A. 选项内容"的格式,并将所有选项以换行符\n连接成一个字符串。

    2. prompt构建: 使用f-string格式化字符串,创建一个包含题目描述、问题和选项的prompt模版。模版中包含了对解答者的角色定位(逻辑推理专家)、解题策略(闭世界假设)以及输出格式要求(如“答案是:A”)的指导。

  • 返回值:

    • 返回一个字符串,即构造完成的prompt模版,用于后续的逻辑推理任务。

(4)结果抽取

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]

 此函数的目标是从给定的文本中提取出特定格式的答案。下面是详细的步骤说明:

  1. 定义正则表达式模式:

    • 使用re.compile函数创建一个正则表达式对象ans_pattern
    • 正则表达式的模式为r"答案是:(.)",其中:
      • "答案是:": 匹配字符串“答案是:”。
      • (.): 后面的.表示匹配任何单个字符(除了换行符),括号()表示捕获这个字符,以便后续使用。
  2. 执行模式匹配:

    • 调用findall方法在input_text中搜索所有与ans_pattern匹配的部分。
    • findall返回一个包含所有匹配结果的列表。
  3. 处理匹配结果:

    • 首先检查problems是否为空列表,这通常意味着在input_text中没有找到匹配项。
    • 如果problems为空,函数返回字符串'A'作为默认值。
    • 如果problems不为空,则返回列表中的第一个元素problems[0],即第一次匹配到的答案字符。

(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

此函数用于处理一组数据datas,利用指定的模型MODEL_NAME为数据集中每个问题生成答案。

1. 初始化与配置

  • 初始化结果列表:results用于收集处理后包含答案的数据。
  • 配置线程池:使用ThreadPoolExecutor创建最大工作线程数为16的线程池,用于并行处理数据集中的任务。

2. 提交任务

  • 遍历数据集:通过tqdm库显示进度条,遍历datas中的每一个数据项。
  • 构造提示信息:对每个数据项中的问题,调用get_prompt函数根据问题、选项等信息构建模型输入的提示。
  • 提交任务至线程池:使用executor.submit提交一个异步任务,调用api_retry函数,传入模型名和提示信息作为参数。同时,记录任务与原始数据的对应关系,以便后续处理。
  • 控制提交速率:每提交一个任务后,休眠0.6秒,以避免过快提交导致的问题。

3. 处理完成的任务

  • 监控任务完成情况:使用tqdmas_completed监控并显示已完成任务的进度。
  • 提取结果:当任务完成后,尝试从任务结果中提取答案,使用extract函数处理API响应。
  • 更新数据:将提取的答案添加到原始数据的相应位置,然后将更新后的数据追加到results列表中。
  • 异常处理:若处理过程中遇到错误,如API调用失败或数据解析错误,将记录错误日志,并继续处理其他任务。

4. 返回处理结果

  • 最终,函数返回包含所有已处理数据的results列表,其中每个数据项都已填充了由模型生成的答案。

(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

 此函数主要负责从指定的输入文件中读取数据,处理数据,并返回处理后的结果列表。同时,它会检查输出文件是否存在,若存在则不执行后续操作。

参数说明

  • ifn: 输入文件名,用于读取原始数据。
  • ofn: 输出文件名,用于后续可能的操作或记录,但当前实现中未使用。

主体逻辑

  1. 输出文件检查

    • 使用 os.path.exists(ofn) 检查输出文件是否存在,如果存在,则直接跳过后续步骤。
  2. 数据读取与转换

    • 初始化空列表 data,用于存储读取的数据。
    • 使用 with open(ifn) as reader: 打开输入文件 ifn 并逐行读取。
    • 对于读取的每一行,使用 json.loads(line) 将 JSON 格式的字符串转换为 Python 字典,并追加到 data 列表中。
  3. 数据处理

    • data 赋值给 datas,这步操作看似冗余,但可能是为了代码清晰度或后续可能的扩展。
    • 调用 process_datas(datas, MODEL_NAME) 函数处理数据,其中 MODEL_NAME 是预定义的模型名称。处理后的结果存储在 return_list 中。
  4. 结果输出与返回

    • 打印处理后列表的长度 len(return_list) 和提示信息 "All tasks finished!"
    • 最终返回 return_list,即处理后的数据列表。

(7)启动函数

if __name__ == '__main__':
    return_list = main('round1_test_data.jsonl', 'upload.jsonl')

 调用主函数,启动整个模型

3. 纠错与结果文件生成

(1)去重与排序

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

has_complete_answer 函数解析

  • 功能:检查传入的questions列表中的每一个字典是否都包含一个名为'answer'的键。
  • 参数questions - 一个由字典组成的列表,每个字典代表一个问题及其相关信息。
  • 逻辑
    1. 遍历questions列表中的每一个字典。
    2. 检查字典中是否存在'answer'键。
    3. 如果任何一个字典缺少'answer'键,则立即返回False
    4. 如果所有字典都包含'answer'键,则返回True

filter_problems 函数解析

  • 功能:从传入的数据列表data中筛选出具有完整答案的问题,并确保结果中每个问题只出现一次。
  • 参数data - 一个由字典组成的列表,每个字典包含'problem', 'questions', 和'id'等键。
  • 逻辑
    1. 初始化一个空列表result和一个空集合problem_set用于存储最终结果和已处理的问题。
    2. 遍历data列表中的每一个字典item
      • 获取item中的'problem'键值。
      • 检查'problem'是否已经存在于problem_set中。
        • 如果存在,查找result中与之对应的问题字典。
        • 判断item中的'questions'是否为完整答案(调用has_complete_answer函数)。
          • 如果是,更新result中对应问题的'questions''id'信息。
        • 如果'problem'不在problem_set中,检查item中的'questions'是否为完整答案。
          • 如果是,将item添加到result列表中,并将'problem'添加到problem_set中。
    3. 返回result列表,其中包含了经过筛选、去重且具有完整答案的问题列表。

(2)纠错

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)

 此函数用于查找一个由字典组成的列表中,以字典键 'id' 的后三位数字表示的序号是否有缺失,并返回这些缺失的序号。假设 'id' 的值是一个字符串,且我们关注的是其最后三位数字。

参数

  • dict_list: 一个字典列表,每个字典都包含一个 'id' 键,其值为字符串形式的 ID。

实现步骤

  1. 提取序号:

    • 遍历 dict_list 中的每一个字典 d
    • d['id'] 字符串中提取最后三位字符。
    • 将这三位字符转换成整数,添加到集合 extracted_ids 中。使用集合可以自动去重。
  2. 创建完整序号集合:

    • 构建一个从 0 到 499(含)的整数集合 all_ids。这里假定序号范围是 0 到 499。
  3. 找出缺失的序号:

    • 计算 all_idsextracted_ids 的差集,即 all_ids - extracted_ids
    • 差集的结果是那些存在于 all_ids 中但不在 extracted_ids 中的序号,即为缺失的序号。
  4. 排序并返回结果:

    • 对缺失的序号集合进行排序,确保结果有序。
    • 返回排序后的缺失序号列表。

(3)补全

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

这段代码的主要目的是从JSON Lines文件中读取数据、修改特定记录的数据、并对数据进行排序。下面是对代码的逐行详细解释:

  1. 初始化一个空列表data,但实际上并未在后续代码中使用,这可能是代码的一个小错误或遗留部分。

  2. 打开文件'round1_test_data.jsonl',使用with open()语句确保文件正确关闭。jsonl文件是一种每行包含一个JSON对象的文件格式。

  3. 遍历文件的每一行,使用enumerate()获取行的索引id和实际行内容line

  4. 判断当前行的索引id是否在missing_ids列表中。missing_ids列表应该是预先定义好的,包含了需要处理的特定行的索引。

  5. 如果idmissing_ids中,那么将当前行的字符串内容line解析成Python字典sample,使用json.loads()函数完成这一转换。

  6. 遍历sample字典中的'questions'列表,将每个问题的'answer'字段值设为'A'。这一步是在修改数据,将所有问题的答案统一设置为'A'

  7. 将修改后的sample字典添加到sorted_data列表中。这里应该注意,sorted_data列表是在循环外部初始化的,但在代码中未显示,可能是在代码片段之外定义的。

  8. 在循环结束后,对sorted_data列表进行排序。排序的键是每个字典中'id'字段的最后三位数字,通过str(x['id'])[-3:]获取,然后将其转换为整数类型int(),以作为排序的依据。

  9. 排序后的sorted_data列表包含了按照特定规则修改和排序后的数据。

(4)储存

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

 此段代码的主要功能是将一个列表(sorted_data)中的元素以JSON格式写入到一个.jsonl文件中。以下是详细步骤:

  1. 打开文件:使用with open('upload.jsonl', 'w') as writer:语句以写入模式('w')打开一个名为upload.jsonl的文件。with语句确保即使在写入过程中发生错误,文件也会被正确关闭。

  2. 遍历数据:通过for sample in sorted_data:循环遍历sorted_data列表中的每一个元素。sorted_data应该是一个包含可序列化数据结构(如字典或列表)的列表。

  3. 转换并写入数据:对于sorted_data中的每一个元素(sample),使用json.dumps(sample, ensure_ascii=False)将其转换为JSON格式的字符串。参数ensure_ascii=False允许非ASCII字符直接写入,而不是转义。转换后的字符串通过writer.write()方法写入到文件中。

  4. 添加换行符:每写入一个数据后,使用writer.write('\n')在数据之间插入一个换行符,确保每个数据项都在新的一行开始,这是.jsonl文件格式的一个特点。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值