从头开始构建GPT!最全实践指南来了-60行代码 (从头开始构建大型语言模型 pdf)

教程大全 2025-07-16 17:05:57 浏览

60行代码,从头开始构建GPT?

最近,一位开发者做了一个实践指南,用Numpy代码从头开始实现GPT。

你还可以将 OpenAI发布的GPT-2模型权重加载到构建的GPT中,并生成一些文本。

话不多说,直接开始构建GPT。

什么是GPT?

GPT代表生成式预训练Transformer,是一种基于Transformer的神经网络结构。

– 生成式(Generative):GPT生成文本。

– 预训练(Pre-trained):GPT是根据书本、互联网等中的大量文本进行训练的。

– Transformer:GPT是一种仅用于解码器的Transformer神经网络。

大模型,如OpenAI的GPT-3、谷歌的LaMDA,以及Cohere的Command XLarge,背后都是GPT。它们的特别之处在于, 1) 非常大(拥有数十亿个参数),2) 受过大量数据(数百GB的文本)的训练。

直白讲,GPT会在提示符下生成文本。

即便使用非常简单的API(输入=文本,输出=文本),一个训练有素的GPT也可以做一些非常棒的事情,比如写邮件,总结一本书,为Instagram发帖提供想法,给5岁的孩子解释黑洞,用SQL编写代码,甚至写遗嘱。

以上就是 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是文本的子片段,使用分词器生成。我们可以使用词汇表将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 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"

简而言之:

– 有一个字符串。

– 使用分词器将其分解成称为token的小块。

– 使用词汇表将这些token映射为整数。

在实践中,我们会使用更先进的分词方法,而不是简单地用空白来分割,比如字节对编码(BPE)或WordPiece,但原理是一样的:

vocab将字符串token映射为整数索引

encode方法,可以转换str -> list[int]

decode 方法,可以转换 list[int] -> str ([2])

输出

输出是一个二维数组,其中 output[i][j] 是模型预测的概率,即 vocab[j] 处的token是下一个tokeninputs[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.750.10.00.150.00.00.0]# given just "not", the model predicts the word "all" with the highest probability#["all", "not", "heroes", "the", "wear", ".", "capes"]# output[1] =[0.00.00.80.10.00.00.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.00.00.00.10.00.050.85]# given the whole sequence ["not", "all", "heroes", "wear"], the model predicts the word "capes" with the highest probability

