原文:
towardsdatascience.com/llms-and-transformers-from-scratch-the-decoder-d533008629c5
本文由 Rafael Nardi 共同撰写。
引言
在本文中,我们深入探讨 Transformer 架构中的解码器组件,重点关注其与编码器的异同。解码器的独特特征是其类似循环的迭代性质,这与编码器的线性处理形成对比。解码器的核心是两种改进的注意力机制:掩码多头注意力和编码器-解码器多头注意力。
解码器中的掩码多头注意力确保了标记的顺序处理,这是一种防止每个生成的标记受到后续标记影响的方法。这种掩码对于保持生成数据的顺序和连贯性非常重要。解码器的输出(来自掩码注意力)与编码器的输出之间的交互在编码器-解码器注意力中得到了突出。这一最后步骤为解码器的处理提供了输入上下文。
我们还将演示如何使用 Python 和 NumPy 实现这些概念。我们创建了一个简单的例子,将句子从英语翻译成葡萄牙语。这种实用方法将有助于说明解码器在 Transformer 模型中的内部工作原理,并提供对其在大型语言模型(LLMs)中作用的更清晰理解。
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/3e1fe7a79cd435fc3d647915f9849528.png
图 1:我们解码了 LLM 解码器(作者使用 DALL-E 制作)
如往常一样,代码可在我们的 GitHub 上找到。
一个大型的循环
在我们之前的 文章 中描述了 Transformer 架构中编码器的内部工作原理后,我们将看到下一个部分,即解码器部分。在比较 Transformer 的两个部分时,我们认为强调主要相似点和差异是有益的。注意力机制是两者的核心。具体来说,它在解码器中出现两个地方。它们与编码器中最简单的版本相比都有重要的改进:掩码多头注意力和编码器-解码器多头注意力。谈到差异,我们指出解码器的循环特性与线性编码器形成对比。解码器是一个大型的循环。
自注意力机制通过这些标记的向量表示 Q 和 K 的(缩放)内积来计算单词/标记之间的关系,这些标记在训练过程中捕捉了这些单词出现在同一句子中的概率。这些标量积形成了一个权重矩阵,该矩阵乘以相同标记的另一个表示 V。因此,向量 V 通过简单的矩阵乘法通过权重进行更新,这样 V 的堆叠中的每个向量都接收来自 Q 和 K 堆叠中每个向量的信息。这涵盖了来自语言学的核心思想,“通过其同伴了解一个单词”,归功于约翰·鲁珀特·费思。更正式地说,我们有:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/55f9e62f96e5b8ec0e93553b8a5061da.png
但现在,在解码器部分,我们希望算法每次只考虑已经生成的先前标记来创建一个标记。为了使这正常工作,我们需要禁止标记从句子的右侧获取信息。这是通过掩码权重矩阵来完成的,使其最终成为三角形形状,其对角线以上的所有成分都是零。因此,我们写出:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/79f78feb0725084e098505b6ef1df4d4.png
其中 M 是对角线以上的成分的掩码矩阵,在 softmax 指数化时产生零。向量堆叠 V 的更新方式是这样的,V 堆叠中的第一个只从自身获取信息,第二个从自身和前一个获取信息,依此类推。
注意力机制在解码器段发生的另一个实例是所谓的编码器-解码器注意力。基本区别在于 Q 向量是掩码注意力(在残差连接之后)的输出,而 K 和 V 向量来自编码器部分。
当我们考虑编码器和解码器部分的变压器之间的主要区别时,这会变得更加清晰,正如我们之前提到的:解码器的 while-loop 特性。
用户无法控制掩码注意力层的输入。相反,它由之前在其自己的循环中生成的向量构成,从特殊标记开始,以另一个特殊标记结束,该标记表示句子的结束:。这里有一个需要注意的问题。如果这个过程完全独立工作,它将完全没有来自用户的信息来源,因此翻译任务将变得不可能。因此,编码器-解码器注意力桥接了编码器中的嵌入 K 和 V,它携带了想要处理的原信息,以及解码器循环中生成的 Q 向量。
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/4d6d95c81a873a256f00df02b54f45bc.png
图 2:Transformers 架构(来源)
此外,值得注意的是,正是在这里可以捕捉到这些向量集名称的动机:循环中生成的 Q 向量就像真正的查询(因为它们找到了循环中已生成的那些标记的匹配项)一样,通过标量积与 K(键)向量配对,并产生一个结果,即更新的 V(值)向量。
跟随数字
在以下内容中,我们考虑我们在之前的 文章 中使用的相同句子,现在将解码器用于执行将葡萄牙语作为目标的翻译任务。
正如我们所见,编码器取句子“今天是星期天”的初始嵌入 X。这次我们使用一个 5 维的初始嵌入(原因很快就会清楚):
‘Today’ – (1,0,0,0,0)
‘is’ – (0,1,0,0,0)
‘sunday’ – (0,0,1,0,0)
来自我们词典中的总共 4 个标记,这些标记也有
‘saturday’ – (0,0,0,1,0)。
因此,编码器的输入是:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/df6b45ceca79a5558f9c998aea156dbd.png
X_e 通过矩阵乘法映射到 3 组向量,Q、K 和 V:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/53259a0781ea3e51785efe4440c760f1.png
其中 W 矩阵是随机初始化并在训练过程中调整的。我们还为编码器添加了下标e。多头注意力产生 V_e^{updated}。
对于解码器部分,我们保留向量堆栈和 V_e^{updated}:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/a079071f2b5044392cdc17986ab85602.png
正如我们提到的,解码器从我们选择的标记开始迭代工作,如下所示:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/74c3e047ab92af62eed50b5776fbd263.png
这将是掩码注意力后的输入:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/460e87d476dcb821fdb8d3c7914eae26.png
在掩码注意力领域,现在从 3 个其他矩阵 W^m 生成了其他 3 组向量,现在命名为 Q_m、K_m 和 V_m,这些矩阵由可学习的参数组成,随机初始化:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/463cb225e9a1e25d59a697af52534170.png
这导致 3 个向量:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/4cb97088e97976754ab21acdf29b9697.png
计算注意力得分矩阵。在这种情况下,它恰好是一个数字:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/9a6f0a4b344def5b0ba66133ada28e33.png
然后将其添加到掩码矩阵(这只是一个数字 0)中,然后通过除以 Q 和 K 的维度的平方根进行缩放(在这种情况下 d_k=5):
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/4fcd095578d8d538ed6eef35379bea3c.png
对于第一次迭代,应用 softmax 函数总是得到 1。这作为 V_m 向量的权重:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/1271e5965bdc2b988c365669a8dcbd3c.png
这种计算的结果是一个单一的向量 V_e^{updated},然后它经历残差连接:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/102c623c8c17e53cd3286be9fdc978d2.png
我们可以使用 Python 和仅使用 NumPy 以以下方式实现它:
classOne_Head_Masked_Attention:def__init__(self,d_model,d_k,d_v):self.d_model=d_model self.W_mat=W_matrices(self.d_model,d_k,d_v)defcompute_QKV(self,X):self.Q=np.matmul(X,self.W_mat.W_Q)self.K=np.matmul(X,self.W_mat.W_K)self.V=np.matmul(X,self.W_mat.W_V)defprint_QKV(self):print('Q : n',self.Q)print('K : n',self.K)print('V : n',self.V)defcompute_1_head_masked_attention(self):Attention_scores=np.matmul(self.Q,np.transpose(self.K))# print('Attention_scores before normalization : n', Attention_scores)ifAttention_scores.ndim>1:M=np.zeros(Attention_scores.shape)foriinrange(Attention_scores.shape[0]):forjinrange(i+1,Attention_scores.shape[1]):M[i,j]=-np.infelse:M=0Attention_scores+=M# print('Attention_scores after masking : n', Attention_scores)Attention_scores=Attention_scores/np.sqrt(self.d_model)# print('Attention scores after Renormalization: n ', Attention_scores)ifAttention_scores.ndim>2:Softmax_Attention_Matrix=np.apply_along_axis(lambdax:np.exp(x)/np.sum(np.exp(x)),1,Attention_scores)else:Softmax_Attention_Matrix=np.exp(Attention_scores)/np.sum(np.exp(Attention_scores))# print('result after softmax: n', Softmax_Attention_Matrix)ifAttention_scores.ndim>1:ifSoftmax_Attention_Matrix.shape[1]!=self.V.shape[0]:raiseValueError("Incompatible shapes!")result=np.matmul(Softmax_Attention_Matrix,self.V)else:result=Softmax_Attention_Matrix*self.V# result = np.matmul(Softmax_Attention_Matrix, self.V)# print('softmax result multiplied by V: n', result)returnresultdefbackpropagate(self):# do smth to update W_matpassclassMulti_Head_Masked_Attention:def__init__(self,n_heads,d_model,d_k,d_v):self.d_model=d_model self.n_heads=n_heads self.d_k=d_k self.d_v=d_v self.d_concat=self.d_v*self.n_heads self.W_0=np.random.uniform(-1,1,size=(self.d_concat,self.d_v))self.heads=[]i=0whilei<self.n_heads:self.heads.append(One_Head_Masked_Attention(d_model=d_model,d_k=d_k,d_v=d_v))i+=1defprint_W_0(self):print('W_0 : n',self.W_0)defprint_QKV_each_head(self):i=0whilei<self.n_heads:print(f'Head{i}: n')self.heads[i].print_QKV()i+=1defprint_W_matrices_each_head(self):i=0whilei<self.n_heads:print(f'Head{i}: n')self.heads[i].W_mat.print_W_matrices()i+=1defcompute(self,X):self.heads_results=[]forheadinself.heads:head.compute_QKV(X)self.heads_results.append(head.compute_1_head_masked_attention())ifX.ndim>1:multi_head_results=np.concatenate(self.heads_results,axis=1)V_updated=np.matmul(multi_head_results,self.W_0)else:multi_head_results=np.concatenate(self.heads_results,axis=0)print('Dimension of multihead_results:',multi_head_results.shape)print('Dimension of W_0:',self.W_0.shape)V_updated=np.matmul(multi_head_results,self.W_0)returnV_updateddefback_propagate(self):# backpropagate W_0# call _backprop for each headpass现在,我们准备好处理下一部分,编码器-解码器注意力:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/62130a3f6616fcb14f88d73ac786b6d6.png
再次,我们开始计算注意力分数,现在形成一个 1×3 的矩阵:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/889f4edd4a27febbebff5f1c2a44e7d6.png
我们应用缩放和 softmax:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/587ee52f5c1be83ae3fca97d1d19b2c3.png
最后,我们得到更新的 V 向量 V*:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/2a80d3d12762a5136c62d233228a3997.png
再次,我们可以使用 NumPy 实现上述操作:
classOne_Head_Encoder_Decoder_Attention:def__init__(self,d_k):self.d_k=d_kdefprint_QKV(self):print('Q : n',self.Q)print('K : n',self.K)print('V : n',self.V)defcompute_1_head_attention(self,Q,K,V):self.Q=Q#from masked attention in decoderself.K=K#from encoderself.V=V#final result from encoderAttention_scores=np.matmul(self.Q,np.transpose(self.K))# print('Attention_scores before normalization : n', Attention_scores)Attention_scores=Attention_scores/np.sqrt(self.d_k)# print('Attention scores after Renormalization: n ', Attention_scores)Softmax_Attention_Matrix=np.exp(Attention_scores-np.max(Attention_scores,axis=-1,keepdims=True))Softmax_Attention_Matrix/=np.sum(Softmax_Attention_Matrix,axis=-1,keepdims=True)# print('result after softmax: n', Softmax_Attention_Matrix)ifSoftmax_Attention_Matrix.ndim>1:ifSoftmax_Attention_Matrix.shape[1]!=self.V.shape[0]:raiseValueError("Incompatible shapes!")result=np.matmul(Softmax_Attention_Matrix,self.V)returnresult这最后一步提供了与句子“Today is Sunday”中的语义关系相关的信息。结果 V*经过另一个残差连接实例。
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/62938cc5f11372bf33b054910e683981.png
下一个部分是最终的馈送前网络(FFN)。原始论文由 2 个可学习参数的矩阵组成。随后是另一个 softmax 实例,它将映射到一个概率分布空间,在该空间中,通过 one-hot 编码识别葡萄牙语中的标记。这意味着,如果 softmax(FFN(RC))的结果是
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/840004986df14f3b32985385c2faba05.png
然后,相关的标记将是:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/615e2252598eec3bdf0a905643fd3532.png
使用葡萄牙语词汇表,我们会得到:
vocabulary={'Hoje':np.array([1,0,0,0,0]),'é':np.array([0,1,0,0,0]),'domingo':np.array([0,0,1,0,0]),'sábado':np.array([0,0,0,1,0]),'EOS':np.array([0,0,0,0,1]),'START':np.array([0.2,0.2,0.2,0.2,0.2])}因此,我们的标记如下:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/c9544e9b8f2f54425ad3cb30c19af677.png
考虑到我们例子中的句子,预期的结果将是“Hoje”(“Today”的葡萄牙语翻译)。回想一下,我们还没有训练我们的模型(没有反向传播)。
这是解码循环的结束。现在,输出被用来再次喂入循环,将之前的 X = 与与“sábado”相关的向量(例如(0,0,0,1,0))添加到葡萄牙语嵌入空间中。
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/bf184fbc40084a9a63012d0522a6ac56.png
注意,在下一轮将生成两个标记而不是一个。这个数字在每个迭代中加一,模型继续将最后生成的标记添加到前面,直到识别出标记。
放大掩码注意力层
为了更好地理解掩码注意力,观察当矩阵大小略大于一个数字时发生的情况是有益的。我们考虑第三步,考虑到在前两步中,以下标记已被生成:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/db7cc5fdd00fd0666a935f782900fc13.png
这意味着现在,掩码注意力部分的输入是由它们堆叠而成的,较旧的在堆栈顶部,较新的在底部(因为我们选择在掩码矩阵的上三角使用-inf),从标记开始:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/a88711d72990421141139d7eb978c5bf.png
这通过位置编码,
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/5af8d515f9e253bc98a0a6d12a94d964.png
并生成 Q、K 和 V 堆栈:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/5025d7c7e1655a0ed31737def2058783.png
注意力分数矩阵是:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/a31951345d38eed416c1805f765c961b.png
并且掩码 M 矩阵是:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/a31951345d38eed416c1805f765c961b.png
它们的总和是:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/d5a780310a0accfb52c12384f7d4280e.png
在缩放后,它采取以下形式:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/78c11be8b3124e4cb0d1e364ca086c38.png
现在它通过 softmax:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/cc97f9c7ee8384cff1cb8512ebdcebcd.png
我们最终将其乘以 V 向量以获得 V³_updated:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/1192ade3fed45c47155212726d61fc41.png
在这里,我们可以看到第一个向量/行没有变化。回想一下,这是由于将 softmax(Scaled_Masked_Attention_Scores)³的第一行乘以 V 堆栈中的所有列的结果。但由于除了第一个分量(其值为 1)之外的所有分量都是 0,因此堆栈中的这个第一个向量只获得来自自身的贡献,并且乘以一个等于 1 的权重。
第二个更新的向量从第一个和第二个向量中获得贡献,其权重之和为 1。类似地,第三个更新的 V 向量从 V 堆栈中的自身和前两个向量中获得加权贡献。
我们强调在掩码注意力缩放分数的每一行单独应用 softmax 函数的重要性,以正确分配权重。
我们可以将完整的过程运行 10 次迭代,或者直到我们找到一个 EOS 标记:
ENDCOLOR='