大型语言模型(LLMs)微调入门指南
LLMs微调入门指南
介绍
踏上人工智能和自然语言处理(NLP)进化的旅程。在眨眼之间,人工智能迅速发展,改变了我们的世界。通过对大型语言模型进行微调,NLP产生了巨大的影响,彻底改变了我们的技术互动方式。回到2017年,一个具有里程碑意义的时刻,“Attention is all you need”诞生了,孕育了开创性的“Transformer”架构。这个架构现在成为NLP的基石,也是每个大型语言模型配方中不可或缺的成分,包括著名的ChatGPT。
想象一下毫不费力地生成连贯、富有上下文的文本,这就是像GPT-3这样的模型的魔力。作为聊天机器人、翻译和内容生成的强大工具,它们的卓越之处源于架构和预训练与微调的复杂协同。我们即将发布的文章将深入探讨这个交响乐,揭示了运用大型语言模型进行任务的艺术,以及如何巧妙地运用预训练和微调这一动态二重奏。加入我们,解密这些具有变革性的技术!
学习目标
- 了解构建大型语言模型应用的不同方法。
- 学习特征提取、层微调和适配器方法等技术。
- 使用Huggingface transformers库在下游任务上进行大型语言模型的微调。
开始使用大型语言模型
LLMs代表大型语言模型。LLMs是深度学习模型,旨在理解类似人类文本的含义,并执行各种任务,如情感分析、语言建模(下一个词预测)、文本生成、文本摘要等等。它们是在大量文本数据上进行训练的。
- “ChatGPT 机器学习代码解释器- 是有效的吗?”
- 使用Pandera对PySpark应用程序进行数据验证
- 最强大的机器学习模型解析(Transformers, CNNs, RNNs, GANs …)
我们每天都在使用基于这些LLMs的应用,甚至没有意识到。谷歌使用BERT(双向编码器表示的转换器)进行各种应用,例如查询完成、理解查询的上下文、输出更相关和准确的搜索结果、语言翻译等等。
这些模型建立在深度学习技术、深度神经网络和自注意力等先进技术的基础上。它们通过在大量文本数据上进行训练来学习语言的模式、结构和语义。
由于这些模型是在庞大的数据集上进行训练的,所以训练它们需要大量的时间和资源,而且从头开始训练它们是没有意义的。我们可以通过一些技术直接将这些模型用于特定任务。现在让我们详细讨论这些技术。
构建LLM应用的不同方法概述
我们经常在日常生活中看到令人兴奋的LLM应用。你是否好奇如何构建LLM应用?以下是构建LLM应用的3种方法:
- 从头开始训练LLMs
- 微调大型语言模型
- 提示
从头开始训练LLMs
人们常常对这两个术语:训练和微调LLMs感到困惑。这两种技术的工作方式相似,即改变模型参数,但训练目标不同。
从头开始训练LLMs也被称为预训练。预训练是一种技术,通过在大量未标记的文本上对大型语言模型进行训练。但问题是,“如何在训练模型时使用未标记的数据,并期望模型准确地预测数据呢?” 这里就引入了“自我监督学习”的概念。在自我监督学习中,模型屏蔽一个词,并试图通过前面的词来预测下一个词。例如,我们有一个句子:“我是一名数据科学家。”
模型可以从这个句子中创建自己的标记数据,如:
这被称为下一个词预测,由掩码语言模型(MLM)完成。BERT是一种掩码语言模型,它使用这种技术来预测掩码的词。我们可以将MLM看作是“填空”的概念,模型预测什么词可以填入空白处。有不同的方法来预测下一个词,但本文只讨论BERT,即MLM。BERT可以查看前面和后面的词,理解句子的上下文,并预测掩码的词。
因此,预训练的高级概述就是一种技术,模型通过学习来预测文本中的下一个单词。
微调大型语言模型
微调是调整模型参数以使其适用于执行特定任务的过程。在模型进行预训练之后,它会进行微调,或者简单地说,训练模型以执行特定任务,例如情感分析、文本生成、查找文档相似性等。我们不需要再对大量文本进行训练,而是使用已经训练好的模型来执行我们想要的任务。我们将在本文中详细讨论如何对大型语言模型进行微调。
提示
提示是三种技术中最简单的一种,但也有些棘手。它涉及到给模型提供一个基于其执行任务的上下文(提示)。可以将其看作是详细教授孩子一章节的内容,对解释非常谨慎,然后要求他们解决与该章节相关的问题。
对于LLM,以ChatGPT为例,我们设置一个上下文,要求模型按照指示解决给定的问题。
假设我想让ChatGPT向我提问有关Transformers的一些面试问题。为了获得更好的体验和准确的输出,您需要设置一个合适的上下文并提供详细的任务描述。
例如:我是一名有两年经验的数据科学家,目前正在为某某公司的工作面试做准备。我热爱解决问题,并且目前正在使用最先进的NLP模型进行工作。我了解最新的趋势和技术。请问我关于该公司根据以往经验可能会问到的Transformer模型的非常困难的问题。问我十个问题,并给出问题的答案。
您的提示越详细和具体,结果就会越好。最有趣的部分是,您可以从模型本身生成提示,然后添加个人风格或所需的信息。
了解不同的微调技术
通常有不同的方法来对模型进行微调,不同的方法取决于您想要解决的具体问题。让我们讨论一下微调模型的技术。
通常有三种传统的对LLM进行微调的方法。
特征提取
人们使用这种技术从给定的文本中提取特征,但为什么我们希望从给定的文本中提取嵌入?答案很简单。因为计算机无法理解文本,所以需要一种我们可以用来进行各种任务的文本表示。一旦我们提取了嵌入,它们就可以执行诸如情感分析、识别文档相似性等任务。在特征提取中,我们锁定模型的主干层,这意味着我们不会更新这些层的参数;只有分类器层的参数会得到更新。分类器层包括全连接层。
完整模型微调
顾名思义,这种技术在特定数量的时期内对自定义数据集上的每个模型层进行训练。根据新的自定义数据集调整模型的所有层的参数。这可以提高模型在数据和我们要执行的特定任务上的准确性。考虑到微调的大型语言模型中存在数十亿个参数,这需要大量的计算资源和时间。
基于适配器的微调
基于适配器的微调是一个相对较新的概念,其中向网络添加一个额外的随机初始化的层或模块,然后针对特定任务进行训练。在这种技术中,模型的参数保持不变,或者可以说模型的参数不会改变或调整。相反,适配器层的参数会被训练。这种技术可以帮助以计算效率的方式微调模型。
实施:在下游任务上微调BERT
现在我们了解了微调技术,让我们使用BERT对IMDB电影评论进行情感分析。BERT是一个结合了Transformer层的大型语言模型,只有编码器。它由Google开发,并在各种任务上表现出色。BERT有不同的大小和变种,例如BERT-base-uncased、BERT Large、RoBERTa、LegalBERT等等。
使用BERT模型进行情感分析
让我们使用BERT模型对IMDB电影评论进行情感分析。建议使用Google Colab以获得免费的GPU加速。让我们首先加载一些重要的库来进行训练。
由于BERT(双向编码器表示)基于Transformer,所以第一步是在我们的环境中安装transformers。
!pip install transformers
让我们加载一些将帮助我们按照BERT模型所需的方式加载数据、对加载的数据进行标记化、加载用于分类的模型、执行训练-测试分离、加载我们的CSV文件以及其他一些函数的库。
import pandas as pd
import numpy as np
import os
from sklearn.model_selection import train_test_split
import torch
import torch.nn as nn
from transformers import BertTokenizer, BertModel
为了加快计算速度,我们需要将设备从CPU更改为GPU。
device = torch.device("cuda")
下一步是加载我们的数据集并查看数据集中的前5条记录。
df = pd.read_csv('/content/drive/MyDrive/movie.csv')
df.head()
我们将数据集划分为训练集和验证集。您也可以将数据划分为训练集、验证集和测试集,但为了简单起见,我只将数据集划分为训练集和验证集。
x_train, x_val, y_train, y_val = train_test_split(df.text, df.label, random_state = 42, test_size = 0.2, stratify = df.label)
导入并加载BERT模型
让我们导入并加载BERT模型和分词器。
from transformers.models.bert.modeling_bert import BertForSequenceClassification
# 导入BERT-base预训练模型
BERT = BertModel.from_pretrained('bert-base-uncased')
# 加载BERT分词器
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
我们将使用分词器将文本转换为最大长度为250的标记,并在需要时进行填充和截断。
train_tokens = tokenizer.batch_encode_plus(x_train.tolist(), max_length = 250, pad_to_max_length=True, truncation=True)
val_tokens = tokenizer.batch_encode_plus(x_val.tolist(), max_length = 250, pad_to_max_length=True, truncation=True)
分词器返回一个包含三对键值对的字典,其中包含输入ID(input_ids),这些ID与特定单词相关联;token_type_ids,这是一个区分输入的不同部分或段落的整数列表;还有一个attention_mask,用于指示哪些标记需要关注。
将这些值转换为张量
train_ids = torch.tensor(train_tokens['input_ids'])
train_masks = torch.tensor(train_tokens['attention_mask'])
train_label = torch.tensor(y_train.tolist())
val_ids = torch.tensor(val_tokens['input_ids'])
val_masks = torch.tensor(val_tokens['attention_mask'])
val_label = torch.tensor(y_val.tolist())
加载TensorDataset和DataLoader来进一步预处理数据,使其适合模型使用。
from torch.utils.data import TensorDataset, DataLoader
train_data = TensorDataset(train_ids, train_masks, train_label)
val_data = TensorDataset(val_ids, val_masks, val_label)
train_loader = DataLoader(train_data, batch_size = 32, shuffle = True)
val_loader = DataLoader(val_data, batch_size = 32, shuffle = True)
我们的任务是使用我们的分类器冻结BERT的参数,然后在我们的自定义数据集上微调这些层。因此,让我们冻结模型的参数。for param in BERT.parameters():param.requires_grad = False现在,我们将需要为我们添加的层定义前向和后向传递。BERT模型将充当特征提取器,而我们将需要显式地为分类定义前向和后向传递。
class Model(nn.Module):
def __init__(self, bert):
super(Model, self).__init__()
self.bert = bert
self.dropout = nn.Dropout(0.1)
self.relu = nn.ReLU()
self.fc1 = nn.Linear(768, 512)
self.fc2 = nn.Linear(512, 2)
self.softmax = nn.LogSoftmax(dim=1)
def forward(self, sent_id, mask):
# 将输入传递给模型
outputs = self.bert(sent_id, mask)
cls_hs = outputs.last_hidden_state[:, 0, :]
x = self.fc1(cls_hs)
x = self.relu(x)
x = self.dropout(x)
x = self.fc2(x)
x = self.softmax(x)
return x
让我们将模型移动到GPU上
model = Model(BERT)
# 将模型推送到GPU上
model = model.to(device)
定义优化器
# 从hugging face transformers导入优化器
from transformers import AdamW
# 定义优化器
optimizer = AdamW(model.parameters(),lr = 1e-5)
到目前为止,我们已经预处理了数据集并定义了我们的模型。现在是训练模型的时候了。我们需要编写一个代码来训练和评估模型。训练函数:
def train():
model.train()
total_loss, total_accuracy = 0, 0
total_preds = []
for step, batch in enumerate(train_loader):
# 如果可用,将批次移动到GPU上
batch = [item.to(device) for item in batch]
sent_id, mask, labels = batch
# 清除之前计算的梯度
optimizer.zero_grad()
# 获取当前批次的模型预测结果
preds = model(sent_id, mask)
# 计算预测值和标签之间的损失
loss_function = nn.CrossEntropyLoss()
loss = loss_function(preds, labels)
# 将损失添加到总损失中
total_loss += loss.item()
# 反向传播和梯度更新
loss.backward()
optimizer.step()
# 将预测结果移动到CPU并转换为numpy数组
preds = preds.detach().cpu().numpy()
# 添加模型预测结果
total_preds.append(preds)
# 计算平均损失
avg_loss = total_loss / len(train_loader)
# 连接预测结果
total_preds = np.concatenate(total_preds, axis=0)
# 返回平均损失和预测结果
return avg_loss, total_preds
评估函数
def evaluate():
model.eval()
total_loss, total_accuracy = 0, 0
total_preds = []
for step, batch in enumerate(val_loader):
# 如果可用,将批次移动到GPU上
batch = [item.to(device) for item in batch]
sent_id, mask, labels = batch
# 清除之前计算的梯度
optimizer.zero_grad()
# 获取当前批次的模型预测结果
preds = model(sent_id, mask)
# 计算预测值和标签之间的损失
loss_function = nn.CrossEntropyLoss()
loss = loss_function(preds, labels)
# 将损失添加到总损失中
total_loss += loss.item()
# 反向传播和梯度更新
loss.backward()
optimizer.step()
# 将预测结果移动到CPU并转换为numpy数组
preds = preds.detach().cpu().numpy()
# 添加模型预测结果
total_preds.append(preds)
# 计算平均损失
avg_loss = total_loss / len(val_loader)
# 连接预测结果
total_preds = np.concatenate(total_preds, axis=0)
# 返回平均损失和预测结果
return avg_loss, total_preds
现在我们将使用这些函数来训练模型:
# 将初始损失设置为无穷大
best_valid_loss = float('inf')
# 定义轮数
epochs = 5
# 空列表用于存储每个轮次的训练和验证损失
train_losses=[]
valid_losses=[]
# 对于每个轮次
for epoch in range(epochs):
print('\n 第 {:} / {:} 轮'.format(epoch + 1, epochs))
# 训练模型
train_loss, _ = train()
# 评估模型
valid_loss, _ = evaluate()
# 保存最佳模型
if valid_loss < best_valid_loss:
best_valid_loss = valid_loss
torch.save(model.state_dict(), 'saved_weights.pt')
# 添加训练和验证损失
train_losses.append(train_loss)
valid_losses.append(valid_loss)
print(f'\n训练损失: {train_loss:.3f}')
print(f'验证损失: {valid_loss:.3f}')
就是这样。您可以使用训练好的模型推断任何您选择的数据或文本。
结论
本文探讨了微调大型语言模型(LLMs)以及它们对自然语言处理(NLP)的重要影响。讨论了预训练过程,其中LLMs使用自监督学习在大量无标签文本上进行训练。还深入探讨了微调,包括将预训练模型调整为特定任务和提示,其中模型提供上下文以生成相关输出。此外,我们还研究了不同的微调技术,如特征提取、完整模型微调和基于适配器的微调。大型语言模型在NLP领域产生了革命性的影响,并继续推动各种应用的进展。