李哥深度学习 第四节 图像分类前置知识
一 、为什么图片分类任务要用卷积神经网络?
前面我们学习了最简单的线性回归,但是其输入是一个向量,而图片分类任务的输入是图片,即使将其展开,也会因为参数量爆炸而不适合做训练,所以需要一种做法来提取特征,降低输入量。
二、卷积神经网络是什么?
1、网络结构

特征图:3x6x6,深度为3,形状为6x6
卷积核:3x3x3,深度为3,形状为3x3

如图所示,3x3的卷积核每次会与特征图上3x3大小的面积上逐元素相乘累加,得到一幅新的特征图
在深度为3时,会先得到深度为3的中间特征图,然后对应位置相加,就可以把深度变成1,然后全部拼起来,变成新特征图
新的特征图: 作为输出,可以与新的卷积核进行卷积
2、网络结构分析与参数量计算

1,3*3

可见,原特征图的宽被卷积了3次,所以新特征图应为3x3
2,2x2
和第一问同理
3,4x4
padding是一种防止新特征图不断缩小的方法,它会扩充原本的特征图,并填充0。例如本题padding=1,即:
可见,原特征图的宽均被卷积了4次,所以新特征图应为4x4
4,98x98
通过前面的分析,可以发现原特征图的宽总是被卷积 原特征图的宽 - 卷积核的宽 + 1 次,所以新特征图的宽总是为:
**原特征图的宽 - 卷积核的宽 + 1 **,本题中也就是 100 - 3 + 1 = 98 ,故形状为 98x98
5,100x100
padding=1,故原特征图的宽变为102,新特征图宽为 102 - 3 + 1 = 100 ,故形状为 100x100
6,2x2 7x7
特征图:8-7+1=2,故为2x2
参数量:卷积核参数量就是一共有多少个数,就是7x7
7,94x94
100-7+1=94

1,3
100+2*padding-7+1 = 100,解得 padding=3
2,224*224
224+2*2-5+1=224
3,3*3*3 27
和卷积核有关的参数有:卷积核数量,卷积核深度和形状
卷积核数量为1已知,卷积核深度与特征图深度相同为3,卷积核形状 4+2*1-x+1=4,解得x=3,故形状为3*3
卷积核参数量为 1*3*3*3=27
4,7*4*4
卷积核数量为7,故卷出7个新的特征图,每个特征图的大小为 4+2*1-3+1=4 即4*4,故为7*4*4
5,64*224*224
224+2-3+1=224,故为64*224*224
6,128*224*224 64 3*3*64*128
224+2-3+1=224,故为128*224*224
X=64,卷积核深度永远与特征图一致
128*64*3*3
3、特征图怎么变小
卷积可以减小特征图,但是不太够。所以又引进了扩大步长和池化的方法
3.1扩大步长


3.2池化(Pooling)


三、卷积神经网络的基本网络结构和loss
1、基本网络结构
卷积核的数量会影响新特征图的深度,卷积核的形状会影响新特征图的形状,所以通过设计这两样参数,理论上我们可以获得任意的输出。
最少的情况下,一次卷积就可以获得想要的结果,但是实战中往往会设计更多的连接层来获得更好的效果
通过卷积和池化,我们缩小了特征图,减小了参数量,然后再进行展开,接入线性回归模型,就可以实现预测。


1024*7*7=50176
2、loss的计算
首先,我们可以看到,预测值和真实值往往有较大的差异,所以要先借助Softmax公式,将其映射为概率分布

接下来计算loss,下图第一张ppt说的是如何处理求将一个预测向量的loss,第二张ppt说的是如何处理一个batch的loss

四、经典卷积模型
1、AlexNet
1.1模型特点
AlexNet在当时主要的创新点在于relu激活函数,drop out,池化和归一化
drop put是随机丢弃一部分神经元
归一化就是减均值除以标准差
1.2模型分析

