GPT / GPT-2 / GPT-3 总结

GPT系列模型发布时间

 

GPT1

论文:GPT: Improving Language Understanding by Generative Pre-Training

背景

让我们把视角回到 2018 年,那个时候 NLP 在深度学习上基本还处于 word2vec 以及为不同任务做定制化深度模型的情况,虽然已经有 ELMo 这类预训练模型出现,但是其影响力还远远不足。在这个背景下,GPT 第一代预训练语言模型出现了。

 

模型结构

GPT1采用了transform模型的deconder部分(和transform的decoder相比,GPT1的decoder核心组件只有masked multi self attention 和 ffn,少了cross multi self attention),GPT1由12个decoder堆叠而成

GPT1的输入也是token embedding和position embedding的和,但是GPT1的position embedding是模型训练得到的

 

训练流程

GPT-1 采用了两阶段训练的方式:

1. 第一阶段 pre-training,在海量文本上训练,无需label,根据前k-1个词预测第k个单词是什么,第一阶段的训练让模型拥有了很多的先验知识,模型具有非常强的泛化性

2. 第二阶段在特定任务上supervised fine-tuning ,让模型能适应不同的任务,提高模型在特定任务上的准确性

supervised fine-tuning 就是在有label的数据集上微调,具体来说就是替换掉第一阶段的最后一层,在监督数据集上训练

针对不同的任务,模型的输入token序列是有区别的。简单总结如下:

 

代码实现

import torch
import torch.nn as nn
import torch.nn.functional as F


class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, num_heads):
        super(MultiHeadAttention, self).__init__()
        assert d_model % num_heads == 0, "d_model must be divisible by num_heads"
        self.d_model = d_model
        self.num_heads = num_heads
        self.d_k = d_model // num_heads

        self.W_q = nn.Linear(d_model, d_model)
        self.W_k = nn.Linear(d_model, d_model)
        self.W_v = nn.Linear(d_model, d_model)
        self.W_o = nn.Linear(d_model, d_model)

    def attention(self, Q, K, V, mask=None):
        scores = torch.matmul(Q, K.transpose(-2, -1)) / torch.sqrt(torch.tensor(self.d_k, dtype=torch.float32))
        if mask is not None:
            scores = scores.masked_fill(mask == 0, -1e9)
        attn_weights = F.softmax(scores, dim=-1)
        output = torch.matmul(attn_weights, V)
        return output

    def split_heads(self, x):
        batch_size, seq_length, d_model = x.size()
        return x.view(batch_size, seq_length, self.num_heads, self.d_k).transpose(1, 2)

    def combine_heads(self, x):
        batch_size, num_heads, seq_length, d_k = x.size()
        return x.transpose(1, 2).contiguous().view(batch_size, seq_length, self.d_model)

    def forward(self, Q, K, V, mask=None):
        Q = self.split_heads(self.W_q(Q))
        K = self.split_heads(self.W_k(K))
        V = self.split_heads(self.W_v(V))

        attn_output = self.attention(Q, K, V, mask)
        output = self.W_o(self.combine_heads(attn_output))
        return output


class PositionwiseFeedForward(nn.Module):
    def __init__(self, d_model, d_ff):
        super(PositionwiseFeedForward, self).__init__()
        self.fc1 = nn.Linear(d_model, d_ff)
        self.fc2 = nn.Linear(d_ff, d_model)
        self.relu = nn.ReLU()

    def forward(self, x):
        return self.fc2(self.relu(self.fc1(x)))


class DecoderLayer(nn.Module):
    def __init__(self, d_model, num_heads, d_ff):
        super(DecoderLayer, self).__init__()
        self.self_attn = MultiHeadAttention(d_model, num_heads)
        self.feed_forward = PositionwiseFeedForward(d_model, d_ff)
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)

    def forward(self, x, mask):
        attn_output = self.self_attn(x, x, x, mask)
        x = self.norm1(x + attn_output)
        ff_output = self.feed_forward(x)
        x = self.norm2(x + ff_output)
        return x


class GPT1(nn.Module):
    def __init__(self, vocab_size, d_model, num_heads, num_layers, max_seq_length, d_ff):
        super(GPT1, self).__init__()
        self.token_embeddings = nn.Embedding(vocab_size, d_model)
        self.position_embeddings = nn.Embedding(max_seq_length, d_model)
        self.decoder_layers = nn.ModuleList([
            DecoderLayer(d_model, num_heads, d_ff) for _ in range(num_layers)
        ])
        self.fc = nn.Linear(d_model, vocab_size)
        self.max_seq_length = max_seq_length

    def forward(self, input_ids):
        seq_length = input_ids.size(1)
        positions = torch.arange(0, seq_length, dtype=torch.long, device=input_ids.device).unsqueeze(0).repeat(
            input_ids.size(0), 1)
        token_embeds = self.token_embeddings(input_ids)
        position_embeds = self.position_embeddings(positions)
        embeddings = token_embeds + position_embeds

        mask = torch.tril(torch.ones(seq_length, seq_length, device=input_ids.device)).unsqueeze(0).unsqueeze(0)

        x = embeddings
        for layer in self.decoder_layers:
            x = layer(x, mask)

        logits = self.fc(x)
        return logits


