如何才能使用 PyTorch 从头构建自己的大型语言模型 (LLM),今天就告诉你答案!

简述


本文介绍了一种构建和训练名为 EtoM 的大型语言模型 (LLM) 的方法,该模型使用 PyTorch 基于 Transformer 架构,主要功能是将英语翻译成马来西亚语。

摘要


该文本详细介绍了开发称为“EtoM”的大型语言模型 (LLM) 的教程,该模型旨在将文本从英语翻译为马来西亚语。本文首先介绍 Transformer 架构,介绍其在高级人工智能聊天机器人中的基础作用。然后,系统地介绍了模型的每个组成部分,包括数据准备、分词器创建、数据集编码、位置编码的输入嵌入、多头注意力块、前馈网络、层归一化和整体编码器-解码器结构。该模型还使用 Huggingface 的数据集解释了训练和验证过程,最后给出了测试模型翻译功能的说明。本文旨在让读者掌握构建相似语言模型的知识,为自然语言处理领域做出贡献。

LLM 是大多数流行的人工智能聊天机器人(如 ChatGPT、Gemini、MetaAI、Mistral AI 等)的核心基础。每个 LLM 的核心都有一个名为 Transformer 的架构。因此,本文首先基于著名论文“Attention is all you need”构建 Transformer 架构。

首先,逐块构建Transformer模型的所有组件。然后,把所有模块组装在一起来构建最终的模型。之后,使用从hugging face数据集获得的数据集来训练和验证构建的模型。最后,通过对新的翻译文本数据执行翻译来测试模型的效果。

Step 1 加载数据集

为了使 LLM 模型能够执行从英语到马来西亚语的翻译任务,需要使用同时具有源(英语)和目标(马来西亚语)语言对的数据集。因此,使用 Huggingface 中名为“Helsinki-NLP/opus-100”的数据集。该数据集拥有 100 万对英语-马来西亚语训练数据集,可以使模型获得良好的准确性,验证数据集和测试数据集各有 2000 个数据。它已经进行了预分割,因此不必再次进行数据集分割。

代码如下:

# 导入必要的库  
# 如果尚未安装datasets和tokenizers库,请先安装(!pip install datasets, tokenizers)。  
import os  
import math  
import torch  
import torch.nn as nn  
from torch.utils.data import Dataset, DataLoader  
from pathlib import Path  
from datasets import load_dataset  
from tqdm import tqdm  
  
# 将设备值设为"cuda"以在GPU上训练,如果GPU不可用,则默认使用"cpu"。  
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")    
  
# 从huggingface路径加载训练、验证和测试数据集。  
raw_train_dataset = load_dataset("Helsinki-NLP/opus-100", "en-ms", split='train')  
raw_validation_dataset = load_dataset("Helsinki-NLP/opus-100", "en-ms", split='validation')  
raw_test_dataset = load_dataset("Helsinki-NLP/opus-100", "en-ms", split='test')  
  
# 存储数据集文件的目录。  
os.mkdir("./dataset-en")  
os.mkdir("./dataset-my")  
  
# 在每个EPOCHS后保存模型的目录(在第10步)。  
os.mkdir("./malaygpt")  
  
# 存储源语言和目标语言分词器的目录。  
os.mkdir("./tokenizer_en")  
os.mkdir("./tokenizer_my")  
  
dataset_en = []       
dataset_my = []  
file_count = 1  
  
# 为了训练分词器(在第2步),我们将训练数据集分成英语和马来语。  
# 创建多个大小为50k数据的小文件,并存储到dataset-en和dataset-my目录中。  
for data in tqdm(raw_train_dataset["translation"]):  
    dataset_en.append(data["en"].replace('\n', " "))  
    dataset_my.append(data["ms"].replace('\n', " "))  
    if len(dataset_en) == 50000:  
        with open(f'./dataset-en/file{file_count}.txt', 'w', encoding='utf-8') as fp:  
            fp.write('\n'.join(dataset_en))  
            dataset_en = []  
  
        with open(f'./dataset-my/file{file_count}.txt', 'w', encoding='utf-8') as fp:  
            fp.write('\n'.join(dataset_my))  
            dataset_my = []  
        file_count += 1  


Step 2 创建分词器(Tokenizer )

Transformer 模型不处理原始文本,它只处理数字。因此,必须采取一些措施将原始文本转换为数字。为此,使用一种名为 BPE tokenizer 的分词器,它是一种在 GPT3 等模型中使用的分词器。在步骤 1 中准备的语料库数据(本例中的训练数据集)上训练 BPE 分词器。流程如下图所示:

训练完成后,tokenzier 会生成英语和马来西亚语的词汇表。词汇是来自语料库数据的独特标记的集合。由于正在执行翻译任务,因此需要两种语言的分词器。BPE 分词器获取原始文本,其与词汇表中的标记进行映射,并为输入原始文本中的每个单词返回一个标记。标记可以是单个单词或子单词。这是子词 tokenzier 相对于其他 tokenizer 的优势之一,因为它可以克服 OOV(词汇外)问题。然后,标记生成器返回词汇表中标记的唯一索引或位置 ID,这将进一步用于创建嵌入。

代码如下:

# 导入分词器库的类和模块  
from tokenizers import Tokenizer  
from tokenizers.models import BPE  
from tokenizers.trainers import BpeTrainer  
from tokenizers.pre_tokenizers import Whitespace  
  
# 训练数据集文件的路径,用于训练分词器  
path_en = [str(file) for file in Path('./dataset-en').glob("**/*.txt")]  
path_my = [str(file) for file in Path('./dataset-my').glob("**/*.txt")]  
  
# [创建源语言分词器 - 英语]  
# 创建额外的特殊标记,如 [UNK] - 表示未知词,[PAD] - 填充标记以保持模型中序列长度一致  
# [CLS] - 表示句子开始,[SEP] - 表示句子结束  
tokenizer_en = Tokenizer(BPE(unk_token="[UNK]"))  
trainer_en = BpeTrainer(min_frequency=2, special_tokens=["[PAD]","[UNK]","[CLS]", "[SEP]", "[MASK]"])  
  
