怎样用BitFit进行大模型高效微调——手把手DeepLn教程

大模型的概述

大语言模型(LLM)是一类构建于深度神经网络之上、拥有数百亿个参数的语言处理模型。这些模型通常采用自监督学习方法,通过大规模未标注文本进行训练。自2018年以来,包括Google、OpenAI、Meta、百度、华为在内的多家领先科技公司和研究机构纷纷推出了BERT、GPT等多种先进模型,这些模型在各类自然语言处理任务中均表现卓越。2019年,大型语言模型的发展迎来了飞跃,尤其是在2022年11月,ChatGPT的推出不仅标志着技术的一大突破,也引起了全球范围内的广泛关注。用户现在可以通过自然语言与模型进行交互,完成问答、文本分类、内容摘要、翻译、聊天等多种任务,从理解到生成,体现了模型对世界知识的深刻把握与语言理解的强大能力。随着时间的推移,模型的参数量也在呈指数级增长,表明只要模型设计得当、参数足够丰富、训练数据充足,这些模型便能在语言生成任务中展现出色的性能。

下图为大型语言模型的发展时间线。
在这里插入图片描述

参数高效微调(PEFT)

参数高效微调是在对大型语言模型进行微调时的一种重要策略,它旨在改进模型性能的同时,尽量减少对额外参数的需求。具有以下方面的优势:
**1. 降低计算成本:**大型语言模型通常包含数十亿甚至数百亿的参数,全模型微调需要大量的计算资源和时间。参数高效的微调方法,如使用适配器层或提示调整,可以大幅减少需要训练的参数数量,从而降低模型训练和推理的硬件要求和能耗。

**2. 减少训练时间:**参数高效的微调方法可以显著减少训练时间。例如,仅对模型中的少量参数进行更新,或者调整输入的提示,可以实现快速微调,使模型迅速适应新的任务或数据集。
**3. 避免过拟合:**在数据量较小的任务上进行全模型微调时,模型容易发生过拟合,即模型过度适应训练数据,而在实际应用中的泛化能力下降。通过仅训练模型的一部分参数,可以有效控制模型的复杂度,从而减少过拟合的风险。

下图为:参数高效微调方法图
参数高效微调论文地址
在这里插入图片描述
参数高效微调方法分类。主要基于三大类方法:基于additive、基于selective和基于reparametrization-based。在additive方法中,主要两大类:adapters方法和soft prompts。

基于additive方法

additive方法,顾名思义“增量式”,通常向预训练模型添加额外的小型网络层或模块,而不直接修改原有模型的权重。这种方法能够实现在保留预训练模型通用性能的同时,针对特定任务进行优化。下面是几种常见的additive微调方法:
1. 适配器模块(Adapter Modules)
- 适配器是一种轻量级的神经网络层,嵌入到预训练模型的各个层之间或之内。这些适配器只有少量的参数需要训练,而模型的其他参数保持固定。适配器通常包括一些简单的前馈神经网络层,例如,一个下降维度的线性层,一个激活函数,以及一个恢复原始维度的线性层。适配器可以非常有效地针对特定任务调整模型的行为,而不需要重新训练整个模型。
2. 提示调整 (Prompt-Tuning)
- 虽然提示调整不涉及到添加新的网络层,但它可以被视为一种additive策略,因为它通过添加或修改输入序列的一部分(即提示)来影响模型的输出。这种方法利用了预训练模型的灵活性,通过最优化一组固定的词(prompt tokens),以达到微调模型的目的。
3. 前缀调整 (Prefix-Tuning)
- 类似于提示调整,前缀调整在模型处理每个输入之前添加一系列可训练的向量(称为前缀)。这些前缀在模型的解码器或注意力机制中作为额外的上下文使用,从而引导模型生成特定于任务的响应。

基于selective方法

selective方法,顾名思义“选择式”,涉及选择性地调整或优化模型的部分参数,而不是对整个模型的所有参数进行微调。这种方法旨在在维持大部分预训练参数不变的情况下,仅对影响最大的部分进行优化。以下是几种常见的selective微调方法:
1. BitFit 方法
- 下面单独介绍。
2. 稀疏微调 (Sparse Fine-Tuning)
- 稀疏微调涉及在模型的参数中选择性地更新一个小比例的权重。通过使用诸如L0正则化等技术,可以实现在训练过程中动态地选择哪些参数应该被更新。这种方法允许模型在几乎不改变其大部分权重的情况下,对特定任务作出微调。

基于reparametrization-based方法

