当前位置: 首页 > news >正文

用 pytorch 从零开始创建大语言模型(六):对分类进行微调

用 pytorch 从零开始创建大语言模型(六):对分类进行微调

  • 6 微调用于分类
    • 6.1 微调的不同类别
    • 6.2 准备数据集
    • 6.3 创建数据加载器
    • 6.4 使用预训练权重初始化模型
    • 6.5 添加分类头部
    • 6.6 计算分类损失和准确率
    • 6.7 在监督数据上微调模型
    • 6.8 使用LLM进行垃圾信息分类
  • 总结

6 微调用于分类

本章内容包括:

  • 介绍不同的LLM微调方法
  • 为文本分类准备数据集
  • 修改一个预训练的LLM以进行微调
  • 微调LLM以识别垃圾信息
  • 评估微调后LLM分类器的准确率
  • 使用微调后的LLM对新数据进行分类

到目前为止,我们已经编写了LLM架构,对其进行了预训练,并学习了如何将来自外部来源(如OpenAI)的预训练权重导入我们的模型中。现在我们将开始收获劳动成果,通过在特定目标任务上对LLM进行微调,例如文本分类。本章中我们要研究的具体示例是将短信分类为“垃圾信息”或“非垃圾信息”。图6.1强调了对LLM进行微调的两种主要方式:用于分类的微调(步骤8)和用于指令跟随的微调(步骤9)。


在这里插入图片描述图6.1 编写LLM的三个主要阶段。本章重点是阶段3(步骤8):将预训练的LLM微调为分类器。


6.1 微调的不同类别

微调语言模型最常见的方式是指令微调和分类微调。指令微调是指通过使用特定指令对语言模型进行一组任务的训练,从而提升其理解和执行以自然语言提示描述的任务的能力,如图6.2所示。


在这里插入图片描述图6.2 展示了两种不同的指令微调场景。上方的场景中,模型的任务是判断给定文本是否为垃圾信息。下方的场景中,模型被给予将一个英语句子翻译成德语的指令。


在分类微调中,如果你有机器学习背景,可能已经熟悉这个概念,模型被训练用于识别一组特定的类别标签,例如“垃圾信息”和“非垃圾信息”。分类任务的例子不限于LLM和电子邮件过滤:它们还包括通过图像识别不同植物物种;将新闻文章分类到体育、政治和科技等主题中;以及在医学图像中区分良性和恶性肿瘤。

关键点在于,一个经过分类微调的模型只能预测它在训练期间遇到过的类别。例如,它可以判断某样东西是“垃圾信息”还是“非垃圾信息”,如图6.3所示,但它无法对输入文本给出其他判断。


在这里插入图片描述图6.3 使用LLM的文本分类场景。一个为垃圾信息分类而微调的模型在输入旁边不需要额外的指令。与一个经过指令微调的模型相比,它只能回答“垃圾信息”或“非垃圾信息”。


与图6.3中所示的分类微调模型相反,一个经过指令微调的模型通常能够执行更广泛的任务。我们可以将分类微调模型视为高度专业化的模型,而通常来说,开发一个专用模型要比开发一个能够胜任多种任务的通用模型更容易。

选择合适的方法
指令微调提高了模型根据特定用户指令理解并生成响应的能力。指令微调最适用于需要根据复杂用户指令处理多种任务的模型,从而提升模型的灵活性和交互质量。分类微调则非常适合需要将数据精确划分到预定义类别中的项目,例如情感分析或垃圾信息检测。


尽管指令微调更具通用性,但它需要更大的数据集和更高的计算资源来训练能够胜任各种任务的模型。相比之下,分类微调所需的数据和计算资源更少,但其用途仅限于模型所训练过的特定类别。

6.2 准备数据集

我们将对之前实现并预训练的GPT模型进行修改并进行分类微调。我们首先下载并准备数据集,如图6.4所示。为了提供一个直观且有实际意义的分类微调示例,我们将使用一个包含垃圾信息和非垃圾信息的短信数据集进行训练。


在这里插入图片描述图6.4 对LLM进行分类微调的三阶段流程。阶段1涉及数据集准备,阶段2专注于模型设置,阶段3涵盖模型的微调与评估。


注意: 短信通常是通过手机发送的,而非电子邮件。然而,相同的步骤同样适用于电子邮件分类。感兴趣的读者可在附录B中找到电子邮件垃圾分类数据集的链接。

第一步是下载数据集。

清单 6.1 下载并解压数据集

import urllib.request
import zipfile
import os
from pathlib import Pathurl = "https://archive.ics.uci.edu/static/public/228/sms+spam+collection.zip"
zip_path = "sms_spam_collection.zip"
extracted_path = "sms_spam_collection"
data_file_path = Path(extracted_path) / "SMSSpamCollection.tsv"def download_and_unzip_spam_data(url, zip_path, extracted_path, data_file_path):if data_file_path.exists():print(f"{data_file_path} already exists. Skipping download and extraction.")return# Downloading the filewith urllib.request.urlopen(url) as response:with open(zip_path, "wb") as out_file:out_file.write(response.read())# Unzipping the filewith zipfile.ZipFile(zip_path, "r") as zip_ref:zip_ref.extractall(extracted_path)# Add .tsv file extensionoriginal_file_path = Path(extracted_path) / "SMSSpamCollection"os.rename(original_file_path, data_file_path)print(f"File downloaded and saved as {data_file_path}")try:download_and_unzip_spam_data(url, zip_path, extracted_path, data_file_path)
except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError) as e:print(f"Primary URL failed: {e}. Trying backup URL...")url = "https://f001.backblazeb2.com/file/LLMs-from-scratch/sms%2Bspam%2Bcollection.zip"download_and_unzip_spam_data(url, zip_path, extracted_path, data_file_path)    
File downloaded and saved as sms_spam_collection\SMSSpamCollection.tsv

执行前面的代码后,数据集会以制表符分隔的文本文件形式保存在sms_spam_collection文件夹中的SMSSpamCollection.tsv中。我们可以如下所示将其加载为一个pandas的DataFrame:

import pandas as pddf = pd.read_csv(data_file_path, sep="\t", header=None, names=["Label", "Text"])
print(df)
     Label                                               Text
0      ham  Go until jurong point, crazy.. Available only ...
1      ham                      Ok lar... Joking wif u oni...
2     spam  Free entry in 2 a wkly comp to win FA Cup fina...
3      ham  U dun say so early hor... U c already then say...
4      ham  Nah I don't think he goes to usf, he lives aro......                                                ...
5567  spam  This is the 2nd time we have tried 2 contact u...
5568   ham               Will ü b going to esplanade fr home?
5569   ham  Pity, * was in mood for that. So...any other s...
5570   ham  The guy did some bitching but I acted like i'd...
5571   ham                         Rofl. Its true to its name[5572 rows x 2 columns]

让我们来看看类标签的分布情况:

print(df["Label"].value_counts())

执行前面的代码后,我们发现数据中包含 “火腿肠”(即非垃圾邮件)的频率远远高于 “垃圾邮件”:

Label
ham     4825
spam     747
Name: count, dtype: int64

为简化处理,并且因为我们更倾向于使用一个小型数据集(这将加快LLM的微调速度),我们选择对数据集进行下采样,使其每个类别包含747个样本。

注意: 还有其他几种处理类别不平衡的方法,但这些内容超出了本书的范围。对处理不平衡数据方法感兴趣的读者可以在附录B中找到更多信息。

我们可以使用以下代码中的内容对数据集进行下采样并创建一个平衡的数据集。

清单 6.2 创建平衡数据集

def create_balanced_dataset(df):# Count the instances of "spam"num_spam = df[df["Label"] == "spam"].shape[0]# Randomly sample "ham" instances to match the number of "spam" instancesham_subset = df[df["Label"] == "ham"].sample(num_spam, random_state=123)# Combine ham "subset" with "spam"balanced_df = pd.concat([ham_subset, df[df["Label"] == "spam"]])return balanced_dfbalanced_df = create_balanced_dataset(df)
print(balanced_df["Label"].value_counts())

执行之前的代码平衡数据集后,我们可以看到现在垃圾邮件和非垃圾邮件的数量相等:

Label
ham     747
spam    747
Name: count, dtype: int64

接下来,我们将字符串类型的类别标签 “ham” 和 “spam” 分别转换为整数类型的类别标签 0 和 1:

balanced_df["Label"] = balanced_df["Label"].map({"ham": 0, "spam": 1})

这个过程类似于将文本转换为 token ID。然而,不同的是,这里我们不是使用包含五万多个词的GPT词汇表,而是只处理两个 token ID:0 和 1。

      Label                                               Text
4307      0  Awww dat is sweet! We can think of something t...
4138      0                             Just got to  <#>
4831      0  The word "Checkmate" in chess comes from the P...
4461      0  This is wishing you a great day. Moji told me ...
5440      0      Thank you. do you generally date the brothas?...                                                ...
5537      1  Want explicit SEX in 30 secs? Ring 02073162414...
5540      1  ASKED 3MOBILE IF 0870 CHATLINES INCLU IN FREE ...
5547      1  Had your contract mobile 11 Mnths? Latest Moto...
5566      1  REMINDER FROM O2: To get 2.50 pounds free call...
5567      1  This is the 2nd time we have tried 2 contact u...[1494 rows x 2 columns]

接下来,我们创建一个名为random_split的函数,将数据集划分为三个部分:70%用于训练,10%用于验证,20%用于测试。(这些比例在机器学习中很常见,用于训练、调整和评估模型。)

清单 6.3 分割数据集

