ChatGPT|从0构建GPT-工程实践篇

从0构建GPT在普通pc上执行可能不?确实可以,之前Andrej Karpathy(前特斯拉AI总监,李飞飞高徒)就有一个视频如何实现。
于是我尝试了一下使用Karpathy的代码,从工程实践上来分析如何将只有代码到训练原始数据最后变成一个简易版的GPT。

1、环境

(1)下载代码

Git clone https://github.com/karpathy/nanoGPT.git

(2)安装库

由于nanoGPT代码没有requirements.txt文件,大家可以使用我用pipreqs生成的

datasets==2.11.0
numpy==1.23.5
requests==2.28.2
tiktoken==0.3.3
torch==2.0.0
tqdm==4.64.1
transformers==4.26.1
wandb==0.14.0

将以上文件写入到requirements.txt文件中,然后执行:

Python install -r requirements.txt

2、训练步骤

nanoGPT使用的是莎士比亚的作品,其中大概为1M文件,但是这里我想训练中文,所以选择鲁迅的《故事新篇》。

(1)准备训练数据

先新建一个luxunluxun_char文件夹,实现prepare.py,然后预处理,命令如下:

python data/luxun_char/prepare.py

输出:

vocab size: 2,950 # 文件大小
train has 77,688 tokens # 训练的token数
val has 8,633 tokens # 验证的token数

(2)如果您有GPU可以执行

python train.py config/train_luxun_char.py
python sample.py --out_dir=out-luxun-char

(3)如果没有GPU,使用cpu方式

为了训练更快,我们将默认参数做了修改:

eval_iters = 200
block_size = 256
n_layer = 6
n_head = 6
n_embd = 384
dropout = 0.2
max_iters = 5000
lr_decay_iters = 5000  # make equal to max_iters usually

噪音大一点但更快的估计,设置–eval_iters=20;
上下文大小只有64个字符,并且每次迭代的批量大小只有12个示例;
使用更小的Transforme(4 layer、4 head、128 n_embd),并将迭代次数减少到1000;
因为网络层很小,所以也简化了正则化 (–dropout=0.0);
最后命令如下:

python train.py config/train_luxun_char.py --device=cpu --compile=False --eval_iters=20 --log_interval=1 --block_size=64 --batch_size=12 --n_layer=4 --n_head=4 --n_embd=128 --max_iters=1000 --lr_decay_iters=1000 --dropout=0.0
python sample.py --out_dir=out-luxun-char --device=cpu

(4)如果是mac,用mps方式

python train.py config/train_luxun_char.py --device=mps --compile=False --eval_iters=20 --log_interval=1 --block_size=64 --batch_size=12 --n_layer=4 --n_head=4 --n_embd=128 --max_iters=1000 --lr_decay_iters=1000 --dropout=0.0
python sample.py --out_dir=out-luxun-char --device=mps

(5)开始训练

由于我是mac M1机器,所以就直接本地训练,训练输出如下:

step 0: train loss 8.0306, val loss 7.9959
...
iter 988: loss 0.7229, time 978.67ms, mfu 0.08%
iter 989: loss 0.7352, time 967.18ms, mfu 0.08%
iter 990: loss 0.6666, time 1021.36ms, mfu 0.08%
iter 991: loss 0.6828, time 980.37ms, mfu 0.08%
iter 992: loss 0.6305, time 985.37ms, mfu 0.08%
iter 993: loss 0.5959, time 1001.88ms, mfu 0.07%
iter 994: loss 0.6709, time 1008.65ms, mfu 0.07%
iter 995: loss 0.6172, time 931.57ms, mfu 0.07%
iter 996: loss 0.7069, time 968.76ms, mfu 0.07%
iter 997: loss 0.6525, time 1002.15ms, mfu 0.07%
iter 998: loss 0.6313, time 944.04ms, mfu 0.07%
iter 999: loss 0.6986, time 970.70ms, mfu 0.07%
step 1000: train loss 0.6809, val loss 7.0533
iter 1000: loss 0.6475, time 1433.12ms, mfu 0.07%

运行时间持续20分钟,每一轮大概需要花费1000ms,训练损失精度0.6475,验证损失精度7.0533,顺便说一下这两者是什么:

  • train loss是训练集上的损失,衡量模型的拟合能力;
  • val loss是验证集上的损失,衡量是在未见过的数据的拟合能力,也就是泛化;

