60行代码就能构建GPT!网友:比之前的教程都要清晰|附代码

  Pine 发自凹非寺

  量子位公众号 QbitAI

  现在只用 60 行代码,就能从 0 构建 GPT 了!

图片

  想当初,前特斯拉前 AI 总监的 minGPT 和 nanoGPT 也都还要 300 行代码。

  这个 60 行代码的 GPT 也有名字,博主将它命名为 PicoGPT。

  不过和此前 minGPT 和 nanoGPT 的教程不同,今天要讲的这个博主的教程,更侧重于代码实现部分,模型的权重则用已经训练好的。

  对此,博主解释称这篇教程的重点在于提供一个简单且易于破解的完整技术介绍。

  这对还不理解 GPT 背后概念的盆友,算是非常友好了。

图片

  还有网友称赞,这篇博客介绍得非常清晰,第一部分尤为如此。

这篇介绍 GPT 模型的文章太好了,它比我之前看到的介绍都要清晰,至少在第一部分讨论文本生成和取样是这样的。

图片

  目前,此项目在 GitHub 上标星已破百,HackerNews 上的点击量也即将破千。

图片

  从 GPT 是什么讲起

  在介绍之前,还是需要说明一下,这篇教程不是完全零门槛,需要读者提前熟悉 Python、NumPy 以及一些基本的训练神经网络。

  教程的重点聚焦在技术介绍上,统共有六大部分:

  什么是 GPT?

  按照惯例,在正式构建 GPT 之前得先对它做一些基本介绍,教程从输入/输出、生成文本以及训练三个部分分别来讲 GPT 是如何工作的。

图片

  在这趴,博主附上代码,甚至还用了一些比喻来让读者们更好地理解 GPT。

  举个栗子,在输入这一部分,作者将句子比作一条绳子,tokenizer 则会将其分割成一小段一小段(单词),被称作 token。

  又比如说,在生成文本这 part 介绍自动回归时,博主直接贴上代码:

def generate (inputs, n_tokens_to_generate):
    for _ in range (n_tokens_to_generate): # auto-regressive decode loop
        output = gpt (inputs) # model forward pass
        next_id = np.argmax (output[-1]) # greedy sampling
        inputs = np.append (out, [next_id]) # append prediction to input
    return list (inputs[len (inputs) - n_tokens_to_generate :])  # only return generated ids

input_ids = [1, 0] # "not" "all"
output_ids = generate (input_ids, 3) # output_ids = [2, 4, 6]
output_tokens = [vocab[i] for i in output_ids] # "heroes" "wear" "capes"

  在每次迭代中,它会将预测的 token 追加回输入,这个预测未来值并将其添加回输入的过程就是 GPT 被描述为自动回归的原因。

  60 行代码怎么运行?

  了解完 GPT 的基本概念之后,就直接快进到了如何在电脑上运行这个 PicoGPT。

  博主先是甩出了他那只有 60 行的代码:

import numpy as np


def gpt2(inputs, wte, wpe, blocks, ln_f, n_head):
    pass # TODO: implement this


def generate (inputs, params, n_head, n_tokens_to_generate):
    from tqdm import tqdm

    for _ in tqdm (range (n_tokens_to_generate), "generating"):  # auto-regressive decode loop
        logits = gpt2(inputs, **params, n_head=n_head)  # model forward pass
        next_id = np.argmax (logits[-1])  # greedy sampling
        inputs = np.append (inputs, [next_id])  # append prediction to input

    return list (inputs[len (inputs) - n_tokens_to_generate :])  # only return generated ids


def main (prompt: str, n_tokens_to_generate: int = 40, model_size: str = "124M", models_dir: str = "models"):
    from utils import load_encoder_hparams_and_params

    # load encoder, hparams, and params from the released open-ai gpt-2 files
    encoder, hparams, params = load_encoder_hparams_and_params (model_size, models_dir)

    # encode the input string using the BPE tokenizer
    input_ids = encoder.encode (prompt)

    # make sure we are not surpassing the max sequence length of our model
    assert len (input_ids) + n_tokens_to_generate < hparams["n_ctx"]

    # generate output ids
    output_ids = generate (input_ids, params, hparams["n_head"], n_tokens_to_generate)

    # decode the ids back into a string
    output_text = encoder.decode (output_ids)

    return output_text


