Featured image of post 神经网络视觉基础

神经网络视觉基础

本人整理的关于神经网络视觉的内容,包括了各种图层的介绍,还有一些经典神经网络的结构和原理。

目录

神经网络视觉基础

基础知识

神经网络的基本原理

CNN

神经网络的训练,主要目的就是通过学习算法得到各层神经元之间的连接权重和偏置参数等,然后通过参数计算出输入值的输出。

卷积神经网络包括输入层、中间层、输出层。而中间层可以细化为卷积层、激活层、池化层和全连接层

卷积层(局部感知、权值共享)–(图像特征提取)

卷积层中,卷积操作由一个或者多个卷积核(filter)在前层图像上选择相应的区域做卷积运算,然后按一定的步长作滑动运算,依次提取图像区域的像素级特征,图像特征综合后经过激活函数激活,完成一次输入到输出的特征提取过程,卷积后的特征图反映了前层图像的融合特征

卷积操作
卷积运算

经卷积后的矩阵尺寸大小计算公式为(输入图片大小WxW,卷积核FxF,步长S,padding = P) $$ N = (W - F + 2P) / S + 1 $$

激活层 (通过函数把特征保留并映射到输出端)

卷积神经网络在卷积操作后作用非线性激活函数,可以实现对输入信息的非线性变化,从而使网络的输入和输出产生非线性映射关系,激活层对卷积后的逐元素作用激活函数,实现输入和输出信息的同维。引入激活函数的目的是为了增加神经网络的非线性拟合能力

  • “为什么一定要非线性?”

    因为神经网络的每一次输入和输出都是线性求和过程,下一层输出承接了上一层输入函数的线性变换,如果没有非线性激活函数,那么无论神经网络有多少层,最后的输出都是输入的线性组合。这样的线性组合并不能解决复杂的问题。

激活函数
  1. Sigmoid(又称“逻辑函数”)

$$ f(x) = \frac {1} {1 + e^{-x}} $$

$$ y = \begin{cases} 1, 当S(x) \ge 0.5 \\\ 0, 当S(x) < 0.5 \end{cases} $$

$$ S(x)’ = \frac{e^{-x}}{(1+e^{-x})^2} $$

Sigmoid函数图像

优点

  • 值域为[0,1],他对每个神经元的输出进行了归一化,适合用于将预测概率作为输出的模型
  • 梯度平滑,避免了跳跃式 的输出

缺点

  • 接近0或1的神经元梯度趋近于0,容易引起梯度消失,即无法反向传播更新权重。
  • 计算量大,需要更高的算力
  1. ReLU(目前主流的激活函数)

$$ f(x) = Max(0, x) $$

$$ f’(x) = Sgn(x) $$

​ 相比sigmoid函数,其计算速度快收敛速度快(因为输入为负值时,神经元不会被激活,所以网络很稀疏,能更好的提取相关特征,拟合训练数据)。ReLU函数能够最大化发挥神经元的筛选能力。

  • 不足:很容易训练过程中使部分kernel废掉,且无法再次被激活。

  • “ReLU是分段线性函数,它是怎么实现非线性的?”

    ReLU在整个定义域内并不是线性的,组合多个(线性操作+ReLU)就可以任意划分空间,对于层数比较少的神经网络,用ReLU作为激活函数,那非线性肯定没有那么强,但是当层数多达几十甚至上千,虽然,单独的隐藏层是线性的,但是很多的隐藏层表现出来的就是非线性的。(即用很多小的直线可以拟合出曲线效果一样)。

  1. Leaky ReLU
ReLu
$$ f(x) = max(\alpha x, x) $$ ​ 其中$\alpha$为(0,1)的系数,可以有效解决*ReLU*函数神经元死亡的现象。
  1. Softmax (所有输出概率和为1)

$$ o_i = \frac {e^{y_i}} {\sum_je^{y_j}} $$

  • 用于多分类问题的激活函数
  • 在零点不可微,负输入的梯度为零会产生“死亡神经元”

对于二分类问题,理论上使用sigmoid和softmax没有区别,因为数学表达式的形式是一样的。 对于多分类非互斥问题(多标签分类)如人和女人,使用sigmoid更合适。 对于多分类互斥问题(单标签分类),使用softmax更合适。

池化层(对特征图进行稀疏处理)

降低信息冗余,提升模型的尺度不变性、旋转不变性、防止过拟合。

MaxPooling(最大值池化)

最大值池化

全连接层(特征空间–>样本标记空间)

卷积层提取了各种特征,但很多物体可能拥有同一类特征,全连接层相当于组合了这些特征起到了分类器的功能。

全连接层
损失函数

损失函数是用来衡量模型预测值$f(x)$与真实值$Y$的差异程度的运算函数,他是一个非负实数值函数,通常表示为$L(Y|f(x))$。

损失函数使用主要是在模型的训练阶段,每批训练数据送入模型后,通过前向传播输出预测值,然后损失函数会计算出预测值和真实值之间的差异值,也就是损失值。得到损失值之后,模型通过反向传播去更新各个参数,来降低真实值与预测值之间的损失,使模型越来越准确。

“反向传播过程” $$ \omega_{11} = \omega_{11} - \eta \frac{\partial \delta}{\partial \omega_{11}} $$ 通过链式求导法则梯度下降,逐步修改权重参数$\omega$。其中 $\eta$ 为学习率,$\delta$为损失值。

基于距离度量的损失函数

  1. 均方误差损失函数(MSE) $$ L(Y|f(x)) = \frac{1}{n} \sum_{i=1}^N(Y_i - f(x_i))^2 $$

  2. L2损失函数(欧氏距离) $$ L(Y|f(x)) = \sqrt{\frac{1}{n} \sum_{i=1}^N (Y_i - f(x_i))^2} $$

  3. L2损失函数(曼哈顿距离) $$ L(Y|f(x)) = \sum_{i=1}^N |Y_i - f(x_i)| $$

  4. Smooth L1损失函数(主要用在目标检测中防止梯度爆炸) $$ L(Y|f(x)) = \begin{cases} \frac{1}{2}(Y-f(x))^2 \qquad \quad |Y-f(x)| < 1 \\\ |Y-f(x)|- \frac{1}{2} \qquad |Y-f(x)| \ge 1 \end{cases} $$

  5. huber损失函数(平方损失+绝对损失) $$ L(Y|f(x)) = \begin{cases} \frac{1}{2}(Y-f(x))^2 \qquad \qquad |Y-f(x)| \le \delta \\\ \delta |Y-f(x)|- \frac{1}{2} \delta^2 \qquad |Y-f(x)| > \delta \end{cases} $$

