如何创建一个 GPT 模型(字符级别)

0.GPT 模型概述

GPT 模型是 Generative Pretrained Transformer 的缩写,是专为生成类似人类的文本而设计的高级深度学习模型。这些由 OpenAI 开发的模型已经进行了多次迭代:GPT-1、GPT-2、GPT-3,以及最近的 GPT-4。

GPT 是一种基于 transformer 架构的 AI 语言模型,该模型经过预训练、生成、无监督,能够在零/一/少多任务设置中表现良好。它从 NLP 任务的标记序列中预测下一个标记(字符序列的实例),它尚未经过训练。在只看到几个例子之后,它可以在某些基准测试中达到预期的结果,包括机器翻译、问答和完形填空任务。GPT 模型主要基于条件概率计算一个单词出现在另一个文本中的可能性,因为它出现在另一个文本中。例如,在句子中,“玛格丽特正在组织车库销售……也许我们可以买那个旧的……”椅子这个词可能比“大象”这个词更合适。此外,转换器模型使用多个称为注意力块的单元来学习要关注文本序列的哪些部分。一个转换器可能有多个注意力块,每个注意力块学习一门语言的不同方面。

此外,GPT 模型还具有许多功能,例如生成前所未有的高质量合成文本样本。如果用输入启动模型,它将生成一个较长的延续。GPT 模型在不使用特定领域训练数据的情况下,优于在维基百科、新闻和书籍等领域训练的其他语言模型。GPT 仅从文本中学习语言任务,例如阅读理解、总结和问答,而无需特定任务的训练数据。这些任务的分数(“分数”是指模型分配的数值,用于表示给定输出或结果的可能性或概率)不是最好的,但它们表明具有足够数据和计算的无监督技术可以使任务受益。

以下测试环境为 ubuntu 22.04.03 tls

1.数据集准备

1.0 获取数据

使用Tiny Shakespeare 数据集

1.1 读取 Tiny Shakespeare 数据集,并打印数据集长度

setup1.1.py 代码如下:

运行 python setup1.1.py

1.2 统计数据集中都包含哪些字符种类:

setup1.2.py 代码如下:

运行 python setup1.2.py

2.Tokenize

Tokenize 指的是,将原始文本转化为一些数值的序列,即 Token 序列。

如何将自然语言文本变为 Token 序列,有很多高级算法,比如:google/sentencepieceopenai/tiktoken

前面说到,本文中采用的基于字符级别的语言模型,它的 Tokennizer 算法十分简单。前面代码中的 chars 包含了语料中所有字符的种类,给出一个字符,只要看该字符在 chars 中的 indexOf,就得到了一种数值化的方法。

2.1 将上面的词汇表(chars)映射为整数

stoi 字符到整数的映射,itos 整数到字符的映射。encode 和 decode 分别是对字符串的编解码。

setup2.1.py 代码如下:

运行 python setup2.1.py

在上面代码中,实现了一个编解码方法,能够将文本编码为 “Token” 序列。之所以 Token 要打引号,因为在基于字符级别的粒度下,算法直观但是过于简单。还有一点需要注意的是,接下来使用的自然语言,必须是 chars 中的字符,不能超出这个范围。

不论是高级算法还是本文中的简化方法:原理都是一样的,将文本转为数值序列

2.2 以 tiktoken(有 50257 种 Tokens)为例:

setup2.2.py 代码如下:

运行 python setup2.2.py

这里可能需要你安装 tiktoken 模块

可以看到,使用起来与本文是一样的。但是:

2.3 将整个 Tiny Shakespeare 编码后,转为 PyTorch 序列

setup2.3.py 代码如下:

运行 python setup2.3.py

这里可能需要你安装 torch 模块

3. 训练集、验证集、数据切分

Tiny Shakespeare 的前 90% 用于训练,后 10% 用于验证

分片(Chunk)称之为 block,分片的大小称之为 block_size

setup3.1.py 代码如下:

运行 python setup3.1.py

这里取了训练集中的第一个 Block。block_size 大小为 8,为什么我们取了 9 个 Token 呢?

为了理解这个问题,首先看训练方式,将 Chunck 拆分为两个子集 x 和 y,其中 x 表示输入 Token 序列,在使用时是累增的,y 表示基于该输入,与其的输出。

