Transformer学习(2)

这是Transformer的第二篇文章,上篇文章中我们了解了分词算法BPE,本文我们继续了解Transformer中的位置编码和核心模块——多头注意力。下篇文章就可以实现完整的Transformer架构。

位置编码

我们首先根据BPE算法得到文本切分后的子词标记,然后经过输入嵌入层将每个标记转换为对应的向量表示,但Transformer不再基于类似RNN循环的方式,而是可以一次为所有的标记进行建模,因此丢失了输入中单词之间的相对位置关系。

在真正喂给Transformer模型之前,一个重要的操作是为嵌入向量表示增加位置表示,即位置编码。位置编码可以通过学习得到也可以通过固定设置,这里介绍Transformer原始论文中使用的基于正弦函数和余弦函数的固定位置编码。

一个好的位置编码应该具有以下性质:

1. 每个时间步(位置)的编码应该唯一;
2. 任意两个时间步的距离应该与句子长度无关;
3. 取值应该是有界的;
我们从这几个方面来分析下Transformer中使用的位置编码:
在这里插入图片描述
其中pos表示标记所在的位置,假设取值从0~100;i代表维度,即位置编码的每个维度对应一个波长不同的正弦或余弦波,波长从2 π到10000 ⋅ 2 π成等比数列;d表示位置编码的最大维度,和词嵌入的维度相同,假设是512;

这里假设最长时间步(位置)为100,每个位置的编码都是一个512维度的向量。我们先来回顾下常规正余弦函数sin ⁡ ( x )和cos ⁡ ( x )的图像:
在这里插入图片描述
正余弦函数的图像如上图所示,显然它的取值是有界的,取值范围在[-1,+1]。但Transformer用的正弦函数的波长不同。
对于每个位置,由于我们有512个维度,因此我们有256对正弦值和余弦值,i的取值在[0,255]。假设考虑所有维度,计算位置pos处的位置编码向量每个元素(维度)的值:
在这里插入图片描述

对于位置0的编码为:

在这里插入图片描述

是一个交替0和1的向量;

对于位置1的编码为:

在这里插入图片描述
在这里插入图片描述

波长就是一个周期的距离,波长越长,走过一个周期越缓慢。单纯看这些数字没有意义,下面尝试可视化它们。

在这里插入图片描述
上图分别表示位置pos从0到512的过程中,不同波长的函数图像。上图左表示维度0波长2 π的图像,可以看到在0到1之间疯狂地变化;而上图右对应10000 ⋅ 2 π的波长,从0变化到0.06,波动非常小。
从这里我们可知满足了性质1和3,性质3好理解,取值在[-1,1]之间,是有界的。如果理解满足性质1呢?
假设我们想用二进制格式表示一个数字:
在这里插入图片描述
我们通过4位就可以表示最多到十进制15,我们可以发现不同位之间的变化率,第0位(红色)在每个数字上交替变化;第1位(蓝色)在每两个数字上重复;最高位(橙色)每八个数字上变化一次。
而Transformer不同波长(频率)的正余弦,所达到的效果是类似的。
在这里插入图片描述
或者可以理解为时钟上的指针(对应3个维度),波长(频率)对应指针的转速,秒针转速最快,就是第0位;时针转速最慢就是最高一位。在最高一位的周期内是不会重复的。所以性质3满足。

我们来看性质2,其实意思就是可以体现相对位置关系,pos + k的位置编码可以被位置pos \text{pos}pos(?)线性表示。这里需要用到三角函数公式:
在这里插入图片描述

在这里插入图片描述

对于pos + k的位置编码:

在这里插入图片描述

根据式( 3 )和( 4 )整理上式有:

在这里插入图片描述
在这里插入图片描述

这也是为什么作者要交替使用正余弦函数,而不仅仅使用其中一个。???

pos处的位置嵌入可以表示为:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
参考文章4给出位置之间内积的关系:
在这里插入图片描述

可以看到内积会随着相对位置的递增而减少,从而可以表示位置的相对距离。内积的结果是对称的,所以没有方向信息。

最后得到的位置编码需要和标记的词嵌入向量进行相加。
引用邱锡鹏老师关于问题为什么 Bert 的三个 Embedding 可以进行相加?的分析,来理解一下为什么可以相加。
文本可以看成是时序信号,一个时序的波可以用多个不同频率的正弦波叠加来表示,可能在神经网络中得到解耦,可能也不需要解耦。不管怎么,我们为词嵌入赋予了位置信息。下面先贴出代码实现:

class PositionalEncoding(nn.Module):
    def __int__(
        self, d_model: int = 512, dropout: float = 0.1, max_positions: int = 5000
    ) -> None:
        super().__init__()
        self.dropout = nn.Dropout(p=dropout)
        # pe (max_positions, d_model)
        pe = torch.zeros(max_positions, d_model)
        # position (max_positions, 1)
        # create position column
        position = torch.arange(0, max_positions).unsqueeze(1)
        # div_term(d_model/2)
        # calculate the divisor for positional encoding
        div_term = torch.exp(
            torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model)
        )

        #calculate sine values on even indices
        #position * div_term will be broadcast to (max_positions, d_model/2)
        pe[:, 0::2] = torch.sin(position * div_term)
        #calculate cosine values on odd indices
        pe[:, 1::2] = torch.cos(position * div_term)
        #add a batch dimension: pe (1, max_positions, d_model)
        pe = pe.unsqueeze(0)
        #buffers will not be trained
        self.register_buffer("pe", pe)

    def forward(self, x: Tensor) -> Tensor:
        """
        Args:
            x (Tensor): (batch_size, seq_len, d_model)embeddings

        Returns:
            Tensor: (batch_size, seq_len, d_model)
        """

        #x.size(1) is the max sequence length
        x = x + self.pe[:, : x.size(1)]
        return self.dropout(x)

在这里插入图片描述
最后一项就是代码的实现形式,14行代码得到一个d_model/2维度的行向量;position被定义成一个max_len维度的列向量,position * div_term会被广播成(max_len, d_model/2)。

然后根据公式(1)和(2),分别为偶数和奇数维度赋值正余弦项;最后扩充pe的维度使得维度个数和输入一致。

最后通过register_buffer将计算出来的pe保存成模型的buffer而不是parameter,buffer的特点就是不需要更新。

多头注意力

自注意力

首先回顾下注意力机制,注意力机制允许模型为序列中不同的元素分配不同的权重。而自注意力中的"自"表示输入序列中的输入相互之间的注意力,即通过某种方式计算输入序列每个位置相互之间的相关性。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

缩放点积注意力

从文章注意力机制中我们知道有很多种计算注意力的方式,最高效的是点积注意力,即两个输入之间做点积。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这种计算注意力的方式和我们在seq2seq中遇到的不同,seq2seq是用解码器的隐状态与编码器所有时刻的输出计算,而自注意力是输入自己与自己进行计算。参与计算的只是输入本身。
但Transformer使用的是更加复杂一点的计算方式,来捕获更加丰富的信息。

在Transformer计算注意力的过程中,每个输入扮演了三种不同角色:

1. Query: 与所有的输入进行比较,为当前关注的点。
2. Key:作为与Query进行比较的角色,用于计算和Query之间的相关性。
3. Value:用于计算当前注意力关注点的输出,根据注意力权重对不同的Value进行加权和。
在这里插入图片描述

如果把注意力过程类比成搜索的话,那么假设在百度中输入"自然语言处理是什么",那么Query就是这个搜索的语句;Key相当于检索到的网页的标题;Value就是网页的内容。

在这里插入图片描述
Query和Key是用于比较的,Value是用于提取特征的。通过将输入映射到不同的角色,使模型具有更强的学习能力。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
计算自注意力的第一步就是,为编码器层的每个输入,都创建三个向量,分别是query向量,key向量和value向量。正如我们上面所说,每个向量都是乘上一个权重矩阵得到的,这些权重矩阵是随模型一起训练的。进行线性映射的目的是转换向量的维度,转换成一个更小的维度。原文中是将512维转换为64 6464维。
在这里插入图片描述
第二步是计算注意力得分,假设我们想计算单词“Thinking”的注意力得分,我们需要对输入序列中的所有单词(包括自身)都进行某个操作。得到单词“Thinking”对于输入序列中每个单词的注意力得分,如果某个位置的得分越大,那么在生成编码时就越需要考虑这个位置。或者说注意力就是衡量q和k的相关性,相关性越大,那么在得到最终输出时,k对应的v在生成输出时贡献也越大。
那么这里所说的操作是什么呢?其实很简单,就是点乘。表示两个向量在多大程度上指向同一方向。类似余弦相似度,除了没有对向量的模进行归一化。
在这里插入图片描述
在这里插入图片描述
第三步和第四步 是进行进行缩放,然后经过softmax函数,使得每个得分都是正的,且总和为1。

经过Softmax之后的值就可以看成是一个权重了,也称为注意力权重。决定每个单词在生成这个位置的编码时能够共享多大程度。

