• 我的订阅
  • 科技

高能技巧!60 行 NumPy 代码 从头实现一个 GPT

类别:科技 发布时间:2023-02-15 11:00:00 来源:CSDN

【CSDN 编者按】近日,一名工程师 Jay Mody 在一篇文章汇总将用 60 行 NumPy 代码从头实现一个 GPT。并把 GPT-2 模型权重加载到实现中,从而生成文本。

原文链接:https://jaykmody.com/blog/gpt-from-scratch/

作者 |Jay Mody译者| 禾木木

出品 | CSDN(ID:CSDNnews)

在本篇文章中,作者将用 60 行 NumPy 代码从头实现一个 GPT。并把 GPT-2 模型权重加载到实现中,从而生成文本。

注:这篇文章假设你已熟悉 Python、NumPy 和一些训练神经网络的基本经验。这个实现缺少大量的功能,目的是在保持完整的同时尽可能的简单。我们的目标是为 GPT 作为一种教育工具提供一个简单而完整的技术介绍。

了解 GPT 架构只是 LLM 难题中至关重要的一小部分。

什么是 GPT?

GPT 是 Generative Pre-trained Transformer 的缩写。这是一种基于 Transformer 的神经网络架构。Generative:GPT 生成文本。Pre-trained:GPT 根据大量的书籍、互联网等文本上训练出来。Transformer:GPT 是一个仅有解码器的变换器神经网络。

像 GPT-3、LaMDA 和 Command XLarge 这类的大型语言模型(LLMs)都只是底层的 GPT。它们的特殊之处在于:1)非常大(数十亿的参数);2)在大量的数据上进行训练(数百GB的文本)。

从根本上说,GPT 生成的是有提示的文本,即使有了这个非常简单的 API(input=text,output=text),一个训练有素的 GPT 可以做一些非常棒的事情,例如写一封电子邮件、总结一本书、给 Instagram 起一些标题、向一个 5 岁的孩子解释黑洞,用 SQL 编码,甚至写遗嘱。

这就是对 GPT 及其能力的高级概述。接下来让我们深入了解更多细节。

Input / Output

GPT 的函数签名大致如下所示:defgpt(inputs: list[int])-> list[list[float]]:# inputs has shape [n_seq]# output has shape [n_seq, n_vocab]output = # beep boop neural network magicreturnoutput

Input

Input 是一个整数序列,表示某些文本的标记:#integers represent tokens inour text, forexample:#text = "not all heroes wear capes":#tokens = "not""all""heroes""wear""capes"inputs = [1, 0, 2, 4, 6]

我们基于标记器的词汇量来确定一个指令的整数值:# the index of a token in the vocab represents the integer id for that token# i.e. the integer id for "heroes" would be 2, since vocab[2] = "heroes"vocab= ["all", "not", "heroes", "the", "wear", ".", "capes"]

# a pretend tokenizer that tokenizes on whitespacetokenizer= WhitespaceTokenizer(vocab)

# the encode method converts a str -> list[int]ids= tokenizer.encode("not all heroes wear") # ids = [1, 0, 2, 4]

# we can see what the actual tokens are via our vocab mappingtokens= [tokenizer.vocab[i] for i in ids] # tokens = ["not", "all", "heroes", "wear"]

# the decode method converts back a list[int] -> strtext= tokenizer.decode(ids) # text = "not all heroes wear"

简而言之:我们有一个字符串我们使用一个标记器将其分解成更小的部分,称为指令(tokens)我们使用词汇表将这些标记映射成整数。

在实践中,我们使用更先进的标记化方法,而不是简单地通过空白分割,例如 Byte-Pair Encoding 或 WordPiece,但原理是一样的:有一个词汇表,将字符串标记映射为整数索引有一个编码方法可以将str->list[int]转换。有一个解码方法可以将list[int] -> str转换。

Output

Output 是一个二维数组,其中 output[i][j] 是模型的预测概率,即 vocab[j] 的令牌是下一个指令 inputs[i+1]。例如:vocab = ["all", "not", "heroes", "the", "wear", ".", "capes"]inputs = [1, 0, 2, 4] # "not""all""heroes""wear"output = gpt(inputs)# ["all", "not", "heroes", "the", "wear", ".", "capes"]# output[0] = [0.75 0.1 0.0 0.15 0.0 0.0 0.0 ]# given just "not", the model predicts the word "all"with the highest probability

# ["all", "not", "heroes", "the", "wear", ".", "capes"]# output[1] = [0.0 0.0 0.8 0.1 0.0 0.0 0.1 ]# given the sequence ["not", "all"], the model predicts the word "heroes"with the highest probability

# ["all", "not", "heroes", "the", "wear", ".", "capes"]# output[-1] = [0.0 0.0 0.0 0.1 0.0 0.05 0.85 ]# given the whole sequence ["not", "all", "heroes", "wear"], the model predicts the word "capes"with the highest probability

为了获得整个序列的下一个指令预测,我们只需取 output[-1] 中概率最高的一个指令:vocab= ["all", "not", "heroes", "the", "wear", ".", "capes"]inputs= [1, 0, 2, 4] # "not""all""heroes""wear"output= gpt(inputs)next_token_id= np.argmax(output[-1]) # next_token_id = 6next_token= vocab[next_token_id] # next_token = "capes"

将概率最高的指令作为我们的最终预测,通常被称为 greedy decoding 或 greedy sampling。

预测一个序列中的下一个逻辑词的任务被称为语言建模。因此,我们可以把 GPT 称为语言模型。

生成一个词是很酷,但整个句子、段落等呢...?

生成文本

自回归

我们可以通过反复询问模型预测下一个指令来生成完整的句子。在每次迭代时,我们将预测的指令追加到输入中:def generate(inputs, n_tokens_to_generate):for_ inrange(n_tokens_to_generate): # auto-regressive decode loopoutput = gpt(inputs) # model forward passnext_id = np.argmax(output[-1]) # greedy samplinginputs = np.append(out, [next_id]) # append prediction to inputreturnlist(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] fori inoutput_ids] # "heroes""wear""capes"

这个预测未来值(回归),并将其加回输入(自动)的过程就是为什么你可能看到 GPT 被描述为自回归的原因。

