transformer手绘图解(注意力机制的本质)

文摘   2024-12-25 11:08   中国香港  

点击下方卡片,关注“自动驾驶之星

这里有一群奋斗在自动驾驶量产第一线的小伙伴等你加入Introduction
本期概述

可以说,Transformer仍然是目前最通用、最强大的模型架构之一。它在自然语言处理,计算机视觉,语音和音频处理,多模态学习,生物信息学等领域具有绝对的霸主地位。

几乎每一位深度学习从业员都无法绕开transformer。本期文章,我们从以下几点展开介绍~欢迎大家点赞收藏奥~

  • 为什么是transformer?

  • 单层transformer的结构和数据流,手撕实现

  • 编码器&解码器

  • transformer的热门应用

为什么是transformer

非常多的研究领域都用到了transformer,相信大家一定有很大的疑问号:

为什么transformer如此特别?他和其他的网络构架相比,究竟解决了什么问题?

我想这个问题要比transformer的实现方法更为重要。李小毛主要研究计算机视觉多模态学习图神经网络大模型这四个邻域,结合我的实践经验和同行朋友们的观点,认为transformer的突出优点在以下几个方面:

  1. token化处理:模型的输入不再依赖于特征图的具体维度尺寸。只要特征点的特征维度一致,理论上transformer能够接受一切模态(语音、图像、文字、甚至抽象化的指令)的信息,这是transformer在多模态学习中具有一席之地的重要原因

  2. 较少的归纳偏置:传统的卷积神经网络(CNN)对全局语义信息的收集依赖于感受野和编码深度,而transformer能够将全局所有的数据通过注意力机制整体处理,相当于每一次计算都是一次全局信息感知。这是开了“上帝视角”。

  3. 并行化处理能力:与传统的循环神经网络(RNN)不同,Transformer不依赖于顺序数据处理,而是通过注意力机制处理输入序列中的每一个元素,因此能够在一次性处理整个序列。这使得Transformer在处理大规模数据时比RNN更具优势,尤其适用于计算资源丰富的环境。

  4. 可解释性(轻微):相比其余的模型构架,transformer在可解释性上已经有了进步,其自注意力机制的透明性,可以通过查看每个输入元素之间的注意力权重来理解模型如何做出决策。这为Transformer在需要解释性和可理解性的领域(如医疗、金融等)提供了更大的应用潜力。

  5. 拓展性和迁移学习:Transformer架构特别适合进行迁移学习。在NLP领域,像BERT、GPT等预训练的Transformer模型已经被证明在多种下游任务中表现出色。类似的,Transformer也能在其他领域通过大规模预训练,并在具体任务上进行微调,从而提升任务的性能。

单层transformer的数据流

  

transformer和变形金刚(英文Transformer)同名,其实两者具有一些“相似”。

我们明白,每一个汽车或者汽车人,都是由多个零件构成的。每一次transformer层计算,其本质就是将原来的汽车拆解开来,计算所有部件之间的关联,然后重新组装成汽车人的过程。

Transformer 模型的核心是通过计算输入序列中每个零件之间的相关性,来动态地调整该零件对其他零件的“关注”程度,即自注意力机制(Self-Attention)。输入形状为:

分别表示批量大小,特征点(token)的数量,特征维度。输出维度和输入保持一致。

自注意力机制:

点积操作是一个常见的相似度计算方法,表示两个向量在空间上的相关性(相似程度)。点积越大,表示两个向量越“相似”或越“相关”。

假设矩阵Q(Query,查询向量)表示某个词关注哪些其他词。矩阵K(Key,键向量)表示某个词能提供的“内容描述”。那么能够得到相似度矩阵

这个结果是一个 的矩阵,其中第 个元素表示序列中第 个词的Query与第 个词的Key的点积相似度。

但是点积的值随着向量维度 增大而变大,这可能导致数值过大,影响梯度稳定性。因此需要对点积除以 ,缩放至一个更适合的范围。同时对缩放后的相似度分数应用 Softmax 函数,将其转换为概率分布:

非常好,我们得到了transformer最最核心的公式,自注意力公式。

特征提取

但是我们似乎还没有理清Q和K是怎么得到的? 很简单,遇事不决深度学习,使用一层线性层求出来!至于输入的特征究竟是怎么生成我们想要的特征查询(Q)和特征键(K)呢?这个问题交给模型去头疼就好啦!

 是输入向量(序列)。 是分别用于生成Q和K的可训练权重矩阵。

