目录
一 理论学习
1.MobileNet V1
传统卷积神经网络,内存需求大、运算量大,导致无法在移动设备以及嵌入式设备上运行。
MobileNet网络是由google团队在2017年提出的,专注于移动端或者嵌入式设备中的轻量级CNN网络。相比传统卷积神经网络,在准确率小幅降低的前提下大大减少模型参数与运算量。(v1相比VGG16准确率减少了0.9%,但模型参数只有VGG的1/32)
网络中的亮点:
- Depthwise Convolution(大大减少运算量和参数数量)
- 增加超参数a、β
DW卷积
普通卷积:每个卷积核的深度都与输入图像的深度相同,输出特征的深度是由卷积核的数量决定
DW卷积:每个卷积核的深度都是1,只负责与输入特征矩阵的一个chanel进行卷积运算,得到一个输出矩阵的channel
PW卷积:卷积核大小为1的普通卷积
深度可分卷积操作:DW卷积+PW卷积
计算量(理论上普通卷积计算量是DW+PW的8到9倍)
DF(输入矩阵的高宽)、DK(卷积核大小)、M(输入矩阵的深度)、N(输出矩阵的深度)
- DW+PW=Dk*Dk*M*Df*Df +M*N*Df*Df
- 普通卷积=Dk*Dk*M*N*Df*Df
对于普通卷积而言,假设有一个3×3大小的卷积层,其输入通道为16、输出通道为32。具体为,32个3×3大小的卷积核会遍历16个通道中的每个数据,最后可得到所需的32个输出通道,所需参数为16×32×3×3=4608个。
对于深度可分离卷积结构块而言,假设有一个深度可分离卷积结构块,其输入通道为16、输出通道为32,其会用16个3×3大小的卷积核分别遍历16通道的数据,得到了16个特征图谱。在融合操作之前,接着用32个1×1大小的卷积核遍历这16个特征图谱,所需参数为16×3×3+16×32×1×1=656个。
网络结构
根据不同的需求选择不同的α和β
实际使用:depthwise部分的卷积核容易费掉,即卷积核参数大部分为零——MobileNet v2解决
2.MobileNet v2
MobileNet v2网络是由google团队在2018年提出的,相比MobileNet V1网络,准确率更高,模型更小。
网络中的亮点:
- Inverted Residuals ( 倒残差结构)
- Linear Bottlenecks
倒残差结构:
原残差先用1x1卷积降维,再用3x3卷积,最后用1x1卷积升维。而倒残差先用1*1卷积核进行升维,再用3*3的DW卷积进行卷积,最后通过1*1的卷积进行降维处理,与原残差正好相反。
代码学习
class InvertedResidual(nn.Module):
def __init__(self, in_channel, out_channel, stride, expand_ratio):
super(InvertedResidual, self).__init__()
# 第一层卷积核的个数 expand_ratio:扩展因子t
hidden_channel = in_channel * expand_ratio
# 是否使用捷径分支
self.use_shortcut = stride == 1 and in_channel == out_channel
layers = []
#如果等于1,没有第一个1*1的卷积层
if expand_ratio != 1:
# 1x1 pointwise conv
# group数=in_channel DW卷积
layers.append(ConvBNReLU(in_channel, hidden_channel, kernel_size=1))
layers.extend([
# 3x3 depthwise conv
ConvBNReLU(hidden_channel, hidden_channel, stride=stride, groups=hidden_channel),
# 1x1 pointwise conv(linear) 不额外添加激活函数就是linear激活函数
nn.Conv2d(hidden_channel, out_channel, kernel_size=1, bias=False),
nn.BatchNorm2d(out_channel),
])
self.conv = nn.Sequential(*layers)
def forward(self, x):
# 判断是否使用捷径分支
if self.use_shortcut:
return x + self.conv(x)
else:
return self.conv(x)
ReLU6激活函数:
论文中,对于最后一个卷积,使用了线性的激活函数,作者通过实验,发现ReLU激活函数对低维特征信息造成大量损失,对高维特征信息造成的损失很小,所以需要一个线性的激活函数来避免信息损失
当stride=1且输入特征矩阵与输出特征矩阵shape相同时才有shortcut连接
网络结构:
- t是扩展因子;c是输出特征矩阵深度charnel;n是bottleneck(倒残差结构)的重复次数;s是步距(一个block由一系列bottleneck组成,s针对每个block第一层bottleneck的步距,其他为1)
- 最后一层是全连接层,k是分类的类别个数
性能对比
代码学习:
class MobileNetV2(nn.Module):
#分类类别个数、超参数(控制卷积层使用卷积核个数的倍率)
def __init__(self, num_classes=1000, alpha=1.0, round_nearest=8):
super(MobileNetV2, self).__init__()
block = InvertedResidual
# 把卷积核个数调整为round_nearest的整数倍(四舍五入)
input_channel = _make_divisible(32 * alpha, round_nearest)
last_channel = _make_divisible(1280 * alpha, round_nearest)
inverted_residual_setting = [
# 对应官方表格的 t(扩展因子), c(输出channel), n(bottlrneck重复次数), s(布距)
[1, 16, 1, 1],
[6, 24, 2, 2],
[6, 32, 3, 2],
[6, 64, 4, 2],
[6, 96, 3, 1],
[6, 160, 3, 2],
[6, 320, 1, 1],
]
features = []
# conv1 layer 表格第一行
features.append(ConvBNReLU(3, input_channel, stride=2))
# building inverted residual residual blockes 定义一系列block结构
for t, c, n, s in inverted_residual_setting:
# 调整输出channel个数
output_channel = _make_divisible(c * alpha, round_nearest)
# 搭建每个block中的倒残差结构
for i in range(n):
# 第一层是s,不是第一层就是1
stride = s if i == 0 else 1
features.append(block(input_channel, output_channel, stride, expand_ratio=t))
# 作为下一层输入矩阵的深度
input_channel = output_channel
# building last several layers
features.append(ConvBNReLU(input_channel, last_channel, 1))
# combine feature layers
self.features = nn.Sequential(*features)
# ——————————————特征提取部分完成——————————————
# building classifier 分类器部分
# 自适应的平均池化下采样操作
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.classifier = nn.Sequential(
nn.Dropout(0.2),
nn.Linear(last_channel, num_classes)
)
# 权重初始化
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out')
if m.bias is not None:
nn.init.zeros_(m.bias)
elif isinstance(m, nn.BatchNorm2d): #如果是BN层
nn.init.ones_(m.weight)
nn.init.zeros_(m.bias)
elif isinstance(m, nn.Linear): #如果是全连接层
nn.init.normal_(m.weight, 0, 0.01)
nn.init.zeros_(m.bias)
def forward(self, x):
x = self.features(x)
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.classifier(x)
return x
3.MobileNet V3
网络亮点:
- 更新Block (bneck)
- 使用NAS搜索参数(Neural Architecture Search)
- 重新设计耗时层结构
Block
加入SE模块(注意力机制),对于第一个全连接层,全连接层的结点个数等于特征矩阵channel的1/4,第一个全连接层的结点个数和channel保持一致。经过该模块后,根据模块的重要性分配了权重关系
假设特征图channel=2,首先进行平均池化,得到了每个channel的均值,再依次经过两个全连接层(第一层结点个数2/4,第二层2),输出得到每个channel的权重
重新设计耗时层结构
- 减少第一个卷积层的卷积核个数(32->16)
- 精简Last Stage:减少了7ms
重新设计激活函数
目前比较常用的swish激活函数,计算、求导复杂,对量化过程不友好
在此基础上,作者用h-sigmoid替换sigmoid,提出了h-swish激活函数,对量化过程友好
网络结构
- exp_size:第一个升维卷积需要把维度升到多少
- SE:是否使用注意力机制
- NBN:不需要使用bneck结构
为什么有的激活函数用RE有的用HS?
应用非线性的成本随着我们深入网络而降低,因为每一层激活内存通常在分辨率下降时减半。顺便说一句,我们发现只有在更深的层次上使用swish才能实现大部分的好处。因此,在我们的架构中,我们只在模型的后半部分使用h-swish。
注意第二行:第一个1*1卷积层升维后和输入的维度一样,所以这一层没有1*1卷积层
代码学习:
class MobileNetV3(nn.Module):
def __init__(self,
inverted_residual_setting: List[InvertedResidualConfig],
# 倒数第二个卷积层输出节点个数
last_channel: int,
# 类别个数
num_classes: int = 1000,
block: Optional[Callable[..., nn.Module]] = None,
norm_layer: Optional[Callable[..., nn.Module]] = None):
super(MobileNetV3, self).__init__()
# 对数据进行检查
if not inverted_residual_setting:
raise ValueError("The inverted_residual_setting should not be empty.")
elif not (isinstance(inverted_residual_setting, List) and
all([isinstance(s, InvertedResidualConfig) for s in inverted_residual_setting])):
raise TypeError("The inverted_residual_setting should be List[InvertedResidualConfig]")
if block is None:
block = InvertedResidual
if norm_layer is None:
norm_layer = partial(nn.BatchNorm2d, eps=0.001, momentum=0.01)
layers: List[nn.Module] = []
# building first layer 表格第一行
firstconv_output_c = inverted_residual_setting[0].input_c
layers.append(ConvBNActivation(3,
firstconv_output_c,
kernel_size=3,
stride=2,
norm_layer=norm_layer,
activation_layer=nn.Hardswish))
# building inverted residual blocks
for cnf in inverted_residual_setting:
layers.append(block(cnf, norm_layer))
# building last several layers 最后一个bneck下面那行
lastconv_input_c = inverted_residual_setting[-1].out_c
lastconv_output_c = 6 * lastconv_input_c
layers.append(ConvBNActivation(lastconv_input_c,
lastconv_output_c,
kernel_size=1,
norm_layer=norm_layer,
activation_layer=nn.Hardswish))
self.features = nn.Sequential(*layers)
self.avgpool = nn.AdaptiveAvgPool2d(1)
self.classifier = nn.Sequential(nn.Linear(lastconv_output_c, last_channel),
nn.Hardswish(inplace=True),
nn.Dropout(p=0.2, inplace=True),
nn.Linear(last_channel, num_classes))
#-------------到此网络结构定义完毕---------------
# initial weights 初始化
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode="fan_out")
if m.bias is not None:
nn.init.zeros_(m.bias)
elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
nn.init.ones_(m.weight)
nn.init.zeros_(m.bias)
elif isinstance(m, nn.Linear):
nn.init.normal_(m.weight, 0, 0.01)
nn.init.zeros_(m.bias)
def _forward_impl(self, x: Tensor) -> Tensor:
x = self.features(x)
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.classifier(x)
return x
def forward(self, x: Tensor) -> Tensor:
return self._forward_impl(x)
4.SENet
在计算机视觉中,常用的注意力机制包括通道注意力、空间注意力、通道与空间融合注意力
SENet是通道注意力机制的典型实现。它可以显式地建模特征通道之间的相互依赖关系,通过学习的方式来自动获取到每个特征通道的重要程度,然后依照这个重要程度去提升有用的特征并抑制对当前任务用处不大的特征。
利用SENet,可以让网络关注它最需要关注的通道。
模块结构如下:
- Squeeze:顺着空间维度来进行特征压缩,将每个二维的特征通道变成一个实数,这个实数某种程度上具有全局的感受野,并且输出的维度和输入的特征通道数相匹配。它表征着在特征通道上响应的全局分布,而且使得靠近输入的层也可以获得全局的感受野,这一点在很多任务中都是非常有用的。
- Excitation:类似于循环神经网络中门的机制。通过参数来为每个特征通道生成权重,其中参数被学习用来显式地建模特征通道间的相关性。
- Reweight:将Excitation的输出的权重看做是经过特征选择后的每个特征通道的重要性,然后通过乘法逐通道加权到先前的特征上,完成在通道维度上的对原始特征的重标定。
具体过程:
- 对输入进来的特征层进行全局平均池化。
- 进行两次全连接,第一次全连接神经元个数较少,第二次全连接神经元个数和输入特征层相同。
- 在完成两次全连接后,再取一次Sigmoid将值固定到0-1之间,此时我们获得了输入特征层每一个通道的权值(0-1之间)。
- 在获得这个权值后,将该权值乘上原输入特征层即可。
优势:
SENet很容易被部署,无论网络的深度如何,SE模块都能够给网络带来性能上的增益。SE的增益效果不仅仅局限于某些特殊的网络结构,它具有很强的泛化性。
代码学习:
class se_block(nn.Module):
def __init__(self, channel, ratio=16):
super(se_block, self).__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.fc = nn.Sequential(
nn.Linear(channel, channel // ratio, bias=False),
nn.ReLU(inplace=True),
nn.Linear(channel // ratio, channel, bias=False),
nn.Sigmoid()
)
def forward(self, x):
b, c, _, _ = x.size()
y = self.avg_pool(x).view(b, c)
y = self.fc(y).view(b, c, 1, 1)
return x * y
5.论文阅读
论文结合残差网络和稠密卷积网络,提出了一种用于HSI分类的双路径网络(dual-path network, DPN)。
高光谱图像
常见的RGB彩色图像只有三个通道,而高光谱图像有几十甚至几百个通道,每个像素点都有很多的数来描述,单个通道上的“灰度值”反映了被拍摄对象对于某一波段的光的反射情况。
二维卷积和三维卷积
二维卷积主要用于提取空间特征,在卷积的过程中,图片与卷积核进行卷积,输出是一张二维的特征图,因此二维卷积只能提取二维的平面特征。
三维卷积的卷积核可以看作一个数据立方体,因此三维卷积处理的对象是一个立方体图像,三维卷积的思想与二维卷积相同,只不过多了一个维度,所以三维卷积不仅提取处理空间特征,也可以提取另一维度的特征。
从文献中可以看出,仅使用2D-CNN或3D-CNN分别存在通道关系信息缺失或模型非常复杂等缺点。由于高光谱图像是体积数据,同时也具有光谱维数,2D-CNN无法从光谱维中提取出具有良好鉴别能力的特征图;3D-CNN的计算复杂,对于在光谱带上具有相似纹理的图像分类不佳。
3D-CNN和2D-CNN层被组装起来,可以充分利用光谱和空间特征图,达到最大可能的精度。
网络结构:
1.三维卷积部分:
- conv1:(1, 30, 25, 25), 8个 7x3x3 的卷积核 ==>(8, 24, 23, 23)
- conv2:(8, 24, 23, 23), 16个 5x3x3 的卷积核 ==>(16, 20, 21, 21)
- conv3:(16, 20, 21, 21),32个 3x3x3 的卷积核 ==>(32, 18, 19, 19)
2. reshape前面的 32*18,得到 (576, 19, 19)
3.二维卷积:(576, 19, 19) 64个 3x3 的卷积核,得到 (64, 17, 17)
4. flatten 操作,变为 18496 维的向量,
5.依次为256,128节点的全连接层,都使用比例为0.4的 Dropout,
6.最后输出为 16 个节点,是最终的分类类别数。
二 代码学习
1.定义 HybridSN 类
网络结构:
class HybridSN(nn.Module):
def __init__(self, in_channels=1, out_channels=class_num):
super(HybridSN, self).__init__()
#三维卷积层
self.conv3d = nn.Sequential(
nn.Conv3d(1,8,kernel_size=(7,3,3)),
nn.ReLU(),
nn.Conv3d(8,16,kernel_size=(5,3,3)),
nn.ReLU(),
nn.Conv3d(16,32,kernel_size=(3,3,3)),
nn.ReLU()
)
#二维卷积层
self.conv2d = nn.Sequential(
nn.Conv2d(576, 64, kernel_size=(3,3)),
nn.ReLU()
)
#全连接层
self.fc = nn.Sequential(
nn.Linear(64 * 17 * 17, 256),
nn.ReLU(),
nn.Dropout(0.4),
nn.Linear(256, 128),
nn.ReLU(),
nn.Dropout(0.4),
nn.Linear(128, 16)
)
# self.soft = nn.LogSoftmax(dim=1)
# 加入注意力机制
# self.se= se_block(32 * 18)
def forward(self, t):
t = self.conv3d(t)
#进行卷积降维3d->2d
t = t.view(t.shape[0], t.shape[1]*t.shape[2], t.shape[3], t.shape[4])
# 加senet
# t = self.se(t)
t = self.conv2d(t)
#进行卷积降维进行flatten
t = t.view(t.shape[0],-1)
t = self.fc(t)
# t = self.soft(t)
return t
2.创建数据集
3.模型训练
多次测试,发现每次准确率结果都不一样
测试次数 | 准确率 | |
1 | 97.14 | |
2 | 90.58% | |
3 | 94.76 |
这是因为在训练模式中,网络采用了Dropout使得网络在训练的过程中随机删除神经元,抗噪声能力更强,防止过拟合。但是在测试模型的时候,随机的drop,就会导致最终结果的不一致。为了解决这个问题,在训练模型的时候要加上net.train()开启drop;在测试模型的时候加上net.eval()关掉drop,以此来保持结果一致。
添加net.eval()后,测试结果稳定在97.08%,训练准确率和生成classification map如下图:
4.模型改进——融入注意力机制
加网络中加入SENet注意力机制,网络准确率有所提升
class HybridSN(nn.Module):
def __init__(self, in_channels=1, out_channels=class_num):
super(HybridSN, self).__init__()
#三维卷积层
self.conv3d = nn.Sequential(
nn.Conv3d(1,8,kernel_size=(7,3,3)),
nn.ReLU(),
nn.Conv3d(8,16,kernel_size=(5,3,3)),
nn.ReLU(),
nn.Conv3d(16,32,kernel_size=(3,3,3)),
nn.ReLU()
)
self.conv2d = nn.Sequential(
nn.Conv2d(576, 64, kernel_size=(3,3)),
nn.ReLU()
)
self.fc = nn.Sequential(
nn.Linear(64 * 17 * 17, 256),
nn.ReLU(),
nn.Dropout(0.4),
nn.Linear(256, 128),
nn.ReLU(),
nn.Dropout(0.4),
nn.Linear(128, 16)
)
# self.soft = nn.LogSoftmax(dim=1)
self.se= se_block(32 * 18)
def forward(self, t):
t = self.conv3d(t)
t = t.view(t.shape[0], t.shape[1]*t.shape[2], t.shape[3], t.shape[4])
#加senet
t = self.se(t)
t = self.conv2d(t)
t = t.view(t.shape[0],-1)
t = self.fc(t)
# t = self.soft(t)
return t
测试结果如下: