Kaggle猫狗大战——基于Pytorch的CNN网络分类:CNN网络、调参(3)
CNN网络是整个项目的核心部分,我准备分两部分讲,首先是Pytorch中的CNN网络代码的结构,便于大家之后修改,形成自己的网络。另一个是测试一些常见的优秀网络,比如VGG、ResNet等等。
CNN:卷积神经网络
要详细讲卷积神经网络真的是班门弄斧,建议大家还是去找些关键的文献或者专门讲这些的大佬,我这里只讲下pytorch中的神经网络层的函数。首先上框架(一个简单的CNN网络):
class myCNN1(nn.Module):
def __init__(self, num_classes=2):
super(myCNN1, self).__init__()
self.conv1 = nn.Conv2d(3,16,3,padding=1) # 第一个卷积层,输入通道数3,输出通道数16,卷积核大小3*3
self.conv2 = nn.Conv2d(16,16,3,padding=1) # 第二个卷积层,输入通道数16,输出通道数16,卷积核大小3*3
self.fc1 = nn.Linear(56*56*16,128) # 第一个全连接层,线性连接,输入节点数56*56*16,输出节点数128
self.fc2 = nn.Linear(128,64) # 第二个全连接层,线性连接,输入节点数128,输出节点数64
self.fc3 = nn.Linear(64,2) # 第三个全连接层,线性连接,输入节点数64,输出节点数2
def forward(self, x): # 重写父类forward方法,即前向计算,通过该方法获取网络输入数据后的输出值
x = self.conv1(x) # 第一次卷积
x = F.relu(x) # 第一次卷积结果经过ReLU激活函数处理
x = F.max_pool2d(x,2) # 第一次池化,池化大小2*2,方式Max pooling
x = self.conv2(x) # 第二次卷积
x = F.relu(x) # 第二次卷积结果经过ReLU激活函数处理
x = F.max_pool2d(x, 2) # 第二次池化,池化大小2*2,方式Max pooling
x = x.view(x.size()[0],-1) # 由于全连层输入的是一维张量,因此需要对输入的[56*56*16]格式数据排列
x = F.relu(self.fc1(x)) # 第一次全连,ReLU激活
x = F.relu(self.fc2(x)) # 第二次全连,ReLU激活
x = F.relu(self.fc3(x)) # 第三次激活
return x # 返回x
基本有点基础的都大概会知道,卷积神经网络一般分为卷积层、池化层、全连接层、Dropout层等等,另外还会经常用到ReLU激活函数。大部分CNN网络都是由这几个部分组成的,下面我将一边讲这些处理层在Pytorch中的函数形式,一边简单解释下这些处理层的含义。
卷积层(Conv2d函数)
torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True, padding_mode='zeros')
卷积层就很好理解,利用一个NxN的卷积核,将MxM的图片进行卷积,生成一个新的图片。卷积层的作用在于深入挖掘图片信息,尽可能多的获取特征。
第一个参数in_channels表示上一层数据的输入通道数(彩色图片,3通道);
第二个参数out_channels表示输出通道数;
第三个参数kernel_size表示卷积核尺寸;
第四个参数stride表示卷积核移动的步长,默认每隔一步一动;
第四个参数(padding=n),表示在原图周围增加n圈0像素,这样用卷积核进行卷积时,就不会过于缩小原图尺寸(比如如果5x5的卷积核,就加2圈像素);
第五、六个参数一般用不上;
第七个参数bias,一般默认要用,但并不是必须,对于网络性能的影响并不是很显著,除非网络太小导致拟合能力太差。(但是我看AlexNet里就关掉了,不是很懂~小声BB)
池化层(max_pool2d)
torch.nn.max_pool2d(input,kernel_size, stride=None, padding=0, dilation=1, return_indices=False, ceil_mode=False)
这里举的例子是最大值化的池化函数,表示保留nxn池化区域中的最大值,作为整个区域的值,也很好理解。池化层的作用是缩小图片尺寸,降低后续计算的参数量。
第一个参数input是输入的图像;
第二个参数kernel_size表示池化核的尺寸;
其余大部分参数都和卷积层一样,很好理解。
全连接层(Linear)
torch.nn.Linear(in_features,out_features,bias = True)
全连接层最好理解了,因为它不是CNN的概念,而是神经网络的概念,将二维图像信息平铺开来,成为一个一维信息,然后通过多层全连接,最后得到输出结果。
需要注意的是,第一个全连接层的第一个参数需要是其前一层卷积层的输出图像的尺寸*通道数(尺寸一般取决于池化了几次,如用2x2的池化层池化了两次,边长就是224/2/2=56)。
而最后一个全连接层的第二个参数最好设置成与分类项数相同(如本项目中是2分类问题,最终输出参数是2)。当然,2分类问题不一定就要输出为2,也有其他的设置方式。
bias如果设定为FALSE,则图层不会学习附加偏差。
另外就是,全连接层的参数量非常大,比如上例中,这一层的参数量就是56x56x16x128。尽管全连接层看起来简单,但是参数量远远超过卷积层,经常神经网络过大,GPU out of memory的报错都是因为全连接层太大,设参数的时候需要多注意。
激活函数(ReLU)
x = F.relu(x) # 第一次卷积结果经过ReLU激活函数处理
ReLU:全称 Rectified Linear Units激活函数,作用也很明显了,就是剔除小于0的数值,保留大于0的数值。Softplus是它的平滑版。
sigmoid导数值的范围(0, 0.25),tanh的导数值范围(0, 1),可以看出sigmoid的弱点:
对于深度网络,sigmoid在最好的情况下也会把传递的导数数值缩小至0.25倍,下层网络得到的梯度值明显小很多。这会导致模型训练效果很差。对于浅层网络这种影响不明显,但对于深度网络,反向传导逐渐变成了一个“漫长累积”的过程。
ReLU的优点:
(1)反向传播时,可以避免梯度消失。
(2)使一部分神经元的输出为0,形成稀疏网络,减少了参数的相互依存关系,缓解了过拟合问题的发生。
(3)求导简单,整个过程的计算量节省很多。
缺点:
(1)左侧神经元为0,导致神经元死亡,不再更新。
(2)输出非负,仍然存在zigzag现象。
网络测试
接下来是几个网络的测试,首先是VGG19网络(开山之作),但是VGG19参数量有点大(主要在全连接层),RTX2060跑不动,最后 我测试的是VGG16网络,好消息是,根本不收敛 --_–(跳过)
class VGG19(nn.Module):
def __init__(self, num_classes=2):
super(VGG19, self).__init__()
self.conv1 = nn.Conv2d(3,64,3,padding=1)
self.conv2 = nn.Conv2d(64,64,3,padding=1)
self.conv3 = nn.Conv2d(64,128,3,padding=1)
self.conv4 = nn.Conv2d(128,128,3,padding=1)
self.conv5 = nn.Conv2d(128,256,3,padding=1)
self.conv6 = nn.Conv2d(256,256,3,padding=1)
self.conv7 = nn.Conv2d(256,512,3,padding=1)
self.conv8 = nn.Conv2d(512,512,3, padding=1)
self.avgpool = nn.AvgPool2d(7, stride=1)
self.fc1 = nn.Linear(512,num_classes)
def forward(self, x): # 重写父类forward方法,即前向计算,通过该方法获取网络输入数据后的输出值
x = F.relu(self.conv1(x))
x = F.relu(self.conv2(x))
x = F.max_pool2d(x, 2)
x = F.relu(self.conv3(x))
x = F.relu(self.conv4(x))
x = F.max_pool2d(x,2)
x = F.relu(self.conv5(x))
x = F.relu(self.conv6(x))
x = F.relu(self.conv6(x))
x = F.relu(self.conv6(x))
x = F.max_pool2d(x, 2)
x = F.relu(self.conv7(x))
x = F.relu(self.conv8(x))
x = F.relu(self.conv8(x))
x = F.relu(self.conv8(x))
x = F.max_pool2d(x, 2)
x = F.relu(self.conv8(x))
x = F.relu(self.conv8(x))
x = F.relu(self.conv8(x))
x = F.relu(self.conv8(x))
x = F.max_pool2d(x, 2)
x = self.avgpool(x)
x = x.view(x.size()[0],-1) # 由于全连层输入的是一维张量,因此需要对输入的[x*x*x]格式数据排列
x = self.fc1(x) # 第一次全连,ReLU激活
return x # 返回x
VGG网络最大的特点就是大量利用了3x3的卷积核,AlexNet则利用了更多奇形怪状(指尺寸)的卷积核,没有太大的区别。好消息是,也没有收敛,不知道是不是我哪儿设置的不好。
class AlexNet(nn.Module):
def __init__(self, num_classes=2):
super(AlexNet, self).__init__()
self.conv1 = nn.Conv2d(3,96,11,4,padding=2,bias=False)
self.conv2 = nn.Conv2d(96,192,5,padding=2,bias=False)
self.conv3 = nn.Conv2d(192,384,3,padding=1,bias=False)
self.conv4 = nn.Conv2d(384,256,3,padding=1,bias=False)
self.conv5 = nn.Conv2d(256,256,3,padding=1,bias=False)
self.fc1 = nn.Linear(6*6*256,4096)
self.fc2 = nn.Linear(4096,4096)
self.fc3 = nn.Linear(4096,num_classes)
self.dp1 = nn.Dropout(p=0.5)
def forward(self, x): # 重写父类forward方法,即前向计算,通过该方法获取网络输入数据后的输出值
x = F.relu(self.conv1(x),inplace=True)
x = F.max_pool2d(x,3,2,padding=0)
x = F.relu(self.conv2(x),inplace=True)
x = F.max_pool2d(x,3,2,padding=0)
x = F.relu(self.conv3(x),inplace=True)
x = F.relu(self.conv4(x),inplace=True)
x = F.relu(self.conv5(x),inplace=True)
x = F.max_pool2d(x,3,2,padding=0)
x = x.view(x.size()[0],-1) # 由于全连层输入的是一维张量,因此需要对输入的[x*x*x]格式数据排列
x = self.dp1(x)
x = F.relu(self.fc1(x),inplace=True) # 第一次全连,ReLU激活
x = self.dp1(x)
x = F.relu(self.fc2(x),inplace=True) # 第二次全连,ReLU激活
x = self.fc3(x) # 第三次全连
return x # 返回x
最后我测试的是ResNet34残差网络,结果感人的好,残差网络不愧是CNN网络跨越式的进步~!!!(代码是抄的,抄了好几个ResNet,个人感觉只有这个写得最好,原网址 link.)
def conv3x3(in_planes, out_planes, stride=1):
"""3x3 convolution with padding"""
return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,padding=1, bias=False)
class BasicBlock(nn.Module):
expansion = 1
def __init__(self, inplanes, planes, stride=1, downsample=None):
super(BasicBlock, self).__init__()
self.conv1 = conv3x3(inplanes, planes, stride)
self.bn1 = nn.BatchNorm2d(planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = conv3x3(planes, planes)
self.bn2 = nn.BatchNorm2d(planes)
self.downsample = downsample
self.stride = stride
def forward(self, x):
residual = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
if self.downsample is not None:
residual = self.downsample(x)
out += residual
out = self.relu(out)
return out
class Bottleneck(nn.Module):
expansion = 4
def __init__(self, inplanes, planes, stride=1, downsample=None):
super(Bottleneck, self).__init__()
self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, bias=False)
self.bn1 = nn.BatchNorm2d(planes)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride,
padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(planes)
self.conv3 = nn.Conv2d(planes, planes * self.expansion, kernel_size=1, bias=False)
self.bn3 = nn.BatchNorm2d(planes * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stride = stride
def forward(self, x):
residual = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
if self.downsample is not None:
residual = self.downsample(x)
out += residual
out = self.relu(out)
return out
class ResNet(nn.Module):
def __init__(self, block, layers, num_classes=1000):
self.inplanes = 64
super(ResNet, self).__init__()
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3,
bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.layer1 = self._make_layer(block, 64, layers[0])
self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
self.avgpool = nn.AvgPool2d(7, stride=1)
self.fc = nn.Linear(512 * block.expansion, num_classes)
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
def _make_layer(self, block, planes, blocks, stride=1):
downsample = None
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
nn.Conv2d(self.inplanes, planes * block.expansion,
kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(planes * block.expansion),
)
layers = []
layers.append(block(self.inplanes, planes, stride, downsample))
self.inplanes = planes * block.expansion
for i in range(1, blocks):
layers.append(block(self.inplanes, planes))
return nn.Sequential(*layers)
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.avgpool(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
def model_A(num_classes):
return ResNet(BasicBlock,[3,4,6,3],num_classes)
自行设计网络
说实话作业让自行设计网络真是要逼着一个小白去死,还是在原网络的基础上进行一些改进比较现实。我的设计思路是基于VGG19网络(对,就是我RTX2060跑不动那个),进行改进,一方面利用1x1的卷积核对每个卷积层进行修改,降低参数量,详情可以看这篇博客(拜大佬) link.
另一方面,添加残差回归,降低网络加深的影响(其实本质就是ResNet网络),但是确实菜,还请大家别介意。跟ResNet的区别可能就是,ResNet采用跳跃卷积(步长=2)来缩小图片尺寸,而我用池化层来缩小图片尺寸。最后添加了全连接层。(代码在最后)
训练效果还可以,30次迭代后准确率已经稳定在了95%以上(所以说残差网络永远滴神!!!)这里显示得不太好,最优准确率被挡住了。
如果不使用数据增广,训练集收敛的更快了,但是测试集的最终准确率略有降低,所以说数据增广的泛化能力还是有意义的。(数据增广后,训练难度加大,但是训练出来的网络适应性更强,是这个道理没错了)
如果不使用残差机理,只是缩小了网络参数,还是能够训练+收敛,不过准确率大幅下降了。(所以说残差网络还是永远滴神!!!)
models.py
from torchvision import models
import torch.nn as nn
import torch
import torch.utils.data
import torch.nn.functional as F
import math
class Bottleneck(nn.Module):
expansion = 4
def __init__(self, inplanes, planes, stride=1, downsample=None):
super(Bottleneck, self).__init__()
self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, bias=False)
self.bn1 = nn.BatchNorm2d(planes)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(planes)
self.conv3 = nn.Conv2d(planes, planes * self.expansion, kernel_size=1, bias=False)
self.bn3 = nn.BatchNorm2d(planes * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stride = stride
def forward(self, x):
residual = x
out = self.relu(self.bn1(self.conv1(x)))
out = self.relu(self.bn2(self.conv2(out)))
out = self.bn3(self.conv3(out))
if self.downsample is not None:
residual = self.downsample(x)
out += residual
out = self.relu(out)
return out
class myCNN(nn.Module):
def __init__(self, block, layers, num_classes=2):
self.inplanes = 32
super(myCNN, self).__init__()
self.conv1 = nn.Conv2d(3, 32, kernel_size=7, stride=2, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(32)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(3, stride=2,padding=1)
self.layer1 = self._make_layer(block, 16, layers[0])
self.layer2 = self._make_layer(block, 32, layers[1])
self.layer3 = self._make_layer(block, 64, layers[2])
self.layer4 = self._make_layer(block, 128, layers[3])
self.layer5 = self._make_layer(block, 128, layers[4])
self.avgpool = nn.AvgPool2d(7, stride=1)
self.fc1 = nn.Linear(128 * block.expansion, 512)
self.fc2 = nn.Linear(512,num_classes)
for m in self.modules():
if isinstance(m, nn.Conv2d):
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
def _make_layer(self, block, planes, blocks, stride=1):
downsample = None
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
nn.Conv2d(self.inplanes, planes * block.expansion, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(planes * block.expansion),
)
layers = []
layers.append(block(self.inplanes, planes, stride, downsample))
self.inplanes = planes * block.expansion
for i in range(1, blocks):
layers.append(block(self.inplanes, planes))
return nn.Sequential(*layers)
def forward(self, x):
x = self.relu(self.bn1(self.conv1(x)))
x = self.maxpool(self.layer1(x))
x = self.maxpool(self.layer2(x))
x = self.maxpool(self.layer3(x))
x = self.maxpool(self.layer4(x))
x = self.avgpool(self.layer5(x))
x = x.view(x.size(0), -1)
x = self.relu(self.fc1(x))
x = self.fc2(x)
return x
def model_A(num_classes):
return myCNN(Bottleneck,[1,2,4,4,4],num_classes)