基于概率分布度量的损失函数:(涉及概率分布或预测类别出现的概率的问题中应用广泛)

  1. KL散度函数(相对熵) $$ L(Y|f(x)) = \sum_{i=1}^n Y_i \times log(\frac{Y_i}{f(x_i)}) $$

  2. 交叉熵损失 $$ L(Y|f(x)) = - \sum_{i=1}^n Y_i \times logf(x_i) $$ 交叉熵损失函数刻画了实际输出概率与期望输出概率之间的相似度,交叉熵的值越小,两个概率分布就越接近,特别是在正负样本不均衡的分类问题中,常用交叉熵作为损失函数。

    目前,交叉熵损失函数是卷积神经网络中最常使用的分类损失函数,它可以有效避免梯度消散。在二分类情况下也叫做对数损失函数。

  3. softmax损失函数 $$ L(Y|f(x)) = -\frac{1}{n} \sum_{i=1}^n Y_i \times log \frac{e^{f_{Y_i}}}{\sum_{j=1}^c e^{f_j}} $$

  4. Focal loss $$ FE = \begin{cases} -\alpha(1-p)^\gamma log(p) \qquad \quad y = 1 \\\ -(1 - \alpha)p^\gamma log(1-p) \quad y = 0 \end{cases} $$

优化器

优化器就是在深度学习反向传播过程中,指引损失函数(目标函数)的各个参数往正确的方向更新合适的大小,使得更新后的各个参数让损失函数(目标函数)值不断逼近全局最小

  1. SGD优化器随机梯度下降法,易受噪声影响,可能陷入局部最优解)

$$ \omega_{t+1} = \omega_t - \alpha \cdot g(\omega_t) $$

​ $\alpha$为学习率,$g(\omega_t)$ 为$t$ 时刻对参数$\omega_t$ 的损失梯度。

优点

  • 每次只用一个样本更新参数,训练速度快

  • 随机梯度下降所带来的波动有利于优化的方向从当前的局部极小值点跳到另一个更好的局部极小值点,这样对于非凸函数,最终收敛于一个较好的局部极值点,甚至全局极值点。

缺点

  • 当遇到局部最优点或鞍点时,梯度为0,无法继续更新参数
  • 沿陡峭方向震荡,而沿平缓维度进展缓慢,难以迅速收敛
  1. SGD+Momentum优化器(引入动量,抑制样本噪声的干扰)

$$ v_t = \eta \cdot v_{t-1} + \alpha \cdot g(\omega_t) \\\ \omega_{t+1} = \omega_t - v_t $$

​ $\alpha$为学习率,$g(\omega_t)$ 为$t$ 时刻对参数$\omega_t$ 的损失梯度,$\eta(0.9)$ 为动量系数。

  • 加入了动量因素,缓解了SGD在局部最优点无法持续更新的问题和震荡幅度过大的问题,但并没有完全解决,当局部沟壑比较深,动量加持用完了,依然会困在局部最优里来回振荡。
  1. Adagrad优化器(自适应学习率,二阶动量)

$$ s_t = s_{t-1} + g(\omega_t) \cdot g(\omega_t) \\\ \omega_{t+1} = \omega_t - \frac{\alpha} {\sqrt{s_t + \varepsilon}} \cdot g(\omega_t) $$

​ $\alpha$为学习率,$g(\omega_t)$ 为$t$ 时刻对参数$\omega_t$ 的损失梯度,$\varepsilon(10^{-7})$ 为防止分母为零的小数。学习率下降过快容易未收敛就停止训练

  1. RMSProp优化器(自适应学习率)

$$ s_t = \eta \cdot s_{t-1} + (1-\eta) \cdot g(\omega_t) \cdot g(\omega_t) \\\ \omega_{t+1} = \omega_t - \frac{\alpha} {\sqrt{s_t + \varepsilon}} \cdot g(\omega_t) $$

​ $\alpha$为学习率,$g(\omega_t)$ 为$t$ 时刻对参数$\omega_t$ 的损失梯度,$\eta(0.9)$ 控制衰减速度,$\varepsilon(10^{-7})$ 为防止分母为零的小数。

  1. Adam优化器(自适应学习率)

$$ m_t = \beta_1 \cdot m_{t-1} + (1 - \beta_1) \cdot g(\omega_t) \qquad一阶动量 \\\ v_t = \beta_2 \cdot v_{t-1} + (1 - \beta_2) \cdot g(\omega_t) \cdot g(\omega_t) \quad二阶动量 \\\ \hat{m_t} = \frac{m_t}{1 - \beta^t_1} \quad \hat{\frac{v_t} {1 - \beta^t_2}} \\\ \omega_{t + 1} = \omega_t - \frac{\alpha} {\sqrt{\hat{v_t} + \varepsilon}} \hat{m_t} $$

​ $\alpha$为学习率,$g(\omega_t)$ 为$t$ 时刻对参数$\omega_t$ 的损失梯度,$\beta_1(0.9)$、$\beta_2(0.999)$ 控制衰减速度,$\varepsilon(10^{-7})$ 为防止分母为零的小数。

  • 通过一阶动量和二阶动量,有效控制学习率和梯度方向,防止梯度的振荡和在鞍点的静止。
  • 可能错过全局最优解。自适应学习率算法可能会对前期出现的特征过拟合,后期才出现的特征很难纠正前期的拟合效果。后期Adam的学习率太低,影响了有效的收敛。
评估指标–F1分数

二分类:

二分类的混淆矩阵

阳性样本的真实数量:$TP + FN$

阴性样本的真实数量:$FP + TN$

