手撕Transformer(一)| 经典Positional_encoding 用法and代码详解


本节我们主要介绍Transformer的重要的第一部分

我会非常注重代码的解析~因为那是我最初学习的痛苦的来源,希望你们不受这些痛苦折磨哈哈哈

1 为什么要用?

Transformer本身并没有位置信息~该怎么理解呢?

具体来说,就是在不适用位置编码的情况下,更换词语的位置,输出结果是不会发生变化的

比如这两句话

“你喜欢她”和“她喜欢你”

两句哈意思差别很大(不过还是希望两句都成立,你们双向奔赴哈哈哈)

但是送到Transformer里面,出来的结果是一样的,因而是不行的,这会闹出笑话的

因而研究者们一拍桌子,不行,需要给Transformer输入嵌入位置编码

2 直接使用,直观感受

我们先直观感受一下Positional_encoding怎么使用

首先对于一批数据(往往是固定数量的句子,假设有4个句子,每个句子有12个字)我们取字符级Token,每个Token会被转换为512维度向量~

春风拂面桃花盛开景色宜人
夜幕降临繁星点点月色朦胧
书页翻动墨香四溢知识海洋
晨跑归来汗水淋漓心情愉悦

最后输入就是(4,12,512)分别代表的含义是4个句子,每个句子12个token,每个token是512维度的。

OK ,那么实际上位置编码就在原本这个输入的基础上加一个量PE,这个量怎么计算呢?大家肯定在很多文章上看到这样一个公式

在这里插入图片描述

其中的pos代表某一个token在一个句子中的位置,比如”春风拂面桃花盛开景色宜人“这句话中的春 pos=0,风 pos=1,佛 pos=2 ……以此类推

i代表某一个token的第i个维度,当i为偶数时,加sin,当i为奇数时候,加cos,d_model就是512维度的

可能还是有点抽象,我们马上举一个例子,在举这样一个例子之前,请大家思考一个问题

不同句子的位置编码一样吗?

实际上你会发现,是一样的!

所以我们只需要研究清楚一个句子的位置编码,计算一遍,然后给这一批次(batch)中每一个句子都加一次相同的位置编码即可

好滴,那我们聚焦于一个句子

如下图计算

在这里插入图片描述

注意我们是从0开始的,所以纵的0-11,横的0-512

然后一个句子是这样计算PE的,实际上每一个句子都是这样的~

这时候你可能懂了,然后也理解了计算方法

3 代码初探 写法一

你觉得比较简单,这不就是两个循环解决的事情吗哈哈哈

然后你可能会写出这样的代码(好吧,实际这是我写的代码)不过这是直观最容易想到的代码

import math
import torch
from torch import nn

input=torch.ones(4,10,512) #模拟一个输入,输入量都是1(实际中不可能都是1,只是为了简单起见)

def positional_encoding(input): #对单个句子做计算
    num=input.size(0)  #获得句子的token数,相当于上面的行数
    ndim=input.size(1) #获得每个的token维度,相当于上面的列数
    for k in range(num):
        for i in range(ndim):
            if i%2==0:
                input[k][i]+=math.sin(k/(10000.0**(2*i/ndim)))
            else:
                input[k][i]+=math.cos(k/(10000.0**(2*i/ndim)))
    return input.unsqueeze(0)
temp=[]
for x in input:
    temp.append(positional_encoding(x))
    output=torch.cat(temp,dim=0)
print(output.shape)
print(output)


这种写法很烂

直观来看

首先

1.每个句子的位置编码是一样的,我却多计算了好多遍

2.再者两层循环,Python的循环慢的要死

虽然这种写法很烂,但我还要给自己树一个大拇指

我们接下来学习大师的写法

4 高手代码 写法二

其实上面的代码典型的CPU思维,也就是没有用到矩阵,而我们知道深度学习最重要的就是并行计算,并行计算怎么实现,实际上也就是通过矩阵的方式实现,通过空间换时间

而高手是通过torch进行矩阵并行运算

看看官方代码

import math
import torch
from torch import nn