最后按照鲁迅故事新篇风格输出文章:

    “有好好!你还是这里是不好了!”

    “你的“——我那就好么?”他们看见他的说,只是一个人的说。“不过没有什么呢?”他一不懂。

    “我这是这回来呢,我们一个字的,便是走过去了。”墨子说,“我的人们还是听不见去了,就是下了。自己…”

    “不过去的性情,一是意思,是的话,可是听是不好,所以死时也,是怎么人的时不忙,不肯的竟从鼎毛的…………………”

    “奴倒为这去了呢。”

    “我的。我们是难道这么?”他的说。“我明年就是的品民”

    “没有好像家正是这一个侏儒道的在要死死见,是放下去了。我的下去寻义的脸骨,太太平静的在后面,只能都又不再开口起来,看来,还是走了。先生,可是谁全身子。他并不少,约一想起身。上。他吐得平静的回出好像一个。

    “先生,”老婆子们的时候,说。“那里面,不可以妃子,您只好像我的人,先生的说。”他们就是贱人的学生话。他的义,要说是“你的。我的人,我并没有人来了师和哩!”

    “公输般就是下去了脾气,”老子说。

    “倒倒是要肚子去。有什么?”女道。这才站起来,他们也站在它的想:“你

是不是有点意思~~~,但是实际效果,感觉有点上句不接下句,因此调整网络层数,继续执行:

python train.py config/train_luxun_char.py --device=mps --compile=False --eval_iters=20 --log_interval=1 --block_size=64 --batch_size=12 --n_layer=6 --n_head=6 --n_embd=384 --max_iters=1000 --lr_decay_iters=1000 --dropout=0.0

最后输出虽然train loss在不断收敛到0.1097,但是val loss却一直在7以上。

什么原因呢?

ChatGPT|从0构建GPT-工程实践篇

通常训练过程中会同时计算train loss和val loss,并根据它们来评价模型和调整超参数:

  • 如果train loss迅速下降但val loss基本不变或上升,表示模型过拟合,需要采取正则化等措施改善泛化能力。
  • 如果train loss和val loss都较小且相近,那么模型既在训练数据上拟合良好,又有较强的泛化能力,模型效果较好。
  • 如果train loss和val loss都较大,那么模型的拟合能力和泛化能力都不够,需要进一步优化模型超参数或结构。

如何解决?
结合了一些资料,发现正则化的确可以让模型的泛化性能更好,它是通过在损失函数中加入正则项来惩罚模型的复杂度,从而避免过拟合。
其中正则化方法有如下方案:

  • L1正则化:在损失函数中添加L1范数惩罚项,惩罚模型中参数的绝对值之和,它会让许多参数变为0,产生稀疏的解。
  • L2正则化:在损失函数中添加L2范数惩罚项,惩罚模型中参数的平方和,它会让参数值变小,但不会产生稀疏效果。
  • Dropout:在训练时随机丢弃一定比例的神经节点,防止节点间的依赖关系,增强模型的泛化能力。
  • Early Stopping:提前终止训练,防止模型过拟合。
  • 数据扩增:通过适当倾斜、旋转、裁剪等手段扩充训练数据,减轻过拟合。

为了最快的验证,直接调整dropout即可(可以设置>0.1)。

(5)增加数据集,调整dropout和增加迭代次数

之前样本集数据有点小,又找了《活着》和《围城》这两本书加进去,大概600k的数据,token增加了很多:

vocab size: 3,587  
train has 281,072 tokens  
val has 31,231 tokens 

继续训练:

python train.py config/train_luxun_char.py --device=mps --compile=False --eval_iters=20 --log_interval=1 --block_size=64 --batch_size=12 --n_layer=6 --n_head=6 --n_embd=384 --max_iters=2000 --lr_decay_iters=2000 --dropout=0.5

以下是迭代2000次的输出:

iter 1993: loss 2.3874, time 1645.04ms, mfu 0.44%
iter 1994: loss 2.4565, time 1641.69ms, mfu 0.44%
iter 1995: loss 2.5139, time 1649.36ms, mfu 0.44%
iter 1996: loss 2.4896, time 1644.68ms, mfu 0.44%
iter 1997: loss 2.4910, time 1644.31ms, mfu 0.44%
iter 1998: loss 2.5958, time 1670.09ms, mfu 0.44%
iter 1999: loss 2.3236, time 1650.41ms, mfu 0.44%
step 2000: train loss 1.7447, val loss 5.0064
iter 2000: loss 2.5489, time 2282.96ms, mfu 0.43%

train loss到为1.7447,val loss为5.0064。执行python sample.py --out_dir=out-luxun-char --device=mps输出:

ChatGPT|从0构建GPT-工程实践篇

