pytorch系列6——模型层(CNN,RNN, Transformer...)

本文以pytorch1.10进行解读:torch — PyTorch 1.10 documentation

文本的操作在github上都有Shirley-Xie/pytorch_exercise · GitHub,且有运行结果。

本文还需更改,部分未完成。先发布只为了方便后面发布。

1.基础层

1.1全连接层

torch.nn.Linear(in_features, # 输入的神经元个数
           out_features, # 输出神经元个数
           bias=True # 是否包含偏置
           )

是一个线性变换函数:在这里插入图片描述。Linear()函数通常用于设置网络中的全连接层

x = torch.randn(8, 3)  # 输入样本
fc = torch.nn.Linear(3, 5)  # 3为输入样本大小,5为输出样本大小
output = fc(x)
print('fc.weight.shape:\n ', fc.weight.shape, fc.weight)
print('fc.bias.shape:\n', fc.bias.shape)
print('output.shape:\n', output.shape)

ans = torch.mm(x, torch.t(fc.weight)) + fc.bias  # 计算结果与fc(x)相同
print('ans.shape:\n', ans.shape)

print(torch.equal(ans, output))

结果:

fc.weight.shape:
  torch.Size([5, 3]) Parameter containing:
tensor([[ 0.4531, -0.2219, -0.0119],
        [-0.4232, -0.2946, -0.0595],
        [ 0.1261,  0.4851, -0.1614],
        [-0.4154, -0.1646, -0.2402],
        [ 0.1123, -0.0469,  0.1111]], requires_grad=True)
fc.bias.shape:
 torch.Size([5])
output.shape:
 torch.Size([8, 5])
ans.shape:
 torch.Size([8, 5])
True

        nn.linear(3,5)其权重的shape为(5,3),所以x与其相乘时,用torch.t求了nn.linear的转置,这样(8,3)*(3,5)得到全连接层后的输出维度(8,5),结果也与fc(x)验证是一致的, torch.mm就是数学上的两个矩阵 相乘。

1.2 归一化层

公式: y = \frac{x - \mathrm{E}[x]}{ \sqrt{\mathrm{Var}[x] + \epsilon}} * \gamma + \beta

  •  结构化数据的BatchNorm1D归一化 :结构化数据的主要区分度来自每个样本特征在全体样本中的排序,将全部样本的某个特征都进行相同的放大缩小平移操作,样本间的区分度基本保持不变,所以结构化数据可以做BatchNorm,但LayerNorm会打乱全体样本根据某个特征的排序关系,引起区分度下降
  • 图片数据的各种归一化(一般常用BatchNorm2D):图片数据的主要区分度来自图片中的纹理结构,所以图片数据的归一化一定要在图片的宽高方向上操作以保持纹理结构,此外在Batch维度上操作还能够引入少许的正则化,对提升精度有进一步的帮助。
  • 文本数据的LayerNorm归一化 :文本数据的主要区分度来自于词向量(Embedding向量)的方向,所以文本数据的归一化一定要在 特征(通道)维度上操作 以保持 词向量方向不变。此外文本数据还有一个重要的特点是不同样本的序列长度往往不一样,所以不可以在Sequence和Batch维度上做归一化,否则将不可避免地让padding位置对应的向量变成非零向量。图中蓝色部分为一个句子的表达。
  • 可自适应学习的归一化SwitchableNorm:可应用于各种场景且有一定的效果提升,将BN、LN、IN结合,赋予权重,让网络自己去学习归一化层应该使用什么方法。

归一化作用:

        任何归一化的意义都是为了让使用norm的网络的输入的数据分布变得更好,也就是转换为标准正态分布,数值进入敏感度区间,以减缓梯度消失,从而更容易训练。

1. 加快收敛速度。数据分布一样训练容易收敛。

2.防止梯度爆炸和梯度消失。会限制输出范围,不会让梯度过小或者过大。

3. 防止过拟合。batch随机抽选。

1.2.1 BatchNorm2d

batch_size, channel, height, width = 32, 16, 128, 128

tensor = torch.arange(0,32*16*128*128).view(32,16,128,128).float() 

bn = nn.BatchNorm2d(num_features=channel,affine=False)
bn_out = bn(tensor)


channel_mean = torch.mean(bn_out[:,0,:,:]) 
channel_std = torch.std(bn_out[:,0,:,:])
print("channel mean:",channel_mean.item())
print("channel std:",channel_std.item())
#结果
channel mean: 1.1920928955078125e-07
channel std: 1.0000009536743164