# 基于空格分割标记  
tokenizer_en.pre_tokenizer = Whitespace()  
  
# 分词器训练第1步创建的数据集文件  
tokenizer_en.train(files=path_en, trainer=trainer_en)  
  
# 保存分词器以备将来使用  
tokenizer_en.save("./tokenizer_en/tokenizer_en.json")  
  
# [创建目标语言分词器 - 马来语]  
tokenizer_my = Tokenizer(BPE(unk_token="[UNK]"))  
trainer_my = BpeTrainer(min_frequency=2, special_tokens=["[PAD]","[UNK]","[CLS]", "[SEP]", "[MASK]"])  
tokenizer_my.pre_tokenizer = Whitespace()  
tokenizer_my.train(files=path_my, trainer=trainer_my)  
tokenizer_my.save("./tokenizer_my/tokenizer_my.json")  
  
tokenizer_en = Tokenizer.from_file("./tokenizer_en/tokenizer_en.json")  
tokenizer_my = Tokenizer.from_file("./tokenizer_my/tokenizer_my.json")  
  
# 获取两个分词器的词汇表大小  
source_vocab_size = tokenizer_en.get_vocab_size()  
target_vocab_size = tokenizer_my.get_vocab_size()  
  
# 定义标记ID变量,训练模型时需要这些  
CLS_ID = torch.tensor([tokenizer_my.token_to_id("[CLS]")], dtype=torch.int64).to(device)  
SEP_ID = torch.tensor([tokenizer_my.token_to_id("[SEP]")], dtype=torch.int64).to(device)  
PAD_ID = torch.tensor([tokenizer_my.token_to_id("[PAD]")], dtype=torch.int64).to(device)  


Step 3 准备数据集和DataLoader

在此步骤中,将为源语言(英语)和目标语言(马来西亚语)准备数据集,稍后将使用该数据集来训练和验证本文将要构建的模型。创建一个接受原始数据集的类,并定义使用源 (tokenizer_en) 和目标 (tokenizer_my) 分词器分别对源文本和目标文本进行编码的函数。最后,将为训练和验证数据集创建 DataLoader,该数据集批量迭代数据集(在本文的示例中,批量大小设置为 10)。批量大小可以根据数据大小和可用处理能力进行更改。

代码如下:

# 该类接收原始数据集和max_seq_len(整个数据集中序列的最大长度)。  
class EncodeDataset(Dataset):  
    def __init__(self, raw_dataset, max_seq_len):  
        super().__init__()  
        self.raw_dataset = raw_dataset  
        self.max_seq_len = max_seq_len  
  
    def __len__(self):  
        return len(self.raw_dataset)  
  
    def __getitem__(self, index):  
  
        # 获取给定索引的原始文本,包含源和目标文本对。  
        raw_text = self.raw_dataset[index]  
  
        # 将文本分离为源文本和目标文本,稍后用于编码。  
        source_text = raw_text["en"]  
        target_text = raw_text["ms"]  
  
        # 使用源分词器(tokenizer_en)对源文本进行编码,使用目标分词器(tokenizer_my)对目标文本进行编码。  
        source_text_encoded = torch.tensor(tokenizer_en.encode(source_text).ids, dtype = torch.int64).to(device)      
        target_text_encoded = torch.tensor(tokenizer_my.encode(target_text).ids, dtype = torch.int64).to(device)  
  
        # 为了训练模型,每个输入序列的长度应等于最大序列长度。  
        # 因此,如果长度小于max_seq_len,则会在输入序列中添加额外的填充数量。  
        num_source_padding = self.max_seq_len - len(source_text_encoded) - 2  
        num_target_padding = self.max_seq_len - len(target_text_encoded) - 1  
  
        encoder_padding = torch.tensor([PAD_ID] * num_source_padding, dtype = torch.int64).to(device)  
        decoder_padding = torch.tensor([PAD_ID] * num_target_padding, dtype = torch.int64).to(device)  
  
        # encoder_input 以句首标记 CLS_ID 开始,接着是源编码,然后是句尾标记 SEP。  
        # 为了达到所需的最大序列长度,会在末尾添加额外的 PAD 标记。  
        encoder_input = torch.cat([CLS_ID, source_text_encoded, SEP_ID, encoder_padding]).to(device)      
  
        # decoder_input 以句首标记 CLS_ID 开始,接着是目标编码。  
        # 为了达到所需的最大序列长度,会在末尾添加额外的 PAD 标记。解码器输入中没有句尾标记 SEP。  
        decoder_input = torch.cat([CLS_ID, target_text_encoded, decoder_padding ]).to(device)             
  
        # target_label 以目标编码开始,接着是句尾标记 SEP。目标标签中没有句首标记 CLS。  
        # 为了达到所需的最大序列长度,会在末尾添加额外的 PAD 标记。  
        target_label = torch.cat([target_text_encoded,SEP_ID,decoder_padding]).to(device)                 
  
        # 由于我们在输入编码中添加了额外的填充标记,在训练过程中,我们不希望模型训练这些标记,因为这些标记没有可学习的内容。  
        # 因此,我们将使用编码器掩码在计算自注意力输出之前使填充标记值无效。  
        encoder_mask = (encoder_input != PAD_ID).unsqueeze(0).unsqueeze(0).int().to(device)               
  
        # 在解码阶段,我们也不希望任何标记受到未来标记的影响。因此,在掩码多头注意力中实施因果掩码来处理这个问题。  
        decoder_mask = (decoder_input != PAD_ID).unsqueeze(0).unsqueeze(0).int() & causal_mask(decoder_input.size(0)).to(device)   
  
        return {  
            'encoder_input': encoder_input,  
            'decoder_input': decoder_input,  
            'target_label': target_label,  
            'encoder_mask': encoder_mask,  
            'decoder_mask': decoder_mask,  
            'source_text': source_text,  
            'target_text': target_text  
        }  
  
