Transformers:它们如何转换您的数据?

news2024/10/5 17:16:09

一、说明

        在快速发展的人工智能和机器学习领域,一项创新因其对我们处理、理解和生成数据的方式产生深远影响而脱颖而出:Transformers。Transformer 彻底改变了自然语言处理 (NLP) 及其他领域,为当今一些最先进的 AI 应用程序提供动力。但究竟什么是变形金刚,它们如何以如此开创性的方式转换数据?本文揭开了 Transformer 模型内部工作的神秘面纱,重点介绍了编码器架构。我们将首先在 Python 中实现 Transformer 编码器,分解其主要组件。然后,我们将可视化 Transformer 在训练期间如何处理和调整输入数据。

        虽然这篇博客没有涵盖所有架构细节,但它提供了一个实现和对变形金刚变革力量的整体理解。要深入了解变形金刚,我建议您查看优秀的斯坦福 CS224-n 课程。

        我还建议遵循与本文关联的 GitHub 存储库以获取更多详细信息。😊

二、什么是 Transformer 编码器架构?

Attention Is All You Need 的 Transformer 模型

        这张图片显示了原始的 Transformer 架构,它结合了编码器和解码器,用于序列到序列语言任务。

        在本文中,我们将重点介绍编码器架构(图片上的红色块)。这就是流行的BERT模型在引擎盖下使用的内容:主要关注点是理解和表示数据,而不是生成序列。它可用于各种应用:文本分类、命名实体识别 (NER)、抽取式问答等。

        那么,这种架构实际上是如何转换数据的呢?我们将详细解释每个组件,但这里是该过程的概述。

  • 输入文本被标记化:Python 字符串被转换为标记(数字)列表
  • 每个标记都通过嵌入层传递,该层为每个标记输出矢量表示
  • 然后,使用位置编码层对嵌入进行进一步编码,添加有关序列中每个标记位置的信息
  • 这些新的嵌入由一系列编码器层使用自注意力机制进行转换
  • 可以添加特定于任务的头。例如,我们稍后将使用分类头将电影评论分类为正面或负面

重要的是要理解 Transformer 架构通过将嵌入向量从高维空间中的一种表示映射到同一空间中的另一个表示,并应用一系列复杂的转换来转换嵌入向量。

三、在 Python 中实现编码器体系结构

3.1 位置编码器图层

与 RNN 模型不同,注意力机制不使用输入序列的顺序。PositionalEncoder 类使用两个数学函数(余弦和正弦)将位置编码添加到输入嵌入中。

来自 Attention Is All You Need 的位置编码矩阵定义

请注意,位置编码不包含可训练的参数:有确定性计算的结果,这使得这种方法非常容易处理。此外,正弦和余弦函数的值介于 -1 和 1 之间,并且具有有用的周期性属性,以帮助模型学习有关单词相对位置的模式。

class PositionalEncoder(nn.Module):
    def __init__(self, d_model, max_length):
        super(PositionalEncoder, self).__init__()
        self.d_model = d_model
        self.max_length = max_length
        
        # Initialize the positional encoding matrix
        pe = torch.zeros(max_length, d_model)

        position = torch.arange(0, max_length, dtype=torch.float).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2, dtype=torch.float) * -(math.log(10000.0) / d_model))
        
        # Calculate and assign position encodings to the matrix
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        self.pe = pe.unsqueeze(0)
    
    def forward(self, x):
        x = x + self.pe[:, :x.size(1)] # update embeddings
        return x

3.2 多头自我关注

自注意力机制是编码器架构的关键组件。让我们暂时忽略“多头”。注意是一种确定每个标记(即每个嵌入)所有其他嵌入与该标记的相关性的方法,以获得更精细和上下文相关的编码。

“它”如何注意序列中的其他单词?(《变形金刚图解》))

自我注意力机制有 3 个步骤。

  • 使用矩阵 Q、K 和 V 分别转换输入 “query”、“key” 和 “value”。请注意,对于自注意力,query、key 和 values 都等于我们的输入嵌入
  • 使用查询之间的余弦相似度(点积)计算注意力分数。分数按嵌入维度的平方根进行缩放,以在训练期间稳定梯度
  • 使用 softmax 层使这些分数概率
  • 输出是的加权平均值,使用注意力分数作为权重

从数学上讲,这对应于以下公式。

Attention Is All Your Need 中的注意力机制

“multi-head” 是什么意思?基本上,我们可以并行多次应用所描述的自我注意力机制过程,并连接和投影输出。这允许每个头在句子的不同语义方面进行讨论。

