初识大语言模型
概念
大语言模型的概念就是通过数据集进行训练,经过不断调整指令参数来解决推理问题。
目前已知的成熟大模型:
- chatGPT: https://chat.openai.com/chat
- 文心一言: https://yiyan.baidu.com/
- kimi: https://kimi.moonshot.cn/
- 通义千问:https://tongyi.aliyun.com/qianwen/
- …
能力&特点
- 大规模数据训练:使用大量的数据集(数千亿)进行训练,以学习语言的复杂性和多样性。
- 上下文Context:能够理解上下文语言环境对问题进行处理。
- 高度的泛化能力:能够处理多种语言任务,如文本生成、翻译、摘要、问答等。
- 多语言能力:一些模型能够处理多种语言,实现跨语言的理解和生成。
- 逐步推理: "思维链(Chain of Thought, CoT) "策略。
对比
大语言模型 | 数据量大 | 资源消耗大,速度慢 | 能够处理广泛复杂的逻辑 |
---|---|---|---|
传统机器学习 | 数据量少 | 模型训练&推理速度较快 | 缺乏灵活性 |
小规模语言模型 | 数据量少 | 资源消耗少 | 难以处理广泛或复杂的语言任务 |
如何实现? —> 提示工程
概念:
训练大模型,常见的实现方案便是提示工程(Prompt Engineering)
通过设计和定制特定的提示(prompts),即输入到模型中的文本,引导大模型生成预期的输出或执行特定的任务。
例子:
例如本次赛题中的数据集中,第一个文本如下:
{
"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"
}
提示工程 就是要把它转化成MD格式的prompt,让大模型能够理解并进行分析。
你是一个逻辑推理专家,擅长解决逻辑推理问题。以下是一个逻辑推理的题目,形式为单项选择题。所有的问题都是(close-world assumption)闭世界假设,即未观测事实都为假。请逐步分析问题并在最后一行输出答案,最后一行的格式为"答案是:A"。题目如下:
### 题目:
有一群人和一些食物类型。下列是关于这些个体和食物的已知信息:
鸡肉是一种食物。
苹果是一种食物。
如果X吃了Y,且X活着,则Y是一种食物。
Bill存活。
Bill吃了花生。
John吃所有食物。
Sue吃所有Bill吃的食物。
John喜欢所有食物。
根据以上信息,回答以下选择题:
### 问题:
选择题 1:
谁喜欢吃花生?
A. Bill
B. Sue
C. John
D. None of the above
代码解释
1️⃣ 安装库
!pip install scipy openai tiktoken retry dashscope loguru
- scipy: 建立在NumPy的基础上,用于数学、科学和工程计算。
- openai: 允许用户与OpenAI的API进行交互,进行文本生成或微调。
- tiktoken: 将文本分解成更小的单元,如单词、子词或字符,以便于机器学习模型的处理。
- retry: 遇到特定异常时自动重试代码块。
- dashscope: 模型服务灵积。
- loguru: 日志库。
2️⃣ 配置好ApiKey
dashscope.api_key="sk-*****"
相关库导入
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")
选择模型
# 这里用到的模型是qwen2-7b-instruct,可以替换其他模型,跑出更高的分数
MODEL_NAME = 'qwen2-7b-instruct'
=> 使用该模型分数为0.6521
=> 换了一个模型qwen2-72b-instruct
得分明显提高 0.8389
=> 又换了一个模型deepseek-chat
,得分提高 0.8547
4️⃣ 答案生成
🍑 大模型
- 定义一个函数
call_qwen_api
,该函数调用灵积的API来生成文本。
- 参数1:模型名称(也就是上述的
qwen2-7b-instruct
) - 参数2:query
- 返回结果:生成的文本内容
具体可参考下面👇
# 定义了一个名为 call_qwen_api 的函数,该函数用于调用一个名为 dashscope.Generation 的 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',
)
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()
- 定义一个函数
api_retry
,用于出错重试。
def api_retry(MODEL_NAME, query):
# 最大尝试次数
max_retries = 5
# 再次尝试等待时间:60s
retry_delay = 60
# 出错次数
attempts = 0
# 最大尝试次数在五次之内,重调call_qwen_api函数
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
🍑 prompt模板
上面的提示工程讲到,训练大模型需要prompt。我们这里定义一个prompt模板函数,将使用大模型生成的文本内容调整成我们需要的格式。
- 参数1:题干
- 参数2:问题
- 参数3:选项
- 返回结果: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}
"""
# print(prompt)
return prompt
🍑 抽取
这一步骤是将上一步大语言模型生成的文本抽取成 正确选项。
-
re模块是正则
-
re.compile(r"答案是:(.)", re.S)
编译一个正则表达式模式。 -
r"答案是:(.)"
是正则表达式模式,其中:答案是:
是一个固定字符串,表示要匹配的文本前缀。(.)
是一个捕获组,匹配任何单个字符(除了换行符)。括号()
用于捕获匹配的内容,以便后续使用。
-
re.S
是一个标志,表示单行模式。这意味着.
会匹配包括换行符在内的所有字符。
-
# 获得抽取的结果
def extract(input_text):
ans_pattern = re.compile(r"答案是:(.)", re.S)
# 在 input_text 中查找所有匹配 ans_pattern 的子字符串。
problems = ans_pattern.findall(input_text)
# 如果没有,默认为A
if(problems == ''):
return 'A'
return problems[0]
🍑 多线程
主要就是提高程序的执行效率,尤其是在这种 I/O 密集型任务。 (向 API 发送请求并处理返回结果,这是一种典型的 I/O 密集型任务)
def process_datas(datas,MODEL_NAME):
results = []
# 定义线程池 选择16线程
with ThreadPoolExecutor(max_workers=16) as executor:
# 这里我们使用future_data 存储每个线程的数据
future_data = {}
# 这里的lens记录了调用api的次数,也就是我们每个问题背景下的所有子问题之和。
lens = 0
# 送入多线程任务
# 这里每个data下是一个问题背景,其中包含多个子问题。
for data in tqdm(datas, desc="Submitting tasks", total=len(datas)):
problem = data['problem']
# 这里面我们用enumerate方法每次循环得到问题的序号id和实际的问题。
for id,question in enumerate(data['questions']):
prompt = get_prompt(problem,
question['question'],
question['options'],
)
# 这里送入线程池等待处理,使用api_retry,向api_retry传入MODEL_NAME, prompt参数
future = executor.submit(api_retry, MODEL_NAME, prompt)
# 每个线程我们存储对应的json问题数据以及问题序号id,这样我们就能定位出执行的是哪个子问题
future_data[future] = (data,id)
time.sleep(0.6) # 控制每0.6秒提交一个任务 防止接口超过并发数
lens += 1
# 处理多线程任务
for future in tqdm(as_completed(future_data), total=lens, desc="Processing tasks"):
# print('data',data)
# 取出每个线程中的字典数据及对应的问题id
data = future_data[future][0]
problem_id = future_data[future][1]
try:
# 获取api运行结果
res = future.result()
# 抽取大语言模型返回结果
extract_response = extract(res)
# print('res',extract_response)
# 装入answer字段
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
🍑 启动
- 参数1:输入的数据集文件
- 参数2:输出的文件
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
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')
5️⃣ 纠错&结果文件
🍑 去重&排序
-
定义一个函数
has_complete_answer
,用于判断这个答案是否有效。- 依据:
'answer'
键
- 依据:
def has_complete_answer(questions):
# 这里假设完整答案的判断逻辑是:每个question都有一个'answer'键
for question in questions:
if 'answer' not in question:
return False
return True
- 定义一个函数
filter_problems
,用于从输入的数据中去除重复的问题,只保留具有完整答案的记录。如果有重复的问题,保留最新且答案完整的记录。
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
-
然后进行排序
lambda x: ...
是一个匿名函数,x
是列表中的每个元素。str(x['id'])
将id
转换为字符串。str(x['id'])[-3:]
获取字符串的最后三位字符。int(str(x['id'])[-3:])
将这三位字符转换为整数,以便按数值进行排序。
return_list
return_list = filter_problems(return_list)
sorted_data = sorted(return_list, key=lambda x: int(str(x['id'])[-3:]))
print(sorted_data)
🍑 纠错
定义一个find_missing_ids
函数,返回缺失 ID 的排序列表。
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)
len(missing_ids)
🍑 改错
拿到上述缺失的序号列表missing_ids
,我们将每个answer字段默认填充为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:]))
6️⃣ 将结果存储到文件中
打开upload.jsonl
文件,将结果写入。
with open('upload.jsonl', 'w') as writer:
for sample in sorted_data:
writer.write(json.dumps(sample, ensure_ascii=False))
writer.write('\n')