# 训练示例
def train_gpt1(model, dataloader, criterion, optimizer, device):
    model.train()
    total_loss = 0
    for input_ids in dataloader:
        input_ids = input_ids.to(device)
        optimizer.zero_grad()
        logits = model(input_ids)
        shift_logits = logits[..., :-1, :].contiguous()
        shift_labels = input_ids[..., 1:].contiguous()
        loss = criterion(
            shift_logits.view(-1, shift_logits.size(-1)),
            shift_labels.view(-1)
        )
        loss.backward()
        optimizer.step()
        total_loss += loss.item()
    return total_loss / len(dataloader)


# 推理示例
def generate_text(model, input_ids, max_length, device):
    model.eval()
    input_ids = input_ids.to(device)
    with torch.no_grad():
        for _ in range(max_length - input_ids.size(1)):
            logits = model(input_ids)
            next_token_logits = logits[:, -1, :]
            next_token_id = torch.argmax(next_token_logits, dim=-1).unsqueeze(-1)
            input_ids = torch.cat([input_ids, next_token_id], dim=-1)
    return input_ids


# 示例参数
vocab_size = 10000
d_model = 128
num_heads = 4
num_layers = 2
max_seq_length = 512
d_ff = 512
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 创建模型
model = GPT1(vocab_size, d_model, num_heads, num_layers, max_seq_length, d_ff).to(device)

# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)

# 模拟数据加载器
batch_size = 16
num_batches = 10
dummy_data = [
    torch.randint(0, vocab_size, (batch_size, max_seq_length))
    for _ in range(num_batches)
]
dataloader = torch.utils.data.DataLoader(dummy_data, batch_size=None)

# 训练模型
num_epochs = 5
for epoch in range(num_epochs):
    loss = train_gpt1(model, dataloader, criterion, optimizer, device)
    print(f"Epoch {epoch + 1}/{num_epochs}, Loss: {loss:.4f}")

# 推理示例
input_text = torch.randint(0, vocab_size, (1, 10))
generated_text = generate_text(model, input_text, max_length=20, device=device)
print("Generated text:", generated_text)
    
GPT1

 

GPT1总结

1. 它是最早一批提出在 NLP 任务上使用 pre-train + fine-tuning 范式的工作。

2. GPT 的实验证明了模型的精度和泛化能力会随着解码器层数增加而不断提升,而且目前还有提升空间

3. 预训练模型具有 zero-shot 的能力,并且能随着预训练的进行不断增强

 

GPT2

论文:GPT-2: Language Models are Unsupervised Multitask Learners

背景

GPT1采用了pre-train + fine-tuning训练方式,也就是说为了适应不同的训练任务,模型还是需要在特定任务的数据集上微调,仍然存在较多人工干预的成本。GPT-2 想彻底解决这个问题,通过 zero-shot,在迁移到其他任务上的时候不需要额外的标注数据,也不需要额外的模型训练。

 

训练数据改造

在 GPT-1 中,下游任务需要对不同任务的输入序列进行改造,在序列中加入了开始符、分隔符和结束符之类的特殊标识符,但是在 zero-shot 前提下,我们无法根据不同的下游任务去添加这些标识符,因为不进行额外的微调训练,模型在预测的时候根本不认识这些特殊标记。所以在 zero-shot 的设定下,不同任务的输入序列应该与训练时见到的文本长得一样,也就是以自然语言的形式去作为输入,例如下面两个任务的输入序列是这样改造的:

机器翻译任务:translate to french, { english text }, { french text }
阅读理解任务:answer the question, { document }, { question }, { answer }
GPT-2 的核心思想就是,当模型的容量非常大且数据量足够丰富时,仅仅靠语言模型的学习便可以完成其他有监督学习的任务,不需要在下游任务微调。
 

模型结构

在模型结构方面,整个 GPT-2 的模型框架与 GPT-1 相同,只是做了几个地方的调整,这些调整更多的是被当作训练时的 trick,而不作为 GPT-2 的创新,具体为以下几点:

1. 后置层归一化( post-norm )改为前置层归一化( pre-norm );

