抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Hi大家好,这里依旧是默元。今天完成了Datawhale AI夏令营的第一次任务,有所收获。

关于

从零入门AI+逻辑推理 是 Datawhale 2024 年 AI 夏令营第三期的学习活动(“AI+逻辑推理”方向),基于“第二届世界科学智能大赛逻辑推理赛道:复杂推理能力评估” 开展学习活动。

适合想学习大语言模型、想用AI完成逻辑推理任务的学习者参与。

Task1内容

报名赛事

报名第二届世界科学智能大赛逻辑推理赛道:复杂推理能力评估比赛。

申领大模型API

使用的开源模型

这次我们为了保证大家可以顺利体验大模型,同时满足比赛要求,采用阿里开源大模型Qwen系列,通过api的形式调用,使用的模型是目前限时免费的 qwen1.5-1.8b-chat 模型。

开通 DashScope(阿里云灵积平台) 可以获赠一些其他模型的限时免费使用额度,大部分有效期为30天。
本Task代码跑一次需要大概 500w Tokens,大家可以尝试灵积平台上提供的其他开源模型的API服务,但注意不要超出赠送额度,否则会扣费。

创建一个API key并保存在安全的地方。不要忘记或泄露!

什么是“API”?

API(Application Programming Interface,应用程序编程接口)是一些预先定义的函数,目的是提供应用程序与开发人员基于某软件或硬件得以访问一组例程的能力,而又无需访问源码,或理解内部工作机制的细节。

API是接口。

我们在日常生活中可能有这样的烦恼:为什么数据线的种类有这么多?micro、type C……我们有时直接叫做安卓口、苹果口、梯形口……

当我们有一些不方便改变的接口,又不适合我们的需求时,我们就会用到下面这一类小东西:转接头。

同样的,我们现在写了一堆代码。如何我们想要实现某个功能,但是这时我们发现这个功能不好集成到我们的代码中,或者干脆不好写、懒得写,恰巧别人写过这个功能了,我们就希望拿来使用。这时,功能的所有者可以把全部代码给我们,也可以自己保留代码,而给我们一个接口,我们只需要调用这个接口就可以使用他的函数。API就像一座桥梁,把我们连接起来。如果说我的代码是一块儿正方体,他的代码是一块儿球体,那么API就像一个一段是正方形一端是圆形的皮套把我们连接在一起。

以上是我自己的理解和浅显的解答,更多内容可以看这一篇文章什么是API?(看完你就理解了)

什么是“API key”?

通过上面的解释,我们可以知道API权限对于API创建者来说很重要,要保证别人不能拿到API权限,也就是说,你是正方体,我是球体,你想要让我们结合,我就借你一个一面正方形一面圆形的皮套让我们连接起来。但是这个皮套是我的,不能让你随意使用,更不能让你对我的球体做更改。

API 密钥是一个字母数字字符串,API 开发人员使用该字符串来控制对其 API 的访问权限。

详细解释可以看amazon的这篇文章:什么是 API 密钥?

启动魔搭Notebook

使用默认的配置,点击启动。等待实例启动后,点击“查看Notebook”。

什么是“Notebook”?

Notebook我们都知道,是英文“笔记本”、“笔记本电脑”的意思。

而在这里,“Notebook”指的其实就是“Jupyter Notebook”。我们可以看到,在魔搭平台页面上写明了:

ModelScope社区与阿里云合作,Notebook功能由阿里云提供产品和资源支持。

什么是“Jupyter Notebook”?

Jupyter Notebook是基于网页的用于交互计算的应用程序。其可被应用于全过程计算:开发、文档编写、运行代码和展示结果。

简而言之,Jupyter Notebook是以网页的形式打开,可以在网页页面中 直接 编写代码和运行代码,代码的运行结果也会直接在代码块下显示。如在编程过程中需要编写说明文档,可在同一个页面中直接编写,便于作及时的说明和解释。

我们可以看到,点击“查看Notebook”后,正是一个“Jupyter Notebook”界面。

体验baseline

下载代码及测试集

代码及测试集在DataWhale学习手册中可以找到,我就不再转载。

baseline01.ipynb是代码文件。round1_test_data.jsonl文件是测试集文件。

将这两个文件拖拽到Jupyter Notebook左侧文件栏进行上传。

