TensorFlow2.0深度学习(十)卷积神经网络

1. 全连接网络的问题

        考虑一个简单的4层全连接网络,输入是28×28打平后为784节点的手写数字图片向量,中间三个隐藏层的节点数都是256,输出层的节点数是10,如图所示。

        其参数量如表所示:

        全连接层较高的内存占用量严重限制了神经网络朝着更深层数的发展。

        接下来探索如何避免全连接网络的参数集大的缺陷。

1.1 局部相关性

        对于2D的图片数据,在进入全连接层之前,需要把矩阵数据打平成1D向量,然后每个像素点与每个输出节点两两相连,把连接关系非常形象地对应到图片的像素上,如下图所示。

        可以看出,网络层的每个输出节点都与所有的输入节点相连接,用于提取所有输入节点的特征信息,这种稠密(Dense)的连接方式是全连接层参数量大、计算代价高的根本原因。全连接层也称为稠密连接层(Dense Layer),当全连接层的激活函数\sigma为空时,全连接层也称为线性层。

o_j=\sigma{\left(\sum_{i\in nodes(I)}w_{ij}x_i+b_j\right)}

其中nodes(I)表示I层的节点集合。

        可以考虑输入节点对输出节点的重要性分布,选出最重要的一部分输入节点,而抛弃重要性较低的部分节点,这样输出节点只需要与最重要的一部分输入节点相连接:

o_j=\sigma\left(\sum_{i\in top(I,j,k)}w_{ij}x_i+b_j\right)

其中top(I,j,k)表示I层中对于J层中的j号节点重要性最高的前k个节点集合。通过这种方式,可以把全连接层的||I||*||J||个权值连接减少到k*||J||个。

        那么问题就转变为探索I层输入节点对于j号输出节点的重要性分布。以2D图片数据为例,如果简单地认为与当前像素欧式距离小于和等于\frac{k}{\sqrt{2}}的像素点重要性较高,欧式距离大于\frac{k}{\sqrt{2}}的像素点重要性较低,那么就可以很轻松的化简每个像素点的重要性分布问题。如下图所示,以实心网格所在的像素为参考,它周边欧式距离小于和等于\frac{k}{\sqrt{2}}的像素点以矩形网格表示,网格内的像素点重要性较高,网格外的像素点较低。这个高宽为k的窗口称为感受野,它表征了每个像素对于中心像素的重要性分布情况,网格内的像素才会被考虑,网格外的像素对于中心像素会被简单地忽略。

        基于距离的重要性分布假设称为局部相关性,它只关注和自己距离较近的部分节点,而忽略距离较远的节点,在这种重要性分布假设下,全连接层的连接模型变成了如下图所示。

        此时网络层的输入输出关系表达如下:

o_j=\sigma\left(\sum_{dist(i,j)\leq\frac{k}{\sqrt{2}}}w_{ij}x_i+b_j\right)

其中dist(i,j)表示节点i,j之间的欧式距离。

1.2 权值共享

        每个输出节点仅与感受野区域内k*k个输入节点相连接,输出层节点数为||J||,则当前层的参数量为k*k*||J||,相对于全连接层的||I||*||J||,成功地将参数量减少了不少。

        通过权重共享的思想,对于每个输出节点o_j,均使用相同的权值矩阵W,那么无论输出节点的数量||J||,网络层的参数量总是k*k。如下图所示,在计算左上角位置的输出像素时,使用权值参数

\text{W}=\begin{bmatrix}w_{00}&w_{01}&w_{02}\\w_{10}&w_{11}&w_{12}\\w_{20}&w_{21}&w_{22}\end{bmatrix}

与对应的感受野内部的像素相乘累加,作为左上角像素的输出值;在计算右下方感受野区域时,共享权值参数W,即使用相同的权值参数W相乘累加,得到右下角像素的输出值,此时网络层的参数量只有3*3=9个,且与输入、输出节点数无关。

        通过运用局部相关性和权重共享的思想,成功把网络的参数量从||I||*||J||减少到k*k。这种局部连接、权值共享的网络其实就是卷积神经网络。

1.3 卷积

        在局部相关性的先验加持下,先讲述简化的“局部连接层”,对于窗口k内的所有像素,采用权值相乘累加的方式提取特征信息。对于图片数据,这种权值相乘累加的运算其实是信息处理领域的一种标准运算:离散卷积运算。卷积的“卷”是指翻转平移操作,“积”是指积分运算。

        ID连续卷积定义为:

(f*g)(n)=\int_{-\infty}^{\infty}f(\tau)g(n-\tau)d\tau

        离散卷积将积分变成累加运算:

(f*g)(n)=\sum_{\tau=-\infty}^\infty f(\tau)g(n-\tau)

        在计算机视觉中,卷积运算基于2D图片函数f(m,n)和2D卷积核g(m,n),其中f(m,n)和g(m,n)仅在各自窗口有效区域存在值,其他区域视为0,如下图所示。此时的2D离散卷积定义为:

[f*g](m,n)=\sum_{i=-\infty}^{\infty}\sum_{j=-\infty}^{\infty}f(i,j)g(m-i,n-j)

        这里详细介绍2D离散卷积运算。

        首先,将卷积核g(i,j)函数翻转(沿着x和y方向各翻转一次),变成g(-i,-j)。当(m,n)=(-1,-1)时,g(-1-i,-1-j)表示卷积核向左、向上个平移一个单元,此时

\begin{aligned} [f*g](-1,-1)& =\sum_{i=-\infty}\sum_{j=-\infty}f(i,j)g(-1-i,-1-j) \\ &=\sum_{i\in[-1,1]}\sum_{j\in[-1,1]}f(i,j)g(-1-i,-1-j) \end{aligned}

2D函数只在i∈[-1,1],j∈[-1,1]存在有效值,其他位置为0。按着计算公式,可以得到[f*g](0,-1)=7,如图所示。

同样的方法,(m,n)=(0,-1)时

[f*g](0,-1)=\sum_{i\in[-1,1]}\sum_{j\in[-1,1]}f(i,j)g(0-i,-1-j)

即卷积核向上平移一个单元后对应位置相乘累加,[f*g](0,-1)=7:

当(m,n)=(1,-1)时

[f*g](1,-1)=\sum_{i\in[-1,1]}\sum_{j\in[-1,1]}f(i,j)g(1-i,-1-j)

即卷积核向右、向上各平移一个单元后对应位置相乘累加,[f*g](1,-1)=1:

当(m,n)=(-1,0)时

[f*g](-1,0)=\sum_{i\in[-1,1]}\sum_{j\in[-1,1]}f(i,j)g(-1-i,-j)

即卷积核向左平移一个单元后对应位置相乘累加,[f*g](-1,0)=1:

循环计算,可以计算出函数[f*g](m,n),m∈[-1,1],n∈[-1,1]的所有值。如下图所示:

至此,成功完成一个图片与卷积核的卷积运算,得到一个新的图片。

[f\cdot g](m,n)=\sum_{i\in[-w/2,w/2]}\sum_{j\in[-h/2,h/2]}f(i,j)g(i-m,j-m)

        权重函数g(m,n)称为卷积核,也叫Filter, Weight等。

        小结2D离散卷积运算流程:每次通过移动卷积核窗口与图片对应位置处的像素相乘累加,得到此位置的输出值。卷积核即是窗口为k大小的权值矩阵W,也就是大小为k的窗口即为感受野,感受野与权值矩阵W相乘累加,得到此位置的输出值。通过权值共享,从左上方逐步向左、向下移动卷积核,提取每个位置上的像素特征,直至最右下方,完成卷积运算。

        在计算机视觉领域,2D卷积运算能够提取数据的有用特征,通过特定的卷积核与输入图片进行卷积运算,能够获得不同特征的输出图片。

2. 卷积神经网络

        以2D图片数据为例,卷积核接受高、宽分别为h,w,通道数为c_{in}的输入特征图x,在c_{out}个高、宽都为k,通道数为c_{in}的卷积核作用下,生成高、宽分别为h',w',通道数为c_{out}的特征图输出。

2.1 单通道输入,单卷积核

        首先讨论单通道输入c_{in}=1,如灰度图片只有灰度值一个通道,单个卷积核c_{out}=1的情况。以输入X为5×5的矩阵,卷积核为3×3的矩阵为例,如下图所示:与卷积核同大小的感受野(输入X上方的绿色方框)首先移动至输入X最左上方,选中输入X上3×3的感受野元素,与卷积核对应元素相乘:

\begin{bmatrix}1&-1&0\\-1&-2&2\\1&2&-2\end{bmatrix}*\begin{bmatrix}-1&1&2\\1&-1&3\\0&-1&-2\end{bmatrix}=\begin{bmatrix}-1&-1&0\\-1&2&6\\0&-2&4\end{bmatrix}

相乘后得到3×3的矩阵,这9个数值全部相加:

-1-1+0-1+2+6+0-2+4=7

得到标量7,写入输出矩阵第一行,第一列的位置。

        完成第一个感受野区域的特征提取后,感受野窗口向右移动一个步长单位,按照同样的计算方法,与卷积核对应元素相乘累加,得到输出10,写入第一行、第二列位置。

        感受野窗口再次向右移动一个步长单位,选中图中绿色方框中的元素,并与卷积核相乘累加,得到输出3,并写入输出的第一行、第三列位置。

        此时感受野已经移动至输入X的有效像素的最右边,无法向右边继续移动,因此感受野窗口向下移动一个步长单位,并回到当前行的行首位置,继续选中新的感受野元素区域,与卷积核运算得到输出-1,写入第二行、第一列位置。

        最终输出得到一个3×3的矩阵,比输入5×5略小,这是因为感受野不能超出元素边界的缘故。

        可以观察到,卷积运算的输出矩阵大小由卷积核的大小k,输入X的高宽h/w,移动步长s,是否填充等因素决定。

