【阅读记录-章节7】Build a Large Language Model (From Scratch)

embedded/2024/12/26 15:18:04/

系列文章目录

【阅读记录-章节1】Build a Large Language Model (From Scratch)
【阅读记录-章节2】Build a Large Language Model (From Scratch)
【阅读记录-章节3】Build a Large Language Model (From Scratch)
【阅读记录-章节4】Build a Large Language Model (From Scratch)
【阅读记录-章节5】Build a Large Language Model (From Scratch)
【阅读记录-章节6】Build a Large Language Model (From Scratch)
【阅读记录-章节7】Build a Large Language Model (From Scratch)


文章目录

  • 系列文章目录
  • 7. Fine-tuning to follow instructions
    • 7.1 Introduction to instruction fine-tuning
    • 7.2 Preparing a dataset for supervised instruction fine-tuning
      • 下载数据集
      • 实现提示格式化函数
    • 7.3 Organizing data into training batches
      • 1. 实现指令数据集类
      • 自定义填充过程的实现
        • 自定义 `collate` 函数的初版
        • 改进版 `collate` 函数(包含目标 token IDs)
      • 添加 `-100` 掩码的改进
      • 使用 `-100` 的损失计算原理
    • 7.4 Creating data loaders for an instruction dataset
    • 7.5 Loading a pretrained LLM
      • 加载预训练模型
      • 基线评估
        • 输出:
      • 结果分析
    • 7.6 Fine-tuning the LLM on instruction data
        • 计算初始损失
      • 硬件限制处理
      • 设置训练过程
        • 训练输出
      • 结果分析
      • 损失曲线可视化
      • 下一步:提取和保存生成的响应
    • 7.7 Extracting and saving responses
      • 提取和对比响应
      • 示例输出
      • 结果分析
      • 自动化评估
      • 自动化对话评估的实现
      • 保存微调后的模型
    • 7.8 Evaluating the fine-tuned LLM
      • 通过 Web API 使用更大的 LLM
      • 安装 Ollama 并验证其运行状态
        • 安装 Ollama
        • 运行 Ollama
        • 替代的 Ollama 模型
      • 在新的 Python 会话中运行代码
        • 查询本地 Ollama 模型
        • 评估指令微调的 LLM
    • 7.9 Conclusions
      • 7.9.1 接下来做什么?
      • 在快速发展的领域中保持最新
      • 7.9.3 最后的话
      • 总结


7. Fine-tuning to follow instructions

之前,我们实现了LLM(大语言模型)的架构,进行了预训练,并将预训练的权重从外部资源导入到我们的模型中。接着,我们专注于对LLM进行针对特定分类任务的微调:区分垃圾短信和非垃圾短信。现在,我们将实现微调LLM以遵循人类指令的过程,如图7.1所示。
在这里插入图片描述

指令微调是开发用于聊天机器人应用程序、个人助理以及其他对话任务的LLM的主要技术之一。图7.1展示了微调LLM的两种主要方式:用于分类的微调(步骤8)和指令微调(步骤9)。我们在第6章中已经实现了步骤8。现在,我们将使用一个指令数据集对LLM进行微调。

7.1 Introduction to instruction fine-tuning

我们现在知道,LLM的预训练涉及一种训练过程,在这个过程中模型通过一次生成一个单词来学习。由此生成的预训练LLM具有文本补全的能力,也就是说,它可以根据输入的片段完成句子或撰写文本段落。然而,预训练的LLM通常在面对特定指令时表现欠佳,比如“修正这段文本的语法”或“将这段文本转换为被动语态”。稍后,我们将研究一个具体示例,在该示例中我们加载预训练LLM作为指令微调(也称为监督指令微调)的基础。

在这里,我们专注于提升LLM遵循这些指令并生成期望响应的能力,如图7.2所示。
在这里插入图片描述

准备数据集是指令微调的关键部分。接下来,我们将完成指令微调过程中三个阶段的所有步骤,从数据集准备开始,如图7.3所示。
在这里插入图片描述

7.2 Preparing a dataset for supervised instruction fine-tuning

让我们下载并格式化用于对预训练LLM进行指令微调的指令数据集。该数据集由1100个指令-响应对组成,类似于图7.2中的示例。这一数据集是专门为本书创建的,但有兴趣的读者可以在附录B中找到其他公开可用的指令数据集。

以下代码实现并执行了一个函数,用于下载这个数据集。数据集是一个相对较小的文件(仅204 KB),以JSON格式存储。JSON(JavaScript Object Notation)类似于Python字典的结构,提供了一种简单的、既可读又便于机器处理的数据交换格式。

下载数据集

import json
import os
import urllibdef download_and_load_file(file_path, url):if not os.path.exists(file_path):with urllib.request.urlopen(url) as response:text_data = response.read().decode("utf-8")with open(file_path, "w", encoding="utf-8") as file:file.write(text_data)else:with open(file_path, "r", encoding="utf-8") as file:text_data = file.read()with open(file_path, "r") as file:data = json.load(file)return datafile_path = "instruction-data.json"
url = ("https://raw.githubusercontent.com/rasbt/LLMs-from-scratch""/main/ch07/01_main-chapter-code/instruction-data.json"
)
data = download_and_load_file(file_path, url)
print("Number of entries:", len(data))

执行上述代码后,输出结果为:

Number of entries: 1100

从JSON文件加载的data列表包含指令数据集的1100条记录。让我们打印一条记录以查看其结构:

print("Example entry:\n", data[50])

示例记录的内容为:

Example entry:
{'instruction': 'Identify the correct spelling of the following word.','input': 'Ocassion','output': "The correct spelling is 'Occasion.'"}

从示例中可以看到,这些记录是Python字典对象,每个对象包含三个字段:instruction(指令)、input(输入)和output(输出)。我们再看另一个示例:

print("Another example entry:\n", data[999])

输出结果表明,有时input字段可能为空:

Another example entry:
{'instruction': "What is an antonym of 'complicated'?",'input': '','output': "An antonym of 'complicated' is 'simple'."}

指令微调的核心是使用明确提供的输入-输出对(如从JSON文件提取的记录)来训练模型。可以通过多种方法将这些记录格式化为适合LLM的输入格式。
在这里插入图片描述

图7.4展示了两种常用的格式化方法(提示样式),这些方法被用于训练知名的LLM,例如Alpaca和Phi-3。

  • Alpaca 是最早公开其指令微调过程的LLM之一。
  • Phi-3 是由微软开发的,用来展示提示样式的多样性。

本章其余部分采用Alpaca提示样式,因为它是最流行的样式之一,帮助定义了微调的初始方法。

实现提示格式化函数

以下是将数据列表中的记录转换为Alpaca样式输入格式的format_input函数:

def format_input(entry):instruction_text = (f"Below is an instruction that describes a task. "f"Write a response that appropriately completes the request."f"\n\n### Instruction:\n{entry['instruction']}")input_text = (f"\n\n### Input:\n{entry['input']}" if entry["input"] else "")return instruction_text + input_text

format_input函数接收一个字典记录作为输入,并构造一个格式化字符串。让我们测试它对数据集中第50条记录的效果:

model_input = format_input(data[50])
desired_response = f"\n\n### Response:\n{data[50]['output']}"
print(model_input + desired_response)

格式化后的输入如下:

Below is an instruction that describes a task. Write a response that
appropriately completes the request.### Instruction:
Identify the correct spelling of the following word.### Input:
Ocassion### Response:
The correct spelling is 'Occasion.'

请注意,format_input函数会跳过可选的### Input:部分,如果input字段为空。我们可以通过将format_input函数应用到之前查看的data[999]记录上来测试这一点:

model_input = format_input(data[999])
desired_response = f"\n\n### Response:\n{data[999]['output']}"
print(model_input + desired_response)

输出结果显示,对于input字段为空的记录,格式化后的输入中不会包含### Input:部分:

Below is an instruction that describes a task. Write a response that
appropriately completes the request.### Instruction:
What is an antonym of 'complicated'?### Response:
An antonym of 'complicated' is 'simple'.

在进入下一节并设置PyTorch数据加载器之前,我们将数据集划分为训练集、验证集和测试集,这与上一章垃圾邮件分类数据集的划分方法类似。以下代码展示了如何计算各部分的比例:

# 使用85%的数据作为训练集
train_portion = int(len(data) * 0.85)
# 使用10%的数据作为测试集
test_portion = int(len(data) * 0.1)
# 剩余5%的数据作为验证集
val_portion = len(data) - train_portion - test_portiontrain_data = data[:train_portion]
test_data = data[train_portion:train_portion + test_portion]
val_data = data[train_portion + test_portion:]print("Training set length:", len(train_data))
print("Validation set length:", len(val_data))
print("Test set length:", len(test_data))

这段代码将数据集按以下比例划分:

  • 85% 用于训练
  • 10% 用于测试
  • 5% 用于验证

执行代码后,划分后的数据集大小如下:

Training set length: 935
Validation set length: 55
Test set length: 110

成功下载并划分数据集后,我们已经清楚了数据集的提示格式化方法。接下来,我们将专注于开发构建训练批次的方法,为LLM的指令微调过程做好准备。

7.3 Organizing data into training batches

随着我们进入指令微调过程的实现阶段,下一步(如图7.5所示)是有效地构建训练批次。这需要定义一种方法,以确保模型在微调过程中接收经过格式化的训练数据。
在这里插入图片描述

在上一章中,训练批次是由PyTorch的DataLoader类自动创建的,它使用默认的collate函数将样本列表合并为批次。collate函数的作用是将一组独立的数据样本合并为一个单一的批次,以便模型在训练时可以高效处理。

然而,对于指令微调来说,批次处理过程稍显复杂,因此我们需要创建一个自定义的collate函数,稍后将其插入到DataLoader中。我们实现这个自定义的collate函数是为了满足指令微调数据集的特定需求和格式化要求。

我们将分几个步骤解决批次处理问题,包括编码自定义的collate函数,如图7.6所示。
在这里插入图片描述

首先,为了实现步骤2.1和2.2,我们需要编写一个InstructionDataset类,该类会应用format_input函数并对数据集中所有输入进行预分词(pretokenization),这与第6章中的SpamDataset类似。这一两步过程的细节(如图7.7所示)将在InstructionDataset类的__init__构造方法中实现。
在这里插入图片描述

1. 实现指令数据集类

指令数据集类 InstructionDataset 的任务是将数据集的每条指令-响应对格式化并预处理为可用于模型训练的编码格式。

import torch
from torch.utils.data import Datasetclass InstructionDataset(Dataset):def __init__(self, data, tokenizer):self.data = dataself.encoded_texts = []# 预分词每条数据for entry in data:instruction_plus_input = format_input(entry)response_text = f"\n\n### Response:\n{entry['output']}"full_text = instruction_plus_input + response_textself.encoded_texts.append(tokenizer.encode(full_text))def __getitem__(self, index):return self.encoded_texts[index]def __len__(self):return len(self.data)

类似于分类微调中使用的方法,为了加速训练,我们通过将多个训练样本收集到一个批次中。这需要将所有输入填充到相同的长度。与分类微调一样,我们使用 <|endoftext|> 作为填充标记。

与其将 <|endoftext|> 直接附加到文本输入中,我们可以将与 <|endoftext|> 对应的 token ID 直接附加到预分词的输入中。我们可以使用分词器的 .encode 方法对 <|endoftext|> 进行编码,以确认使用哪个 token ID:

import tiktokentokenizer = tiktoken.get_encoding("gpt2")
print(tokenizer.encode("<|endoftext|>", allowed_special={"<|endoftext|>"}))

输出的 token ID 是 50256
在这里插入图片描述

进入流程的步骤 2.3(参见图 7.6),我们采用了一种更复杂的方法,开发了一个可以传递给数据加载器的自定义 collate 函数。该自定义 collate 函数将每个批次中的训练样本填充到相同的长度,同时允许不同批次具有不同的长度(如图 7.8 所示)。这种方法通过仅将序列扩展到每个批次中最长的序列长度,而非整个数据集的最大长度,从而最大限度地减少了不必要的填充。
在这里插入图片描述

自定义填充过程的实现

为了处理指令微调中的批次数据,我们实现了一个自定义的 collate 函数,用于填充序列并生成目标 token IDs。

自定义 collate 函数的初版

以下是初版的 collate 函数,用于将输入填充到批次中最长的序列长度:

def custom_collate_draft_1(batch, pad_token_id=50256, device="cpu"):batch_max_length = max(len(item) + 1 for item in batch)  # 找到批次中最长的序列长度inputs_lst = []for item in batch:new_item = item.copy()new_item += [pad_token_id]  # 添加一个填充值padded = new_item + [pad_token_id] * (batch_max_length - len(new_item))  # 填充到最长长度inputs = torch.tensor(padded[:-1])  # 删除额外的填充值inputs_lst.append(inputs)inputs_tensor = torch.stack(inputs_lst).to(device)return inputs_tensor

测试代码:

inputs_1 = [0, 1, 2, 3, 4]
inputs_2 = [5, 6]
inputs_3 = [7, 8, 9]
batch = [inputs_1, inputs_2, inputs_3]
print(custom_collate_draft_1(batch))

输出:

tensor([[    0,     1,     2,     3,     4],[    5,     6, 50256, 50256, 50256],[    7,     8,     9, 50256, 50256]])

我们刚刚实现了第一个自定义的 collate 函数,用于将输入列表创建为批次。然而,正如我们之前所学的,我们还需要为每个输入 ID 批次创建对应的目标 token ID 批次。这些目标 ID(如图 7.9 所示)至关重要,因为它们表示我们希望模型生成的内容,并且在训练过程中用于计算权重更新所需的损失。因此,我们需要修改自定义的 collate 函数,使其不仅返回输入 token ID,还返回目标 token ID。

在这里插入图片描述


改进版 collate 函数(包含目标 token IDs)

在这里插入图片描述

与我们用于预训练大语言模型(LLM)的过程类似,目标 token ID 与输入 token ID 相同,但向右偏移一个位置。这种设置(如图 7.10 所示)使 LLM 能够学习如何预测序列中的下一个 token。以下更新后的 collate 函数通过输入 token ID 生成目标 token ID:

def custom_collate_draft_2(batch, pad_token_id=50256, device="cpu"):batch_max_length = max(len(item) + 1 for item in batch)  # 找到批次中最长序列长度inputs_lst, targets_lst = [], []for item in batch:new_item = item.copy()new_item += [pad_token_id]  # 添加填充值padded = new_item + [pad_token_id] * (batch_max_length - len(new_item))  # 填充到最长长度inputs = torch.tensor(padded[:-1])  # 输入序列去掉最后一个 tokentargets = torch.tensor(padded[1:])  # 目标序列右移一位inputs_lst.append(inputs)targets_lst.append(targets)inputs_tensor = torch.stack(inputs_lst).to(device)targets_tensor = torch.stack(targets_lst).to(device)return inputs_tensor, targets_tensor

