目录
一、baseline代码详解
下面将逐块对本期给出的baseline01代码进行解析,以确保对基础代码有较完善的理解。
1. 环境配置
(1)库安装
!pip install scipy openai tiktoken retry dashscope loguru
该Jupyter命令行代码段用于安装一系列Python库,具体功能如下:
scipy
:
- 用途: 科学计算工具箱,提供数学、科学和工程领域的高级函数。
- 功能: 包括信号处理、优化、统计、线性代数、傅里叶变换等功能。
openai
:
- 用途: 访问OpenAI API,特别是其语言模型服务。
- 功能: 可以生成文本、翻译、总结、问答等自然语言处理任务。
tiktoken
:
- 用途: 与OpenAI的GPT系列模型兼容的编码器。
- 功能: 提供了将文本转换为模型可理解的token序列的能力,用于预处理输入数据。
retry
:
- 用途: 异常处理和重试机制。
- 功能: 允许用户定义在遇到错误时自动重试函数调用的次数和间隔,提高程序的健壮性。
dashscope
:
- 用途: 达摩院的API客户端,用于访问其人工智能服务。
- 功能: 包括但不限于图像识别、语音识别、自然语言处理等AI能力。
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
用于处理JSON格式的数据,包括解析和生成JSON。
json
提供了与操作系统交互的功能,如文件路径操作、环境变量管理等。
os
用于以更易读的格式打印Python数据结构。
pprint
支持正则表达式操作,用于字符串搜索、替换和分割等。
re
用于显示进度条,常用于循环中显示处理进度。
tqdm
提供随机数生成器,用于生成伪随机数。
random
用于生成全局唯一标识符(UUID),常用于分布式系统中确保ID的唯一性。
uuid
提供了访问OpenAI API的接口,用于调用人工智能模型,如GPT系列。
openai
用于处理文本编码,特别是与OpenAI模型兼容的编码。
tiktoken
提供了强大的数值计算能力,支持大型多维数组和矩阵运算。
numpy
用于发送HTTP请求,获取网页内容或API数据。
requests
提供了函数重试机制,当函数失败时自动重试。
retry
提供了稀疏矩阵的存储和运算功能,适用于大数据集的高效存储和处理。
scipy.sparse
用于访问阿里云DashScope平台的各种AI服务。
dashscope
用于并行执行任务,提高程序运行效率。
ThreadPoolExecutor
,as_completed
提供了高级的日志记录功能,简化日志配置和输出。
loguru
提供了时间相关功能,如时间戳、计时器等。
time
定义了HTTP状态码,用于理解HTTP响应状态。
HTTPStatus
用于中文分词,是中文自然语言处理的重要工具。
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模型,执行自然语言处理任务。以下是函数的详细解析:
参数说明
MODEL_NAME
: 指定调用的模型名称。query
: 用户输入的查询或指令。构建请求体
- 函数首先创建一个
messages
列表,其中包含一个字典,字典中的role
字段设为user
,content
字段设为query
。这表示用户向模型发出的请求。调用DashScope API
- 使用
dashscope.Generation.call
方法发起API请求,传入MODEL_NAME
和messages
作为参数。- 设置
result_format
为message
,意味着期望从模型得到的结果将以消息格式返回。处理响应
- 成功情况:如果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: 发送给模型的查询或输入文本。
内部逻辑
重试机制:
- 定义最大重试次数
max_retries
为5次。- 每次重试之间等待的时间
retry_delay
为60秒。循环重试:
- 使用
while
循环来尝试调用call_qwen_api
函数。- 如果调用成功,立即返回结果。
- 如果调用失败(捕获到任何类型的异常),增加尝试计数
attempts
并检查是否达到最大重试次数。
- 如果未达到最大重试次数,记录警告信息,说明当前尝试失败,并将在指定延迟后再次尝试。
- 如果达到最大重试次数,记录错误信息,说明所有尝试均失败,并将具体的异常信息一并记录,最后抛出该异常。
异常处理:
- 通过
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
: 列表类型,包含多个字符串元素,每个元素代表一个可能的答案选项。内部处理流程:
选项格式化: 使用列表推导式和字符串连接,将
options
中的每个选项与一个大写字母索引结合,形成类似"A. 选项内容"的格式,并将所有选项以换行符\n
连接成一个字符串。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]
此函数的目标是从给定的文本中提取出特定格式的答案。下面是详细的步骤说明:
定义正则表达式模式:
- 使用
re.compile
函数创建一个正则表达式对象ans_pattern
。- 正则表达式的模式为
r"答案是:(.)"
,其中:
"答案是:"
: 匹配字符串“答案是:”。(.):
后面的.
表示匹配任何单个字符(除了换行符),括号()
表示捕获这个字符,以便后续使用。执行模式匹配:
- 调用
findall
方法在input_text
中搜索所有与ans_pattern
匹配的部分。findall
返回一个包含所有匹配结果的列表。处理匹配结果:
- 首先检查
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. 处理完成的任务
- 监控任务完成情况:使用
tqdm
和as_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
: 输出文件名,用于后续可能的操作或记录,但当前实现中未使用。主体逻辑
输出文件检查
- 使用
os.path.exists(ofn)
检查输出文件是否存在,如果存在,则直接跳过后续步骤。数据读取与转换
- 初始化空列表
data
,用于存储读取的数据。- 使用
with open(ifn) as reader:
打开输入文件ifn
并逐行读取。- 对于读取的每一行,使用
json.loads(line)
将 JSON 格式的字符串转换为 Python 字典,并追加到data
列表中。数据处理
- 将
data
赋值给datas
,这步操作看似冗余,但可能是为了代码清晰度或后续可能的扩展。- 调用
process_datas(datas, MODEL_NAME)
函数处理数据,其中MODEL_NAME
是预定义的模型名称。处理后的结果存储在return_list
中。结果输出与返回
- 打印处理后列表的长度
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
- 一个由字典组成的列表,每个字典代表一个问题及其相关信息。- 逻辑:
- 遍历
questions
列表中的每一个字典。- 检查字典中是否存在
'answer'
键。- 如果任何一个字典缺少
'answer'
键,则立即返回False
。- 如果所有字典都包含
'answer'
键,则返回True
。
filter_problems
函数解析
- 功能:从传入的数据列表
data
中筛选出具有完整答案的问题,并确保结果中每个问题只出现一次。- 参数:
data
- 一个由字典组成的列表,每个字典包含'problem'
,'questions'
, 和'id'
等键。- 逻辑:
- 初始化一个空列表
result
和一个空集合problem_set
用于存储最终结果和已处理的问题。- 遍历
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
中。- 返回
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。实现步骤
提取序号:
- 遍历
dict_list
中的每一个字典d
。- 从
d['id']
字符串中提取最后三位字符。- 将这三位字符转换成整数,添加到集合
extracted_ids
中。使用集合可以自动去重。创建完整序号集合:
- 构建一个从 0 到 499(含)的整数集合
all_ids
。这里假定序号范围是 0 到 499。找出缺失的序号:
- 计算
all_ids
和extracted_ids
的差集,即all_ids - extracted_ids
。- 差集的结果是那些存在于
all_ids
中但不在extracted_ids
中的序号,即为缺失的序号。排序并返回结果:
- 对缺失的序号集合进行排序,确保结果有序。
- 返回排序后的缺失序号列表。
(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文件中读取数据、修改特定记录的数据、并对数据进行排序。下面是对代码的逐行详细解释:
初始化一个空列表
data
,但实际上并未在后续代码中使用,这可能是代码的一个小错误或遗留部分。打开文件
'round1_test_data.jsonl'
,使用with open()
语句确保文件正确关闭。jsonl
文件是一种每行包含一个JSON对象的文件格式。遍历文件的每一行,使用
enumerate()
获取行的索引id
和实际行内容line
。判断当前行的索引
id
是否在missing_ids
列表中。missing_ids
列表应该是预先定义好的,包含了需要处理的特定行的索引。如果
id
在missing_ids
中,那么将当前行的字符串内容line
解析成Python字典sample
,使用json.loads()
函数完成这一转换。遍历
sample
字典中的'questions'
列表,将每个问题的'answer'
字段值设为'A'
。这一步是在修改数据,将所有问题的答案统一设置为'A'
。将修改后的
sample
字典添加到sorted_data
列表中。这里应该注意,sorted_data
列表是在循环外部初始化的,但在代码中未显示,可能是在代码片段之外定义的。在循环结束后,对
sorted_data
列表进行排序。排序的键是每个字典中'id'
字段的最后三位数字,通过str(x['id'])[-3:]
获取,然后将其转换为整数类型int()
,以作为排序的依据。排序后的
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
文件中。以下是详细步骤:
打开文件:使用
with open('upload.jsonl', 'w') as writer:
语句以写入模式('w'
)打开一个名为upload.jsonl
的文件。with
语句确保即使在写入过程中发生错误,文件也会被正确关闭。遍历数据:通过
for sample in sorted_data:
循环遍历sorted_data
列表中的每一个元素。sorted_data
应该是一个包含可序列化数据结构(如字典或列表)的列表。转换并写入数据:对于
sorted_data
中的每一个元素(sample
),使用json.dumps(sample, ensure_ascii=False)
将其转换为JSON格式的字符串。参数ensure_ascii=False
允许非ASCII字符直接写入,而不是转义。转换后的字符串通过writer.write()
方法写入到文件中。添加换行符:每写入一个数据后,使用
writer.write('\n')
在数据之间插入一个换行符,确保每个数据项都在新的一行开始,这是.jsonl
文件格式的一个特点。