图片分类任务
文章目录
- 图片分类任务
- 分类任务
- 回归和分类
- 如何做分类的输出
- 图片分类
- 卷积神经网络
- 保持特征图大小不变
- 更大的卷积核和更多的卷积核层数
- 特征图怎么变小
- 卷积神经网络中特征图改变
- 卷积到全连接
- 分类任务的LOSS
- 一个基本的分类神经网络
- 经典神经网络
- AlexNet
- VggNet
- ResNet
- 神经网络透析
- 卷积和全连接的关系
- 深度学习= 玩特征
分类任务
许多现实任务,都不是预测问题(预测一个值,是在一个连续的范围取结果),而是分类问题。分类问题就像一道选择题,比如判断当前的动物是猫🐱还是狗🐕,问题的答案要么是猫要么是狗,是一个离散的结果。
回归和分类
从数据的角度看,回归任务和分类任务的区别:
- 回归任务是找到一个模型,模型能够预测输入的位置。
- 分类任务是找的一个模型,模型能够将不同类型的数据分隔开来。
如何做分类的输出
从训练流程上看,回归任务和分类任务的区别是模型的输入和输出是不同的,输入是给定的,但输出的类型是值得研究的,如果像回归任务一样通过深度神经网络,在神经网络的最后一层为输出维度为1,即使用nn.Linear(dim,1)
,然后根据得到的预测值与表示不同类型的值的关系的远近,是不合理的。
举个例子,假设预测值和各类型所表示的数据的距离关系如下:
如果是通过预测值和各类型所表示的数据的距离的远近来判断是哪个类型,离0最近,也就是任务是0表示的类型,但也默认了一件事,相比4表示的类型,更可能是1表示的类型,因为距离不同,但是分类任务的性质是每个类型是等价的,如果有0,1,2,3,4几个类型,是0类型,就不可能是别的类型,其他类型没有区别,因此该方法是存在问题的。
分类任务的标签Y是用独热编码表示类型,模型的输出结果是对应长度的向量,认为最大值所在的下标为预测类。
图片分类
图片分类的输入X为表示图片的矩阵,输出为类型。
由于模型的输入类型是确定的,通过规定输入的图片为224*224大小的,模型得到的预测值为图片为各个类型的概率,LOSS采用交叉熵的方式进行计算。
卷积神经网络
图片的表示方式通常是RGB三元色表示矩阵,三种颜色分别对应一个表示矩阵,图片分类的输入为一个3*224*224的矩阵,为了能通过神经网络模型,使用全连接方式,我们尝试把图片矩阵“拉直“成三个向量,然后合并成一个向量输入可以吗?
答案是不行的,因为这样做的话输入为近十几万维度的向量,这样的数据量太大了,很容易出现过拟合等的问题。
为了解决以上的问题,引入卷积神经网络。为了引入卷积神经网络,我们举一个例子,我们来考虑如下场景:
人工智能让机器模仿人类的方式解决问题,因此我们先考虑一下对于这个问题,人类是如何思考的?小图是一个3*3的图像,因此将其与大图中每个3*3的子图进行对比,如果存在和小图一样的子图,就说明大图含有小图。仿照这个方式,让机器如何做呢?,在矩阵中1表示红色,-1表示黑色:
将大图和小图用矩阵表示:
让小图表示矩阵分别和大图中不同位置的子图矩阵进行比对,这个作用于不同位置的滚动过程称为”卷“,每次对比时让小图和子图的矩阵对应位置相乘然后求和称为”积“,得到一个新的矩阵:
将用于对比的小图称为卷积核,将能够进行卷积的图称为特征图。
假设要做的任务是,判断图片中是否为鸟类,卷积核中可以是鸟的某样有特点的部分,比如鸟嘴,鸟眼睛,鸟脚。如果图中是鸟,那么在卷积到图中相应位置,会得到数据较大的值,就可以判断图中大概率是鸟,因此不需要对整张图片都卷积:
由于卷积核的选取是不唯一的,因此卷积神经网路目标就是如何卷积核变得像我们想要的,最后得到一个有意义的特征图。
由于图片是由RGB三元色矩阵表示的,因此卷积核也要对应有3个矩阵,卷积时卷积核中三个矩阵和特征图子图对应矩阵的对应位置进行积。卷积核的大小称为权重(参数量),也称为神经元的感受野。假设如下图卷积核大小为3*3*3,那么每次”积“的时候就是27个乘积相加得到一个值,加入到特征图中。
保持特征图大小不变
直接使用特征图进行卷积,得到的特征图大小会比被卷积的特征图大小小:
为了统一规划,需要让特征图卷积后的大小不变,为此要给被卷积的特征图进行零填充,也就是在周围加上一圈零:
更大的卷积核和更多的卷积核层数
卷积核更大形象来说就是看到更多的特征信息,也就是更大的感受野:
要加深卷积神经网络的层数,就会有更多的卷积核层数。加深卷积神经网络层数就是多次对得到的特征图进行卷积,首先对原始图片进行卷积,得到特征图,有多个卷积核时,会得到多个特征图,若要继续进行卷积,需要将得到的多个特征图看成多层的特征图进行卷积,卷积核的层数要和其层数一致:
在卷积的过程中,原始图片是给出的,特征图是计算出的,因此卷积神经网络中模型就是卷积核,卷积核的数据量即为参数量(忽略偏置)。
手算卷积神经网络题目
举例计算题目6图例:
另外有计算公式为:卷出来的特征图的大小=特征图大小-卷积核大小+1。(大小为4,即为图片为4*4)注意零填充是在周围一圈一圈加零,零填充后图片大小为原大小加上2倍的padding。
举例计算题目6:
特征图怎么变小
扩大步长
将特征图中的像素点,每隔一个去掉一个,减少特征图的样本量,不影响判断图中的类型,也就是减少一部分像素点不影响判断:
扩大步长就是卷积不是一个格子一个格子来的,不会把每个对应子图进行卷积,如果对每个对应子图进行卷积,每次只移动一步,扩大步长是在对一个子图卷积后,和移动多步后得到的子图卷积:
采用扩大步长时,计算卷积得到的特征图大小有如下公式:
- O=(I-K+2P)/S+1
- I:特征图大小 K:卷积核大小 P: padding S :步长 。
比如:输入 3*224*224, 卷积核大小为11, padding = 2 Stride = 4 , 卷积核数量64, 问输出多少。 (64*55*55)
扩大步长存在的问题是会丢失信息,且引入计算。
池化(pooling)
池化是通过选择最大值或平均值等方法来保留该区域的重要信息,或者说用一个数据代表多个数据。如下图,用一个值代表原先2*2大小的数据:
池化分为最大池化和平均池化。
- 最大池化(Max Pooling):选择池化窗口内的最大值作为输出。它保留了局部区域内最显著的特征,常用于提取强特征。
- 平均池化(Average Pooling):计算池化窗口内所有值的平均值作为输出。它会更平滑地保留特征,不会像最大池化那样强调最强的特征。
- 最大池化更加常用,因为最大池化不引入计算,平均池化引入了计算,并且对于一个图片通过更关注最显著的特征。
Pooling(2)
表示用一个数表示原有2*2的数据。AdaptiveAvgPool(7)
表示将原图像池化成7*7大小的。假设有224*224大小的图片池化成7*7大小的图片,需要用一个数据表示原有32*32大小的数据。(从行的角度思考,列同理,原先有224行,变成7行就是每224/7=32变成一个数)
卷积神经网络中特征图改变
**改变特征图的厚度(通道数)需要用卷积核进行卷积,**每个卷积核卷积得到厚度为1的特征图,有几个卷积核就能得到多少厚度的特征图。改变特征图的大小使用池化。
深度学习是一个”黑盒子“,卷积和池化的顺序和次数怎么安排比较好是不确定的,但通常在卷积神经网络中卷积和池化交替使用,通道数一般要大,特征图大小一般要小。
特征图的参数量=通道数*特征图大小,特征图大小减小的速度通常比通道数增大的速度快,因此参数量是在减小的。
卷积到全连接
通过卷积减小数据量,是为了通过全连接得到预测值向量。
标签Y中的数据表示是某一类的概率,而全连接得到的输出显而易见不是概率,因为相加都不一定为1,因此和标签的值表示的含义不同,需要将其通过计算转化,将其输入softmax后得到的输出作为模型预测的概率分布,在代码中实现就是调用nn.Softmax
:
分类任务的LOSS
分类任务的预测值和标签不是单纯的一个数值,不能用简单的相减的方法计算LOSS值,采用交叉熵的方式计算LOSS值。
库中提供的交叉熵计算函数只需要给出全连接输出和标签即可,全连接输出的转换函数自身会完成。
一个基本的分类神经网络
- 前向过程:将输入的特征图通过若干次卷积和池化,然后通过若干层全连接,将全连接输出通过softmax得到预测值,计算LOSS。
- 回传过程:根据损失函数对模型参数的梯度进行计算,然后通过优化算法(如梯度下降)调整模型参数,减少损失。
分类任务的模型要通过大量的数据训练才能有很好的效果。
经典神经网络
AlexNet
AlexNet 在 ImageNet LSVRC-2012的比赛中, 取得了top-5错误率为15.3%的成绩。 第二名 是30多的错误率。 AlexNet有6亿个参数和650,000个神经元,包 含5个卷积层,有些层后面跟了max-pooling层, 3个全连接层 自AlexNet,引起了深度学习的狂潮。
- relu:方便计算,一定程度减小梯度消失。
- dropout:在每一轮训练过程中,随机"丢弃"神经网络中一定比例的神经元(即设置为0),可以缓解过拟合。
- 归一化:消除数据量纲的影响。
归一化可以在样本中对某一列进行归一化,也可以对一批处理过的数据进行批归一化。
AlexNet模型
Conv2d
表示卷积。Pool(3,2)
表示每次池化大小为3*3的区域,池化区域的变化每次步长为2,实际效果等于行,列减半。具体来说,第一次取1,2,3行,第二次取3,4,5行。AdaPool(6)
表示池化为6*6的大小。- 图中蓝色为特征图大小。
- 图中参数量忽略了偏置值,每个卷积核有一个偏置值。
模型代码表示
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
class AlexNet(nn.Module):
def __init__(self, num_classes):
super(AlexNet, self).__init__()
# 定义网络结构
self.conv1 = nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2) # 输入3个通道,输出64个通道,卷积核大小11x11,步长4,填充2
self.conv2 = nn.Conv2d(64, 192, kernel_size=5, padding=2) # 输入64个通道,输出192个通道,卷积核大小5x5,填充2
self.conv3 = nn.Conv2d(192, 384, kernel_size=3, padding=1) # 输入192个通道,输出384个通道,卷积核大小3x3,填充1
self.conv4 = nn.Conv2d(384, 256, kernel_size=3, padding=1) # 输入384个通道,输出256个通道,卷积核大小3x3,填充1
self.conv5 = nn.Conv2d(256, 256, kernel_size=3, padding=1) # 输入256个通道,输出256个通道,卷积核大小3x3,填充1
self.fc1 = nn.Linear(256 * 6 * 6, 4096) # 将卷积层输出展平成一维,输入到全连接层
self.fc2 = nn.Linear(4096, 4096) # 第二个全连接层
self.fc3 = nn.Linear(4096, num_classes) # 最后的分类层
# 定义池化层
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2) # 最大池化层,卷积核大小3x3,步长2
# 定义Dropout层
self.dropout = nn.Dropout(p=0.5) # Dropout层,防止过拟合
def forward(self, x):
# 第一层卷积
x = self.conv1(x) # 输入(224, 224, 3) -> 卷积(64, 55, 55)
x = F.relu(x) # 激活函数
x = self.maxpool(x) # 池化 -> 输出(64, 27, 27)
# 第二层卷积
x = self.conv2(x) # 输入(64, 27, 27) -> 卷积(192, 27, 27)
x = F.relu(x) # 激活函数
x = self.maxpool(x) # 池化 -> 输出(192, 13, 13)
# 第三层卷积
x = self.conv3(x) # 输入(192, 13, 13) -> 卷积(384, 13, 13)
x = F.relu(x) # 激活函数
# 第四层卷积
x = self.conv4(x) # 输入(384, 13, 13) -> 卷积(256, 13, 13)
x = F.relu(x) # 激活函数
# 第五层卷积
x = self.conv5(x) # 输入(256, 13, 13) -> 卷积(256, 13, 13)
x = F.relu(x) # 激活函数
x = self.maxpool(x) # 池化 -> 输出(256, 6, 6)
# 展平特征图
x = x.view(x.size(0), -1) # 将(256, 6, 6)展平成(256*6*6,)
# 第一个全连接层
x = self.fc1(x) # 输入到全连接层(4096)
x = F.relu(x) # 激活函数
x = self.dropout(x) # Dropout
# 第二个全连接层
x = self.fc2(x) # 输入到全连接层(4096)
x = F.relu(x) # 激活函数
x = self.dropout(x) # Dropout
# 输出层
x = self.fc3(x) # 最终输出 (num_classes)
return x
VggNet
用小卷积核代替大的卷积核的原理:
用一个5*5的卷积核得到的卷积效果和两个3*3的卷积核的卷积效果是相同的。
Vgg13模型
代码表示
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
class VGG13(nn.Module):
def __init__(self, num_classes):
super(VGG13, self).__init__()
# 定义网络结构
self.conv1_1 = nn.Conv2d(3, 64, kernel_size=3, padding=1) # 输入3通道,输出64通道,卷积核大小3x3,填充1
self.conv1_2 = nn.Conv2d(64, 64, kernel_size=3, padding=1) # 输入64通道,输出64通道,卷积核大小3x3,填充1
self.maxpool1 = nn.MaxPool2d(kernel_size=2, stride=2) # 最大池化层,卷积核大小2x2,步长2
self.conv2_1 = nn.Conv2d(64, 128, kernel_size=3, padding=1) # 输入64通道,输出128通道,卷积核大小3x3,填充1
self.conv2_2 = nn.Conv2d(128, 128, kernel_size=3, padding=1) # 输入128通道,输出128通道,卷积核大小3x3,填充1
self.maxpool2 = nn.MaxPool2d(kernel_size=2, stride=2) # 最大池化层,卷积核大小2x2,步长2
self.conv3_1 = nn.Conv2d(128, 256, kernel_size=3, padding=1) # 输入128通道,输出256通道,卷积核大小3x3,填充1
self.conv3_2 = nn.Conv2d(256, 256, kernel_size=3, padding=1) # 输入256通道,输出256通道,卷积核大小3x3,填充1
self.conv3_3 = nn.Conv2d(256, 256, kernel_size=3, padding=1) # 输入256通道,输出256通道,卷积核大小3x3,填充1
self.maxpool3 = nn.MaxPool2d(kernel_size=2, stride=2) # 最大池化层,卷积核大小2x2,步长2
self.conv4_1 = nn.Conv2d(256, 512, kernel_size=3, padding=1) # 输入256通道,输出512通道,卷积核大小3x3,填充1
self.conv4_2 = nn.Conv2d(512, 512, kernel_size=3, padding=1) # 输入512通道,输出512通道,卷积核大小3x3,填充1
self.maxpool4 = nn.MaxPool2d(kernel_size=2, stride=2) # 最大池化层,卷积核大小2x2,步长2
self.conv5_1 = nn.Conv2d(512, 512, kernel_size=3, padding=1) # 输入512通道,输出512通道,卷积核大小3x3,填充1
self.conv5_2 = nn.Conv2d(512, 512, kernel_size=3, padding=1) # 输入512通道,输出512通道,卷积核大小3x3,填充1
self.maxpool5 = nn.MaxPool2d(kernel_size=2, stride=2) # 最大池化层,卷积核大小2x2,步长2
# 全连接层
self.fc1 = nn.Linear(512 * 7 * 7, 4096) # 将卷积后的输出展平后输入到全连接层
self.fc2 = nn.Linear(4096, 4096) # 第二个全连接层
self.fc3 = nn.Linear(4096, num_classes) # 最后的分类层
def forward(self, x):
# 第一层卷积
x = self.conv1_1(x) # 输入(224, 224, 3) -> 卷积(64, 224, 224)
x = F.relu(x) # 激活函数
x = self.conv1_2(x) # 输入(64, 224, 224) -> 卷积(64, 224, 224)
x = F.relu(x) # 激活函数
x = self.maxpool1(x) # 池化 -> 输出(64, 112, 112)
# 第二层卷积
x = self.conv2_1(x) # 输入(64, 112, 112) -> 卷积(128, 112, 112)
x = F.relu(x) # 激活函数
x = self.conv2_2(x) # 输入(128, 112, 112) -> 卷积(128, 112, 112)
x = F.relu(x) # 激活函数
x = self.maxpool2(x) # 池化 -> 输出(128, 56, 56)
# 第三层卷积
x = self.conv3_1(x) # 输入(128, 56, 56) -> 卷积(256, 56, 56)
x = F.relu(x) # 激活函数
x = self.conv3_2(x) # 输入(256, 56, 56) -> 卷积(256, 56, 56)
x = F.relu(x) # 激活函数
x = self.conv3_3(x) # 输入(256, 56, 56) -> 卷积(256, 56, 56)
x = F.relu(x) # 激活函数
x = self.maxpool3(x) # 池化 -> 输出(256, 28, 28)
# 第四层卷积
x = self.conv4_1(x) # 输入(256, 28, 28) -> 卷积(512, 28, 28)
x = F.relu(x) # 激活函数
x = self.conv4_2(x) # 输入(512, 28, 28) -> 卷积(512, 28, 28)
x = F.relu(x) # 激活函数
x = self.maxpool4(x) # 池化 -> 输出(512, 14, 14)
# 第五层卷积
x = self.conv5_1(x) # 输入(512, 14, 14) -> 卷积(512, 14, 14)
x = F.relu(x) # 激活函数
x = self.conv5_2(x) # 输入(512, 14, 14) -> 卷积(512, 14, 14)
x = F.relu(x) # 激活函数
x = self.maxpool5(x) # 池化 -> 输出(512, 7, 7)
# 展平特征图
x = x.view(x.size(0), -1) # 将(512, 7, 7)展平成(512 * 7 * 7,)
# 第一个全连接层
x = self.fc1(x) # 输入到全连接层(4096)
x = F.relu(x) # 激活函数
# 第二个全连接层
x = self.fc2(x) # 输入到全连接层(4096)
x = F.relu(x) # 激活函数
# 输出层
x = self.fc3(x) # 最终输出 (num_classes)
return x
ResNet
1*1卷积的特点是不改变特征图大小,但改变特征图的厚度。1*1卷积的作用是降维,减少参数量。比如对于一个256*112*112的特征图,想要直接用3*3的卷积得到256*112*112的特征图需要的参数量是256*256*3*3,可以改为使用64*256*1*1的卷积核先降维然后使用3*3卷积,再通过1*1卷积改变维度,这样做参数量少很多。
由于回传过程中求梯度时,需要将多个导数值相乘,如果每个导数值都小于1,多个相乘趋于0,这就是梯度消失,如多每个导数值都大于1,多个相乘值很大,这就是梯度爆炸。由于Relu导数值为1,因此一定程度上减轻梯度消失,sigmoid在输入很大时趋于0,容易加深梯度消失。
ResNet 的设计通过引入 残差连接来减缓甚至避免梯度消失的问题。通过 跳跃连接(即输入 x 直接传递到输出层)使得 Out = f(x) + x。这个连接让梯度在反向传播时可以直接传递给输入层。这样,梯度不仅依赖于 f(x)(变换部分),还依赖于 x,从而提供了一个直接的梯度路径,避免了在深层网络中梯度消失。
Out = f(x) + x, 输出与输入层数大小维度不同时,可以通过1*1卷积改变维度,通过增大步长改变大小:
ResNet模型
模型代码表示
import torch.nn as nn
import torchvision.models as models
# 加载预训练的ResNet18模型
resNet = models.resnet18()
print(resNet)
class Residual_block(nn.Module): # 定义残差块
def __init__(self, input_channels, out_channels, down_sample=False, strides=1):
super().__init__()
# 第一个卷积层:卷积核大小为3,步长为strides,填充为1
self.conv1 = nn.Conv2d(input_channels, out_channels,
kernel_size=3, padding=1, stride=strides)
# 第二个卷积层:卷积核大小为3,步长为1,填充为1
self.conv2 = nn.Conv2d(out_channels, out_channels,
kernel_size=3, padding=1, stride= 1)
# 如果输入和输出通道不一致,需要额外添加一个1x1的卷积来匹配维度
if input_channels != out_channels:
self.conv3 = nn.Conv2d(input_channels, out_channels,
kernel_size=1, stride=strides)
else:
self.conv3 = None # 如果输入和输出通道一致,则不需要1x1卷积
# 批归一化层,规范化每一层的输出
self.bn1 = nn.BatchNorm2d(out_channels)
self.bn2 = nn.BatchNorm2d(out_channels)
# ReLU激活函数
self.relu = nn.ReLU()
def forward(self, X):
# 第一个卷积操作后激活
out = self.relu(self.bn1(self.conv1(X)))
# 第二个卷积操作后激活
out = self.bn2(self.conv2(out))
# 如果有conv3(即输入和输出通道不同),则执行1x1卷积
if self.conv3:
X = self.conv3(X)
# 跳跃连接:将输入X与输出相加
out += X
# 返回经过ReLU激活后的结果
return self.relu(out)
class MyResNet18(nn.Module):
def __init__(self):
super(MyResNet18, self).__init__()
self.conv1 = nn.Conv2d(3, 64, 7, 2, 3) # 输入(3, 224, 224) -> 卷积(64, 112, 112)
self.bn1 = nn.BatchNorm2d(64)
self.pool1 = nn.MaxPool2d(3, stride=2, padding=1) # 输入(64, 112, 112) -> 最大池化(64, 56, 56)
self.relu = nn.ReLU()
self.layer1 = nn.Sequential(
Residual_block(64, 64), # 输入(64, 56, 56) -> 卷积(64, 56, 56)
Residual_block(64, 64) # 输入(64, 56, 56) -> 卷积(64, 56, 56)
)
self.layer2 = nn.Sequential(
Residual_block(64, 128, strides=2), # 输入(64, 56, 56) -> 卷积(128, 28, 28)
Residual_block(128, 128) # 输入(128, 28, 28) -> 卷积(128, 28, 28)
)
self.layer3 = nn.Sequential(
Residual_block(128, 256, strides=2), # 输入(128, 28, 28) -> 卷积(256, 14, 14)
Residual_block(256, 256) # 输入(256, 14, 14) -> 卷积(256, 14, 14)
)
self.layer4 = nn.Sequential(
Residual_block(256, 512, strides=2), # 输入(256, 14, 14) -> 卷积(512, 7, 7)
Residual_block(512, 512) # 输入(512, 7, 7) -> 卷积(512, 7, 7)
)
self.flatten = nn.Flatten()
self.adv_pool = nn.AdaptiveAvgPool2d(1) # 输入(512, 7, 7) -> 自适应平均池化(512, 1, 1)
self.fc = nn.Linear(512, 1000)
def forward(self, x):
x = self.conv1(x) # 输入(3, 224, 224) -> 卷积(64, 112, 112)
x = self.bn1(x)
x = self.relu(x)
x = self.pool1(x) # 输入(64, 112, 112) -> 最大池化(64, 56, 56)
x = self.layer1(x) # 输入(64, 56, 56) -> 卷积(64, 56, 56)
x = self.layer2(x) # 输入(64, 56, 56) -> 卷积(128, 28, 28)
x = self.layer3(x) # 输入(128, 28, 28) -> 卷积(256, 14, 14)
x = self.layer4(x) # 输入(256, 14, 14) -> 卷积(512, 7, 7)
x = self.adv_pool(x) # 输入(512, 7, 7) -> 自适应平均池化(512, 1, 1)
x = self.flatten(x)
x = self.fc(x)
return x
神经网络透析
卷积和全连接的关系
卷积是一种参数共享的 “不全连接" 卷积时参数为卷积核,卷积核要被多次重复使用因此是参数共享的,卷积操作中的每个输出单元都只依赖于输入数据的一个局部区域(而不是整个输入)因此为”不全连接“,因此卷积求梯度方法本质上和全连接一样。相比于直接图片展开为向量进行全连接,卷积使得参数量减少了许多,避免了过拟合。
深度学习= 玩特征
在图像任务中会从图像的原始像素开始,通过多个卷积层逐步提取更高层次的特征。
直到最后求出预测值之前,都属于特征提取。