# 因果掩码确保当前标记之后的任何标记都被掩码,即值被替换为负无穷大,在softmax函数后转换为零或接近零。  
# 因此,模型将忽略这些值或无法从这些值中学习任何内容。  
def causal_mask(size):  
  # 因果掩码的维度(batch_size, seq_len, seq_len)  
  mask = torch.triu(torch.ones(1, size, size), diagonal = 1).type(torch.int)  
  return mask == 0  
  
# 计算整个训练数据集中源和目标数据集的最大序列长度。  
max_seq_len_source = 0  
max_seq_len_target = 0  
  
for data in raw_train_dataset["translation"]:  
    enc_ids = tokenizer_en.encode(data["en"]).ids  
    dec_ids = tokenizer_my.encode(data["ms"]).ids  
    max_seq_len_source = max(max_seq_len_source, len(enc_ids))  
    max_seq_len_target = max(max_seq_len_target, len(dec_ids))  
  
print(f'max_seqlen_source: {max_seq_len_source}')   #530  
print(f'max_seqlen_target: {max_seq_len_target}')   #526  
  
# 为了简化训练过程,我们将只取一个最大序列长度,并增加20以覆盖序列中额外的标记长度,如PAD、CLS、SEP。  
max_seq_len = 550  
  
# 实例化EncodeRawDataset类,并创建编码后的训练和验证数据集。  
train_dataset = EncodeDataset(raw_train_dataset["translation"], max_seq_len)  
val_dataset = EncodeDataset(raw_validation_dataset["translation"], max_seq_len)  
  
# 为训练和验证数据集创建DataLoader包装器。这个数据加载器将在后续的LLM模型训练和验证阶段使用。  
train_dataloader = DataLoader(train_dataset, batch_size = 10, shuffle = True, generator=torch.Generator(device='cuda'))  
val_dataloader = DataLoader(val_dataset, batch_size = 1, shuffle = True, generator=torch.Generator(device='cuda'))

Step 4 输入嵌入和位置编码

输入嵌入:第 2 步中由分词器生成的 token id 序列将被输入到嵌入层。嵌入层将 token-id 映射到词汇表,并为每个 token 生成维度为 512 的嵌入向量。嵌入向量能够根据其所训练的训练数据集捕获标记的语义。嵌入向量内的每个维度值代表与令牌相关的某种特征。例如,如果标记是“狗”,则某个维度值代表眼睛、嘴巴、腿、身高等。如果本文在 n 维空间中绘制一个向量,则外观相似的对象(例如狗、猫)彼此靠近而外观不相似的物体(例如学校、家庭嵌入向量)会位于很远的地方。

位置编码:Transformer 架构的优点之一是它可以并行处理任意数量的输入序列,这减少了大量训练时间,并使预测速度更快。然而,Transformer的一个缺点是,在并行处理许多标记序列时,句子中标记的位置不会按顺序排列。这可能会导致句子的不同含义或上下文,具体取决于标记的位置。因此,为了解决这个问题,注意力论文实现了位置编码方法。该论文建议在每个标记512维度的索引级别上应用两个数学函数(一个是正弦,一个是余弦)。下面是简单的正弦和余弦数学函数。

Sin 函数应用于每个偶数维度值,而 Cosine 函数应用于嵌入向量的奇数维度值。最后,得到的位置编码器向量被添加到嵌入向量中。现在,有了嵌入向量,它可以捕获标记的语义以及标记的位置。请注意,位置编码的值在每个序列中保持相同。

代码如下:

   `return self.dropout(input_embdding)`
# 输入嵌入和位置编码  
class EmbeddingLayer(nn.Module):  
    def __init__(self, vocab_size: int, d_model: int):  
        super().__init__()  
        self.d_model = d_model  
  
        # 使用PyTorch的嵌入层模块将标记ID映射到词汇表,然后转换为嵌入向量。  
        # vocab_size是第二步中标记器在训练语料库数据集时创建的训练数据集的词汇表大小。  
        self.embedding = nn.Embedding(vocab_size, d_model)  
  
    def forward(self, input):  
        # 除了将输入序列送入嵌入层外,还通过乘以d_model的平方根来规范化嵌入层输出  
        embedding_output = self.embedding(input) * math.sqrt(self.d_model)  
        return embedding_output  
  
  
class PositionalEncoding(nn.Module):  
    def __init__(self, max_seq_len: int, d_model: int, dropout_rate: float):  
        super().__init__()  
        self.dropout = nn.Dropout(dropout_rate)  
  
        # 创建一个与嵌入向量形状相同的矩阵。  
        pe = torch.zeros(max_seq_len, d_model)  
  
        # 计算位置编码函数的位置部分。  
        pos = torch.arange(0, max_seq_len, dtype=torch.float).unsqueeze(1)  
  
        # 计算位置编码函数的除法部分。注意,除法部分的表达式与论文中的表达式略有不同,因为这种指数函数似乎效果更好。  
        div_term = torch.exp(torch.arange(0, d_model, 2).float()) * (-math.log(10000)/d_model)  
  
        # 用正弦和余弦数学函数的结果填充奇数和偶数矩阵值。  
        pe[:, 0::2] = torch.sin(pos * div_term)  
        pe[:, 1::2] = torch.cos(pos * div_term)  
  
        # 由于我们期望输入序列以批次形式出现,因此在第0位置添加了额外的batch_size维度。  
        pe = pe.unsqueeze(0)      
  
    def forward(self, input_embdding):  
        # 将位置编码与输入嵌入向量相加。  
        input_embdding = input_embdding + (self.pe[:, :input_embdding.shape[1], :]).requires_grad_(False)    
  
        # 执行dropout以防止过拟合。

Step 5 多头注意力块

