原文towardsdatascience.com/transformers-how-do-they-transform-your-data-72d69e383e0d?sourcecollection_archive---------0-----------------------#2024-03-28深入探索 Transformer 架构及其在语言任务中无敌的原因https://medium.com/maxwolf34?sourcepost_page---byline--72d69e383e0d--------------------------------https://towardsdatascience.com/?sourcepost_page---byline--72d69e383e0d-------------------------------- Maxime Wolf·发表于Towards Data Science ·阅读时间 11 分钟·2024 年 3 月 28 日–https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/4dd0f4cd464558d94b73f8274580ac94.png作者提供的图片在人工智能和机器学习飞速发展的今天有一种创新脱颖而出对我们处理、理解和生成数据的方式产生了深远影响Transformer。Transformer 彻底改变了自然语言处理NLP及其他领域为今天一些最先进的 AI 应用提供了动力。但究竟什么是 Transformer它们又是如何以如此突破性的方式转换数据的呢本文将揭秘 Transformer 模型的内部工作原理重点讲解编码器架构。我们将从 Python 中 Transformer 编码器的实现入手逐步解析其主要组件。然后我们将可视化 Transformer 如何在训练过程中处理并适应输入数据。虽然本博客并未涵盖所有架构细节但它提供了一个实现并帮助你全面理解 Transformer 的变革性力量。想深入了解 Transformer 的工作原理我建议你参考斯坦福大学的优秀 CS224-n 课程。我还建议关注与本文相关的GitHub 仓库以获取更多细节。什么是 Transformer 编码器架构https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/df39c5bc0e96c04388b637bb391a7fed.png来自Attention Is All You Need的 Transformer 模型这张图片展示了原始 Transformer 架构将编码器和解码器结合用于序列到序列的语言任务。在本文中我们将重点介绍编码器架构图中的红色块。这正是流行的 BERT 模型在后台使用的架构其主要关注的是理解和表示数据而不是生成序列。它可以用于多种应用文本分类、命名实体识别NER、抽取式问答等。那么这些数据究竟是如何通过该架构进行转换的呢我们将详细解释每个组件但这里是过程的概述。输入文本被标记化Python 字符串被转换成标记数字列表。每个标记都通过一个嵌入层该层输出每个标记的向量表示。然后这些嵌入会通过位置编码层进一步编码添加每个标记在序列中的位置信息。这些新的嵌入通过一系列编码器层进行转换使用自注意力机制。可以添加一个任务特定的头。例如我们稍后将使用一个分类头将电影评论分类为正面或负面。重要的是要理解Transformer 架构通过将嵌入向量从高维空间中的一个表示映射到同一空间中的另一个表示应用一系列复杂的变换来转换这些嵌入。在 Python 中实现编码器架构位置编码器层与 RNN 模型不同自注意力机制不利用输入序列的顺序。PositionalEncoder 类通过使用两种数学函数余弦和正弦向输入嵌入添加位置编码。https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/578ef5784eef3551c213f00d3bfdbdd4.png位置编码矩阵定义来自Attention Is All You Need注意位置编码不包含可训练的参数它们是确定性计算的结果这使得该方法非常可处理。此外正弦和余弦函数的值介于-1 和 1 之间并具有有助于模型学习单词相对位置的有用周期性特性。classPositionalEncoder(nn.Module):def__init__(self,d_model,max_length):super(PositionalEncoder,self).__init__()self.d_modeld_model self.max_lengthmax_length# Initialize the positional encoding matrixpetorch.zeros(max_length,d_model)positiontorch.arange(0,max_length,dtypetorch.float).unsqueeze(1)div_termtorch.exp(torch.arange(0,d_model,2,dtypetorch.float)*-(math.log(10000.0)/d_model))# Calculate and assign position encodings to the matrixpe[:,0::2]torch.sin(position*div_term)pe[:,1::2]torch.cos(position*div_term)self.pepe.unsqueeze(0)defforward(self,x):xxself.pe[:,:x.size(1)]# update embeddingsreturnx多头自注意力自注意力机制是编码器架构的关键组件。我们暂时忽略“多头”部分。注意力是一种方法用来确定每个标记即每个嵌入与所有其他嵌入与该标记的相关性从而获得更精细和与上下文相关的编码。https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/cd6d84be3aae29b2a4108ac244d7dd6b.png“它”是如何关注序列中其他单词的(The Illustrated Transformer)自注意力机制有三个步骤。使用矩阵 Q、K 和 V 分别转换输入的“查询”、“键”和“值”。请注意对于自注意力机制查询、键和值都是等于我们的输入嵌入。通过余弦相似度点积计算查询和键之间的注意力得分。得分会通过嵌入维度的平方根进行缩放以稳定训练过程中的梯度。使用 softmax 层将这些得分转化为概率。输出是值的加权平均使用注意力得分作为权重。从数学角度来看这对应于以下公式。https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/c5773d753ad9db7bab34c7e7739de18c.png注意力机制来自《Attention Is All You Need》“多头”是什么意思基本上我们可以并行多次应用上述自注意力机制过程并将输出进行拼接和投影。这使得每个头可以专注于句子的不同语义方面。我们首先定义头的数量、嵌入的维度d_model以及每个头的维度head_dim。我们还初始化了 Q、K 和 V 矩阵线性层以及最终的投影层。classMultiHeadAttention(nn.Module):def__init__(self,d_model,num_heads):super(MultiHeadAttention,self).__init__()self.num_headsnum_heads self.d_modeld_model self.head_dimd_model//num_heads self.query_linearnn.Linear(d_model,d_model)self.key_linearnn.Linear(d_model,d_model)self.value_linearnn.Linear(d_model,d_model)self.output_linearnn.Linear(d_model,d_model)使用多头注意力时我们对每个注意力头应用一个减少维度的处理使用 head_dim 而非 d_model就像原论文中所述这使得总的计算成本类似于一个全维度的单头注意力层。请注意这只是一个逻辑上的拆分。多头注意力之所以强大是因为它仍然可以通过单一的矩阵操作来表示从而使得 GPU 上的计算非常高效。defsplit_heads(self,x,batch_size):# Split the sequence embeddings in x across the attention headsxx.view(batch_size,-1,self.num_heads,self.head_dim)returnx.permute(0,2,1,3).contiguous().view(batch_size*self.num_heads,-1,self.head_dim)我们计算注意力得分并使用掩码避免在填充的标记上使用注意力。我们应用 softmax 激活函数将这些得分转化为概率。defcompute_attention(self,query,key,maskNone):# Compute dot-product attention scores# dimensions of query and key are (batch_size * num_heads, seq_length, head_dim)scoresquery key.transpose(-2,-1)/math.sqrt(self.head_dim)# Now, dimensions of scores is (batch_size * num_heads, seq_length, seq_length)ifmaskisnotNone:scoresscores.view(-1,scores.shape[0]//self.num_heads,mask.shape[1],mask.shape[2])# for compatibilityscoresscores.masked_fill(mask0,float(-1e20))# mask to avoid attention on padding tokensscoresscores.view(-1,mask.shape[1],mask.shape[2])# reshape back to original shape# Normalize attention scores into attention weightsattention_weightsF.softmax(scores,dim-1)returnattention_weightsforward属性执行多头逻辑拆分并计算注意力权重。然后我们通过将这些权重与值相乘来获得输出。最后我们重新调整输出的形状并通过线性层进行投影。defforward(self,query,key,value,maskNone):batch_sizequery.size(0)queryself.split_heads(self.query_linear(query),batch_size)keyself.split_heads(self.key_linear(key),batch_size)valueself.split_heads(self.value_linear(value),batch_size)attention_weightsself.compute_attention(query,key,mask)# Multiply attention weights by values, concatenate and linearly project outputsoutputtorch.matmul(attention_weights,value)outputoutput.view(batch_size,self.num_heads,-1,self.head_dim).permute(0,2,1,3).contiguous().view(batch_size,-1,self.d_model)returnself.output_linear(output)编码器层这是该架构的主要组件它利用了多头自注意力。我们首先实现一个简单的类通过 2 个全连接层执行前向操作。classFeedForwardSubLayer(nn.Module):def__init__(self,d_model,d_ff):super(FeedForwardSubLayer,self).__init__()self.fc1nn.Linear(d_model,d_ff)self.fc2nn.Linear(d_ff,d_model)self.relunn.ReLU()defforward(self,x):returnself.fc2(self.relu(self.fc1(x)))现在我们可以编写编码器层的逻辑。我们首先对输入应用自注意力得到一个相同维度的向量。然后我们使用带有层归一化层的小型前馈网络。请注意我们在应用归一化之前还使用了跳跃连接。classEncoderLayer(nn.Module):def__init__(self,d_model,num_heads,d_ff,dropout):super(EncoderLayer,self).__init__()self.self_attnMultiHeadAttention(d_model,num_heads)self.feed_forwardFeedForwardSubLayer(d_model,d_ff)self.norm1nn.LayerNorm(d_model)self.norm2nn.LayerNorm(d_model)self.dropoutnn.Dropout(dropout)defforward(self,x,mask):attn_outputself.self_attn(x,x,x,mask)xself.norm1(xself.dropout(attn_output))# skip connection and normalizationff_outputself.feed_forward(x)returnself.norm2(xself.dropout(ff_output))# skip connection and normalization将一切整合在一起现在是时候创建我们的最终模型了。我们通过嵌入层将数据传递进去。这将原始标记整数转换为数值向量。然后我们应用位置编码器和若干num_layers编码器层。classTransformerEncoder(nn.Module):def__init__(self,vocab_size,d_model,num_layers,num_heads,d_ff,dropout,max_sequence_length):super(TransformerEncoder,self).__init__()self.embeddingnn.Embedding(vocab_size,d_model)self.positional_encodingPositionalEncoder(d_model,max_sequence_length)self.layersnn.ModuleList([EncoderLayer(d_model,num_heads,d_ff,dropout)for_inrange(num_layers)])defforward(self,x,mask):xself.embedding(x)xself.positional_encoding(x)forlayerinself.layers:xlayer(x,mask)returnx我们还创建了一个ClassifierHead类用于将最终的嵌入转换为分类任务的类别概率。classClassifierHead(nn.Module):def__init__(self,d_model,num_classes):super(ClassifierHead,self).__init__()self.fcnn.Linear(d_model,num_classes)defforward(self,x):logitsself.fc(x[:,0,:])# first token corresponds to the classification tokenreturnF.softmax(logits,dim-1)注意稠密层和 softmax 层只应用于第一个嵌入对应于输入序列的第一个标记。这是因为在分词文本时第一个标记是[CLS]标记代表“分类”。[CLS]标记的设计目的是将整个序列的信息聚合成一个单一的嵌入向量作为可以用于分类任务的摘要表示。注意包含[CLS]标记的概念源自 BERTBERT 最初是在类似下一句预测任务上进行训练的。[CLS]标记被插入以预测句子 B 是否跟随句子 A而[SEP]标记则分隔这两个句子。对于我们的模型[SEP]标记只是标记输入句子的结束如下所示。https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/d3feb19e839643803e928bfc05828ad2.pngBERT 架构中的[CLS]标记 (All About AI)当你仔细想想真是令人震惊这个单一的[CLS]嵌入能够捕获如此多关于整个序列的信息这要归功于自注意力机制能够权衡和综合每个文本片段之间相互关系的重要性。训练与可视化希望上一节能帮助你更好地理解我们的 Transformer 模型是如何转换输入数据的。接下来我们将编写我们的训练流程用于处理 IMDB 数据集电影评论的二分类任务。然后我们将可视化训练过程中[CLS]标记的嵌入看看我们的模型是如何转换它的。我们首先定义超参数并且使用 BERT 分词器。在 GitHub 仓库中你可以看到我还编写了一个函数来选择数据集的一个子集其中包含 1200 个训练样本和 200 个测试样本。num_classes2# binary classificationd_model256# dimension of the embedding vectorsnum_heads4# number of heads for self-attentionnum_layers4# number of encoder layersd_ff512\.# dimension of the dense layers in the encoder layerssequence_length256# maximum sequence lengthdropout0.4# dropout to avoid overfittingnum_epochs20batch_size32loss_functiontorch.nn.CrossEntropyLoss()datasetload_dataset(imdb)datasetbalance_and_create_dataset(dataset,1200,200)# check GitHub repotokenizerAutoTokenizer.from_pretrained(bert-base-uncased,model_max_lengthsequence_length)你可以尝试在其中一个句子上使用 BERT 分词器print(tokenized_datasets[train][input_ids][0])每个序列应以 101 号标记开始对应[CLS]接着是一些非零整数如果序列长度小于 256则用零填充。注意这些零在使用我们的“掩码”进行自注意力计算时会被忽略。tokenized_datasetsdataset.map(encode_examples,batchedTrue)tokenized_datasets.set_format(typetorch,columns[input_ids,attention_mask,label])train_dataloaderDataLoader(tokenized_datasets[train],batch_sizebatch_size,shuffleTrue)test_dataloaderDataLoader(tokenized_datasets[test],batch_sizebatch_size,shuffleTrue)vocab_sizetokenizer.vocab_size encoderTransformerEncoder(vocab_size,d_model,num_layers,num_heads,d_ff,dropout,max_sequence_lengthsequence_length)classifierClassifierHead(d_model,num_classes)optimizertorch.optim.Adam(list(encoder.parameters())list(classifier.parameters()),lr1e-4)现在我们可以编写训练函数deftrain(dataloader,encoder,classifier,optimizer,loss_function,num_epochs):forepochinrange(num_epochs):# Collect and store embeddings before each epoch starts for visualization purposes (check repo)all_embeddings,all_labelscollect_embeddings(encoder,dataloader)reduced_embeddingsvisualize_embeddings(all_embeddings,all_labels,epoch,showFalse)dic_embeddings[epoch][reduced_embeddings,all_labels]encoder.train()classifier.train()correct_predictions0total_predictions0forbatchintqdm(dataloader,descTraining):input_idsbatch[input_ids]attention_maskbatch[attention_mask]# indicate where padded tokens are# These 2 lines make the attention_mask a matrix instead of a vectorattention_maskattention_mask.unsqueeze(-1)attention_maskattention_maskattention_mask.transpose(1,2)labelsbatch[label]optimizer.zero_grad()outputencoder(input_ids,attention_mask)classificationclassifier(output)lossloss_function(classification,labels)loss.backward()optimizer.step()predstorch.argmax(classification,dim1)correct_predictionstorch.sum(predslabels).item()total_predictionslabels.size(0)epoch_accuracycorrect_predictions/total_predictionsprint(fEpoch{epoch}Training Accuracy:{epoch_accuracy:.4f})你可以在 GitHub 仓库中找到 collect_embeddings 和 visualize_embeddings 函数。它们存储了训练集每个句子的[CLS]标记嵌入应用一种叫做 t-SNE 的降维技术将其转化为二维向量而不是 256 维向量并保存动画图表。让我们来可视化结果。https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/23987d3afe279c76504d4c31a47e8066.png每个训练点的投影[CLS]嵌入蓝色代表正向句子红色代表负向句子通过观察每个训练点的[CLS]嵌入的投影图我们可以看到经过若干轮训练后正向蓝色和负向红色句子之间的明显区别。这一可视化展示了 Transformer 架构随着时间推移调整嵌入的显著能力突出了自注意力机制的强大功能。数据以一种方式进行转化使得每个类别的嵌入得到了良好的分离从而大大简化了分类器头的任务。结论随着我们对 Transformer 架构的探索的结束显然这些模型擅长将数据定制化以适应特定任务。通过使用位置编码和多头自注意力机制Transformer 不仅仅是处理数据它们以一种前所未见的复杂程度来解释和理解信息。能够动态地权衡输入数据不同部分的相关性使得对输入文本的理解和表示更加细致。这提升了在各种下游任务中的表现包括文本分类、问答、命名实体识别等。现在你已经更好地理解了编码器架构你可以深入探讨解码器和编码器-解码器模型这些模型与我们刚刚探讨的非常相似。解码器在生成任务中起着至关重要的作用是流行的 GPT 模型的核心部分。随时在LinkedIn上与我联系请在GitHub上关注我获取更多内容访问我的网站: maximewolf.com参考文献[1] Vaswani, Ashish, 等人. “Attention Is All You Need.”第 31 届神经信息处理系统会议NIPS 2017, 美国加利福尼亚州长滩.[2] “The Illustrated Transformer.”Jay Alammar 的博客, 2018 年 6 月,jalammar.github.io/illustrated-transformer/[3] Transformer 架构的官方 PyTorch 实现.GitHub 代码库, PyTorch,github.com/pytorch/pytorch/blob/master/torch/nn/modules/transformer.py[4] Manning, Christopher, 等人. “CS224n: 使用深度学习进行自然语言处理.”斯坦福大学, 斯坦福 CS224N NLP 课程,web.stanford.edu/class/cs224n/
Transformer:它们如何转化你的数据?
发布时间:2026/6/4 5:38:07
原文towardsdatascience.com/transformers-how-do-they-transform-your-data-72d69e383e0d?sourcecollection_archive---------0-----------------------#2024-03-28深入探索 Transformer 架构及其在语言任务中无敌的原因https://medium.com/maxwolf34?sourcepost_page---byline--72d69e383e0d--------------------------------https://towardsdatascience.com/?sourcepost_page---byline--72d69e383e0d-------------------------------- Maxime Wolf·发表于Towards Data Science ·阅读时间 11 分钟·2024 年 3 月 28 日–https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/4dd0f4cd464558d94b73f8274580ac94.png作者提供的图片在人工智能和机器学习飞速发展的今天有一种创新脱颖而出对我们处理、理解和生成数据的方式产生了深远影响Transformer。Transformer 彻底改变了自然语言处理NLP及其他领域为今天一些最先进的 AI 应用提供了动力。但究竟什么是 Transformer它们又是如何以如此突破性的方式转换数据的呢本文将揭秘 Transformer 模型的内部工作原理重点讲解编码器架构。我们将从 Python 中 Transformer 编码器的实现入手逐步解析其主要组件。然后我们将可视化 Transformer 如何在训练过程中处理并适应输入数据。虽然本博客并未涵盖所有架构细节但它提供了一个实现并帮助你全面理解 Transformer 的变革性力量。想深入了解 Transformer 的工作原理我建议你参考斯坦福大学的优秀 CS224-n 课程。我还建议关注与本文相关的GitHub 仓库以获取更多细节。什么是 Transformer 编码器架构https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/df39c5bc0e96c04388b637bb391a7fed.png来自Attention Is All You Need的 Transformer 模型这张图片展示了原始 Transformer 架构将编码器和解码器结合用于序列到序列的语言任务。在本文中我们将重点介绍编码器架构图中的红色块。这正是流行的 BERT 模型在后台使用的架构其主要关注的是理解和表示数据而不是生成序列。它可以用于多种应用文本分类、命名实体识别NER、抽取式问答等。那么这些数据究竟是如何通过该架构进行转换的呢我们将详细解释每个组件但这里是过程的概述。输入文本被标记化Python 字符串被转换成标记数字列表。每个标记都通过一个嵌入层该层输出每个标记的向量表示。然后这些嵌入会通过位置编码层进一步编码添加每个标记在序列中的位置信息。这些新的嵌入通过一系列编码器层进行转换使用自注意力机制。可以添加一个任务特定的头。例如我们稍后将使用一个分类头将电影评论分类为正面或负面。重要的是要理解Transformer 架构通过将嵌入向量从高维空间中的一个表示映射到同一空间中的另一个表示应用一系列复杂的变换来转换这些嵌入。在 Python 中实现编码器架构位置编码器层与 RNN 模型不同自注意力机制不利用输入序列的顺序。PositionalEncoder 类通过使用两种数学函数余弦和正弦向输入嵌入添加位置编码。https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/578ef5784eef3551c213f00d3bfdbdd4.png位置编码矩阵定义来自Attention Is All You Need注意位置编码不包含可训练的参数它们是确定性计算的结果这使得该方法非常可处理。此外正弦和余弦函数的值介于-1 和 1 之间并具有有助于模型学习单词相对位置的有用周期性特性。classPositionalEncoder(nn.Module):def__init__(self,d_model,max_length):super(PositionalEncoder,self).__init__()self.d_modeld_model self.max_lengthmax_length# Initialize the positional encoding matrixpetorch.zeros(max_length,d_model)positiontorch.arange(0,max_length,dtypetorch.float).unsqueeze(1)div_termtorch.exp(torch.arange(0,d_model,2,dtypetorch.float)*-(math.log(10000.0)/d_model))# Calculate and assign position encodings to the matrixpe[:,0::2]torch.sin(position*div_term)pe[:,1::2]torch.cos(position*div_term)self.pepe.unsqueeze(0)defforward(self,x):xxself.pe[:,:x.size(1)]# update embeddingsreturnx多头自注意力自注意力机制是编码器架构的关键组件。我们暂时忽略“多头”部分。注意力是一种方法用来确定每个标记即每个嵌入与所有其他嵌入与该标记的相关性从而获得更精细和与上下文相关的编码。https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/cd6d84be3aae29b2a4108ac244d7dd6b.png“它”是如何关注序列中其他单词的(The Illustrated Transformer)自注意力机制有三个步骤。使用矩阵 Q、K 和 V 分别转换输入的“查询”、“键”和“值”。请注意对于自注意力机制查询、键和值都是等于我们的输入嵌入。通过余弦相似度点积计算查询和键之间的注意力得分。得分会通过嵌入维度的平方根进行缩放以稳定训练过程中的梯度。使用 softmax 层将这些得分转化为概率。输出是值的加权平均使用注意力得分作为权重。从数学角度来看这对应于以下公式。https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/c5773d753ad9db7bab34c7e7739de18c.png注意力机制来自《Attention Is All You Need》“多头”是什么意思基本上我们可以并行多次应用上述自注意力机制过程并将输出进行拼接和投影。这使得每个头可以专注于句子的不同语义方面。我们首先定义头的数量、嵌入的维度d_model以及每个头的维度head_dim。我们还初始化了 Q、K 和 V 矩阵线性层以及最终的投影层。classMultiHeadAttention(nn.Module):def__init__(self,d_model,num_heads):super(MultiHeadAttention,self).__init__()self.num_headsnum_heads self.d_modeld_model self.head_dimd_model//num_heads self.query_linearnn.Linear(d_model,d_model)self.key_linearnn.Linear(d_model,d_model)self.value_linearnn.Linear(d_model,d_model)self.output_linearnn.Linear(d_model,d_model)使用多头注意力时我们对每个注意力头应用一个减少维度的处理使用 head_dim 而非 d_model就像原论文中所述这使得总的计算成本类似于一个全维度的单头注意力层。请注意这只是一个逻辑上的拆分。多头注意力之所以强大是因为它仍然可以通过单一的矩阵操作来表示从而使得 GPU 上的计算非常高效。defsplit_heads(self,x,batch_size):# Split the sequence embeddings in x across the attention headsxx.view(batch_size,-1,self.num_heads,self.head_dim)returnx.permute(0,2,1,3).contiguous().view(batch_size*self.num_heads,-1,self.head_dim)我们计算注意力得分并使用掩码避免在填充的标记上使用注意力。我们应用 softmax 激活函数将这些得分转化为概率。defcompute_attention(self,query,key,maskNone):# Compute dot-product attention scores# dimensions of query and key are (batch_size * num_heads, seq_length, head_dim)scoresquery key.transpose(-2,-1)/math.sqrt(self.head_dim)# Now, dimensions of scores is (batch_size * num_heads, seq_length, seq_length)ifmaskisnotNone:scoresscores.view(-1,scores.shape[0]//self.num_heads,mask.shape[1],mask.shape[2])# for compatibilityscoresscores.masked_fill(mask0,float(-1e20))# mask to avoid attention on padding tokensscoresscores.view(-1,mask.shape[1],mask.shape[2])# reshape back to original shape# Normalize attention scores into attention weightsattention_weightsF.softmax(scores,dim-1)returnattention_weightsforward属性执行多头逻辑拆分并计算注意力权重。然后我们通过将这些权重与值相乘来获得输出。最后我们重新调整输出的形状并通过线性层进行投影。defforward(self,query,key,value,maskNone):batch_sizequery.size(0)queryself.split_heads(self.query_linear(query),batch_size)keyself.split_heads(self.key_linear(key),batch_size)valueself.split_heads(self.value_linear(value),batch_size)attention_weightsself.compute_attention(query,key,mask)# Multiply attention weights by values, concatenate and linearly project outputsoutputtorch.matmul(attention_weights,value)outputoutput.view(batch_size,self.num_heads,-1,self.head_dim).permute(0,2,1,3).contiguous().view(batch_size,-1,self.d_model)returnself.output_linear(output)编码器层这是该架构的主要组件它利用了多头自注意力。我们首先实现一个简单的类通过 2 个全连接层执行前向操作。classFeedForwardSubLayer(nn.Module):def__init__(self,d_model,d_ff):super(FeedForwardSubLayer,self).__init__()self.fc1nn.Linear(d_model,d_ff)self.fc2nn.Linear(d_ff,d_model)self.relunn.ReLU()defforward(self,x):returnself.fc2(self.relu(self.fc1(x)))现在我们可以编写编码器层的逻辑。我们首先对输入应用自注意力得到一个相同维度的向量。然后我们使用带有层归一化层的小型前馈网络。请注意我们在应用归一化之前还使用了跳跃连接。classEncoderLayer(nn.Module):def__init__(self,d_model,num_heads,d_ff,dropout):super(EncoderLayer,self).__init__()self.self_attnMultiHeadAttention(d_model,num_heads)self.feed_forwardFeedForwardSubLayer(d_model,d_ff)self.norm1nn.LayerNorm(d_model)self.norm2nn.LayerNorm(d_model)self.dropoutnn.Dropout(dropout)defforward(self,x,mask):attn_outputself.self_attn(x,x,x,mask)xself.norm1(xself.dropout(attn_output))# skip connection and normalizationff_outputself.feed_forward(x)returnself.norm2(xself.dropout(ff_output))# skip connection and normalization将一切整合在一起现在是时候创建我们的最终模型了。我们通过嵌入层将数据传递进去。这将原始标记整数转换为数值向量。然后我们应用位置编码器和若干num_layers编码器层。classTransformerEncoder(nn.Module):def__init__(self,vocab_size,d_model,num_layers,num_heads,d_ff,dropout,max_sequence_length):super(TransformerEncoder,self).__init__()self.embeddingnn.Embedding(vocab_size,d_model)self.positional_encodingPositionalEncoder(d_model,max_sequence_length)self.layersnn.ModuleList([EncoderLayer(d_model,num_heads,d_ff,dropout)for_inrange(num_layers)])defforward(self,x,mask):xself.embedding(x)xself.positional_encoding(x)forlayerinself.layers:xlayer(x,mask)returnx我们还创建了一个ClassifierHead类用于将最终的嵌入转换为分类任务的类别概率。classClassifierHead(nn.Module):def__init__(self,d_model,num_classes):super(ClassifierHead,self).__init__()self.fcnn.Linear(d_model,num_classes)defforward(self,x):logitsself.fc(x[:,0,:])# first token corresponds to the classification tokenreturnF.softmax(logits,dim-1)注意稠密层和 softmax 层只应用于第一个嵌入对应于输入序列的第一个标记。这是因为在分词文本时第一个标记是[CLS]标记代表“分类”。[CLS]标记的设计目的是将整个序列的信息聚合成一个单一的嵌入向量作为可以用于分类任务的摘要表示。注意包含[CLS]标记的概念源自 BERTBERT 最初是在类似下一句预测任务上进行训练的。[CLS]标记被插入以预测句子 B 是否跟随句子 A而[SEP]标记则分隔这两个句子。对于我们的模型[SEP]标记只是标记输入句子的结束如下所示。https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/d3feb19e839643803e928bfc05828ad2.pngBERT 架构中的[CLS]标记 (All About AI)当你仔细想想真是令人震惊这个单一的[CLS]嵌入能够捕获如此多关于整个序列的信息这要归功于自注意力机制能够权衡和综合每个文本片段之间相互关系的重要性。训练与可视化希望上一节能帮助你更好地理解我们的 Transformer 模型是如何转换输入数据的。接下来我们将编写我们的训练流程用于处理 IMDB 数据集电影评论的二分类任务。然后我们将可视化训练过程中[CLS]标记的嵌入看看我们的模型是如何转换它的。我们首先定义超参数并且使用 BERT 分词器。在 GitHub 仓库中你可以看到我还编写了一个函数来选择数据集的一个子集其中包含 1200 个训练样本和 200 个测试样本。num_classes2# binary classificationd_model256# dimension of the embedding vectorsnum_heads4# number of heads for self-attentionnum_layers4# number of encoder layersd_ff512\.# dimension of the dense layers in the encoder layerssequence_length256# maximum sequence lengthdropout0.4# dropout to avoid overfittingnum_epochs20batch_size32loss_functiontorch.nn.CrossEntropyLoss()datasetload_dataset(imdb)datasetbalance_and_create_dataset(dataset,1200,200)# check GitHub repotokenizerAutoTokenizer.from_pretrained(bert-base-uncased,model_max_lengthsequence_length)你可以尝试在其中一个句子上使用 BERT 分词器print(tokenized_datasets[train][input_ids][0])每个序列应以 101 号标记开始对应[CLS]接着是一些非零整数如果序列长度小于 256则用零填充。注意这些零在使用我们的“掩码”进行自注意力计算时会被忽略。tokenized_datasetsdataset.map(encode_examples,batchedTrue)tokenized_datasets.set_format(typetorch,columns[input_ids,attention_mask,label])train_dataloaderDataLoader(tokenized_datasets[train],batch_sizebatch_size,shuffleTrue)test_dataloaderDataLoader(tokenized_datasets[test],batch_sizebatch_size,shuffleTrue)vocab_sizetokenizer.vocab_size encoderTransformerEncoder(vocab_size,d_model,num_layers,num_heads,d_ff,dropout,max_sequence_lengthsequence_length)classifierClassifierHead(d_model,num_classes)optimizertorch.optim.Adam(list(encoder.parameters())list(classifier.parameters()),lr1e-4)现在我们可以编写训练函数deftrain(dataloader,encoder,classifier,optimizer,loss_function,num_epochs):forepochinrange(num_epochs):# Collect and store embeddings before each epoch starts for visualization purposes (check repo)all_embeddings,all_labelscollect_embeddings(encoder,dataloader)reduced_embeddingsvisualize_embeddings(all_embeddings,all_labels,epoch,showFalse)dic_embeddings[epoch][reduced_embeddings,all_labels]encoder.train()classifier.train()correct_predictions0total_predictions0forbatchintqdm(dataloader,descTraining):input_idsbatch[input_ids]attention_maskbatch[attention_mask]# indicate where padded tokens are# These 2 lines make the attention_mask a matrix instead of a vectorattention_maskattention_mask.unsqueeze(-1)attention_maskattention_maskattention_mask.transpose(1,2)labelsbatch[label]optimizer.zero_grad()outputencoder(input_ids,attention_mask)classificationclassifier(output)lossloss_function(classification,labels)loss.backward()optimizer.step()predstorch.argmax(classification,dim1)correct_predictionstorch.sum(predslabels).item()total_predictionslabels.size(0)epoch_accuracycorrect_predictions/total_predictionsprint(fEpoch{epoch}Training Accuracy:{epoch_accuracy:.4f})你可以在 GitHub 仓库中找到 collect_embeddings 和 visualize_embeddings 函数。它们存储了训练集每个句子的[CLS]标记嵌入应用一种叫做 t-SNE 的降维技术将其转化为二维向量而不是 256 维向量并保存动画图表。让我们来可视化结果。https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/23987d3afe279c76504d4c31a47e8066.png每个训练点的投影[CLS]嵌入蓝色代表正向句子红色代表负向句子通过观察每个训练点的[CLS]嵌入的投影图我们可以看到经过若干轮训练后正向蓝色和负向红色句子之间的明显区别。这一可视化展示了 Transformer 架构随着时间推移调整嵌入的显著能力突出了自注意力机制的强大功能。数据以一种方式进行转化使得每个类别的嵌入得到了良好的分离从而大大简化了分类器头的任务。结论随着我们对 Transformer 架构的探索的结束显然这些模型擅长将数据定制化以适应特定任务。通过使用位置编码和多头自注意力机制Transformer 不仅仅是处理数据它们以一种前所未见的复杂程度来解释和理解信息。能够动态地权衡输入数据不同部分的相关性使得对输入文本的理解和表示更加细致。这提升了在各种下游任务中的表现包括文本分类、问答、命名实体识别等。现在你已经更好地理解了编码器架构你可以深入探讨解码器和编码器-解码器模型这些模型与我们刚刚探讨的非常相似。解码器在生成任务中起着至关重要的作用是流行的 GPT 模型的核心部分。随时在LinkedIn上与我联系请在GitHub上关注我获取更多内容访问我的网站: maximewolf.com参考文献[1] Vaswani, Ashish, 等人. “Attention Is All You Need.”第 31 届神经信息处理系统会议NIPS 2017, 美国加利福尼亚州长滩.[2] “The Illustrated Transformer.”Jay Alammar 的博客, 2018 年 6 月,jalammar.github.io/illustrated-transformer/[3] Transformer 架构的官方 PyTorch 实现.GitHub 代码库, PyTorch,github.com/pytorch/pytorch/blob/master/torch/nn/modules/transformer.py[4] Manning, Christopher, 等人. “CS224n: 使用深度学习进行自然语言处理.”斯坦福大学, 斯坦福 CS224N NLP 课程,web.stanford.edu/class/cs224n/