引言
承接上一篇讲的部署大模型的方法,本篇文章讲一下如何实现自动化的测试。
思路
要实现对大模型进行自动化的测试,就需要自行写一个可以进行自动化测试的文件,我使用的是python来实现自动化的测试,其功能为自动读取包含测试数据的json文件,然后将其作为问题输入给大模型,再将大模型返回的答案、问题和正确答案一并导出到txt文本文件当中,最后再算其正确率。
具体实现
配置模型的参数
@dataclass
class GenerationConfig:
#允许用户输入的最大长度
max_length: int = 32768
#用于控制采样时,保留概率累积的阈值。在采样时,会选择概率累积超过这个阈值的最高概率的词。
top_p: float = 0.8
#控制生成文本时的多样性。较高的温度值会使生成的文本更加多样化,但可能会牺牲一些语义连贯性。
temperature: float = 0.8
#布尔值,控制是否使用采样方法生成文本。如果为True,则使用采样方法,否则使用贪婪解码方法。
do_sample: bool = True
#控制生成文本中重复内容的惩罚因子。较高的值会降低生成文本中重复内容的可能性。
repetition_penalty: float = 1.005
这个生成类用于保存模型的配置参数。
加载模型与分词器
@pytest.fixture(scope="module")
def model_and_tokenizer():
# 修改为模型的实际路径
model_path = './merged'
# 修改为tokenizer的实际路径
tokenizer_path = './merged'
print("Loading model from:", model_path)
# 加载大语言模型
model = AutoModelForCausalLM.from_pretrained(model_path, trust_remote_code=True).to(torch.bfloat16).cuda()
print("Model loaded successfully.")
# 加载对应的分词器
tokenizer = AutoTokenizer.from_pretrained(tokenizer_path, trust_remote_code=True)
print("Tokenizer loaded successfully.")
return model, tokenizer
加载预训练语言模型和分词器的路径。使用AutoModelForCausalLM.from_pretrained加载语言模型,并将其转换为torch.bfloat16格式,然后移动到CUDA设备上。使用AutoTokenizer.from_pretrained加载对应的分词器。最后将加载好的语言模型和分词器作为元组返回,以供后续使用。
需要注意的是要将模型和分词器的路径改为自己本地的实际路径。
combine_history
def combine_history(prompt, chat_history):
messages = chat_history
meta_instruction = ('你是考研政治题库,内在是InternLM-7B大模型。你将对考研政治单选题,多选题以及综合题做详细、耐心、充分的解答,并给出解析。')
total_prompt = f"<s><|im_start|>system\n{meta_instruction}<|im_end|>\n"
for message in messages:
cur_content = message['content']
if message['role'] == 'user':
cur_prompt = user_prompt.format(user=cur_content)
elif message['role'] == 'robot':
cur_prompt = robot_prompt.format(robot=cur_content)
else:
raise RuntimeError
total_prompt += cur_prompt
# 将整合的信息存到total_prompt中
total_prompt = total_prompt + cur_query_prompt.format(user=prompt)
return total_prompt
消息合并:将当前会话中所有的用户和机器人消息按照特定格式组合成一个长字符串。
添加当前查询:将用户最新输入的问题(即prompt)添加到整合的对话历史末尾,以构成一个完整的对话提示。
格式化:根据预设的对话提示模板,将每条消息以及最新的用户输入按照角色(user或robot)格式化成特定的字符串。
这个整合后的对话提示将会作为生成模型的输入,帮助模型更好地理解上下文并生成相关的响应。
如果不进行这一步,没有给模型提示“你是考研政治题库,内在是InternLM-7B大模型。你将对考研政治单选题,多选题以及综合题做详细、耐心、充分的解答,并给出解析。”大语言模型将无法回答出经过微调后的正确结果,因此这一步需要格外的注意。
生成响应结果
def generate_response(model, tokenizer, prompt, generation_config):
# 模拟聊天历史(通常是过去交互的列表)
chat_history = []
# 结合历史与当前提示
total_prompt = combine_history(prompt, chat_history)
# 标记化并转换为张量
inputs = tokenizer(total_prompt, return_tensors='pt').to('cuda')
# 生成响应
start_time = time.time()
outputs = model.generate(
inputs['input_ids'],
# 设置最大输入长度
max_length=generation_config.max_length,
# 设置是否使用采样方法生成文本。如果为True,则使用采样方法,否则使用贪婪解码方法。
do_sample=generation_config.do_sample,
# 设置采样时,保留概率累积的阈值。在采样时,会选择率累积超过这个值的最高概率的词.
top_p=generation_config.top_p,
# 设置生成文本时的多样性。较高的温度值会使生成的文本更加多样化,但可能会牺牲一些语义连贯性。
temperature=generation_config.temperature,
# 设置生成文本中重复内容的惩罚因子。较高的值会降低生成文本中重复内容的可能性。
repetition_penalty=generation_config.repetition_penalty
)
end_time = time.time()
# 解码生成的输出
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
generation_time = end_time - start_time
return response, generation_time
合并历史与当前提示:
调用 combine_history 函数,将当前用户输入的提示和先前的对话历史合并为一个完整的输入序列,以便于模型理解上下文。
标记化与张量化:
使用提供的分词器(tokenizer),将合并后的输入序列标记化并转换为模型需要的张量格式。这些张量会被移动到CUDA设备(如果可用)。
生成响应:
调用预训练语言模型(在这里是 model)的 generate 方法,根据提供的输入序列生成响应文本。
通过调整的生成配置(如 max_length, top_p, temperature, repetition_penalty 等参数),控制生成文本的长度、多样性和重复性。
解码输出:
使用分词器的 decode 方法解码模型生成的输出,去除特殊标记并得到最终的生成文本。
性能计时:
记录生成响应所花费的时间,以便评估模型的响应速度。
需要注意的是,使用combine_history
函数准备完整的对话提示,通过将测试提示与模拟的对话历史结合起来。在实际的测试设置中,这个历史记录通常为空,但在实际应用中,它会包括过去的交互记录。
测试模型
def test_model(model_and_tokenizer):
model, tokenizer = model_and_tokenizer
# 读取JSON文件
#将路径修改为自己本地的json测试文件路径
file_path = 'F:/test.json'
#读取json文件
with open(file_path, 'r', encoding='utf-8') as f:
json_data = f.read()
# 将json字符串解析为Python对象
data = json.loads(json_data)
# 初始化两个空列表来存储input和output
# 用于存放测试问题
test_prompts = []
# 用于存放问题的答案
answers = []
# 遍历每个conversation中的内容,提取input和output存入列表
for conv in data:
for item in conv['conversation']:
test_prompts.append(item['input'])
answers.append(item['output'])
#修改大模型的配置参数
generation_config = GenerationConfig(
# 控制允许输入的最大长度
max_length=32678, # 可根据需要调整
# 用于控制采样时,保留概率累积的阈值
top_p=0.8,
# 控制生成文本时的多样性
temperature=0.8,
# 控制是否使用采样方法生成文本
do_sample=True,
# 重复内容的惩罚因子
repetition_penalty=1.005
)
#结果集合
results = []
#响应时间总和
total_time = 0
#响应单词的长度总和
total_length = 0
#索引
i = 0
# 结果正确的个数
right_sum = 0
# 问题个数
total_question = len(test_prompts)
for prompt in test_prompts:
response, generation_time = generate_response(model, tokenizer, prompt, generation_config)
#模型响应的内容
response_t=response[78:].replace(prompt,'')
response_u = response_t[15:]
#响应内容的长度
response_length = len(response_u)
#模型响应时长
total_time += generation_time
#内容的总长度
total_length += response_length
#问题的正确答案以及解析
answer = answers[i]
i += 1
# 模型给出的答案
# 选择ABCD。
model_ans_temp = response_t[15:22]
# 使用正则表达式查找大写字母
t1 = re.findall(r'[A-Z]', model_ans_temp)
# 将结果连接成字符串
model_answer = ''.join(t1)
# 正确答案 选择ABCD。
correct_ans_temp = answer[0:7]
# 使用正则表达式查找大写字母
t2 = re.findall(r'[A-Z]', correct_ans_temp)
# 将结果连接成字符串
correct_answer = ''.join(t2)
#模型输出是否正确
res = (model_answer == correct_answer)
if res:
right_sum += 1
#将结果存在一个results列表当中
results.append({
"prompt": prompt,
"response": response_u,
"answer": answer,
"result": "right" if res else "wrong",
"generation_time": generation_time,
"response_length": response_length
})
#打印信息
print(f"Prompt: {prompt}")
print(f"Response: {response_u}")
print(f"Answer: {answer}")
print(f"Generation Time: {generation_time:.2f} seconds")
print(f"Response Length: {response_length} words")
print('-' * 100)
#计算平均响应时间
avg_time = total_time / len(test_prompts)
#计算平均响应的词的长度
avg_length = total_length / len(test_prompts)
# 计算正确率
right_percent = right_sum / total_question
#打印信息
print(f"Average Generation Time: {avg_time:.2f} seconds")
print(f"Average Response Length: {avg_length} words")
# 导出结果到文本文件
export_file_name = "test_results.txt"
with open(export_file_name, 'w', encoding='utf-8') as f:
for result in results:
f.write(f"Prompt: {result['prompt']}\n")
f.write(f"Response: {result['response']}\n")
f.write(f"Correct Answer: {result['answer']}\n")
f.write(f"Result: {result['result']}\n")
f.write(f"Generation Time: {result['generation_time']:.2f} seconds\n")
f.write(f"Response Length: {result['response_length']} words\n")
f.write('-' * 100 + '\n')
f.write(f"测试题目总数量: {total_question} \n")
f.write(f"正确数量: {right_sum} \n")
f.write(f"回答正确率: {right_percent*100}% \n")
f.write(f"平均响应时间:{avg_time}\n")
f.write(f"平均响应回答的长度:{avg_length}\n")
print(f"Results exported to {export_file_name}")
json测试文件的数据格式:
[
{
"conversation":[
{
"input":"问题1",
"output":"答案1",
"system":"你是考研政治题库,内在是InternLM-7B大模型。你将对考研政治单选题,多选题以及综合题做详细、耐心、充分的解答,并给出解析。"
}
]
},
{
"conversation":[
{
"input":"问题2",
"output":"答案2",
"system":"你是考研政治题库,内在是InternLM-7B大模型。你将对考研政治单选题,多选题以及综合题做详细、耐心、充分的解答,并给出解析。"
}
]
},
{
"conversation":[
{
"input":"问题3",
"output":"答案3",
"system":"你是考研政治题库,内在是InternLM-7B大模型。你将对考研政治单选题,多选题以及综合题做详细、耐心、充分的解答,并给出解析。"
}
]
}
]
问题和答案的提取:
我们首先将JSON数据作为字符串保存在
json_data
中,然后使用json.loads()
方法将其解析为Python对象。接着,我们初始化了两个空列表test_prompts和answers,然后使用嵌套的循环遍历JSON数据,从中提取每个conversation中的input和output,并将它们添加到相应的列表中。这样就可以把json文件中的问题和答案分别提取出来。
生成响应:
对每个测试提示调用 generate_response 函数,利用加载的模型和分词器生成模型对该提示的响应。记录生成的响应内容、生成时间和响应长度。
评估和输出结果:
比较生成的响应与预期的正确答案。
输出每个测试提示的生成响应、正确答案、生成时间和响应长度。
计算和输出平均生成时间、平均响应长度和准确率,以评估模型的性能和生成效果。
导出结果:
将测试结果写入文本文件 test_results.txt,记录每个测试提示的详细生成信息,包括响应内容、正确答案、生成时间、响应长度、准确率、平均响应时间、平均响应长度、正确个数、总测试问题个数等。
最后的txt文件如下图所示:
Response后面的内容为模型输出的答案,Correct Answer为问题的正确答案,result后面为这道题是否答对,通过这种方式,就可以快速的比对模型的输出是否正确,十分方便。
最后附上完整代码:
import pytest
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from dataclasses import asdict, dataclass
from typing import List
import time
import streamlit as st
import json
import re
#配置模型的参数
@dataclass
class GenerationConfig:
#允许用户输入的最大长度
max_length: int = 32768
#用于控制采样时,保留概率累积的阈值。在采样时,会选择概率累积超过这个阈值的最高概率的词。
top_p: float = 0.8
#控制生成文本时的多样性。较高的温度值会使生成的文本更加多样化,但可能会牺牲一些语义连贯性。
temperature: float = 0.8
#布尔值,控制是否使用采样方法生成文本。如果为True,则使用采样方法,否则使用贪婪解码方法。
do_sample: bool = True
#控制生成文本中重复内容的惩罚因子。较高的值会降低生成文本中重复内容的可能性。
repetition_penalty: float = 1.005
#加载预训练的语言模型和对应的分词器(tokenizer),并将它们返回供后续使用。
@pytest.fixture(scope="module")
def model_and_tokenizer():
# 修改为模型的实际路径
model_path = './merged'
# 修改为tokenizer的实际路径
tokenizer_path = './merged'
print("Loading model from:", model_path)
# 加载大语言模型
model = AutoModelForCausalLM.from_pretrained(model_path, trust_remote_code=True).to(torch.bfloat16).cuda()
print("Model loaded successfully.")
# 加载对应的分词器
tokenizer = AutoTokenizer.from_pretrained(tokenizer_path, trust_remote_code=True)
print("Tokenizer loaded successfully.")
return model, tokenizer
#这个变量定义了用户消息的模板格式。在对话历史中,每当有用户输入时,其内容会被插入到这个模板中。
#例如,当程序处理用户输入时,会使用user_prompt.format(user=cur_content)来生成实际的用户消息字符串,然后将其添加到对话历史中。
user_prompt = '<|im_start|>user\n{user}<|im_end|>\n'
#这个变量定义了机器人(或助手)消息的模板格式。类似于user_prompt,当程序需要将机器人的响应添加到对话历史中时,
#会使用robot_prompt.format(robot=cur_content)来生成实际的机器人响应字符串。
robot_prompt = '<|im_start|>assistant\n{robot}<|im_end|>\n'
#这个变量定义了当前查询(或当前用户输入)的模板格式。它用于将最新的用户查询添加到整合的对话历史末尾,
#以便为生成模型提供一个完整的输入。在combine_history函数中,最新的用户输入会以这种格式加入到对话历史的末尾。
cur_query_prompt = '<|im_start|>user\n{user}<|im_end|>\n\
<|im_start|>assistant\n'
#将当前会话的所有消息历史和用户最新的输入整合成一个完整的对话提示。
def combine_history(prompt, chat_history):
messages = chat_history
meta_instruction = ('你是考研政治题库,内在是InternLM-7B大模型。你将对考研政治单选题,多选题以及综合题做详细、耐心、充分的解答,并给出解析。')
total_prompt = f"<s><|im_start|>system\n{meta_instruction}<|im_end|>\n"
for message in messages:
cur_content = message['content']
if message['role'] == 'user':
cur_prompt = user_prompt.format(user=cur_content)
elif message['role'] == 'robot':
cur_prompt = robot_prompt.format(robot=cur_content)
else:
raise RuntimeError
total_prompt += cur_prompt
# 将整合的信息存到total_prompt中
total_prompt = total_prompt + cur_query_prompt.format(user=prompt)
return total_prompt
#使用预训练的语言模型生成对话或文本响应
def generate_response(model, tokenizer, prompt, generation_config):
# 模拟聊天历史(通常是过去交互的列表)
chat_history = []
# 结合历史与当前提示
total_prompt = combine_history(prompt, chat_history)
# 标记化并转换为张量
inputs = tokenizer(total_prompt, return_tensors='pt').to('cuda')
# 生成响应
start_time = time.time()
outputs = model.generate(
inputs['input_ids'],
# 设置最大输入长度
max_length=generation_config.max_length,
# 设置是否使用采样方法生成文本。如果为True,则使用采样方法,否则使用贪婪解码方法。
do_sample=generation_config.do_sample,
# 设置采样时,保留概率累积的阈值。在采样时,会选择率累积超过这个值的最高概率的词.
top_p=generation_config.top_p,
# 设置生成文本时的多样性。较高的温度值会使生成的文本更加多样化,但可能会牺牲一些语义连贯性。
temperature=generation_config.temperature,
# 设置生成文本中重复内容的惩罚因子。较高的值会降低生成文本中重复内容的可能性。
repetition_penalty=generation_config.repetition_penalty
)
end_time = time.time()
# 解码生成的输出
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
generation_time = end_time - start_time
return response, generation_time
#对预训练语言模型进行测试和评估,以确保模型在给定的一组测试提示下能够生成符合预期的响应
def test_model(model_and_tokenizer):
model, tokenizer = model_and_tokenizer
# 读取JSON文件
#将路径修改为自己本地的json测试文件路径
file_path = 'F:/test.json'
#读取json文件
with open(file_path, 'r', encoding='utf-8') as f:
json_data = f.read()
# 将json字符串解析为Python对象
data = json.loads(json_data)
# 初始化两个空列表来存储input和output
# 用于存放测试问题
test_prompts = []
# 用于存放问题的答案
answers = []
# 遍历每个conversation中的内容,提取input和output存入列表
for conv in data:
for item in conv['conversation']:
test_prompts.append(item['input'])
answers.append(item['output'])
#修改大模型的配置参数
generation_config = GenerationConfig(
# 控制允许输入的最大长度
max_length=32678, # 可根据需要调整
# 用于控制采样时,保留概率累积的阈值
top_p=0.8,
# 控制生成文本时的多样性
temperature=0.8,
# 控制是否使用采样方法生成文本
do_sample=True,
# 重复内容的惩罚因子
repetition_penalty=1.005
)
#结果集合
results = []
#响应时间总和
total_time = 0
#响应单词的长度总和
total_length = 0
#索引
i = 0
# 结果正确的个数
right_sum = 0
# 问题个数
total_question = len(test_prompts)
for prompt in test_prompts:
response, generation_time = generate_response(model, tokenizer, prompt, generation_config)
#模型响应的内容
response_t=response[78:].replace(prompt,'')
response_u = response_t[15:]
#响应内容的长度
response_length = len(response_u)
#模型响应时长
total_time += generation_time
#内容的总长度
total_length += response_length
#问题的正确答案以及解析
answer = answers[i]
i += 1
# 模型给出的答案
# 选择ABCD。
model_ans_temp = response_t[15:22]
# 使用正则表达式查找大写字母
t1 = re.findall(r'[A-Z]', model_ans_temp)
# 将结果连接成字符串
model_answer = ''.join(t1)
# 正确答案 选择ABCD。
correct_ans_temp = answer[0:7]
# 使用正则表达式查找大写字母
t2 = re.findall(r'[A-Z]', correct_ans_temp)
# 将结果连接成字符串
correct_answer = ''.join(t2)
#模型输出是否正确
res = (model_answer == correct_answer)
if res:
right_sum += 1
#将结果存在一个results列表当中
results.append({
"prompt": prompt,
"response": response_u,
"answer": answer,
"result": "right" if res else "wrong",
"generation_time": generation_time,
"response_length": response_length
})
#打印信息
print(f"Prompt: {prompt}")
print(f"Response: {response_u}")
print(f"Answer: {answer}")
print(f"Generation Time: {generation_time:.2f} seconds")
print(f"Response Length: {response_length} words")
print('-' * 100)
#计算平均响应时间
avg_time = total_time / len(test_prompts)
#计算平均响应的词的长度
avg_length = total_length / len(test_prompts)
# 计算正确率
right_percent = right_sum / total_question
#打印信息
print(f"Average Generation Time: {avg_time:.2f} seconds")
print(f"Average Response Length: {avg_length} words")
# 导出结果到文本文件
export_file_name = "test_results.txt"
with open(export_file_name, 'w', encoding='utf-8') as f:
for result in results:
f.write(f"Prompt: {result['prompt']}\n")
f.write(f"Response: {result['response']}\n")
f.write(f"Correct Answer: {result['answer']}\n")
f.write(f"Result: {result['result']}\n")
f.write(f"Generation Time: {result['generation_time']:.2f} seconds\n")
f.write(f"Response Length: {result['response_length']} words\n")
f.write('-' * 100 + '\n')
f.write(f"测试题目总数量: {total_question} \n")
f.write(f"正确数量: {right_sum} \n")
f.write(f"回答正确率: {right_percent*100}% \n")
f.write(f"平均响应时间:{avg_time}\n")
f.write(f"平均响应回答的长度:{avg_length}\n")
print(f"Results exported to {export_file_name}")
if __name__ == '__main__':
pytest.main([__file__])
通过这样一个python自动化测试程序,就可以方便的测试大模型的泛化能力,不用耗费那么多的时间来一个一个问题的测试,将结果输出到txt文本文件中,可以很清楚的的看到模型回答正确的个数、模型的正确率、模型回答的答案等,不用再人工统计正确率,节省了大量的时间。