批量归一化
为什么需要归一化?
每层网络变换 之后的输出分布都不一样,由于输入是前一层的输出,层层叠加,所以每层的输出分布更奇奇怪怪
归一化:在每层网络之后,将每个样本归一化到均值为0,方差为1的分布中。
但是并不希望每一层的样本分布完全相同,所以每一层都有一个γ和β的线性变化,进行轻微的偏移。γ和β随着训练过程自动调整,所以几乎不完全相同。
分布归一化和非线性激活顺序可以调换。第一个可以是其他的变换,卷积之类的
代码:
引入第三方库
import torch
from torch import nn
from d2l import torch as d2l
归一化处理函数
def batch_norm(x,gamma,beta,moving_mean,moving_var,eps,momentum):
#gamma和beta是可学习参数,moving_mean和moving_var是全局变量,是整个数据集上的均值和方差
#momentum用来更新mean或者var通常取0.9,eps也一样,是固定的值
if not torch.is_grad_enabled():#如果不算梯度,是预测模式
x_hat=(x-moving_mean)/torch.sqrt(moving_var+eps)
#
else:#算梯度,是训练模式
assert len(x.shape) in (2,4)#x的形状是2或者4
#下面求均值和方差
if len(x.shape)==2:#全连接层
mean=x.mean(dim=0)#均值
var=((x-mean)**2).mean(dim=0)#方差也是一个行向量
else:#4是卷积层
mean=x.mean(dim=(0,2,3),keepdim=True)
#把x的第0维,第2维和第3维求均值
#最终是1*n*1*1的矩阵
var=((x-mean)**2).mean(dim=(0,2,3),keepdim=True)
#得到x的初步位置,没有算上拉伸和偏移(可学习参数)
x_hat=(x-mean)/torch.sqrt(var+eps)
#更新两个全局变量
moving_mean=momentum*moving_mean+(1.0-momentum)*mean
moving_var=momentum*moving_var+(1.0-momentum)*var
#得到x的该层的最终估计值
y=gamma*x_hat+beta
return y,moving_mean.data,moving_var.data
定义一个batch norm类
class BatchNorm(nn.Module):
# num_features:完全连接层的输出数量或卷积层的输出通道数
# num_dims:2表示完全连接层,4表示卷积层
def __init__(self,num_features,num_dims):
super().__init__()
if num_dims==2:
shape=(1,num_features)
else:
shape=(1,num_features,1,1)
# 参与求梯度和迭代的拉伸和偏移参数,分别初始化成1和0
#需要去拟合的方差
self.gamma=nn.Parameter(torch.ones(shape))
#需要去拟合的均值
self.beta=nn.Parameter(torch.zeros(shape))
# 非模型参数的变量初始化为0和1
self.moving_mean=torch.zeros(shape)
self.moving_var=torch.ones(shape)
def forward(self,x):
if self.moving_mean.device!=x.device:
# 如果X不在内存上,将moving_mean和moving_var复制到X所在显存上
self.moving_mean=self.moving_mean.to(x.device)
self.moving_var=self.moving_var.to(x.device)
y,self.moving_mean,self.moving_var=batch_norm(x,self.gamma,self.beta,self.moving_mean,self.moving_var,eps=1e-5,momentum=0.9)
return y
定义网络
net = nn.Sequential(nn.Conv2d(1, 6, kernel_size=5),
BatchNorm(6, num_dims=4),#6个输入通道、4是表示卷积层
nn.Sigmoid(),
#卷积、归一化、激活
nn.AvgPool2d(kernel_size=2, stride=2),
#池化
nn.Conv2d(6, 16, kernel_size=5),
BatchNorm(16, num_dims=4), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Flatten(),
nn.Linear(16*4*4, 120),
BatchNorm(120, num_dims=2),
nn.Sigmoid(),
#线性变换、归一化、激活
nn.Linear(120, 84),
BatchNorm(84, num_dims=2),
nn.Sigmoid(),
nn.Linear(84, 10))
训练
lr, num_epochs, batch_size = 1.0, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
结果
简洁版的批量归一化网络
netjianming = nn.Sequential(nn.Conv2d(1, 6, kernel_size=5),
nn.BatchNorm2d(6),
#直接调用学习框架定义的batchnorm,不需要手动定义函数和类
nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, kernel_size=5),
nn.BatchNorm2d(16),
nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Flatten(),
nn.Linear(256, 120),
nn.BatchNorm1d(120),
nn.Sigmoid(),
nn.Linear(120, 84),
nn.BatchNorm1d(84),
nn.Sigmoid(),
nn.Linear(84, 10))
d2l.train_ch6(netjianming, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
残差网络 ResNet
在深度学习中,为了增强模型的学习能力,网络层会变得越来越深,但是随着深度的增加,也带来了比较一些问题,主要包括:
- 模型复杂度上升,网络训练困难;
- 梯度消失/梯度爆炸
- 网络退化,也就是说模型的学习能力达到了饱和,增加网络层数并不能提升精度了。
为此提出了残差网络
因为残差块保留了原始输入的信息,所以网络有如下优势:
- 随着深度的增加,可以获取更高的精度,因为其学习的残差越准确;
- 网络优化比较简单;
- 比较通用;
假如新加的这些层的学习效果非常差,那我们就可以通过一条残差边将这一部分直接“跳过”。实现这一目的很简单,将这些层的权重参数设置为0就行了。这样一来,不管网络中有多少层,效果好的层我们保留,效果不好的我们可以跳过。总之,添加的新网络层至少不会使效果比原来差,就可以较为稳定地通过加深层数来提高模型的效果了。
残差网络为什么可以避免梯度消失其实也很好理解:
在没有残差边的时候,假如网络层数很深的话,要想更新底层的(靠近输入数据部分)的网络权重,首先对其求梯度,根据链式法则需要一直向前累乘,只要其中的任何一个因数过小就会导致求出来的梯度很小很小,这个小梯度就算乘以再大的学习率也是无济于事;
而当我们有了残差边时,求梯度可以直接经过“高速公路”直达我们想要求梯度的对象,此时不管通过链式法则走正常路线得到的梯度多么小,两条路线相加的结果都不会小,就可以很有效地进行梯度更新了
细节
由于需要x+g(x),所以要求x输入通道数与g(x)的输出通道数相同。
如果想要改变通道数,需要引入一个1*1conv来改变x的输入通道数
残差块 由两个3*3卷积层组成,每个卷积层后跟着一个批量归一化和激活函数。而x的输入直接加在第2个卷积层之后
至于加在卷积之后、归一化之激活之后,都可以。效果都差不多捏。
具体代码如下:
import d2l.torch
import torch
from torch.nn import functional as F
from torch import nn
#定义残差类
class Residual(nn.Module): #@save
def __init__(self,input_channels,num_channels,use_1x1conv=False,strides=1):
super().__init__()#继承Module模块的初始化函数
self.conv1=
nn.Conv2d(input_channels,num_channels,kernel_size=3,padding=1,stride=strides)
self.conv2=n
n.Conv2d(num_channels,num_channels,kernel_size=3,padding=1)
if use_1x1conv:
self.conv3=
nn.Conv2d(input_channels,num_channels,kernel_size=1,stride=strides)
else:
self.conv3=None
self.bn1=nn.BatchNorm2d(num_channels)
self.bn2=nn.BatchNorm2d(num_channels)
def forward(self,x):
y=F.relu(self.bn1(self.conv1(x)))#卷积、归一化、激活
y=self.bn2(self.conv2(y))#卷积、归一化
if self.conv3:#看看要不要让x卷积再相加
x=self.conv3(x)
y+=x
return F.relu(y)
#定义ResNet网络的各个模块
#ResNet第一个模块跟GoogleNet第一个模块相同
b1 = nn.Sequential(nn.Conv2d(
in_channels=1,out_channels=64,kernel_size=7,padding=3,stride=2),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3,padding=1,stride=2))
#conv2d尺寸减半,池化也减半,所以第一个模块后,图像尺寸缩小4倍
#开始定义其他模块,这些模块才是真正的残差
#定义一个ResNet块,通常包含两个残差块Residul块(也即是包含两个残差网络层)
#一个ResNet块通常通道数加倍,尺寸形状高和宽减半,
#对应到由第一个残差块输出通道是输入通道两倍,尺寸大小减半
#第二个残差块输入输出通道数相同,输入输出尺寸形状大小不变
#但除开第2个ResNet块,因为第一个ResNet块将输入尺寸形状大小降低了4倍
def resnet_block(input_channels,num_channels,num_residuls,first_block=False):
block = []
for i in range(num_residuls):
if i==0 and not first_block:
#如果该块是两组块中的第一个块,且不是全局第2个块
block.append(
#要加一个1*1的卷积
Residual(input_channels,num_channels,use_1x1conv=True,strides=2))
#调用残差类
else:
block.append(
#直接加x就可以,不用卷积
Residual(num_channels,num_channels))
return block
#剩下的直接调用上述的残差函数
b2 = nn.Sequential(*resnet_block(64,64,2,True))
#*是把列表展开
#第二个ResNet块,输入输出通道数不变,
#因为true,不经过卷积,直接加上x,所以输入输出尺寸形状大小不变
b3 = nn.Sequential(*resnet_block(64,128,2,False))
#第三个ResNet块,输出通道数是输入通道数2倍,
#经过步幅为2的卷积,输出尺寸形状是输入尺寸形状高和宽的1/2
b4 = nn.Sequential(*resnet_block(128,256,2,False))
#第二个ResNet块,输出通道数是输入通道数2倍,则输出尺寸形状是输入尺寸形状高和宽的1/2
b5 = nn.Sequential(*resnet_block(256,512,2,False))
#第二个ResNet块,输出通道数是输入通道数2倍,则输出尺寸形状是输入尺寸形状高和宽的1/2
#把这些模块连接起来
resnet = nn.Sequential(b1,b2,b3,b4,b5,
nn.AdaptiveAvgPool2d((1,1)),
nn.Flatten(),
nn.Linear(in_features=512,out_features=10))
#查看每一层输出的通道数和形状尺寸大小
X = torch.randn(size=(1,1,224,224))
for layer in resnet:
X = layer(X)
print(layer.__class__.__name__," output shape :\t",X.shape)
训练模块
lr,num_epochs,batch_size = 0.05,10,256
train_iter,test_iter = d2l.load_data_fashion_mnist(batch_size,resize=96)
d2l.train_ch6(resnet,train_iter,test_iter,num_epochs,lr,device=d2l.try_gpu())
数据增强
只在训练的时候做增强,测试的时候不做。
算是一个正则项
上下翻转取决于数据集的样子
数据增强,通过变形数据来获取多样性而使得模型泛化性更好
%matplotlib inline
import torch
import torchvision
from torch import nn
from d2l import torch as d2l
#获取图片
d2l.set_figsize()
img=d2l.Image.open("C:/Users/13930/Pictures/Saved Pictures/摩天轮.jpg")
d2l.plt.imshow(img);
#图像增广函数
def apply(img,aug,num_rows=2,num_cols=4,scale=1.5):
y=[aug(img) for _ in range(num_rows*num_cols)]
d2l.show_images(y,num_rows,num_cols,scale=scale)
#随机水平翻转
apply(img,torchvision.transforms.RandomHorizontalFlip())
#随机上下翻转
apply(img,torchvision.transforms.RandomVerticalFlip())
#随机裁剪
shape_aug = torchvision.transforms.RandomResizedCrop(size=(200,200),scale=(0.1,1),ratio=(0.5,2))
#大小200*200,裁剪出来的图片要保存多大的原始图片(10%到100%都可以),高宽比要在0.5和2之间
apply(img,shape_aug)
#随机更改图像的亮度,随机值为原始图像的50%( 1−0.5 )到150%( 1+0.5 )之间(brightness=0.5)。
bright_aug = torchvision.transforms.ColorJitter(brightness=0.5,contrast=0,saturation=0,hue=0)
apply(img,bright_aug)
#改变色调
hue_aug = torchvision.transforms.ColorJitter(brightness=0,contrast=0,saturation=0,hue=0.5)
apply(img,hue_aug)
#亮度、饱和度、色调一起改
colors_aug = torchvision.transforms.ColorJitter(brightness=0.5,contrast=0.5,saturation=0.5,hue=0.5)
apply(img,colors_aug)
#结合多种图像增广的方法
augs = torchvision.transforms.Compose([torchvision.transforms.RandomHorizontalFlip(),colors_aug,shape_aug])
apply(img,augs)