$Precision$ (精确率,又称查准率)(值越高表示误诊数越低): $$ Precision = \frac {TP} {TP + FP} $$ $Recall$ (召回率,又称查全率)(值越高表示漏掉的病人越少): $$ Recall = \frac {TP} {TP + FN} $$ $Accuracy$ (准确率)(正确的样本占样本总数的比例): $$ Accuracy = \frac {TP + TN} {TP + FN + FP + TN} $$ $F1分数$ (综合考虑了精确率和召回率,认为他们同等重要): $$ F1 = 2 \times \frac {Precision \times Recall} {Precision + Recall} $$ 多分类问题:

  • 微观: $$ (查准率) \quad microP = \frac {TP_1 + TP_2} {TP_1 + FP_1 + TP_2 + FP_2} $$

    $$ (查全率) \quad microR = \frac {TP_1 + TP_2} {TP_1 + FN_1 + TP_2 + FN_2} $$

    $$ microF1 = 2 \times \frac {microP \times microR} {microP + microR} $$

  • 宏观: $$ (查准率) \quad macroP = \frac {Precision1 + Precision2} {2} $$

    $$ (查全率) \quad macroR = \frac {Recall1 + Recall2} {2} $$

    $$ macroF1 = 2 \times \frac {macroP \times macroR} {macroP + macroR} $$

  • “评估指标的选择”

    当类别的分布相似时,可以使用准确率,当类别的分布不平衡时,F1分数是更好的评估指标。

图像处理中的卷积神经网络

LeNet模型(1998)

由Yann Le Cun于1998年提出,奠定了卷积神经网络的基础,由两个卷积层、两个全连接层和一个输出层组成。激活函数采用softmax,池化层采用平均池化。该模型早期主要用于手写字符的识别和分类。

LeNet

ALexNet模型(2012)

  • 首次利用GPU进行网络加速训练
  • 使用了ReLu激活函数,使用最大池化方法
  • 使用了LRN局部相应归一化
  • 在全连接层的前两层使用了Dropout随机失活神经元,减少过拟合
AlexNet

过拟合根本原因是特征维度过多,模型假设过于复杂,参数过多而训练数据过少,噪声多,导致拟合的函数完美的预测了训练集而对测试集预测结果差。

卷积计算公式:

$$ N = \frac {(W - F + 2P)} {S} + 1 $$

AlexNet 网络的具体实现

原图输入224x224 实际上进行了随机裁剪,实际大小为227x227

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
from keras.layers import Input, Conv2D, MaxPooling2D, Flatten, Dense, deserialize
from keras.models import Model

def ALexNet(input_shape=None, num_classes=100):
    img_input = Input(shape=input_shape)

    # Block 1
    # (227, 227, 3) --> (27, 27, 96)
    x = Conv2D(96, (11, 11), 
                    activation='relu',
                    padding='valid',
                    strides=(4, 4),
                    name='conv1')(img_input)
    # (55, 55, 96)
    x = MaxPooling2D((3, 3), strides=(2, 2), name='maxpool1')(x)

    # Block 2
    # (27, 27, 96) --> (13, 13, 256)
    x = Conv2D(256, (5, 5),
                    activation='relu',
                    padding='same',
                    strides=(1, 1),
                    name='conv2')(x)
    # (27, 27, 256)
    x = MaxPooling2D((3, 3), strides=(2, 2), name='maxpool2')(x)

    # Block 3
    # (13, 13, 256) --> (13, 13, 384)
    x = Conv2D(384, (3, 3),
                    activation='relu',
                    padding='same',
                    strides=(1, 1),
                    name='conv3')(x)

    # Block 4
    # (13, 13, 384) --> (13, 13, 384)
    x = Conv2D(384, (3, 3),
                    activation='relu',
                    padding='same',
                    strides=(1, 1),
                    name='conv4')(x)

    # Block 5
    # (13, 13, 384) --> (6, 6, 256)

    x = Conv2D(256, (3, 3),
                    activation='relu',
                    padding='same',
                    strides=(1, 1),
                    name='conv5')(x)
    # (13, 13, 256)
    x = MaxPooling2D((3, 3), strides=(2, 2), name='maxpool3')(x)

    # Block 6
    # (6, 6, 256) --> (1, 4096)
    x = Flatten(name='flatten')(x)
    x = Dense(num_classes, activation='softmax', name='predictions')(x)

    inputs = img_input
    model = Model(inputs, x, name='alexnet')
    return model

if __name__ == '__main__':
    model = ALexNet(input_shape=(227, 227, 3))
    model.summary()

GoogleNet模型(2014)

  • 通过引入Inception模块来增加网络宽度
  • 引入1x1的卷积层来压缩通道数量,降低计算量,从而进一步增加网络深度
  • 添加两个softmax辅助分类器,缓解梯度消失现象

Inception就是把多个卷积或池化操作放在一起组装成一个网络模块。

VGGNet模型(2014)

  • 使用多个3x3小尺寸卷积核和池化层构造深度卷积
  • 在最后使用三层全连接层,用最后一层全连接层的输出作为分类的预测
  • 成功证明了增加网络的深度,可以更好的学习图像中的特征模式
VGG

VGG16 网络的具体结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
from keras.models import Model
from keras.layers import Input,Activation,Dropout,Reshape,Conv2D,MaxPooling2D,Dense,Flatten