我们得到了transformer的第二个公式。现在和第一个公式组合一下,同时因为第一个公式只是得到了相关性矩阵,还需要把这个矩阵和特征自身的信息V相乘,表示对特征信息的处理(V当然也是线性层求出来的啦)

代码实现:

import numpy as npdef attention(Q, K, V, d_k):    """    实现 Transformer 中的 Scaled Dot-Product Attention 计算过程    Args:        Q: Query 矩阵,形状为 (batch_size, sequence_length, d_k)        K: Key 矩阵,形状为 (batch_size, sequence_length, d_k)        V: Value 矩阵,形状为 (batch_size, sequence_length, d_v)        d_k: Key 的维度(用于缩放)    Returns:        Attention Output: 注意力计算后的输出,形状为 (batch_size, sequence_length, d_v)    """    # Step 1: 点积相似性计算    scores = np.matmul(Q, K.transpose(0, 2, 1))  # (batch_size, sequence_length, sequence_length)    # Step 2: 缩放 (scaled)    scores = scores / np.sqrt(d_k)  # (batch_size, sequence_length, sequence_length)    # Step 3: Softmax 转化为权重    attention_weights = np.exp(scores) / np.sum(np.exp(scores), axis=-1, keepdims=True)  # Softmax    # Step 4: 用权重加权 Value    output = np.matmul(attention_weights, V)  # (batch_size, sequence_length, d_v)    return output, attention_weights

位置编码

Transformer是一个并行计算结构,无法像RNN那样通过时间步或CNN的局部感受野隐式捕捉顺序信息。因此需要人为的输入顺序信息,相当于在特征维度的每一个位置都加上一个唯一的数值:

给定序列中的位置 和嵌入维度中的维度索引 :

公式说明

  • pos: 表示位置索引,例如第几个单词。
  •  :表示嵌入向量中的第 维。
  •  :嵌入向量的维度(embedding size)。
  • 正弦 ( ) 用于偶数维,余弦(cos)用于奇数维。

这里10000是一个缩放系数。

代码实现:

import numpy as npdef positional_encoding(sequence_length, d_model):    # 初始化位置编码矩阵    PE = np.zeros((sequence_length, d_model))    # 位置序列    position = np.arange(0, sequence_length)[:, np.newaxis]  # shape: (sequence_length, 1)    # 维度序列    div_term = np.exp(np.arange(0, d_model, 2) * -(np.log(10000.0) / d_model))  # shape: (d_model / 2)    # 偶数维使用sin    PE[:, 0::2] = np.sin(position * div_term)    # 奇数维使用cos    PE[:, 1::2] = np.cos(position * div_term)    return PE

多头注意力

通过并行计算多个独立的注意力头,每个头可以学习和关注输入序列的不同部分,然后将这些不同头的结果合并,生成最终的注意力表示。

这个比较简单,我们直接给代码:

def multi_head_attention(X, W_Q, W_K, W_V, W_O, num_heads):    """    实现多头注意力机制
    Args:        X: 输入矩阵,形状为 (batch_size, sequence_length, d_model)        W_Q: Query 权重矩阵,形状为 (num_heads, d_model, d_k)        W_K: Key 权重矩阵,形状为 (num_heads, d_model, d_k)        W_V: Value 权重矩阵,形状为 (num_heads, d_model, d_v)        W_O: 输出权重矩阵,形状为 (num_heads * d_v, d_model)        num_heads: 注意力头的数量
    Returns:        Multi-Head Attention Output: 形状为 (batch_size, sequence_length, d_model)    """    batch_size, sequence_length, d_model = X.shape    d_k = W_Q.shape[-1]    d_v = W_V.shape[-1]
    # 初始化多头注意力的输出    multi_head_outputs = []
    for h in range(num_heads):        # 计算每个头的 Q, K, V        Q = np.matmul(X, W_Q[h])  # (batch_size, sequence_length, d_k)        K = np.matmul(X, W_K[h])  # (batch_size, sequence_length, d_k)        V = np.matmul(X, W_V[h])  # (batch_size, sequence_length, d_v)
        # 计算单头注意力输出        head_output, _ = attention(Q, K, V, d_k)  # (batch_size, sequence_length, d_v)        multi_head_outputs.append(head_output)
    # 将所有头的输出拼接    concatenated = np.concatenate(multi_head_outputs, axis=-1)  # (batch_size, sequence_length, num_heads * d_v)
    # 通过输出权重矩阵映射回 d_model 维度    output = np.matmul(concatenated, W_O)  # (batch_size, sequence_length, d_model)
    return output