def random_split(df, train_frac, validation_frac):# Shuffle the entire DataFramedf = df.sample(frac=1, random_state=123).reset_index(drop=True)# Calculate split indicestrain_end = int(len(df) * train_frac)validation_end = train_end + int(len(df) * validation_frac)# Split the DataFrametrain_df = df[:train_end]validation_df = df[train_end:validation_end]test_df = df[validation_end:]return train_df, validation_df, test_dftrain_df, validation_df, test_df = random_split(balanced_df, 0.7, 0.1)
# Test size is implied to be 0.2 as the remainder

让我们把数据集保存为 CSV(逗号分隔值)文件,以便以后再次使用:

train_df.to_csv("train.csv", index=None)
validation_df.to_csv("validation.csv", index=None)
test_df.to_csv("test.csv", index=None)

到目前为止,我们已经下载了数据集,对其进行了平衡,并将其分为训练子集和评估子集。现在,我们将设置用于训练模型的 PyTorch 数据加载器。

6.3 创建数据加载器

我们将开发与之前处理文本数据时实现的概念上类似的 PyTorch 数据加载器。之前,我们使用滑动窗口技术生成统一长度的文本块,然后将它们分组成批,以提高模型训练的效率。每个文本块都作为一个独立的训练样本。然而,我们现在处理的是一个包含不同长度短信的垃圾短信数据集。为了像处理文本块那样对这些短信进行批处理,我们有两个主要选项:

  • 将所有短信截断为数据集或批次中最短短信的长度;
  • 将所有短信填充到数据集或批次中最长短信的长度。

第一个选项在计算上更为廉价,但如果较短的短信明显短于平均长度或最长短信,这可能导致重要信息的丢失,从而降低模型性能。因此,我们选择第二个选项,它可以保留所有短信的完整内容。

为了实现批处理并将所有短信填充到数据集中最长短信的长度,我们需要为所有较短的短信添加填充标记。为此,我们使用"<|endoftext|>"作为填充标记。

然而,我们不会直接在每条短信末尾附加字符串"<|endoftext|>",而是将"<|endoftext|>"对应的标记ID添加到编码后的短信中,如图6.6所示。50256是填充标记"<|endoftext|>"的标记ID。我们可以使用之前使用过的tiktoken包中的GPT-2分词器对"<|endoftext|>"进行编码,以再次确认该标记ID是否正确:

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

在这里插入图片描述图 6.6 输入文本的预处理过程。首先,每条输入短信被转换为一系列标记ID。然后,为了确保序列长度一致,较短的序列会被填充一个填充标记(在本例中为标记ID 50256),以匹配最长序列的长度。


事实上,执行前面的代码会返回:

[50256]

我们首先需要实现一个 PyTorch 数据集(Dataset),该数据集指定了如何加载和处理数据,然后才能实例化数据加载器。为此,我们定义了 SpamDataset 类,该类实现了图 6.6 中的概念。SpamDataset 类处理了几个关键任务:它识别训练数据集中最长的序列,对短信进行编码,并确保所有其他序列都使用填充标记填充到与最长序列相同的长度。

代码清单 6.4 设置 PyTorch Dataset 类

class SpamDataset(Dataset):def __init__(self, csv_file, tokenizer, max_length=None, pad_token_id=50256):self.data = pd.read_csv(csv_file)# Pre-tokenize textsself.encoded_texts = [tokenizer.encode(text) for text in self.data["Text"]]if max_length is None:self.max_length = self._longest_encoded_length()else:self.max_length = max_length# Truncate sequences if they are longer than max_lengthself.encoded_texts = [encoded_text[:self.max_length]for encoded_text in self.encoded_texts]# Pad sequences to the longest sequenceself.encoded_texts = [encoded_text + [pad_token_id] * (self.max_length - len(encoded_text))for encoded_text in self.encoded_texts]def __getitem__(self, index):encoded = self.encoded_texts[index]label = self.data.iloc[index]["Label"]return (torch.tensor(encoded, dtype=torch.long),torch.tensor(label, dtype=torch.long))def __len__(self):return len(self.data)def _longest_encoded_length(self):max_length = 0for encoded_text in self.encoded_texts:encoded_length = len(encoded_text)if encoded_length > max_length:max_length = encoded_lengthreturn max_length# Note: A more pythonic version to implement this method# is the following, which is also used in the next chapter:# return max(len(encoded_text) for encoded_text in self.encoded_texts)

SpamDataset 类从我们之前创建的 CSV 文件中加载数据,使用 tiktoken 中的 GPT-2 分词器对文本进行分词,并允许我们将序列填充或截断为由最长序列或预定义最大长度确定的统一长度。这确保了每个输入张量具有相同的尺寸,这是我们接下来实现的训练数据加载器中创建批次所必需的。

train_dataset = SpamDataset(csv_file="train.csv",max_length=None,tokenizer=tokenizer
)

最长序列长度存储在数据集的 max_lengthattribute 中。如果您想查看最长序列中的标记数,可以使用下面的代码:

print(train_dataset.max_length)

代码输出120,说明最长的序列不超过120个token,这对于短信而言是一个常见的长度。考虑到模型的上下文长度限制为1024个token,它最多可以处理长度为1024的序列。如果你的数据集中包含更长的文本,在创建训练数据集时可以通过传入参数 max_length=1024 来确保数据不会超过模型支持的最大输入(上下文)长度。

接下来,我们对验证集和测试集进行填充,使其长度与最长的训练序列保持一致。需要注意的是,任何超过训练集中最长样本长度的验证集和测试集样本,都会在我们之前定义的 SpamDataset 代码中使用 encoded_text[:self.max_length] 进行截断。这种截断操作是可选的;如果验证集和测试集中不存在长度超过1024个token的序列,也可以将 max_length=None 传入验证集和测试集。

val_dataset = SpamDataset(csv_file="validation.csv",max_length=train_dataset.max_length,tokenizer=tokenizer
)
test_dataset = SpamDataset(csv_file="test.csv",max_length=train_dataset.max_length,tokenizer=tokenizer
)

练习 6.1 增加上下文长度
将输入填充到模型所支持的最大token数量,并观察它对预测性能的影响。

我们可以通过将最大长度设置为1024,将输入填充到模型所支持的最大token数量:

max_length = 1024train_dataset = SpamDataset(base_path / "train.csv", max_length=max_length, tokenizer=tokenizer)
val_dataset = SpamDataset(base_path / "validation.csv", max_length=max_length, tokenizer=tokenizer)
test_dataset = SpamDataset(base_path / "test.csv", max_length=max_length, tokenizer=tokenizer)

或者,同样地,我们可以通过以下方式定义max_length:

max_length = model.pos_emb.weight.shape[0]

max_length = BASE_CONFIG["context_length"]

使用这些数据集作为输入,我们可以像之前处理文本数据那样实例化数据加载器。不过,在这种情况下,目标值表示的是类别标签,而不是文本中的下一个token。例如,如果我们选择的batch大小为8,那么每个batch将由8个长度为120的训练样本及其对应的类别标签组成,如图6.7所示。


在这里插入图片描述图6.7 一个训练批次由八条文本消息组成,每条消息以token ID表示。每条文本消息由120个token ID构成。一个类别标签数组存储这八条消息对应的类别标签,这些标签可以是0(“非垃圾信息”)或1(“垃圾信息”)。


下表中的代码创建了训练集、验证集和测试集数据加载器,这些数据加载器以 8 为单位分批加载文本信息和标签。

清单 6.5 创建 PyTorch 数据加载器

from torch.utils.data import DataLoadernum_workers = 0
batch_size = 8torch.manual_seed(123)train_loader = DataLoader(dataset=train_dataset,batch_size=batch_size,shuffle=True,num_workers=num_workers,drop_last=True,
)val_loader = DataLoader(dataset=val_dataset,batch_size=batch_size,num_workers=num_workers,drop_last=False,
)test_loader = DataLoader(dataset=test_dataset,batch_size=batch_size,num_workers=num_workers,drop_last=False,
)

为了确保数据加载器正常工作并确实返回了期望大小的批次,我们遍历训练集的加载器,并打印最后一个批次的张量维度:

print("Train loader:")
for input_batch, target_batch in train_loader:passprint("Input batch dimensions:", input_batch.shape)
print("Label batch dimensions", target_batch.shape)

输出结果为:

Train loader:
Input batch dimensions: torch.Size([8, 120])
Label batch dimensions torch.Size([8])

正如我们所看到的,输入批次由8个训练样本组成,每个样本包含120个token,与预期一致。标签张量存储了对应这8个训练样本的类别标签。

最后,为了了解数据集的规模,我们打印每个数据集中批次数量:

print(f"{len(train_loader)} training batches")
print(f"{len(val_loader)} validation batches")
print(f"{len(test_loader)} test batches")

每个数据集中的批次数如下:

130 training batches
19 validation batches
38 test batches

现在我们已经准备好了数据,接下来我们需要为微调准备模型。

6.4 使用预训练权重初始化模型

我们必须为分类微调任务准备模型,以识别垃圾信息。我们首先初始化预训练模型,如图6.8所示。


在这里插入图片描述图6.8 用于对LLM进行分类微调的三阶段流程。完成第1阶段(准备数据集)后,我们现在需要初始化LLM,接下来我们将对其进行微调以分类垃圾短信。


为了开始模型准备流程,我们采用与预训练无标签数据时相同的配置:

CHOOSE_MODEL = "gpt2-small (124M)"
INPUT_PROMPT = "Every effort moves"BASE_CONFIG = {"vocab_size": 50257,     # Vocabulary size"context_length": 1024,  # Context length"drop_rate": 0.0,        # Dropout rate"qkv_bias": True         # Query-key-value bias
}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},
}BASE_CONFIG.update(model_configs[CHOOSE_MODEL])assert train_dataset.max_length <= BASE_CONFIG["context_length"], (f"Dataset length {train_dataset.max_length} exceeds model's context "f"length {BASE_CONFIG['context_length']}. Reinitialize data sets with "f"`max_length={BASE_CONFIG['context_length']}`"
)

