让我们逐行重现 GPT-2:第 1 部分
欢迎来到雲闪世界。Andrej Karpathy 是人工智能 (AI) 领域的顶尖研究人员之一。他是 OpenAI 的创始成员之一,曾领导特斯拉的 AI 部门,目前仍处于 AI 社区的前沿。
在第一部分中,我们重点介绍如何实现 GPT-2 的架构。虽然 GPT-2 于 2018 年由 OpenAI 开源,但它是用 Tensor Flow 编写的,这是一个比 PyTorch 更难调试的框架。因此,我们将使用更常用的工具重新创建 GPT-2。仅使用我们今天要创建的代码,您就可以创建自己的 LLM!
块大小— 告诉我们 Transformer 可以处理输入长度中的多少个位置。一旦超过此限制,性能就会下降,因为您必须回绕(您可以在我的 Long RoPE 博客中详细了解我们如何扩展此限制,而无需从头开始训练新模型)
词汇量——告诉我们模型能够理解和使用多少个独特的标记。一般来说,研究人员发现,词汇量越大,模型对语言的理解就越精确,并且能够捕捉到响应中的更多细微差别。
层 —我们神经网络的隐藏层的一部分。具体来说,我们指的是下面灰色框中显示的重复计算的次数:
我们的模型中的一层
嵌入——我们传递给模型的数据的向量表示。
多头注意力——我们不是运行一次注意力,而是运行 n 次,然后将所有结果连接在一起以获得最终结果。
让我们进入代码吧!
GPT 类及其参数
@dataclass
class GPTConfig:block_size : int = 1024vocab_size : int = 50257n_layer : int = 12n_head : int = 12n_embd : int = 768
首先,我们在 GPTConfig 类中设置 5 个超参数。block_size
似乎与 和 一样有些随意n_layer
。n_head
换句话说,这些值是根据研究人员认为具有最佳性能的值经验选择的。 此外,我们选择 786,因为n_embd
这是为 GPT-2 论文选择的值,我们决定模仿它。
但是,是根据我们将使用的 GPT-2 标记器vocab_size
设置的。GPT-2 标记器是使用字节对编码算法创建的(在此处信息)。它从一组初始词汇开始(在我们的例子中是 256 个),然后根据新词汇出现在训练集中的频率,通过训练数据创建新词汇。它会一直这样做,直到达到极限(在我们的例子中是 50,000)。最后,我们将词汇留作内部使用(在我们的例子中是结束标记字符)。把它们加起来,我们得到了 50,257。tiktoken
class GPT(nn.Module):def __init__(self, config):super().__init__()self.config = config# ...
设置好配置后,我们创建一个 GPT 类,它是 torchnn.Module
类的一个实例。这是所有 PyTorch 神经网络的基类,因此通过使用它,我们可以访问 PyTorch 针对这些类型的模型的所有优化。每个优化nn.Module
都有一个forward
函数来定义模型前向传递期间发生的情况(稍后会详细介绍)。
我们首先运行基类中的超级构造函数,然后创建一个transformer
对象ModuleDict
。创建它是因为它可以让我们像对象一样进行索引transformer
,当我们想要从 HuggingFace 加载权重时,以及当我们想要调试并快速浏览我们的模型时,它都会派上用场。
class GPT(nn.Module):def __init__(self, config):# ...self.transformer = nn.ModuleDict(dict(wte = nn.Embedding(config.vocab_size, config.n_embd),wpe = nn.Embedding(config.block_size, config.n_embd),h = nn.ModuleList([Block(config) for _ in range(config.n_layer)]),ln_f = nn.LayerNorm(config.n_embd)))
这里transformer
有 4 个主要部分需要加载:token 嵌入的权重 ( wte
)、位置编码的权重 ( wpe
)、隐藏层 ( h
) 和层规范化 ( )。此设置主要ln_f
遵循“Attention is All You Need”中 Transformer 架构的解码器部分(输出嵌入 ~ 、位置编码 ~ 、隐藏层 ~ )。一个关键的区别是,在我们的架构中,所有隐藏层完成后,我们还有一个额外的规范化层。wte
wte
h
ln_f
“Attention is All You Need”中展示的解码器架构的一半
wte
和都是wpe
嵌入,因此我们自然nn.Embedding
会使用 类 来表示它们。我们的隐藏层是 Transformer 的大部分逻辑所在,所以我稍后会详细介绍。现在,只需注意我们正在创建对象的循环,以便Block
我们拥有n.layer
。最后,我们使用内置的nn.LayerNorm
,ln_f
它将根据以下等式对我们的输出进行规范化(其中 x 和 y 是输入和输出,E[x] 是平均值,γ 和 β 是可学习的权重)。
PyTorch 中的层归一化方程
class GPT(nn.Module):def __init__(self, config):# ...self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False)# weight sharing schemeself.transformer.wte.weight = self.lm_head.weight# initalize weightsself.apply(self._init_weights)
接下来,我们设置网络的最后一个线性层,它将生成模型的逻辑。在这里,我们从模型的嵌入维度(768)投影到模型的词汇量(50,257)。这里的想法是,我们采用了隐藏状态并将其扩展以映射到我们的词汇表上,以便我们的解码器头可以使用每个词汇表上的值来确定下一个标记应该是什么。
最后,在我们的构造函数中,我们有一个有趣的优化,我们告诉模型使标记器权重与线性层权重相同。这样做是因为我们希望线性层和标记器对标记有相同的理解(如果两个标记在输入模型时相似,则模型输出时相同的两个标记也应该相似)。最后,我们初始化模型的权重,以便我们可以开始训练。
class GPT(nn.Module):
# ...def forward(self, idx, targets=None):B, T = idx.size() assert T <= self.config.block_size, f"maximum sequence length breached"pos = torch.arange(0, T, dtype=torch.long, device=idx.device)pos_emb = self.transformer.wpe(pos)tok_emb = self.transformer.wte(idx)x = tok_emb + pos_emb # hidden broadcastfor block in self.transformer.h:x = block(x)x = self.transformer.ln_f(x)logits = self.lm_head(x)loss = Noneif targets is not None:loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1))return logits, loss
我们的前向函数精确地说明了我们的模型在前向传递过程中的行为方式。我们首先验证我们的序列长度不大于我们配置的最大值 ( block_size
)。一旦验证通过,我们将创建一个值为 0 到 T-1 的张量(例如,如果 T = 4,我们将有 tensor([0, 1, 2, 3]) 并通过我们的位置嵌入权重运行它们。完成后,我们将通过 token 嵌入权重运行输入张量。
我们将 token 和位置嵌入组合成x
,需要广播来组合它们。由于 大于tok_emb
(pos_emb
在我们的示例中为 50257 vs 1024),x 的尺寸为tok_emb
。现在是我们的隐藏状态,我们将通过 for 循环将其传递到隐藏层。我们小心地在每次通过一个块后x
进行更新。x
接下来,我们x
通过 LayerNormalization进行归一化ln_f
,然后进行线性投影以获得预测下一个标记所需的 logit。如果我们正在训练模型(我们通过参数发出信号targets
),我们将计算我们刚刚生成的 logit 和变量中保存的地面真实值之间的交叉熵targets
。我们通过损失函数实现这一点cross_entropy
。为了正确做到这一点,我们需要通过 将logits
和转换target
为正确的形状.view()
。我们要求 pytorch 在通过 -1 时推断出正确的大小。
这个类中还有一个函数,即初始化函数,但我们稍后会讨论初始化逻辑。现在,让我们深入研究 Block 逻辑,它将帮助我们实现多头注意力和 MLP。
块类
class Block(nn.Module):def __init__(self, config):super().__init__()self.ln_1 = nn.LayerNorm(config.n_embd)self.attn = CausalSelfAttention(config)self.ln_2 = nn.LayerNorm(config.n_embd)self.mlp = MLP(config)
# ...
Block 被实例化为nn.Module
,因此我们也在开始时调用超级构造函数进行优化。接下来,我们设置与“Attention is All You Need”论文中所述的相同计算 — 2 层规范化、注意力计算和通过 MLP 的前馈层。
《注意力就是一切》中的隐藏层
class Block(nn.Module):
# ...def forward(self, x):x = x + self.attn(self.ln_1(x))x = x + self.mlp(self.ln_2(x))return x
然后,我们定义一个forward
函数,PyTorch 将在模型的每次前向传递中调用该函数。请注意,这里我们做的事情与“注意力就是你所需要的一切”不同。我们分别将层规范化设置为在注意力和前馈之前发生。这是 GPT-2 论文见解的一部分,你可以看到,像这样进行微小的改变会带来巨大的不同。请注意,原始张量的添加保持在相应的相同位置。当我们设置权重初始化函数时,这 2 个添加将非常重要。
这个类是一个很好的抽象,因为它让我们可以交换注意力的实现或者选择除了 MLP 之外的另一种前馈函数,而不必大规模重构代码。
CausalSelfAttention 类
class CausalSelfAttention(nn.Module):def __init__(self, config):super().__init__()assert config.n_embd % config.n_head == 0self.c_attn = nn.Linear(config.n_embd, 3*config.n_embd)self.c_proj = nn.Linear(config.n_embd, config.n_embd)self.c_proj.NANOGPT_SCALE_INIT = 1self.n_head = config.n_headself.n_embd = config.n_embdself.register_buffer('bias', torch.tril(torch.ones(config.block_size, config.block_size)).view(1,1, config.block_size, config.block_size))
# ...
注意力是我们模型的重要组成部分,因此这里自然有许多配置。我们使用 assert 语句作为调试工具,以确保我们传递的配置维度是兼容的。然后我们创建一些辅助函数,这些函数将在我们进行自我注意力时为我们提供帮助。首先,我们有c_attn
和,c_proj
它们是线性投影,可将我们的隐藏状态转换为注意力计算所需的新维度。c_proj.NANOGPT_SCALE_INIT
是我们在此处和 MLP 中设置的标志,它将帮助我们稍后进行权重初始化(实际上,这可以命名为任何名称)。
最后,我们告诉 torch 创建一个名为 的缓冲区,该缓冲区在训练期间不会更新bias
。偏差将是一个尺寸为block_size
x的下三角矩阵block_size
,然后我们将其转换为尺寸为 1 x 1 x block_size
x 的4D 张量block_size
。1 x 1 已完成,以便我们可以在单个通道中批量计算这些。此缓冲区将用于对我们的多头注意力应用掩码。
class CausalSelfAttention(nn.Module):
# ...def forward(self, x):B, T, C = x.size() # batch size, sequence length, channelsqkv = self.c_attn(x)q, k, v = qkv.split(self.n_embd, dim=2)# transpose is done for efficiency optimizationk = k.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)q = q.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)v = v.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)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)y = att @ vy = y.transpose(1,2).contiguous().view(B, T, C)y = self.c_proj(y)return y
现在开始实现注意力机制,重点是让它在 torch 中表现优异。逐行来看,我们首先找到输入张量 x 中的批处理大小、序列长度和通道。然后,我们将调用c_attn
之前的函数将隐藏状态投影到我们需要的维度中。然后,我们将结果拆分为 3 个 (B、T、C) 形状的张量(具体来说,一个用于查询,一个用于键,一个用于值)。
然后,我们调整 q、k 和 v 的维度,以便能够高效地对这些维度进行多头注意。通过将维度从 (B, T, C) 更改为 (B, T, self.n_head, C // self.n_head),我们正在划分数据,以便每个头部都有自己独特的数据来操作。我们转置视图,以便我们可以制作T
第三维度和self.n_head
第二维度,从而让我们更容易地连接头部。
注意力方程式来自“注意力就是你所需要的一切”
现在我们有了值,我们可以开始计算了。我们在查询和键之间执行矩阵乘法(确保将键转置为正确的方向),然后除以 k 大小的平方根。经过此计算后,我们应用来自寄存器的偏差,以便未来 token 的注意力数据不会影响当前的 token(这就是为什么我们只对时间和通道维度大于 T 的 token 应用掩码的原因)。完成后,我们应用 softmax 仅传递某些信息。
一旦打开掩码,我们将值乘以 v,然后将值转置回 (B, T, self.n_head, C // self.n_head) 设置。我们调用.contiguous()
以确保内存中的所有数据都彼此相邻排列,最后将张量转换回它所带的 (B, T, C) 维度(因此,在此步骤中连接我们的注意力头)。
最后,我们使用线性投影c_proj
转换回隐藏状态的原始维度。
MLP 类
class MLP(nn.Module):def __init__(self, config):super().__init__()self.c_fc = nn.Linear(config.n_embd, 4 * config.n_embd)self.gelu = nn.GELU(approximate="tanh")self.c_proj = nn.Linear(4 * config.n_embd, config.n_embd)self.c_proj.NANOGPT_SCALE_INIT = 1
# ...
与之前的所有类一样,MLP 继承自nn.Module
。我们首先设置一些辅助函数——特别是c_fc
和c_proj
线性投影层,分别从我们的嵌入扩展到 4 倍大小,然后再扩大回来。接下来,我们有 GELU。Karpathy 强调说,这里的近似参数只是为了让我们能够紧密匹配 GPT-2 论文而设置的。虽然当时 GELU 的近似是必要的,但现在我们不再需要近似——我们可以精确计算。
class MLP(nn.Module):
# ...def forward(self, x):x = self.c_fc(x)x = self.gelu(x)x = self.c_proj(x)return x
那么我们的前向传递就相对简单了。我们在输入张量上调用每个函数并返回最终结果。
拥抱脸部连接代码
由于 GPT-2 是开源的,因此可以在 Hugging Face 上使用。虽然我们的目标是训练我们自己的模型,但能够将我们的结果与 OpenAI 在训练中发现的结果进行比较还是不错的。为了做到这一点,我们有下面的函数来提取权重并将其填充到我们的GPT
类中。
此代码还允许我们重复使用此代码来从 Hugging Face 中提取基础模型并对其进行微调(进行了一些修改,因为目前它仅针对 gpt-2 进行了优化)。
class GPT(nn.Module):
# ...@classmethoddef from_pretrained(cls, model_type):"""Loads pretrained GPT-2 model weights from huggingface"""assert model_type in {'gpt2', 'gpt2-medium', 'gpt2-large', 'gpt2-xl'}from transformers import GPT2LMHeadModelprint("loading weights from pretrained gpt: %s" % model_type)# n_layer, n_head and n_embd are determined from model_typeconfig_args = {'gpt2': dict(n_layer=12, n_head=12, n_embd=768), # 124M params'gpt2-medium': dict(n_layer=24, n_head=16, n_embd=1024), # 350M params'gpt2-large': dict(n_layer=36, n_head=20, n_embd=1280), # 774M params'gpt2-xl': dict(n_layer=48, n_head=25, n_embd=1600), # 1558M params}[model_type]config_args['vocab_size'] = 50257 # always 50257 for GPT model checkpointsconfig_args['block_size'] = 1024 # always 1024 for GPT model checkpoints# create a from-scratch initialized minGPT modelconfig = GPTConfig(**config_args)model = GPT(config)sd = model.state_dict()sd_keys = sd.keys()sd_keys = [k for k in sd_keys if not k.endswith('.attn.bias')] # discard this mask / buffer, not a param
# ...
从顶部开始,我们引入 HuggingFace 的transformers
库并设置在 GPT-2 模型的不同变体之间变化的超参数。由于和vocab_size
没有block_size
变化,您可以看到我们将它们硬编码进去。然后我们将这些变量传递到GPTConfig
之前的类中,然后实例化模型对象(GPT
)。最后,我们从模型中删除所有以结尾的键.attn.bias
,因为这些不是权重,而是我们之前设置的寄存器,用于帮助我们的注意力函数。
class GPT(nn.Module):
# ...@classmethoddef from_pretrained(cls, model_type):
# ...model_hf = GPT2LMHeadModel.from_pretrained(model_type)sd_hf = model_hf.state_dict()# copy while ensuring all of the parameters are aligned and match in names and shapessd_keys_hf = sd_hf.keys()sd_keys_hf = [k for k in sd_keys_hf if not k.endswith('.attn.masked_bias')] # ignore these, just a buffersd_keys_hf = [k for k in sd_keys_hf if not k.endswith('.attn.bias')] # same, just the mask (buffer)transposed = ['attn.c_attn.weight', 'attn.c_proj.weight', 'mlp.c_fc.weight', 'mlp.c_proj.weight']# basically the openai checkpoints use a "Conv1D" module, but we only want to use a vanilla Linear# this means that we have to transpose these weights when we import themassert len(sd_keys_hf) == len(sd_keys), f"mismatched keys: {len(sd_keys_hf)} != {len(sd_keys)}"
接下来,我们从 HuggingFace 类中加载模型GPT2LMHeadModel
。我们从这个模型中取出键,同样忽略attn.masked_bias
和attn.bias
键。然后我们有一个断言来确保 hugging face 模型中的键数与我们模型中的键数相同。
class GPT(nn.Module):
# ...@classmethoddef from_pretrained(cls, model_type):
# ...for k in sd_keys_hf:if any(k.endswith(w) for w in transposed):# special treatment for the Conv1D weights we need to transposeassert sd_hf[k].shape[::-1] == sd[k].shapewith torch.no_grad():sd[k].copy_(sd_hf[k].t())else:# vanilla copy over the other parametersassert sd_hf[k].shape == sd[k].shapewith torch.no_grad():sd[k].copy_(sd_hf[k])return model
为了完善该函数,我们循环遍历 Hugging Face 模型中的每个键,并将其权重添加到我们模型中的相应键。有些键需要进行操作,以便它们适合我们使用的数据结构。我们运行该函数.t()
将 hugging face 矩阵转置为我们所需的尺寸。对于其余部分,我们直接复制它们。你会注意到我们正在使用torch.no_grad()
。这告诉 torch 它不需要缓存模型反向传播的值,这是另一个使其运行更快的优化。
生成我们的第一个预测(采样循环)
有了现在的类,我们可以运行模型并让它给我们输出标记(只需确保如果你按顺序执行此操作,请在 GPT 构造函数中注释掉 _init_weights 调用)。下面的代码显示了我们如何做到这一点。
device = "cpu"
if torch.cuda.is_available():device = "cuda"
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():device = "mps"
print(f"device {device}")torch.manual_seed(1337)model = GPT(GPTConfig())
model.eval()
model.to(device)
我们首先确定可以使用哪些设备。Cuda 是 NVIDIA 的平台,可以运行极快的 GPU 计算,因此如果我们可以使用使用 CUDA 的芯片,我们就会使用它们。如果我们无法访问,但我们使用的是 Apple Silicon,那么我们将使用它。最后,如果我们两者都没有,那么我们会回到 CPU(这将是最慢的,但每台计算机都有一个,所以我们知道我们仍然可以在它上面进行训练)。
然后,我们使用默认配置实例化我们的模型,并将模型置于“ eval
”模式 —(这会执行许多操作,例如禁用 dropout,但从高层次来看,它可以确保我们的模型在推理过程中更加一致)。设置完成后,我们将模型移动到我们的设备上。请注意,如果我们想使用 HuggingFace 权重而不是我们的训练权重,我们将修改倒数第三行以读取:model = GPT.from_pretrained(‘gpt2’)
import tiktoken
enc = tiktoken.get_encoding('gpt2')
tokens = enc.encode("Hello, I'm a language model,")
tokens = torch.tensor(tokens, dtype=torch.long)
tokens = tokens.unsqueeze(0).repeat(num_return_sequences, 1)
x = tokens.to(device)
我们现在引入tiktoken
gpt2 编码并让其标记化我们的提示。我们获取这些标记并将其放入张量中,然后在下面的行中将其转换为批次。unsqueeze()
将向张量添加一个大小为 1 的新的第一个维度,并将在第一个维度内repeat
重复整个张量num_return_sequences
时间,在第二个维度内重复一次。我们在这里所做的是格式化我们的数据以适合我们的模型期望的批处理模式。具体来说,我们现在匹配 (B, T) 格式:num_return_sequences
x 编码的提示长度。一旦我们将输入张量传递到模型的开头,我们的wte
和wpe
将创建 C 维度。
while x.size(1) < max_length:with torch.no_grad():logits, _ = model(x)logits = logits[:, -1, :]probs = F.softmax(logits, dim=-1)topk_probs, topk_indices = torch.topk(probs, 50, dim=-1)ix = torch.multinomial(topk_probs, 1)xcol = torch.gather(topk_indices, -1, ix)x = torch.cat((x, xcol), dim=1)
现在它们已经准备好了,我们将它们发送到设备并开始采样循环。该循环将完全是前向传递,因此我们将其包装在中torch.no_grad
以阻止其缓存任何后向传播。我们的 logits 形状为 (batch_size, seq_len, vocab_size) — (B,T,C),其中 C 位于模型的前向传递之后。
我们只需要序列中的最后一项来预测下一个标记,因此我们取出[:, -1, :]
然后我们获取这些 logit 并将其运行softmax
以获得标记概率。取前 50 名,然后我们选择前 50 名的随机索引并将其选为我们的预测标记。然后我们获取有关该信息并将其添加到我们的张量中x
。通过连接xcol
到x
,我们可以根据我们刚刚预测的内容进入下一个标记。这就是我们编写自回归的方法。
for i in range(num_return_sequences):tokens = x[i, :max_length].tolist()decoded = enc.decode(tokens)print(f">> {decoded}")
采样循环完成后,我们可以遍历每个选定的标记并对其进行解码,向用户显示响应。我们从i
批处理中的第个标记中抓取数据并对其进行解码以获取下一个标记。
如果您在我们的初始模型上运行采样循环,您会注意到输出有很多不足之处。这是因为我们还没有训练任何权重。接下来的几节课将展示如何开始对模型进行简单的训练。
数据加载器精简版
所有训练都需要高质量的数据。对于 Karpathy 的视频,他喜欢使用公共领域的莎士比亚文本(更纤细的数据可在此处找到)。
class DataLoaderLite:def __init__(self, B, T):self.B = Bself.T = Twith open('shakespeare.txt', "r") as f:text = f.read()enc = tiktoken.get_encoding('gpt2')tokens = enc.encode(text)self.tokens = torch.tensor(tokens)print(f"1 epoch = {len(self.tokens) // B * T} batches")self.current_position = 0
我们首先简单地打开文件并读入文本。此数据源仅为 ASCII,因此我们不必担心任何意外的二进制字符。我们使用tiktoken
来获取正文的编码,然后将这些标记转换为张量。然后,我们创建一个名为的变量current_position
,它将让我们知道我们当前正在从标记张量中的哪个位置进行训练(自然,这被初始化为开头)。请注意,此类不是从继承的nn.Module
,主要是因为我们这里不需要该forward
函数。与采样循环的提示部分一样,我们的 DataLoaderLite 类只需要生成形状为 (B, T) 的张量。
class DataLoaderLite:
# ...def next_batch(self):B, T = self.B, self.Tbuf = self.tokens[self.current_position: self.current_position+(B*T + 1)]x = (buf[:-1]).view(B, T)y = (buf[1:]).view(B,T)self.current_position += B * Tif self.current_position + (B*T+1) > len(self.tokens):self.current_position = 0return x,y
在上面我们定义了next_batch
帮助训练的函数。为了让程序运行得更快,我们喜欢批量运行计算。我们使用 B 和 T 字段来确定我们将要训练的批次大小 (B) 和序列长度 (T)。使用这些变量,我们创建一个缓冲区来保存我们将要训练的标记,将维度设置为行 B 和列 T。请注意,我们从 读取到current_position
,current_position + (B*T + 1)
其中 +1 是为了确保我们拥有批次的所有基本事实值B*T
。
然后,我们按照相同的方式设置模型输入 ( x
) 和预期输出 ( )。是除最后一个字符之外的整个缓冲区,是除第一个字符之外的整个缓冲区。基本思想是,给定令牌缓冲区中的第一个值,我们期望从模型中获取令牌缓冲区中的第二个令牌。y
x
y
最后,我们更新current_position
并返回x
和y
。
权重初始化
因为我们处理的是概率,所以我们想要为权重选取初始值,这样就可能需要更少的训练次数才能正确计算。我们的_init_weights
函数可以帮助我们做到这一点,方法是用零或正态分布初始化权重。
class GPT(nn.Module):
# ...def _init_weights(self, module):# layer norm is by default set to what we want, no need to adjust itif isinstance(module, nn.Linear):std = 0.02if hasattr(module, "NANOGPT_SCALE_INIT"):std *= (2 * self.config.n_layer) ** -0.5 # 2 * for 2 additions (attention & mlp)torch.nn.init.normal_(module.weight, mean=0.0, std=std)# reasonable values are set based off a certain equationif module.bias is not None:torch.nn.init.zeros_(module.bias)elif isinstance(module, nn.Embedding):torch.nn.init.normal_(module.weight, mean=0.0, std=0.02 )
如果您还记得之前的内容,我们将 GPT 类的每个字段都传入了_init_weights
,因此我们正在处理nn.Module
s 。我们使用 Xavier 方法来初始化我们的权重,这意味着我们将采样分布的标准差设置为1 / sqrt(hidden_layers)
。您会注意到,在代码中,我们经常使用硬编码的 0.02 作为标准差。虽然这看起来可能很随意,但从下表中您可以看到,由于 GPT-2 使用的隐藏维度都大约为 0.02,因此这是一个很好的近似值。
查看代码时,我们首先检查nn.Module
正在操作的模块属于哪种子类型。
如果模块是线性的,那么我们将检查它是否是我们从MLP
或CasualSelfAttention
类中投影的投影之一(通过检查它是否设置了NANO_GPT_INIT
标志)。如果是,那么我们的 0.02 近似值将不起作用,因为这些模块中的隐藏层数量正在增加(这是我们在类中添加张量的函数Block
)。因此,GPT-2 论文使用缩放函数来解释这一点:1/sqrt(2 * self.config.n_layer)
。2*
是因为我们的Block
有 2 个地方我们要添加张量。
如果线性模块中有偏差,我们将首先将它们全部初始化为零。
如果我们有一个Embedding
模块(如令牌或位置编码部分),我们将使用相同的正态分布(标准差为 0.02)对其进行初始化。
如果您还记得的话,我们的模型中还有另一种模块子类型:nn.LayerNorm
。此类已使用正态分布进行了初始化,因此我们认为这已经足够好了,不需要进行任何更改。
训练循环
现在我们已经设置了训练基础,让我们组合一个快速训练循环来训练我们的模型。
device = "cpu"
if torch.cuda.is_available():device = "cuda"
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():device = "mps"
print(f"device {device}")num_return_sequences = 5
max_length = 30torch.manual_seed(1337)train_loader = DataLoaderLite(B=4, T=32)model = GPT(GPTConfig())
model.to(device)
您可以看到,我们重复设备计算以获得最佳性能。然后,我们将数据加载器设置为使用批处理大小 4 和序列长度 32(任意设置,尽管 2 的幂对内存效率最好)。
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)
for i in range(50):x, y = train_loader.next_batch()x, y = x.to(device), y.to(device)optimizer.zero_grad() #have to start with a zero gradientlogits, loss = model(x, y)loss.backward() #adds to the gradient (+=, which is why they must start as 0)optimizer.step()print(f"loss {loss.item()}, step {i}")
现在我们有了优化器,它将帮助我们训练模型。优化器是一个 PyTorch 类,它接收应该训练的参数(在我们的例子中是从类中给出的参数GPT
),然后接收学习率,它是训练期间的一个超参数,决定了我们应该以多快的速度调整参数——学习率越高意味着每次运行后权重的变化越大。我们根据 Karpathy 的建议选择了我们的值。
然后我们使用 50 个训练步骤来训练模型。我们首先获取训练批次并将其移动到我们的设备上。我们将优化器的梯度设置为零(pytorch 中的梯度是总和,因此如果我们不将其归零,我们将从上一批中携带信息)。我们计算模型的 logits 和损失,然后运行反向传播以确定新的权重模型应该是什么。最后,我们运行optimizer.step()
以更新我们所有的模型参数。
完整性检查
要查看上述所有代码的运行情况,您可以查看我的 Google Colab,我将所有这些代码组合起来并在 NVIDIA T4 GPU 上运行。运行我们的训练循环,我们看到损失从 ~11 开始。为了进行合理性测试,我们预计在开始时预测正确标记的几率是 (1/ vocab_size
)。通过简化的损失函数-ln
,我们得到 ~10.88,这正是我们开始的地方!
感谢关注雲闪世界。(亚马逊aws和谷歌GCP服务协助解决云计算及产业相关解决方案)
订阅频道(https://t.me/awsgoogvps_Host)
TG交流群(t.me/awsgoogvpsHost)
相关文章:
让我们逐行重现 GPT-2:第 1 部分
欢迎来到雲闪世界。Andrej Karpathy 是人工智能 (AI) 领域的顶尖研究人员之一。他是 OpenAI 的创始成员之一,曾领导特斯拉的 AI 部门,目前仍处于 AI 社区的前沿。 在第一部分中,我们重点介绍如何实现 GPT-2 的架构。虽然 GPT-2 于 2018 年由 …...
第十九天内容
上午 1、构建vue发行版本 2、java环境配置 jdk软件包路径: https://download.oracle.com/java/22/latest/jdk-22_linux-x64_bin.tar.gz 下午 1、安装tomcat软件 tomcat软件包路径: https://dlcdn.apache.org/tomcat/tomcat-10/v10.1.26/bin/apache-to…...
Hive之扩展函数(UDF)
Hive之扩展函数(UDF) 1、概念讲解 当所提供的函数无法解决遇到的问题时,我们通常会进行自定义函数,即:扩展函数。Hive的扩展函数可分为三种:UDF,UDTF,UDAF。 UDF:一进一出 UDTF:一进多出 UDAF:…...
jdk1.8中HashMap为什么不直接用红黑树
最开始使用链表的时候,空间占用比较少,而且由于链表短,所以查询时间也没有太大的问题。可是当链表越来越长,需要用红黑树的形式来保证查询的效率。 参考资料: https://blog.51cto.com/u_13294304/3075723...
消息推送只会用websocket、轮询?试试SSE,轻松高效。
SSE介绍 HTTP Server-Sent Events (SSE) 是一种基于 HTTP 的服务器推送技术,它允许服务器向客户端推送数据,而无需客户端发起请求。以下是 HTTP SSE 的主要特点: 单向通信: SSE 是一种单向通信协议,服务器可以主动向客户端推送数据,而客户端只能被动接收数据。 持久连接: SS…...
Spring-Retry 框架实战经典重试场景
Spring-Retry框架是Spring自带的功能,具备间隔重试、包含异常、排除异常、控制重试频率等特点,是项目开发中很实用的一种框架。 1、引入依赖 坑点:需要引入AOP,否则会抛异常。 xml <!-- Spring-Retry --> <dependency&…...
人工智能在医疗领域的应用与挑战
随着人工智能技术的不断发展,其在医疗领域的应用也越来越广泛。从辅助诊断到治疗决策,人工智能正在逐步改变着传统的医疗模式。然而,人工智能在医疗领域的应用也面临着诸多挑战,如数据隐私、伦理道德等问题。本文将探讨人工智能在…...
Windows下nmap命令及Zenmap工具的使用方法
一、Nmap简介 nmap是一个网络连接端扫描软件,用来扫描网上电脑开放的网络连接端。确定哪些服务运行在哪些连接端,并且推断计算机运行哪个操作系统(这是亦称 fingerprinting)。它是网络管理员必用的软件之一,以及用以评…...
深入了解-什么是CUDA编程模型
CUDA(Compute Unified Device Architecture,统一计算架构)是NVIDIA推出的一种面向GPU的并行计算平台和编程模型。它允许开发者利用NVIDIA的GPU进行通用目的的并行计算,从而加速应用程序的运行速度。CUDA编程模型为开发者提供了强大…...
111111111111111111
11111111111111111111...
环境如何搭建部署Nacos
这里我使用的是Centos7, Nacos 依赖 Java环境来运行。如果您是从代码开始构建并运行Nacos,还需要为此配置 Maven环境,请确保是在以下版本环境中安装使用 ## 1、下载安装JDK wget https://download.oracle.com/java/17/latest/jdk-17_linux-x6…...
什么是 5G?
什么是 5G? 5G 是第五代无线蜂窝技术,与以前的网络相比,它提供了更高的上传和下载速度、更一致的连接以及更高的容量。5G 比目前流行的 4G 网络更快、更可靠,并有可能改变我们使用互联网访问应用程序、社交网络和信息的方式。例如…...
优化冗余代码:提升前端项目开发效率的实用方法
目录 前言代码复用与组件化模块化开发与代码分割工具辅助与自动化结束语 前言 在前端开发中,我们常常会遇到代码冗余的问题,这不仅增加了代码量,还影响了项目的可维护性和开发效率。还有就是有时候会接到紧急业务需求,要求立马完…...
SpringCloud Alibaba 微服务(四):Sentinel
目录 前言 一、什么是Sentinel? Sentinel 的主要特性 Sentinel 的开源生态 二、Sentinel的核心功能 三、Sentinel 的主要优势与特性 1、丰富的流控规则 2、完善的熔断降级机制 3、实时监控和控制台 4、多数据源支持 5、扩展性强 四、Sentinel 与 Hystrix …...
Python 3.12新功能(1)
Python 3.12正式发布已经很久了,我才将主要电脑的Python版本从3.11升级到最新。最近刚好工作没有那么紧张了,就来领略下这个最新版本中的新特性。 改善了错误消息 Python作为一门编程语言,简单易学容易上手,童叟无欺,深…...
c++STL容器中vector的使用,模拟实现及迭代器使用注意事项和迭代器失效问题
目录 前言: 1.vector的介绍及使用 1.2 vector的使用 1.2 1 vector的定义 1.2 2 vector iterator(迭代器)的使用 1.2.3 vector 空间增长问题 1.2.4 vector 增删查改 1.2.5vector 迭代器失效问题。 2.vector模拟实现 2.1 std::vect…...
Android笔试面试题AI答之Activity常见考点
Activity的常见考点可以总结如下: 生命周期管理:理解Activity在不同情况下(如屏幕旋转、配置更改、用户操作等)的生命周期变化,包括但不限于onCreate、onStart、onResume、onPause、onStop和onDestroy等回调方法。 启…...
RK3568笔记四十九:W25Q64驱动开发(硬件SPI1)
若该文为原创文章,转载请注明原文出处。 一、SPI介绍 串行外设接口 (Serial Peripheral interface) 简称 SPI,是一种高速的,全双工,同步的通信总线,并 且在芯片的管脚上只占用四根线,节约了芯片的管脚。 …...
TypeScript 定义不同的类型(详细示例)
还是大剑师兰特:曾是美国某知名大学计算机专业研究生,现为航空航海领域高级前端工程师;CSDN知名博主,GIS领域优质创作者,深耕openlayers、leaflet、mapbox、cesium,canvas,webgl,ech…...
[工具推荐]前端加解密之Burp插件Galaxy
如果觉得该文章有帮助的,麻烦师傅们可以搜索下微信公众号:良月安全。点个关注,感谢师傅们的支持。 免责声明 本号所发布的所有内容,包括但不限于信息、工具、项目以及文章,均旨在提供学习与研究之用。所有工具安全性…...
课题项目结题测试的作用
课题项目结题测试是课题项目研究过程中的一个重要环节,它对于确保课题项目的质量和成果具有重要的作用。本文将详细介绍课题项目结题测试的作用。 一、确保课题项目质量 课题项目结题测试是对课题项目研究成果的全面评估和检测。通过结题测试,可以对课…...
中国工商银行长春分行开展“工驿幸福 健康财富”长辈客群康养活动
中国工商银行长春分行作为国有大行,持续完善有温度、专业化、安全稳健的养老场景服务,以工行驿站为依托、以长辈客群养老需求为中心,积极对接社区构建敬老、康养的“金融泛金融”工行驿站服务生态,进一步提升长辈客群的到店体验。…...
机器学习 第十四章
目录 前言 一、隐马尔可夫模型 二、马尔可夫随机场 三、条件随机场 四、学习和推断 1.变量消去 2.信念传播 五、近似推断 1.MCMC采样 2.变分推断 六、话题模型 总结 前言 机器学习最重要的任务是根据一些已观察到的证据来对感兴趣的未知变量进行估计和推测。概率模…...
未来RPA财税的发展前景
近年来,全球数字化进程持续提速,越来越多企业受到效率及运营成本的压力,正努力寻求企业增长发展的新路径,而财务作为企业战略的“大脑”,成为企业数字化转型的重要突破口。RPA技术由于能够自动化各种重复性和繁琐的任务…...
快速设置 terminator 透明背景
看图,按步骤设置后⭐重启一个终端则为透明效果 效果展示:...
Redis+Unity 数据库搭建
游戏中需要存放排行榜等数据,而且是实时存放,所以就涉及到数据库的问题。这里找服务器大神了解到可以用Redis来做存储,免费的效率极高。 Redis的搭建参考上文的文章,同时也感谢这位网友。 搭建Redis 并测试数据 搭建Redis 1.下…...
WebTracing:如何使用一款SDK实现前端全链路监控
引言 在产品的开发和运营过程中我们经常会遇到一些问题,如用户反馈说无法对某某商品下单,而另一位负责运营的同事也提到某某广告在手机上打不开。尽管这些问题被多次报告,但我们却难以复现这些故障,这让团队感到十分棘手。如何有效地记录项目中的错误并能够重现这些问题,…...
【Story】编程迷航:从 “ 我怎么才学会 ? ” 到 “ 我怎么这么厉害 ! ”
目录 大学生编程入门指南:选择语言、制定计划与避坑技巧1. 选择适合的编程语言1.1 Python1.2 Java1.3 C/C1.4 JavaScript1.5 SQL 2. 制定有效的学习计划2.1 设定明确的目标2.2 制定学习时间表2.3 选择学习资源2.4 实践和项目 3. 避免常见学习陷阱3.1 避免过度焦虑3.…...
基于“日志审计应用”的 DNS 日志洞察实践
作者:羿莉 (萧羿) 基础背景 DNS(Domain Name System) [ 1] 是任何网络活动的基础。它将易于记忆的域名转换为机器能够理解的 IP 地址。监控 DNS 服务可以帮助用户识别网络活动并保持系统安全。出于合规和安全性的考虑,公司通常要求对网络日志进行存储和…...
大学按照学科类别、办学层次、教育性质分类有哪些?创龙教仪一文带您了解
大学的分类多种多样,主要可以从学科类别、办学层次、教育性质等方面进行划分。 一、按学科类别划分 综合类大学 特点:学科门类齐全,文理渗透,科研实力强。 优势:拥有较多的国家级重点学科和实验室,师资…...
专业的网站开发服务/知乎推广渠道
相信大家都玩过微信H5游戏,例如前几年的围住神经猫,也曾收到过好友分享来的H5游戏链接,因为好奇点进去一探究竟。现在,越来越多的商家开始将H5游戏运用的品牌营销上来,H5游戏营销也受到了重视,那么…...
网络营销导向型企业网站建设的原则/小学培训机构
git clone --branch indigo-devel https://github.com/ros-perception/depthimage_to_laserscan$catkin_make $source devel/setup.bash $rospack profile...
长沙优化网站排名/广告联盟怎么加入
页面间对象传递的方法 request,session,application,cookie 等转载于:https://www.cnblogs.com/zhaozhaozhang/p/5856750.html...
网站怎么自适应/seo关键词排名优
项目场景: 清明放假闲的无聊,随手写了个爬虫,主要还是爬取电脑壁纸,虽然这个爬虫很简单,但博主刚刚简单的学习完数据结构与算法分析,这次爬虫恰巧运行到了其中的递归调用自身,特此记录一下。 爬…...
射阳做企业网站哪家好/百度云资源链接分享群组
在光线较暗深空中,无数小白点在这里自由移动,形成一片让人非常向往的星辰大海,此时,一艘高科技太空船以每秒13.95公里超快速度在这里穿行,不发出任何声音,它的前方是漆黑星际空间,后方是缩小成点…...
南宁网站建设流程/上海高端网站定制
全球qs排名查询 中国qs排名查询 百度百科: qs排名 中国排名世界排名大学地区117清华大学中国218北京大学中国322香港大学(HKU)中国香港431复旦大学中国534香港科技大学(HKUST)中国香港639香港中文大学中国香港745浙江大学中国8…...