输入图片大小为3*224*224
放入卷积层Conv2d(3, 64, 11, 4,2),输出深度为64,输出宽度(224+2*2-11)/4+1=55.25,得到新特征图64*55*55
池化层为3,步长为2,(55-3+1)/2+1=27.5,故池化后为64*27*27
放入卷积层Conv2d(64, 192, 5, 1, 2),输出深度为192,输出宽度(27+2*2-5)/1+1=27,得到新特征图192*27*27
池化层为3,步长为2,(27-3+1)/2+1=13.5,故池化后为192*13*13
放入卷积层Conv2d(192, 384, 3, 1, 1),输出深度为384,输出宽度(13+1*2-3)/1+1=13,得到新特征图384*13*13
放入卷积层Conv2d(384, 256, 3, 1, 1),输出深度为256,输出宽度(13+1*2-3)/1+1=13,得到新特征图256*13*13
放入卷积层Conv2d(256, 256, 3, 1, 1),输出深度为256,输出宽度(13+1*2-3)/1+1=13,得到新特征图256*13*13
池化层为3,步长为2,(13-3+1)/2+1=6.5,故池化后为256*6*6
adaPool(6)将尺寸固定为6*6
flatten拉直,拉直后特征量为256*6*6=9216
放入全连接层Linear(9216, 4096),特征量被映射为4096
放入全连接层Linear(4096, 4096),特征量被映射为4096
放入全连接层Linear(4096, 1000),特征量被映射为1000
1.3参数量统计
(每一层的卷积核总大小和偏置量,池化层没有卷积核也就没有参数)
Conv2d(3, 64, 11, 4,2) : 3*11*11*64+64 = 23296
Conv2d(64, 192, 5, 1, 2) : 307392
Conv2d(192, 384, 3, 1, 1):663936
Conv2d(384, 256, 3, 1, 1):884992
Conv2d(256, 256, 3, 1, 1):590080
Linear(9216, 4096) : 9216*4096+4096 = 37752832
Linear(4096, 4096) : 16781312
Linear(4096, 1000) : 4097000
总计:61100840
1.4代码
只有前向
import torchvision.models as models
import torch
import torch.nn as nn
alexnet = models.alexnet()
print(alexnet)
class myAlexnet(nn.Module):
def __init__(self):
super(myAlexnet, self).__init__()
self.conv1 = nn.Conv2d(3, 64, 11, 4, padding=2) #bias 64 23232
self.pool1 = nn.MaxPool2d(3,2)
self.conv2 = nn.Conv2d(64, 192, 5, 1, padding=2)
self.pool2 = nn.MaxPool2d(3,2)
self.conv3 = nn.Conv2d(192, 384, 3, 1, 1)
self.conv4 = nn.Conv2d(384, 256, 3, 1, 1)
self.conv5 = nn.Conv2d(256, 256, 3, 1, 1)
self.pool3 = nn.MaxPool2d(3, 2)
self.pool4 = nn.AdaptiveAvgPool2d(6)
self.fc1 = nn.Linear(9216, 4096)
self.fc2 = nn.Linear(4096, 4096)
self.fc3 = nn.Linear(4096, 1000)
def forward(self, x):
x = self.conv1(x)
x = self.pool1(x)
x = self.conv2(x)
x = self.pool2(x)
x = self.conv3(x)
x = self.conv4(x)
x = self.conv5(x)
x = self.pool3(x)
x = self.pool4(x)
x = x.view(x.size()[0], -1) # view 变形 batch * tezheng ,保留了第一个维度,也就是batch的维度,剩下的全部合并到第二个维度
x = self.fc1(x)
x = self.fc2(x)
x = self.fc3(x)
return x
mymodel = myAlexnet() #实例化
input = torch.ones((4, 3, 224, 224)) # batch * channel * H * W
out = mymodel(input)
print(out.shape)
def get_parameter_number(model):
total_num = sum(p.numel() for p in model.parameters())
trainable_num = sum(p.numel() for p in model.parameters() if p.requires_grad)
return {'Total': total_num, 'Trainable': trainable_num}
print(get_parameter_number((mymodel)))
# print(get_parameter_number((mymodel.conv1)))
# print(get_parameter_number((mymodel.conv2)))
# print(get_parameter_number((mymodel.conv3)))
# print(get_parameter_number((mymodel.conv4)))
# print(get_parameter_number((mymodel.conv5)))
# print(get_parameter_number((mymodel.fc1)))
# print(get_parameter_number((mymodel.fc2)))
# print(get_parameter_number((mymodel.fc3)))
# print(get_parameter_number((mymodel.conv1))['Total']+get_parameter_number((mymodel.conv2))['Total']+
# get_parameter_number((mymodel.conv3))['Total']+get_parameter_number((mymodel.conv4))['Total']+
# get_parameter_number((mymodel.conv5))['Total']+get_parameter_number((mymodel.fc1))['Total']+
# get_parameter_number((mymodel.fc2))['Total']+get_parameter_number((mymodel.fc3))['Total'])
2、VggNet
2.1模型特点