def VGG16(input_shape=None, classes=1000):
    img_input = Input(shape=input_shape)

    # Block 1
    # (224, 224, 3) --> (112, 112, 64)
    x = Conv2D(64, (3, 3),
                      activation='relu',
                      padding='same',
                      name='block1_conv1')(img_input)
    x = Conv2D(64, (3, 3),
                      activation='relu',
                      padding='same',
                      name='block1_conv2')(x)
    x = MaxPooling2D((2, 2), strides=(2, 2), name='block1_pool')(x)

    # Block 2
    # (112, 112, 64) --> (56, 56, 128)
    x = Conv2D(128, (3, 3),
                      activation='relu',
                      padding='same',
                      name='block2_conv1')(x)
    x = Conv2D(128, (3, 3),
                      activation='relu',
                      padding='same',
                      name='block2_conv2')(x)
    x = MaxPooling2D((2, 2), strides=(2, 2), name='block2_pool')(x)

    # Block 3
    # (56, 56, 128) --> (28, 28, 256)
    x = Conv2D(256, (3, 3),
                      activation='relu',
                      padding='same',
                      name='block3_conv1')(x)
    x = Conv2D(256, (3, 3),
                      activation='relu',
                      padding='same',
                      name='block3_conv2')(x)
    x = Conv2D(256, (3, 3),
                      activation='relu',
                      padding='same',
                      name='block3_conv3')(x)
    x = MaxPooling2D((2, 2), strides=(2, 2), name='block3_pool')(x)

    # Block 4
    # (28, 28, 256) --> (14, 14, 512)
    x = Conv2D(512, (3, 3),
                      activation='relu',
                      padding='same',
                      name='block4_conv1')(x)
    x = Conv2D(512, (3, 3),
                      activation='relu',
                      padding='same',
                      name='block4_conv2')(x)
    x = Conv2D(512, (3, 3),
                      activation='relu',
                      padding='same',
                      name='block4_conv3')(x)
    x = MaxPooling2D((2, 2), strides=(2, 2), name='block4_pool')(x)

    # Block 5
    # (14, 14, 512) --> (7, 7, 512)
    x = Conv2D(512, (3, 3),
                      activation='relu',
                      padding='same',
                      name='block5_conv1')(x)
    x = Conv2D(512, (3, 3),
                      activation='relu',
                      padding='same',
                      name='block5_conv2')(x)
    x = Conv2D(512, (3, 3),
                      activation='relu',
                      padding='same',
                      name='block5_conv3')(x)
    x = MaxPooling2D((2, 2), strides=(2, 2), name='block5_pool')(x)

    # 对结果进行平铺
    x = Flatten(name='flatten')(x)

    # 两次神经元为4096的全连接层
    x = Dense(4096, activation='relu', name='fc1')(x)
    x = Dense(4096, activation='relu', name='fc2')(x)

    # 再全连接到1000维,用以分类任务
    x = Dense(classes, activation='softmax', name='predictions')(x)

    inputs = img_input

    model = Model(inputs, x, name='vgg16')
    return model

if __name__ == '__main__':
    model = VGG16(intput_shape=(224, 224, 3))
    model.summary()

SSD模型(2016)

SSD是基于一个前向传播反馈的CNN网络,属于one-stage类型。

  • 对多尺度特征图进行检测
  • 设置不同长宽比的先验框
SSD

基本的SSD模型是在VGG网络模型的基础上构建的,通过融合不同卷积层的特征图来增强网络对特征的表达能力,采用多尺度卷积检测的方法来进行目标检测其结构如图所示:

该模型基于VGG模型(改进版)来提取特征,将各级的卷积特征图作为该一级的特征表示,不同卷积级别的图像卷积特征描述了不同的语义,卷积层越深表达的图像特征信息级别越高。SSD模型中特征的提取采用的是逐层提取并抽象化的思想,低层的特征主要对应于占比较小的目标,高层的特征主要对应于占比较大的目标的抽象化的信息。 基本的SSD模型通过金字塔特征层进行特征提取,且各特征层之间相互独立,没有目标信息的相互补充,低特征层仅有Conv4_3层用于检测占比小的目标因而在缺乏充足的特征信息的情况下存在特征提取不充分的问题,因而导致对小型目标的识别效果一般

SSD

SSD 网络的具体结构:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import keras.backend as K
import numpy as np
from keras.engine.topology import InputSpec, Layer
from keras.layers import (Activation, Concatenate, Conv2D, Flatten, Input, Reshape)
from keras.models import Model

from vgg import VGG16

class Normalize(Layer):
    def __init__(self, scale, **kwargs):
        self.axis = 3
        self.scale = scale
        super(Normalize, self).__init__(**kwargs)

    def build(self, input_shape):
        self.input_spec = [InputSpec(shape=input_shape)]
        shape = (input_shape[self.axis],)
        init_gamma = self.scale * np.ones(shape)
        self.gamma = K.variable(init_gamma, name='{}_gamma'.format(self.name))
        self.trainable_weights = [self.gamma]

    def call(self, x, mask=None):
        output = K.l2_normalize(x, self.axis)
        output *= self.gamma
        return output

