Datawhale AI 夏令营 AI+逻辑推理 Task02 总结

一、关于调用模型

    由于是第一次使用大模型,关于token之类的没有太多的关注。于是,跑了几次任务之后发现欠费了。去官网的模型调用才发现原来已经用完token了。

关于模型的调用价格,之前我一直没有关注,直到阿里云的欠费短信,我才开始关注其价目明细表(如图):

之前在群里也看到有人说换一个模型可能效果会更好。于是,找到了代码的这一行。先后分别体验了一下qwen2-7b(线程分别为8、12)、Chatglm等模型,使用的prompt相同(目前认为在这种问答中,不同模型对于同一个prompt之间的响应应该存在比较小的差异)。

MODEL_NAME = 'qwen2-7b-instruct'#选择合适的大语言模型,可以进行更改,详情见价格记录表

下面列出表格说明得分情况:

不同模型的表现
模型名称线程得分score

qwen2-7b-instruct

160.6551
qwen2-7b-instruct320.6559
chatglm3-6b320.4172
qwen-vl-chat-v1320.4172
qwen-max-longcontext320.4759
qwen1.5-110b-chat320.5964
qwen2-57b-a14b-instruct320.6883
baichuan-7b-v132 0.4172

在使用完这一些模型后,我们对那一些非限免的模型观察一下token的变化值:

qwen-max-longcontext、qwen1.5-110b-chat在跑完模型后花掉了大概15-20w的token,而qwen2-57b-a14b-instruct直接花掉了55w的token。

总结:在线程并行度上,增量并不能引起太多的变化,主要在选择的模型上。

值得一提的是,qwen2-57b-a14b-instruct模型在执行上比其他模型快很多。

从模型上来看,带有instruct的模型,或者说标明的参数越多的服务会更加的好,参数多的表现性能可能会更好(好模型还是要留在最后上分时使用滴!!!)。

二、再谈Baseline

我们先来看主函数部分:

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

#调用main函数
 return_list = main('round1_test_data.jsonl', 'upload.jsonl')

总体流程:调用大模型->设计Prompt->数据处理与信息抽取->访问模型->填写答案

具体细节:

1.模型获取

①由于是调用大模型作为控制台输出,而不是正常程序的控制台输出,因此,需要移除原来的,新建一个控制台输出。

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

②引入自己想要的模型,根据API-KEY(需要通过这个API去访问Dashscope),在此之前需要去官网上开通Dashscope服务(有一定的扣费)。

定义常量:模型名称与 API-KEY。

后期需要想办法导入可以开源的模型(不过在时间上可能会稍微慢一点,因此后面才需要特意为其写一个加速的函数),节省token的费用。

③访问大模型

设置JSON格式目的是为了能够得到响应的状态,返回的数据形成一定的格式。

如果得到响应就返回结果,否则打印出错误信息。

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

2.设计Prompt

Prompt的设计取决于数据的格式:

由于这是一个大模型问答任务,因此数据上面结构化明显。主要分为:题目内容、问题、选项三个部分。