前馈神经网络

前馈神经网络是 Transformer 中每一层的重要组成部分,位于多头注意力机制之后。它的主要作用是对每个时间步(即序列中每个位置)的特征表示进行非线性变换,增强模型的表达能力。

对于每个位置的输入向量 ,前馈神经网络的计算公式如下:

  •  :线性变换的权重矩阵,形状为 。
  •  :对应的偏置向量,形状为 。
  •  :第二个线性变换的权重矩阵,形状为 。
  •  :第二个线性变换的偏置向量,形状为 。
  •  :输入和输出向量的维度 (Transformer 的嵌入维度)。
  •  :FFN 的隐藏层维度,通常远大于 ,比如 ,而 。

python实现

import numpy as npdef feed_forward(x, W1, b1, W2, b2):    """    实现前馈神经网络 (FFN)    Args:        x: 输入张量,形状为 (batch_size, sequence_length, d_model)        W1: 第一层权重矩阵,形状为 (d_model, d_ffn)        b1: 第一层偏置,形状为 (d_ffn,)        W2: 第二层权重矩阵,形状为 (d_ffn, d_model)        b2: 第二层偏置,形状为 (d_model,)    Returns:        输出张量,形状为 (batch_size, sequence_length, d_model)    """    # Step 1: 第一层线性变换    h1 = np.matmul(x, W1) + b1  # (batch_size, sequence_length, d_ffn)    # Step 2: 激活函数 ReLU    h2 = np.maximum(0, h1)  # ReLU 激活    # Step 3: 第二层线性变换    output = np.matmul(h2, W2) + b2  # (batch_size, sequence_length, d_model)    return output小结

小结

将注意力机制和前馈网络结合起来,描述一个transformer层的计算流程:

1.Multi-Head Attention:

2.前馈网络:

 是该层的最终输出,作为下一层的输入。

单层transformer的整体代码实现:

import numpy as np
def attention(Q, K, V, d_k):    """    实现 Scaled Dot-Product Attention
    Args:        Q: Query 矩阵,形状为 (batch_size, sequence_length, d_k)        K: Key 矩阵,形状为 (batch_size, sequence_length, d_k)        V: Value 矩阵,形状为 (batch_size, sequence_length, d_v)        d_k: Key 的维度(用于缩放)
    Returns:        Attention Output: 注意力计算后的输出,形状为 (batch_size, sequence_length, d_v)    """    # Step 1: 点积相似性计算    scores = np.matmul(Q, K.transpose(021))  # (batch_size, sequence_length, sequence_length)
    # Step 2: 缩放 (scaled)    scores = scores / np.sqrt(d_k)
    # Step 3: Softmax 转化为权重    attention_weights = np.exp(scores) / np.sum(np.exp(scores), axis=-1, keepdims=True)
    # Step 4: 用权重加权 Value    output = np.matmul(attention_weights, V)  # (batch_size, sequence_length, d_v)
    return output, attention_weights

def multi_head_attention(X, W_Q, W_K, W_V, W_O, num_heads):    """    实现多头注意力机制
    Args:        X: 输入矩阵,形状为 (batch_size, sequence_length, d_model)        W_Q: Query 权重矩阵,形状为 (num_heads, d_model, d_k)        W_K: Key 权重矩阵,形状为 (num_heads, d_model, d_k)        W_V: Value 权重矩阵,形状为 (num_heads, d_model, d_v)        W_O: 输出权重矩阵,形状为 (num_heads * d_v, d_model)        num_heads: 注意力头的数量
    Returns:        Multi-Head Attention Output: 形状为 (batch_size, sequence_length, d_model)    """    batch_size, sequence_length, d_model = X.shape    d_k = W_Q.shape[-1]    d_v = W_V.shape[-1]
    # 初始化多头注意力的输出    multi_head_outputs = []
    for h in range(num_heads):        # 计算每个头的 Q, K, V        Q = np.matmul(X, W_Q[h])  # (batch_size, sequence_length, d_k)        K = np.matmul(X, W_K[h])  # (batch_size, sequence_length, d_k)        V = np.matmul(X, W_V[h])  # (batch_size, sequence_length, d_v)
        # 计算单头注意力输出        head_output, _ = attention(Q, K, V, d_k)  # (batch_size, sequence_length, d_v)        multi_head_outputs.append(head_output)
    # 将所有头的输出拼接    concatenated = np.concatenate(multi_head_outputs, axis=-1)  # (batch_size, sequence_length, num_heads * d_v)
    # 通过输出权重矩阵映射回 d_model 维度    output = np.matmul(concatenated, W_O)  # (batch_size, sequence_length, d_model)
    return output