接下来,我们从 gpt_download.py 文件中导入 download_and_load_gpt2 函数,并复用第5章预训练中使用的 GPTModel 类和 load_weights_into_gpt 函数,将下载的权重加载到GPT模型中。

清单 6.6 加载预训练的 GPT 模型

from gpt_download import download_and_load_gpt2
from previous_chapters import GPTModel, load_weights_into_gptmodel_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();

gpt_download.py, previous_chapters.py

在将模型权重加载到GPTModel之后,我们复用了第4章和第5章中的文本生成工具函数,以确保模型能够生成连贯的文本:

from previous_chapters import (generate_text_simple,text_to_token_ids,token_ids_to_text
)text_1 = "Every effort moves you"token_ids = generate_text_simple(model=model,idx=text_to_token_ids(text_1, tokenizer),max_new_tokens=15,context_size=BASE_CONFIG["context_length"]
)print(token_ids_to_text(token_ids, tokenizer))

以下输出表明模型能够生成连贯的文本,这说明模型权重已经被正确加载:

Every effort moves you forward.The first step is to understand the importance of your work

在我们开始将模型微调为垃圾信息分类器之前,我们可以先看看该模型是否已经能够通过指令进行垃圾信息分类:

text_2 = ("Is the following text 'spam'? Answer with 'yes' or 'no':"" 'You are a winner you have been specially"" selected to receive $1000 cash or a $2000 award.'"
)token_ids = generate_text_simple(model=model,idx=text_to_token_ids(text_2, tokenizer),max_new_tokens=23,context_size=BASE_CONFIG["context_length"]
)print(token_ids_to_text(token_ids, tokenizer))

模型输出如下:

Is the following text 'spam'? Answer with 'yes' or 'no': 'You are a winner you have been specially selected to receive $1000 cash or a $2000 award.'The following text 'spam'? Answer with 'yes' or 'no': 'You are a winner

从该输出可以明显看出,模型在理解指令方面存在困难。这一结果是预期之中的,因为模型仅接受过预训练,尚未经历过指令微调。因此,我们现在开始准备对模型进行分类微调。

6.5 添加分类头部

我们必须对预训练的LLM进行修改,以使其适应分类微调。为此,我们将原始的输出层替换掉。原始输出层将隐藏表示映射到一个包含50257个词汇的词表,而我们将其替换为一个更小的输出层,仅映射到两个类别:0(“非垃圾信息”)和1(“垃圾信息”),如图6.9所示。我们仍然使用之前的模型,只是替换了输出层。

