[Datawhale AI夏令营] —— 逻辑推理赛道:复杂推理能力评估

Datawhale 学习指南:https://linklearner.com/activity/12/3/3
赛事链接:http://competition.sais.com.cn/competitionDetail/532231/format
免费大模型KEY:https://dashscope.console.aliyun.com/apiKey

1. 逻辑推理介绍

参考文献:Logic programming and knowledge representation

以下内容为自己阅读论文以及查阅资料后的理解,如果存在问题欢迎大家进行交流讨论

1.1 逻辑推理概念

逻辑推理是一种系统化的推理过程,通过应用特定的逻辑规则从已知事实或前提中得出结论。逻辑推理包括多种形式,如:演绎推理、归纳推理和类比推理等。在人工智能和计算机科学领域,逻辑推理常用于知识表示和自动推理系统中。

逻辑程序(Logic Programs)是一种用于知识表示的形式系统,通常使用规则的形式来描述事实和推理过程。每个规则包括一个头部(结论)和一个主体(前提)。通过不断应用规则,可以从已知的事实集合中推导出新的事实集合。

1.2 复杂推理能力评估

复杂推理能力的评估通常涉及对逻辑程序的表达能力和计算复杂性的分析。表达能力(Expressiveness)是指逻辑程序能够表示和处理的知识范围,而计算复杂性(Complexity)则涉及在给定逻辑语义下进行推理的计算资源需求。

评估标准

  1. 表达能力:逻辑程序的表达能力与其所能描述的知识范围有关。例如,带有经典否定和不确定性推理能力的扩展逻辑程序(Extended Logic Programs)和具有不确定性和歧义处理能力的析取逻辑程序(Disjunctive Logic Programs)。

  2. 计算复杂性:评估逻辑程序推理过程的计算复杂性,通常使用问题的决策复杂性(Decision Problem Complexity)来表示。常见的复杂性分类包括P类、NP完全和co-NP完全等。例如,在稳定模型语义(Stable Model Semantics)下,一阶不含函数符号的析取逻辑程序的计算复杂性是Π2-完全的,而在良基语义(Well-Founded Semantics)下,其数据复杂性是多项式的。

  3. 综合总结
    逻辑推理的概念涵盖了从已知事实中得出结论的系统化过程,而逻辑程序则提供了一种表达和执行这些推理的形式化工具。复杂推理能力的评估不仅涉及逻辑程序能够表示的知识范围,还包括在不同语义下进行推理时的计算复杂性。这些评估标准对于理解和改进逻辑程序在实际应用中的有效性和效率至关重要。

2. 赛题介绍

赛题信息链接:http://competition.sais.com.cn/competitionDetail/532231/competitionData

原始信息可以从上述链接中查阅,以下为自己对赛题的理解

2.1 背景知识

作为一名参与者,我深知这次比赛的核心是考验我们的逻辑推理和问题解决能力。比赛设计了一系列复杂的逻辑谜题,涉及多个领域的推理挑战,这要求我们具备以下几方面的背景知识:

逻辑推理概念:理解基本的逻辑推理概念至关重要。命题逻辑和谓词逻辑是基础,我们需要能够从一组假设中推导出结论,并且能够识别和运用逻辑关系和规则。

结构化问题解决:

  • 应用事实和规则:在面对复杂问题时,能够有效地应用已知事实和规则进行有条理的推理和解题。
  • 关系和属性推理:掌握推理中常见的关系(如相邻、位置、顺序等),并通过这些关系进行推理是解决问题的关键。
  • 常见逻辑谜题类型:熟悉各类逻辑谜题如斑马问题、房间问题等,这些谜题通常涉及多种关系和属性的推理,需要通过分析和组合已知信息来解决。

自然语言模型:

  • 知识抽取方法:可能需要使用相关的知识抽取技术,比如命名实体识别和关系抽取等。
  • 生成模型方法:相关生成模型,如语言模型等,可能在比赛中有所应用。

2.2 任务理解