采样

我们可以通过从概率分布中抽样而不是贪婪地样,为我们的生成引入一些随机性(随机性):inputs = [1, 0, 2, 4] # "not" "all" "heroes" "wear"output = gpt(inputs)np.random.choice(np.arange(vocab_size), p=output[-1]) # capesnp.random.choice(np.arange(vocab_size), p=output[-1]) # hatsnp.random.choice(np.arange(vocab_size), p=output[-1]) # capesnp.random.choice(np.arange(vocab_size), p=output[-1]) # capesnp.random.choice(np.arange(vocab_size), p=output[-1]) # pants

它不仅允许我们为相同的输入生成不同的句子,而且与 greedy decoding 相比,它还提高了输出的质量。

在抽样之前,使用 top-k、top-p 和温度等技术来修改概率分布也是很常见的。这进一步提高了生成的质量,并引入了超参数,我们可以通过这些参数来获得不同的生成行为(例如,增加温度使我们的模型承担更多的风险,从而更有 "创造性")。

训练

我们像其他神经网络一样训练 GPT,使用梯度下降法来训练一些损失函数。在 GPT 的情况下,我们将交叉熵损失用于语言建模任务:deflm_loss(inputs: list[int], params)-> float:# the labels y are just the input shifted 1 to the left## inputs = [not, all, heros, wear, capes]# x = [not, all, heroes, wear]# y = [all, heroes, wear, capes]# # of course, we don't have a label for inputs[-1], so we exclude it from x## as such, for N inputs, we have N - 1 langauge modeling example pairsx, y = inputs[:-1], inputs[1:]# forward pass# all the predicted next token probability distributions at each positionoutput = gpt(x, params)# cross entropy loss# we take the average over all N-1 examplesloss = np.mean(-np.log(output[y]))

returnloss

deftrain(texts: list[list[str]], params)-> float:fortext intexts:inputs = tokenizer.encode(text)loss = lm_loss(inputs, params)gradients = compute_gradients_via_backpropagation(loss, params)params = gradient_descent_update_step(gradients, params)returnparams

为了清楚起见,我们在 GPT 的输入中添加了 params 参数。在训练循环的每一次迭代中,我们执行梯度下降步骤来更新模型参数,使我们的模型在看到每一段新的文本时都能更好地进行语言建模。这是一个非常简化的训练设置。

请注意,我们没有使用明确标记的数据。相反,我们能够从原始文本本身产生输入/标签对。这就是所谓的自我监督学习。

这将意味着我们可以非常容易地扩大训练数据,只需向模型展示尽可能多的原始文本。例如,GPT-3 使用了来自互联网和书籍的 3000 亿个文本标记上进行训练。

高能技巧!60 行 NumPy 代码 从头实现一个 GPT

GPT-2 论文中的表 2.3

你需要一个足够大的模型,以便能够从所有数据中学习,这就是为什么 GPT-3 有 1750 亿个参数,并且可能需要花费 100 万到 1000 万美元的计算成本来训练。

这种自我监督的训练步骤被称为预训练,因为我们可以重复使用 "预训练 "的模型权重,以便在下游任务上进一步训练模型,例如分类推文是否有毒。

在下游任务上训练模型被称为微调,因为模型的权重已经被预训练成能够理解语言,所以它只是针对当前的具体任务进行微调。

这种 "一般任务进行预训练+特定任务进行微调 "的策略被称为转移学习。

提示

原则上,最初的 GPT 只是关于预训练转换学习的转化器模型的益处,类似于 BERT。

直到在 GPT-2 和 GPT-3 的论文中,我们才意识到一个预训练好的 GPT 模型本身能够执行任何任务,只需提示它并进行自回归语言建模,不需要微调。这被称为语境学习,因为该模型只是利用提示的语境来执行任务。语境学习可以是零次、一次或几次。

高能技巧!60 行 NumPy 代码 从头实现一个 GPT

当然,你可以将 GPT 看作是一个聊天机器人,而不是让它明确地做 "任务"。对话历史被作为提示传递到模型中,也许会在前面加上一些描述,如 "你是一个聊天机器人等"。如果你改变了提示,你甚至可以给你的聊天机器人一个角色。

有了这些,让我们最后来看看实际的实现吧。

安装

克隆本教程的存储库: git clonehttps://github.com/jaymody/picoGPTcdpicoGPT

安装依赖项:pipinstall-rrequirements.txt

请注意,如果你使用的是 M1 Macbook,则在运行 pip 安装之前,需要在 requirements.txt 中将 tensorflow 更改为 tensorflow macos。此代码在 Python 3.9.10上 进行了测试。

每个文件的快速细分:encoder.py 包含 OpenAI 的 BPE Tokenizer 的代码;utils.py 包含下载和加载 GPT-2 模型权重、标记器和超参数的代码;gpt2.py 包含实际的 GPT 模型和生成代码,我们可以将其作为 python 脚本运行;gpt2pico.py 与 gpt2.py 相同,但代码行更少。

我们将从头开始重新实现 gpt2.py,因此让我们删除它并将其重新创建为空文件:rmgpt2.pytouchgpt2.py

首先,将以下代码粘贴到 :gpt2.pyimportnumpy asnp

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

defgenerate(inputs, params, n_head, n_tokens_to_generate):fromtqdm importtqdm

for_ intqdm(range(n_tokens_to_generate), "generating"): # auto-regressive decode looplogits = gpt2(inputs, **params, n_head=n_head) # model forward passnext_id = np.argmax(logits[-1]) # greedy samplinginputs = np.append(inputs, [next_id]) # append prediction to input

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

defmain(prompt: str, n_tokens_to_generate: int = 40, model_size: str = "124M", models_dir: str = "models"):fromutils importload_encoder_hparams_and_params

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

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

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

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

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

returnoutput_text

if__name__ == "__main__":importfire

fire.Fire(main)

细分为 4 个部分:

1、gpt2 函数是我们将要实现的实际 GPT 代码。您会注意到,除了输入之外,函数签名还包含一些额外的内容:wte、wpe、block 和 lnf 是我们模型的参数。n_head 是正向传递期间需要的超参数。

