在使用深度学习方法解决计算机视觉问题的过程中,用得最多的网络架构是一种叫作卷积神经网络(Convolutional Neural Network)的模型。卷积神经网络是人工神经网络的变化和升级,是科学家通过模拟人类大脑的工作原理而发明的。
y j = f ( ∑ i = 1 n w i j x i − θ j ) y_{j}=f\left(\sum_{i=1}^{n} w_{i j} x_{i}-\theta_{j}\right) yj=f(i=1∑nwijxi−θj)
如果处于二维空间中,那么 w ⋅ x + b = 0 w \cdot x+b= 0 w⋅x+b=0 对应的就是对输入数据进行二分类的那条直线,在感知机中我们也把这条直线叫作分割超平面(Separating Hyperplane)。
不过感知机也存在极为明显的优缺点,优点是很容易处理线性可分问题,缺点是不能处理异或问题,也就是说不能处理非线性问题。 所以,之后出现了能够处理非线性问题的多层感知机模型:
多层感知机和单层感知机的最大区别是多层感知机在它的输入层(Input Layer)和输出层(Output Layer)之间加入了新的网络层次→隐藏层(Hidden Layer)(一层或多层)。同时,多层感知机具备了一种后向传播能力,我们可以暂时将后向传播理解为多层感知机模型进行自我学习和优化的一种方法。
不能只对网络层次进行机械性累加,否则在进行后向传播的过程中会出现梯度消失的问题。对于深层次神经网络模型,我们必须有特别的优化和控制手段。
有一种一种有效的解决方案,是通过无监督预训练对权值进行初始化和有监督训练微调模型(利用现有的样本数据,通过科学的方法不断微调模型参数,使模型的预测结果和真实结果之间的误差值不断减小)。
传统的计算机视觉大致分为信息的收集、信息的分析和信息的处理三部分内容。
图像的目标识别 Object Recognition:对识别出的类别对象用长方形进行框选并在框上打上标签名)和语义分割(Semantic Segmentation:对识别出的类别使用同一种像素进行标识并打上标签)可以说是图片分类的升级版本。
监督学习是有训练的,过程是:
(1)回归问题
线性回归的使用场景是我们已经获得一部分有对应关系的原始数据,并且问题的最终答案是得到一个连续的线性映射关系,其过程就是使用原始数据对建立的初始模型不断地进行训练,让模型不断拟合和修正,最后得到我们想要的线性模型,这个线性模型能够对我们之后输入的新数据准确地进行预测。
(2)分类问题
建立的是一个离散的映射关系。
图片描述的是二分类模型。也可以有多分类模型。
无监督学习是没有训练,靠直觉的,过程如下:
在无监督训练的整个过程中,我们需要做的仅仅是将训练数据提供给我们的无监督模型,让它自己挖掘数据中的特征和关系。
可以将搭建的模型是否发生欠拟合或者过拟合作为评价模型的拟合程度好坏的指标。
(a)是已获得的房屋的大小和价格的关系数据;(b)是一个欠拟合模型;(c)所示的是一个较好的拟合模型。
解决问题的方式:
深度神经网络中的损失 = 预测值 - 真实值,是衡量我们训练出来的模型泛化能力(机器学习算法对新鲜样本的适应能力)好坏的重要指标。预测值和真实值的差距越小,则说明我们训练的模型预测越准确,具有更好的泛化能力。
对模型进行优化的最终目的是尽可能地在不过拟合的情况下降低损失值。
(1)均方误差(Mean Square Error,简称 MSE)
M S E = 1 N ∑ i = 1 N ( y true i − y pred i ) 2 M S E=\frac{1}{N} \sum_{i=1}^{N}\left(y_{\text {true }}^{i}-y_{\text {pred }}^{i}\right)^{2} MSE=N1i=1∑N(ytrue i−ypred i)2
就是方差。
(2)均方根误差(Root Mean Square Error,简称 RMSE)
R M S E = 1 N ∑ i = 1 N ( y true i − y pred i ) 2 R M S E=\sqrt{\frac{1}{N} \sum_{i=1}^{N}\left(y_{\text {true }}^{i}-y_{\text {pred }}^{i}\right)^{2}} RMSE=N1i=1∑N(ytrue i−ypred i)2
(3)平均绝对误差(Mean Absolute Error,MAE)
M A E = 1 N ∑ i = 1 N ∣ ( y true i − y pred i ) ∣ M A E=\frac{1}{N} \sum_{i=1}^{N}\left|\left(y_{\text {true }}^{i}-y_{\text {pred }}^{i}\right)\right| MAE=N1i=1∑N∣∣(ytrue i−ypred i)∣∣
在实践操作中最常用到的是一阶优化函数,一阶优化函数在优化过程中求解的是参数的一阶导数,这些一阶导数的值就是模型中参数的微调值。典型的一阶优化函数包括 GD、SGD、Momentum、Adagrad、Adam,等等。
(1)全局梯度下降
梯度下降(Gradient Descent,简称 GD)是参数优化的基础方法。虽然梯度下降已被广泛应用,但是其自身纯在许多不足,所以在其基础上改进的优化函数也非常多。
全局梯度下降的参数更新公式如下:
θ j = θ j − η × ∂ J ( θ j ) ∂ θ j \theta_{j}=\theta_{j}-\eta \times \frac{\partial J\left(\theta_{j}\right)}{\partial \theta_{j}} θj=θj−η×∂θj∂J(θj)
学习速率 η \eta η 用于控制梯度更新的快慢。
因为模型的训练依赖于整个数据集,所以增加了计算损失值的时间成本和模型训练过程中的复杂度。
(2)批量梯度下降(Batch Gradient Descent,简称 BGD)
每次用一个批量的数据来对模型进行训练,并以这个批量计算得到的损失值为基准来对模型中的全部参数进行梯度更新,默认这个批量只使用一次。
θ j = θ j − η × ∂ J b a t c h ( θ j ) ∂ θ j \theta_{j}=\theta_{j}-\eta \times \frac{\partial J_{batch}\left(\theta_{j}\right)}{\partial \theta_{j}} θj=θj−η×∂θj∂Jbatch(θj)
如果我们将批量划分得足够好,则计算损失函数的时间成本和模型训练的复杂度将会大大降低,不过选择批量梯度下降很容易导致优化函数的最终结果是局部最优解。
(3)随机梯度下降(Stochastic Gradient Descent,简称 SGD)
通过随机的方式从整个参与训练的数据集中选取一部分来参与模型的训练。只要随机选取的数据集大小合适,就不用担心计算损失函数的时间成本和模型训练的复杂度,而且与整个参与训练的数据集的大小没有关系。
θ j = θ j − η × ∂ J s t o c h a s t i c ( θ j ) ∂ θ j \theta_{j}=\theta_{j}-\eta \times \frac{\partial J_{stochastic}\left(\theta_{j}\right)}{\partial \theta_{j}} θj=θj−η×∂θj∂Jstochastic(θj)
模型会受到随机训练数据集中噪声数据的影响,又因为有随机的因素,所以也容易导致模型最终得到局部最优解。
(4)自适应时刻估计方法(Adaptive Moment Estimation,简称 Adam)
Adam 在模型训练优化的过程中通过让每个参数获得自适应的学习率,来达到优化质量和速度的双重提升。
例如:假设我们在一开始进行模型参数的训练时损失值比较大,则这时需要使用较大的学习速率让模型参数进行较大的梯度更新,但是到了后期我们的损失值已经趋近于最小了,这时就需要使用较小的学习速率让模型参数进行较小的梯度更新,以防止在优化过程中出现局部最优解。
Adam 收敛速度快、学习效果好的优点脱不了干系,而且对于在优化过程中出现的学习速率消失、收敛过慢、高方差的参数更新等导致损失值波动等问题,Adam 都有很好的解决方案。
没有激活函数的单层神经网络模型:
f ( x ) = W ⋅ X f(x) = W\cdot X f(x)=W⋅X其中的大写字母代表矩阵或者张量。
加入偏置的单层神经网络模型:
f ( x ) = W ⋅ X + b f(x) = W\cdot X + b f(x)=W⋅X+b
加入激活函数的二层神经网络:
f ( x ) = max ( W 2 ⋅ max ( W 1 ⋅ X + b 1 , 0 ) + b 2 , 0 ) f(x)=\max \left(W_{2} \cdot \max \left(W_{1} \cdot X+b_{1}, 0\right)+b_{2}, 0\right) f(x)=max(W2⋅max(W1⋅X+b1,0)+b2,0)激活条件是比较 0 和输入值中的最大值。
三层神经网络模型,并且每层的神经输出都使用同样的激活函数:
f ( x ) = max ( W 3 ⋅ max ( W 2 ⋅ max ( W 1 ⋅ X + b 1 , 0 ) + b 2 , 0 ) + b 3 , 0 ) f(x)=\max \left(W_{3} \cdot \max \left(W_{2} \cdot \max \left(W_{1} \cdot X+b_{1}, 0\right)+b_{2}, 0\right)+b_{3}, 0\right) f(x)=max(W3⋅max(W2⋅max(W1⋅X+b1,0)+b2,0)+b3,0)
如果没有激活函数,而我们只是一味地加深模型层次,则搭建出来的神经网络数学表示如下:
f ( x ) = W 3 ⋅ ( W 2 ⋅ ( W 1 ⋅ X + b 1 ) + b 2 ) + b 3 f(x)=W_{3} \cdot\left(W_{2} \cdot\left(W_{1} \cdot X+b_{1}\right)+b_{2}\right)+b_{3} f(x)=W3⋅(W2⋅(W1⋅X+b1)+b2)+b3
可以看出,上面的模型仍然是一个线性模型,如果不引入激活函数,则无论我们加深多少层,其结果都一样,线性模型在应对非线性问题时会存在很大的局限性。激活函数的引入给我们搭建的模型带来了非线性因素。
f ( x ) = 1 1 + e − x f(x)=\frac{1}{1+e^{-x}} f(x)=1+e−x1
缺点:
f ( x ) = e x − e − x e x + e − x f(x)=\frac{e^{x}-e^{-x}}{e^{x}+e^{-x}} f(x)=ex+e−xex−e−x
tanh 函数的输出结果是零中心数据,所以解决了激活函数在模型优化过程中收敛速度变慢的问题。而 tanh 函数的导数取值区间为 0~1,仍然不够大,所以,在深度神经网络模型的后向传播过程中仍有可能出现梯度消失的情况。
是目前在深度神经网络模型中使用率最高的激活函数:
f ( x ) = m a x ( 0 , x ) f(x) = max(0, x) f(x)=max(0,x)
ReLU 函数的收敛速度非常快,其计算效率远远高于 Sigmoid 和 tanh。
缺点是ReLU 的输出并不是零中心数据,这可能会导致某些神经元永远不会被激活,并且这些神经元相对应的参数不能被更新。这一般是由于模型参数在初始化时使用了全正或者全负的值,或者在后向传播过程中设置的学习速率太快导致的。
其解决方法是对模型参数使用更高级的初始化方法如Xavier,以及设置合理的后向传播学习速率,推荐使用自适应的算法如 Adam。
一个标准的卷积神经网络架构主要由卷积层、池化层和全连接层等核心层次构成。
主要作用是对输入的数据进行特征提取,而完成该功能的是卷积层中的卷积核(Filter)。我们可以将卷积核看作一个指定窗口大小的扫描器,扫描器通过一次又一次地扫描输入的数据,来提取数据中的特征。如果我们输入的是图像数据,那么在通过卷积核的处理后,就可以识别出图像中的重要特征了。
在卷积层中是如何定义这个卷积核?
输入图像:高度×宽度×深度 = 32×32×3,3指图像具有 R、G、B 三个色彩通道。
卷积核:高度×宽度×深度 = 5×5×3,深度要保证与输入图像的色彩通道一致。
如果输入图像是单色彩通道的,那么卷积核的深度就是 1。
−8 = 0×4 + 0×0 + 0×0 + 0×0 + 1×0 + 1×0 + 0×0 + 1×0 + 2×(−4)
下面,根据我们定义的卷积核步长对卷积核窗口进行滑动。卷积核的步长其实就是卷积核窗口每次滑动经过的图像上的像素点数量。
用于提升卷积效果的边界像素填充方式:Valid 方式就是直接对输入图像进行卷积,不对输入图像进行任何前期处理和像素填充,这种方式的缺点是可能会导致图像中的部分像素点不能被滑动窗口捕捉;Same 方式是在输入图像的最外层加上指定层数的值全为 0 的像素边界,这样做是为了让输入图像的全部像素都能被滑动窗口捕捉。
卷积通用公式:用于计算输入图像经过一轮卷积操作后的输出图像的宽度和高度的参数
W output = W input − W filter + 2 P S + 1 H output = H input − H filter + 2 P S + 1 \begin{gathered} W_{\text {output }}=\frac{W_{\text {input }}-W_{\text {filter }}+2 P}{S}+1 \\ H_{\text {output }}=\frac{H_{\text {input }}-H_{\text {filter }}+2 P}{S}+1 \end{gathered} Woutput =SWinput −Wfilter +2P+1Houtput =SHinput −Hfilter +2P+1
一种提取输入数据的核心特征的方式,不仅实现了对原始数据的压缩,还大量减少了参与模型计算的参数,从某种意义上提升了计算效率。其中,最常被用到的池化层方法是平均池化层和最大池化层,池化层处理的输入数据在一般情况下是经过卷积操作之后生成的特征图。
池化层也需要定义一个类似卷积层中卷积核的滑动窗口,但是这个滑动窗口仅用来提取特征图中的重要特征,本身并没有参数。下面是平均池化层和最大池化层。
上面两张图是对单层特征图进行的操作,并且滑动窗口的步长为 2。高度×宽度是 2×2,滑动窗口的深度和特征图的深度保持一致。
池化通用公式:计算输入的特征图经过一轮池化操作后输出的特征图的宽度和高度
W output = W input − W filter S + 1 H output = H input − H filter S + 1 \begin{aligned} &W_{\text {output }}=\frac{W_{\text {input }}-W_{\text {filter }}}{S}+1 \\ &H_{\text {output }}=\frac{H_{\text {input }}-H_{\text {filter }}}{S}+1 \end{aligned} Woutput =SWinput −Wfilter +1Houtput =SHinput −Hfilter +1
池化层不仅能够最大限度地提取输入的特征图的核心特征,还能够对输入的特征图进行压缩。
值得一提的是,当时使用Pooling(池化)操作时,是为了简化计算,但是现在的计算量足够,为了避免丢失信息。因此现在的学术界已经倾向于不用Pooling了。
全连接层的主要作用是将输入图像在经过卷积和池化操作后提取的特征进行压缩,并且根据压缩的特征完成模型的分类功能。
Conv是卷积层、MaxPool是最大池化层、FC是全连接层
相对 AlexNet 而言,在 VGGNet 模型中统一了卷积中使用的参数,比如卷积核滑动窗口的高度和宽度统一为 3×3,卷积核步长统一为 1,Padding 统一为 1,等等;而且增加了卷积神经网络模型架构的深度,分别定义了 16 层的 VGG16 模型和 19 层的 VGG19 模型,与 AlexNet 的 8 层结构相比,深度更深。
这两个重要的改变对于人们重新定义卷积神经网络模型架构也有不小的帮助,至少证明使用更小的卷积核并且增加卷积神经网络的深度,可以更有效地提升模型的性能。
与 VGGNet 模型相比较,GoogleNet 模型的网络深度已经达到了 22 层,而且在网络架构中引入了 Inception 单元。这两个重要的改变证明,通过使用 Inception 单元构造的深层次卷积神经网络模型,能进一步提升模型整体的性能。
(1)Naive Inception 单元的结构:
前一层(Previous Layer)是 Naive Inception 单元的数据输入层,之后被分成了 4 个部分,这 4 个部分分别对应滑动窗口的高度和宽度为 1×1 的卷积层、3×3 的卷积层、5×5 的卷积层和 3×3 的最大池化层,然后将各层计算的结果汇聚至合并层(Filter Concatenation),在完成合并后将结果输出。
【实例:】
输入一个32×32×256的特征图,该特征图先被复制成 4 份并分别被传至接下来的 4 个部分(设这 4 个部分对应的滑动窗口的步长均为 1),其中:
之后通过计算,分别得到这 4 部分输出的特征图为 32×32×128、32×32×192、32×32×96 和 32×32×256,最后在合并层进行合并(相加),得到32×32×672 的特征图。
缺点:
(2)GoogleNet 模型中的 Inception 单元结构
NIN(Network in Network)中 1×1 卷积层的作用:能够完成特征图通道的聚合或发散。
【举例说明:】
现在有一个维度为 50×50×100 的特征图,将之输入 1×1 的卷积层中,卷积核的滑动窗口维度为 1×1×100。
特征图通道的聚合:如果我们想要输出一个深度为 90 的特征图,则卷积 90 次,就变成了维度为 50×50×90的特征图。
特征图通道的发散:如果我们想要输出一个深度为 100 的特征图,则卷积 100 次,就变成了维度为 50×50×100的特征图。
通过 1×1 卷积层来控制特征图最后输出的深度,从而间接影响了与其相关联的层的卷积参数数量。
比如将一个 32×32×10 的特征图输入 3×3 的卷积层中,要求最后输出的特征图深度为 20,那么在这个过程中需要用到的卷积参数为 10×3×3×20 = 1800 个。
如果将 32×32×10 的特征图先输入 1×1 的卷积层中,使其变成 32×32×5 的特征图,再将其输入 3x3 的卷积层中,那么在这个过程中需要用到的卷积参数减少至 1×1×10×5 + 3×3×5×20 = 950 个。
总结:使用 1×1 的卷积层使卷积参数几乎减少了一半,极大提升了模型的性能。
GoogleNet 在 Naive Inception 单元的基础上对单元结构进行了改进:
GoogleNet 中的 Inception 单元与 Naive Inception 单元的结构相比,就是在上图的相应位置增加了 1×1 的卷积层。
假设新增加的 1×1 的卷积的输出深度为 64,步长为 1,Padding 为 0,其他卷积和池化的输出深度、步长都和之前在 Naive Inception 单元中定义的一样,前一层输入的数据仍然使用同之前一样的维度为 32×32×256 的特征图,通过计算,分别得到这 4 部分输出的特征图维度为 32×32×128、32×32×192、32×32×96 和 32×32×64,将其合并后得到维度为 32×32×480 的特征图,将这 4 部分输出的特征图进行相加,最后 Inception 单元输出的特征图维度是 32×32×480。
在输出的结果中,32×32×128、32×32×192、32×32×96 和之前的 Naive Inception 单元是一样的,但其实这三部分因为 1×1 卷积层的加入,总的卷积参数数量已经大大低于之前的 Naive Inception 单元,而且因为在最大池化层之前也加入了 1×1 的卷积层,所以最终输出的特征图的深度也降低了。
(3)GoogleNet 的网络架构
起始部分:
Local Response Normalization 是在模型中使用的局部响应归一化层。这个起始部分的输出结果作为Inception 单元堆叠部分的输入。
分类输出部分:
最后分类输出部分的输入数据来自 Inception 单元堆叠部分最后一个Inception 单元的合并输出,AveragePool 层对应模型中的平均池化层(Average pooling),FC 层对应模型中的全连接层,Softmax 对应模型最后进行分类使用的 Softmax 激活函数。
小结:在 GoogLeNet 模型中使用 Inception 单元,使卷积神经网络模型的搭建实
现了模块化,如果我们想要增加或者减少 GoogLeNet 模型的深度,则只需增添或者减少相应的 Inception 单元就可以了,非常方便。另外,为了避免出现深层次模型中的梯度消失问题,在 GoogLeNet 模型结构中还增加了两个额外的辅助 Softmax 激活函数,用于向前传导梯度。
更详细的讲解可以参考这里。
ResNet 模型中引入了一种残差网络(Residual Network)结构,通过使用残差网络结构,深层次的卷积神经网络模型不仅避免了出现模型性能退化的问题,反而取得了更好的性能。下面是一个具有 34 层网络结构的 ResNet 模型:
虽然 ResNet 模型的深度达到了 34 层,但是其实在 ResNet 模型中大部分结构都是残差网络结构,所以同样具备了模块化的性质。
在 ResNet 模型中大量使用了一些相同的模块来搭建深度更深的网络,却没有出现梯度消失、极易过拟合等模型性能退化的问题,其中一个非常关键的因素就是模型累加的模块并非简单的单输入单输出的结构,而是一种设置了附加关系(恒等映射:Identity Mapping)的新结构(残差网络结构)。
【没有设置附加关系的单输入单输出模块:】
【设置附加的恒等映射关系的残差网络结构:】
上面两个结构唯一不同的是残差模块的最终输出结果 = 输入数据 X 经过两个卷积之后的输出 F(X) + 输入数据的恒等映射。
这个简单的加法并不会给整个 ResNet 模型增加额外的参数和计算量,却能加快模型的训练速度,提升模型的训练效果;另外,在我们搭建的 ResNet 模型的深度加深时,使用残差模块的网络结构不仅不会出现模型退化问题,性能反而有所提升。
这里需要注意附加的恒等映射关系的两种不同的使用情况,残差模块的输入数据若和输出结果的维度一致,则直接相加;若维度不一致,则先进行线性投影,在得到一致的维度后,再进行相加或者对维度不一致的部分使用 0 填充。
【让网络结构更深的残差模块:】
在之前的残差模块的基础上引入了 NIN,使用 1×1 的卷积层来减少模型训练的参数量,同时减少整个模型的计算量,使得拓展更深的模型结构成为可能。于是出现了拥有 50层、101 层、152 层的 ResNet 模型,这不仅没有出现模型性能退化的问题,而且错误率和计算复杂度都保持在很低的程度。
TensorFlow、Caffe、PyTorch都是在cuDNN的基础上的框架
TensorFlow是静态的,首先会用一大段代码定义计算图的样子,然后给新的数据(只能在同一个计算图上运行)
TensorFlow构建好的图是不变的,只是在同一张图上跑不同的数据
TensorFlow支持分布式的GPU,更方便企业
PyTorch是动态的,每个数据可以有自己不同的地方
PyTorch的网络结构可以依赖于数据
主要是下面这三个部分。
张量,多维数组,相当于numpy中的ndarray,不过Tensor可以在GPU上跑。
可以记住每一个Tensor在计算图中的位置,可以计算出当前Variable和之前的Variable在逻辑上的关系。
记住当前变量是依赖于哪一些变量,是怎样依赖的,这样,就可以用一句loss.backward()来求导。
所以不用Variable是不能求导的,必须将感兴趣的变量用 Variable 初始化封装一下,才可以自动求导,不过有一些变量我们对它们的导数不感兴趣,就可以用requires_grad=False取消求导。 一般对输入不感兴趣,对权重感兴趣。
前向传播:给一个值,算出输出;后向传播:给一个值,输出梯度。
Variable之上,一个最顶层的抽象。对应神经网络中不同的层layer。
PyTorch中,nn这个库提供了Module,可以非常简洁地实现神经网络模型和损失函数的构造,直接调用即可。
optim这个包里面包含了很多常见的优化算法,权值更新时就调用optimizer.step()就可以更新模型中的所有参数**