要获得整个序列的下一个token预测,我们只需获取 output[-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 = 6next_token = vocab[next_token_id] # next_token = "capes"

将概率最高的token作为我们的预测,称为贪婪解码(Greedy Decoding)或贪婪采样(greedy sampling)。

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

生成一个单词很酷,但整个句子、段落等又如何呢?

生成文本

自回归

我们可以通过迭代从模型中获得下一个token预测来生成完整的句子。在每次迭代中,我们将预测的token追加回输入:

def generate(inputs, n_tokens_to_generate):for _ in range(n_tokens_to_generate): # auto-regressive decode loopoutput = gpt(inputs) # model forward passnext_id = np.argmax(output[-1]) # greedy samplinginputs.append(int(next_id)) # append prediction to inputreturn inputs[len(inputs) - n_tokens_to_generate :]# only return generated 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"

这个预测未来值(回归)并将其添加回输入(自)的过程,就是为什么你可能会看到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

这样,我们就能在输入相同内容的情况下生成不同的句子。

如果与top-k、top-p和温度等在采样前修改分布的技术相结合,我们的输出质量就会大大提高。

这些技术还引入了一些超参数,我们可以利用它们来获得不同的生成行为(例如,提高温度会让我们的模型承担更多风险,从而更具「创造性」)。

训练

我们可以像训练其他神经网络一样,使用梯度下降法训练GPT,并计算损失函数。对于GPT,我们采用语言建模任务的交叉熵损失:

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函数签名中添加了params (为了简单起见,我们在前面的章节中没有添加)。在训练循环的每一次迭代期间:

– 对于给定的输入文本实例,计算了语言建模损失

– 损失决定了我们通过反向传播计算的梯度

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

请注意,我们不使用显式标记的数据。相反,我们能够仅从原始文本本身生成输入/标签对。这被称为自监督学习。

自监督使我们能够大规模扩展训练数据,只需获得尽可能多的原始文本并将其投放到模型中。例如,GPT-3接受了来自互联网和书籍的3000亿个文本token的训练:

当然,你需要一个足够大的模型才能从所有这些数据中学习,这就是为什么GPT-3有1750亿个参数,训练的计算成本可能在100万至1000万美元之间。

这个自监督的训练步骤被称为预训练,因为我们可以重复使用「预训练」的模型权重来进一步训练模型的下游任务。预训练的模型有时也称为「基础模型」。

在下游任务上训练模型称为微调,因为模型权重已经经过了理解语言的预训练,只是针对手头的特定任务进行了微调。

「一般任务的前期训练+特定任务的微调」策略被称为迁移学习。

提示

原则上,最初的GPT论文只是关于预训练Transformer模型用于迁移学习的好处。

论文表明,当对标记数据集进行微调时,预训练的117M GPT在各种自然语言处理任务中获得了最先进的性能。

直到GPT-2和GPT-3论文发表后,我们才意识到,基于足够的数据和参数预训练的GPT模型,本身能够执行任何任务,不需要微调。

只需提示模型,执行自回归语言建模,然后模型就会神奇地给出适当的响应。这就是所谓的「上下文学习」(in-context learning),因为模型只是利用提示的上下文来完成任务。

语境中学习可以是0次、一次或多次。

在给定提示的情况下生成文本也称为条件生成,因为我们的模型是根据某些输入生成一些输出的。

GPT并不局限于NLP任务。

你可以根据你想要的任何条件来微调这个模型。比如,你可以将GPT转换为聊天机器人(如ChatGPT),方法是以对话历史为条件。

说到这里,让我们最后来看看实际的实现。

设置

克隆本教程的存储库:

git clonepicoGPT

然后安装依赖项:

pip install -r requirements.txt

注意:这段代码是用Python 3.9.10测试的。

每个文件的简单分类:

– encoder.py包含OpenAI的BPE分词器的代码,这些代码直接取自gpt-2 repo。

– utils.py包含下载和加载GPT-2模型权重、分词器和超参数的代码。- gpt2.py包含实际的GPT模型和生成代码,我们可以将其作为python脚本运行。- gpt2_pico.py与gpt2.py相同,但代码行数更少。

我们将从头开始重新实现gpt2.py ,所以让我们删除它并将其重新创建为一个空文件:

rm gpt2.pytouch 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)

将4个部分分别分解为:

– gpt2函数是我们将要实现的实际GPT代码。你会注意到,除了inputs之外,函数签名还包括一些额外的内容:

wte、 wpe、 blocks和ln_f是我们模型的参数。

n_head是前向传递过程中需要的超参数。

– generate函数是我们前面看到的自回归解码算法。为了简单起见,我们使用贪婪抽样。tqdm是一个进度条,帮助我们可视化解码过程,因为它一次生成一个token。

– main函数处理:

加载分词器(encoder)、模型权重(params)和超参数(hparams)

使用分词器将输入提示编码为token ID

调用生成函数

将输出ID解码为字符串

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

让我们更详细地了解一下笔记本中的encoder 、 hparams和params,或者在交互式的Python会话中,运行:

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

这将把必要的模型和分词器文件下载到models/124M ,并将encoder、 hparams和params加载到我们的代码中。

编码器

encoder是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."

使用分词器的词汇表(存储在encoder.decoder中),我们可以看到实际的token是什么样子的:

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

请注意,我们的token有时是单词(例如Not),有时是单词但前面有空格(例如Ġall,Ġ表示空格),有时是单词的一部分(例如Capes分为Ġcap和es),有时是标点符号(例如.)。

