一、关于调用模型
由于是第一次使用大模型,关于token之类的没有太多的关注。于是,跑了几次任务之后发现欠费了。去官网的模型调用才发现原来已经用完token了。
关于模型的调用价格,之前我一直没有关注,直到阿里云的欠费短信,我才开始关注其价目明细表(如图):
之前在群里也看到有人说换一个模型可能效果会更好。于是,找到了代码的这一行。先后分别体验了一下qwen2-7b(线程分别为8、12)、Chatglm等模型,使用的prompt相同(目前认为在这种问答中,不同模型对于同一个prompt之间的响应应该存在比较小的差异)。
MODEL_NAME = 'qwen2-7b-instruct'#选择合适的大语言模型,可以进行更改,详情见价格记录表
下面列出表格说明得分情况:
模型名称 | 线程 | 得分score |
qwen2-7b-instruct | 16 | 0.6551 |
qwen2-7b-instruct | 32 | 0.6559 |
chatglm3-6b | 32 | 0.4172 |
qwen-vl-chat-v1 | 32 | 0.4172 |
qwen-max-longcontext | 32 | 0.4759 |
qwen1.5-110b-chat | 32 | 0.5964 |
qwen2-57b-a14b-instruct | 32 | 0.6883 |
baichuan-7b-v1 | 32 | 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.模型需要选择合适的,最好控制一下成本。