深度学习之全面理解Self-Attention

Transformer 模型中大量使用了 self-attention机制 (Masked self-attention、Crosss attention)。
Bert 相当于 Transformer 的 encoder 部分,GPT 相当于 Transformer 的 decoder 部分。
encoder 编码信息, decoder 产生输出。
Multi-Head Attention 多头注意力机制 在 Self-Attention 的基础上添加了 head 维度,用来处理不同类型的输入。
Scaled Dot-Product Attention 缩放点积注意力机制(单头)

Self-Attention

截图_20235106065111.png
每一个单词都会分割成QKV三部分(矩阵线性变换)
每个单词的 Q 会和所有单词的 K 做 注意(Attention),实际上就是相乘。每次都必须和自己做 注意,所以称为 自注意力机制。
做完注意力之后会得到相关性大小,再作用到 V 上得到最终的输出

import torch
from torch import nn


class SelfAttention(nn.Module):
    def __init__(self, hidden_size) -> None:
        super(SelfAttention, self).__init__()
        self.q_layer = nn.Sequential(
            nn.Linear(in_features=hidden_size, out_features=hidden_size)
        )
        self.k_layer = nn.Sequential(
            nn.Linear(in_features=hidden_size, out_features=hidden_size)
        )
        self.v_layer = nn.Sequential(
            nn.Linear(in_features=hidden_size, out_features=hidden_size)
        )

    def forward(self, x):
        """
        前向过程
        :param x: [n,t,e] n个文本, t个时刻, 每个时刻e维的向量
        :return: [n,t,e]
        """
        # 1. 获取q、k、v
        q = self.q_layer(x)
        k = self.k_layer(x)  # [n,t,e]
        v = self.v_layer(x)  # [n,t,e]

        # 2. 计算q和k之间的相关性 -> F函数
        scores = torch.matmul(q, torch.permute(k, dims=(0, 2, 1)))

        # 3. 转换为权重
        alpha = torch.softmax(scores, dim=2)  # [n,t,t]
        # 4. 值的合并
        v = torch.matmul(alpha, v)  # [n,t,e]
        return v
    

@torch.no_grad()
def t0():
    token_id = torch.tensor([
        [1, 3, 5],  # 一个样本, 三个时刻
        [1, 6, 3]
    ])
    # 静态特征向量提取 Word2Vec EmbeddingLayer
    emb_layer = nn.Embedding(num_embeddings=10, embedding_dim=4)
    x1 = emb_layer(token_id)  # [2,3,4]
    print(x1[0][0])  # 第一个样本的第一个token对应的向量
    print(x1[1][0])
    print('='*100)

    # 基于self-attention的提取
    att = SelfAttention(hidden_size=4)
    x3 = att(x1)
    print(x3[0][0])  # 第一个样本的第一个token对应的向量
    print(x3[1][0])  # 第二个样本的第一个token对应的向量
    print(x3[1][1])  # 第二个样本的第二个token对应的向量

    att2 = SelfAttention(hidden_size=4)
    x4 = att2(x1)

    x5 = torch.concat([x3, x4], dim=2)
    print(x5.shape)

    print(x5[0][0])  # 第一个样本的第一个token对应的向量
    print(x5[1][0])  # 第二个样本的第一个token对应的向量


if __name__ == '__main__':
    t0()


Self-Attention mechanism,single head

截图_20235706065704.png
注意力机制用于提取序列中的的重要机制。

Multi-head Self-Attention (2 head as example)

截图_20231206071219.png
截图_20231506071528.png
嵌入层:将每个输入元素转换为一个向量表示
注意力:计算序列中每个元素与其他元素的关联程度,来确定每个元素在输出中的重要程度
链接: 将多个加权向量串联起来,以融合不同注意力头的信息

import torch
from torch import nn