测试代码:

inputs, targets = custom_collate_draft_2(batch)
print(inputs)
print(targets)

输出:

Inputs:
tensor([[    0,     1,     2,     3,     4],[    5,     6, 50256, 50256, 50256],[    7,     8,     9, 50256, 50256]])Targets:
tensor([[    1,     2,     3,     4, 50256],[    6, 50256, 50256, 50256, 50256],[    8,     9, 50256, 50256, 50256]])

在下一步中,我们将为所有填充的 token 分配一个占位值 -100,如图 7.11 所示。这一特殊值使我们能够在训练损失计算中排除这些填充 token,从而确保只有有意义的数据影响模型的学习。我们将在实现此修改后进一步详细讨论这一过程。(在分类微调中,我们不需要担心这一点,因为我们仅基于最后一个输出 token 训练模型。)
在这里插入图片描述

然而,请注意,我们会在目标列表中保留一个结束文本的 token,ID 为 50256,如图 7.12 所示。保留这一 token 使 LLM 能够学习在响应指令时何时生成结束文本 token,这是判断生成响应完成的标志。
在这里插入图片描述

添加 -100 掩码的改进

在下面的代码中,我们修改了自定义的 collate 函数,将目标列表中 ID 为 50256 的 token 替换为 -100。此外,我们引入了一个 allowed_max_length 参数,用于可选地限制样本的最大长度。这一调整对于计划使用超过 GPT-2 模型支持的 1,024-token 上下文长度的数据集时非常有用。

def custom_collate_fn(batch, pad_token_id=50256, ignore_index=-100, device="cpu"):batch_max_length = max(len(item) + 1 for item in batch)inputs_lst, targets_lst = [], []for item in batch:new_item = item.copy()new_item += [pad_token_id]padded = new_item + [pad_token_id] * (batch_max_length - len(new_item))inputs = torch.tensor(padded[:-1])targets = torch.tensor(padded[1:])targets[targets == pad_token_id] = ignore_index  # 将填充值替换为 -100inputs_lst.append(inputs)targets_lst.append(targets)inputs_tensor = torch.stack(inputs_lst).to(device)targets_tensor = torch.stack(targets_lst).to(device)return inputs_tensor, targets_tensor

测试输出:

Inputs:
tensor([[    0,     1,     2,     3,     4],[    5,     6, 50256, 50256, 50256],[    7,     8,     9, 50256, 50256]])Targets:
tensor([[    1,     2,     3,     4, 50256],[    6, -100, -100, -100, -100],[    8,     9, -100, -100, -100]])

修改后的 collate 函数按预期运行,通过在目标列表中插入 token ID -100 来进行调整。那么,这一调整背后的逻辑是什么呢?让我们来探讨这一修改的核心目的。


使用 -100 的损失计算原理

为了演示这一点,我们来看一个简单且独立的例子,其中每个输出 logit 对应于模型词汇表中的一个潜在 token。以下是模型在预测 token 序列时如何计算交叉熵损失的过程(在第 5 章中已介绍),这与我们在模型预训练和分类微调时所做的工作类似:

logits_1 = torch.tensor(
[[-1.0, 1.0],
[-0.5, 1.5]]
)
targets_1 = torch.tensor([0, 1]) # 正确的目标 token 索引
loss_1 = torch.nn.functional.cross_entropy(logits_1, targets_1)
print(loss_1)# 第一个 token 的预测
# 第二个 token 的预测

上述代码计算的损失值为 1.1269

tensor(1.1269)

如果添加一个新的 token 索引,损失值的计算也会相应受到影响:

logits_2 = torch.tensor(
[[-1.0, 1.0],
[-0.5, 1.5],
[-0.5, 1.5]]
)
targets_2 = torch.tensor([0, 1, 1])
loss_2 = torch.nn.functional.cross_entropy(logits_2, targets_2)
print(loss_2)

添加第三个 token 后,损失值为 0.7936

到目前为止,我们通过 PyTorch 的交叉熵损失函数进行了相对直观的示例计算。这与我们在模型预训练和分类微调时使用的损失函数相同。接下来,来看一个更有趣的情况:如果将第三个目标 token ID 替换为 -100 会发生什么:

targets_3 = torch.tensor([0, 1, -100])
loss_3 = torch.nn.functional.cross_entropy(logits_2, targets_3)
print(loss_3)
print("loss_1 == loss_3:", loss_1 == loss_3)

结果输出如下:

tensor(1.1269)
loss_1 == loss_3: tensor(True)

对于这三个训练样本,损失值与之前仅使用两个训练样本计算的损失值相同。换句话说,交叉熵损失函数忽略了 targets_3 向量中的第三项(对应于 -100 的 token ID)。(有兴趣的读者可以尝试将 -100 替换为其他不是 01 的 token ID,这将导致错误。)

那么,-100 有什么特殊之处使其被交叉熵损失忽略?原因在于 PyTorch 的交叉熵函数默认设置为 cross_entropy(..., ignore_index=-100)。这意味着所有标记为 -100 的目标 token 将被忽略。我们利用这个 ignore_index 属性来忽略那些用于填充的额外结束文本 token(padding tokens),从而使每个批次中的训练样本具有相同的长度。

然而,我们保留了目标中的一个 50256(结束文本)token ID,因为它有助于 LLM 学习在响应完成时生成结束文本 token,这可以用作生成响应完成的指示器。

除了屏蔽填充 token,还可以屏蔽与指令相关的目标 token ID(如图 7.13 所示)。通过屏蔽 LLM 的目标 token ID 中与指令相关的部分,交叉熵损失只针对生成的响应目标 ID 计算。这样,模型会更专注于生成准确的响应,而不是记忆指令,从而有助于减少过拟合。
在这里插入图片描述

