GPT-3(Generative Pre-trained Transformer 3)是一种大型自回归语言模型,由OpenAI团队训练和发布。GPT-3 拥有1750亿个参数,是当时发布的最大的非稀疏(non-sparse)语言模型之一。其参数规模是前一代模型(如GPT-2)的10倍以上。GPT-3的目标是通过大规模的参数量和广泛的预训练来实现对多种语言任务的few-shot学习,即通过少量示例而无需额外的任务特定训练或微调来完成下游任务。
GPT-3 的架构基于 Transformer 架构,特别是自回归 Transformer,这与其前代模型 GPT-2 基本相同,但在参数数量、深度和模型容量上进行了显著的扩展。具体架构细节如下:
自回归 Transformer 是一种基于 Transformer 架构的语言模型,用于生成序列数据。它通过逐步预测下一个单词来生成整个句子或序列。这种生成方式称为自回归(autoregressive),即模型基于之前生成的内容来预测下一个内容。自回归 Transformer 模型特别适合自然语言生成任务,比如机器翻译、对话系统、文本补全等。
**自回归(autoregressive)**是一种生成策略,其中模型使用当前时间步的输出作为输入来预测下一个时间步的输出。例如,假设有一个目标序列 y 1 , y 2 , … , y T y_1, y_2, \ldots, y_T y1,y2,…,yT,自回归模型会通过如下的方式进行生成: P ( y 1 , y 2 , … , y T ) = P ( y 1 ) ⋅ P ( y 2 ∣ y 1 ) ⋅ P ( y 3 ∣ y 1 , y 2 ) ⋯ P ( y T ∣ y 1 , y 2 , … , y T − 1 ) P(y_1, y_2, \ldots, y_T) = P(y_1) \cdot P(y_2 \mid y_1) \cdot P(y_3 \mid y_1, y_2) \cdots P(y_T \mid y_1, y_2, \ldots, y_{T-1}) P(y1,y2,…,yT)=P(y1)⋅P(y2∣y1)⋅P(y3∣y1,y2)⋯P(yT∣y1,y2,…,yT−1)即每一步的输出是基于之前生成的所有内容。这种方式可以确保模型的生成过程是顺序的,且每一步都根据之前生成的部分进行预测。
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import numpy as np
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=5000):
super(PositionalEncoding, self).__init__()
# 创建位置编码矩阵,形状为 (max_len, d_model)
pe = torch.zeros(max_len, d_model)
# 创建位置的张量 (0, 1, 2, ..., max_len-1) 并扩展其维度
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
# 计算正弦和余弦函数的除数项
div_term = torch.exp(torch.arange(0, d_model, 2).float() * -(np.log(10000.0) / d_model))
# 对位置编码的偶数索引应用正弦函数,奇数索引应用余弦函数
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
# 添加一个额外的维度以便与批次兼容
pe = pe.unsqueeze(0).transpose(0, 1)
# 注册位置编码为缓冲区,在训练期间不更新
self.register_buffer('pe', pe)
def forward(self, x):
# 将位置编码加到输入的嵌入上
return x + self.pe[:x.size(0), :]
class TransformerEncoderLayerCustom(nn.Module):
def __init__(self, d_model, nhead, dim_feedforward, dropout=0.1):
super(TransformerEncoderLayerCustom, self).__init__()
# 自定义多头自注意力机制
self.d_model = d_model
self.nhead = nhead
self.dropout = nn.Dropout(dropout)
# 前馈网络,包含两个线性层和一个激活函数(ReLU)
self.linear1 = nn.Linear(d_model, dim_feedforward)
self.linear2 = nn.Linear(dim_feedforward, d_model)
# 层归一化
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
# 激活函数
self.activation = F.relu
def scaled_dot_product_attention(self, query, key, value, mask=None):
# 计算注意力分数
scores = torch.matmul(query, key.transpose(-2, -1)) / np.sqrt(self.d_model // self.nhead)
# 应用掩码(如果有)
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
# 计算注意力权重
attn_weights = F.softmax(scores, dim=-1)
# 应用注意力权重到值上
return torch.matmul(attn_weights, value)
def forward(self, src, src_mask=None):
batch_size, seq_len, _ = src.size()
head_dim = self.d_model // self.nhead
# 将输入分割成多个头
query = key = value = src.view(batch_size, seq_len, self.nhead, head_dim).transpose(1, 2)
# 计算多头注意力
attn_output = self.scaled_dot_product_attention(query, key, value, mask=src_mask)
attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, seq_len, self.d_model)
attn_output = self.dropout(attn_output)
# 残差连接和层归一化
src = self.norm1(src + attn_output)
# 前馈网络
src2 = self.linear2(self.dropout(self.activation(self.linear1(src))))
# 残差连接和层归一化
src = self.norm2(src + src2)
return src
上述代码中一些解释
torch.matmul(query, key.transpose(-2, -1))
的作用是计算**查询(query)和键(key)**之间的点积相似度,这一步是实现自注意力机制的核心。
具体解释如下:
query
和 key
:
query
和 key
是嵌入向量,它们的维度通常为 (batch_size, nhead, seq_len, head_dim)
。query
是用于寻找相关性的信息,而 key
是与 query
进行匹配的对象。通过计算 query
和 key
的点积相似度,模型可以判断输入序列中不同位置之间的关系。key.transpose(-2, -1)
:
key
的维度为 (batch_size, nhead, seq_len, head_dim)
。key.transpose(-2, -1)
将 key
的最后两个维度进行转置,结果的维度为 (batch_size, nhead, head_dim, seq_len)
。这样可以确保矩阵乘法的维度匹配。torch.matmul(query, key.transpose(-2, -1))
:
query
和 key
的矩阵乘法,其结果为注意力得分矩阵。(batch_size, nhead, seq_len, seq_len)
,表示序列中每个位置与其他位置的相关性。seq_len x seq_len
的得分矩阵,其中每个元素代表输入序列中某个位置与另一个位置的相似度。总结来说,这段代码的作用是计算 query
和 key
的点积来衡量相似度,从而帮助模型确定在自注意力机制中每个位置应该关注序列中的哪些部分。
torch.matmul(attn_weights, value)
这段代码的作用是应用注意力权重到值(value)上,从而生成最终的注意力输出。
具体解释如下:
attn_weights
:
attn_weights
是通过 query
和 key
之间的点积计算得出的注意力权重。(batch_size, nhead, seq_len, seq_len)
,表示每个序列中每个位置对其他位置的注意力强度。value
:
value
是输入嵌入的一部分,维度通常为 (batch_size, nhead, seq_len, head_dim)
。attn_weights
通过选择性加权的方式来对这些内容进行组合。torch.matmul(attn_weights, value)
:
torch.matmul(attn_weights, value)
进行矩阵乘法,使用注意力权重加权 value
向量。(batch_size, nhead, seq_len, head_dim)
,即为每个序列位置在应用注意力后的表示。value
进行加权求和,使模型能够聚焦于最相关的部分,从而生成更加丰富且具有上下文相关性的输出表示。class AutoregressiveTransformer(nn.Module):
def __init__(self, vocab_size, d_model, nhead, num_encoder_layers, dim_feedforward, max_len):
super(AutoregressiveTransformer, self).__init__()
# 嵌入层,将标记索引转换为稠密向量
self.embedding = nn.Embedding(vocab_size, d_model)
# 位置编码,用于将序列信息添加到嵌入中
self.pos_encoder = PositionalEncoding(d_model, max_len)
# 自定义的 Transformer 编码器层,使用指定的参数
encoder_layers = TransformerEncoderLayerCustom(d_model, nhead, dim_feedforward)
# 堆叠多个 Transformer 编码器层
self.transformer_encoder = nn.ModuleList([encoder_layers for _ in range(num_encoder_layers)])
self.d_model = d_model
# 线性层,将编码器的输出投影到词汇表大小
self.decoder = nn.Linear(d_model, vocab_size)
def forward(self, src, src_mask):
# 对源标记应用嵌入层,并按 sqrt(d_model) 进行缩放
src = self.embedding(src) * np.sqrt(self.d_model)
# 将位置编码加到嵌入后的标记上
src = self.pos_encoder(src)
# 通过所有的 Transformer 编码器层
for layer in self.transformer_encoder:
src = layer(src, src_mask)
# 将输出投影到词汇表大小
output = self.decoder(src)
return output
def generate_square_subsequent_mask(self, sz):
# 生成一个掩码,以防模型关注未来的位置
mask = (torch.triu(torch.ones(sz, sz)) == 1).transpose(0, 1)
mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
return mask
def train(model, data, vocab_size, num_epochs=10, learning_rate=0.0005):
# 定义损失函数为交叉熵损失
criterion = nn.CrossEntropyLoss()
# 使用 Adam 优化器进行训练
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
for epoch in range(num_epochs):
model.train() # 将模型设置为训练模式
total_loss = 0
for batch in data:
# 输入序列为除最后一个标记外的所有标记
src = batch[:-1]
# 目标序列为除第一个标记外的所有标记(向右移动一个位置)
tgt = batch[1:]
# 为输入序列生成后续掩码
src_mask = model.generate_square_subsequent_mask(len(src)).to(src.device)
optimizer.zero_grad() # 在反向传播前将梯度归零
# 通过模型进行前向传播
output = model(src, src_mask)
# 计算模型输出与目标序列之间的损失
loss = criterion(output.view(-1, vocab_size), tgt.view(-1))
# 反向传播损失
loss.backward()
# 更新模型参数
optimizer.step()
total_loss += loss.item()
# 打印每个 epoch 的平均损失
print(f"Epoch {epoch+1}, Loss: {total_loss / len(data)}")
def predict(model, start_token, max_len, vocab_size):
model.eval() # 将模型设置为评估模式
generated_sequence = [start_token] # 使用起始标记初始化生成的序列
# 使用起始标记创建初始输入张量
src = torch.tensor([start_token]).unsqueeze(1) # 形状为 (seq_len, batch_size)
for _ in range(max_len - 1):
# 为当前输入序列生成掩码
src_mask = model.generate_square_subsequent_mask(len(src)).to(src.device)
# 通过模型进行前向传播
output = model(src, src_mask)
# 获取具有最高概率的标记作为下一个标记
next_token = torch.argmax(output[-1, 0, :], dim=-1).item()
# 将预测的标记添加到生成的序列中
generated_sequence.append(next_token)
# 通过添加新标记更新输入序列
src = torch.cat([src, torch.tensor([[next_token]])], dim=0)
return generated_sequence
# 超参数
vocab_size = 100 # 示例词汇表大小
d_model = 32 # 嵌入向量的维度
nhead = 2 # 注意力头的数量
num_encoder_layers = 2 # Transformer 编码器层的数量
dim_feedforward = 64 # Transformer 中前馈网络的维度
max_len = 20 # 输入序列的最大长度
# 实例化模型
model = AutoregressiveTransformer(vocab_size, d_model, nhead, num_encoder_layers, dim_feedforward, max_len)
# 示例数据(随机生成,仅用于演示)
data = [torch.randint(0, vocab_size, (10,)) for _ in range(100)]
# 训练模型
train(model, data, vocab_size, num_epochs=10)
# 预测序列
start_token = 1 # 序列生成的起始标记
generated_sequence = predict(model, start_token, max_len, vocab_size)
print("Generated Sequence:", generated_sequence)
上述代码中一些难点的解释:
torch.tensor([start_token]).unsqueeze(1)
的作用是将 start_token
转换为一个形状适合于模型输入的张量。具体步骤如下:
torch.tensor([start_token])
:
start_token
的 1D 张量。假设 start_token
为 1
,那么这一步生成的张量为 [1]
,其形状为 (1,)
。.unsqueeze(1)
:
.unsqueeze(1)
用于在张量的第一个维度之后插入一个新的维度,使得张量的形状从 (1,)
变为 (1, 1)
。(seq_len, batch_size)
的张量,其中 seq_len=1
和 batch_size=1
。生成的张量形状为 (1, 1)
,表示一个序列长度为 1、批大小为 1 的张量,这样它可以作为模型的输入。在序列生成任务中,通常需要明确批次维度,即使批大小只有 1,这也是保持代码通用性和与模型输入接口兼容的常见做法。
next_token = torch.argmax(output[-1, 0, :], dim=-1).item()
的作用是从模型的输出中获取下一个最可能的标记。具体解释如下:
output[-1, 0, :]
:
output
的形状通常是 (seq_len, batch_size, vocab_size)
。output[-1, 0, :]
表示取最后一个时间步(即当前预测的词),对于第一个样本(批大小为 1 的情况下就是唯一的样本),从词汇表中获取所有词的概率分布。output[-1, 0, :]
的结果是一个长度为 vocab_size
的张量,表示最后一个时间步生成的每个词的概率。torch.argmax(output[-1, 0, :], dim=-1)
:
torch.argmax(..., dim=-1)
会返回概率最大的元素的索引,这意味着它会找到最可能的下一个词的索引(即具有最高概率的词汇表中的词)。.item()
:
.item()
将单元素张量转换为 Python 标量。最终的 next_token
是一个整数,表示模型预测的下一个标记的索引。总结来说,这段代码的作用是从模型的输出中找到下一个最可能的词的索引,然后将其用于序列的下一步生成。在预测阶段,模型根据这个最可能的词来不断扩展生成的序列。