class MultiHeadAttention(nn.Module):
    def __init__(self, hidden_size, num_header):
        super(MultiHeadAttention, self).__init__()
        assert hidden_size % num_header == 0, f"head数目无法整除: {hidden_size}/{num_header}"

        self.hidden_size = hidden_size  # 向量维度大小, 即e
        self.num_header = num_header  # 头的数目

        self.wq = nn.Linear(in_features=hidden_size, out_features=hidden_size)
        self.wk = nn.Linear(in_features=hidden_size, out_features=hidden_size)
        self.wv = nn.Sequential(
            nn.Linear(in_features=hidden_size, out_features=hidden_size),
            nn.ReLU()
        )
        self.wo = nn.Sequential(
            nn.Linear(in_features=hidden_size, out_features=hidden_size),
            nn.ReLU()
        )
    
    def split(self, vs):
        n, t, _ = vs.shape
        vs = torch.reshape(vs, shape=(n,t, self.num_header, -1)).transpose(1, 2)
        return vs


    def forward(self, x):
        """
        前向过程
        :param x: [N,T,E] 输入向量
        :return: [N,T,E] 输出向量
        """
        # 1. 获取qkv
        q = self.wq(x)  # [N,T,E]
        k = self.wk(x)
        v = self.wv(x)
        
        q = self.split(q)  # [n,t,e] --> [n,h,t,v]  e=h*v h是head数, v就是每个头中self-attention的维度大小
        k = self.split(k)  # [n,t,e] --> [n,h,t,v]
        v = self.split(v)  # [n,t,e] --> [n,h,t,v]

        # 2. 计算q和k之间的相关性 --> F函数
        # matmul([n,h,t,v],) --> [n,h,t,t] 每个样本每个self-attention/头中每个时刻和其它t个时刻相关性
        # torch.transpose(k, dim0=-2, dim1=-1)
        scores = torch.matmul(q, torch.permute(k, dims=(0, 1, 3, 2)))  # [n,h,t,t] 每个时刻和每个时刻的相关性
        n, h, _, _ = q.shape
        for i in range(n):
            for j in range(h):
                qij = q[i][j]  # [t,v]
                kij = k[i][j].T  # [v,t]
                scores[i][j]=torch.matmul(qij, kij)  # [t,t]

        # 3. 转换为权重
        alpha = torch.softmax(scores, dim=-1)  # [n,h,t,t]
        # 4. 值的合并
        # matmul([n,h,t,t],[n,h,t,v])->[n,h,t,v]
        v = torch.matmul(alpha, v)  # [n,h,t,v]

        # 5. 输入
        v = torch.permute(v, dims=(0,2,1,3))  # [n,h,t,v] --> [n,t,h,v]
        n, t, _, _ = v.shape
        v = v.contiguous().view(n, t, -1)  #[n,t,h,v] --> [n,t,e]
        v = self.wo(v)  # 多个头之间的特征组合合并
        return v


@torch.no_grad()
def t0():
    token_id = torch.tensor([
        [1, 3, 5],  # 一个样本, 三个时刻
        [1, 6, 3],
        [2, 3, 1],
        [5, 1, 2],
        [6, 1, 2]
    ])

    # 静态特征向量提取 Word2Vec EmbeddingLayer
    emb_layer = nn.Embedding(num_embeddings=10, embedding_dim=8)
    x1 = emb_layer(token_id)  # [2,3,4]
    print(x1[0][0])  # 第一个样本的第一个token对应的向量
    print(x1[1][0])
    print('='*100)

    atten = MultiHeadAttention(hidden_size=8, num_header=2)
    x2 = atten(x1)
    print(x2[0][0])
    print(x2[1][0])
    print(x2[1][1])


if __name__ == '__main__':
    t0()

Mask (掩膜)

截图_20231606071626.png
控制模型在计算注意力全重视对输入序列中的默写位置进行屏蔽或忽略,从而控制模型关注范围,提高性能和泛化能力。

屏蔽就是把某些位置置0

import torch
from torch import nn
# mask https://blog.csdn.net/weixin_40548136/article/details/118698301