我们首先定义磁头的数量、嵌入的尺寸 (d_model) 和每个磁头的尺寸 (head_dim)。我们还初始化了 Q、K 和 V 矩阵(线性层)以及最终的投影层。

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, num_heads):
        super(MultiHeadAttention, self).__init__()
        self.num_heads = num_heads
        self.d_model = d_model
        self.head_dim = d_model // num_heads

        self.query_linear = nn.Linear(d_model, d_model)
        self.key_linear = nn.Linear(d_model, d_model)
        self.value_linear = nn.Linear(d_model, d_model)      
        self.output_linear = nn.Linear(d_model, d_model)

当使用多头注意力时,我们像原始论文一样以缩小的维度(head_dim而不是d_model)应用每个注意力头,使总计算成本类似于具有全维的单头注意力层。请注意,这只是逻辑拆分。多注意力之所以如此强大,是因为它仍然可以通过单个矩阵运算来表示,这使得 GPU 上的计算非常高效。

def split_heads(self, x, batch_size):
        # Split the sequence embeddings in x across the attention heads
        x = x.view(batch_size, -1, self.num_heads, self.head_dim)
        return x.permute(0, 2, 1, 3).contiguous().view(batch_size * self.num_heads, -1, self.head_dim)

我们计算注意力分数并使用掩码来避免在填充标记上使用注意力。我们应用softmax激活来使这些分数具有概率。

def compute_attention(self, query, key, mask=None):
      # Compute dot-product attention scores
      # dimensions of query and key are (batch_size * num_heads, seq_length, head_dim)
      scores = query @ key.transpose(-2, -1) / math.sqrt(self.head_dim)
      # Now, dimensions of scores is (batch_size * num_heads, seq_length, seq_length)
      if mask is not None:
          scores = scores.view(-1, scores.shape[0] // self.num_heads, mask.shape[1], mask.shape[2]) # for compatibility
          scores = scores.masked_fill(mask == 0, float('-1e20')) # mask to avoid attention on padding tokens
          scores = scores.view(-1, mask.shape[1], mask.shape[2]) # reshape back to original shape
      # Normalize attention scores into attention weights
      attention_weights = F.softmax(scores, dim=-1)

      return attention_weights

forward 属性执行多头逻辑拆分并计算注意力权重。然后,我们通过将这些权重乘以值来获得输出。最后,我们重塑输出并用线性层进行投影。

def forward(self, query, key, value, mask=None):
      batch_size = query.size(0)

      query = self.split_heads(self.query_linear(query), batch_size)
      key = self.split_heads(self.key_linear(key), batch_size)
      value = self.split_heads(self.value_linear(value), batch_size)

      attention_weights = self.compute_attention(query, key, mask)
          
      # Multiply attention weights by values, concatenate and linearly project outputs
      output = torch.matmul(attention_weights, value)
      output = output.view(batch_size, self.num_heads, -1, self.head_dim).permute(0, 2, 1, 3).contiguous().view(batch_size, -1, self.d_model)
      return self.output_linear(output)

3.3 编码器层

        这是该架构的主要组件,它利用了多头自注意力。我们首先实现一个简单的类,通过 2 个密集层执行前馈操作。

class FeedForwardSubLayer(nn.Module):
    def __init__(self, d_model, d_ff):
        super(FeedForwardSubLayer, self).__init__()
        self.fc1 = nn.Linear(d_model, d_ff)
        self.fc2 = nn.Linear(d_ff, d_model)
        self.relu = nn.ReLU()

    def forward(self, x):
        return self.fc2(self.relu(self.fc1(x)))

        现在,我们可以对编码器层的逻辑进行编码。我们首先对输入应用自我注意力,这给出了一个相同维度的向量。然后,我们将迷你前馈网络与层范数层一起使用。请注意,在应用规范化之前,我们还使用跳过连接。

class EncoderLayer(nn.Module):
    def __init__(self, d_model, num_heads, d_ff, dropout):
        super(EncoderLayer, self).__init__()
        self.self_attn = MultiHeadAttention(d_model, num_heads)
        self.feed_forward = FeedForwardSubLayer(d_model, d_ff)
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, mask):
        attn_output = self.self_attn(x, x, x, mask)
        x = self.norm1(x + self.dropout(attn_output)) # skip connection and normalization
        ff_output = self.feed_forward(x)
        return self.norm2(x + self.dropout(ff_output)) # skip connection and normalization

3.4 把所有东西放在一起

是时候创建我们的最终模型了。我们通过嵌入层传递数据。这会将我们的原始标记(整数)转换为数值向量。然后,我们应用我们的位置编码器和几个 (num_layers) 编码器层。