class MultiHeadAttention(nn.Module):  
    def __init__(self, d_model: int, num_heads: int, dropout_rate: float):  
        super().__init__()  
        # 定义dropout以防止过拟合。  
        self.dropout = nn.Dropout(dropout_rate)  
  
        # 引入权重矩阵,它们都是可学习的参数。  
        self.W_q = nn.Linear(d_model, d_model)  
        self.W_k = nn.Linear(d_model, d_model)  
        self.W_v = nn.Linear(d_model, d_model)  
        self.W_o = nn.Linear(d_model, d_model)  
  
        self.num_heads = num_heads  
        assert d_model % num_heads == 0, "d_model必须能被头的数量整除"  
  
        # d_k是每个分割后的自注意力头的新的维度  
        self.d_k = d_model // num_heads  
  
    def forward(self, q, k, v, encoder_mask=None):  
  
        # 我们将使用多个序列批次并行训练模型,因此需要在形状中包含batch_size。  
        # 通过输入嵌入与相应权重的矩阵乘法计算查询、键和值。  
        # 形状变化:q(batch_size, seq_len, d_model) @ W_q(d_model, d_model) => query(batch_size, seq_len, d_model) [键和值同理]。  
        query = self.W_q(q)   
        key = self.W_k(k)  
        value = self.W_v(v)  
  
        # 将查询、键和值分割成多个头。d_model在8个头中被分割成d_k。  
        # 形状变化:query(batch_size, seq_len, d_model) => query(batch_size, seq_len, num_heads, d_k) -> query(batch_size,num_heads, seq_len,d_k) [键和值同理]。  
        query = query.view(query.shape[0], query.shape[1], self.num_heads ,self.d_k).transpose(1,2)  
        key = key.view(key.shape[0], key.shape[1], self.num_heads ,self.d_k).transpose(1,2)  
        value = value.view(value.shape[0], value.shape[1], self.num_heads ,self.d_k).transpose(1,2)  
  
        # :: 自注意力块开始 ::  
  
        # 计算注意力分数,以找出查询与自身和序列中所有其他嵌入的键之间的相似度或关系。  
        # 形状变化:query(batch_size,num_heads, seq_len,d_k) @ key(batch_size,num_heads, seq_len,d_k) => attention_score(batch_size,num_heads, seq_len,seq_len)。  
        attention_score = (query @ key.transpose(-2,-1))/math.sqrt(self.d_k)  
  
        # 如果提供了掩码,注意力分数需要根据掩码值进行修改。详情参见第4点。  
        if encoder_mask is not None:  
            attention_score = attention_score.masked_fill(encoder_mask==0, -1e9)  
  
        # softmax函数计算所有注意力分数的概率分布。它为较高的注意力分数分配较高的概率值。意味着更相似的标记获得更高的概率值。  
        # 形状变化:与attention_score相同  
        attention_weight = torch.softmax(attention_score, dim=-1)  
  
        if self.dropout is not None:  
            attention_weight = self.dropout(attention_weight)  
  
        # 自注意力块的最后一步是,注意力权重与值嵌入向量进行矩阵乘法。  
        # 形状变化:attention_score(batch_size,num_heads, seq_len,seq_len) @  value(batch_size,num_heads, seq_len,d_k) => attention_output(batch_size,num_heads, seq_len,d_k)  
        attention_output = attention_score @ value  
  
        # :: 自注意力块结束 ::  
  
        # 现在,所有头将被合并回一个单头  
        # 形状变化:attention_output(batch_size,num_heads, seq_len,d_k) => attention_output(batch_size,seq_len,num_heads,d_k) => attention_output(batch_size,seq_len,d_model)          
        attention_output = attention_output.transpose(1,2).contiguous().view(attention_output.shape[0], -1, self.num_heads * self.d_k)  
  
        # 最后,attention_output与输出权重矩阵进行矩阵乘法,得到最终的多头注意力输出。  
        # 多头输出的形状与嵌入输入相同  
        # 形状变化:attention_output(batch_size,seq_len,d_model) @ W_o(d_model, d_model) => multihead_output(batch_size, seq_len, d_model)  
        multihead_output = self.W_o(attention_output)  
  
        return multihead_output  


Step 6 前馈网络、层标准化和 AddAndNorm

前馈网络:前馈网络使用深度神经网络来学习跨两个线性层的嵌入向量的所有特征(第一个具有 d_model 节点,第二个具有 d_ff 节点,根据注意力论文分配值)并将 ReLU 激活函数应用于输出第一个线性层基本上为嵌入值提供非线性,并应用 dropout 来进一步避免过度拟合。


层标准化:对嵌入值应用层归一化,以确保网络中嵌入向量的值分布保持一致。这样就保证了学习的顺利进行。本文将使用称为 gamma 和 beta 的额外学习参数来根据网络需要缩放和移动嵌入值。

AddAndNorm:这由跳过连接和分层标准化组成(前面已解释)。在前向传递过程中,Skip 连接确保了前一层的特征在后期仍然可以被记住,为计算输出做出必要的贡献。类似地,在反向传播过程中,Skip 连接需要在每个阶段少执行一次反向传播,从而确保防止梯度消失。AddAndNorm 被用于编码器(2 次)和解码器块(3 次)。它从前一层获取输入,在添加到前一层的输出之前先对其进行标准化。代码如下:

# 前馈网络、层归一化和AddAndNorm模块  
class FeedForward(nn.Module):  
    def __init__(self, d_model: int, d_ff: int, dropout_rate: float):  
        super().__init__()  
  
        self.layer_1 = nn.Linear(d_model, d_ff)  
        self.activation_1 = nn.ReLU()  
        self.dropout = nn.Dropout(dropout_rate)  
        self.layer_2 = nn.Linear(d_ff, d_model)  
  
    def forward(self, input):  
        return self.layer_2(self.dropout(self.activation_1(self.layer_1(input))))  
  