输出层节点数
从技术角度讲,我们也可以只使用一个输出节点,因为这只是一个二分类任务。然而这会要求我们修改损失函数,正如我在《Losses Learned—Optimizing Negative Log-Likelihood and Cross-Entropy in PyTorch》(https://mng.bz/NRZ2)中讨论的那样。因此,我们选择一种更通用的方法,即输出节点的数量等于类别数量。例如,对于一个三分类问题,比如将新闻文章分类为“科技”、“体育”或“政治”,我们会使用三个输出节点,依此类推。


在这里插入图片描述图6.9通过修改架构将GPT模型用于垃圾信息分类。最初,模型的线性输出层将768个隐藏单元映射到一个包含50257个词的词汇表中。为了检测垃圾信息,我们将这一层替换为一个新的输出层,它将相同的768个隐藏单元映射到仅两个类别,表示“垃圾信息”和“非垃圾信息”。


在尝试图 6.9 所示的修改之前,我们先通过 print(model) 打印模型结构:

GPTModel((tok_emb): Embedding(50257, 768)(pos_emb): Embedding(1024, 768)(drop_emb): Dropout(p=0.0, inplace=False)(trf_blocks): Sequential((0): TransformerBlock((att): MultiHeadAttention((W_query): Linear(in_features=768, out_features=768, bias=True)(W_key): Linear(in_features=768, out_features=768, bias=True)(W_value): Linear(in_features=768, out_features=768, bias=True)(out_proj): Linear(in_features=768, out_features=768, bias=True)(dropout): Dropout(p=0.0, inplace=False))(ff): FeedForward((layers): Sequential((0): Linear(in_features=768, out_features=3072, bias=True)(1): GELU()(2): Linear(in_features=3072, out_features=768, bias=True)))(norm1): LayerNorm()(norm2): LayerNorm()(drop_resid): Dropout(p=0.0, inplace=False))(1): TransformerBlock((att): MultiHeadAttention((W_query): Linear(in_features=768, out_features=768, bias=True)(W_key): Linear(in_features=768, out_features=768, bias=True)(W_value): Linear(in_features=768, out_features=768, bias=True)(out_proj): Linear(in_features=768, out_features=768, bias=True)(dropout): Dropout(p=0.0, inplace=False))(ff): FeedForward((layers): Sequential((0): Linear(in_features=768, out_features=3072, bias=True)(1): GELU()(2): Linear(in_features=3072, out_features=768, bias=True)))(norm1): LayerNorm()(norm2): LayerNorm()(drop_resid): Dropout(p=0.0, inplace=False))(2): TransformerBlock((att): MultiHeadAttention((W_query): Linear(in_features=768, out_features=768, bias=True)(W_key): Linear(in_features=768, out_features=768, bias=True)(W_value): Linear(in_features=768, out_features=768, bias=True)(out_proj): Linear(in_features=768, out_features=768, bias=True)(dropout): Dropout(p=0.0, inplace=False))(ff): FeedForward((layers): Sequential((0): Linear(in_features=768, out_features=3072, bias=True)(1): GELU()(2): Linear(in_features=3072, out_features=768, bias=True)))(norm1): LayerNorm()(norm2): LayerNorm()(drop_resid): Dropout(p=0.0, inplace=False))(3): TransformerBlock((att): MultiHeadAttention((W_query): Linear(in_features=768, out_features=768, bias=True)(W_key): Linear(in_features=768, out_features=768, bias=True)(W_value): Linear(in_features=768, out_features=768, bias=True)(out_proj): Linear(in_features=768, out_features=768, bias=True)(dropout): Dropout(p=0.0, inplace=False))(ff): FeedForward((layers): Sequential((0): Linear(in_features=768, out_features=3072, bias=True)(1): GELU()(2): Linear(in_features=3072, out_features=768, bias=True)))(norm1): LayerNorm()(norm2): LayerNorm()(drop_resid): Dropout(p=0.0, inplace=False))(4): TransformerBlock((att): MultiHeadAttention((W_query): Linear(in_features=768, out_features=768, bias=True)(W_key): Linear(in_features=768, out_features=768, bias=True)(W_value): Linear(in_features=768, out_features=768, bias=True)(out_proj): Linear(in_features=768, out_features=768, bias=True)(dropout): Dropout(p=0.0, inplace=False))(ff): FeedForward((layers): Sequential((0): Linear(in_features=768, out_features=3072, bias=True)(1): GELU()(2): Linear(in_features=3072, out_features=768, bias=True)))(norm1): LayerNorm()(norm2): LayerNorm()(drop_resid): Dropout(p=0.0, inplace=False))(5): TransformerBlock((att): MultiHeadAttention((W_query): Linear(in_features=768, out_features=768, bias=True)(W_key): Linear(in_features=768, out_features=768, bias=True)(W_value): Linear(in_features=768, out_features=768, bias=True)(out_proj): Linear(in_features=768, out_features=768, bias=True)(dropout): Dropout(p=0.0, inplace=False))(ff): FeedForward((layers): Sequential((0): Linear(in_features=768, out_features=3072, bias=True)(1): GELU()(2): Linear(in_features=3072, out_features=768, bias=True)))(norm1): LayerNorm()(norm2): LayerNorm()(drop_resid): Dropout(p=0.0, inplace=False))(6): TransformerBlock((att): MultiHeadAttention((W_query): Linear(in_features=768, out_features=768, bias=True)(W_key): Linear(in_features=768, out_features=768, bias=True)(W_value): Linear(in_features=768, out_features=768, bias=True)(out_proj): Linear(in_features=768, out_features=768, bias=True)(dropout): Dropout(p=0.0, inplace=False))(ff): FeedForward((layers): Sequential((0): Linear(in_features=768, out_features=3072, bias=True)(1): GELU()(2): Linear(in_features=3072, out_features=768, bias=True)))(norm1): LayerNorm()(norm2): LayerNorm()(drop_resid): Dropout(p=0.0, inplace=False))(7): TransformerBlock((att): MultiHeadAttention((W_query): Linear(in_features=768, out_features=768, bias=True)(W_key): Linear(in_features=768, out_features=768, bias=True)(W_value): Linear(in_features=768, out_features=768, bias=True)(out_proj): Linear(in_features=768, out_features=768, bias=True)(dropout): Dropout(p=0.0, inplace=False))(ff): FeedForward((layers): Sequential((0): Linear(in_features=768, out_features=3072, bias=True)(1): GELU()(2): Linear(in_features=3072, out_features=768, bias=True)))(norm1): LayerNorm()(norm2): LayerNorm()(drop_resid): Dropout(p=0.0, inplace=False))(8): TransformerBlock((att): MultiHeadAttention((W_query): Linear(in_features=768, out_features=768, bias=True)(W_key): Linear(in_features=768, out_features=768, bias=True)(W_value): Linear(in_features=768, out_features=768, bias=True)(out_proj): Linear(in_features=768, out_features=768, bias=True)(dropout): Dropout(p=0.0, inplace=False))(ff): FeedForward((layers): Sequential((0): Linear(in_features=768, out_features=3072, bias=True)(1): GELU()(2): Linear(in_features=3072, out_features=768, bias=True)))(norm1): LayerNorm()(norm2): LayerNorm()(drop_resid): Dropout(p=0.0, inplace=False))(9): TransformerBlock((att): MultiHeadAttention((W_query): Linear(in_features=768, out_features=768, bias=True)(W_key): Linear(in_features=768, out_features=768, bias=True)(W_value): Linear(in_features=768, out_features=768, bias=True)(out_proj): Linear(in_features=768, out_features=768, bias=True)(dropout): Dropout(p=0.0, inplace=False))(ff): FeedForward((layers): Sequential((0): Linear(in_features=768, out_features=3072, bias=True)(1): GELU()(2): Linear(in_features=3072, out_features=768, bias=True)))(norm1): LayerNorm()(norm2): LayerNorm()(drop_resid): Dropout(p=0.0, inplace=False))(10): TransformerBlock((att): MultiHeadAttention((W_query): Linear(in_features=768, out_features=768, bias=True)(W_key): Linear(in_features=768, out_features=768, bias=True)(W_value): Linear(in_features=768, out_features=768, bias=True)(out_proj): Linear(in_features=768, out_features=768, bias=True)(dropout): Dropout(p=0.0, inplace=False))(ff): FeedForward((layers): Sequential((0): Linear(in_features=768, out_features=3072, bias=True)(1): GELU()(2): Linear(in_features=3072, out_features=768, bias=True)))(norm1): LayerNorm()(norm2): LayerNorm()(drop_resid): Dropout(p=0.0, inplace=False))(11): TransformerBlock((att): MultiHeadAttention((W_query): Linear(in_features=768, out_features=768, bias=True)(W_key): Linear(in_features=768, out_features=768, bias=True)(W_value): Linear(in_features=768, out_features=768, bias=True)(out_proj): Linear(in_features=768, out_features=768, bias=True)(dropout): Dropout(p=0.0, inplace=False))(ff): FeedForward((layers): Sequential((0): Linear(in_features=768, out_features=3072, bias=True)(1): GELU()(2): Linear(in_features=3072, out_features=768, bias=True)))(norm1): LayerNorm()(norm2): LayerNorm()(drop_resid): Dropout(p=0.0, inplace=False)))(final_norm): LayerNorm()(out_head): Linear(in_features=768, out_features=50257, bias=False)
)

这个输出清晰地展示了我们在第4章中构建的架构。如前所述,GPTModel由嵌入层开始,接着是12个相同的transformer模块(图中为简洁起见仅展示了最后一个模块),然后是一个最终的LayerNorm层和输出层out_head。

接下来,我们将用一个新的输出层(参见图6.9)替换掉out_head,并对其进行微调。

微调部分层还是全部层
由于我们是从一个预训练模型开始,因此并不需要对所有模型层进行微调。在基于神经网络的语言模型中,较低的层通常捕捉的是通用的语言结构和语义,这些在各种任务和数据集之间都是适用的。因此,仅微调最后几层(即靠近输出的层)通常就足以使模型适应新的任务,因为这些层更关注微妙的语言模式和任务相关特征。一个额外的好处是,只微调少量层在计算上更高效。感兴趣的读者可以在附录B中找到更多相关信息和实验,了解哪些层值得微调。

为了让模型准备好进行分类微调,我们首先对模型进行“冻结”,这意味着我们将所有层设置为不可训练的:

for param in model.parameters():param.requires_grad = False

然后,我们替换输出层(model.out_head),该层最初将输入映射到50257维,即词汇表的大小(见图6.9)。

代码清单6.7 添加分类层

torch.manual_seed(123)num_classes = 2
model.out_head = torch.nn.Linear(in_features=BASE_CONFIG["emb_dim"], out_features=num_classes)

为了使代码更通用,我们使用 BASE_CONFIG["emb_dim"],在“gpt2-small(124M)”模型中该值为768。因此,我们也可以使用相同的代码处理更大的GPT-2模型变体。

这个新的 model.out_head 输出层的 requires_grad 属性默认设置为 True,这意味着它是模型中唯一会在训练期间被更新的层。从技术上讲,仅训练我们刚添加的输出层就足够了。然而,正如我在实验中发现的,微调额外的层会明显提升模型的预测性能。(更多细节见附录B。)

我们还将最后一个transformer模块(model.trf_blocks[-1])以及将该模块连接到输出层的最终LayerNorm模块(model.final_norm)配置为可训练的,如图6.10所示。

为了使最终的LayerNorm和最后一个transformer模块可训练,我们将它们各自的 requires_grad 属性设置为 True

for param in model.trf_blocks[-1].parameters():param.requires_grad = Truefor param in model.final_norm.parameters():param.requires_grad = True

练习 6.2 微调整个模型
与其只微调最后一个 transformer 块,不如对整个模型进行微调,并评估其对预测性能的影响。

与其只微调最后一个 transformer 块,我们也可以通过删除以下代码行来微调整个模型:

for param in model.parameters():param.requires_grad = False


在这里插入图片描述图 6.10 GPT 模型包含 12 个重复的 transformer 块。除了输出层外,我们将最终的 LayerNorm 和最后一个 transformer 块设置为可训练。其余的 11 个 transformer 块以及嵌入层保持为不可训练状态。


尽管我们添加了一个新的输出层,并将某些层标记为可训练或不可训练,我们仍然可以像以前一样使用这个模型。例如,我们可以将一条与之前示例相同的文本传入模型:

inputs = tokenizer.encode("Do you have time")
inputs = torch.tensor(inputs).unsqueeze(0)
print("Inputs:", inputs)
print("Inputs dimensions:", inputs.shape) # shape: (batch_size, num_tokens)

输出显示上述代码将输入编码为一个包含 4 个输入 token 的张量:

Inputs: tensor([[5211,  345,  423,  640]])
Inputs dimensions: torch.Size([1, 4])

然后,我们可以像往常一样将编码后的 token ID 传递给模型:

with torch.no_grad():outputs = model(inputs)print("Outputs:\n", outputs)
print("Outputs dimensions:", outputs.shape) # shape: (batch_size, num_tokens, num_classes)

输出张量如下所示:

Outputs:tensor([[[-1.5854,  0.9904],[-3.7235,  7.4548],[-2.2661,  6.6049],[-3.5983,  3.9902]]])
Outputs dimensions: torch.Size([1, 4, 2])

在这里插入图片描述图 6.11 展示了 GPT 模型在接收四个 token 的示例输入时的输出。由于我们修改了输出层,输出张量包含两列。我们在将模型用于垃圾信息分类微调时,只关注最后一行,也就是最后一个 token 对应的输出。


类似的输入在先前的模型中会产生一个形状为 [ 1 , 4 , 50257 ] [1,4,50257] [1,4,50257] 的输出张量,其中 50257 表示词表的大小。输出张量的行数与输入 token 数量一致(此例中为四个)。不过,现在每个输出的嵌入维度(列数)是 2 而不是 50257,因为我们替换了模型的输出层。

请记住,我们希望微调这个模型,以返回一个类别标签,判断输入是否为“垃圾信息”或“非垃圾信息”。我们不需要对所有四行输出都进行微调,而是只需关注一个输出 token。具体来说,我们只关注输出张量中的最后一行,也就是最后一个 token 的输出,如图 6.11 所示。

要从输出张量中提取最后一个 token 的输出,我们可以使用以下代码:

print("Last output token:", outputs[:, -1, :])

输出为:

Last output token: tensor([[-3.5983,  3.9902]])

我们仍然需要将这些值转换为类别预测。但首先,让我们理解为何只关注最后一个输出 token。

我们已经学习过注意力机制,它建立了每个输入 token 与其他输入 token 之间的联系,以及 GPT 类模型中常用的因果注意力掩码(参见第 3 章)。该掩码限制了一个 token 的注意力范围,使其只能关注当前位置及之前的位置,从而保证每个 token 只能受到自己和先前 token 的影响,如图 6.12 所示。


在这里插入图片描述图 6.12 展示了因果注意力机制,其中输入 token 之间的注意力得分以矩阵形式呈现。空白单元格表示由于因果注意力掩码而被屏蔽的位置,防止 token 关注未来的 token。单元格中的数值表示注意力得分;最后一个 token “time” 是唯一一个可以对所有前面 token 计算注意力得分的 token。


根据图 6.12 中的因果注意力掩码设置,序列中的最后一个 token 聚合的信息最多,因为它是唯一一个能够访问所有先前 token 数据的 token。因此,在垃圾信息分类任务中,我们在微调过程中重点关注最后一个 token。

现在我们已经准备好将最后一个 token 的输出转化为类别标签预测,并计算模型的初始预测准确率。随后,我们将正式对模型进行垃圾信息分类任务的微调。


练习 6.3 微调第一个 token 与最后一个 token 的比较
尝试微调第一个输出 token。观察与微调最后一个输出 token 相比,在预测性能方面的差异。

我们可以通过以下方式,将微调的目标从最后一个输出 token 改为第一个输出 token:

将代码中的
model(input_batch)[:, -1, :]
改为
model(input_batch)[:, 0, :]
即可在代码中的所有位置使用第一个输出 token 进行微调。


6.6 计算分类损失和准确率

在我们开始微调模型之前,还剩下一项小任务:我们必须实现微调过程中用于模型评估的函数,如图6.13所示。

在实现这些评估工具之前,我们先简单讨论一下如何将模型的输出转换为类别标签预测。此前,我们通过softmax函数将50,257维的输出转换为概率,然后用argmax函数返回最大概率的位置,以此计算LLM生成的下一个token的token ID。在这里,我们采用同样的方法来计算模型对于给定输入是否输出“垃圾信息”或“非垃圾信息”的预测,如图6.14所示。唯一的区别在于,我们现在处理的是2维输出,而不是50,257维输出。


在这里插入图片描述图6.13 展示了对LLM进行分类微调的三阶段流程。我们已经完成了前六步。现在准备执行第2阶段的最后一步:实现模型性能评估函数,以便在微调前、中、后用于垃圾信息分类任务。



在这里插入图片描述图6.14 模型对应于最后一个token的输出被转换为每条输入文本的概率分数。通过查找概率最高值的位置索引得到类别标签。由于模型尚未经过训练,因此会错误地预测垃圾信息标签。


我们通过一个具体示例来考察最后一个token的输出:

print("Last output token:\n", outputs[:, -1, :])

对应于最后一个token的张量值为:

Last output token:tensor([[-3.5983,  3.9902]])

我们可以通过如下方式获取类别标签:

probas = torch.softmax(outputs[:, -1, :], dim=-1)
label = torch.argmax(probas)
print("Class label:", label.item())

在这个例子中,代码返回Class label: 1,意味着模型预测该输入文本为“垃圾信息”。在此处使用softmax函数是可选的,因为最大输出值直接对应于最大概率分数。因此,我们可以省略softmax函数,简化代码如下:

logits = outputs[:, -1, :]
label = torch.argmax(logits)
print("Class label:", label.item())

这个概念可以用于计算分类准确率,即模型在整个数据集中预测正确的比例。

为了计算分类准确率,我们将基于argmax的预测代码应用于数据集中的所有样本,并定义一个calc_accuracy_loader函数来计算预测正确的比例。

清单 6.8 计算分类准确率

def calc_accuracy_loader(data_loader, model, device, num_batches=None):model.eval()correct_predictions, num_examples = 0, 0if num_batches is None:num_batches = len(data_loader)else:num_batches = min(num_batches, len(data_loader))for i, (input_batch, target_batch) in enumerate(data_loader):if i < num_batches:input_batch, target_batch = input_batch.to(device), target_batch.to(device)with torch.no_grad():logits = model(input_batch)[:, -1, :]  # Logits of last output tokenpredicted_labels = torch.argmax(logits, dim=-1)num_examples += predicted_labels.shape[0]correct_predictions += (predicted_labels == target_batch).sum().item()else:breakreturn correct_predictions / num_examples

我们使用该函数来估计多个数据集上的分类准确率,为了提高效率,仅使用10个batch进行估计:

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")# Note:
# Uncommenting the following lines will allow the code to run on Apple Silicon chips, if applicable,
# which is approximately 2x faster than on an Apple CPU (as measured on an M3 MacBook Air).
# As of this writing, in PyTorch 2.4, the results obtained via CPU and MPS were identical.
# However, in earlier versions of PyTorch, you may observe different results when using MPS.#if torch.cuda.is_available():
#    device = torch.device("cuda")
#elif torch.backends.mps.is_available():
#    device = torch.device("mps")
#else:
#    device = torch.device("cpu")
#print(f"Running on {device} device.")model.to(device) # no assignment model = model.to(device) necessary for nn.Module classestorch.manual_seed(123) # For reproducibility due to the shuffling in the training data loadertrain_accuracy = calc_accuracy_loader(train_loader, model, device, num_batches=10)
val_accuracy = calc_accuracy_loader(val_loader, model, device, num_batches=10)
test_accuracy = calc_accuracy_loader(test_loader, model, device, num_batches=10)print(f"Training accuracy: {train_accuracy*100:.2f}%")
print(f"Validation accuracy: {val_accuracy*100:.2f}%")
print(f"Test accuracy: {test_accuracy*100:.2f}%")

通过设置device参数,如果有支持Nvidia CUDA的GPU可用,模型将自动在GPU上运行;否则将在CPU上运行。输出如下:

Training accuracy: 46.25%
Validation accuracy: 45.00%
Test accuracy: 48.75%

如我们所见,预测准确率接近随机猜测的水平(在本例中为50%)。为了提高预测准确率,我们需要对模型进行微调。

但在开始微调模型之前,我们必须先定义训练过程中要优化的损失函数。我们的目标是最大化模型在垃圾信息分类任务上的准确率,这意味着上述代码应输出正确的类别标签:非垃圾信息对应0,垃圾信息对应1。

由于分类准确率不是一个可导函数,我们使用交叉熵损失(cross-entropy loss)作为其代理来最大化准确率。因此,calc_loss_batch函数保持不变,仅作一个调整:我们仅关注优化最后一个token,即model(input_batch)[:,-1,:],而不是所有的tokens,即model(input_batch)

def calc_loss_batch(input_batch, target_batch, model, device):input_batch, target_batch = input_batch.to(device), target_batch.to(device)logits = model(input_batch)[:, -1, :]  # Logits of last output tokenloss = torch.nn.functional.cross_entropy(logits, target_batch)return loss

我们使用calc_loss_batch函数来计算从前面定义的数据加载器中获取的单个批次的损失。为了计算一个数据加载器中所有批次的损失,我们像之前一样定义calc_loss_loader函数。

清单 6.9 计算分类损失

# Same as in chapter 5
def calc_loss_loader(data_loader, model, device, num_batches=None):total_loss = 0.if len(data_loader) == 0:return float("nan")elif num_batches is None:num_batches = len(data_loader)else:# Reduce the number of batches to match the total number of batches in the data loader# if num_batches exceeds the number of batches in the data loadernum_batches = min(num_batches, len(data_loader))for i, (input_batch, target_batch) in enumerate(data_loader):if i < num_batches:loss = calc_loss_batch(input_batch, target_batch, model, device)total_loss += loss.item()else:breakreturn total_loss / num_batches

与计算训练精度类似,我们现在计算每个数据集的初始损失:

with torch.no_grad(): # Disable gradient tracking for efficiency because we are not training, yettrain_loss = calc_loss_loader(train_loader, model, device, num_batches=5)val_loss = calc_loss_loader(val_loader, model, device, num_batches=5)test_loss = calc_loss_loader(test_loader, model, device, num_batches=5)print(f"Training loss: {train_loss:.3f}")
print(f"Validation loss: {val_loss:.3f}")
print(f"Test loss: {test_loss:.3f}")

初始损耗值为:

Training loss: 2.453
Validation loss: 2.583
Test loss: 2.322

接下来,我们将实现一个训练函数来微调模型,也就是说通过调整模型以最小化训练集损失。最小化训练集损失将有助于提高分类准确率,而这正是我们的总体目标。

6.7 在监督数据上微调模型

我们必须定义并使用训练函数来微调预训练的LLM,从而提升其对垃圾信息的分类准确率。图6.15所示的训练循环与我们用于预训练的整体训练循环相同;唯一的区别在于,我们现在是计算分类准确率,而不是生成示例文本来评估模型。


在这里插入图片描述图6.15 PyTorch中用于训练深度神经网络的典型训练循环包含多个步骤,在训练集上按批次迭代多个周期。在每次循环中,我们计算每个训练批次的损失,用于确定损失梯度,并利用这些梯度来更新模型权重,以最小化训练集损失。


实现图6.15中概念的训练函数也与我们在预训练模型时使用的train_model_simple函数非常相似。唯一的两个区别是,我们现在追踪的是已经看到的训练样本数量(examples_seen),而不是token数量,并且我们在每个周期后计算准确率,而不是打印示例文本。

清单 6.10 微调模型以分类垃圾邮件

# Overall the same as `train_model_simple` in chapter 5
def train_classifier_simple(model, train_loader, val_loader, optimizer, device, num_epochs, eval_freq, eval_iter):# Initialize lists to track losses and examples seentrain_losses, val_losses, train_accs, val_accs = [], [], [], []examples_seen, global_step = 0, -1# Main training loopfor epoch in range(num_epochs):model.train()  # Set model to training modefor input_batch, target_batch in train_loader:optimizer.zero_grad() # Reset loss gradients from previous batch iterationloss = calc_loss_batch(input_batch, target_batch, model, device)loss.backward() # Calculate loss gradientsoptimizer.step() # Update model weights using loss gradientsexamples_seen += input_batch.shape[0] # New: track examples instead of tokensglobal_step += 1# Optional evaluation stepif global_step % eval_freq == 0:train_loss, val_loss = evaluate_model(model, train_loader, val_loader, device, eval_iter)train_losses.append(train_loss)val_losses.append(val_loss)print(f"Ep {epoch+1} (Step {global_step:06d}): "f"Train loss {train_loss:.3f}, Val loss {val_loss:.3f}")# Calculate accuracy after each epochtrain_accuracy = calc_accuracy_loader(train_loader, model, device, num_batches=eval_iter)val_accuracy = calc_accuracy_loader(val_loader, model, device, num_batches=eval_iter)print(f"Training accuracy: {train_accuracy*100:.2f}% | ", end="")print(f"Validation accuracy: {val_accuracy*100:.2f}%")train_accs.append(train_accuracy)val_accs.append(val_accuracy)return train_losses, val_losses, train_accs, val_accs, examples_seen

evaluate_modelfunction与我们用于预训练的函数完全相同:

# Same as chapter 5
def evaluate_model(model, train_loader, val_loader, device, eval_iter):model.eval()with torch.no_grad():train_loss = calc_loss_loader(train_loader, model, device, num_batches=eval_iter)val_loss = calc_loss_loader(val_loader, model, device, num_batches=eval_iter)model.train()return train_loss, val_loss

接下来,我们初始化优化器,设置训练轮数(epochs),并使用train_classifier_simple函数启动训练。在一台M3版MacBook Air笔记本电脑上,训练大约需要6分钟;而在V100或A100 GPU上,训练则不到半分钟即可完成:

import timestart_time = time.time()torch.manual_seed(123)optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5, weight_decay=0.1)num_epochs = 5
train_losses, val_losses, train_accs, val_accs, examples_seen = train_classifier_simple(model, train_loader, val_loader, optimizer, device,num_epochs=num_epochs, eval_freq=50, eval_iter=5,
)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.153, Val loss 2.392
Ep 1 (Step 000050): Train loss 0.617, Val loss 0.637
Ep 1 (Step 000100): Train loss 0.523, Val loss 0.557
Training accuracy: 70.00% | Validation accuracy: 72.50%
Ep 2 (Step 000150): Train loss 0.561, Val loss 0.489
Ep 2 (Step 000200): Train loss 0.419, Val loss 0.397
Ep 2 (Step 000250): Train loss 0.409, Val loss 0.353
Training accuracy: 82.50% | Validation accuracy: 85.00%
Ep 3 (Step 000300): Train loss 0.333, Val loss 0.320
Ep 3 (Step 000350): Train loss 0.340, Val loss 0.306
Training accuracy: 90.00% | Validation accuracy: 90.00%
Ep 4 (Step 000400): Train loss 0.136, Val loss 0.200
Ep 4 (Step 000450): Train loss 0.153, Val loss 0.132
Ep 4 (Step 000500): Train loss 0.222, Val loss 0.137
Training accuracy: 100.00% | Validation accuracy: 97.50%
Ep 5 (Step 000550): Train loss 0.207, Val loss 0.143
Ep 5 (Step 000600): Train loss 0.083, Val loss 0.074
Training accuracy: 100.00% | Validation accuracy: 97.50%
Training completed in 0.93 minutes.

