NLP_构建GPT模型并完成文本生成任务

文章目录

  • 搭建GPT模型(解码器)
  • 构建文本生成任务的数据集
  • 训练过程中的自回归
  • 文本生成中的自回归(贪婪搜索)
  • 完整代码
  • 小结


搭建GPT模型(解码器)

GPT 只使用了 Transformer的解码器部分,其关键组件如图所示:
在这里插入图片描述

搭建 GPT 模型的代码的关键组件如下。

  • 组件1 多头自注意力:通过ScaledDotProductAttention 类实现缩放点积注意力机制,然后通过MultiHeadAttention 类实现多头自注意力机制。
  • 组件2 逐位置前馈网络:通过PoswiseFeedForwardNet类实现逐位置前馈网络。
  • 组件3 正弦位置编码表:通过get_sin_code_table 函数生成正弦位置编码表。
  • 组件4 填充掩码:通过get_attn_pad_mask函数为填充 token《pad》生成注意力掩码,避免注意力机制关注无用的信息。
  • 组件5 后续掩码:通过get_attn_subsequent_mask函数为后续token(当前位置后面的信息)生成注意力掩码,避免解码器中的注意力机制“偷窥”未来的目标数据。
  • 组件6 解码器层:通过DecoderLayer类定义解码器的单层。
  • 组件7 解码器:通过 Decoder类定义Transformer模型的完整解码器部分。
  • 组件8 GPT:在解码器的基础上添加一个投影层,将解码器输出的特征向量转换为预测结果,实现文本生成。

上述组件1~组件5,和前面的Transformer的相应组件完全相同,因此这里略过。下面的代码说明从组件6解码器层讲起。

组件6 解码器层类:

因为GPT模型没有编码器组件,也不需要来自编码器的输出,因此GPT解码器的实现更简洁。GPT模型也省略了编码器一解码器注意力机制,因此模型的训练速度更快。其解码器结构和Transformer解码器结构的特点见下表:
在这里插入图片描述

下面构建 GPT 模型的解码器层代码:

# 定义解码器层类
class DecoderLayer(nn.Module):
    def __init__(self):
        super(DecoderLayer, self).__init__()
        self.self_attn = MultiHeadAttention()  # 多头自注意力层
        self.feed_forward = PoswiseFeedForwardNet()  # 逐位置前馈网络层
        self.norm1 = nn.LayerNorm(d_embedding)  # 第一个层归一化
        self.norm2 = nn.LayerNorm(d_embedding)  # 第二个层归一化
    def forward(self, dec_inputs, attn_mask=None):
        # 使用多头自注意力处理输入
        attn_output, _ = self.self_attn(dec_inputs, dec_inputs, dec_inputs, attn_mask)
        # 将注意力输出与输入相加并进行第一个层归一化
        norm1_outputs = self.norm1(dec_inputs + attn_output)
        # 将归一化后的输出输入到位置前馈神经网络
        ff_outputs = self.feed_forward(norm1_outputs)
        # 将前馈神经网络输出与第一次归一化后的输出相加并进行第二个层归一化
        dec_outputs = self.norm2(norm1_outputs + ff_outputs)
        return dec_outputs # 返回解码器层输出

GPT的解码器层的输入仅为dec_inputs和attn_mask,没有使用编码器的输出,输出为 dec_outputs。

GPT 解码器层的构造比Transformer的解码器层简单,仅包含一个多头自注意力层MultiHeadAttention和一个逐位置前馈网络层PosFeedForwardNet,后面接了两个层归一化nn.LayerNorm。
解码器层中,两个层归一化的作用如下。

  • 第一个层归一化norm1:在多头自注意力self_attn 处理后,将注意力输出attn_ output与原始输入dec_inputs相加。这种加和操作实现了残差连接,可以加速梯度反向传播,有助于训练深层网络。将相加后的结果进行层归一化。层归一化对输入进行标准化处理,使其具有相同的均值和方差。这有助于减少梯度消失或梯度爆炸问题,从而提高模型训练的稳定性。

  • 第二个层归一化 norm2:在逐位置前馈网络feed_forward处理后,将前馈神经网络输出 ff_outputs 与第一个层归一化输出norm1_outputs 相加。这里同样实现了残差连接。将相加后的结果进行层归一化。这一步骤的目的与第一个层归一化相同,即标准化输入数据,以提高训练稳定性。

通过这两个层归一化操作,第一个解码器层可以在多头自注意力和逐位置前馈网络之间实现更稳定的信息传递,从而提高模型的训练效果。

组件7 解码器类:
下面基于解码器层来搭建解码器。

#  定义解码器类
n_layers = 6  # 设置 Decoder 的层数
class Decoder(nn.Module):
    def __init__(self, vocab_size, max_seq_len):
        super(Decoder, self).__init__()
        # 词嵌入层(参数为词典维度)
        self.src_emb = nn.Embedding(vocab_size, d_embedding)  
        # 位置编码层(参数为序列长度)
        self.pos_emb = nn.Embedding(max_seq_len, d_embedding)
        # 初始化 N 个解码器层       
        self.layers = nn.ModuleList([DecoderLayer() for _ in range(n_layers)]) 
    def forward(self, dec_inputs):        
        # 创建位置信息
        positions = torch.arange(len(dec_inputs), device=dec_inputs.device).unsqueeze(-1)
        # 将词嵌入与位置编码相加
        inputs_embedding = self.src_emb(dec_inputs) + self.pos_emb(positions)
        # 生成自注意力掩码
        attn_mask = get_attn_subsequent_mask(inputs_embedding).to(device)
        # 初始化解码器输入,这是第一层解码器层的输入 
        dec_outputs =  inputs_embedding 
        for layer in self.layers:
            # 将输入数据传递给解码器层,并返回解码器层的输出,作为下一层的输入
            dec_outputs = layer(dec_outputs, attn_mask) 
        return dec_outputs # 返回解码器输出

GPT解码器的结构比Transformer解码器的结构简单,因为GPT是一个单向生成式模型,只关注生成文本而不关注源文本。GPT不需要实现编码器-解码器注意力的部分,仅接收解码器的输入,然后进行词嵌入和位置编码,并将二者相加,继而生成后续自注意力掩码,来保证每个位置只能看到当前位置之前的信息,以保持生成文本的自回归特性。最后把嵌入向量和掩码信息传递给解码器层,并行处理,并接收结果向量 dec_outputs,然后把它返回给GPT 模型。