1.2.2 LayerNorm

batch_size, sequence, features = 32, 100, 2048
tensor = torch.arange(0,32*100*2048).view(32,100,2048).float() 
ln = nn.LayerNorm(normalized_shape=[features],
                  elementwise_affine = False)
ln_out = ln(tensor)

token_mean = torch.mean(ln_out[0,0,:]) 
token_std = torch.std(ln_out[0,0,:])
print("token_mean:",token_mean.item())
print("token_mean:",token_std.item())
# 结果
token_mean: -5.8673322200775146e-08
token_mean: 1.0002442598342896

1.2.3 问答

1. BatchNorm放在激活函数前还是激活函数后?

        原始论文认为将BatchNorm放在激活函数前效果较好,后面的研究一般认为将BatchNorm放在激活函数之后更好。

2. BatchNorm在训练过程和推理过程的逻辑是否一样?

不一样!训练过程BatchNorm的均值和方差和根据mini-batch中的数据估计的,而推理过程中BatchNorm的均值和方差是用的训练过程中的全体样本估计的。因此预测过程是稳定的,相同的样本不会因为所在批次的差异得到不同的结果,但训练过程中则会受到批次中其他样本的影响所以有正则化效果。

3. BatchNorm的精度效果与batch_size大小有何关系? 

如果受到GPU内存限制,不得不使用很小的batch_size,训练阶段时使用的mini-batch上的均值和方差的估计和预测阶段时使用的全体样本上的均值和方差的估计差异可能会较大,效果会变差。这时候,可以尝试LayerNorm或者GroupNorm等归一化方法。

4. 为什么BN层一般用在线性层和卷积层后面,而不是放在非线性单元后?

        因为非线性单元的输出形状会在训练的过程中变化,归一化无法消除它的方差。相反全连接和卷积的输出一般是对称的,非稀疏的一个分布,更加类似于高斯分布,对归一化产生更加稳定的分布。

5. 为什么transformer用Layer Norm?不用BN?

        如果在同一维度内进行normalization,那么在这个维度内,相对大小有意义的,是可以比较的;但是在normalization后的不同的维度之间,相对大小这是没有意义的。

        BN(batch normalization)广泛应用于CV,针对同一特征,以跨样本的方式开展归一化,也就是对不同样本的同一通道间的所有像素值进行归一化,因此不会破坏不同样本同一特征之间的关系,毕竟“减均值,除标准差”只是一个平移加缩放的线性操作。这一性质进而决定了经过归一化操作后,样本之间仍然具有可比较性。但是,特征与特征之间的不再具有可比较性,也就是所说的“舍弃了除此维度之外其他维度的其他信息”。

NLP中不用BN,而用LN呢?

  • 对不同样本同一特征的信息进行归一化没有意义:
    • 三个样本(为中华之崛起而读书;我爱中国;母爱最伟大)中,“为”、“我”、“母”归一到同一分布没有意义。
  • 舍弃不了BN中舍弃的其他维度的信息,也就是同一个样本的不同维度的信息:
    • “为”、“我”、“母”归一到同一分布后,第一句话中的“为”和“中”就没有可比性了,何谈同一句子之间的注意力机制?

CV中:

  • 对不同样本同一特征(channel)的信息进行归一化有意义:
    • 因为同一个channel下的所有信息都是遵循统一规则下的大小比较的,比如黑白图中越白越靠近255,反之越黑越靠近0
  • 可以舍弃其他维度的信息,也就是同一个样本的不同维度间(channel)的信息:
    • 举例来说,RGB三个通道之间互相比较意义不大

1.3 nn.identity

A placeholder identity operator that is argument-insensitive.不区分参数的占位符标识运算符。

        输入是啥,直接给输出,不做任何的改变。百度翻译,其实意思就是这个网络层的设计是用于占位的,即不干活,只是有这么一个层,放到残差网络里就是在跳过连接的地方用这个层,显得没有那么空虚!

2. 卷积层

        介绍下卷积的几种:

普通卷积:普通卷积的操作分成3个维度,在空间维度(H和W维度)是共享卷积核权重滑窗相乘求和(融合空间信息),在输入通道维度是每一个通道使用不同的卷积核参数并对输入通道维度求和(融合通道信息),在输出通道维度操作方式是并行堆叠(多种),有多少个卷积核就有多少个输出通道。