2.2 多通道输入,单卷积核

        多通道输入的卷积层更为常见,比如彩色图片包含了R/G/B三个通道,每个通道上面的像素值表示R/G/B色彩的强度。下面以3通道输入,单个卷积核为例,将单通道输入的卷积运算方法推广到多通道的情况。

        如下图所示,每行的最左边5×5的矩阵表示输入X的0/1/2通道,第2列的3×3矩阵分别表示卷积核的0/1/2通道,第3列的矩阵表示当前通道运算结果的中间矩阵,最右边一个矩阵表示卷积运算的最终输出。

        在初始状态,每个通道上面的感受野窗口同步落在对应通道上面的最左边、最上方位置,每个通道上感受野区域元素与卷积核对应通道上面的矩阵相乘累加,分别得到三个通道上面的输出7,-11,-1的中间变量,这些中间变量相加得到输出-5,写入对应位置。

        随后,感受野窗口同步在X的每个通道上向右移动s=1个步长单位。

        循环往复同步移动感受野窗口,直至最右边、最下方位置,此时全部完成输入和卷积核的卷积运算,得到3×3的输出矩阵。

        整个的计算示意图如下图所示。输入的每个通道处的感受野均与卷积核的对应通道相乘累加,得到与通道数量相等的中间变量,这些中间变量全部相加即得到当前位置的输出值。输出通道的通道数决定了卷积核的通道数。一个卷积核只能得到一个输出矩阵,无论输入X的通道数量。

2.3 多通道输入,多卷积核

        多通道输入、多卷积核是深度神经网络中间最常见的形式。当出现多卷积核时,第i个卷积核与输入X运算得到第i个输出矩阵(也称为输出张量O的通道i),最后全部的输出矩阵在通道维度上进行拼接(Stack操作,创建输出通道数的新维度),产生输出张量O,O包含了n个通道数。

        以3通道输入,2个卷积核的卷积层为例。第一个卷积核与输入X运算得到输出O的第一个通道,第二个卷积核与输入X运算得到输出O的第二个通道,输出的两个通道拼接在一起形成了最终输出O。每个卷积核的大小k,步长s,填充设定等都是统一设置,这样才能保证输出的每个通道大小一致,从而满足拼接的条件。

2.4 步长

        感受野密度的控制手段一般是通过移动步长(Strides)实现的。

        如下图,感受野沿x.y方向的步长均为2。

        循环往复移动,直至达到最下方,最右边边缘位置,最终卷积层输出的高宽只有2×2。对比前面s=1的情形,输出高宽由3×3降低为2×2,感受野的数量减少为仅4个。

        可以看到,通过设定步长s,可以有效的控制信息密度的提取。当步长设计的较小时,感受野以较小幅度移动窗口,有利于提取到更多的特征信息,输出张量的尺寸也更大;感受野以较大幅度移动窗口,有利于减少计算代价,过滤冗余信息,输出张量的尺寸也更小。

2.5 填充

        经过卷积运算后的输出O的高宽一般会小于输入X的高宽,即使是步长s=1时,输出O的高宽也会略小于输入高宽。在网络模型设计时,有时希望输出O的高宽能够与输入X的高宽相同,从而方便网络参数的设计,残差连接等等。为了让输出O的高宽能够与输入X相等,一般通过在原输入X的高和宽维度上面进行填充若干无效元素操作,得到增大的输入X'。通过精心设计填充单元的数据,在X'上面进行卷积运算得到输出O的高宽可以和原输入X相等,甚至更大。

        卷积神经层的输出尺寸[b,h',w',c_{out}]由卷积核的数量c_{out},卷积核的大小k,步长s,填充数p以及输入X的高宽h/w共同决定,它们之间的数学关系可以表达为:

\mathrm h'=\left[\frac{\mathrm h+2*\mathrm p_h-\mathrm k}{\mathrm s}\right]+1