组件8 GPT模型:

# 定义 GPT 模型
class GPT(nn.Module):
    def __init__(self, vocab_size, max_seq_len):
        super(GPT, self).__init__()
        self.decoder = Decoder(vocab_size, max_seq_len) # 解码器,用于学习文本生成能力
        self.projection = nn.Linear(d_embedding, vocab_size)  # 全连接层,输出预测结果
    def forward(self, dec_inputs):        
        dec_outputs = self.decoder(dec_inputs) # 将输入数据传递给解码器
        logits = self.projection(dec_outputs) # 传递给全连接层以生成预测
        return logits # 返回预测结果

在这个简化版的GPT模型中:解码器类负责学习文本生成能力;一个全连接层将解码器输出的特征向量映射到一个概率分布,表示生成每个单词的概率logits,用于将解码器的输出转换为与词汇表大小相匹配的预测结果。

GPT模型仅包含解码器部分,没有编码器部分。因此,它更适用于无条件文本生成任务,而不是类似机器翻译或问答等需要编码器——解码器结构的任务。

构建文本生成任务的数据集

选择一个适合GPT模型的任务——文本生成。准备一个训练语料库。这个语料库是由现实中存在的文字组成的。比起维基百科等大型语料库,我们的语料库中的数据比较少,你可以把它看成人类语料库的一个缩影。

下面,构建语料库类LanguageCorpus,用于读入并整理语料,创建批次数据。

# 构建语料库
from collections import Counter
class LanguageCorpus:
    def __init__(self, sentences):
        self.sentences = sentences
        # 计算语言的最大句子长度,并加 2 以容纳特殊符号 <sos> 和 <eos>
        self.seq_len = max([len(sentence.split()) for sentence in sentences]) + 2
        self.vocab = self.create_vocabulary() # 创建源语言和目标语言的词汇表
        self.idx2word = {v: k for k, v in self.vocab.items()} # 创建索引到单词的映射
    def create_vocabulary(self):
        vocab = {'<pad>': 0, '<sos>': 1, '<eos>': 2}
        counter = Counter()
        # 统计语料库的单词频率
        for sentence in self.sentences:
            words = sentence.split()
            counter.update(words)
        # 创建词汇表,并为每个单词分配一个唯一的索引
        for word in counter:
            if word not in vocab:
                vocab[word] = len(vocab)
        return vocab
    def make_batch(self, batch_size, test_batch=False):
        input_batch, output_batch = [], [] # 初始化批数据
        sentence_indices = torch.randperm(len(self.sentences))[:batch_size] # 随机选择句子索引
        for index in sentence_indices:
            sentence = self.sentences[index]
            # 将句子转换为索引序列
            seq = [self.vocab['<sos>']] + [self.vocab[word] for word in sentence.split()] + [self.vocab['<eos>']]
            seq += [self.vocab['<pad>']] * (self.seq_len - len(seq)) # 对序列进行填充
            # 将处理好的序列添加到批次中
            input_batch.append(seq[:-1])
            output_batch.append(seq[1:])
        return torch.LongTensor(input_batch), torch.LongTensor(output_batch)

这个类的主要功能是创建词汇表、将句子转换为索引序列、生成批次数据等,其中最重要的是make_batch方法中生成批次数据时的“向右位移”操作,这是训练生成式语言模型的关键所在。
(1)在_init_方法中,初始化实例变量,包括语料库中的句子、最大句子长度(加上特殊符号《sos》和《eos》)、词汇表及索引到单词的映射。
(2) create_vocabulary方法用于创建词汇表。首先定义特殊符号,然后统计所有句子中的单词频率。最后,为每个单词分配一个唯一的索引。
(3) make_batch方法用于生成批次数据。首先随机选择句子索引,然后将选定的句子转换为索引序列并进行填充,接着通过“向右位移”操作生成输入序列和输出(目标)序列(seq[:-1]表示去掉最后一个元素的序列,用作输入序列;seq[1:]表示从第二个元素开始的序列,用作目标序列)。最后,将处理好的序列添加到输入批次和输出批次中。
假设有一个句子序列为“《sos》咖哥 喜欢 小冰 《eos》”。
输入序列 input_batch :《sos》 咖哥 喜欢小冰。
目标序列output_batch :咖哥喜欢 小冰 《eos》。

根据文件创建一个语料库实例,并显示其中的一些信息。

with open("lang.txt", "r") as file: # 从文件中读入语料
    sentences = [line.strip() for line in file.readlines()]
corpus = LanguageCorpus(sentences) # 创建语料库
vocab_size = len(corpus.vocab) # 词汇表大小
max_seq_len = corpus.seq_len # 最大句子长度(用于设置位置编码)
print(f" 语料库词汇表大小 : {vocab_size}") # 打印词汇表大小
print(f" 最长句子长度 : {max_seq_len}") # 打印最大序列长

在这里插入图片描述

现在,有了语料库和批次数据,可以开始GPT模型的训练。

训练过程中的自回归

import torch.optim as optim # 导入优化器
device = "cuda" if torch.cuda.is_available() else "cpu" # 设置设备
model = GPT(vocab_size, max_seq_len).to(device) # 创建 GPT 模型实例
criterion = nn.CrossEntropyLoss() # 损失函数
optimizer = optim.Adam(model.parameters(), lr=0.0001) # 优化器
epochs = 500 # 训练轮次
for epoch in range(epochs):  # 训练 epochs 轮
    optimizer.zero_grad() # 梯度清零
    inputs, targets = corpus.make_batch(batch_size) # 创建训练数据
    inputs, targets = inputs.to(device), targets.to(device)
    outputs = model(inputs) # 获取模型输出 
    loss = criterion(outputs.view(-1, vocab_size), targets.view(-1)) # 计算损失
    if (epoch + 1) % 100 == 0: # 打印损失
        print(f"Epoch: {epoch + 1:04d} cost = {loss:.6f}")
    loss.backward() # 反向传播
    optimizer.step() # 更新参数