setup3.2.py 代码如下:

运行 python setup3.2.py

给出一个 Block,分为几轮。第一轮,用第一个 Token 推测第二个 Token。第二轮,用前两个 Token推测第三个 Token。以此类推,到了第八轮,用前八个 Token 推测第九个 Token。

block_size 大小为 8,表示我们的最大训练长度为 8。每一批数据有 9 个元素,其中第九个元素不参与训练,只参与验证。

我们将 Tiny Shakespeare 切分称一系列 Block,就相当于一系列考试题,每道题是一个长度为 9 的连续 Token 序列,按照上图方式,考语言模型。

4. Batch 划分

将训练集进行 Block 切分后,我们可以一个一个向 GPU 投喂(训练)。但是,我们想,GPU 什么能力最强大?并行计算能力!一个一个向 GPU 投喂喂不饱。为了能够充分发挥出 GPU 的并行运算能力,我们将多个 Block 打包成一批(Batch),一批一批向 GPU 投喂。总之一句话,不能让 GPU 闲着,提升训练效率。

值得一提的是,尽管一个 Batch 内的 Blocks 是一批进入 GPU 的,但是它们之间相互隔离,互相不知道对方的存在,互不干扰

setup4.1.py 代码如下:

运行 python setup4.1.py

从代码中可以看出:Block 的大小为 8。Batch 的大小为 4,即一个 Batch 包含 4 个 Blocks。另外锁定了随机数种子为 1337,这样我们都能复现跟 Andrej Karpathy 一样的训练效果。

上述代码,运行后的日志输出如上。可以看出:输入由 1 个 8 元素向量变为 4 个。验证向量也变为 4 个。都 Batch 化了。在后续的推理释义中,也是将 Batch 内每个 Block 的推理过程打印出来。

5. BigramLanguageModel V1

二元语言模型(BigramLanguageModel),概括说:根据前一个词,来推测下一个词。举例来说:例如,对于句子 “I love to play football”,会得到以下的词组:”I love”, “love to”, “to play”, “play football”。

接下来,我们来实现第一个 BigramLanguageModel,与视频不同之处在于,我称之为 BigramLanguageModelV1,后续每进行一次更改,都会创建一个新类,并提升版本。

模型继承自 Pytorch 的 Module,在构造方法中声明模型内部包含的层。可见该模型只有一层(nn.Embedding)。模型还包括 forward,前向传播过程,用于训练。模型一旦训练好后,通过 generate 可进行文本生成。

setup5.1.py 代码实现如下:

运行 python setup5.1.py

logits 是模型做出预测前的一组未经归一化的分数,反映了不同结果的相对可能性。如何理解 logits 的 shape 呢?xb(4×8)的每个元素(Token,字母在词汇表中的排序),在 forward 中,都要输入 nn.Embedding,得到一个大小为 65(词汇表大小)的向量。该向量中的每个元素,表示有当前 Token,推测出该向量表示 Token 的可能性(未归一化)。

下面以第一个词为例,它的长度为 65 的表示 65 种字符可能性的向量为:

setup5.2.py 完整的代码如下:

运行 python setup5.2.py

其中,值最大的元素的序号,就是最可能的那个字母。注意,模型还没有进行任何训练,处于神经错乱状态,预测地与事实不符是正常的。

有了这个词嵌入向量后,计算出模型与 Target(事实的下一个 Token)之间的误差了。这里使用了交叉熵误差。

前向传播过程看完了,接下来尝试调用 generate 进行一次文本生成:

setup5.3.py 完整的代码如下:

运行 python setup5.3.py

6. BigramLanguageModel V1 训练

使用如下代码对模型进行一万次训练,用训练后模型,生成一个长度为 100 的序列看看

setup6.1.py 完整的代码如下:

运行 python setup6.1.py

尽管还是乱码,但是有点 Tiny Shakespeare 剧本对话的意思了。

再训练一万次(累计 2w 次)

setup6.1.py 修改后代码如下:

运行 python setup6.1.py

训练10万次

setup6.1.py 修改后代码如下:

运行 python setup6.1.py

7. 使用矩阵乘法实现累增运算

一个示例,展示如何通过矩阵乘法进行‘加权聚合