def SSD300(input_shape, num_classes=21):

    # ------ 输入尺寸(300, 300, 3) ------ #
    input_tensor = Input(shape=input_shape)
    
    net = VGG16(input_tensor)
    
    # ------- 对主干网络提取到的有效特征进行处理 ------- #

    # 对conv4_3的通道进行l2标准化处理 
    # (38, 38, 512)
    net['conv4_3_norm'] = Normalize(20, name='conv4_3_norm')(net['conv4_3'])
    num_anchors = 4

    # 对预测框的处理
    # num_anchors表示每个网格点先验框的数量
    # 4 是x,y(框中心偏移),h,w(框的高和宽)的调整
    net['conv4_3_norm_mbox_loc']        = Conv2D(num_anchors * 4, kernel_size=(3,3), padding='same', name='conv4_3_norm_mbox_loc')(net['conv4_3_norm'])
    net['conv4_3_norm_mbox_loc_flat']   = Flatten(name='conv4_3_norm_mbox_loc_flat')(net['conv4_3_norm_mbox_loc'])
    # num_classes是所分的类
    net['conv4_3_norm_mbox_conf']       = Conv2D(num_anchors * num_classes, kernel_size=(3,3), padding='same',name='conv4_3_norm_mbox_conf')(net['conv4_3_norm'])
    net['conv4_3_norm_mbox_conf_flat']  = Flatten(name='conv4_3_norm_mbox_conf_flat')(net['conv4_3_norm_mbox_conf'])

    # 对fc7层进行处理
    # (19, 19, 1024)
    num_anchors = 6
    # 预测框的处理
    # 4 是x,y,h,w的调整
    net['fc7_mbox_loc']         = Conv2D(num_anchors * 4, kernel_size=(3,3),padding='same',name='fc7_mbox_loc')(net['fc7'])
    net['fc7_mbox_loc_flat']    = Flatten(name='fc7_mbox_loc_flat')(net['fc7_mbox_loc'])
    # num_classes是所分的类
    net['fc7_mbox_conf']        = Conv2D(num_anchors * num_classes, kernel_size=(3,3),padding='same',name='fc7_mbox_conf')(net['fc7'])
    net['fc7_mbox_conf_flat']   = Flatten(name='fc7_mbox_conf_flat')(net['fc7_mbox_conf'])

    # 对conv6_2进行处理
    # (10, 10, 512)
    num_anchors = 6
    # 预测框的处理
    # 4 是x,y,h,w的调整
    net['conv6_2_mbox_loc']         = Conv2D(num_anchors * 4, kernel_size=(3,3), padding='same',name='conv6_2_mbox_loc')(net['conv6_2'])
    net['conv6_2_mbox_loc_flat']    = Flatten(name='conv6_2_mbox_loc_flat')(net['conv6_2_mbox_loc'])
    # num_classes是所分的类
    net['conv6_2_mbox_conf']        = Conv2D(num_anchors * num_classes, kernel_size=(3,3), padding='same',name='conv6_2_mbox_conf')(net['conv6_2'])
    net['conv6_2_mbox_conf_flat']   = Flatten(name='conv6_2_mbox_conf_flat')(net['conv6_2_mbox_conf'])

    # 对conv7_2进行处理
    # (5, 5, 256)
    num_anchors = 6
    # 预测框的处理
    # 4 是x,y,h,w的调整
    net['conv7_2_mbox_loc']         = Conv2D(num_anchors * 4, kernel_size=(3,3), padding='same',name='conv7_2_mbox_loc')(net['conv7_2'])
    net['conv7_2_mbox_loc_flat']    = Flatten(name='conv7_2_mbox_loc_flat')(net['conv7_2_mbox_loc'])
    # num_classes是所分的类
    net['conv7_2_mbox_conf']        = Conv2D(num_anchors * num_classes, kernel_size=(3,3), padding='same',name='conv7_2_mbox_conf')(net['conv7_2'])
    net['conv7_2_mbox_conf_flat']   = Flatten(name='conv7_2_mbox_conf_flat')(net['conv7_2_mbox_conf'])

    # 对conv8_2进行处理
    # (3, 3, 256)
    num_anchors = 4
    # 预测框的处理
    # 4是x,y,h,w的调整
    net['conv8_2_mbox_loc']         = Conv2D(num_anchors * 4, kernel_size=(3,3), padding='same',name='conv8_2_mbox_loc')(net['conv8_2'])
    net['conv8_2_mbox_loc_flat']    = Flatten(name='conv8_2_mbox_loc_flat')(net['conv8_2_mbox_loc'])
    # num_classes是所分的类
    net['conv8_2_mbox_conf']        = Conv2D(num_anchors * num_classes, kernel_size=(3,3), padding='same',name='conv8_2_mbox_conf')(net['conv8_2'])
    net['conv8_2_mbox_conf_flat']   = Flatten(name='conv8_2_mbox_conf_flat')(net['conv8_2_mbox_conf'])

    # 对conv9_2进行处理
    # (1, 1, 256)
    num_anchors = 4
    # 预测框的处理
    # 4是x,y,h,w的调整
    net['conv9_2_mbox_loc']         = Conv2D(num_anchors * 4, kernel_size=(3,3), padding='same',name='conv9_2_mbox_loc')(net['conv9_2'])
    net['conv9_2_mbox_loc_flat']    = Flatten(name='conv9_2_mbox_loc_flat')(net['conv9_2_mbox_loc'])
    # num_classes是所分的类
    net['conv9_2_mbox_conf']        = Conv2D(num_anchors * num_classes, kernel_size=(3,3), padding='same',name='conv9_2_mbox_conf')(net['conv9_2'])
    net['conv9_2_mbox_conf_flat']   = Flatten(name='conv9_2_mbox_conf_flat')(net['conv9_2_mbox_conf'])
    
    # ------- 将所有结果进行堆叠 ------- #
    net['mbox_loc'] = Concatenate(axis=1, name='mbox_loc')([net['conv4_3_norm_mbox_loc_flat'],
                                                            net['fc7_mbox_loc_flat'],
                                                            net['conv6_2_mbox_loc_flat'],
                                                            net['conv7_2_mbox_loc_flat'],
                                                            net['conv8_2_mbox_loc_flat'],
                                                            net['conv9_2_mbox_loc_flat']])
                                    
    net['mbox_conf'] = Concatenate(axis=1, name='mbox_conf')([net['conv4_3_norm_mbox_conf_flat'],
                                                            net['fc7_mbox_conf_flat'],
                                                            net['conv6_2_mbox_conf_flat'],
                                                            net['conv7_2_mbox_conf_flat'],
                                                            net['conv8_2_mbox_conf_flat'],
                                                            net['conv9_2_mbox_conf_flat']])
    # (8732, 4)
    net['mbox_loc']     = Reshape((-1, 4), name='mbox_loc_final')(net['mbox_loc'])
    # (8732, 21)
    net['mbox_conf']    = Reshape((-1, num_classes), name='mbox_conf_logits')(net['mbox_conf'])
    net['mbox_conf']    = Activation('softmax', name='mbox_conf_final')(net['mbox_conf'])
    # (8732, 25)
    net['predictions']  = Concatenate(axis =-1, name='predictions')([net['mbox_loc'], net['mbox_conf']])

    model = Model(net['input'], net['predictions'])
    return model

总结而言,SSD是把一张图片划分为不同的网格,当某一物体的中心点落在这个区域,这个物体就由这个网格来确定。

MobileNet(2017)

MobileNetV1

网络结构
  1. Deptwise Convolution(深度可分离卷积)(大大减少运算量和参数数量)

标准的卷积网络结构:

CNN

深度可分离卷积网络结构:

深度可分离卷积

当输入特征图的 shape 是$D_F \times D_F \times M$,其中 $M$ 为通道数,输出特征图的 shape 为$D_G \times D_G \times N$,通道数为 $N$ ,标准卷积核的尺寸为$D_k \times D_k \times M$时,卷积核的参与个数为 $D_k \cdot D_k \cdot M \cdot N$ 。深度可分离卷积一共分为两个步骤的卷积,其中 Depthwise Convolution 的卷积核为$D_k \times D_k \times 1$, Pointwise Convolution 的卷积核为$1 \times 1 \times M$。那么可以得出如下结论:

标准卷积的运算量: $$ D_k \cdot D_k \cdot M \cdot N \cdot D_F \cdot D_F = D_F \cdot D_F \cdot M \cdot (D^2_K \cdot N) $$ 深度可分离卷积的运算量: $$ D_k \cdot D_k \cdot M \cdot D_F \cdot D_F + M \cdot N \cdot D_F \cdot D_F = D_F \cdot D_F \cdot M \cdot (D^2_K + N) $$ 运算量对比: $$ \frac {D_F \cdot D_F \cdot M \cdot (D^2_K + N)} {D_F \cdot D_F \cdot M \cdot (D^2_k \cdot N)} = \frac {1} {N} + \frac {1} {D^2_K} $$

深度可分离卷积的tensorflow代码实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
from keras import layers
from keras import backend

"""
DepthwiseConv2D的原函数定义:
tf.keras.layers.DepthwiseConv2D(
    kernel_size,
    strides=(1, 1),
    padding="valid",
    depth_multiplier=1,
    data_format=None,
    dilation_rate=(1, 1),   # 膨胀率
    activation=None,
    use_bias=True,  # 是否使用偏置向量
    depthwise_initializer="glorot_uniform",
    bias_initializer="zeros",   # 偏置向量的初始值
    depthwise_regularizer=None,
    bias_regularizer=None,
    activity_regularizer=None,
    depthwise_constraint=None,
    bias_constraint=None,
    **kwargs
)
"""

def _depthwise_conv_block(
        inputs,
        pointwise_conv_filters,
        alpha,
        depth_multiplier=1,
        strides=(1, 1),
        block_id=1):

    """通道的处理channel"""
    channel_axis = 1 if backend.image_data_format() == "channels_first" else -1
    """alpha超参数"""
    pointwise_conv_filters = int(pointwise_conv_filters * alpha)

    """步长的处理padding"""
    if strides == (1, 1):
        x = inputs
    else:
        x = layers.ZeroPadding2D(((0, 1), (0, 1)), name="conv_pad_%d" % block_id)(inputs)

    """逐通道卷积"""
    x = layers.DepthwiseConv2D(
            (3, 3),
            padding="same" if strides == (1, 1) else "valid",
            depth_multiplier=depth_multiplier,
            strides=strides,
            use_bias=False,
            name="conv_dw_%d" % block_id
            )(x)
    x = layers.BatchNormalization(axis=channel_axis, name="conv_dw_%d_bn" % block_id)(x)
    x = layers.ReLU(6.0, name="conv_dw_%d_relu" % block_id)(x)

    """逐点卷积"""
    x = layers.Conv2D(
            pointwise_conv_filters,
            (1, 1),
            padding="same",
            use_bias=False,
            strides=(1, 1),
            name="conv_pw_%d" % block_id
            )(x)
    """Batch Normalization是2015年一篇论文中提出的数据归一化方法,往往用在深度神经网络中激活层之前。
    其作用可以加快模型训练时的收敛速度,使得模型训练过程更加稳定,避免梯度爆炸或者梯度消失。并且起到一定的正则化作用,几乎代替了Dropout。"""
    x = layers.BatchNormalization(axis=channel_axis, name="conv_pw_%d_bn" % block_id)(x)

    return layers.ReLU(6.0, name="conv_pw_%d_relu" % block_id)(x)
  • “Conv2D 和 Depthwise_conv2D的区别”

    • Conv2d在每个通道上卷积,然后求和,Depthwise_conv2D卷积,不求和。

    • Depthwise_conv2D的输出维度和输入维度始终是一致的。

    标准卷积 深度可分离卷积
    运算特点 每个卷积核的通道与输入通道相同,每个通道单独做卷积运算然后相加 DW卷积:一个卷积核只有一个通道,单独负责一个通道
    PW卷积:将上一步的特征图在通道方向上进行扩展
  1. 超参数 $\alpha$ $\rho$
  • $\alpha$ 宽度系数,对网络中每一层卷积的通道数乘以 $\alpha$ 取值范围[0,1],比较典型的值为1、0.75、0.5、0.35

$$ 计算量: \qquad D_k \cdot D_k \cdot \alpha M \cdot D_F \cdot D_F + \alpha M \cdot \alpha N \cdot D_F \cdot D_F $$

  • $\rho$ 分辨率系数,只改变网络的计算量而不影响网络的参数量

$$ 计算量: \qquad D_k \cdot D_k \cdot \alpha M \cdot \rho D_F \cdot \rho D_F + \alpha M \cdot \alpha N \cdot \rho D_F \cdot \rho D_F $$

DepthWise部分的卷积核容易废掉,即卷积核参数大部分为零。(很重要的一个原因是因为 ReLU 激活函数对0值的梯度是0,后续无论怎么迭代这个节点都不会恢复,即“废掉了”)

  • “你知道吗?”

    深度可分离卷积将一个标准卷积分割成了两个卷积(逐深度,逐点),因此减小了参数量,对应也减小了总计算量。 深度可分离卷积总计算量变小了,但是深度可分离卷积的层数变多了

    GPU是并行处理大规模数据(矩阵内积)运算的平台,而CPU则更倾向于对数据串行计算。

    因此影响GPU总运算时间的主导因素一般是网络的层数

    而影响CPU总运算时间的主导因素是总计算量

    所以才会出现MobileNet在某些计算能力有限的CPU平台上速度竟然高于某些GPU平台上的速度。

MobileNet的tensorflow代码实现:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
from keras import backend
from keras import layers
from keras.models import Model
from keras.applications import imagenet_utils
from keras.applications.efficientnet import block