然后,我们使用 Matplotlib 绘制训练集和验证集的损失函数图。

清单 6.11 绘制分类损失图

import matplotlib.pyplot as pltdef plot_values(epochs_seen, examples_seen, train_values, val_values, label="loss"):fig, ax1 = plt.subplots(figsize=(5, 3))# Plot training and validation loss against epochsax1.plot(epochs_seen, train_values, label=f"Training {label}")ax1.plot(epochs_seen, val_values, linestyle="-.", label=f"Validation {label}")ax1.set_xlabel("Epochs")ax1.set_ylabel(label.capitalize())ax1.legend()# Create a second x-axis for examples seenax2 = ax1.twiny()  # Create a second x-axis that shares the same y-axisax2.plot(examples_seen, train_values, alpha=0)  # Invisible plot for aligning ticksax2.set_xlabel("Examples seen")fig.tight_layout()  # Adjust layout to make roomplt.savefig(f"{label}-plot.pdf")plt.show()epochs_tensor = torch.linspace(0, num_epochs, len(train_losses))
examples_seen_tensor = torch.linspace(0, examples_seen, len(train_losses))plot_values(epochs_tensor, examples_seen_tensor, train_losses, val_losses)

图 6.16 绘制了由此得出的损耗曲线。


在这里插入图片描述图6.16 模型在五轮训练中的训练损失和验证损失。实线表示训练损失,虚线表示验证损失。可以看到,这两者在第一轮时迅速下降,并在第五轮左右逐渐趋于稳定。这一趋势表明模型学习进展良好,说明模型不仅从训练数据中学习到了知识,还很好地泛化到了未见过的验证数据上。


