0%

斯坦福的羊驼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方法

随着各大语言模型的开源,很多研究在没有计算资源的情况下,唯一可想的办法就是在各大开源模型上利用垂直领域的数据集来finetune开源大模型,充分利用pretrained模型的能力,而又能让它解决特定的任务。 斯坦福的羊驼alpaca模型的发布掀起了instruction tuning的一阵狂潮,国内很多工作也在模仿Stanford这个工作方法做自己领域的大模型,其实在接触这些工作的时候我一直有一个疑问,就是什么时候我们该finetune?手里有多少数据的时候你可以finetune。

如果我们要开展微调,数据以及如何组织数据形式和微调的方式是两个主要问题。

微调的方式

现有的LLM规模太大,因此完全微调模型已并非易事。因此无论学界还是业界都需要一种高效的方法在下游任务数据上训练,这就为参数高效微调(Parameter-efficient fine-tuning,PEFT)带来了研究空间。PEFT的目的是只训练一小部分参数(可以是大模型自身的,也可以是额外引入的)就能提升模型在下游任务的效果。

Scaling Down to Scale Up: A Guide to Parameter-Efficient Fine-Tuning 一文总结了现有peft的主要方法,并做了分类:

PEFT methods分类

可以看到lora也就是我们现在经常用到的微调手段被分配到了reparametrization-based里面。并且在additive这个分类里,又分了两个子类:adapter-like和soft prompts。对于每一个分类的解释可以看paper或者参考这篇知乎博客

我这里只做简单的对比:

  • prefix tuning,prompt tuning根本就是为了解决人工为每一个任务构造prompt太过于随机性,而且构造的模板有时候是离散化的;另外一个痛点就是如果对模型在下游任务上进行全参数调整,每一个任务得保存一份参数副本,对存储和训练都是考验。所以这一类方法就是让模型通过学习来自动寻找最合适的prompt,称之为soft prompt.LLM那一部分的参数不做调整,只调整添加的这一部分参数。之后清华大学提出的ptuning和ptuning v2版本,可以简单的将P-Tuning认为是针对Prompt Tuning的改进,P-Tuning v2认为是针对Prefix Tuning的改进。
  • lora这一类方法(AdaLoRA,Qlora)基于的理论是:将LLM在下游任务进行微调时发现改变的参数都是哪些低秩矩阵,所以这些方法重构了一种低秩矩阵运算,这些矩阵里面的参数就可以代表下游任务的知识,这样将LLM的预训练参数和这部分参数进行合并之后就可以适配下游任务了。

微调实践Applications

这一章节主要介绍一些值得关注的微调实践工作,可以给我们在实际工作中提供一些微调的思路,比如看看别人数据集是如何组织的,有多少的数据量,针对什么任务有了performance的提高,还有做evaluation是咋做的。

Stanford Alpaca

这是开创instruction tuning工作的鼻祖,而且斯坦福的代码写的很优秀,特别是用GPT3生成instruction-input-output那一部分,把代码通读一遍可以加深self-instruct方法的理解。而且斯坦福的这个数据集只有52k条,但是有意思的是它在组织这52k条数据的时候是需要充分保持task的多样性的,毕竟我们知道instruction tuning当时在 Finetuned Language Models Are Zero-Shot Learners 文中被提出的时候,其实是为了提高模型在unseen task上的zero-shot能力,它有一个重要的前提是finetune的task要多样,这个非常重要!

现在有一些国内的工作就是直接弄了一些数据就开始finetune,然后叫自己是instruction tuning,我觉得不太合理。

最近出了一个工作: LIMA: Less Is More for Alignment,在LLaMa-65B基础上用1000条instruction数据训练的模型,在43%的情况下,LIMA可以超过或者和GPT4平齐,这真的很厉害了,毕竟只用了1000条数据,而且作者也用斯坦福的方法复刻了52k微调llama-65B的大羊驼,发现还是LIMA优秀一点,作者猜测是因为数据集质量,这1000条数据是精心策划的。

Chatglm-6B p-tuning

基于chatglm-6B的微调项目超级多,chatglm有天然的中文优势,所以国内好多语言模型都是基于清华的这个语言模型做的工作。chatglm-6B给出的官方github repo中包含了p-tuning v2的代码, p tuning v2的原理就是将应该人工写的那一部分prompt用参数来学习,LLM预训练好的那一部分参数固定住,只更新添加的这部分参数。参考chatglm-6B自己给出的再ADGEN(广告生成的数据集)上finetuneg chatglm6B的代码: https://github.com/THUDM/ChatGLM-6B/tree/main/ptuning, 这部分代码的数据组织部分蛮有意思的,数据集长这样:

