写在前面
这篇博客作为datawhale夏令营的打卡第一天的笔记,也是我本人的第一篇博客,作为零基础的小白,我将以我自己的视角,详细地解读baseline01的代码,也为以后想要加入datawhale的小白们提供一些帮助(下面的有些必要的库我默认读者们都已经安装完了,如果不会安装的话就pip或者conda安装一下就好)
必要的基础
tqdm库
tqdm库时python的一个进度条的库,用于显示进度条,这里只介绍一些最常用的一些用法,以便于看懂后面的讲解。
tqdm库是一个显示循环的进度条的库。taqadum( تقدّم)在阿拉伯语中的意思是进展。tqdm可以在长循环中添加一个进度提示信息,用户只需要 封装任意的迭代器 tqdm(iterator),是一个快速、扩展性强的进度条工具库。
使用方法:
1.传入可迭代对象
import time
from tqdm import *
for i in tqdm(range(1000)):
time.sleep(.01)
tqdm一般情况下是在for循环中使用的,每循环一次进度条向前移动一个单位,在循环体内部是每一次任务的具体执行逻辑。在tqdm内部我在这写了一个参数,表示总共执行多少次,这样我们就写出了一个简单的进度条,结果如下:
100%|███████████████████████████████████████████████████████████████████████████| 1000/1000 [00:10<00:00, 96.43it/s]
有时候,我们会把“tqdm(range(1000)”写成"trange(1000)"
from tqdm import trange
for i in trange(1000):
time.sleep(.01)
2.加一个描述------desc
desc 是description的缩写,顾名思义,就是对这个进度条的一个描述,所以我们可以这样:
Processing:: 88%|███████████████████████████████████████████████████████▍ | 880/1000 [00:09<00:01, 96.46it/s]
有了以上基础就可以了,如果想要系统的了解tqdm的功能,可以参考这篇博客:
dashscope库
DashScope(模型服务灵积),灵积通过灵活、易用的模型API服务,让各种模态模型的能力,都能方便的为AI开发者所用。通过灵积API,开发者不仅可以直接集成大模型的强大能力,也可以对模型进行训练微调,实现模型定制化。
首先,我们申领一个api-key:https://dashscope.console.aliyun.com/apiKey
这里我们就注册/登录后点击”创建新的API-KEY“即可
dashscope库的快速入门
dashscope库由于是刚刚出的,b站上,网上缺少一些系统的教程,我们在这里就简单对他快速入门
如果对这个dashscope比较感兴趣的话也可以参考阿里云的官方文档:首页>模型服务灵积>快速入门
我们这里也采用官方文档里给出的示例代码来进行讲解。
官方给出了message方法和prompt方法来进行调用。这两种调用的区别官方文档原话是这么说的:
您可以通过prompt方式来调用通义千问大模型。通过prompt方式调用时,输入与输出的数据格式比messages方式简单,更适合单轮问答等简单场景;在更复杂的场景中推荐您通过messages方式调用。
接下来我们就来看看这两种调用方法,首先是message调用:
from http import HTTPStatus
import dashscope
def call_with_messages():
messages = [{'role': 'system', 'content': 'You are a helpful assistant.'},
{'role': 'user', 'content': '请介绍一下通义千问'}]
response = dashscope.Generation.call(
dashscope.Generation.Models.qwen_turbo,
messages=messages,
result_format='message', # 将返回结果格式设置为 message
)
if response.status_code == HTTPStatus.OK:
print(response)
else:
print('Request id: %s, Status code: %s, error code: %s, error message: %s' % (
response.request_id, response.status_code,
response.code, response.message
))
if __name__ == '__main__':
dashscope.api-key = "#这里是你获取的api-key"
call_with_messages()
这段代码里他用两段比较标准的格式封装成message,并且再response函数中把封装好的这两个字典作为参数传入到这个Generation.call函数中。如果调用成功就返回结果,否则报错。(会爬虫同学的可能更好理解一些,但是不会的也没有关系)
下面我们给出prompt调用,我们将这两段代码对比着来看:
from http import HTTPStatus
import dashscope
def call_with_prompt():
response = dashscope.Generation.call(
model=dashscope.Generation.Models.qwen_turbo,
prompt='请介绍一下通义千问'
)
# 如果调用成功,则打印模型的输出
if response.status_code == HTTPStatus.OK:
print(response)
# 如果调用失败,则打印出错误码与失败信息
else:
print(response.code)
print(response.message)
if __name__ == '__main__':
dashscope.api-key = ".....这里是你获取的api-key"
call_with_prompt()
这两段代码生成的结果均如下:
PS C:\Users\Hp\pyqt_learn> & C:/Users/Hp/anaconda3/anaconda_3/python.exe c:/Users/Hp/pyqt_learn/temp.py
{"status_code": 200, "request_id": "09bec800-2a4f-9566-8ede-09b44a6e0b3a", "code": "", "message": "", "output": {"text":
"通义千问是阿里云自主研发的超大规模语言模型,能够回答问题、创
作文字,还能表达观点、撰写代码。它在预训练过程中学习了海量文本数据和知识,因此具备广泛的知识背景
和语言理解能力。通义千问可以用于各种场景,如生成文章、故事、诗歌,提供技术解决方案,解答疑问等。
它的目标是为用户提供高效、准确的信息和服务支持。", "finish_reason": "stop", "choices": null},
"usage": {"input_tokens": 14, "output_tokens": 88, "total_tokens": 102}}
看起来这两种常用的调用方法都遵循了以下原则,就是不管用什么方法弄出来个response,再输出就行,就是弄出response的过程略有差异。在message调用时先封装这个message,然后再将message传入Generation.call这个函数,最后指定这个输出格式‘message’。而prompt似乎只需要问问题就可以了。然后错误处理。后续看代码的时候脑子里想着这两段代码的基本思路就很容易理解baseline的代码了
loguru库
loguru库是一个建立日志文件的库,虽然说在这个baseline里并没有什么比较明显的,不可或缺的作用,但作为一个知识点,我们最好给他讲一下。
Loguru 是一个第三方 Python 日志库,以其简洁的 API 和灵活的配置而著称,提供了一种简单而强大的方式记录应用程序的日志。
我们这里只讲这个baseline里提到过的loguru代码:
第一个是logger.remove(),它是用来移除之前的文件,比如说这段代码在之前测试过,我需要移除之前的日志。
第二个是logger.add("logs/app_{time:YYYY-MM-DD}.log", level="INFO", rotation="00:00", retention="10 days", compression="zip")
这个设置了日志文件的格式,首先log/是保存在log路径下,如果没有将会自动创建,第二个是{time:YYYY-MM-DD}
是一个时间格式占位符,表示日志文件名将包含当前日期,格式为 年-月-日
。这意味着每天会创建一个新的日志文件,文件名将根据当天的日期来命名,例如 logs/app_2024-07-28.log
。
level = "INFO"指的是日志记录的最低级别。日志不是说所有信息都要记录的,于是就有了这个,
以下是常见的日志级别,按照从低到高的严重性排序:
-
DEBUG:提供详细的信息,通常用于问题诊断。在生产环境中很少记录此级别,因为它可能会生成大量的日志数据。
-
INFO:用于记录程序的正常运行时的一般信息,比如程序启动、配置信息、正常流程的完成等。
-
WARNING:表示有潜在问题的情况,但程序仍能正常运行。比如,使用了不推荐的功能或参数,或者程序在某些非关键部分未能按预期执行。
-
ERROR:表示严重的问题,影响了程序的部分功能。这通常意味着发生了异常或错误,但程序可能仍能继续运行。
-
CRITICAL:最高级别的日志,表示非常严重的问题,可能导致程序完全停止或崩溃。这通常用于记录需要立即关注的严重错误或系统故障
rotation="00:00"
表示日志文件将在每天的午夜(00:00)进行旋转,即创建一个新的日 志文件,旧的日志文件将被关闭并根据其他参数进行处理。
retention="10 days"
表示日志文件将被保留 10 天,之后将被删除。
compression="zip"
表示当日志文件被删除时,它们将被压缩成 ZIP 格式以节省空间。
以上这些了解一下即可。
以上这些可以说是进行了一些初始化的设置,下面我来简单讲解一下这个logger的常用方法:
logger.info("This is an info message")
logger.debug("This is a debug message")
logger.warning("This is a warning message")
logger.error("This is an error message")
logger.critical("This is a critical message")
这些都是在日志文件中记录相应的东西,从严重性排序可分为info,debug,warning,error,critical.
还有一件事要注意一下,那就是千万不要直接import logger,因为logger是另外一个库。
f-string格式化字符串
这个简单的拿一个示例说一下就行
name = "Alice"
age = 30
print(f"Name: {name}, Age: {age}")
就是f后接引号括起来相应的内容,然后将变量替换成数值,如本例name替换成Alice
多线程
最后,我们简单讲解一下多线程的几个基本的操作:
这个baseline里import了一个这个:
from concurrent.futures import ThreadPoolExecutor, as_completed
这个是一个多线程的第三方库,如果不了解多线程的话,就看一下这篇博文:python(进阶篇)——多线程_python多线程tongshiduqu bianlian-CSDN博客
接下来,我们看一下这个第三方库的一些用法,
我自己对这个库的用法总结为如下的模板:
from concurrent.futures import ThreadPoolExecutor
'''
这是你的其他操作
'''
def task(a):
#这是这个函数的逻辑
with ThreadPoolExecutor(max_workers=16) as executor:
for i in range(10):
#亿些操作
future = executor.submit(task, i)#或者类似的语法
#你的其他操作
其中,ThreadPoolExecutor(max_workers=16) 这个是说明是最多有16个工作单元同时进行
executor.submit函数有几个参数,一个是task,即要进行的函数名,后面是task这个函数的参数,有几个写几个。
剩下的一些内容我会在后续讲解代码中提到。
解读baseline代码
好了,初看这个代码可能就会被一系列的import给弄蒙了,好了,不要慌,我们直奔main:
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')
上面的是测试代码,我们不去管他,整个main里只执行了这个main函数(是自己定义的,不是python执行的时候的main函数),这个所谓的main函数有两个参数,看起来是两个jsonl文件,一个是输入文件,一个是输出文件,看起来输入文件是问题,输出文件是结果,那么这个函数看起来像是处理这个输入文件去,得到了输出文件,我们找到这个所谓的“main”函数
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
这个代码主要干了两件事情,一是提取出json的信息,另一个是给一个名为process_datas的函数执行,并且返回了一个列表。可能出于某些原因,他原来的那个代码并没有给我们写出这个写入操作,我们可以这么加一下:
with open(ofn, 'w') as writer:
for item in return_list:
writer.write(json.dumps(item) + '\n')
然后我们的重点来了,就是这个process_datas函数,我们先看一下这个函数本身:
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
先看上半段,也就是第一个for循环,他把接收到的datas数据解析成一个相应的格式,然后通过这个executor.submit来当作一个参数来在一个线程里执行api_retry函数
到这里可能同学们会有两个问题,一个是这个prompt这个解析,有点迷惑啊,还有一个是这个api_retry是什么函数,不着急,我们先一个一个分析:
第一个,我们看着可能有些疑惑,这些值都是哪来的?我们可以看一下输入文档的这个jsonl文件,我们取一行来观察:
{"problem": "有一个简单的过程来逐行打印一个列表的所有元素。以下是具体的描述:\n\n1. 列表的每个元素都将被依次打印,每个元素占据一行。\n2. 当列表为空时,打印过程将停止。\n3. 示例列表包括原子、数字和复杂结构。\n\n请根据以下描述回答相关选择题。\n\n已知列表 `[a, 1, 2, 3, foo(bar)]` 包含以下元素:\n- 一个原子 `a`\n- 数字 `1`\n- 数字 `2`\n- 数字 `3`\n- 复杂结构 `foo(bar)`\n\n下面的问题是关于这个列表的打印过程。", "questions": [{"question": "选择题 1:\n当程序 `writelist([a,1,2,3,foo(bar)])` 执行时,会发生什么?", "options": ["列表中的元素将逐行打印,最后程序返回 True。", "程序将引发错误,因为列表包含不同类型的元素。", "程序将只打印列表中的数字元素。", "列表中的元素将逐行打印,但程序返回 False。"]}], "id": "round1_test_data_345"}
这个里面我们可以看到有如下几个部分组成:problems,questions,options,id,这样我们可以理解这个是怎么来的了,然后我们结合一下get_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
有些同学可能会想,我们在前面学习的时候不是说这个prompt调用的话随便提一个问题不就可以了吗,但是我们这个输入的jsonl文件是这种格式,我们必须把这个看起来不太像人话的东西通过这些操作变得”像人话“,然后结合我们学过的最基本的格式,我们就可以向ai进行批量提问了。
但是有同学又会说了,我怎么没在这个函数中看到我们前面学的最基本的prompt调用的样子呢?这就是我们要说的第二个问题,就是这个submit函数里的api_retry。
我们先看一下他的逻辑:
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
这个也很好理解,整体的逻辑就是,设置了一个最大的try的此时max_retries,小于这个数时向ai提问,大于这个数时报错,当然,这个报错就用到了我们之前讲的logger.warning和logger.error了,并且里面是f-string格式化字符串
这个call_qwen_api这个函数就是把前面我们提到的这个调用的方法封装起来了
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()
扒开这一层一层的封装,我们终于看到我们前面讲的最基础的内容,就是我们前面所讲到的这个prompt的调用方式
好了,我们再回到这个process_datas这个函数,看第二个for循环:
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
这里我们看到有一个新的函数,叫做as_completed,它和ThreadPoolExecutor隶属于一个库,他返回一个迭代器,意思就是说,在这个as_completed函数中,这些future同时执行,哪个执行完就执行下面的相应的其他操作。
这时候就有人注意到,这个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]
这时候看到有个re.compile,这是一个正则表达式,意思是带“答案是.”字样的形式的文字,后面这个findall则是告诉python,就照这个样,在输出结果中找出这个形式的话。(学过爬虫的可以对比理解一下)
这个 excpet Exception as e 就是e是这个Exception的一个实例,这个东西是自己生成的,并且在后面我们也可以看到这个在日志中记录的字符串中也有这个{e}
好了,这个是我们这个baseline01代码的整体的大致思路,我们也可以画一个流程图:
细心的同学们可能都注意到了,这个文件在__main__函数后面又定义了一些函数,这些是一些方法,(以备不时之需,或者是写这个代码的人调试的时候自己写的)这些大家自己看看也好,这个baseline大致的思路就是这样,我接下来简单介绍一下下面的函数:
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
这两个就是过滤器,把这个原来有答案的这个就直接当结果
由于本人是零基础,如有不恰当的地方希望大家批评指正,多多支持哦 :)
祝大家学习快乐