class TransformerEncoder(nn.Module):
    def __init__(self, vocab_size, d_model, num_layers, num_heads, d_ff, dropout, max_sequence_length):
        super(TransformerEncoder, self).__init__()
        self.embedding = nn.Embedding(vocab_size, d_model)
        self.positional_encoding = PositionalEncoder(d_model, max_sequence_length)
        self.layers = nn.ModuleList([EncoderLayer(d_model, num_heads, d_ff, dropout) for _ in range(num_layers)])
    
    def forward(self, x, mask):
        x = self.embedding(x)
        x = self.positional_encoding(x)
        for layer in self.layers:
            x = layer(x, mask)
        return x

我们还创建了一个 ClassifierHead 类,用于将最终嵌入转换为分类任务的类概率。


class ClassifierHead(nn.Module):
    def __init__(self, d_model, num_classes):
        super(ClassifierHead, self).__init__()
        self.fc = nn.Linear(d_model, num_classes)

    def forward(self, x):
        logits = self.fc(x[:, 0, :]) # first token corresponds to the classification token
        return F.softmax(logits, dim=-1)

请注意,dense 和 softmax 层仅应用于第一个嵌入(对应于输入序列的第一个标记)。这是因为在标记文本时,第一个标记是 [CLS] 标记,它代表“分类”。[CLS] 标记旨在将整个序列的信息聚合到单个嵌入向量中,用作可用于分类任务的汇总表示。

注意:包含 [CLS] 标记的概念源自 BERT,它最初是针对下一句话预测等任务进行训练的。插入 [CLS] 标记以预测句子 B 跟随句子 A 的可能性,并使用 [SEP] 标记分隔 2 个句子。对于我们的模型,[SEP] 标记只是标记输入句子的末尾,如下所示。

[CLS]BERT 架构中的代币(关于 AI 的一切))

        仔细想想,这个单一的 [CLS] 嵌入能够捕获有关整个序列的如此多的信息,这真是令人震惊,这要归功于自注意力机制能够权衡和综合文本中每一段文本的重要性。

四、训练和可视化

        希望上一节能让您更好地了解我们的 Transformer 模型如何转换输入数据。现在,我们将使用IMDB数据集(电影评论)为我们的二进制分类任务编写训练管道。然后,我们将可视化 [CLS] 令牌在训练过程中的嵌入,以了解我们的模型如何转换它。

        我们首先定义超参数,以及 BERT 分词器。在 GitHub 存储库中,您可以看到我还编写了一个函数,以选择数据集的子集,其中只有 1200 个训练和 200 个测试示例。

num_classes = 2 # binary classification
d_model = 256 # dimension of the embedding vectors
num_heads = 4 # number of heads for self-attention
num_layers = 4 # number of encoder layers
d_ff = 512. # dimension of the dense layers in the encoder layers
sequence_length = 256 # maximum sequence length 
dropout = 0.4 # dropout to avoid overfitting
num_epochs = 20
batch_size = 32

loss_function = torch.nn.CrossEntropyLoss()

dataset = load_dataset("imdb")
dataset = balance_and_create_dataset(dataset, 1200, 200) # check GitHub repo

tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased', model_max_length=sequence_length)

        您可以尝试在以下句子中使用 BERT 分词器:

print(tokenized_datasets['train']['input_ids'][0])

        每个序列都应以标记 101 开头,对应于 [CLS],后跟一些非零整数,如果序列长度小于 256,则用零填充。请注意,在使用我们的“掩码”进行自注意力计算时,这些零被忽略。

tokenized_datasets = dataset.map(encode_examples, batched=True)
tokenized_datasets.set_format(type='torch', columns=['input_ids', 'attention_mask', 'label'])

train_dataloader = DataLoader(tokenized_datasets['train'], batch_size=batch_size, shuffle=True)
test_dataloader = DataLoader(tokenized_datasets['test'], batch_size=batch_size, shuffle=True)

vocab_size = tokenizer.vocab_size

encoder = TransformerEncoder(vocab_size, d_model, num_layers, num_heads, d_ff, dropout, max_sequence_length=sequence_length)
classifier = ClassifierHead(d_model, num_classes)

optimizer = torch.optim.Adam(list(encoder.parameters()) + list(classifier.parameters()), lr=1e-4)

        我们现在可以编写我们的训练函数:

def train(dataloader, encoder, classifier, optimizer, loss_function, num_epochs):
    for epoch in range(num_epochs):        
        # Collect and store embeddings before each epoch starts for visualization purposes (check repo)
        all_embeddings, all_labels = collect_embeddings(encoder, dataloader)
        reduced_embeddings = visualize_embeddings(all_embeddings, all_labels, epoch, show=False)
        dic_embeddings[epoch] = [reduced_embeddings, all_labels]
        
        encoder.train()
        classifier.train()
        correct_predictions = 0
        total_predictions = 0
        for batch in tqdm(dataloader, desc="Training"):
            input_ids = batch['input_ids']
            attention_mask = batch['attention_mask'] # indicate where padded tokens are
            # These 2 lines make the attention_mask a matrix instead of a vector
            attention_mask = attention_mask.unsqueeze(-1)
            attention_mask = attention_mask & attention_mask.transpose(1, 2) 
            labels = batch['label']
            optimizer.zero_grad()
            output = encoder(input_ids, attention_mask)
            classification = classifier(output)
            loss = loss_function(classification, labels)
            loss.backward()
            optimizer.step()
            preds = torch.argmax(classification, dim=1)
            correct_predictions += torch.sum(preds == labels).item()
            total_predictions += labels.size(0)
        
        epoch_accuracy = correct_predictions / total_predictions
        print(f'Epoch {epoch} Training Accuracy: {epoch_accuracy:.4f}')

        可以在 GitHub 存储库中找到 collect_embeddings 和 visualize_embeddings 函数。它们存储训练集每个句子的 [CLS] 标记嵌入,应用称为 t-SNE 的降维技术使它们成为 2D 向量(而不是 256 维向量),并保存动画绘图。

        让我们可视化结果。

        每个训练点的投影 [CLS] 嵌入(蓝色对应正句,红色对应负句)

        观察每个训练点的投影 [CLS] 嵌入图,我们可以看到几个纪元后肯定(蓝色)和否定(红色)句子之间的明显区别。该视觉效果显示了 Transformer 架构随时间推移调整嵌入的卓越能力,并突出了自注意力机制的强大功能。数据的转换方式使得每个类的嵌入都很好地分离,从而大大简化了分类器头的任务。

五、结论

        当我们结束对 Transformer 架构的探索时,很明显,这些模型擅长为给定的任务定制数据。通过使用位置编码和多头自注意力,变形金刚超越了单纯的数据处理:它们以前所未有的复杂程度解释和理解信息。动态权衡输入数据不同部分的相关性的能力允许对输入文本进行更细致的理解和表示。这增强了各种下游任务的性能,包括文本分类、问答、命名实体识别等。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

代码随想录-二叉树(节点)

目录 104. 二叉树的最大深度 题目描述: 输入输出描述: 思路和想法: 111. 二叉树的最小深度 题目描述: 输入输出描述: 思路和想法: 222. 完全二叉树的节点个数 题目描述: ​输入输出描…

关于举办《Llama3关键技术深度解析与构建Responsible AI、算法及开发落地实战》线上高级研修讲座的通知

关于举办《Llama3关键技术深度解析与构建Responsible AI、算法及开发落地实战》线上高级研修讲座的通知

Mybatis逆向工程的2种方法,一键高效快速生成Pojo、Mapper、XML,摆脱大量重复开发

一、写在开头 最近一直在更新《Java成长计划》这个专栏,主要是Java全流程学习的一个记录,目前已经更新到Java并发多线程部分,后续会继续更新;而今天准备开设一个全新的专栏 《EfficientFarm》。 EfficientFarm:高效农…

Tomcat PUT方法任意写文件漏洞(CVE-2017-12615)

1 漏洞原理 在Apache Tomcat服务器中,PUT方法通常用于上传文件。攻击者可以通过发送PUT请求,将恶意文件上传到服务器。 当攻击者发送PUT请求时,Tomcat服务器会将请求中的数据写入指定的文件。如果攻击者能够控制文件路径,那么他们…

Mac brew安装Redis之后更新配置文件的方法

安装命令 brew install redis 查看安装位置命令 brew list redis #查看redis安装的位置 % brew list redis /usr/local/Cellar/redis/6.2.5/.bottle/etc/ (2 files) /usr/local/Cellar/redis/6.2.5/bin/redis-benchmark /usr/local/Cellar/redis/6.2.5/bin/redis-check-ao…

跨平台桌面客户端开发框架

跨平台桌面客户端开发框架允许开发者创建能够在多个操作系统上运行的桌面应用程序。以下是一些流行的跨平台桌面客户端开发框架。这些框架各有优势,选择哪个框架取决于项目需求、团队的技术栈以及对特定特性的偏好。 1.Electron : 使用JavaScript, HTML…

Java IO流(三)

1. 字符流 1.1 什么是字符流 在Java中,字符流是指提供了基于字符的I/O能力的API。 Java 1.0中提供的基于字节的I/O流API只能支持8位字节流,无法妥善地处理16位Unicode字符。由于需要支持Unicode处理国际化字符,因此Java 1.1 对基础流式I/O库…

