pytorch模型训练特征图可视化
参考:
1
2
- tensorflow使用tensorboard来可视化,pytorch使用tensorboardX来可视化。需要安装。
使用cuda训练
当单个cuda
x = torch.Tensor([1,2,3]).cuda
当多个cuda,不指定时默认使用cuda0
x = torch.Tensor([1,2,3],device = torch.device("cuda:0")
或者这样写也可以
x = torch.Tensor([1,2,3].to(device = torch.device('cuda:0'))
pytorch的扩展
自定义模型、自定义层、自定义激活函数、自定义损失函数都属于pytorch的扩展。
要实现自定义扩展,有两种方式:
- 方式一:通过继承torch.nn.Module类来实现扩展
- 方式一具有的特点是:
- 特点一:torch.nn.functional是专门为神经网络定义的函数集合
- 特点二:只需要重新实现__init__和forward函数,求导的函数是不需要设置的,会自动按照求导规则求导,在Module类里面是没有backward这个函数的
- 特点三:可以保存参数和状态信息
- 方式二:通过继承torch.nn.Function类来实现扩展
- 方式二具有的三个特点是:
- 特点一:在有些操作通过组合pytorch中已经有的层或者是已经有的方法实现不了的时候,比如需要实现一个新的方法,这个新的方法需要forward和backward一起写,然后自己写对中间变量的操作。
- 特点二:需要重新实现__init__和forward函数,以及backward函数,需要自己定义求导规则。
- 特点三: 不可以保存参数和状态信息
-
总结:当不需要自动求导机制,需要自定义求导规则时候,就要扩展torch.autograde.Function类,否则就是用torch.nn.Module类,后面这种是最常用的。nn.Module是一个包装好的类,具体定义了一个网络层,可以维护状态和存储参数信息,而nn.function仅仅提供一个计算,不会维护状态信息和存储参数,在Module内部,层的功能实际上又是通过nn.functional来实现的。对于一些不需要存储参数和状态信息的层,比如activation函数(比如relu和sigmoid等),比如dropout层、pooling层等都没有训练参数,所以可以使用functional模块,但是仍然可以使用nn.Module类完成,后者仍然更常用。
不管是自定义层还是损失函数,方法都多种,但是通过统一的接口nn.Module是最便于查看的。这也是pytorch的优点:模型、层、损失函数的定义具有统一性。都是通过Module类来完成的。
DataLoader
参考:1
- pytorch 的数据加载到模型的操作顺序是这样的:
第一步:创建一个 Dataset 对象
第二步:创建一个 DataLoader 对象
第三步:循环这个 DataLoader 对象,将img, label加载到模型中进行训练
如下代码所示:
dataset = MyDataset()
dataloader = DataLoader(dataset)
num_epoches = 100
for epoch in range(num_epoches):
for img, label in dataloader:
......
-
所以,作为直接对数据进入模型中的关键一步, DataLoader非常重要。
-
DataLoader,它是PyTorch中数据读取的一个重要接口,该接口定义在dataloader.py中
-
只要是用PyTorch来训练模型基本都会用到DataLoader接口,该接口的目的:将自定义的Dataset根据batch size大小、是否shuffle等封装成一个Batch Size大小的Tensor,用于后面的训练。
-
官方对DataLoader的说明是:“数据加载由数据集和采样器组成,基于python的单、多进程的iterators来处理数据。”关于iterator和iterable的区别和概念请自行查阅,在实现中的差别就是iterators有__iter__和__next__方法,而iterable只有__iter__方法。
-
DataLoader本质上就是一个iterable(跟python的内置类型list等一样),并利用多进程来加速batch data的处理,使用yield来使用有限的内存
-
dataloader本质是一个可迭代对象,使用iter()访问,不能使用next()访问;
-
使用iter(dataloader)返回的是一个迭代器,然后可以使用next访问;
-
也可以使用for inputs, labels in dataloaders
进行可迭代对象的访问;
-
实现一个datasets对象,传入到dataloader中;然后内部使用yeild返回每一次batch的数据;
dataset(Dataset): 传入的数据集
batch_size(int, optional): 每个batch有多少个样本
shuffle(bool, optional): 在每个epoch开始的时候,对数据进行重新排序
sampler(Sampler, optional): 自定义从数据集中取样本的策略,如果指定这个参数,那么shuffle必须为False
batch_sampler(Sampler, optional): 与sampler类似,但是一次只返回一个batch的indices(索引),需要注意的是,一旦指定了这个参数,那么 batch_size,shuffle,sampler,drop_last就不能再制定了(互斥——Mutually exclusive)
num_workers (int, optional): 这个参数决定了有几个进程来处理data loading。0意味着所有的数据都会被load进主进程。(默认为0)
collate_fn (callable, optional): 将一个list的sample组成一个mini-batch的函数
pin_memory (bool, optional): 如果设置为True,那么data loader将会在返回它们之前,将tensors拷贝到CUDA中的固定内存(CUDA pinned memory)中.
drop_last (bool, optional): 如果设置为True:这个是对最后的未完成的batch来说的,比如你的batch_size设置为64,而一个epoch只有100个样本,那么训练的时候后面的36个就被扔掉了…
如果为False(默认),那么会继续正常执行,只是最后的batch_size会小一点。
timeout(numeric, optional): 如果是正数,表明等待从worker进程中收集一个batch等待的时间,若超出设定的时间还没有收集到,那就不收集这个内容了。这个numeric应总是大于等于0。默认为0
worker_init_fn (callable, optional): 每个worker初始化函数 If not None, this will be called on each worker subprocess with the worker id (an int in [0,num_workers - 1]) as input, after seeding and before data loading. (default: None)
unsqueeze()函数
在实践中的使用如下:
bottle=torch.Tensor(bottle).permute(2,0,1).unsqueeze(0).div(255).cuda()
下面用例子解释这个函数:
a = t.arange(0,6)
a.view(2,3)
上面的代码会输出:
tensor([[0,1,2],
[3,4,5]])
会有这样的输出是因为a的值从0到6,包括0,不包括6,可以看出a的维度是(2,3)
为了实现在第二维增加一个维度,使得a的维度变成(2,1,3),用如下代码实现
a.unsqueeze(1)#注意形状,在第一维(要注意的是下标是从0开始的)上增加"1"
所以,得到的输出是
tensor([[[0,1,2],
[3,4,5]]])
可以看到输出中a的维度已经变成了(2,1,3)
同样的,如果是需要在倒数第二维上增加一个维度,那么使用如下代码
a.unsqueeze(-2)
调试网络时查看模型每一层的输出尺寸
from torchsummary import sunmmary
summary(your_model,input_size=(channels,H,W))
pip install torchsummary
独热编码
在pytorch中,使用交叉熵损失函数时会自动把label转化为onehot,所以不要手动转化,而使用MSE需要手动转化成onehot编码
import torch
class_num = 8
batch_size = 4
def one_hot(label):
#将一维编码转化为独热编码
label = label.resize_(batch_size,1)
m_zeros = torch.zeros(batch_size,class_num)
#从value中取值,然后根据dim和index给相应位置赋值
onehot = m_zeros.scatter_(1,label,1)#(dim,index,value)
return onehot.numpy()#Tensor转化为numpy
label = torch.LongTensor(batch_size).random_() % class_num#对随机数取余
print(one_hot(label))
'''
输出如下:
output:
[[0. 0. 0. 1. 0. 0. 0. 0.]
[0. 0. 0. 0. 1. 0. 0. 0.]
[0. 0. 1. 0. 0. 0. 0. 0.]
[0. 1. 0. 0. 0. 0. 0. 0.]]
共4行8列
batch size值为4,类别数目为8
'''
防止爆显存现象,防止出现out of memory
- 验证模型时不需要求导,即不需要梯度计算,关闭autograd,可以提高速度,节约内存,如果不关闭可能会爆显存
with torch.no_grad():
#使用model进行预测的代码
pass
- pytorch训练时,无用的临时变量可能会越来越多,导致out of memory,使用下面的代码来清理不需要的变量
torch.cuda.empty_cache()
- 实时监控内存的工具
sudo apt-get install htop
htop -d=0.1
-d表示更新频率,表示每0.1s更新一次
- 实时监控显存
watch -n 0.1 nvidia-smi
-n表示更新频率,每0.1s更新一次
- out of memory出现的原因:显存装不下模型权重以及中间变量,还有optimizer算法时产生的巨量的中间参数。优化的方法:及时清空中间变量,减少batch值。
-
显存的占用=模型参数+计算产生的中间变量
- 以VGG16为例
- 上图中的例子里由于默认的数据格式是8-bit而不是32-bit,所以最后的结果要乘上一个4。
- 只要一计算就能发现当batch_size=256时,中间变量所产生的参数量是有多巨大。
- 反向传播时,中间变量+原来保存的中间变量,存储量会翻倍。
- 一些适用于移动端的网络,例如mobilenet等,计算量是变少了,但对显存的占用变大了,原因就是中间参数存储增加。这个网络利用DepthWise Convolution的技术,核心思想是:把一个卷积操作拆分成两个相对简单的操作的组合,如下图:左边是原始卷积操作,右边是两个特殊而又简单的卷积操作的组合,右边的上面那个图类似于池化,下边类似于全连接。
-
一些缓解out of memory的措施
- 措施一:激活函数ReLU()有一个默认参数inplace,默认设置为False,当设置为True的时候,在通过relu()计算时得到的新值不会占用新的空间而是直接覆盖原来的值,所以当inplace赋值为True的时候可以节省一部分内存。
- 措施二:用del一边计算一边消除中间变量
- 措施三:牺牲计算速度来缓解爆显存问题:pytorch0.4以后出来的功能:可以将一个计算过程分成两半,如果一个模型需要占用的显存太大,就可以先计算一半,保存后一半需要的中间结果,然后再计算后一半。即是说:新的checkpoint允许只存储反向传播所需要的部分内容,如果当中缺少一个输出(为了节省内存而导致),那么checkpoint会从最近的检查点重新计算中间输出,以便减少内存使用。同时计算时间也会随之增加(带来的缺点)
- 措施四:一般占用显存最多的是:卷积等等层的输出,模型参数占用的相对少且不太好优化。减少batch_size值,避免使用全连接(一般只留下最后一层分类用的全连接层),多使用下采样(例如NCHW变化成(1/4) x NCHW )
- 在程序刚开始时加入如下一条代码:
torch.backends.cudnn.benchmark=True
加入这条语句可以提升一点训练速度,且无额外开销。
- 措施五:每次迭代都会引入临时变量,会导致训练速度越来越慢,基本呈现线性增长,如果周期性地使用torch.cuda.empty_cache()语句的话就可以缓解这个问题。
-
估测模型所占的内存
- 上面讲过了,模型的权重参数和模型存储的中间变量占用了显存。权重参数一般来说是不会占用太多的显存空间,主要占用显存的还是:计算产生的中间变量。
-
下面的代码实现计算模型权重参数所占用的数据量。
#假设有如下这样一个model
Sequential(
(conv_1):Conv2d(3,64,kernel_size=(3,3),stride=(1,1),padding=(1,1))
(relu_1):ReLU(inplace)
(conv_2):Conv2d(64,64,kernel_size=(3,3),stride=(1,1),padding=(1,1))
(relu_2):ReLU(inplace)
(pool_2):MaxPool2d(kernel_size=2,stride=2,padding=0,dilation=1,ceil_node=False)
(conv_3):Conv2d(64,128,kernel_size=(3,3),stride=(1,1),padding=(1,1)
)
import numpy as np
#model是在pytorch中定义的神经网络层
#model.parameters()取出这个model所有的权重参数
para = sum([np.prod(list(p.size())) for p in model.parameters()])
'''
通过上面的代码计算出来的仅仅是权重参数的“数量”,单位是B,所以需要转化一下。
用下面的代码转化
'''
#type_size=4,因为参数是float32也就是4B,4个字节
print('Model {} :params:{:4f}M'.format(model._get_name(),para*type_size/1000/1000)))
#得到最后的输出结果是:Model Sequential:params:0.450.04M
- 有计算模型的中间变量所占用显存的代码,讲的很好
-
模型参数的显存占用
-
只有有参数的层才会占用显存,这部分的显存占用与输入无关,模型加载完毕就会占用。
- 有参数的层包括:卷积层、全连接层、BatchNorm层、Embedding层等
- 无参数的层:多数的激活层(如Sigmoid和ReLU等)、池化层、Dropout层等
- 更具体的说,这里先都不考虑偏置项,只考虑权重项。模型的参数数目为:
全连接层:Linear,从输入形状M到输出形状N,参数数目是B x M x N,B是bacth size
Conv2d层:(Cin,Cout,K),(K为卷积核尺寸,输出为H x W,输入通道Cin,输出通道Cout,B是batch size)参数数目是:B x H x W x Cout x Cin x K x K
BatchNorm层(N):参数数目:2 x N
Embedding嵌入层(N,W):参数数目:N x W
补充:关于嵌入层的解释,讲得好
- 参数占用显存=参数数目 x n
当n=4时:float32
当n=2时:float16
当n=8时,double64
-
模型中与输入无关的显存占用包括了:参数W,梯度dW,优化器的动量(普通SGD没有动量,momentum-SGD动量与梯度一样,Adam优化器动量的数量是梯度的两倍)
-
输入输出的显存占用,这部分的占用主要看输出的feature map形状。
-
卷积的输入与输出满足如下的关系,这个关系较重要,好好记忆好好理解!!
- 根据上面给出的卷积的输入与输出的关系公式,所以可以计算出每一层输出的Tensor的形状,然后能够计算出相应的显存占用。
-
模型输出的显存占用,包括:需要计算每一层的feature map的形状(多维数组的形状),需要保存输出对应的梯度用于反向传播(链式法则)、显存占用与batch size成正比、模型输出不需要存储相应的动量信息。
-
深度学习中神经网络的显存占用,有公式:显存占用=模型显存占用+batch_size x 每个样本的显存占用。
- 输入数据或者图片一般不需要计算梯度。
- 神经网络的每一层输入输出都需要保存下来,用来反向传播,但是在一些特殊情况下可以不用保存输入。例如:ReLU中,使用nn.ReLU(inplace=True)能将激活函数ReLU的输出直接覆盖保存在模型的输入之中,节省内存。
训练时关于神经网络模型的选择
ResNet深度残差神经网络
参考:1
- 神经网络并不是层次越深越好,因为神经网络在反向传播的过程中要不断的传播梯度,当网络层次加深的时候,梯度在传播过程中会逐渐消失.加入采用sigmoid函数,对于幅度为1的信号,每向后传播一层,梯度就衰减为原来的0.25,所以层数越多衰减也就越厉害.导致无法对前面网络层的权重进行调整.
-
所以就需要一种解决办法,既要加深网络层次又要解决梯度消失的问题.解决办法就是深度残差网络Deep Residual Network
- 实验现象表明:不断加深神经网络的深度时,模型的准确率会先上升然后趋于饱和,然后再增加深度时就会导致准确率下降,如下图所示:
-
做一个假设,假如有一个比较浅层的网络,并且已经达到了饱和的准确率,这时在他后面加几个恒等映射层,identity mapping,也即是说y=x,输出等于输入的网络层.这样的话使得这个浅层网络的深度得到了提高,并且误差是不会增加的,因为增加的层都是满足输出等于输入.这样就实现了更深的网络层但是准确率不会下降.这种思路使用恒等映射将前一层的输出传到后一层的思路就是ResNet的思路.
-
ResNet引入了残差网络residual network.通过残差网络结构,可以把网络弄得很深,目前可以达到1000层.残差网络的基本结构如下,是带有跳跃结构的:
-
残差网络借鉴高速网络的跨层链接思想,但是改进部分在于:原来高速网络的残差项是带有权重的,而残差网络将其改为是恒等映射.
-
所以如果已经学习到较饱和的准确率(或者当发现下层的误差变大时),那么接下来的学习目标就转变为恒等映射的学习,也就是使输入x近似于输出H(x),以保持在后面的层次中不会造成精度下降。
-
在上面的图片残差网络的基本结构图中,通过捷径链接的方式,直接把输入x传到输出作为初始结果,
-
输出结果为H(x)=F(x)+x,当F(x)=0时,那么H(x)=x,也就是上面所提到的恒等映射。
-
所以ResNet相当于将学习目标改变了,不再是学习一个完整的输出,而是目标值H(x)和x之间的差值. 也就是所谓的残差F(x) := H(x)-x
-
所以接下来的训练目标就是让残差值接近于0,从而是的随着网络层数加深但是准确率不下降
-
这种残差跳跃式的结构打破了传统的神经网络n-1层的输出只能给n层作为输入,至此神经网络得以达到上千层,给高级语义的特征提取提供了方法
-
如下是34层的残差神经网络的结构图
-
上图34层残差神经网络的结构图中有些跳跃链接是实线,有些跳跃连接是虚线.
-
这是因为经过跳跃连接后,H(x)=F(x)+x,如果F(x)和x的通道相同那么可以直接相加,至于通道不同则如何相加,上面的实线和虚线就是为了区分这两种情况.
-
结构图中,实线的连接部分表示通道相同,同下图来说明:截取一部分结构图
上图中第一个紫色矩形和第三个紫色矩形之间是实线连接,这是因为F(x)和x是相同通道,实线连接的两个紫色矩形都是3x3x64的特征图,由于通道相同,所以采用计算方式为H(x)=F(x)+x
-
虚线的连接部分,表示通道不同,如上图截取的部分结构图中的第一个绿色矩形和第三个绿色矩形,分别是3x3x64和3x3x128的特征图,通道不同,采用的计算方式为H(x)=F(x)+Wx,其中W是卷积操作,用来调整x维度的。
-
除了两层残差学习单元,还有三层残差学习单元,两层的是下图中的左图,三层的是下图中的右图,两层残差学习单元对应的有:ResNet34,三层残差学习单元对应的有ResNet50/101/152.左图的两层残差学习单元是两个3x3x256的卷积,参数个数3x3x256x256x2=1179648,而右图是第一个1x1的卷积将把256通道降低到64通道,然后再在最后通过1x1卷积恢复.用到的参数个数是:1x1x256x64 + 3x3x64x64 + 1x1x64x256 = 69632,
19. 得到结论三层的残差学习单元的参数数量比两层的残差学习单元的参数个数减少了16.94倍,减少参数量也就减少了计算量.
DenseNet
参考:
1
2
如今比ResNet性能更好的卷积神经网络模型
- DenseNet的基本思路与ResNet一致,但是它建立的是前面所有层与后面层的密集连接,DenseNet的另外一个特色是通过特征在channel上的连接来实现特征重用。
- 相比ResNet,DenseNet提出了一个更激进的密集连接机制:即互相连接所有的层,也就是说每个层都会接受其前面所有层以作为其额外的输入。
-
对比ResNet和DenseNet
- 下图是ResNet的结构图,图中的+号表示元素级相加操作
- 下图是DenseNet的结构图,图中的c表示是channel级连接操作
- 上面的结构图都是简图,实际上在l层和l+1层之间可能存在多个卷积层
- DenseNet的优势体现在如下三个方面
- 第一个优势:由于密集连接的方式,DenseNet提升了梯度的反向传播,使得我那个罗曾更容易训练,由于每层可以直达最后的误差信号,实现了隐式的deep supervision
- 第二个优势:参数更小且计算高效,DenseNet是通过concat特征实现短路连接,实现了特征重用,且采用较小的growth rate,每个层独有的特征图是较小的
- 第三个优势:由于特征复用,最后的分类器使用了低级特征
-
选择不同的网络参数可以实现不同深度的DenseNet,例如DenseNet121等,在pytorch中提供了预训练好的网络参数
CNN网络一般要经过Pooling或者是stride>1的Conv来降低特征图的大小,而DenseNet的密集连接方式需要特征图大小保持一致。
通过公式原理来理解ResNet和DenseNet
- ResNet的公式如下:公式中l表示层,xl表示l层的输出,Hl表示一个非线性变换,ResNet的l层的输出是l-1层的输出加上对l-1层输出的非线性变换
- DenseNet的公式如下,[x0,x1,x2,……xl-1]表示将0到l-1层的输出feature map做concatenation,contatenation是做通道的合并,就像inception一样,在ResNet中,是做值的相加,而通道数是不变的,Hl包括BN,Relu和3x3卷积
下图是一个4层的Dense block
下图是一个DenseNet的结构图,这个DenseNet结构中包含了3个dense block,在两个dense block之间是transition layers,并且通过convolution和pooling来改变feature map
下图是整个网络的结构图,表格中的k值是growth rate,表示每个dense block中每层输出的feature map的个数,
- 在上图中,为了避免网络变得很宽,所以一般都采用较小的k值,比如k=32,小的k值也会有更好的性能效果,
- dense block中,后面几层可以得到前面所有层的输入,因此concat后的输入channel是较大的。
- 从上图中的网络结构图的表格中发现,每个dense block的3x3卷积前面都包含一个1x1卷积操作,即bottle neck layer。目的是减少输入的feature map数量,降维减少计算量,且融合各个通道的特征。
- 在DenseNet中,为了进一步压缩参数,从上面的网络结构表格图中发现,每两个dense block之间增加了1x1卷积操作,这个增加的transition layer的1x1卷积的默认输出是输入channel的一半,
- DenseNet-C网络,这个名字就表示含有上面结构图表格中的transition layer
- DenseNet-BC网络,这个名字表示有bottleneck layer和transition layer
Dense Block操作
- 在每个Dense block中都包含很多子结构,以DenseNet-169的Dense block为例,这个Dense block包含32个1x1和3x3的卷积操作,也就是第32个子结构的输入是前面31层的输出结果,每层输出的channel是32(growth rate),如果不做bottleneck操作,第32层的3x3卷积操作的输入是31x32+(上一个dense block的输出channel),这个输入值将近1000
- 而如果是加上1x1卷积,代码中的1x1卷积的channel是growth rate x 4,即128,然后再作为3x3卷积的输入。故减少了计算量,这也就是bottleneck
transition layer
- transition layer放在两个dense block中间,因为每个dense block结束后的输出channel个数很多,需要用1x1的卷积核来降维。
- transition layer有参数reduction,范围是0到1,表示将这些输出缩小到原来的多少倍,默认0.5,这样传给下一个dense block的时候channel数量会减小一半,这是transition layer的作用。
- 在DenseNet中还用到了dropout操作来随机减小分支,避免过拟合。
- 参数减少除了节省内存还可以减少过拟合。
提升模型性能的手段之一——调参
- 修改学习率
- 修改优化器的种类
- 冰冻某些层,不让它更新参数
- 使不一样的网络层有不同的学习率
- 修改pooling层
- 修改bacth normalization层
- 修改全连接层
- 改变输入数据的格式和大小
- 修改loss function
调整优化器、学习率,意义不大的原因是使用pretrained model已经调得很优了
好的调参最多能浮动几个百分点,最重要的还是data
数据不均的解决办法:
- data augmentation
- 修改loss function
- 修改输入batch的数据类别分布
若对数据不理解,是不能随便做数据增强的,因为医疗数据的稍微变动则label可能就变了
Pretrained model都是用imagenet训练的,但是ImageNet里面没有医疗数据。
- 若自己项目的数据量少,则不建议train from scratch
- 用pretrained model的参数来初始化model的参数,而这些pretrained model的参数是经过专家精确调参出来的,效果肯定比随机生成参数好很多。
清理数据
参考:1
- 一般的做法是把新的数据作为测试数据,用旧数据训练出的模型来测试新数据。
- 如果新的数据数据量大并且可能和新的数据分布不相似。那么做法可以是:
- 第一步,假设给的数据大部分是标注正确的
- 第二步,用cross_validation的方式,将新数据分成5组,用其中4组fine_tune模型 旧数据模型 10-15个epoch,剩下的一组做test
- 第三步,在test的预测结果中,选取预测概率值低于0.8的数据重新标注。
尽量多方位探索有用信息,在做深度优化时,先拿别的组的超声波数据train一遍模型,然后再在自己数据上train,效果也会有提升。
数据预处理很重要,医疗数据有彩色的、模糊的、黑白的、高光的,对数据标准化可以提高准确率
一开始用数据本身的mean和std做标准化,后面发现用imagenet的mean和std性能会更好(实践发现,不一定通用
很多网络都使用了1x1卷积核
参考:1
- 对于单通道的feature map和单个卷积核之间的卷积来说,可以如下理解:1x1卷积核是对输入的一个比例缩放,因为1x1卷积核只有一个参数,这个核在输入上滑动,就相当于给输入数据乘以一个系数。
-
1x1卷积核的作用有如下两个:
- 作用一:实现跨通道的交互和信息整合
- 作用二:进行卷积核通道数的降维和升维。
- 作用三:在保持feature map尺寸不变的前提下,即不损失分辨率的前提下大幅增加非线性特性,把网络做的很deep。
虽然卷积核较小,但是当输入和输出的通道数很大时,乘起来也会使得卷积核参数变得很大。
CNN中的卷积大都是多通道的feature map和多通道的卷积核之间的操作,输入的多通道的feature map和一组卷积核做卷积求和得到一个输出的feature map。如果使用1x1卷积核,这个操作实现的就是多个feature map的线性组合,可以实现feature map在通道个数上的变化。若接在普通的卷基层的后面,配上激活函数,就可以实现network in network的结构。
学会用一个文档详细记录所有数据,从一个工程到一个工程结束,要把所有实验的详细信息都集中记录在一个地方,总结所有实验数据,会规避许多未来无用的实验。
####### 需要弄清楚模型是在记数据,还是在学数据,即模型的泛化能力。对数据增加特殊噪音,或者对图片稍微改动,模型识别率也许就会下降。借鉴MobileNet轻模型的构造思路,修改了重模型DenseNet,并且删减了DenseNet一定的层数,使得最终DenseNet参数大大减少,但是泛化能力比mobilenet好,但是准确率比densenet低。
ImageNet预训练模型
- 使用ImageNet预训练模型可以在训练早期加快收敛速度,但是不一定带来正则化的效果或者是最终提高目标任务的准确率。
- 使用在预训练任务中的学到的特征表示,能够将其中有用的信息传递给另一个目标任务。一个通用的方法是使用大规模数据,例如ImageNet,对模型进行预训练,然后在具有较少训练数据的目标任务上对模型进行微调。
- 使用ImageNet预训练能加快收敛速度,特别在训练初期,但是随机初始化的训练方法可以在训练了一段时间后赶上,该时间相当于ImageNet预训练加上微调的事件之和。在研究目标任务时经常忽略ImageNet预训练的成本,因此会有使用预训练能缩短时间成本的假象,相反也忽略了随机初始化训练方法的真正作用。
- 使用ImageNet预训练的方法并不能提供更好的正则化效果,当用较少的图像训练时,必须选择新的超参数微调(微调初始参数来自预训练)以避免过拟合。但是当使用这些初始超参数进行随机初始化训练时,该模型可以达到使用预训练方法的精度并且不需要额外的正则化。
- 预训练之后,模型可以直接利用已经学习的低级特征(边缘、纹理等),而无需重新学习。
- 在一个例子中,一个从头开始训练的模型相比预训练的模型,需要超过3倍的迭代数才能达到收敛
- 对于一个大学习率,训练更长的时间是有用的,但是对于较小学习率,训练太久往往导致过拟合
- 默认情况下,Detectron中的Mask R-CNN在测试时不使用数据扩增,在训练时也只使用水平翻转的增强操作。
正则化
- 正则化能辅助模型的优化过程。有效的正则化策略包括标准的参数初始和激活正则化层。
-
dropout防止过拟合(防止过拟合方法很多,例如L2正则化,早停(就是在测试集训练到误差最低时停止训练)
- dropout即在每次正向推断神经元的时候随机掐死一部分神经元,阻断其输入输出,这样可以起到正则化作用。
- 可以这样理解,皇上雨露均沾,这样就避免了杨贵妃那样集宠爱于一身的神经元,从而防止某些神经元一家独大。所有神经元处于平等地位,防止过拟合。
- 批标准化BN是最流行的正则化方法。
- 不像图形分类中的输入,目标检测器使用高分辨率的图像输入进行训练,BN策略虽然可以减少批量大小内存,但是小批量的输入会严重降低模型的准确率。
- 缓解小批量输入问题的两种正则化方法:
- 方法一:Group Normalization:作为BN的替代方法,GN方法的计算与输入的批量维度无关,引用该正则化方法时,模型准确性对输入的批量大小不敏感
- 方法二:Synchronized Batch Normalization:是BN的跨设备(GPU)实现,能统计对个设备的批量大小情况,当使用多个GPU时,该正则化方法能增加BN的有效批量大小,从而避免小批量输入的问题
- batch Normalization,也就是对每一个layer做Feature Scaling特征缩放
-
Internal Covariate Shift
- 深度神经网络涉及到很多层的叠加,而每一层的参数更新会导致上层的输入数据分布发生变化,通过层层叠加,高层的输入分布变化会非常剧烈,这就使得高层需要不断去重新适应底层的参数更新。
- 为了训好模型,需要谨慎地去设定学习率、初始化权重、以及尽可能细致的参数更新策略。Google 将这一现象总结为 Internal Covariate Shift。
- 将每一层的输入作为一个分布看待,由于底层的参数随着训练更新,导致相同的输入分布得到的输出分布改变了。机器学习中有个很重要的假设:IID独立同分布假设,就是假设训练数据和测试数据是满足相同分布的,这是通过训练数据获得的模型能够在测试集获得好的效果的一个基本保障。
- 细化到神经网络的每一层间,每轮训练时分布都是不一致,那么相对的训练效果就得不到保障,所以称为层间的covariate shift。
- 因此,每个神经元的输入数据不再是“独立同分布”,导致了以下问题:
- 问题一:上层网络需要不断适应新的输入数据分布,降低学习速度。
- 问题二:下层输入的变化可能趋向于变大或者变小,导致上层落入饱和区,使得学习过早停止。
- 问题三:每层的更新都会影响到其它层,因此每层的参数更新策略需要尽可能的谨慎。
- 如果把每一个layer的feature都做Normalization,把每一个layer的feature的output都做Normalization,让他们永远都是,比如说,平均值为0,方差为1,对下一个layer来看,前个layer的statistics就会是固定的,他的training可能就会更容易一点。
-
训练过程参数在调整时前一个层是后一个层的输入,当前一个层的参数改变后,也会改变后一层的参数。当后面的参数按照前面的参数学好了后,前面的layer就变了,因为前面的layer也是不断在变的。输入数据很好normalization,因为输入数据是固定的,但是后面层的参数在不断变化,根本就不能容易算出mean和variance,所以需要Batch normalization。
-
GPU加速batch计算的原理
- Batch的数据是平行计算的。
-
Batch Normalization标准化针对输入数据的单一维度进行,根据每一个batch计算均值与标准差,由于从形象上是纵向的计算,又称为纵向标准化,如下图:
-
Batch Normalization的好处
- Normalization标准化就是将分布变换为均值方差一致的分布。训练过程中,随着网络加深,分布逐渐发生变动,导致整体分布逐渐往激活函数的饱和区间移动,从而反向传播时底层出现梯度消失,也就是收敛越来越慢的原因。Normalization则是把分布强行拉回到均值为0方差为1的标准正态分布,使得激活输入值落在非线性函数对输入比较敏感的区域,这样输入的小变化就会导致损失函数较大的变化,避免梯度消失问题产生,加速收敛。
-
如下图所示,标准化后变成了图片中的红色曲线:标准化的操作也就是讲输入x的取值正态分布整体右移,(均值变化),图形曲线更平缓了(方差变化)。
- 好处一:解决了Internal Covariate Shift的问题:Internal Covariate Shift限制了学习率设置较大的值,Batch Normalization后,学习率可以设大一点,training就会快一点。
- 好处二:防止gradient vanish。如果用sigmoid/tanh函数,很容易遇到gradient vanish。如果input是落在靠近值很大或者很小的地方,很容易gradient vanish。但Batch Normalization可以确保激活函数的input在零附近,是斜率较大的地方,gradient较大的地方不会有gradient vanish。具体过程可以由下图解释:
标准正态分布图如下图:
在一个标准差范围,有68%的概率x其值落在[-1,1]的范围内;在两个标准差范围,有95%的概率x其值落在了[-2,2]的范围内。
若激活函数为sigmoid,如下图:
在[-2, 2]的范围内,即是标准正态分布两个标注差范围内,在sigmoid函数中为线性变换区域,微小的变化就能得到很大的改变,也即是梯度比较大。
如果不经过变换,存在一个均值为-6,方差为1的分布,对应到激活函数中就是[-8, -4]的区域,这已经是饱和区了,这也就是所谓的梯度消失。
标准化其实就是把大部分激活的值落入非线性函数的线性区内,其对应的导数远离导数饱和区,这样来加速训练收敛过程。
- 好处三:对参数定义的initialization影响较小:很多方法对参数的initialization非常sensitive,而Batch Normalization对参数的initialization的影响较小。
- 好处四:对抗overfitting:在batch Normalization时等同做regularization,把所有feature都Normalize 到固定的mean,一样的variance,如果在test的时候导致mean有一个shift,shift没关系,反正会做Normalization,所以batch Normalization有对抗overfitting的效果。
数据
- 遥感数据很大的特点是多方向性,遥感数据多是俯拍,船只的朝向东南西北都是可能的,因此训练集的朝向要丰富,避免学到的模型不具有泛性。
- 解决办法一是:在模型里面加一个旋转不变层,以解决遥感图像多方向性问题。
- 解决办法二:选择几种数据增强的方法:随机90度倍数旋转,水平翻转等
- 解决办法三:TTA单图多测:TTA阶段将待测图片翻转、旋转获取多个待测图片,然后同一模型对这几张图片分别检测,投票产生最终检测结果,提高系统鲁棒性
- 解决的实际问题看似可以归类到实例分割,但是实际上是unet模型的使用。这是因为遥感船只数据集有如下两个特点:
- 特点一:图内只有一个类别的物体,不存在多类别,没有船只的图片很多,占比70%,有船只的图片占比30%,可见负样本很多。(在制作数据集时暂时丢掉无船只的那些图片数据,这是为了提升训练速度,但是在训练的最后几个epoch需要把所有的样本都放进来训练,这是为了防止假阳性过于严重
- 特点二:船只粘连不严重,重叠的很少
TTA多模型融合
- TTA即test time augmentation,是测试阶段增强,一般大的框架会提供一些TTA功能接口,但是有些是需要自己写代码实现去补充的。例如Detectron就有TTA参考配置文件,多尺度检测和水平翻转是提供的,但是其他的增强是需要写代码实现
- 类似于集成学习,单图多scale检测投票,scale:0.6/0.8/1.0/1.2/1.4 rotate:90/180/270,都是可以尝试的,然后多模型投票取好的结果
善用模型融合
- 例如unet+mask r-cnn模型融合,模型融合的策略因人而异,模型与模型之间优势互补是提分利器。
将训练好的模型保存,然后加载用于测试
- 第一种方式:只保存和恢复模型中的参数,使用这第一种方法,需要自己导入模型的结构信息。
#实现保存
#模板
torch.save(model.state_dict(),PATH)
#例子
torch.save(resnet50.state_dict(),'ckp/model.pth')
#实现恢复
#模板
model = ModelClass(*args,*kwargs)
model.load_state_dict(torch.load(PATH))
#例子
resnet = resnet50(pretrained=True)
resnet.load_state_dict(torch.load('ckp/model.pth'))
- 第二种方法,会保存模型的参数和结构信息
#实现保存
torch.save(model,PATH)
#实现恢复
model = torch.load(PATH)
注意力机制
- 深度学习与视觉注意力机制结合的研究,大多集中在使用掩码来形成注意力机制,掩码的原理在于通过另一层新的权重,将图片数据中关键的特征标识出来,通过训练,让网络学到每一张图片中需要关注的区域,也就形成了注意力。
batch size
- 在一般的目标检测框架中,batch size值往往很小,比如RCNN和Faster RCNN中batch size值为2,在RetinaNet和Mask RCNN中,batch size值为16,相比之下,在imagenet中分类模型的batch size值一般设置为256,可以发现这两者间的batch size值差距很大。
- 在目标检测中batch size值很小存在的三个潜在问题:
(1)问题一:训练时间长
(2)问题二:BN统计不准确
(3)问题三:正负样本比例失衡
- 解决上面提出的在目标检测上batch size值过小可能带来的问题:facebook的group normalization试图解决这个问题,这个解决思路在本质上是要在通道上做一些归一化的操作。