0%

这篇文章来源于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

斯坦福的羊驼alpaca开启了大家微调大语言模型的先河,现在很多国内的工作都是基于斯坦福的羊驼模型的范式来微调chatglm-6B,alpaca的github.

之前我一个很厉害的师兄说过,要想写代码写的厉害或者有所提高,最重要的就是要多读别人优秀的代码,多思考别人为什么这么写,如果让自己写的话是不是可以做到如此高效。这就是我想写这篇博客的原因,我是最近几个月才接触的huggingface的transformer库,发现很多API虽然设计的很简单,但里面功能丰富,不可能一下子就掌握住,所以我的办法是多看别人是如何写的,我的主要参考repo就是alpaca和chatglm-6B官方repo给出的那些finetune LLM的代码。

回到羊驼alpca这份代码,不得不说斯坦福的这份代码写的真的很优秀,值得一句一句去debug。

数据准备部分

代码在generate_instruction.py内。

这部分代码主要功能是实现由seed_tasks.jsonl作为模板,让GPT3.5来根据两个seed生成一些instruction和input,output。思想基于self-instruct的理念,在我另外一篇博客instrcution tuning中有所详细介绍。

这里的启动函数是def generate_instruction_following_data(), 首先会将seed_tasks读取进来,然后从中随机选取num_prompt_instructions个数据,默认是三个:

1
prompt_instructions = random.sample(seed_instruction_data, num_prompt_instructions)

注意这里羊驼采用了向chatgpt传输batch请求的方式,也就是一次性向chatgpt传输多个prompt,程序里默认是5个prompt一起传输给gpt,然后每一个prompt长什么样子呢?

举一个简单的例子, 下面这是一个seed_task

1
{"id": "seed_task_1", "name": "antonym_relation", "instruction": "What is the relation between the given pairs?", "instances": [{"input": "Night : Day :: Right : Left", "output": "The relation between the given pairs is that they are opposites."}], "is_classification": false}

作者将三个seed_task拼接在一起,然后前面加上事先定义好的prompt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def encode_prompt(prompt_instructions):
"""Encode multiple prompt instructions into a single string."""
prompt = open("./prompt.txt").read() + "\n"

for idx, task_dict in enumerate(prompt_instructions):
(instruction, input, output) = task_dict["instruction"], task_dict["input"], task_dict["output"]
instruction = re.sub(r"\s+", " ", instruction).strip().rstrip(":")
input = "<noinput>" if input.lower() == "" else input
prompt += f"###\n"
prompt += f"{idx + 1}. Instruction: {instruction}\n"
prompt += f"{idx + 1}. Input:\n{input}\n"
prompt += f"{idx + 1}. Output:\n{output}\n"
prompt += f"###\n"
prompt += f"{idx + 2}. Instruction:"
return prompt

事先定义好的prompt长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
You are asked to come up with a set of 20 diverse task instructions. These task instructions will be given to a GPT model and we will evaluate the GPT model for completing the instructions.

Here are the requirements:
1. Try not to repeat the verb for each instruction to maximize diversity.
2. The language used for the instruction also should be diverse. For example, you should combine questions with imperative instrucitons.
3. The type of instructions should be diverse. The list should include diverse types of tasks like open-ended generation, classification, editing, etc.
4. A GPT language model should be able to complete the instruction. For example, do not ask the assistant to create any visual or audio output. For another example, do not ask the assistant to wake you up at 5pm or set a reminder because it cannot perform any action.
5. The instructions should be in English.
6. The instructions should be 1 to 2 sentences long. Either an imperative sentence or a question is permitted.
7. You should generate an appropriate input to the instruction. The input field should contain a specific example provided for the instruction. It should involve realistic data and should not contain simple placeholders. The input should provide substantial content to make the instruction challenging but should ideally not exceed 100 words.
8. Not all instructions require input. For example, when a instruction asks about some general information, "what is the highest peak in the world", it is not necssary to provide a specific context. In this case, we simply put "<noinput>" in the input field.
9. The output should be an appropriate response to the instruction and the input. Make sure the output is less than 100 words.

List of 20 tasks:

理解起来就是作者在list of 20 tasks后面跟上了三个seed tasks,也就是给gpt打个样,让它知道按这个模式去生成。这里有个地方值得参考的:

用模板的时候给每一个example配上分隔符,这里作者采用了###, 不仅如此,作者还采用了序号的方式,这些都是为了方便后面对gpt返回的text进行处理

这里还有一个值得学习的地方在utils.py内,我们在使用openai的api获取回复时,有时候会遇到prompt过长的问题,羊驼catch了这个报错,将prompt的长度变为原来的80%,然后再向gpt发送请求,正是由于这份耐心,这个代码的耦合性就没那么高,所以易用性非常强,非常值得野生程序员学习。