reparametrization-based方法,顾名思义“重新参数化式”,该方法利用低秩表示来最大程度地减少可训练参数的数量。Lora是其最有代表性的方法,LoRA 通过向预训练模型中的特定层(通常是自注意力层)引入低秩矩阵来调整模型的权重。这些低秩矩阵作为权重矩阵的增量使用,从而实现对模型行为的微调而不改变原始权重。具体来说,LoRA 会在自注意力机制中的查询(Q)、键(K)和值(V)矩阵中插入低秩矩阵。这些低秩矩阵是可训练的,并且因其低秩特性,参数数量远少于原始的权重矩阵,从而实现高效的微调。

BitFit

Elad Ben Zaken, Yoav Goldberg, Shauli Ravfogel等人于2022年5月在ACL发表一篇名为BitFit: Simple Parameter-efficient Fine-tuning for Transformer-based Masked Language-models的文章,该文章首次提出了BitFit方法,这是一种稀疏微调的方法,其中仅修改了模型(或子集)中的bais项。对于中小型训练数据,在预训练的BERT模型上应用BitFit与微调整个模型具有竞争力(有时甚至更好)。对于较大的数据,该方法与其他稀疏微调方法相比具有竞争力。BitFit具有三个关键属性:

  1. 匹配完全微调模型的结果。
  2. 使任务能够到达流中,这样就不需要同时访问所有数据集。
  3. 仅对模型参数的一小部分进行微调。

BitFit论文地址

BitFit具体方法

BitFit(BIas-Term FIne-Tuning)的方法实质上是冻结了大部分transformer-encoder参数,只训练bais和特定任务的classification layer。该方法是参数高效的:每个新任务只需要存储bais参数向量(占参数总数的 0.1% 以下)和特定任务的classification layer。
具体来说:BERT 编码器由 L 层组成,其中每层 l 以 M 个自注意力头开始,其中自注意力头 (m, l) 具有key、quary和value 编码器,每个编码器都采用线性层的形式:
在这里插入图片描述
其中 x 是前一个编码器层的输出(对于第一个编码器层,x 是嵌入层的输出)。然后使用不涉及新参数的注意力机制将这些参数组合在一起:
在这里插入图片描述
接着送入一个带分层归一化(LN)的MLP:
在这里插入图片描述
由于bais偏置项在整个网络参数中占比非常小,所以只训练bais参数,显存占用很小、模型训练也会很快,同时也能起到不错的效果。

BitFit代码实现

以text-classification任务为例,使用bitfit微调。
数据集:carblacac/twitter-sentiment-analysis
模型:nlptown/bert-base-multilingual-uncased-sentiment
由于本地我本地电脑显存不够用,本次训练在DeepLn算力云平台线上实现,该平台算力白菜价,大显存卡多,网络也几乎无延迟,体验非常好。
在这里插入图片描述
这里我租用一张4090显卡,选择的torch版本为torch2.2.1+cuda12.1,如以下图所示:
在这里插入图片描述
这里点击code-server,并复制code-server密码,即可进入开发环境界面。
在这里插入图片描述
模型和数据集均在huggingface.co下载获得。

import os
os.environ["https_proxy"] = "http://127.0.0.1:10809"
os.environ["https_proxy"] = "http://127.0.0.1:10809"

Step 1: 安装必要的库

!pip install transformers peft datasets evaluate -i https://pypi.tuna.tsinghua.edu.cn/simple

Step 2: 导入相关的包

from transformers import AutoTokenizer,TrainingArguments,Trainer,AutoModelForSequenceClassification,DataCollatorWithPadding
from datasets import load_dataset
import evaluate
import torch

Step 3: 加载数据集

dataset = load_dataset("carblacac/twitter-sentiment-analysis")
dataset
DatasetDict({
    train: Dataset({
        features: ['text', 'feeling'],
        num_rows: 119988
    })
    validation: Dataset({
        features: ['text', 'feeling'],
        num_rows: 29997
    })
    test: Dataset({
        features: ['text', 'feeling'],
        num_rows: 61998
    })
})

Step 4: 数据预处理

dataset = dataset.filter(lambda x :x["text"] is not None)
simple_datasets = dataset["train"].select(range(10))
simple_datasets[:3]
{'text': ['@fa6ami86 so happy that salman won.  btw the 14sec clip is truely a teaser',
  "@phantompoptart .......oops.... I guess I'm kinda out of it.... Blonde moment -blushes- epic fail",
  "@bradleyjp decidedly undecided. Depends on the situation. When I'm out with the people I'll be in Chicago with? Maybe."],
 'feeling': [0, 0, 1]}
