【tiny-universe】llm eval:动手实现大模型评测系统

1 大模型评测

传统的模型框架都会有写好的测评框架,使用广泛认可的公开数据集来说明模型的任务解决能力。
大模型的各种垂直任务的测评都不相同,并且公布模型的时候,训练、评测的数据集不会公开。
做测评的时候,有两个核心的问题

  • 怎么选择metric
  • 怎么根据metric进行分词
    实际上,上述两个问题在不同的模型和不同的垂直任务中有不同的答案,没有统一的范式。

选定好metric后,一整个评测的pipeline如下
在这里插入图片描述
选定metric后,根据目标数据的形式总结模型的引导prompt,根据模型初步预测结果采纳合理的抽取方式,对相应的pred和answer进行得分计算。

2 实验数据集和metric

数据集地址为
dataset

一共有三个任务,根据输出的类型,选择对应的meric如下:
| name | type | metric|
|multi_news|长文本问答| Rouge|
| multifieldqa_zh | 短文本问答 | F1|
|trec|生成式选则| accuracy|

3 评测过程讲解

作者给出的讲解示例中有短文本问答的详细讲解。

3.1 生成式的F1

下面说明多领域问答任务的metric的选择原因。
传统的分类任务中,会采用F1指标。
F1 指标(F1 Score)是用于评估分类模型性能的统计指标,特别适用于二分类问题。它综合考虑了模型的精确率(Precision)和召回率(Recall),提供了一个衡量模型准确性和完整性的平衡值。
那么对于一个短文本问答任务,模型的输出是很简短的,主要是因为我们可以根据匹配结果获取简短有针对性的答案。
比如提问9月有多少天,其实答案就是30,那么只要我们对模型生成的答案进行提取,就可以找到其中的30天来和数据集的标准答案进行是否完全相同的对比。
那么模型的输出其实可以完全和数据集的答案进行对比,和分类任务的标签对比相同,因此可以用F1。
针对生成式的F1,计算代码为

def f1_score(prediction, ground_truth, **kwargs):
	# 以字典的形式存储各个句子对应的词和个数, &取其交集,表示生成的答案和数据集的标签相同
	common = Counter(prediction) & Counter(ground_truth)
	# 计算个数
	num_same = sum(common.values())
	if num_same == 0:
		return 0
	# 精确度:即模型预测正确的样本数量与总预测样本数量的比值
    precision = 1.0 * num_same / len(prediction)
    # 召回率:模型正确预测的样本数量与总实际样本数量的比值         
    recall = 1.0 * num_same / len(ground_truth)           
    f1 = (2 * precision * recall) / (precision + recall)
    return f1

举个例子:

"pred": "57081.86元", "answers": "人民币57081.86元。"

经过分词和数据清洗

"pred": ['5708186', '元'], "answers": ['人民币', '5708186', '元']"

那么couter就会提取两者中具有相同数量的词元,匹配上了就是common。在这个例子中,匹配上的结果就是[‘5708186’, ‘元’]。
此时就算输出的结果和答案完全相同的匹配。

这样的提取规则其实并不完美。如果出现了比如answer是"厦门大学",而pred是"不是厦门大学"/“厦大”, 其实问答任务的成功和失败的判断是会出错的,则按照当前的评分指标则是有失偏颇的。

3.2 pipeline的实现

有一个训练好的模型,使用第2节中给出的数据集,采用F1,对短文本问答任务进行评测。

3.2.1 模型推理

首先需要引导模型生成答案,这个时候需要给模型一个引导的prompt,也就是告诉模型,我有一个什么样的任务,我需要你来完成什么工作,给出什么样的回复。
这个引导的prompt一般评测数据集都会提供。

阅读以下文字并用中文简短回答:\n\n{context}\n\n现在请基于上面的文章回答下面的问题,只告诉我答案,不要输出任何其他字词。\n\n问题:{input}\n回答:

接下来需要制定模型,这个模型需要完成的任务有

  • 加载指定的模型和tokenizer
  • 读取数据集生成prompt,
  • 通过model chat得到生成的输出,在这个任务中,可以看成是获取predction
  • 对输出进行处理,提取出需要的答案
  • 生成的内容必然没有统一的结构,此时需要从中提取出我们需要的问题的答案。

prompt会因为数据集而具有不同的长度,使用模型进行推理的时候需要配置输入长度,也就是配置模型的max_token,这个值需要比模型的max_position_embeddings短或相等。不然到了模型层也会进行限制。

根据上述任务,创建评测中的模型基类如下

class BaseLLM:
	def __init__(self, path:str, model_name:str)->None:
		self.path = path
		self.model_name = model_name
	def build_chat(self, tokenizer:str, prompt:str, model_name:str):
		pass
	def load_model_and_tokenizer(self, path:str, model_name:str, device):
		pass
	def post_process(self, response:str, model_name:str):
		pass
	def get_pred(self, data:list, max_length:int, max_gen:int, prompt_format:str, device, out_path:str):
		pass

上述函数的实现根据使用模型来写,比如,build_chat需要把数据集的引导prompt加载进来后,换成模型固有的数据加载模型。

# internlm2为例
def build_chat(self, prompt):
        prompt = f'<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant\n'
        return prompt

最大的要点在于get_pred不能简单地直接用model.chat()来获取输出。
因为:

原因就在于截断策略,对于模型而言,尤其是制定了输入的长度,如果使用阶段命令则其会在输入的末尾进行阶段,但由于引导性prompt的存在,在inputs的两端均有关键信息,故需要对两端的信息进行保留,对中间部位进行截断操作,才能最大限度地抱持输出效果!

那么实现的时候,就可以参考model的chat函数,进行仿写。可以参考,这里的代码为:

def get_pred(self, data, max_length, max_gen, prompt_format, device,out_path):
	model, tokenizer = self.load_model_and_tokenizer(self.path, device)
	for json_obj in tqdm(data):
		prompt = prompt_format.format(**json_obj)
		tokenized_prompt = tokenizer(prompt, truncation=False, return_tensors="pt").input_ids[0]
		if len(tokenizeed_prompt) > max_length:
			half = int(max_length/2)
			prompt = tokenizer.decode(tokenized_prompt[:half], skip_special_tokens=True)+tokenizer.decode(tokenized_prompt[-half:], skip_special_tokens=True)
		prompt = self.build_chat(prompt)	
		input = tokenizer(prompt, truncation=False, return_tensors="pt").to(device)
		context_length = input.input_ids.shape[-1]
		eos_token_id = [tokenizer.eos_token_id, tokenizer.convert_tokens_to_ids(["<|im_end|>"])[0]]

        output = model.generate(
             **input,
             max_new_tokens=max_gen,
             do_sample=False,
             temperature=1.0,
             eos_token_id=eos_token_id,
         )[0]
         
         pred = tokenizer.decode(output[context_length:], skip_special_tokens=True)
         pred = self.post_process(pred)
         
         with open(out_path, "a", encoding="utf-8") as f:
             json.dump({"pred": pred, "answers": json_obj["answers"], "all_classes": json_obj["all_classes"], "length": json_obj["length"]}, f, ensure_ascii=False)
             f.write('\n')

4 总结思考

其实大模型评测也就是根据特定数据集使用大模型得到特定输出的过程,通过学习了一个小demo,让我对评测的pipeline和一次思路有了理解。
因为时间问题没有自己试着跑一下代码,后续在更新~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值