def MobileNet(input_shape, alpha=1.0, depth_multiplier=1, dropout=None, classes=1000):

    img_input = layers.Input(shape=input_shape)
    # (224, 224, 3)
    x = _conv_block(img_input, 32, alpha, strides=(2, 2))
    # (112, 112, 32)
    x = _depthwise_conv_block(x, 64, alpha, depth_multiplier, block_id=1)
    # (112, 112, 64)
    x = _depthwise_conv_block(x, 128, alpha, depth_multiplier, strides=(2, 2), block_id=2)
    # (56, 56, 64)
    x = _depthwise_conv_block(x, 128, alpha, depth_multiplier, block_id=3)
    # (56, 56 ,128)
    x = _depthwise_conv_block(x, 256, alpha, depth_multiplier, strides=(2, 2), block_id=4)
    # (28, 28, 256)
    x = _depthwise_conv_block(x, 256, alpha, depth_multiplier, block_id=5)
    # (28, 28, 256)
    x = _depthwise_conv_block(x, 512, alpha, depth_multiplier, strides=(2, 2), block_id=6)
    # (14, 14, 512)
    x = _depthwise_conv_block(x, 512, alpha, depth_multiplier, block_id=7)
    x = _depthwise_conv_block(x, 512, alpha, depth_multiplier, block_id=8)
    x = _depthwise_conv_block(x, 512, alpha, depth_multiplier, block_id=9)
    x = _depthwise_conv_block(x, 512, alpha, depth_multiplier, block_id=10)
    x = _depthwise_conv_block(x, 512, alpha, depth_multiplier, block_id=11)

    # (14, 14, 512)
    x = _depthwise_conv_block(x, 1024, alpha, depth_multiplier, strides=(2, 2), block_id=12)
    # (7, 7, 1024)
    x = _depthwise_conv_block(x, 1024, alpha, depth_multiplier, block_id=13)

    # (7, 7, 1024)
    x = layers.GlobalAveragePooling2D(keepdims=True)(x)
    x = layers.Dropout(dropout, name="drpout")(x)
    x = layers.Conv2D(classes, (1, 1), padding="same", name="conv_preds")(x)
    x = layers.Reshape((classes,), name="reshape_2")(x)
    x = layers.Activation(activation="softmax", name="predictions")(x)

    model = Model(img_input, x, name="mobilenet")
    return model

def _conv_block(inputs, filters, alpha, kernel=(3, 3), strides=(1, 1)):
    filters = int(filters * alpha)
    x = layers.Conv2D(
            filters,
            kernel,
            padding="same",
            use_bias=False,
            strides=strides,
            name="conv1"
        )(inputs)
    x = layers.BatchNormalization(name="conv1_bn")(x)
    return layers.ReLU(6.0, name="conv1_relu")(x)

def _depthwise_conv_block(
        inputs,
        pointwise_conv_filters,
        alpha,
        depth_multiplier=1,
        strides=(1, 1),
        block_id=1):

    """通道的处理channel"""
    channel_axis = 1 if backend.image_data_format() == "channels_first" else -1
    """alpha超参数"""
    pointwise_conv_filters = int(pointwise_conv_filters * alpha)

    """步长的处理padding"""
    if strides == (1, 1):
        x = inputs
    else:
        x = layers.ZeroPadding2D(((0, 1), (0, 1)), name="conv_pad_%d" % block_id)(inputs)

    """逐通道卷积(处理长宽方向的信息)"""
    x = layers.DepthwiseConv2D(
            (3, 3),
            padding="same" if strides == (1, 1) else "valid",
            depth_multiplier=depth_multiplier,
            strides=strides,
            use_bias=False,
            name="conv_dw_%d" % block_id
            )(x)
    x = layers.BatchNormalization(axis=channel_axis, name="conv_dw_%d_bn" % block_id)(x)
    x = layers.ReLU(6.0, name="conv_dw_%d_relu" % block_id)(x)

    """逐点卷积(处理跨通道方向的信息)"""
    x = layers.Conv2D(
            pointwise_conv_filters,
            (1, 1),
            padding="same",
            use_bias=False,
            strides=(1, 1),
            name="conv_pw_%d" % block_id
            )(x)
    x = layers.BatchNormalization(axis=channel_axis, name="conv_pw_%d_bn" % block_id)(x)

    return layers.ReLU(6.0, name="conv_pw_%d_relu" % block_id)(x)

if __name__ == '__main__':
    model = MobileNet(input_shape=(224, 224, 3))
    model.summary()

MobileNet v2

网络结构
  1. Inverted Residuals(倒残差结构)

    倒残差结构是从ResNet中的残差结构而来的。ResNet中Residuals结构中,先用1x1的卷积实现了降维,然后通过3x3卷积,最后通过1x1卷积实现升维,即两头大中间小

    而在MobileNetV2中,先用1x1的卷积升维,然后将3x3卷积换为3x3DW卷积,再用1x1的卷积实现降维,即两头小中间大

    MobileNetV2的倒残差结构示意图:

stateDiagram Direction LR [*] --> Conv1x1,ReLU6 :升维 Conv1x1,ReLU6 --> Dwise3x3,ReLU6 Dwise3x3,ReLU6 --> conv1x1,Linear conv1x1,Linear --> Add :降维,线性激活 [*] --> Add :残差连接 Add --> [*]

ReLU6激活函数: $$ y = ReLU6(x) = min(max(x, 0), 6) $$

  1. Linear Bottlenecks(线性瓶颈层)

    作者发现当信息从高维空间经过非线性映射到低维空间时,会发生信息坍塌,所以在倒残差结构进行降维操作的时候,使用了线性激活函数(实现方式就是不使用激活函数)。

    “思考”

    之所以使用倒残差结构,和线性瓶颈层,是因为作者通过数学证明的方式,得出了在降维过度时,ReLU会造成大量的信息丢失,即升维之后更容易保持可逆

MobileNetV2的tensorflow实现:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
from keras import backend
from keras import layers
from keras.models import Model
from keras.applications import imagenet_utils