BPE的一个优点是它可以对任意字符串进行编码。如果它遇到词汇表中没有的内容,它只会将其分解为它能够理解的子字符串:

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

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

len(encoder.decoder)50257

词汇表以及确定如何拆分字符串的字节对合并是通过训练分词器获得的。

当我们加载分词器时,我们从一些文件加载已经训练好的单词和字节对合并,当我们运行load_encoder_hparams_and_params时,这些文件与模型文件一起下载。

超参数

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数组。我们会得到:

>>> 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]}

这些是从原始OpenAI TensorFlow检查点加载的:

import tensorflow as tftf_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词典。

作为参考,以下是params的形状,但用它们所代表的hparams替换了数字:

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

基本层

在我们进入实际的GPT体系结构本身之前,最后一件事是,让我们实现一些非特定于GPT的更基本的神经网络层。

GPT-2选择的非线性(激活函数)是GELU(高斯误差线性单元),它是REU的替代方案:

它由以下函数近似表示:

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

Good ole softmax:

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)技巧来保证数值稳定性。

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:

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

层归一化确保每一层的输入始终在一致的范围内,这会加快和稳定训练过程。

与批处理归一化一样,归一化输出随后被缩放,并使用两个可学习向量gamma和beta进行偏移。分母中的小epsilon项用于避免除以零的误差。

由于种种原因,Transformer采用分层定额代替批量定额。

我们在输入的最后一个轴上应用层归一化。

>>> 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.])Linear

你的标准矩阵乘法+偏差:

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架构遵循Transformer的架构:

从高层次上讲,GPT体系结构有三个部分:

文本+位置嵌入

一种transformer解码器堆栈

向单词步骤的映射

在代码中,它如下所示:

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]

把所有放在一起

把所有这些放在一起,我们得到了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.

它成功了!

我们可以使用下面的Dockerfile测试我们的实现与OpenAI官方GPT-2 repo的结果是否一致。

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

这应该会产生相同的结果:

the most powerful machines on the planet.

下一步呢?

这个实现很酷,但它缺少很多花哨的东西:

GPU/TPU支持

将NumPy替换为JAX:

import jax.numpy as np

你现在可以使用代码与GPU,甚至TPU!只需确保正确安装了JAX即可。

反向传播

同样,如果我们用JAX替换NumPy:

import jax.numpy as np
然后,计算梯度就像以下操作一样简单:
def lm_loss(params, inputs, n_head) -> float:x, y = inputs[:-1], inputs[1:]output = gpt2(x, **params, n_head=n_head)loss = np.mean(-np.log(output[y]))return lossgrads = jax.grad(lm_loss)(params, inputs, n_head)Batching

再一次,如果我们用JAX替换NumPy:

import jax.numpy as np
然后,对gpt2函数进行批处理非常简单:
gpt2_batched = jax.vmap(gpt2, in_axes=[0, None, None, None, None, None])gpt2_batched(batched_inputs) # [batch, seq_len] -> [batch, seq_len, vocab]

推理优化

我们的实现效率相当低。你可以进行的最快、最有效的优化(在GPU+批处理支持之外)将是实现KV缓存。

训练

训练GPT对于神经网络来说是相当标准的(梯度下降是损失函数)。

当然,在训练GPT时,你还需要使用标准的技巧包(例如,使用ADAM优化器、找到最佳学习率、通过辍学和/或权重衰减进行正则化、使用学习率调度器、使用正确的权重初始化、批处理等)。

训练一个好的GPT模型的真正秘诀是调整数据和模型的能力,这才是真正的挑战所在。

对于缩放数据,你需要一个大、高质量和多样化的文本语料库。

– 大意味着数十亿个token(TB级的数据)。

– 高质量意味着您想要过滤掉重复的示例、未格式化的文本、不连贯的文本、垃圾文本等。

评估