截至本文撰写时,研究人员对于在指令微调过程中屏蔽指令是否普遍有益仍存在分歧。例如,2024 年 Shi 等人发表的论文 “Instruction Tuning With Loss Over Instructions” (https://arxiv.org/abs/2405.14394) 表明,不屏蔽指令会提升 LLM 的性能(更多细节见附录 B)。在这里,我们不对指令应用屏蔽,并将其作为一个可选练习供感兴趣的读者尝试。

7.4 Creating data loaders for an instruction dataset

我们已经完成了多个阶段的工作,成功实现了 InstructionDataset 类和 custom_collate_fn 函数,如图 7.14 所示。
在这里插入图片描述

现在,我们可以将 InstructionDataset 对象和 custom_collate_fn 函数直接集成到 PyTorch 的数据加载器中。通过这些加载器,训练数据将会被自动打乱并组织成批次,用于 LLM 的指令微调过程。

在实现数据加载器的创建步骤之前,我们需要简要讨论一下 custom_collate_fn 中的设备设置问题。custom_collate_fn 函数包含将输入和目标张量移动到指定设备(例如 torch.stack(inputs_lst).to(device))的代码。这个设备可以是 "cpu",也可以是 "cuda"(用于 NVIDIA GPU),或者(可选)是 "mps"(适用于配备 Apple Silicon 芯片的 Mac)。

注意: 如果使用 "mps" 设备,由于 PyTorch 对 Apple Silicon 的支持仍处于实验阶段,可能会导致与本章内容存在数值差异。

在此之前,我们通常在主训练循环中将数据移至目标设备(例如,设置 device="cuda" 时,数据被移动到 GPU 内存中)。而将这个过程作为 collate 函数的一部分,可以将数据传输作为后台任务进行,从而避免在模型训练时阻塞 GPU。

以下代码初始化了设备变量:

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 如果使用 Apple Silicon GPU,可以取消注释以下两行代码:
# if torch.backends.mps.is_available():
#     device = torch.device("mps")print("Device:", device)

运行后,输出结果将是 "Device: cpu""Device: cuda",具体取决于您的计算机配置。

为了在 custom_collate_fn 函数中复用上述设备设置,我们使用 Python 标准库 functools 中的 partial 函数,预填 custom_collate_fndevice 参数。此外,我们将 allowed_max_length 设置为 1024,以限制数据的最大长度至 GPT-2 模型支持的上下文长度(我们稍后会对其进行微调)。

from functools import partialcustomized_collate_fn = partial(custom_collate_fn,device=device,allowed_max_length=1024
)

接下来,我们可以像之前一样设置数据加载器,但这次会使用自定义的 collate 函数来进行批次处理:

from torch.utils.data import DataLoadernum_workers = 0  # 可根据操作系统支持调整,您可以尝试增加 `num_workers` 的值(如果您的操作系统支持并行 Python 进程)
batch_size = 8
torch.manual_seed(123)  # 设置随机种子以确保结果可复现# 初始化训练数据集和加载器
train_dataset = InstructionDataset(train_data, tokenizer)
train_loader = DataLoader(train_dataset,batch_size=batch_size,collate_fn=customized_collate_fn,shuffle=True,drop_last=True,  # 丢弃不足一个批次的数据num_workers=num_workers
)# 初始化验证数据集和加载器
val_dataset = InstructionDataset(val_data, tokenizer)
val_loader = DataLoader(val_dataset,batch_size=batch_size,collate_fn=customized_collate_fn,shuffle=False,drop_last=False,  # 保留所有批次的数据num_workers=num_workers
)# 初始化测试数据集和加载器
test_dataset = InstructionDataset(test_data, tokenizer)
test_loader = DataLoader(test_dataset,batch_size=batch_size,collate_fn=customized_collate_fn,shuffle=False,drop_last=False,num_workers=num_workers
)

检查训练加载器生成的输入和目标批次的维度:

print("Train loader:")
for inputs, targets in train_loader:print(inputs.shape, targets.shape)

输出结果如下(为节省空间部分省略):

Train loader:
torch.Size([8, 61]) torch.Size([8, 61])
torch.Size([8, 76]) torch.Size([8, 76])
torch.Size([8, 73]) torch.Size([8, 73])
...
torch.Size([8, 74]) torch.Size([8, 74])
torch.Size([8, 69]) torch.Size([8, 69])

输出显示第一个输入和目标批次的维度是 8 × 61,其中 8 表示批次大小,61 表示该批次中每个训练样本的 token 数。第二个批次的 token 数不同,例如 76。得益于自定义的 collate 函数,数据加载器能够创建具有不同序列长度的批次。在下一部分中,我们将加载一个预训练的大语言模型(LLM),并使用这个数据加载器对其进行微调。

7.5 Loading a pretrained LLM

我们花费了大量时间为指令微调准备数据集,这是监督微调过程中的一个关键环节。除了数据准备,微调的其他许多方面与预训练类似,因此我们可以复用前几章中的大部分代码。

在开始指令微调之前,我们需要先加载一个预训练的 GPT 模型作为基础(如图 7.15 所示)。
在这里插入图片描述

与之前使用的 1.24 亿参数的最小模型不同,这次我们加载了一个 3.55 亿参数的中型模型。选择这一模型的原因是,1.24 亿参数的模型能力有限,难以通过指令微调获得令人满意的结果。具体来说,较小的模型缺乏足够的容量来学习和保留完成高质量指令跟随任务所需的复杂模式和细微行为。

加载预训练模型的过程与我们在第 5.5 节中预训练数据和第 6.4 节中分类微调时的代码相同,只是这次我们指定的是 “gpt2-medium (355M)” 而不是 “gpt2-small (124M)”。

注意:运行这段代码时,将下载中型 GPT 模型,约需 1.42 GB 的存储空间,是小型模型存储需求的约三倍。

加载预训练模型

from gpt_download import download_and_load_gpt2
from chapter04 import GPTModel
from chapter05 import load_weights_into_gptBASE_CONFIG = {"vocab_size": 50257, # 词汇表大小"context_length": 1024, # 上下文长度"drop_rate": 0.0, # Dropout 率"qkv_bias": True # Query-key-value 的偏置
}model_configs = {"gpt2-small (124M)": {"emb_dim": 768, "n_layers": 12, "n_heads": 12},"gpt2-medium (355M)": {"emb_dim": 1024, "n_layers": 24, "n_heads": 16},"gpt2-large (774M)": {"emb_dim": 1280, "n_layers": 36, "n_heads": 20},"gpt2-xl (1558M)": {"emb_dim": 1600, "n_layers": 48, "n_heads": 25},
}CHOOSE_MODEL = "gpt2-medium (355M)"
BASE_CONFIG.update(model_configs[CHOOSE_MODEL])
model_size = CHOOSE_MODEL.split(" ")[-1].lstrip("(").rstrip(")")# 下载并加载模型
settings, params = download_and_load_gpt2(model_size=model_size,models_dir="gpt2"
)model = GPTModel(BASE_CONFIG)
load_weights_into_gpt(model, params)
model.eval();

运行代码后,将下载以下文件:

checkpoint: 100%|██████████| 77.0/77.0 [00:00<00:00, 156kiB/s]
encoder.json: 100%|██████████| 1.04M/1.04M [00:02<00:00, 467kiB/s]
hparams.json: 100%|██████████| 91.0/91.0 [00:00<00:00, 198kiB/s]
model.ckpt.data-00000-of-00001: 100%|██████████| 1.42G/1.42G [05:50<00:00, 4.05MiB/s]
model.ckpt.index: 100%|██████████| 10.4k/10.4k [00:00<00:00, 18.1MiB/s]
model.ckpt.meta: 100%|██████████| 927k/927k [00:02<00:00, 454kiB/s]
vocab.bpe: 100%|██████████| 456k/456k [00:01<00:00, 283kiB/s]

基线评估

在微调之前,我们可以通过验证任务中的一个示例来评估预训练 LLM 的性能,并将其输出与预期响应进行比较。这将帮助我们了解模型在未微调时对指令任务的表现,为之后的微调效果提供基线。

使用验证集中的第一个示例进行评估:

torch.manual_seed(123)
input_text = format_input(val_data[0])
print(input_text)

指令的内容如下:

Below is an instruction that describes a task. Write a response that
appropriately completes the request.### Instruction:
Convert the active sentence to passive: 'The chef cooks the meal every day.'

使用与第 5 章中相同的 generate 函数生成模型的响应:

from chapter05 import generate, text_to_token_ids, token_ids_to_texttoken_ids = generate(model=model,idx=text_to_token_ids(input_text, tokenizer),max_new_tokens=35,context_size=BASE_CONFIG["context_length"],eos_id=50256,
)generated_text = token_ids_to_text(token_ids, tokenizer)

由于 generate 函数将输入和输出文本连接起来,需移除输入部分以获得模型生成的纯响应:

response_text = generated_text[len(input_text):].strip()
print(response_text)
输出:
### Response:
The chef cooks the meal every day.
### Instruction:
Convert the active sentence to passive: 'The chef cooks the

结果分析

上述输出表明,预训练模型尚未能正确执行给定指令。尽管它创建了一个 “Response” 部分,但只是简单地重复了输入句子和部分指令,而未能将主动句转换为被动语态。为了提高模型理解和响应此类请求的能力,接下来我们将实现指令微调过程。

7.6 Fine-tuning the LLM on instruction data

现在是时候对 LLM 进行指令微调了(如图 7.16 所示)。
在这里插入图片描述

我们将使用上一节加载的预训练模型,并用本章前面准备的指令数据集对其进一步训练。我们在本章开始时已经完成了数据集处理的复杂工作。对于微调过程本身,我们可以重用第 5 章中实现的损失计算和训练函数:

from chapter05 import (calc_loss_loader,train_model_simple
)
计算初始损失

在开始训练之前,让我们先计算训练集和验证集的初始损失:

model.to(device)
torch.manual_seed(123)
with torch.no_grad():train_loss = calc_loss_loader(train_loader, model, device, num_batches=5)val_loss = calc_loss_loader(val_loader, model, device, num_batches=5)
print("Training loss:", train_loss)
print("Validation loss:", val_loss)

初始损失值如下:

Training loss: 3.825908660888672
Validation loss: 3.7619335651397705

我们的目标是最小化损失。


硬件限制处理

使用和训练更大的模型(如 GPT-2 中型模型,3.55 亿参数)比较小的 GPT-2 模型(1.24 亿参数)需要更多的计算资源。如果因硬件限制而遇到问题,可以切换到较小的模型,只需将 CHOOSE_MODEL = "gpt2-medium (355M)" 修改为 CHOOSE_MODEL = "gpt2-small (124M)"(详见第 7.5 节)。此外,为加速模型训练,可以考虑使用 GPU。可参考代码库中的补充章节获取云 GPU 使用选项:https://mng.bz/EOEq。

以下是不同设备上训练各模型两轮(epoch)的参考运行时间:

模型名称设备两轮训练时间
gpt2-medium (355M)CPU (M3 MacBook Air)15.78 分钟
gpt2-medium (355M)GPU (NVIDIA L4)1.83 分钟
gpt2-medium (355M)GPU (NVIDIA A100)0.86 分钟
gpt2-small (124M)CPU (M3 MacBook Air)5.74 分钟
gpt2-small (124M)GPU (NVIDIA L4)0.69 分钟
gpt2-small (124M)GPU (NVIDIA A100)0.39 分钟

设置训练过程

以下代码展示了如何设置训练,包括初始化优化器、设置训练轮数、定义评估频率,以及在训练期间使用验证集的第一个指令(val_data[0])来评估生成的 LLM 响应。

import timestart_time = time.time()
torch.manual_seed(123)# 初始化优化器
optimizer = torch.optim.AdamW(model.parameters(), lr=0.00005, weight_decay=0.1
)# 设置训练轮数
num_epochs = 2# 开始训练
train_losses, val_losses, tokens_seen = train_model_simple(model, train_loader, val_loader, optimizer, device,num_epochs=num_epochs, eval_freq=5, eval_iter=5,start_context=format_input(val_data[0]), tokenizer=tokenizer
)end_time = time.time()
execution_time_minutes = (end_time - start_time) / 60
print(f"Training completed in {execution_time_minutes:.2f} minutes.")
训练输出

以下是两轮训练的输出结果,显示了训练和验证损失值的稳步下降,这表明模型在指令跟随和生成合适响应方面的能力逐步提高:

Ep 1 (Step 000000): Train loss 2.637, Val loss 2.626  
Ep 1 (Step 000005): Train loss 1.174, Val loss 1.103  
Ep 1 (Step 000010): Train loss 0.872, Val loss 0.944  
Ep 1 (Step 000015): Train loss 0.857, Val loss 0.906  
...  
Ep 1 (Step 000115): Train loss 0.520, Val loss 0.665  
Below is an instruction that describes a task. Write a response that appropriately completes the request.  
### Instruction: Convert the active sentence to passive: 'The chef cooks the meal every day.'  
### Response: The meal is prepared every day by the chef.<|endoftext|>  
The following is an instruction that describes a task. Write a response that appropriately completes the request.  
### Instruction: Convert the active sentence to passive:  Ep 2 (Step 000120): Train loss 0.438, Val loss 0.670  
Ep 2 (Step 000125): Train loss 0.453, Val loss 0.685  
Ep 2 (Step 000130): Train loss 0.448, Val loss 0.681  
Ep 2 (Step 000135): Train loss 0.408, Val loss 0.677  
...  
Ep 2 (Step 000230): Train loss 0.300, Val loss 0.657  
Below is an instruction that describes a task. Write a response that appropriately completes the request.  
### Instruction: Convert the active sentence to passive: 'The chef cooks the meal every day.'  
### Response: The meal is cooked every day by the chef.<|endoftext|>  

结果分析

训练输出表明,模型学习效果显著,两轮训练中,训练和验证损失值持续下降,显示了模型在理解指令和生成合适响应方面的进步。这些结果表明:

  1. 模型学习效率高: 从第一轮开始,训练和验证损失快速下降,表明模型能够快速捕捉数据中的有意义模式和表示。
  2. 逐步收敛: 第二轮训练中,损失下降速度放缓,表明模型正微调其学到的表示,并逐步收敛到稳定的解决方案。

在每轮训练结束时,通过验证集示例生成的响应可以检查模型的进展。例如,在第二轮结束时,模型成功地将主动句 “The chef cooks the meal every day.” 转换为被动句 “The meal is cooked every day by the chef.”

此外,由于模型在两轮训练中表现出了有效的学习能力,延长训练到第三轮或更多轮可能并不必要,反而可能会导致过拟合。


损失曲线可视化

我们可以绘制训练和验证损失曲线,以更直观地分析模型的学习过程:

from chapter05 import plot_lossesepochs_tensor = torch.linspace(0, num_epochs, len(train_losses))
plot_losses(epochs_tensor, tokens_seen, train_losses, val_losses)

在这里插入图片描述

从损失曲线中可以观察到:

  1. 初始阶段损失迅速下降,表明模型快速学习数据中的模式。
  2. 第二轮训练时损失下降速度放缓,表明模型正在逐步调整其学到的表示,并收敛到稳定解。

下一步:提取和保存生成的响应

尽管损失曲线表明模型训练效果良好,最关键的指标是生成响应的质量和准确性。接下来,我们将提取模型生成的响应并将其存储为可用于评估和量化响应质量的格式。

7.7 Extracting and saving responses

在完成指令数据集的训练部分微调后,现在可以评估模型在保留出的测试集上的表现。我们将提取模型生成的响应,与测试集中的期望答案进行对比以进行手动分析,然后量化评估生成响应的质量(如图 7.18 所示)。

在这里插入图片描述

提取和对比响应

以下代码提取测试集前 3 条数据的模型生成响应,并将其与期望答案并排显示以便对比:

torch.manual_seed(123)# 遍历测试集的前三个示例
for entry in test_data[:3]:input_text = format_input(entry)token_ids = generate(model=model,idx=text_to_token_ids(input_text, tokenizer).to(device),max_new_tokens=256,context_size=BASE_CONFIG["context_length"],eos_id=50256)generated_text = token_ids_to_text(token_ids, tokenizer)response_text = (generated_text[len(input_text):].replace("### Response:", "").strip())print(input_text)print(f"\nCorrect response:\n>> {entry['output']}")print(f"\nModel response:\n>> {response_text.strip()}")print("-------------------------------------")

示例输出

以下是模型生成响应与期望答案的对比结果:

  1. 示例 1

    Below is an instruction that describes a task. Write a response that appropriately completes the request.
    ### Instruction:
    Rewrite the sentence using a simile.
    ### Input:
    The car is very fast.Correct response:
    >> The car is as fast as lightning.Model response:
    >> The car is as fast as a bullet.
    
  2. 示例 2

    Below is an instruction that describes a task. Write a response that appropriately completes the request.
    ### Instruction:
    What type of cloud is typically associated with thunderstorms?Correct response:
    >> The type of cloud typically associated with thunderstorms is cumulonimbus.Model response:
    >> The type of cloud associated with thunderstorms is a cumulus cloud.
    
  3. 示例 3

    Below is an instruction that describes a task. Write a response that appropriately completes the request.
    ### Instruction:
    Name the author of ‘Pride and Prejudice.’Correct response:
    >> Jane Austen.Model response:
    >> The author of ‘Pride and Prejudice’ is Jane Austen.
    

结果分析

  • 正确生成: 示例 1 和示例 3 的响应与期望答案一致。
  • 部分正确: 示例 2 的回答接近但不完全正确,模型生成了 “cumulus cloud” 而非期望答案 “cumulonimbus”。尽管如此,值得注意的是,积云(cumulus)可能会发展为积雨云(cumulonimbus),后者能够引发雷暴。

这表明,模型的性能在大多数情况下令人满意,但仍有改进空间。


自动化评估

相比于通过简单的分类精度(如垃圾邮件/非垃圾邮件)量化微调效果,对指令微调的 LLM 进行评估更为复杂。实际中,可以通过以下几种方法评估指令微调后的 LLM:

  1. 短回答和多选题基准测试:
    Measuring Massive Multitask Language Understanding (MMLU),用于测试模型的通用知识。(参考文献)

  2. 与其他 LLM 的人类偏好对比:
    LMSYS chatbot arena。(参考链接)

  3. 自动对话基准:
    使用其他 LLM(如 GPT-4)对生成响应进行自动评估,例如 AlpacaEval。(参考链接)

考虑到我们更关注模型的对话性能而非多选题回答能力,人类评价自动化评估 更为相关。


自动化对话评估的实现

通过使用类似 AlpacaEval 的方法,我们可以利用另一个 LLM 对模型生成的响应进行评估。这种方法无需大量人力即可高效地评估响应质量。

以下代码将测试集的模型响应保存为一个 JSON 文件,以便后续分析:

from tqdm import tqdm# 遍历整个测试集,生成响应并保存
for i, entry in tqdm(enumerate(test_data), total=len(test_data)):input_text = format_input(entry)token_ids = generate(model=model,idx=text_to_token_ids(input_text, tokenizer).to(device),max_new_tokens=256,context_size=BASE_CONFIG["context_length"],eos_id=50256)generated_text = token_ids_to_text(token_ids, tokenizer)response_text = (generated_text[len(input_text):].replace("### Response:", "").strip())test_data[i]["model_response"] = response_text# 保存结果到 JSON 文件
with open("instruction-data-with-response.json", "w") as file:json.dump(test_data, file, indent=4)

运行上述代码后,验证结果可以通过检查文件内容确认:

print(test_data[0])

输出:

{'instruction': 'Rewrite the sentence using a simile.','input': 'The car is very fast.','output': 'The car is as fast as lightning.','model_response': 'The car is as fast as a bullet.'
}

保存微调后的模型

最后,将微调后的模型保存为 .pth 文件以供后续使用:

import refile_name = f"{re.sub(r'[ ()]', '', CHOOSE_MODEL)}-sft.pth"
torch.save(model.state_dict(), file_name)
print(f"Model saved as {file_name}")

模型可以通过以下代码重新加载:

model.load_state_dict(torch.load("gpt2-medium355M-sft.pth"))

7.8 Evaluating the fine-tuned LLM

之前,我们通过查看测试集前三个示例的响应来评估指令微调模型的性能。尽管这种方法可以让我们对模型的表现有一个粗略的了解,但它并不适用于大规模响应评估。因此,我们将通过另一个更大的 LLM 实现对微调模型响应的自动化评估(如图 7.19 所示)。
在这里插入图片描述
为了以自动化的方式评估测试集响应,我们使用了 Meta AI 开发的 80 亿参数的指令微调模型 Llama 3。此模型可以通过开源应用 Ollama 在本地运行(Ollama官网)。

注意:
Ollama 是一个高效的工具,用于在笔记本电脑上运行大型语言模型(LLMs)。它是开源库 llama.cpp(项目链接)的封装,使用纯 C/C++ 实现,以最大化运行效率。但需要注意的是,Ollama 仅支持 LLM 的推理(生成文本),并不支持训练或微调 LLM。


通过 Web API 使用更大的 LLM

Llama 3 模型(80 亿参数)是一个非常强大的本地运行模型。然而,与 OpenAI 提供的专有 LLM(如 GPT-4)相比,它的能力相对有限。如果您对使用 GPT-4 通过 OpenAI API 评估生成的模型响应感兴趣,可以参考本书补充材料中的代码笔记本:代码链接。


安装 Ollama 并验证其运行状态

安装 Ollama
  1. 访问 Ollama 官网:
    https://ollama.com

  2. 按照您的操作系统说明进行安装:

    • macOS 和 Windows 用户: 打开下载的 Ollama 应用程序。如果系统提示是否安装命令行支持,选择“是”。
    • Linux 用户: 在 Ollama 网站上找到对应的安装命令并运行。
运行 Ollama
  1. 启动 Ollama:
    您可以启动 Ollama 应用程序,或者在终端中运行以下命令启动服务:

    ollama serve
    
  2. 下载 Llama 3 模型并验证:
    在另一个终端窗口中运行以下命令(非 Python 会话)以尝试 80 亿参数的 Llama 3 模型:

    ollama run llama3
    

    首次运行时,该命令会自动下载模型,占用约 4.7 GB 的存储空间。

    下载日志示例:

    pulling manifest
    pulling 6a0746a1ec1a... 100% |████████████████| 4.7 GB
    ...
    success
    
  3. 验证运行状态:
    完成下载后,您可以通过命令行与模型交互。例如:

    >>> What do llamas eat?
    Llamas are ruminant animals, which means they have a four-chambered
    stomach and eat plants that are high in fiber. In the wild,
    llamas typically feed on:
    1. Grasses: They love to graze on various types of grasses, including tall
    grasses, wheat, oats, and barley.
    
  4. 结束会话:
    使用 /bye 命令退出交互会话。但在后续步骤中,请确保 ollama serve 或 Ollama 应用程序始终保持运行状态。


在这里插入图片描述

在不同的终端中运行 Ollama 应用程序或 ollama serve,然后在命令行(而非 Python 会话中)执行以下命令,以试用 80 亿参数的 Llama 3 模型:

ollama run llama3

第一次执行此命令时,该模型(占用 4.7 GB 的存储空间)将自动下载。输出如下所示:

pulling manifest
pulling 6a0746a1ec1a... 100% |████████████████| 4.7 GB
pulling 4fa551d4f938... 100% |████████████████| 12 KB
pulling 8ab4849b038c... 100% |████████████████| 254 B
pulling 577073ffcc6c... 100% |████████████████| 110 B
pulling 3f8eb4da87fa... 100% |████████████████| 485 B
verifying sha256 digest
writing manifest
removing any unused layers
success
替代的 Ollama 模型

ollama run llama3 命令中,llama3 指的是经过指令微调的 80 亿参数 Llama 3 模型。使用 Ollama 运行 llama3 模型大约需要 16 GB 的内存。如果您的机器内存不足,可以尝试使用较小的模型,例如 38 亿参数的 phi3 模型,通过 ollama run phi3 命令运行,该模型仅需约 8 GB 的内存。

对于更强大的计算机,您还可以使用更大的 700 亿参数 Llama 3 模型,只需将 llama3 替换为 llama3:70b。然而,该模型需要显著更多的计算资源。

模型下载完成后,我们将看到一个命令行界面,可以与模型进行交互。例如,尝试问模型:“羊驼吃什么?”

>>> What do llamas eat?
Llamas are ruminant animals, which means they have a four-chambered
stomach and eat plants that are high in fiber. In the wild,
llamas typically feed on:
1. Grasses: They love to graze on various types of grasses, including tall
grasses, wheat, oats, and barley.

请注意,您看到的响应可能有所不同,因为截至目前 Ollama 不是确定性的。

您可以使用输入 /bye 结束此 ollama run llama3 会话。然而,请确保在本章剩余部分中保持 ollama serve 命令或 Ollama 应用程序处于运行状态。

以下代码在我们使用 Ollama 评估测试集响应之前,验证 Ollama 会话是否正常运行:

import psutildef check_if_running(process_name):running = Falsefor proc in psutil.process_iter(["name"]):if process_name in proc.info["name"]:running = Truebreakreturn runningollama_running = check_if_running("ollama")
if not ollama_running:raise RuntimeError("Ollama not running. Launch ollama before proceeding.")
print("Ollama running:", check_if_running("ollama"))

确保执行上述代码的输出显示 Ollama 正在运行:

True

如果显示为 False,请确认 ollama serve 命令或 Ollama 应用程序正在积极运行。

在新的 Python 会话中运行代码

如果您已关闭 Python 会话,或希望在不同的 Python 会话中执行剩余代码,可以使用以下代码,它加载我们之前创建的指令和响应数据文件,并重新定义我们之前使用的 format_input 函数(后续将使用 tqdm 进度条工具):

import json
from tqdm import tqdmfile_path = "instruction-data-with-response.json"
with open(file_path, "r") as file:test_data = json.load(file)def format_input(entry):instruction_text = (f"Below is an instruction that describes a task. "f"Write a response that appropriately completes the request."f"\n\n### Instruction:\n{entry['instruction']}")input_text = (f"\n\n### Input:\n{entry['input']}" if entry["input"] else "")return instruction_text + input_text

通过 Python 使用其 REST API 与模型交互是 ollama run 命令的另一种方式。以下列表中的 query_model 函数演示了如何使用 API。

查询本地 Ollama 模型
import urllib.requestdef query_model(prompt,model="llama3",url="http://localhost:11434/api/chat"
):data = {"model": model,# 创建数据 payload 作为字典"messages": [{"role": "user", "content": prompt}],"options": {"seed": 123,  # 设置确定性响应"temperature": 0,"num_ctx": 2048}}# 将字典转换为 JSON 格式字符串并编码为字节payload = json.dumps(data).encode("utf-8")request = urllib.request.Request(url,data=payload,method="POST")# 创建请求对象,设置方法为 POST 并添加必要的头部request.add_header("Content-Type", "application/json")response_data = ""with urllib.request.urlopen(request) as response:while True:line = response.readline().decode("utf-8")if not line:breakresponse_json = json.loads(line)response_data += response_json["message"]["content"]return response_data

发送请求并捕获响应:

在运行此笔记本中的后续代码单元之前,确保 Ollama 仍在运行。前面的代码单元应打印 “Ollama running: True” 以确认模型处于活动状态并准备接收请求。

以下是如何使用我们刚刚实现的 query_model 函数的示例:

model = "llama3"
result = query_model("What do Llamas eat?", model)
print(result)

生成的响应如下:

Llamas are ruminant animals, which means they have a four-chambered
stomach that allows them to digest plant-based foods. Their diet
typically consists of:
1. Grasses: Llamas love to graze on grasses, including tall grasses,
short grasses, and even weeds.
...

使用前面定义的 query_model 函数,我们可以评估微调模型生成的响应,要求 Llama 3 模型根据给定的测试集响应参考,对我们的微调模型的响应进行 0 到 100 的评分。

首先,我们将这种方法应用于我们之前检查的测试集中的前三个示例:

for entry in test_data[:3]:prompt = (f"Given the input `{format_input(entry)}` "f"and correct output `{entry['output']}`, "f"score the model response `{entry['model_response']}`"f" on a scale from 0 to 100, where 100 is the best score. ")print("\nDataset response:")print(">>", entry['output'])print("\nModel response:")print(">>", entry["model_response"])print("\nScore:")print(">>", query_model(prompt))print("\n-------------------------")

此代码打印的输出类似于以下内容(截至目前,Ollama 不是完全确定性的,因此生成的文本可能有所不同):

Dataset response:
>> The car is as fast as lightning.
Model response:
>> The car is as fast as a bullet.
Score:
>> I’d rate the model response “The car is as fast as a bullet.” an 85 out of 100. Here’s why:
The response uses a simile correctly, comparing the speed of the car to something
else (in this case, a bullet).
The comparison is relevant and makes sense, as bullets are known for their high
velocity.
The phrase “as fast as” is used correctly to introduce the simile.
The only reason I wouldn’t give it a perfect score is that some people might find the
comparison slightly less vivid or evocative than others. For example, comparing
something to lightning (as in the original response) can be more dramatic and atten-
tion grabbing. However, “as fast as a bullet” is still a strong and effective simile that
effectively conveys the idea of the car’s speed.
Overall, I think the model did a great job!Dataset response:
>> The type of cloud typically associated with thunderstorms is cumulonimbus.
Model response:
>> The type of cloud associated with thunderstorms is a cumulus cloud.
Score:
>> I’d score this model response as 40 out of 100. Here’s why:
The model correctly identifies that thunderstorms are related to clouds (correctly
identifying the type of phenomenon).
However, it incorrectly specifies the type of cloud associated with thunderstorms.
Cumulus clouds are not typically associated with thunderstorms; cumulonimbus
clouds are.
The response lacks precision and accuracy in its description.
Overall, while the model attempts to address the instruction, it provides an incorrect
answer, which is a significant error.Dataset response:
>> Jane Austen.
Model response:
>> The author of ‘Pride and Prejudice’ is Jane Austen.
Score:
>> I’d rate my own response as 95 out of 100. Here’s why:
The response accurately answers the question by naming the author of Pride and Prej-
udice as Jane Austen.
The response is concise and clear, making it easy to understand.
There are no grammatical errors or ambiguities that could lead to confusion.
The only reason I wouldn’t give myself a perfect score is that the response is slightly
redundant—it’s not necessary to rephrase the question in the answer. A more con-
cise response would be simply “Jane Austen.”

生成的响应显示,Llama 3 模型提供了合理的评估,并能够在模型的答案不完全正确时给予部分分数。例如,考虑“积雨云”答案的评估时,模型承认了响应的部分正确性。

前一个提示返回了高度详细的评估以及分数。我们可以修改提示,仅生成 0 到 100 的整数分数,其中 100 代表最佳分数。此修改允许我们计算模型的平均分数,从而作为其性能的更简洁和定量的评估。以下列表中的 generate_model_scores 函数使用修改后的提示,要求模型“仅回复整数数字”。

评估指令微调的 LLM
def generate_model_scores(json_data, json_key, model="llama3"):scores = []for entry in tqdm(json_data, desc="Scoring entries"):prompt = (f"Given the input `{format_input(entry)}` "f"and correct output `{entry['output']}`, "f"score the model response `{entry[json_key]}`"f" on a scale from 0 to 100, where 100 is the best score. "f"Respond with the integer number only.")try:score = query_model(prompt, model)# 修改指令行,仅返回分数scores.append(int(score))except ValueError:print(f"Could not convert score: {score}")continuereturn scores

现在让我们将 generate_model_scores 函数应用于整个 test_data 集,这在 M3 Macbook Air 上大约需要 1 分钟:

scores = generate_model_scores(test_data, "model_response")
print(f"Number of scores: {len(scores)} of {len(test_data)}")
print(f"Average score: {sum(scores)/len(scores):.2f}\n")

结果如下:

Scoring entries: 100%|████████████████████████| 110/110 [01:10<00:00, 1.56it/s]
Number of scores: 110 of 110
Average score: 50.32

评估输出显示,我们的微调模型在测试集上的平均分数超过 50,这为与其他模型的比较或尝试不同的训练配置以提高模型性能提供了有用的基准。

值得注意的是,截至目前,Ollama 在不同操作系统上的确定性并不完全一致,这意味着您获得的分数可能会略有不同。为了获得更稳健的结果,您可以多次重复评估并平均结果分数。

为了进一步提高我们模型的性能,我们可以探索各种策略,例如:

  • 调整微调期间的超参数,如学习率、批量大小或训练轮数。
  • 增加训练数据集的大小或多样化示例,以涵盖更广泛的主题和风格。
  • 尝试不同的提示或指令格式,以更有效地引导模型的响应。
  • 使用更大的预训练模型,这可能具有更强的捕捉复杂模式和生成更准确响应的能力。

注意:作为参考,使用本文所述的方法时,未经任何微调的 Llama 3 80B 基础模型在测试集上获得的平均分数为 58.51。经过通用指令微调数据集微调的 Llama 3 80B 指令模型,获得了令人印象深刻的 82.6 的平均分数。


7.9 Conclusions

本章标志着我们在大型语言模型(LLM)开发周期中的旅程的结束。我们涵盖了所有基本步骤,包括实现 LLM 架构、对 LLM 进行预训练以及针对特定任务进行微调,如图 7.21 所总结。让我们讨论下一步可以深入研究的一些想法。
在这里插入图片描述

7.9.1 接下来做什么?

虽然我们涵盖了最基本的步骤,但在指令微调之后还有一个可选步骤:偏好微调。偏好微调对于将模型定制得更符合特定用户偏好特别有用。如果您有兴趣进一步探索这一点,请参阅本书补充的 GitHub 仓库中的 04_preference-tuning-with-dpo 文件夹,网址为 https://mng.bz/dZwD。除了本书涵盖的主要内容外,GitHub 仓库还包含大量您可能觉得有价值的额外材料。要了解更多关于这些附加资源的信息,请访问仓库 README 页面上的“附加材料”部分:https://mng.bz/r12g。

在快速发展的领域中保持最新

人工智能和大型语言模型研究领域发展迅速(根据不同人的看法,这一速度令人兴奋)。跟上最新进展的一种方式是探索 arXiv 上的最新研究论文,网址为 https://arxiv.org/list/cs.LG/recent。此外,许多研究人员和从业者在社交媒体平台如 X(前身为 Twitter)和 Reddit 上非常活跃,分享和讨论最新的发展。特别是子论坛 r/LocalLLaMA 是一个与社区联系并了解最新工具和趋势的良好资源。我也定期在我的博客上分享见解并撰写有关 LLM 研究最新进展的文章,博客地址为 https://magazine.sebastianraschka.com 和 https://sebastianraschka.com/blog/。

7.9.3 最后的话

希望您喜欢从零开始实现一个 LLM 并从头编写预训练和微调功能的这段旅程。在我看来,从头构建一个 LLM 是深入理解 LLM 工作原理的最有效方式。希望这种实践方法为您提供了有价值的见解和坚实的 LLM 开发基础。

总结

虽然本书的主要目的是教育,但您可能有兴趣在实际应用中使用不同且更强大的 LLM。为此,我建议探索一些流行工具,如 Axolotl (https://github.com/OpenAccess-AI-Collective/axolotl) 或 LitGPT (https://github.com/Lightning-AI/litgpt),我积极参与这些工具的开发。

感谢您与我一同踏上这段学习旅程,祝愿您在充满活力的 LLM 和人工智能领域的未来事业中一切顺利!



http://www.ppmy.cn/embedded/148921.html

相关文章

SDL3 GPU编程探索

目录 前言 总体印象 一般流程 创建窗口 创建设备 准备资源 1.VBO&#xff0c;IBO 2.图像 3.资源转储 4.采样器 5.着色器 6.渲染管线 绘制 插曲 1.忘记初始化 2.关于数学库 3.SDL3_image 结语 前言 几天前冲浪才得知SDL有了一下代&#xff0c;而且还支持GPU编程…

Hive其五,使用技巧,数据查询,日志以及复杂类型的使用

目录 一、关于Hive使用的一些技巧 二、表的数据查询 三、Hive默认的日志 四、复杂数据类型 1、Array的使用 2、展开函数的使用 explode 3、Map的使用 4、Struct结构体 一、关于Hive使用的一些技巧 1、可以直接不进入hive的情况下执行sql语句 通过shell的参数 -e 可以执…

微服务分布式(一、项目初始化)

微服务是将大型的web项目拆分成成多个小型的springboot工程&#xff0c;依靠spring cloud相关分布式框架的工具进行整合&#xff0c;通常将一个大项目中不同的业务和技术分别拆分成不同的小模块独立运行。 下面我们先进行项目初始化&#xff0c;项目工具特别方便&#xff0c;我…

冰狐智能辅助使用插件化开发集成三方ocr

插件化开发&#xff08;Plugin Development&#xff09;是一种软件开发模式&#xff0c;它将一个应用程序的功能拆分为模块&#xff0c;并允许在运行时动态加载、卸载和扩展这些模块&#xff0c;以增强应用程序的功能。冰狐支持动态加载和卸载第三方插件模块&#xff0c;开发者…

离心式压缩机设计的自动化方法

离心式压缩机设计中的挑战 长期以来&#xff0c;人们一直使用不同形状和尺寸的压缩机&#xff0c;通过向流经转子的连续流体添加能量来产生压力上升。它们也在汽车工业的涡轮增压器中得到广泛使用。图 1 显示了典型的离心式&#xff08;径向&#xff09;压缩机叶轮。 图 1 叶轮…

Android简洁缩放Matrix实现图像马赛克,Kotlin

Android简洁缩放Matrix实现图像马赛克&#xff0c;Kotlin 原理&#xff0c;通过Matrix把一个原图缩小到原先的1/n&#xff0c;然后再把缩小后的小图放大n倍&#xff0c;自然就是马赛克效果&#xff08;相当于是放大后像素“糊”成一片了&#xff09;。 import android.content.…

kanzi3.6.10 窗口插件-查找绑定信息

文章目录 1. 创建kanzi窗口插件2. 业务逻辑3. 关键代码3.1 遍历本工程的绑定信息3.2 遍历所有工程的绑定信息3.3 DiguiSearchNode实现3.4 双击跳转 4. 安装 背景&#xff1a;在kanzi studio里按CtrlF&#xff0c;可以查找&#xff0c;能查找节点/资源等等&#xff0c;但是不能查…

光谱相机在农业中的具体应用案例

作物生长监测与产量预测 美国爱荷华州玉米种植园&#xff1a;农场主使用无人机搭载高光谱相机&#xff0c;定期对玉米田进行拍摄。通过分析光谱数据&#xff0c;获取玉米的叶面积指数、叶绿素含量等生长参数。在玉米生长关键期&#xff0c;依据这些参数及时调整施肥和灌溉方案…