因此设计的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}
"""
    return prompt

下面解读这一段Prompt:

模型定位:你是一个逻辑推理专家,擅长解决逻辑推理问题。

任务指示:以下是一个逻辑推理的题目,形式为单项选择题。

处理问题的条件:所有的问题都是(close-world assumption)闭世界假设,即未观测事实都为假。

任务:请逐步分析问题并在最后一行输出答案,最后一行的格式为"答案是:A"。

下面是分成3个板块对问题进行描述。

注意到这里还设计了输出的格式,意即提示大模型返回的格式。

3.数据加工处理

先看一眼函数:

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

这里有了很多不太明白的东西,比如:tqdm,线程并行。

enumerate:迭代变量,这里是指数据集中的问题。

在这中间我们注意到time.sleep(0.6)这一行:

有充足时间放到队列里面。

这是为了保证大模型有比较合适的响应时间,从而能够提高大模型的回答的质量。

这里需要用线程并行的原因是:AI处理数据在并行度较高的情况下可以加快数据处理的速度。

tqdm主要用于跟进任务进度,能够显示出你当前任务的进度的情况。

TreadExecutor用于创建一个线程池。

【补充并行计算与线程池的相关知识:

在计算机执行任务的过程中,我们都会有一些指令序列要让计算机去执行。

在一个时刻,我们如果有太多的指令需求要处理,如果还是按照单线程(正常指令的处理方式,即一个一个去处理),那么就会降低处理效率,导致处理速度过于缓慢。

因此,我们可以创建多个消息队列去处理这些需求,(多条通道去执行)

所谓的线程池,就是多线程(多个消息队列)的处理方式,可以灵活运行。

我们可以看到,在代码这一行里,

with ThreadPoolExecutor(max_workers=16) as executor:

我们有一个max_worker,这里就是指最大的线程开工数(最多允许多少个消息队列同时工作)

线程池的用处:

最大限度的可以灵活使用硬件资源(开一个消息队列要占用资源),从而提高处理效率。

在线程池中,最大线程宽度主要取决于模型,也就是说,这个模型可能最大的并行度是有多大。

对于参数较大的模型而言,可能需要开的线程数要稍微多一点。

在这里的代码里,是将2部分内容分别打散在多个线程中执行。

每一个线程内执行两个步骤。

数据处理的细节:

在并行计算的环境下执行:

①提取数据集,从数据集中提取题目、问题、选项的标签。

将提取结果组成一个Prompt输入请求。

将组合结果通过执行任务上传到大模型的请求任务。(提交进程中调用api_retry主要是为了防止线程数溢出出现问题,导致程序中断)。

将任务加入到列表中,即最后提交时对应的问题按钮。

②请求访问/提交任务。

提取每一条问题的problem环节,使用try-except机制(防止中途出现某个问题导致程序异常,错误信息可以在运行日志中去查),获取答案,将答案加入到列表中。

4.弥补缺失答案

因为在答案的填写过程中,有一部分题目的答案是缺失的(即大模型也不清楚答案),所以这个时候需要用蒙的策略(毕竟是选择题),填入答案。

首先要遍历一遍答案是否有缺漏(即data['answer']==【】(空列表)):

这里采用集合相减的方式搜集:

#return_list为主函数执行后返回的问答结果列表

#构建或更新形成了答案的列表
def filter_problems(data):
    result = []
    problem_set = set()

    for item in data:
        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

return_list = filter_problems(return_list)
sorted_data = sorted(return_list, key=lambda x: int(str(x['id'])[-3:]))
#按照关键字排序

#寻找缺失的
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)

有一点我们要关注:生成后的答案需要经过一次按编号的排序,主要是因为大语言模型先后处理的可能顺序不同。(并行计算允许多个任务执行,由于每个任务结束的时间可能不同,从而导致插入在列表中的顺序可能会发生错乱)。

5.用文件with方式写答案

这里是在原来sorted得到的列表内,查询编号在missing_ids的题目序号,默认填入选项A。(为什么不可以改成随机填入呢?可能也是一个概率的问题吧)

#读入数据
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格式
with open('upload.jsonl', 'w') as writer:
    for sample in sorted_data:
        writer.write(json.dumps(sample, ensure_ascii=False))
        writer.write('\n')

补充:由于这一个Task中我们没有用到训练集,因此本来应该还有一步是为了评估大模型的准确性,从而能够适合去进行微调,这里先并不放出评价的相关代码。

三、关于上分的方向的思考

1.从代码中我们可以看到train_data似乎还没有用上,或者更多样式的训练数据可以指导大模型如何去回答。

2.适当延长一下大模型的反应时间。

3.后面Task03所提到的微调模型以及Prompt设计优化。

Prompt优化(根据问题做进一步的细化)

4.换用更好的模型(可能得要氪一点金)。

四、关于大模型竞赛的编写代码总结

1.申请API、大模型请求;读入数据、多线程处理;写文件。

2.需要考虑一些特殊的情况(工程需求上异常情况的处理)

3.根据题目要求,选择更加细化的Prompt。

4.管理好线程的运行结果,以及在最后进行数据的填充。

5.由于每天的提交次数有限,可以试着在本地的训练集上去进行训练,评估结果。

6.模型需要选择合适的,最好控制一下成本。

  • 23
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

追逐着明

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值