0%

最近在关注模型performance评估的问题,打算在这个主题上做一个整理,也是受到很多博客和文章的启发写这篇文章,所以就将所有推荐阅读的文章放在前面,感兴趣的小伙伴可以拓展阅读。

  1. 老刘说NLP 公众号中8.10发的一篇文章《如何让自己的大模型榜单评分更高》 这篇文章有点借鉴了hugging face的Open LLM 排行榜近况
  2. https://mp.weixin.qq.com/s/WiF3yRU5MZS7ulLTP8FIpw

首先说一下这个LLM榜单, 有四个benchmark,其中上面的博客就是重点讲了为什么同样一个模型比如LLaMA在MMLU上评测的结果会不如llama文章中提的效果,trick就在作者使用MMLU这个benchmark的方式有很大不同,这里来看看MMLU这个benchmark。

MMLU benchmark

首先看一下这个数据集到底是什么数据集,长什么样子,先给出文章中的定义:

MMLU (Massive Multitask Language Understanding) is a new benchmark designed to measure knowledge acquired during pretraining by evaluating models exclusively in zero-shot and few-shot settings.

这个评测集合里包含了57个学科,也就是57个task。原始的数据集长这样,里面的每个问题包含四个可能选项,且每个问题只有一个正确答案。:

image-20230816145305589

可以看到基本上就是question,answer的组织。注意这里看到原始数据的时候我还有点没看明白,作者的readme中也没写,还是对beginner有点不友好,第一列表示question,第二到第四列表示四个选项,最后一列是答案。所以可以看到原作者在evaluation的代码中这样处理的:

1
2
3
4
5
6
7
8
9
choices = ["A", "B", "C", "D"] # 首先定义选项

def eval(args, subject, engine, dev_df, test_df):
cors = []
all_probs = []
answers = choices[:test_df.shape[1]-2] # 对于每一个csv文件读取进来后取answers
...

label = test_df.iloc[i, test_df.shape[1]-1] # label这里其实是取得最后一列,也就是答案

但这个评测数据集在用来评测LLM的过程中衍生出了很多版本,基本是prompt的变化:

MMLU的不同实现

同样的问答对,比如上面的选择题,Harness没有指令,并且衍生的两个版本也就是helm和harness版本还加了Question这个前缀,harness在选线之前还加了Choices。就这么一点差距,就导致同一个llm的出来的分数不一样:

LLM在不同MMLU实现上的评分

关于如何使用这个benchmark,参考MMLU原始实现,作者写的是用chatgpt来产生答案,prompt为:prompt = "The following are multiple choice questions (with answers) about {}.\n\n".format(format_subject(subject))

这三种实现方式不仅prompt的形式不同,也就是上面提到的。并且它在计算F1score的时候的机制也不同。

  1. 原始实现

在原始实现中的评估的代码是这样写的:

1
2
3
4
5
6
7
8
9
10
11
12
for ans in answers:
try:
lprobs.append(c["choices"][0]["logprobs"]["top_logprobs"][-1][" {}".format(ans)]) # c是chatgpt的回答
except:
print("Warning: {} not found. Artificially adding log prob of -100.".format(ans))
lprobs.append(-100)
pred = {0: "A", 1: "B", 2: "C", 3: "D"}[np.argmax(lprobs)]
probs = softmax(np.array(lprobs))

cor = pred == label
cors.append(cor)
all_probs.append(probs)

该方法在评估的时候,仅仅比较了模型对四个选项字母的预测概率,哪个选项的概率高就选哪个,即便是在极端情况下四个选项的概率值都很低的情况下也会选择某个选项,但其实模型有时候会回答很多不相关的东西(都是很高的概率的token),所以这种方式有点”放水“,整体评估出来的分数会偏高。

  1. HELM实现

HELM实现是根据模型预测的下一个输出词元的概率来选择输出文本,并将生成的文本与正确答案的文本进行对比。这种方式有效避免了如果模型的答案中出现概率高的token不是选项中的任意一个,那么就会判为错误答案。