1
2
3
4
{
"content": "类型#上衣*版型#宽松*版型#显瘦*图案#线条*衣样式#衬衫*衣袖型#泡泡袖*衣款式#抽绳",
"summary": "这件衬衫的款式非常的宽松,利落的线条可以很好的隐藏身材上的小缺点,穿在身上有着很好的显瘦效果。领口装饰了一个可爱的抽绳,漂亮的绳结展现出了十足的个性,配合时尚的泡泡袖型,尽显女性甜美可爱的气息。"
}

也就是不像alpaca数据集有instruction了,只是一个映射关系,而且在main.py训练代码里也没有见到处理数据sample时要给每一个数据前加instruction,唯一加的就是在content前加了一个“问:”,在summary前加了一个“答:”。

Gorilla

这是一个微调LLaMa-7B让LLM实现用语言方式让LLM返回API调用代码的工作。

参考:

image-20230608133459274

产生数据集的方式跟alpaca如出一辙,利用了self-instruct方式,让GPT-4来产生instruction-API的训练数据。而且它分了两种模式来训练,一种是有retriever的模式,在instruction中添加了数据库里关于这个API的帮助信息,一种就是instruction-API的格式。因为repo里并没有开源这部分的训练数据,所以我们也只能看论文来猜测数据是长这样的,等作者公布了数据可以再补充这部分数据到底长什么样子。

我们在使用这个model进行推理时,输入给他的就是一串我呢本,告诉它你想获得一个怎么样的API,比如“I would like to identify the objects in an image”或者更模糊一点:“I am going to the zoo, and would like to track animals”。在zero-shot模式下这个instruction会直接给到gorilla,模型会给你返回一串API调用的代码,像这样:

image-20230608134439167

总体来说这个项目想法蛮有意思的,就是很多东西暂时还没开源,我们拭目以待吧,现在就用用就行。

让LLM适配specific的下游任务,两条线:1. 在prompt engineering上下功夫 2. Fine-tune LLM. 其实这两条线并不分家,中间也有一些技术是有overlap的。prompt engineering并不只是手动设计prompt让LLM返回更好的结果,使得其在下游任务中得以使用,一些研究并不想自己手动设计prompt,那就产生了很多自动产生prompt的方式。刘鹏飞博士的review文章Pre-train, Prompt, and Predict: A Systematic Survey of Prompting Methods in Natural Language Processing将这些技术统一到一个体系里来,分类方式也比较清晰:

characteristics of different tuning strategies

Full Fine-tune(Promptless Finetune)

Bert就是一个典型的应用,将模型在一个很大的语料库上pretrain之后,再在一些任务的数据集上对模型参数进行调整。注意这里模型的所有参数都会进行调整。

image-20230519142832464

对所有模型参数调整就带来很多问题:

  • 要维护每一个task上的模型,有一些模型的参数量都是亿级别的,这对存储是一个考验
  • finetune所有参数就需要数据集达到一定的数量级,这在特定领域不一定是可以达到的;如果没有很多数据,有可能finetune完之后还会引起perfomance的下降或者过拟合。
  • 计算资源的限制

More Efficient Ways of Tuning

或许有更合适的tuning方式,less overfitting and more efficient finetuning and inference

Prefix-Tuning

Prefix-Tuning: Optimizing Continuous Prompts for Generation

prefix Tuning

一开始理解prefix tuning其实是从“如果不调整所有参数,那么是不是可以调整部分参数”来思考这个模型的。但是看了原文之后会发现作者的思考路径其实有点不太一样。paper中说

Prefix-tuning draws inspiration from prompting, allowing subsequent tokens to attend to this prefix as if it were “virtual tokens”

lilian的博客对于这个也解释的蛮有意思:

Prompt is a sequence of prefix tokens that increase the probability of getting desired output given input. Therefore we can treat them as trainable parameters and optimize them directly on the embedding space via gradient descent, such as AutoPrompt (Shin et al., 2020, Prefix-Tuning (Li & Liang (2021)), P-tuning (Liu et al. 2021) and Prompt-Tuning (Lester et al. 2021). This section in my “Controllable Neural Text Generation” post has a good coverage of them. The trend from AutoPrompt to Prompt-Tuning is that the setup gets gradually simplified.

也就是既然我们发现in-context learning是可以促进大语言模型解决特定问题(因为我们让LLM以更高的概率输出我们想要的结果了),那么是不是可以可以把这一部分信息编码进模型参数里,从而在特定地数据集上单独训练这些参数。

所以研究者也想了一些办法如何以最小的成本为特定的任务增加一些参数,fix住预训练模型的大部分参数,而去finetune给每一个任务增加的那一部分参数。其中adapter-tuning就是一种。