GPT模型的数据准备工作与二元语法模型相似,此处不再赘述,重点放在GPT模型的设计与训练过程上。从前文提到的GPT模型结构可知,GPT模型的核心为Transformer解码器,由多个Transformer模块组成。每个Transformer模块包含多头自注意力机制和前馈神经网络。
笔者将从Transformer模块开始,逐步构建GPT模型,并从单个Transformer模块扩展至多个Transformer模块,以更好地理解GPT模型的设计与训练过程。
我们需要先定义一个Transformer模块,然后将多个Transformer模块组合起来,形成所举模型。我们可以参考Andrej Karpathy的工作,解析Transformer模块的设计,相关代码如下。
该模块包括两个主要部分:一是多头自注意力机制,二是前馈神经网络。这两部分均为Transformer模型的核心组件,负责特征提取与映射。自注意力、层归一化、前馈网络和残差连接的设计模式构成了Transformer模型的核心,使其能够有效处理序列数据,学习长距离依赖,同时保证深层网络中的梯度有效传播。
实现多头自注意力机制的相关代码如下。
多头注意力机制的设计相对简洁,其核心计算逻辑如下:
❑ 对每个头调用其forward方法,每个头处理输入x并返回其注意力计算结果。
❑ 将所有头的输出,通过torch.cat函数串联在一起。
❑ 使用定义好的线性层self.proj对拼接后的输出进行线性变换,以融合不同头的信息,然后再随机丢弃一部分信息,以减少过拟合。
这部分的核心是关于单个头的定义,实现Head方法的相关代码如下。
单个自注意力模型的基础定义包括5个部分。
查询(query)的主要作用在于计算其与每个键的兼容性或相似度。每个查询对应输入序列中的一个元素(或一个时间步),用于与所有键进行比较,以确定模型应聚焦于序列的哪些部分。
键(key)用于与查询匹配,并生成一个分数(通常通过点积计算),该分数反映每个查询与序列中各元素的相关性程度。
在自注意力机制中,每个输入元素(如一个词或时间序列中的一个点)都有对应的“值”(value)向量,该向量携带了关于该元素的重要信息。当每个元素的注意力权重(即哪些元素更为重要,需被关注)被计算出后,这些权重用于对“值”进行加权。加权后的“值”将被合并,形成最终输出,代表整个输入序列的综合表达。
缓冲区用于存储一个下三角矩阵,在计算注意力分数时屏蔽未来信息。
随机失活(Dropout)指在训练神经网络的过程中,通过随机丢弃部分神经元,以降低神经网络的复杂度,进而防止过拟合。
查询、键和值的矩阵通常为密集矩阵,矩阵大小为 n embd ×head size 。这三个线性层通过模型训练过程不断更新权重,使模型更好地学习输入数据中的模式和关系。此学习与调整通过优化损失函数、进行反向传播及权重更新来实现。在无偏置项的情况下,每个线性变换仅依赖其权重矩阵,从而减少模型参数数量,有时也有助于避免过拟合。
单头注意力机制模型中forward函数的核心计算逻辑包括以下三个方面。
❑ 对查询、键和值进行线性变换,然后计算注意力得分。
❑ 通过softmax激活函数计算注意力权重,然后使用这些权重对值进行加权处理。
❑ 最后返回加权后的值,这个值代表输入序列的一个综合表达。
其中,计算注意力分数的核心步骤值得注意:
这行代码是自注意力机制中计算注意力得分的核心部分。
其中, q 表示查询矩阵,其维度为( B , T , hs ),其中 B 表示批次大小, T 表示序列长度, hs 表示每个注意力头的尺寸。 k 表示键矩阵,其维度同样为( B , T , hs )。
对上述计算逻辑详细说明如下。
❑ 矩阵转置:k.transpose(-2,-1),该操作将 k 矩阵的最后两个维度进行转置。由于 k 的原始维度为( B , T , hs ),转置后的维度变为( B , hs,T )。这样做的目的是将键矩阵的特征维度和查询矩阵的时间维度对齐,从而计算每个查询向量和所有键向量之间的点积。
❑ 矩阵乘法:q@k.transpose(-2,-1),这里使用@符号执行批量矩阵乘法。查询矩阵 q 与转置后的键矩阵 k 相乘,结果为一个新的三维矩阵( B , T , T )。该矩阵中的每个元素( b , i , j )表示第 b 个样本中,第 i 个查询向量与第 j 个键向量的点积,即查询 i 对键 j 的注意力得分。
❑ 缩放因子:*k.shape[-1]**-0.5,该代码对得分矩阵进行缩放处理。k.shape[-1]为键向量的维度
hs
,
hs
**-0.5等价于
。该缩放基于缩放点积注意力机制,以防止计算softmax时因过大的得分值引起梯度消失问题。通过除以键向量维度的平方根,确保得分矩阵在合理数值范围内,有利于梯度稳定和模型训练效果。
掩码自注意力是另一值得注意的部分:
该行代码采用下三角矩阵屏蔽未来信息。在自注意力机制中,模型在预测当前词时只能参考先前的词,无法“看到”未来的词。此掩码机制对文本生成和语言模型的理解至关重要。在此,通过下三角矩阵屏蔽未来信息,即将未来信息设置为负无穷,这样在计算softmax时,未来信息的对应权重将变为0,从而实现对未来信息的屏蔽。
对上述计算逻辑详细说明如下。
❑ 截取下三角矩阵:self.tril[:T,:T],该操作截取下三角矩阵的前 T 行与 T 列,以确保与注意力得分矩阵wei的维度匹配。
❑ 掩码操作:self.tril[:T,:T] == 0,这一步生成了一个布尔矩阵,其中下三角矩阵中为0的位置标记为True(即对角线上方的位置),其余为False。
❑ 填充操作:wei.masked_fill函数将wei矩阵中对应布尔矩阵为True的位置替换为指定值,这里的值是float('-inf ')(负无穷大)。这意味着,在执行softmax函数前,所有因果关系不合理的位置(即一个时间步应该无法“看到”它之后的时间步的信息)都被设置为负无穷。
这段代码通过将未来时间步的注意力得分设为负无穷,成功阻止了信息向前泄露。在执行softmax函数时,这些被设为负无穷的位置的指数值接近0,从而确保每个时间步只能关注其之前(包括自身)的时间步。这是生成任务(如语言模型中的下一个词预测)以及其他需要保持因果关系的场景中的关键机制。
自注意力机制的最后一步是计算加权逻辑:
out=wei@v #矩阵计算尺寸变换注释( B , T , T )@( B , T , hs )->( B , T , hs )
其中,变量说明如下。
❑ wei:通过查询( q )与键( k )的点积计算所得的注意力得分矩阵,维度为( B , T , T ),其中 B 为批次大小, T 为序列长度。
❑ v:值矩阵,维度为( B , T , hs ),每一行表示对应时间步的值向量,这些向量包含输入序列的信息,并根据计算出的注意力权重加权。
对上述操作的详细说明如下。
❑ 使用@符号进行批量矩阵乘法操作。在此操作中,wei的维度为( B , T , T ),而 v 的维度为( B , T , hs )。结果矩阵out的维度为( B , T , hs )。
❑ 对于每个批次(每个 B ),每个时间步 T 的输出是通过对所有时间步的值向量 v 进行加权求和计算得出的,权重由wei提供。具体而言,输出中的每个元素( b , i , j )是通过wei[ b , i ,:]和 v [ b ,:, j ]之间的加权求和计算得出的。
在此步骤中,wei中的每一行作为权重系数,与 v 中的所有行(值向量)加权,生成加权后的值向量,反映模型对每个时间步应关注信息的总结。该过程本质上是将注意力机制的权重应用于值向量。最终输出的out提供了综合整个序列信息的表示,每个时间步的信息是基于之前(以及当前时间步,如未使用掩码)的信息动态加权获得的。