setup7.1.py 完整的代码如下:

运行 python setup7.1.py

GPT 中包含 Attention 自注意力机制(self-attention),简单来说,对上面的每一轮进行加权:

如何实现上述累增运算呢?一种直观方法是使用循环,但是这样效率低。The mathematical trick 指的就是使用一个矩阵运算来替代循环,矩阵运算效率更高,啪的一下就全算完了。

这里使用的矩阵是三角阵,下三角是权重,上三角都是 0:

我们以这个阵的每一行,与 idx 列向量相乘,是不是就把这一轮中的头几个元素,与权重相乘了?

为此,引入一个新的超参数 Channels:

“Channel” 参数指的是在神经网络,尤其是在处理自注意力(Self-Attention)机制时,数据的一个维度,它表示输入数据中的特征数量

例如,在计算机视觉任务中,对于彩色图像,常见的通道数为3,分别代表红、绿、蓝(RGB)颜色通道。在自然语言处理(NLP)和Transformer模型的上下文中,“Channel” 通常指的是嵌入向量(embedding vector)的维度,或者说,每个单词或标记(token)被表示成的向量的大小。这些嵌入向量是高维空间中的点,每一个维度(或”channel”)可以被看作是捕捉输入数据中某种特定方面的特征。

这里以通道数 C=2 作为示意。

下面介绍两种矩阵运算方法。第一种运算:

setup7.2.py 完整的代码如下:

运行 python setup7.2.py

这段代码的主要目的是创建一个下三角矩阵,并用它来对输入数据x进行加权平均。

这样,便完成了对输入序列的每轮累增处理,并在每轮累进中进行加权。下面再介绍第二种等效运算:

setup7.3.py 完整的代码如下:

运行 python setup7.3.py

8. 实现 Masked Self-Attention

下面来实现 Masked Self-Attention。

setup8.1.py 完整的代码如下:

运行 python setup8.1.py

这段代码实现了一个带有掩码的自注意力(Masked Self-Attention)机制。Masked Self-Attention 允许模型在处理序列数据时,仅考虑当前位置之前的信息,常用于如生成文本的任务中,以避免未来信息的泄露。

自注意力机制的三个核心组件:查询(query)、键(key)和值(value),它们都来源于同一个输入数据 x。这里使用 nn.Linear() 对每个组件进行线性变换(映射),以生成不同的表示空间。这是实现注意力机制的标准做法,通过这种方式,可以让模型学习到如何最有效地表示数据。

如果移除用于将权重矩阵 wei 中特定位置设置为负无穷的代码行(wei.masked_fill(tril == 0, float('-inf'))),那么该实现将不再是一个带有掩码的自注意力机制,而是变回一个标准的自注意力机制。标准的自注意力允许每个序列元素“注意”序列中的所有其他元素,而不是仅仅是之前的元素。

自注意力机制的一个关键特性:查询(query)、键(key)和值(value)向量都来源于同一个输入 x。这意味着自注意力机制能够在输入数据的内部找到元素之间的关系。

注:如果将 query输入为x,key,value输入为 y,便成为另一种注意力机制——交叉注意力(cross-attention)。在交叉注意力设置中,查询(query)向量来自于一个输入(例如 x),而键(key)和值(value)向量来自于另一个不同的输入(例如 y)。这种机制常用于处理两种不同的序列,例如在机器翻译任务中,模型需要考虑源语言句子(作为 x)和目标语言句子(作为 y)之间的关系。

这段代码中,最后的几行代码(从生成下三角阵到 softmax normalize)我们已经比较熟悉了。新增的部分是引入自注意力的 query, key, value,构成了一个单头的自注意力机制。注:可以看到 Channels 变成了 32,词向量多大,这里的 C 就跟着多大。

9. Weight Normalization for Softmax

在原版论文的公式中,有一个分母:

Softmax函数:Softmax函数是一种将实数向量转换为概率分布的函数。对于任意实数向量,Softmax函数会压缩每个元素的范围到[0, 1],并且使得所有元素的和为1。这在多类分类问题中非常有用,特别是在模型的输出层,可以用来代表概率分布。

注意力机制中的Softmax:在注意力机制中,Softmax用于计算注意力权重,即确定在生成输出时应该给予序列中每个元素多少“注意力”。通过Softmax,模型能够决定在聚合信息时对哪些元素给予更多的重视。

