ChaosstuffAI

「好文推荐」LLM微调综合指南

简介

在过去的几年里,自然语言处理(NLP)的格局发生了显著的变化,这一切都归功于大型语言模型的出现。这些复杂的模型为广泛的应用打开了大门,从语言翻译到情感分析,甚至智能聊天机器人的创建。

但它们的多功能性使这些型号与众不同。对它们进行微调以处理特定的任务和领域已成为一种标准做法,释放它们的真正潜力,并将它们的性能提升到新的高度。在这篇全面的指南中,我们将深入研究微调大型语言模型的世界,涵盖从基础到高级的所有内容。

了解预训练语言模型

预训练的语言模型是在大量文本数据语料库上训练的大型神经网络,这些数据通常来自互联网。训练过程包括预测给定句子或序列中缺失的单词或标记,这使模型对语法、上下文和语义有了深刻的理解。通过处理数十亿个句子,这些模型可以掌握语言的复杂性,并有效地捕捉其细微差别。

流行的预训练语言模型的例子包括BERT(来自变换器的双向编码器表示)、GPT-3(生成式预训练Transformer模型)、RoBERTa(鲁棒优化的BERT预训练方法)等等。众所周知,这些模型能够以令人印象深刻的熟练程度执行文本生成、情感分类和语言理解等任务。

让我们详细讨论其中一种语言模型。

GPT-3

GPT-3(Generative Pre-trained Transformer 3)是一个突破性的语言模型架构,它改变了自然语言的生成和理解。Transformer模型是GPT-3体系结构的基础,该体系结构包含多个参数以产生卓越的性能。

一堆Transformer编码器层组成了GPT-3。多头自注意机制和前馈神经网络构成了每一层。当前馈网络处理和转换编码的表示时,注意力机制使模型能够识别单词之间的依赖性和关系。

GPT-3的主要创新在于其巨大的体积,由于其惊人的1750亿个参数,它可以获取大量的语言知识。

Image
Transformer 结构

可以使用OpenAI API与OpenAI的GPT-3模型进行交互。以下是使用GPT-3生成文本的示例。

import openai

# Set up your OpenAI API credentials
openai.api_key = 'YOUR_API_KEY'

# Define the prompt for text generation
prompt = "A quick brown fox jumps"

# Make a request to GPT-3 for text generation
response = openai.Completion.create(
  engine="text-davinci-003",
  prompt=prompt,
  max_tokens=100,
  temperature=0.6
)

# Retrieve the generated text from the API response
generated_text = response.choices[0].text

# Print the generated text
print(generated_text)

微调:根据我们的需求定制模型

这里有一个转折点:虽然预先训练的语言模型非常出色,但它们在任何特定任务中都不是天生的专家。他们可能对语言有着令人难以置信的掌握,但他们需要在情感分析、语言翻译或回答有关特定领域的问题等任务中进行一些微调。

微调就像为这些多功能型号提供了点睛之笔。想象一下,有一个多才多艺的朋友,他在各个领域都很出色,但你需要他们在特殊场合掌握一项特定的技能。你会在这方面给他们一些具体的培训,对吧?这正是我们在微调过程中对预训练的语言模型所做的。

Image

微调涉及在较小的、特定于任务的数据集上训练预训练的模型。这个新的数据集标记有与目标任务相关的示例。通过将模型暴露在这些标记的示例中,它可以调整其参数和内部表示,使其非常适合目标任务。

虽然预先训练的语言模型非常出色,但默认情况下它们并不是特定于任务的。微调是调整这些通用模型以更准确、更高效地执行专门任务。当我们遇到特定的NLP任务,如客户评论的情绪分析或特定领域的问题回答时,我们需要对预先训练的模型进行微调,以了解该特定任务和领域的细微差别。

微调的好处是多方面的。首先,它利用了在预训练过程中学到的知识,节省了从头开始训练模型所需的大量时间和计算资源。其次,微调使我们能够更好地执行特定任务,因为模型现在已经适应了微调所针对领域的复杂性和细微差别。

LLM微调过程:分步指南

微调过程通常包括将特定任务的数据集馈送到预先训练的模型,并通过反向传播调整其参数。目标是最小化损失函数,该函数测量模型的预测与数据集中的基本事实标签之间的差异。这个微调过程会更新模型的参数,使其更适合目标任务。

在这里,我们将介绍对情绪分析的大型语言模型进行微调的过程。我们将使用Hugging Face Transformers library,该库提供了对预先训练的模型和实用程序的轻松访问,以便进行微调。

Step 1:加载预训练语言模型和Tokenizer对于这个例子,我们将使用“distillery-base-uncased”模型,这是BERT的一个较轻版本。

from transformers import DistilBertTokenizer, DistilBertForSequenceClassification

# Load the pre-trained tokenizer
tokenizer = DistilBertTokenizer.from_pretrained('distilbert-base-uncased')

# Load the pre-trained model for sequence classification
model = DistilBertForSequenceClassification.from_pretrained('distilbert-base-uncased')

Step 2:准备情绪分析数据集我们需要一个带有文本样本和相应情绪的标记数据集来进行情绪分析。为了便于说明,让我们创建一个小数据集:

texts = ["I loved the movie. It was great!",
         "The food was terrible.",
         "The weather is okay."]
sentiments = ["positive", "negative", "neutral"]

# Tokenize the text samples
encoded_texts = tokenizer(texts, padding=True, truncation=True, return_tensors='pt')

# Extract the input IDs and attention masks
input_ids = encoded_texts['input_ids']
attention_mask = encoded_texts['attention_mask']

# Convert the sentiment labels to numerical form
sentiment_labels = [sentiments.index(sentiment) for sentiment in sentiments]

Step 3:添加自定义分类头预先训练的语言模型本身不包括分类头。我们必须在模型中添加一个来进行情绪分析。在这种情况下,我们将添加一个简单的线性层。

import torch.nn as nn

# Add a custom classification head on top of the pre-trained model
num_classes = len(set(sentiment_labels))
classification_head = nn.Linear(model.config.hidden_size, num_classes)

# Replace the pre-trained model's classification head with our custom head
model.classifier = classification_head

Step 4:微调模型

import torch.optim as optim

# Define the optimizer and loss function
optimizer = optim.AdamW(model.parameters(), lr=2e-5)
criterion = nn.CrossEntropyLoss()

# Fine-tune the model
num_epochs = 3
for epoch in range(num_epochs):
    optimizer.zero_grad()
    outputs = model(input_ids, attention_mask=attention_mask, labels=torch.tensor(sentiment_labels))
    loss = outputs.loss
    loss.backward()
    optimizer.step()

什么是指令微调?

指令微调是一种专门的技术,用于定制大型语言模型,以基于显式指令执行特定任务。虽然传统的微调涉及在特定任务的数据上训练模型,但指令微调通过结合高级指令或演示来指导模型的行为,从而走得更远。

Image

这种方法允许开发人员指定所需的输出,鼓励某些行为,或实现对模型响应的更好控制。

指令微调过程:分步指南

如果我们能够超越传统的微调,提供明确的指令来指导模型的行为,会怎么样?指令微调做到了这一点,为模型输出提供了新的控制和精度水平。在这里,我们将探讨教学微调大型语言模型进行情感分析的过程。

Step 1:加载预训练语言模型和Tokenizer在这个例子中,我们将使用GPT-3。

from transformers import GPT2Tokenizer, GPT2ForSequenceClassification

# Load the pre-trained tokenizer
tokenizer = GPT2Tokenizer.from_pretrained('gpt2')

# Load the pre-trained model for sequence classification
model = GPT2ForSequenceClassification.from_pretrained('gpt2')

Step 2:准备指令数据和情绪分析数据集对于指令微调,我们需要用模型的明确指令来增强情绪分析数据集。让我们创建一个小数据集进行演示:

texts = ["I loved the movie. It was great!",
         "The food was terrible.",
         "The weather is okay."]
sentiments = ["positive", "negative", "neutral"]
instructions = ["Analyze the sentiment of the text and identify if it is positive.",
                "Analyze the sentiment of the text and identify if it is negative.",
                "Analyze the sentiment of the text and identify if it is neutral."]

                # Tokenize the texts, sentiments, and instructions
encoded_texts = tokenizer(texts, padding=True, truncation=True, return_tensors='pt')
encoded_instructions = tokenizer(instructions, padding=True, truncation=True, return_tensors='pt')

# Extract input IDs, attention masks, and instruction IDs
input_ids = encoded_texts['input_ids']
attention_mask = encoded_texts['attention_mask']
instruction_ids = encoded_instructions['input_ids']

Step 3:使用指令自定义模型体系结构为了在微调过程中包含指令,我们需要自定义模型架构。我们可以通过将指令ID与输入ID连接来实现这一点:

import torch

# Concatenate instruction IDs with input IDs and adjust attention mask
input_ids = torch.cat([instruction_ids, input_ids], dim=1)
attention_mask = torch.cat([torch.ones_like(instruction_ids), attention_mask], dim=1)

Step 4:微调模型有了这些指令,我们现在可以在增强数据集上微调GPT-3模型。在微调过程中,指令将指导模型的情绪分析行为。

import torch.optim as optim

# Define the optimizer and loss function
optimizer = optim.AdamW(model.parameters(), lr=2e-5)
criterion = torch.nn.CrossEntropyLoss()