def feed_forward(H, W1, b1, W2, b2):    """    实现前馈神经网络 (FFN)
    Args:        H: 输入矩阵,形状为 (batch_size, sequence_length, d_model)        W1: 第一层权重矩阵,形状为 (d_model, d_ffn)        b1: 第一层偏置,形状为 (d_ffn,)        W2: 第二层权重矩阵,形状为 (d_ffn, d_model)        b2: 第二层偏置,形状为 (d_model,)
    Returns:        输出矩阵,形状为 (batch_size, sequence_length, d_model)    """    # Step 1: 第一层线性变换    h1 = np.matmul(H, W1) + b1  # (batch_size, sequence_length, d_ffn)
    # Step 2: 激活函数 ReLU    h2 = np.maximum(0, h1)  # ReLU 激活
    # Step 3: 第二层线性变换    output = np.matmul(h2, W2) + b2  # (batch_size, sequence_length, d_model)
    return output

def transformer_layer(X, W_Q, W_K, W_V, W_O, W1, b1, W2, b2, num_heads):    """    实现 Transformer 的一个编码器层,包括多头注意力和前馈网络
    Args:        X: 输入矩阵,形状为 (batch_size, sequence_length, d_model)        W_Q, W_K, W_V: 多头注意力的权重矩阵        W_O: 多头注意力的输出权重矩阵        W1, b1, W2, b2: 前馈网络的权重和偏置        num_heads: 注意力头的数量
    Returns:        输出矩阵,形状为 (batch_size, sequence_length, d_model)    """    # Step 1: 多头注意力机制    attention_output = multi_head_attention(X, W_Q, W_K, W_V, W_O, num_heads)
    # Step 2: 残差连接 + 多头注意力的输出    H1 = X + attention_output  # 残差连接
    # Step 3: 前馈网络    ffn_output = feed_forward(H1, W1, b1, W2, b2)
    # Step 4: 残差连接 + FFN 的输出    H2 = H1 + ffn_output  # 残差连接
    return H2
batch_size = 2sequence_length = 4d_model = 8  # 嵌入维度d_ffn = 16  # 前馈网络的隐藏层维度d_k = d_v = 4  # 每个头的 Key 和 Value 维度num_heads = 2  # 注意力头数量
# 随机初始化输入和权重X = np.random.rand(batch_size, sequence_length, d_model)  # 输入序列W_Q = np.random.rand(num_heads, d_model, d_k)  # Query 权重矩阵W_K = np.random.rand(num_heads, d_model, d_k)  # Key 权重矩阵W_V = np.random.rand(num_heads, d_model, d_v)  # Value 权重矩阵W_O = np.random.rand(num_heads * d_v, d_model)  # 多头输出权重矩阵W1 = np.random.rand(d_model, d_ffn)  # FFN 第一层权重b1 = np.random.rand(d_ffn)  # FFN 第一层偏置W2 = np.random.rand(d_ffn, d_model)  # FFN 第二层权重b2 = np.random.rand(d_model)  # FFN 第二层偏置
# 调用 Transformer 层output = transformer_layer(X, W_Q, W_K, W_V, W_O, W1, b1, W2, b2, num_heads)

编码器&解码器

现在我们对单层的transformer已经了然于胸了对嘛!那么编码器、解码器就是transformer的叠加~直接上结构。

因为不同的研究领域都会采用不一样的构架形式,如何去编排transformer并没有统一的回答(例如多模态处理会涉及自注意力-跨注意力交叉,语言处理会采用单向注意力,有些生成式大模型不使用编码器)。非常多的变体就是从这里展开的。

前面的手撕transformer只是为了辅助理解~实际工作中一般只需要使用pytorch库即可,下面是一个分类任务的实现:

import torchimport torch.nn as nnimport torch.nn.functional as F
class TransformerEncoderModel(nn.Module):    def __init__(self, vocab_size, d_model, nhead, num_encoder_layers, d_ffn, max_seq_len, num_classes, dropout=0.1):        """        Transformer 编码器模型        Args:            vocab_size: 词汇表大小            d_model: 嵌入维度            nhead: 多头注意力头数量            num_encoder_layers: Transformer 编码器层数            d_ffn: 前馈网络的隐藏层维度            max_seq_len: 最大序列长度            num_classes: 分类任务的类别数量            dropout: Dropout 概率        """        super(TransformerEncoderModel, self).__init__()
        # 词嵌入 + 位置编码        self.embedding = nn.Embedding(vocab_size, d_model)        self.positional_encoding = nn.Parameter(torch.zeros(1, max_seq_len, d_model))
        # Transformer 编码器层        encoder_layer = nn.TransformerEncoderLayer(d_model=d_model, nhead=nhead, dim_feedforward=d_ffn, dropout=dropout)        self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_encoder_layers)
        # 分类头        self.classifier = nn.Linear(d_model, num_classes)
    def forward(self, input_ids, mask=None):        """        前向计算        Args:            input_ids: 输入序列,形状为 (batch_size, seq_len)            mask: 注意力掩码,形状为 (seq_len, seq_len),可选        Returns:            logits: 分类输出,形状为 (batch_size, num_classes)        """        # 获取输入的形状        batch_size, seq_len = input_ids.size()
        # 嵌入层 (词嵌入 + 位置编码)        x = self.embedding(input_ids) + self.positional_encoding[:, :seq_len, :]
        # Transformer 编码器        x = self.encoder(x.transpose(01), src_key_padding_mask=mask)  # (seq_len, batch_size, d_model)        x = x.transpose(01)  # 转回 (batch_size, seq_len, d_model)
        # 池化操作:取 [CLS] 位置(或其他方式,如平均池化)        cls_token = x[:, 0, :]  # (batch_size, d_model)
        # 分类        logits = self.classifier(cls_token)  # (batch_size, num_classes)        return logits

Transformer的热门应用

GPT 采用仅解码器的架构和自回归训练来生成连贯且上下文相关的文本。同时采用单向注意力保证文字的因果关系

from transformers import GPT2LMHeadModel, GPT2Tokenizer
tokenizer = GPT2Tokenizer.from_pretrained('gpt2')model = GPT2LMHeadModel.from_pretrained('gpt2')
input_text = "Once upon a time, "inputs = tokenizer(input_text, return_tensors='pt')output = tokenizer.decode(    model.generate(        **inputs,        max_new_tokens=100,    )[0],    skip_special_tokens=True)
input_ids = tokenizer(input_text, return_tensors='pt')
print(output)
知识星球,新年优惠券重磅来来袭!,结识一群志同道合的小伙伴一起成长。








知识星球,新人优惠券来袭,结识一群志同道合的小伙伴一起成长。


下一个风口会不会是生成式AI 与具身智能的时代,我们特意创建了生成式AI与具身智能交流社区,关于大模型,机器人的相关业界动态,学术方向,技术解读等等都会在社区与大家交流,欢迎感兴趣的同学加入我们(备注具身智能)!   

自动驾驶之星知识星球主打自动驾驶量产全技术栈学习,并包括: 学习板块,求职面试,有问必答,论文速递,行业动态五大板块!星球内部包括端到端大模型,VLM大模型,BEV 障碍物/车道线/Occ 等的学习资料!

生成式AI与具身智能知识星球,我们相信生成式AI 与具身智能会碰撞出出乎我们意料的内容,本知识形象并包括: 学习板块,求职面试,有问必答,论文速递,行业动态五大板块!星球内部包括生成式AI大模型,具身智能,业界资料整理等的学习资料!


自动驾驶之星是面向自动驾驶&智能座舱量产向相关的交流社区,欢迎大家添加小助手加入我们的交流群里,这里有一批奋斗在量产第一线的小伙伴等你的加入!

👇点个“赞”和“在看”吧

自动驾驶之星
自动驾驶之星,是一个以自动驾驶\x26amp;智能座舱量产交流为主的社区。这里有自动驾驶\x26amp;智能座舱量产第一线的前沿动态,有一群奋斗在自动驾驶\x26amp;智能座舱量产第一线的小伙伴在分享他们的量产经历。期待你的加入!希望每个人在这个浪潮中都能成为自动驾驶之星!
 最新文章