目录
1. 简介
Llama 3.1 是 Meta 公司于 2024 年 7 月 23 日发布的最新开源大语言模型。这一版本在先前 Llama 3 的基础上进行了增强,支持多模态处理,并具有多种参数规模,包括 8B、70B 和 405B 参数。Llama 3.1 经过大规模文本数据的预训练,并结合了超过 1000 万个人工标注的示例,显著提高了模型的性能和适应性。Llama 3.1 尤其在多语言对话和编码能力方面表现出色,是目前 Meta 最先进的大语言模型之一。
Meta公司开源了Llama 3.1大模型,任何人都可以下载并用于二次开发。本教程的目的是帮助用户从头开始构建自己的Llama 3.1模型,包括在本地搭建自己的大模型和对大模型进行微调。通过详细的步骤讲解,我们将引导您完成Llama 3.1模型的安装、配置、训练以及微调,帮助您充分利用这个先进的开源大语言模型。
2. 准备工作
2.1 系统要求
在开始之前,确保您的系统满足以下要求:
- 操作系统:Linux (推荐Ubuntu)
- Python 版本:Python 3.8及以上
- GPU:NVIDIA GPU,支持CUDA 11及以上
- CUDA和cuDNN已正确安装
2.2 安装必要的依赖库
首先,安装所需的Python库:
pip install torch matplotlib tiktoken
2.3 下载模型和分词器
从Meta的官方网站下载Llama 3.1模型权重和分词器文件。假设下载链接如下:
- 模型权重:
https://llama.meta.com/llama3.1/model.pth
- 分词器:
https://llama.meta.com/llama3.1/tokenizer.model
下载后,将它们保存在您的工作目录中。
3. 核心实现步骤
3.1 加载分词器
使用 tiktoken 库加载和使用 Llama 3.1 的分词器。以下是下载分词器模型并进行初始化的详细步骤。
安装 tiktoken
首先,确保您已经安装了 tiktoken 库。如果没有安装,请使用以下命令进行安装:
pip install tiktoken
下载分词器模型
从Meta的官方网站下载Llama 3.1的分词器模型。假设下载链接为:
- 分词器模型:
https://llama.meta.com/llama3.1/tokenizer.model
将下载的文件保存到您的工作目录中,例如 path/to/tokenizer.model
。
初始化分词器
接下来,使用 tiktoken 库加载并初始化分词器模型。以下是示例代码:
from pathlib import Path
import tiktoken
# 指定分词器模型的路径
tokenizer_path = "path/to/tokenizer.model"
# 特殊令牌,可以根据需要添加更多的特殊令牌
special_tokens = ["<|startoftext|>", "<|endoftext|>"]
# 加载分词器模型
mergeable_ranks = tiktoken.load(tokenizer_path)
# 初始化分词器
tokenizer = tiktoken.Encoding(
name=Path(tokenizer_path).name,
mergeable_ranks=mergeable_ranks,
special_tokens={token: len(mergeable_ranks) + i for i, token in enumerate(special_tokens)},
)
# 测试分词器
encoded = tokenizer.encode("hello world!")
decoded = tokenizer.decode(encoded)
print("Encoded:", encoded)
print("Decoded:", decoded)
代码解析
- 安装和导入库:首先确保安装了 tiktoken 库,并导入需要的模块。
- 指定路径:设置分词器模型的路径为
tokenizer_path
。 - 定义特殊令牌:定义一些特殊令牌,如文本开始和结束标记。
- 加载模型:使用
tiktoken.load
方法加载分词器模型。 - 初始化分词器:通过
tiktoken.Encoding
初始化分词器,将模型名称、可合并的词汇表(mergeable_ranks)和特殊令牌传递给它。 - 测试分词器:使用
encode
方法将文本转换为 token,并使用decode
方法将 token 转换回文本,以验证分词器的工作是否正常。
3.2 读取模型文件
从模型文件中读取参数和权重是构建 Llama 3.1 模型的关键步骤之一。这部分将详细介绍如何读取模型文件中的参数和权重,并进行初始化设置。
安装依赖库
确保安装了 PyTorch 库,因为我们将使用 PyTorch 来加载模型文件。
pip install torch
读取模型文件
首先,我们需要从模型文件中加载模型权重和配置参数。假设我们已经下载了模型权重文件 model.pth
和配置文件 params.json
。
import torch
import json
# 加载模型权重
model_path = "path/to/model.pth"
model = torch.load(model_path)
# 打印模型的部分键以验证加载
print(json.dumps(list(model.keys())[:20], indent=4))
# 加载配置参数
config_path = "path/to/params.json"
with open(config_path, "r") as f:
config = json.load(f)
# 提取配置参数
dim = config["dim"]
n_layers = config["n_layers"]
n_heads = config["n_heads"]
n_kv_heads = config["n_kv_heads"]
vocab_size = config["vocab_size"]
multiple_of = config["multiple_of"]
ffn_dim_multiplier = config["ffn_dim_multiplier"]
norm_eps = config["norm_eps"]
rope_theta = torch.tensor(config["rope_theta"])
# 打印配置参数以验证加载
print(f"Model Dimension: {dim}")
print(f"Number of Layers: {n_layers}")
print(f"Number of Heads: {n_heads}")
print(f"Vocabulary Size: {vocab_size}")
代码解析
- 安装和导入库:首先,确保安装了 PyTorch,并导入必要的库。
- 加载模型权重:使用
torch.load
方法加载模型文件model.pth
。加载后,可以通过打印模型的键来验证权重是否正确加载。 - 加载配置文件:打开并读取配置文件
params.json
,该文件包含了模型的超参数和其他设置。 - 提取配置参数:从配置文件中提取必要的参数,如模型维度、层数、头数、词汇表大小等。这些参数将在后续步骤中用于初始化模型结构。
- 打印配置参数:为了确保配置参数正确加载,打印一些关键参数进行验证。
3.3 文本到Token的转换
在构建语言模型时,首先需要将输入的自然语言文本转换为模型所需的 tokens。Llama 3.1 使用的分词器可以将文本转换为一系列 tokens,这些 tokens 是模型可以理解和处理的基本单元。
转换步骤
以下是将输入文本转换为模型所需 tokens 的详细步骤:
- 定义输入文本:首先,我们需要定义要转换的输入文本。
- 使用分词器进行编码:使用已经加载和初始化的分词器将输入文本转换为 tokens。
- 添加特殊 tokens:在模型的要求下,有时需要在 tokens 序列的开头或结尾添加特殊 tokens。
- 将 tokens 转换为张量:为了便于后续的处理,将 tokens 序列转换为 PyTorch 张量。
示例代码
以下是具体的代码实现:
# 定义输入文本
prompt = "the answer to the ultimate question of life, the universe, and everything is "
# 使用分词器将文本编码为 tokens
tokens = tokenizer.encode(prompt)
# 打印编码后的 tokens
print(f"Encoded tokens: {tokens}")
# 添加特殊 tokens
# 假设 128000 是模型要求的特殊 token
tokens = [128000] + tokens
# 将 tokens 转换为 PyTorch 张量
tokens_tensor = torch.tensor(tokens)
# 打印转换后的张量
print(f"Tokens Tensor: {tokens_tensor}")
代码解析
- 定义输入文本:
prompt
是我们要转换的输入文本。 - 使用分词器进行编码:
tokenizer.encode(prompt)
方法将输入文本转换为 tokens。tokenizer
是之前加载并初始化的分词器对象。 - 打印编码后的 tokens:打印编码后的 tokens 序列以验证分词器的输出。
- 添加特殊 tokens:根据模型的要求,可能需要在 tokens 序列的开头添加特殊 tokens。在此示例中,我们假设 128000 是一个特殊 token,并将其添加到 tokens 序列的开头。
- 将 tokens 转换为 PyTorch 张量:使用
torch.tensor(tokens)
方法将 tokens 序列转换为 PyTorch 张量。这一步是为了便于后续的模型处理,因为 PyTorch 模型通常处理张量数据。 - 打印转换后的张量:打印转换后的张量以验证最终的 tokens 张量。
3.4 Token到Embedding的转换
将 tokens 转换为 embeddings 是深度学习模型处理文本数据的重要步骤。Embedding 是一种将离散的 tokens 转换为连续向量表示的方法,使模型能够处理和理解文本数据。
依赖库
确保您已经安装并导入了必要的库:
import torch
import torch.nn as nn
步骤详解
- 初始化 Embedding 层:使用模型的词汇表大小和向量维度初始化一个 Embedding 层。
- 加载预训练的 Embedding 权重:将模型中预训练的权重加载到 Embedding 层。
- 转换 Tokens:将 tokens 转换为 embeddings。
- 处理数据类型:将 embeddings 转换为适当的数据类型,以适应模型的需求。
示例代码
以下是将 tokens 转换为 embeddings 的具体代码实现:
# 定义模型参数
vocab_size = 50257 # 假设词汇表大小为 50257
dim = 768 # 假设每个 token 的 embedding 维度为 768
# 初始化 Embedding 层
embedding_layer = nn.Embedding(vocab_size, dim)
# 加载预训练的 Embedding 权重
embedding_layer.weight.data.copy_(model["tok_embeddings.weight"])
# 将 tokens 转换为 embeddings
token_embeddings_unnormalized = embedding_layer(tokens_tensor).to(torch.bfloat16)
# 打印 Embeddings 的形状
print(f"Token Embeddings Shape: {token_embeddings_unnormalized.shape}")
代码解析
- 定义模型参数:首先,定义模型的词汇表大小 (
vocab_size
) 和 embedding 向量的维度 (dim
)。 - 初始化 Embedding 层:使用
nn.Embedding
初始化一个 Embedding 层。这个层将词汇表中的每个 token 映射到一个维度为dim
的向量。 - 加载预训练的 Embedding 权重:从模型中加载预训练的 embedding 权重。
model["tok_embeddings.weight"]
包含了预训练的 embedding 权重,通过embedding_layer.weight.data.copy_
方法将这些权重复制到初始化的 Embedding 层中。 - 将 tokens 转换为 embeddings:使用
embedding_layer
将 tokens 转换为 embeddings。tokens_tensor
是之前步骤中转换的 tokens 张量。.to(torch.bfloat16)
将 embeddings 转换为bfloat16
数据类型,以适应后续的模型处理需求。 - 打印 Embeddings 的形状:通过
print
打印 embeddings 的形状,以验证转换是否正确。输出的形状应该是(序列长度, embedding 维度)
。
3.5 归一化Embedding
对 embeddings 进行归一化处理是确保模型稳定性和性能的重要步骤。归一化有助于避免数值不稳定,确保各层之间的数值范围一致,从而提升训练效率和模型性能。
归一化的原理
在本教程中,我们使用均方根归一化(RMS Normalization)来处理 embeddings。均方根归一化是一种对向量进行归一化的方法,它通过计算向量的均方根(RMS)并使用该值对向量进行缩放。
均方根的计算方法如下:
归一化后的向量为:
在实现过程中,我们还需要使用归一化权重(norm_weights)来进行缩放。
示例代码
以下是对 embeddings 进行归一化处理的具体实现步骤:
import torch
# 定义均方根归一化函数
def rms_norm(tensor, norm_weights, norm_eps=1e-6):
rms = torch.rsqrt(tensor.pow(2).mean(-1, keepdim=True) + norm_eps)
return tensor * rms * norm_weights
# 模型参数示例
norm_weights = model["layers.0.attention_norm.weight"]
norm_eps = 1e-6
# 对 token_embeddings_unnormalized 进行归一化处理
token_embeddings = rms_norm(token_embeddings_unnormalized, norm_weights)
# 打印归一化后的 embeddings 形状
print(f"Normalized Token Embeddings Shape: {token_embeddings.shape}")
代码解析
- 定义均方根归一化函数:
rms_norm
函数接受三个参数:待归一化的张量tensor
、归一化权重norm_weights
和一个小的常数norm_eps
以防止除零错误。该函数计算张量的均方根,并使用归一化权重对张量进行缩放。 - 加载模型参数:从模型中提取归一化权重
norm_weights
,这是在预训练时学习到的参数。 - 归一化处理:使用
rms_norm
函数对token_embeddings_unnormalized
进行归一化处理。将未经归一化的 embeddings 和归一化权重传递给函数,得到归一化后的 embeddings。 - 打印归一化后的 embeddings 形状:通过
print
打印归一化后的 embeddings 形状,以验证处理是否正确。输出的形状应与输入的张量形状一致。
3.6 构建Transformer层
在构建 Transformer 层时,我们需要实现自注意力机制(Self-Attention)。在此步骤中,我们将实现 Query、Key 和 Value 的计算,并进行自注意力计算。
初始化 Transformer 层
Transformer 层由多个子层组成,其中最重要的是自注意力机制和前馈神经网络。在实现自注意力机制时,我们需要计算 Query(查询向量)、Key(键向量)和 Value(值向量)。
实现 Query
首先,我们实现 Query 的计算。Query 的计算过程如下:
- 提取权重:从预训练模型中提取 Query 的权重矩阵。
- 调整权重矩阵的形状:将权重矩阵调整为适当的形状,以便进行矩阵乘法运算。
- 计算 Query 向量:将输入的 token embeddings 与 Query 的权重矩阵相乘,得到 Query 向量。
示例代码
以下是实现 Query 的详细步骤和代码:
import torch
# 假设以下是模型配置参数
n_heads = 12 # 假设模型有 12 个头
dim = 768 # 假设每个 token 的 embedding 维度为 768
# 从模型中提取 Query 的权重矩阵
q_layer0 = model["layers.0.attention.wq.weight"]
# 计算每个头的维度
head_dim = q_layer0.shape[0] // n_heads
# 调整权重矩阵的形状以便进行矩阵乘法运算
# 形状调整为 (n_heads, head_dim, dim)
q_layer0 = q_layer0.view(n_heads, head_dim, dim)
# 将 token embeddings 与 Query 的权重矩阵相乘,得到 Query 向量
# 假设 token_embeddings 是归一化后的 embeddings
q_per_token = torch.matmul(token_embeddings, q_layer0[0].T)
# 打印 Query 向量的形状
print(f"Query per Token Shape: {q_per_token.shape}")
代码解析
- 导入库:确保已经导入 PyTorch 库。
- 模型配置参数:定义模型的配置参数,如头数 (
n_heads
) 和 embedding 维度 (dim
)。 - 提取 Query 的权重矩阵:从预训练模型中提取 Query 的权重矩阵。
model["layers.0.attention.wq.weight"]
包含了第一个层的 Query 权重。 - 计算每个头的维度:通过将总的维度除以头数,得到每个头的维度 (
head_dim
)。 - 调整权重矩阵的形状:使用
view
方法将权重矩阵调整为形状(n_heads, head_dim, dim)
,以便进行矩阵乘法运算。 - 计算 Query 向量:使用
torch.matmul
方法将输入的 token embeddings 与调整后的 Query 权重矩阵相乘,得到每个 token 的 Query 向量。 - 打印 Query 向量的形状:通过
print
打印 Query 向量的形状,以验证计算是否正确。输出的形状应为(序列长度, head_dim)
。
3.7 位置编码(RoPE)
位置编码(Rotary Position Embedding, RoPE)是一种用于 Transformer 模型的位置编码方法,可以增强模型对位置信息的理解。RoPE 通过将位置信息嵌入到 Query 和 Key 向量中,使得模型能够更好地处理序列数据。
位置编码的实现步骤
- 将 Query 向量拆分为实数对:首先,将 Query 向量拆分为实数对,以便进行复数表示和旋转。
- 计算频率矩阵:计算频率矩阵,其中包含了不同位置的旋转频率。
- 将实数对转换为复数表示:使用复数表示,将实数对表示的 Query 向量转换为复数表示。
- 进行旋转:将复数表示的 Query 向量与频率矩阵进行旋转计算。
- 将复数表示转换回实数对:将旋转后的复数表示转换回实数对表示。
- 恢复原始形状:将实数对表示恢复为原始的 Query 向量形状。
示例代码
以下是实现位置编码的详细步骤和代码:
import torch
# 假设以下是模型配置参数
rope_theta = torch.tensor([10000.0]) # 假设使用的频率基数
n_heads = 12 # 假设模型有 12 个头
dim = 768 # 假设每个 token 的 embedding 维度为 768
head_dim = dim // n_heads
# 将 Query 向量拆分为实数对
q_per_token_split_into_pairs = q_per_token.float().view(q_per_token.shape[0], -1, 2)
# 计算频率矩阵
zero_to_one_split_into_64_parts = torch.arange(64) / 64
freqs = 1.0 / (rope_theta ** zero_to_one_split_into_64_parts)
freqs_for_each_token = torch.outer(torch.arange(17), freqs)
freqs_cis = torch.polar(torch.ones_like(freqs_for_each_token), freqs_for_each_token)
# 将实数对转换为复数表示
q_per_token_as_complex_numbers = torch.view_as_complex(q_per_token_split_into_pairs)
# 进行旋转
q_per_token_as_complex_numbers_rotated = q_per_token_as_complex_numbers * freqs_cis
# 将复数表示转换回实数对
q_per_token_split_into_pairs_rotated = torch.view_as_real(q_per_token_as_complex_numbers_rotated)
# 恢复原始形状
q_per_token_rotated = q_per_token_split_into_pairs_rotated.view(q_per_token.shape)
# 打印旋转后的 Query 向量形状
print(f"Rotated Query per Token Shape: {q_per_token_rotated.shape}")
代码解析
- 导入库:确保已经导入 PyTorch 库。
- 模型配置参数:定义模型的配置参数,如频率基数 (
rope_theta
)、头数 (n_heads
) 和 embedding 维度 (dim
)。 - 将 Query 向量拆分为实数对:使用
view
方法将 Query 向量拆分为实数对表示,以便后续进行复数表示和旋转。 - 计算频率矩阵:生成频率矩阵,其中包含了不同位置的旋转频率。
torch.arange(64) / 64
生成一个从 0 到 1 的序列,1.0 / (rope_theta ** zero_to_one_split_into_64_parts)
计算相应的频率。 - 将实数对转换为复数表示:使用
torch.view_as_complex
方法将实数对表示的 Query 向量转换为复数表示。 - 进行旋转:将复数表示的 Query 向量与频率矩阵进行旋转计算。使用复数相乘来进行旋转。
- 将复数表示转换回实数对:使用
torch.view_as_real
方法将旋转后的复数表示转换回实数对表示。 - 恢复原始形状:将实数对表示恢复为原始的 Query 向量形状,便于后续的模型处理。
- 打印旋转后的 Query 向量形状:通过
print
打印旋转后的 Query 向量形状,以验证计算是否正确。输出的形状应与输入的张量形状一致。
3.8 Key的实现
在实现 Transformer 层时,除了计算 Query(查询向量),我们还需要计算 Key(键向量)。Key 的计算过程类似于 Query 的计算,并且同样需要进行位置编码(RoPE)的处理。
Key的计算步骤
- 提取权重:从预训练模型中提取 Key 的权重矩阵。
- 调整权重矩阵的形状:将权重矩阵调整为适当的形状,以便进行矩阵乘法运算。
- 计算 Key 向量:将输入的 token embeddings 与 Key 的权重矩阵相乘,得到 Key 向量。
- 进行位置编码处理:对 Key 向量进行位置编码(RoPE)处理。
示例代码
以下是实现 Key 的详细步骤和代码:
import torch
# 假设以下是模型配置参数
n_kv_heads = 8 # 假设模型有 8 个键值头
dim = 768 # 假设每个 token 的 embedding 维度为 768
head_dim = dim // n_kv_heads
# 从模型中提取 Key 的权重矩阵
k_layer0 = model["layers.0.attention.wk.weight"]
# 调整权重矩阵的形状以便进行矩阵乘法运算
# 形状调整为 (n_kv_heads, head_dim, dim)
k_layer0 = k_layer0.view(n_kv_heads, k_layer0.shape[0] // n_kv_heads, dim)
# 将 token embeddings 与 Key 的权重矩阵相乘,得到 Key 向量
k_per_token = torch.matmul(token_embeddings, k_layer0[0].T)
# 将 Key 向量拆分为实数对
k_per_token_split_into_pairs = k_per_token.float().view(k_per_token.shape[0], -1, 2)
# 将实数对转换为复数表示
k_per_token_as_complex_numbers = torch.view_as_complex(k_per_token_split_into_pairs)
# 进行旋转
k_per_token_as_complex_numbers_rotated = k_per_token_as_complex_numbers * freqs_cis
# 将复数表示转换回实数对
k_per_token_split_into_pairs_rotated = torch.view_as_real(k_per_token_as_complex_numbers_rotated)
# 恢复原始形状
k_per_token_rotated = k_per_token_split_into_pairs_rotated.view(k_per_token.shape)
# 打印旋转后的 Key 向量形状
print(f"Rotated Key per Token Shape: {k_per_token_rotated.shape}")
代码解析
- 导入库:确保已经导入 PyTorch 库。
- 模型配置参数:定义模型的配置参数,如键值头数 (
n_kv_heads
) 和 embedding 维度 (dim
)。 - 提取 Key 的权重矩阵:从预训练模型中提取 Key 的权重矩阵。
model["layers.0.attention.wk.weight"]
包含了第一个层的 Key 权重。 - 调整权重矩阵的形状:使用
view
方法将权重矩阵调整为形状(n_kv_heads, head_dim, dim)
,以便进行矩阵乘法运算。 - 计算 Key 向量:使用
torch.matmul
方法将输入的 token embeddings 与调整后的 Key 权重矩阵相乘,得到每个 token 的 Key 向量。 - 将 Key 向量拆分为实数对:使用
view
方法将 Key 向量拆分为实数对表示,以便后续进行复数表示和旋转。 - 将实数对转换为复数表示:使用
torch.view_as_complex
方法将实数对表示的 Key 向量转换为复数表示。 - 进行旋转:将复数表示的 Key 向量与频率矩阵进行旋转计算。使用复数相乘来进行旋转。
- 将复数表示转换回实数对:使用
torch.view_as_real
方法将旋转后的复数表示转换回实数对表示。 - 恢复原始形状:将实数对表示恢复为原始的 Key 向量形状,便于后续的模型处理。
- 打印旋转后的 Key 向量形状:通过
print
打印旋转后的 Key 向量形状,以验证计算是否正确。输出的形状应与输入的张量形状一致。
3.9 计算Self-Attention
在 Transformer 模型中,自注意力机制(Self-Attention)是核心组件之一。自注意力机制通过计算 Query、Key 和 Value 之间的相关性来捕捉输入序列中各个元素之间的依赖关系。自注意力机制的实现步骤如下:
- 计算 Query 和 Key 的点积:首先计算 Query 和 Key 的点积,得到每个 token 与其他 token 之间的相关性分数。
- 缩放分数:将分数除以
(头维度的平方根),以避免点积值过大。
- 应用 Softmax 函数:对缩放后的分数应用 Softmax 函数,将其转换为概率分布。
- 计算 Attention 输出:将概率分布与 Value 相乘,得到注意力机制的输出。
示例代码
以下是计算 Self-Attention 的详细步骤和代码:
import torch
# 假设以下是模型配置参数
head_dim = 64 # 假设每个头的维度为 64
# 计算 Query 和 Key 的点积,并进行缩放
qk_per_token = torch.matmul(q_per_token_rotated, k_per_token_rotated.transpose(-2, -1)) / (head_dim ** 0.5)
# 打印 Query 和 Key 点积的形状
print(f"QK per Token Shape: {qk_per_token.shape}")
# 对点积结果应用 Softmax 函数,得到注意力分数
attention_scores = torch.nn.functional.softmax(qk_per_token, dim=-1)
# 打印注意力分数的形状
print(f"Attention Scores Shape: {attention_scores.shape}")
# 从模型中提取 Value 的权重矩阵
v_layer0 = model["layers.0.attention.wv.weight"]
v_layer0 = v_layer0.view(n_kv_heads, v_layer0.shape[0] // n_kv_heads, dim)
# 计算 Value 向量
v_per_token = torch.matmul(token_embeddings, v_layer0[0].T)
# 将注意力分数与 Value 相乘,得到注意力输出
attention_output = torch.matmul(attention_scores, v_per_token)
# 打印注意力输出的形状
print(f"Attention Output Shape: {attention_output.shape}")
代码解析
- 计算 Query 和 Key 的点积并进行缩放:
qk_per_token = torch.matmul(q_per_token_rotated, k_per_token_rotated.transpose(-2, -1)) / (head_dim ** 0.5)
计算每个 Query 向量和 Key 向量的点积,并将结果除以头维度的平方根进行缩放。这一步骤有助于稳定梯度。
- 打印 Query 和 Key 点积的形状:
print(f"QK per Token Shape: {qk_per_token.shape}")
打印点积结果的形状,以验证计算是否正确。输出形状应为 (序列长度, 序列长度)
。
- 应用 Softmax 函数:
attention_scores = torch.nn.functional.softmax(qk_per_token, dim=-1)
对缩放后的点积结果应用 Softmax 函数,将其转换为概率分布。这样,每个 token 对其他 token 的注意力分数之和为 1。
- 打印注意力分数的形状:
print(f"Attention Scores Shape: {attention_scores.shape}")
打印注意力分数的形状,以验证计算是否正确。输出形状应为 (序列长度, 序列长度)
。
- 提取 Value 的权重矩阵:
v_layer0 = model["layers.0.attention.wv.weight"]
v_layer0 = v_layer0.view(n_kv_heads, v_layer0.shape[0] // n_kv_heads, dim)
从预训练模型中提取 Value 的权重矩阵,并调整其形状以便进行矩阵乘法运算。
- 计算 Value 向量:
v_per_token = torch.matmul(token_embeddings, v_layer0[0].T)
使用调整后的 Value 权重矩阵计算每个 token 的 Value 向量。
- 计算注意力输出:
attention_output = torch.matmul(attention_scores, v_per_token)
将注意力分数与 Value 向量相乘,得到注意力机制的输出。
- 打印注意力输出的形状:
print(f"Attention Output Shape: {attention_output.shape}")
打印注意力输出的形状,以验证计算是否正确。输出形状应与 token_embeddings 的形状一致。
通过上述步骤,您已经成功实现了 Self-Attention 计算。接下来,您可以继续进行 Query Key Scores Masking 和其他实现步骤。
3.10 Query Key Scores Masking
在 Transformer 模型中,Query Key Scores Masking 是确保模型在进行自注意力计算时,只考虑当前 token 及其之前的 token,而忽略未来 token 的重要步骤。尤其在语言建模任务中,这种操作至关重要,以避免信息泄露问题,即模型在生成某个 token 时,不应看到该 token 之后的内容。
Masking 的实现步骤
- 创建掩码矩阵:首先,我们创建一个掩码矩阵,其形状与 Query Key Scores 矩阵相同。这个掩码矩阵用于屏蔽未来的 token。
- 应用上三角矩阵:使用上三角矩阵(triu)函数,将对角线以上的位置填充为负无穷大(-inf),表示这些位置的值在计算时被忽略。
- 将掩码应用到 Query Key Scores 矩阵:将掩码添加到 Query Key Scores 矩阵中,确保模型在计算自注意力时只关注合法的位置。
示例代码
以下是实现 Query Key Scores Masking 的详细步骤和代码:
import torch
# 假设以下是模型配置参数
seq_len = len(tokens) # 序列长度
# 创建掩码矩阵
mask = torch.full((seq_len, seq_len), float("-inf"), device=tokens.device)
# 应用上三角矩阵,将对角线以上的位置填充为负无穷大(-inf)
mask = torch.triu(mask, diagonal=1)
# 将掩码应用到 Query Key Scores 矩阵
qk_per_token += mask
# 打印添加掩码后的 Query Key Scores 矩阵
print(f"Masked QK per Token:\n{qk_per_token}")
代码解析
- 导入库:确保已经导入 PyTorch 库。
- 定义序列长度:
seq_len = len(tokens)
序列长度等于 token 的数量。
- 创建掩码矩阵:
mask = torch.full((seq_len, seq_len), float("-inf"), device=tokens.device)
使用 torch.full
函数创建一个形状为 (seq_len, seq_len)
的矩阵,并填充为负无穷大(-inf)。
- 应用上三角矩阵:
mask = torch.triu(mask, diagonal=1)
使用 torch.triu
函数将对角线以上的位置填充为负无穷大(-inf),即上三角矩阵。diagonal=1
参数表示从对角线右侧的第一个元素开始。
- 将掩码应用到 Query Key Scores 矩阵:
qk_per_token += mask
将掩码矩阵添加到 Query Key Scores 矩阵中,确保在自注意力计算时,模型只考虑当前 token 及其之前的 token。
- 打印添加掩码后的 Query Key Scores 矩阵:
print(f"Masked QK per Token:\n{qk_per_token}")
通过 print
打印添加掩码后的 Query Key Scores 矩阵,以验证计算是否正确。输出应显示对角线以上的位置被填充为负无穷大。
4. 微调模型
为了使预训练的 Llama 3.1 模型在特定任务上表现更好,可以对其进行微调。微调过程包括准备数据集、定义损失函数和优化器、训练循环和保存模型。以下是详细的步骤和代码示例:
4.1 准备数据集
为了微调 Llama 3.1 模型,首先需要准备一个特定任务的数据集。可以选择一个公开数据集,如 Kaggle 上的某个数据集,或者使用您自己的数据集。数据集应包括输入数据和相应的标签。以下是详细的步骤和代码示例,展示了如何准备数据集并创建数据加载器。
数据集的选择与下载
选择一个合适的公开数据集,如分类任务中的 IMDb 电影评论数据集,或是情感分析任务中的 Twitter 数据集。如果您选择了 Kaggle 上的数据集,可以使用 Kaggle API 进行下载:
# 安装 Kaggle API 工具
pip install kaggle
# 配置 Kaggle API 密钥
mkdir ~/.kaggle
mv kaggle.json ~/.kaggle/
chmod 600 ~/.kaggle/kaggle.json
# 下载数据集(以 IMDb 数据集为例)
kaggle datasets download -d lakshmi25npathi/imdb-dataset-of-50k-movie-reviews
unzip imdb-dataset-of-50k-movie-reviews.zip
自定义 Dataset 类
接下来,我们需要定义一个自定义的 Dataset
类,用于加载和处理数据集。该类将实现 PyTorch 的 Dataset
接口,并负责将文本数据转换为模型可以处理的 tokens。
import torch
from torch.utils.data import DataLoader, Dataset
class CustomDataset(Dataset):
def __init__(self, texts, labels, tokenizer):
self.texts = texts
self.labels = labels
self.tokenizer = tokenizer
def __len__(self):
return len(self.texts)
def __getitem__(self, idx):
text = self.texts[idx]
label = self.labels[idx]
tokens = self.tokenizer.encode(text)
return torch.tensor(tokens), torch.tensor(label)
示例数据集
为了演示,我们可以使用一个简单的示例数据集。实际使用中,应替换为您的具体数据集。
# 示例数据集
texts = ["example sentence 1", "example sentence 2", "example sentence 3"]
labels = [0, 1, 0] # 示例标签,0 和 1 表示不同的类别
创建数据加载器
使用自定义的 CustomDataset
类,我们可以创建一个数据加载器,用于在训练过程中批量加载数据。数据加载器可以有效地处理数据的读取和预处理,并将其打包为小批量数据。
# 初始化自定义数据集
dataset = CustomDataset(texts, labels, tokenizer)
# 创建数据加载器
dataloader = DataLoader(dataset, batch_size=2, shuffle=True)
完整示例代码
以下是上述步骤的完整示例代码,展示了如何准备数据集、定义自定义 Dataset
类,并创建数据加载器:
import torch
from torch.utils.data import DataLoader, Dataset
class CustomDataset(Dataset):
def __init__(self, texts, labels, tokenizer):
self.texts = texts
self.labels = labels
self.tokenizer = tokenizer
def __len__(self):
return len(self.texts)
def __getitem__(self, idx):
text = self.texts[idx]
label = self.labels[idx]
tokens = self.tokenizer.encode(text)
return torch.tensor(tokens), torch.tensor(label)
# 示例数据集
texts = ["example sentence 1", "example sentence 2", "example sentence 3"]
labels = [0, 1, 0]
# 初始化自定义数据集
dataset = CustomDataset(texts, labels, tokenizer)
# 创建数据加载器
dataloader = DataLoader(dataset, batch_size=2, shuffle=True)
# 检查数据加载器
for batch in dataloader:
tokens, labels = batch
print(f"Tokens: {tokens}, Labels: {labels}")
通过上述步骤,您已经成功准备了用于微调的特定任务数据集。接下来,您可以继续进行模型的微调,包括定义损失函数和优化器、训练循环和保存模型。
4.2 定义损失函数和优化器
在微调过程中,选择合适的损失函数和优化器对于模型的训练效果至关重要。损失函数用于衡量模型预测与真实标签之间的差距,而优化器则负责更新模型参数以最小化损失函数的值。在这里,我们将使用交叉熵损失函数和 Adam 优化器。
定义损失函数
交叉熵损失函数(Cross-Entropy Loss)常用于分类任务中,它能够有效衡量模型预测的概率分布与真实分布之间的差异。在 PyTorch 中,可以通过 torch.nn.CrossEntropyLoss
定义交叉熵损失函数。
import torch
# 定义损失函数
criterion = torch.nn.CrossEntropyLoss()
交叉熵损失函数的公式如下:
其中是真实标签,
是模型预测的概率。
定义优化器
Adam 优化器(Adaptive Moment Estimation)结合了动量和自适应学习率调整的优点,在许多深度学习任务中表现良好。它通过计算一阶和二阶矩的偏差校正来动态调整学习率。我们可以通过 torch.optim.Adam
定义 Adam 优化器,并设置适当的学习率。
# 定义优化器
optimizer = torch.optim.Adam(model.parameters(), lr=1e-5)
Adam 优化器的更新公式如下:
其中和
是动量参数,
是梯度,
是学习率。
完整示例代码
以下是定义损失函数和优化器的完整示例代码:
import torch
# 定义损失函数
criterion = torch.nn.CrossEntropyLoss()
# 定义优化器
optimizer = torch.optim.Adam(model.parameters(), lr=1e-5)
# 打印损失函数和优化器的定义
print("损失函数定义为:", criterion)
print("优化器定义为:", optimizer)
优化器参数调整
在实际训练过程中,可以根据模型的表现动态调整优化器的参数。例如,可以在训练初期使用较大的学习率,以加速收敛;在训练后期逐步降低学习率,以提高模型的精度。以下是一个动态调整学习率的示例:
from torch.optim.lr_scheduler import StepLR
# 定义学习率调度器,每 10 个 epoch 将学习率减少一半
scheduler = StepLR(optimizer, step_size=10, gamma=0.5)
# 在训练循环中调用学习率调度器
for epoch in range(num_epochs):
for inputs, labels in dataloader:
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
scheduler.step()
print(f"Epoch {epoch + 1}/{num_epochs}, Loss: {loss.item()}, Learning Rate: {scheduler.get_last_lr()}")
通过上述步骤,您已经成功定义了适用于微调 Llama 3.1 模型的损失函数和优化器。接下来,您可以进入训练循环部分,开始实际的模型训练。
4.3 训练循环
训练循环是微调 Llama 3.1 模型的核心步骤。它包括多个 epoch,每个 epoch 中,我们遍历整个数据集,计算损失并更新模型参数。训练循环的主要目的是通过反复迭代优化模型参数,使模型在特定任务上表现更佳。
设置训练参数
首先,定义训练的基本参数,如训练的 epoch 数量和设备(CPU 或 GPU)。
import torch
# 设置训练参数
num_epochs = 3
# 检查是否有 GPU 可用
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")
# 将模型移到设备上
model.to(device)
训练循环的实现
训练循环包括以下几个步骤:
- 遍历数据集:使用数据加载器(DataLoader)遍历数据集中的所有样本。
- 将数据移到设备上:将输入数据和标签移到指定的设备(CPU 或 GPU)。
- 清零梯度:在每次迭代之前清零优化器的梯度。
- 前向传播:将输入数据传递给模型,计算输出。
- 计算损失:使用定义的损失函数计算预测输出与真实标签之间的损失。
- 反向传播:计算梯度,通过反向传播算法更新模型参数。
- 更新参数:使用优化器更新模型参数。
- 输出当前损失:在每个 epoch 结束后输出当前的损失值,以监控训练进展。
以下是训练循环的详细代码实现:
# 训练循环
for epoch in range(num_epochs):
model.train() # 设置模型为训练模式
running_loss = 0.0 # 初始化累计损失
for inputs, labels in dataloader:
# 将输入和标签数据移到设备上
inputs, labels = inputs.to(device), labels.to(device)
# 清零梯度
optimizer.zero_grad()
# 前向传播
outputs = model(inputs)
# 计算损失
loss = criterion(outputs, labels)
# 反向传播
loss.backward()
# 更新参数
optimizer.step()
# 累计损失
running_loss += loss.item()
# 计算并输出当前 epoch 的平均损失值
average_loss = running_loss / len(dataloader)
print(f"Epoch {epoch + 1}/{num_epochs}, Loss: {average_loss:.4f}")
验证模型性能
在训练过程中,为了防止过拟合,通常还需要在每个 epoch 结束后使用验证集评估模型性能。以下是验证步骤的代码示例:
# 训练循环
for epoch in range(num_epochs):
model.train() # 设置模型为训练模式
running_loss = 0.0 # 初始化累计损失
for inputs, labels in dataloader:
# 将输入和标签数据移到设备上
inputs, labels = inputs.to(device), labels.to(device)
# 清零梯度
optimizer.zero_grad()
# 前向传播
outputs = model(inputs)
# 计算损失
loss = criterion(outputs, labels)
# 反向传播
loss.backward()
# 更新参数
optimizer.step()
# 累计损失
running_loss += loss.item()
# 计算并输出当前 epoch 的平均损失值
average_loss = running_loss / len(dataloader)
print(f"Epoch {epoch + 1}/{num_epochs}, Loss: {average_loss:.4f}")
# 验证模型
model.eval() # 设置模型为评估模式
val_loss = 0.0
with torch.no_grad():
for val_inputs, val_labels in val_dataloader:
val_inputs, val_labels = val_inputs.to(device), val_labels.to(device)
val_outputs = model(val_inputs)
val_loss += criterion(val_outputs, val_labels).item()
val_loss /= len(val_dataloader)
print(f"Validation Loss after epoch {epoch + 1}: {val_loss:.4f}")
代码解析
- 设置模型为训练模式:
model.train()
在训练循环的开始和结束分别设置模型为训练模式和评估模式。
- 初始化累计损失:
running_loss = 0.0
在每个 epoch 开始时初始化累计损失。
- 遍历数据集:
for inputs, labels in dataloader:
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
逐批次处理数据,进行前向传播、计算损失、反向传播和参数更新。
- 计算并输出平均损失:
average_loss = running_loss / len(dataloader)
print(f"Epoch {epoch + 1}/{num_epochs}, Loss: {average_loss:.4f}")
在每个 epoch 结束时计算并输出平均损失。
- 验证模型性能:
model.eval()
val_loss = 0.0
with torch.no_grad():
for val_inputs, val_labels in val_dataloader:
val_inputs, val_labels = val_inputs.to(device), val_labels.to(device)
val_outputs = model(val_inputs)
val_loss += criterion(val_outputs, val_labels).item()
val_loss /= len(val_dataloader)
print(f"Validation Loss after epoch {epoch + 1}: {val_loss:.4f}")
在每个 epoch 结束后使用验证集评估模型性能,避免过拟合。
通过上述步骤,您已经成功完成了 Llama 3.1 模型的训练循环。接下来,您可以继续进行模型的保存和部署。
4.4 保存模型
微调完成后,可以将模型的状态字典保存到文件中,以便以后加载和使用。这一步非常重要,因为它确保了您的工作不会丢失,并且可以在未来重新加载并继续训练或进行推理。以下是详细的步骤和示例代码。
保存模型状态字典
在训练完成后,我们需要将模型的权重和参数保存到一个文件中。PyTorch 提供了简单的方法来实现这一点,使用 torch.save
函数即可将模型的状态字典保存到指定路径。
# 保存模型状态字典
torch.save(model.state_dict(), 'llama3.1-finetuned.pth')
print("模型已保存到 'llama3.1-finetuned.pth'")
上述代码将模型的状态字典保存为 llama3.1-finetuned.pth
文件。这使得我们可以在以后重新加载模型而无需重新训练。
加载保存的模型
为了验证模型保存的正确性,或在未来使用已保存的模型,我们需要能够重新加载模型状态。以下是加载已保存模型的示例代码:
# 定义模型架构(需与保存时的模型架构一致)
model = YourModelClass(*args, **kwargs)
# 加载模型状态字典
model.load_state_dict(torch.load('llama3.1-finetuned.pth'))
model.to(device)
print("模型已成功加载并移动到设备")
确保加载时的模型架构与保存时的模型架构一致,否则会引发错误。
保存和加载优化器状态
在有些情况下,您可能希望在重新加载模型时继续训练。这时除了保存模型状态外,还需要保存优化器的状态。以下是保存和加载优化器状态的代码示例:
# 保存模型和优化器状态字典
torch.save({
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
}, 'llama3.1-finetuned-checkpoint.pth')
print("模型和优化器状态已保存到 'llama3.1-finetuned-checkpoint.pth'")
# 加载模型和优化器状态字典
checkpoint = torch.load('llama3.1-finetuned-checkpoint.pth')
model.load_state_dict(checkpoint['model_state_dict'])
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
model.to(device)
print("模型和优化器状态已成功加载并移动到设备")
这种方法确保您可以完全恢复训练状态,继续之前中断的训练过程。
自动保存和加载
在实际项目中,我们通常会设置自动保存模型的功能,以防止因意外中断而丢失训练进度。以下是自动保存和加载的示例:
import os
# 设置保存路径和条件
save_path = 'llama3.1-finetuned.pth'
best_loss = float('inf')
for epoch in range(num_epochs):
running_loss = 0.0
model.train()
for inputs, labels in dataloader:
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
average_loss = running_loss / len(dataloader)
print(f"Epoch {epoch + 1}/{num_epochs}, Loss: {average_loss:.4f}")
# 保存表现最好的模型
if average_loss < best_loss:
best_loss = average_loss
torch.save(model.state_dict(), save_path)
print(f"模型在第 {epoch + 1} 个 epoch 后已保存到 '{save_path}'")
# 加载最好的模型
model.load_state_dict(torch.load(save_path))
model.to(device)
print("最好的模型已成功加载并移动到设备")
代码解析
- 保存模型状态字典:
torch.save(model.state_dict(), 'llama3.1-finetuned.pth')
使用 torch.save
函数将模型的状态字典保存到文件中。
- 加载保存的模型:
model.load_state_dict(torch.load('llama3.1-finetuned.pth'))
model.to(device)
使用 torch.load
函数加载保存的模型状态字典,并将模型移动到指定设备。
- 保存和加载优化器状态:
torch.save({
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
}, 'llama3.1-finetuned-checkpoint.pth')
checkpoint = torch.load('llama3.1-finetuned-checkpoint.pth')
model.load_state_dict(checkpoint['model_state_dict'])
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
保存和加载优化器状态,以便在重新加载模型时能够继续训练。
- 自动保存和加载:
if average_loss < best_loss:
best_loss = average_loss
torch.save(model.state_dict(), save_path)
model.load_state_dict(torch.load(save_path))
model.to(device)
在每个 epoch 结束后保存表现最好的模型,并在需要时重新加载。
通过上述步骤,您可以确保微调后的 Llama 3.1 模型得到妥善保存,并且能够在需要时方便地重新加载和使用。
5. 实战项目
在本章节中,我们将通过一个具体的实战项目来演示如何使用 Llama 3.1 模型进行数据处理、模型训练、评估和优化部署。我们将选择一个公开数据集,进行全面的项目实施。
5.1 项目简介与数据集选择
在本项目中,我们将选择一个公开的Kaggle数据集进行分析和预测。这里以经典的Kaggle泰坦尼克号乘客生存预测数据集为例。
项目目标:预测泰坦尼克号乘客的生还情况。
数据集内容:
PassengerId
:乘客编号Survived
:是否生还(0=No,1=Yes)Pclass
:船舱等级(1=1等舱,2=2等舱,3=3等舱)Name
:乘客姓名Sex
:性别Age
:年龄SibSp
:船上兄弟姐妹/配偶数量Parch
:船上父母/子女数量Ticket
:票号Fare
:票价Cabin
:船舱号Embarked
:登船港口(C=Cherbourg,Q=Queenstown,S=Southampton)
5.2 数据预处理
数据预处理是数据科学项目的关键步骤,包含数据清洗、特征工程和数据分割等。
步骤:
- 数据清洗:处理缺失值和异常值。
- 特征工程:转换和创建新特征,处理分类特征。
- 数据分割:将数据集分为训练集和测试集。
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
# 加载数据集
data = pd.read_csv('titanic.csv')
# 数据清洗
data = data.drop(columns=['Cabin', 'Ticket'])
data['Age'].fillna(data['Age'].median(), inplace=True)
data['Embarked'].fillna(data['Embarked'].mode()[0], inplace=True)
# 特征工程
numeric_features = ['Age', 'Fare', 'SibSp', 'Parch']
numeric_transformer = Pipeline(steps=[
('scaler', StandardScaler())
])
categorical_features = ['Pclass', 'Sex', 'Embarked']
categorical_transformer = Pipeline(steps=[
('onehot', OneHotEncoder())
])
preprocessor = ColumnTransformer(
transformers=[
('num', numeric_transformer, numeric_features),
('cat', categorical_transformer, categorical_features)
])
# 数据分割
X = data.drop(columns=['Survived'])
y = data['Survived']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 预处理
X_train = preprocessor.fit_transform(X_train)
X_test = preprocessor.transform(X_test)
5.3 模型训练与评估
选择合适的模型进行训练,并使用交叉验证和评分指标评估模型性能。这里我们使用决策树作为基线模型。
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import cross_val_score
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
# 构建决策树模型
model = DecisionTreeClassifier(random_state=42)
# 交叉验证
cv_scores = cross_val_score(model, X_train, y_train, cv=5)
print(f'Cross-Validation Scores: {cv_scores}')
print(f'Mean Cross-Validation Score: {cv_scores.mean()}')
# 训练模型
model.fit(X_train, y_train)
# 预测与评估
y_pred = model.predict(X_test)
print(f'Accuracy: {accuracy_score(y_test, y_pred)}')
print(f'Precision: {precision_score(y_test, y_pred)}')
print(f'Recall: {recall_score(y_test, y_pred)}')
print(f'F1 Score: {f1_score(y_test, y_pred)}')
5.4 模型优化
通过网格搜索或随机搜索优化模型参数,将优化后的模型保存并部署到生产环境。
from sklearn.model_selection import GridSearchCV
import joblib
# 定义参数网格
param_grid = {
'max_depth': [3, 5, 7, 10],
'min_samples_split': [2, 5, 10],
'min_samples_leaf': [1, 2, 4]
}
# 网格搜索
grid_search = GridSearchCV(estimator=model, param_grid=param_grid, cv=5, scoring='accuracy', n_jobs=-1)
grid_search.fit(X_train, y_train)
# 输出最佳参数
print(f'Best Parameters: {grid_search.best_params_}')
print(f'Best Cross-Validation Score: {grid_search.best_score_}')
# 使用最佳参数训练最终模型
best_model = grid_search.best_estimator_
best_model.fit(X_train, y_train)
# 最终评估
final_predictions = best_model.predict(X_test)
print(f'Final Model Accuracy: {accuracy_score(y_test, final_predictions)}')
# 保存最终模型
joblib.dump(best_model, 'final_model.joblib')
print("优化后的模型已保存到 'final_model.joblib'")
5.5 部署模型
将优化后的模型部署到生产环境,确保其能够在实际应用中进行预测。
步骤:
- 加载模型:
model = joblib.load('final_model.joblib')
- API 服务:使用 Flask 或 FastAPI 创建一个简单的 API 服务。
from flask import Flask, request, jsonify
import joblib
app = Flask(__name__)
model = joblib.load('final_model.joblib')
@app.route('/predict', methods=['POST'])
def predict():
data = request.json
prediction = model.predict([data['features']])
return jsonify({'prediction': prediction.tolist()})
if __name__ == '__main__':
app.run(debug=True)
- 测试 API:使用 Postman 或 curl 测试 API 服务,确保其能够正常返回预测结果。
本主页会定期更新,为了能够及时获得更新,敬请关注我:点击左下角的关注。也可以关注公众号:请在微信上搜索公众号“AI与编程之窗”并关注,或者扫描以下公众号二维码关注,以便在内容更新时直接向您推送。