在这里插入图片描述

这段代码与之前示例中的训练代码毫无二致。训练数据由给定的输入句子构成,这些句子已经被编码为数字表示(词汇表中的索引)。在每个训练批次中,模型的输入是当前单词序列,而目标输出是该序列中每个单词的下一个单词。为了计算损失,模型预测下一个单词的概率分布(对于整个词汇表),然后使用交叉熵损失函数比较这些预测概率和实际目标单词。

这里的“自回归”体现在模型的训练目标上,也就是输入序列和目标序列的构造及损失的计算上。模型需要预测给定前文的下一个单词,这意味着模型在每个时间步生成一个条件概率,这个概率依赖于先前的所有单词。

  • 输入序列和目标序列的创建:通过右移操作,目标序列是输入序列向右移动一个位置的结果。这样,模型在学习预测给定上下文的下一个单词时,能够利用先前的单词信息;而通过后续注意力掩码,模型在注意力计算过程中看不到后面的信息。

  • 损失计算:将模型的输出序列与目标序列进行比较以计算损失。因为输出序列的每个位置对应一个预测的单词,所以这个损失体现了模型在预测给定上文的下一个单词时的性能。交叉熵损失用于衡量预测分布与实际分布之间的差异。

当然了,基于Transformer架构的并行处理能力,虽然在训练阶段没有显式地将自回归过程建模,但自回归过程通过后续注意力掩码实现;在推理阶段(即生成新文本时),模型会根据先前生成的单词来生成下一个单词,从而体现出自回归特性。

文本生成中的自回归(贪婪搜索)

def generate_text(model, input_str,max_len=50):
    model.eval()#将模型设置为评估(测试)模式,关闭dropout 和 batch normalization等训练相关的层
    # 将输入字符串中的每个token转换为其在词汇表中的索引 
    input_tokens = [corpus.vocab[token] for token in input_str]
    #创建一个新列表,将输人的tokens复制到输出tokens中,目前只有输入的词 
    output_tokens = input_tokens.copy()
    with torch.no_grad():# 禁用梯度计算,以节省内存并加速测试过程 
        for _ in range(max_len):   # 生成最多 max_len个tokens
            #将输出的 token 转换为PyTorch张量,并增加一个代表批次的维度[1,len(output_tokens)] 
            inputs = torch.LongTensor(output_tokens).unsqueeze(0).to(device)
            outputs = model(inputs) # 输出 logits 形状为[1, len(output_tokens), vocab_size]
            # 在最后一个维度上获取logits中的最大值,并返回其索引(即下一个token)
            _, next_token = torch.max(outputs[:, -1, :], dim=-1)
            next_token = next_token.item()#将张量转换为Python整数 
            if next_token == corpus.vocab["<eos>"]:
                break #如果生成的token是EOS(结束符),则停止生成过程
            output_tokens.append(next_token) #将生成的 tokens 添加到output_tokens 列表
    #将输出 tokens转换回文本字符串
    output_str = " ".join([corpus.idx2word[token] for token in output_tokens])
    return output_str
                           
input_str = ["Python"]#输入一个词:Python
generated_text = generate_text(model, input_str) #模型根据这个词生成后续文本 
print("生成的文本:",generated_text) #打印预测文本

在这里插入图片描述

自回归是生成式模型的一个重要特征,可在文本生成任务中逐步生成序列。

这段代码的generate_text函数的目的就是根据给定的输入字符串生成一个后续的文本序列。首先,代码将输入字符串转换为一个单词索引的列表input_tokens,然后,将这些输入的token作为初始生成的文本output_tokens。接下来,函数进入一个循环,该循环将逐个生成新的token,直到达到最大长度max_len或者遇到句子结束标记《eos》。在每次循环中,代码将当前的output_tokens 输入模型,然后从模型的输出中选择具有最高概率的单词作为下一个生成的单词。这个新生成的单词被添加到 output_tokens列表中,再在下一轮迭代中被用作输入单词。

这个过程就是自回归,因为在每一步中,模型都根据之前生成的单词序列生成下一个单词。这使得生成的文本在语法和上下文方面具有连贯性。

在生成文本的算法选择上,这个函数使用的是贪婪搜索算法,也就是贪婪解码。所谓贪婪解码,指的就是我们在每个时间步只选择概率最高的输出单词。在代码_,next_ token = torch.max(outputs[:,-1,:],dim=-1)中,选取了outputs词汇表这个维度中具有最大概率的单词索引作为next_token。然后,这个单词会被添加到output_ tokens 列表中,用作下一个时间步的输入。

贪婪解码在我们这个例子中,效果还算不错。在有些情况下,贪婪解码计算效率高,但容易产生一些问题,如tokens(比如《eos》)反复出现,无意义词句组合循环出现,这是因为算法陷入局部最优解。而另一种常见的搜索算法是集束搜索,它能够更好地平衡全局最优解和局部最优解。

完整代码

组件1 多头自注意力:通过ScaledDotProductAttention 类实现缩放点积注意力机制,然后通过MultiHeadAttention 类实现多头自注意力机制。

import numpy as np # 导入 numpy 库
import torch # 导入 torch 库
import torch.nn as nn # 导入 torch.nn 库
d_k = 64 # K(=Q) 维度
d_v = 64 # V 维度
# 定义缩放点积注意力类
class ScaledDotProductAttention(nn.Module):
    def __init__(self):
        super(ScaledDotProductAttention, self).__init__()        
    def forward(self, Q, K, V, attn_mask):
        #------------------------- 维度信息 --------------------------------        
        # Q K V [batch_size, n_heads, len_q/k/v, dim_q=k/v] (dim_q=dim_k)
        # attn_mask [batch_size, n_heads, len_q, len_k]
        #----------------------------------------------------------------
        # 计算注意力分数(原始权重)[batch_size,n_heads,len_q,len_k]
        scores = torch.matmul(Q, K.transpose(-1, -2)) / np.sqrt(d_k) 
        #------------------------- 维度信息 --------------------------------        
        # scores [batch_size, n_heads, len_q, len_k]
        #-----------------------------------------------------------------        
        # 使用注意力掩码,将 attn_mask 中值为 1 的位置的权重替换为极小值
        #------------------------- 维度信息 -------------------------------- 
        # attn_mask [batch_size, n_heads, len_q, len_k], 形状和 scores 相同
        #-----------------------------------------------------------------    
        scores.masked_fill_(attn_mask, -1e9) 
        # 对注意力分数进行 softmax 归一化
        weights = nn.Softmax(dim=-1)(scores) 
        #------------------------- 维度信息 -------------------------------- 
        # weights [batch_size, n_heads, len_q, len_k], 形状和 scores 相同
        #-----------------------------------------------------------------         
        # 计算上下文向量(也就是注意力的输出), 是上下文信息的紧凑表示
        context = torch.matmul(weights, V) 
        #------------------------- 维度信息 -------------------------------- 
        # context [batch_size, n_heads, len_q, dim_v]
        #-----------------------------------------------------------------    
        return context, weights # 返回上下文向量和注意力分数
