手写GPT实现小说生成(一)

这篇具有很好参考价值的文章主要介绍了手写GPT实现小说生成(一)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

引言

本文开始从零实现GPT1做一个小说续写器,即只需要给出一些文本,让模型帮你续写,主要内容包含:

  • 模型编写
  • 训练适配小说的中文分词器
  • 将小说按固定大小拆分生成数据集
  • 拆分训练/测试集
  • 训练
  • 体验小说续写效果

同时结合HuggingFace的transformers,可以将处理好的数据集、训练好的分词器和模型上传到HuggingFace Hub。

本文主要实现模型编写,剩下的内容请见下篇文章。

模型架构

手写GPT实现小说生成(一),NLP项目实战,# 从Transformer到LLM,自然语言处理,gpt,小说续写

GPT模型架构如上图所示,由多层Tranformer Decoder组成的单向语言模型,是Tranformer的一个变种。它的Transformer Block比较简单,由两个子层组成,第一个子层输入上应用一个多头注意力层,输入和输出经过残差连接,紧着的是一个层归一化;第二个子层是前馈层、残差连接和层归一化。

整个GPT可以分为三部分:

  1. 输入层
  2. 编码层
  3. 输出层

输入层计算出Transformer Block的输入表示;编码层经过叠加的多层Transformer Block进行编码;最后输出层应用Softmax计算输出标记的分布。

其训练过程包含两个阶段:无监督预训练和有监督微调。

无监督阶段可以在大规模文本语料上学习一个高容量的语言模型,然后可以根据下游具体任务进行微调。

无监督预训练

GPT是一个单向模型,也是仅解码器模型(Decoder Only),即只能自左向右(或反之)对文本序列建模,采用的是Transformer的解码器结构,同时引入了同样的解码策略保证输入文本每个位置只能依赖当前和过去时刻的信息。

给定文本序列 w = w 1 w 2 ⋯ w n w=w_1w_2\cdots w_n w=w1w2wn,首先通过输入层将其编码成稠密向量:
u i = u i e + u i p (1) \pmb u_i = \pmb u_i^e + \pmb u_i^p \tag 1 uuui=uuuie+uuuip(1)
输入层由两个子层组成:词嵌入层和位置编码层。

其中 u i e \pmb u_i^e uuuie w i w_i wi经过词嵌入层得到的词向量; u i p \pmb u_i^p uuuip w i w_i wi的经过位置编码层得到的位置向量; u i \pmb u_i uuui为第 i i i个位置的标记经过输入层后的输出。

GPT的位置编码和原始Transformer中固定的不同,它是一种可学习的位置编码。

经过输入层得到每个标记带位置信息的词嵌入表示序列 u = u 1 ⋯ u n \pmb u= \pmb u_1 \cdots \pmb u_n uuu=uuu1uuun,接着将 u \pmb u uuu输入GPT的编码层,编码层由 L L L个Transformer Block组成,每一层的Block都能计算出带有上下文信息的向量表示,经过多层编码后,能得到更复杂、强大的向量表示,计算过程为:
h l = transformer_block l ( h l − 1 )    ∀ l ∈ [ 1 , L ] (2) \pmb h^l = \text{transformer\_block}^l(\pmb h^{l-1}) \,\,\forall l \in [1,L] \tag 2 hhhl=transformer_blockl(hhhl1)l[1,L](2)
其中我们令 h 0 = u \pmb h^0 = \pmb u hhh0=uuu,即输入层计算出来的输出; h l ∈ R d × n \pmb h^{l} \in \R^{d \times n} hhhlRd×n表示由第 l l l层计算出来的表示向量序列, d d d是模型隐藏层维度, n n n为序列长度; L L L为总层数。

而输出层基于最后一层的向量表示 h L \pmb h^L hhhL计算每个位置上输出标记的概率分布:
P ( w i ∣ w 1 , ⋯   , w i − 1 ) = softmax ( W e h i L ) (3) P(w_i|w_1,\cdots ,w_{i-1}) = \text{softmax}(\pmb W^e \pmb h^L_i ) \tag 3 P(wiw1,,wi1)=softmax(WWWehhhiL)(3)
这里 W e ∈ R ∣ V ∣ × d \pmb W^e \in \R ^{|\Bbb V| \times d} WWWeRV×d是词向量矩阵; ∣ V ∣ |\Bbb V| V为词表大小;注意这里 h i L \pmb h_i^L hhhiL的维度是 d × 1 d \times 1 d×1