tokenizer = AutoTokenizer.from_pretrained("nlptown/bert-base-multilingual-uncased-sentiment")
tokenizer_simple = tokenizer(simple_datasets["text"],max_length=256,padding="max_length",truncation=True)
print(tokenizer_simple["input_ids"][1])
[101, 137, 36984, 49822, 92361, 119, 119, 119, 119, 119, 119, 119, 74245, 13478, 119, 119, 119, 119, 151, 41567, 22523, 151, 112, 155, 16540, 10112, 10871, 10108, 10197, 119, 119, 119, 119, 58779, 14068, 118, 24849, 79580, 10107, 118, 31612, 37498, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
tokenizer_simple.keys()
dict_keys(['input_ids', 'token_type_ids', 'attention_mask'])
def process_function(examples):
    tokenized_dataset = tokenizer(examples["text"],max_length=256,padding="max_length",truncation=True)
    tokenized_dataset["label"] = examples["feeling"]
    return tokenized_dataset
tokenized_datasets = dataset.map(process_function,batched=True,remove_columns=dataset["train"].column_names)
tokenized_datasets
DatasetDict({
    train: Dataset({
        features: ['input_ids', 'token_type_ids', 'attention_mask', 'label'],
        num_rows: 119988
    })
    validation: Dataset({
        features: ['input_ids', 'token_type_ids', 'attention_mask', 'label'],
        num_rows: 29997
    })
    test: Dataset({
        features: ['input_ids', 'token_type_ids', 'attention_mask', 'label'],
        num_rows: 61998
    })
})

Step 5: 模型加载

model = AutoModelForSequenceClassification.from_pretrained("nlptown/bert-base-multilingual-uncased-sentiment")

Step 6:定义评估函数和优化器

def eval_metric(eval_predict):
    clf_metrics = evaluate.combine(["accuracy", "f1", "precision", "recall"])
    predictions, labels = eval_predict
    predictions = predictions.argmax(axis=-1)
    clf = clf_metrics.compute(predictions=predictions, references=labels)
    return clf
from transformers.optimization import Adafactor, AdafactorSchedule
optimizer = Adafactor(model.parameters(), scale_parameter=True, relative_step=True, warmup_init=True, lr=None)
lr_scheduler = AdafactorSchedule(optimizer)

Step 7:配置训练参数

train_args = TrainingArguments(output_dir="./checkpoints",      # 输出文件夹
                               overwrite_output_dir=True,       # 是否覆盖输出文件夹
                               #eval_accumulation_steps=64,     # 评估时累加步数
                               per_device_train_batch_size=64,  # 训练时的batch_size
                               per_device_eval_batch_size=128,  # 验证时的batch_size
                               logging_steps=50,                # log 打印的频率
                               evaluation_strategy="epoch",     # 评估策略
                               save_strategy="epoch",           # 保存策略
                               save_total_limit=2,              # 最大保存数
                               learning_rate=2e-5,              # 学习率
                               weight_decay=0.01,               # weight_decay
                               metric_for_best_model="f1",      # 设定评估指标
                               load_best_model_at_end=True,     # 训练完成后加载最优模型
                               seed=1236)                       # 随机种子)

Step 8:创建训练器

trainer = Trainer(model=model_bitfit,                                         # 加载模型
                  args=train_args,                                            # 加载训练参数
                  train_dataset=tokenized_datasets["train"],                  # 加载训练数据
                  eval_dataset=tokenized_datasets["test"],                    # 加载训练数据
                  data_collator=DataCollatorWithPadding(tokenizer=tokenizer), # 数据流处理
                  optimizers =(optimizer, lr_scheduler),                      # 加载优化器
                  compute_metrics=eval_metric)  

Step 9:模型训练

trainer.train()

在这里插入图片描述

trainer.evaluate(tokenized_datasets["test"])
{'eval_loss': 0.42688414454460144, 'eval_accuracy': 0.802622665247266, 'eval_f1': 0.8194893127406293, 'eval_precision': 0.7555900114248408, 'eval_recall': 0.8951948177511361, 'eval_runtime': 167.1236, 'eval_samples_per_second': 370.971, 'eval_steps_per_second': 2.902, 'epoch': 3.0}

Step 10:模型推理

from transformers import pipeline
pipe = pipeline("text-classification", model=model, tokenizer=tokenizer,device=0)
sen = ["i love you",
        "I am very happy working in the deepln team, the boss treats us very well"]
pipe(sen)
    [{'label': '2 stars', 'score': 0.8235601186752319},
     {'label': '2 stars', 'score': 0.9687787890434265}]

Step 11:BitFit

model_bitfit= AutoModelForSequenceClassification.from_pretrained("nlptown/bert-base-multilingual-uncased-sentiment")
pipe = pipeline("text-classification", model=model, tokenizer=tokenizer,device=0)
total_num_param = sum(param.numel() for param in model_bitfit.parameters())
num_param_bitfit = 0
for name,param in model_bitfit.named_parameters():
    if "bias" not in name:
        param.requires_grad = False
    else:
        num_param_bitfit += param.numel()
print("model parameters:",total_num_param ,"num_param_bitfit:",num_param_bitfit,"ratio%:",(num_param_bitfit/total_num_param)*100)
def eval_metric(eval_predict):
    clf_metrics = evaluate.combine({
        "accuracy": evaluate.load("accuracy"),
        "f1": evaluate.load("f1", config={"average": "macro"}),  # 设置 F1 的平均方法为 'macro'
        "precision": evaluate.load("precision", config={"average": "macro"}),
        "recall": evaluate.load("recall", config={"average": "macro"})
    })
    predictions, labels = eval_predict
    predictions = predictions.argmax(axis=-1)
    clf = clf_metrics.compute(predictions=predictions, references=labels)
    return clf

optimizer = Adafactor(model_bitfit.parameters(), scale_parameter=True, relative_step=True, warmup_init=True, lr=None)
lr_scheduler = AdafactorSchedule(optimizer)

train_args = TrainingArguments(output_dir="./checkpoint_bitfit",   # 输出文件夹
                               overwrite_output_dir=True,          # 是否覆盖输出文件夹
                               #eval_accumulation_steps=64,        # 评估时累加步数
                               per_device_train_batch_size=64,     # 训练时的batch_size
                               per_device_eval_batch_size=128,     # 验证时的batch_size
                               logging_steps=10,                   # log 打印的频率
                               evaluation_strategy="epoch",        # 评估策略
                               save_strategy="epoch",              # 保存策略
                               save_total_limit=2,                 # 最大保存数
                               learning_rate=2e-5,                 # 学习率
                               weight_decay=0.01,                  # weight_decay
                               metric_for_best_model="f1",         # 设定评估指标
                               load_best_model_at_end=True,        # 训练完成后加载最优模型
                               seed=1236)   
trainer = Trainer(model=model_bitfit,                                         # 加载模型
                  args=train_args,                                            # 加载训练参数
                  train_dataset=tokenized_datasets["train"],                  # 加载训练数据集
                  eval_dataset=tokenized_datasets["test"],                    # 加载测试数据集
                  data_collator=DataCollatorWithPadding(tokenizer=tokenizer), # 数据流处理
                  optimizers =(optimizer, lr_scheduler),                      # 加载优化器
                  compute_metrics=eval_metric)  
    model parameters: 167360261 num_param_bitfit: 102917 ratio%: 0.061494287464095194
trainer.train()

在这里插入图片描述

print(trainer.evaluate(tokenized_datasets["test"]))
{'eval_loss': 0.43755027651786804, 'eval_accuracy': 0.8015097261201974, 'eval_f1': 0.8043810008266039, 'eval_precision': 0.793657266539101, 'eval_recall': 0.8153984981791228, 'eval_runtime': 361.3451, 'eval_samples_per_second': 171.576, 'eval_steps_per_second': 1.342, 'epoch': 3.0}
from transformers import pipeline
pipe = pipeline("text-classification", model=model_bitfit, tokenizer=tokenizer,device=0)
sen = ["i love you",
        "I am very happy working in the deepln team, the boss treats us very well"]
pipe(sen)
[{'label': '2 stars', 'score': 0.9304160475730896},
 {'label': '2 stars', 'score': 0.9857051372528076}]

效果展示

  1. 首先看显存方面,由于冻结大部分参数,显存占用下降了5个G左右。
    未使用bitfit微调方法:
    在这里插入图片描述
    使用bitfit微调方法:
    在这里插入图片描述

  2. 训练时间方面,由原来的50.53分钟,降低到37.08分钟。

  3. 效果方面,使用bitfit微调方法后,四个指标都略低于未使用bitfit微调方法。但是在模型推理方面,两者效果未有显著区别。

另外,不知道大家发现了吗,DeepLn算力云性价比超高的同时,用户权限和数据隔离的平衡性做的非常好,即使我租用的容器内直接拥有了root权限,但是它还具备宿主机内不同容器root权限隔离,看不到其他用户的进程,提升了数据安全,可以说是非常的厉害了。

参考

[1] https://paperswithcode.com/paper/scaling-down-to-scale-up-a-guide-to-parameter
[2] https://aclanthology.org/2022.acl-short.1
[3] https://github.com/zyds/transformers-code
[4] https://github.com/huggingface/transformers

  • 66
    点赞
  • 61
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值