2. 在模型最后一个自注意力层之后,额外增加一个层归一化;

3. 调整参数的初始化方式,按残差层个数进行缩放,缩放比例为 1 : √n;

4. 输入序列的最大长度从 512 扩充到 1024;

GPT-2 进行上述模型调整的主要原因在于,随着模型层数不断增加,梯度消失和梯度爆炸的风险越来越大,这些调整能够减少预训练过程中各层之间的方差变化,使梯度更加稳定

 

GPT2总结

整体来看,GPT-2 相比于 GPT-1 有如下几点区别:

1. 主推 zero-shot,而 GPT-1 为 pre-train + fine-tuning;
2. 训练数据规模更大,GPT-2 为 800w 文档 40G,GPT-1 为 5GB;
3. 模型大小,GPT-2 最大 15 亿参数,GPT-1为 1 亿参数;
4. 模型结构调整,层归一化和参数初始化方式;
5. 训练参数,batch_size 从 64 增加到 512,上文窗口大小从 512 增加到 1024,等等;
 
 

GPT3

论文:GPT-3: Language Models are Few-Shot Learners

背景

虽然 GPT-2 主推的 zero-shot 在创新度上有比较高的水平,但是由于其在效果上表现平平,所以在业界并没有取得比较大的影响力,而 GPT-3 正是为了解决效果上的问题而提出的。GPT-3 不再去追求那种极致的不需要任何样本就可以表现很好的模型,而是考虑像人类的学习方式那样,仅仅使用极少数样本就可以掌握某一个任务。

 

模型结构

在模型结构上,GPT-3 延续使用 GPT 模型结构,但是引入了 Sparse Transformer 中的 sparse attention 模块(稀疏注意力)。

sparse attention 与传统 self-attention(称为 dense attention) 的区别在于:

dense attention:每个 token 之间两两计算 attention,复杂度 O(n²)
sparse attention:每个 token 只与其他 token 的一个子集计算 attention,复杂度 O(n*logn)

具体来说,sparse attention 除了相对距离不超过 k 以及相对距离为 k,2k,3k,... 的 token,其他所有 token 的注意力都设为 0,如下图所示:

使用 sparse attention 的好处主要有以下两点:

1. 减少注意力层的计算复杂度,节约显存和耗时,从而能够处理更长的输入序列;

2. 具有“局部紧密相关和远程稀疏相关”的特性,对于距离较近的上下文关注更多,对于距离较远的上下文关注较少

 

下游任务评估方法

GPT-3 在下游任务的评估与预测时,提供了三种不同的方法:

Zero-shot:仅使用当前任务的自然语言描述,不进行任何梯度更新;
One-shot:当前任务的自然语言描述,加上一个简单的输入输出样例,不进行任何梯度更新;
Few-shot:当前任务的自然语言描述,加上几个简单的输入输出样例,不进行任何梯度更新;

其中 Few-shot 也被称为 in-context learning,虽然它与 fine-tuning 一样都需要一些有监督标注数据,但是两者的区别是:

1. 【本质区别】fine-tuning 基于标注数据对模型参数进行更新,而 in-context learning 使用标注数据时不做任何的梯度回传,模型参数不更新;

2. in-context learning 依赖的数据量(10~100)远远小于 fine-tuning 一般的数据量;

最终通过大量下游任务实验验证,Few-shot 效果最佳,One-shot 效果次之,Zero-shot 效果最差

 

GPT3总结

整体来看,GPT-3 相比于 GPT-2 有如下几点区别:

1. 效果上,超出 GPT-2 非常多,能生成人类难以区分的新闻文章;
2. 主推 few-shot,相比于 GPT-2 的 zero-shot,具有很强的创新性;
3. 模型结构略微变化,采用 sparse attention 模块;
4. 海量训练语料 45TB(清洗后 570GB),相比于 GPT-2 的 40GB;
5. 海量模型参数,最大模型为 1750 亿,GPT-2 最大为 15 亿参数;
GPT3还存在一些局限性:
1. 当生成文本长度较长时,GPT-3 还是会出现各种问题,比如重复生成一段话,前后矛盾,逻辑衔接不好等等;
2. 模型最终呈现的效果取决于训练数据,这会导致模型会出现各种各样的“偏见”

 

few-shot 相比于 zero-shot 为什么更有效?

在few-shot给的几个样例在新任务时会作为条件输入,相当于模型拥有了该任务更多的先验知识

 

参考资料

GPT / GPT-2 / GPT-3 / InstructGPT 进化之路 

 

posted @ 2023-12-25 20:54  AI_Engineer  阅读(396)  评论(0)    收藏  举报