# 定义多头自注意力类
d_embedding = 512  # Embedding 的维度
n_heads = 8  # Multi-Head Attention 中头的个数
batch_size = 3 # 每一批的数据大小
class MultiHeadAttention(nn.Module):
    def __init__(self):
        super(MultiHeadAttention, self).__init__()
        self.W_Q = nn.Linear(d_embedding, d_k * n_heads) # Q的线性变换层
        self.W_K = nn.Linear(d_embedding, d_k * n_heads) # K的线性变换层
        self.W_V = nn.Linear(d_embedding, d_v * n_heads) # V的线性变换层
        self.linear = nn.Linear(n_heads * d_v, d_embedding)
        self.layer_norm = nn.LayerNorm(d_embedding)
    def forward(self, Q, K, V, attn_mask): 
        #------------------------- 维度信息 -------------------------------- 
        # Q K V [batch_size, len_q/k/v, embedding_dim] 
        #-----------------------------------------------------------------        
        residual, batch_size = Q, Q.size(0) # 保留残差连接
        # 将输入进行线性变换和重塑,以便后续处理
        q_s = self.W_Q(Q).view(batch_size, -1, n_heads, d_k).transpose(1,2)        
        k_s = self.W_K(K).view(batch_size, -1, n_heads, d_k).transpose(1,2)
        v_s = self.W_V(V).view(batch_size, -1, n_heads, d_v).transpose(1,2)
        #------------------------- 维度信息 -------------------------------- 
        # q_s k_s v_s: [batch_size, n_heads, len_q/k/v, d_q=k/v]
        #----------------------------------------------------------------- 
        # 将注意力掩码复制到多头 attn_mask: [batch_size, n_heads, len_q, len_k]
        attn_mask = attn_mask.unsqueeze(1).repeat(1, n_heads, 1, 1)
        #------------------------- 维度信息 -------------------------------- 
        # attn_mask [batch_size, n_heads, len_q, len_k]
        #----------------------------------------------------------------- 
        # 使用缩放点积注意力计算上下文和注意力权重
        context, weights = ScaledDotProductAttention()(q_s, k_s, v_s, attn_mask)
        #------------------------- 维度信息 -------------------------------- 
        # context [batch_size, n_heads, len_q, dim_v]
        # weights [batch_size, n_heads, len_q, len_k]
        #----------------------------------------------------------------- 
        # 通过调整维度将多个头的上下文向量连接在一起
        context = context.transpose(1, 2).contiguous().view(batch_size, -1, n_heads * d_v) 
        #------------------------- 维度信息 -------------------------------- 
        # context [batch_size, len_q, n_heads * dim_v]
        #-----------------------------------------------------------------        
        # 用一个线性层把连接后的多头自注意力结果转换,原始地嵌入维度
        output = self.linear(context) 
        #------------------------- 维度信息 -------------------------------- 
        # output [batch_size, len_q, embedding_dim]
        #-----------------------------------------------------------------        
        # 与输入 (Q) 进行残差链接,并进行层归一化后输出
        output = self.layer_norm(output + residual)
        #------------------------- 维度信息 -------------------------------- 
        # output [batch_size, len_q, embedding_dim]
        #-----------------------------------------------------------------        
        return output, weights # 返回层归一化的输出和注意力权重

组件2 逐位置前馈网络:通过PoswiseFeedForwardNet类实现逐位置前馈网络。

# 定义逐位置前馈网络类
class PoswiseFeedForwardNet(nn.Module):
    def __init__(self, d_ff=2048):
        super(PoswiseFeedForwardNet, self).__init__()
        # 定义一维卷积层 1,用于将输入映射到更高维度
        self.conv1 = nn.Conv1d(in_channels=d_embedding, out_channels=d_ff, kernel_size=1)
        # 定义一维卷积层 2,用于将输入映射回原始维度
        self.conv2 = nn.Conv1d(in_channels=d_ff, out_channels=d_embedding, kernel_size=1)
        # 定义层归一化
        self.layer_norm = nn.LayerNorm(d_embedding)
    def forward(self, inputs): 
        #------------------------- 维度信息 -------------------------------- 
        # inputs [batch_size, len_q, embedding_dim]
        #----------------------------------------------------------------                       
        residual = inputs  # 保留残差连接 
        # 在卷积层 1 后使用 ReLU 激活函数 
        output = nn.ReLU()(self.conv1(inputs.transpose(1, 2))) 
        #------------------------- 维度信息 -------------------------------- 
        # output [batch_size, d_ff, len_q]
        #----------------------------------------------------------------
        # 使用卷积层 2 进行降维 
        output = self.conv2(output).transpose(1, 2) 
        #------------------------- 维度信息 -------------------------------- 
        # output [batch_size, len_q, embedding_dim]
        #----------------------------------------------------------------
        # 与输入进行残差链接,并进行层归一化
        output = self.layer_norm(output + residual) 
        #------------------------- 维度信息 -------------------------------- 
        # output [batch_size, len_q, embedding_dim]
        #----------------------------------------------------------------
        return output # 返回加入残差连接后层归一化的结果

组件3 正弦位置编码表:通过get_sin_code_table 函数生成正弦位置编码表。

