基于Transformers的LLM微调,以及改写模型的一些Tips
前言
总感觉生活像一组连续剧呢,上一集结束,下一集开始,主角一直是那个人,事情也确实能寻出因果。之所以突然这么想,是我突然意识到,作为生活中的社恐人士的我,好像特别喜欢在每一篇“技术文章”开头写一点自己的想法呢。 如此说来,或许如果有谁恰好看到了我写的一系列文章,大抵会有一种看连续剧的感觉吧。抱歉啦,耽误各位大佬的时间看我这些奇怪的想法。总体而言,自从分手后,心情一直不是很好呢,总感觉整个人处于一种动态平衡之中(相信爱情与不相信爱情),此前沉迷于爱情,确实对研究疏忽了许多,分手后遇到了好的导师,遇到的好的学长、学妹,这是否也是一种动态平衡呢?这倒确实是一个做研究的好机会,也希望大家能不嫌弃我的无能,我也会尽自己最大的努力完成大家的梦想吧。
Transformers对LLM的使用
Transformers库在python中,如果你要做大语言模型,又用过huggingface,那么一定不会陌生,虽然我也不知道这个库从何而来,但是会用大概就行了,下面简要介绍一下LLM最基本的使用流程。
基本流程
这里主要探讨通用的文本生成模型,即指诸如GPT、Bert、Llama等的BaseModel,其最原始的目的为文本生成。
给定一个文本输入
T
T
T,我们首先需要使用模型对应的tokenizer对其进行编码,使其成为Tokens序列的形式,从而才能被模型读取作为输入。即有:
I
=
t
o
k
e
n
i
z
e
r
(
T
)
I = tokenizer(T)
I=tokenizer(T)
在Transformers中,tokenizer的加载通常而言需要使用Transformers提供的类AutoTokenizer,即:
tokenizer = AutoTokenizer.from_pretrained("./testModel")
I = tokenizer( \
T, \
max_length=self.max_length,\
padding=False,\
truncation=True,\
return_tensors="pt",\
add_special_tokens=False,\
)
之后将编码所得的结果(一般包含tokens和attention_mask) 作为模型的输入即可:
model = AutoModelForCausalLM.from_pretrianed("./testModel")
ret = model(**I, return_output=True)
需要注意的是,直接这样调用,返回的ret值,实际上是下一个token的logits值,即返回字典中每个词出现在下一个token位置的logits值。一般训练过程实际上也是这样做的,通常为对于给定的一句话,通过一个滑动的定长窗口,截取当前一句话作为输入,对下一个词和模型输出做CrossEntropyLoss,从而优化模型结果。
在测试阶段、或者应用阶段,则可以使用model.generate()函数,直接生成模型的全部输出,此时的输出为一句话的tokens(包含输入),此时就可以直接Decode得到人话了。
AutoModelForCausalLM与AutoTokenizer如何Auto
AutoModelForCausalLM与AutoTokenizer作为Transformers中最常用的,也是最简单的类,其最好的性质就是可以自动加载各式各样的模型,而不需要我们手动根据不同的模型专门写不同的名字,这种统一给我们写代码提供了巨大的方便。
那么这种自动是如何实现的呢?这取决于我们下载的大模型文件中,总是包含一个config.json文件,其中记载着模型的具体信息:
上图即是gemma2-9b的config.json的内容,可以看到其中记录了architecture为Gemma2ForCausalLM,如此我们就可以明白,实际上在最终的加载中,模型是以Gemma2ForCausalLM类加载的,而AutoTokenizer的加载目前我没有特别想到有什么不同,我们直接使用模型路径即可。因为每个模型的字典往往也会以Json的形式存储下来。
基于BaseModel的Continous Pretraining和Supervised Finetune
一个BaseModel拿过来不加任何调整,我们想要其高质量得实现想要的特定任务,这显然是不可能的,因为BaseModel通常只会在某些大的文本数据集上训练,对特定的领域的训练显然是没有的,因此,我们通常需要将其在我们自己的数据集上进行所谓的Continous Pretraining或Supervised Finetune。具体的实现过程就是像前文所述,做Next Token Prediction,然后计算CrossEntropyLoss。
可能有人会问Continous Pretraining和Supervised Finetune的区别是什么,或者两者有什么关系。就我目前肤浅的理解,Continous Pretraining更多的是一种Pretrianing,即在大量文本的基础上进行训练,其目的是为了让模型了解特殊领域一些术语的使用方式,或理解领域知识和逻辑。而Supervised Finetune则更多的是针对任务进行训练,其目的是为了让输出符合任务的形式,这一阶段的数据就不再是大量的纯文本,更多的则是向问答,比如输入是问题,输出的回答这样的成对数据。只有经过这一阶段的序列,模型的输出才能稳定按照某一个格式进行。
通过继承写一个自己的模型
通过继承Transformers的模型类来完成我们自己的模型,相比于直接继承自nn.Module,有许多好处。两者虽然都能正常完成forward,backward,但是关键在于,我们该如何加载Transformers模型的参数呢?聪明的小伙伴可能会说,你直接重写一个from_pretrained不好了——说的没错,这样做确实可以,可是大家有没有考虑过后事。加载可以这样做,那我如何保存呢?如果使用开源框架,别人的框架是基于Transformers模型写的,我该怎么办?全部重写吗?以及诸如DeepSpeed加速,我该怎么加速一整个模型呢?(这是因为DeepSpeed模型的参数加载需要按照json中的切片进行加载,如果继承自nn.Module,模型中会找不到对应的weight从而报错,这时你就只能拆开)。
综上所述,我们选择继承自transformers的模型是十分有必要的,这种继承可以帮我们省去许多不便利。
- 第一步,确定想要继承的模型的具体类型。这里可以参考前面提到的Auto原理,找到模型对应的具体类,继承这个类,则可以继承其weight的名称,从而不需要我们完全重写一摸一样的weight名字才能完成DeepSpeed的参数加载。
- 第二部,继承这个类,再在init函数里加上自己想要的定值层,比如MLP等,注意,这时模型中不需要调用AutoModelForCausalLM.from_pretrained来读取路径,而应该直接super().init(dir),因为我们已经继承了这个类,直接调用这个类的初始函数即可,同时from_pretrained就直接使用的父类的这个函数。
- 第三步,重写forward。这里需要注意的是,想要调用父类进行推理,直接使用super().forward()即可。一般而言获取其last_hidden_state用于后续的计算。有些模型的last_hidden_state的名字不是固定的,可能是别的,这里就需要自己找了。