input=torch.ones(4,10,512)
#尽量减少循环的使用~
class PositionalEncoding(nn.Module):
    "Implement the PE function."

    def __init__(self, d_model, dropout, max_len=5000):
        super(PositionalEncoding, self).__init__()
        # Compute the positional encodings once in log space.
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2) *
                             -(math.log(10000.0) / d_model))
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        pe = pe.unsqueeze(0)
        self.register_buffer('pe', pe)
        self.dropout = nn.Dropout(p=dropout)

    def forward(self, x):
        x = x + self.pe[:, :x.size(1)].detach()
        return self.dropout(x)

positional_encoding=PositionalEncoding(64,0.1)
output=positional_encoding(input)
print(output.shape)
print(output)

虽然,短短几行代码,却凝缩了大智慧~

从上往下,我们依次讲解,首先我们还是模拟上面的例子,给一个输入(4,10,512)

4.1 PE矩阵的计算

1

大师申明了一个类,并将pe的计算放在了初始化函数里面,妙啊!这样只需要初始化时候计算一遍,之后就可以一直用了~不用每次前向传递的时候计算

2

首先 pe = torch.zeros(max_len, d_model) 初始化一个pe矩阵

position = torch.arange(0, max_len).unsqueeze(1) 这里生成了一个从0到可能的最大长度矩阵(5000),然后用unsqueeze函数增加一维度之后变为(1,5000)

position实际上就是上面原理中讲到的pos,我们写法一是通过循环得到pos的,写法二直接一个等差列矩阵生成pos 就很快

关于张量形状变换的总结,可以看我这篇博客

【pytorch进阶】| 各类张量形状变化函数总结对比分析,view,reshape,pernute,transpose,squeeze,unsqueeze-CSDN博客

3

div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model)) div_term 是计算分母一项,你可能发现分母不太一样和公式里的,是因为进行了指数对数变换,pos我们不需要考虑,前面已经计算过了,只考虑分母部分

具体如下

1 1000 0 2 i / d m o d e l = e − l n ( 1000 0 2 i / d m o d e l ) = e − 2 i / d m o d e l ∗ l n 10000 \frac{1}{10000^{2i/d_{model}}}=e^{-ln(10000^{2i/d_{model})}}=e^{-2i/d_{model}*ln10000} 100002i/dmodel1=eln(100002i/dmodel)=e2i/dmodelln10000 然后你对应代码看一看就可以理解了

这里为什么要折腾指数变换,其实我还是有一点疑惑的,有朋友了解可以在评论区告诉我,我之后懂了会补充回来,据说是为了稳定性?

4

接下来就通过矩阵切片操作

pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)

实现并行计算~

4.2 注册缓冲区

代码中有一句 是self.register_buffer(‘pe’, pe)

我查询后得知

register_buffer 注册缓冲区,缓冲区的作用:缓冲区通常用于存储模块的状态,但不被视为模型参数,缓冲下来后就不用每次都计算了。

例如,BatchNorm 的 running_mean 就是一个缓冲区,它记录了模块的状态。

在深度学习模型中,存储模块的状态是非常重要的。这些状态可能包括但不限于学习率、优化器的状态、BatchNorm层的运行平均值和方差等。这些状态对于模型的训练和推理都是非常重要的。

  1. 模型训练:在模型训练过程中,我们需要跟踪和更新这些状态。例如,BatchNorm层的运行平均值和方差会在每个batch训练时进行更新,这对于模型的训练是必要的。
  2. 模型保存和加载:当我们保存模型时,除了模型的参数,我们还需要保存这些状态。这样,当我们重新加载模型进行推理或者继续训练时,我们可以从上次训练结束的状态开始,而不是从头开始。
  3. 模型推理:在模型推理阶段,我们需要使用这些状态。例如,BatchNorm层在推理时会使用存储的运行平均值和方差,而不是当前batch的平均值和方差。

在大师的代码中,self.register_buffer('pe', pe)这行代码是在将位置编码(positional encoding)保存为模块的状态。这样,无论何时我们需要使用这个模块,位置编码都是可用的,而不需要每次都重新计算。