def MobileNetV2(input_shape=None, alpha=1.0, classes=1000):

    img_input = layers.Input(shape=input_shape)

    channel_axis = 1 if backend.image_data_format() == "channels_first" else -1

    first_block_filters = _make_divisible(32 * alpha, 8)

    # 输入(224, 224, 3)
    x = layers.Conv2D(
        first_block_filters,
        kernel_size=3,
        strides=(2, 2),
        padding="same",
        use_bias=False,
        name="Conv1",
    )(img_input)
    x = layers.BatchNormalization(
        axis=channel_axis, epsilon=1e-3, momentum=0.999, name="bn_Conv1"
    )(x)
    x = layers.ReLU(6.0, name="Conv1_relu")(x)

    # (112, 112, 32)
    x = _inverted_res_block(x, filters=16, alpha=alpha, stride=1, expansion=1, block_id=0)

    # (112, 112, 16)
    x = _inverted_res_block(x, filters=24, alpha=alpha, stride=2, expansion=6, block_id=1)
    x = _inverted_res_block(x, filters=24, alpha=alpha, stride=1, expansion=6, block_id=2)

    # (56, 56, 24)
    x = _inverted_res_block(x, filters=32, alpha=alpha, stride=2, expansion=6, block_id=3)
    x = _inverted_res_block(x, filters=32, alpha=alpha, stride=1, expansion=6, block_id=4)
    x = _inverted_res_block(x, filters=32, alpha=alpha, stride=1, expansion=6, block_id=5)

    # (28, 28, 32)
    x = _inverted_res_block(x, filters=64, alpha=alpha, stride=2, expansion=6, block_id=6)
    x = _inverted_res_block(x, filters=64, alpha=alpha, stride=1, expansion=6, block_id=7)
    x = _inverted_res_block(x, filters=64, alpha=alpha, stride=1, expansion=6, block_id=8)
    x = _inverted_res_block(x, filters=64, alpha=alpha, stride=1, expansion=6, block_id=9)

    # (14, 14, 64)
    x = _inverted_res_block(x, filters=96, alpha=alpha, stride=1, expansion=6, block_id=10)
    x = _inverted_res_block(x, filters=96, alpha=alpha, stride=1, expansion=6, block_id=11)
    x = _inverted_res_block(x, filters=96, alpha=alpha, stride=1, expansion=6, block_id=12)

    # (14, 14, 96)
    x = _inverted_res_block(x, filters=160, alpha=alpha, stride=2, expansion=6, block_id=13)
    x = _inverted_res_block(x, filters=160, alpha=alpha, stride=1, expansion=6, block_id=14)
    x = _inverted_res_block(x, filters=160, alpha=alpha, stride=1, expansion=6, block_id=15)

    # (7, 7, 160)
    x = _inverted_res_block(x, filters=320, alpha=alpha, stride=1, expansion=6, block_id=16)

    # 宽度因子α
    if alpha > 1.0:
        last_block_filters = _make_divisible(1280 * alpha, 8)
    else:
        last_block_filters = 1280

    # (7, 7, 320)
    x = layers.Conv2D(
        last_block_filters, kernel_size=1, use_bias=False, name="Conv_1"
    )(x)
    x = layers.BatchNormalization(
        axis=channel_axis, epsilon=1e-3, momentum=0.999, name="Conv_1_bn"
    )(x)
    x = layers.ReLU(6.0, name="out_relu")(x)

    # (7, 7, 1280)
    x = layers.GlobalAveragePooling2D()(x)
    imagenet_utils.validate_activation("softmax", "imagenet")
    x = layers.Dense(
            classes, activation="softmax", name="predictions"
    )(x)
    # 预测层(1, 1, classes)

    # 返回模型实例
    return Model(img_input, x, name=f"mobilenetv2")


def _inverted_res_block(inputs, expansion, stride, alpha, filters, block_id):
    """倒残差结构"""
    channel_axis = 1 if backend.image_data_format() == "channels_first" else -1

    in_channels = backend.int_shape(inputs)[channel_axis] # 返回张量或变量的shape,作为int或者None条目的元组
    pointwise_conv_filters = int(filters * alpha)
    # 确保最后一个1x1卷积上的滤波器个数可以被8整除
    pointwise_filters = _make_divisible(pointwise_conv_filters, 8)
    x = inputs
    prefix = f"block_{block_id}_"

    if block_id:
        # 点卷积升维
        x = layers.Conv2D(
            expansion * in_channels,
            kernel_size=1,
            padding="same",
            use_bias=False,
            activation=None,
            name=prefix + "expand",
        )(x)
        x = layers.BatchNormalization(
            axis=channel_axis,
            epsilon=1e-3,
            momentum=0.999,
            name=prefix + "expand_BN",
        )(x)
        x = layers.ReLU(6.0, name=prefix + "expand_relu")(x)
    else:
        prefix = "expanded_conv_"

    # Dw卷积
    if stride == 2:
        x = layers.ZeroPadding2D(
            padding=imagenet_utils.correct_pad(x, 3), name=prefix + "pad"
        )(x)
    x = layers.DepthwiseConv2D(
        kernel_size=3,
        strides=stride,
        activation=None,
        use_bias=False,
        padding="same" if stride == 1 else "valid",
        name=prefix + "depthwise",
    )(x)
    x = layers.BatchNormalization(
        axis=channel_axis,
        epsilon=1e-3,
        momentum=0.999,
        name=prefix + "depthwise_BN",
    )(x)

    x = layers.ReLU(6.0, name=prefix + "depthwise_relu")(x)

    # 点卷积降维,线性激活函数(即None)
    x = layers.Conv2D(
        pointwise_filters,
        kernel_size=1,
        padding="same",
        use_bias=False,
        activation=None,
        name=prefix + "project",
    )(x)
    x = layers.BatchNormalization(
        axis=channel_axis,
        epsilon=1e-3,
        momentum=0.999,
        name=prefix + "project_BN",
    )(x)

    if in_channels == pointwise_filters and stride == 1:
        return layers.Add(name=prefix + "add")([inputs, x])
    return x


def _make_divisible(v, divisor, min_value=None):
    if min_value is None:
        min_value = divisor
    new_v = max(min_value, int(v + divisor / 2) // divisor * divisor)
    # 确保向下舍入不会下降超过10%
    if new_v < 0.9 * v:
        new_v += divisor
    return new_v

if __name__ == '__main__':
    model = MobileNetV2(input_shape=(224, 224, 3))
    model.summary()

待续未完。。。

Licensed under CC BY-NC-SA 4.0
最后更新于 2024-02-21 12:35 CST