如图6.16中陡峭的下降趋势所示,模型从训练数据中学得很好,而且几乎没有过拟合的迹象;也就是说,训练集和验证集的损失之间没有明显的差距。

选择训练轮数(epochs)
前面在启动训练时,我们将训练轮数设置为5。训练轮数取决于数据集的规模和任务的难度,没有通用的解决方案或建议,尽管将轮数设为5通常是一个不错的起点。如果在前几轮后模型出现了过拟合,如损失曲线图(见图6.16)所示,那么你可能需要减少训练轮数。反之,如果趋势线表明验证损失在继续训练后可能进一步下降,则应增加训练轮数。在这个具体案例中,5轮训练是一个合理的选择,因为没有出现早期过拟合的迹象,而且验证损失接近于0。

接下来,我们使用相同的plot_values函数绘制分类准确率图:

epochs_tensor = torch.linspace(0, num_epochs, len(train_accs))
examples_seen_tensor = torch.linspace(0, examples_seen, len(train_accs))plot_values(epochs_tensor, examples_seen_tensor, train_accs, val_accs, label="accuracy")

图6.17显示了最终的准确率曲线。模型在第4轮和第5轮后达到了相对较高的训练和验证准确率。值得注意的是,我们之前将eval_iter=5进行了设置。


在这里插入图片描述图6.17 无论是训练准确率(实线)还是验证准确率(虚线)在训练初期都大幅上升,随后趋于平稳,几乎达到了完美的准确率得分1.0。两条曲线在整个训练轮数过程中保持非常接近,这表明模型对训练数据并没有出现明显的过拟合现象。


当使用train_classifier_simple函数时,这意味着我们对训练和验证性能的估计仅基于5个批次,这是为了在训练过程中提高效率。

现在,我们必须通过运行以下代码,对整个数据集的训练集、验证集和测试集计算性能指标,这一次不再定义eval_iter值:

train_accuracy = calc_accuracy_loader(train_loader, model, device)
val_accuracy = calc_accuracy_loader(val_loader, model, device)
test_accuracy = calc_accuracy_loader(test_loader, model, device)print(f"Training accuracy: {train_accuracy*100:.2f}%")
print(f"Validation accuracy: {val_accuracy*100:.2f}%")
print(f"Test accuracy: {test_accuracy*100:.2f}%")

最终得到的准确率如下:

Training accuracy: 97.21%
Validation accuracy: 97.32%
Test accuracy: 95.67%

训练集和测试集的性能几乎相同。训练集与测试集准确率之间的轻微差异表明对训练数据的过拟合很小。通常情况下,验证集的准确率会略高于测试集的准确率,这是因为模型开发过程中常常会调整超参数以在验证集上获得良好表现,但这些调整可能无法有效泛化到测试集上。这种情况很常见,但可以通过调整模型设置来尽量缩小这种差距,例如增加dropout率( d r o p _ r a t e drop\_rate drop_rate)或在优化器配置中提高 w e i g h t _ d e c a y weight\_decay weight_decay参数。

6.8 使用LLM进行垃圾信息分类

在完成了模型的微调与评估之后,我们现在已经准备好对垃圾信息进行分类了(见图6.18)。我们将使用基于GPT的垃圾信息分类微调模型。下面的classify_review函数遵循了与之前实现的SpamDataset中相似的数据预处理步骤。接着,在将文本处理成token ID之后,该函数使用模型来预测一个整数类别标签(类似于我们在第6.6节中实现的内容),并返回相应的类别名称。


在这里插入图片描述图6.18 对LLM进行分类微调的三阶段过程。第10步是第3阶段的最后一步——使用微调后的模型对新的垃圾信息进行分类。


代码清单6.12 使用模型对新文本进行分类

def classify_review(text, model, tokenizer, device, max_length=None, pad_token_id=50256):model.eval()# Prepare inputs to the modelinput_ids = tokenizer.encode(text)supported_context_length = model.pos_emb.weight.shape[0]# Note: In the book, this was originally written as pos_emb.weight.shape[1] by mistake# It didn't break the code but would have caused unnecessary truncation (to 768 instead of 1024)# Truncate sequences if they too longinput_ids = input_ids[:min(max_length, supported_context_length)]# Pad sequences to the longest sequenceinput_ids += [pad_token_id] * (max_length - len(input_ids))input_tensor = torch.tensor(input_ids, device=device).unsqueeze(0) # add batch dimension# Model inferencewith torch.no_grad():logits = model(input_tensor)[:, -1, :]  # Logits of the last output tokenpredicted_label = torch.argmax(logits, dim=-1).item()# Return the classified resultreturn "spam" if predicted_label == 1 else "not spam"

让我们在一个文本示例中试试这个 classify_review 函数:

text_1 = ("You are a winner you have been specially"" selected to receive $1000 cash or a $2000 award."
)print(classify_review(text_1, model, tokenizer, device, max_length=train_dataset.max_length
))

由此产生的模型能正确预测 spam . 让我们再举一个例子:

text_2 = ("Hey, just wanted to check if we're still on"" for dinner tonight? Let me know!"
)print(classify_review(text_2, model, tokenizer, device, max_length=train_dataset.max_length
))

模型再次做出了正确预测,并返回了 not spam 的标签。

最后,我们来保存模型,以便在日后再次使用时无需重新训练。我们可以使用torch.save方法:

torch.save(model.state_dict(), "review_classifier.pth")

保存后,就可以加载模型:

model_state_dict = torch.load("review_classifier.pth", map_location=device, weights_only=True)
model.load_state_dict(model_state_dict)

总结

  • 针对LLM(大语言模型)的微调有多种策略,包括分类微调和指令微调。

  • 分类微调的做法是通过一个较小的分类层替换LLM的输出层。

  • 在将文本信息分类为“垃圾信息”或“非垃圾信息”的任务中,新的分类层仅包含两个输出节点。而此前我们使用的输出节点数量等于词汇表中唯一token的数量(即50,256个)。

  • 与预训练阶段预测文本中下一个token不同,分类微调训练模型输出正确的类别标签——例如“垃圾信息”或“非垃圾信息”。

  • 用于微调的模型输入是经过token ID转换的文本,与预训练阶段相似。

  • 在微调LLM之前,我们先加载预训练模型作为基础模型。

  • 评估分类模型的方式是计算分类准确率(即正确预测的比例或百分比)。

  • 对分类模型的微调使用的仍是交叉熵损失函数,与预训练LLM时使用的相同。

相关文章:

用 pytorch 从零开始创建大语言模型(六):对分类进行微调

用 pytorch 从零开始创建大语言模型&#xff08;六&#xff09;&#xff1a;对分类进行微调 6 微调用于分类6.1 微调的不同类别6.2 准备数据集6.3 创建数据加载器6.4 使用预训练权重初始化模型6.5 添加分类头部6.6 计算分类损失和准确率6.7 在监督数据上微调模型6.8 使用LLM进…...

Android Compose 层叠布局(ZStack、Surface)源码深度剖析(十三)