Weight Normalization for Softmax:权重正规化是一种技术,用于调整权重向量的尺度,使其具有一定的统计性质(例如,使方差为1)。在注意力机制的上下文中,这是通过调整查询(query)和键(key)的点积结果来实现的,从而影响Softmax函数的输入。

为什么需要权重正规化?

  • 避免Softmax饱和:如果没有正规化,当head_size(即,每个注意力头的维度)很大时,查询和键的点积结果可能会非常大,导致Softmax输入的值域过大。这会使得Softmax函数的输出变得极端,即大多数的注意力权重都集中在少数几个值上,而其他值几乎被忽略。
  • 保持梯度稳定:通过控制Softmax输入的尺度,可以帮助保持梯度的稳定性,从而避免训练过程中的梯度爆炸或消失问题。

如何实现权重正规化?

  • 权重正规化可以通过除以 dk^1/2来实现,其中 dk 是head_size。这个操作确保了当head_size很大时,点积结果的方差大约是1,从而缩小了Softmax输入的值域。

对应的代码实现如下:

10. 单头自注意力模块

基于前面的知识储备,单头注意力模块实现如下:

其中,引入了 Dropout,在训练时随机丢掉部分权重,来提升训练效果,避免 overfiting.

11. 多头自注意力模块

组装多个单头自注意力模块,便得到了多头自注意力模块:

多头自注意力通过并行运行多个自注意力机制来增加模型的表达能力。每个头关注输入数据的不同部分,从而能够捕获不同的信息和特征。这些不同头的输出会有不同的表示空间和维度。通过拼接这些输出,我们获得了一个综合了所有头信息的表示,但这个综合后的表示的维度会比原始输入大。

线性变换(self.proj)在这里的作用是将这个维度更大的表示压缩回原始输入数据的维度。这不仅使得多头自注意力模块的输出可以无缝地融入后续层,而且还通过这个过程整合了来自不同头的信息,增强了模型对输入数据的理解能力。

此外,线性变换还提供了额外的参数,为模型的学习提供了更多的灵活性和能力,有助于模型更好地拟合和理解数据。通过训练,这些参数可以调整以优化模型的性能,从而提高模型对于特定任务的准确性和效率。

12. FeedForward Layer

对多头自注意力模块进行整合:

13. LayerNorm

关于 LayerNorm(层归一化)具体可点击阅读笔记。

14. Positional encoding

Attention 机制通过注意到序列中的其它元素实现了能力提升。但是,Attention 本身是不考虑元素在序列中的顺序的。Positional Encoding 可解决这一问题。

在视频给出了一种位置编码方法,使用 torch.arrage 与 nn.Embedding 生成位置向量

其中:

  • 单词嵌入(Token Embeddings)tok_emb = self.token_embedding_table(idx) 这一行代码通过查找嵌入表将输入的单词索引idx转换成对应的嵌入向量tok_emb。嵌入表是一个预先训练好的,可以将每个唯一单词映射到一个高维空间中的向量的表。这里的(B,T,C)表示批次大小为B,序列长度为T,嵌入维度为C。
  • 位置嵌入(Positional Embeddings)pos_emb = self.position_embedding_table(torch.arange(T, device=device)) 这行代码生成一个位置嵌入,其中torch.arange(T)生成一个从0到T-1的序列,对应于输入序列中每个位置的索引。self.position_embedding_table是一个预先定义的嵌入表,它将这些位置索引映射到C维的向量上,这样每个位置就有了自己的位置嵌入。这个嵌入向量能够代表或编码该位置在序列中的相对或绝对位置信息。
  • 合并嵌入x = tok_emb + pos_emb 最后,通过将单词嵌入和位置嵌入相加,为每个单词生成了一个包含了位置信息的最终嵌入。这个操作确保了模型的输入既包含了单词的语义信息(通过单词嵌入),也包含了单词的位置信息(通过位置嵌入)。这样,即使在处理序列的时候,模型也能够识别出单词的顺序,从而更好地理解语言或序列数据的结构和含义。

15. GPT Block 组件