看了helm的代码仓库,着实有点丰富。内容很多我都没有找到在哪个文件里做的evaluation的计算,只知道了读取csv的地方。有好心的小伙伴可以私信我告诉我在哪里。

  1. harness实现

这是hugging face的llm榜单所用的实现。它不再是只是统计选项,而是连同选项字母以及后面的答案一起被考虑进来,计算的是整个序列的概率(获取每个词元的概率 (与上面其他实现一样) 并求它们的联合概率),那么很容易一些长文本的联合概率会比短文本的联合概率大,所以作者说可以在联合概率的基础上在做一个归一化,也就是用对数联合概率/ token数。

MMLU三种实现对于模型输出的总结

例如实现如下,基于GPT2计算句子联合概率的一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
data = [
"A multilayer perceptron (MLP) is a class of feedforward artificial neural network (ANN)",
"The term MLP is used ambiguously, sometimes loosely to any feedforward ANN, sometimes strictly to refer to networks composed of multiple layers of perceptrons (with threshold activation); see § Terminology",
'Multilayer perceptrons are sometimes colloquially referred to as "vanilla" neural networks, especially when they have a single hidden layer.[1]',
"An MLP consists of at least three layers of nodes: an input layer, a hidden layer and an output layer. Except for the input nodes, each node is a neuron that uses a nonlinear activation function.",
]
model = transformers.GPT2LMHeadModel.from_pretrained("gpt2")
tok = transformers.GPT2Tokenizer.from_pretrained("gpt2")
tgs = []
for dat in data:
random.seed(dat)
# print(model(tok.encode(dat, return_tensors="pt"))[0][0])
toks = tok.encode(dat, return_tensors="pt")
ind = random.randrange(len(toks[0]) - 1)
logits = F.log_softmax(model(toks)[0], dim=-1)[:, :-1] # [batch, seq, vocab]
res = torch.gather(logits, 2, toks[:, 1:].unsqueeze(-1)).squeeze(-1)[0]
tgs.append(float(res[ind:].sum()))

在“老刘说NLP”的博客中也提到了一点,就是上面的方式都是开源模型,所以很容易就能得到每一个token的预测概率,所以返回结果可以拆的这么细致来分析。如果是闭源模型只返回response的话,这时候就需要用正则的方式来抽取回答内容里的选项,比如CEVAL的测试方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def extract_cot_answer(self, line, gen_ans):
m = re.findall(r'所以答案是(.+?)。', gen_ans, re.M)
if len(m) > 0 and m[-1] in self.choices:
return m[-1], True
answer_patterns = [
r'([ABCD])是正确的',
r'选项([ABCD])正确',
r'答案为([ABCD])',
r'答案是([ABCD])',
r'答案([ABCD])',
r'选择([ABCD])',
r'答案:([ABCD])',
r'选择答案([ABCD])'
]
# RE extraction
for answer_pattern in answer_patterns:
m = re.search(answer_pattern, gen_ans, re.M)
if m:
answer = m.group(1)
return answer, False
# only containing one choice-character
m = re.findall(r'[ABCD]', gen_ans, re.M)
if len(m) == 1:
answer = m[0]
return answer, False
answer_word_counter = 0
# only containing one choice-context
for c in self.choices:
if str(line[f'{c}']) in gen_ans:
answer = c
answer_word_counter += 1
if answer_word_counter == 1:
return answer, False
return '-', False

对CLEVA评测平台感兴趣的可以看原文paper或者参考文章。原文说CLEVA是专门为评估中文语言模型而设计的平台。

这篇文章来源于liianwen的blog,初看这篇博客时感觉太多新技术看不懂,再者今天突然看到新智元公众号发了一篇文章,乍一看特别熟悉,对比了下确实是完全照搬翻译,让人读起来一头雾水,不仅如此,跟原博客相比缺失了很多内容。

强烈建议先食用blog, 很通俗的讲解了LLM发展到现在成为Agents的原因,这里引用博客中的一句话:

Recent months have seen the emergence of a powerful new trend in which large language models are augmented to become “agents”—software entities capable of performing tasks on their own, ultimately in the service of a goal, rather than simply responding to queries from human users

也就是研究人员已经不满足于让LLM仅仅是根据query回答问题,更希望它能帮我们完成一些任务,成为我们工作生活的“助手”,就好像你有一个秘书一样,你让他去定一个航班,秘书可能会进行一系列的操作,比如他要考量你的时间安排,还要考虑航班的情况等等,你最终就是拿到了秘书给你的机票,但其实秘书在中间做了超级多事情。那我们现在就希望能把LLM培养成这样的角色,他不仅能接受命令还能自己做决策,然后把任务完成了。刚刚提到的博客里还讲了一个购买车的例子。

那我们知道我们的终极目标是要实现一个高级别的私人助理,那么实现这个目标需要哪些技术呢,这时候才到了lilian wen的这篇博客部分。引言就是现在一些agents的雏形比如autoGPT, GPT-engineer和BabyAGI出现了。

lilian的博客认为agents是以LLM作为大脑,配置三个主要的components:planing,memory和tool。Planning主要是将复杂任务拆分,不仅如此它还要负责自我反省,吸取以往错误的教训,从而能够产生更好的结果。

overview of a llm-powered autonomous agent system

Memory包含短期记忆和长期记忆,前者可以理解成in-context learning中应用的记忆,后者主要是应用外部的向量数据库或者本地知识库抽取的知识。Tool就是agent可以拥有调用各个外部API的能力,就像你的武器库一样,不同的武器适合不同的作战场景,这些API就可以弥补预训练完的模型所欠缺的能力,比如对于当下实时信息的获取。

Component 1: Planning

拆解任务有两种主流办法:1. CoT chain of thought 2. Tree of Thoughts

前者被讲烂了,后者是对CoT的扩展,将任务拆解成一个子任务树,然后采用宽度优先搜索或者广度优先搜索的方式去决定接下去先解决哪个子任务。

planning这个子模块还有一个更重要的功能就是自我反思,人都是需要从错误中进步的,大语言模型也是一样。思想有点类似于增强学习。首先讲到的是ReAct, 说实话lilian博客里写的这一段我没看懂,所以还是找原文paper来看了下。

comparison results

从上面的例子就可以理解作者提出的办法就是将thought和action结合起来了,也就是单纯的思考比如chain of thought并不能很好的回答问题,受制于预训练模型自己模型内存储的知识,而如果只有action呢?就是不停的去搜索,如果搜索不到正确的答案那也是白搭。其实我理解就是作者提出我们要做一个通用的人工智能,你要告诉他在行动的时候也要思考,思考清楚之后再去考虑下一步已经采取什么样的行动,同时每一次行动也会从环境中得到反馈,比如作者举的第二个例子,你去countertop(台面)的时候,你看到了苹果,面包,胡椒粉瓶子和一个花瓶,既然我们要把胡椒粉瓶子放到抽屉里,那就可以拿走胡椒粉瓶子啦!其实这也好理解,一个优秀的人其实也是要边做边思考的,所以就形成了作者提出的prompt新范式:

1
2
3
4
Thought: ...
Action: ...
Observation: ...
... (Repeated many times)
frames

近期中文大语言模型出现了好多产品,独领风骚的chatglm-6B确实表现很不错,所以大家就纷纷下场想做一个自己领域的大语言模型,最近看到一篇Lawyer LLaMA Technical Report,作者基于llama模型做了一个法律的语言模型。我觉得这篇文章值得想在垂直领域做语言模型的研究者参考,特别是它所采取的路径:

training process of lawyer LLaMA

也有博客粗略介绍了这篇文章。

我重点关注的是这个工作里的信息抽取:

image-20230630110134378