然后使用一个常规的语言建模目标优化 w w w的最大似然估计:
L PT = − ∑ i log ⁡ P ( w i ∣ w i − k ⋯ w i − 1 ; Θ ) (4) \mathcal L^{\text{PT}} = -\sum_i \log P(w_i|w_{i-k}\cdots w_{i-1};\Theta) \tag 4 LPT=ilogP(wiwikwi1;Θ)(4)
这里的 k k k是上下文窗口,根据前 k k k个标记来预测当前标记; Θ \Theta Θ表示模型参数。

这就是预训练(pretrain)阶段的损失函数。

有监督微调

无监督预训练使得模型具有一定的通用语义表示能力,下游任务微调目的使通用语义表示可以适配不同具体的下游任务。

微调一般需要利用有标签数据集进行,假设一个有标签数据集 C \mathcal C C,其中每个样本包含一个输入序列 x = x 1 x 2 ⋯ x n x=x_1x_2\cdots x_n x=x1x2xn和一个输出标签 y y y

x x x输入给预训练好的模型,我们用最后一层Transformer Block的最后一个位置的输出 h n L \pmb h_n^L hhhnL来进行预测,具体地可以接一个全连接层结合 softmax \text{softmax} softmax函数得到预测标签的概率分布:
p ( y ∣ x 1 ⋯ x n ) = softmax ( h n L W y ) (5) p(y|x_1\cdots x_n) = \text{softmax}(\pmb h^L_n \pmb W^y) \tag 5 p(yx1xn)=softmax(hhhnLWWWy)(5)
其中 W y ∈ R d × c \pmb W^y \in \R ^{d \times c} WWWyRd×c为全连接层参数; c c c为标签个数。通过对整个标注数据集进行优化,我们又可以得到微调目标函数:
L FT ( C ) = − ∑ ( x , y ) log ⁡ P ( y ∣ x 1 ⋯ x n ) (6) \mathcal L^{\text{FT}} (\mathcal C) =- \sum_{(x,y)} \log P(y|x_1\cdots x_n) \tag 6 LFT(C)=(x,y)logP(yx1xn)(6)
在下游任务微调过程中,如果仅针对微调目标进行优化,很可能会使模型遗忘预训练阶段所学习到的通用语义表示知识,从而损失模型的通用性和泛化能力,即灾难性遗忘(Catastrophic Forgetting)。因此将语言建模任务作为一个辅助目标函数加到微调阶段可以有助于学习,具体地,我们优化下面的目标函数:
L = L FT ( C ) + λ L PT ( C ) (7) \mathcal L =\mathcal L^{\text{FT}} (\mathcal{C}) + \lambda \mathcal L^{\text{PT}}(\mathcal C) \tag 7 L=LFT(C)+λLPT(C)(7)
其中 λ \lambda λ是用于平衡这两个目标函数的权重,可以取值 0.5 0.5 0.5

模型实现

本节我们开始从零实现GPT,有了上篇文章从零实现Transformer的基础,实现GPT也不是太难。

本次实现参考了HuggingFace的源码,使得我们后面可以很容易的应用HuggingFace实现的GPT。

开始之前,我们回顾下GPT论文中实现细节。

实现细节

模型设定

  • 模型主要沿用原始的Transformer;
  • 训练了一个带掩码自注意力头(状态维度768,12个头)的12层仅解码器的Transformer;
  • 对于位置感知的前馈网络,使用3072作为内部隐状态维度;
  • 使用Adam优化器和最大学习率2.5e-4;
  • 学习率在前2000步内逐渐从0开始线性地增加,然后使用余弦调度器降低到0;
  • 在批大小为64的长度为512的序列样本上训练;
  • 由于模型中广泛使用层归一化,因此简单地(高斯)权重初始化;
  • 使用了一个包含40000个合并的字节对编码(BPE)词表;
  • 应用残差、嵌入和注意力的Dropout为0.1进行正则化;
  • 采用了修改版的L2正则化;
  • 对所有非偏置或增益权重使用 w = 0.01 w=0.01 w=0.01
  • 对于激活函数,使用GELU;
  • 使用了学习的位置嵌入,而不是原始工作中的正弦版本。

微调细节

  • 基本重复使用了无监督预训练的超参数设置;
  • 在分类器中添加了0.1的Dropout;
  • 对于大多数任务,使用6.25e-5的学习率和32的批量大小;
  • 模型可以快速微调,大多数情况下3个epoch就足够了;
  • 使用线性学习率衰减调度,并在0.2%的训练期上进行预热;
  • 两个损失函数间的 λ λ λ设置为0.5;

我们按照从下至上的原则依次实现。

输入层

上面我们知道,输入层由两个子层:词嵌入层和可学习的位置编码层组成,那就非常简单了,实际上就是两个嵌入层:

te=nn.Embedding(vocab_size, embed_dim )  # token emebedding 词嵌入层
pe=nn.Embedding(max_positions, embed_dim ) # 位置编码层