第五步 用每个单词的value向量乘上对应的注意力权重。这一步用于保存我们想要注意单词的信息(给定一个很大的权重),而抑制我们不关心的单词信息(给定一个很小的权重)。

==第六步 累加第五步的结果,得到一个新的向量,也就是自注意力层在这个位置(这里是对于第一个单词“Thinking”来说)的输出。==举一个极端的例子,假设某个单词的权重非常大,比如是1,其他单词都是0,那么这一步的输出就是该单词对应的value向量。
在这里插入图片描述
这就是计算第一个单词的自注意力输出完整过程。自注意力层的魅力在于,计算所有单词的输出可以通过矩阵运算一次完成。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

多头注意力

上面介绍的缩放点积注意力把原始的x映射到不同的空间后,去做注意力。每次映射相当于是在特定空间中去建模特定的语义交互关系,类似卷积中的多通道可以得到多个特征图,那么多个注意力可以得到多个不同方面的语义交互关系。可以让模型更好地关注到不同位置的信息,捕捉到输入序列中不同依赖关系和语义信息。有助于处理长序列、解决语义消歧、句子表示等任务,提高模型的建模能力。
在这里插入图片描述
得到这些多头注意力的组合以后,再把它们拼接起来,然后通过一个线性变化映射回原来的维度,保证输入和输出的维度一致。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

class MultiHeadAttention(nn.Module):
    def __init__(
        self,
        d_model: int = 512,
        n_heads: int = 8,
        dropout: float = 0.1,
    ) -> None:
        """
        Args:
            d_model (int, optional): dimension of embeddings. Defaults to 512.
            n_heads (int, optional): number of self attention heads. Defaults to 8.
            dropout (float, optional): dropout ratio. Defaults to 0.1.
        """
        super().__init__()
        assert d_model % n_heads == 0
        self.d_model = d_model
        self.n_heads = n_heads
        self.d_keys = d_model // n_heads # dimension of every head

        self.q = nn.linear(d_model, d_model) # query matrix
        self.k = nn.linear(d_model, d_model) # key matrix
        self.v = nn.linear(d_model, d_model) # value matrix
        self.concat = nn.linear(d_model, d_model) #output
        
        self.dropout = nn.Dropout(dropout)

    def split_heads(self, x:Tensor, is_key:bool = False) -> Tensor:
        batch_size = x.size(0)
        # x(batch_size, seq_len, n_heads, d_key)
        x = x.view(batch_size, -1, self.n_heads, self.d_keys)
        if is_key:
            # (batch_size, n_heads, d_key, seq_len)
            return x.permute(0, 1, 2, 3)

        # (batch_size, n_heads, seq_len, d_key)
        return x.transpose(1, 2)

    def merge_heads(self, x:Tensor) -> Tensor:
        x = x.transpose(1, 2).contiguous().view(x.size(0), -1, self.d_model)

        return x

    def attention(
        self,
        query: Tensor,
        key: Tensor,
        value: Tensor,
        mask: Tensor = None,
        keep_attentions: bool = False,
    ):
        scores = torch.matmul(query, key) / math.sqrt(self.d_key)

        if mask is not None:
            # Fill those positions of product as -1e9 where mask positions are 0, because exp(-1e9) will get zero.
            # Note that we cannot set it to negative infinity, as there may be a situation where nagative infinity is divided by negative infinity.
            scores = scores.masked_fill(mask == 0, -1e9)

        # weights (batch_size, n_heads, q_length, k_length)
        weights = self.dropout(torch.softmax(scores, dim=-1))
        # (batch_size, n_heads, q_length, k_length)  x  (batch_size, n_heads, v_length, d_key) -> (batch_size, n_heads, q_length, d_key)
        # assert k_length == v_length
        # attn_ouput (batch_size, n_heads, q_length, d_key)
        attn_ouput = torch.matmul(weights, value)

        if keep_attentions:
            self.weights = weights
        else:
            del weights
        
        return attn_ouput
    
    def forward(
        self,
        query: Tensor,
        key: Tensor,
        value: Tensor,
        mask: Tensor = None,
        keep_attentions: bool = False,
    ) -> Tuple[Tensor, Tensor]:
        
        """

        Args:
            query(Tensor): (batch_size, q_length, d_model)
            key(Tensor): (batch_size, k_length, d_model)
            value(Tensor): (batch_size, v_length, d_model)
            mask(Tensor, optional): mask for padding or decoder. Defaults to None.
            keep_attentions(bool): whether keep attention weigths or not. Defaults to Flase.

        Returns:
            output(Tensor): (batch_size, q_length, d_model) attention output
        
        """
        query, key, value = self.q(query), self.k(key), self.v(value)
        query, key, value =(
            self.split_heads(query),
            self.split_heads(key, is_key=True)
            self.split_heads(value),
        )

        attn_output = self.attention(query, key, value, mask, keep_attentions)
        
        del query
        del key
        del value

        # Concat
        concat_output = self.merge_heads(attn_output)
        # the final linear
        # output (batch_szie, q_length, d_model)
        output = self.concat(concat_output)

        return output

