第四节 图像分类任务与卷积神经网络
一、分类任务概述:

图像分类任务和之前的回归任务在逻辑框架大体上相同;但由于输入为图像,因此必须对图像进行处理,提取特征,把任务转化成为我们在回归任务中的类似流程(输出、loss计算等存在差异),这里采用卷积来处理图像。
二、卷积神经网络基础
1.如何提取图像特征?
我们知道图像是由RGB三通道组成的H*W矩阵,所以我们可以把图像展平,把各层矩阵中各元素都放到一个列向量上,每个元素就是一个特征;但是这样特征过于多了,计算过于复杂,同时特征过多会增加过拟合的风险,所以我们要作进一步处理。
2.卷积

卷积是一种针对图像等网格结构数据的特征提取操作,核心是通过卷积核在输入特征图上滑动对区域局部加权求和,从而生成包含局部特征的输出特征图
注:这里的卷积在工程中一般指的是互相关操作,其与数学卷积上的区别在:数学卷积在计算前需要翻转核(核矩阵分别以主对角线和副对角线来进行元素对称交换操作),而互相关操作不需要
卷积核(过滤器)
卷积核是一个可学习的小的权重矩阵,其通道数与输入特征图通道数相同,它在输入数据上滑动以执行卷积运算;卷积核的大小决定卷积的感受野的大小;卷积就其卷积核的特性,同一卷积核在卷积层的所有空间上权重共享,这里不同于全连接
填充Padding
Padding操作是在输入图像周围加几圈额外像素(通常为0),核心是防止边缘特征丢失,同时可以使输入特征图的大小和输出特征图大小相同
步长Stride
Stride决定了卷积核在输入特征图中每次滑动的步幅
多个过滤器和深度

每一个卷积层可以有多个卷积核,卷积核数量决定输出特征图的深度
一个卷积核的形状(K*K*特征图通道数),卷积结果特征图各通道点对点相加,得到一张特征图
输出特征图尺寸的计算
输出尺寸 = (I(输入特征图尺寸) - K(卷积核尺寸) + 2P(填充))/S(步幅) + 1
卷积核参数量的计算
参数量 = 卷积核数量 * K * K * 输入通道数(不含偏置项)
3.池化
一个问题:当特征图保持224×224不变时,参数数量会随着卷积层增加而急剧膨胀(如3×3×64×128的参数量),需要通过降采样减少计算量;
降采样方法:
步长降采样:通过增大Stride来实现降采样,具有计算复杂度低,结构简单的特点,但它会丢失部分特征信息,步长过大时(S>3),尤为显著,故实际工程中一般采用S=2,来实现降采样。
池化:用局部的统计特征代替原始数据,从而实现降维,抗噪声,减少特征数,更不容易过拟合
最大池化:

顾名思义,取区域中最大值代替原始数据,保留最显著特征,计算复杂度低,CNN中最常用的池化方式;
平均值池化:

取区域中平均值代替原始数据,理论上更全面反应区域特征,实际上由于计算复杂且效果有限,使用频率低于最大值池化;
特殊变体:Adaptive Avg Pool(自适应平均池化):强制输出制定尺寸
三、卷积神经网络结构

卷积层设计
交替变化
卷积和池化交替进行,避免一次卷积+多次池化的简单结构
通道扩展
随着特征图尺寸(H*W)减小,通道数C应该相应增加,典型模式为“通道翻倍,尺寸减半”;参数量应控制在一个合理区间
全连接层设计
1.维度转化方法
(1)展平操作
将三维张量(如1024*7*7)拉伸为一维向量(50176)(*存在维度优先级)
(2)降维策略
通过MaxPooling将特征图直接降维至C*1*1
2.全连接设计
通过linear进行一层或多层全连接操作
输出层设计
1.OneHot编码
采用OneHot编码表示输出类别,通过 Softmax 激活将原始得分转化为概率分布,取概率向量中最大值对应的索引,该索引映射的 OneHot 编码即为预测类别
四、Softmax与损失函数
1.Softmax函数

将一个n维向量转化为概率分布,在分类任务中我们使用Softmax函数将原始得分转化为概率分布
2.交叉熵损失


五、经典卷积神经网络
1.AlexNet