vocab_size是词表大小;embed_dim是模型嵌入大小;max_positions是最大可学习位置长度。

编码层

手写GPT实现小说生成(一),NLP项目实战,# 从Transformer到LLM,自然语言处理,gpt,小说续写

编码层由 L L L层Transformer Block组成,每个Block的结构如上图所示。我们依次实现。

GELU

激活函数使用GELU而不是RELU,我们来看下GELU的图像(蓝线):

手写GPT实现小说生成(一),NLP项目实战,# 从Transformer到LLM,自然语言处理,gpt,小说续写

其近似公式为:
0.5 x ( 1 + tanh ⁡ [ 2 / π ( x + 0.044715 x 3 ) ] ) (8) 0.5x(1 + \tanh[\sqrt{2/π}(x + 0.044715x^ 3)]) \tag 8 0.5x(1+tanh[2/π (x+0.044715x3)])(8)
从图像可以看到,相比RELU和ELU,GELU有以下优势:

  • 平滑性: GELU函数在整个输入范围内是光滑的,而ReLU在负数部分不是光滑的(其导数为0),虽然ELU在负数部分是光滑的,但变化不够平滑。这使得GELU更容易优化;
  • 高性能: GELU函数表现出比ReLU和ELU更好的性能;
  • 非线性:GELU函数是非线性的,引入类似sigmoid函数的变换,使得GELU函数的输出可以落在一个更广的范围内,有助于加速模型的收敛;

按照公式实现即可:

class GELU(nn.Module):
    def forward(self, x: Tensor) -> Tensor:
        return (
            0.5
            * x
            * (
                1.0
                + torch.tanh(
                    math.sqrt(2.0 / math.pi)
                    * (input + 0.044715 * torch.pow(input, 3.0))
                )
            )
        )

但是为了速度快一点,我们应用Pytorch内建的torch.nn.functional.gelu

一维卷积层

OpenAI GPT的作者把Transformer中的线性层命名为一维卷积,因为它们的操作是相等的(卷积的filter大小为1)。

我们通过图片来直观理解一下, https://ezyang.github.io/convolution-visualizer/ 提供了一个很好地可视化页面。

手写GPT实现小说生成(一),NLP项目实战,# 从Transformer到LLM,自然语言处理,gpt,小说续写

实际上filter大小为1的一维卷积就是让输入中每个位置与权重相乘(即序列长度维度上是并行独立计算的),通过out_channels控制输出维度。

我们可以通过代码验证一下:

import torch
import torch.nn as nn

embed_dim = 10
seq_len = 3
batch_size = 2
hidden_size = 5
# 定义输入数据,表示
x = torch.randn(batch_size, seq_len, embed_dim)

# 定义前馈网络
fc = torch.nn.Linear(embed_dim, hidden_size)

# 定义一维卷积核
conv = torch.nn.Conv1d(embed_dim, hidden_size, kernel_size=1)

# 设置前馈网络和一维卷积核的参数相同
conv.weight = nn.Parameter(fc.weight.reshape(hidden_size, embed_dim, 1))
conv.bias = fc.bias

# 计算前馈网络和一维卷积的输出结果
fc_output = fc(x)
x_conv = x.permute(0, 2, 1)
conv_output = conv(x_conv)

# 比较输出结果是否相同
conv_output = conv_output.permute(0, 2, 1)

print(torch.allclose(fc_output, conv_output))
True

所以它只是一个命名上的技巧,实际上实现起来还是通过前馈网络,不过与FeedForward中权重参数的维度位置相反,我们先看这里Conv1D的实现:

class Conv1D(nn.Module):
    def __init__(self, in_features: int, out_features: int) -> None:
        """1D-convolutional layer as defined by Radford et al. for OpenAI GPT.

        Args:
            in_features (int): the number of input features.
            out_features (int): the number of output features.
        """
        super().__init__()
        self.out_features = out_features
        self.weight = nn.Parameter(torch.empty(in_features, out_features))
        self.bias = nn.Parameter(torch.zeros(out_features))
        nn.init.normal_(self.weight, std=0.02)

    def forward(self, x: Tensor) -> Tensor:
        """

        Args:
            x (Tensor): (batch_size, seq_len, embed_dim)

        Returns:
            Tensor: (batch_size, seq_len, out_features)
        """
        # size_out (batch_size, seq_len, out_features)
        size_out = x.size()[:-1] + (self.out_features,)
        # self.bias + x @ self.weight
        # x -view-> (batch_size *  seq_len,embed_dim)
        # (batch_size * seq_len,embed_dim) x (embed_dim, out_features)
        # -> (batch_size * seq_len, out_features)
        x = torch.addmm(self.bias, x.view(-1, x.size(-1)), self.weight)
        # x (batch_size, seq_len, out_features)
        x = x.view(size_out)

        return x

