大模型的概述
大语言模型(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具有三个关键属性:
- 匹配完全微调模型的结果。
- 使任务能够到达流中,这样就不需要同时访问所有数据集。
- 仅对模型参数的一小部分进行微调。
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}]
效果展示
-
首先看显存方面,由于冻结大部分参数,显存占用下降了5个G左右。
未使用bitfit微调方法:
使用bitfit微调方法:
-
训练时间方面,由原来的50.53分钟,降低到37.08分钟。
-
效果方面,使用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