\mathrm{w'=\left[\frac{w+2*p_w-k}{strides}\right]+1}

其中p_h,p_w分别表示高、宽方向的填充数量,\left \lfloor \cdot \right \rfloor表示向下取整。

        在TensorFlow中,在s=1时,如果希望输出O和输入X高、宽相等,只需要简单地设置参数padding="SAME"即可使TensorFlow自动计算padding数量,非常方便。

3. 卷积层实现

        在TensorFlow中,既可以通过自定义权值的底层实现方式搭建神经网络,也可以直接调用现成的卷积层类的高层方式快速搭建复杂网络。主要以2D卷积为例。

3.1 自定义权值

        在TensorFlow中,通过tf.nn.conv2d函数可以方便地实现2D卷积运算。tf.nn.conv2d基于输入X:[b,h,w,c_{in}]和卷积核W:[k,k,c_{in},c_{out}]进行卷积运算,得到输出O:[b,h',w',c_{out}],其中c_{in}表示输入通道数,c_{out}表示卷积核的数量,也是输出的特征图的通道数。

x = tf.random.normal([2,5,5,3]) # 模拟输入,3通道,高宽为5
# 需要根据[k,k,cin,cout]格式创建w张量,4个3×3大小卷积核
w = tf.random.normal([3,3,3,4])
# 步长为1,padding为0
out = tf.nn.conv2d(x,w,strides=1,padding=[[0,0],[0,0],[0,0],[0,0]])

其中,padding参数的设置格式为:

padding=[[0,0],[上,下],[左,右],[0,0]]

tf.nn.conv2d函数是没有实现偏置向量计算的,添加偏置只需要手动累加偏置张量即可:

# 在卷积输出上叠加偏置向量,它会自动broadcasting为[b,h',w',cout]
out = out + b
3.2 卷积层类

        通过卷积层类layers.Conv2D可以不需要手动定义卷积核W和偏置b张量,直接调用类实例即可完成卷积层的前向计算,实现更加高层和快捷。在TensorFlow中,API的命名有一定的规律,首字母大写的对象一般表示类,全部小写的一般表示函数,如layers.Conv2D表示卷积层类,nn.conv2d表示卷积运算函数。

  • 使用类方式会自动创建需要的权重张量和偏置向量,用户不需要记忆卷积核张量的定义格式,因此使用起来更简单方便,但是灵活性也较低。
  • 函数方式的接口需要自行定义权值和偏置等,更加灵活和底层。

        在新建卷积层类时,只需要指定卷积核数量参数filters,卷积核大小kernel_size,步长strides,填充padding等即可,如下创建4个3×3大小的卷积核的卷积层,步长为1,padding方案为'SAME':

layer = layers.Conv2D(4, kernel_size=3, strides=1, padding='SAME')

如下创建4个3×4大小的卷积核,竖直方向移动步长s_h=2,水平方向移动步长s_w=1:

layer = layers.Conv2D(4,kernel_size=(3,4),strides=(2,1),padding='SMAE')

        创建完成后,通过调用实例(__call__方法)即可完成前向计算:

layer = layers.Conv2D(4, kernel_size=3, strides=1, padding='SAME')
out = layer(x) # 前向计算
out.shape

        在类Conv2D中,保存了卷积核张量W和偏置b,可以通过类成员trainable_variables直接返回W, b的列表。

4. LeNet-5 实战

        下图是LeNet-5的网络结构图,它接受32×32大小的数字、字符图片,经过第一个卷积层得到[b,28,28,6]形状的张量,经过一个向下采样层,张量尺寸缩小到[b,14,14,6],经过第二个卷积层,得到[b,10,10,16]形状的张量,同样经过下采样层,张量尺寸缩小到[b,5,5,16],在进入全连接层之前,先将张量打成[b,400]的张量,送入输出节点数分别为120,84的2个全连接层,得到[b,84]的张量,最后通过Gaussian connections层。

        在LeNet-5 的基础上进行少许调整,使得它更容易在现代深度学习框架上实现。首先将输入X形状由32×32调整为28×28,然后将2个下采样层实现为最大池化层(降低特征图的高、宽),最后利用全连接层替换掉Gaussian connections层。调整后的网络结构如下:

        首先通过Sequential容器创建LeNet-5:

from tensorflow.keras import Sequential

network = Sequential([ # 网络容器
    layers.Conv2D(6, kernel_size=3, strides=1),  # 第一个卷积层,6个3×3卷积核
    layers.MaxPooling2D(pool_size=2, strides=2),  # 高宽各减半的池化层
    layers.ReLU(),  # 激活函数
    layers.Conv2D(16, kernel_size=3, strides=1),  # 第二个卷积层,16个3×3卷积核
    layers.MaxPooling2D(pool_size=2, strides=2),  # 高宽各减半的池化层
    layers.ReLU(),  # 激活函数
    layers.Flatten(),  # 打平层,方便全连接层处理

    layers.Dense(120, activation='relu'), # 全连接层,120个节点
    layers.Dense(84, activation='relu'),  # 全连接层,84个节点
    layers.Dense(10) # 全连接层,10个节点
])

# build一次网络模型,给输入X的形状,其中4位随意给的batches
network.build(input_shape=(4,28,28,1))
# 统计网络信息
network.summary()

        网络结构信息和每层参数量详情如下:

可以看到,卷积层的参数量非常少,主要的参数量集中在全连接层。由于卷积层将输入特征维度降低很多,从而使得全脸层的参数不至于过大,整个模型的参数量约60K,而上表中网络参数量达到了335K,因此通过卷积神经网络可以显著降低网络参数量,同时增加网络深度。

        在训练阶段,首先将数据集中shape为[b,28,28]的输入X增加一个维度,调整shape为[b,28,28,1],送入模型进行前向计算,得到输出张量output, shape为[b,10]。新建交叉熵损失函数类用于处理分类任务,通过设定from_logits=True标志位将softmax激活函数实现在损失函数中,不需要手动添加损失函数,提升数值计算稳定性:

# 导入误差计算,优化器模块
from tensorflow.keras import losses, optimizers
# 创建损失函数的类,在实际计算时直接调用类实例即可
criteon = losses.CategoricalCrossentropy(from_logits=True)

训练部分实现如下:

# 构建梯度记录环境
with tf.GradientTape() as tape:
    # 插入通道维数, => [b,28,28,1]
    x = tf.expand_dim(x, axis=3)
    # 前向计算,获得10类别的概率分布, [b, 784] => [b, 10]
    out = network(x)
    # 真实标签one-hot编码, [b] => [b, 10]
    y_onehot = tf.one_hot(y, depth=10)
    # 计算交叉熵损失函数,标量
    loss = criteon(y_onehot, out)

获得损失值后,通过TensorFlow的梯度记录器tf.GradientTape()来计算损失函数loss对网络参数network.trainable_variables之间的梯度,并通过optimizer对象自动更新网络权重参数。

# 自动计算梯度
grads = tape.gradient(loss, network.trainable_variables)
# 自动更新参数
optimizers.apply_gradients(zip(grads, network.trainable_variables))

重复上述步骤若干次后即可完成训练工作。

        在测试阶段,由于不需要记录梯度信息,代码一般不写在with tf.GradientTape() as tape环境中。前向计算得到的输出经过softmax后,代表了网络预测图片输入x属于类别i的概率P。通过argmax函数选取概率最大的元素所在的索引,作为当前x的预测类别,与真实标注y比较,通过比较结果中间True的数量并求和来统计预测正确的样本的个数,最后除以总样本的个数,得出网络的测试准确度:

# 记录预测正确的数量,总样本数量
correct, total = 0, 0
for x, y in db_test: # 遍历所有训练集样本
    # 插入通道维度, => [b,28,28,1]
    x = tf.expand_dims(x, axis=3)
    # 前向计算
    out = network(x)
    
    pred = tf.argmax(out, axis=-1)
    y = tf.cast(y, tf.int64)
    # 统计预测正确数量
    correct += float(tf.reduce_sum(tf.cast(tf.equal(pred, y), tf.float32)))
    # 统计预测样本总数
    total += x.shape[0]
# 计算准确率
print('test acc:', correct/total)

5. 表示学习

        通过将每层的特征图利用反卷积网络映射回输入图片,即可查看学习到的特征分布,如下图所示,可以观察到,第二层的特征对应到边、角、色彩等底层的图像提取;第三层开始捕获到纹理这些中间特征;第四、五层呈现了物体的部分特征,如小狗的脸部,鸟类的脚部等高层特征。通过这些可视化的手段,可以一定程度上感受卷积神经网络的特征学习过程。

        图片数据的识别过程一般认为也是表示学习的过程,从接受到的原始像素开始,逐渐提取边缘、角点等底层特征,再到纹理等中层特征,再到头部、物体部件等高层特征,最后的网络层基于这些学习到的抽象特征表示做分类逻辑的学习。学习到的特征越高层,越准确,就越有利于分类器的分类,从而获得较好的性能。从表示学习的角度来理解,卷积神经网络通过层层堆叠来逐层提取特征,网络训练的过程可以看成特征的学习过程,基于学习到的高层抽象特征可以方便地进行分类任务。

6. 梯度传播

        卷积层通过移动感受野的方式进行离散卷积操作,那么它的梯度传播是怎么进行的呢?

        考虑简单的情形,输入为3×3的单通道矩阵,与一个2×2的卷积核,进行卷积运算,输出结果打平后直接与虚构的标注计算误差,如下图所示。

        首先推导出输出张量O的表达形式:

\begin{aligned}o_{00}&={x_{00}}^*{w_{00}}+{x_{01}}^*{w_{01}}+{x_{10}}^*{w_{10}}+{x_{11}}^*{w_{11}}+b\\o_{01}&={x_{01}}^*{w_{00}}+{x_{02}}^*{w_{01}}+{x_{11}}^*{w_{10}}+{x_{12}}^*{w_{11}}+b\\o_{10}&={x_{10}}^*{w_{00}}+{x_{11}}^*{w_{01}}+{x_{20}}^*{w_{10}}+{x_{21}}^*{w_{11}}+b\\o_{11}&={x_{11}}^*{w_{00}}+{x_{12}}^*{w_{01}}+{x_{21}}^*{w_{10}}+{x_{22}}^*{w_{11}}+b\end{aligned}

w_{00}的梯度计算为例,通过链式法则分解:

\frac{\partial\mathcal{L}}{\partial w_{00}}=\sum_{i\in\{00,01,10,11\}}\frac{\partial\mathcal{L}}{\partial o_i}\frac{\partial o_i}{\partial w_i}

其中\frac{\partial\mathcal{L}}{\partial o_i}可以直接由误差函数推导出来,直接来考虑\frac{\partial o_i}{\partial w_i}

\frac{\partial o_{00}}{\partial w_{00}}=\frac{\partial\left({x_{00}}^*w_{00}+{x_{01}}^*w_{01}+{x_{10}}^*w_{10}+{x_{11}}^*w_{11}+b\right)}{w_{00}}=x_{00}

同样的方法,可以推导出:

\begin{gathered} \frac{\partial o_{01}}{\partial w_{00}} =\frac{\partial({x_{01}}^{*}w_{00}+{x_{02}}^{*}w_{01}+{x_{11}}^{*}w_{10}+{x_{12}}^{*}w_{11}+b)}{w_{00}} =\chi_{01} \\ \frac{\partial o_{10}}{\partial w_{00}} =\frac{\partial({x_{10}}^{*}w_{00}+{x_{11}}^{*}w_{01}+{x_{20}}^{*}w_{10}+{x_{21}}^{*}w_{11}+b)}{w_{00}} =x_{10} \\ \frac{\partial o_{11}}{\partial w_{00}} =\frac{\partial({x_{11}}^{*}w_{00}+{x_{12}}^{*}w_{01}+{x_{21}}^{*}w_{10}+{x_{22}}^{*}w_{11}+b)}{w_{00}} =x_{11} \end{gathered}

可以观察到,通过循环移动感受野的方式并没有改变网络层可导性,同时梯度的推导也并不复杂,只是当网络层数增大以后,人工梯度推导将变得十分的繁琐。不过不需要担心,深度学习框架可以帮我们自动完成所有参数的梯度计算与更新,我们只需要设计好网络结构即可。

7. 池化层

        池化层同样基于局部相关性的思想,通过从局部相关的一组元素中进行采样或信息聚合,从而得到新的元素值。特别地,最大池化层从局部相关元素集中选取最大的一个元素值,平均池化层从局部相关元素集中计算平均值并返回。

        由于池化层没有需要学习的参数,计算简单,可以有效降低特征图的尺寸,非常适合图片这种类型的数据,在计算机视觉任务中得到了广泛的应用。

        通过精心设计池化层感受野的高宽k和步长s参数,可以实现各种降维运算。比如,一种常用的池化层设定是感受野大小k=2,步长s=2,这样可以实现输出只有输入宽高一半的目的。

8. BatchNorm层

        卷积神经网络的出现,网络参数量大大减低,使得几十层的深层网络成为可能。然而,在残差网络出现之前,网络的加深使得网络训练变得非常不稳定,甚至出现网络长时间不更新或者不收敛的情形,同时网络对超参数比较敏感,超参数的微量扰动也会导致网络的训练轨迹完全改变。

        BN层(Batch Normalization)使得网络的超参数的设定更加自由,比如更大的学习率,更随意的网络初始化等,同时网络的收敛速度更快,性能也更好。BN层提出后便广泛地应用在各种深度网络模型上,卷积层、BN层、ReLU层、池化层一度成为网络模型的标配单元,通过堆叠Conv-BN-ReLU-Pooling方式往往可以获得不错的模型性能。

        考虑Sigmoid激活函数和它的梯度分布,当x>2或x<-2时,Sigmoid函数的导数变得很小,逼近于0,容易出现梯度弥散现象,将函数输入x标准化映射到0附近这一段较小区间将变得非常重要,此时导数值不至于过小,从而不容易出现梯度弥散现象。

        可以经验性归纳出:网络层输入x分布相近,并且分布在较小范围内时(如0附近),更有利于函数的优化。那么如何保证输入x的分布相近呢?数据标准化可以实现此目的,通过数据标准化操作可以将数据x映射到\hat{x}

\widehat{x}=\frac{x-\mu_r}{\sqrt{\sigma_r{}^2+\epsilon}}

其中\mu_r,\sigma_r^2来自统计的所有数据的均值和方差,\epsilon是为防止出现除0错误而设置的较小数字,如1e-8.

        考虑Batch内部的均值\mu_B和方差\sigma_B^2

\begin{aligned}\mu_B&=\frac1m\sum_{i=1}^mx_i\\\\{\sigma_B}^2&=\frac1m\sum_{i=1}^m(x_i-\mu_B)^2\end{aligned}

可以视为近似于\mu_r,\sigma_r^2,其中m为Batch样本数。因此,在训练阶段,通过

\widehat{x}_{train}=\frac{x_{train}-\mu_B}{\sqrt{\sigma_B{}^2+\epsilon}}

标准化输入,并记录每个Batch的统计数据\mu_B\sigma_B^2,用于计算真实的\mu_r,\sigma_r^2

        在测试阶段,根据记录的每个Batch的\mu_B\sigma_B^2,计算出所有训练数据的\mu_r,\sigma_r^2,按着

\widehat{x}_{test}=\frac{x_{test}-\mu_r}{\sqrt{\sigma_{r}^2+\epsilon}}

将每层的输入标准化。

        上述的标准化运算并没有引入额外的待优化变量,\mu_r,\sigma_r^2\mu_B\sigma_B^2,均由统计得到,不需要参与梯度更新。实际上,为了提高BN层的表达能力,将\hat{x}变量再次映射变换:

\tilde{x}=\widehat{x}*\gamma+\beta

8.1 前向传播

        将BN层的输入记为x,输出记为\hat{x}

训练阶段

        首先计算当前Batch的\mu_B\sigma_B^2,根据

\tilde{x}_{train}=\frac{x_{train}-\mu_{B}}{\sqrt{\sigma_{B}^{2}+\epsilon}}*\gamma+\beta

计算BN层的输出。

        同时按着

\mu_r\leftarrow momentum*\mu_r+(1-momentum)*\mu_B

\sigma_r\leftarrow momentum*\sigma_r+(1-momentum)*\sigma_B

迭代更新全局训练数据的统计值\mu_r,\sigma_r^2,其中momentum是需要设置一个超参数,用于平衡\mu_r,\sigma_r^2的更新幅度:当momentum=0时,\mu_r,\sigma_r^2直接被设置为最新的一个Batch的一个\mu_B\sigma_B^2;当momentum=1时,\mu_r,\sigma_r^2保持不变,忽略最新一个Batch的\mu_B\sigma_B^2,在TensorFlow中,momentum默认设置为0.99.

测试阶段

        BN层根据

\tilde{x}_{test}=\frac{x_{test}-\mu_r}{\sqrt{\sigma_r^2+\epsilon}}*\gamma+\beta

计算输出\tilde{x}_{test},其中\mu_r,\sigma_r^2,\gamma,\beta均来自训练阶段统计或优化的结果,在测试阶段直接使用,并不会更新这些参数。

8.2 反向更新

        在训练模式下的反向更新阶段,反向传播算法根据损失\mathcal{L}求解梯度\frac{\partial\mathcal{L}}{\partial\gamma},\frac{\partial\mathcal{L}}{\partial\beta},并按着梯度更新法则自动优化\gamma, \beta参数。

        需要注意的是,对于输入X:[b,h,w,c],BN层并不是计算每个点的\mu_B\sigma_B^2,而是在通道轴c上面统计每个通道上面所有数据的\mu_B\sigma_B^2,因此\mu_B\sigma_B^2是每个通道上所有其他维度的均值和方差。以shape为[100,32,32,3]的输入为例,在通道轴c上面的均值计算如下:

# 构造输入
x = tf.random.normal([100,32,32,3])
# 将其他维度合并,仅保留通道维度
x = tf.reshape(x, [-1,3])
# 计算其他维度的均值
ub = tf.reduce_mean(x, axis=0)
ub

数据有c个通道数,则有c个均值产生。

        除了在c轴上面统计数据\mu_B\sigma_B^2的方式,也很容易将其推广至其他维度计算均值的方式:

  • Layer Norm: 统计每个样本的所有特征的均值和方差
  • Instance Norm: 统计每个样本的每个通道上特征的均值和方差
  • Group Norm: 将c通道分为若干组,统计每个样本的通道组内的特征均值和方差

8.3 BN层实现

        在TensorFlow中,通过layers.BatchNormalization()类可以非常方便地实现BN层:

# 创建BN层
layer = layers.BatchNormalization()

与全连接层、卷积层不同,BN层的训练阶段和测试阶段的行为不同,需要通过设置training标志位来区分训练模式还是测试模式。

        以LeNet-5的网络模型为例,在卷积层后添加BN层:

network = Sequential([ # 网络容器
    layers.Conv2D(6,kernel_size=3,strides=1),
    # 插入BN层
    layers.BatchNormalization(),
    layers.MaxPooling2D(pool_size=2,strides=2),
    layers.ReLU(),
    layers.Conv2D(16,kernel_size=3,strides=1),
    # 插入BN层
    layers.BatchNormalization(),
    layers.MaxPooling2D(pool_size=2,strides=2),
    layers.ReLU(),
    layers.Flatten(),
    layers.Dense(120,activation='relu'),
    # 此处也可以插入BN层
    layers.Dense(84, activation='relu'),
    # 此处也可以插入BN层
    layers.Dense(10)
])

        在训练阶段,需要设置网络的参数training=True以区分BN层是训练还是测试模型

with tf.GradientTape() as tape:
    # 插入通道维度
    x = tf.expand_dims(x, axis=3)
    # 前向计算
    out = network(x, training=True)

        在测试阶段,需要设置training=False,避免BN层采用错误的行为:

for x,y in db_test:
    # 插入通道维度
    x = tf.expand_dims(x, axis=3)
    # 前向计算
    out = network(x, training=False)

9. 经典卷积网络

9.1 AlexNet

9.2 VGG系列

9.3 GoogLeNet

10. 卷积层变种

10.1 空洞卷积

        空洞卷积在普通卷积的感受野上增加一个dilation rate参数,用于控制感受野区域的步长,如下图所示:当感受野的采样步长dilation rate为1时,每个感受野采样点之间的距离为1,此时空洞卷积退化为普通的卷积;但dilation rate为2时,感受野每2个单元采样一个点,每个采样格子之间的距离为2;同样的方法,dilation rate为3,采样步长为3。尽管dilation rate的增大会使得感受野区域增大,但是实际参与运算的点数仍然保持不变。

        空洞卷积在不增加网格参数的条件下,提供了更大的感受野窗口。但是在使用空洞卷积设置网络模型时,需要精心设计dilation rate参数来避免出现网格效应,同时较大的dilation rate参数并不利于小物体的检测、语义分割等任务。

        在TensorFlow中,可以通过设置layers.Conv2D()类的dilation_rate参数来选择使用普通卷积还是空洞卷积:

x = tf.random.normal([1,7,7,1])
layer = layers.Conv2D(1, kernal_size=3, strides=1, dilation_rate=2)
out = layer(x)
out.shape
10.2 转置卷积

        转置卷积(Transposed Convolution或Fractionally Strided Convolution)通过在输入之间填充大量的padding来实现输出高宽大于输入的效果,从而实现向上采样的目的。

10.3 分离卷积

        这里以深度可分离卷积为例。卷积核的每个通道与输入的每个通道进行卷积运算,得到多个通道的中间特征,如下图所示。这个多通道的中间特征量接下来进行多个1×1卷积核的普通卷积运算,得到多个高宽不变的输出,这些输出在通道轴上面进行拼接,从而产生最终的分离卷积层的输出。

        可以看到,分离卷积包含了两步卷积运算,第一步卷积运算是单个卷积核,第二个卷积运算包含了多个卷积核。

        分离卷积一个很明显的优势在于,其参数量是普通卷积的1/3。

11. 深度残差网络

        AlexNet,VGG,GoogLeNet 等网络模型的出现将神经网络的发展带入了几十层的阶段,研究人员发现网络的层数越深,越有可能获得更好的泛化能力。但是当模型加深以后,网络变得越来越难训练,这主要是由于梯度弥散现象造成的。在较深层数的神经网络中间,梯度信息由网络的末层逐层传向网络的首层时,传递的过程中会出现梯度接近于 0的现象。网终层数越深,梯度弥散现象可能会越严重。
        怎么解决深层神经网络的梯度弥散现象呢? 一个很自然的想法是,既然浅层神经网络不容易出现梯度弥散现象,那么可以尝试给深层神经网络添加一种回退到浅层神经网络的机制。当深层神经网络可以轻松地回退到浅层神经网络时,深层神经网络可以获得与浅层神经网络相当的模型性能,而不至于更糟糕。

        通过在输入和输出之间添加一条直接连接的 Skip Connection 可以让神经网络具有回退的能力。以 VGG13 深度神经网络为例,假设观察到 VGG13 模型出现梯度弥散现象,而10 层的网络模型并没有观测到梯度弥散现象,那么可以考虑在最后的两个卷积层添加SkipConnection,如图 所示: 通过这种方式网络模型可以自动选择是否经由这两个卷积层完成特征变换,还是直接跳过这两个卷积层而选择 Skip Connection,亦或结合两个卷积层和 Skip Connection 的输出。\mathcal{F}(x)

11.1 ResNet原理

        ResNet通过在卷积层的输入和输出之间添加Skip Connection实现层数回退机制,如下图所示,输入x通过两个卷积层,得到特征变换后的输出\mathcal{F}(x),与输入x进行对应元素的相加运算,得到最终输出

\mathcal{H}(x)=x+\mathcal{F}(x)

\mathcal{H}(x)叫做残差模块。由于被Skip Connection包围的卷积神经网络需要学习映射\mathcal{F}(x)=\mathcal{H}(x)-x,故称为残差网络。

        为了能够满足输入x与卷积层的输出\mathcal{F}(x)能够相加运算,需要输入x的shape与\mathcal{F}(x)的shape完全一致。当出现shape不一致时,一般通过在Skip Connection上添加额外的卷积运算环节将输入x变换到与\mathcal{F}(x)相同的shape,如上图中的identity(x)函数所示,其中identity(x)以1×1的卷积运算居多,主要用于调整输入的通道数。

11.2 ResBlock实现

        深度残差网络模型并没有增加新的网络类型,只是通过在输入和输出之间添加一条Skip Connection,因此并没有针对 ResNet 的底层实现。在 TensorFlow 中通过调用普通卷积层即可实现残差模块。

        首先创建一个新类,在初始化阶段创建残差块中需要的卷积层,激活函数层等,首先新建\mathcal{F}(x)卷积层:

class BasicBlock(layers.Layer):
    # 残差模块类
    def __init__(self, filter_num, stride=1):
        super(BasicBlock, self).__init__()
        # f(x)包含了2个普通卷积层,创建卷积层1
        self.conv1 = layers.Conv2D(filter_num, (3, 3), strides=stride, padding='same')
        self.bn1 = layers.BatchNormalization()
        self.relu = layers.Activation('relu')   
        # 创建卷积层2
        self.conv2 = layers.Conv2D(filter_num, (3, 3), strides=1, padding='same')
        self.bn2 = layers.BatchNormalization()

        # F(x)与x不同时,需要新建ideneity(x)卷积层
        if stride != 1
            self.downsample = Sequential()
            self.downsample.add(layers.Conv2D(filter_num, (1, 1), strides=stride))
        else:
            self.downsample = lambda x:x

在前向传播时,只需要将\mathcal{F}(x)与identity(x)相加,并添加ReLU激活函数即可。

def call(self, inputs, training=None):
    # 前向传播函数
    out = self.conv1(inputs)
    out = self.bn1(out)
    out = self.relu(out)
    out = self.conv2(out)
    out = self.bn2(out)
    # 输入通过identity()转换
    identity = self.downsample(inputs)
    # f(x) + x运算
    output = layers.add([out, identity])
    # 再通过激活函数并返回
    output = tf.nn.relu(output)
    return output

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值