3、代码解释

(1)准备数据代码prepare.py

# get all the unique characters that occur in this text
chars = sorted(list(set(data)))
vocab_size = len(chars)
print("all the unique characters:"''.join(chars))
print(f"vocab size: {vocab_size:,}")

# create a mapping from characters to integers
stoi = {ch: i for i, ch in enumerate(chars)}
itos = {i: ch for i, ch in enumerate(chars)}

...
train_data = data[:int(n*0.9)]
val_data = data[int(n*0.9):]

# encode both to integers
train_ids = encode(train_data)
val_ids = encode(val_data)
print(f"train has {len(train_ids):,} tokens")
print(f"val has {len(val_ids):,} tokens")

# export to bin files
train_ids = np.array(train_ids, dtype=np.uint16)
val_ids = np.array(val_ids, dtype=np.uint16)
train_ids.tofile(os.path.join(os.path.dirname(__file__), 'train.bin'))
val_ids.tofile(os.path.join(os.path.dirname(__file__), 'val.bin'))
...

首先读取input.txt文件中的数据,并计算数据集中的字符数和唯一字符数。然后,它创建一个从字符到整数的映射,并使用该映射将训练数据和验证数据编码为整数。
最后,代码将编码后的数据保存到二进制文件中,如train.binval.bin,并将编码器和解码器以及其他相关信息保存到pickle(meta.pkl)文件中。文件中存储数据如下:

meta = {
    'vocab_size': vocab_size, # 唯一字符串大小
    'itos': itos, # 位置与字符关系
    'stoi': stoi, # 字符与位置关系
}

(2)训练数据

train.pymodel.py是两个主要的实现文件,整体代码量不大,但是涉及原理细节较多(下一篇将详细讲述),本次主要讲大概流程。

model.py是一个GPT语言模型,主要参考两份代码:
1)OpenAI发布的官方GPT-2 TensorFlow实现:https://github.com/openai/gpt-2/blob/master/src/model.py
2)huggingface/transformers的PyTorch实现:https://github.com/huggingface/transformers/blob/main/src/transformers/models/gpt2/modelinggpt2.py

class CausalSelfAttention(nn.Module):

    def __init__(self, config):
        super().__init__()
        assert config.n_embd % config.n_head == 0
        # key, query, value projections for all heads, but in a batch
        self.c_attn = nn.Linear(config.n_embd, 3 * config.n_embd, bias=config.bias)
        # output projection
        self.c_proj = nn.Linear(config.n_embd, config.n_embd, bias=config.bias)
        # regularization
        self.attn_dropout = nn.Dropout(config.dropout)
        self.resid_dropout = nn.Dropout(config.dropout)
        self.n_head = config.n_head
        self.n_embd = config.n_embd
        self.dropout = config.dropout
        # flash attention make GPU go brrrrr but support is only in PyTorch >= 2.0
        self.flash = hasattr(torch.nn.functional, 'scaled_dot_product_attention')
        if not self.flash:
            print("WARNING: using slow attention. Flash Attention requires PyTorch >= 2.0")
            # causal mask to ensure that attention is only applied to the left in the input sequence
            self.register_buffer("bias", torch.tril(torch.ones(config.block_size, config.block_size))
                                        .view(1, 1, config.block_size, config.block_size))

    def forward(self, x):
        B, T, C = x.size() # batch size, sequence length, embedding dimensionality (n_embd)

        # calculate query, key, values for all heads in batch and move head forward to be the batch dim
        q, k, v  = self.c_attn(x).split(self.n_embd, dim=2)
        k = k.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) # (B, nh, T, hs)
        q = q.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) # (B, nh, T, hs)
        v = v.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) # (B, nh, T, hs)

        # causal self-attention; Self-attend: (B, nh, T, hs) x (B, nh, hs, T) -> (B, nh, T, T)
        if self.flash:
            # efficient attention using Flash Attention CUDA kernels
            y = torch.nn.functional.scaled_dot_product_attention(q, k, v, attn_mask=None, dropout_p=self.dropout if self.training else 0, is_causal=True)
        else:
            # manual implementation of attention
            att = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(k.size(-1)))
            att = att.masked_fill(self.bias[:,:,:T,:T] == 0, float('-inf'))
            att = F.softmax(att, dim=-1)
            att = self.attn_dropout(att)
            y = att @ v # (B, nh, T, T) x (B, nh, T, hs) -> (B, nh, T, hs)
        y = y.transpose(1, 2).contiguous().view(B, T, C) # re-assemble all head outputs side by side

        # output projection
        y = self.resid_dropout(self.c_proj(y))
        return y