Android Compose 层叠布局&#xff08;ZStack、Surface&#xff09;源码深度剖析 一、引言 在 Android 应用开发领域&#xff0c;用户界面&#xff08;UI&#xff09;的设计与实现一直是至关重要的环节。随着技术的不断演进&#xff0c;Android Compose 作为一种全新的声明式…...

计算机网络-2 物理层

【考纲内容】 &#xff08;一&#xff09;通信基础 信道、信号、带宽、码元、波特、速率、信源与信宿等基本概念&#xff1b; 奈奎斯特定理与香农定理&#xff1b;编码与调制&#xff1b; 电路交换、报文交换与分组交换&#xff1b;数据报与虚电路① 视频讲解 &#xff08;二…...

如何解决微服务调用链性能问题(优化 JVM 配置,降低 Full GC 频率)

1. 问题背景 在微服务架构中&#xff0c;服务之间的调用链较长&#xff0c;且频繁的远程调用可能导致性能瓶颈。同时&#xff0c;JVM 的 Full GC&#xff08;Full Garbage Collection&#xff09;频繁发生会导致应用暂停时间过长&#xff0c;影响用户体验。具体问题表现为&…...

深入理解 C# 反射 的使用

总目录 前言 反射是.NET框架中一个强大的特性&#xff0c;允许程序在运行时检查和操作类型信息。通过反射&#xff0c;开发者可以动态地创建对象、调用方法、访问属性等&#xff0c;为程序提供了极大的灵活性。本文将详细讲解C#反射的使用方法及其应用场景。 一、什么是反射&a…...

Java面试第十三山!《设计模式》

大家好&#xff0c;我是陈一。如果文章对你有帮助&#xff0c;请留下一个宝贵的三连哦&#xff5e; 万分感谢&#xff01; 一、设计模式入门指南 1. 什么是设计模式&#xff1f; 设计模式是可复用的解决方案模板&#xff0c;用于解决软件开发中常见的架构问题。如同建筑领域的…...

AI+视频赋能智慧农业:EasyCVR打造全域可视化农场监管平台

随着科技的飞速发展&#xff0c;传统农业正加速向智慧农业转型&#xff0c;农场管理也迎来了前所未有的变革机遇。在这一进程中&#xff0c;如何有效整合先进的信息技术&#xff0c;实现农场的精准化、智能化管理&#xff0c;成为了摆在农场主和农业管理者面前的关键课题。 基于…...

wsl2配置xv6全解(包括22.04Jammy)

文章目录 获取xv6源代码Ubuntu20.04 Version安装指令成功测试参考MIT2021年官方文档 24.04 Version安装指令成功测试参考MIT2024年官方文档 Ubuntu 22.04没有官方文档&#xff1f; 配置大体流程1. 卸载原本qemu&#xff08;如果之前安装了&#xff09;2. clone qemu官方源代码&…...

区块链技术的应用场景和优势

区块链技术是一种分布式数据库技术&#xff0c;它的应用场景和优势包括但不限于以下几点&#xff1a; 金融领域&#xff1a;区块链可以用于数字货币的交易和结算&#xff0c;实现去中心化的金融交易&#xff0c;提供更安全、透明和高效的支付方式&#xff1b;另外&#xff0c;也…...

基于深度学习的相位调制算法步骤

1.构建网络结构 2.制作数据集 3.训练网络 4.引入评价指标 5.迭代优化 总结 通过以上步骤&#xff0c;可以实现基于深度学习的相位调制算法&#xff1a; 使用 U-Net 构建神经网络。 生成数据集并训练网络。 使用训练好的网络预测相位分布。 通过相关系数 γ 评估调制效果&…...

Linux的I2C总线的原理和结构详解

Linux的I2C总线的原理和结构讲解 我前面基本已经吃透了Platform总线&#xff0c;关于Platform总线的原理和结构&#xff0c;详情见下面三篇博文&#xff1a; https://blog.csdn.net/wenhao_ir/article/details/145023181 https://blog.csdn.net/wenhao_ir/article/details/14…...

深入理解Linux中的SCP命令:使用与原理

在Linux系统中&#xff0c;文件传输是一个常见的操作。无论是将文件从本地传输到远程服务器&#xff0c;还是从远程服务器下载文件到本地&#xff0c;SCP&#xff08;Secure Copy Protocol&#xff09;都是一个非常实用的工具。本文将详细介绍SCP命令的使用方法&#xff0c;并深…...

【Android】VehiclePropertyAccess引起CarService崩溃

VehiclePropertyAccess引起CarService崩溃 VehiclePropertyAccess VehiclePropertyAccess属性&#xff0c;用于定义车辆属性的访问权限。权限包括 读&#xff1a;READ&#xff0c;只可以读取&#xff0c;不能写入。 VehiclePropertyAccess:READ写&#xff1a;WRITE&#xf…...

小米AX6000解锁ssh避坑笔记

经过网上教程不断尝试,终于解锁成功。 环境信息: Win10 笔记本 + AX210 WIFI6E网卡Vmware 16小米AX60000.可以先备份路由器的配置信息 1.首先降级小米AX6000到1.0.55 1.0.55下载路径 升级时注意: 清除当前所有用户配置升级完成后,选择不自动升级2.升级完成后,笔记本重新…...

论华为 Pura X 折叠屏性能检测

在科技浪潮中&#xff0c;折叠屏手机以其创新形态掀起市场热潮。华为 Pura X 作为华为最新折叠手机&#xff0c;承载前沿科技与精湛工艺&#xff0c;成为行业焦点。它融合先进折叠屏技术与优质材质&#xff0c;致力于打破传统手机使用边界&#xff0c;为用户开启全新体验。但产…...

关于极端场景下,数据库更新与 MQ 消息一致性保障方案的详细总结

目录 一、核心问题场景 二、RocketMQ 事务消息方案 1. 核心机制 2. 执行流程 3. 关键优势 4. 局限性 三、消息表方案 1. 核心机制 2. 执行流程 3. 关键优势 4. 局限性 四、方案对比与选择 五、实施建议 六、总结 一、核心问题场景 当数据库更新后,若 MQ 消息未…...

面试题精选《剑指Offer》:JVM类加载机制与Spring设计哲学深度剖析-大厂必考

一、JVM类加载核心机制 &#x1f525; 问题5&#xff1a;类从编译到执行的全链路过程 完整生命周期流程图 关键技术拆解 编译阶段 查看字节码指令&#xff1a;javap -v Robot.class 常量池结构解析&#xff08;CONSTANT_Class_info等&#xff09; 类加载阶段 // 手动加载…...

透析主流CSS预处理器的区别

Sass 和 Less 是两种主流的 CSS 预处理器&#xff08;CSS Preprocessor&#xff09;&#xff0c;它们通过扩展原生 CSS 的语法&#xff0c;提供了变量、嵌套、混合&#xff08;Mixins&#xff09;、函数等高级功能&#xff0c;帮助开发者编写更高效、可维护的样式代码。以下是它…...

Redis 本地安装

首先安装&#xff1a; https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-from-source/ 进入root目录 tar -xzvf redis-stable.tar.gz cd redis-stable make然后 install sudo make install最后可以直接启动 redis-server但是此时启…...

Android Launcher3 首屏图标锁定技术方案解析

一、需求背景与技术挑战 在Android 13系统定制开发中&#xff0c;需实现Launcher首屏图标固定功能。该需求需在以下技术维度进行突破&#xff1a; 拖拽事件拦截机制&#xff1a;需精准识别拖拽目标区域 布局层级判定&#xff1a;准确识别第一屏的布局标识 跨屏操作限制&…...

MySQL 处理重复数据:保留一条与两条的实现方案

在数据库管理中&#xff0c;处理重复数据是一项常见的任务。本文将详细介绍如何在 MySQL 数据库里&#xff0c;针对 test 表中 fd 和 fe 字段存在的重复数据进行处理&#xff0c;分别实现保留一条和两条数据的操作。 表结构与需求概述 假设 test 表包含三个字段&#xff1a;id…...

Go红队开发—CLI框架(一)

CLI开发框架 命令行工具开发&#xff0c;主要是介绍开发用到的包&#xff0c;集成了一个框架&#xff0c;只要学会了基本每个人都能开发安全工具了。 该文章先学flags包&#xff0c;是比较经典的一个包&#xff0c;相比后面要学习的集成框架这个比较自由比较细化点&#xff0…...

deque

deque概念 双端数组&#xff0c;可以对头端进行插入删除操作 deque和vector差别(就像数据结构中的栈和队列) vector对于头部的插入删除效率低&#xff0c;而deque则相对高效 vector和deque都支持随机访问&#xff0c;但是vector的随机访问效率低&#xff0c;而deque则相对高效…...

【Oracle资源损坏类故障】:详细了解坏块

目录 1、物理坏块与逻辑坏块 1.1、物理坏块 1.2、逻辑坏块 2、两个坏块相关的参数 2.1、db_block_checksum 2.2、db_block_checking 3、检测坏块 3.1、告警日志 3.2、RMAN 3.3、ANALYZE 3.4、数据字典 3.5、DBVERIFY 4、修复坏块 4.1、RMAN修复 4.2、DBMS_REPA…...

数据分析处理库-Pandas

1.1 Pandas概述 核心概念&#xff1a; Pandas 是基于 NumPy 的数据分析库&#xff0c;核心数据结构&#xff1a;Series&#xff08;一维&#xff09;和 DataFrame&#xff08;二维&#xff09;。 应用场景&#xff1a;数据清洗、转换、统计分析、时间序列处理。 特点&#x…...

阿里云平台Vue项目打包发布

目录&#xff1a; 1、vue项目打包2、通过ngixn发布vue的打包文件 1、vue项目打包 在你的vue项目下执行npm run build命令进行打包。 2、通过ngixn发布vue的打包文件 直接将打包的dist文件拷贝到nginx目录下即可。 修改nginx.conf的配置文件的相关配置&#xff0c;如端口或者ro…...