这次比赛主要提供基于自然语言的逻辑推理问题,涉及各种场景,包括关系预测、数值计算和谜题等。我需要通过仔细分析和推理数据,利用机器学习、深度学习算法或者大语言模型,建立有效的预测模型。这不仅仅是对我逻辑推理能力的测试,更是对我运用现代技术解决复杂问题的能力的全面考验。

在参与过程中,我将重点放在以下几方面:

  1. 深入理解逻辑推理的基础知识,确保在解题时能够灵活运用这些知识。
  2. 提升结构化问题解决能力,通过实践各种逻辑谜题,训练自己的推理技巧。
  3. 掌握自然语言处理的相关技术,包括知识抽取和生成模型的应用,为比赛中的实际操作做好准备。

总的来说,我对这次比赛充满期待,希望通过比赛提升自己的逻辑推理和问题解决能力,并能在比赛中取得优异成绩。

3. 数据集介绍

初赛数据集为逻辑推理数据,其中训练集中包含500条训练数据,测试集中包含500条测试数据。每个问题包括若干子问题,每个子问题为单项选择题,选项不定(最多5个)。目标是为每个子问题选择一个正确答案。推理答案基于闭世界假设(closed-world assumption),即未观测事实或者无法推断的事实为假。

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

{
	"problem": "在一场山地自行车比赛中,四位选手分别取得了第一、第二、第三和第四名。不同的颜色代表不同的排名。下面是一些关于这场比赛和选手排名的信息:\n\n1. Alan 名列第一。\n2. 第二名的选手穿红色服装。\n3. John 没有穿黄色服装。\n4. 第四名的选手穿蓝色服装。\n5. Steve 的排名和他的服装色是相同的名次。\n6. Kev 的名次排在 Steve 前面。\n7. 第二名的选手穿的不是青色。\n8. 黄色服穿的选手的成绩排在绿色服穿的选手前面。\n9. 确保每四个参数中的所有元素都不相同和符合排名顺序。\n\n根据上述信息, 回答以下选择题:", 
	"questions": [
		{
			"question": "选择题 1:\n根据比赛结果,排在第二名之后的可能是哪些名次?", 
			"options": ["第一名 第三名", "第三名 第四名"], 
			"answer": "B"
		}, 
		{
			"question": "选择题 2:\n第一名是不是在第三名之前?", 
			"options": ["是", "否"], 
			"answer": "A"
		}, 
		{
			"question": "选择题 3:\n第一名是不是在第一名之前?", 
			"options": ["是", "否"], 
			"answer": "B"
		}, 
		{
			"question": "选择题 4:\n第一名是不是在第四名之前?", 
			"options": ["是", "否"], 
			"answer": "A"
		}
	], 
	"id": "round1_train_data_002"}

4. 利用大模型对解题

在这里插入图片描述

流程图参考:https://datawhaler.feishu.cn/wiki/CvNRwdXDHimxJskZaArcvYqDnIc

4.1 大模型选择

在这里插入图片描述

参考链接:https://help.aliyun.com/zh/dashscope/developer-reference/tongyi-qianwen-7b-14b-72b-metering-and-billing?spm=a2c4g.11186623.0.0.6d2b7a82qIPxrT

其中qwen2-1.5b-instruct现时免费中

4.2 导入包

!pip install scipy openai tiktoken retry dashscope loguru
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")

MODEL_NAME = 'qwen2-7b-instruct'

# 注意:这里需要填入你的key~ 
dashscope.api_key="sk-"

安装使用的包以及选择了模型后,即可在后续进行调用

4.3 通过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()


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_retrycall_qwen_api,它们的目的是通过一个名为 dashscope 的 API 调用模型推理,并在调用失败时进行重试。下面是对这两个函数的详细解释及其含义。

  • api_retry 函数:用于调用 call_qwen_api 函数,并在调用失败时进行多次重试。这个函数的主要功能是确保在调用 call_qwen_api 函数时,若出现异常,可以进行最多 5 次重试,每次重试之间间隔 60 秒。如果所有重试都失败,则记录错误信息并抛出异常。
  • call_qwen_api 函数负责实际调用 dashscope API 进行模型推理,将用户的输入(query)封装成 JSON 格式发送给 dashscope,然后返回模型的生成结果。如果调用成功(即返回状态码为 200),则返回生成的内容;否则,记录请求的详细信息并抛出异常。

