Task1 逻辑推理方向 baseline01笔记 # datawhale夏令营

写在前面

这篇博客作为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的功能,可以参考这篇博客:

【PyTorch总结】tqdm的使用

dashscope库

DashScope(模型服务灵积),灵积通过灵活、易用的模型API服务,让各种模态模型的能力,都能方便的为AI开发者所用。通过灵积API,开发者不仅可以直接集成大模型的强大能力,也可以对模型进行训练微调,实现模型定制化。

官网模型服务灵积 DashScope - 阿里云

首先,我们申领一个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"指的是日志记录的最低级别。日志不是说所有信息都要记录的,于是就有了这个,

以下是常见的日志级别,按照从低到高的严重性排序:

  1. DEBUG:提供详细的信息,通常用于问题诊断。在生产环境中很少记录此级别,因为它可能会生成大量的日志数据。

  2. INFO:用于记录程序的正常运行时的一般信息,比如程序启动、配置信息、正常流程的完成等。

  3. WARNING:表示有潜在问题的情况,但程序仍能正常运行。比如,使用了不推荐的功能或参数,或者程序在某些非关键部分未能按预期执行。

  4. ERROR:表示严重的问题,影响了程序的部分功能。这通常意味着发生了异常或错误,但程序可能仍能继续运行。

  5. 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

这两个就是过滤器,把这个原来有答案的这个就直接当结果

由于本人是零基础,如有不恰当的地方希望大家批评指正,多多支持哦 :)

祝大家学习快乐

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值