如何评价一个LLM,这是一个很难的问题。

停止生成

当前的实现要求我们提前指定要生成的token的确切数量。这并不是一个好方法,因为我们生成的token最终会过长、过短或在句子中途中断。

为了解决这个问题,我们可以引入一个特殊的句尾(EOS)标记。

在预训练期间,我们将EOS token附加到输入的末尾(即tokens = [“not”, “all”, “heroes”, “wear”, “capes”, “.”, “<|EOS|>”])。

在生成期间,只要我们遇到EOS token(或者如果我们达到了某个最大序列长度),就会停止:

def generate(inputs, eos_id, max_seq_len):prompt_len = len(inputs)while inputs[-1] != eos_id and len(inputs) < max_seq_len:output = gpt(inputs)next_id = np.argmax(output[-1])inputs.append(int(next_id))return inputs[prompt_len:]

GPT-2没有预训练EOS token,所以我们不能在我们的代码中使用这种方法。

无条件生成

使用我们的模型生成文本需要我们使用提示符对其进行条件调整。

但是,我们也可以让我们的模型执行无条件生成,即模型在没有任何输入提示的情况下生成文本。

这是通过在预训练期间将特殊的句子开始(BOS)标记附加到输入开始(即tokens = [“<|BOS|>”, “not”, “all”, “heroes”, “wear”, “capes”, “.”])来实现的。

然后,要无条件地生成文本,我们输入一个只包含BOS token的列表:

def generate_unconditioned(bos_id, n_tokens_to_generate):inputs = [bos_id]for _ in range(n_tokens_to_generate):output = gpt(inputs)next_id = np.argmax(output[-1])inputs.append(int(next_id))return inputs[1:]

GPT-2预训练了一个BOS token(名称为<|endoftext|>),因此使用我们的实现无条件生成非常简单,只需将以下行更改为:

input_ids = encoder.encode(prompt) if prompt else [encoder.encoder["<|endoftext|>"]]
然后运行:
python gpt2.py ""
这将生成:
The first time I saw the new version of the game, I was so excited. I was so excited to see the new version of the game, I was so excited to see the new version

因为我们使用的是贪婪采样,所以输出不是很好(重复),而且是确定性的(即,每次我们运行代码时都是相同的输出)。为了得到质量更高且不确定的生成,我们需要直接从分布中抽样(理想情况下,在应用类似top-p的方法之后)。

无条件生成并不是特别有用,但它是展示GPT能力的一种有趣的方式。

微调

我们在训练部分简要介绍了微调。回想一下,微调是指当我们重新使用预训练的权重来训练模型执行一些下游任务时。我们称这一过程为迁移学习。

从理论上讲,我们可以使用零样本或少样本提示,来让模型完成我们的任务,

然而,如果你可以访问token的数据集,微调GPT将产生更好的结果(在给定更多数据和更高质量的数据的情况下,结果可以扩展)。

有几个与微调相关的不同主题,我将它们细分如下:

分类微调

在分类微调中,我们给模型一些文本,并要求它预测它属于哪一类。

例如,以IMDB数据集为例,它包含将电影评为好或差的电影评论:

--- Example 1 ---Text: I wouldn't rent this one even on dollar rental night.Label: Bad--- Example 2 ---Text: I don't know why I like this movie so well, but I never get tired of watching it.Label: Good--- Example 3 ---...

为了微调我们的模型,我们将语言建模头替换为分类头,并将其应用于最后一个token输出:

def gpt2(inputs, wte, wpe, blocks, ln_f, cls_head, n_head):x = wte[inputs] + wpe[range(len(inputs))]for block in blocks:x = transformer_block(x, **block, n_head=n_head)x = layer_norm(x, **ln_f)# project to n_classes# [n_embd] @ [n_embd, n_classes] -> [n_classes]return x[-1] @ cls_head

我们只使用最后一个token输出x[-1],因为我们只需要为整个输入生成单一的概率分布,而不是语言建模中的n_seq分布。