vue3+ts 原生 js drag drop 实现

vue3ts 原生 js drag drop 实现 一直以来没有涉及的一个领域就是 drag drop 拖动操作,研究了下,实现了,所以写个教程。 官方说明页面及实例:https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API 最终效果&…

SpringCloud整合Gateway结合Nacos

目录 一、引入依赖 二、开启两个测试项目 2.1 order service ​编辑 2.2 user service 三、gateway项目 3.1 新建一个bootstrap.yml文件 3.2 将我们的的网关配置写道nacos里的配置里 3.3 测试:看能够根据网关路由到两个测试的项目 四、 优化 4.1 将项目打包…

数据库(MySQL)—— DQL语句(聚合,分组,排序,分页)

数据库(MySQL)—— DQL语句(聚合,分组,排序,分页) 聚合函数常见的聚合函数语法 分组查询语法 排序查询语法 分页查询语法 DQL的执行顺序 我们今天来继续学习MySQL的DQL语句的聚合和分组查询&…

【面试经典 150 | 数组】接雨水

文章目录 写在前面Tag题目来源解题思路方法一:预处理方法二:单调栈方法三:双指针 写在最后 写在前面 本专栏专注于分析与讲解【面试经典150】算法,两到三天更新一篇文章,欢迎催更…… 专栏内容以分析题目为主&#xff…

【大模型学习】Transformer(学习笔记)

Transformer介绍 word2vec Word2Vec是一种用于将词语映射到连续向量空间的技术,它是由Google的Tomas Mikolov等人开发的。Word2Vec模型通过学习大量文本数据中的词语上下文信息,将每个词语表示为高维空间中的向量。在这个向量空间中,具有相似…

【C++】学习笔记——string_2

文章目录 六、string类2. 反向迭代器const迭代器 string类对象的容量操作(补)size() 3. string类的元素访问4. string类的修改 未完待续 结合文档食用~ 六、string类 2. 反向迭代器 一般来说,迭代器都是正向的遍历容器,虽然可以…

LuaJIT源码分析(三)字符串

LuaJIT源码分析(三)字符串 要表示一个字符串,核心就是需要知道字符串的长度,以及存放字符串具体数据的地址。lua的字符串是内化不可变的,也就是lua字符串变量存放的不是字符串的拷贝,而是字符串的引用。那么…

C语言⼆级指针如何操作字符串数组(指针数组)?

一、问题 对于字符串数组该如何操作(⽽且是使⽤指针数组存储)? 二、解答 使⽤指针的指针实现对字符串数组中字符串的输出。指向指针的指针即是指向指针数据的指针变量。这⾥创建⼀个指针数组 strings,它的每个数组元素相当于⼀个…

springcloud自定义全局异常

自行创建一个实体类 /*** 全局异常处理类**/ ControllerAdvice public class GlobalExceptionHandler {ExceptionHandler(Exception.class) ResponseBody public Result error(Exception e){e.printStackTrace(); return Result.fail();}/*** 自定义异常处理方法* param e * re…

神奇的Vue3 - 组件探索

神奇的Vue3 第一章 神奇的Vue3—基础篇 第二章 神奇的Vue3—Pinia 文章目录 神奇的Vue3了解组件一、注册组件1. 全局注册​2. 局部注册3. 组件命名 二、属性详解1. Props(1)基础使用方法(2)数据流向:单项绑定原则&…

深入图像分类:使用美国手语数据集训练定制化神经网络

引言 在前一篇博客中,我们探讨了如何使用MNIST数据集训练一个基础的神经网络来进行手写数字识别。在本文中,我们将更进一步,使用美国手语字母表(ASL)数据集来构建一个定制化的图像分类模型。通过这个过程,…

YOLOv9/YOLOv8算法改进【NO.128】 使用ICCV2023超轻量级且高效的动态上采样器( DySample)改进yolov8中的上采样

前 言 YOLO算法改进系列出到这,很多朋友问改进如何选择是最佳的,下面我就根据个人多年的写作发文章以及指导发文章的经验来看,按照优先顺序进行排序讲解YOLO算法改进方法的顺序选择。具体有需求的同学可以私信我沟通: 首推…

静态库、动态库回顾

回顾一下库相关的知识点&#xff0c;总结备忘一下。在某种情况下&#xff0c;你有了如下的代码&#xff0c;结构如下 //pra.h #include <stdio.h> void test_01(); //pra.c #include "pra.h" void test_01() {printf("xxxxxxx----->%s %s()\n",…