首页 >> 大全

60行NumPy手搓GPT

2023-08-14 大全 23 作者:考证青年

本文约24000字,建议阅读30分钟

本文我们将仅仅使用60行Numpy[6],从0-1实现一个GPT。

本文原载于尹志老师博客:[1]。

本文还是来自Jay Mody[2],那篇被 手动点赞[3]的GPT in 60 Lines of NumPy[4](已获原文作者授权)。

LLM大行其道,然而大多数GPT模型都像个黑盒子一般隐隐绰绰,甚至很多人都开始神秘化这个技术。我觉得直接跳进数学原理和代码里看看真实发生了什么,才是最有效的理解某项技术的方法。正如的 所说:

这些都是电脑程序。

这篇文章细致的讲解了GPT模型的核心组成及原理,并且用Numpy手搓了一个完整的实现(可以跑的那种),读起来真的神清气爽。项目代码也完全开源,叫做[5](pico,果然是不能再小的GPT了)。

关于译文几点说明:

翻译基本按照原作者的表述和逻辑,个别部分译者做了补充和看法;

在本文中,我们将仅仅使用60行Numpy[6],从0-1实现一个GPT。然后我们将发布的GPT-2模型的权重加载进我们的实现并生成一些文本

注意:

GPT是什么?

GPT代表生成式预训练( Pre- )。这是一类基于[8]的神经网络架构。Jay 的“GPT3是如何工作的”[9]一文在宏观视角下对GPT进行了精彩的介绍。但这里简单来说:

译者注:就是一种特定的神经网络结构

类似的GPT-3[10], 谷歌的LaMDA[11]还有的 [12]的大语言模型的底层都是GPT模型。让它们这么特殊的原因是

根本上来看,给定一组提示,GPT能够基于此生成文本。即使是使用如此简单的API(input = 文本, = 文本),一个训练好的GPT能够完成很多出色的任务,比如帮你写邮件[13],总结一本书[14],给你的起标题[15],给5岁的小孩解释什么是黑洞[16],写SQL代码[17],甚至帮你写下你的遗嘱[18]。

以上就是宏观视角下关于GPT的概览以及它能够做的事情。现在让我们深入一些细节吧。

输入/输入

一个GPT的函数签名基本上类似这样:

def gpt(inputs: list[int]) -> list[list[float]]:# inputs has shape [n_seq]# output has shape [n_seq, n_vocab]output = # beep boop neural network magicreturn output

输入

输入是一些文本,这些文本被表示成一串整数序列,每个整数都与文本中的token对应:

# integers represent tokens in our text, for example:
# text   = "not all heroes wear capes":
# tokens = "not"  "all" "heroes" "wear" "capes"
inputs =   [1,     0,    2,      4,     6]

token是文本的小片段,它们由某种分词器()产生。我们可以通过一个词汇表()将映射为整数:

# 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 whitespace
tokenizer = 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 mapping
tokens = [tokenizer.vocab[i] for i in ids] # tokens = ["not", "all", "heroes", "wear"]# the decode() method converts back a list[int] -> str
text = tokenizer.decode(ids) # text = "not all heroes wear"

简单说:

在实际中,我们不仅仅使用简单的通过空白分隔去做分词,我们会使用一些更高级的方法,比如Byte-Pair [19]或者[20],但它们的原理是一样的:

有一个vocab即词汇表,可以将字符串token映射到整数索引。

有一个方法,即编码方法,可以实现str -> list[int]的转化。

有一个方法,即解码方法,可以实现list[int] -> str的转化

输出

输出是一个二维数组,其中[i][j]表示模型的预测概率,这个概率代表了词汇表中位于vocab[j]的token是下一个[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  ]
# 在"not"给出的情况下,我们可以看到,(对于下一个token)模型预测"all"具有最高的概率#              ["all", "not", "heroes", "the", "wear", ".", "capes"]
# output[1] =  [0.0     0.0      0.8     0.1    0.0    0.0   0.1  ]
# 在序列["not", "all"]给出的情况下,(对于下一个token)模型预测"heroes"具有最高的概率#              ["all", "not", "heroes", "the", "wear", ".", "capes"]
# output[-1] = [0.0     0.0     0.0     0.1     0.0    0.05  0.85  ]
# 在整个序列["not", "all", "heroes", "wear"]给出的情况下,(对于下一个token)模型预测"capes"具有最高的概率

为了针对整个序列获得下一个token预测, 我们可以简单的选择[-1]中概率最大的那个token:

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 = 6
next_token = vocab[next_token_id] # next_token = "capes"

将具有最高概率的token作为我们的预测,叫做 [21]或者 (贪心采样)。

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

生成一个单词是挺酷的(但也就那样了),但是要是生成整个句子、整篇文章呢?