class LayerNorm(nn.Module):  
    def __init__(self, eps: float = 1e-5):  
        super().__init__()  
        # Epsilon是一个非常小的值,它在防止潜在的除以零问题中起着重要作用。  
        self.eps = eps  
  
        # 引入额外的学习参数gamma和beta,根据网络需要来缩放和偏移嵌入值。  
        self.gamma = nn.Parameter(torch.ones(1))  
        self.beta = nn.Parameter(torch.zeros(1))  
  
    def forward(self, input):  
        mean = input.mean(dim=-1, keepdim=True)        
        std = input.std(dim=-1, keepdim=True)        
  
        return self.gamma * ((input - mean)/(std + self.eps)) + self.beta  
  
  
class AddAndNorm(nn.Module):  
    def __init__(self, dropout_rate: float):  
        super().__init__()  
        self.dropout = nn.Dropout(dropout_rate)  
        self.layer_norm = LayerNorm()  
  
    def forward(self, input, sub_layer):  
        return input + self.dropout(sub_layer(self.layer_norm(input)))  


Step 7 编码器块和编码器

编码器块:编码器块内部有两个主要组件:多头注意力和前馈。还有 2 个 Add & Norm 单元。首先按照注意力论文中的流程将所有这些组件组装在 EncoderBlock 类中。根据论文,该编码器块已重复 6 次.

Encoder:然后将创建名为 Encoder 的附加类,它将获取 EncoderBlock 列表并将其堆叠并给出最终的 Encoder 输出。

代码如下:

class EncoderBlock(nn.Module):  
    def __init__(self, multihead_attention: MultiHeadAttention, feed_forward: FeedForward, dropout_rate: float):  
        super().__init__()  
        self.multihead_attention = multihead_attention  
        self.feed_forward = feed_forward  
        self.add_and_norm_list = nn.ModuleList([AddAndNorm(dropout_rate) for _ in range(2)])  
  
    def forward(self, encoder_input, encoder_mask):  
        # 第一个AddAndNorm单元接收来自跳跃连接的编码器输入,并与多头注意力块的输出相加。  
        encoder_input = self.add_and_norm_list[0](encoder_input, lambda encoder_input: self.multihead_attention(encoder_input, encoder_input, encoder_input, encoder_mask))  
  
        # 第二个AddAndNorm单元接收来自跳跃连接的多头注意力块输出,并与前馈层的输出相加。  
        encoder_input = self.add_and_norm_list[1](encoder_input, self.feed_forward)  
  
        return encoder_input  
  
class Encoder(nn.Module):  
    def __init__(self, encoderblocklist: nn.ModuleList):  
        super().__init__()  
  
        # 编码器类通过接收编码器块列表进行初始化。  
        self.encoderblocklist = encoderblocklist  
        self.layer_norm = LayerNorm()  
  
    def forward(self, encoder_input, encoder_mask):  
        # 遍历所有编码器块 -6次。  
        for encoderblock in self.encoderblocklist:  
            encoder_input = encoderblock(encoder_input, encoder_mask)  
  
        # 对最终的编码器块输出进行归一化并返回。此编码器输出将作为解码器块中交叉注意力的键和值使用。  
        encoder_output = self.layer_norm(encoder_input)  
        return encoder_output  


Step 8 解码器块、解码器和投影层

解码器块:解码器块中有三个主要组件:Masked Multi-Head Attention、Multi-Head Attention 和 Feedforward。解码器块还具有 3 个 Add & Norm 单元。本文将按照 Attention 论文中的流程将所有这些组件组装在 DecoderBlock 类中。根据论文,该解码器块已重复 6 次。

解码器:创建名为 Decoder 的附加类,它将获取 DecoderBlock 列表,将其堆叠并给出最终的 Decoder 输出。

解码器块中有两种类型的多头注意力。第一个是 Masked Multi-Head Attention。它接收解码器输入作为查询、键和值以及解码器掩码(也称为因果掩码)。因果掩码可防止模型查看序列顺序中前面的嵌入。步骤 3 和步骤 5 中提供了其工作原理的详细说明。

投影层:最终的解码器输出将传递到投影层。在这一层中,解码器输出将首先被馈送到线性层,其中嵌入的形状将发生变化,如下面的代码部分所示。随后,softmax 函数将解码器输出转换为词汇表上的概率分布,并选择具有最高概率的标记作为预测输出。

代码如下:

class DecoderBlock(nn.Module):  
    def __init__(self, masked_multihead_attention: MultiHeadAttention,multihead_attention: MultiHeadAttention, feed_forward: FeedForward, dropout_rate: float):  
        super().__init__()  
        self.masked_multihead_attention = masked_multihead_attention  
        self.multihead_attention = multihead_attention  
        self.feed_forward = feed_forward  
        self.add_and_norm_list = nn.ModuleList([AddAndNorm(dropout_rate) for _ in range(3)])  
  
    def forward(self, decoder_input, decoder_mask, encoder_output, encoder_mask):  
        # 第一个AddAndNorm单元接收来自跳跃连接的解码器输入,并与掩码多头注意力块的输出相加。  
        decoder_input = self.add_and_norm_list[0](decoder_input, lambda decoder_input: self.masked_multihead_attention(decoder_input,decoder_input, decoder_input, decoder_mask))  
        # 第二个AddAndNorm单元接收来自跳跃连接的掩码多头注意力块的输出,并与多头注意力块的输出相加。  
        decoder_input = self.add_and_norm_list[1](decoder_input, lambda decoder_input: self.multihead_attention(decoder_input,encoder_output, encoder_output, encoder_mask))            # 交叉注意力  
        # 第三个AddAndNorm单元接收来自跳跃连接的多头注意力块的输出,并与前馈层的输出相加。  
        decoder_input = self.add_and_norm_list[2](decoder_input, self.feed_forward)  
        return decoder_input  
  
class Decoder(nn.Module):  
    def __init__(self,decoderblocklist: nn.ModuleList):  
        super().__init__()  
        self.decoderblocklist = decoderblocklist  
        self.layer_norm = LayerNorm()  
  
    def forward(self, decoder_input, decoder_mask, encoder_output, encoder_mask):  
        for decoderblock in self.decoderblocklist:  
            decoder_input = decoderblock(decoder_input, decoder_mask, encoder_output, encoder_mask)  
  
        decoder_output = self.layer_norm(decoder_input)  
        return decoder_output  
  