# 生成正弦位置编码表的函数,用于在 Transformer 中引入位置信息
def get_sin_enc_table(n_position, embedding_dim):
    #------------------------- 维度信息 --------------------------------
    # n_position: 输入序列的最大长度
    # embedding_dim: 词嵌入向量的维度
    #-----------------------------------------------------------------    
    # 根据位置和维度信息,初始化正弦位置编码表
    sinusoid_table = np.zeros((n_position, embedding_dim))    
    # 遍历所有位置和维度,计算角度值
    for pos_i in range(n_position):
        for hid_j in range(embedding_dim):
            angle = pos_i / np.power(10000, 2 * (hid_j // 2) / embedding_dim)
            sinusoid_table[pos_i, hid_j] = angle    
    # 计算正弦和余弦值
    sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:, 0::2])  # dim 2i 偶数维
    sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:, 1::2])  # dim 2i+1 奇数维    
    #------------------------- 维度信息 --------------------------------
    # sinusoid_table 的维度是 [n_position, embedding_dim]
    #----------------------------------------------------------------   
    return torch.FloatTensor(sinusoid_table)  # 返回正弦位置编码表

组件4 填充掩码:通过get_attn_pad_mask函数为填充 token《pad》生成注意力掩码,避免注意力机制关注无用的信息。

# 定义填充注意力掩码函数
def get_attn_pad_mask(seq_q, seq_k):
    #------------------------- 维度信息 --------------------------------
    # seq_q 的维度是 [batch_size, len_q]
    # seq_k 的维度是 [batch_size, len_k]
    #-----------------------------------------------------------------
    batch_size, len_q = seq_q.size()
    batch_size, len_k = seq_k.size()
    # 生成布尔类型张量
    pad_attn_mask = seq_k.data.eq(0).unsqueeze(1)  # <PAD>token 的编码值为 0
    #------------------------- 维度信息 --------------------------------
    # pad_attn_mask 的维度是 [batch_size,1,len_k]
    #-----------------------------------------------------------------
    # 变形为与注意力分数相同形状的张量 
    pad_attn_mask = pad_attn_mask.expand(batch_size, len_q, len_k)
    #------------------------- 维度信息 --------------------------------
    # pad_attn_mask 的维度是 [batch_size,len_q,len_k]
    #-----------------------------------------------------------------
    return pad_attn_mask # 返回填充位置的注意力掩码
# 定义编码器层类
class EncoderLayer(nn.Module):
    def __init__(self):
        super(EncoderLayer, self).__init__()        
        self.enc_self_attn = MultiHeadAttention() # 多头自注意力层        
        self.pos_ffn = PoswiseFeedForwardNet() # 位置前馈神经网络层
    def forward(self, enc_inputs, enc_self_attn_mask):
        #------------------------- 维度信息 --------------------------------
        # enc_inputs 的维度是 [batch_size, seq_len, embedding_dim]
        # enc_self_attn_mask 的维度是 [batch_size, seq_len, seq_len]
        #-----------------------------------------------------------------
        # 将相同的 Q,K,V 输入多头自注意力层 , 返回的 attn_weights 增加了头数  
        enc_outputs, attn_weights = self.enc_self_attn(enc_inputs, enc_inputs,
                                               enc_inputs, enc_self_attn_mask)
        #------------------------- 维度信息 --------------------------------
        # enc_outputs 的维度是 [batch_size, seq_len, embedding_dim] 
        # attn_weights 的维度是 [batch_size, n_heads, seq_len, seq_len]      
        # 将多头自注意力 outputs 输入位置前馈神经网络层
        enc_outputs = self.pos_ffn(enc_outputs) # 维度与 enc_inputs 相同
        #------------------------- 维度信息 --------------------------------
        # enc_outputs 的维度是 [batch_size, seq_len, embedding_dim] 
        #-----------------------------------------------------------------
        return enc_outputs, attn_weights # 返回编码器输出和每层编码器注意力权重
# 定义编码器类
n_layers = 6  # 设置 Encoder 的层数
class Encoder(nn.Module):
    def __init__(self, corpus):
        super(Encoder, self).__init__()        
        self.src_emb = nn.Embedding(len(corpus.src_vocab), d_embedding) # 词嵌入层
        self.pos_emb = nn.Embedding.from_pretrained( \
          get_sin_enc_table(corpus.src_len+1, d_embedding), freeze=True) # 位置嵌入层
        self.layers = nn.ModuleList(EncoderLayer() for _ in range(n_layers))# 编码器层数
    def forward(self, enc_inputs):  
        #------------------------- 维度信息 --------------------------------
        # enc_inputs 的维度是 [batch_size, source_len]
        #-----------------------------------------------------------------
        # 创建一个从 1 到 source_len 的位置索引序列
        pos_indices = torch.arange(1, enc_inputs.size(1) + 1).unsqueeze(0).to(enc_inputs)
        #------------------------- 维度信息 --------------------------------
        # pos_indices 的维度是 [1, source_len]
        #-----------------------------------------------------------------             
        # 对输入进行词嵌入和位置嵌入相加 [batch_size, source_len,embedding_dim]
        enc_outputs = self.src_emb(enc_inputs) + self.pos_emb(pos_indices)
        #------------------------- 维度信息 --------------------------------
        # enc_outputs 的维度是 [batch_size, seq_len, embedding_dim]
        #-----------------------------------------------------------------
        # 生成自注意力掩码
        enc_self_attn_mask = get_attn_pad_mask(enc_inputs, enc_inputs) 
        #------------------------- 维度信息 --------------------------------
        # enc_self_attn_mask 的维度是 [batch_size, len_q, len_k]        
        #-----------------------------------------------------------------         
        enc_self_attn_weights = [] # 初始化 enc_self_attn_weights
        # 通过编码器层 [batch_size, seq_len, embedding_dim]
        for layer in self.layers: 
            enc_outputs, enc_self_attn_weight = layer(enc_outputs, enc_self_attn_mask)
            enc_self_attn_weights.append(enc_self_attn_weight)
        #------------------------- 维度信息 --------------------------------
        # enc_outputs 的维度是 [batch_size, seq_len, embedding_dim] 维度与 enc_inputs 相同
        # enc_self_attn_weights 是一个列表,每个元素的维度是 [batch_size, n_heads, seq_len, seq_len]          
        #-----------------------------------------------------------------
        return enc_outputs, enc_self_attn_weights # 返回编码器输出和编码器注意力权重