# Fine-tune the model
num_epochs = 3
for epoch in range(num_epochs):
    optimizer.zero_grad()
    outputs = model(input_ids, attention_mask=attention_mask, labels=torch.tensor(sentiments))
    loss = outputs.loss
    loss.backward()
    optimizer.step()

指令微调将标准微调的力量提升到了一个新的水平,使我们能够精确地控制大型语言模型的行为。通过提供明确的说明,我们可以指导模型的输出,并实现更准确和量身定制的结果。

标准微调和指令微调的区别

标准的微调包括在标记的数据集上训练模型,磨练其有效执行特定任务的能力。但是,如果我们想提供明确的指令来指导模型的行为,指令微调就会发挥作用,它提供了无与伦比的控制和适应性。

以下是指令微调和标准微调之间的关键区别:

  • 数据要求:标准微调依赖于特定任务的大量标记数据,而指令微调得益于明确指令提供的指导,使其更适合有限的标记数据。
  • 控制和精度:指令微调允许开发人员指定所需的输出,鼓励某些行为,或更好地控制模型的响应。标准微调可能无法提供这种级别的控制。
  • 从指令中学习:指令微调需要将指令纳入模型架构的额外步骤,而标准微调则没有。

灾难性遗忘:一个危险的挑战

当我们驶向微调的世界时,我们遇到了灾难性遗忘的危险挑战。当模型对新任务的微调抹去或“忘记”了在预训练中获得的知识时,就会出现这种现象。该模型失去了对更广泛语言结构的理解,因为它只关注新任务。

把我们的语言模型想象成一个装满各种知识容器的货舱,每个知识容器都代表着不同的语言细微差别。在预培训期间,这些容器被仔细地装满了对语言的理解。当我们接近一项新任务并开始微调时,船员会重新安排集装箱。它们清空一些,为新的任务特定知识腾出空间。不幸的是,一些原始知识丢失了,导致了灾难性的遗忘。

为了驶出灾难性遗忘的水域,我们需要策略来保护在预训练中获得的宝贵知识。有两种可能的方法。

多任务微调:渐进式学习

在这里,我们逐步将新任务介绍给模型。最初,该模型专注于预训练知识,并慢慢地结合新的任务数据,将灾难性遗忘的风险降至最低。多任务教学微调通过在多个任务上同时训练语言模型,包含了一种新的范式。我们不是一次为一个任务微调模型,而是为每个任务提供明确的指令,指导模型在微调过程中的行为。

Image

多任务指令微调的好处:

  • 知识转移:该模型通过对多个任务进行培训,从不同领域获得见解和知识,增强其整体语言理解能力。
  • 共享表示:多任务指令微调允许模型在任务之间共享表示。这种知识共享提高了模型的泛化能力。
  • 效率:与单独微调每个任务相比,同时对多个任务进行训练可以减少计算成本和时间。

参数高效微调:迁移学习

在这里,我们在微调过程中冻结模型的某些层。通过冻结负责基本语言理解的早期层,我们保留了核心知识,同时只为特定任务微调后期层。

什么是参数高效微调?

参数高效微调(PEFT)技术只更新一小部分参数,而不是完全微调,这在监督学习期间更新每个模型权重。一些路径技术集中于微调现有模型参数的一部分,如特定的层或组件,同时冻结大多数模型权重。其他方法添加一些新的参数或层,并且仅微调新的组件;它们不会影响原始模型权重。大部分(如果不是全部的话)LLM参数使用PEFT保持冻结。因此,与原始LLM相比,训练的参数明显更少。所需要的显存及算力也比标准微调小得多。

Image

PEFT使参数高效模型具有令人印象深刻的性能,彻底改变了NLP的格局。在低数据状态下,PEFT方法也被证明优于标准微调,并能更好地推广到领域外场景。以下是我们使用PEFT的几个原因:

  • 降低计算成本:PEFT需要更少的GPU和GPU时间,使其更易于访问,更具成本效益,用于训练大型语言模型。
  • 更快的训练时间:使用PEFT,模型可以更快地完成训练,从而实现快速迭代和在现实应用中更快地部署。
  • 更低的硬件要求:PEFT使用更小的GPU高效工作,并且需要更少的内存,使其适用于资源受限的环境。
  • 改进的建模性能:PEFT通过减少过拟合,为不同的任务生成更稳健、更准确的模型。
  • 节省空间的存储:通过跨任务共享权重,PEFT最大限度地减少了存储需求,优化了模型部署和管理。

使用PEFT进行微调

Step 1:加载模型在这个例子中,我们使用opt-6.7b模型,其半精度模型(float16)在Hub上的权重约为13GB。如果我们以8-bit加载,它将需要大约7GB的内存。