gpt返回的文本羊驼模型还做了similarity的计算,将相似度超过一定阈值的instrcution剔除掉。这部分代码可以直接复用。

train中的数据处理

羊驼模型采用了一个函数生成transformers库所需要的参数: make_supervised_data_module,该函数返回一个dict,其中字典的key就是我们初始化Trainer类所需要的train_dataset,eval_dataset和data_collator。这个函数里首先是构建数据集的类SupervisedDataset,继承自Dataset. 注意pytorch里规定如果你想要创建一个自建的Dataset,这个继承自Dataset的子类必须重写__len____getitem__两个方法,羊驼这里写的是:

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
class SupervisedDataset(Dataset):
"""Dataset for supervised fine-tuning."""

def __init__(self, data_path: str, tokenizer: transformers.PreTrainedTokenizer):
super(SupervisedDataset, self).__init__()
logging.warning("Loading data...")
list_data_dict = utils.jload(data_path)

logging.warning("Formatting inputs...")
prompt_input, prompt_no_input = PROMPT_DICT["prompt_input"], PROMPT_DICT["prompt_no_input"]
sources = [
prompt_input.format_map(example) if example.get("input", "") != "" else prompt_no_input.format_map(example)
for example in list_data_dict
]
targets = [f"{example['output']}{tokenizer.eos_token}" for example in list_data_dict]

logging.warning("Tokenizing inputs... This may take some time...")
data_dict = preprocess(sources, targets, tokenizer)

self.input_ids = data_dict["input_ids"]
self.labels = data_dict["labels"]

def __len__(self):
return len(self.input_ids)

def __getitem__(self, i) -> Dict[str, torch.Tensor]:
return dict(input_ids=self.input_ids[i], labels=self.labels[i]) # 这里key必须是input_ids和labels,这是由于是llama模型的规定。

羊驼模型用的DataCollator是自定义的,首先解释下datacollator是什么东西,transformers的官方文档的解释是:

Data collators are objects that will form a batch by using a list of dataset elements as input. These elements are of the same type as the elements of train_dataset or eval_dataset.

也就是你把所有的文本用tokenizer转化成input_ids和labels之后,要把他们组织成batch的形式,不仅如此,collator还能做一些数据处理的工作。它的输入就是我们之前的数据集,注意我们数据集的组织形式每一个数据sample它是一个字典,字典有两个key。所以羊驼这里首先将其拆分, 一句话解决,非常善于利用[ for 句式],让我写的话应该是写成非常冗余的两个for循环。

1
input_ids, labels = tuple([instance[key] for instance in instances] for key in ("input_ids", "labels"))

后面就是很简单的train了

1
2
3
trainer.train()
trainer.save_state()
trainer.save_model(output_dir=training_args.output_dir)

我当时看到这里的时候有点奇怪,因为我之前的习惯还是tensorflow那一套,基本上你把数据处理完之后还要prefetech,batch等,但是这里感觉transformer全部做了集成,可以仔细看羊驼模型的训练启动命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
torchrun --nproc_per_node=4 --master_port=<your_random_port> train.py \
--model_name_or_path "facebook/opt-6.7b" \
--data_path ./alpaca_data.json \
--bf16 True \
--output_dir <your_output_dir> \
--num_train_epochs 3 \
--per_device_train_batch_size 4 \
--per_device_eval_batch_size 4 \
--gradient_accumulation_steps 8 \
--evaluation_strategy "no" \
--save_strategy "steps" \
--save_steps 2000 \
--save_total_limit 1 \
--learning_rate 2e-5 \
--weight_decay 0. \
--warmup_ratio 0.03 \
--lr_scheduler_type "cosine" \
--logging_steps 1 \
--fsdp "full_shard auto_wrap" \
--fsdp_transformer_layer_cls_to_wrap 'OPTDecoderLayer' \
--tf32 True

其中的per_device_train_batch_size就指定了一个device上弄几个数据,也就是batch_size是多少,另外loss计算这些都是pretrained模型定好的,所以不用管,包括optimizer,所以我们在训练的时候只需要指定训练过程中用到的参数,比如保存步数,学习率,训练多少个epoch等。这是finetune大语言模型和之前做深度学习模型不一样的地方,技术往往更新的太快,都快看不懂大家写的代码了,怎么咔咔一两句就开始训练了,所以函数集成太厉害也不是只有好处。

推荐阅读

  • https://mp.weixin.qq.com/s/ehEM04xmeJyqB4z7rmLKBQ 讲解self-instruct方法