而Pytorch中FeedForward的实现(去掉一些细节)为:

class Linear(Module):
    def __init__(self, in_features: int, out_features: int, bias: bool = True,
                 device=None, dtype=None) -> None:
        super(Linear, self).__init__()
        self.in_features = in_features
        self.out_features = out_features
        self.weight = Parameter(torch.empty((out_features, in_features), **factory_kwargs))
        if bias:
            self.bias = Parameter(torch.empty(out_features, **factory_kwargs))
        else:
            self.register_parameter('bias', None)
        self.reset_parameters()

    def reset_parameters(self) -> None:
        init.kaiming_uniform_(self.weight, a=math.sqrt(5))
        if self.bias is not None:
            fan_in, _ = init._calculate_fan_in_and_fan_out(self.weight)
            bound = 1 / math.sqrt(fan_in) if fan_in > 0 else 0
            init.uniform_(self.bias, -bound, bound)

    def forward(self, input: Tensor) -> Tensor:
        return F.linear(input, self.weight, self.bias)

我们来看应用Conv1D的例子:

embed_dim = 768
conv1d = Conv1D(embed_dim, embed_dim * 3)
# (batch_size, seq_len, embed_dim)
x = torch.rand(2, 5, embed_dim)
# (batch_size, seq_len, embed_dim * 3)
x = conv1d(x)
print(x.shape)
torch.Size([2, 5, 2304])

前馈层

那么就可以应用上面的一维卷积来实现前馈层了:

from torch.nn import functional as F

class MLP(nn.Module):
    def __init__(self, config: GPTConfig) -> None:
        super().__init__()
        embed_dim = config.n_embd
        self.c_fc = Conv1D(embed_dim, embed_dim * 4)
        self.c_proj = Conv1D(embed_dim * 4, embed_dim)
        self.act = F.gelu
        self.dropout = nn.Dropout(config.dropout)

    def forward(self, x: Tensor) -> Tensor:
        """

        Args:
            x (Tensor): (batch_size, seq_len, embed_dim)

        Returns:
            Tensor: (batch_size, seq_len, embed_dim)
        """
        # h (batch_size, seq_len, embed_dim * 4)
        h = self.act(self.c_fc(x))
        # h (batch_size, seq_len, embed_dim)
        h = self.c_proj(h)
        return self.dropout(h)

层归一化

层归一化这里我们直接使用Pytorch内建的torch.nn.LayerNorm

掩码多头注意力

下面我们来实现掩码多头注意力,GPT中的注意力需要防止泄露未来的信息,因此自带一个下三角矩阵。

这可以通过以下代码实现:

import torch

n_positions = 10