思路大概就是和斯坦福的dsp一个思路,用户问一个问题,第一种方式就是直接把这个问题抛给llm,让它去回答,第二种就是在把问题抛给llm之前先过一道retrival模型,这个模型会抽取一些和回答这个问题相关的一些context,将这些context加入prompt中,然后再抛给llm。这有一个专门的名词,在Demonstrate-Search-Predict: Composing retrieval and language models for knowledge-intensive NLP 中有所介绍: retrieve-then-read。有兴趣的可以再看一下斯坦福在做这部分工作时出的notebook介绍,看完你就知道为啥要retrieve一些context加入到prompt中去。

可惜的是laywer llama这篇文章并没有详细的介绍它的retrieve咋做的,文章中也是一笔带过,数据规模包括组织形式一概没说。我主要是参考斯坦福的dsp这框架中retrieve的实现,它是基于ColBERTv2做的抽取模块,但其实colbert预训练的模型是在微软的msmarco数据集上训练的,也就是没有中文,那必然对中文的适配度应该就不太行,所以我就想找找有咩有好心的博主写过自己用colbert在自己组织的语料库上训练的过程,发现没有,所以这就是我写这篇文章的动机啦,希望能给后面想用自己组建的数据集训练一个colbert做信息抽取的童鞋一点参考。

以下的内容参考colbert github

数据组织

模型使用

用官方训练好的checkpoints

参考https://github.com/stanford-futuredata/ColBERT/blob/main/docs/intro.ipynb

里面的输就LoTTE的url已经失效了,包括checkpoints的下载地址。可以到hugging face的仓库找

  • colbertv2.0 checkpoints : https://huggingface.co/colbert-ir/colbertv2.0/tree/main
  • lotte 数据集地址: https://huggingface.co/datasets/colbertv2/lotte/tree/main

注意上面这个lotte的数据集的组织方式和colbert官方github仓库里的介绍不一样,这一点上colbert2这个代码仓库的维护做的蛮糟糕的,比羊驼可差远了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import os
import sys
sys.path.insert(0, '../')

# 下面这些包是colbert写好的类,可以导入预训练的checkpoints和你自己的querys,documents
from colbert.infra import Run, RunConfig, ColBERTConfig
from colbert.data import Queries, Collection
from colbert import Indexer, Searcher

dataroot = 'downloads/lotte' # 假设你已经把lotte放到downloads文件夹下
dataset = 'lifestyle'
datasplit = 'dev'

queries = os.path.join(dataroot, dataset, datasplit, 'questions.search.tsv') # questions.search.txv没找到
collection = os.path.join(dataroot, dataset, datasplit, 'collection.tsv') # collections.tsv也没找到

queries = Queries(path=queries)
collection = Collection(path=collection)

f'Loaded {len(queries)} queries and {len(collection):,} passages'

# indexing 主要用于对所有的document进行index
nbits = 2 # encode each dimension with 2 bits
doc_maxlen = 300 # truncate passages at 300 tokens

checkpoint = 'downloads/colbertv2.0'
index_name = f'{dataset}.{datasplit}.{nbits}bits'

with Run().context(RunConfig(nranks=4, experiment='notebook')): # nranks specifies the number of GPUs to use.
config = ColBERTConfig(doc_maxlen=doc_maxlen, nbits=nbits)

indexer = Indexer(checkpoint=checkpoint, config=config)
indexer.index(name=index_name, collection=collection, overwrite=True)

# search
with Run().context(RunConfig(experiment='notebook')):
searcher = Searcher(index=index_name)
query = queries[37] # or supply your own query

print(f"#> {query}")

# Find the top-3 passages for this query
results = searcher.search(query, k=3)

# Print out the top-k retrieved passages
for passage_id, passage_rank, passage_score in zip(*results):
print(f"\t [{passage_rank}] \t\t {passage_score:.1f} \t\t {searcher.collection[passage_id]}")

ok 偶然看到官方仓库昨天更新了intro的notebook,我看到里面下载数据集的仓库变化了,然后导入包的时候也写得更清晰了,加了条件:https://github.com/stanford-futuredata/ColBERT/blob/main/docs/intro2new.ipynb