class MaskedMultiHeadAttention(nn.Module):
    def __init__(self, hidden_size, num_header, masked=False):
        super(MaskedMultiHeadAttention, self).__init__()
        assert hidden_size % num_header == 0, f"head数目无法整除: {hidden_size}/{num_header}"

        self.hidden_size = hidden_size  # 向量维度大小, 即e
        self.num_header = num_header  # 头的数目

        self.wq = nn.Linear(in_features=hidden_size, out_features=hidden_size)
        self.wk = nn.Linear(in_features=hidden_size, out_features=hidden_size)
        self.wv = nn.Sequential(
            nn.Linear(in_features=hidden_size, out_features=hidden_size),
            nn.ReLU()
        )
        self.wo = nn.Sequential(
            nn.Linear(in_features=hidden_size, out_features=hidden_size),
            nn.ReLU()
        )
        self.masked = masked
    
    def split(self, vs):
        n, t, _ = vs.shape
        vs = torch.reshape(vs, shape=(n,t, self.num_header, -1)).transpose(1, 2)
        return vs


    def forward(self, x):
        """
        前向过程
        :param x: [N,T,E] 输入向量
        :return: [N,T,E] 输出向量
        """
        # 1. 获取qkv
        q = self.wq(x)  # [N,T,E]
        k = self.wk(x)
        v = self.wv(x)
        
        q = self.split(q)  # [n,t,e] --> [n,h,t,v]  e=h*v h是head数, v就是每个头中self-attention的维度大小
        k = self.split(k)  # [n,t,e] --> [n,h,t,v]
        v = self.split(v)  # [n,t,e] --> [n,h,t,v]

        # 2. 计算q和k之间的相关性 --> F函数
        # matmul([n,h,t,v],) --> [n,h,t,t] 每个样本每个self-attention/头中每个时刻和其它t个时刻相关性
        scores = torch.matmul(q, torch.permute(k, dims=(0, 1, 3, 2)))  # [n,h,t,t] 每个时刻和每个时刻的相关性

        # 3. 转换为权重
        alpha = torch.softmax(scores, dim=-1)  # [n,h,t,t]
        if self.masked:
            _, _, t, _ = scores.shape
            mask = torch.ones((t, t))  # mask只在训练时用, 用来增强模型健壮性
            mask = torch.triu(mask, diagonal=1) * -10000
            mask = mask[None][None]  # [t,t,1,1]
            scores = scores + mask

        # 4. 值的合并
        # matmul([n,h,t,t],[n,h,t,v])->[n,h,t,v]
        v = torch.matmul(alpha, v)  # [n,h,t,v]

        # 5. 输入
        v = torch.permute(v, dims=(0,2,1,3))  # [n,h,t,v] --> [n,t,h,v]
        n, t, _, _ = v.shape
        v = v.contiguous().view(n, t, -1)  #[n,t,h,v] --> [n,t,e]
        v = self.wo(v)  # 多个头之间的特征组合合并
        return v


@torch.no_grad()
def t0():
    token_id = torch.tensor([
        [1, 3, 5],  # 一个样本, 三个时刻
        [1, 6, 3],
        [2, 3, 1],
        [5, 1, 2],
        [6, 1, 2]
    ])

    # 静态特征向量提取 Word2Vec EmbeddingLayer
    emb_layer = nn.Embedding(num_embeddings=10, embedding_dim=8)
    x1 = emb_layer(token_id)  # [2,3,4]
    print(x1[0][0])  # 第一个样本的第一个token对应的向量
    print(x1[1][0])
    print('='*100)

    atten = MaskedMultiHeadAttention(hidden_size=8, num_header=2)
    x2 = atten(x1)
    print(x2[0][0])
    print(x2[1][0])
    print(x2[1][1])


if __name__ == '__main__':
    t0()

截图_20232206072219.png
Masked Multi-Head Attention 是用在多头注意力机制中的Mask的变体。
在某些任务中,需要限制模型只关注当前位置之前的信息,而不会受到未来信息的干扰。

BERT (Bidirectional Encoder Representations from Transformers)

BERT的全称是Bidirectional Encoder Representations from Transformers,即基于Transformer的双向编码器表示。它是由Google在2018年提出的一种预训练语言模型,可以用于各种自然语言处理任务,如文本分类、命名实体识别、问答等。
输入是一段无标签文本
使用 WordPiece 嵌入将单词分割为更细粒度的子词,并将每个子词生成一个嵌入向量。

WordPiece是一种用于文本处理的子词切分算法。它将单词切分成更小的子词单元,从而能够处理未登录词和词形变化等问题。WordPiece是Google在2015年提出的一种算法,被广泛应用于自然语言处理任务中。
WordPiece算法的核心思想是通过迭代地合并最频繁出现的子词单元来构建词表。具体来说,算法首先将每个单词切分成字符的序列,然后统计每个字符序列的出现频率。接着,算法会不断地合并出现频率最高的字符序列,直到达到预设的词表大小或者满足其他停止条件为止。最终得到的词表中包含了常见的单词和子词单元。
WordPiece算法的一个重要应用是在预训练语言模型中使用WordPiece作为词汇表的基本单位。预训练语言模型(如BERT)会使用大规模的未标记文本进行训练,其中也使用了WordPiece算法进行分词。这样可以使得模型能够更好地处理未登录词和词形变化,提高模型在各种自然语言处理任务中的表现。
总而言之,WordPiece是一种子词切分算法,通过将单词切分成更小的子词单元来解决未登录词和词形变化等问题。它被广泛应用于自然语言处理任务中,并在预训练语言模型中发挥了重要作用。

在输入序列的开始位置添加一个特别的 [cls] 标记,来表示整个序列的语义信息。
对每个子词,添加一个特殊标记,表示该子词的位置信息。
这样,每个子词都有三个嵌入向量:词嵌入(Word Embedding)、位置嵌入(Position Embedding)、段嵌入(Segment Embedding)
三个嵌入向量按元素相加,得到最终的输入嵌入向量,再将最终序列传入 transformer 进一步处理。

  • 25
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值