用两次卷积代替了一次卷积
2.2模型分析

输入图片大小为3*224*224
VggNet每个layer都是固定两次卷积一次池化,会提升深度,降低图大小
例如layer1:
放入卷积层Conv2d(3, 64, 3, 1, 1),新特征图64*224*224
放入卷积层Conv2d(64, 64, 3, 1, 1),新特征图64*224*224
最大池化MaxPool2d(2),窗口为2*2,步长为2,新特征图为64*112*112
接下来以此类推,新特征图依次为:
128*64*64
256*32*32
512*16*16
最后一次的池化变为自适应平均池化AdaptiveAvgPool2d(7),故输出新特征图512*7*7
flatten拉直,拉直后特征量为512*7*7=25088
放入全连接层,特征量依次被映射为4096,1000
2.3参数量统计
layer1 =(64*3*3*3+64)+(64*64*3*3+64)=38720
layer2 = 221440
layer3 = 885248
layer4 = 3539968
layer5 = 4719616
fc1 = 102764544
fc2 = 16781312
fc3 = 4097000
总计:133047848
2.4代码
只有前向
import torchvision.models as models
import torch.nn as nn
vgg = models.vgg13()
print(vgg)
class vggLayer(nn.Module):
def __init__(self,in_cha, mid_cha, out_cha):
super(vggLayer, self).__init__()
self.relu = nn.ReLU()
self.pool = nn.MaxPool2d(2)
self.conv1 = nn.Conv2d(in_cha, mid_cha, 3, 1, 1)
self.conv2 = nn.Conv2d(mid_cha, out_cha, 3, 1, 1)
def forward(self, x):
x = self.conv1(x)
x= self.relu(x)
x = self.conv2(x)
x = self.relu(x)
x = self.pool(x)
return x
class MyVgg(nn.Module):
def __init__(self):
super(MyVgg, self).__init__()
self.layer1 = vggLayer(3, 64, 64)
self.layer2 = vggLayer(64, 128, 128)
self.layer3 = vggLayer(128, 256, 256)
self.layer4 = vggLayer(256, 512, 512)
self.layer5 = vggLayer(512, 512, 512)
self.adapool = nn.AdaptiveAvgPool2d(7)
self.relu = nn.ReLU()
self.fc1 = nn.Linear(25088, 4096)
self.fc2 = nn.Linear(4096, 4096)
self.fc3 = nn.Linear(4096, 1000)
def forward(self,x):
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.layer5(x)
x = self.adapool(x)
x= self.adapool(x)
x = x.view(x.size()[0], -1)
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
x = self.relu(x)
x = self.fc3(x)
x = self.relu(x)
return x
import torch
myVgg = MyVgg()
img = torch.zeros((1, 3, 224,224))
out = myVgg(img)
print(out.size())
def get_parameter_number(model):
total_num = sum(p.numel() for p in model.parameters())
trainable_num = sum(p.numel() for p in model.parameters() if p.requires_grad)
return {'Total': total_num, 'Trainable': trainable_num}
# print(get_parameter_number(myVgg.layer1))
# print(get_parameter_number(myVgg.layer2))
# print(get_parameter_number(myVgg.layer3))
# print(get_parameter_number(myVgg.layer4))
# print(get_parameter_number(myVgg.layer5))
# print(get_parameter_number(myVgg.fc1))
# print(get_parameter_number(myVgg.fc2))
# print(get_parameter_number(myVgg.fc3))
#
# print("\n")
#
# print(get_parameter_number(myVgg.layer1)['Total']+get_parameter_number(myVgg.layer2)['Total']
# +get_parameter_number(myVgg.layer3)['Total']+get_parameter_number(myVgg.layer4)['Total']
# +get_parameter_number(myVgg.layer5)['Total']+get_parameter_number(myVgg.fc1)['Total']
# +get_parameter_number(myVgg.fc2)['Total']+get_parameter_number(myVgg.fc3)['Total'])
# print(get_parameter_number(vgg))
3、ResNet
3.1模型特点
resNet的创新主要在于残差连接和1x1卷积
残差连接将输入x直接加到输出结果f(x)上,是的out=f(x)+x