if name == "__main__":
    import fire

    fire.Fire (main)

  然后从克隆存储库,安装依赖项等步骤一步步教你如何在电脑上运行 GPT。

  其中,还不乏一些贴心的小 tips,比如说如果使用的是 M1 Macbook,那在运行 pip install 之前,需要将 requments.txt 中的 tensorflow 更改为 tensorflow-macos。

  此外,对于代码的四个部分:gpt2,generate,main 以及 fire.Fire (main),博主也有做详细解释。

  等到代码能够运行之后,下一步博主就准备详细介绍编码器、超参数(hparams)以及参数(params)这三部分了。

图片

  直接在笔记本或者 Python 会话中运行下面这个代码:

from utils import load_encoder_hparams_and_params
encoder, hparams, params = load_encoder_hparams_and_params ("124M", "models")

  Bingo!一些必要的模型和 tokenizer 文件就直接下载到 model/124M,编码器、hparams 和 params 也能直接加载。

  更具体的内容这里就不多说了,教程的链接已经附在文末。

  一些基础神经网络层的介绍

  这一趴涉及到的知识就更加基础了,因为下一趴是实际 GPT 自身的架构,所以在此之前,需要了解一些非特定于 GPT 的更基本的神经网络层

  博主介绍了 GeLU、Softmax 函数以及 Layer Normalization 和 Linear。

图片

  GPT 架构

  终于!这部分要来讲 GPT 自身的架构了,博主从 transformer 的架构引入。

图片

  transformer 架构

  GPT 的架构只使用了 transformer 中的解码器堆栈(即图表的右边部分),并且其中的的“交叉注意”层也没有用到。

图片

  △GPT 架构

  随后,博主将 GPT 的架构总结成了三大部分:

  • 文本 + 位置嵌入

  • 变压器解码器堆栈

  • 下一个 token 预测头

  并且还将这三部分用代码展示了出来,是酱紫的:

def gpt2(inputs, wte, wpe, blocks, ln_f, n_head):  # [n_seq] -> [n_seq, n_vocab]
    # token + positional embeddings
    x = wte[inputs] + wpe[range (len (inputs))]  # [n_seq] -> [n_seq, n_embd]

    # forward pass through n_layer transformer blocks
    for block in blocks:
        x = transformer_block (x, block, n_head=n_head)  # [n_seq, n_embd] -> [n_seq, n_embd]

    # projection to vocab
    x = layer_norm (x, ln_f)  # [n_seq, n_embd] -> [n_seq, n_embd]
    return x @ wte.T  # [n_seq, n_embd] -> [n_seq, n_vocab]

  再后面,就是关于这三部分的更多细节……

  测试构建的 GPT

  这部分将全部的代码组合在一起,就得到了 gpt2.py,统共有 120 行代码,删除注释和空格的话,就是 60 行。

  然后测试一下!

python gpt2.py \
    "Alan Turing theorized that computers would one day become" \
    --n_tokens_to_generate 8

  结果是这样的:

the most powerful machines on the planet.

  成功了!

  一些后续补充

  最后一部分,博主也总结了这短短 60 行代码的不足:非常低效!

  不过他还是给出了两个可以让 GPT 变高效的方法:

  •   同时地而不是顺序地执行注意力计算。

  •   实现 KV 缓存。

  此外,博主还推荐了一些训练模型、评估模型以及改进架构的方法和教程。

  感兴趣的话,直接戳文末链接~

  作者介绍

  Jay Mody,目前在加拿大一家 NLP 初创公司 Cohere 从事机器学习的工作,此前,他还分别在特斯拉和亚马逊作为软件工程师实习过一段时间。

图片

  除了这篇教程之外,小哥的博客网站上还有更新其他文章,并且都有附代码~

图片

  代码传送门:

https://github.com/jaymody/picoGPT/blob/29e78cc52b58ed2c1c483ffea2eb46ff6bdec785/gpt2_pico.py#L3-L58

  教程链接:

https://jaykmody.com/blog/gpt-from-scratch/#putting-it-all-together