以下是关于大模型微调课程(https://learn.deeplearning.ai/finetuning-large-language-models/lesson/1/introduction)的总结。
微调
为什么要微调LLMs?
微调是重要的,因为它允许我们将通用型模型(例如GPT-3)专门用于特定任务或领域。可以将其类比为将全科医生变成心脏病医生或皮肤科医生。微调使我们能够向模型提供更多数据,使其能够学习和适应特定用例或领域。
以下是为什么微调很重要的原因:
- 学习新信息: 微调允许模型从额外的数据中学习,增强其知识和专业知识。
- 一致性: 它有助于模型提供更一致和上下文相关的输出。
- 减少幻觉: 微调可以减少模型生成不正确或虚构信息的情况。
- 定制: 微调将模型定制为特定用例,使其对代码生成、客户支持或特定领域的内容非常相关。
比较微调和提示工程
提示工程涉及制定查询或提示,以指导模型的响应。虽然这是一种有用的技术,但它也有局限性:
- 提示的优点: 无需额外数据,前期成本低,无需技术专业知识。
- 提示的缺点: 数据容量有限,可能导致不正确的响应,难以处理更大的数据集。
另一方面,微调是提示的补充,并提供了多种优势:
- 微调的优点: 可以处理几乎无限量的数据,能够纠正先前学到的不正确信息,适用于企业和特定领域的用例。
- 微调的缺点: 需要更多高质量的数据,涉及前期计算成本,需要一些技术知识。
总之,提示适用于快速和通用的用例,而微调则适用于专业化、企业级的应用,其中精度和一致性至关重要。
微调自己的LLM的好处
微调自己的LLM提供了许多好处:
- 性能改进: 微调的模型更擅长避免不准确和在特定领域内保持一致性。
- 隐私: 在我们自己的环境中进行微调可以增强数据安全性并防止数据泄露。
- 成本控制: 微调可以降低每个请求的成本,优化可用性并减少特定应用程序的延迟。
- 审查: 我们可以实施自定义规则和审查以指导模型的行为。
预训练
-
预训练是微调之前的第一步,在这一步骤中,模型从完全随机的状态开始,没有关于世界的知识。
-
预训练的学习目标通常是下一个标记的预测,或者更简单地说,就是预测下一个词语。
-
在预训练期间,模型通过阅读大量的未标记数据,通常是从互联网上抓取的数据,来学习语言和知识。
-
这个过程通常被称为自监督学习,因为模型通过自己进行下一个标记的预测来进行训练。
-
预训练是资源密集且耗时的过程,需要大量的数据和计算资源。
微调的特点
-
微调是在预训练之后的步骤,用于将预训练模型定制为特定任务。
-
微调允许使用未标记数据或包含标签的数据,以适应不同的任务。
-
与预训练相比,微调需要的数据较少,因为模型在预训练期间已经获得了大量知识。
-
微调是将通用语言模型(LLM)转化为适用于特定应用程序的关键工具,例如聊天机器人或信息检索任务。
-
微调的任务通常与预训练相同,即下一个标记的预测。
微调的任务
- 微调任务通常包括文本输入和文本输出,适用于语言模型。
- 微调任务可以分为两大类:提取和扩展。
- 提取任务涉及将文本输入转化为更短的文本输出,例如关键词提取或路由任务。
- 扩展任务涉及将文本输入转化为更长的文本输出,例如生成代码、编写电子邮件或回答问题。
- 微调的成功取决于清晰定义任务、知道好的、坏的和更好的输出是什么,以及选择适当的数据格式。
数据集
- 预训练数据集通常包含大量的非结构化数据,从互联网上抓取而来。
- 微调数据集通常更加结构化,与任务相关,通常包含问题-答案对或指令-响应对。
- 结构化微调数据可以有助于任务清晰性和模型性能。
指令微调
- 指令微调涉及使用模板来引导模型的响应,以便更好地结构化输入和输出数据。
- 模板可以帮助模型生成适当的响应,并通常包括指示模型应该期望什么类型的问题或指令。
- 以下为通过模版构建指令微调的数据集数据准备
数据准备
数据准备的重要性
- 数据质量至关重要:为微调提供高质量的数据是关键,因为低质量的数据会导致模型输出垃圾结果。确保提供优质的输入数据,以获得有意义的输出。
- 数据多样性:数据应具有多样性,涵盖我们用例的各个方面。如果输入和输出都相同,模型可能会记住它们,而不是生成多样性的输出。
- 真实数据与生成数据:尽管可以使用生成数据,但实际数据通常更有效和有用,特别是对于涉及文本创作任务的情况。生成数据具有固定的模式,而真实数据更具多样性和真实性。
- 数据量大:通常情况下,数据量越大越好,但预训练已经处理了一部分问题,因此数据量不如数据质量、多样性和真实性重要。
数据准备步骤
数据准备涉及以下步骤:
- 收集指令-响应对:首先,我们需要收集指令和相应的数据对。这可以是问题-答案对或其他形式的对话数据。
- 连接数据对或添加提示模板:将数据对连接在一起,或者添加提示模板,以便将其传递给模型进行微调。
- 对数据进行token化:将文本数据转换为数字,以便模型处理。token化不仅仅是按词汇划分,它还依赖于字符出现的频率。
- 添加填充或截断数据:确保数据的长度适合输入模型,这通常需要填充或截断文本。模型操作的数据必须具有相同的长度。
标记化
- 标记化将文本转换为数字表示,这样模型可以处理它们。
- 标记化不仅仅是按词划分,还涉及字符的编码。
- 使用正确的标记化器与模型匹配很重要,否则模型可能无法理解数据。
数据集拆分
- 将数据集拆分为训练集和测试集是微调的关键步骤。
- 随机化数据集的顺序以增加模型的多样性。
代码1
import pandas as pd
import datasets
from pprint import pprint
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("EleutherAI/pythia-70m")
# 数据集读取与制作
filename = "lamini_docs.jsonl"
instruction_dataset_df = pd.read_json(filename, lines=True)
examples = instruction_dataset_df.to_dict()
if "question" in examples and "answer" in examples:
text = examples["question"][0] + examples["answer"][0]
elif "instruction" in examples and "response" in examples:
text = examples["instruction"][0] + examples["response"][0]
elif "input" in examples and "output" in examples:
text = examples["input"][0] + examples["output"][0]
else:
text = examples["text"][0]
prompt_template = """### Question:
{question}
### Answer:"""
num_examples = len(examples["question"])
finetuning_dataset = []
for i in range(num_examples):
question = examples["question"][i]
answer = examples["answer"][i]
text_with_prompt_template = prompt_template.format(question=question)
finetuning_dataset.append({"question": text_with_prompt_template, "answer": answer})
from pprint import pprint
print("One datapoint in the finetuning dataset:")
pprint(finetuning_dataset[0])
One datapoint in the finetuning dataset:
{'answer': 'Lamini has documentation on Getting Started, Authentication, '
'Question Answer Model, Python Library, Batching, Error Handling, '
'Advanced topics, and class documentation on LLM Engine available '
'at https://lamini-ai.github.io/.',
'question': '### Question:\n'
'What are the different types of documents available in the '
'repository (e.g., installation guide, API documentation, '
"developer's guide)?\n"
'\n'
'### Answer:'}
# 定义tokenize函数
def tokenize_function(examples):
if "question" in examples and "answer" in examples:
text = examples["question"][0] + examples["answer"][0]
elif "input" in examples and "output" in examples:
text = examples["input"][0] + examples["output"][0]
else:
text = examples["text"][0]
tokenizer.pad_token = tokenizer.eos_token
tokenized_inputs = tokenizer(
text,
return_tensors="np",
padding=True,
)
max_length = min(
tokenized_inputs["input_ids"].shape[1],
2048
)
tokenizer.truncation_side = "left"
tokenized_inputs = tokenizer(
text,
return_tensors="np",
truncation=True,
max_length=max_length
)
return tokenized_inputs
# Tokenize数据集
finetuning_dataset_loaded = datasets.load_dataset("json", data_files=filename, split="train")
tokenized_dataset = finetuning_dataset_loaded.map(
tokenize_function,
batched=True,
batch_size=1,
drop_last_batch=True
)
print(tokenized_dataset)
Dataset({
features: ['question', 'answer', 'input_ids', 'attention_mask'],
num_rows: 1400
})
tokenized_dataset = tokenized_dataset.add_column("labels", tokenized_dataset["input_ids"])
# 分割数据集
split_dataset = tokenized_dataset.train_test_split(test_size=0.1, shuffle=True, seed=123)
print(split_dataset)
训练语言模型 (LLM)
了解训练过程
- 训练概览
- 训练 LLM 类似于训练其他神经网络,比如前馈或卷积神经网络。
- 它包括提供训练数据、计算损失、反向传播和迭代地更新模型权重。
- 训练数据
- 首先提供用于训练的数据集。
- 损失计算
- 模型进行初始预测,然后将其与实际期望响应进行比较,得出损失值。
- 权重更新
- 通过模型反向传播损失来更新其权重。
- 这个过程迭代进行,直到模型改进并与期望响应更好地一致。
LLM 训练中的超参数
- 超参数选择
- 许多超参数会影响训练过程,包括学习率、学习调度程序和优化器设置等。
在 PyTorch 中进行训练
- 使用 PyTorch 进行训练
- 概述了在 PyTorch 中训练模型的基本步骤,包括加载数据、分批处理、模型前向传递、损失计算和权重更新。
使用高级库简化训练
- 高级训练库
- 高级库(如 Lamini Llama)显著简化了训练流程,将复杂的代码减少到仅几行。
- 这些库可以在外部 GPU 上运行,并有效处理开源模型。
- 建议的模型大小
- 虽然小型模型可能在 CPU 上运行,但实际的应用通常需要更大的模型(大约十亿个参数或更多)以获得显著的性能改进。
微调模型
- 微调过程
- 微调涉及在特定数据上训练现有模型,以使其适应特定任务。
- 良好的微调模型可以显著提高性能。
- 用于探索的小型模型
- 由于计算要求较小,因此小型模型通常用于探索性目的。
通过微调实现更好的结果
- 微调以提高性能
- 微调模型可以显著改进其响应,使其更符合期望的输出。
- 改进输出的示例
- 通过更多步骤的训练,可以改进模型响应,如在轻微微调模型和更彻底微调模型生成的答案比较中所示。
上下文响应的调节
- 数据中的调节
- 在训练数据中加入调节线索可以鼓励模型保持相关的讨论,避免离题。
使用 Llamani Llama 简化训练
- 使用 Llamani Llama 进行训练
- Llamani Llama 提供了一种简化的方式来在只有几行代码的情况下训练模型。
- 用户可以轻松加载模型、数据集,并启动训练。
- 评估模型性能
- 一旦模型训练完成,评估其在测试数据或其他查询上的性能是评估其有效性的关键。
代码2
# 加载模型
base_model = AutoModelForCausalLM.from_pretrained(model_name)
# 设置device
device_count = torch.cuda.device_count()
if device_count > 0:
logger.debug("Select GPU device") # 选择GPU设备
device = torch.device("cuda")
else:
logger.debug("Select CPU device") # 选择CPU设备
device = torch.device("cpu")
base_model.to(device)
# 设置步长
max_steps = 3
# 保存checkpoint路径
trained_model_name = f"lamini_docs_{max_steps}_steps"
output_dir = trained_model_name
# 设置训练参数
training_args = TrainingArguments(
# 学习率
learning_rate=1.0e-5,
# 训练周期数
num_train_epochs=1,
# 最大训练步数(每一步是一个数据批次)
# 如果不是-1,则会覆盖num_train_epochs
max_steps=max_steps,
# 训练批次大小
per_device_train_batch_size=1,
# 保存模型检查点的目录
output_dir=output_dir,
# 其他参数
overwrite_output_dir=False, # 覆盖输出目录的内容
disable_tqdm=False, # 禁用进度条
eval_steps=120, # 两次评估之间的更新步数
save_steps=120, # 在#步骤后保存模型
warmup_steps=1, # 学习率调度器的预热步数
per_device_eval_batch_size=1, # 评估的批次大小
evaluation_strategy="steps",
logging_strategy="steps",
logging_steps=1,
optim="adafactor",
gradient_accumulation_steps=4,
gradient_checkpointing=False,
# 早停止的参数
load_best_model_at_end=True,
save_total_limit=1,
metric_for_best_model="eval_loss",
greater_is_better=False
)
# model浮点操作数计算
model_flops = (
base_model.floating_point_ops(
{
"input_ids": torch.zeros(
(1, training_config["model"]["max_length"])
)
}
)
* training_args.gradient_accumulation_steps
)
print(base_model)
print("Memory footprint", base_model.get_memory_footprint() / 1e9, "GB")
print("Flops", model_flops / 1e9, "GFLOPs")
# 开始训练
trainer = Trainer(
model=base_model,
model_flops=model_flops,
total_steps=max_steps,
args=training_args,
train_dataset=train_dataset,
eval_dataset=test_dataset,
)
training_output = trainer.train()
# 模型保存
save_dir = f'{output_dir}/final'
trainer.save_model(save_dir)
print("Saved model to:", save_dir)
模型训练后的评估与分析
在完成模型训练之后,下一步是进行评估,查看模型的表现如何。这是非常重要的一步,因为AI的关键在于不断迭代改进。以下是有关模型评估和分析的详细总结:
评估方法
- 评估生成模型通常非常困难,因为没有明确的度量标准,而这些模型的性能会随时间不断提高,难以用度量标准来跟踪。
- 人工评估通常是最可靠的方法,即由了解领域的专家来评估模型的输出。
- 使用高质量的测试数据集至关重要,它需要准确、广泛覆盖多个测试用例,并且不能包含在训练数据中。
- ELO比赛排名是另一种新兴的评估方法,它类似于A-B测试,比较多个模型之间的性能,特别是在棋类比赛中使用。
基于多种方法的综合评估
- 存在一种常见的开放式LLM基准测试套件,该套件综合了多种评估方法,通过对这些方法的综合排名来评估模型性能。
- 例如,EleutherAI开发了一套不同基准,包括ARC(一组小学问题)、HellaSwag(常识测试)、MMLU(涵盖多个小学科目)和TruthfulQA(模型复制在线常见虚假信息的能力)。
- 基准测试套件的排名通常会根据多个评估方法的平均值进行排序。
模型性能评估
- 模型性能评估通常不限于使用单一指标,例如精确匹配,而可以使用多种方法,如嵌入、与目标的距离等。
- 手动检查是非常有效的方法,通过检查模型生成的答案与目标答案之间的关系来评估模型性能。
错误分析
- 错误分析是一种对模型性能的分析方法,通过分类错误来了解常见错误类型,首先训练模型,然后执行分析。
- 对于微调,由于已经有一个经过预训练的基础模型,因此可以在微调之前执行错误分析,以了解基础模型的性能和优化方向。
- 常见的错误类型包括拼写错误、冗长回答以及重复性回答,分析这些错误可以帮助改进模型。
运行评估和基准测试
- 可以通过运行模型在测试数据集上的评估和基准测试来了解模型的性能。
- 使用合适的代码可以高效地在GPU上批处理运行模型,但通常在CPU上也可以进行一些简单的测试。
- 需要选择适合任务的评估指标,不同任务可能需要不同的指标来衡量性能。
- 模型性能在不同任务上可能会有所不同,因此选择适当的评估方法对于实际应用非常重要。
这些是评估和分析模型性能的一些关键方面,对于不同的任务和用例,可能需要使用不同的评估方法和指标来确定模型的效果。
代码
# 微调模型加载
model_name = "lamini/lamini_docs_finetuned"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name)
# 防止dropout等的影响
model.eval()
# 定义评估函数,此处只定义了一个简单的评估函数
def is_exact_match(a, b):
return a.strip() == b.strip()
# 使用以上评估函数进行评估
n = 10
metrics = {'exact_matches': []}
predictions = []
for i, item in tqdm(enumerate(test_dataset)):
print("i Evaluating: " + str(item))
question = item['question']
answer = item['answer']
try:
predicted_answer = inference(question, model, tokenizer)
except:
continue
predictions.append([predicted_answer, answer])
#fixed: exact_match = is_exact_match(generated_answer, answer)
exact_match = is_exact_match(predicted_answer, answer)
metrics['exact_matches'].append(exact_match)
if i > n and n != -1:
break
print('Number of exact matches: ', sum(metrics['exact_matches']))
# 将预测回答与标签对比
df = pd.DataFrame(predictions, columns=["predicted_answer", "target_answer"])
print(df)
# 使用ARC benchmark进行评估
!python lm-evaluation-harness/main.py --model hf-causal --model_args pretrained=lamini/lamini_docs_finetuned --tasks arc_easy --device cpu
模型微调和高级训练方法
实用的微调步骤
- 首先,确定我们的任务,收集与任务输入和输出相关的数据,并按照任务的结构组织数据。
- 如果没有足够的数据,可以考虑生成一些数据或使用提示模板来生成更多数据。
- 最初,建议使用一个拥有 4 亿到 10 亿参数的小型模型,以了解该模型的性能情况。
- 根据需要变化提供给模型的数据量,以了解数据量对模型性能的影响。
- 然后,评估模型,查看哪些方面表现良好,哪些方面表现不佳。
- 最后,收集更多的数据,通过评估来改进模型。
提高任务复杂性和模型大小
- 逐步提高任务的复杂性,使任务更加困难。
- 随着任务复杂性的增加,可以增加模型的大小,以提高在更复杂任务上的性能。
- 写作任务通常更具挑战性,因为它们涉及更多的模型生成标记。这包括聊天、写电子邮件、编写代码等任务。
- 难度更大的任务通常需要更大的模型来处理。
- 可以通过将多个任务组合在一起,要求模型执行多个任务的组合,而不仅仅是一个任务,从而增加任务的难度。
计算资源和硬件需求
- 需要考虑用于运行模型的硬件资源。通常需要使用GPU进行训练和推理。
- GPU的内存大小对于可以运行的模型大小具有限制。训练时需要更多的内存来存储梯度和优化器,而推理时需要的内存较少。
- 可以选择合适的GPU类型和配置,以满足模型大小和性能需求。
参数高效微调(PEFT)
- 参数高效微调(PEFT)是一组不同的方法,可以更有效地使用参数并训练模型。
- 一个PEFT的方法是LoRa,它可以显著减少需要训练的权重数量,从而减少GPU内存的需求。
- 使用LoRa,我们可以训练新的权重并将其与预训练的权重分开。这些新权重通过原始权重的秩分解矩阵进行更改。
- 在推理时,我们可以将这些新权重与预训练权重合并,以获得更高效的微调模型,同时减少内存占用。
- LoRa还可以用于将模型适应新任务,我们可以在不同客户的数据上训练模型,并在推理时根据需要将它们合并。
这些是关于模型微调和高级训练方法的关键总结。在选择模型大小、任务复杂性和硬件资源时,需要谨慎考虑,并根据实际需求采取适当的步骤和方法。