class ProjectionLayer(nn.Module):  
    def __init__(self, vocab_size: int, d_model: int):  
        super().__init__()  
        self.projection_layer = nn.Linear(d_model, vocab_size)  
  
    def forward(self, decoder_output):  
        # 投影层首先接收解码器输出,并将其传递到形状为(d_model, vocab_size)的线性层中。  
        # 形状变化:decoder_output(batch_size, seq_len, d_model) @ linear_layer(d_model, vocab_size) => output(batch_size, seq_len, vocab_size)  
        output = self.projection_layer(decoder_output)  
  
        # softmax函数输出词汇表上的概率分布  
        return torch.log_softmax(output, dim=-1)  


Step 9 创建并构建 Transfomer

首先创建一个 Transformer 类,它将初始化组件类的所有实例。在 Transformer 类中,首先定义编码函数,该函数执行 Transformer 编码器部分的所有任务并生成编码器输出。其次,定义解码函数,它完成变压器解码器部分的所有任务并生成解码器输出。在第三步中,定义了一个项目函数,它接收解码器输出并将输出映射到词汇表以进行预测。

现在,变压器架构已准备就绪。现在可以通过定义一个函数来构建本文的翻译 LLM 模型,该函数接受下面代码中给出的所有必要参数。

class Transformer(nn.Module):  
    def __init__(self, source_embed: EmbeddingLayer, target_embed: EmbeddingLayer, positional_encoding: PositionalEncoding, multihead_attention: MultiHeadAttention, masked_multihead_attention: MultiHeadAttention, feed_forward: FeedForward, encoder: Encoder, decoder: Decoder, projection_layer: ProjectionLayer, dropout_rate: float):          
        super().__init__()  
  
        # 初始化Transformer架构中所有组件类的实例  
        self.source_embed = source_embed  
        self.target_embed = target_embed  
        self.positional_encoding = positional_encoding  
        self.multihead_attention = multihead_attention          
        self.masked_multihead_attention = masked_multihead_attention  
        self.feed_forward = feed_forward  
        self.encoder = encoder  
        self.decoder = decoder  
        self.projection_layer = projection_layer  
        self.dropout = nn.Dropout(dropout_rate)  
  
    # 编码函数接收编码器输入,在所有编码器块内进行必要的处理并给出编码器输出  
    def encode(self, encoder_input, encoder_mask):  
        encoder_input = self.source_embed(encoder_input)  
        encoder_input = self.positional_encoding(encoder_input)  
        encoder_output = self.encoder(encoder_input, encoder_mask)  
        return encoder_output  
  
    # 解码函数接收解码器输入,在所有解码器块内进行必要的处理并给出解码器输出  
    def decode(self, decoder_input, decoder_mask, encoder_output, encoder_mask):  
        decoder_input = self.target_embed(decoder_input)  
        decoder_input = self.positional_encoding(decoder_input)  
        decoder_output = self.decoder(decoder_input, decoder_mask, encoder_output, encoder_mask)  
        return decoder_output  
  
    # 投影函数接收解码器输出并将其通过投影层映射到词汇表以进行预测  
    def project(self, decoder_output):  
        return self.projection_layer(decoder_output)  
  
def build_model(source_vocab_size, target_vocab_size, max_seq_len=1135, d_model=512, d_ff=2048, num_heads=8, num_blocks=6, dropout_rate=0.1):  
  
    # 定义并赋值Transformer架构所需的所有参数  
    source_embed = EmbeddingLayer(source_vocab_size, d_model)  
    target_embed = EmbeddingLayer(target_vocab_size, d_model)  
    positional_encoding = PositionalEncoding(max_seq_len, d_model, dropout_rate)  
    multihead_attention = MultiHeadAttention(d_model, num_heads, dropout_rate)  
    masked_multihead_attention = MultiHeadAttention(d_model, num_heads, dropout_rate)  
    feed_forward = FeedForward(d_model, d_ff, dropout_rate)      
    projection_layer = ProjectionLayer(target_vocab_size, d_model)  
    encoder_block = EncoderBlock(multihead_attention, feed_forward, dropout_rate)  
    decoder_block = DecoderBlock(masked_multihead_attention,multihead_attention, feed_forward, dropout_rate)  
  
    encoderblocklist = []  
    decoderblocklist = []  
  
    for _ in range(num_blocks):  
        encoderblocklist.append(encoder_block)     
  
    for _ in range(num_blocks):  
        decoderblocklist.append(decoder_block)  
  
    encoderblocklist = nn.ModuleList(encoderblocklist)              
    decoderblocklist = nn.ModuleList(decoderblocklist)  
  
    encoder = Encoder(encoderblocklist)  
    decoder = Decoder(decoderblocklist)  
  
    # 通过提供所有参数值实例化Transformer类  
    model = Transformer(source_embed, target_embed, positional_encoding, multihead_attention, masked_multihead_attention,feed_forward, encoder, decoder, projection_layer, dropout_rate)  
  
    for param in model.parameters():  
        if param.dim() > 1:  
            nn.init.xavier_uniform_(param)  
  
    return model  
  
# 最后,调用build_model并将其赋值给model变量。  
# 该模型现已完全准备好训练和验证我们的数据集。  
# 训练和验证后,我们可以使用此模型执行新的翻译任务  
  
model = build_model(source_vocab_size, target_vocab_size)  


Step 10 训练和验证构建的 LLM 模型

‍训练过程非常简单。本文将使用在步骤 3 中创建的训练 DataLoader。由于训练数据集总数为 100 万个,我强烈建议在 GPU 设备上训练本文的模型。我花了大约 5 小时完成 20 个 epoch。在每个时期之后,本文将保存模型权重以及优化器状态,以便更容易从停止之前的点恢复训练,而不是从头开始恢复。