torch.tril(torch.ones(n_positions, n_positions))
tensor([[1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
        [1., 1., 0., 0., 0., 0., 0., 0., 0., 0.],
        [1., 1., 1., 0., 0., 0., 0., 0., 0., 0.],
        [1., 1., 1., 1., 0., 0., 0., 0., 0., 0.],
        [1., 1., 1., 1., 1., 0., 0., 0., 0., 0.],
        [1., 1., 1., 1., 1., 1., 0., 0., 0., 0.],
        [1., 1., 1., 1., 1., 1., 1., 0., 0., 0.],
        [1., 1., 1., 1., 1., 1., 1., 1., 0., 0.],
        [1., 1., 1., 1., 1., 1., 1., 1., 1., 0.],
        [1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]])

先来看一下初始化方法:

def __init__(self, config: GPTConfig, scale: bool = False) -> None:
    super().__init__()
    self.n_embd = config.n_embd

    assert config.n_embd % config.n_head == 0

    self.scale = scale
    self.n_head = config.n_head

    self.c_attn = Conv1D(self.n_embd, self.n_embd * 3)
    self.c_proj = Conv1D(self.n_embd, self.n_embd)
    # use flash attention or not
    self.flash = hasattr(torch.nn.functional, "scaled_dot_product_attention")
    if not self.flash:
        self.register_buffer(
            "bias",
            torch.tril(torch.ones(config.n_positions, config.n_positions)).view(
                1, 1, config.n_positions, config.n_positions
            ),
            persistent=False,  # will not be saved alongside parameters
        )

    self.attn_dropout = nn.Dropout(config.dropout)
    self.proj_dropout = nn.Dropout(config.dropout)

主要操作是调用上面实现的Conv1Dc_attn这样定义为了可以同时计算query,key,value所有的头,因为在GPT中只有自注意力,由同一个输入计算出不同的query,key,value值,所以可以这样实现。

如果还Pytorch2.0及以上的版本,则torch.nn.functionalscaled_dot_product_attention函数,它利用Flash Attention高效计算。

否则通过register_buffer将下三角矩阵注册为buffer,并且不需要随着模型参数保存,转换为(1,1,n_positions,n_positions)的形状是为了适配批次和多个头。

接下来实现forward()函数:

 def forward(self, x: Tensor, output_attentions: bool = False) -> list[Tensor]:
        """

        Args:
            x (Tensor): (batch_size, seq_len, n_embd)

        Returns:
            Tensor: (batch_size, seq_len, n_embd) attn_output
            Tensor(optional): (batch_size, n_head, seq_len, seq_len) attn_weights

        """
        # calculate query, key ,value for all heads in batch
        # x (batch_size, seq_len, n_embd * 3)
        x = self.c_attn(x)
        #  query, key, value (batch_size, seq_len, n_embd)
        query, key, value = x.split(self.n_embd, dim=2)
        # query (batch_size,  n_head, seq_len, n_embd / n_head)
        query = self.split_heads(query)
        # key (batch_size, n_head, n_embd / n_head, seq_len)
        key = self.split_heads(key, is_key=True)
        # value (batch_size,  n_head, seq_len, n_embd / n_head)
        value = self.split_heads(value)
        # attn_output (batch_size,  n_head, seq_len, n_embd / n_head)
        attn_outputs = self._attn(query, key, value, output_attentions)
        attn_output = attn_outputs[0]
        # output (batch_size, seq_len, n_embd)
        output = self.merge_heads(attn_output)
        # (batch_size, seq_len, n_embd)
        output = self.c_proj(output)

        output = self.proj_dropout(output)

        outputs = [output] + attn_outputs[1:]
        return outputs

主要过程为:

  1. 通过c_attn一次计算出所有头的q,k,v值,得到的输出维度是(batch_size, seq_len, n_embd * 3)

  2. 调用split在最后一个维度上将输出拆分成q,k,v矩阵;

  3. 在q,k,v上分别调用split_heads()拆分成n_head个头;

  4. 传入q,k,v调用_attn()得到注意力计算结果;

  5. 调用merge_heads()拼接多头注意力的结果;

  6. 最后经过一个线性变换c_proj

split_heads其实就是一个变形(view)操作:

    def split_heads(self, x: Tensor, is_key: bool = False) -> Tensor:
        """

        Args:
            x (Tensor): (batch_size, seq_len, n_embd)
            is_key (bool, optional): is key or not. Defaults to False.

        Returns:
            Tensor: (batch_size, n_head, n_embd / n_head, seq_len) if is_key = True ,
              else (batch_size,  n_head, seq_len, n_embd / n_head)
        """
        # (batch_size, seq_len, n_head, n_embd / n_head)
        new_shape = x.size()[:-1] + (self.n_head, x.size(-1) // self.n_head)
        # x (batch_size, seq_len, n_head, n_embd / n_head)
        x = x.view(*new_shape)
        if is_key:
            # (batch_size, n_head, n_embd / n_head, seq_len)
            return x.permute(0, 2, 3, 1)
        # (batch_size,  n_head, seq_len, n_embd / n_head)
        return x.permute(0, 2, 1, 3)

接着就是核心的注意力操作_attn

def _attn(
    self,
    q: Tensor,
    k: Tensor,
    v: Tensor,
    attention_mask: Tensor = None,
    output_attentions: bool = False,
) -> list[Tensor]:
    """

    Args:
        q (Tensor): (batch_size,  n_head, seq_len, n_embd / n_head)
        k (Tensor): (batch_size, n_head, n_embd / n_head, seq_len)
        v (Tensor): (batch_size,  n_head, seq_len, n_embd / n_head)

    Returns:
        Tensor: (batch_size,  n_head, seq_len, n_embd / n_head) attn_output
        Tensor(optional): (batch_size, n_head, seq_len, seq_len) attn_weights

    """
    if self.flash:
        # 使用flash attention
        attn_output = torch.nn.functional.scaled_dot_product_attention(
            q,
            k,
            v,
            attn_mask=None,
            dropout_p=self.attn_dropout.p if self.training else 0,
            is_causal=True, # 传入True的话attn_mask必须为None
        )
        weights = None
    else:
        # scores (batch_size,  n_head, seq_len, seq_len)
        scores = torch.matmul(q, k)
        if self.scale:
            scores = scores / math.sqrt(v.size(-1))

        # scores = scores.masked_fill(
        #    self.bias[:, :, : scores.size(-2), : scores.size(-1)] == 0, float("-inf")
        # )
        bias = self.bias[:, :, : scores.size(-2), : scores.size(-1)]
        # more efficient than masked_fill
        scores = scores * bias + -1e9 * (1 - bias)

        # weights (batch_size,  n_head, seq_len, seq_len)
        weights = self.attn_dropout(F.softmax(scores, dim=-1))

        if attention_mask is not None:
            weights = weights + attention_mask

        del scores
        # attn_output (batch_size,  n_head, seq_len, n_embd / n_head)
        attn_output = torch.matmul(weights, v)

    outputs = [attn_output]
    if output_attentions:
        outputs.append(weights)

    return outputs

与上篇文章Transformer中实现的注意力计算几乎没有变化,对注意力得分scores进行一个下三角掩码,这里实现的时候采用比masked_fill更高效的乘法和加法的方式。

然后调用softmax得到注意力权重,与v矩阵相乘得到最后的注意力输出。

接下来通过merge_heads拼接多个注意力头的结果:

    def merge_heads(self, x: Tensor) -> Tensor:
        """

        Args:
            x (Tensor):  (batch_size,  n_head, seq_len, n_embd / n_head)

        Returns:
            Tensor: (batch_size, seq_len, n_embd)
        """
        # x (batch_size,  seq_len, n_head, n_embd / n_head)
        x = x.permute(0, 2, 1, 3).contiguous()
        # (batch_size, seq_len, n_embd)
        new_shape = x.size()[:-2] + (x.size(-2) * x.size(-1),)
        return x.view(*new_shape)

其实也是变形操作。最后经过一次线性投影。

此时模型还未进行过非线性操作,为了增强表达能力,通过前馈层引入非线性操作。

实现Block

class Block(nn.Module):
    def __init__(self, config: GPTConfig, scale: bool = False) -> None:
        super().__init__()
        n_embd = config.n_embd
        self.attn = Attention(config, scale)
        self.ln_1 = nn.LayerNorm(n_embd)
        self.mlp = MLP(config)
        self.ln_2 = nn.LayerNorm(n_embd)

    def forward(
        self, x: Tensor, attention_mask: Tensor = None, output_attentions: bool = False
    ) -> Tensor:
        """_summary_

        Args:
            x (Tensor): (batch_size, seq_len, n_embd)
            attention_mask (Tensor, optional)
            output_attentions (bool, optional)

        Returns:
            Tensor: (batch_size, seq_len, n_embd) block output
            Tensor(optional): (batch_size, n_head, seq_len, seq_len) attn_weights

        """

        attn_outputs = self.attn(x, attention_mask, output_attentions)
        # a : attention output (batch_size, n_head, seq_len, n_embd / n_head)
        a = attn_outputs[0]

        # resident connection and layer norm
        # n (batch_size, seq_len, n_embd)
        n = self.ln_1(x + a)
        # m (batch_size, seq_len, n_embd)
        m = self.mlp(n)
        # resident connection and layer norm
        # h (batch_size, seq_len, n_embd)
        h = self.ln_2(n + m)

        outputs = [h] + attn_outputs[1:]

        return outputs

Block的实现就很简单,按照架构图实现即可。这里的attention_mask是用于对对填充Token进行掩码。

实现GPT模型

首先我们要继承transformersPreTrainedModel,最终可以将训练好的模型上传到HuggingFace的Hub上分享给大家。

在这之前我们需要编写自定义配置,包含构建模型所需的所有信息:

from transformers import PretrainedConfig


class GPTConfig(PretrainedConfig):
    model_type = "openai-gpt" # 这个就是openai的gpt1

    def __init__(
        self,
        vocab_size=5000,
        n_positions=1024,
        n_embd=768,
        n_layer=12,
        n_head=12,
        dropout=0.1,
        initializer_range=0.02,
        **kwargs
    ) -> None:
        """

        Args:
            vocab_size (int, optional): vocabulary size. Defaults to 5000.
            n_positions (int, optional): the maximum sequence length that this model might ever be used with. Defaults to 1024.
            n_embd (int, optional): dimensionality of the embeddings and hidden states. Defaults to 768.
            n_layer (int, optional): number of hidden layers. Defaults to 12.
            n_head (int, optional): number of attention heads for each attention layer. Defaults to 12.
            dropout (float, optional): the dropout probability. Defaults to 0.1.
            initializer_range (tuple, optional): the standard deviation of the truncated_normal_initializer for initializing all weight matrices. Defaults to (0.02,).
        """
        self.vocab_size = vocab_size
        self.n_positions = n_positions
        self.n_embd = n_embd
        self.n_layer = n_layer
        self.n_head = n_head
        self.dropout = dropout
        self.initializer_range = initializer_range

        super().__init__(**kwargs)

编写自定义配置需要注意三点:

  • 继承自PretrainedConfig
  • __init__方法中必须存在接收任何参数的kwargs
  • 这些kwargs需要传递给父类的__init__方法;

通过继承我们可以获得Transformers库的额外功能,另外两个条件是接收PretrainedConfig额外的字段。

有了配置后,我们继续编写GPT模型,同样继承类似的PreTrainedModel。先定义一个基类,主要传入配置文件、定义参数初始化方法。

class GPTPreTrainedModel(PreTrainedModel):
    """
    An abstract class to handle weights initialization and a simple interface for downloading and loading pretrained
    models.
    """

    config_class = GPTConfig
    base_model_prefix = "transformer"

    def __init__(self, config: PretrainedConfig):
        super().__init__(config)

    def _init_weights(self, module):
        if isinstance(module, (nn.Linear, Conv1D)):
            module.weight.data.normal_(mean=0.0, std=self.config.initializer_range)
            if module.bias is not None:
                module.bias.data.zero_()
        elif isinstance(module, nn.Embedding):
            module.weight.data.normal_(mean=0.0, std=self.config.initializer_range)
            if module.padding_idx is not None:
                module.weight.data[module.padding_idx].zero_()
        elif isinstance(module, nn.LayerNorm):
            module.bias.data.zero_()
            module.weight.data.fill_(1.0)

现在就可以定义我们的GPT模型了:

class GPTModel(GPTPreTrainedModel):

    def __init__(self, config: GPTConfig) -> None:
        super().__init__(config)
        self.config = config
        self.tokens_embed = nn.Embedding(config.vocab_size, config.n_embd)
        self.positions_embed = nn.Embedding(config.n_positions, config.n_embd)

        self.dropout = nn.Dropout(config.dropout)
        self.h = nn.ModuleList(
            [Block(config, scale=True) for _ in range(config.n_layer)]
        )

        self.register_buffer(
            "position_ids", torch.arange(config.n_positions), persistent=False
        )
        self.post_init()

继承自上面定义的GPTPreTrainedModel,接收配置类。这里负责定义词嵌入和位置编码,对于这个可学习的位置编码,还需要定义表示位置的序列,从0到最大位置,即position_ids

然后堆叠多层Block,最后调用self.post_init(),这是PreTrainedModel中为我们实现的一个方法,它实际会调用我们自己定义的_init_weights

再来看前向传播方法:

def forward(
    self,
    input_ids: torch.LongTensor,
    attention_mask: Tensor = None,
    output_attentions: bool = False,
    output_hidden_states: bool = False,
    return_dict: bool = False,
) -> Union[Tuple[torch.Tensor], BaseModelOutput]:
    """
    Args:
        input_ids (torch.LongTensor): (batch_size, seq_len)
        output_attentions (bool, optional): whether or not to return the attentions tensors of all attention layers. Defaults to False.
        output_hidden_states (bool, optional): whether or not to return the hidden states of all layers. Defaults to False.
        return_dict (bool, optional): whether or not to return a ModelOutput instead of a plain tuple. Defaults to False.



    Returns:
        Union[Tuple[torch.Tensor], BaseModelOutput]: tuple or BaseModelOutput
    """

    input_shape = input_ids.size()

    inputs_embeds = self.tokens_embed(input_ids)
    # generate position ids
    position_ids = self.position_ids[None, : input_shape[-1]]

    position_embeds = self.positions_embed(position_ids)

    hidden_states = inputs_embeds + position_embeds

    hidden_states = self.dropout(hidden_states)

    all_attentions = () if output_attentions else None
    all_hidden_states = () if output_hidden_states else None

    for _, block in enumerate(self.h):
        if output_hidden_states:
            all_hidden_states = all_hidden_states + (hidden_states,)
        outputs = block(hidden_states, attention_mask, output_attentions)
        hidden_states = outputs[0]
        if output_attentions:
            all_attentions = all_attentions + (outputs[1],)

    # add last layer
    if output_hidden_states:
        all_hidden_states = all_hidden_states + (hidden_states,)

    if not return_dict:
        return tuple(
            v
            for v in [hidden_states, all_hidden_states, all_attentions]
            if v is not None
        )

    return BaseModelOutput(
        last_hidden_state=hidden_states,
        hidden_states=all_hidden_states,
        attentions=all_attentions,
    )

这样我们的GPT编码层就实现好了。文章来源地址https://www.toymoban.com/news/detail-809066.html

到了这里,关于手写GPT实现小说生成(一)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 深度学习实战29-AIGC项目:利用GPT-2(CPU环境)进行文本续写与生成歌词任务

    大家好,我是微学AI,今天给大家介绍一下深度学习实战29-AIGC项目:利用GPT-2(CPU环境)进行文本续写与生成歌词任务。在大家没有GPU算力的情况,大模型可能玩不动,推理速度慢,那么我们怎么才能跑去生成式的模型呢,我们可以试一下GPT-2完成一些简单的任务,让大家在CPU环

    2024年02月08日
    浏览(46)
  • NLP_GPT生成式自回归模型

    自回归(Autoregressive)是自然语言处理模型的一种训练方法,其核心思想是基于已有的序列(词或字符)来预测下一个元素。在GPT中,这意味着模型会根据给定的上文来生成下一个词,如图所示。 在GPT模型的训练和推理这两个相互独立的过程中,“自回归”的含义是不同的。 训练

    2024年02月21日
    浏览(32)
  • 想要成为 NLP 领域的大牛?从 ChatGPT 的 5 大自然语言模型开始了解吧(LM、Transformer、GPT、RLHF、LLM)——小白也能看得懂

      如果想在自然语言处理(Natural Language Processing,NLP)领域内脱颖而出,那么你一定不能错过 ChatGPT 的 5 大自然语言模型:LM、Transformer、GPT、RLHF 和 LLM。这些模型是 NLP 领域中最为重要的基础,涵盖了 语言模型、预训练模型、生成模型 等关键知识点。即使你是一个 NLP 小白

    2024年02月09日
    浏览(50)
  • AIGC实战——GPT(Generative Pre-trained Transformer)

    注意力机制能够用于构建先进的文本生成模型, Transformer 是用于序列建模的强大神经网络,该神经网络不需要复杂的循环或卷积架构,而只依赖于注意力机制。这种方法克服了循环神经网络 ( Recurrent Neural Network , RNN ) 方法难以并行化的缺陷( RNN 必须逐符号处理序列)。 Transf

    2024年03月12日
    浏览(45)
  • 【NLP相关】GPT-X合集:GPT类模型介绍(附相关论文和Github项目地址)

    ❤️觉得内容不错的话,欢迎点赞收藏加关注😊😊😊,后续会继续输入更多优质内容❤️ 👉有问题欢迎大家加关注私戳或者评论(包括但不限于NLP算法相关,linux学习相关,读研读博相关......)👈 GPT(Generative Pre-trained Transformer)是一类基于Transformer架构的预训练语言模型

    2024年02月01日
    浏览(58)
  • GitHub 2800颗星,支持GPT/Transformer,字节跳动这个开源项目是怎么来的?

    AI 绘画、机器翻译、多轮对话……对于各类 AI 相关的功能来说,总有一个痛点,困扰着所有训模型的算法工程师们: 想要效果更好,那么 AI 模型一般都很大,耗费的算力更多不说,运行起来还更费时间; 如果希望模型小、运行快,那么效果通常不如前者好。 这就像天平的

    2024年02月09日
    浏览(65)
  • 三、MNIST手写数字分类任务项目实战

    分类任务和回归任务本质上并没有太大的区别,只是最终得到的结果和损失函数不同而已。 MNIST手写数字分类任务,最终得到的是10个值,也类似一个one-hot编码格式,表示该图片是0-9数字的概率,概率值最大的就是预测的最终结果 当然标签也得是one-hot编码格式,例如标签图

    2024年02月09日
    浏览(36)
  • 【】实现GPT中Transformer模型之框架概念

      作者:黑夜路人 时间:2023年7月 GPT是什么意思 GPT 的全称是 Generative Pre-trained Transformer(生成型预训练变换模型),它是基于大量语料数据上训练,以生成类似于人类自然语言的文本。其名称中的“预训练”指的是在大型文本语料库上进行的初始训练过程,其中模型学习预

    2024年02月16日
    浏览(34)
  • NLP项目实战01--之电影评论分类

    欢迎来到本篇文章!在这里,我们将探讨一个常见而重要的自然语言处理任务——文本分类。具体而言,我们将关注情感分析任务,即通过分析电影评论的情感来判断评论是正面的、负面的。 展示: 训练展示如下: 实际使用如下: 实现方式: 选择PyTorch作为深度学习框架,

    2024年02月05日
    浏览(39)
  • 机器学习实战 | MNIST手写数字分类项目(深度学习初级)

    准备写个系列博客介绍机器学习实战中的部分公开项目。首先从初级项目开始。 本文为初级项目第二篇:利用MNIST数据集训练手写数字分类。 项目原网址为:Deep Learning Project – Handwritten Digit Recognition using Python。 第一篇为:机器学习实战 | emojify 使用Python创建自己的表情符号

    2024年02月15日
    浏览(51)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包