GPT-2 骨架代码 的详细举例解释,全网最详细!!!

先看Gpt-2 的 骨架代码。

代码抄自openAI大神Andrej Karpathy最新发布的四小时gpt-2 复现视频。
B站连接如下:https://www.bilibili.com/video/BV12s421u7sZ?t=1653.2
非常推荐学习Andrej Karpathy大神的系列教学视频。

# GPT_2 骨架
class GPT(nn.Module):

    def __init__(self, config):
        super().__init__()
        self.config = 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)]),  # 包含多个 Block 的模型列表
            ln_f = nn.LayerNorm(config.n_embd),  # 输出层归一化, gpt-2 需要一个额外的最终层规范
        ))
        self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False)  # 语言模型头的线性层,投射到词汇表大小(分类)

    def forward(self, idx):
        # idx 是输入的 token 索引,形状为 (B, T)
        B, T = idx.size()  # 获取 batch size【输入的样本数量】 和序列长度 【序列长度(T)指的是输入的 token 序列的长度。】
        assert T <= self.config.block_size, f"Cannot forward sequence of length {T}, block size is only {self.config.block_size}"
        
        # forward the token and position embeddings 【嵌入层】
        pos = torch.arange(0, T, dtype=torch.long, device=idx.device)  # 将位置索引转换为 long 类型【shape(T)】
        pos_emb = self.transformer.wpe(pos)  # 将位置索引 映射到 位置嵌入【shape(T, n_embd) 】
        tok_emb = self.transformer.wte(idx)  # 将 token 索引 映射到  token 嵌入 【shape(B, T, n_embd) 】
        x = tok_emb + pos_emb  # 将 token 嵌入 和 位置嵌入 相加【shape(B, T, n_embd) 】

        # forward the blocks of the transformer
        for block in self.transformer.h:  # 遍历每个block
            x = block(x)  # 传递输入到block,得到输出

        # 应用最终的层规范 【forward the final layernorm and classifier】
        x = self.transformer.ln_f(x)  # 应用最终的层规范
        logits = self.lm_head(x)  # 应用语言模型头,得到 logits
        return logits

骨架中其他模块的代码在我整理好后会第一时间发布出来,需要的小伙伴可以关注一下。
在这里插入图片描述

现在进行详细的举例解释这个骨架代码执行过程:

详细说明一个批量大小为2的完整forward过程。假设我们有两个句子:

  1. “Hello, world!”
  2. “GPT is great.”

我们先将这些句子转换为token索引。假设词汇表如下:

["Hello", ",", "world", "!", "GPT", "is", "great", ".", "<EOS>"]
[   0,     1,    2,     3,     4,     5,     6,     7,     8]

假设我们使用特殊的结束token "<EOS>",对应索引为8。

现在我们将两句话转换为token索引:

  1. “Hello, world!” -> [0, 1, 2, 3, 8]
  2. “GPT is great.” -> [4, 5, 6, 7, 8]

所以我们的批量输入是:

idx = torch.tensor([
    [0, 1, 2, 3, 8],  # 第一句话
    [4, 5, 6, 7, 8]   # 第二句话
])

输入 idx 的形状是 (2, 5),其中 2 是批量大小,5 是序列长度。

【注意】:在处理变长序列时,通常需要对输入进行填充(padding)操作,以确保每个批次中所有序列的长度相同。这样做的目的是为了使数据可以被批量处理,同时保持张量的形状一致。

填充操作(Padding) 假设我们有两个句子,第三个句子比较短:
  • “Hello, world!” -> [0, 1, 2, 3, 8]
  • “GPT is great.” -> [4, 5, 6, 7, 8]
  • “Hi!” -> [9, 3, 8]
    为了将这些句子放入同一个批次中,我们需要找到最长的序列,然后对其他序列进行填充,使其长度与最长序列相同。这里最长的序列长度是5(“Hello, world!” 和 “GPT is great.” 的长度)。我们可以使用一个特殊的填充token,例如 “”,其索引为10。填充后的序列如下:

“Hello, world!” -> [0, 1, 2, 3, 8]
“GPT is great.” -> [4, 5, 6, 7, 8]
“Hi!” -> [9, 3, 8, 10, 10]

填充后的输入张量 idx 将是:

python idx = torch.tensor([
[0, 1, 2, 3, 8], # 第一句话
[4, 5, 6, 7, 8], # 第二句话
[9, 3, 8, 10, 10] # 第三句话 ])

输入 idx 的形状是 (3, 5),其中 3 是批量大小,5 是填充后的序列长度。

【【【【【处理填充的序列】】】】】
在实际的模型实现中,填充token通常不会对模型的实际输出产生影响。可以通过以下方式处理填充的序列:

  • 掩码(Masking):掩码用于在计算注意力时忽略填充的部分。掩码张量的形状与输入序列相同,对填充位置赋值0,对有效token位置赋值1。
  • 忽略填充token的损失计算:在训练过程中,计算损失时需要忽略填充token的贡献。可以通过掩码来实现这一点。

完整的forward过程

1. 使用nn.Embedding将token索引idx转换为嵌入向量
tok_emb = self.transformer.wte(idx)

假设嵌入维度 n_embd 为4,词汇表大小 vocab_size 为9。输入 idx 是形状 (2, 5) 的张量:

idx = [
    [0, 1, 2, 3, 8],
    [4, 5, 6, 7, 8]
]

nn.Embedding将每个token索引转换为一个嵌入向量。假设嵌入矩阵 wte 看起来像这样:

wte = [
  [0.1, 0.2, 0.3, 0.4],  # "Hello"
  [0.1, 0.1, 0.1, 0.1],  # ","
  [0.2, 0.2, 0.2, 0.2],  # "world"
  [0.3, 0.3, 0.3, 0.3],  # "!"
  [0.4, 0.4, 0.4, 0.4],  # "GPT"
  [0.5, 0.5, 0.5, 0.5],  # "is"
  [0.6, 0.6, 0.6, 0.6],  # "great"
  [0.7, 0.7, 0.7, 0.7],  # "."
  [0.8, 0.8, 0.8, 0.8]   # "<EOS>"
]

嵌入后,tok_emb 的形状为 (2, 5, 4)

tok_emb = [
  [
    [0.1, 0.2, 0.3, 0.4],  # "Hello"
    [0.1, 0.1, 0.1, 0.1],  # ","
    [0.2, 0.2, 0.2, 0.2],  # "world"
    [0.3, 0.3, 0.3, 0.3],  # "!"
    [0.8, 0.8, 0.8, 0.8]   # "<EOS>"
  ],
  [
    [0.4, 0.4, 0.4, 0.4],  # "GPT"
    [0.5, 0.5, 0.5, 0.5],  # "is"
    [0.6, 0.6, 0.6, 0.6],  # "great"
    [0.7, 0.7, 0.7, 0.7],  # "."
    [0.8, 0.8, 0.8, 0.8]   # "<EOS>"
  ]
]
2. 使用nn.Embedding将位置索引转换为位置嵌入向量

在Transformer模型中,pos 通常是根据输入序列的长度来创建的,而不是根据词汇表的大小。pos是一个表示序列中每个位置的索引列表,它从0开始,依次递增,直到序列的最后一个位置。

对于给定的输入序列,`pos` 的长度等于序列中token的数量,而不是词汇表的大小。

这里的 pos是批次中每个序列的共享位置索引,用于将位置信息添加到每个token的嵌入中。每个位置索引将被广播到批次中的所有序列,以便与相应的token嵌入相加。

总结来说,pos是根据输入序列的长度来创建的,它表示序列中的位置信息,而不是词汇表中的token。词汇表大小通常用于创建token嵌入矩阵,而序列长度用于创建位置嵌入矩阵。

pos = torch.arange(0, T, dtype=torch.long, device=idx.device)

生成位置索引 pos

pos = [0, 1, 2, 3, 4]

然后,将位置索引转换为嵌入向量:

pos_emb = self.transformer.wpe(pos)

假设位置嵌入矩阵 wpe 看起来像这样:

wpe = [
  [0.0, 0.1, 0.1, 0.1],
  [0.2, 0.2, 0.2, 0.2],
  [0.3, 0.3, 0.3, 0.3],
  [0.4, 0.4, 0.4, 0.4],
  [0.5, 0.5, 0.5, 0.5]
]

嵌入后,pos_emb 的形状为 (5, 4)

pos_emb = [
  [0.0, 0.1, 0.1, 0.1],
  [0.2, 0.2, 0.2, 0.2],
  [0.3, 0.3, 0.3, 0.3],
  [0.4, 0.4, 0.4, 0.4],
  [0.5, 0.5, 0.5, 0.5]
]
3. 将token嵌入和位置嵌入相加,得到最终的嵌入表示

利用广播机制(Broadcasting)将token嵌入和位置嵌入相加,得到最终的嵌入表示。

广播机制是指当两个张量在进行数学运算时,如果它们的形状不完全相同,PyTorch会【自动“扩展”较小的张量】,使得它们的形状匹配。这种机制使得编写代码更加简洁和高效,而无需显式地调整张量的形状。

具体示例

假设 tok_emb 是输入序列的token嵌入,形状为 (batch_size, seq_length, embedding_dim),而 pos_emb 是位置嵌入,形状为 (seq_length, embedding_dim)

假设以下形状:

  • tok_emb 形状为 (3, 5, 4),其中 3 是批次大小,5 是序列长度,4 是嵌入维度。
  • pos_emb 形状为 (5, 4),即每个位置的嵌入向量。

在广播机制下,PyTorch会自动扩展 pos_emb 以匹配 tok_emb 的形状。具体来说,pos_emb 会扩展为
(1, 5, 4),然后在第一个维度上重复 batch_size 次,使得它的形状变为 (3, 5, 4)

x = tok_emb + pos_emb

这里需要广播位置嵌入,使其与token嵌入具有相同的形状 (2, 5, 4)。相加后的 x

x = [
  [
    [0.1, 0.3, 0.4, 0.5],  # token "Hello" + pos 0
    [0.3, 0.3, 0.3, 0.3],  # token "," + pos 1
    [0.5, 0.5, 0.5, 0.5],  # token "world" + pos 2
    [0.7, 0.7, 0.7, 0.7],  # token "!" + pos 3
    [1.3, 1.3, 1.3, 1.3]   # token "<EOS>" + pos 4
  ],
  [
    [0.4, 0.5, 0.5, 0.5],  # token "GPT" + pos 0
    [0.7, 0.7, 0.7, 0.7],  # token "is" + pos 1
    [0.9, 0.9, 0.9, 0.9],  # token "great" + pos 2
    [1.1, 1.1, 1.1, 1.1],  # token "." + pos 3
    [1.3, 1.3, 1.3, 1.3]   # token "<EOS>" + pos 4
  ]
]
4. 通过transformer的多个块(Block的实例)传递这个嵌入表示

假设我们有2个transformer block,每个block进行自注意力机制和前馈网络等操作:

for block in self.transformer.h:
    x = block(x)

假设经过所有block后,x变成:

x = [
  [
    [0.15, 0.35, 0.45, 0.55],
    [0.35, 0.35, 0.35, 0.35],
    [0.55, 0.55, 0.55, 0.55],
    [0.75, 0.75, 0.75, 0.75],
    [1.35, 1.35, 1.35, 1.35]
  ],
  [
    [0.45, 0.55, 0.55, 0.55],
    [0.75, 0.75, 0.75, 0.75],
    [0.95, 0.95, 0.95, 0.95],
    [1.15, 1.15, 1.15, 1.15],
    [1.35, 1.35, 1.35, 1.35]
  ]
]
5. 应用最终的层归一化
x = self.transformer.ln_f(x)

层归一化通常不会改变张量的形状,只会对每个嵌入向量进行归一化处理。假设归一化后的 x

x = [
  [
    [0.14, 0.34, 0.44, 0.54],
    [0.34, 0.34, 0.34, 0.34],
    [0.54, 0.54, 0.54, 0.54],
    [0.74, 0.74, 0.74, 0.74],
    [1.34, 1.34, 1.34, 1.34]
  ],
  [
    [0.44, 0.54, 0.54, 0.54],
    [0.74, 0.74, 0.74, 0.74],
    [0.94, 0.94, 0.94, 0.94],
    [1.14, 1.14, 1.14, 1.14],
    [1.34, 1.34, 1.34, 1.34]
  ]
]
6. 通过线性层(lm_head)将嵌入表示转换为对词汇表中每个token的预测分数(logits)
logits = self.lm_head(x)

假设 vocab_size 为9,lm_head 是一个线性层,将每个嵌入向量转换为词汇表大小(9)的logits。最终的 logits 形状为 (2, 5, 9)

logits = [
  [
    [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9],
    [0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.1],
    [0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.1, 0.2],
    [0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.1, 0.2, 0.3],
    [0.5, 0.6, 0.7, 0.8, 0.9, 0.1, 0.2, 0.3, 0.4]
  ],
  [
    [0.6, 0.7, 0.8, 0.9, 0.1, 0.2, 0.3, 0.4, 0.5],
    [0.7, 0.8, 0.9, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6],
    [0.8, 0.9, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7],
    [0.9, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8],
    [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
  ]
]

每个位置的logits向量对应于词汇表中每个token的预测分数。通过softmax可以将这些logits转换为概率分布,从而选择最可能的下一个token。

7. 假设我们要预测第一个序列的下一个token,具体步骤如下:
  1. 选择序列:我们选择第一个序列。
  2. 选择位置:我们选择第一个序列的最后一个位置(索引为4)。
  3. 提取logits:提取最后一个位置的logits向量。

从给定的 logits 张量中,我们可以提取第一个序列最后一个位置的logits向量:

# 第一个序列的最后一个位置的logits向量
logits_vector = [0.5, 0.6, 0.7, 0.8, 0.9, 0.1, 0.2, 0.3, 0.4]
  1. 应用softmax:对这个logits向量应用softmax函数,将其转换为概率分布。softmax函数的公式为:

[ \text{softmax}(x_i) = \frac{e^{x_i}}{\sum_{j} e^{x_j}} ]

应用softmax后,我们将得到一个概率分布。让我们计算这些概率:

logits_vector = np.array([0.5, 0.6, 0.7, 0.8, 0.9, 0.1, 0.2, 0.3, 0.4])
# 应用softmax
exp_logits = np.exp(logits_vector)
probabilities = exp_logits / np.sum(exp_logits)
# 打印概率分布
print(probabilities)

假设计算结果为:
probabilities = [0.0928, 0.1025, 0.1134, 0.1258, 0.1397, 0.0617, 0.0684, 0.0758, 0.0840]

  1. 选择token:根据概率分布选择最可能的下一个token,通常选择概率最大的token。我们在概率分布中查找最大值:
predicted_token = np.argmax(probabilities)

predicted_token 的值为 4,因为 0.1397 是最大的概率。这意味着在给定的词汇表中,第一个序列的下一个最可能的token是索引为 4 的token。

  • 批次大小 (2): 表示我们有两个输入序列。
  • 序列长度 (5): 每个序列包含5个token。
  • 词汇表大小 (9): 词汇表中有9个不同的token。
  • 预测过程: 经过softmax转换后,确定概率最高的token,即索引为 4 的token。

通过以上步骤,我们可以确定给定序列的下一个最可能的token。

总结

整个forward过程从输入句子的token索引开始,经过嵌入、位置编码、transformer块的处理、层归一化,最终经过线性层生成预测的logits。这些logits可以用于进一步的推理,如生成下一个token或计算损失进行模型训练。

为什么要用位置嵌入? 位置嵌入(Position Embedding)在Transformer模型中起着关键作用,因为Transformer没有内置的顺序处理能力。传统的循环神经网络(RNN)和卷积神经网络(CNN)通过递归或局部连接自然地捕获序列中的顺序信息,但Transformer依赖于并行计算和自注意力机制,因此需要显式地注入位置信息。

位置嵌入的作用

自注意力机制(Self-Attention)可以同时关注输入序列中的所有位置,而不考虑这些位置的顺序。换句话说,Transformers中的自注意力机制在处理输入时是位置无关的。这种特性虽然带来了并行计算和更长依赖关系捕获的优势,但也带来了顺序信息丢失的问题。

为了在这种位置无关的机制中引入序列顺序信息,我们需要位置嵌入。位置嵌入为每个token添加了位置信息,使得模型能够区分相同token在不同位置的作用。具体来说:

  1. 捕获顺序信息:位置嵌入可以让模型知道每个token在序列中的相对或绝对位置,从而捕获序列中的顺序关系。
  2. 增强模型表达能力:通过将位置嵌入与token嵌入相加,模型可以学习到不仅仅是token之间的关系,还包括这些token在序列中的位置关系。
  3. 提高模型泛化能力:由于位置嵌入考虑了序列中的位置信息,因此模型可以更好地泛化到新的序列上。

位置编码的原理

位置编码是一种将序列中的位置信息编码为向量的方法。在Transformer中,位置编码被添加到输入序列的每个token中,以提供关于该token在序列中的位置的信息。

位置编码的实现

位置编码的实现通常使用三角函数或其他函数来生成位置编码向量。这些函数可以根据序列的长度和位置信息生成不同的位置编码值。
在Transformer中,位置编码通常被添加到输入序列的每个token中,以提供关于该token在序列中的位置的信息。具体来说,位置编码可以被添加到输入序列的嵌入向量中,或者作为额外的输入特征。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值