生成文本 自回归

我们可以迭代地通过模型获取下一个token的预测,从而生成整个句子。在每次迭代中,我们将预测的token再添加回输入中去:

def generate(inputs, n_tokens_to_generate):for _ in range(n_tokens_to_generate): # 自回归的解码循环output = gpt(inputs) # 模型前向传递next_id = np.argmax(output[-1]) # 贪心采样inputs.append(int(next_id)) # 将预测添加回输入return inputs[len(inputs) - n_tokens_to_generate :]  # 只返回生成的idsinput_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"

这个过程是在预测未来的值(回归),并且将预测的值添加回输入中去(auto),这就是为什么你会看到GPT被描述为自回归模型。

采样

我们可以通过对概率分布进行采样来替代贪心采样,从而为我们的生成引入一些随机性():

inputs = [1, 0, 2, 4] # "not" "all" "heroes" "wear"
output = gpt(inputs)
np.random.choice(np.arange(vocab_size), p=output[-1]) # capes
np.random.choice(np.arange(vocab_size), p=output[-1]) # hats
np.random.choice(np.arange(vocab_size), p=output[-1]) # capes
np.random.choice(np.arange(vocab_size), p=output[-1]) # capes
np.random.choice(np.arange(vocab_size), p=output[-1]) # pants

这样子,我们就可以基于同一个输入产生不同的输出句子啦。当我们结合更多的比如top-k[22],top-p[23]和温度[24]这样的技巧的时候,(这些技巧能够能更改采样的分布),我们输出的质量也会有很大的提高。这些技巧也引入了一些超参数,通过调整这些超参,我们可以获得不同的生成表现()。比如提高温度超参,我们的模型就会更加冒进,从而变得更有“创造力”。

训练

我们与训练其它神经网络一样,针对特定的损失函数使用梯度下降[25]训练GPT。对于GPT,我们使用语言建模任务的交叉熵损失[26]:

def lm_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]))return lossdef train(texts: list[list[str]], params) -> float:for text in texts:inputs = tokenizer.encode(text)loss = lm_loss(inputs, params)gradients = compute_gradients_via_backpropagation(loss, params)params = gradient_descent_update_step(gradients, params)return params

以上是一个极度简化的训练设置,但是它基本覆盖了重点。这里注意一下,我们的gpt函数签名中加入了(为了简化,我们在上一节是把它去掉的)。在训练循环的每次迭代中:

我们为给定的输入文本示例计算语言建模损失。

损失决定了我们的梯度,我们可以通过反向传播计算梯度。

我们使用梯度来更新我们的模型参数,使得我们的损失能够最小化(梯度下降)。

请注意,我们在这里并未使用明确的标注数据。取而代之的是,我们可以通过原始文本自身,产生大量的输入/标签对(input/label pairs)。这就是所谓的自监督学习。

自监督学习的范式,让我们能够海量扩充训练数据。我们只需要尽可能多的搞到大量的文本数据,然后将其丢入模型即可。比如,GPT-3就是基于来自互联网和书籍的3000亿token进行训练的:

当然,这里你就需要一个足够大的模型有能力去从这么大量的数据中学到内容,这就是为什么GPT-3模型拥有1750亿的参数,并且大概消耗了100万--1000万美元的计算费用进行训练[27]。

这个自监督训练的步骤称之为预训练,而我们可以重复使用预训练模型权重来训练下游任务上的特定模型,比如对文本进行分类(分类某条推文是有害的还是无害的)。预训练模型有时也被称为基础模型( )。

在下游任务上训练模型被称之为微调,由于模型权重已经预训练好了,已经能够理解语言了,那么我们需要做的就是针对特定的任务去微调这些权重。

译者注:听上去很简单是不是?那就快来入坑啊(doge)

这个所谓“在通用任务上预训练 + 特定任务上微调”的策略就称之为迁移学习[28]。

提示()

本质上看,原始的GPT论文[29]只是提供了用来迁移学习的模型的预训练。文章显示,一个117M的GPT预训练模型,在针对下游任务的标注数据上微调之后,它能够在各种NLP( )任务上达到最优性能。

直到GPT-2[30]和GPT-3[31]的论文出来,我们才意识到,一个GPT模型只要在足够多的数据上训练,只要模型拥有足够多的参数,那么不需要微调,模型本身就有能力执行各种任务。只要你对模型进行提示,运行自回归语言模型,然后你猜咋地?模型就神奇的返回给我们合适的响应了。这,就是所谓的in- , 也就是说模型仅仅根据提示的内容,就能够执行各种任务了。In- 可以是zero shot, one shot, 或者是few shot的:

译者注:我们可以简单的认为,为了执行我们的自己的任务,zero shot表示我们直接拿着大模型就能用于我们的任务了;one shot表示我们需要提供给大模型关于我们特定任务的一个列子;few shot表示我们需要提供给大模型关于我们特定任务的几个例子;

基于提示内容生成文本也被称之为条件生成,因为我们的模型是基于特定的输入(条件)进行生成的。

当然,GPT也不仅限于自然语言处理任务(NLP)。你可以将模型用于任何你想要的条件下。比如你可以将GPT变成一个聊天机器人(即:[32]),这里的条件就是你的对话历史。你也可以进一步条件化你的聊天机器人,通过提示词进行某种描述,限定其表现为某种行为(比如你可以提示:“你是个聊天机器人,请礼貌一点,请讲完整的句子,不要说有害的东西,等等”)。像这样条件化你的模型,你完全可以得到一个定制化私人助理机器人[33]。但是这样的方式不一定很健壮,你仍然可以对你的模型进行越狱,然后让它表现失常[34]。

译者注:原作者在这里主要讲了通过进行条件控制,其实还有很多其它的条件化机器人的方法,有兴趣我可以另开一篇来单独细说

说完了这些,现在终于要开始实际实现了。

准备工作

首先将这个教程的仓库clone下来:

git clone https://github.com/jaymody/picoGPT
cd picoGPT

然后安装依赖:

pip install -r requirements.txt

注意:目前代码在 3.9.10下测试通过。

简单介绍一下每个文件:

在这里,我们将从0-1复现gpt2.py,所以请先将这个文件删掉吧,我们重新建立一个新的gpt2.py文件,然后从头写起:

rm gpt2.py
touch gpt2.py

首先,将下面的代码粘贴到gpt2.py里:

import numpy as npdef gpt2(inputs, wte, wpe, blocks, ln_f, n_head):pass # TODO: implement thisdef generate(inputs, params, n_head, n_tokens_to_generate):from tqdm import tqdmfor _ in tqdm(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.append(int(next_id))  # append prediction to inputreturn inputs[len(inputs) - n_tokens_to_generate :]  # only return generated idsdef 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 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 modelassert len(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)return output_textif __name__ == "__main__":import firefire.Fire(main)

我们将分为四部分进行拆解:

gpt2函数是我们将要实现的实际GPT代码。你会注意到函数签名中除了,还有其它的参数:

函数是我们之前看到的自回归解码算法。为了简洁,我们使用贪心采样算法。tqdm是一个进度条库,它可以帮助我们随着每次生成一个token,可视化地观察解码过程。

main函数主要处理:

fire.Fire(main)将我们的源文件转成一个命令行应用,然后就可以像这样运行我们的代码了: gpt2.py "some here"

我们先在或者交互界面下看看, , ,运行:

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

上述代码将下载必要的模型及分词器文件[36]至/124M,并且加载``,``,``[37]。

编码器

我们的使用的是GPT-2中使用的BPE分词器:

>>> ids = encoder.encode("Not all heroes wear capes.")
>>> ids
[3673, 477, 10281, 5806, 1451, 274, 13]>>> encoder.decode(ids)
"Not all heroes wear capes."

使用分词器的词汇表(存储于.),我们可以看看实际的token到底长啥样:

>>> [encoder.decoder[i] for i in ids]
['Not', 'Ġall', 'Ġheroes', 'Ġwear', 'Ġcap', 'es', '.']

注意,有的时候我们的token是单词(比如:Not),有的时候虽然也是单词,但是可能会有一个空格在它前面(比如Ġall, `Ġ`代表一个空格[38]),有时候是一个单词的一部分(比如:capes被分隔为Ġcap和es),还有可能它就是标点符号(比如:.)。

BPE的一个好处是它可以编码任意字符串。如果遇到了某些没有在词汇表里显示的字符串,那么BPE就会将其分割为它能够理解的子串:

>>> [encoder.decoder[i] for i in encoder.encode("zjqfl")]
['z', 'j', 'q', 'fl']

我们还可以检查一下词汇表的大小:

>>> len(encoder.decoder)
50257

词汇表以及决定字符串如何分解的字节对组合(byte-pair ),是通过训练分词器获得的。当我们加载分词器,就会从一些文件加载已经训练好的词汇表和字节对组合,这些文件在我们运行的时候,随着模型文件被一起下载了。你可以查看/124M/.json(词汇表)和/124M/vocab.bpe(字节对组合)。

超参数

是一个字典,这个字典包含着我们模型的超参:

>>> 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())。

参数

是一个嵌套的json字典,该字典具有模型训练好的权重。json的叶子节点是NumPy数组。如果我们打印, 用他们的形状去表示数组,我们可以得到:

>>> import numpy as np
>>> def shape_tree(d):
>>>     if isinstance(d, np.ndarray):
>>>         return list(d.shape)
>>>     elif isinstance(d, list):
>>>         return [shape_tree(v) for v in d]
>>>     elif isinstance(d, dict):
>>>         return {k: shape_tree(v) for k, v in d.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]
}

这些是从原始的 加载的:

>>> 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)

下述代码[39]将上面的变量转换为字典。

为了对比,这里显示了的形状,但是数字被替代:

_洗衣机双动力和手搓式的哪个好_网络名词搓手手什么意思

{"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(高斯误差线性单元)[40],这是一种类似ReLU的激活函数:

来自GELU论文的图1

它的函数函数如下:

def gelu(x):return 0.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]])

下面是最经典的[41]:

def softmax(x):exp_x = np.exp(x - np.max(x, axis=-1, keepdims=True))return exp_x / np.sum(exp_x, axis=-1, keepdims=True)

这里我们使用了`max(x)`技巧[42]来保持数值稳定性。

用来将一组实数(至之间)转换为概率(至之间,其求和为1)。我们将作用于输入的最末轴上。

>>> x = softmax(np.array([[2, 100], [-5, 0]]))
>>> x
array([[0.00034, 0.99966],[0.26894, 0.73106]])
>>> x.sum(axis=-1)
array([1., 1.])

层归一化

层归一化[43]将数值标准化为均值为0方差为1的值:

其中是的均值,为的方差,和为可学习的参数。

def layer_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 axisreturn g * x + b  # scale and offset with gamma/beta params

层归一化确保每层的输入总是在一个一致的范围里,而这将为训练过程的加速和稳定提供支持。与批归一化[44]类似,归一化之后的输出通过两个可学习参数和进行缩放和偏移。分母中的小项用来避免计算中的分母为零错误。

我们在中用层归一化来替换批归一化的原因有很多[45]。各种不同归一化技巧的不同点在这个博客[46]中进行了精彩的总结。

我们对输入的最末轴进行层归一化:

>>> 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]))
>>> x
array([[-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]return x @ w + b

线性层也通常被认为是投影操作(因为它们将一个向量空间投影到另一个向量空间)。

>>> x = np.random.normal(size=(64, 784)) # input dim = 784, batch/sequence dim = 64
>>> w = np.random.normal(size=(784, 10)) # output dim = 10
>>> b = np.random.normal(size=(10,))
>>> x.shape # shape before linear projection
(64, 784)
>>> linear(x, w, b).shape # shape after linear projection
(64, 10)

GPT架构

GPT的架构是基于[47]的:

但它仅仅使用了解码器层(图中的右边部分):

注意,因为我们已经搞定了编码器,所以中间的"cross-"层也被移除了。

从宏观的角度来看,GPT架构有三个部分组成:

代码层面的话,就像这样:

def gpt2(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 blocksfor block in blocks: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]return x @ wte.T  # [n_seq, n_embd] -> [n_seq, n_vocab]

现在我们将上面三个部分做更细致的拆解。

嵌入层 Token 嵌入 对于神经网络而言,token ID本身并不是一个好的表示。第一,token ID的相对大小会传递错误的信息(比如,在我们的词汇表中,如果Apple = 5,Table=10,那就意味着2 * Table = Apple?显然不对)。其二,单个的数也没有足够的维度喂给神经网络。

译者注:对于第二点补充一句,也就是说单个的数字包含的特征信息不够丰富

为了解决这些限制,我们将利用词向量[48],即通过一个学习到的嵌入矩阵:

wte[inputs] # [n_seq] -> [n_seq, n_embd]

还记得吗?wte是一个[, ]的矩阵。这就像一个查找表,矩阵中的第行对应我们的词汇表中的第个token的向量表示(学出来的)。wte[]使用了 array [49]来检索我们输入中每个token所对应的向量。

就像神经网络中的其他参数,wte是可学习的。也就是说,在训练开始的时候它是随机初始化的,然后随着训练的进行,通过梯度下降不断更新。

位置嵌入( )

单纯的架构的一个古怪地方在于它并不考虑位置。当我们随机打乱输入位置顺序的时候,输出可以保持不变(输入的顺序对输出并未产生影响)。

可是词的顺序当然是语言中重要的部分啊,因此我们需要使用某些方式将位置信息编码进我们的输入。为了这个目标,我们可以使用另一个学习到的嵌入矩阵:

wpe[range(len(inputs))] # [n_seq] -> [n_seq, n_embd]

wpe是一个[n_ctx, ]矩阵。矩阵的第行包含一个编码输入中第个位置信息的向量。与wte类似,这个矩阵也是通过梯度下降来学习到的。

需要注意的是,这将限制模型的最大序列长度为n_ctx。也就是说必须满足len()

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了