这两个函数结合在一起,用于通过 dashscope 的 API 进行模型推理,并在调用失败时进行多次重试。api_retry 函数通过捕获异常并重试调用 call_qwen_api 函数,确保在网络不稳定或其他临时性错误情况下,能够多次尝试获取模型推理的结果。而 call_qwen_api 函数则负责实际的 API 调用和结果处理,确保返回的结果符合预期或在失败时提供详细的错误信息。

这两个函数的设计充分考虑了实际应用中可能遇到的各种情况,通过日志记录和异常处理机制,提升了系统的健壮性和可靠性。

4.4 定义Prompt

提示工程(Prompt Engineering)是一门新兴的学科,致力于开发和优化提示词,以帮助用户在各种场景和研究领域中更有效地利用大语言模型(Large Language Model, LLM)。掌握提示工程相关技能,不仅能让用户更好地了解大型语言模型的能力和局限性,还能显著提升模型在处理复杂任务时的表现,如问答和算术推理。

提示工程不仅局限于提示词的设计和研发,它还涵盖了与大语言模型的交互和开发的各种技能和技术。通过提示工程,研究人员可以增强模型处理复杂任务的能力,而开发人员则可以设计和研发强大的工程技术,实现大语言模型与其他生态工具的高效对接。

提示工程的应用与优势

  1. 任务优化:
    提示工程可以显著提升大语言模型在特定任务中的表现。例如,通过精心设计的提示词,可以提高模型在问答、文本生成、翻译等任务中的准确性和效率。

  2. 多领域适用:
    通过提示工程,用户可以定制提示词,使大语言模型在不同领域(如法律、医学、金融)中表现出色。结合领域知识和外部工具,提示工程能够赋能模型,扩展其应用范围和深度。

  3. 增强安全性:
    提示工程在提高大语言模型安全性方面也发挥重要作用。通过优化提示词,可以减少模型生成有害或偏见内容的风险,确保其输出更加公正和可靠。

  4. 互动与反馈:
    提示工程还包括优化用户与大语言模型的交互过程。通过设计有效的提示词,用户可以更清晰地表达需求,模型也能更准确地理解和响应,从而实现更高效的互动。

提示工程的实践与技术

  1. 提示词设计:

    • 明确任务目标:设计提示词时,首先需要明确任务目标和预期输出。
    • 简洁明了:提示词应尽量简洁明了,避免歧义,以确保模型理解准确。
    • 迭代优化:通过多次迭代和测试,不断优化提示词,提升模型的表现。
  2. 与模型交互:

    • 反馈机制:建立有效的反馈机制,根据模型输出调整提示词,逐步提高模型的性能。
    • 动态调整:根据任务需求和环境变化,动态调整提示词,保持模型的适应性。
  3. 结合外部工具:

    • 知识库集成:将专业领域的知识库与大语言模型结合,提高其在特定领域的表现。
    • 工具辅助:利用计算工具或外部API,增强模型在复杂任务中的处理能力。
  4. 安全性与公平性:

    • 偏见检测:通过提示工程,检测并减少模型输出中的偏见,确保结果公平公正。
    • 隐私保护:在设计提示词时,注意保护用户隐私,遵守相关法律法规。

通过上述方法和技术的应用,提示工程能够显著提升大语言模型的性能和适用性,使其在各个应用场景中更加高效和可靠。掌握提示工程相关技能,不仅可以优化与大语言模型的互动体验,还能拓展其应用边界,推动人工智能技术的发展与创新。


下面将介绍我们自己构建的Propmt:

# 这里定义了Propmt推理模版

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进行修改以提升模型性能

4.5 数据处理

# 这里使用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]

# 多线程处理函数
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