2、该函数是我们此前了解的自回归解码算法。为了简单起见,我们使用贪婪采样。tqdm 是一个进度条,帮助我们可视化解码过程,因为它一次生成一个指令。

3、main( )主函数处理:加载标记器(编码器)、模型权重(参数)和超参数(hparam)使用 tokenizer 将输入提示编码为指令 ID调用生成函数将输出 ID 解码为字符串

4、fire.fire(main)只是将我们的文件转换成一个 CLI 应用程序,因此我们最终可以使用:python-gpt2.py“some prompt here”运行代码

让我们仔细看看编码器、hparam 和 params,在笔记本或交互式 Python 会话中,运行:fromutils importload_encoder_hparams_and_paramsencoder, hparams, params = load_encoder_hparams_and_params("124M", "models")

这将把必要的模型和标记器文件下载到我们的代码中,并将编码器、hparam 和 params 加载到我们的代码中。

编码器

encoder 是 GPT-2 使用的 BPE tokenizer。>>> ids = encoder.encode("Not all heroes wear capes.")>>> ids[3673, 477, 10281, 5806, 1451, 274, 13]

>>> encoder.decode(ids)"Not all heroes wear capes."

使用 tokenize r的词汇(存储在 encoder.decoder 中),我们可以看一下实际的指令是什么。>>> [encoder.decoder[i] fori inids]['Not', 'Ġall', 'Ġheroes', 'Ġwear', 'Ġcap', 'es', '.']

注意,有时我们的指令是单词(如Not),有时是单词但前面会有空格(如 Ġall,Ġ 代表空格),有时是单词的一部分(如 capes 被分成 Ġcap 和 es),有时是标点符号(如.)。

BPE 的一个好处是它可以对任何任意的字符串进行编码。如果它遇到了词汇表中没有的东西,它只是将其分解为它所理解的子字符串:>>> [encoder.decoder[i] fori inencoder.encode("zjqfl")]['z', 'j', 'q', 'fl']

我们还可以检查词汇的大小:>>> len(encoder.decoder)50257

当我们加载 tokenizer 时,我们正在从一些文件中加载已经训练好的词汇和字节对合并,这些文件是在运行 load_encoder_hparams_and_param 时与模型文件一起下载。

超参数