组件5 后续掩码:通过get_attn_subsequent_mask函数为后续token(当前位置后面的信息)生成注意力掩码,避免解码器中的注意力机制“偷窥”未来的目标数据。

# 生成后续注意力掩码的函数,用于在多头自注意力计算中忽略未来信息
def get_attn_subsequent_mask(seq):
    #------------------------- 维度信息 --------------------------------
    # seq 的维度是 [batch_size, seq_len(Q)=seq_len(K)]
    #-----------------------------------------------------------------
    # 获取输入序列的形状
    attn_shape = [seq.size(0), seq.size(1), seq.size(1)]  
    #------------------------- 维度信息 --------------------------------
    # attn_shape 是一个一维张量 [batch_size, seq_len(Q), seq_len(K)]
    #-----------------------------------------------------------------
    # 使用 numpy 创建一个上三角矩阵(triu = triangle upper)
    subsequent_mask = np.triu(np.ones(attn_shape), k=1)
    #------------------------- 维度信息 --------------------------------
    # subsequent_mask 的维度是 [batch_size, seq_len(Q), seq_len(K)]
    #-----------------------------------------------------------------
    # 将 numpy 数组转换为 PyTorch 张量,并将数据类型设置为 byte(布尔值)
    subsequent_mask = torch.from_numpy(subsequent_mask).byte()
    #------------------------- 维度信息 --------------------------------
    # 返回的 subsequent_mask 的维度是 [batch_size, seq_len(Q), seq_len(K)]
    #-----------------------------------------------------------------
    return subsequent_mask # 返回后续位置的注意力掩码

组件6 解码器层:通过DecoderLayer类定义解码器的单层。

# 定义解码器层类
class DecoderLayer(nn.Module):
    def __init__(self):
        super(DecoderLayer, self).__init__()
        self.self_attn = MultiHeadAttention()  # 多头自注意力层
        self.feed_forward = PoswiseFeedForwardNet()  # 逐位置前馈网络层
        self.norm1 = nn.LayerNorm(d_embedding)  # 第一个层归一化
        self.norm2 = nn.LayerNorm(d_embedding)  # 第二个层归一化
    def forward(self, dec_inputs, attn_mask=None):
        # 使用多头自注意力处理输入
        attn_output, _ = self.self_attn(dec_inputs, dec_inputs, dec_inputs, attn_mask)
        # 将注意力输出与输入相加并进行第一个层归一化
        norm1_outputs = self.norm1(dec_inputs + attn_output)
        # 将归一化后的输出输入到位置前馈神经网络
        ff_outputs = self.feed_forward(norm1_outputs)
        # 将前馈神经网络输出与第一次归一化后的输出相加并进行第二个层归一化
        dec_outputs = self.norm2(norm1_outputs + ff_outputs)
        return dec_outputs # 返回解码器层输出

组件7 解码器:通过 Decoder类定义Transformer模型的完整解码器部分。

#  定义解码器类
n_layers = 6  # 设置 Decoder 的层数
class Decoder(nn.Module):
    def __init__(self, vocab_size, max_seq_len):
        super(Decoder, self).__init__()
        # 词嵌入层(参数为词典维度)
        self.src_emb = nn.Embedding(vocab_size, d_embedding)  
        # 位置编码层(参数为序列长度)
        self.pos_emb = nn.Embedding(max_seq_len, d_embedding)
        # 初始化 N 个解码器层       
        self.layers = nn.ModuleList([DecoderLayer() for _ in range(n_layers)]) 
    def forward(self, dec_inputs):        
        # 创建位置信息
        positions = torch.arange(len(dec_inputs), device=dec_inputs.device).unsqueeze(-1)
        # 将词嵌入与位置编码相加
        inputs_embedding = self.src_emb(dec_inputs) + self.pos_emb(positions)
        # 生成自注意力掩码
        attn_mask = get_attn_subsequent_mask(inputs_embedding).to(device)
        # 初始化解码器输入,这是第一层解码器层的输入 
        dec_outputs =  inputs_embedding 
        for layer in self.layers:
            # 将输入数据传递给解码器层,并返回解码器层的输出,作为下一层的输入
            dec_outputs = layer(dec_outputs, attn_mask) 
        return dec_outputs # 返回解码器输出

组件8 GPT:

# 定义 GPT 模型
class GPT(nn.Module):
    def __init__(self, vocab_size, max_seq_len):
        super(GPT, self).__init__()
        self.decoder = Decoder(vocab_size, max_seq_len) # 解码器,用于学习文本生成能力
        self.projection = nn.Linear(d_embedding, vocab_size)  # 全连接层,输出预测结果
    def forward(self, dec_inputs):        
        dec_outputs = self.decoder(dec_inputs) # 将输入数据传递给解码器
        logits = self.projection(dec_outputs) # 传递给全连接层以生成预测
        return logits # 返回预测结果
# 构建语料库
from collections import Counter
class LanguageCorpus:
    def __init__(self, sentences):
        self.sentences = sentences
        # 计算语言的最大句子长度,并加 2 以容纳特殊符号 <sos> 和 <eos>
        self.seq_len = max([len(sentence.split()) for sentence in sentences]) + 2
        self.vocab = self.create_vocabulary() # 创建源语言和目标语言的词汇表
        self.idx2word = {v: k for k, v in self.vocab.items()} # 创建索引到单词的映射
    def create_vocabulary(self):
        vocab = {'<pad>': 0, '<sos>': 1, '<eos>': 2}
        counter = Counter()
        # 统计语料库的单词频率
        for sentence in self.sentences:
            words = sentence.split()
            counter.update(words)
        # 创建词汇表,并为每个单词分配一个唯一的索引
        for word in counter:
            if word not in vocab:
                vocab[word] = len(vocab)
        return vocab
    def make_batch(self, batch_size, test_batch=False):
        input_batch, output_batch = [], [] # 初始化批数据
        sentence_indices = torch.randperm(len(self.sentences))[:batch_size] # 随机选择句子索引
        for index in sentence_indices:
            sentence = self.sentences[index]
            # 将句子转换为索引序列
            seq = [self.vocab['<sos>']] + [self.vocab[word] for word in sentence.split()] + [self.vocab['<eos>']]
            seq += [self.vocab['<pad>']] * (self.seq_len - len(seq)) # 对序列进行填充
            # 将处理好的序列添加到批次中
            input_batch.append(seq[:-1])
            output_batch.append(seq[1:])
        return torch.LongTensor(input_batch), torch.LongTensor(output_batch)