空洞卷积(膨胀卷积 ):和普通卷积相比,空洞卷积可以在保持较小参数规模的条件下增大感受野,常用于图像分割领域。其缺点是可能产生网格效应,即有些像素被空洞漏过无法利用到,可以通过使用不同膨胀因子的空洞卷积的组合来克服该问题。参考文章:3D视觉开发者社区

        上图展示了空洞卷积的过程,其卷积核大小为3×3,步长s=1,r=2。这个r是膨胀因子。当r=1时,表示卷积核各元素之前没有空隙,即相邻两个元素间位置相差1,就是普通卷积,所以广义上说,普通的卷积是一种特殊的空洞卷积;当r=2时,表示卷积核各元素之前有一个空隙,即相邻两个元素间位置相差2。


分组卷积 :和普通卷积相比,分组卷积将输入通道分成g组,卷积核也分成对应的g组,每个卷积核只在其对应的那组输入通道上做卷积,最后将g组结果堆叠拼接。由于每个卷积核只需要在全部输入通道的1/g个通道上做卷积,参数量降低为普通卷积的1/g。分组卷积要求输入通道和输出通道数都是g的整数倍。参考文章:https://zhuanlan.zhihu.com/p/65377955

深度可分离卷积:深度可分离卷积的思想是先用g=m(输入通道数)的分组卷积逐通道作用融合空间信息,再用n(输出通道数)个1乘1卷积融合通道信息。 其参数量为 (m×k×k)+ n×m, 相比普通卷积的参数量 m×n×k×k 显著减小 。


转置卷积 :一般的卷积操作后会让特征图尺寸变小,但转置卷积(也被称为反卷积)可以实现相反的效果,即放大特征图尺寸。对两种方式理解转置卷积,第一种方式是转置卷积是一种特殊的卷积,通过设置合适的padding的大小来恢复特征图尺寸。第二种理解基于卷积运算的矩阵乘法表示方法,转置卷积相当于将卷积核对应的表示矩阵做转置,然后乘上输出特征图压平的一维向量,即可恢复原始输入特征图的大小。参考文章:https://zhuanlan.zhihu.com/p/115070523


上采样层 :除了使用转置卷积进行上采样外,在图像分割领域更多的时候一般是使用双线性插值的方式进行上采样,该方法没有需要学习的参数,通常效果也更好,除了双线性插值之外,还可以使用最邻近插值的方式进行上采样,但使用较少。

2.1 卷积

  •  nn.Conv1d:普通一维卷积,常用于文本。参数个数 = 输入通道数×卷积核尺寸(如3)×卷积核个数 + 卷积核尺寸(如3)
  • nn.Conv2d:普通二维卷积,常用于图像。参数个数 = 输入通道数×卷积核尺寸(如3乘3)×卷积核个数 + 卷积核尺寸(如3乘3)。通过调整dilation参数大于1,可以变成空洞卷积,增加感受野。 通过调整groups参数不为1,可以变成分组卷积。分组卷积中每个卷积核仅对其对应的一个分组进行操作。 当groups参数数量等于输入通道数时,相当于tensorflow中的二维深度卷积层tf.keras.layers.DepthwiseConv2D。 利用分组卷积和1乘1卷积的组合操作,可以构造相当于Keras中的二维深度可分离卷积层tf.keras.layers.SeparableConv2D。前三个参数必写(输入通道,输出通道,卷积核大小)
  •  nn.Conv3d:普通三维卷积,常用于视频。参数个数 = 输入通道数×卷积核尺寸(如3乘3乘3)×卷积核个数 + 卷积核尺寸(如3乘3乘3) 。

参数:

in_channels: 输入通道数

out_channels: 输出通道数

kernel_size: 卷积核尺寸, 这个代表着卷积核的大小

stride: 步长, 这个指的卷积核滑动的时候,每一次滑动几个像素。 下面看个动图来理解步长的概念:左边那个的步长是1, 每一次滑动1个像素,而右边的步长是2,会发现每一次滑动2个像素。

padding: 填充个数, 通常用来保持输入和输出图像的一个尺寸的匹配。 经过卷积之后,输出图像 分辨率会变低,并且我们会发现这种情况卷积的时候边缘部分的像素参与计算的机会比较少。所以加入考虑padding的填充方式,这个也比较简单,就是在原输入周围加入像素,这样就可以保证输出的图像尺寸分辨率和输入的一样,并且边缘部分的像素也受到同等的关注了。