尤其,我们采用最后一个token,因为最后一个token是唯一被允许关注整个序列的token,因此具有关于整个输入文本的信息。

像往常一样,我们优化了w.r.t.交叉熵损失:

def singe_example_loss_fn(inputs: list[int], label: int, params) -> float:logits = gpt(inputs, **params)probs = softmax(logits)loss = -np.log(probs[label]) # cross entropy lossreturn loss

我们还可以通过应用sigmoid而不是softmax来执行多标签分类,并获取关于每个类别的二进制交叉熵损失。

生成式微调

有些任务不能被整齐地归类。例如,总结这项任务。

我们只需对输入和标签进行语言建模,就能对这类任务进行微调。例如,下面是一个总结训练样本:

--- Article ---This is an article I would like to summarize.--- Summary ---This is the summary.

我们像在预训练中一样训练模型(优化w.r.t语言建模损失)。

在预测时间,我们向模型提供直到— Summary —的所有内容,然后执行自回归语言建模以生成摘要。

分隔符— Article —和— Summary —的选择是任意的。如何选择文本的格式由你自己决定,只要它在训练和推理之间保持一致。

注意,我们还可以将分类任务制定为生成式任务(例如使用IMDB):

--- Text ---I wouldn't rent this one even on dollar rental night.--- Label ---Bad

指令微调

如今,大多数最先进的大模型在经过预寻来你后,还会经历额外的指令微调。

在这一步中,模型对数千个人类标记的指令提示+完成对进行了微调(生成)。指令微调也可以称为有监督的微调,因为数据是人为标记的。

那么,指令微调有什么好处呢?

虽然预测维基百科文章中的下一个单词能让模型擅长续写句子,但这并不能让它特别擅长遵循指令、进行对话或总结文档(我们希望GPT能做的所有事情)。

在人类标注的指令+完成对上对其进行微调,是一种教模型如何变得更有用,并使其更易于交互的方法。

这就是所谓的AI对齐,因为我们正在对模型进行对齐,使其按照我们的意愿行事。

参数高效微调

当我们在上述章节中谈到微调时,假定我们正在更新所有模型参数。

虽然这能产生最佳性能,但在计算(需要对整个模型进行反向传播)和存储(每个微调模型都需要存储一份全新的参数副本)方面成本高昂。

解决这个问题最简单的方法就是只更新头部,冻结(即无法训练)模型的其他部分。

虽然这可以加快训练速度,并大大减少新参数的数量,但效果并不是特别好,因为我们失去了深度学习的深度。

相反,我们可以选择性地冻结特定层,这将有助于恢复深度。这样做的结果是,效果会好很多,但我们的参数效率会降低很多,也会失去一些训练速度的提升。

值得一提的是,我们还可以利用参数高效的微调方法。

以Adapters 一文为例。在这种方法中,我们在transformer块中的FFN和MHA层之后添加一个额外的「适配器」层。

适配层只是一个简单的两层全连接神经网络,输入输出维度为 n_embd ,隐含维度小于 n_embd :

隐藏维度的大小是一个超参数,我们可以对其进行设置,从而在参数与性能之间进行权衡。

论文显示,对于BERT模型,使用这种方法可以将训练参数的数量减少到2%,而与完全微调相比,性能只受到很小的影响(<1%)。


初中英语完形填空和阅读技巧