在每个epoch之后,使用验证数据加载器启动验证。验证数据集的大小为 2000,这是相当合理的。在验证过程中,本文只需要计算编码器输出一次,直到解码器输出获得句子结束标记[SEP],这是因为在解码器获得[SEP]标记之前,必须再次发送相同的编码器输出又是没有意义的。

解码器输入首先以句子标记 [CLS] 的开头开始。每次预测后,解码器输入将附加下一个生成的标记,直到到达句子末尾标记 [SEP]。最后,投影层将输出映射到相应的文本表示。‍

def training_model(preload_epoch=None):     
  
    # 整个训练、验证周期将运行20次。  
    EPOCHS = 20  
    initial_epoch = 0  
    global_step = 0  
  
    # Adam是最常用的优化算法之一,它持有当前状态,并根据计算的梯度更新参数。          
    optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)  
  
    # 如果preload_epoch不为空,意味着训练将从上次保存的权重和优化器开始。新的epoch编号将是preload_epoch + 1if preload_epoch is not None:  
        model_filename = f"./malaygpt/model_{preload_epoch}.pt"  
        state = torch.load(model_filename)  
        initial_epoch = state['epoch'] + 1  
        optimizer.load_state_dict(state['optimizer_state_dict'])  
        global_step = state['global_step']  
  
    # CrossEntropyLoss损失函数计算投影输出与目标标签之间的差异。  
    loss_fn = nn.CrossEntropyLoss(ignore_index = tokenizer_en.token_to_id("[PAD]"), label_smoothing=0.1).to(device)  
  
    for epoch in range(initial_epoch, EPOCHS):  
  
        # ::: 训练块开始 :::  
        model.train()    
  
        # 使用第三步中准备的训练dataloder进行训练。      
        for batch in tqdm(train_dataloader):  
            encoder_input = batch['encoder_input'].to(device)   # (batch_size, seq_len)  
            decoder_input = batch['decoder_input'].to(device)    # (batch_size, seq_len)  
            target_label = batch['target_label'].to(device)      # (batch_size, seq_len)  
            encoder_mask = batch['encoder_mask'].to(device)         
            decoder_mask = batch['decoder_mask'].to(device)           
  
            encoder_output = model.encode(encoder_input, encoder_mask)  
            decoder_output = model.decode(decoder_input, decoder_mask, encoder_output, encoder_mask)  
            projection_output = model.project(decoder_output)  
  
            # projection_output(batch_size, seq_len, vocab_size)  
            loss = loss_fn(projection_output.view(-1, projection_output.shape[-1]), target_label.view(-1))  
  
            # 反向传播  
            optimizer.zero_grad()  
            loss.backward()  
  
            # 更新权重  
            optimizer.step()          
            global_step += 1  
  
        print(f'Epoch [{epoch+1}/{EPOCHS}]: Train Loss: {loss.item():.2f}')  
  
        # 每个epoch结束后保存模型状态  
        model_filename = f"./malaygpt/model_{epoch}.pt"  
        torch.save({  
            'epoch': epoch,  
            'model_state_dict': model.state_dict(),  
            'optimizer_state_dict': optimizer.state_dict(),  
            'global_step': global_step  
        }, model_filename)          
        # ::: 训练块结束 :::  
  
        # ::: 验证块开始 :::  
        model.eval()          
        with torch.inference_mode():  
            for batch in tqdm(val_dataloader):                  
                encoder_input = batch['encoder_input'].to(device)   # (batch_size, seq_len)                          
                encoder_mask = batch['encoder_mask'].to(device)  
                source_text = batch['source_text']  
                target_text = batch['target_text']  
  
                # 计算源序列的编码器输出。  
                encoder_output = model.encode(encoder_input, encoder_mask)  
  
                # 对于预测任务,解码器输入的第一个标记是[CLS]标记  
                decoder_input = torch.empty(1,1).fill_(tokenizer_my.token_to_id('[CLS]')).type_as(encoder_input).to(device)  
  
                # 因为我们需要不断将输出添加回输入,直到接收到[SEP] - 结束标记。  
                while True:                       
                    # 检查是否达到了最大长度,如果是,则停止。  
                    if decoder_input.size(1) == max_seq_len:  
                        break  
  
                    # 每次新输出添加到解码器输入以进行下一个标记预测时重新创建掩码  
                    decoder_mask = causal_mask(decoder_input.size(1)).type_as(encoder_mask).to(device)  
  
                    decoder_output = model.decode(decoder_input,decoder_mask,encoder_output,encoder_mask)  
  
                    # 仅对下一个标记应用投影。  
                    projection = model.project(decoder_output[:, -1])  
  
                    # 选择概率最高的标记,这是一种称为贪心搜索的实现。  
                    _, new_token = torch.max(projection, dim=1)  
                    new_token = torch.empty(1,1).type_as(encoder_input).fill_(new_token.item()).to(device)  
  
                    # 将新标记添加回解码器输入。  
                    decoder_input = torch.cat([decoder_input, new_token], dim=1)  
  
                    # 检查新标记是否为结束标记,如果是,则停止。  
                    if new_token == tokenizer_my.token_to_id('[SEP]'):  
                        break  
  
                # 将解码器输出分配为完全追加的解码器输入。  
                decoder_output = decoder_input.squeeze(0)  
                model_predicted_text = tokenizer_my.decode(decoder_output.detach().cpu().numpy())  
  
                print(f'SOURCE TEXT": {source_text}')  
                print(f'TARGET TEXT": {target_text}')  
                print(f'PREDICTED TEXT": {model_predicted_text}')     
                # ::: 验证块结束 :::               
  
# 此函数运行20个epoch的训练和验证  
training_model(preload_epoch=None)  


Step 11 创建一个函数来使用构建的模型测试新的翻译任务

这里为翻译函数指定一个新的通用名称,称为 EtoM。它接收用户输入的英语原始文本并输出马来西亚语翻译文本。运行该函数并尝试一下。