dilation: 孔洞卷积大小。孔洞卷积就可以理解成一个带孔的卷积核, 常用于图像分割任务,主要功能就是提高感受野。也就是输出图像的一个参数,能看到前面图像更大的一个区域。一般我使用dilation=0。

groups: 分组卷积设置, 分组卷积常用于模型的轻量化, 可以减少模型参数。

2.1.1 尺寸计算方式

1. 没有padding:out_{size} = \frac{In_{size}-kernel_{size}}{stride} +1

2. 如果有padding的话:out_{size} = \frac{In_{size}+2\times padding_{size}-kernel_{size}}{stride} + 1

3. 如果再加上孔洞卷积的话:out_{size} = \frac{In_{size}+2\times padding_{size}-dilation_{size}\times (kernel_{size}-1)-1}{stride} + 1

2.1.2 实例

1. 没有padding:

import torch
import torch.nn as nn

# 输入是一个N=20,C=16,H=50,W=100的向量
m = nn.Conv2d(16, 33, 3, stride=2)
input = torch.randn(20, 16, 50, 100)
output = m(input)

print(output.size())
  • 在nn.Conv2d()中第一个参数是输入的通道数要和输入相同(16)。在nn.Conv2d()中第二个参数表示输出的通道数。输出中N=20不变,C=33。通过3X3的卷积核、步长为2,50X100的输入变成了24=(50-3)//2+1, 49=(100-3)//2+1。
torch.Size([20, 33, 24, 49])

2. 有padding 

import torch
import torch.nn as nn

m = nn.Conv2d(16, 33, (3, 5), stride=2, padding=(4, 2))
input = torch.randn(20, 16, 50, 100)
output = m(input)

print(output.size())

  • 这里添加了padding=(4,2),表示在左右方向上添加4圈0填充,在上下方向上添加2圈0,相当于输入是58X104(原来的输入是50X100)。计算也就是增加后的数和无padding计算的一样。

3. 空洞卷积

import torch
import torch.nn as nn

m = nn.Conv2d(16, 33, (3, 5), stride=2, dilation=(3, 1))
input = torch.randn(20, 16, 50, 100)
output = m(input)

print(output.size())
# torch.Size([20, 33, 22, 48])

这里的dilation=(3,1)表示横排隔3个,竖排1个,(50-3*(3-1)-1 )//2 + 1= 22,也就是将卷积核扩充三倍。

2.2 转置卷积

2.3 采样

2.4 池化层

池化运算:对信号进行“收集”并“总结”, 类似水池收集水资源, 因而美其名曰池化层。

  • 收集: 多变少,图像的尺寸由大变小
  • 总结: 最大值/平均值

本质和卷积一样,区别是卷积核计算方式不一样,最大池化就是卷积核找最大值。

 对二维信号(图像)进行最大值池化。

  • kernel_size: 池化核尺寸
  • stride: 步长
  • padding: 填充个数
  • dilation: 池化核间隔大小
  • ceil_mode: 尺寸向上取整
  • return_indices: 记录池化像素索引
# pool of square window of size=3, stride=2
m = nn.MaxPool2d(3, stride=2)
# pool of non-square window
input = torch.randn(20, 16, 50, 100)
m(input).shape

结果和卷积的那个例子一样

torch.Size([20, 16, 24, 49])

 3. 循环网络层

  • nn.LSTM:长短记忆循环网络层【支持多层】。最普遍使用的循环网络层。具有携带轨道,遗忘门,更新门,输出门。可以较为有效地缓解梯度消失问题,从而能够适用长期依赖问题。设置bidirectional = True时可以得到双向LSTM。需要注意的时,默认的输入和输出形状是(seq,batch,feature), 如果需要将batch维度放在第0维,则要设置batch_first参数设置为True。
  • nn.GRU:门控循环网络层【支持多层】。LSTM的低配版,不具有携带轨道,参数数量少于LSTM,训练速度更快。
  • nn.RNN:简单循环网络层【支持多层】。容易存在梯度消失,不能够适用长期依赖问题。一般较少使用。
  • nn.LSTMCell:长短记忆循环网络单元。和nn.LSTM在整个序列上迭代相比,它仅在序列上迭代一步。一般较少使用。
  • nn.GRUCell:门控循环网络单元。和nn.GRU在整个序列上迭代相比,它仅在序列上迭代一步。一般较少使用。
  • nn.RNNCell:简单循环网络单元。和nn.RNN在整个序列上迭代相比,它仅在序列上迭代一步。一般较少使用。