在forward()中,首先利用三个线性变换分别计算query,key,value矩阵(后续文章GPT实现中可以看到这个三个线性编变换也可以合并成一个)。接着拆分成多个头,传给attention()计算多头注意力,通过keep_attentions参数可以指定是否保存注意力权重,后续可以进行观察。然后合并多头注意力的结果。最后经过一个用作拼接的线性层。

注意力这里的拆分和合并其实都是reshape操作,在代码的最后删除掉不需要的引用,以帮助GC释放GPU缓存。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

baremaps 部署

参考:https://baremaps.apache.org/documentation/ 一、基础环境 1、安装 JDK 版本需要至少 Java 17 下载:https://www.oracle.com/cn/java/technologies/downloads/ tar -zxf jdk-17_linux-x64_bin.tar.gz -C /usr/local cd /usr/local mv jdk-17.…

centos安装vscode的教程

centos安装vscode的教程 步骤一:打开vscode官网找到历史版本 历史版本链接 步骤二:找到文件下载的位置 在命令行中输入(稍等片刻即可打开): /usr/share/code/bin/code关闭vscode后,可在应用程序----编程…

商品最大价值-第13届蓝桥杯选拔赛Python真题精选

[导读]:超平老师的Scratch蓝桥杯真题解读系列在推出之后,受到了广大老师和家长的好评,非常感谢各位的认可和厚爱。作为回馈,超平老师计划推出《Python蓝桥杯真题解析100讲》,这是解读系列的第77讲。 商品最大价值&…

在windows操作系统上安装MariaDB

最近收到关于数据库在哪里看的评论,所以就一不做二不休,把安装数据库的步骤写一篇文章吧。 这篇文章介绍如何在windows上完成MariaDB-10.6.5版本的安装,对应MySQL-8.x版本。 第一步:下载安装包 通过以下网盘链接下载MariaDB-10.6…

免杀基本知识,shellcode混淆免杀

一、shellcode分析及免杀的必要性 shellcode是一段十六进制的机器码,插入内存后会被翻译成为CPU的指令,用于执行相关操作。渗透中的shellcode的主要功能就是反弹shell。将shellcode编译成为exe文件后,执行文件主要进行以下三个操作&#xff…

若依:mybatis查询的结果未映射到实体类报null

开启驼峰命名转换: mapUnderscoreToCamelCase: true 我的是mtybatis配置开启驼峰命名转换不生效,还需要在MyBatisConfig中配置 // 配置mybatis自动转驼峰 生效 sessionFactory.getObject().getConfiguration().setMapUnderscoreToCamelCase(true)&#x…

2041:【例5.9】新矩阵

#include <iostream> using namespace std; int main(){const int N 21;//几行几列 int g[N][N] {};int n 0;cin >> n;for (int i 1; i < n; i){for (int j 1; j < n; j){// 输入到几行几列 cin >> g[i][j];if (i j || i j n 1){//如果是这种…

六西格玛绿带考试攻略:自学VS报班?一文帮你理清思路

近年来&#xff0c;六西格玛绿带作为质量管理领域的重要认证&#xff0c;已经成为许多企业和个人追求高质量、高效率的必备证书。然而&#xff0c;面对即将到来的六西格玛绿带考试&#xff0c;很多人都会陷入一个纠结的境地&#xff1a;究竟是选择自学备考&#xff0c;还是报名…

C++并发之线程(std::thread)

目录 1 概述2 使用实例3 接口使用3.1 construct3.2 assigns3.3 get_id3.4 joinable3.5 join3.6 detach3.7 swap3.8 hardware_concurrency 1 概述 Thread类来表示执行的各个线程。   执行线程是指可以在多线程环境中与其他此类序列同时执行的指令序列&#xff0c;同时共享相同…

Go 语言的函数详解:语法、用法与最佳实践

