如何进行参数高效微调LLM

在这个人工智能飞速发展的时代,咱们得琢磨琢磨,怎么用更聪明的办法来用好这些大型语言模型。这不仅仅是为了省事儿,更是为了环保,减少能源消耗和碳排放。

现在,有种特别火的方法叫做“参数高效微调”,这招儿就是在不增加太多计算负担的情况下,让那些已经训练好的模型能更好地适应新任务。这样,咱们就能在各种设备上训练AI了,哪怕是那些计算能力不是很强的,比如笔记本、手机或者物联网设备。

咱们来聊聊现在流行的几种参数高效微调方法:前缀调整、适配器,还有LLaMA-适配器。这篇文章会带你深入了解微调到底是怎么一回事,还会探讨这些流行的方法。最后,我们还会看看最近挺火的LLaMA-适配器,教你怎么在实际中用它来提升模型表现。

微调大型语言模型

自从GPT-2( https://d4mucfpksywv.cloudfront.net/better-language-models/language_models_are_unsupervised_multitask_learners.pdf , Radford等人)和GPT-3( https://arxiv.org/abs/2005.14165  , Brown等人)以来,我们发现在一般文本语料库上预训练的生成性大型语言模型(LLM)能够进行上下文学习,这不需要我们进一步训练或微调预训练的LLM,如果我们想执行LLM没有明确训练过的具体或新任务。

上下文学习是一种有价值且用户友好的方法,适用于直接访问大型语言模型(LLM)受限的情况,例如,当我们通过API或用户界面与LLM交互时。

然而,如果我们能够访问LLM,使用目标领域数据对目标任务进行调整和微调通常会得到更好的结果。那么,我们如何使模型适应目标任务呢?下面图中概述了三种传统方法。

图片

上述方法适用于生成性(解码器风格)模型,如GPT,以及以嵌入为中心(编码器风格)的模型,如BERT。与这三种方法相比,上下文学习仅适用于生成性模型。还值得一提的是,当我们微调生成性模型时,我们使用并构建它们创建的嵌入,而不是生成的输出文本。

基于特征的方法

咱们用一种基于特征的方法,就是先弄一个现成的超大型语言模型(LLM),然后把它用在我们的目标数据集上。我们主要想干的是,给目标训练集整出一些输出嵌入,这些嵌入就能当训练分类模型的输入特征。虽然这个方法特别适合那种以嵌入为中心的BERT模型,但我们也能从那种生成式的GPT风格的模型里头提取嵌入。

接下来,分类模型可以是逻辑回归、随机森林,或者是XGBoost——就是说,咱们爱用啥就用啥。(不过,根据我的经验,像逻辑回归这样的线性分类器在这种情况下效果最好。)

概念上,我们可以用以下代码来说明基于特征的方法:

model = AutoModel.from_pretrained("distilbert-base-uncased")

# ...

# tokenize dataset

# ...

# generate embeddings

@torch.inference_mode()

def get_output_embeddings(batch):

    output = model(

        batch["input_ids"],

        attention_mask=batch["attention_mask"]

    ).last_hidden_state[:, 0]

return {"features": output}

dataset_features = dataset_tokenized.map(

  get_output_embeddings, batched=True, batch_size=10)

X_train = np.array(dataset_features["train"]["features"])

y_train = np.array(dataset_features["train"]["label"])

X_val = np.array(dataset_features["validation"]["features"])

y_val = np.array(dataset_features["validation"]["label"])

X_test = np.array(dataset_features["test"]["features"])

y_test = np.array(dataset_features["test"]["label"])

# train classifier

from sklearn.linear_model import LogisticRegression

clf = LogisticRegression()

clf.fit(X_train, y_train)

print("Training accuracy", clf.score(X_train, y_train))

print("Validation accuracy", clf.score(X_val, y_val))

print("test accuracy", clf.score(X_test, y_test))

微调I — 更新输出层

与上述基于特征的方法相关的一个流行方法是微调输出层(我们将这种方法称为微调I)。类似于基于特征的方法,我们保持预训练LLM的参数不变。我们只训练新添加的输出层,类似于在嵌入特征上训练逻辑回归分类器或小型多层感知器。

在代码中,这将如下所示:

model = AutoModelForSequenceClassification.from_pretrained(

    "distilbert-base-uncased",

     num_labels=2  # 假设目标任务是二元分类任务

)

# freeze all layers

for param in model.parameters():

    param.requires_grad = False

# then unfreeze the two last layers (output layers)

for param in model.pre_classifier.parameters():

    param.requires_grad = True

for param in model.classifier.parameters():

    param.requires_grad = True

# finetune model

lightning_model = CustomLightningModule(model)

trainer = L.Trainer(

    max_epochs=3,

    ...

)

trainer.fit(

  model=lightning_model,

  train_dataloaders=train_loader,

  val_dataloaders=val_loader)

# evaluate model

trainer.test(lightning_model, dataloaders=test_loader)

从理论上讲,这种方法应该在建模性能和速度方面与基于特征的方法表现相似,因为我们使用了相同的冻结背景模型。然而,由于基于特征的方法稍微更容易预先计算和存储训练数据集的嵌入特征,基于特征的方法可能在特定的实际场景中更方便。

微调II — 更新所有层

在咱们用BERT( https://arxiv.org/abs/1810.04805 , Devlin等人)这种预训练的大型语言模型(LLM)的时候,有两种方法可以调整模型来适应特定的任务。微调I和微调II,这俩策略都挺有用的,但微调II通常会让模型表现得更好。

微调I,就是说咱们只更新模型的输出层,这层大概只有1500个参数。这样做的好处是,计算量小,因为不需要动模型里的其他参数,大概有1.1亿个参数呢。但是,这种方法可能不会完全发挥出模型的潜力,因为它没有考虑到咱们特定任务的一些细节。

微调II呢,就是把模型的所有层都更新一遍,不仅仅是输出层。虽然这样做计算量更大,因为它涉及到更多的参数,但是它能让模型更好地适应咱们的任务。在实践中,这种方法通常会带来更好的效果,因为它可以让模型在预训练的基础上,进一步学习到任务相关的特征。

总的来说,如果你想要让你的模型在特定任务上表现得更好,而且计算资源也允许的话,微调II是个不错的选择。不过,如果你想要快速地得到结果,或者计算资源有限,微调I也可以考虑。每种方法都有它的优势和适用场景。

model = AutoModelForSequenceClassification.from_pretrained(

    "distilbert-base-uncased",

     num_labels=2  # 假设目标任务是二元分类任务

)

# don't freeze layers

# for param in model.parameters():

#    param.requires_grad = False

# finetune model

lightning_model = LightningModel(model)

trainer = L.Trainer(

    max_epochs=3,

    ...

)

trainer.fit(

  model=lightning_model,

  train_dataloaders=train_loader,

  val_dataloaders=val_loader)

# evaluate model

trainer.test(lightning_model, dataloaders=test_loader)

上面的代码片段可以被用来训练一个电影评论分类器,使用预训练的DistilBERT基础模型:

  • 基于特征的方法与逻辑回归:83%的测试准确率

  • 微调I,更新最后2层:87%的准确率

  • 微调II,更新所有层:92%的准确率。

这些结果与一般规则一致,即微调更多层通常能带来更好的性能,但成本更高。

图片

 

参数高效微调

在前面的部分,我们了解到微调更多层通常会得到更好的结果。现在,上面的实验是基于DistilBERT模型的,它相对较小。如果我们想微调更大的模型,比如最新生成的LLM,它们几乎刚好适合GPU内存,例如,我们可以使用上述基于特征的方法或微调I方法。但是,如果我们想要获得与微调II类似的建模质量呢?

多年来,研究人员开发了几种技术( https://arxiv.org/abs/2303.15647 , Lialin等人),以高建模性能对LLM进行微调,同时只要求训练少量参数。这些方法通常被称为参数高效微调技术(PEFT)。

一些最广泛使用的PEFT技术总结在下面的图中。

图片

 

最近引起轰动的一种PEFT技术是LLaMA-适配器,它是为Meta流行的LLaMA模型( https://arxiv.org/abs/2302.13971 , Touvron等人)提出的——然而,尽管LLaMA-适配器是在LLaMA的背景下提出的,但这个想法是模型不可知的。

为了理解LLaMA-适配器的工作原理,我们需要(小)回顾一下两种相关技术:前缀调整和适配器——LLaMA-适配器( https://arxiv.org/abs/2303.16199 , 张等人)结合并扩展了这两种想法。

提示调整和前缀调整

原始的提示调整概念是指通过改变输入提示来获得更好的建模结果的技术。例如,如果我们对将英文句子翻译成德语感兴趣,我们可以以不同的方式询问模型,如下所示。

图片

 

现在,上述概念被称为硬提示调整,因为我们直接改变了不可微分的离散输入标记。

与硬提示调整相比,软提示调整将输入标记的嵌入与可以通过反向传播优化以改善目标任务建模性能的可训练张量连接起来。

提示调整的一种特定风味是前缀调整(https://arxiv.org/abs/2101.00190, Li & Liang)。前缀调整的想法是在每个变换器块中添加一个可训练的张量,而不是像软提示调整那样仅在输入嵌入中添加。下面的图示说明了普通变换器块和用前缀修改的变换器块之间的区别。

图片

 

请注意,在上图中,“全连接层”指的是一个小的多层感知器(两个全连接层之间有一个非线性激活函数)。这些全连接层将软提示嵌入到与变换器块输入相同维度的特征空间中,以确保可以连接。

使用(Python)伪代码,我们可以如下说明普通变换器块和用前缀修改的变换器块之间的区别:

图片

根据原始的前缀调整论文,前缀调整实现了与微调所有层相当的建模性能,同时只要求训练0.1%的参数——实验是基于GPT-2模型的。此外,在许多情况下,前缀调整甚至超过了微调所有层的性能,这可能是因为涉及的参数更少,有助于减少对较小目标数据集的过拟合。

最后,为了澄清在推理期间使用软提示的情况:在学习了软提示后,我们必须将其作为前缀提供,以便执行我们对模型进行微调的特定任务。这允许模型根据特定任务定制其响应。此外,我们可以有多个软提示,每个提示对应不同的任务,并在推理期间提供适当的前缀,以实现特定任务的最佳结果。

适配器

原始的适配器方法(https://arxiv.org/abs/1902.00751 , Houlsby等人)与前述的前缀调整有些相关,它们也在每个变换器块中添加了额外的参数。然而,与在输入嵌入中添加前缀不同,适配器方法在两个地方添加了适配器层,如下所示。

图片

 

对于喜欢(Python)伪代码的读者,适配器层可以这样写:

图片

请注意,适配器的全连接层通常相对较小,并且具有类似于自动编码器的瓶颈结构。每个适配器块的第一个全连接层(就像是个压缩器),将输入投影到低维表示中。第二个全连接层(就像是解压器),将输入投影回输入维度。这是如何实现参数高效的?例如,假设第一个全连接层将1024维的输入投影到24维,第二个全连接层将其投影回1024维。这意味着我们引入了1,024 x 24 + 24 x 1,024 = 49,152个权重参数。相比之下,一个将1024维的输入投影到1024维空间的全连接层将有1,024 x 1,024 = 1,048,576个参数。

根据原始适配器论文,使用适配器方法训练的BERT模型达到了与完全微调的BERT模型相当的建模性能,同时只要求训练3.6%的参数。

现在,问题是适配器方法与前缀调整相比如何。根据原始前缀调整论文,当调整总模型参数的0.1%时,适配器方法的性能略低于前缀调整方法。然而,当适配器方法用于调整模型参数的3%时,该方法与调整0.1%模型参数的前缀调整方法相当。因此,我们可以得出结论,前缀调整方法是两者中更有效的。

扩展前缀调整和适配器:LLaMA-适配器

扩展前缀调整和原始适配器方法的思想,研究人员最近提出了LLaMA-适配器( https://arxiv.org/abs/2303.16199 , 张等人),这是一种针对LLaMA(LLaMA是Meta流行的GPT替代品)的参数高效微调方法。

像前缀调整一样,LLaMA-适配器方法在嵌入输入前添加可调的提示张量。值得注意的是,在LLaMA-适配器方法中,前缀是在嵌入表内学习和维护的,而不是外部提供的。模型中的每个变换器块都有自己的独特学习前缀,允许在不同模型层中进行更定制化的调整。

此外,LLaMA-适配器引入了一个零初始化的注意力机制和门控。这种所谓的零初始化注意力和门控的动机是,适配器和前缀调整可能会通过引入随机初始化的张量(前缀提示或适配器层)来破坏预训练LLM的语言知识,导致初始训练阶段的不稳定微调和高损失值。

与前缀调整和原始适配器方法相比,另一个区别是LLaMA-适配器只在最上面的(即,前几个)变换器块中添加可学习的适配提示。作者认为,这种方法可以更有效地调整语言表示,专注于更高层次的语义信息。

虽然LLaMA适配器方法的基本思想与前缀调整(添加可调软提示)相关,但在实现上有一些额外的细微差别。例如,只有自注意力输入的键和值序列通过可调软提示进行修改。然后,根据门控因子(在训练开始时设置为零),决定是否使用前缀修改的注意力。这个概念在下面的可视化中进行了说明。

图片

在伪代码中,我们可以这样表达:

图片

总之,LLaMA-适配器与常规前缀调整的区别在于,LLaMA-适配器只修改顶部(即,前几个)变换器块,并引入门控机制来稳定训练。虽然研究人员特别针对LLaMA进行了实验,但他们提出的适配器方法也是一种通用方法,也可以应用于其他类型的LLM(如GPT)。

使用LLaMA-适配器方法,研究人员能够在1小时内(使用八个A100 GPU)在包含52k指令对的数据集上微调一个70亿参数的LLaMA模型。此外,微调的LLaMA-适配器模型在这项研究中比较的所有模型中,在问答任务上表现最佳,而只需微调120万个参数(适配器层)。

本文详细讲了怎么用一种效率高的方法来微调大型语言模型,这样既能保持模型不大,计算需求也不高,又能提高模型在特定任务上的表现。文章里还提供了一些实际的代码例子,教我们怎么用LLaMA-适配器这个方法来做微调。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值