于完形填空题涉及面广,综合性强,能力要求高。 因此,要提高正确率,除了掌握一定的词汇量和一定的语法知识,具备一定的阅读能力、分析能力和逻辑推理能力外,还必须掌握科学的解题方法,提高解题能力。 我们可以采用以下步骤与技巧。 完形填空一般无标题,首句一般不留空白,是完整的一句,全文信息从此开始。 细读首句,可判断文章体裁,预测全文大意和主旨。 完形填空题的文章尽管是有意地抽掉了一些词,使信息中断,造成间隔性的词义空白,但仍不失为完整的语篇。 阅读全文要一气呵成,尽管有空格、生词或不明白的地方,仍要快速读下去。 读时要注意找出关键词、中心词,划出某些代表人物和情节的词,以便于形成思路。 对空格要填的词可作试探性地猜测,为下一步选择答案做好准备,打好基础。 要注意不要在未掌握大意的基础上,边阅读,边做题,这样速度慢、准确率低。 通过通读全文,掌握了文章的大意后,可以从头开始边细读边分析。 根据上下文意思选取语法正确、语义贴切、语言准确的词语。 在这一过程中,一定要瞻前顾后,灵活答题。 所谓“瞻前顾后”,即先读所填词的句子,回顾上一句,兼顾下一句。 如果一句中有两个空白待填,在初定答案时要“双管齐下”,在两处同时试填,然后通读全句,确定答案。 另外在做题时要采取先易后难的原则,对把握性强的选项要做到一锤子定音。 而对那些把握性不强的选项,不妨先放一下,接着再往下做,然后再回过头来补填。 这时,由于通过你对上下文的精研细磨,上文其意自现。 答题时可采用 1)择优法:根据文章及结构边读边填,如果能够立刻判定最佳答案的,不必再去逐个考证其余答案。 2)排除法:如答案一时难以确定,可按空格位置,从语法结构、词语搭配、上下文语境、习惯用法、词义辨析等方面,对选项逐项分析试填。 排除干扰项,从而确定正确答案。

高分求 九月份迎新生售联通卡计划书

2009年“新势力”校园迎新计划书活动背景新学期的即将到来,为了让新同学感受到联通的温暖,体会到宾至如归的感觉,使他们的大学生活有个崭新的开端,也让各位家长高兴而来满意而归,切实做好校园迎新工作。 分部按照分公司的统一部署,通力配合、群策群力、团结拼搏、精心布置,使迎新工作成为热情周到、文明礼貌、以人为本、师生满意、便捷高效的窗口工程、形象工程。 为此,特制定如下迎新工作:成立重庆房地产职业学院迎新直销团队,因为该校情况比较特殊,为私立学校,前期我团队做过相应的营销,虽然取得的一些成绩,但是引来了校方的不满,现学校明确表态,严禁校园内在出现此内活动。 因此我部在该校园将以扫楼为主,活动时间是从开学活动之日起直至2周时间。 二、学校情况概述及直销队伍的建立;我重庆房地产职业学院:新生人数大概在2500人左右,老生人数2500人左右,学生寝室800间,教职员工250人左右。 新生报到时间为2008年8月29日起至8月30。 竞争对手《移动公司》也将与我公司在8月28日同时进场,宣传摊点现在还不清楚,分别是学校大门和新生报到处,学生食堂及寝室根据当时情况,与校方商议后将设置临时摊点。 目前该校已有校园地推组2个,每组3-4人,负责当时每天的现场地推。 扫楼人员30名,男生20名,女生10名,扫楼队长男女各2名,负责每天男生寝室及女生寝室的扫楼工作。 三、营销思路及方法分三阶段来实施第一阶段:前期准备阶段1:跟校方经办人及时沟通联系,跟学校保卫处搭建良好关系。 在活动期间便于我们工作的开展,也为以后对学校维系工作开展,跟新增工作有序开展打下基础。 还要利用好自己现在在学校的一切关系和影响力,和自己主场的优势和特点,发挥最大的作用。 2:在迎新过程中学校校将在学生会和社团选拔一些同学去各个地方迎接新同学的到来,我准备尽最大努力将我手下的直销人员安排进去,为在迎新和过后的销售过程中做更好是准备。 3:迎新物质方面的准备:帐篷,桌子,遮阳伞,宣传资料,直销人员衣服等其他物质在各学校开学前一周准备到位。 第二阶段:新学生集中到校阶段希望公司能在预计于各学校开学前二天把我方帐篷、桌子等物质投放到预定位置,一定先于竞争对手《移动公司》前占领有利位置。 1:在校门处放置帐篷2个,桌子2张。 直销组长学生2名,促销4名。 2—4名直销学生负责散发传单,由直销组长负责售卡。 构筑第一道拦截屏障。 2:新生报道处由分部人员负责。 帐篷一个、桌子三张,直销组长1名,学生直销4名。 直销2名散发传单拉拢客户,直销组长及另2位直销人员。 构筑第二层销售屏障3:在校门新生报道处设直销学生10名,采取跟随的方法跟着新生或其家长争取当即完成卡号的即时销售。 构筑第三层销售屏障。 4:在新生进入寝室布置床位的时候,至少10~20名直销在各楼各层进行扫楼。 中午吃饭时间可转移到食堂促销。 构筑第四层销售屏障。 5:晚上本校销售组长和直销人员采取扫楼的方法在进行促销。 构成第五层销售屏障。 以上工作持续到学生军训开始。 并根据每天的的实际情况做出调整,第三阶段:后期发展学生开始军训后会很快转入正常的教学工作中。 不可能大规模的现场促销,前期由于部分新学生已经使用完卡内话费,处于高流失风险阶段。 如果能及时抓住该阶段帮助学生冲值,帮助其认真了解自己使用的资费处理的不满,把流失风险降低。 在推出吸引同学们的新业务来挽留老用户和发展新用户。 四、所需物资的名细(每天)我房地产职业学院希望公司能给我们提供足够的物资,如:提供帐篷5顶、遮阳伞5把、桌子10张、桌布10张、迎新横幅5条、矿泉水100瓶、小礼品300份、校园资费海报50张。 四、任务目标本次在我房地产职业学院组场目标新增新势力品牌G网用户800户,所以在暑假期间我们这边要充足安排好人远和计划好怎么样才能更好发挥好组场的优势,为迎新做好充足的准备。 我们通过本次暑假在公司做直销工作的锻炼,我相信我们在9月迎新工作中,在公司人员的带领下和我们共同努力下,一定能在9月迎新工作中取得圆满成功。 我们的宗旨是不希望把这个迎新工作做得很好,只希望把这次迎新工作做得更好。