hparams 是一个字典,包含模型的超参数:>>> hparams{"n_vocab": 50257, # number of tokens in our vocabulary"n_ctx": 1024, # maximum possible sequence length of the input"n_embd": 768, # embedding dimension (determines the "width" of the network)"n_head": 12, # number of attention heads (n_embd must be divisible by n_head)"n_layer": 12# number of layers (determines the "depth" of the network)}

我们将在代码的注释中使用这些符号来显示事物的基本形态。我们还将使用 n_seq 来表示我们输入序列的长度(即n_seq = len(inputs))。

参数

params 是一个嵌套的 Json 字典,用来保存我们模型的训练权重。Json 的叶节点是 NumPy 数组。如果我们打印 params,用它们的形状替换数组,我们会得到:>>> importnumpy asnp>>> defshape_tree(d):>>> ifisinstance(d, np.ndarray):>>> returnlist(d.shape)>>> elifisinstance(d, list):>>> return[shape_tree(v) forv ind]>>> elifisinstance(d, dict):>>> return{k: shape_tree(v) fork, v ind.items}>>> else:>>> ValueError("uh oh")>>> >>> print(shape_tree(params)){"wpe": [1024, 768],"wte": [50257, 768], "ln_f": {"b": [768], "g": [768]},"blocks": [{"attn": {"c_attn": {"b": [2304], "w": [768, 2304]},"c_proj": {"b": [768], "w": [768, 768]},},"ln_1": {"b": [768], "g": [768]},"ln_2": {"b": [768], "g": [768]},"mlp": {"c_fc": {"b": [3072], "w": [768, 3072]},"c_proj": {"b": [768], "w": [3072, 768]},},},... # repeat for n_layers]}

这些都是从最初的 OpenAI tensorflow 检查点加载的:>>> import tensorflow as tf>>> tf_ckpt_path = tf.train.latest_checkpoint("models/124M")>>> for name, _ in tf.train.list_variables(tf_ckpt_path):>>> arr = tf.train.load_variable(tf_ckpt_path, name).squeeze>>> print(f"{name}: {arr.shape}")model/h0/attn/c_attn/b: (2304,)model/h0/attn/c_attn/w: (768, 2304)model/h0/attn/c_proj/b: (768,)model/h0/attn/c_proj/w: (768, 768)model/h0/ln_1/b: (768,)model/h0/ln_1/g: (768,)model/h0/ln_2/b: (768,)model/h0/ln_2/g: (768,)model/h0/mlp/c_fc/b: (3072,)model/h0/mlp/c_fc/w: (768, 3072)model/h0/mlp/c_proj/b: (768,)model/h0/mlp/c_proj/w: (3072, 768)model/h1/attn/c_attn/b: (2304,)model/h1/attn/c_attn/w: (768, 2304)...model/h9/mlp/c_proj/b: (768,)model/h9/mlp/c_proj/w: (3072, 768)model/ln_f/b: (768,)model/ln_f/g: (768,)model/wpe: (1024, 768)model/wte: (50257, 768)

以下代码将上述 tensorflow 变量转换为 params 字典。

作为参考,以下是参数的形状,但数字由它们所代表的 hparams 代替:{"wpe": [n_ctx, n_embd],"wte": [n_vocab, n_embd], "ln_f": {"b": [n_embd], "g": [n_embd]},"blocks": [{"attn": {"c_attn": {"b": [3*n_embd], "w": [n_embd, 3*n_embd]},"c_proj": {"b": [n_embd], "w": [n_embd, n_embd]},},"ln_1": {"b": [n_embd], "g": [n_embd]},"ln_2": {"b": [n_embd], "g": [n_embd]},"mlp": {"c_fc": {"b": [4*n_embd], "w": [n_embd, 4*n_embd]},"c_proj": {"b": [n_embd], "w": [4*n_embd, n_embd]},},},... # repeat for n_layers]}

当我们实现 GPT 时,你可能会需要回来参考这个字典来检查权重的形状。为了一致性,我们将把代码中的变量名与此字典的关键字进行匹配。

基本层

在我们进入实际的 GPT 架构本身之前,让我们实现一些对 GPT 不特定的更基本的神经网络层。

GELU

GPT-2 选择的非线性(激活函数)是 GELU(高斯误差线性单位),是 ReLU 的替代方案。

高能技巧!60 行 NumPy 代码 从头实现一个 GPT

该图来自 GELU 论文

它与以下函数近似:defgelu(x):return0.5* x * (1+ np.tanh(np.sqrt(2/ np.pi) * (x + 0.044715* x**3)))

与 ReLU 一样,GELU 对输入元素进行操作:>>> gelu(np.array([[1, 2], [-2, 0.5]]))array([[ 0.84119, 1.9546 ],[-0.0454 , 0.34571]])

BERT 普及了 GeLU 在 transformer 模型中的使用。

Softmax

好的 Softmax:defsoftmax(x):exp_x = np.exp(x - np.max(x, axis=-1, keepdims=True))returnexp_x / np.sum(exp_x, axis=-1, keepdims=True)

我们使用 max(x) 的技巧来实现数值的稳定性。

Softmax 用于将一组实数转换为概率(在 0 和 1 之间,数字的总和为 1)。我们在输入的最后一个轴上应用 softmax。>>> x = softmax(np.array([[2, 100], [-5, 0]]))>>> xarray([[0.00034, 0.99966],[0.26894, 0.73106]])>>> x.sum(axis=-1)array([1., 1.])

层标准化

层标准化将值标准化为平均值为 0,方差为 1:deflayer_norm(x, g, b, eps: float = 1e-5):mean = np.mean(x, axis=-1, keepdims=True)variance = np.var(x, axis=-1, keepdims=True)x = (x - mean) / np.sqrt(variance + eps) # normalize x to have mean=0 and var=1 over last axisreturng * x + b # scale and offset with gamma/beta params

层标准化确保每层的输入始终在一致的范围内,这可以加快和稳定训练过程。与批量标准化一样,标准化输出随后被缩放,并用两个可学习向量 gamma 和 beta 进行偏移。分母中的小 ε 项用于避免除以零的误差。

我们在输入的最后一个轴上应用层标准化。>>> x = np.array([[2, 2, 3], [-5, 0, 1]])>>> x = layer_norm(x, g=np.ones(x.shape[-1]), b=np.zeros(x.shape[-1]))>>> xarray([[-0.70709, -0.70709, 1.41418],[-1.397, 0.508, 0.889]])>>> x.var(axis=-1)array([0.99996, 1. ]) # floating point shenanigans>>> x.mean(axis=-1)array([-0., -0.])

线性

标准矩阵乘法 + 偏差:def linear(x, w, b): # [m, in], [in, out], [out] -> [m, out]returnx @ w + b

线性层通常被称为投影(因为它们是从一个矢量空间投射到另一个矢量空间)。

GPT 架构

GPT 架构遵循 transformer 的架构:

高能技巧!60 行 NumPy 代码 从头实现一个 GPT

但仅使用解码器堆栈(图表的右侧部分):

高能技巧!60 行 NumPy 代码 从头实现一个 GPT

GPT架构

概括来说,GPT 架构有三个部分:文本+位置嵌入一个 transformer 解码器栈一个投射到词汇的步骤

在代码中,它看起来像这样:defgpt2(inputs, wte, wpe, blocks, ln_f, n_head):# [n_seq] -> [n_seq, n_vocab]# token + positional embeddingsx = wte[inputs] + wpe[range(len(inputs))] # [n_seq] -> [n_seq, n_embd]# forward pass through n_layer transformer blocksforblock inblocks:x = transformer_block(x, **block, n_head=n_head) # [n_seq, n_embd] -> [n_seq, n_embd]# projection to vocabx = layer_norm(x, **ln_f) # [n_seq, n_embd] -> [n_seq, n_embd]returnx @ wte.T # [n_seq, n_embd] -> [n_seq, n_vocab]

接下来我们将这三部分的内容逐一细化。

嵌入

指令嵌入

对于神经网络来说,指令 ID 本身并不是很好的表示。首先,指令 ID 的相对大小错误地传达了信息(例如,如果在我们的词汇中Apple = 5Table = 10 ,那么我们就意味着 2 * Table = Apple)。其次,对于神经网络来说,单个数字的维度并不高。

为了解决这些限制,我们将利用词向量的优势,尤其是通过学习嵌入矩阵:wte[inputs]# [n_embd]-> [n_seq, n_embd]

会想一下,wte 是一个 [n_vocab, n_embd] 矩阵。它作为一个查找表,矩阵中的第 3 行对应于我们词汇中第 1 个指令的学习向量。wte[inputs] 使用整数数组索引来检索对应于我们输入中每个指令的向量。

像我们网络中的其他参数一样,wte 是学习的。也就是说,它在训练开始时是随机初始化的,然后通过梯度下降进行更新。

位置嵌入

Transformer 架构的一个怪癖是它没有考虑到位置。也就是说,如果我们随机打乱输入,然后相应地取消打乱输出,输出将与我们一开始从未打乱输入的情况相同(输入的排序对输出没有任何影响)。

当然,单词的排序是语言的一个关键部分(duh),所以我们需要一些方法来将位置信息编码到我们的输入中。为此,我们可以直接使用另一个学习的嵌入矩阵:wpe[range(len(inputs))]# [n_seq]-> [n_seq, n_embd]

回想一下,wpe 是一个 [n_ctx, n_embd] 矩阵。矩阵的第 3 行包含一个矢量,编码输入中第 1 个位置的信息。与 wte 类似,这个矩阵是在梯度下降过程中学习的。

注意,这将我们的模型限制在最大序列长度为 n_ctx。也就是说,len(inputs)<= n_ctx 必须成立。

组合

我们可以把我们的标记和位置嵌入加在一起,得到一个同时编码标记和位置信息的组合嵌入。# token + positional embeddingsx = wte[inputs] + wpe[range(len(inputs))] # [n_seq] -> [n_seq, n_embd]# x[i] represents the word embedding for the ith word + the positional# embedding for the ith position

解码器栈

这是所有魔法发生的地方,也是深度学习中的 "深度 "所在。我们将传递 n_layer 转化器-解码器块传递嵌入。# forward pass through n_layer transformer blocksforblock inblocks:x = transformer_block(x, **block, n_head=n_head) # [n_seq, n_embd] -> [n_seq, n_embd]

堆叠更多的层使我们能够控制我们的网络深度。例如,GPT-3 有高达 96 层。另一方面,选择一个更大的 n_embd 值可以让我们控制我们的网络的宽度(例如,GPT-3 使用的嵌入尺寸为 12288)。

投影到Vocab

在我们的最后一步中,我们将最后的 transformer 块的输出投射到我们的词汇表的概率分布上。# projection to vocabx = 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]

注意:

1、在进行投射到 vocab 之前,我们首先将 x 通过最后一层标准化层。这是 GPT-2 架构所特有的。

2、我们正在重新使用嵌入矩阵 wte 进行投影。其他 GPT 实现可以选择使用单独的学习权重矩阵进行投影,但是共享嵌入矩阵有几个好处。你可以节省一些参数(尽管在GPT-3的规模下,也忽略不计)。由于该矩阵既负责到词的映射,又负责从词的映射,所以从理论上讲,与拥有两个单独的矩阵相比,它可能会学到更丰富的表示。

3、我们不在最后应用 softmax,所以我们的输出将是逻辑,而不是 0 和 1 之间的概率。这样做有以下几个原因:softmax 是单调,所以对于贪婪采样来说,np.argmax(logits) 等同于np.argmax(softmax(logits)),使得 softmax 成为多余。softmax 是不可逆,这意味着我们总是可以通过应用 softmax 从逻辑到概率,但我们不能从概率回到逻辑,所以为了获得最大的灵活性,我们输出逻辑数值稳定(例如,为了计算交叉熵损失,与 log_softmax(logits)相比,取log(softmax(logits))在数值上是不稳定的。

投射到词汇表的步骤有时也被称为语言建模的头。"头 "是什么意思?一旦你的 GPT 被预训练,你可以用其他类型的投影来替换语言建模头,比如分类头,用于在某些分类任务上对模型进行微调。所以你的模型可以有多个头,有点像九头蛇。

这就是高水平的 GPT 架构,让我们实际深入了解一下解码器块在做什么。

解码器块

transformer 解码器块由两个子层组成:多头因果自我关注定位的前馈神经网络deftransformer_block(x, mlp, attn, ln_1, ln_2, n_head):# [n_seq, n_embd] -> [n_seq, n_embd]# multi-head causal self attentionx = x + mha(layer_norm(x, **ln_1), **attn, n_head=n_head) # [n_seq, n_embd] -> [n_seq, n_embd]

# position-wise feed forward networkx = x + ffn(layer_norm(x, **ln_2), **mlp) # [n_seq, n_embd] -> [n_seq, n_embd]

returnx

每个子层在其输入上以及剩余连接都利用了层标准化(即将子层的输入加到子层的输出上)。

注意:

1、多头因果自我关注是促进输入之间交流的因素。在网络的其他任何地方,该模型都不允许输入 "看到 "对方。嵌入、位置前馈网络、层规范和 vocab 的投影都是基于我们的输入位置上操。对输入之间的关系进行建模的任务完全由注意力来完成。

2、位置式前馈神经网络只是一个普通的 2 层完全连接神经网络。这只是为我们的模型增加了一堆可学习的参数,以促进学习。

3、在最初的变压器论文中,层范数被放在输出层 _norm(x + sublayer(x)) 上,而我们将层规范放在输入 x + sublayer(layer_norm(x)) 上以匹配 GPT-2。这被称为预规范,已被证明对提高变压器的性能很重要。

4、剩余连接(由ResNet推广)有不同的用途:更容易优化深度神经网络(即有很多层的网络)。这里的想法是,我们为梯度回流网络提供 "捷径",使其更容易优化网络中的早期层。如果没有剩余连接,更深的模型在增加层数时性能会下降(可能是因为梯度很难在不丢失信息的情况下全部流回深度网络)。剩余连接似乎给深层网络带来了一些准确性的提升。可以帮助解决梯度消失/爆炸的问题。

让我们对这两个子层进行更深入地了解。

位置式前馈网络

这只是一个具有 2 层的简单多层感知器:defffn(x, c_fc, c_proj):# [n_seq, n_embd] -> [n_seq, n_embd]# project upa = gelu(linear(x, **c_fc)) # [n_seq, n_embd] -> [n_seq, 4*n_embd]# project back downx = linear(a, **c_proj) # [n_seq, 4*n_embd] -> [n_seq, n_embd]returnx

我们只是从 n_embd 投射到一个更高的维度 4*n_embd,然后再回落到 n_embd。

回顾一下,在我们的参数字典中,我们的 mlp 参数是这样的:"mlp": {"c_fc": {"b": [4*n_embd], "w": [n_embd, 4*n_embd]},"c_proj": {"b": [n_embd], "w": [4*n_embd, n_embd]},}

多头因果的自我关注

这一层可能是 transformer 中最难理解的部分。因此,让我们通过把每个词分解成自己的部分,来达到 "多头因果的自我关注"。注意力自身因果多头

注意力

我们从头开始推导出原始变压器论文中提出的缩放点积方程:

我们只需从博文中改编我们的注意力实现:defattention(q, k, v):# [n_q, d_k], [n_k, d_k], [n_k, d_v] -> [n_q, d_v]returnsoftmax(q @ k.T / np.sqrt(q.shape[-1])) @ v

自我

当 q、k 和 v 都来自同一来源时,我们正在进行自我关注(即让我们的输入序列关注自己):defself_attention(x):# [n_seq, n_embd] -> [n_seq, n_embd]returnattention(q=x, k=x, v=x)

我们可以通过引入 q、k、v 和注意力输出的投射来加强自我注意。defself_attention(x, w_k, w_q, w_v, w_proj):# [n_seq, n_embd] -> [n_seq, n_embd]# qkv projectionsq = x @ w_k # [n_seq, n_embd] @ [n_embd, n_embd] -> [n_seq, n_embd]k = x @ w_q # [n_seq, n_embd] @ [n_embd, n_embd] -> [n_seq, n_embd]v = x @ w_v # [n_seq, n_embd] @ [n_embd, n_embd] -> [n_seq, n_embd]

# perform self attentionx = attention(q, k, v) # [n_seq, n_embd] -> [n_seq, n_embd]

# out projectionx = x @ w_proj # [n_seq, n_embd] @ [n_embd, n_embd] -> [n_seq, n_embd]

returnx

这使我们的模型能够学习一个 q、k 和 v 的映射,最好地帮助注意力区分输入之间的关系。

如果我们把 w_q、w_k 和 w_v 合并成一个单一的矩阵 w_fc,进行投影,然后分割结果,就可以把矩阵乘法的次数从 4 次减少到 2 次:defself_attention(x, w_fc, w_proj):# [n_seq, n_embd] -> [n_seq, n_embd]# qkv projectionsx = x @ w_fc # [n_seq, n_embd] @ [n_embd, 3*n_embd] -> [n_seq, 3*n_embd]

# split into qkvq, k, v = qkv = np.split(x, 3, axis=-1) # [n_seq, 3*n_embd] -> 3 of [n_seq, n_embd]

# perform self attentionx = attention(q, k, v) # [n_seq, n_embd] -> [n_seq, n_embd]

# out projectionx = x @ w_proj # [n_seq, n_embd] @ [n_embd, n_embd] = [n_seq, n_embd]

returnx

这样做的效率更高一些,因为现代加速器(GPU)可以更好地利用一个大的矩阵乘法,而不是 3 个独立的小的矩阵乘法顺序发生。

最后,我们添加偏置向量以匹配 GPT-2 的实现,使用我们的线性函数,并重新命名我们的参数以匹配我们的字典 linearparams。defself_attention(x, c_attn, c_proj):# [n_seq, n_embd] -> [n_seq, n_embd]# qkv projectionsx = linear(x, **c_attn) # [n_seq, n_embd] -> [n_seq, 3*n_embd]

# split into qkvq, k, v = qkv = np.split(x, 3, axis=-1) # [n_seq, 3*n_embd] -> 3 of [n_seq, n_embd]

# perform self attentionx = attention(q, k, v) # [n_seq, n_embd] -> [n_seq, n_embd]

# out projectionx = linear(x, **c_proj) # [n_seq, n_embd] @ [n_embd, n_embd] = [n_seq, n_embd]

returnx

回顾一下,从我们的参数字典中,我们的 attn 参数看起来像这样:"attn": {"c_attn": {"b": [3*n_embd], "w": [n_embd, 3*n_embd]},"c_proj": {"b": [n_embd], "w": [n_embd, n_embd]},},

因果关系

我们目前的自我注意力设置有一点问题,我们的输入可以看到未来!例如,如果我们的输入是 ["not", "all", "heroes", "wear", "capes"],在自我关注期间,我们允许 "wear" 看到 "capes"。这意味着我们对 "wear" 的输出概率会有偏差,因为模型已经知道正确答案是 "capes"。这是不好的,由于我们的模型刚刚学会,输入的正确答案可以从输入中得到。

为了防止这种情况,我们需要以某种方式修改我们的注意力矩阵,以隐藏或掩盖我们的输入,使其无法看到未来。例如,让我们假装我们的注意力矩阵看起来像这样:notallheroeswearcapesnot0.1160.1590.0550.2260.443all0.1800.3970.1420.1060.175heroes0.1560.4530.0280.1290.234wear0.4990.0550.1330.0170.295capes0.0890.2900.2400.2280.153

每一行对应于一个查询,每一列对应于一个键。在这种情况下,看一下 "wear"这一行,你可以看到它在最后一列参加 "capes",权重为0.295。为了防止这种情况,我们要将该条目设置为0.0。notallheroeswearcapesnot0.1160.1590.0550.2260.443all0.1800.3970.1420.1060.175heroes0.1560.4530.0280.1290.234wear0.4990.0550.1330.0170.capes0.0890.2900.2400.2280.153

一般来说,为了防止我们的输入中的所有查询看向未来,我们把所有的位置都设置为0。notallheroeswearcapesnot0.1160. 0. 0. 0.all0.1800.3970. 0. 0.heroes0.1560.4530.0280. 0.wear0.4990.0550.1330.0170.capes0.0890.2900.2400.2280.153

我们将这称为掩蔽。上述掩蔽方法的一个问题是,我们的行数之和不再是 1(因为我们在应用 softmax 后将其设置为 0)。为了确保我们的行之和为 1,我们需要在应用 softmax 之前修改我们的注意矩阵。

这可以通过在 softmax 之前设置要被屏蔽的条目来实现:defattention(q, k, v, mask): # [n_q, d_k], [n_k, d_k], [n_k, d_v], [n_q, n_k]-> [n_q, d_v]returnsoftmax(q@ k.T / np.sqrt(q.shape[-1]) + mask) @ v

其中矩阵(用于):maskn_seq=5

我们使用 -1e10 而不是 -np.inf,因为 -np.inf 会导致 nans。

在我们的注意力矩阵中加入掩码,而不是明确地将数值设置为 -1e10,因为实际上,任何数字加上 -inf 就是 -inf。

我们可以在 NumPy 中用(1-np.tri(n_seq))计算掩码矩阵 * -1e10.

综上所述,我们得到:defattention(q, k, v, mask):# [n_q, d_k], [n_k, d_k], [n_k, d_v], [n_q, n_k] -> [n_q, d_v]returnsoftmax(q @ k.T / np.sqrt(q.shape[-1]) + mask) @ v

defcausal_self_attention(x, c_attn, c_proj):# [n_seq, n_embd] -> [n_seq, n_embd]# qkv projectionsx = linear(x, **c_attn) # [n_seq, n_embd] -> [n_seq, 3*n_embd]

# split into qkvq, k, v = qkv = np.split(x, 3, axis=-1) # [n_seq, 3*n_embd] -> 3 of [n_seq, n_embd]

# causal mask to hide future inputs from being attended tocausal_mask = (1- np.tri(x.shape[0])) * -1e10# [n_seq, n_seq]

# perform causal self attentionx = attention(q, k, v, causal_mask) # [n_seq, n_embd] -> [n_seq, n_embd]

# out projectionx = linear(x, **c_proj) # [n_seq, n_embd] @ [n_embd, n_embd] = [n_seq, n_embd]

returnx

多头

我们可以通过执行 n_head 单独的注意力计算来进一步改进我们的实现,将我们的查询、键和值分割成头:defmha(x, c_attn, c_proj, n_head):# [n_seq, n_embd] -> [n_seq, n_embd]# qkv projectionx = linear(x, **c_attn) # [n_seq, n_embd] -> [n_seq, 3*n_embd]

# split into qkvqkv = np.split(x, 3, axis=-1) # [n_seq, 3*n_embd] -> [3, n_seq, n_embd]

# split into headsqkv_heads = list(map(lambdax: np.split(x, n_head, axis=-1), qkv)) # [3, n_seq, n_embd] -> [3, n_head, n_seq, n_embd/n_head]

# causal mask to hide future inputs from being attended tocausal_mask = (1- np.tri(x.shape[0])) * -1e10# [n_seq, n_seq]

# perform attention over each headout_heads = [attention(q, k, v, causal_mask) forq, k, v inzip(*qkv_heads)] # [3, n_head, n_seq, n_embd/n_head] -> [n_head, n_seq, n_embd/n_head]

# merge headsx = np.hstack(out_heads) # [n_head, n_seq, n_embd/n_head] -> [n_seq, n_embd]

# out projectionx = linear(x, **c_proj) # [n_seq, n_embd] -> [n_seq, n_embd]

returnx

这里增加了三个步骤:

1.将 q、k、v 分成 n_head 头:# split into headsqkv_heads = list(map(lambda x: np.split(x, n_head, axis=-1), qkv)) # [3, n_seq, n_embd] -> [n_head, 3, n_seq, n_embd/n_head]

2.计算每个头部的注意力:# perform attention over each headout_heads = [attention(q, k, v) forq, k, v inzip(*qkv_heads)] # [n_head, 3, n_seq, n_embd/n_head] -> [n_head, n_seq, n_embd/n_head]

3.合并每个头的输出:# merge headsx = np.hstack(out_heads) # [n_head, n_seq, n_embd/n_head] -> [n_seq, n_embd]

注意,这将每个注意力计算的维度从 n_embd 减少到 n_embd/n_head。为了降低维度,我们的模型在通过注意力建立关系模型时得到了额外的子空间。例如,也许一个注意力头负责将代词与代词所指的人联系起来。也许另一个可能负责按时期对句子进行分组。另一个可能只是负责识别哪些词是实体,哪些不是。虽然,它可能只是另一个神经网络黑盒子。

我们编写的代码在一个循环中按顺序对每个头进行注意力计算(一次一个),这样的效率并不高。在实践中,你会希望并行地进行这些计算。为了简单起见,我们还是让它按顺序进行。

至此,我们终于完成了我们的 GPT 实现,剩下的就是把它放在一起并运行我们的代码。

整合

将所有内容放在一起,我们得到 gpt2.py,整个代码只有 120 行(如果删除注释和空格,则为 60 行)。

我们将通过一下的方式测试实现:pythongpt2.py \"Alan Turing theorized that computers would one day become" \--n_tokens_to_generate 8

输出:themost powerful machines onthe planet.

结果证明,它是有效的!

我们可以使用以下 Dockerfile 测试我们的实现是否与 OpenAI 的官方 GPT-2 存储库给出相同的结果:docker build -t "openai-gpt-2""https://gist.githubusercontent.com/jaymody/9054ca64eeea7fad1b58a185696bb518/raw/Dockerfile"docker run -dt "openai-gpt-2"--name "openai-gpt-2-app"docker exec-it "openai-gpt-2-app"/bin/bash -c 'python3 src/interactive_conditional_samples.py --length 8 --model_type 124M --top_k 1'# paste "Alan Turing theorized that computers would one day become" when prompted

这应该给出相同的结果:themost powerful machines onthe planet.

以上就是 Jay Mody 在博客的内容,大家有兴趣的可以自己试一下~忽略规模,GPT 的训练是非常标准的,相对于语言建模损失的梯度下降。当然还有很多技巧,但这些都是标准的东西。训练一个好的 GPT 模型的真正秘诀是能够扩展数据和模型。对此,你有哪些看法呢~

☞别光骂谷歌了!新版 Bing 花式“翻车”,还让用户向它道歉?

☞“C# 不停止膨胀,必将走向灭亡”

☞个人掏5000万美元、获2.3亿美金认购,造中国版OpenAI,45岁前美团联合创始人王慧文再创业! 返回搜狐,查看更多

责任编辑:

以上内容为资讯信息快照,由td.fyun.cc爬虫进行采集并收录,本站未对信息做任何修改,信息内容不代表本站立场。

快照生成时间:2023-02-15 12:45:08

本站信息快照查询为非营利公共服务,如有侵权请联系我们进行删除。

信息原文地址:

PyTorch官宣:告别CUDA,GPU推理迎来Triton加速新时代
...调试器和性能分析工具组成的工具链,,帮助开发者优化代码简而言之,CUDA使GPU加速LLM训练变为现实,大幅缩短了训练时间
2024-09-07 09:48:00
还在人工炼丹?自动提示工程指南来了,还带从头实现
...了自动提示词工程的概念、原理和工作流程,并通过代码从头实现了这一方法。自动提示词工程是什么?自动提示词工程(APE)是指自动生成和优化 LLM 提示词的技术,目标是提升模型在
2024-09-10 13:39:00
以假乱真,天工音乐大模型带来颠覆式AI体验
...性能提升,其模型技术知识能力提升超过20%,数学/推理/代码/文创能力提升超过30%。同时,「天工3.0」新增了搜索增强、研究模式、调用代码及绘制图表、多次调用联网搜索等能力
2024-04-03 11:35:00
2022生成模型进展有多快,新论文盘点9类生成模型代表作
...atGPT一个,光是基于文本输入的就有7种——图像、视频、代码、3D模型、音频、文本、科学知识……尤其2022年,效果好的AI生成模型层出不穷
2023-01-30 16:34:00
史上最快AI芯片「Sohu」,速度10倍于B200,哈佛辍学生打造
... GPU 和 TPU 上,软件是一场噩梦。处理任意 CUDA 和 PyTorch 代码需要极其复杂的编译器。第三方 AI 芯片(如 AMD
2024-06-27 09:24:00
商汤升级“日日新5.0”大模型,对标GPT-4Turbo
...窗口达200K左右。本次更新主要增强知识、数学、推理及代码能力,全面对标GPT-4 Turbo。在文科能力方面,“日日新5.0”的创意写作能力、推理能力及总结能力均有提升,相
2024-04-24 09:32:00
微软开源爆火1bit大模型推理框架!千亿参数模型量化后单CPU可跑
...Net b1.58将每个参数仅用三元值表示,但是所有这些都需要从头开始训练模型,并不是谁都有预算来进行LLM预训练
2024-10-23 12:05:00
比LoRA更高效!上交大&哈佛推出新微调框架,瞄准特定任务方向
...的潜力,提出新的高效微调方法LoRA-Dash。来看具体内容。从头搭建特定任务方向的框架随着大型语言模型的发展,针对特定任务微调模型往往需要大量计算资源。为了解决这一问题,参
2024-09-18 09:57:00
被AI冲击的游戏公司,会上演“集体失业”吗?
...正向辅助,比如说我平常写代码,以前可能写一个算法得从头想或者找别人写过的代码来改,现在可以直接问GPT,它会给你写一版,我不会直接用,会先看看写的对不对,然后去试用,大多数时
2023-06-09 17:46:00
更多关于科技的资讯:
Ta来消博了 | 全球头部游戏公司娱美德将携“传奇”IP系列产品参展第五届消博会
南海网4月2日消息(记者 王子遥)记者从第五届中国国际消费品博览会组委会获悉,全球头部游戏公司娱美德集团(下称“娱美德”)将携旗下经典游戏IP“传奇”系列产品《传奇M
2025-04-02 21:26:00
让外科手术实现“细胞级精准” 合肥这项技术登上《Nature》
大皖新闻讯 日前,国际顶级权威学术期刊《Nature》专访安徽树突光学科技有限公司(以下简称“树突精密”),并发表关于细胞级荧光导引技术的报道
2025-04-02 21:34:00
果然财经|“精致感和场景化”上差异竞争,威海首店经济频出圈
齐鲁晚报·齐鲁壹点 李孟霏 潘佳蓬2024年,威海社会消费品零售总额达到1447亿元,增长6.1%,分别高于全省、全国1
2025-04-02 22:22:00
汇川:“5G+工业互联网”炼就“电梯智造王国”
多彩贵州网讯 在贵州中航电梯有限责任公司智能化生产线上,26台智能机器人正演绎着“数字芭蕾”——机械臂精准完成折弯、焊接
2025-04-02 22:33:00
高通第四代骁龙8s支持新一代帧生成算法:60fps游戏秒变120fps
快科技4月2日消息,今日下午,高通正式推出了第四代骁龙8s移动平台,这一新品在多个关键领域实现了显著升级。首先,其采用了全大核CPU架构
2025-04-02 18:40:00
美国加征25%让低价车无钱可赚 奔驰考虑在美撤下入门车型
快科技4月2日消息,据报道,美国将对进口汽车加征25%的额外关税,自4月2日起生效。分析师称,这将使每辆汽车的成本增加数千美元
2025-04-02 18:40:00
REDMI首发!高通第四代骁龙8s正式发布:系列首次全大核
快科技4月2日消息,今天下午,高通正式发布了全新旗舰平台——第四代骁龙8s。高通将其定义为“新生代旗舰”,专为追求出色娱乐体验和创作体验的用户打造
2025-04-02 18:40:00
蔚来乐道总裁官宣离职:辜负期待 深感愧疚
“做不到,就下课”3月销量放榜第二天,蔚来乐道品牌总裁艾铁成兑现承诺,官宣离职。去年11月广州车展上,艾铁成公开立下“军令状”
2025-04-02 18:40:00
3月交付过没过两万:蔚来乐道总裁艾铁成“下课”
难抵压力,乐道汽车总裁艾铁成还是“下课了”。4 月 2 日上午,艾铁成通过乐道 App 发表文章宣布离职,不再担任乐道汽车总裁和蔚来高级副总裁
2025-04-02 18:40:00
小米汽车司机驾驶中睡着上热搜 客服回应:智驾系统只是辅助
快科技4月2日消息,近日,有网友曝光小米汽车车主驾驶中睡着,双手离开方向盘。同行友人连喊三遍“减速”避让小米汽车。据视频显示
2025-04-02 18:40:00
永辉高管谈调改 于东来直接打断!质问月赚200万为何不涨工资
快科技4月2日消息,在近日的2025中国超市周活动现场,永辉超市负责调改的高管正在台上演讲,被胖东来创始人于东来登台打断
2025-04-02 18:40:00
博主吐槽新能源汽车门把手根本找不到 打车感觉自己像土鳖
快科技4月2日消息,近日,一位博主在社交媒体上吐槽新能源汽车的门把手设计,引发了广泛关注和讨论。随着网约车的普及,乘客们发现
2025-04-02 18:40:00
全球富豪榜变化:马斯克仍高居榜首 巴菲特跃居第五
今年年初,全球千亿美元级超级富豪数量为16人,但如今已降至13人。这一变化源于美股在一季度遭遇自2022年以来最严重的季度跌幅
2025-04-02 19:10:00
REDMI/iQOO首批商用!一图了解高通第四代骁龙8s
快科技4月2日消息,今天下午,高通正式发布第四代骁龙8s。据悉,第四代骁龙8s基于台积电4nm工艺制造,其Kryo CPU采用1+3+2+2的 “1超7大” 架构设计
2025-04-02 19:10:00
比亚迪21%毛利率背后的变革
2025年只过了3个月,比亚迪的营收能力再次震慑车圈。3月24日,比亚迪发布2024年财务报告。财报显示,2024年比亚迪营业收入7771
2025-04-02 19:10:00