with open("lang.txt", "r") as file: # 从文件中读入语料
    sentences = [line.strip() for line in file.readlines()]
corpus = LanguageCorpus(sentences) # 创建语料库
vocab_size = len(corpus.vocab) # 词汇表大小
max_seq_len = corpus.seq_len # 最大句子长度(用于设置位置编码)
print(f" 语料库词汇表大小 : {vocab_size}") # 打印词汇表大小
print(f" 最长句子长度 : {max_seq_len}") # 打印最大序列长

在这里插入图片描述

import torch.optim as optim # 导入优化器
device = "cuda" if torch.cuda.is_available() else "cpu" # 设置设备
model = GPT(vocab_size, max_seq_len).to(device) # 创建 GPT 模型实例
criterion = nn.CrossEntropyLoss() # 损失函数
optimizer = optim.Adam(model.parameters(), lr=0.0001) # 优化器
epochs = 500 # 训练轮次
for epoch in range(epochs):  # 训练 epochs 轮
    optimizer.zero_grad() # 梯度清零
    inputs, targets = corpus.make_batch(batch_size) # 创建训练数据
    inputs, targets = inputs.to(device), targets.to(device)
    outputs = model(inputs) # 获取模型输出 
    loss = criterion(outputs.view(-1, vocab_size), targets.view(-1)) # 计算损失
    if (epoch + 1) % 100 == 0: # 打印损失
        print(f"Epoch: {epoch + 1:04d} cost = {loss:.6f}")
    loss.backward() # 反向传播
    optimizer.step() # 更新参数

在这里插入图片描述

def generate_text(model, input_str,max_len=50):
    model.eval()#将模型设置为评估(测试)模式,关闭dropout 和 batch normalization等训练相关的层
    # 将输入字符串中的每个token转换为其在词汇表中的索引 
    input_tokens = [corpus.vocab[token] for token in input_str]
    #创建一个新列表,将输人的tokens复制到输出tokens中,目前只有输入的词 
    output_tokens = input_tokens.copy()
    with torch.no_grad():# 禁用梯度计算,以节省内存并加速测试过程 
        for _ in range(max_len):   # 生成最多 max_len个tokens
            #将输出的 token 转换为PyTorch张量,并增加一个代表批次的维度[1,len(output_tokens)] 
            inputs = torch.LongTensor(output_tokens).unsqueeze(0).to(device)
            outputs = model(inputs) # 输出 logits 形状为[1, len(output_tokens), vocab_size]
            # 在最后一个维度上获取logits中的最大值,并返回其索引(即下一个token)
            _, next_token = torch.max(outputs[:, -1, :], dim=-1)
            next_token = next_token.item()#将张量转换为Python整数 
            if next_token == corpus.vocab["<eos>"]:
                break #如果生成的token是EOS(结束符),则停止生成过程
            output_tokens.append(next_token) #将生成的 tokens 添加到output_tokens 列表
    #将输出 tokens转换回文本字符串
    output_str = " ".join([corpus.idx2word[token] for token in output_tokens])
    return output_str
                           
input_str = ["Python"]#输入一个词:Python
generated_text = generate_text(model, input_str) #模型根据这个词生成后续文本 
print("生成的文本:",generated_text) #打印预测文本

在这里插入图片描述

小结

文本生成中的GPT自回归,贪婪解码在我们这个例子中,效果还算不错。在有些情况下,贪婪解码计算效率高,但容易产生一些问题,如tokens(比如《eos》)反复出现,无意义词句组合循环出现,这是因为算法陷入局部最优解。而另一种常见的搜索算法是集束搜索,它能够更好地平衡全局最优解和局部最优解。


学习的参考资料:
(1)书籍
利用Python进行数据分析
西瓜书
百面机器学习
机器学习实战
阿里云天池大赛赛题解析(机器学习篇)
白话机器学习中的数学
零基础学机器学习
图解机器学习算法

动手学深度学习(pytorch)

(2)机构
光环大数据
开课吧
极客时间
七月在线
深度之眼
贪心学院
拉勾教育
博学谷
慕课网
海贼宝藏

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/402806.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

中医笔记(阴阳,五行,十二经脉,天干地支,子午流注,倪海厦中医笔记)

目录 一.阴阳1.1 什么是阴阳&#xff1f;1.2 作用1.3 阴阳理论在中医上的运用 二.五行2.1 五行之间的关系2.2 五行对应的力量2.3 原理&#xff1a; 三.天干地支四.子午流注十二经脉与子午流注之间的关系 五.十二经脉足太阳膀胱经 六.中医笔记小肠是火气化膀胱的水&#xff08;如…

java效率为什么比c/c++慢,蓝桥杯上java只得50分,c++通过?

java效率为什么比c/c慢,蓝桥杯上java只得50分&#xff0c;c通过&#xff1f; 在开始前我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「c的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给大…

车载测试,检测项目标准

检测项目&#xff1a; 二.GB/T 31486-2015电动汽车用动力蓄电池电性能要求及试验方法 说明&#xff1a;本标准规定了电动汽车用动力蓄电池(以下简称蓄电池)的 电性能要求、试验方法、检验规则。本标准适用于装载在电动汽车 上的锂离子蓄电池和金属氢化 物镍蓄电池单体和模块&a…

跟着pink老师前端入门教程(JavaScript)-day05

六、语句 &#xff08;一&#xff09;表达式和语句 1、表达式 表达式是可以被求值的代码&#xff0c;JavaScript 引擎会将其计算出一个结果。 2、语句 语句是一段可以执行的代码。 比如&#xff1a; prompt() 可以弹出一个输入框&#xff0c;还有 if语句 for 循环语句等…

创建型设计模式 - 原型设计模式 - JAVA