缓冲区默认是持久的,什么意思呢?就是会与参数一起保存在模块的 state_dict 属性中。当然,我们可以通过将 persistent 参数设置为 False 来改变这种行为。非持久的缓冲区不会包含在模块的 state_dict 中。

4.3 加入dropout 正则化

dropout正则化可以防止过拟合

将某一些位置的值置零

4.4 前向过程

利用广播机制为每一个句子都添加位置编码

5 本质原理

为什么这样做,为什么这样做位置编码很高效?

本质是因为 用cos,sin这样的方式既可以表现相对距离,又可以表现绝对距离

具体的数学推到可以看

Transformer升级之路:1、Sinusoidal位置编码追根溯源 - 科学空间|Scientific Spaces

6 思考

虽然这部分代码很短,却给我了很多思考

包括如何利用矩阵替代循环计算,高效利用广播机制等等,值得反复思考

  • 27
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Swin Transformer使用的是Learned Positional Encoding,如果要将其替换为Sinusoidal Positional Encoding,需要进行一些修改。 首先,可以定义一个Sinusoidal Positional Encoding的函数,如下所示: ```python import math import torch import torch.nn as nn class SinusoidalPositionalEmbedding(nn.Module): def __init__(self, d_model, max_len=512): super().__init__() self.d_model = d_model self.max_len = max_len pe = torch.zeros(max_len, d_model) position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] = torch.sin(position * div_term) pe[:, 1::2] = torch.cos(position * div_term) pe = pe.unsqueeze(0).transpose(0, 1) self.register_buffer('pe', pe) def forward(self, x): x = x * math.sqrt(self.d_model) seq_len = x.size(1) pe = self.pe[:seq_len, :] pe = pe.repeat(x.size(0), 1, 1) x = x + pe.to(x.device) return x ``` 然后,在Swin Transformer的构造函数中,将使用Learned Positional Encoding的部分替换为Sinusoidal Positional Encoding,如下所示: ```python import torch import torch.nn as nn from einops.layers.torch import Rearrange class SwinTransformer(nn.Module): def __init__(self, img_size=224, patch_size=4, in_chans=3, num_classes=1000, embed_dim=96, depths=[2, 2, 18, 2], num_heads=[3, 6, 12, 24], window_size=7, mlp_ratio=4., qkv_bias=True, qk_scale=None, drop_rate=0., attn_drop_rate=0., drop_path_rate=0.): super().__init__() norm_layer = nn.LayerNorm self.num_classes = num_classes self.num_features = self.embed_dim = embed_dim # stochastic depth decay rule dpr = [x.item() for x in torch.linspace(0, drop_path_rate, sum(depths))] # stochastic depth decay rule # patch embedding self.patch_embed = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_size, stride=patch_size) self.norm1 = norm_layer(embed_dim) # pos embedding self.pos_embed = SinusoidalPositionalEmbedding(embed_dim, max_len=(img_size//patch_size)**2+1) # swin transformer blocks self.blocks = nn.ModuleList([ SwinTransformerBlock(dim=embed_dim, num_heads=num_heads[i], window_size=window_size, shift_size=window_size // 2 if i == 0 else 0, mlp_ratio=mlp_ratio, qkv_bias=qkv_bias, qk_scale=qk_scale, drop=drop_rate, attn_drop=attn_drop_rate, drop_path=dpr[sum(depths[:i]):sum(depths[:i+1])]) for i in range(len(depths))]) # norm before classifier self.norm2 = norm_layer(embed_dim) # classification head self.head = nn.Linear(embed_dim, num_classes) if num_classes > 0 else nn.Identity() def forward_features(self, x): x = self.patch_embed(x) x = self.norm1(x) x = x.flatten(2).transpose(1, 2) x = self.pos_embed(x) for i, blk in enumerate(self.blocks): x = blk(x) x = self.norm2(x) return x def forward(self, x): x = self.forward_features(x) x = x.mean(dim=1) # global average pooling x = self.head(x) return x ``` 这样,就完成了Swin Transformer模型中Positional Encoding形式的替换。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值