以下对 extract 函数和多线程处理函数 process_datas 的详细解释及其含义。

  • extract 函数用于从文本中提取答案。使用正则表达式模式 r"答案是:(.)" 来匹配文本中的答案部分,捕获答案字母。re.S 使得 . 可以匹配包括换行符在内的任何字符。使用 findall 方法查找所有匹配的答案,并将其存储在 problems 列表中。如果没有匹配到答案,默认返回 ‘A’;否则,返回匹配到的第一个答案。
  • process_datas 函数用于多线程处理一组数据,调用 API 并提取结果。
    首先使用 ThreadPoolExecutor 创建一个最大工作线程数为16 的线程池,并初始化一个空字典 future_data 和计数器 lens
    results = []
    with ThreadPoolExecutor(max_workers=16) as executor:
        future_data = {}
        lens = 0
    
    遍历数据集 datas,为每个问题生成提示文本 Prompt 并提交给 api_retry 函数执行,将返回的 future 对象存储在 future_data 字典中,控制提交任务的速率为每 0.6 秒一个。
    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.6秒提交一个任务
        lens += 1
    
    使用 as_completed 方法遍历已完成的 future 对象,获取其对应的 dataproblem_id,提取结果并更新数据,捕获并记录异常。
    for future in tqdm(as_completed(future_data), total=lens, desc="Processing tasks"):
    data = future_data[future][0]
    problem_id = future_data[future][1]
    try:
        res  = future.result()
        extract_response = extract(res)
        data['questions'][problem_id]['answer'] = extract_response
        results.append(data)
        
    except Exception as e:
        logger.error(f"Failed to process text: {data}. Error: {e}")
    

这两个函数的结合使用实现了从数据集中批量处理逻辑推理问题,通过多线程方式并发调用 API 并提取答案。extract 函数负责从 API 响应文本中提取答案,而 process_datas 函数则负责并发处理数据集中的所有问题,生成提示文本、调用 API 并提取和存储结果。此设计提高了处理效率,并通过日志记录和异常处理机制提升了系统的健壮性。

4.6 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

def evaluate(ofn):
    data = []
    with open(ofn) as reader:
        for line in reader:
            sample = json.loads(line)
            data.append(sample)

    pse = 0
    cnt = 0
    tot = 0
    for task in data:
        for question in task['questions']:
            
            if MODEL_NAME in question:
                tot += 1
                cnt += question[MODEL_NAME] == question['answer']
            else:
                pse += 1

    print(cnt, tot, cnt/tot, pse)

if __name__ == '__main__':
    return_list = main('round1_test_data.jsonl', 'upload.jsonl')

main 函数的主要功能是读取输入文件中的数据,调用 process_datas 函数处理数据,并将结果保存到输出文件中。evaluate 函数的主要功能是评估处理结果的准确性:遍历所有任务中的问题,统计每个问题是否包含 MODEL_NAME 作为键,如果包含则增加总计数 tot 并判断答案是否正确,正确时增加正确计数 cnt,否则增加伪计数 pse

4.7 截图

在这里插入图片描述

4.8 结果后处理

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

return_list
return_list = filter_problems(return_list)
sorted_data = sorted(return_list, key=lambda x: int(str(x['id'])[-3:]))
print(sorted_data)

has_complete_answer函数用于判断问题列表中的每个问题是否都有一个 ‘answer’ 键。如果所有问题都包含 ‘answer’ 键,则返回 True;否则返回 False。

filter_problems函数用于过滤和合并具有相同问题描述的数据项,确保每个问题描述只保留一个具有完整答案的数据项。如果出现重复的问题描述,会保留最新的、具有完整答案的数据项。

sorted对结果进行重排,并保存到sorted_data

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)


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:]))
        

find_missing_ids 这个函数用于从给定的字典列表中提取ID,并找出其中缺失的ID。假设每个字典的 id 字段包含一个三位数字序号,该函数会找出从0到499之间缺失的序号。

之后让缺失的值默认设为A,或者再让模型推理一次

4.9 保存结果并提交

with open('upload.jsonl', 'w') as writer:
    for sample in sorted_data:
        writer.write(json.dumps(sample, ensure_ascii=False))
        writer.write('\n')
  • 14
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值