原型设计模式 一 .简介二. 案例三. 补充知识 前言 这是我在这个网站整理的笔记,有错误的地方请指出&#xff0c;关注我&#xff0c;接下来还会持续更新。 作者&#xff1a;神的孩子都在歌唱 一 .简介 原型模式提供了一种机制&#xff0c;可以将原始对象复制到新对象&#xff0…

Vue3_基础使用_3_Hooks模块化

今天主要学习的是hooks, vue3的使用比vue2方便很多了&#xff0c;但是呢各个功能块的逻辑有时候还是会缠绕在一起&#xff0c;这个时候使用hooks进行模块化管理开发&#xff0c;说白了就是将每个单独的业务放到自己的.ts中去写&#xff0c;以后修改就找到这个ts 不用到处去翻…

第三百六十一回

文章目录 1. 概念介绍2. 实现方法2.1 环绕效果2.2 立体效果 3. 示例代码4. 内容总结 我们在上一章回中介绍了"自定义SlideImageSwitch组件"相关的内容&#xff0c;本章回中将介绍两种阴影效果.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我们在本…

HL小祭記0221

早上很好&#xff0c;浑身酸疼&#xff0c;像被人*了 上午将字符串 一言难尽 中午天有点小雨 炸金花 额 潇寞手麻了&#xff0c;好快啊&#xff01; 靠开牌小赚一下 下午调题 动不动就一百行代码…… 小雨&#xff0c;中雨&#xff0c;大雨&#xff0c;电闪雷鸣 是不…

代码随想录算法训练营第58天 | 392.判断子序列 115.不同的子序列

判断子序列 这道题可以双指针方法解决。 class Solution { public:bool isSubsequence(string s, string t) {int s_index 0;for(int t_index 0; t_index < t.size(); t_index) {if(s[s_index] t[t_index]) {s_index;}}return s_index s.size();} };用动态规划也是可解…

Shader基础的简单实现(基于URP渲染)

一个模型是很多个顶点组成&#xff0c;顶点数据中包含坐标、法线、切线、UV坐标、顶点颜色等等组成。 URP(Universal Render Pipeline)通用渲染管线&#xff0c;是Unity在2019.3版本之后推出的一种新的渲染管线。传统的渲染管线在渲染多光源的情况&#xff0c;是把每一个主要光…

13款强大的开源API测试工具,不容错过!

使用SOA和微服务作为软件架构的趋势不断上升&#xff0c;催生了多种用于服务API自动化测试的工具。 API是应用程序接口&#xff08;application programming interface&#xff09;的缩写&#xff0c;是一套用于构建和集成应用软件的定义和协议&#xff0c;是两个或多个计算机…

2024新版Java高频面试题+Java八股文面试真题

Java面试题_2024新版Java高频面试题Java八股文面试真题 Java高频面试专题视频课程&#xff0c;瓤括了Java生态下的主流技术面试题&#xff0c;课程特色&#xff1a; 1、全面&#xff0c;jvm、并发编程、mysql、rabbitmq、spring、mybatis、redis、分布式、微服务、数据结构等等…

解决 ModuleNotFoundError: No module named ‘transformers‘

Traceback (most recent call last): File “start_cli_test.py”, line 2, in import transformers ModuleNotFoundError: No module named ‘transformers’ Traceback (most recent call last): File “/usr/local/python3/lib/python3.8/runpy.py”, line 185, in _run_m…

OpenCascade——BRepPrimAPI图元创建接口

OpenCascade BRepPrimAPI包提供了创建以下图元&#xff08;primitive&#xff09;的 API&#xff1a; 盒;锥体;柱体;棱镜。 可以创建部分实体&#xff0c;例如一定经度范围内的球体。在实际模型中&#xff0c;图元可用于轻松创建特定的子部件。 BRepPrimAPI也提供了扫掠方式…

鱼哥赠书活动第⑧期:《基础软件之路:企业级实践及开源之路》

鱼哥赠书活动第⑧期&#xff1a;《基础软件之路&#xff1a;企业级实践及开源之路》 作者介绍&#xff1a;1.静态分析工具在当前软件开发流程中的应用2.编译相关技术在静态分析工具中的应用3.编译相关技术在提升软件质量和性能上的更多应用4. 未来展望图书推荐&#xff1a;赠书…

Jest单元测试:玩转代码的小捉迷藏!

嗨&#xff0c;码农小伙伴们&#xff01;在这个金秋十月&#xff0c;国庆与中秋重叠&#xff0c;我们有一个特殊的任务——通过 Jest 单元测试&#xff0c;找到代码的神秘“中秋蛋糕”&#xff01;这是一个简单又有趣的冒险&#xff0c;就像在中秋晚会上找月亮一样容易。让我们…

springboot208基于springboot物流管理系统

基于spring boot物流管理系统设计与实现 摘 要 社会发展日新月异&#xff0c;用计算机应用实现数据管理功能已经算是很完善的了&#xff0c;但是随着移动互联网的到来&#xff0c;处理信息不再受制于地理位置的限制&#xff0c;处理信息及时高效&#xff0c;备受人们的喜爱。…

大数据Kafka--概述

文章目录 概述定义消息队列传统消息队列的应用场景消息队列的两种模式 Kafka基础架构 Kafka快速入门安装部署集群规划集群部署集群启停脚本 Kafka命令行操作主题命令行操作生产者命令行操作消费者命令行操作 概述 定义 消息队列 目前企业中比较常见的消息队列产品主要有 Kafk…

PLC_博图系列☞基本指令“插入输入”

PLC_博图系列☞基本指令“插入输入” 文章目录 PLC_博图系列☞基本指令“插入输入”背景介绍插入输入说明参数示例 关键字&#xff1a; PLC、 西门子、 博图、 Siemens 、 插入输入 背景介绍 这是一篇关于PLC编程的文章&#xff0c;特别是关于西门子的博图软件。我并不是专…

LeetCode每日刷题:101. 对称二叉树

题目&#xff1a; 解题思路&#xff1a;可以新写一个函数&#xff0c;从root开始&#xff0c;root的left的头结点将记为lefttree&#xff08;左子树&#xff09;,root的lright的头结点将记为righttree&#xff08;右子树&#xff09;&#xff0c; 然后递归左子树的root.left与右…