创新点
1.Relu激活函数
Relu函数具有计算简单,缓解梯度消失问题(正区间梯度为1)、引入稀疏性,提升模型泛化能力(将所有负值置为0,使部分神经元休眠)
2.Dropout技术
Dropout技术的核心机制:在训练阶段,随机将部分神经元置为 “失活” 状态,不参与训练;
目标:模拟集成学习(多子模型加权平均→降低方差)、抑制神经元共适应(学习通用特征→提升鲁棒性)、隐式 L2 正则化(约束小权重→模型更简单);
效果:有效缓解模型过拟合问题
3.优化池化
提出最大池化代替平均池化,提出重叠池化
4.归一化
首次提出并应用 “局部响应归一化“;归一化(将数值约束到一个区间内,保持学习有效性、缓解梯度消失和梯度爆炸、抑制过拟合)
代码示写
import torch.nn as nn
import torchvision.models as models
import torch
alexNet = models.alexnet()
print(alexNet)
# 手动实现AlexNet模型
class myAlexNet(nn.Module):
def __init__(self):
super(myAlexNet, self).__init__() # 继承myAlexNet父类的__init__,绑定到self上
self.relu = nn.ReLU()
self.drop = nn.Dropout(0.5) # 训练阶段,每个神经元50%概率被丢弃
self.conv1 = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=11, stride=4, padding=2)
self.pool1 = nn.MaxPool2d(3, stride=2)
self.conv2 = nn.Conv2d(64, 192, 5, 1, 2)
self.pool2 = nn.MaxPool2d(3, stride=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.adaPool = nn.AdaptiveAvgPool2d(6) # 将特征图尺寸转化为6*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.relu(x)
x = self.pool1(x)
x = self.conv2(x)
x = self.relu(x)
x = self.pool2(x)
x = self.conv3(x)
x = self.relu(x)
x = self.conv4(x)
x = self.relu(x)
x = self.conv5(x)
x = self.relu(x)
x = self.pool3(x)
x = self.adaPool(x)
x = x.flatten(start_dim=1) # 从维度1开始展平
x = self.fc1(x)
x = self.relu(x)
x = self.drop(x)
x = self.fc2(x)
x = self.relu(x)
x = self.drop(x)
x = self.fc3(x)
x = self.relu(x)
return x
myModel = myAlexNet()
def get_parameters(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_parameters(myModel)) # 输出参数量
img = torch.zeros((4,3,224,224)) # 模拟四张224*224图像
out = myModel(img) # 输出结果
print(out.size()) # 打印尺寸
2.VGGNet
创新点
确立了“小卷积核(采用大小为3*3的卷积核) + 深网络 + 逐步降维”的CNN设计范式;统一的结构设计(卷积块+池化)降低了 CNN 的复现成本;VGGNet对比以往网络更深,证明了深度是提升 CNN 性能的核心因素
VGG13代码简略实现
import torch.nn as nn
import torchvision.models as models
import torch
VGGNet = models.vgg13()
print(VGGNet)
# 神经网络子模块
class vggLayer(nn.Module):
def __init__(self, input_size, mid_size, output_size):
super(vggLayer, self).__init__()
self.relu = nn.ReLU()
self.pool = nn.MaxPool2d(2)
self.conv1 = nn.Conv2d(input_size, mid_size, 3, 1, 1)
self.conv2 = nn.Conv2d(mid_size, output_size, 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
# 模拟VGG13
class myVGG(nn.Module):
def __init__(self):
super(myVGG, self).__init__()
self.relu = nn.ReLU()
self.drop = nn.Dropout(0.5)
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.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 = x.flatten(start_dim=1) # 从一维处展开
x = self.fc1(x)
x = self.relu(x)
x = self.drop(x)
x = self.fc2(x)
x = self.relu(x)
x = self.drop(x)
x = self.fc3(x)
x = self.relu(x)
return x
myModel = myVGG()
def get_parameters(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_parameters(myModel)) # 输出参数量
img = torch.zeros((4, 3, 224, 224)) # 模拟四张224*224图像
out = myModel(img) # 输出结果
print(out.size()) # 打印尺寸
3.ResNet
深度网络退化问题:
理论上CNN性能应随深度提高而提高,但实际上当网络深度超过 20 层(如 VGG19)后,训练集准确率会先饱和,再快速下降;本质:传统深度网络因需学习完整映射,导致深层难以拟合恒等映射,出现性能退化
残差连接
残差连接直接将浅层的输入跳过若干层,与深层的输出相加;简单公式表示:H(x) = F(x) + x,其中x是浅层输入,F(x)是深层网络学习到的“残差映射”,H(x)是最终的输出(若维度F(x)与x维度不匹配,将x通过1*1卷积变化成同维度)
ResNet18代码实现

import torch.nn as nn
import torch
import torchvision.models as models
resNet = models.resnet18()
print(resNet)
# 残差块
class Residual_block(nn.Module):
def __init__(self, input_channels, out_channels, down_sample=False, strides=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)
if input_channels != out_channels: # 若输入通道数!=输出通道数,要将X进行维度变换
self.conv3 = nn.Conv2d(input_channels, out_channels, kernel_size=1, stride=strides)
else:
self.conv3 = None
self.bn1 = nn.BatchNorm2d(out_channels) # 归一化
self.bn2 = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU()
def forward(self, X):
out = self.relu(self.bn1(self.conv1(X))) # 保留X,用于残差连接
out = self.bn2(self.conv2(out))
if self.conv3:
X = self.conv3(X)
out += X
return self.relu(out)
# ResNet18
class MyResNet(nn.Module):
def __init__(self):
super(MyResNet, self).__init__()
self.relu = nn.ReLU()
self.conv1 = nn.Conv2d(3, 64, 7, 2, 3)
self.bn1 = nn.BatchNorm2d(64) # 归一化
self.pool1 = nn.MaxPool2d(3, 2)
self.layer1 = nn.Sequential(
Residual_block(64, 64),
Residual_block(64, 64)
) # Sequential():把多个残差块打包成一个整体模块,有序;自动向前:把第一个输出传给第二个输入...
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.avgPool = 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.avgPool(x)
x = x.flatten(start_dim=1)
x = self.fc(x)
return x
myModel = MyResNet()
def get_parameters(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_parameters(myModel)) # 输出参数量
img = torch.zeros((4, 3, 224, 224)) # 模拟四张224*224图像
out = myModel(img) # 输出结果
print(out.size()) # 打印尺寸
更多推荐



所有评论(0)