残差连接缓解了梯度消失,因为我们知道网络层数较深时,使用链式求导法则展开很容易让式子变得特别长,

若每一项都小于1,很容易造成梯度特别特别小。而通过残差连接,out=f(x)+x,那么out’=f’(x)+1,可以让偏导数不会消失
如果输入和输出的维度不匹配会导致无法进行残差连接,比如:

输入维度和输出维度不匹配,于是考虑卷积核使用1x1卷积(也就是卷积核的尺寸为1x1),同时输出调整为128,通过这两个参数就实现了深度从64->128的变化,然后步长设置为2,这样尺寸的宽会缩小一半,也就是从56*56->28*28,padding不变,就实现了维度的控制。

3.2模型分析
ResNet有很多种结构,本例的代码实现的是18-layer结构。不同于AlexNet和VggNet的固定结构,ResNet这张表参数并不完整,需要自行计算+一些约定俗成的设计思想来完成填补

3.3代码
import torch
import torch.nn as nn
import torchvision.models as models
resNet = models.resnet18()
print(resNet)
class Residual_block(nn.Module): # @save
def __init__(self, input_channels, out_channels, down_sample=False, strides=1): # 步长默认为1
super().__init__()
self.conv1 = nn.Conv2d(input_channels, out_channels,
kernel_size=3, padding=1, stride=strides) # 步长可变
self.conv2 = nn.Conv2d(out_channels, out_channels,
kernel_size=3, padding=1, stride= 1) # 步长恒为1
if input_channels != out_channels: # 若输入通道数和输出通道数不一致,则需要一个1x1的卷积层
self.conv3 = nn.Conv2d(input_channels, out_channels,
kernel_size=1, stride=strides)
else: # 若输入通道数和输出通道数一致,则不需要一个1x1的卷积层
self.conv3 = None
self.bn1 = nn.BatchNorm2d(out_channels) # 批量归一化层
self.bn2 = nn.BatchNorm2d(out_channels) # 批量归一化层
self.relu = nn.ReLU() # ReLU层
def forward(self, X):
out = self.relu(self.bn1(self.conv1(X))) # 经过第一层需要激活
out= self.bn2(self.conv2(out))
if self.conv3:
X = self.conv3(X) # X进行维度变换
out += X # 残差连接
return self.relu(out)
class MyResNet18(nn.Module):
def __init__(self):
super(MyResNet18, self).__init__()
self.conv1 = nn.Conv2d(3, 64, 7, 2, 3)
self.bn1 = nn.BatchNorm2d(64) # 将64个通道的特征图做归一化处理
self.pool1 = nn.MaxPool2d(3, stride=2, padding=1)
self.relu = nn.ReLU()
self.layer1 = nn.Sequential( # 容器模块,用于按顺序堆叠多个神经网络层。
Residual_block(64, 64),
Residual_block(64, 64)
)
self.layer2 = nn.Sequential(
Residual_block(64, 128, strides=2),
Residual_block(128, 128)
)
self.layer3 = nn.Sequential(
Residual_block(128, 256, strides=2),
Residual_block(256, 256)
)
self.layer4 = nn.Sequential(
Residual_block(256, 512, strides=2),
Residual_block(512, 512)
)
self.flatten = nn.Flatten()
self.adv_pool = nn.AdaptiveAvgPool2d(1)
self.fc = nn.Linear(512, 1000)
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.pool1(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.adv_pool(x)
x = self.flatten(x)
x = self.fc(x)
return x
myres = MyResNet18()
def get_parameter_number(model):
total_num = sum(p.numel() for p in model.parameters())
trainable_num = sum(p.numel() for p in model.parameters() if p.requires_grad)
return {'Total': total_num, 'Trainable': trainable_num}
print(get_parameter_number(myres.layer1))
print(get_parameter_number(myres.layer1[0].conv1))
print(get_parameter_number(resNet.layer1[0].conv1))
x = torch.rand((1,3,224,224))
out = resNet(x)
out = myres(x)
以上均为课后的个人总结,可能会存在一些问题,欢迎指正
更多推荐
所有评论(0)