在 Go 语言的世界里&#xff0c;函数是构建和维护任何应用程序的基石。不仅因为它们提供了一种将大问题划分为更小、更易管理部分的方法&#xff0c;而且还因为它们在 Go 程序中扮演着至关重要的角色。从简单的工具函数到复杂的系统级调用&#xff0c;理解和利用 Go 的函数特性…

企业因未安全保存个人信息被罚:警示网络数据安全重要性

网络攻击的隐蔽性越来越强&#xff0c;对网络安全提出了更高的要求。在进行等保测试时&#xff0c;网络运营商能够对系统的安全保护状况有一个大致的认识&#xff0c;并对系统内部和外部都有可能出现的安全问题进行分析&#xff0c;并对其进行加固和修正&#xff0c;以此来增强…

GPT-4与GPT-4O的区别详解:面向小白用户

1. 模型介绍 在人工智能的语言模型领域&#xff0c;OpenAI的GPT-4和GPT-4O是最新的成员。这两个模型虽然来源于相同的基础技术&#xff0c;但在功能和应用上有着明显的区别。 GPT-4&#xff1a;这是一个通用型语言模型&#xff0c;可以理解和生成自然语言。无论是写作、对话还…

全新STC12C5A60S2单片机+LCD19264大屏万年历农历生肖节气节日显示+闹钟+温湿度+台灯

资料下载地址&#xff1a;全新STC12C5A60S2单片机LCD19264大屏万年历农历生肖节气节日显示闹钟温湿度台灯 这是旧版 退役拆解了 新版 与电路图所示 共设置4个按键 短按开关台灯 加减键调光 长按进入菜单 1.台灯 加入PCA PWM 调光 STC12C5A60S2的PCA PWM非常好用 设置简单无极…

文件夹突变解析:类型变文件的数据恢复与预防

在数字化时代&#xff0c;文件夹作为我们存储和组织数据的基本单元&#xff0c;其重要性不言而喻。然而&#xff0c;有时我们可能会遇到一种令人困惑的情况——文件夹的类型突然变为文件&#xff0c;导致无法正常访问其中的内容。这种现象不仅会影响我们的工作效率&#xff0c;…

如何把几个pdf文件合成在一个pdf文件

PDF合并&#xff0c;作为一种常见的文件处理方式&#xff0c;无论是在学术研究、工作汇报还是日常生活中&#xff0c;都有着广泛的应用。本文将详细介绍PDF合并的多种方法&#xff0c;帮助读者轻松掌握这一技能。 打开 “轻云处理pdf官网” 的网站&#xff0c;然后上传pdf。 pd…

dnf手游版游玩感悟

dnf手游于5月21号正式上线&#xff0c;作为一个dnf端游老玩家&#xff0c;并且偶尔上线ppk&#xff0c;自然下载了手游版&#xff0c;且玩了几天。 不得不说dnf手游的优化做到了极好的程度。 就玩法系统这块&#xff0c;因为dnf属于城镇地下城模式&#xff0c;相比…

神经网络是什么?有什么作用?

人工智能是当前的热门科技领域&#xff0c;在自动驾驶、金融服务、智能家居、零售和电商、工业制造、医疗领域、教育领域、交通领域、娱乐领域、能源管理、农业、航空航天等很多领域都有越来越多的应用。 发展人工智能&#xff0c;离不开算力&#xff08;芯片&#xff09;、算…

【Python】 Python装饰器的魔法:深入理解functools.wraps

基本原理 在Python中&#xff0c;装饰器是一种设计模式&#xff0c;用于修改或增强函数或方法的功能。functools.wraps是一个装饰器工厂&#xff0c;它用来帮助我们保持被装饰函数的元数据&#xff0c;比如函数的名字、文档字符串等。 当你创建一个装饰器时&#xff0c;你可能…

进口泰国榴莲注意事项 | 国际物流运输服务 | 箱讯科技

进口泰国榴莲&#xff0c;这个看似简单的行为背后其实隐藏着许多需要注意的细节和相关费用。下面小编带大家了解一下有哪些细节和费用需要注意。 01清关费用 进口泰国榴莲涉及的清关费用包括&#xff1a; 国外提货成本、港口服务费、报关手续费。 国际运输费&#xff0c;可能…

《幸福》期刊杂志投稿发表

《幸福》杂志是由国家新闻出版总署批准&#xff0c;武汉出版社主管&#xff0c;武汉市妇联和武汉出版社联合主办&#xff0c;面向全国发行的人文社科综合期刊。办刊宗旨&#xff1a;宣传普及科学知识及科学方法的研究&#xff1b;倡导新型的人际关系&#xff0c;推介健康的家庭…