import os
os.environ["CUDA_VISIBLE_DEVICES"]="0"
import torch
import torch.nn as nn
import bitsandbytes as bnb
from transformers import AutoTokenizer, AutoConfig, AutoModelForCausalLM

model = AutoModelForCausalLM.from_pretrained(
    "facebook/opt-6.7b", 
    load_in_8bit=True, 
    device_map='auto',
)

tokenizer = AutoTokenizer.from_pretrained("facebook/opt-6.7b")

Step 2:模型上的后处理在对8-bit模型应用一些后处理以启用训练之前,让我们冻结所有层,并在float32中投射层规范以获得稳定性。出于同样的原因,我们也将最后一层的输出投射到float32中。

for param in model.parameters():
  param.requires_grad = False  # freeze the model - train adapters later
  if param.ndim == 1:
    param.data = param.data.to(torch.float32)

model.gradient_checkpointing_enable()  # reduce number of stored activations
model.enable_input_require_grads()

class CastOutputToFloat(nn.Sequential):
  def forward(self, x): return super().forward(x).to(torch.float32)
model.lm_head = CastOutputToFloat(model.lm_head)

Step 3:使用LoRA加载PeftModel,我们将使用peft的get_peft_model函数来使用低秩适配器(LoRA)。该函数计算并打印给定模型中可训练参数和所有参数的总数。连同可训练参数的百分比,提供模型复杂性和训练资源需求的概述。

def print_trainable_parameters(model):

     # Prints the number of trainable parameters in the model.

       trainable_params = 0
    all_param = 0
    for _, param in model.named_parameters():
        all_param += param.numel()
        if param.requires_grad:
            trainable_params += param.numel()
    print(
        f"trainable params: {trainable_params} || all params: {all_param} || 
          trainable%: {100 * trainable_params / all_param}"

    )

使用Peft库创建具有特定配置设置的LoRA模型,包括丢弃、偏差和任务类型。然后,它获得模型的可训练参数,并打印可训练参数和所有参数的总数,以及可训练参数的百分比。

from peft import LoraConfig, get_peft_model 

config = LoraConfig(
    r=16,
    lora_alpha=32,
    target_modules=["q_proj", "v_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM"
)

model = get_peft_model(model, config)
print_trainable_parameters(model)

Step 3:训练模型使用Hugging Face Transformers and Datasets libraries在给定的数据集上训练语言模型。使用“transformers.Trainer”类定义训练设置,包括批量大小、学习率和其他与训练相关的配置,然后在指定的数据集上训练模型。

import transformers
from datasets import load_dataset
data = load_dataset("Abirate/english_quotes")
data = data.map(lambda samples: tokenizer(samples['quote']), batched=True)

trainer = transformers.Trainer(
    model=model, 
    train_dataset=data['train'],
    args=transformers.TrainingArguments(
        per_device_train_batch_size=4, 
        gradient_accumulation_steps=4,
        warmup_steps=100, 
        max_steps=200, 
        learning_rate=2e-4, 
        fp16=True,
        logging_steps=1, 
        output_dir='outputs'
    ),
    data_collator=transformers.DataCollatorForLanguageModeling(tokenizer, mlm=False)
)
model.config.use_cache = False  # silence the warnings. Please re-enable for inference!
trainer.train()

微调LLM的现实应用

我们将更深入地研究一些令人兴奋的现实世界中微调大型语言模型的用例,在这些用例中,NLP的进步正在改变行业并赋予创新解决方案权力。

  • 情绪分析:情绪分析的微调语言模型允许企业分析客户反馈、产品评论和社交媒体情绪,以了解公众感知并做出数据驱动的决策。
  • 命名实体识别(NER):通过微调NER模型,可以从文本中自动提取名称、日期和位置等实体,从而实现信息检索和文档分类等应用。
  • 语言翻译:经过微调的模型可以用于机器翻译,打破语言障碍,实现不同语言之间的无缝沟通。
  • 聊天机器人和虚拟助理:通过微调语言模型,聊天机器人和数字助理可以提供更准确和与上下文相关的响应,增强用户体验。
  • 医学文本分析:微调模型可以帮助分析医学文档、电子健康记录和医学文献,帮助医疗保健专业人员进行诊断和研究。
  • 财务分析:微调语言模型可用于财务情绪分析、预测市场趋势以及从庞大的数据集生成财务报告。
  • 法律文档分析:经过微调的模型可以帮助进行法律文档分析、合同审查和自动文档摘要,为法律专业人员节省时间和精力。

在现实世界中,对大型语言模型进行微调已经在不同的行业中找到了应用,使企业和研究人员能够利用NLP的能力执行广泛的任务,从而提高效率,改进决策,丰富用户体验。

原文链接:https://www.analyticsvidhya.com/blog/2023/08/fine-tuning-large-language-models/