这段代码实现了CausalSelfAttention,用于自回归模型中的自注意力机制。

在初始化函数中,它首先断言embedding维度nembd必须是nhead的倍数,然后定义了三个线性变换,用于将输入x映射到query、key和value向量。

接着定义了一个输出变换,将所有头的输出拼接起来并映射回nembd维度。

此外,还定义了两个dropout层,用于正则化,以及一些其他的参数。如果系统不支持Flash Attention,则会使用手动实现的自注意力机制。

在前向传播函数中,它首先将输入x分别映射到query、key和value向量,然后计算自注意力矩阵,最后将所有头的输出拼接起来并映射回nembd维度。

class MLP(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.c_fc    = nn.Linear(config.n_embd, 4 * config.n_embd, bias=config.bias)
        self.c_proj  = nn.Linear(4 * config.n_embd, config.n_embd, bias=config.bias)
        self.dropout = nn.Dropout(config.dropout)

    def forward(self, x):
        x = self.c_fc(x)
        x = new_gelu(x)
        x = self.c_proj(x)
        x = self.dropout(x)
        return x

这段代码实现MLP的类,继承自nn.Module。
在初始化函数中,它定义了三个层:

  • self.cfc是一个线性层,它将输入的维度从config.nembd扩展到4*config.nembd;
  • self.cproj是另一个线性层,它将输入的维度从4*config.nembd缩小回config.nembd;
  • self.dropout是一个dropout层,它将输入随机置零以防止过拟合;在前向函数中,输入首先通过self.cfc和一个新的gelu激活函数,然后通过self.cproj和self.dropout。最后,输出被返回。

class GPT(nn.Module)是最重要的部分,继承自nn.Module的类,它是一个基于Transformer的语言模型,它的主要作用是生成文本。
包含的主要方法如下:

  • get_num_params(self, non_embedding=True):计算模型的参数数量;
  • forward(self, input_ids, position_ids=None, attention_mask=None, past_key_values=None, use_cache=None, output_attentions=None, output_hidden_states=None, return_dict=None):模型的前向传播函数,接收输入的token id序列,返回生成的文本;
  • generate:生成文本的函数,接收输入的token id序列,返回生成的文本。

train.py是训练部分代码,主要功能是生成GPT模型,根据参数决定pytorch的训练方式和计算loss值等,下一篇将详细讲述循环迭代的实现。

(3)运行结果

运行sample.py代码,start是就是输入您想写的起始部分内容。

...

start_ids = encode(start)
x = (torch.tensor(start_ids, dtype=torch.long, device=device)[None, ...])

# run generation
with torch.no_grad():
    with ctx:
        for k in range(num_samples):
            y = model.generate(x, max_new_tokens, temperature=temperature, top_k=top_k)
            print(decode(y[0].tolist()))
            print('---------------')

整个文件首先加载模型,然后根据start生成指定数量的文本样本,生成的文本样本的数量和每个样本中生成的最大标记数都可以通过更改num_samples和max_new_tokens变量来控制。 

此外,还可以通过更改temperature和top_k变量来控制生成的文本的随机性和多样性,和使用OpenAI的接口类似。最后,生成的文本将通过decode函数进行解码,以便进行打印和显示。

4、扩展

(1)使用OpenWebText数据集训练

如果您有A100 40GB的GPU,可以用OpenWebText数据集复现GPT-2 (124M),支持如下几种训练方式:

python train.py eval_gpt2 # 参数:124M
python train.py eval_gpt2_medium # 参数:350M
python train.py eval_gpt2_large # 参数:774M
python train.py eval_gpt2_xl # 参数:1558M

(2)微调

可以输入以下命令用指定的模型微调:

python data/luxun/prepare.py # 先预处理数据
python train.py config/finetune_luxun.py --init_from='gpt2' --device=mps --compile=False # 微调

(3)抽样/推理

python sample.py 
    --start="xx是谁?" 
    --num_samples=5 --max_new_tokens=100 --device=mps --compile=False

不过我用自己模型跑的,给出的结果效果不太好,如果您设备可以尝试gpt2_xl试一下。

注意:其中文件夹或者文件名带{luxun}是我自己新的代码和文件夹,后续也会整理开源,如果您想从0到1训练一个模型,可以用参考代码,用自己的数据集试试。

下一篇是分析基于nanoGPT的原理篇,希望自己不要拖更。


原文始发于微信公众号(周末程序猿):ChatGPT|从0构建GPT-工程实践篇

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/169422.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!