2025/03/19 Cursor使用方法(Java方向,适合Java后端把家从idea搬家到cursor)

Cursor介绍 官网:Cursor - The AI Code Editor 中文教程网:学习 Cursor &#xff0c;拥抱 AI 编程 | Cursor 101 Cursor 是一款专为程序员打造的集成开发环境&#xff08;IDE&#xff09;&#xff0c;它结合了大语言模型的能力&#xff0c;旨在提高开发效率. 与传统的 IDE&…...

平台与架构:深度解析与开发实践

平台与架构&#xff1a;深度解析与开发实践 1. 什么是平台与架构&#xff1f; 平台&#xff08;Platform&#xff09;&#xff1a;指操作系统或运行环境&#xff0c;例如 linux、windows、darwin&#xff08;macOS&#xff09;、android 等。架构&#xff08;Architecture&…...

xss-labs第八、九关卡以及XSS GAME的Ok,Boomer关卡

第八关 靶场代码 <!DOCTYPE html><!--STATUS OK--><html> <head> <meta http-equiv"content-type" content"text/html;charsetutf-8"> <script> window.alert function() { confirm("完成的不错&#…...

electron框架(1.0)认识electron和基础创建

----什么是electron框架 中文网地址&#xff08;https://electronjs.p2hp.com/docs/latest/tutorial/quick-start&#xff09; ----electron流程模型 ----项目搭建 --起步&#xff08;需下载&#xff09;&#xff1a; node -v npm -v--创建初始文件&#xff1a; mkdir my-e…...

考OCP认证要交哪些费用?

考OCP认证要交哪些费用? 考OCP认证&#xff0c;指的是Oracle数据库管理员中级认证 Oracle Certified Professional&#xff0c;这是Oracle非常有名的一个认证&#xff0c;对于个人帮助巨大。 OCP认证要交不少钱&#xff0c;些费用因考试版本、培训机构和地区差异而有所不同&a…...

基于漂浮式海上风电场系统的浮式风力发电机matlab仿真

目录 1.课题概述 2.系统仿真结果 3.核心程序与模型 4.系统原理简介 5.完整工程文件 1.课题概述 基于漂浮式海上风电场系统的浮式风力发电机matlab仿真&#xff0c;通过MATLAB数值仿真对浮式风力发电机的性能做模拟与仿真。 2.系统仿真结果 3.核心程序与模型 版本&#x…...

Jupyter Notebook 常用命令(自用)

最近有点忘记了一些常见命令&#xff0c;这里就记录一下&#xff0c;懒得找了。 文章目录 一、文件操作命令1. %cd 工作目录2. %pwd 显示路径3. !ls 列出文件4. !cp 复制文件5. !mv 移动或重命名6. !rm 删除 二、代码调试1. %time 时间2. %timeit 平均时长3. %debug 调试4. %ru…...

RabbitMQ 详细原理解析

RabbitMQ 是一个基于 AMQP&#xff08;Advanced Message Queuing Protocol&#xff09; 协议的开源消息代理中间件&#xff0c;广泛用于分布式系统中的异步通信、服务解耦、流量削峰等场景。其核心设计围绕生产者、消费者、队列、交换机和虚拟主机等组件&#xff0c;结合 AMQP …...

HTTP状态码全解析

1. 状态码分类 类别范围含义1xx100-199信息性&#xff1a;请求被接收&#xff0c;需进一步处理&#xff08;临时响应&#xff09;2xx200-299成功&#xff1a;请求被正确处理3xx300-399重定向&#xff1a;需后续操作完成请求&#xff08;如跳转到新URL&#xff09;4xx400-499客…...

从零实现本地文生图部署(Stable Diffusion)

1. 依赖安装 文件打包下载地址&#xff08;Stable Diffusion&#xff09; # git &#xff1a; 用于下载源码 https://git-scm.com/downloads/win # Python 作为基础编译环境 https://www.python.org/downloads/ # Nvidia 驱动&#xff0c;用于编译使用GPU显卡硬件 https://ww…...

手撕算法——链表

算法基础——链表-CSDN博客 一、排队顺序 题⽬来源&#xff1a;洛⾕ 题⽬链接&#xff1a;B3630 排队顺序 - 洛谷 难度系数&#xff1a;★ 1. 题目描述 2. 算法原理 本题相当于告诉了我们每⼀个点的后继&#xff0c;使⽤静态链表的存储⽅式能够很好的还原这个队列。 数组中 [1,…...

css-grid布局

文章目录 1、布局2、网格轨道3、间距Gap4、网格线5、网格别名 当一个 HTML 元素将 display 属性设置为 grid 或 inline-grid 后&#xff0c;它就变成了一个网格容器&#xff0c;这个元素的所有直系子元素将成为网格元素。 1、布局 启用grid布局类似与flex布局&#xff0c;不过g…...

1.企业级AD活动目录核心解析:架构、组件与集成实践

在当今数字化时代&#xff0c;企业级网络环境日益复杂&#xff0c;高效、安全的资源管理和用户认证成为企业 IT 运营的关键。AD&#xff08;Active Directory&#xff09;活动目录作为微软 Windows 系列服务器中的重要目录服务&#xff0c;为企业级网络管理提供了强大的解决方案…...

哈尔滨工业大学DeepSeek公开课人工智能:大模型原理 技术与应用-从GPT到DeepSeek|附视频下载方法

导 读INTRODUCTION 今天继续哈尔滨工业大学车万翔教授带来了一场主题为“DeepSeek 技术前沿与应用”的报告。 本报告深入探讨了大语言模型在自然语言处理&#xff08;NLP&#xff09;领域的核心地位及其发展历程&#xff0c;从基础概念出发&#xff0c;延伸至语言模型在机器翻…...

ChatGPT vs DeepSeek vs Copilot vs Claude:谁将问鼎AI王座?

李升伟 整理 2025年的人工智能领域创新涌动&#xff0c;ChatGPT、DeepSeek、Copilot和Claude四大模型各领风骚。这些AI系统各具特色&#xff0c;分别专注于编程、创意写作、技术推理和AI伦理等不同领域。本文将深入解析这些AI模型的功能特性及其优势领域。 核心AI模型解析 C…...

【嵌入式Linux】基于ArmLinux的智能垃圾分类系统项目

目录 1. 功能需求2. Python基础2.1 特点2.2 Python基础知识2.3 dict嵌套简单说明 3. C语言调用Python3.1 搭建编译环境3.2 直接调用python语句3.3 调用无参python函数3.4 调用有参python函数 4. 阿里云垃圾识别方案4.1 接入阿里云4.2 C语言调用阿里云Python接口 5. 香橙派使用摄…...

Vue3中router最佳封装落地

文章目录 前言一、拆分路由文件夹&#xff1f;二、main.ts中注册路由总结 前言 router在使用过程中如果我们直接在一个文件的一个数组中配置&#xff0c;最后路由越来越多会导致不易管理&#xff0c;我们可以将一个页面的路由配置在一个数组中最后统一导入&#xff0c;这样就会…...

[Linux] make自动化构建

目录 一.什么是make 二.Makefile结构 2.1 典型结构 2.2 变量 1. 普通变量&#xff08;User-Defined Variables&#xff09; 2. 自动变量&#xff08;Automatic Variables&#xff09; 3. 预定义变量&#xff08;Built-in Variables&#xff09; 4. 函数变量&#xff0…...

剑指 Offer II 113. 课程顺序

comments: true edit_url: https://github.com/doocs/leetcode/edit/main/lcof2/%E5%89%91%E6%8C%87%20Offer%20II%20113.%20%E8%AF%BE%E7%A8%8B%E9%A1%BA%E5%BA%8F/README.md 剑指 Offer II 113. 课程顺序 题目描述 现在总共有 numCourses 门课需要选&#xff0c;记为 0 到 n…...

蓝桥杯 小球反弹

问题描述 有一个长方形&#xff0c;长为 343720 单位长度&#xff0c;宽为 233333 单位长度。 在其内部左上角顶点有一小球&#xff08;无视其体积&#xff09;&#xff0c;其初速度方向如图所示&#xff0c;且保持运动速率不变。分解到长宽两个方向上的速率之比为&#xff1…...

Python 监听模式(Observer Pattern)

1. 监听模式技术方案 监听模式&#xff08;Observer Pattern&#xff09;是一种行为设计模式&#xff0c;允许对象&#xff08;称为“观察者”或“监听者”&#xff09;在另一个对象&#xff08;称为“被观察者”或“主题”&#xff09;的状态发生变化时接收通知。这种模式的核…...

蓝桥备赛(25)算法篇【差分】

一、差分 前缀和和差分的核心思想是预处理 &#xff0c; 可以在暴力枚举的过程中 &#xff0c; 快速给出查询结果 &#xff0c; 从而优化时间复杂度 。 最经典的用空间替换时间的做法。 学完差分之后 &#xff0c; 大家会发现 &#xff0c; 前缀和与差分是一对互逆的运算 二、一…...

Linux|fork命令及其使用的写时拷贝技术

fork复制进程 fork通过以下步骤来复制进程&#xff1a; 分配新的进程控制块&#xff1a;内核为新进程分配一个新的进程控制块&#xff08;PCB&#xff09;&#xff0c;用于存储进程的相关信息&#xff0c;如进程 ID、状态、寄存器值、内存指针等。复制进程地址空间&#xff1…...

sgpt 终端使用指南

1. 什么是 sgpt&#xff1f; sgpt 是一个基于 OpenAI API 的命令行工具&#xff0c;允许用户在终端中与 AI 进行交互&#xff0c;支持自然语言对话、代码生成、Shell 命令生成等功能。本文将介绍 sgpt 的安装方法、基本用法、配置文件路径及修改方式&#xff0c;并提供完整的配…...