nn.RNN(input_size, hidden_size, num_layers=1, nonlinearity=tanh, bias=True, batch_first=False, dropout=0, bidirectional=False)
参数说明

input_size输入特征的维度, 一般rnn中输入的是词向量,那么 input_size 就等于一个词向量的维度
hidden_size隐藏层神经元个数,或者也叫输出的维度(因为rnn输出为各个时间步上的隐藏状态)
num_layers网络的层数
nonlinearity激活函数
bias是否使用偏置
batch_first输入数据的形式,默认是 False,就是这样形式,(seq(num_step), batch, input_dim),也就是将序列长度放在第一位,batch 放在第二位
dropout是否应用dropout, 默认不使用,如若使用将其设置成一个0-1的数字即可
birdirectional是否使用双向的 rnn,默认是 False
注意某些参数的默认值在标题中已注明


输入输出shape
input_shape = [时间步数, 批量大小, 特征维度] = [num_steps(seq_length), batch_size, input_dim]
在前向计算后会分别返回输出和隐藏状态h,其中输出指的是隐藏层在各个时间步上计算并输出的隐藏状态,它们通常作为后续输出层的输⼊。需要强调的是,该“输出”本身并不涉及输出层计算,形状为(时间步数, 批量大小, 隐藏单元个数);隐藏状态指的是隐藏层在最后时间步的隐藏状态:当隐藏层有多层时,每⼀层的隐藏状态都会记录在该变量中;对于像⻓短期记忆(LSTM),隐藏状态是⼀个元组(h, c),即hidden state和cell state(此处普通rnn只有一个值)隐藏状态h的形状为(层数, 批量大小,隐藏单元个数)

4. Transformer

这部分只针对Transformer的重要部分多头注意力进行解读。pytorch是有封装好的Transformer的。

  •  nn.Transformer:Transformer网络结构。Transformer网络结构是替代循环网络的一种结构,解决了循环网络难以并行,难以捕捉长期依赖的缺陷。它是目前NLP任务的主流模型的主要构成部分。
  •  nn.TransformerEncoder:Transformer编码器结构。由多个 nn.TransformerEncoderLayer编码器层组成。
  •  nn.TransformerDecoder:Transformer解码器结构。由多个 nn.TransformerDecoderLayer解码器层组成。
  •  nn.TransformerEncoderLayer:Transformer的编码器层。主要由Multi-Head self-Attention, Feed-Forward前馈网络, LayerNorm归一化层, 以及残差连接层组成。
  •  nn.TransformerDecoderLayer:Transformer的解码器层。主要由Masked Multi-Head self-Attention, Multi-Head cross-Attention, Feed-Forward前馈网络, LayerNorm归一化层, 以及残差连接层组成。
  •  nn.MultiheadAttention:多头注意力层。用于在序列方向上融合特征。使用的是Scaled Dot Production Attention,并引入了多个注意力头。
import torch 
from torch import nn 

#验证MultiheadAttention和head数量无关
inputs = torch.randn(8,200,64) #batch_size, seq_length, features

attention_h8 = nn.MultiheadAttention(
    embed_dim = 64,
    num_heads = 8,
    bias=True,
    batch_first=True
)

attention_h16 = nn.MultiheadAttention(
    embed_dim = 64,
    num_heads = 16,
    bias=True,
    batch_first=True
)


out_h8 = attention_h8(inputs,inputs,inputs)
out_h16 = attention_h16(inputs,inputs,inputs)

参考:

torch.nn.Linear()函数_一位不愿暴露自己的郑某人的博客-CSDN博客

Batch Normalization层的原理与作用_batchnorm的原理和作用_陈 洪 伟的博客-CSDN博客

Conv2d函数详解(Pytorch)_phil__naiping的博客-CSDN博客

Dropout 层应该加在什么地方?_dropout层加在哪里-CSDN博客

系统学习Pytorch笔记五:nn的网络层介绍(卷积层,池化层,激活函数,全连接层等)_nn中常用的层_翻滚的小@强的博客-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值