GPT 是由多个 Block 组件串起来的。(注,这里说的 Block 不是前面的序列切片,这里指 GPT 的组成模块)。它的构造如下:

  • 加入多头自注意力
  • 多头自注意力后面加 Feed Forward Layer
  • 加入 residual connection(残差连接)
  • 加入 LayerNorm,Pre-LayerNorm,其中,后者是在进入多头自注意力之前,就先进行层归一化

具体代码实现如下:

16. 基于 BigramLanguageModel 魔改 GPT

接下来,我们基于已有 BigramLanguageModel 的框架,加上前面几节中的知识,魔改出 GPT:

17. 训练 GPT

下面是完整的训练代码

这个脚本概述了使用简化的Transformer架构创建和训练一个字符级别的语言模型的过程。该模型在一个数据集上进行训练,这个数据集很可能来自于Tiny Shakespeare语料库,以生成类似风格的文本。以下是脚本中关键组件和过程的逐步解析:

  1. 超参数设置:定义了批处理大小、块大小、学习率以及模型架构细节,如嵌入层的数量、头部数、层数和丢弃率等训练参数。
  2. 数据准备
    • 从文件中加载文本数据。
    • 从文本中创建一个独特字符的词汇表,并将字符映射为整数(以及相反的映射)以便处理。
    • 将数据分割为训练集和验证集。
  3. 批处理准备:实现了一个函数,为训练和验证生成数据批次。每个批次由输入序列及其对应的目标序列组成,目标序列本质上是输入序列向右移动一个字符。
  4. 模型组件:定义了Transformer模型的关键组成部分:
    • Head:实现了单个自注意力头。
    • MultiHeadAttention:汇总多个自注意力头。
    • FeedForward:一个简单的线性层,后跟ReLU激活。
    • Block:将注意力和前馈组件组合成单个Transformer块。
  5. 模型架构:使用嵌入层构建语言模型,用于令牌和位置嵌入,多个Transformer块,最后的层正则化,以及一个线性层来预测下一个字符。
  6. 损失估计:定义了一个函数,以在不更新模型权重的情况下,评估模型在训练和验证集上的性能。
  7. 训练循环:通过以下步骤迭代训练模型:
    • 抽取数据批次。
    • 计算损失。
    • 执行反向传播。
    • 更新模型的权重。
    • 定期在训练和验证集上评估模型,以监控性能。
  8. 文本生成:实现了一种方法,从给定开始上下文的模型中生成文本。它通过从模型的预测中采样,迭代添加新字符来扩展上下文,以生成指定长度的序列。
  9. 执行:最后,脚本初始化模型,将其移动到适当的设备(如果可用,则为GPU),并打印出模型的参数数量。然后进入训练循环,定期报告损失,完成后,从训练好的模型生成文本序列。

该脚本展示了如何实现一个基于Transformer的模型,用于字符级文本生成任务,凸显了Transformer架构对于序列建模任务的灵活性和有效性。

setup17.1 完整代码

运行 setup17.1

运行 setup17.1

18.保存 model

setup18.1 完整代码

运行 setup18.1.py

step 49000: train loss 1.2699, val loss 1.5956
step 49999: train loss 1.2681, val loss 1.5861

What thy bridal?

STANLEY:
He madest my best eyes.
One stride, and be that fought thee by their
caues with my face, and zoke he on
the office commandion will beg it ended mine
Stirs in oversumed; the next is waked. Anly die;
For humble comforwater and plaw you:
The sensemoumes are we’ll like to thee
To Wicknes do evimes to them, and Tieuted Keep ContemenDusgued.

参考链接:

https://medium.com/@fareedkhandev/understanding-transformers-a-step-by-step-math-example-part-1-a7809015150a
https://medium.com/@fareedkhandev/create-gpt-from-scratch-using-python-part-1-bd89ccf6206a
https://www.leewayhertz.com/build-a-gpt-model/
https://garden.maxieewong.com/087.%E8%A7%86%E9%A2%91%E5%BA%93/YouTube/Andrej%20Karpathy/Let’s%20build%20GPT%EF%BC%9Afrom%20scratch,%20in%20code,%20spelled%20out./
https://colab.research.google.com/drive/1JMLa53HDuA-i7ZBmqV7ZnA3c_fvtXnx-?usp=sharing#scrollTo=nql_1ER53oCf

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部