def malaygpt(user_input_text):  
  model.eval()  
  with torch.inference_mode():  
    user_input_text = user_input_text.strip()  
    user_input_text_encoded = torch.tensor(tokenizer_en.encode(user_input_text).ids, dtype = torch.int64).to(device)  
  
    num_source_padding = max_seq_len - len(user_input_text_encoded) - 2  
    encoder_padding = torch.tensor([PAD_ID] * num_source_padding, dtype = torch.int64).to(device)  
    encoder_input = torch.cat([CLS_ID, user_input_text_encoded, SEP_ID, encoder_padding]).to(device)  
    encoder_mask = (encoder_input != PAD_ID).unsqueeze(0).unsqueeze(0).int().to(device)  
  
    # 计算源序列的编码器输出  
    encoder_output = model.encode(encoder_input, encoder_mask)  
    # 对于预测任务,解码器输入的第一个token是[CLS] token  
    decoder_input = torch.empty(1,1).fill_(tokenizer_my.token_to_id('[CLS]')).type_as(encoder_input).to(device)  
  
    # 由于我们需要不断将输出添加回输入,直到接收到[SEP] - 结束token。  
    while True:  
        # 检查是否达到了最大长度  
        if decoder_input.size(1) == max_seq_len:  
            break  
        # 每次将新输出添加到解码器输入以进行下一个token预测时,重新创建掩码  
        decoder_mask = causal_mask(decoder_input.size(1)).type_as(encoder_mask).to(device)  
        decoder_output = model.decode(decoder_input,decoder_mask,encoder_output,encoder_mask)  
  
        # 仅对下一个token应用投影  
        projection = model.project(decoder_output[:, -1])  
  
        # 选择概率最高的token,这是贪婪搜索的实现  
        _, new_token = torch.max(projection, dim=1)  
        new_token = torch.empty(1,1).type_as(encoder_input).fill_(new_token.item()).to(device)  
  
        # 将新token添加回解码器输入  
        decoder_input = torch.cat([decoder_input, new_token], dim=1)  
  
        # 检查新token是否是结束token  
        if new_token == tokenizer_my.token_to_id('[SEP]'):  
            break  
    # 最终的解码器输出是直到结束token的解码器输入的连接  
    decoder_output = decoder_input.squeeze(0)  
    model_predicted_text = tokenizer_my.decode(decoder_output.detach().cpu().numpy())  
  
    return model_predicted_text  

如何学习大模型 AI ?

由于新岗位的生产效率,要优于被取代岗位的生产效率,所以实际上整个社会的生产效率是提升的。

但是具体到个人,只能说是:

“最先掌握AI的人,将会比较晚掌握AI的人有竞争优势”。

这句话,放在计算机、互联网、移动互联网的开局时期,都是一样的道理。

我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。

我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在人工智能学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多互联网行业朋友无法获得正确的资料得到学习提升,故此将并将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。

在这里插入图片描述

第一阶段(10天):初阶应用

该阶段让大家对大模型 AI有一个最前沿的认识,对大模型 AI 的理解超过 95% 的人,可以在相关讨论时发表高级、不跟风、又接地气的见解,别人只会和 AI 聊天,而你能调教 AI,并能用代码将大模型和业务衔接。

  • 大模型 AI 能干什么?
  • 大模型是怎样获得「智能」的?
  • 用好 AI 的核心心法
  • 大模型应用业务架构
  • 大模型应用技术架构
  • 代码示例:向 GPT-3.5 灌入新知识
  • 提示工程的意义和核心思想
  • Prompt 典型构成
  • 指令调优方法论
  • 思维链和思维树
  • Prompt 攻击和防范

第二阶段(30天):高阶应用

该阶段我们正式进入大模型 AI 进阶实战学习,学会构造私有知识库,扩展 AI 的能力。快速开发一个完整的基于 agent 对话机器人。掌握功能最强的大模型开发框架,抓住最新的技术进展,适合 Python 和 JavaScript 程序员。

  • 为什么要做 RAG
  • 搭建一个简单的 ChatPDF
  • 检索的基础概念
  • 什么是向量表示(Embeddings)
  • 向量数据库与向量检索
  • 基于向量检索的 RAG
  • 搭建 RAG 系统的扩展知识
  • 混合检索与 RAG-Fusion 简介
  • 向量模型本地部署

第三阶段(30天):模型训练

恭喜你,如果学到这里,你基本可以找到一份大模型 AI相关的工作,自己也能训练 GPT 了!通过微调,训练自己的垂直大模型,能独立训练开源多模态大模型,掌握更多技术方案。

到此为止,大概2个月的时间。你已经成为了一名“AI小子”。那么你还想往下探索吗?

  • 为什么要做 RAG
  • 什么是模型
  • 什么是模型训练
  • 求解器 & 损失函数简介
  • 小实验2:手写一个简单的神经网络并训练它
  • 什么是训练/预训练/微调/轻量化微调
  • Transformer结构简介
  • 轻量化微调
  • 实验数据集的构建

第四阶段(20天):商业闭环

对全球大模型从性能、吞吐量、成本等方面有一定的认知,可以在云端和本地等多种环境下部署大模型,找到适合自己的项目/创业方向,做一名被 AI 武装的产品经理。

  • 硬件选型
  • 带你了解全球大模型
  • 使用国产大模型服务
  • 搭建 OpenAI 代理
  • 热身:基于阿里云 PAI 部署 Stable Diffusion
  • 在本地计算机运行大模型
  • 大模型的私有化部署
  • 基于 vLLM 部署大模型
  • 案例:如何优雅地在阿里云私有部署开源大模型
  • 部署一套开源 LLM 项目
  • 内容安全
  • 互联网信息服务算法备案

学习是一个过程,只要学习就会有挑战。天道酬勤,你越努力,就会成为越优秀的自己。

如果你能在15天内完成所有的任务,那你堪称天才。然而,如果你能完成 60-70% 的内容,你就已经开始具备成为一名大模型 AI 的正确特征了。

这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值