填写API key

在相应位置填写API key。

下载结果文件

等待代码运行完成后( 需要约30分钟 ),右键upload.jsonl文件,下载到本地。

提交至大赛

将得到的结果文档提交至大赛,大约两分钟后可以查看分数。

文件详解

代码文件详解

我们在Jupyter Notebook中再次打开代码文件。

pip install scipy openai tiktoken retry dashscope loguru

pip是包管理工具。这里通过pip来下载一个包。

下面跟的这一大堆是pip install过程中的输出。由于我的已经运行过一遍,所以可以看到一堆”Requirement already satisfied”(请求已经满足了),后面还有一些路径,就是我们要下载的包,它已经存在于这个路径下。

这里提示我们更新pip,因为没必要,所以不用管它。而我记得在自己电脑上,这是一个大坑,为什么是大坑我忘了反正搞了不知道多少次都没搞好。。。

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

这俩关于日志的不是太懂。

不过我可以猜测一下:
  1. 移除默认的控制台输出。这个注释里也告诉我们了。
  2. 添加日志语句。添加的内容就是引号里面的。
  3. time:YYYY-MM-DD是典型的日期格式了,YYYY表示四位数year,MM表示两位数month,DD表示两位数date,中间用“-”连接。
  4. level=”INFO”,很明显是说日志等级。INFO为information,我们可以认为是一般重要的日志信息。我们可以猜测level还有error、warning值,它们不仅是等级标识,也可以作为分类依据。
  5. 后面的确实不懂。
MODEL_NAME = 'qwen2-7b-instruct'

模型名称。可以猜得到是通义千问系列。

# 注意:这里需要填入你的key~ 咱们在第二步申请的。
dashscope.api_key="sk-"

填写API key,让我们的可以通过接口访问得到资源。

这段代码定义了一个名为 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、状态码、错误代码和错误消息,并抛出异常,以便上层调用者能够捕获并处理这些错误。

接下来有一串注释。注释中的“这段代码”指向并不是特别清楚,我们可以CTRL+F找一下“call_qwen_api”函数。

我们可以看到,下面两块儿代码是与这有关的。

def api_retry(MODEL_NAME, query):

定义api_retry函数,传入参数是 MODEL_NAME 与 query 。可以想得到这个函数的作用就是“重试”,在学习过程中,计算机网络这门课中出现次数比较多。MODEL_NAME为模型名字,上文已经定义。query意为查询,接下来解析。

    max_retries = 5
    retry_delay = 60  # in seconds
    attempts = 0

max_retries最大重试次数。如果连接不到api,我们不可能一直重试,这里我们选择最大重试次数为5,即如果重试次数达到5次,那就不要再试了,浪费时间,中断、显示error。

retry_delay = 60,单位是秒。如果连接不到api,立马重试的话,多半还是连接不上。我们可以稍等一会儿再去连接。这里等待的时间就是1分钟。

attempts是我们已经尝试的次数,初始值为0.每尝试一次,使attempts加1,再与max_retries比较。

    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……except……是python中常用的处理异常的语句,我们把可能发生错误的语句放在try模块里,用except来处理异常。

这里,我们试图(try)调用call_qwen_api函数(下一大块儿代码中定义的)。如果发生异常,则重试次数+1,检查重试次数是否超过max_retries。如果未超过,日志打印一份”Warning”,提示“Retrying in {retry_delay} seconds…”(将在{retry_delay}秒后重试),而{retry_delay}就是我们设定的60.接下来,time.sleep(retry_delay)休眠60s。由于是while语句,接下来将返回try部分。如果重试次数已经超过最大重试次数,则日志打印”error”,将错误原因”e”打印出来。从这里我们可以看出,上文对于日志的猜测是完全正确的。

为什么错误原因是”e”呢?因为except Exception as e:

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

这里就到call_qwen_api函数本身了,可以看上面的那一块儿注释。

# 这里定义了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 ?

在使用大语言模型时,使用 prompt 往往可以获得更好、更精确的结果。prompt直译为“提示语”。现在已经有专门研究大语言模型prompt的工作(prompt engineer)。