失恋了怎么挽回

60行代码

挽回女友有几大错误是不能犯的,偏偏又是最常见容易被犯的错误,不少男生就因此而被拉黑删好友,导致印象越来越差。 那么,以下这几个雷区,你有没有触碰?1.讲道理。 男生都会说,女生是不讲理的动物,但偏偏他们就是喜欢不按套路出牌,你不爱讲道理是吧,那我就偏要跟你说,说到你服为止。 分手后不停地讲大道理,分析事情对错,企图让女生明白错在她身上,是她不讲理。 结果可想而知,道理你赢了,爱情却输得彻底。 2.死缠烂打。 有种直男癌,他们觉得女的天性就喜欢逆来顺受,不能宠着让着哄着,必须以压力使其屈服。 你说分手,那我就找别人来评评理,请出父母和朋友出面讲和;你不听电话,那我就继续打,轰炸到你肯接为止。 3.当发现做完以上这些,女生都不为所动后,有的男生会开始服软,并且是180度的大转变,彻底以跪舔求和的方式,试图让对方心软。 但是,这种做法不仅丢失了原有的框架,更容易引起厌恶,这是另一种死缠烂打的做法,相同的是,只会让女生更加反感。 以上几种做法,都是分手后最不理智的行为,只会让对方觉得你不成熟,自己的离开是对的。 如果情况严重,可能直接会拉黑删好友,断联个耳根清净。 如果以上雷区你都中招了,那接下来要挽回是比较难的,机会不是完全没有,只是要找到对的时机才出手。 一般来说,这时候女生会经历以下两个阶段。 1.抗拒期。 视乎你之前错得有多离谱,女生在这个阶段会有长短不一的抗拒期。 这时候她会拒绝接收有关你的联系,甚至连从朋友口中听到你的消息都感到厌烦。 在这个阶段,就不要尝试硬碰硬的去恢复联系了,除非你有足够让她无法拒绝的借口,即便如此,还是需要用语言技巧更舒服的让对方接受。 用什么话题,先说后说顺序非常重要,只要走错一步可能接下来就是万劫不复。 并且这阶段主动联系风险较大,如非必要而紧急情况,比如说接下来她可能就转头找别人相亲结婚了这种,不建议贸然行动。 给点时间她释放负面情绪,同时也是对你负面印象的清洗,经历过这个抗拒期后再试图联系不迟。 2.放松期。 经历过上面的抗拒期后,她对你不至于那么防备了,这时候她才有空闲和足够的理智去思考你们这段失败的感情经历。 对她来说,哪怕是自己认定的分手,她都会反思到底问题出在哪里,因为没有人希望在感情路上重蹈覆辙再次受伤的。 所以这时候是你最好的联系时机,要稳稳地抓住,过了可能接下来就没你什么事了。 这个环节对时机的判断是最关键的。 你当然可以通过朋友圈了解她的现状和心情,如果为了更有把握,还可以通过共同好友的试探去做,但切勿暴露是你在试探她,所以这个好友一定要是可控而可靠的。 如果时机合适,接下来你就可以着手做下一步的挽回了。 接着你要做的,是先无目的的接近她。 之所以说无目的,是因为目的性太强的接触,会让她产生防备,因为你是有所图的,你想复合,而她并不想。 所以你的接近需要师出有名,并且是以对方无法拒绝的名义。 比如说你们共同朋友组织的聚会,在还有朋友圈交集的情况下,如果她知道你已经没有挽回的目的,那么是很有可能参加的。 但需要注意这种场合一定不要设得太像个“局”,最开始为了避免让对方感到害怕不敢赴会,不要选择私密性强的封闭场所,即便是饭局,也最好选择在大厅而不是包间进行。 同时坐的位置也有讲究,不用刻意坐对方的身边,或者有意的和她保持距离,最好的情况是你假装因为工作太忙等晚点再到,然后找到空位顺势坐下。 又或是比她要更早到达,由你先坐下,接下来她会坐到哪里,就全凭她的意思了。 一两次的饭局后可能你们会逐渐消除之前的尴尬。 接下来可以试试通过偶遇,这里的偶遇可以是经过精心设计的,而不仅仅是靠缘分和运气。 同样的,偶遇要表现得自然,不要显得是事先早就知道会发生的结果。 见面后自然的打招呼,注意要绝口不提感情的事。 如果她对你还多少显得疏远的话,打完招呼后表示自己有事主动先离开现场就好。 如果对方并未表现得反感,可以问问她接下来的计划,有时间可以一起坐下来喝杯咖啡。 这里不管气氛如何,切忌提出复合,哪怕是一丁点的想法都不要透露。 但是如果前面已经被删了好友,这时候就可以顺势提出来,重新建立联系了。 最后要做的事,就是在接下来自然接触的过程中增加你们之间的交集,让感情逐步升温,同时时机适当又不刻意的情况下去传达给对方你的改变,最好是以前她觉得你不好的地方,让她对你印象重新打分。 恢复联系后这个阶段,注意建立亲密感,是避免停留在朋友区的重要手段。 但建立亲密感之前要避免暴露你对她超越朋友的好感,否则目的性太强,太急功近利反而容易前功尽弃。 感情的事很难是一帆风顺的,过程中总会遇到各种问题和矛盾,而挽回过程中的各种因素,也可能会影响最终复合的走向。 所以在进行每一步之前,谨慎地做出选择,采取最有利于挽回的方法去推进,那么挽回其实也是可以预见和计划好的事。 重要的是,在这个过程中自己也应该学会处理自己的情感问题,习得情感自由。

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

发表评论

热门推荐