在我的理解中,大语言模型其实就是一个基于概率机器人。它会根据你的问题,计算各个字(实际上通常是是几个字几个字作为整体)的概率,输出概率最大的那个。并且,前面的回答内容也将作为依据加入到后续回答的计算中,这也就是为什么我们看到的大语言模型基本全是几个字几个字地在输出,并不是为了显示效果,而是技术决定的。(我们当然也可以先将各结果放在一个缓冲区一样的东西中,再一并输出给用户。)

因此,通过prompt为大模型设置一个身份(在这里,我们告诉大模型“你是一个逻辑推理专家”),可以让大模型计算概率时向这方面靠拢。但是设置prompt也并不像我们自然语言表达的那样简单,由于大模型将几个字几个字看作为一组数据,我们对prompt的更改并不总会起作用。例如(只是例如):“你是一个逻辑推理专家”和“你是一个逻辑推理学者”的区别对大模型的影响可能微乎其微,也可能影响很大。这就是prompt工程师的工作。

以上是代码的通用部分一些简单的分析。接下来就是一些具体的分析过程,包括提交任务,读取数据,返回结果,格式化结果等,不再详细介绍。

测试集文件详解

下面对测试集文件做简单分析。

学习手册中写明:

测试集中包含500条测试数据。每个问题包括若干子问题,每个子问题为单项选择题,选项不定(最多5个)具体的,每条训练数据包含 content, questions字段,其中content是题干,questions为具体的子问题。questions是一个子问题列表,每个子问题包括options和answer字段,其中options是一个列表,包含具体的选项,按照ABCDE顺序排列,answer是标准答案。测试集 round1_test_data.jsonl 不包含answer字段。

我们使用记事本或类似软件(我使用NotePad–)打开测试集文件。

简单看一下第一行:

{"problem": "有一群人和一些食物类型。下列是关于这些个体和食物的已知信息:\n\n1. 鸡肉是一种食物。\n2. 苹果是一种食物。\n3. 如果X吃了Y,且X活着,则Y是一种食物。\n4. Bill存活。\n5. Bill吃了花生。\n6. John吃所有食物。\n7. Sue吃所有Bill吃的食物。\n8. John喜欢所有食物。\n\n根据以上信息,回答以下选择题:", "questions": [{"question": "选择题 1:\n谁喜欢吃花生?", "options": ["Bill", "Sue", "John", "None of the above"]}], "id": "round1_test_data_000"}

\n表示换行。但不是在文件本身中换行,而是要在读取数据的时候换行。为了方便阅读,我们改写一下格式:

{
    problem: 
        有一群人和一些食物类型。下列是关于这些个体和食物的已知信息:
        
        1. 鸡肉是一种食物。
        2. 苹果是一种食物。
        3. 如果X吃了Y,且X活着,则Y是一种食物。
        4. Bill存活。
        5. Bill吃了花生。
        6. John吃所有食物。
        7. Sue吃所有Bill吃的食物。
        8. John喜欢所有食物。
        
        根据以上信息,回答以下选择题:
    
    questions:
        选择题 1:
        谁喜欢吃花生?
        
    options:
        ["Bill", "Sue", "John", "None of the above"]}]
        
    id: 
        round1_test_data_000
}
    

注意实际程序中并不是这样处理数据的,我们只是为了方便研究而改写格式!

哈哈,是一道很简单的问题,我们人类可以很轻松地回答出来。但是对于大语言模型,这个问题真的好回答吗?读者可以复制上面改写好格式的文本去考考大语言模型。

结果文件详解

最后我们看一下得到的结果文件(upload.jsonl)的第一行。

{"problem": "有一群人和一些食物类型。下列是关于这些个体和食物的已知信息:\n\n1. 鸡肉是一种食物。\n2. 苹果是一种食物。\n3. 如果X吃了Y,且X活着,则Y是一种食物。\n4. Bill存活。\n5. Bill吃了花生。\n6. John吃所有食物。\n7. Sue吃所有Bill吃的食物。\n8. John喜欢所有食物。\n\n根据以上信息,回答以下选择题:", "questions": [{"question": "选择题 1:\n谁喜欢吃花生?", "options": ["Bill", "Sue", "John", "None of the above"], "answer": "D"}], "id": "round1_test_data_000"}

我们对比可以发现,仅多了"answer": "D"这一字段。看来我们的这个模型做错了呢。

评论

评论区不是无人区喵