0%

第七章 生成对抗网络

7.1 GAN基本概念

7.1.1 如何通俗理解GAN

生成对抗网络(GAN, Generative adversarial network)自从2014年被Ian Goodfellow提出以来,掀起来了一股研究热潮。GAN由生成器和判别器组成,生成器负责生成样本,判别器负责判断生成器生成的样本是否为真。生成器要尽可能迷惑判别器,而判别器要尽可能区分生成器生成的样本和真实样本。在GAN的原作中,作者将生成器比喻为印假钞票的犯罪分子,判别器则类比为警察。犯罪分子努力让钞票看起来逼真,警察则不断提升对于假钞的辨识能力。二者互相博弈,随着时间的进行,都会越来越强。那么类比于图像生成任务,生成器不断生成尽可能逼真的假图像。判别器则判断图像是否是真实的图像,还是生成的图像,二者不断博弈优化。最终生成器生成的图像使得判别器完全无法判别真假。

7.1.2 GAN的形式化表达

​上述例子只是简要介绍了一下GAN的思想,下面对于GAN做一个形式化的,更加具体的定义。通常情况下,无论是生成器还是判别器,我们都可以用神经网络来实现。那么,我们可以把通俗化的定义用下面这个模型来表示:
GAN网络结构

​上述模型左边是生成器G,其输入是$z$,对于原始的GAN,$z$是由高斯分布随机采样得到的噪声。噪声$z$通过生成器得到了生成的假样本。

​生成的假样本与真实样本放到一起,被随机抽取送入到判别器D,由判别器去区分输入的样本是生成的假样本还是真实的样本。整个过程简单明了,生成对抗网络中的“生成对抗”主要体现在生成器和判别器之间的对抗。

7.1.3 GAN的目标函数是什么?

​对于上述神经网络模型,如果想要学习其参数,首先需要一个目标函数。GAN的目标函数定义如下:

​这个目标函数可以分为两个部分来理解:

​第一部分:判别器的优化通过$\mathop {\max}\limitsD V(D,G)$实现,$V(D,G)$为判别器的目标函数,其第一项${\rm E}{x\sim{p{data}(x)}}[\log D(x)]$表示对于从真实数据分布 中采用的样本 ,其被判别器判定为真实样本概率的数学期望。对于真实数据分布 中采样的样本,其预测为正样本的概率当然是越接近1越好。因此希望最大化这一项。第二项${\rm E}{z\sim{p_z}(z)}[\log (1 - D(G(z)))]$表示:对于从噪声$P_z(z)​$分布当中采样得到的样本,经过生成器生成之后得到的生成图片,然后送入判别器,其预测概率的负对数的期望,这个值自然是越大越好,这个值越大, 越接近0,也就代表判别器越好。

​第二部分:生成器的优化通过$\mathop {\min }\limits_G({\mathop {\max }\limits_D V(D,G)})$来实现。注意,生成器的目标不是$\mathop {\min }\limits_GV(D,G)$,即生成器不是最小化判别器的目标函数,二是最小化判别器目标函数的最大值,判别器目标函数的最大值代表的是真实数据分布与生成数据分布的JS散度(详情可以参阅附录的推导),JS散度可以度量分布的相似性,两个分布越接近,JS散度越小。

7.1.4 GAN的目标函数和交叉熵有什么区别?

​判别器目标函数写成离散形式即为:

​可以看出,这个目标函数和交叉熵是一致的,即判别器的目标是最小化交叉熵损失,生成器的目标是最小化生成数据分布和真实数据分布的JS散度

7.1.5 GAN的Loss为什么降不下去?

对于很多GAN的初学者在实践过程中可能会纳闷,为什么GAN的Loss一直降不下去。GAN到底什么时候才算收敛?其实,作为一个训练良好的GAN,其Loss就是降不下去的。衡量GAN是否训练好了,只能由人肉眼去看生成的图片质量是否好。不过,对于没有一个很好的评价是否收敛指标的问题,也有许多学者做了一些研究,后文提及的WGAN就提出了一种新的Loss设计方式,较好的解决了难以判断收敛性的问题。下面我们分析一下GAN的Loss为什么降不下去?
​对于判别器而言,GAN的Loss如下:

​从$\mathop {\min }\limits_G \mathop {\max }\limits_D V(D,G)​$可以看出,生成器和判别器的目的相反,也就是说两个生成器网络和判别器网络互为对抗,此消彼长。不可能Loss一直降到一个收敛的状态。

  • 对于生成器,其Loss下降快,很有可能是判别器太弱,导致生成器很轻易的就”愚弄”了判别器。
  • 对于判别器,其Loss下降快,意味着判别器很强,判别器很强则说明生成器生成的图像不够逼真,才使得判别器轻易判别,导致Loss下降很快。

​也就是说,无论是判别器,还是生成器。loss的高低不能代表生成器的好坏。一个好的GAN网络,其GAN Loss往往是不断波动的。

​看到这里可能有点让人绝望,似乎判断模型是否收敛就只能看生成的图像质量了。实际上,后文探讨的WGAN,提出了一种新的loss度量方式,让我们可以通过一定的手段来判断模型是否收敛。

7.1.6 生成式模型、判别式模型的区别?

​对于机器学习模型,我们可以根据模型对数据的建模方式将模型分为两大类,生成式模型和判别式模型。如果我们要训练一个关于猫狗分类的模型,对于判别式模型,只需要学习二者差异即可。比如说猫的体型会比狗小一点。而生成式模型则不一样,需要学习猫张什么样,狗张什么样。有了二者的长相以后,再根据长相去区分。具体而言:

  • 生成式模型:由数据学习联合概率分布P(X,Y), 然后由P(Y|X)=P(X,Y)/P(X)求出概率分布P(Y|X)作为预测的模型。该方法表示了给定输入X与产生输出Y的生成关系

  • 判别式模型:由数据直接学习决策函数Y=f(X)或条件概率分布P(Y|X)作为预测模型,即判别模型。判别方法关心的是对于给定的输入X,应该预测什么样的输出Y。

​对于上述两种模型,从文字上理解起来似乎不太直观。我们举个例子来阐述一下,对于性别分类问题,分别用不同的模型来做:

​1)如果用生成式模型:可以训练一个模型,学习输入人的特征X和性别Y的关系。比如现在有下面一批数据:

Y(性别) 0 1
X(特征) 0 1/4 3/4
1 3/4 1/4

​这个数据可以统计得到,即统计人的特征X=0,1….的时候,其类别为Y=0,1的概率。统计得到上述联合概率分布P(X, Y)后,可以学习一个模型,比如让二维高斯分布去拟合上述数据,这样就学习到了X,Y的联合分布。在预测时,如果我们希望给一个输入特征X,预测其类别,则需要通过贝叶斯公式得到条件概率分布才能进行推断:

​2)如果用判别式模型:可以训练一个模型,输入人的特征X,这些特征包括人的五官,穿衣风格,发型等。输出则是对于性别的判断概率,这个概率服从一个分布,分布的取值只有两个,要么男,要么女,记这个分布为Y。这个过程学习了一个条件概率分布P(Y|X),即输入特征X的分布已知条件下,Y的概率分布。

​显然,从上面的分析可以看出。判别式模型似乎要方便很多,因为生成式模型要学习一个X,Y的联合分布往往需要很多数据,而判别式模型需要的数据则相对少,因为判别式模型更关注输入特征的差异性。不过生成式既然使用了更多数据来生成联合分布,自然也能够提供更多的信息,现在有一个样本(X,Y),其联合概率P(X,Y)经过计算特别小,那么可以认为这个样本是异常样本。这种模型可以用来做outlier detection。

7.1.7 什么是mode collapsing?

​某个模式(mode)出现大量重复样本,例如:
model collapsing
​上图左侧的蓝色五角星表示真实样本空间,黄色的是生成的。生成样本缺乏多样性,存在大量重复。比如上图右侧中,红框里面人物反复出现。

7.1.8 如何解决mode collapsing?

方法一:针对目标函数的改进方法

​为了避免前面提到的由于优化maxmin导致mode跳来跳去的问题,UnrolledGAN采用修改生成器loss来解决。具体而言,UnrolledGAN在更新生成器时更新k次生成器,参考的Loss不是某一次的loss,是判别器后面k次迭代的loss。注意,判别器后面k次迭代不更新自己的参数,只计算loss用于更新生成器。这种方式使得生成器考虑到了后面k次判别器的变化情况,避免在不同mode之间切换导致的模式崩溃问题。此处务必和迭代k次生成器,然后迭代1次判别器区分开[8]。DRAGAN则引入博弈论中的无后悔算法,改造其loss以解决mode collapse问题[9]。前文所述的EBGAN则是加入VAE的重构误差以解决mode collapse。

方法二:针对网络结构的改进方法

​Multi agent diverse GAN(MAD-GAN)采用多个生成器,一个判别器以保障样本生成的多样性。具体结构如下:

​相比于普通GAN,多了几个生成器,且在loss设计的时候,加入一个正则项。正则项使用余弦距离惩罚三个生成器生成样本的一致性。

​MRGAN则添加了一个判别器来惩罚生成样本的mode collapse问题。具体结构如下:

​输入样本$x​$通过一个Encoder编码为隐变量$E(x)​$,然后隐变量被Generator重构,训练时,Loss有三个。$D_M​$和$R​$(重构误差)用于指导生成real-like的样本。而$D_D​$则对$E(x)​$和$z​$生成的样本进行判别,显然二者生成样本都是fake samples,所以这个判别器主要用于判断生成的样本是否具有多样性,即是否出现mode collapse。

方法三:Mini-batch Discrimination

​Mini-batch discrimination在判别器的中间层建立一个mini-batch layer用于计算基于L1距离的样本统计量,通过建立该统计量,实现了一个batch内某个样本与其他样本有多接近。这个信息可以被判别器利用到,从而甄别出哪些缺乏多样性的样本。对生成器而言,则要试图生成具有多样性的样本。

7.2 GAN的生成能力评价

7.2.1 如何客观评价GAN的生成能力?

​最常见评价GAN的方法就是主观评价。主观评价需要花费大量人力物力,且存在以下问题:

  • 评价带有主管色彩,有些bad case没看到很容易造成误判

  • 如果一个GAN过拟合了,那么生成的样本会非常真实,人类主观评价得分会非常高,可是这并不是一个好的GAN。

因此,就有许多学者提出了GAN的客观评价方法。

7.2.2 Inception Score

​对于一个在ImageNet训练良好的GAN,其生成的样本丢给Inception网络进行测试的时候,得到的判别概率应该具有如下特性:

  • 对于同一个类别的图片,其输出的概率分布应该趋向于一个脉冲分布。可以保证生成样本的准确性。
  • 对于所有类别,其输出的概率分布应该趋向于一个均匀分布,这样才不会出现mode dropping等,可以保证生成样本的多样性。

​因此,可以设计如下指标:

​根据前面分析,如果是一个训练良好的GAN,$p_M(y|x)$趋近于脉冲分布,$p_M(y)$趋近于均匀分布。二者KL散度会很大。Inception Score自然就高。实际实验表明,Inception Score和人的主观判别趋向一致。IS的计算没有用到真实数据,具体值取决于模型M的选择。

特点:可以一定程度上衡量生成样本的多样性和准确性,但是无法检测过拟合。Mode Score也是如此。不推荐在和ImageNet数据集差别比较大的数据上使用。

7.2.3 Mode Score

Mode Score作为Inception Score的改进版本,添加了关于生成样本和真实样本预测的概率分布相似性度量一项。具体公式如下:

7.2.4 Kernel MMD (Maximum Mean Discrepancy)

计算公式如下:

​对于Kernel MMD值的计算,首先需要选择一个核函数$k$,这个核函数把样本映射到再生希尔伯特空间(Reproducing Kernel Hilbert Space, RKHS) ,RKHS相比于欧几里得空间有许多优点,对于函数内积的计算是完备的。将上述公式展开即可得到下面的计算公式:

MMD值越小,两个分布越接近。

特点:可以一定程度上衡量模型生成图像的优劣性,计算代价小。推荐使用。

7.2.5 Wasserstein distance

​Wasserstein distance在最优传输问题中通常也叫做推土机距离。这个距离的介绍在WGAN中有详细讨论。公式如下:

​Wasserstein distance可以衡量两个分布之间的相似性。距离越小,分布越相似。

特点:如果特征空间选择合适,会有一定的效果。但是计算复杂度为$O(n^3)​$太高

7.2.6 Fréchet Inception Distance (FID)

​FID距离计算真实样本,生成样本在特征空间之间的距离。首先利用Inception网络来提取特征,然后使用高斯模型对特征空间进行建模。根据高斯模型的均值和协方差来进行距离计算。具体公式如下:

$\mu,C​$分别代表协方差和均值。

特点:尽管只计算了特征空间的前两阶矩,但是鲁棒,且计算高效。

7.2.7 1-Nearest Neighbor classifier

​使用留一法,结合1-NN分类器(别的也行)计算真实图片,生成图像的精度。如果二者接近,则精度接近50%,否则接近0%。对于GAN的评价问题,作者分别用正样本的分类精度,生成样本的分类精度去衡量生成样本的真实性,多样性。

  • 对于真实样本$x_r$,进行1-NN分类的时候,如果生成的样本越真实。则真实样本空间$\mathbb R$将被生成的样本$x_g$包围。那么$x_r$的精度会很低。
  • 对于生成的样本$x_g​$,进行1-NN分类的时候,如果生成的样本多样性不足。由于生成的样本聚在几个mode,则$x_g​$很容易就和$x_r​$区分,导致精度会很高。

特点:理想的度量指标,且可以检测过拟合。

7.2.8 其他评价方法

AIS,KDE方法也可以用于评价GAN,但这些方法不是model agnostic metrics。也就是说,这些评价指标的计算无法只利用:生成的样本,真实样本来计算。

7.3 其他常见的生成式模型有哪些?

7.3.1 什么是自回归模型:pixelRNN与pixelCNN?

自回归模型通过对图像数据的概率分布$p_{data}(x)$进行显式建模,并利用极大似然估计优化模型。具体如下:

​上述公式很好理解,给定$x1,x_2,…,x{i-1}$条件下,所有$p(x_i)$的概率乘起来就是图像数据的分布。如果使用RNN对上述依然关系建模,就是pixelRNN。如果使用CNN,则是pixelCNN。具体如下[5]:

​显然,不论是对于pixelCNN还是pixelRNN,由于其像素值是一个个生成的,速度会很慢。语音领域大火的WaveNet就是一个典型的自回归模型。

7.3.2 什么是VAE?

PixelCNN/RNN定义了一个易于处理的密度函数,我们可以直接优化训练数据的似然;对于变分自编码器我们将定义一个不易处理的密度函数,通过附加的隐变量$z$对密度函数进行建模。 VAE原理图如下[6]:

在VAE中,真实样本$X$通过神经网络计算出均值方差(假设隐变量服从正太分布),然后通过采样得到采样变量$Z$并进行重构。VAE和GAN均是学习了隐变量$z$到真实数据分布的映射。但是和GAN不同的是:

  • GAN的思路比较粗暴,使用一个判别器去度量分布转换模块(即生成器)生成分布与真实数据分布的距离。
  • VAE则没有那么直观,VAE通过约束隐变量$z$服从标准正太分布以及重构数据实现了分布转换映射$X=G(z)$

生成式模型对比

  • 自回归模型通过对概率分布显式建模来生成数据
  • VAE和GAN均是:假设隐变量$z$服从某种分布,并学习一个映射$X=G(z)$,实现隐变量分布$z$与真实数据分布$p_{data}(x)$的转换。
  • GAN使用判别器去度量映射$X=G(z)$的优劣,而VAE通过隐变量$z$与标准正太分布的KL散度和重构误差去度量。

7.4 GAN的改进与优化

7.4.1 如何生成指定类型的图像——条件GAN

条件生成对抗网络(CGAN, Conditional Generative Adversarial Networks)作为一个GAN的改进,其一定程度上解决了GAN生成结果的不确定性。如果在Mnist数据集上训练原始GAN,GAN生成的图像是完全不确定的,具体生成的是数字1,还是2,还是几,根本不可控。为了让生成的数字可控,我们可以把数据集做一个切分,把数字0~9的数据集分别拆分开训练9个模型,不过这样太麻烦了,也不现实。因为数据集拆分不仅仅是分类麻烦,更主要在于,每一个类别的样本少,拿去训练GAN很有可能导致欠拟合。因此,CGAN就应运而生了。我们先看一下CGAN的网络结构:
CGAN网络结构
​ 从网络结构图可以看到,对于生成器Generator,其输入不仅仅是随机噪声的采样z,还有欲生成图像的标签信息。比如对于mnist数据生成,就是一个one-hot向量,某一维度为1则表示生成某个数字的图片。同样地,判别器的输入也包括样本的标签。这样就使得判别器和生成器可以学习到样本和标签之间的联系。Loss如下:

Loss设计和原始GAN基本一致,只不过生成器,判别器的输入数据是一个条件分布。在具体编程实现时只需要对随机噪声采样z和输入条件y做一个级联即可。

7.4.2 CNN与GAN——DCGAN

前面我们聊的GAN都是基于简单的神经网络构建的。可是对于视觉问题,如果使用原始的基于DNN的GAN,则会出现许多问题。如果输入GAN的随机噪声为100维的随机噪声,输出图像为256x256大小。也就是说,要将100维的信息映射为65536维。如果单纯用DNN来实现,那么整个模型参数会非常巨大,而且学习难度很大(低维度映射到高维度需要添加许多信息)。因此,DCGAN就出现了。具体而言,DCGAN将传统GAN的生成器,判别器均采用GAN实现,且使用了一下tricks:

  • 将pooling层convolutions替代,其中,在discriminator上用strided convolutions替代,在generator上用fractional-strided convolutions替代。
  • 在generator和discriminator上都使用batchnorm。
  • 移除全连接层,global pooling增加了模型的稳定性,但伤害了收敛速度。
  • 在generator的除了输出层外的所有层使用ReLU,输出层采用tanh。
  • 在discriminator的所有层上使用LeakyReLU。

网络结构图如下:
CGAN网络结构图

7.4.3 如何理解GAN中的输入随机噪声?

为了了解输入随机噪声每一个维度代表的含义,作者做了一个非常有趣的工作。即在隐空间上,假设知道哪几个变量控制着某个物体,那么僵这几个变量挡住是不是就可以将生成图片中的某个物体消失?论文中的实验是这样的:首先,生成150张图片,包括有窗户的和没有窗户的,然后使用一个逻辑斯底回归函数来进行分类,对于权重不为0的特征,认为它和窗户有关。将其挡住,得到新的生成图片,结果如下:
DCGAN输入噪声理解
此外,将几个输入噪声进行算数运算,可以得到语义上进行算数运算的非常有趣的结果。类似于word2vec。
DCGAN输入噪声算术运算

7.4.4 GAN为什么容易训练崩溃?

所谓GAN的训练崩溃,指的是训练过程中,生成器和判别器存在一方压倒另一方的情况。
GAN原始判别器的Loss在判别器达到最优的时候,等价于最小化生成分布与真实分布之间的JS散度,由于随机生成分布很难与真实分布有不可忽略的重叠以及JS散度的突变特性,使得生成器面临梯度消失的问题;可是如果不把判别器训练到最优,那么生成器优化的目标就失去了意义。因此需要我们小心的平衡二者,要把判别器训练的不好也不坏才行。否则就会出现训练崩溃,得不到想要的结果

7.4.5 WGAN如何解决训练崩溃问题?

WGAN作者提出了使用Wasserstein距离,以解决GAN网络训练过程难以判断收敛性的问题。Wasserstein距离定义如下:

通过最小化Wasserstein距离,得到了WGAN的Loss:

  • WGAN生成器Loss:$- {\rm E}_{x\sim{p_g}(x)}[f_w(x)]​$
  • WGAN判别器Loss:$L=-{\rm E}{x\sim{p{data}}(x)}[fw(x)] + {\rm E}{x\sim{p_g}(x)}[f_w(x)]$

从公式上GAN似乎总是让人摸不着头脑,在代码实现上来说,其实就以下几点:

  • 判别器最后一层去掉sigmoid
  • 生成器和判别器的loss不取log
  • 每次更新判别器的参数之后把它们的绝对值截断到不超过一个固定常数c

7.4.6 WGAN-GP:带有梯度正则的WGAN

实际实验过程发现,WGAN没有那么好用,主要原因在于WAGN进行梯度截断。梯度截断将导致判别网络趋向于一个二值网络,造成模型容量的下降。
于是作者提出使用梯度惩罚来替代梯度裁剪。公式如下:

由于上式是对每一个梯度进行惩罚,所以不适合使用BN,因为它会引入同个batch中不同样本的相互依赖关系。如果需要的话,可以选择Layer Normalization。实际训练过程中,就可以通过Wasserstein距离来度量模型收敛程度了:
Wass距离随迭代次数变化
上图纵坐标是Wasserstein距离,横坐标是迭代次数。可以看出,随着迭代的进行,Wasserstein距离趋于收敛,生成图像也趋于稳定。

7.4.7 LSGAN

LSGAN(Least Squares GAN)这篇文章主要针对标准GAN的稳定性和图片生成质量不高做了一个改进。作者将原始GAN的交叉熵损失采用最小二乘损失替代。LSGAN的Loss:

实际实现的时候非常简单,最后一层去掉sigmoid,并且计算Loss的时候用平方误差即可。之所以这么做,作者在原文给出了一张图:
LSGAN交叉熵与最小二乘损失对比图
上面是作者给出的基于交叉熵损失以及最小二乘损失的Loss函数。横坐标代表Loss函数的输入,纵坐标代表输出的Loss值。可以看出,随着输入的增大,sigmoid交叉熵损失很快趋于0,容易导致梯度饱和问题。如果使用右边的Loss设计,则只在x=0点处饱和。因此使用LSGAN可以很好的解决交叉熵损失的问题。

7.4.8 如何尽量避免GAN的训练崩溃问题?

  • 归一化图像输入到(-1,1)之间;Generator最后一层使用tanh激活函数
  • 生成器的Loss采用:min (log 1-D)。因为原始的生成器Loss存在梯度消失问题;训练生成器的时候,考虑反转标签,real=fake, fake=real
  • 不要在均匀分布上采样,应该在高斯分布上采样
  • 一个Mini-batch里面必须只有正样本,或者负样本。不要混在一起;如果用不了Batch Norm,可以用Instance Norm
  • 避免稀疏梯度,即少用ReLU,MaxPool。可以用LeakyReLU替代ReLU,下采样可以用Average Pooling或者Convolution + stride替代。上采样可以用PixelShuffle, ConvTranspose2d + stride
  • 平滑标签或者给标签加噪声;平滑标签,即对于正样本,可以使用0.7-1.2的随机数替代;对于负样本,可以使用0-0.3的随机数替代。 给标签加噪声:即训练判别器的时候,随机翻转部分样本的标签。
  • 如果可以,请用DCGAN或者混合模型:KL+GAN,VAE+GAN。
  • 使用LSGAN,WGAN-GP
  • Generator使用Adam,Discriminator使用SGD
  • 尽快发现错误;比如:判别器Loss为0,说明训练失败了;如果生成器Loss稳步下降,说明判别器没发挥作用
  • 不要试着通过比较生成器,判别器Loss的大小来解决训练过程中的模型坍塌问题。比如:
    While Loss D > Loss A:
    Train D
    While Loss A > Loss D:
    Train A
  • 如果有标签,请尽量利用标签信息来训练
  • 给判别器的输入加一些噪声,给G的每一层加一些人工噪声。
  • 多训练判别器,尤其是加了噪声的时候
  • 对于生成器,在训练,测试的时候使用Dropout

7.3 GAN的应用(图像翻译)

7.3.1 什么是图像翻译?

GAN作为一种强有力的生成模型,其应用十分广泛。最为常见的应用就是图像翻译。所谓图像翻译,指从一副图像到另一副图像的转换。可以类比机器翻译,一种语言转换为另一种语言。常见的图像翻译任务有:

  • 图像去噪
  • 图像超分辨
  • 图像补全
  • 风格迁移

本节将介绍一个经典的图像翻译网络及其改进。图像翻译可以分为有监督图像翻译和无监督图像翻译:

  • 有监督图像翻译:原始域与目标域存在一一对应数据
  • 无监督图像翻译:原始域与目标域不存在一一对应数据

7.3.2 有监督图像翻译:pix2pix

在这篇paper里面,作者提出的框架十分简洁优雅(好用的算法总是简洁优雅的)。相比以往算法的大量专家知识,手工复杂的loss。这篇paper非常粗暴,使用CGAN处理了一系列的转换问题。下面是一些转换示例:
pix2pix结果示例

上面展示了许多有趣的结果,比如分割图$\longrightarrow$街景图,边缘图$\longrightarrow$真实图。对于第一次看到的时候还是很惊艳的,那么这个是怎么做到的呢?我们可以设想一下,如果是我们,我们自己会如何设计这个网络?

直观的想法

最直接的想法就是,设计一个CNN网络,直接建立输入-输出的映射,就像图像去噪问题一样。可是对于上面的问题,这样做会带来一个问题。生成图像质量不清晰。

拿左上角的分割图$\longrightarrow$街景图为例,语义分割图的每个标签比如“汽车”可能对应不同样式,颜色的汽车。那么模型学习到的会是所有不同汽车的评均,这样会造成模糊。pix2pix语义地图L1loss结果

如何解决生成图像的模糊问题

这里作者想了一个办法,即加入GAN的Loss去惩罚模型。GAN相比于传统生成式模型可以较好的生成高分辨率图片。思路也很简单,在上述直观想法的基础上加入一个判别器,判断输入图片是否是真实样本。模型示意图如下:
pix2pix模型示意图

上图模型和CGAN有所不同,但它是一个CGAN,只不过输入只有一个,这个输入就是条件信息。原始的CGAN需要输入随机噪声,以及条件。这里之所有没有输入噪声信息,是因为在实际实验中,如果输入噪声和条件,噪声往往被淹没在条件C当中,所以这里直接省去了。

7.3.3 其他图像翻译的tricks

从上面两点可以得到最终的Loss由两部分构成:

  • 输出和标签信息的L1 Loss。

  • GAN Loss

  • 测试也使用Dropout,以使输出多样化

采用L1 Loss而不是L2 Loss的理由很简单,L1 Loss相比于L2 Loss保边缘(L2 Loss基于高斯先验,L1 Loss基于拉普拉斯先验)。

GAN Loss为LSGAN的最小二乘Loss,并使用PatchGAN(进一步保证生成图像的清晰度)。PatchGAN将图像换分成很多个Patch,并对每一个Patch使用判别器进行判别(实际代码实现有更取巧的办法),将所有Patch的Loss求平均作为最终的Loss。

7.3.4 如何生成高分辨率图像和高分辨率视频?

pix2pix提出了一个通用的图像翻译框架。对于高分辨率的图像生成以及高分辨率的视频生成,则需要利用更好的网络结构以及更多的先验只是。pix2pixHD提出了一种多尺度的生成器以及判别器等方式从而生成高分辨率图像。Vid2Vid则在pix2pixHD的基础上利用光流,时序约束生成了高分辨率视频。

7.3.5 有监督的图像翻译的缺点?

许多图像翻译算法如前面提及的pix2pix系列,需要一一对应的图像。可是在许多应用场景下,往往没有这种一一对应的强监督信息。比如说以下一些应用场景:
CycleGAN结果例子
以第一排第一幅图为例,要找到这种一一配对的数据是不现实的。因此,无监督图像翻译算法就被引入了。

7.3.6 无监督图像翻译:CycleGAN

模型结构

总体思路如下,假设有两个域的数据,记为A,B。对于上图第一排第一幅图A域就是普通的马,B域就是斑马。由于A->B的转换缺乏监督信息,于是,作者提出采用如下方法进行转换:

a. A->fake_B->rec_A
b. B->fake_A->rec_B

对于A域的所有图像,学习一个网络G_B,该网络可以生成B。对于B域的所有图像,也学习一个网络G_A,该网络可以生成G_B。

训练过程分成两步,首先对于A域的某张图像,送入G_B生成fake_B,然后对fake_B送入G_A,得到重构后的A图像rec_A。对于B域的某一张图像也是类似。重构后的图像rec_A/rec_B可以和原图A/B做均方误差,实现了有监督的训练。此处值得注意的是A->fake_B(B->fake_A)和fake_A->rec_B(fake_B->rec_A)的网络是一模一样的。下图是形象化的网络结构图:
CycleGAN模型示意图
cycleGAN的生成器采用U-Net,判别器采用LS-GAN。

Loss设计

总的Loss就是X域和Y域的GAN Loss,以及Cycle consistency loss:

整个过程End to end训练,效果非常惊艳,利用这一框架可以完成非常多有趣的任务

7.3.7 多领域的无监督图像翻译:StarGAN

cycleGAN模型较好的解决了无监督图像转换问题,可是这种单一域的图像转换还存在一些问题:

  • 要针对每一个域训练一个模型,效率太低。举例来说,我希望可以将橘子转换为红苹果和青苹果。对于cycleGAN而言,需要针对红苹果,青苹果分别训练一个模型。
  • 对于每一个域都需要搜集大量数据,太麻烦。还是以橘子转换为红苹果和青苹果为例。不管是红苹果还是青苹果,都是苹果,只是颜色不一样而已。这两个任务信息是可以共享的,没必要分别训练两个模型。而且针对红苹果,青苹果分别取搜集大量数据太费事。

starGAN则提出了一个多领域的无监督图像翻译框架,实现了多个领域的图像转换,且对于不同领域的数据可以混合在一起训练,提高了数据利用率

7.4 GAN的应用(文本生成)

7.4.1 GAN为什么不适合文本任务?

GAN在2014年被提出之后,在图像生成领域取得了广泛的研究应用。然后在文本领域却一直没有很惊艳的效果。主要在于文本数据是离散数据,而GAN在应用于离散数据时存在以下几个问题:

  • GAN的生成器梯度来源于判别器对于正负样本的判别。然而,对于文本生成问题,RNN输出的是一个概率序列,然后取argmax。这会导致生成器Loss不可导。还可以站在另一个角度理解,由于是argmax,所以参数更新一点点并不会改变argmax的结果,这也使得GAN不适合离散数据。
  • GAN只能评估整个序列的loss,但是无法评估半句话,或者是当前生成单词对后续结果好坏的影响。
  • 如果不加argmax,那么由于生成器生成的都是浮点数值,而ground truth都是one-hot encoding,那么判别器只要判别生成的结果是不是0/1序列组成的就可以了。这容易导致训练崩溃。

7.4.2 seqGAN用于文本生成

seqGAN在GAN的框架下,结合强化学习来做文本生成。 模型示意图如下:

seqGAN模型
在文本生成任务,seqGAN相比较于普通GAN区别在以下几点:

  • 生成器不取argmax。
  • 每生成一个单词,则根据当前的词语序列进行蒙特卡洛采样生成完成的句子。然后将句子送入判别器计算reward。
  • 根据得到的reward进行策略梯度下降优化模型。

7.5 GAN在其他领域的应用

7.5.1 数据增广

GAN的良好生成特性近年来也开始被用于数据增广。以行人重识别为例,有许多GAN用于数据增广的工作[1-4]。行人重识别问题一个难点在于不同摄像头下拍摄的人物环境,角度差别非常大,导致存在较大的Domain gap。因此,可以考虑使用GAN来产生不同摄像头下的数据进行数据增广。以论文[1]为例,本篇paper提出了一个cycleGAN用于数据增广的方法。具体模型结构如下:

cycleGAN数据增广

对于每一对摄像头都训练一个cycleGAN,这样就可以实现将一个摄像头下的数据转换成另一个摄像头下的数据,但是内容(人物)保持不变。
在CVPR19中,[9]进一步提升了图像的生成质量,进行了“淘宝换衣”式的高质量图像生成(如下图),提供了更高质量的行人训练数据。

DG-Net数据增广

7.5.2 图像超分辨与图像补全

图像超分辨与补全均可以作为图像翻译问题,该类问题的处理办法也大都是训练一个端到端的网络,输入是原始图片,输出是超分辨率后的图片,或者是补全后的图片。文献[5]利用GAN作为判别器,使得超分辨率模型输出的图片更加清晰,更符合人眼主管感受。日本早稻田大学研究人员[6]提出一种全局+局部一致性的GAN实现图像补全,使得修复后的图像不仅细节清晰,且具有整体一致性。

7.5.3 语音领域

相比于图像领域遍地开花,GAN在语音领域则应用相对少了很多。这里零碎的找一些GAN在语音领域进行应用的例子作为介绍。文献[7]提出了一种音频去噪的SEGAN,缓解了传统方法支持噪声种类稀少,泛化能力不强的问题。Donahue利用GAN进行语音增强,提升了ASR系统的识别率。

Windows C++配置教程

配置环境

操作系统:Windows7
vscode版本号:1.48.2

下载工具

安装cpptools工具

  • 打开vscode,点击Extensions;
  • 搜索c++,选择第一个c/c++(1.0.1)
  • 点击install安装
    安装cpptools工具

安装code runner工具

在VScode中编译文件,结束后并不会像我们经常使用的IDE一样,终端会停留在面前然后告诉你“按任意键继续”,在VScode中,编译运行完成后往往cmd会一闪而过,然后直接:
The program ‘d:\MinGW\Projicts\test\test.exe’ has exited with code 0 (0x00000000).
这个时候我们往往会使用在结束的return语句前加上getchar() 或
system(“pause”)(注意:使用这个的时候需要加上头文件#include<stdlib.h>

我们可以使用code runner工具来解决这个问题
首先和上面一样 找到这个插件并安装它
安装code runner工具

下载MinGW

下载地址:https://sourceforge.net/projects/mingw-w64/files/
下载的文件:进入网站后不要点击 “Download Lasted Version”,往下滑,找到最新版的x86_64-posix-seh
安装MinGW:下载后是一个7z的压缩包,解压后移动到你想安装的位置即可。

配置MinGW环境变量

配置对象:WinGW,就是把刚刚安装WinGW的路径拷贝一下
右键点击,计算机 → 属性 → 高级系统设置 → 环境变量 → 双击系统变量Path → 复制MinGW安装路径的bin文件夹
例如我的是:D:\MinGW\mingw64\bin
记得每一步都要点确定,配置完成后我们使用命令行检验一下是否配置成功
win+R 输入cmd后回车打开命令行,输入g++ -v
出现下图界面说明配置成功
配置MinGW环境变量
然后打开以下界面进行操作



即可

配置C++环境

创建一个c/cpp文件

首先随便打开或者新建一个文件夹CppCode, 然后在文件夹里创建一个c或cpp文件

1
2
3
4
5
6
7
8
#include<iostream>

int main()
{
std::cout<<"Hello World"<<std::endl;
getchar();
return 0;
}

创建json文件

先创建一个你打算存放代码的文件夹(称作工作区),路径不能含有中文和空格和引号。c语言和c++需要建立不同的工作区(除非你懂得下面json文件的某些选项,则可以做到一个工作区使用不同的build task)。

打开VS Code,选打开文件夹(不要选“添加工作区文件夹”,理由见上一句),选择刚才那个文件夹,点VS Code上的新建文件夹,名称为.vscode(这样做的原因是Windows的Explorer不允许创建的文件夹第一个字符是点),然后创建launch.json,tasks.json,settings.json,c_cpp_properties.json放到.vscode文件夹下。
特别注意:C/C++文件放在与.vscode 的所在的同级目录中,.vscode 只放置4个json文件。

launch.json配置文件

stopAtEntry可根据自己喜好修改;cwd可以控制程序运行时的相对路径,如有需要可以改为${fileDirname}。其他无需更改,除非你不用windows,则可以用lldb调试(需要自己装)。type和request不变色是正常现象。

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
{
"version": "0.2.0",
"configurations": [

{
"name": "g++.exe build and debug active file", // 配置名称,将会在启动配置的下拉菜单中显示
"type": "cppdbg", // 配置类型,这里只能为cppdbg
"request": "launch", // 请求配置类型,可以为launch(启动)或attach(附加)
"program": "${fileDirname}/${fileBasenameNoExtension}.exe", // 将要进行调试的程序的路径
"args": [], // 程序调试时传递给程序的命令行参数,一般设为空即可
"stopAtEntry": true, // 设为true时程序将暂停在程序入口处,我一般设置为true
"cwd": "${workspaceFolder}", // 调试程序时的工作目录
"environment": [], // (环境变量?)
"externalConsole": true, // 调试时是否显示控制台窗口,一般设置为true显示控制台
"internalConsoleOptions": "neverOpen", // 如果不设为neverOpen,调试时会跳到“调试控制台”选项卡,你应该不需要对gdb手动输命令吧?
"MIMode": "gdb", // 指定连接的调试器,可以为gdb或lldb。但目前lldb在windows下没有预编译好的版本。
"miDebuggerPath": "D:/MinGW/mingw64/bin/gdb.exe", // 调试器路径。
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": false
}
],
"preLaunchTask": "task g++" // 调试会话开始前执行的任务,一般为编译程序。与tasks.json的label相对应
}
]
}

tasks.json配置文件

reveal可根据自己喜好修改,即使设为never,也只是编译时不跳转到“终端”而已,手动点进去还是可以看到,我个人设为never。
命令行参数方面,-std根据自己的需要修改。如果使用Clang编写C语言,把command的值改成clang。
如果使用MinGW,编译C用gcc,编译c++用g++,并把-target和-fcolor那两条删去。如果不想要额外警告,把-Wall那一条删去。

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
{
"version": "2.0.0",
"tasks": [
{
"label": "task g++", // 任务名称,与launch.json的preLaunchTask相对应
"command": "D:/MinGW/mingw64/bin/g++.exe", // 要使用的编译器
"args": [
"${file}",
"-o", // 指定输出文件名,不加该参数则默认输出a.exe
"${fileDirname}/${fileBasenameNoExtension}.exe",
"-g", // 生成和调试有关的信息
"-Wall", // 开启额外警告
"-static-libgcc", // 静态链接
//"-fcolor-diagnostics",
//"--target=x86_64-w64-mingw", // 默认target为msvc,不加这一条就会找不到头文件
"-std=c++17" // C语言最新标准为c11,或根据自己的需要进行修改
], // 编译命令参数
"type": "shell",
"group": {
"kind": "build",
"isDefault": true // 设为false可做到一个tasks.json配置多个编译指令,需要自己修改本文件,我这里不多提
},
"presentation": {
"echo": true,
"reveal": "always", // 在“终端”中显示编译信息的策略,可以为always,silent,never。具体参见VSC的文档
"focus": false, // 设为true后可以使执行task时焦点聚集在终端,但对编译c和c++来说,设为true没有意义
"panel": "shared" // 不同的文件的编译信息共享一个终端面板
},
"options": {
"cwd": "D:/MinGW/mingw64/bin"
},
"problemMatcher": [
"$gcc"
]
}
]
}

c_cpp_properties.json配置文件

此文件内容来自于 Microsoft/vscode-cpptools ;这个json不允许有注释(其实按照标准本来就不能有)。

如果你没有合并Clang和MinGW,则该文件中的compilerPath必需修改成MinGW的完整路径,精确到gcc.exe,否则会提示找不到头文件;Linux下应该是/usr/bin/gcc。
没有该文件运行时,会报错”includepath”设置问题
记得把路径修改为你自己的路径,查找自己路径方法为打开cmd 输入:gcc -v -E -x c++ -

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
{
"configurations": [
{
"name": "MinGW",
"includePath": [
"${workspaceRoot}",
"D:/MinGW/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/8.1.0/include/c++",
"D:/MinGW/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/8.1.0/include/c++/x86_64-w64-mingw32",
"D:/MinGW/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/8.1.0/include/c++/backward",
"D:/MinGW/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/8.1.0/include",
"D:/MinGW/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/8.1.0/include-fixed",
"D:/MinGW/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/8.1.0/../../../../x86_64-w64-mingw32/include"
],
"defines": [
"_DEBUG",
"UNICODE",
"__GNUC__=6",
"__cdecl=__attribute__((__cdecl__))"
],
"intelliSenseMode": "msvc-x64",
"browse": {
"limitSymbolsToIncludedHeaders": true,
"databaseFilename": "",
"path": [
"${workspaceRoot}",
"D:/MinGW/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/8.1.0/include/c++",
"D:/MinGW/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/8.1.0/include/c++/x86_64-w64-mingw32",
"D:/MinGW/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/8.1.0/include/c++/backward",
"D:/MinGW/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/8.1.0/include",
"D:/MinGW/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/8.1.0/include-fixed",
"D:/MinGW/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/8.1.0/../../../../x86_64-w64-mingw32/include"
]
}
}
],
"version": 4
}

运行文件

这个时候我们已经配置完成,可以开始运行我们刚才创建的测试代码啦
F5:VScode调试
CTRL+ALT+N:code runner调试

注意:若出现标记“&&”不是此版本中的有效语句分隔符错误,
选择 File-> Preferences -> Settings,在搜索框输入setting.json。
进入setting.json,添加这一句话

1
terminal.integrated.shell.windows": "powershell.exe",

使用powershell的格式来进行编译执行,会自动将 && 符号换为 ; 号,即可解决。
如果code runner出现中文乱码,尝试在setting.json添加:
1
"cpp": "cd $dir && g++ -fexec-charset=GBK -std=c++11 $fileName -o $fileNameWithoutExt && $dir$fileNameWithoutExt",

VS Code构建C++远程开发环境

操作系统:win7 远程服务器:Ubuntu 16.04.4
通过过Windows远程Linux进行开发,同时也能拥有IDE一般的开发体验。
方法:

  • Visual Studio支持的远程Linux开发功能:Visual Studio堪称宇宙IDE,功能强大而且Windows和Linux的开发体验一脉相承。然而程序体积庞大,并且配置较复杂,启动时间较长。如果希望使用cmake来进行项目构建,Visual Studio所提供的cmake模板与常规在Linux下构建cmake项目的模板结构有些差异。使用起来不太顺手;
  • CLion的远程开发功能:配置相对简单,编程体验也非常优秀(尤其是CLion自带的很多重构方式,极大提升编程效率)。然而远程开发首先要在本地构建项目,然后通过CLion将项目文件传输到目标Linux机器,然后才开始调试。不能实现直接打开远端的项目(可能我没找到) 来进行编程。
  • 通过VS Code进行远程开发:承袭VS Code的编程体验,流畅度和稳定度都非常良好。而且程序较轻量,打开速度很快。各种插件几乎涵盖日常编程需求。各种语言都可以统一在VS Code下面来编写(个人觉得这个特点非常优越)。美中不足就是毕竟不是传统IDE,在一些重构和自动生成的功能不如Visual Studio和CLion。不过瑕不掩瑜,这是当前个人最提倡的远程开发C++的方式。

环境配置

Remote SSH配置

微软的PowerShell团队已经支持openssh,所以安装文件我们可以在github的powershell团队项目下进行下载
下载地址: https://github.com/PowerShell/Win32-OpenSSH/releases
根据你自己的系统对应下载。
把OpenSSH整个目录进行复制到 C:\Program Files (其实哪个目录都可以,不过建议安装在这里)
回到Windows桌面,在 计算机(windows7)或此电脑(windows10),右键 —> 属性 —> 高级系统设置 —> 环境变量—系统变量,在此框里面找到 Path 进行编辑
使用cmd命令打开dos命令行,输入ssh或scp,出现如下情况则配置成功!

VS Code添加扩展包

从VS code中的扩展商店中添加Remote Development插件,如下图所示。

添加完成后,我们发现多了这些插件以及Remote SSH的图标。

配置私钥

在.ssh目录下,用ssh-keygen命令生成密钥,如果.ssh目录已有id_rsa文件,可跳过。
然后将生成的id_rsa.pub文件传到远程服务器的根目录下.ssh文件夹中。
用ssh命令(ssh username@ip -p port)连接远程主机,并将idrsa.pub加入到authorized_keys

1
2
3
ssh gxk@10.103.238.162 -p 22
cd .ssh
cat id_rsa.pub >> authorized_keys //该指令可以免密登录

退出连接(exit命令),改用私钥登录(ssh username@ip -p port –i id_rsa),即ssh gxk@10.103.238.162 -p 22 –i id_rsa

添加配置文件

点击Remote SSH的图标后再点击箭头所指的齿轮

会弹出菜单让你选择需要编辑的配置文件,一般选第一个

选择之后可以按照下图添加配置信息

参数的含义分别为:
Host 连接的主机的名称,可自定
Hostname 远程主机的IP地址
User 用于登录远程主机的用户名
Port 用于登录远程主机的端口
IdentityFile 本地的id_rsa的路径
如果需要多个连接,可按照如上配置多个。配置完成并保存后,左边栏中多了远程主机的图标。

C++基础知识

static 静态变量

全局静态变量

在整个程序运行期间一直存在。
初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);
作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。

局部静态变量

存储在静态存储区
初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);
作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;

静态函数

在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。
函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突;
warning:不要再头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰;

类的静态成员

在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用

类的静态函数

静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。
在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);

C++和C的区别

设计思想上:C++是面向对象的语言,而C是面向过程的结构化编程语言。
语法上:(1)C++具有封装、继承和多态三种特性;(2)C++相比C,增加多许多类型安全的功能,比如强制类型转换、智能指针等;(3)C++支持范式编程,比如模板类、函数模板等。

封装

封装是在设计类的一个基本原理,就是将数据与对数据进行的操作进行有机的结合,形成“类”,其中数据和函数都是类的成员。

继承

如果一个类别B“继承自”另一个类别A,就把这个B称为“A的子类”,而把A称为“B的父类别”也可以称“A是B的超类”。继承可以使得子类具、有父类别的各种属性和方法,而不需要再次编写相同的代码。在令子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能。
(1)访问权限
public:父类对象内部、父类对象外部、子类对象内部、子类对象外部都可以访问。
protected:父类对象内部、子类对象内部可以访问,父类对象外部、子类对象外部都不可访问。
private:父类对象内部可以访问,其他都不可以访问。
(2)继承方式
三种继承方式不影响子类对父类的访问权限,子类对父类只看父类的访问控制权。继承方式是为了控制子类(也称派生类)的调用方(也叫用户)对父类(也称基类)的访问权限。
public、protected、private三种继承方式,相当于把父类的public访问权限在子类中变成了对应的权限。 如protected继承,把父类中的public成员在本类中变成了protected的访问控制权限;private继承,把父类的public成员和protected成员在本类中变成了private访问控制权。

多态

多态性可以简单地概括为“一个接口,多种方法”,程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念。

静态多态

静态多态也称为静态绑定或早绑定。编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用那个函数,如果有对应的函数就调用该函数,否则出现编译错误。
(1)函数重载
编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译器来说是这样的)。函数的调用,在编译器间就已经确定了,是静态的。也就是说,它们的地址在编译期就绑定了(早绑定)。
(2)泛型编程
泛型编程就是指编写独立于特定类型的代码,泛型在C++中的主要实现为模板函数和模板类。
泛型的特性:
a) 函数模板并不是真正的函数,它只是C++编译生成具体函数的一个模子。
b) 函数模板本身并不生成函数,实际生成的函数是替换函数模板的那个函数,比如上例中的add(sum1,sum2),这种替换是编译期就绑定的。
c) 函数模板不是只编译一份满足多重需要,而是为每一种替换它的函数编译一份。
d) 函数模板不允许自动类型转换。
e) 函数模板不可以设置默认模板实参。比如template 不可以。

动态多态

c++的动态多态是基于虚函数的。对于相关的对象类型,确定它们之间的一个共同功能集,然后在基类中,把这些共同的功能声明为多个公共的虚函数接口。各个子类重写这些虚函数,以完成具体的功能。客户端的代码(操作函数)通过指向基类的引用或指针来操作这些对象,对虚函数的调用会自动绑定到实际提供的子类对象上去。

C++的四种cast转换

C++中四种类型转换是:static_cast, dynamic_cast, const_cast, reinterpret_cast
(1) const_cast
用于将const变量转为非const
(2) static_cast
用于各种隐式转换,比如非const转const,void*转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;
(3) dynamic_cast
用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。
向上转换:指的是子类向基类的转换;
向下转换:指的是基类向子类的转换;
它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
(4) reinterpret_cast
几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用。

为什么不使用C的强制转换?C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。

C++的四种智能指针

C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被11弃用。
为什么要使用智能指针?智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。

auto_ptr(c++98的方案,cpp11已经抛弃)

auto_ptr的缺点是:存在潜在的内存崩溃问题!

unique_ptr(替换auto_ptr)

unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。

shared_ptr

shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。
shared_ptr 是为了解决 unique_ptr 在对象所有权上的局限性(unique_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。
成员函数:
use_count:返回引用计数的个数;
unique:返回是否是独占所有权( use_count 为 1);
swap:交换两个 shared_ptr 对象(即交换所拥有的对象)
reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少;
get:返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr sp(new int(1)); sp 与 sp.get()是等价的。

weak_ptr

weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr可以用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

指针和引用的区别

定义

(1)引用:引用就是某一变量的一个别名,对引用的操作与对变量直接操作完全一样。引用是C++对C语言的重要扩充。
(2)指针:指针存储的是变量在内存区域的地址。

区别

(1)指针有自己的一块空间,而引用只是一个别名;
(2)指针可以被初始化为NULL(nullptr),而引用必须被初始化且必须是一个已有对象 的引用;
(3)作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引 用的修改都会改变引用所指向的对象;
(4)指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变;
(5)指针可以有多级指针(**p),而引用至多一级。

malloc 和new

(1)new分配内存按照数据类型进行分配,malloc分配内存按照指定的大小分配;
(2)new返回的是指定对象的指针,而malloc返回的是void,因此malloc的返回值一般都需要进行类型转化。
(3)new不仅分配一段内存,而且会调用构造函数,malloc不会。
(4)new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会。
(5)new是一个操作符可以重载,malloc是一个库函数。
(6)申请数组时: new一次分配所有内存,多次调用构造函数,搭配使用delete,delete多次调用析构函数,销毁数组中的每个对象。而malloc则只能sizeof(int)
n。

虚函数和多态

多态的实现主要分为静态多态和动态多态,静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定。比如说,一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数。
虚函数的实现:在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。但是由于使用了虚函数,会增加访问内存开销,降低效率。

静态函数和虚函数

静态函数在编译的时候就已经确定运行时机;
虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销。

基类的析构函数

为什么析构函数必须是虚函数?为什么C++默认的析构函数不是虚函数?
将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。
C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。

C++中析构函数的作用

析构函数与构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数。
析构函数名也应与类名相同,只是在函数名前面加一个位取反符~,以区别于构造函数。它不能带任何参数,也没有返回值(包括void类型)。一个类只能有一个析构函数,不能重载。
如果用户没有编写析构函数,编译系统会自动生成一个析构函数(即使自定义了析构函数,编译器也总是会为我们合成一个析构函数,并且如果自定义了析构函数,编译器在执行时会先调用自定义的析构函数再调用合成的析构函数)。
如果一个类中有指针成员,而且在使用的过程中动态的申请了内存,那么最好显示构造析构函数在销毁类之前,释放掉申请的内存空间,避免内存泄漏。

map和set

map和set都是C++的关联容器,其底层实现都是红黑树(RB-Tree)。由于 map 和set所开放的各种操作接口,RB-tree 也都提供了,所以几乎所有的 map 和set的操作行为,都只是转调 RB-tree 的操作行为。
map和set区别在于:
(1)map中的元素是key-value(关键字—值)对:关键字起到索引的作用,值则表示与索引相关联的数据;Set与之相对就是关键字的简单集合,set中每个元素只包含一个关键字。
(2)set的迭代器是const的,不允许修改元素的值;map允许修改value,但不允许修改key。其原因是因为map和set是根据关键字排序来保证其有序性的,如果允许修改key的话,那么首先需要删除该键,然后调节平衡,再插入修改后的键值,调节平衡,如此一来,严重破坏了map和set的结构,导致iterator失效,不知道应该指向改变前的位置,还是指向改变后的位置。所以STL中将set的迭代器设置成const,不允许修改迭代器的值;而map的迭代器则不允许修改key值,允许修改value值。
(3)map支持下标操作,set不支持下标操作。map可以用key做下标,map的下标运算符[ ]将关键字作为下标去执行查找,如果关键字不存在,则插入一个具有该关键字和mapped_type类型默认值的元素至map中,因此下标运算符[ ]在map应用中需要慎用。如果find能解决需要,尽可能用find。

Vector 和list

Vector

(1)连续存储的容器,动态数组,在堆上分配空间。
(2)底层实现:数组
(3)两倍容量增长:vector 增加(插入)新元素时,如果未超过当时的容量,则还有剩余空间,那么直接添加到最后(插入指定位置),然后调整迭代器。如果没有剩余空间了,则会重新配置原有元素个数的两倍空间,然后将原空间元素通过复制的方式初始化新空间,再向新空间增加元素,最后析构并释放原空间,之前的迭代器会失效。
(4)性能:
访问:O(1)
插入:在最后插入(空间够)速度很快;在最后插入(空间不够)则需要内存申请和释放,以及对之前数据进行拷贝。
在中间插入(空间够)内存拷贝,在中间插入(空间不够)需要内存申请和释放,以及对之前数据进行拷贝。
删除:在最后删除速度很快,在中间删除则需要内存拷贝。
适用场景:经常随机访问,且不经常对非尾节点进行插入删除。

List

(1)动态链表,在堆上分配空间,每插入一个元数都会分配空间,每删除一个元素都会释放空间。
(2)底层:双向链表
(3)性能:
访问:随机访问性能很差,只能快速访问头尾节点;
插入:很快,一般是常数开销;
删除:很快,一般是常数开销;
适用场景:经常插入删除大量数据。
适用场景:经常插入删除大量数据

vector和list区别

1)vector底层实现是数组;list是双向 链表。
2)vector支持随机访问,list不支持。
3)vector是顺序内存,list不是。
4)vector在中间节点进行插入删除会导致内存拷贝,list不会。
5)vector一次性分配好内存,不够时才进行2倍扩容;list每次插入新节点都会进行内存申请。
6)vector随机访问性能好,插入删除性能差;list随机访问性能差,插入删除性能好。

vector和list应用

vector拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用vector。
list拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list。

STL

STL主要由:以下几部分组成:
容器、迭代器、仿函数、算法、分配器、配接器
他们之间的关系:
1)分配器给容器分配存储空间;
2)算法通过迭代器获取容器中的内容;
3)仿函数可以协助算法完成各种操作;
4)配接器用来套接适配仿函数。
在C++标准中,STL被组织为下面的13个头文件:

STL中迭代器的作用

迭代器

Iterator(迭代器)模式又称Cursor(游标)模式,用于提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。或者这样说可能更容易理解:Iterator模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的方法)访问聚合对象中的各个元素。
由于Iterator模式的以上特性:与聚合对象耦合,在一定程度上限制了它的广泛运用,一般仅用于底层聚合支持类,如STL的list、vector、stack等容器类及ostream_iterator等扩展iterator。

迭代器和指针的区别

迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,通过重载了指针的一些操作符,->、、++、—等。迭代器封装了指针,是一个“可遍历STL( Standard Template Library)容器内全部或部分元素”的对象, 本质是封装了原生指针,是指针概念的一种提升(lift),提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,—等操作。
迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用
取值后的值而不能直接输出其自身。

迭代器产生原因

Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。

C++源文件从文本到可执行文件经历的过程

对于C++源文件,从文本到可执行文件一般需要四个过程:
(1)预处理阶段:对源代码文件中文件的头文件、宏定义进行分析和替换,生成预编译文件;
(2)编译阶段:将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件;
(3)汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件;
(4)链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件。

include头文件

双引号和尖括号的区别:编译器预处理阶段查找头文件的路径不一样。
对于使用双引号包含的头文件,查找头文件路径的顺序为:
1)当前头文件目录;
2)编译器设置的头文件路径(编译器可使用-I显式指定搜索路径);
3)系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径。
对于使用尖括号包含的头文件,查找头文件的路径顺序为:
1)编译器设置的头文件路径(编译器可使用-I显式指定搜索路径);
2)系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径。

内存溢出和内存泄漏

内存溢出

指程序申请内存时,没有足够的内存供申请者使用。内存溢出就是你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误。
内存溢出原因:
(1)内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
(2)集合类中有对对象的引用,使用完后未清空,使得不能回收;
(3)代码中存在死循环或循环产生过多重复的对象实体
使用的第三方软件中的BUG;
(4)启动参数内存值设定的过小。

内存泄漏

在编写应用程序的时候,程序分配了一块内存,但已经不再持有引用这块内存的对象(通常是指针),虽然这些内存被分配出去,但是无法收回,将无法被其他的进程所使用,我们说这块内存泄漏了,被泄漏的内存将在整个程序声明周期内都不可使用。
主要原因:是在使用new或malloc动态分配堆上的内存空间,而并未使用delete或free及时释放掉内存。
内存泄漏情况:
(1)不匹配使用new[] 和 delete[];
(2)delet void * 的指针,导致没有调用到对象的析构函数,析构的所有清理工作都没有去执行从而导致内存的泄露;
(3)没有将基类的析构函数定义为虚函数,当基类的指针指向子类时,delete该对象时,不会调用子类的析构函数。

C++内存管理

在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。
(1)代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
(2)数据段:存储程序中已初始化的全局变量和静态变量
(3)bss 段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。
(4)堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。
(5)映射区:存储动态链接库以及调用mmap函数进行的文件映射。
(6)栈:使用栈空间存储函数的返回地址、参数、局部变量、返回值。

C++11新特性

C++11 最常用的新特性如下:
(1)auto关键字:编译器可以根据初始值自动推导出类型。但是不能用于函数传参以及数组类型的推导;
(2)nullptr关键字:nullptr是一种特殊类型的字面值,它可以被转换成任意其它的指针类型;而NULL一般被宏定义为0,在遇到重载时可能会出现问题。
(3)智能指针:C++11新增了std::shared_ptr、std::weak_ptr等类型的智能指针,用于解决内存管理的问题。
(4)初始化列表:使用初始化列表来对类进行初始化。
(5)右值引用:基于右值引用可以实现移动语义和完美转发,消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
(6)atomic原子操作用于多线程资源互斥操作。
(7)新增STL容器array以及tuple。

右值引用

右值引用是C++11中引入的新特性 , 它实现了转移语义和精确传递。它的主要目的有两个方面:
1)消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
2)能够更简洁明确地定义泛型函数。

左值和右值的概念:
左值:能对表达式取地址、或具名对象/变量。一般指表达式结束后依然存在的持久对象。
右值:不能对表达式取地址,或匿名对象。一般指表达式结束就不再存在的临时对象。

LRU缓存

LRU缓存是一种以LRU策略(距离当前最久没使用过的数据应该被淘汰)为缓存策略的缓存。
而所谓的缓存策略,就是当缓存满了之后,又有新数据需要加入到缓存中时,我们怎么从缓存中删除旧数据为新数据腾出空间的策略。
LRU,Least Recently Used的简写,即近期最少使用算法。该算法依据于程序的局部性原理, 其淘汰旧数据的策略是,距离当前最久没有被访问过的数据应该被淘汰。

实现原理

实现LRU的数据结构设计:unordered_map + double linked list。
(1)维护一个双向链表,该链表将缓存中的数据块按访问时间从新到旧排列起来(由于双向链表节点的交换代价很低,所以使用双向链表作为主要数据结构),节点为包含key,value的结构体(一条记录);
(2)使用哈希表(map)保证缓存中数据的访问速度(由于引入哈希表可以提高查询速度,所以使用哈希表作为辅助数据结构)。map中的一个元素包含键值key以及链表中键值为key的迭代器(指针),通过key查找记录的地址,即可O(1)时间内访问链表中访问的记录。
接口描述
int get(int key)
功能:在哈希表中查找键值为key的元素,如果不存在返回-1;如果存在返回该key对应的value值.
实现:
这里说存在key的情况,如何get:
step1: 将键值为key的记录与链表首元交换位置;
step2: 更新哈希表中键值为key的迭代器
void put(int key, int value)
将key,value这条记录放入缓存,如果该记录已经在缓存中,更新该记录到缓存链表头部;如果不在缓存中且缓存未满,插入缓存链表头部,如果缓存满,删除尾部数据。

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
class LRUCache {
private:
typedef int key_t;
typedef int value_t;
typedef struct{
key_t key;
value_t value;
} Node_t;
typedef list<Node_t> cacheList_t;
typedef map<key_t,cacheList_t::iterator> map_t;

int m_capacity;
cacheList_t m_cacheList;
map_t m_mp;

public:
LRUCache(int capacity) : m_capacity(capacity){

}

int get(int key) {
auto it = m_mp.find(key);
// not cached
if(it == m_mp.end()) return -1;
// cached
else{
auto list_it = m_mp[key];
Node_t node = {key,list_it->value};
m_cacheList.erase(list_it);
m_cacheList.push_front(node);
m_mp[key] = m_cacheList.begin();
return m_cacheList.begin()->value;
}
}

void put(int key, int value) {
auto it = m_mp.find(key);
// cached
if(it != m_mp.end()){
auto listIt = m_mp[key];
// delete the cached node, and then insert it to the list head
Node_t node = {key, value};
m_cacheList.erase(listIt);
m_cacheList.push_front(node);
m_mp[key] = m_cacheList.begin();

}
// not cached
else{
// cache is full
if(m_cacheList.size() == m_capacity){
m_mp.erase(m_cacheList.back().key);
m_cacheList.pop_back();
}
// cache is not full
Node_t node = {key,value};
m_cacheList.push_front(node);
m_mp[key] = m_cacheList.begin();

}

}
};

C++_inline函数(内嵌函数)

在上下文切换中过程中需要一定的时间和空间开销(保护现场和恢复现场),C++提供了一种更高效的方法,即在编译时将所调用函数的代码直接嵌入到主调函数中,而不是将流程转出去。这种嵌入到主调函数中的函数称为内嵌函数,用inline 声明。
如:inline int max(int a, int b);
如果在类体中定义的成员函数(注:需在类体中定义函数体)中不包括循环等控制结构,C++系统自动的对它们作为内嵌函数来处理,无需显式的声明。
如果成员函数不在类体内定义,而在类体外定义,系统并不把它默认为内嵌函数,此时需作显式的声明。

腾讯实习工作总结

AI 在视频生产流程里的应用。首先是是智能拆条,视频进来之后,把片头、片尾和广告去掉,其次是标注,就像把所有的商品打上标签一样,根据这个标签进行推荐和搜索。然后是审核,过滤盗版、情色等内容。最后是播放处理,比如自动生成封面和动态海报图,帮助对剪辑不熟悉的自媒体用户选取魅力封面图,也提高了海量视频的分发效率。
视频标注为视频打上时间 - 语义标签,是 AI 视频理解的关键。标签有不同类别、层次和粒度,例如识别不同人物、行为、场景、物品等等,还可以用于对内容和用户的精细化分析,例如年龄、表情、服装款式,手机品牌等,更好地进行搜索推荐,实现精准的内容分发。
AI 应用在文字识别,可以对字幕进行文字识别和语义理解,对审核图片、视频中的不合规文字信息进行识别和反垃圾过滤。

视频分类

广电场景下的海量短视频剧名分类:在算法流程中,我们首先把视频的图片进行解码,凑齐N帧,保证每个视频都有相同的采样帧,然后采用resnet网络提取视觉特征,采用NetVlad提取融合时间特征,最后融合多张图片得到视频的分类结果。
我们在总共有957万个短视频,只取出现频次在50-3000类别的短视频总共有552万,剧名类别有8740种,对其进行训练,分类的top1的准确率只到0.18。因此,我们考虑从关键帧和多级分类器方面提高算法的准召率。
抽取关键帧的思路也很简单:我们知将两帧图像进行差分,得到图像的平均像素强度可以用来衡量两帧图像的变化大小。因此,基于帧间差分的平均强度,每当视频中的某一帧与前一阵画面内容产生大的变化,我们便认为它是关键帧,并将其提取出来。
我们可以使用如下三种方法中的一种来提取关键帧,这都是基于帧间差分的:
(1)使用差分强度顺序:我们对所有帧按照平均帧间差分强度进行排序,选择平均帧间差分强度最大的若干张图片作为视频的关键帧。
(2)使用差分强度阈值:我们悬着平均帧间差分强度高于预设阈值的帧作为视频的关键帧。
(3)使用局部最大值:我们选择具有平均帧间差分强度局部最大值作为视频的关键帧。这种方法的提取结果在丰富度上表现更好,提取结果均匀分散在视频中。
在抽帧方面,每个视频里的关键帧数目不同,如果一个视频只有四帧,我们如何取八帧呢?有两个方法,第一个办法是再补尾帧,后面重复实现。还有一个办法是均匀插帧,经评测,均匀补帧比插尾帧效果好很多,因为插尾帧的时候,相当于把最后第四帧插了很多帧无效的信息。如果第四帧是黑帧,或者是无意义的帧,它会带来负面的影响。
除了在每个短视频中提取关键帧获取视频更具判别性的视觉特征外,在全类目的视频分类效果差的原因在于,每个视频的内容、风格差异非常大,从历史题材到都市生活、从科幻大片到动漫题材,模型很难去拟合出有效的高层语义信息对剧名进行分类。因此,我们考虑训练多级分类器,为每个题材的内容训练对应的分类模型,能有效提高模型的分类效果。我们首先利用每个视频的一级标签(电影、电视剧、动漫、综艺、体育、军事、纪录片、游戏)等8个一级类目训练一级分类器,然后对每个类目下的视频分别训练各自的剧名分类器模型。即网络会先判断该短视频是属于电影、电视剧类别,然后判断该短视频的剧名。在实验数据集中,一级分类器的准召率为0.82/0.71,但在看点、企鹅号和ugc数据中,模型分类的准召率分别为0.6/0.46,0.57/0.51以及0.47/0.5。而二级的剧名分类像电影、电视剧的准召率为0.86/0.79、0.75/0.54,而像军事、体育的准召率可达0.96/0.94。对实验结果进行分析,我们可以看到,每个像一些内容单一、题材近似的视频类别,如军事、体育等单独训练的分类模型具有较高的准确性,因此通过多级分类器对视频剧名进行分类方案是可行的。但是我们认为简单的将剧名分为8个一级标签,一级分类器的准召率一般,且模型的泛化性不行。我们认为是由于像电影、电视剧这种标签类别过于宽泛,视频题材丰富,模型很难去拟合有效的特征。
因此,我们最终考虑采用视频多标签技术,对每个视频提取语义标签,然后通过聚类和人工筛选,提取了近20个的二级标签(都市生活、娱乐综艺、历史文化、科幻战争等),然后通过这些类别标签训练多级分类器,在全类目的剧名分类模型中,算法的最终准召率为0.58/0.52,分类效果得到有效的提升。

NetVlad模型

VLAD(Vector of locally aggregated descriptors,局部聚合向量)是一种类似于BOF(bag-of-feature)的描述图像的特征方法,可以理解是将局部特征表示成全局特征的编码方法。在这之前,BoF(词袋模型)通常用来描述基于Sift局部特征,在图像检索和分类模型中有着广泛的应用。相比于BoF,VLAD更加能够对于图像的准确表达,得到更具有判别力的特征,并且便于降维(PCA),同时降维对于准确率的影响也较小。
NetVLAD是将传统的VLAD结构嵌入到CNN网络结构中,得到一个新的VLAD层。NetVLAD可以很容易的运用到任何的CNN结构中,并且可以使用反向传播进行优化。局部聚合向量(VLAD)能够抓取图像中局部特征在图像中聚合的统计信息,视觉词袋聚合是记录每个词的数目,而VLAD是记录每个词的残差和。
给定$N$个D维局部图像描述子$x_i$作为输入,$K$个聚类中心作为$c_k$作为VLAD的参数,VLAD的输出是一个$K\times D$的矩阵,但是这个矩阵被转换成向量表示,然后再进行归一化,计算公式如下:

其中,$xi(j)$和$c_k(j)$表示是第$i$个局部描述子和第$k$个聚类中心的第$j$个特征值。$a_k(X_i)$我们可以简单理解为第i个局部特征属于第k个聚类的权重,也就是说,如果它的值等于1就说明它属于这个聚类的簇。如果是传统的VLAD,那么这个值只能是1或0.直观上看,$V$表征着所有局部特征在每一个聚类簇上的残差$(x_i-c_k)$和。
在传统的VLAD中,由于$a_k(X_i)$是一个不连续的值,取值为1或0,并且满足$\sum
{i=1}^Ka_k(X_i)=1$,使其不能进行反向传播。而NetVLAD采用一种近似的方式,来对$a_k(X_i)$来做软分配(soft assignment)如下式:

这个权重的分配可以把它当做一个模糊聚类的分配方式,根据每个局部特征到聚类中心的距离来产生一个概率函数权重。对于一个局部特征描述$X_i$在每个聚类簇下的权重的范围在0~1之间,权重最高的可以理解为该特征离聚类簇中心的聚类最近,权重低说明其离簇中心较远。我们将上式进行平方展开,可得到VLAD特征向量为:

NetVLAD通过这种在不同聚类簇上的软分配(soft-assignment)方式,能够有效的聚合了局部特征空间中不同部分(聚类)的一阶残差的统计量。另外我们可以注意到,NetVLAD中包含着三个参数,$W_k$,$b_k$和$c_k$,原始VLAD中只有一个参数$c_k$,这使得NetVLAD相对于传统的方法更具有灵活性,并且所有的参数在特定的任务下可以通过端到端的方式来学习。
由图可看出,NetVLAD层可以直接接在卷积网络中的最后一层$(H\times W\times D)$上,把最后一层的特征图看作$N$个密集的$D$维局部描述子。soft-assignment过程可分为两个步骤:1)通过K个滤波器$W_k$将$N\times D$矩阵来学到$s_k(X_i)=W_k^TX_i+b_k$;2)卷积输出然后经过一个soft-max函数来生成$\overline{a_k}(X_i)$。

视频多标签

视频多标签是为视频标注出语义标签,是视频理解的关键,但在视频多标签领域仍存在很多问题和难点。我们先看一下视频多标签实验的数据集分布,在总共2000万个短视频中,标签出现频次大于100的标签类别有18916种。在这里我列举了部分标签及其出现的频次,我们可以看到标签的分布及其不均衡,像内地综艺出现的频次高达2194775次,而像“芒果讲”、“行车视线”出现的频次只有100次。还有一些标签,如“创业”、“聊天”、“恶搞”标签定义的过于抽象,模型很难学习这类标签的特征。当然,标签中还存在一些标签语义非常近似,如“内地综艺”和“综艺片段”,“宝宝秀”和“天真萌娃”,这类标签问题和数据标注有很大关系,我们暂且不考虑。为了更直观的看到标签的分布情况,我统计了标签出现的频次发现,在标签出现频次大于5000的类别,只占所有类别总数的10%,但这部分标签出现频次之和占所有标签出现频次之和的84%。在标签频次小于500的标签类别占所有标签类别的56%,但由于每个标签出现的频次较少,该部分标签频次之和只占所有标签频次和的3%。标签的类别严重不均衡,使得模型在训练的过程中更容易去拟合频次出现过的标签的特征,而很难去学习绝大部分的出现频次较少的标签特征。面对这个问题,我们决定重构模型的loss函数,使得标签能够去学习那些少量样本的特征。我们认为,模型无法有效地学习标签之间的特征,主要在于标签的正负样本的之间差距过大,以及每个标签之间的相对的差距过大导致的。因此,我们要调整每个标签的正负样本之间的权重,调整每个标签之间相对的权重,通过均衡每个样本在参数更新过程中的贡献来训练更好的模型。我们设负样本数为$N_2$,正样本数为$P_2$,负样本权重为$W_2^n$,正样本权重为$W_2^p$,标签整体的样本权重为$W_2$,其中$W_2^p=1$,则

对于正负样本间的权重,我们认为负样本数超过正样本数一定阈值,我们就将其倒数乘上相应阈值作为正负样本的权重;对于每个样本间的权重,为了简化计算,我们选择和最小频次的样本进行比较,当然为了防止最小频次的标签的样本数过小,拉低了标签的整体贡献,我们设置最小频次的标签样本数为1000。每个标签的调整的负样本数和正样本数之和就是该标签对在loss计算中的贡献,将其和最小的频次标签的贡献进行对比,就可以得到标签的整体分布权重$W_2$,则损失函数:

我们绘制了每个标签调整后的频次占比,发现大于5000的频次占比为42%,小于500的标签占比为28%,标签分布得到有效的均衡。
基于梯度均衡的损失函数,是2019年AAAI的论文提出的思想,作者认为影响单阶段检测器训练的本质问题,其根本原因是由于不同难度的样本的分布不均衡导致。我们随机统计了一批不同标签的训练样本中的梯度模长,发现大部分样本是十分容易预测的,这些可被准确预测的样本所占的比重也很大,而梯度g接近于1的样本的比例也相对较大,我们认为这是一些离群样本,可能是由于数据标注本身不准确或样本比较特殊难学习造成。对于一个已收敛的模型,强行学习这些离群样本可能会导致模型参数的较大偏差,反而影响模型的准确率。我们利用论文中提出的梯度均衡的概念,即根据样本模长分布的比例对不同样本产生的梯度进行加权,使得各类型的样本对模型参数有更均衡的贡献,而这种加权在损失函数上也可以达到同样的效果。在重构的loss函数中,我们将梯度模长的取值范围划分为若干区域,统计梯度模长g落在每个区域内的样本数量,而密度就是其所在的单位区域内的样本数量除以该单位区域的长度,而梯度密度的倒数就是样本计算loss要乘的权重。不过在多分类任务中,类别标签的onehot编码中,只有属于该类别的位置值为1,其余的均为0。因此在每个batch中计算梯度密度是只需要统计label为1的那个类别的梯度。而在多标签分类中,onehot编码的每个位置都是一个类别的二分类,label为0或1对loss均有影响,因此我们需要分别取统计每个类别的梯度分布,计算每个类别的权重去调整loss函数中每个标签的贡献。
原始的视频多标签的mAP指标为0.2306,添加权重分布的loss函数策略后,mAP指标为0.2742;添家梯度均衡策略的mAP指标为0.2676,最终的mAP指标为0.3284。

mAP的计算

voc2010的AP计算方法是:假设N个样本中有M个正例,那么我们会得到M个recall值(1/M,2/M,…,M/M),对于每个recall值r,我们可以计算出对应(r’>r)的最大precision,然后对这M个precision值取平均即得到最后的AP值。
把recall当场横坐标,precision当场纵坐标,即可得到常用的precision-recall曲线,曲线下的面积也是AP值。
AP衡量的是学出来的模型在每个类别上的好坏,mAP衡量的是学出的模型在所有类别上的好坏,得到AP后,取所有AP的平均值就是mAP。

片段层标题提取

OCR检测器

PSENet是一种基于语义分割的方法检测任意方向的文本,采用渐进式尺度扩展的方法区分邻近的文本块。
网络模型
网络结构类似于FPN的形式,先采用CNN提取四层不同level的feature map,分别是$P_2,P_3,P_4,P_5$,解决文本块尺度变换剧烈的问题,early-stage可用于预测小的文本块,late-stage可用于预测大的文本块,然后通过2倍上采样进行concate来融合特征,得到最后的特征图F。特征图F送入$3\times 3$大小的卷积中输出通道数为256的特征图,将次特征图再送入到$1\times 1$大小卷积层输出n个最终结果,这n个结果用$S_1,S_2,…,S_n$表示,最后通过渐进的尺度扩展算法PSE进一步得到最终的文字检测结果。这里的$S_1,S_2,…,S_n$, $S_i$是图像分割的文字检测框结果,不同之处在于每个结果对应的文字区域大小不一样。如$S_1$对应最小文字分割的结果,$S_n$是最大的文字分割的结果。
渐进式尺度扩展算法
渐进尺度扩展算法(PSE):首先看$S_1$,图中有四个不同的分割区域($C_1,C_2,C_3,C_4$),通过CC(CC是一个寻找连接区域的函数,就是给不同的pixel设置不同的label)将不同分割区域合并得到图b(四个连通域使用不同颜色标记,不同文本行之间的margin很大,很容易区分开),然后合并$S_2$中像素,将属于$S_2$在的kernel但不属于$S_1$中的kernel的像素点分配,将b图所找到的连通域的每个pixel以BFS的方式,逐个上下左右扩展,即相当于把$S_1$中预测的文本行的区域逐渐边框(简单来说,就是讲$S_2$中的每个像素点都分别分配给$S_1$中的某个连通域)。重复上述过程,知道发现最大的核作为预测结果。

OCR识别器

现今基于深度学习的端到端OCR技术有两个主流技术:CRNN OCR和attention OCR。其实这两个方法主要区别在于最后的输出层,即怎么将网络学习到的序列特征信息转化为最终的识别结果。这两大主流技术在其特征学习阶段都采用CNN+RNN的网络结构,CRNN OCR在对齐时采取的方式是CTC算法,,而attention OCR采取的方式则是attention。
网络模型
网络结构包含三部分,从上到下依次为:
(1)卷积层,使用CNN,作用是从输入图像中提取特征序列;
(2)循环层,使用RNN,作用是预测从卷积层获取的特征序列的标签分布;
(3)转录层,使用CTC,作用是从循环层获取的标签分布通过去重整合等操作转换成最终的识别结果。
端到端OCR的难点在哪儿呢?在于怎么处理不定长序列对齐问题!CRNN算法输入100*32归一化高度的词条图像,基于7层CNN(普遍使用VGG16)提取特征图,把特征图按列切分(Map-to-Sequence),每一列的512维特征,输入到两层各256单元的双向LSTM进行分类。在训练过程中,通过CTC损失函数的指导,实现字符位置与类标的近似软对齐。LSTM有256个隐藏节点,经过LSTM后变为长度为T × nclass的向量,再经过softmax处理,列向量每个元素代表对应的字符预测概率,最后再将这个T的预测结果去冗余合并成一个完整识别结果即可。
CRNN算法最大的贡献,是把CNN做图像特征工程的潜力与LSTM做序列化识别的潜力,进行结合。它既提取了鲁棒特征,又通过序列识别避免了传统算法中难度极高的单字符切分与单字符识别,同时序列化识别也嵌入时序依赖(隐含利用语料)。
我们现在要将 RNN 输出的序列翻译成最终的识别结果,RNN进行时序分类时,不可避免地会出现很多冗余信息,比如一个字母被连续识别两次,这就需要一套去冗余机制。

比如我们要识别上面这个文本,其中 RNN 中有 5 个时间步,理想情况下 t0, t1, t2 时刻都应映射为“a”,t3, t4 时刻都应映射为“b”,然后将这些字符序列连接起来得到“aaabb”,我们再将连续重复的字符合并成一个,那么最终结果为“ab”。
这似乎是个比较好的方法,但是存在一个问题,如果是book,hello之类的词,合并连续字符后就会得到 bok 和 helo,这显然不行,所以 CTC 有一个blank机制来解决这个问题。
我们以“-”符号代表blank,RNN 输出序列时,在文本标签中的重复的字符之间插入一个“-”,比如输出序列为“bbooo-ookk”,则最后将被映射为“book”,即有blank字符隔开的话,连续相同字符就不进行合并。即对字符序列先删除连续重复字符,然后从路径中删除所有“-”字符,这个称为解码过程,而编码则是由神经网络来实现。引入blank机制,我们就可以很好地解决重复字符的问题。

标题提取

对于一段新闻视频,由于每个标题出现的位置固定,因此我们1.0版本的算法中采用规则筛选来提取标题。我们首先采用OCR检测器去获取每一帧图片中文本的位置,通过一些位置规则去除一些文本框;然后利用OCR识别器获取剩下的文本框中的文本信息。在1.0版本的算法中,我们会在前5帧中去搜索视频的四个角的文本信息,通过正则匹配查找电视的台标。然后根据台标在配置文件中找到每个电视的配置信息。基于规则的文本筛选主要有,(1)标题出现在视频帧的区域位置,如在横轴方向的0.1-0.9区间,在纵轴方向的0.67-0.9区间;(2)在可能的区间中先搜索文本框最大的文本作为可能的标题,然后通过该文本块搜索临近的文本块,拼接成段落。我们还会通过一些内容匹配,如“新闻联播”、“记者”、“编辑”等文字的正则匹配过滤无效的文本。在配置文件中,我们对每个电视台的阈值进行调整,通过这些规则过滤后剩下的文本内容就是新闻标题。提测反馈结果,基于规则筛选的标题提取对于符合规则的视频可能有很好的识别效果,如“央视新闻”,但在卫视新闻中,对于配置文件中没有的电视台,默认的配置需求可能满足,导致无法识别正确的标题。同时,由于该版本算法没有对文本的内容进行过滤,经常讲记者姓名、插播广告等识别为标题。面对这种情况,我们在2.0版本的算法中提出基于内容过滤的新闻标题提取算法。虽然基于规则的算法在某些电视台能很好的识别出标题,但是这些规则也严格限制了算法性能。我们考虑能否利用少量的规则甚至不利用规则就能很好的识别出新闻标题,因此,我们利用NLP中的bert模型+FC层进行finetune,训练文本二分类模型,并通过sigmoid函数的阈值划分,过滤那些非标题的文本。我们将每个OCR识别文本送入到二分类模型进行过滤,这样的做法的有点在于可以过滤掉很大部分的受访者信息和记者姓名等文本,而且算法没有严格的规则限制,鲁棒性好。当然,文本分类器也可能会将一部分正确的标题文本过滤,甚至对于段落文本,由于逐行的过滤可能导致标题不完整或没有标题。因此我们考虑结合部分宽松条件的规则和文本分类器进行更好的标题提取。先阶段的算法的在自己的100段测试视频中的准召率为0.95和0.91,相对于1.0版本的算法有很大的提升。

什么是GAN

GAN是一种由生成网络和判别网络组成的深度神经网络架构。通过在生成和判别之间多次循环,两个网络相互对抗,试图胜过对方,从而训练了彼此。

生成网络

生成网络使用现有数据生成新数据,比如使用现有图像来生成新图像。生成网络的核心任务是从随机生成的由数字构成的向量(称为“潜在空间”, latent space)中生成数据(比如图像、视频、音频或文本)。在构建生成网络时需要明确该网络的目标,例如生成图像、文本、音频、视频,等等。

判别网络

判别网络试图区分真实数据和由生成网络生成的数据。对于输入的数据,判别网络需要基于事先定义的类别对其分类。这可能是多分类或二分类。通常,GAN 中进行的是二分类。

GAN中重要的概念

KL散度

KL 散度,也称相对熵,用于判定两个概率分布之间的相似度。它可以测量一个概率分布 p相对于另一个概率分布 q 的偏离。

如果 p(x)和 q(x)处处相等,则此时 KL 散度为 0,达到最小值。
由于 KL 散度具有不对称性,因此不用于测量两个概率分布之间的距离,因此也不用作距离的度量(metric)。

JS散度

JS 散度,也称信息半径(information radius, IRaD)或者平均值总偏离(total divergence to the average),是测量两个概率分布之间相似度的另一种方法。它基于 KL 散度,但具有对称性,可用于测量两个概率分布之间的距离。对 JS 散度开平方即可得到 JS 距离, 所以它是一种距离度量。
计算两个概率分布p和q之间JS散度的公式如下。

其中,$frac{p+q}{2}$是p和q的中点测度,$D_{KL}$是KL散度。

纳什均衡

博弈论中的纳什均衡描述了一种在非合作博弈中可以达到的特殊状态。其中每个参与者都试图基于对其他参与者行为的预判,选择使自己获益最多的最佳策略。最终形成的局面是,所有参与者都基于其他参与者的选择,采取了对自己来说最佳的策略,此时已经无法通过改变策略获益了。这种状态就称为纳什均衡。

目标函数

为了使生成网络生成的图像能以假乱真,应尽量提高生成网络所生成数据和真实数据之间的相似度。可使用目标函数测量这种相似度。生成网络和判别网络各有目标函数,训练过程中也分别试图最小化各自的目标函数。 GAN 最终的目标函数如下所示。

其中, D(x)是判别网络模型, G(z)是生成网络模型, p(x)是真实数据分布, p(z)是生成网络生成的数据分布, E 是期望输出。
在训练过程中, D(判别网络, discriminator)试图最大化公式的最终取值,而 G(生成网络,generator)试图最小化该值。如此训练出来的 GAN 中,生成网络和判别网络之间会达到一种平衡,此时模型即“收敛”了。这种平衡状态就是纳什均衡。训练完成之后,就得到了一个可以生成逼真图像的生成网络。

评分算法

GAN 的目标函数不是均方误差(mean-square error)或者交叉熵(cross entropy)这样确定的函数,而是在训练过程中习得的。研究者们提出了多种可以测量模型准确度的评分算法,下面介绍其中几个。

Inception分数

Inception 分数(IS)是应用最广泛的 GAN 评分算法。它使用一个在 Imagenet 上预训练过的Inception V3 网络分别提取真实图像和生成图像的特征。IS 测量生成图片的质量和多样性。计算 IS 的公式如下。

其中,$p_g$表示一个概率分布,$\chi \sim p_g$表示$\chi$是该概率分布中的一个抽样。$p(y|\chi)$是条件类别分布,$p(y)$是边缘类别分布。
计算Inception分数的步骤如下:
1)首先从模型生成的图像中抽取N个样本,记为($\chi^i$)。
2)然后使用如下公式构建边缘类别分布。

3)接着使用如下公式计算KL散度以及期望值。
$IS(G)=exp(E{\chi \sim p_g}D{KL}(p(y|\chi)||p(y)))$
4)最后计算上述结果的指数,即可得到IS。
IS 越高,说明模型质量越好。 IS 虽然是重要的测度(measure),却也存在一些问题。比如模型对于每个类别只生成一张图像,其 IS 仍然可以很高,但这样的模型缺乏多样性。

GAN的优势

1)GAN 是无监督学习方法。带标注数据需要人工制作,非常耗时。 GAN 不需要带标注数据,而可以通过无标注数据进行训练,学习数据的内在表现形式。
2)GAN 可以生成数据。 GAN 可以生成能跟真实数据媲美的数据,应用潜力巨大。 GAN 可以生成图像、文本、音频和视频等,并且和真实数据相差无几。用 GAN 生成图像可应用于市场营销、电子商务、游戏、广告等很多行业。
3)GAN 可以学习数据的概率密度分布。 GAN 可以学习数据的内在表现形式。前面提到了GAN 可以学习混乱而复杂的数据概率分布,有助于解决机器学习领域的很多问题。
4)训练后的判别网络是分类器。 GAN 训练完成之后会得到一个判别网络和一个生成网络,而判别网络可用作分类器。

训练GAN的问题

GAN 也存在一些问题。这些问题通常与训练过程有关,包括模式塌陷、内部协变量转移以及梯度消失等。

模式塌陷

模式塌陷问题指的是生成网络所生成的样本之间差异不大,有时甚至始终只生成同样的图像。有一些概率分布是多峰的(multimodal),构造十分复杂。数据可能是通过不同类型的观测得来的,因此样本中可能会暗含一些细类,每个细类下的样本之间比较相似。这样会导致数据的概率分布出现多个“峰”,每个峰对应一个细类。如果数据的概率分布是多峰的, GAN 有时就会出现模式塌陷问题,无法成功构建模型。如果生成的所有样本几乎都相同,这种情况就被称为“完全塌陷”。
解决模式坍塌问题有多种方法,例如:
1)针对不同的峰训练不同的GAN模型;
2)使用多样化的数据训练GAN。

内部协变量转移

内部协变量转移问题之所以产生,是因为神经网络输入数据的概率分布发生了变化。输入数据的概率分布改变之后,隐藏层会试图适应新的概率分布,训练速度因此放缓,需要很长时间才会收敛到全局最小值。神经网络输入数据的概率分布和该网络之前接触的数据概率分布之间差异过大是问题根源。解决方法包括批归一化以及其他归一化技术。

Aibee(2020.03.09)

一面(电话面):

项目介绍

二面(视频面):

1)介绍论文
论文研究的方向是基于骨骼关键点的人体行为识别,现在的算法采用分层图卷积网络聚集关节更宽范围内邻域的特征,这会减弱局部邻域的信息,针对这种现象首先提出一个残差图卷积操作增强节点的局部领域信息,接着利用密集连接重用不同模块间的上下文时空特征来增加节点的全局和局部特征。在重用特征的过程中会带来冗余信息的干扰,利用通道注意模块计算不同通道间的特征相关性,增强有用特征,抑制无关信息。同时在行为中,并不是所有的帧和关节都对行为识别有用,冗余的帧信息会带来识别的干扰,因此进一步采用注意模块来增强关键帧和关键节点的特征。
2)什么是图卷积网络
卷积是通过计算中心像素点以及相邻像素点的加权和来构成feature map实现空间特征提取。
3)反向传播了解吗,代入L2正则化
4)算法题
找到数组中第k大的数
两个有序数组找中位数

快手(2020.03.11)

一面

1) 最熟悉的项目,重点介绍算法模块
为什么采用openpose?动作分类的评价指标是什么?动作的相似度是怎么判断的?
2) 论文介绍
残差图操作是什么?注意模块怎么实现?邻接矩阵是什么?
3) 基础题
防止网络过拟合的方法有哪些?
BN层原理?具体是怎么实现的?优缺点?
网络压缩中Depthwise卷积核正常卷积差别,减少多少计算量。
SVD和PCA原理
4) 算法题
最大连续子序列和

二面

1) 比较深入和有创新的项目?
动态时间规划在项目中怎么实现
关键点的相似度是怎么计算的?
2) 论文
3) 基础题
分类网络能用mse吗?为什么
4) 算法题
股票问题,只能买卖一次

头条

一面

算法题
1)找到字符串的最长无重复字符子串
2)整数对查找
请设计一个高效算法,找出数组中两数之和为指定值的所有整数对。

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
class FindPair:
def countPairs(self, A, n, tsum):
# write code here
from collections import defaultdict
if not A or len(A)==1:
return 0
dic = defaultdict(int)
ans = 0
setA = list(set(A))
setA.sort()
left, right = 0, len(setA)-1
for num in A:
dic[num] += 1
while left<right:
if setA[left]+setA[right]<tsum:
left += 1
elif setA[left]+setA[right]>tsum:
right -= 1
else:
ans += dic[setA[left]]*dic[setA[right]]
left += 1
right -= 1
if setA[left]*2==tsum:
ans += dic[setA[left]]*(dic[setA[left]]-1)//2
return ans

基础题
知道set和字典的低层实现吗?
在Python中,字典是通过散列表(哈希表)实现的。字典也叫哈希数组或关联数组,所以其本质是数组(如下图),每个 bucket 有两部分:一个是键对象的引用,一个是值对象的引用。
了解哈希表吗?哈希冲突是怎么解决的
哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
解决哈希冲突的方法一般有:开放定址法、链地址法(拉链法)、再哈希法、建立公共溢出区等方法。
开放定址法:从发生冲突的那个单元起,按照一定的次序,从哈希表中找到一个空闲的单元。然后把发生冲突的元素存入到该单元的一种方法。开放定址法需要的表长度要大于等于所需要存放的元素。
在开放定址法中解决冲突的方法有:线行探查法、平方探查法、双散列函数探查法。
链接地址法的思路是将哈希值相同的元素构成一个同义词的单链表,并将单链表的头指针存放在哈希表的第i个单元中,查找、插入和删除主要在同义词链表中进行。链表法适用于经常进行插入和删除的情况。
再哈希法,同时构造多个不同的哈希函数:Hi = RHi(key) i= 1,2,3 … k;当H1 = RH1(key) 发生冲突时,再用H2 = RH2(key) 进行计算,直到冲突不再产生,这种方法不易产生聚集,但是增加了计算时间。
建立公共溢出区:将哈希表分为公共表和溢出表,当溢出发生时,将所有溢出数据统一放到溢出区。
项目介绍
你能介绍一下注意机制吗?
你能介绍一下ResNet机制吗?
梯度消失是什么原因?如何解决梯度消失?
激活函数relu为什么可以解决梯度消失?

阿里

一面 20200408

项目介绍
算法的评估模块是如何实现的?
每一帧的相似度是如何计算的?采用的欧式距离是计算哪些对象的空间距离?
不同的姿态和不同的角度是如何处理的?
不同远近,不同角度、大人和小孩等进行测试的时候出现什么具体的问题,如何解决的,问题的指标是哪些?
用户的动作时间不匹配是如何解决的?
通信机制的设计和解决方案的目的是什么,为什么这么设计?
消息中间件还有哪些?其功能是什么?
服务器的系统架构是怎样的?分布式系统要如何部署?多服务器多人同时在线该如何解决?
hashset的原理及内部实现的细节?
HashSet实现自set接口,set集合中元素无序且不能重复。
因为HashSet底层是基于HashMap实现的,当你new一个HashSet时候,实际上是new了一个map,执行add方法时,实际上调用map的put方法,value始终是PRESENT,所以根据HashMap的一个特性: 将一个key-value对放入HashMap中时,首先根据key的hashCode()返回值决定该Entry的存储位置,如果两个key的hash值相同,那么它们的存储位置相同。如果这个两个key的equalus比较返回true。那么新添加的Entry的value会覆盖原来的Entry的value,key不会覆盖。因此,如果向HashSet中添加一个已经存在的元素,新添加的集合元素不会覆盖原来已有的集合元素。

二面

一个1亿行的文本文件(文件内容为人民日报真实语料),每行有大约100个汉字。计算如下:
1)计算每个汉字出现次数
2)计算出现频率最高的连续10个字符(包含标点符号)
限制:
1)使用多线程实现
2)系统资源:4core,8g内存
3)指标1需要完整代码,指标2计算为开放性问题,伪代码描述思路即可。
4)使用Java实现

腾讯优图

一面

算法题:快排、最长公共子序列、c++创建和删除二维数组
常见的激活函数?
ResNet和DenseNet的区别?
过拟合解决的方法?
Dropout的原理?
R-CNN的实现原理?
Softmax反向传播的推导?
项目介绍。

微软

一面

项目介绍
你知道哪些距离计算方法?为什么采用欧式距离来评估?
比赛的评价指标是什么?是比赛的mAP?mAP是如何计算的?其原理是什么?
什么是NMS?什么是softer NMS?
算法题:
第K大的数?时间复杂度是多少?
两个很大的文件,每一行是url地址,找到一个url地址既在A文件又在B文件中?
在操作系统中,进程和线程的区别?
在python语言中,什么时候会用多进程,什么时候会用多线程?

第三章 深度学习基础

3.1 基本概念

3.1.1 神经网络组成?

神经网络类型众多,其中最为重要的是多层感知机。为了详细地描述神经网络,我们先从最简单的神经网络说起。

感知机

多层感知机中的特征神经元模型称为感知机,由Frank Rosenblatt于1957年发明。

简单的感知机如下图所示:

其中$x_1$,$x_2$,$x_3$为感知机的输入,其输出为:

假如把感知机想象成一个加权投票机制,比如 3 位评委给一个歌手打分,打分分别为4分、1分、-3分,这3位评分的权重分别是1、2、3,则该歌手最终得分为 $4 \times 1 + 1 \times 3 + (-3) \times 2 = 1$ 。按照比赛规则,选取的 $threshold$ 为 $3$,说明只有歌手的综合评分大于$ 3$ 时,才可顺利晋级。对照感知机,该选手被淘汰,因为:

用 $-b$ 代替 $threshold$,输出变为:

设置合适的 $\boldsymbol{x}$ 和 $b$ ,一个简单的感知机单元的与非门表示如下:

当输入为 $0$,$1$ 时,感知机输出为 $ 0 \times (-2) + 1 \times (-2) + 3 = 1$。

复杂一些的感知机由简单的感知机单元组合而成:

多层感知机

多层感知机由感知机推广而来,最主要的特点是有多个神经元层,因此也叫深度神经网络。相比于单独的感知机,多层感知机的第 $ i $ 层的每个神经元和第 $ i-1 $ 层的每个神经元都有连接。

输出层可以不止有$ 1$ 个神经元。隐藏层可以只有$ 1$ 层,也可以有多层。输出层为多个神经元的神经网络例如下图所示:

3.1.2 神经网络有哪些常用模型结构?

下图包含了大部分常用的模型:

3.1.3 如何选择深度学习开发平台?

​ 现有的深度学习开源平台主要有 Caffe, PyTorch, MXNet, CNTK, Theano, TensorFlow, Keras, fastai等。那如何选择一个适合自己的平台呢,下面列出一些衡量做参考。

参考1:与现有编程平台、技能整合的难易程度

​ 主要是前期积累的开发经验和资源,比如编程语言,前期数据集存储格式等。

参考2: 与相关机器学习、数据处理生态整合的紧密程度

​ 深度学习研究离不开各种数据处理、可视化、统计推断等软件包。考虑建模之前,是否具有方便的数据预处理工具?建模之后,是否具有方便的工具进行可视化、统计推断、数据分析。

参考3:对数据量及硬件的要求和支持

​ 深度学习在不同应用场景的数据量是不一样的,这也就导致我们可能需要考虑分布式计算、多GPU计算的问题。例如,对计算机图像处理研究的人员往往需要将图像文件和计算任务分部到多台计算机节点上进行执行。当下每个深度学习平台都在快速发展,每个平台对分布式计算等场景的支持也在不断演进。

参考4:深度学习平台的成熟程度

​ 成熟程度的考量是一个比较主观的考量因素,这些因素可包括:社区的活跃程度;是否容易和开发人员进行交流;当前应用的势头。

参考5:平台利用是否多样性?

​ 有些平台是专门为深度学习研究和应用进行开发的,有些平台对分布式计算、GPU 等构架都有强大的优化,能否用这些平台/软件做其他事情?比如有些深度学习软件是可以用来求解二次型优化;有些深度学习平台很容易被扩展,被运用在强化学习的应用中。

3.1.4 为什么使用深层表示?

  1. 深度神经网络是一种特征递进式的学习算法,浅层的神经元直接从输入数据中学习一些低层次的简单特征,例如边缘、纹理等。而深层的特征则基于已学习到的浅层特征继续学习更高级的特征,从计算机的角度学习深层的语义信息。
  2. 深层的网络隐藏单元数量相对较少,隐藏层数目较多,如果浅层的网络想要达到同样的计算结果则需要指数级增长的单元数量才能达到。

3.1.5 为什么深层神经网络难以训练?

  1. 梯度消失

     梯度消失是指通过隐藏层从后向前看,梯度会变的越来越小,说明前面层的学习会显著慢于后面层的学习,所以学习会卡住,除非梯度变大。
    

    ​ 梯度消失的原因受到多种因素影响,例如学习率的大小,网络参数的初始化,激活函数的边缘效应等。在深层神经网络中,每一个神经元计算得到的梯度都会传递给前一层,较浅层的神经元接收到的梯度受到之前所有层梯度的影响。如果计算得到的梯度值非常小,随着层数增多,求出的梯度更新信息将会以指数形式衰减,就会发生梯度消失。下图是不同隐含层的学习速率:

  1. 梯度爆炸

     在深度网络或循环神经网络(Recurrent Neural Network, RNN)等网络结构中,梯度可在网络更新的过程中不断累积,变成非常大的梯度,导致网络权重值的大幅更新,使得网络不稳定;在极端情况下,权重值甚至会溢出,变为$NaN$值,再也无法更新。
    
  2. 权重矩阵的退化导致模型的有效自由度减少。

    ​ 参数空间中学习的退化速度减慢,导致减少了模型的有效维数,网络的可用自由度对学习中梯度范数的贡献不均衡,随着相乘矩阵的数量(即网络深度)的增加,矩阵的乘积变得越来越退化。在有硬饱和边界的非线性网络中(例如 ReLU 网络),随着深度增加,退化过程会变得越来越快。Duvenaud等人2014年的论文里展示了关于该退化过程的可视化:

随着深度的增加,输入空间(左上角所示)会在输入空间中的每个点处被扭曲成越来越细的单丝,只有一个与细丝正交的方向影响网络的响应。沿着这个方向,网络实际上对变化变得非常敏感。

3.1.6 深度学习和机器学习有什么不同?

机器学习:利用计算机、概率论、统计学等知识,输入数据,让计算机学会新知识。机器学习的过程,就是训练数据去优化目标函数。

深度学习:是一种特殊的机器学习,具有强大的能力和灵活性。它通过学习将世界表示为嵌套的层次结构,每个表示都与更简单的特征相关,而抽象的表示则用于计算更抽象的表示。

​ 传统的机器学习需要定义一些手工特征,从而有目的的去提取目标信息, 非常依赖任务的特异性以及设计特征的专家经验。而深度学习可以从大数据中先学习简单的特征,并从其逐渐学习到更为复杂抽象的深层特征,不依赖人工的特征工程,这也是深度学习在大数据时代受欢迎的一大原因。

3.2 网络操作与计算

3.2.1 前向传播与反向传播?

神经网络的计算主要有两种:前向传播(foward propagation, FP)作用于每一层的输入,通过逐层计算得到输出结果;反向传播(backward propagation, BP)作用于网络的输出,通过计算梯度由深到浅更新网络参数。

前向传播

假设上一层结点 $ i,j,k,… $ 等一些结点与本层的结点 $ w $ 有连接,那么结点 $ w $ 的值怎么算呢?就是通过上一层的 $ i,j,k,… $ 等结点以及对应的连接权值进行加权和运算,最终结果再加上一个偏置项(图中为了简单省略了),最后在通过一个非线性函数(即激活函数),如 $ReLu$,$sigmoid$ 等函数,最后得到的结果就是本层结点 $ w $ 的输出。

最终不断的通过这种方法一层层的运算,得到输出层结果。

反向传播

由于我们前向传播最终得到的结果,以分类为例,最终总是有误差的,那么怎么减少误差呢,当前应用广泛的一个算法就是梯度下降算法,但是求梯度就要求偏导数,下面以图中字母为例讲解一下:

设最终误差为 $ E $且输出层的激活函数为线性激活函数,对于输出那么 $ E $ 对于输出节点 $ y_l $ 的偏导数是 $ y_l - t_l $,其中 $ t_l $ 是真实值,$ \frac{\partial y_l}{\partial z_l} $ 是指上面提到的激活函数,$ z_l $ 是上面提到的加权和,那么这一层的 $ E $ 对于 $ z_l $ 的偏导数为 $ \frac{\partial E}{\partial z_l} = \frac{\partial E}{\partial y_l} \frac{\partial y_l}{\partial z_l} $。同理,下一层也是这么计算,只不过 $ \frac{\partial E}{\partial y_k} $ 计算方法变了,一直反向传播到输入层,最后有 $ \frac{\partial E}{\partial x_i} = \frac{\partial E}{\partial y_j} \frac{\partial y_j}{\partial z_j} $,且 $ \frac{\partial z_j}{\partial x_i} = w_i j $。然后调整这些过程中的权值,再不断进行前向传播和反向传播的过程,最终得到一个比较好的结果。

3.2.2 如何计算神经网络的输出?

如上图,输入层有三个节点,我们将其依次编号为 1、2、3;隐藏层的 4 个节点,编号依次为 4、5、6、7;最后输出层的两个节点编号为 8、9。比如,隐藏层的节点 4,它和输入层的三个节点 1、2、3 之间都有连接,其连接上的权重分别为是 $ w{41}, w{42}, w_{43} $。

为了计算节点 4 的输出值,我们必须先得到其所有上游节点(也就是节点 1、2、3)的输出值。节点 1、2、3 是输入层的节点,所以,他们的输出值就是输入向量本身。按照上图画出的对应关系,可以看到节点 1、2、3 的输出值分别是 $ x_1, x_2, x_3 $。

其中 $ w_{4b} $ 是节点 4 的偏置项。

同样,我们可以继续计算出节点 5、6、7 的输出值 $ a_5, a_6, a_7 $。

计算输出层的节点 8 的输出值 $ y_1 $:

其中 $ w_{8b} $ 是节点 8 的偏置项。

同理,我们还可以计算出 $ y_2 $。这样输出层所有节点的输出值计算完毕,我们就得到了在输入向量 $ x_1, x_2, x_3, x_4 $ 时,神经网络的输出向量 $ y_1, y_2 $ 。这里我们也看到,输出向量的维度和输出层神经元个数相同。

3.2.3 如何计算卷积神经网络输出值?

假设有一个 5*5 的图像,使用一个 3*3 的 filter 进行卷积,想得到一个 3*3 的 Feature Map,如下所示:

$ x{i,j} $ 表示图像第 $ i $ 行第 $ j $ 列元素。$ w{m,n} $ 表示 filter​ 第 $ m $ 行第 $ n $ 列权重。 $ w_b $ 表示 $filter$ 的偏置项。 表$a_i,_j$示 feature map 第 $ i$ 行第 $ j $ 列元素。 $f$ 表示激活函数,这里以$ ReLU$ 函数为例。

卷积计算公式如下:

当步长为 $1$ 时,计算 feature map 元素 $ a_{0,0} $ 如下:

其计算过程图示如下:

以此类推,计算出全部的Feature Map。

当步幅为 2 时,Feature Map计算如下

注:图像大小、步幅和卷积后的Feature Map大小是有关系的。它们满足下面的关系:

​ 其中 $ W_2 $, 是卷积后 Feature Map 的宽度;$ W_1 $ 是卷积前图像的宽度;$ F $ 是 filter 的宽度;$ P $ 是 Zero Padding 数量,Zero Padding 是指在原始图像周围补几圈 $0$,如果 $P$ 的值是 $1$,那么就补 $1$ 圈 $0$;$S$ 是步幅;$ H_2 $ 卷积后 Feature Map 的高度;$ H_1 $ 是卷积前图像的宽度。

​ 举例:假设图像宽度 $ W_1 = 5 $,filter 宽度 $ F=3 $,Zero Padding $ P=0 $,步幅 $ S=2 $,$ Z $ 则

​ 说明 Feature Map 宽度是2。同样,我们也可以计算出 Feature Map 高度也是 2。

如果卷积前的图像深度为 $ D $,那么相应的 filter 的深度也必须为 $ D $。深度大于 1 的卷积计算公式:

​ 其中,$ D $ 是深度;$ F $ 是 filter 的大小;$ w{d,m,n} $ 表示 filter 的第 $ d $ 层第 $ m $ 行第 $ n $ 列权重;$ a{d,i,j} $ 表示 feature map 的第 $ d $ 层第 $ i $ 行第 $ j $ 列像素;其它的符号含义前面相同,不再赘述。

​ 每个卷积层可以有多个 filter。每个 filter 和原始图像进行卷积后,都可以得到一个 Feature Map。卷积后 Feature Map 的深度(个数)和卷积层的 filter 个数相同。下面的图示显示了包含两个 filter 的卷积层的计算。$773$ 输入,经过两个 $333$ filter 的卷积(步幅为 $2$),得到了 $332$ 的输出。图中的 Zero padding 是 $1$,也就是在输入元素的周围补了一圈 $0$。

​ 以上就是卷积层的计算方法。这里面体现了局部连接和权值共享:每层神经元只和上一层部分神经元相连(卷积计算规则),且 filter 的权值对于上一层所有神经元都是一样的。对于包含两个 $ 3 3 3 $ 的 fitler 的卷积层来说,其参数数量仅有 $ (3 3 3+1) * 2 = 56 $ 个,且参数数量与上一层神经元个数无关。与全连接神经网络相比,其参数数量大大减少了。

3.2.4 如何计算 Pooling 层输出值输出值?

​ Pooling 层主要的作用是下采样,通过去掉 Feature Map 中不重要的样本,进一步减少参数数量。Pooling 的方法很多,最常用的是 Max Pooling。Max Pooling 实际上就是在 n*n 的样本中取最大值,作为采样后的样本值。下图是 2*2 max pooling:

​ 除了 Max Pooing 之外,常用的还有 Average Pooling ——取各样本的平均值。
​ 对于深度为 $ D $ 的 Feature Map,各层独立做 Pooling,因此 Pooling 后的深度仍然为 $ D $。

3.2.5 实例理解反向传播

​ 一个典型的三层神经网络如下所示:

​ 其中 Layer $ L_1 $ 是输入层,Layer $ L_2 $ 是隐含层,Layer $ L_3 $ 是输出层。

​ 假设输入数据集为 $ D={x_1, x_2, …, x_n} $,输出数据集为 $ y_1, y_2, …, y_n $。

​ 如果输入和输出是一样,即为自编码模型。如果原始数据经过映射,会得到不同于输入的输出。

假设有如下的网络层:

​ 输入层包含神经元 $ i_1, i_2 $,偏置 $ b_1 $;隐含层包含神经元 $ h_1, h_2 $,偏置 $ b_2 $,输出层为 $ o_1, o_2 $,$ w_i $ 为层与层之间连接的权重,激活函数为 $sigmoid$ 函数。对以上参数取初始值,如下图所示:

其中:

  • 输入数据 $ i1=0.05, i2 = 0.10 $
  • 输出数据 $ o1=0.01, o2=0.99 $;
  • 初始权重 $ w1=0.15, w2=0.20, w3=0.25,w4=0.30, w5=0.40, w6=0.45, w7=0.50, w8=0.55 $
  • 目标:给出输入数据 $ i1,i2 $ ( $0.05$和$0.10$ ),使输出尽可能与原始输出 $ o1,o2 $,( $0.01$和$0.99$)接近。

前向传播

  1. 输入层 —> 输出层

计算神经元 $ h1 $ 的输入加权和:

神经元 $ h1 $ 的输出 $ o1 $ :(此处用到激活函数为 sigmoid 函数):

同理,可计算出神经元 $ h2 $ 的输出 $ o1 $:

  1. 隐含层—>输出层:   

计算输出层神经元 $ o1 $ 和 $ o2 $ 的值:

这样前向传播的过程就结束了,我们得到输出值为 $ [0.75136079 , 0.772928465] $,与实际值 $ [0.01 , 0.99] $ 相差还很远,现在我们对误差进行反向传播,更新权值,重新计算输出。

反向传播

​ 1.计算总误差

总误差:(这里使用Square Error)

但是有两个输出,所以分别计算 $ o1 $ 和 $ o2 $ 的误差,总误差为两者之和:

$E{o1} = \frac{1}{2}(target{o1} - out_{o1})^2
= \frac{1}{2}(0.01 - 0.75136507)^2 = 0.274811083$.

$E_{o2} = 0.023560026$.

$E{total} = E{o1} + E_{o2} = 0.274811083 + 0.023560026 = 0.298371109$.

​ 2.隐含层 —> 输出层的权值更新:

以权重参数 $ w5 $ 为例,如果我们想知道 $ w5 $ 对整体误差产生了多少影响,可以用整体误差对 $ w5 $ 求偏导求出:(链式法则)

下面的图可以更直观的看清楚误差是怎样反向传播的:

3.2.6 神经网络更“深”有什么意义?

前提:在一定范围内。

  • 在神经元数量相同的情况下,深层网络结构具有更大容量,分层组合带来的是指数级的表达空间,能够组合成更多不同类型的子结构,这样可以更容易地学习和表示各种特征。
  • 隐藏层增加则意味着由激活函数带来的非线性变换的嵌套层数更多,就能构造更复杂的映射关系。

3.3 超参数

3.3.1 什么是超参数?

超参数 : 在机器学习的上下文中,超参数是在开始学习过程之前设置值的参数,而不是通过训练得到的参数数据。通常情况下,需要对超参数进行优化,给学习机选择一组最优超参数,以提高学习的性能和效果。

​ 超参数通常存在于:

1.  定义关于模型的更高层次的概念,如复杂性或学习能力。
2.  不能直接从标准模型培训过程中的数据中学习,需要预先定义。
3.  可以通过设置不同的值,训练不同的模型和选择更好的测试值来决定

​ 超参数具体来讲比如算法中的学习率(learning rate)、梯度下降法迭代的数量(iterations)、隐藏层数目(hidden layers)、隐藏层单元数目、激活函数( activation function)都需要根据实际情况来设置,这些数字实际上控制了最后的参数和的值,所以它们被称作超参数。

3.3.2 如何寻找超参数的最优值?

​ 在使用机器学习算法时,总有一些难调的超参数。例如权重衰减大小,高斯核宽度等等。这些参数需要人为设置,设置的值对结果产生较大影响。常见设置超参数的方法有:

  1. 猜测和检查:根据经验或直觉,选择参数,一直迭代。

  2. 网格搜索:让计算机尝试在一定范围内均匀分布的一组值。

  3. 随机搜索:让计算机随机挑选一组值。

  4. 贝叶斯优化:使用贝叶斯优化超参数,会遇到贝叶斯优化算法本身就需要很多的参数的困难。

  5. MITIE方法,好初始猜测的前提下进行局部优化。它使用BOBYQA算法,并有一个精心选择的起始点。由于BOBYQA只寻找最近的局部最优解,所以这个方法是否成功很大程度上取决于是否有一个好的起点。在MITIE的情况下,我们知道一个好的起点,但这不是一个普遍的解决方案,因为通常你不会知道好的起点在哪里。从好的方面来说,这种方法非常适合寻找局部最优解。稍后我会再讨论这一点。

  6. 最新提出的LIPO的全局优化方法。这个方法没有参数,而且经验证比随机搜索方法好。

3.3.3 超参数搜索一般过程?

超参数搜索一般过程:

  1. 将数据集划分成训练集、验证集及测试集。
  2. 在训练集上根据模型的性能指标对模型参数进行优化。
  3. 在验证集上根据模型的性能指标对模型的超参数进行搜索。
  4. 步骤 2 和步骤 3 交替迭代,最终确定模型的参数和超参数,在测试集中验证评价模型的优劣。

其中,搜索过程需要搜索算法,一般有:网格搜索、随机搜过、启发式智能搜索、贝叶斯搜索。

3.4 激活函数

3.4.1 为什么需要非线性激活函数?

为什么需要激活函数?

  1. 激活函数对模型学习、理解非常复杂和非线性的函数具有重要作用。
  2. 激活函数可以引入非线性因素。如果不使用激活函数,则输出信号仅是一个简单的线性函数。线性函数一个一级多项式,线性方程的复杂度有限,从数据中学习复杂函数映射的能力很小。没有激活函数,神经网络将无法学习和模拟其他复杂类型的数据,例如图像、视频、音频、语音等。
  3. 激活函数可以把当前特征空间通过一定的线性映射转换到另一个空间,让数据能够更好的被分类。

为什么激活函数需要非线性函数?

  1. 假若网络中全部是线性部件,那么线性的组合还是线性,与单独一个线性分类器无异。这样就做不到用非线性来逼近任意函数。
  2. 使用非线性激活函数 ,以便使网络更加强大,增加它的能力,使它可以学习复杂的事物,复杂的表单数据,以及表示输入输出之间非线性的复杂的任意函数映射。使用非线性激活函数,能够从输入输出之间生成非线性映射。

3.4.2 常见的激活函数及图像

  1. sigmoid 激活函数

    函数的定义为:$ f(x) = \frac{1}{1 + e^{-x}} $,其值域为 $ (0,1) $。

    函数图像如下:

  1. tanh激活函数

    函数的定义为:$ f(x) = tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}} $,值域为 $ (-1,1) $。

    函数图像如下:

  1. Relu激活函数

    函数的定义为:$ f(x) = max(0, x) $ ,值域为 $ [0,+∞) $;

    函数图像如下:

  1. Leak Relu 激活函数

    函数定义为: $ f(x) = \left{
    \begin{aligned}
    ax, \quad x<0 \\ x, \quad x>0
    \end{aligned}
    \right. $,值域为 $ (-∞,+∞) $。

    图像如下($ a = 0.5 $):

  1. SoftPlus 激活函数

    函数的定义为:$ f(x) = ln( 1 + e^x) $,值域为 $ (0,+∞) $。

    函数图像如下:

  1. softmax 函数

    函数定义为: $ \sigma(z)j = \frac{e^{z_j}}{\sum{k=1}^K e^{z_k}} $。

    Softmax 多用于多分类神经网络输出。

3.4.3 常见激活函数的导数计算?

对常见激活函数,导数计算如下:

原函数 函数表达式 导数 备注
Sigmoid激活函数 $f(x)=\frac{1}{1+e^{-x}}$ $f^{‘}(x)=\frac{1}{1+e^{-x}}\left( 1- \frac{1}{1+e^{-x}} \right)=f(x)(1-f(x))$ 当$x=10$,或$x=-10​$,$f^{‘}(x) \approx0​$,当$x=0​$$f^{‘}(x) =0.25​$
Tanh激活函数 $f(x)=tanh(x)=\frac{e^x-e^{-x}}{e^x+e^{-x}}$ $f^{‘}(x)=-(tanh(x))^2$ 当$x=10$,或$x=-10$,$f^{‘}(x) \approx0$,当$x=0$$f^{`}(x) =1$
Relu激活函数 $f(x)=max(0,x)$ $c(u)=\begin{cases} 0,x<0 \\ 1,x>0 \ undefined,x=0\end{cases}$ 通常$x=0$时,给定其导数为1和0

3.4.4 激活函数有哪些性质?

  1. 非线性: 当激活函数是线性的,一个两层的神经网络就可以基本上逼近所有的函数。但如果激活函数是恒等激活函数的时候,即 $ f(x)=x $,就不满足这个性质,而且如果 MLP 使用的是恒等激活函数,那么其实整个网络跟单层神经网络是等价的;
  2. 可微性: 当优化方法是基于梯度的时候,就体现了该性质;
  3. 单调性: 当激活函数是单调的时候,单层网络能够保证是凸函数;
  4. $ f(x)≈x $: 当激活函数满足这个性质的时候,如果参数的初始化是随机的较小值,那么神经网络的训练将会很高效;如果不满足这个性质,那么就需要详细地去设置初始值;
  5. 输出值的范围: 当激活函数输出值是有限的时候,基于梯度的优化方法会更加稳定,因为特征的表示受有限权值的影响更显著;当激活函数的输出是无限的时候,模型的训练会更加高效,不过在这种情况小,一般需要更小的 Learning Rate。

3.4.5 如何选择激活函数?

​ 选择一个适合的激活函数并不容易,需要考虑很多因素,通常的做法是,如果不确定哪一个激活函数效果更好,可以把它们都试试,然后在验证集或者测试集上进行评价。然后看哪一种表现的更好,就去使用它。

以下是常见的选择情况:

  1. 如果输出是 0、1 值(二分类问题),则输出层选择 sigmoid 函数,然后其它的所有单元都选择 Relu 函数。
  2. 如果在隐藏层上不确定使用哪个激活函数,那么通常会使用 Relu 激活函数。有时,也会使用 tanh 激活函数,但 Relu 的一个优点是:当是负值的时候,导数等于 0。
  3. sigmoid 激活函数:除了输出层是一个二分类问题基本不会用它。
  4. tanh 激活函数:tanh 是非常优秀的,几乎适合所有场合。
  5. ReLu 激活函数:最常用的默认函数,如果不确定用哪个激活函数,就使用 ReLu 或者 Leaky ReLu,再去尝试其他的激活函数。
  6. 如果遇到了一些死的神经元,我们可以使用 Leaky ReLU 函数。

3.4.6 使用 ReLu 激活函数的优点?

  1. 在区间变动很大的情况下,ReLu 激活函数的导数或者激活函数的斜率都会远大于 0,在程序实现就是一个 if-else 语句,而 sigmoid 函数需要进行浮点四则运算,在实践中,使用 ReLu 激活函数神经网络通常会比使用 sigmoid 或者 tanh 激活函数学习的更快。
  2. sigmoid 和 tanh 函数的导数在正负饱和区的梯度都会接近于 0,这会造成梯度弥散,而 Relu 和Leaky ReLu 函数大于 0 部分都为常数,不会产生梯度弥散现象。
  3. 需注意,Relu 进入负半区的时候,梯度为 0,神经元此时不会训练,产生所谓的稀疏性,而 Leaky ReLu 不会产生这个问题。

3.4.7 什么时候可以用线性激活函数?

  1. 输出层,大多使用线性激活函数。
  2. 在隐含层可能会使用一些线性激活函数。
  3. 一般用到的线性激活函数很少。

3.4.8 怎样理解 Relu(< 0 时)是非线性激活函数?

Relu 激活函数图像如下:

根据图像可看出具有如下特点:

  1. 单侧抑制;

  2. 相对宽阔的兴奋边界;

  3. 稀疏激活性;

    ReLU 函数从图像上看,是一个分段线性函数,把所有的负值都变为 0,而正值不变,这样就成为单侧抑制。

    因为有了这单侧抑制,才使得神经网络中的神经元也具有了稀疏激活性。

    稀疏激活性:从信号方面来看,即神经元同时只对输入信号的少部分选择性响应,大量信号被刻意的屏蔽了,这样可以提高学习的精度,更好更快地提取稀疏特征。当 $ x<0 $ 时,ReLU 硬饱和,而当 $ x>0 $ 时,则不存在饱和问题。ReLU 能够在 $ x>0 $ 时保持梯度不衰减,从而缓解梯度消失问题。

3.4.9 Softmax 定义及作用

Softmax 是一种形如下式的函数:

​ 其中,$ \theta_i $ 和 $ x $ 是列向量,$ \theta_i^T x $ 可能被换成函数关于 $ x $ 的函数 $ f_i(x) $

​ 通过 softmax 函数,可以使得 $ P(i) $ 的范围在 $ [0,1] $ 之间。在回归和分类问题中,通常 $ \theta $ 是待求参数,通过寻找使得 $ P(i) $ 最大的 $ \theta_i $ 作为最佳参数。

​ 但是,使得范围在 $ [0,1] $ 之间的方法有很多,为啥要在前面加上以 $ e $ 的幂函数的形式呢?参考 logistic 函数:

​ 这个函数的作用就是使得 $ P(i) $ 在负无穷到 0 的区间趋向于 0, 在 0 到正无穷的区间趋向 1,。同样 softmax 函数加入了 $ e $ 的幂函数正是为了两极化:正样本的结果将趋近于 1,而负样本的结果趋近于 0。这样为多类别提供了方便(可以把 $ P(i) $ 看做是样本属于类别的概率)。可以说,Softmax 函数是 logistic 函数的一种泛化。

​ softmax 函数可以把它的输入,通常被称为 logits 或者 logit scores,处理成 0 到 1 之间,并且能够把输出归一化到和为 1。这意味着 softmax 函数与分类的概率分布等价。它是一个网络预测多酚类问题的最佳输出激活函数。

3.4.10 Softmax 函数如何应用于多分类?

​ softmax 用于多分类过程中,它将多个神经元的输出,映射到 $ (0,1) $ 区间内,可以看成概率来理解,从而来进行多分类!

​ 假设我们有一个数组,$ V_i $ 表示 $ V $ 中的第 $ i $ 个元素,那么这个元素的 softmax 值就是

​ 从下图看,神经网络中包含了输入层,然后通过两个特征层处理,最后通过 softmax 分析器就能得到不同条件下的概率,这里需要分成三个类别,最终会得到 $ y=0, y=1, y=2 $ 的概率值。

继续看下面的图,三个输入通过 softmax 后得到一个数组 $ [0.05 , 0.10 , 0.85] $,这就是 soft 的功能。

更形象的映射过程如下图所示:

****

​ softmax 直白来说就是将原来输出是 $ 3,1,-3 $ 通过 softmax 函数一作用,就映射成为 $ (0,1) $ 的值,而这些值的累和为 $ 1 $(满足概率的性质),那么我们就可以将它理解成概率,在最后选取输出结点的时候,我们就可以选取概率最大(也就是值对应最大的)结点,作为我们的预测目标!

3.4.11 交叉熵代价函数定义及其求导推导

(贡献者:黄钦建-华南理工大学)

​ 神经元的输出就是 a = σ(z),其中$z=\sum w{j}i{j}+b$是输⼊的带权和。

$C=-\frac{1}{n}\sum[ylna+(1-y)ln(1-a)]$

​ 其中 n 是训练数据的总数,求和是在所有的训练输⼊ x 上进⾏的, y 是对应的⽬标输出。

​ 表达式是否解决学习缓慢的问题并不明显。实际上,甚⾄将这个定义看做是代价函数也不是显⽽易⻅的!在解决学习缓慢前,我们来看看交叉熵为何能够解释成⼀个代价函数。

​ 将交叉熵看做是代价函数有两点原因。

​ 第⼀,它是⾮负的, C > 0。可以看出:式子中的求和中的所有独⽴的项都是负数的,因为对数函数的定义域是 (0,1),并且求和前⾯有⼀个负号,所以结果是非负。

​ 第⼆,如果对于所有的训练输⼊ x,神经元实际的输出接近⽬标值,那么交叉熵将接近 0。

​ 假设在这个例⼦中, y = 0 ⽽ a ≈ 0。这是我们想到得到的结果。我们看到公式中第⼀个项就消去了,因为 y = 0,⽽第⼆项实际上就是 − ln(1 − a) ≈ 0。反之, y = 1 ⽽ a ≈ 1。所以在实际输出和⽬标输出之间的差距越⼩,最终的交叉熵的值就越低了。(这里假设输出结果不是0,就是1,实际分类也是这样的)

​ 综上所述,交叉熵是⾮负的,在神经元达到很好的正确率的时候会接近 0。这些其实就是我们想要的代价函数的特性。其实这些特性也是⼆次代价函数具备的。所以,交叉熵就是很好的选择了。但是交叉熵代价函数有⼀个⽐⼆次代价函数更好的特性就是它避免了学习速度下降的问题。为了弄清楚这个情况,我们来算算交叉熵函数关于权重的偏导数。我们将$a={\varsigma}(z)$代⼊到 公式中应⽤两次链式法则,得到:

$\begin{eqnarray}\frac{\partial C}{\partial w{j}}&=&-\frac{1}{n}\sum \frac{\partial }{\partial w{j}}[ylna+(1-y)ln(1-a)]\&=&-\frac{1}{n}\sum \frac{\partial }{\partial a}[ylna+(1-y)ln(1-a)]\frac{\partial a}{\partial w_{j}}\&=&-\frac{1}{n}\sum (\frac{y}{a}-\frac{1-y}{1-a})\frac{\partial a}{\partial w{j}}\&=&-\frac{1}{n}\sum (\frac{y}{\varsigma(z)}-\frac{1-y}{1-\varsigma(z)})\frac{\partial \varsigma(z)}{\partial w{j}}\&=&-\frac{1}{n}\sum (\frac{y}{\varsigma(z)}-\frac{1-y}{1-\varsigma(z)}){\varsigma}’(z)x_{j}\end{eqnarray}$

​ 根据$\varsigma(z)=\frac{1}{1+e^{-z}}$ 的定义,和⼀些运算,我们可以得到 ${\varsigma}’(z)=\varsigma(z)(1-\varsigma(z))$。化简后可得:

$\frac{\partial C}{\partial w{j}}=\frac{1}{n}\sum x{j}({\varsigma}(z)-y)$

​ 这是⼀个优美的公式。它告诉我们权重学习的速度受到$\varsigma(z)-y$,也就是输出中的误差的控制。更⼤的误差,更快的学习速度。这是我们直觉上期待的结果。特别地,这个代价函数还避免了像在⼆次代价函数中类似⽅程中${\varsigma}’(z)$导致的学习缓慢。当我们使⽤交叉熵的时候,${\varsigma}’(z)$被约掉了,所以我们不再需要关⼼它是不是变得很⼩。这种约除就是交叉熵带来的特效。实际上,这也并不是⾮常奇迹的事情。我们在后⾯可以看到,交叉熵其实只是满⾜这种特性的⼀种选择罢了。

​ 根据类似的⽅法,我们可以计算出关于偏置的偏导数。我这⾥不再给出详细的过程,你可以轻易验证得到:

$\frac{\partial C}{\partial b}=\frac{1}{n}\sum ({\varsigma}(z)-y)$

​ 再⼀次, 这避免了⼆次代价函数中类似${\varsigma}’(z)$项导致的学习缓慢。

3.4.12 为什么Tanh收敛速度比Sigmoid快?

(贡献者:黄钦建-华南理工大学)

首先看如下两个函数的求导:

$tanh^{,}(x)=1-tanh(x)^{2}\in (0,1)$

$s^{,}(x)=s(x)*(1-s(x))\in (0,\frac{1}{4}]$

由上面两个公式可知tanh(x)梯度消失的问题比sigmoid轻,所以Tanh收敛速度比Sigmoid快。

3.4.13

3.4.12 内聚外斥 - Center Loss

(贡献者:李世轩-加州大学伯克利分校)

在计算机视觉任务中, 由于其简易性, 良好的表现, 与对分类任务的概率性理解, Cross Entropy Loss (交叉熵代价) + Softmax 组合被广泛应用于以分类任务为代表的任务中. 在此应用下, 我们可将其学习过程进一步理解为: 更相似(同类/同物体)的图像在特征域中拥有“更近的距离”, 相反则”距离更远“. 换而言之, 我们可以进一步理解为其学习了一种低类内距离(Intra-class Distance)与高类间距离(Inter-class Distance)的特征判别模型. 在此Center Loss则可以高效的计算出这种具判别性的特征. 不同于传统的Softmax Loss, Center Loss通过学习“特征中心”从而最小化其类内距离. 其表达形式如下:

$L{C} = \frac{1}{2}\sum^{m}{i=1}||x{i}-c{y{i}}||^{2}{2}$

其中$x{i}$表示FCN(全连接层)之前的特征, $c{y{i}}$表示第$y{i} $个类别的特征中心, $m$表示mini-batch的大小. 我们很清楚的看到$L_{C}$的终极目标为最小化每个特征与其特征中心的方差, 即最小化类内距离. 其迭代公式为:

$\frac{\partial L{C}}{\partial x{i}}=x{i}-c{y_{i}}$

$\Delta{c{j}} = \frac{\sum^{m}{i=1}\delta(y{i}=j)\cdot(c{j}-x{i})}{1+\sum^{m}{i=1}\delta(y_{i}=j)}$

其中$ \delta(condition)=\left{
\begin{array}{rcl}
1 & & {condition\ is\ True}\
0 & & {otherwise}\ \end{array} \right.$

结合Softmax, 我们可以搭配二者使用, 适当平衡这两种监督信号. 在Softmax拉开类间距离的同时, 利用Center Loss最小化类内距离. 例如:

$\begin{eqnarray}L & = & L{S} + \lambda L{C} \ &=& -\sum^{m}{i=1}log\frac{e^{W{y}^{T}x{i}+b{y{i}}}}{\sum^{m}{i=1}e^{W^{T}{j}x{i}+b{j}}} + \frac{\lambda}{2}\sum^{m}{i=1}||x{i}-c{y{i}}||^{2}{2}\ \end{eqnarray}$

即便如此, Center Loss仍有它的不足之处: 其特征中心为存储在网络模型之外的额外参数, 不能与模型参数一同优化. 这些额外参数将与记录每一步特征变化的自动回归均值估计(autoregressive mean estimator)进行更迭. 当需要学习的类别数量较大时, mini-batch可能无力提供足够的样本进行均值估计. 若此Center Loss将需要平衡两种监督损失来以确定更迭, 其过程需要一个对平衡超参数的搜索过程, 使得其择值消耗昂贵.

3.5 Batch_Size

3.5.1 为什么需要 Batch_Size?

Batch的选择,首先决定的是下降的方向。

如果数据集比较小,可采用全数据集的形式,好处是:

  1. 由全数据集确定的方向能够更好地代表样本总体,从而更准确地朝向极值所在的方向。
  2. 由于不同权重的梯度值差别巨大,因此选取一个全局的学习率很困难。 Full Batch Learning 可以使用 Rprop 只基于梯度符号并且针对性单独更新各权值。

对于更大的数据集,假如采用全数据集的形式,坏处是:

  1. 随着数据集的海量增长和内存限制,一次性载入所有的数据进来变得越来越不可行。
  2. 以 Rprop 的方式迭代,会由于各个 Batch 之间的采样差异性,各次梯度修正值相互抵消,无法修正。这才有了后来 RMSProp 的妥协方案。

3.5.2 Batch_Size 值的选择

​ 假如每次只训练一个样本,即 Batch_Size = 1。线性神经元在均方误差代价函数的错误面是一个抛物面,横截面是椭圆。对于多层神经元、非线性网络,在局部依然近似是抛物面。此时,每次修正方向以各自样本的梯度方向修正,横冲直撞各自为政,难以达到收敛。

​ 既然 Batch_Size 为全数据集或者Batch_Size = 1都有各自缺点,可不可以选择一个适中的Batch_Size值呢?

​ 此时,可采用批梯度下降法(Mini-batches Learning)。因为如果数据集足够充分,那么用一半(甚至少得多)的数据训练算出来的梯度与用全部数据训练出来的梯度是几乎一样的。

3.5.3 在合理范围内,增大Batch_Size有何好处?

  1. 内存利用率提高了,大矩阵乘法的并行化效率提高。
  2. 跑完一次 epoch(全数据集)所需的迭代次数减少,对于相同数据量的处理速度进一步加快。
  3. 在一定范围内,一般来说 Batch_Size 越大,其确定的下降方向越准,引起训练震荡越小。

3.5.4 盲目增大 Batch_Size 有何坏处?

  1. 内存利用率提高了,但是内存容量可能撑不住了。
  2. 跑完一次 epoch(全数据集)所需的迭代次数减少,要想达到相同的精度,其所花费的时间大大增加了,从而对参数的修正也就显得更加缓慢。
  3. Batch_Size 增大到一定程度,其确定的下降方向已经基本不再变化。

3.5.5 调节 Batch_Size 对训练效果影响到底如何?

  1. Batch_Size 太小,模型表现效果极其糟糕(error飙升)。
  2. 随着 Batch_Size 增大,处理相同数据量的速度越快。
  3. 随着 Batch_Size 增大,达到相同精度所需要的 epoch 数量越来越多。
  4. 由于上述两种因素的矛盾, Batch_Size 增大到某个时候,达到时间上的最优。
  5. 由于最终收敛精度会陷入不同的局部极值,因此 Batch_Size 增大到某些时候,达到最终收敛精度上的最优。

3.6 归一化

3.6.1 归一化含义?

  1. 归纳统一样本的统计分布性。归一化在 $ 0-1$ 之间是统计的概率分布,归一化在$ -1—+1$ 之间是统计的坐标分布。

  2. 无论是为了建模还是为了计算,首先基本度量单位要同一,神经网络是以样本在事件中的统计分别几率来进行训练(概率计算)和预测,且 sigmoid 函数的取值是 0 到 1 之间的,网络最后一个节点的输出也是如此,所以经常要对样本的输出归一化处理。

  3. 归一化是统一在 $ 0-1 $ 之间的统计概率分布,当所有样本的输入信号都为正值时,与第一隐含层神经元相连的权值只能同时增加或减小,从而导致学习速度很慢。

  4. 另外在数据中常存在奇异样本数据,奇异样本数据存在所引起的网络训练时间增加,并可能引起网络无法收敛。为了避免出现这种情况及后面数据处理的方便,加快网络学习速度,可以对输入信号进行归一化,使得所有样本的输入信号其均值接近于 0 或与其均方差相比很小。

3.6.2 为什么要归一化?

  1. 为了后面数据处理的方便,归一化的确可以避免一些不必要的数值问题。
  2. 为了程序运行时收敛加快。
  3. 同一量纲。样本数据的评价标准不一样,需要对其量纲化,统一评价标准。这算是应用层面的需求。
  4. 避免神经元饱和。啥意思?就是当神经元的激活在接近 0 或者 1 时会饱和,在这些区域,梯度几乎为 0,这样,在反向传播过程中,局部梯度就会接近 0,这会有效地“杀死”梯度。
  5. 保证输出数据中数值小的不被吞食。

3.6.3 为什么归一化能提高求解最优解速度?

​ 上图是代表数据是否均一化的最优解寻解过程(圆圈可以理解为等高线)。左图表示未经归一化操作的寻解过程,右图表示经过归一化后的寻解过程。

​ 当使用梯度下降法寻求最优解时,很有可能走“之字型”路线(垂直等高线走),从而导致需要迭代很多次才能收敛;而右图对两个原始特征进行了归一化,其对应的等高线显得很圆,在梯度下降进行求解时能较快的收敛。

​ 因此如果机器学习模型使用梯度下降法求最优解时,归一化往往非常有必要,否则很难收敛甚至不能收敛。

3.6.4 3D 图解未归一化

例子:

​ 假设 $ w1 $ 的范围在 $ [-10, 10] $,而 $ w2 $ 的范围在 $ [-100, 100] $,梯度每次都前进 1 单位,那么在 $ w1 $ 方向上每次相当于前进了 $ 1/20 $,而在 $ w2 $ 上只相当于 $ 1/200 $!某种意义上来说,在 $ w2 $ 上前进的步长更小一些,而 $ w1 $ 在搜索过程中会比 $ w2 $ “走”得更快。

​ 这样会导致,在搜索过程中更偏向于 $ w1 $ 的方向。走出了“L”形状,或者成为“之”字形。

3.6.5 归一化有哪些类型?

  1. 线性归一化

​ 适用范围:比较适用在数值比较集中的情况。

​ 缺点:如果 max 和 min 不稳定,很容易使得归一化结果不稳定,使得后续使用效果也不稳定。

  1. 标准差标准化

​ 含义:经过处理的数据符合标准正态分布,即均值为 0,标准差为 1 其中 $ \mu $ 为所有样本数据的均值,$ \sigma $ 为所有样本数据的标准差。

  1. 非线性归一化

    适用范围:经常用在数据分化比较大的场景,有些数值很大,有些很小。通过一些数学函数,将原始值进行映射。该方法包括 $ log $、指数,正切等。

3.6.6 局部响应归一化作用

​ LRN 是一种提高深度学习准确度的技术方法。LRN 一般是在激活、池化函数后的一种方法。

​ 在 ALexNet 中,提出了 LRN 层,对局部神经元的活动创建竞争机制,使其中响应比较大对值变得相对更大,并抑制其他反馈较小的神经元,增强了模型的泛化能力。

3.6.7 理解局部响应归一化

​ 局部响应归一化原理是仿造生物学上活跃的神经元对相邻神经元的抑制现象(侧抑制),其公式如下:

其中,
1) $ a $:表示卷积层(包括卷积操作和池化操作)后的输出结果,是一个四维数组[batch,height,width,channel]。

  • batch:批次数(每一批为一张图片)。
  • height:图片高度。
  • width:图片宽度。
  • channel:通道数。可以理解成一批图片中的某一个图片经过卷积操作后输出的神经元个数,或理解为处理后的图片深度。

2) $ a_{x,y}^i $ 表示在这个输出结构中的一个位置 $ [a,b,c,d] $,可以理解成在某一张图中的某一个通道下的某个高度和某个宽度位置的点,即第 $ a $ 张图的第 $ d $ 个通道下的高度为b宽度为c的点。

3) $ N $:论文公式中的 $ N $ 表示通道数 (channel)。

4) $ a $,$ n/2 $, $ k $ 分别表示函数中的 input,depth_radius,bias。参数 $ k, n, \alpha, \beta $ 都是超参数,一般设置 $ k=2, n=5, \alpha=1*e-4, \beta=0.75 $

5) $ \sum $:$ \sum $ 叠加的方向是沿着通道方向的,即每个点值的平方和是沿着 $ a $ 中的第 3 维 channel 方向的,也就是一个点同方向的前面 $ n/2 $ 个通道(最小为第 $ 0 $ 个通道)和后 $ n/2 $ 个通道(最大为第 $ d-1 $ 个通道)的点的平方和(共 $ n+1 $ 个点)。而函数的英文注解中也说明了把 input 当成是 $ d $ 个 3 维的矩阵,说白了就是把 input 的通道数当作 3 维矩阵的个数,叠加的方向也是在通道方向。

简单的示意图如下:

3.6.8 什么是批归一化(Batch Normalization)

​ 以前在神经网络训练中,只是对输入层数据进行归一化处理,却没有在中间层进行归一化处理。要知道,虽然我们对输入数据进行了归一化处理,但是输入数据经过 $ \sigma(WX+b) $ 这样的矩阵乘法以及非线性运算之后,其数据分布很可能被改变,而随着深度网络的多层运算之后,数据分布的变化将越来越大。如果我们能在网络的中间也进行归一化处理,是否对网络的训练起到改进作用呢?答案是肯定的。

​ 这种在神经网络中间层也进行归一化处理,使训练效果更好的方法,就是批归一化Batch Normalization(BN)。

3.6.9 批归一化(BN)算法的优点

下面我们来说一下BN算法的优点:

  1. 减少了人为选择参数。在某些情况下可以取消 dropout 和 L2 正则项参数,或者采取更小的 L2 正则项约束参数;
  2. 减少了对学习率的要求。现在我们可以使用初始很大的学习率或者选择了较小的学习率,算法也能够快速训练收敛;
  3. 可以不再使用局部响应归一化。BN 本身就是归一化网络(局部响应归一化在 AlexNet 网络中存在)
  4. 破坏原来的数据分布,一定程度上缓解过拟合(防止每批训练中某一个样本经常被挑选到,文献说这个可以提高 1% 的精度)。
  5. 减少梯度消失,加快收敛速度,提高训练精度。

3.6.10 批归一化(BN)算法流程

下面给出 BN 算法在训练时的过程

输入:上一层输出结果 $ X = {x_1, x_2, …, x_m} $,学习参数 $ \gamma, \beta $

算法流程:

  1. 计算上一层输出数据的均值

其中,$ m $ 是此次训练样本 batch 的大小。

  1. 计算上一层输出数据的标准差
  1. 归一化处理,得到

其中 $ \epsilon $ 是为了避免分母为 0 而加进去的接近于 0 的很小值

  1. 重构,对经过上面归一化处理得到的数据进行重构,得到

其中,$ \gamma, \beta $ 为可学习参数。

注:上述是 BN 训练时的过程,但是当在投入使用时,往往只是输入一个样本,没有所谓的均值 $ \mu{\beta} $ 和标准差 $ \sigma{\beta}^2 $。此时,均值 $ \mu{\beta} $ 是计算所有 batch $ \mu{\beta} $ 值的平均值得到,标准差 $ \sigma{\beta}^2 $ 采用每个batch $ \sigma{\beta}^2 $ 的无偏估计得到。

3.6.11 批归一化和群组归一化比较

名称 特点
批量归一化(Batch Normalization,以下简称 BN) 可让各种网络并行训练。但是,批量维度进行归一化会带来一些问题——批量统计估算不准确导致批量变小时,BN 的误差会迅速增加。在训练大型网络和将特征转移到计算机视觉任务中(包括检测、分割和视频),内存消耗限制了只能使用小批量的 BN。
群组归一化 Group Normalization (简称 GN) GN 将通道分成组,并在每组内计算归一化的均值和方差。GN 的计算与批量大小无关,并且其准确度在各种批量大小下都很稳定。
比较 在 ImageNet 上训练的 ResNet-50上,GN 使用批量大小为 2 时的错误率比 BN 的错误率低 10.6% ;当使用典型的批量时,GN 与 BN 相当,并且优于其他标归一化变体。而且,GN 可以自然地从预训练迁移到微调。在进行 COCO 中的目标检测和分割以及 Kinetics 中的视频分类比赛中,GN 可以胜过其竞争对手,表明 GN 可以在各种任务中有效地取代强大的 BN。

3.6.12 Weight Normalization和Batch Normalization比较

​ Weight Normalization 和 Batch Normalization 都属于参数重写(Reparameterization)的方法,只是采用的方式不同。

​ Weight Normalization 是对网络权值$ W $ 进行 normalization,因此也称为 Weight Normalization;

​ Batch Normalization 是对网络某一层输入数据进行 normalization。

​ Weight Normalization相比Batch Normalization有以下三点优势:

  1. Weight Normalization 通过重写深度学习网络的权重W的方式来加速深度学习网络参数收敛,没有引入 minbatch 的依赖,适用于 RNN(LSTM)网络(Batch Normalization 不能直接用于RNN,进行 normalization 操作,原因在于:1) RNN 处理的 Sequence 是变长的;2) RNN 是基于 time step 计算,如果直接使用 Batch Normalization 处理,需要保存每个 time step 下,mini btach 的均值和方差,效率低且占内存)。

  2. Batch Normalization 基于一个 mini batch 的数据计算均值和方差,而不是基于整个 Training set 来做,相当于进行梯度计算式引入噪声。因此,Batch Normalization 不适用于对噪声敏感的强化学习、生成模型(Generative model:GAN,VAE)使用。相反,Weight Normalization 对通过标量 $ g $ 和向量 $ v $ 对权重 $ W $ 进行重写,重写向量 $ v $ 是固定的,因此,基于 Weight Normalization 的 Normalization 可以看做比 Batch Normalization 引入更少的噪声。

  3. 不需要额外的存储空间来保存 mini batch 的均值和方差,同时实现 Weight Normalization 时,对深度学习网络进行正向信号传播和反向梯度计算带来的额外计算开销也很小。因此,要比采用 Batch Normalization 进行 normalization 操作时,速度快。 但是 Weight Normalization 不具备 Batch Normalization 把网络每一层的输出 Y 固定在一个变化范围的作用。因此,采用 Weight Normalization 进行 Normalization 时需要特别注意参数初始值的选择。

3.6.13 Batch Normalization在什么时候用比较合适?

(贡献者:黄钦建-华南理工大学)

​ 在CNN中,BN应作用在非线性映射前。在神经网络训练时遇到收敛速度很慢,或梯度爆炸等无法训练的状况时可以尝试BN来解决。另外,在一般使用情况下也可以加入BN来加快训练速度,提高模型精度。

​ BN比较适用的场景是:每个mini-batch比较大,数据分布比较接近。在进行训练之前,要做好充分的shuffle,否则效果会差很多。另外,由于BN需要在运行过程中统计每个mini-batch的一阶统计量和二阶统计量,因此不适用于动态的网络结构和RNN网络。

3.7 预训练与微调(fine tuning)

3.7.1 为什么无监督预训练可以帮助深度学习?

深度网络存在问题:

  1. 网络越深,需要的训练样本数越多。若用监督则需大量标注样本,不然小规模样本容易造成过拟合。深层网络特征比较多,会出现的多特征问题主要有多样本问题、规则化问题、特征选择问题。

  2. 多层神经网络参数优化是个高阶非凸优化问题,经常得到收敛较差的局部解;

  3. 梯度扩散问题,BP算法计算出的梯度随着深度向前而显著下降,导致前面网络参数贡献很小,更新速度慢。

解决方法:

​ 逐层贪婪训练,无监督预训练(unsupervised pre-training)即训练网络的第一个隐藏层,再训练第二个…最后用这些训练好的网络参数值作为整体网络参数的初始值。

经过预训练最终能得到比较好的局部最优解。

3.7.2 什么是模型微调fine tuning

​ 用别人的参数、修改后的网络和自己的数据进行训练,使得参数适应自己的数据,这样一个过程,通常称之为微调(fine tuning).

模型的微调举例说明:

​ 我们知道,CNN 在图像识别这一领域取得了巨大的进步。如果想将 CNN 应用到我们自己的数据集上,这时通常就会面临一个问题:通常我们的 dataset 都不会特别大,一般不会超过 1 万张,甚至更少,每一类图片只有几十或者十几张。这时候,直接应用这些数据训练一个网络的想法就不可行了,因为深度学习成功的一个关键性因素就是大量带标签数据组成的训练集。如果只利用手头上这点数据,即使我们利用非常好的网络结构,也达不到很高的 performance。这时候,fine-tuning 的思想就可以很好解决我们的问题:我们通过对 ImageNet 上训练出来的模型(如CaffeNet,VGGNet,ResNet) 进行微调,然后应用到我们自己的数据集上。

3.7.3 微调时候网络参数是否更新?

答案:会更新。

  1. finetune 的过程相当于继续训练,跟直接训练的区别是初始化的时候。
  2. 直接训练是按照网络定义指定的方式初始化。
  3. finetune是用你已经有的参数文件来初始化。

3.7.4 fine-tuning 模型的三种状态

  1. 状态一:只预测,不训练。
    特点:相对快、简单,针对那些已经训练好,现在要实际对未知数据进行标注的项目,非常高效;

  2. 状态二:训练,但只训练最后分类层。
    特点:fine-tuning的模型最终的分类以及符合要求,现在只是在他们的基础上进行类别降维。

  3. 状态三:完全训练,分类层+之前卷积层都训练
    特点:跟状态二的差异很小,当然状态三比较耗时和需要训练GPU资源,不过非常适合fine-tuning到自己想要的模型里面,预测精度相比状态二也提高不少。

3.8 权重偏差初始化

3.8.1 全都初始化为 0

偏差初始化陷阱: 都初始化为 0。

产生陷阱原因:因为并不知道在训练神经网络中每一个权重最后的值,但是如果进行了恰当的数据归一化后,我们可以有理由认为有一半的权重是正的,另一半是负的。令所有权重都初始化为 0,如果神经网络计算出来的输出值是一样的,神经网络在进行反向传播算法计算出来的梯度值也一样,并且参数更新值也一样。更一般地说,如果权重初始化为同一个值,网络就是对称的。

形象化理解:在神经网络中考虑梯度下降的时候,设想你在爬山,但身处直线形的山谷中,两边是对称的山峰。由于对称性,你所在之处的梯度只能沿着山谷的方向,不会指向山峰;你走了一步之后,情况依然不变。结果就是你只能收敛到山谷中的一个极大值,而走不到山峰上去。

3.8.2 全都初始化为同样的值

​ 偏差初始化陷阱: 都初始化为一样的值。
​ 以一个三层网络为例:
首先看下结构

它的表达式为:

如果每个权重都一样,那么在多层网络中,从第二层开始,每一层的输入值都是相同的了也就是$ a1=a2=a3=…. $,既然都一样,就相当于一个输入了,为啥呢??

如果是反向传递算法(如果这里不明白请看上面的连接),其中的偏置项和权重项的迭代的偏导数计算公式如下

$ \delta $ 的计算公式

如果用的是 sigmoid 函数

把后两个公式代入,可以看出所得到的梯度下降法的偏导相同,不停的迭代,不停的相同,不停的迭代,不停的相同……,最后就得到了相同的值(权重和截距)。

3.8.3 初始化为小的随机数

​ 将权重初始化为很小的数字是一个普遍的打破网络对称性的解决办法。这个想法是,神经元在一开始都是随机的、独一无二的,所以它们会计算出不同的更新,并将自己整合到整个网络的各个部分。一个权重矩阵的实现可能看起来像 $ W=0.01∗np.random.randn(D,H) $,其中 randn 是从均值为 0 的单位标准高斯分布进行取样。通过这个公式(函数),每个神经元的权重向量初始化为一个从多维高斯分布取样的随机向量,所以神经元在输入空间中指向随机的方向(so the neurons point in random direction in the input space). 应该是指输入空间对于随机方向有影响)。其实也可以从均匀分布中来随机选取小数,但是在实际操作中看起来似乎对最后的表现并没有太大的影响。

​ 备注:并不是数字越小就会表现的越好。比如,如果一个神经网络层的权重非常小,那么在反向传播算法就会计算出很小的梯度(因为梯度 gradient 是与权重成正比的)。在网络不断的反向传播过程中将极大地减少“梯度信号”,并可能成为深层网络的一个需要注意的问题。

3.8.4 用 $ 1/\sqrt n $ 校准方差

​ 上述建议的一个问题是,随机初始化神经元的输出的分布有一个随输入量增加而变化的方差。结果证明,我们可以通过将其权重向量按其输入的平方根(即输入的数量)进行缩放,从而将每个神经元的输出的方差标准化到 1。也就是说推荐的启发式方法 (heuristic) 是将每个神经元的权重向量按下面的方法进行初始化: $ w=np.random.randn(n)/\sqrt n $,其中 n 表示输入的数量。这保证了网络中所有的神经元最初的输出分布大致相同,并在经验上提高了收敛速度。

3.8.5 稀疏初始化(Sparse Initialazation)

​ 另一种解决未校准方差问题的方法是把所有的权重矩阵都设为零,但是为了打破对称性,每个神经元都是随机连接地(从如上面所介绍的一个小的高斯分布中抽取权重)到它下面的一个固定数量的神经元。一个典型的神经元连接的数目可能是小到 10 个。

3.8.6 初始化偏差

​ 将偏差初始化为零是可能的,也是很常见的,因为非对称性破坏是由权重的小随机数导致的。因为 ReLU 具有非线性特点,所以有些人喜欢使用将所有的偏差设定为小的常数值如 0.01,因为这样可以确保所有的 ReLU 单元在最开始就激活触发(fire)并因此能够获得和传播一些梯度值。然而,这是否能够提供持续的改善还不太清楚(实际上一些结果表明这样做反而使得性能更加糟糕),所以更通常的做法是简单地将偏差初始化为 0.

3.9 学习率

3.9.1 学习率的作用

​ 在机器学习中,监督式学习通过定义一个模型,并根据训练集上的数据估计最优参数。梯度下降法是一个广泛被用来最小化模型误差的参数优化算法。梯度下降法通过多次迭代,并在每一步中最小化成本函数(cost 来估计模型的参数。学习率 (learning rate),在迭代过程中会控制模型的学习进度。

​ 在梯度下降法中,都是给定的统一的学习率,整个优化过程中都以确定的步长进行更新, 在迭代优化的前期中,学习率较大,则前进的步长就会较长,这时便能以较快的速度进行梯度下降,而在迭代优化的后期,逐步减小学习率的值,减小步长,这样将有助于算法的收敛,更容易接近最优解。故而如何对学习率的更新成为了研究者的关注点。
​ 在模型优化中,常用到的几种学习率衰减方法有:分段常数衰减、多项式衰减、指数衰减、自然指数衰减、余弦衰减、线性余弦衰减、噪声线性余弦衰减

3.9.2 学习率衰减常用参数有哪些

参数名称 参数说明
learning_rate 初始学习率
global_step 用于衰减计算的全局步数,非负,用于逐步计算衰减指数
decay_steps 衰减步数,必须是正值,决定衰减周期
decay_rate 衰减率
end_learning_rate 最低的最终学习率
cycle 学习率下降后是否重新上升
alpha 最小学习率
num_periods 衰减余弦部分的周期数
initial_variance 噪声的初始方差
variance_decay 衰减噪声的方差

3.9.3 分段常数衰减

​ 分段常数衰减需要事先定义好的训练次数区间,在对应区间置不同的学习率的常数值,一般情况刚开始的学习率要大一些,之后要越来越小,要根据样本量的大小设置区间的间隔大小,样本量越大,区间间隔要小一点。下图即为分段常数衰减的学习率变化图,横坐标代表训练次数,纵坐标代表学习率。

3.9.4 指数衰减

​ 以指数衰减方式进行学习率的更新,学习率的大小和训练次数指数相关,其更新规则为:

​ 这种衰减方式简单直接,收敛速度快,是最常用的学习率衰减方式,如下图所示,绿色的为学习率随
训练次数的指数衰减方式,红色的即为分段常数衰减,它在一定的训练区间内保持学习率不变。

3.9.5 自然指数衰减

​ 它与指数衰减方式相似,不同的在于它的衰减底数是$e$,故而其收敛的速度更快,一般用于相对比较
容易训练的网络,便于较快的收敛,其更新规则如下

​ 下图为为分段常数衰减、指数衰减、自然指数衰减三种方式的对比图,红色的即为分段常数衰减图,阶梯型曲线。蓝色线为指数衰减图,绿色即为自然指数衰减图,很明可以看到自然指数衰减方式下的学习率衰减程度要大于一般指数衰减方式,有助于更快的收敛。

3.9.6 多项式衰减

​ 应用多项式衰减的方式进行更新学习率,这里会给定初始学习率和最低学习率取值,然后将会按照
给定的衰减方式将学习率从初始值衰减到最低值,其更新规则如下式所示。

​ 需要注意的是,有两个机制,降到最低学习率后,到训练结束可以一直使用最低学习率进行更新,另一个是再次将学习率调高,使用 decay_steps 的倍数,取第一个大于 global_steps 的结果,如下式所示.它是用来防止神经网络在训练的后期由于学习率过小而导致的网络一直在某个局部最小值附近震荡,这样可以通过在后期增大学习率跳出局部极小值。

​ 如下图所示,红色线代表学习率降低至最低后,一直保持学习率不变进行更新,绿色线代表学习率衰减到最低后,又会再次循环往复的升高降低。

3.9.7 余弦衰减

​ 余弦衰减就是采用余弦的相关方式进行学习率的衰减,衰减图和余弦函数相似。其更新机制如下式所示:

​ 如下图所示,红色即为标准的余弦衰减曲线,学习率从初始值下降到最低学习率后保持不变。蓝色的线是线性余弦衰减方式曲线,它是学习率从初始学习率以线性的方式下降到最低学习率值。绿色噪声线性余弦衰减方式。

3.12 Dropout 系列问题

3.12.1 为什么要正则化?

  1. 深度学习可能存在过拟合问题——高方差,有两个解决方法,一个是正则化,另一个是准备更多的数据,这是非常可靠的方法,但你可能无法时时刻刻准备足够多的训练数据或者获取更多数据的成本很高,但正则化通常有助于避免过拟合或减少你的网络误差。
  2. 如果你怀疑神经网络过度拟合了数据,即存在高方差问题,那么最先想到的方法可能是正则化,另一个解决高方差的方法就是准备更多数据,这也是非常可靠的办法,但你可能无法时时准备足够多的训练数据,或者,获取更多数据的成本很高,但正则化有助于避免过度拟合,或者减少网络误差。

3.12.2 为什么正则化有利于预防过拟合?


左图是高偏差,右图是高方差,中间是Just Right,这几张图我们在前面课程中看到过。

3.12.3 理解dropout正则化

​ Dropout可以随机删除网络中的神经单元,它为什么可以通过正则化发挥如此大的作用呢?

​ 直观上理解:不要依赖于任何一个特征,因为该单元的输入可能随时被清除,因此该单元通过这种方式传播下去,并为单元的四个输入增加一点权重,通过传播所有权重,dropout将产生收缩权重的平方范数的效果,和之前讲的L2正则化类似;实施dropout的结果实它会压缩权重,并完成一些预防过拟合的外层正则化;L2对不同权重的衰减是不同的,它取决于激活函数倍增的大小。

3.12.4 dropout率的选择

  1. 经过交叉验证,隐含节点 dropout 率等于 0.5 的时候效果最好,原因是 0.5 的时候 dropout 随机生成的网络结构最多。
  2. dropout 也可以被用作一种添加噪声的方法,直接对 input 进行操作。输入层设为更接近 1 的数。使得输入变化不会太大(0.8)
  3. 对参数 $ w $ 的训练进行球形限制 (max-normalization),对 dropout 的训练非常有用。
  4. 球形半径 $ c $ 是一个需要调整的参数,可以使用验证集进行参数调优。
  5. dropout 自己虽然也很牛,但是 dropout、max-normalization、large decaying learning rates and high momentum 组合起来效果更好,比如 max-norm regularization 就可以防止大的learning rate 导致的参数 blow up。
  6. 使用 pretraining 方法也可以帮助 dropout 训练参数,在使用 dropout 时,要将所有参数都乘以 $ 1/p $。

3.12.5 dropout有什么缺点?

​ dropout一大缺点就是代价函数J不再被明确定义,每次迭代,都会随机移除一些节点,如果再三检查梯度下降的性能,实际上是很难进行复查的。定义明确的代价函数J每次迭代后都会下降,因为我们所优化的代价函数J实际上并没有明确定义,或者说在某种程度上很难计算,所以我们失去了调试工具来绘制这样的图片。我通常会关闭dropout函数,将keep-prob的值设为1,运行代码,确保J函数单调递减。然后打开dropout函数,希望在dropout过程中,代码并未引入bug。我觉得你也可以尝试其它方法,虽然我们并没有关于这些方法性能的数据统计,但你可以把它们与dropout方法一起使用。

3.13 深度学习中常用的数据增强方法?

(贡献者:黄钦建-华南理工大学)

  • Color Jittering:对颜色的数据增强:图像亮度、饱和度、对比度变化(此处对色彩抖动的理解不知是否得当);

  • PCA Jittering:首先按照RGB三个颜色通道计算均值和标准差,再在整个训练集上计算协方差矩阵,进行特征分解,得到特征向量和特征值,用来做PCA Jittering;

  • Random Scale:尺度变换;

  • Random Crop:采用随机图像差值方式,对图像进行裁剪、缩放;包括Scale Jittering方法(VGG及ResNet模型使用)或者尺度和长宽比增强变换;

  • Horizontal/Vertical Flip:水平/垂直翻转;

  • Shift:平移变换;

  • Rotation/Reflection:旋转/仿射变换;

  • Noise:高斯噪声、模糊处理;

  • Label Shuffle:类别不平衡数据的增广;

3.14 如何理解 Internal Covariate Shift?

(贡献者:黄钦建-华南理工大学)

​ 深度神经网络模型的训练为什么会很困难?其中一个重要的原因是,深度神经网络涉及到很多层的叠加,而每一层的参数更新会导致上层的输入数据分布发生变化,通过层层叠加,高层的输入分布变化会非常剧烈,这就使得高层需要不断去重新适应底层的参数更新。为了训好模型,我们需要非常谨慎地去设定学习率、初始化权重、以及尽可能细致的参数更新策略。

​ Google 将这一现象总结为 Internal Covariate Shift,简称 ICS。 什么是 ICS 呢?

​ 大家都知道在统计机器学习中的一个经典假设是“源空间(source domain)和目标空间(target domain)的数据分布(distribution)是一致的”。如果不一致,那么就出现了新的机器学习问题,如 transfer learning / domain adaptation 等。而 covariate shift 就是分布不一致假设之下的一个分支问题,它是指源空间和目标空间的条件概率是一致的,但是其边缘概率不同。

​ 大家细想便会发现,的确,对于神经网络的各层输出,由于它们经过了层内操作作用,其分布显然与各层对应的输入信号分布不同,而且差异会随着网络深度增大而增大,可是它们所能“指示”的样本标记(label)仍然是不变的,这便符合了covariate shift的定义。由于是对层间信号的分析,也即是“internal”的来由。

那么ICS会导致什么问题?

简而言之,每个神经元的输入数据不再是“独立同分布”。

其一,上层参数需要不断适应新的输入数据分布,降低学习速度。

其二,下层输入的变化可能趋向于变大或者变小,导致上层落入饱和区,使得学习过早停止。

其三,每层的更新都会影响到其它层,因此每层的参数更新策略需要尽可能的谨慎。

参考文献

[1] Rosenblatt, F. The perceptron: A probabilistic model for information storage and organization in the brain.[J]. Psychological Review, 1958, 65(6):386-408.

[2] Duvenaud D , Rippel O , Adams R P , et al. Avoiding pathologies in very deep networks[J]. Eprint Arxiv, 2014:202-210.

[3] Rumelhart D E, Hinton G E, Williams R J. Learning representations by back-propagating errors[J]. Cognitive modeling, 1988, 5(3): 1.

[4] Hecht-Nielsen R. Theory of the backpropagation neural network[M]//Neural networks for perception. Academic Press, 1992: 65-93.

[5] Felice M. Which deep learning network is best for you?| CIO[J]. 2017.

[6] Conneau A, Schwenk H, Barrault L, et al. Very deep convolutional networks for natural language processing[J]. arXiv preprint arXiv:1606.01781, 2016, 2.

[7] Ba J, Caruana R. Do deep nets really need to be deep?[C]//Advances in neural information processing systems. 2014: 2654-2662.

[8] Nielsen M A. Neural networks and deep learning[M]. USA: Determination press, 2015.

[9] Goodfellow I, Bengio Y, Courville A. Deep learning[M]. MIT press, 2016.

[10] 周志华. 机器学习[M].清华大学出版社, 2016.

[11] Kim J, Kwon Lee J, Mu Lee K. Accurate image super-resolution using very deep convolutional networks[C]//Proceedings of the IEEE conference on computer vision and pattern recognition. 2016: 1646-1654.

[12] Chen Y, Lin Z, Zhao X, et al. Deep learning-based classification of hyperspectral data[J]. IEEE Journal of Selected topics in applied earth observations and remote sensing, 2014, 7(6): 2094-2107.

[13] Domhan T, Springenberg J T, Hutter F. Speeding up automatic hyperparameter optimization of deep neural networks by extrapolation of learning curves[C]//Twenty-Fourth International Joint Conference on Artificial Intelligence. 2015.

[14] Maclaurin D, Duvenaud D, Adams R. Gradient-based hyperparameter optimization through reversible learning[C]//International Conference on Machine Learning. 2015: 2113-2122.

[15] Srivastava R K, Greff K, Schmidhuber J. Training very deep networks[C]//Advances in neural information processing systems. 2015: 2377-2385.

[16] Bergstra J, Bengio Y. Random search for hyper-parameter optimization[J]. Journal of Machine Learning Research, 2012, 13(Feb): 281-305.

[17] Ngiam J, Khosla A, Kim M, et al. Multimodal deep learning[C]//Proceedings of the 28th international conference on machine learning (ICML-11). 2011: 689-696.

[18] Deng L, Yu D. Deep learning: methods and applications[J]. Foundations and Trends® in Signal Processing, 2014, 7(3–4): 197-387.

[19] Erhan D, Bengio Y, Courville A, et al. Why does unsupervised pre-training help deep learning?[J]. Journal of Machine Learning Research, 2010, 11(Feb): 625-660.

[20] Dong C, Loy C C, He K, et al. Learning a deep convolutional network for image super resolution[C]//European conference on computer vision. Springer, Cham, 2014: 184-199.

[21] 郑泽宇,梁博文,顾思宇.TensorFlow:实战Google深度学习框架(第2版)[M].电子工业出版社,2018.

[22] 焦李成. 深度学习优化与识别[M].清华大学出版社,2017.

[23] 吴岸城. 神经网络与深度学习[M].电子工业出版社,2016.

[24] Wei, W.G.H., Liu, T., Song, A., et al. (2018) An Adaptive Natural Gradient Method with Adaptive Step Size in Multilayer Perceptrons. Chinese Automation Congress, 1593-1597.

[25] Y Feng, Y Li.An Overview of Deep Learning Optimization Methods and Learning Rate Attenuation Methods[J].Hans Journal of Data Mining,2018,8(4),186-200.

第十七章 模型压缩及移动端部署

​ 深度神经网络在人工智能的应用中,包括语音识别、计算机视觉、自然语言处理等各方面,在取得巨大成功的同时,这些深度神经网络需要巨大的计算开销和内存开销,严重阻碍了资源受限下的使用。本章总结了模型压缩、加速一般原理和方法,以及在移动端如何部署。

17.1 模型压缩理解

​ 模型压缩是指利用数据集对已经训练好的深度模型进行精简,进而得到一个轻量且准确率相当的网络,压缩后的网络具有更小的结构和更少的参数,可以有效降低计算和存储开销,便于部署再受限的硬件环境中。

17.2 为什么需要模型压缩和加速?

(1)随着AI技术的飞速发展,越来越多的公司希望在自己的移动端产品中注入AI能力。

(2)对于在线学习和增量学习等实时应用而言,如何减少含有大量层级及结点的大型神经网络所需要的内存和计算量显得极为重要。

(3)模型的参数在一定程度上能够表达其复杂性,相关研究表明,并不是所有的参数都在模型中发挥作用,部分参数作用有限、表达冗余,甚至会降低模型的性能。

(4)复杂的模型固然具有更好的性能,但是高额的存储空间、计算资源消耗是使其难以有效的应用在各硬件平台上的重要原因。

(5)智能设备的流行提供了内存、CPU、能耗和宽带等资源,使得深度学习模型部署在智能移动设备上变得可行。
(6)高效的深度学习方法可以有效的帮助嵌入式设备、分布式系统完成复杂工作,在移动端部署深度学习有很重要的意义。

17.3 模型压缩的必要性及可行性

必要性 首先是资源受限,其次在许多网络结构中,如VGG-16网络,参数数量1亿3千多万,占用500MB空间,需要进行309亿次浮点运算才能完成一次图像识别任务。
可行性 模型的参数在一定程度上能够表达其复杂性,相关研究表明,并不是所有的参数都在模型中发挥作用,部分参数作用有限、表达冗余,甚至会降低模型的性能。论文提出,很多的深度神经网络仅仅使用很少一部分(5%)权值就足以预测剩余的权值。该论文还提出这些剩下的权值甚至可以直接不用被学习。也就是说,仅仅训练一小部分原来的权值参数就有可能达到和原来网络相近甚至超过原来网络的性能(可以看作一种正则化)。
最终目的 最大程度的减小模型复杂度,减少模型存储需要的空间,也致力于加速模型的训练和推测

17.4 目前有哪些深度学习模型压缩方法?

​ 目前深度学习模型压缩方法主要分为更精细化模型设计、模型裁剪、核的稀疏化、量化、低秩分解、迁移学习等方法,而这些方法又可分为前端压缩和后端压缩。

17.4.1 前端压缩和后端压缩对比

对比项目 前端压缩 后端压缩
含义 不会改变原始网络结构的压缩技术 会大程度上改变原始网络结构的压缩技术
主要方法 知识蒸馏、紧凑的模型结构设计、滤波器层面的剪枝 低秩近似、未加限制的剪枝、参数量化、二值网络
实现难度 较简单 较难
是否可逆 可逆 不可逆
成熟应用 剪枝 低秩近似、参数量化
待发展应用 知识蒸馏 二值网络

17.4.2 网络剪枝

深度学习模型因其稀疏性,可以被裁剪为结构精简的网络模型,具体包括结构性剪枝与非结构性剪枝。

事项 特点 举例
非结构化剪枝 通常是连接级、细粒度的剪枝方法,精度相对较高,但依赖于特定算法库或硬件平台的支持 Deep Compression [5], Sparse-Winograd [6] 算法等;
结构化剪枝 是filter级或layer级、粗粒度的剪枝方法,精度相对较低,但剪枝策略更为有效,不需要特定算法库或硬件平台的支持,能够直接在成熟深度学习框架上运行。 如局部方式的、通过layer by layer方式的、最小化输出FM重建误差的Channel Pruning [7], ThiNet [8], Discrimination-aware Channel Pruning [9];全局方式的、通过训练期间对BN层Gamma系数施加L1正则约束的Network Slimming [10];全局方式的、按Taylor准则对Filter作重要性排序的Neuron Pruning [11];全局方式的、可动态重新更新pruned filters参数的剪枝方法 [12];
https://blog.csdn.net/baidu_31437863/article/details/84474847

如果按剪枝粒度分,从粗到细,可分为中间隐含层剪枝、通道剪枝、卷积核剪枝、核内剪枝、单个权重剪枝。下面按照剪枝粒度的分类从粗(左)到细(右)。

(a)层间剪枝 (b)特征图剪枝 (c)k*k核剪枝 (d)核内剪枝

事项 特点
单个权重粒度 早期 Le Cun[16]提出的 OBD(optimal brain damage)将网络中的任意权重参数都看作单个参数,能够有效地提高预测准确率,却不能减小运行时间;同时,剪枝代价过高,只适用于小网络
核内权重粒度 网络中的任意权重被看作是单个参数并进行随机非结构化剪枝,该粒度的剪枝导致网络连接不规整,需要通过稀疏表达来减少内存占用,进而导致在前向传播预测时,需要大量的条件判断和额外空间来标明零或非零参数的位置,因此不适用于并行计算
卷积核粒度与通道粒度 卷积核粒度与通道粒度属于粗粒度剪枝,不依赖任何稀疏卷积计算库及专用硬件;同时,能够在获得高压缩率的同时大量减小测试阶段的计算时间.由

从剪枝目标上分类,可分为减少参数/网络复杂度、减少过拟合/增加泛化能力/提高准确率、减小部署运行时间/提高网络效率及减小训练时间等。

17.4.3 典型剪枝方法对比

剪枝方法 修剪对象 修剪方式 效果
Deep Compression 权重 随机修剪 50倍压缩
Structured Pruning 权重 组稀疏+排他性稀疏 性能提升
Network Slimming 特征图通道 根据尺度因子修剪 节省计算资源
mProp 梯度 修剪幅值小的梯度 加速

17.4.4 网络蒸馏

​ 网络精馏是指利用大量未标记的迁移数据(transfer data),让小模型去拟合大模型,从而让小模型学到与大模型相似的函数映射.网络精馏可以看成在同一个域上迁移学习[34]的一种特例,目的是获得一个比原模型更为精简的网络,整体的框架图如图 4所示.

17.4.5 前端压缩

(1)知识蒸馏

​ 一个复杂模型可由多个简单模型或者强约束条件训练得到。复杂模型特点是性能好,但其参数量大,计算效率低。小模型特点是计算效率高,但是其性能较差。知识蒸馏是让复杂模型学习到的知识迁移到小模型当中,使其保持其快速的计算速度前提下,同时拥有复杂模型的性能,达到模型压缩的目的。
(2)紧凑的模型结构设计
​ 紧凑的模型结构设计主要是对神经网络卷积的方式进行改进,比如使用两个3x3的卷积替换一个5x5的卷积、使用深度可分离卷积等等方式降低计算参数量。 目前很多网络基于模块化设计思想,在深度和宽度两个维度上都很大,导致参数冗余。因此有很多关于模型设计的研究,如SqueezeNet、MobileNet等,使用更加细致、高效的模型设计,能够很大程度的减少模型尺寸,并且也具有不错的性能。
(3)滤波器层面的剪枝
​ 滤波器层面的剪枝属于非结构花剪枝,主要是对较小的权重矩阵整个剔除,然后对整个神经网络进行微调。此方式由于剪枝过于粗放,容易导致精度损失较大,而且部分权重矩阵中会存留一些较小的权重造成冗余,剪枝不彻底。 具体操作是在训练时使用稀疏约束(加入权重的稀疏正则项,引导模型的大部分权重趋向于0)。完成训练后,剪去滤波器上的这些 0 。

​ 优点是简单,缺点是剪得不干净,非结构化剪枝会增加内存访问成本。

17.4.6 后端压缩

(1)低秩近似
​ 在卷积神经网络中,卷积运算都是以矩阵相乘的方式进行。对于复杂网络,权重矩阵往往非常大,非常消耗存储和计算资源。低秩近似就是用若干个低秩矩阵组合重构大的权重矩阵,以此降低存储和计算资源消耗。

事项 特点
优点 可以降低存储和计算消耗;
一般可以压缩2-3倍;精度几乎没有损失;
缺点 模型越复杂,权重矩阵越大,利用低秩近似重构参数矩阵不能保证模型的性能 ;
超参数的数量随着网络层数的增加呈线性变化趋势,例如中间层的特征通道数等等。
随着模型复杂度的提升,搜索空间急剧增大。

(2)未加限制的剪枝

​ 完成训练后,不加限制地剪去那些冗余参数。

事项 特点
优点 保持模型性能不损失的情况下,减少参数量9-11倍;
剔除不重要的权重,可以加快计算速度,同时也可以提高模型的泛化能力;
缺点 极度依赖专门的运行库和特殊的运行平台,不具有通用性;
压缩率过大时,破坏性能;

(3)参数量化

​ 神经网络的参数类型一般是32位浮点型,使用较小的精度代替32位所表示的精度。或者是将多个权重映射到同一数值,权重共享。量化其实是一种权值共享的策略。量化后的权值张量是一个高度稀疏的有很多共享权值的矩阵,对非零参数,我们还可以进行定点压缩,以获得更高的压缩率。

事项 特点
优点 模型性能损失很小,大小减少8-16倍;
缺点 压缩率大时,性能显著下降;
依赖专门的运行库,通用性较差;
举例 二值化网络:XNORnet [13], ABCnet with Multiple Binary Bases [14],
Bin-net with High-Order Residual Quantization [15], Bi-Real Net [16];
三值化网络:Ternary weight networks [17], Trained Ternary Quantization [18];

W1-A8 或 W2-A8量化: Learning Symmetric Quantization [19];
INT8量化:TensorFlow-lite [20], TensorRT [21];
其他(非线性):Intel INQ [22], log-net, CNNPack [23] 等;
原文:https://blog.csdn.net/baidu_31437863/article/details/84474847 |
| 总结 | 最为典型就是二值网络、XNOR网络等。其主要原理就是采用1bit对网络的输入、权重、响应进行编码。减少模型大小的同时,原始网络的卷积操作可以被bit-wise运算代替,极大提升了模型的速度。但是,如果原始网络结果不够复杂(模型描述能力),由于二值网络会较大程度降低模型的表达能力。因此现阶段有相关的论文开始研究n-bit编码方式成为n值网络或者多值网络或者变bit、组合bit量化来克服二值网络表达能力不足的缺点。 |

(4)二值网络

​ 相对量化更为极致,对于32bit浮点型数用1bit二进制数-1或者1表示,可大大减小模型尺寸。

事项 特点
优点 网络体积小,运算速度快,有时可避免部分网络的overfitting
缺点 二值神经网络损失的信息相对于浮点精度是非常大;
粗糙的二值化近似导致训练时模型收敛速度非常慢

(5)三值网络

事项 特点
优点 相对于二值神经网络,三值神经网络(Ternary Weight Networks)在同样的模型结构下可以达到成百上千倍的表达能力提升;并且,在计算时间复杂度上,三元网络和二元网络的计算复杂度是一样的。
例如,对于ResNet-18层网络中最常出现的卷积核(3x3大小),二值神经网络模型最多可以表达2的3x3次方(=512)种结构,而三元神经网络则可以表达3的3x3次方(=19683)种卷积核结构。在表达能力上,三元神经网络相对要高19683/512 = 38倍。因此,三元神经网络模型能够在保证计算复杂度很低的情况下大幅的提高网络的表达能力,进而可以在精度上相对于二值神经网络有质的飞跃。另外,由于对中间信息的保存更多,三元神经网络可以极大的加快网络训练时的收敛速度,从而更快、更稳定的达到最优的结果。

17.4.6 低秩分解

基于低秩分解的深度神经网络压缩与加速的核心思想是利用矩阵或张量分解技术估计并分解深度模型中的原始卷积核.卷积计算是整个卷积神经网络中计算复杂 度 最 高 的 计 算 操 作,通 过 分 解4D 卷积核张量,可以有效地减少模型内部的冗余性.此外对于2D的全 连 接 层 矩 阵 参 数,同样可以利用低秩分解技术进行处理.但由于卷积层与全连接层的分解方式不同,本文分别从卷积层和全连接层2个不同角度回顾与分析低秩分解技术在深度神经网络中的应用.

在2013年,Denil等人[57]从理论上利用低秩分解的技术并分析了深度神经网络存在大量的冗余信
息,开创了基于低秩分解的深度网络模型压缩与加速的新思路.如图7所示,展示了主流的张量分解后卷积 计 算.

(出自《深度神经网络压缩与加速综述》)

17.4.7 总体压缩效果评价指标有哪些?

​ 网络压缩评价指标包括运行效率、参数压缩率、准确率.与基准模型比较衡量性能提升时,可以使用提升倍数(speedup)或提升比例(ratio)。

评价指标 特点
准确率 目前,大部分研究工作均会测量 Top-1 准确率,只有在 ImageNet 这类大型数据集上才会只用 Top-5 准确率.为方便比较
参数压缩率 统计网络中所有可训练的参数,根据机器浮点精度转换为字节(byte)量纲,通常保留两位有效数字以作近似估计.
运行效率 可以从网络所含浮点运算次数(FLOP)、网络所含乘法运算次数(MULTS)或随机实验测得的网络平均前向传播所需时间这 3 个角度来评价

17.4.8 几种轻量化网络结构对比

网络结构 TOP1 准确率/% 参数量/M CPU运行时间/ms
MobileNet V1 70.6 4.2 123
ShuffleNet(1.5) 69.0 2.9 -
ShuffleNet(x2) 70.9 4.4 -
MobileNet V2 71.7 3.4 80
MobileNet V2(1.4) 74.7 6.9 149

17.4.9 网络压缩未来研究方向有哪些?

网络剪枝、网络精馏和网络分解都能在一定程度上实现网络压缩的目的.回归到深度网络压缩的本质目的上,即提取网络中的有用信息,以下是一些值得研究和探寻的方向.
(1) 权重参数对结果的影响度量.深度网络的最终结果是由全部的权重参数共同作用形成的,目前,关于单个卷积核/卷积核权重的重要性的度量仍然是比较简单的方式,尽管文献[14]中给出了更为细节的分析,但是由于计算难度大,并不实用.因此,如何通过更有效的方式来近似度量单个参数对模型的影响,具有重要意义.
(2) 学生网络结构的构造.学生网络的结构构造目前仍然是由人工指定的,然而,不同的学生网络结构的训练难度不同,最终能够达到的效果也有差异.因此,如何根据教师网络结构设计合理的网络结构在精简模型的条件下获取较高的模型性能,是未来的一个研究重点.
(3) 参数重建的硬件架构支持.通过分解网络可以无损地获取压缩模型,在一些对性能要求高的场景中是非常重要的.然而,参数的重建步骤会拖累预测阶段的时间开销,如何通过硬件的支持加速这一重建过程,将是未来的一个研究方向.
(4) 任务或使用场景层面的压缩.大型网络通常是在量级较大的数据集上训练完成的,比如,在 ImageNet上训练的模型具备对 1 000 类物体的分类,但在一些具体场景的应用中,可能仅需要一个能识别其中几类的小型模型.因此,如何从一个全功能的网络压缩得到部分功能的子网络,能够适应很多实际应用场景的需求.
(5) 网络压缩效用的评价.目前,对各类深度网络压缩算法的评价是比较零碎的,侧重于和被压缩的大型网络在参数量和运行时间上的比较.未来的研究可以从提出更加泛化的压缩评价标准出发,一方面平衡运行速度和模型大小在不同应用场景下的影响;另一方面,可以从模型本身的结构性出发,对压缩后的模型进行评价.

(出自《深度网络模型压缩综述》)

17.5 目前有哪些深度学习模型优化加速方法?

https://blog.csdn.net/nature553863/article/details/81083955

17.5.1 模型优化加速方法

模型优化加速能够提升网络的计算效率,具体包括:
(1)Op-level的快速算法:FFT Conv2d (7x7, 9x9), Winograd Conv2d (3x3, 5x5) 等;
(2)Layer-level的快速算法:Sparse-block net [1] 等;
(3)优化工具与库:TensorRT (Nvidia), Tensor Comprehension (Facebook) 和 Distiller (Intel) 等;

原文:https://blog.csdn.net/nature553863/article/details/81083955

17.5.2 TensorRT加速原理

https://blog.csdn.net/xh_hit/article/details/79769599

​ 在计算资源并不丰富的嵌入式设备上,TensorRT之所以能加速神经网络的的推断主要得益于两点:

  • 首先是TensorRT支持int8和fp16的计算,通过在减少计算量和保持精度之间达到一个理想的trade-off,达到加速推断的目的。

  • 更为重要的是TensorRT对于网络结构进行了重构和优化,主要体现在一下几个方面。

    (1) TensorRT通过解析网络模型将网络中无用的输出层消除以减小计算。

    (2) 对于网络结构的垂直整合,即将目前主流神经网络的Conv、BN、Relu三个层融合为了一个层,例如将图1所示的常见的Inception结构重构为图2所示的网络结构。

    (3) 对于网络结构的水平组合,水平组合是指将输入为相同张量和执行相同操作的层融合一起,例如图2向图3的转化。

​ 以上3步即是TensorRT对于所部署的深度学习网络的优化和重构,根据其优化和重构策略,第一和第二步适用于所有的网络架构,但是第三步则对于含有Inception结构的神经网络加速效果最为明显。

​ Tips: 想更好地利用TensorRT加速网络推断,可在基础网络中多采用Inception模型结构,充分发挥TensorRT的优势。

17.5.3 TensorRT如何优化重构模型?

条件 方法
若训练的网络模型包含TensorRT支持的操作 1、对于Caffe与TensorFlow训练的模型,若包含的操作都是TensorRT支持的,则可以直接由TensorRT优化重构
2、对于MXnet, PyTorch或其他框架训练的模型,若包含的操作都是TensorRT支持的,可以采用TensorRT API重建网络结构,并间接优化重构;
若训练的网络模型包含TensorRT不支持的操作 1、TensorFlow模型可通过tf.contrib.tensorrt转换,其中不支持的操作会保留为TensorFlow计算节点;
2、不支持的操作可通过Plugin API实现自定义并添加进TensorRT计算图;
3、将深度网络划分为两个部分,一部分包含的操作都是TensorRT支持的,可以转换为TensorRT计算图。另一部则采用其他框架实现,如MXnet或PyTorch;

17.5.4 TensorRT加速效果如何?

以下是在TitanX (Pascal)平台上,TensorRT对大型分类网络的优化加速效果:

Network Precision Framework/GPU:TitanXP Avg.Time(Batch=8,unit:ms) Top1 Val.Acc.(ImageNet-1k)
Resnet50 fp32 TensorFlow 24.1 0.7374
Resnet50 fp32 MXnet 15.7 0.7374
Resnet50 fp32 TRT4.0.1 12.1 0.7374
Resnet50 int8 TRT4.0.1 6 0.7226
Resnet101 fp32 TensorFlow 36.7 0.7612
Resnet101 fp32 MXnet 25.8 0.7612
Resnet101 fp32 TRT4.0.1 19.3 0.7612
Resnet101 int8 TRT4.0.1 9 0.7574

17.6 影响神经网络速度的4个因素(再稍微详细一点)

  1. FLOPs(FLOPs就是网络执行了多少multiply-adds操作);

  2. MAC(内存访问成本);

  3. 并行度(如果网络并行度高,速度明显提升);

  4. 计算平台(GPU,ARM)

17.7 压缩和加速方法如何选择?

​ 1)对于在线计算内存存储有限的应用场景或设备,可以选择参数共享和参数剪枝方法,特别是二值量化权值和激活、结构化剪枝.其他方法虽然能够有效的压缩模型中的权值参数,但无法减小计算中隐藏的内存大小(如特征图).
​ 2)如果在应用中用到的紧性模型需要利用预训练模型,那么参数剪枝、参数共享以及低秩分解将成为首要考虑的方法.相反地,若不需要借助预训练模型,则可以考虑紧性滤波设计及知识蒸馏方法.
​ 3)若需要一次性端对端训练得到压缩与加速后模型,可以利用基于紧性滤波设计的深度神经网络压缩与加速方法.
​ 4)一般情况下,参数剪枝,特别是非结构化剪枝,能大大压缩模型大小,且不容易丢失分类精度.对于需要稳定的模型分类的应用,非结构化剪枝成为首要选择.
​ 5)若采用的数据集较小时,可以考虑知识蒸馏方法.对于小样本的数据集,学生网络能够很好地迁移教师模型的知识,提高学生网络的判别性.
​ 6)主流的5个深度神经网络压缩与加速算法相互之间是正交的,可以结合不同技术进行进一步的压缩与加速.如:韩 松 等 人[30]结合了参数剪枝和参数共享;温伟等人[64]以及 Alvarez等人[85]结合了参数剪枝和低秩分解.此外对于特定的应用场景,如目标检测,可以对卷积层和全连接层使用不同的压缩与加速技术分别处理.

参考《深度神经网络压缩与加速综述》

17.8 改变网络结构设计为什么会实现模型压缩、加速?

17.8.1 Group convolution

​ Group convolution最早出现在AlexNet中,是为了解决单卡显存不够,将网络部署到多卡上进行训练而提出。Group convolution可以减少单个卷积1/g的参数量。如何计算的呢?

​ 假设

  • 输入特征的的维度为$H\times W\times C_1$;
  • 卷积核的维度为$H_1\times W_1\times C_1$,共$C_2$个;
  • 输出特征的维度为$H_1\times W_1\times C_2$ 。

传统卷积计算方式如下:
image
传统卷积运算量为:

Group convolution是将输入特征的维度c1分成g份,每个group对应的channel数为c1/g,特征维度H * W * c1/g;,每个group对应的卷积核的维度也相应发生改变为h1 * w1 * c1/g,共c2/g个;每个group相互独立运算,最后将结果叠加在一起。
Group convolution计算方式如下:
image
Group convolution运算量为:

Group卷积相对于传统卷积的运算量:

由此可知:group卷积相对于传统卷积减少了1/g的参数量。

17.8.2. Depthwise separable convolution

Depthwise separable convolution是由depthwise conv和pointwise conv构成。
depthwise conv(DW)有效减少参数数量并提升运算速度。但是由于每个feature map只被一个卷积核卷积,因此经过DW输出的feature map不能包含输入特征图的全部信息,而且特征之间的信息不能进行交流,导致“信息流通不畅”。
pointwise conv(PW)实现通道特征信息交流,解决DW卷积导致“信息流通不畅”的问题。
假设输入特征的的维度为H * W * c1;卷积核的维度为h1 * w1 * c1,共c2个;输出特征的维度为 H1 * W1 * c2。
传统卷积计算方式如下:
image
传统卷积运算量为:

DW卷积的计算方式如下:
image
DW卷积运算量为:

PW卷积的计算方式如下:
image

Depthwise separable convolution运算量为:

Depthwise separable convolution相对于传统卷积的运算量:

由此可知,随着卷积通道数的增加,Depthwise separable convolution的运算量相对于传统卷积更少。

17.8.3 输入输出的channel相同时,MAC最小

卷积层的输入和输出特征通道数相等时MAC最小,此时模型速度最快。
假设feature map的大小为h*w,输入通道$c_1$,输出通道$c_2$。
已知:

根据均值不等式得到(c1-c2)^2>=0,等式成立的条件是c1=c2,也就是输入特征通道数和输出特征通道数相等时,在给定FLOPs前提下,MAC达到取值的下界。

17.8.4 减少组卷积的数量

过多的group操作会增大MAC,从而使模型速度变慢
由以上公式可知,group卷积想比与传统的卷积可以降低计算量,提高模型的效率;如果在相同的FLOPs时,group卷积为了满足FLOPs会是使用更多channels,可以提高模型的精度。但是随着channel数量的增加,也会增加MAC。
FLOPs:

MAC:

由MAC,FLOPs可知:

当FLOPs固定(B不变)时,g越大,MAC越大。

17.8.5 减少网络碎片化程度(分支数量)

模型中分支数量越少,模型速度越快
此结论主要是由实验结果所得。
以下为网络分支数和各分支包含的卷积数目对神经网络速度的影响。
image
实验中使用的基本网络结构,分别将它们重复10次,然后进行实验。实验结果如下:
image
由实验结果可知,随着网络分支数量的增加,神经网络的速度在降低。网络碎片化程度对GPU的影响效果明显,对CPU不明显,但是网络速度同样在降低。

17.8.7 减少元素级操作

元素级操作所带来的时间消耗也不能忽视
ReLU ,Tensor 相加,Bias相加的操作,分离卷积(depthwise convolution)都定义为元素级操作。
FLOPs大多数是对于卷积计算而言的,因为元素级操作的FLOPs相对要低很多。但是过的元素级操作也会带来时间成本。ShuffleNet作者对ShuffleNet v1和MobileNet v2的几种层操作的时间消耗做了分析,发现元素级操作对于网络速度的影响也很大。
image

17.9 常用的轻量级网络有哪些?

17.9.1 SequeezeNet

SqueenzeNet出自F. N. Iandola, S.Han等人发表的论文《SqueezeNet: AlexNet-level accuracy with 50x fewer parameters and < 0.5MB model size》,作者在保证精度不损失的同时,将原始AlexNet压缩至原来的510倍。

1.1 设计思想

在网络结构设计方面主要采取以下三种方式:

  • 用1*1卷积核替换3*3卷积
    • 理论上一个1*1卷积核的参数是一个3*3卷积核的1/9,可以将模型尺寸压缩9倍。
  • 减小3*3卷积的输入通道数
    • 根据上述公式,减少输入通道数不仅可以减少卷积的运算量,而且输入通道数与输出通道数相同时还可以减少MAC。
  • 延迟降采样
    • 分辨率越大的输入能够提供更多特征的信息,有利于网络的训练判断,延迟降采样可以提高网络精度。

      1.2 网络架构

      SqueezeNet提出一种多分支结构——fire model,其中是由Squeeze层和expand层构成。Squeeze层是由s1个1*1卷积组成,主要是通过1*1的卷积降低expand层的输入维度;expand层利用e1个1*1和e3个3*3卷积构成多分支结构提取输入特征,以此提高网络的精度(其中e1=e3=4*s1)。
      image
      SqueezeNet整体网络结构如下图所示:
      image

1.3实验结果

不同压缩方法在ImageNet上的对比实验结果
image
由实验结果可知,SqueezeNet不仅保证了精度,而且将原始AlexNet从240M压缩至4.8M,压缩50倍,说明此轻量级网络设计是可行。

17.9.2 MobileNet

MobileNet 是Google团队于CVPR-2017的论文《MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications》中针对手机等嵌入式设备提出的一种轻量级的深层神经网络,该网络结构在VGG的基础上使用DW+PW的组合,在保证不损失太大精度的同时,降低模型参数量。

2.1 设计思想

  • 采用深度可分离卷积代替传统卷积
    • 采用DW卷积在减少参数数量的同时提升运算速度。但是由于每个feature map只被一个卷积核卷积,因此经过DW输出的feature map不能只包含输入特征图的全部信息,而且特征之间的信息不能进行交流,导致“信息流通不畅”。
    • 采用PW卷积实现通道特征信息交流,解决DW卷积导致“信息流通不畅”的问题。
  • 使用stride=2的卷积替换pooling
    • 直接在卷积时利用stride=2完成了下采样,从而节省了需要再去用pooling再去进行一次下采样的时间,可以提升运算速度。同时,因为pooling之前需要一个stride=1的 conv,而与stride=2 conv的计算量想比要高近4倍(个人理解)。

      2.2 网络架构

  • DW conv和PW conv
    MobileNet的网络架构主要是由DW conv和PW conv组成,相比于传统卷积可以降低$\dfrac{1}{N} + \dfrac{1}{Dk}​$倍的计算量。
    标准卷积与DW conv和PW conv如图所示:
    image
    深度可分离卷积与传统卷积运算量对比:
    image
    网络结构:
    image

  • MobileNets的架构
    image

2.3 实验结果

image
由上表可知,使用相同的结构,深度可分离卷积虽然准确率降低1%,但是参数量减少了6/7。

17.9.3 MobileNet-v2

MobileNet-V2是2018年1月公开在arXiv上论文《Inverted Residuals and Linear Bottlenecks: Mobile Networks for Classification, Detection and Segmentation》,是对MobileNet-V1的改进,同样是一个轻量化卷积神经网络。

3.1 设计思想

  • 采用Inverted residuals
    • 为了保证网络可以提取更多的特征,在residual block中第一个1*1 Conv和3*3 DW Conv之前进行通道扩充
  • Linear bottlenecks
    • 为了避免Relu对特征的破坏,在residual block的Eltwise sum之前的那个 1*1 Conv 不再采用Relu
  • stride=2的conv不使用short-cut,stride=1的conv使用short-cut

3.2 网络架构

  • Inverted residuals
    ResNet中Residuals block先经过1*1的Conv layer,把feature map的通道数降下来,再经过3*3 Conv layer,最后经过一个1*1 的Conv layer,将feature map 通道数再“扩张”回去。即采用先压缩,后扩张的方式。而 inverted residuals采用先扩张,后压缩的方式。
    MobileNet采用DW conv提取特征,由于DW conv本身提取的特征数就少,再经过传统residuals block进行“压缩”,此时提取的特征数会更少,因此inverted residuals对其进行“扩张”,保证网络可以提取更多的特征。
    image
  • Linear bottlenecks
    ReLu激活函数会破坏特征。ReLu对于负的输入,输出全为0,而本来DW conv特征通道已经被“压缩”,再经过ReLu的话,又会损失一部分特征。采用Linear,目的是防止Relu破坏特征。
    image
  • shortcut
    stride=2的conv不使用short-cut,stride=1的conv使用short-cut
    image
  • 网络架构
    image

17.9.4 Xception

Xception是Google提出的,arXiv 的V1 于2016年10月公开《Xception: Deep Learning with Depthwise Separable Convolutions 》,Xception是对Inception v3的另一种改进,主要是采用depthwise separable convolution来替换原来Inception v3中的卷积操作。

4.1设计思想

  • 采用depthwise separable convolution来替换原来Inception v3中的卷积操作
    与原版的Depth-wise convolution有两个不同之处:
    • 第一个:原版Depth-wise convolution,先逐通道卷积,再11卷积; 而Xception是反过来,先1\1卷积,再逐通道卷积;
    • 第二个:原版Depth-wise convolution的两个卷积之间是不带激活函数的,而Xception在经过1*1卷积之后会带上一个Relu的非线性激活函数;

4.2网络架构

feature map在空间和通道上具有一定的相关性,通过Inception模块和非线性激活函数实现通道之间的解耦。增多3*3的卷积的分支的数量,使它与1*1的卷积的输出通道数相等,此时每个3*3的卷积只作用与一个通道的特征图上,作者称之为“极致的Inception(Extream Inception)”模块,这就是Xception的基本模块。
image

17.9.5 ShuffleNet-v1

ShuffleNet 是Face++团队提出的,晚于MobileNet两个月在arXiv上公开《ShuffleNet: An Extremely Efficient Convolutional Neural Network for Mobile Devices 》用于移动端前向部署的网络架构。ShuffleNet基于MobileNet的group思想,将卷积操作限制到特定的输入通道。而与之不同的是,ShuffleNet将输入的group进行打散,从而保证每个卷积核的感受野能够分散到不同group的输入中,增加了模型的学习能力。

5.1 设计思想

  • 采用group conv减少大量参数
    • roup conv与DW conv存在相同的“信息流通不畅”问题
  • 采用channel shuffle解决上述问题
    • MobileNet中采用PW conv解决上述问题,SheffleNet中采用channel shuffle
  • 采用concat替换add操作
    • avg pooling和DW conv(s=2)会减小feature map的分辨率,采用concat增加通道数从而弥补分辨率减小而带来信息的损失

5.2 网络架构

MobileNet中1*1卷积的操作占据了约95%的计算量,所以作者将1*1也更改为group卷积,使得相比MobileNet的计算量大大减少。
image
group卷积与DW存在同样使“通道信息交流不畅”的问题,MobileNet中采用PW conv解决上述问题,SheffleNet中采用channel shuffle。
ShuffleNet的shuffle操作如图所示
image
avg pooling和DW conv(s=2)会减小feature map的分辨率,采用concat增加通道数从而弥补分辨率减小而带来信息的损失;实验表明:多多使用通道(提升通道的使用率),有助于提高小模型的准确率。
image
网络结构:
image

17.9.6 ShuffleNet-v2

huffleNet-v2 是Face++团队提出的《ShuffleNet V2: Practical Guidelines for Ecient CNN Architecture Design》,旨在设计一个轻量级但是保证精度、速度的深度网络。

6.1 设计思想

  • 文中提出影响神经网络速度的4个因素:
    • a. FLOPs(FLOPs就是网络执行了多少multiply-adds操作)
    • b. MAC(内存访问成本)
    • c. 并行度(如果网络并行度高,速度明显提升)
    • d. 计算平台(GPU,ARM)
  • ShuffleNet-v2 提出了4点网络结构设计策略:
    • G1.输入输出的channel相同时,MAC最小
    • G2.过度的组卷积会增加MAC
    • G3.网络碎片化会降低并行度
    • G4.元素级运算不可忽视

6.2 网络结构

depthwise convolution 和 瓶颈结构增加了 MAC,用了太多的 group,跨层连接中的 element-wise Add 操作也是可以优化的点。所以在 shuffleNet V2 中增加了几种新特性。
所谓的 channel split 其实就是将通道数一分为2,化成两分支来代替原先的分组卷积结构(G2),并且每个分支中的卷积层都是保持输入输出通道数相同(G1),其中一个分支不采取任何操作减少基本单元数(G3),最后使用了 concat 代替原来的 elementy-wise add,并且后面不加 ReLU 直接(G4),再加入channle shuffle 来增加通道之间的信息交流。 对于下采样层,在这一层中对通道数进行翻倍。 在网络结构的最后,即平均值池化层前加入一层 1x1 的卷积层来进一步的混合特征。
image
网络结构
image

6.4 ShuffleNet-v2具有高精度的原因

  • 由于高效,可以增加更多的channel,增加网络容量
  • 采用split使得一部分特征直接与下面的block相连,特征复用(DenseNet)

17.10 现有移动端开源框架及其特点

17.10.1 NCNN

1、开源时间:2017年7月   

2、开源用户:腾讯优图    

3、GitHub地址:https://github.com/Tencent/ncnn   

4、特点:

  • 1)NCNN考虑了手机端的硬件和系统差异以及调用方式,架构设计以手机端运行为主要原则。
  • 2)无第三方依赖,跨平台,手机端 CPU 的速度快于目前所有已知的开源框架(以开源时间为参照对象)。
  • 3)基于 ncnn,开发者能够将深度学习算法轻松移植到手机端高效执行,开发出人工智能 APP。

5、功能:

  • 1、NCNN支持卷积神经网络、多分支多输入的复杂网络结构,如vgg、googlenet、resnet、squeezenet 等。
  • 2、NCNN无需依赖任何第三方库。
  • 3、NCNN全部使用C/C++实现,以及跨平台的cmake编译系统,可轻松移植到其他系统和设备上。
  • 4、汇编级优化,计算速度极快。使用ARM NEON指令集实现卷积层,全连接层,池化层等大部分 CNN 关键层。
  • 5、精细的数据结构设计,没有采用需消耗大量内存的通常框架——im2col + 矩阵乘法,使得内存占用极低。
  • 6、支持多核并行计算,优化CPU调度。
  • 7、整体库体积小于500K,可精简到小于300K。
  • 8、可扩展的模型设计,支持8bit 量化和半精度浮点存储。
  • 9、支持直接内存引用加载网络模型。
  • 10、可注册自定义层实现并扩展。

6、NCNN在Android端部署示例

  • 1)选择合适的Android Studio版本并安装。
  • 2)根据需求选择NDK版本并安装。
  • 3)在Android Studio上配置NDK的环境变量。
  • 4)根据自己需要编译NCNN sdk
1
mkdir build-android cd build-android cmake -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \ -DANDROID_ABI="armeabi-v7a" -DANDROID_ARM_NEON=ON \ -DANDROID_PLATFORM=android-14 .. make make install

​ 安装完成之后,install下有include和lib两个文件夹。

​ 备注:

1
2
3
ANDROID_ABI 是架构名字,"armeabi-v7a" 支持绝大部分手机硬件 
ANDROID_ARM_NEON 是否使用 NEON 指令集,设为 ON 支持绝大部分手机硬件
ANDROID_PLATFORM 指定最低系统版本,"android-14" 就是 android-4.0
  • 5)进行NDK开发。
1
2
3
4
5
6
7
8
9
10
1)assets文件夹下放置你的bin和param文件。
2)jni文件夹下放置你的cpp和mk文件。
3)修改你的app gradle文件。
4)配置Android.mk和Application.mk文件。
5)进行java接口的编写。
6)读取拷贝bin和param文件(有些则是pb文件,根据实际情况)。
7)进行模型的初始化和执行预测等操作。
8)build。
9)cd到src/main/jni目录下,执行ndk-build,生成.so文件。
10)接着就可写自己的操作处理需求。

17.10.2 QNNPACK

全称:Quantized Neural Network PACKage(量化神经网络包)   

1、开源时间:2018年10月   

2、开源用户:Facebook    

3、GitHub地址:https://github.com/pytorch/QNNPACK    

4、特点:   

​ 1)低密度卷积优化函数库;   

  2)可在手机上实时运行Mask R-CNN 和 DensePose;

​ 3) 能在性能受限的移动设备中用 100ms 以内的时间实施图像分类;   

5、QNNPACK 如何提高效率?

1)QNNPACK 使用与安卓神经网络 API 兼容的线性量化方案

QNNPACK 的输入矩阵来自低精度、移动专用的计算机视觉模型。其它库在计算A和B矩阵相乘时,重新打包 A 和 B 矩阵以更好地利用缓存层次结构,希望在大量计算中分摊打包开销,QNNPACK 删除所有计算非必需的内存转换,针对 A和B矩阵相乘适用于一级缓存的情况进行了优化。

​ 1)优化了L1缓存计算,不需要输出中间结果,直接输出最终结果,节省内存带宽和缓存占用。

具体分析:

  • 常规实现:在量化矩阵-矩阵乘法中,8位整数的乘积通常会被累加至 32 位的中间结果中,随后重新量化以产生 8 位的输出。遇到大矩阵尺寸时,比如有时K太大,A和B的面板无法直接转入缓存,此时,需利用缓存层次结构,借助GEMM将A和B的面板沿着K维分割成固定大小的子面板,以便于每个子面板都能适应L1缓存,随后为每个子面板调用微内核。这一缓存优化需要 PDOT 为内核输出 32 位中间结果,最终将它们相加并重新量化为 8 位整数。
  • 优化实现:由于 ONNPACK 对于面板 A 和 B 总是适应 L1 缓存的移动神经网络进行了优化,因此它在调用微内核时处理整个 A 和 B 的面板。而由于无需在微内核之外积累 32 位的中间结果,QNNPACK 会将 32 位的中间结果整合进微内核中并写出 8 位值,这节省了内存带宽和缓存占用。

​ 2)取消了矩阵 A 的重新打包。

  • 常规实现:

      矩阵 B 包含静态权重,可以一次性转换成任何内存布局,但矩阵  A 包含卷积输入,每次推理运行都会改变。因此,重新打包矩阵 A 在每次运行时都会产生开销。尽管存在开销,传统的 GEMM实现还是出于以下两个原因对矩阵 A 进行重新打包:
    
      a 缓存关联性及微内核效率受限。如果不重新打包,微内核将不得不读取被潜在的大跨距隔开的几行A。如果这个跨距恰好是 2 的许多次幂的倍数,面板中不同行 A  的元素可能会落入同一缓存集中。如果冲突的行数超过了缓存关联性,它们就会相互驱逐,性能也会大幅下降。
    
      b 打包对微内核效率的影响与当前所有移动处理器支持的  SIMD  向量指令的使用密切相关。这些指令加载、存储或者计算小型的固定大小元素向量,而不是单个标量(scalar)。在矩阵相乘中,充分利用向量指令达到高性能很重要。在传统的  GEMM 实现中,微内核把 MR 元素重新打包到向量暂存器里的 MR 线路中。
    
  • 优化实现:

      a 当面板适配一级缓存时,不会存在缓存关联性及微内核效率受限的问题。
    
      b 在 QNNPACK 实现中,MR  元素在存储中不是连续的,微内核需要把它们加载到不同的向量暂存器中。越来越大的暂存器压力迫使 QNNPACK 使用较小的 MRxNR  拼贴,但实际上这种差异很小,而且可以通过消除打包开销来补偿。例如,在 32 位 ARM 架构上,QNNPACK 使用 4×8 微内核,其中  57% 的向量指令是乘-加;另一方面,gemmlowp 库使用效率稍高的 4×12 微内核,其中 60% 的向量指令是乘-加。微内核加载 A  的多个行,乘以 B 的满列,结果相加,然后完成再量化并记下量化和。A 和 B 的元素被量化为 8 位整数,但乘积结果相加到 32 位。大部分  ARM 和 ARM64 处理器没有直接完成这一运算的指令,所以它必须分解为多个支持运算。QNNPACK  提供微内核的两个版本,其不同之处在于用于乘以 8 位值并将它们累加到 32 位的指令序列。
    

2)从矩阵相乘到卷积

​ 传统实现:

​ 简单的 1×1 卷积可直接映射到矩阵相乘

​ 但对于具备较大卷积核、padding 或子采样(步幅)的卷积而言则并非如此。但是,这些较复杂的卷积能够通过记忆变换 im2col 映射到矩阵相乘。对于每个输出像素,im2col 复制输入图像的图像块并将其计算为 2D 矩阵。由于每个输出像素都受 KHxKWxC 输入像素值的影响(KH 和 KW 分别指卷积核的高度和宽度,C 指输入图像中的通道数),因此该矩阵的大小是输入图像的 KHxKW 倍,im2col 给内存占用和性能都带来了一定的开销。和 Caffe 一样,大部分深度学习框架转而使用基于 im2col 的实现,利用现有的高度优化矩阵相乘库来执行卷积操作。

​ 优化实现:

​ Facebook 研究者在 QNNPACK 中实现了一种更高效的算法。

  • 他们没有变换卷积输入使其适应矩阵相乘的实现,而是调整 PDOT 微内核的实现,在运行中执行 im2col 变换。这样就无需将输入张量的实际输入复制到 im2col 缓存,而是使用输入像素行的指针设置 indirection buffer,输入像素与每个输出像素的计算有关。
  • 研究者还修改了矩阵相乘微内核,以便从 indirection buffer 加载虚构矩阵(imaginary matrix)A 的行指针,indirection buffer 通常比 im2col buffer 小得多。
  • 此外,如果两次推断运行的输入张量存储位置不变,则 indirection buffer 还可使用输入张量行的指针进行初始化,然后在多次推断运行中重新使用。研究者观察到具备 indirection buffer 的微内核不仅消除了 im2col 变换的开销,其性能也比矩阵相乘微内核略好(可能由于输入行在计算不同输出像素时被重用)。

3)深度卷积

分组卷积(grouped convolution)将输入和输出通道分割成多组,然后对每个组进行分别处理。在有限条件下,当组数等于通道数时,该卷积就是深度卷积,常用于当前的神经网络架构中。深度卷积对每个通道分别执行空间滤波,展示了与正常卷积非常不同的计算模式。因此,通常要向深度卷积提供单独实现,QNNPACK 包括一个高度优化版本 3×3 深度卷积。

深度卷积的传统实现是每次都在卷积核元素上迭代,然后将一个卷积核行和一个输入行的结果累加到输出行。对于一个 3×3 的深度卷积,此类实现将把每个输出行更新 9 次。在 QNNPACK 中,研究者计算所有 3×3 卷积核行和 3×3 输入行的结果,一次性累加到输出行,然后再处理下个输出行。

QNNPACK 实现高性能的关键因素在于完美利用通用暂存器(GPR)来展开卷积核元素上的循环,同时避免在 hot loop 中重新加载地址寄存器。32-bit ARM 架构将实现限制在 14 个 GPR。在 3×3 深度卷积中,需要读取 9 个输入行和 9 个卷积核行。这意味着如果想完全展开循环必须存储 18 个地址。然而,实践中推断时卷积核不会发生变化。因此 Facebook 研究者使用之前在 CxKHxKW 中的滤波器,将它们封装进 [C/8]xKWxKHx8,这样就可以仅使用具备地址增量(address increment)的一个 GPR 访问所有滤波器。(研究者使用数字 8 的原因在于,在一个命令中加载 8 个元素然后减去零,在 128-bit NEON 暂存器中生成 8 个 16-bit 值。)然后使用 9 个输入行指针,指针将滤波器重新装进 10 个 GPR,完全展开滤波器元素上的循环。64-bit ARM 架构相比 32-bit 架构,GPR 的数量翻了一倍。QNNPACK 利用额外的 ARM64 GPR,一次性存储 3×5 输入行的指针,并计算 3 个输出行。

7、性能优势:

​ 测试结果显示出 QNNPACK 在端到端基准上的性能优势。在量化当前最优 MobileNetV2 架构上,基于QNNPACK 的 Caffe2 算子的速度大约是 TensorFlow Lite 速度的 2 倍,在多种手机上都是如此。除了 QNNPACK 之外,Facebook 还开源了 Caffe2 quantized MobileNet v2 模型,其 top-1 准确率比相应的 TensorFlow 模型高出 1.3%。

MobileNetV1

MobileNetV1 架构在使用深度卷积(depthwise convolution)使模型更适合移动设备方面具备开创性。MobileNetV1 包括几乎整个 1×1 卷积和 3×3 卷积。Facebook 研究者将量化 MobileNetV1 模型从 TensorFlow Lite 转换而来,并在 TensorFlow Lite 和 QNNPACK 的 32-bit ARM 设备上对 MobileNetV1 进行基准测试。二者运行时均使用 4 线程,研究者观察到 QNNPACK 的运行速度几何平均值是 TensorFlow Lite 的 1.8 倍。

MobileNetV2

作为移动视觉任务的当前最优架构之一,MobileNetV2 引入了瓶颈构造块和瓶颈之间的捷径连接。研究者在 MobileNetV2 分类模型的量化版上对比基于 QNNPACK 的 Caffe2 算子和 TensorFlow Lite 实现。使用的量化 Caffe2 MobileNetV2 模型已开源,量化 TensorFlow Lite 模型来自官方库:https://github.com/tensorflow/tensorflow/blob/master/tensorflow/contrib/lite/g3doc/models.md。下表展示了二者在常用测试集上的 top1 准确率:

​ Facebook 研究者利用这些模型建立了 Facebook AI 性能评估平台(https://github.com/facebook/FAI-PEP)的基准,该基准基于 32-bit ARM 环境的大量手机设备。对于 TensorFlow Lite 线程设置,研究者尝试了一到四个线程,并报告了最快速的结果。结果显示 TensorFlow Lite 使用四线程的性能最优,因此后续研究中使用四线程来对比 TensorFlow Lite 和 QNNPACK。下表展示了结果,以及在典型智能手机和高端机上,基于 QNNPACK 的算子速度比 TensorFlow Lite 快得多。

Facebook开源高性能内核库QNNPACK
https://baijiahao.baidu.com/s?id=1615725346726413945&wfr=spider&for=pc
http://www.sohu.com/a/272158070_610300

支持移动端深度学习的几种开源框架
https://blog.csdn.net/zchang81/article/details/74280019

17.10.3 Prestissimo

1、开源时间:2017年11月   

2、开源用户:九言科技    

3、GitHub地址:https://github.com/in66-dev/In-Prestissimo  

4、功能特点: 

基础功能

  • 支持卷积神经网络,支持多输入和多分支结构
  • 精炼简洁的API设计,使用方便
  • 提供调试接口,支持打印各个层的数据以及耗时
  • 不依赖任何第三方计算框架,整体库体积 500K 左右(32位 约400k,64位 约600k)
  • 纯 C++ 实现,跨平台,支持 android 和 ios
  • 模型为纯二进制文件,不暴露开发者设计的网络结构

极快的速度

  • 大到框架设计,小到汇编书写上全方位的优化,iphone7 上跑 SqueezeNet 仅需 26ms(单线程)
  • 支持浮点(float)和整型(int)两种运算模式,float模式精度与caffe相同,int模式运算速度快,大部分网络用int的精度便已经足够
  • 以巧妙的内存布局提升cpu的cache命中率,在中低端机型上性能依然强劲
  • 针对 float-arm32, float-arm64, int-arm32, int-arm64 四个分支均做了细致的优化,保证arm32位和arm64位版本都有非常好的性能

SqueezeNet-v1.1 测试结果

Note: 手机测试性能存在一定的抖动,连续多次运算取平均时间

Note: 像华为mate8, mate9,Google nexus 6 虽然是64位的CPU,但测试用的是 32位的库,因此cpu架构依然写 arm-v7a

CPU架构 机型 CPU ncnn(4线程) mdl Prestissimo_float(单线程) Prestissimo_int(单线程)
arm-v7a 小米2 高通APQ8064 1.5GHz 185 ms 370 ms 184 ms 115 ms
arm-v7a 小米2s 四核 骁龙APQ8064 Pro 1.7GHz 166 ms - 136 ms 96 ms
arm-v7a 红米Note 4x 骁龙625 四核2.0GHz 124 ms 306 ms 202 ms 110 ms
arm-v7a Google Nexus 6 骁龙805 四核 2.7GHz 84 ms 245 ms 103 ms 63 ms
arm-v7a Vivo x6d 联发科 MT6752 1.7GHz 245 ms 502 ms 370 ms 186 ms
arm-v7a 华为 Mate 8 海思麒麟950 4大4小 2.3GHz 1.8GHz 75 ms 180 ms 95 ms 57 ms
arm-v7a 华为 Mate 9 海思麒麟960 4大4小 2.4GHz 1.8GHz 61 ms 170 ms 94 ms 48 ms
arm-v8 iphone7 Apple A10 Fusion 2.34GHz - - 27 ms 26 ms

未开放特性

  • 多核并行加速(多核机器可以再提升30%-100% 的速度)
  • depthwise卷积运算(支持mobilenet)
  • 模型压缩功能,压缩后的模型体积可缩小到20%以下
  • GPU 运算模式(Android 基于opengl es 3.1,ios 基于metal)

同类框架对比

框架 caffe tensorflow mdl-android mdl-ios ncnn CoreML Prestissimo
计算硬件 cpu cpu cpu gpu cpu gpu cpu (gpu版本未开放)
计算速度 很快 很快 极快 极快
库大小 较大 中等
兼容性 限ios8以上 很好 仅支持 ios11 很好
模型支持度 很好 - 差(仅限指定模型) 较好 - 中等(当前版本不支持mobilenet)

使用方法-模型转换

绝影支持的是私有的模型文件格式,需要把 caffe 训练出来的模型转换为 .prestissimo 格式,模型转换工具为 caffe2Prestissimo.out。caffe2Prestissimo.out 依赖 protobuf 3.30。将 XXX.prototxt 和 YYY.caffemodel 转化为 Prestissimo 模型 ZZZ.prestissimo:(得到)./caffe2Prestissimo.out XXX.prototxt YYY.caffemodel ZZZ.prestissimo

17.10.4 MDL(mobile-deep-learning)

1、开源时间:2017年9月(已暂停更新)   

2、开源用户:百度    

3、GitHub地址:https://github.com/allonli/mobile-deep-learning

4、功能特点:

  • 一键部署,脚本参数就可以切换ios或者android
  • 支持iOS gpu运行MobileNet、squeezenet模型
  • 已经测试过可以稳定运行MobileNet、GoogLeNet v1、squeezenet、ResNet-50模型
  • 体积极小,无任何第三方依赖。纯手工打造。
  • 提供量化函数,对32位float转8位uint直接支持,模型体积量化后4M上下
  • 与ARM相关算法团队线上线下多次沟通,针对ARM平台会持续优化
  • NEON使用涵盖了卷积、归一化、池化所有方面的操作
  • 汇编优化,针对寄存器汇编操作具体优化
  • loop unrolling 循环展开,为提升性能减少不必要的CPU消耗,全部展开判断操作
  • 将大量繁重的计算任务前置到overhead过程

5、框架结构

MDL 框架主要包括:模型转换模块(MDL Converter)、模型加载模块(Loader)、网络管理模块(Net)、矩阵运算模块(Gemmers)及供 Android 端调用的 JNI 接口层(JNI Interfaces)。

​ 其中,模型转换模块主要负责将Caffe 模型转为 MDL 模型,同时支持将 32bit 浮点型参数量化为 8bit 参数,从而极大地压缩模型体积;模型加载模块主要完成模型的反量化及加载校验、网络注册等过程,网络管理模块主要负责网络中各层 Layer 的初始化及管理工作;MDL 提供了供 Android 端调用的 JNI 接口层,开发者可以通过调用 JNI 接口轻松完成加载及预测过程。

6、MDL 的性能及兼容性

  • 体积 armv7 300k+
  • 速度 iOS GPU mobilenet 可以达到 40ms、squeezenet 可以达到 30ms

​ MDL 从立项到开源,已经迭代了一年多。移动端比较关注的多个指标都表现良好,如体积、功耗、速度。百度内部产品线在应用前也进行过多次对比,和已开源的相关项目对比,MDL 能够在保证速度和能耗的同时支持多种深度学习模型,如 mobilenet、googlenet v1、squeezenet 等,且具有 iOS GPU 版本,squeezenet 一次运行最快可以达到 3-40ms。

同类框架对比

​ 框架Caffe2TensorFlowncnnMDL(CPU)MDL(GPU)硬件CPUCPUCPUCPUGPU速度慢慢快快极快体积大大小小小兼容Android&iOSAndroid&iOSAndroid&iOSAndroid&iOSiOS

​ 与支持 CNN 的移动端框架对比,MDL 速度快、性能稳定、兼容性好、demo 完备。

兼容性

​ MDL 在 iOS 和 Android 平台均可以稳定运行,其中 iOS10 及以上平台有基于 GPU 运算的 API,性能表现非常出色,在 Android 平台则是纯 CPU 运行。高中低端机型运行状态和手机百度及其他 App 上的覆盖都有绝对优势。

​ MDL 同时也支持 Caffe 模型直接转换为 MDL 模型。

17.10.5 Paddle-Mobile

1、开源时间:持续更新,已到3.0版本   

2、开源用户:百度    

3、GitHub地址:https://github.com/PaddlePaddle/paddle-mobile 

4、功能特点:

功能特点

  • 高性能支持ARM CPU

  • 支持Mali GPU

  • 支持Andreno GPU

  • 支持苹果设备的GPU Metal实现

  • 支持ZU5、ZU9等FPGA开发板

  • 支持树莓派等arm-linux开发板

17.10.6 MACE( Mobile AI Compute Engine)

1、开源时间:2018年4月(持续更新,v0.9.0 (2018-07-20))   

2、开源用户:小米    

3、GitHub地址:https://github.com/XiaoMi/mace

4、简介:Mobile AI Compute Engine (MACE) 是一个专为移动端异构计算设备优化的深度学习前向预测框架。
MACE覆盖了常见的移动端计算设备(CPU,GPU和DSP),并且提供了完整的工具链和文档,用户借助MACE能够很方便地在移动端部署深度学习模型。MACE已经在小米内部广泛使用并且被充分验证具有业界领先的性能和稳定性。

5、MACE的基本框架:

MACE Model

MACE定义了自有的模型格式(类似于Caffe2),通过MACE提供的工具可以将Caffe和TensorFlow的模型 转为MACE模型。

MACE Interpreter

MACE Interpreter主要负责解析运行神经网络图(DAG)并管理网络中的Tensors。

Runtime

CPU/GPU/DSP Runtime对应于各个计算设备的算子实现。

6、MACE使用的基本流程

1. 配置模型部署文件(.yml)

模型部署文件详细描述了需要部署的模型以及生成库的信息,MACE根据该文件最终生成对应的库文件。

2.编译MACE库

编译MACE的静态库或者动态库。

3.转换模型

将TensorFlow 或者 Caffe的模型转为MACE的模型。

4.1. 部署

根据不同使用目的集成Build阶段生成的库文件,然后调用MACE相应的接口执行模型。

4.2. 命令行运行

MACE提供了命令行工具,可以在命令行运行模型,可以用来测试模型运行时间,内存占用和正确性。

4.3. Benchmark

MACE提供了命令行benchmark工具,可以细粒度的查看模型中所涉及的所有算子的运行时间。

7、MACE在哪些角度������行了优化?

MACE 专为移动端异构计算平台优化的神经网络计算框架。主要从以下的角度做了专门的优化:

  • 性能
    • 代码经过NEON指令,OpenCL以及Hexagon HVX专门优化,并且采用
      Winograd算法来进行卷积操作的加速。
      此外,还对启动速度进行了专门的优化。
  • 功耗

    • 支持芯片的功耗管理,例如ARM的big.LITTLE调度,以及高通Adreno GPU功耗选项。
  • 系统响应
    • 支持自动拆解长时间的OpenCL计算任务,来保证UI渲染任务能够做到较好的抢占调度,
      从而保证系统UI的相应和用户体验。
  • 内存占用
    • 通过运用内存依赖分析技术,以及内存复用,减少内存的占用。另外,保持尽量少的外部
      依赖,保证代码尺寸精简。
  • 模型加密与保护

    • 模型保护是重要设计目标之一。支持将模型转换成C++代码,以及关键常量字符混淆,增加逆向的难度。
  • 硬件支持范围
    • 支持高通,联发科,以及松果等系列芯片的CPU,GPU与DSP(目前仅支持Hexagon)计算加速。
    • 同时支持在具有POSIX接口的系统的CPU上运行。

8、性能对比:

MACE 支持 TensorFlow 和 Caffe 模型,提供转换工具,可以将训练好的模型转换成专有的模型数据文件,同时还可以选择将模型转换成C++代码,支持生成动态库或者静态库,提高模型保密性。

17.10.7 FeatherCNN

1、开源时间:持续更新,已到3.0版本   

2、开源用户:腾讯AI    

3、GitHub地址:https://github.com/Tencent/FeatherCNN

4、功能特点:

FeatherCNN 是由腾讯 AI 平台部研发的基于 ARM 架构的高效 CNN 推理库,该项目支持 Caffe 模型,且具有高性能、易部署、轻量级三大特性。

该项目具体特性如下:

  • 高性能:无论是在移动设备(iOS / Android),嵌入式设备(Linux)还是基于 ARM 的服务器(Linux)上,FeatherCNN 均能发挥最先进的推理计算性能;

  • 易部署:FeatherCNN 的所有内容都包含在一个代码库中,以消除第三方依赖关系。因此,它便于在移动平台上部署。FeatherCNN 自身的模型格式与 Caffe 模型完全兼容。

  • 轻量级:编译后的 FeatherCNN 库的体积仅为数百 KB。

17.10.8 TensorFlow Lite

1、开源时间:2017年11月   

2、开源用户:谷歌   

3、GitHub地址:https://github.com/tensorflow/tensorflow/tree/master/tensorflow/contrib/lite

4、简介:

Google 表示 Lite 版本 TensorFlow 是 TensorFlow Mobile 的一个延伸版本。此前,通过TensorFlow Mobile API,TensorFlow已经支持手机上的模型嵌入式部署。TensorFlow Lite应该被视为TensorFlow Mobile的升级版。

TensorFlow Lite可以与Android 8.1中发布的神经网络API完美配合,即便在没有硬件加速时也能调用CPU处理,确保模型在不同设备上的运行。 而Android端版本演进的控制权是掌握在谷歌手中的,从长期看,TensorFlow Lite会得到Android系统层面上的支持。

5、架构:

其组件包括:

  • TensorFlow 模型(TensorFlow Model):保存在磁盘中的训练模型。
  • TensorFlow Lite 转化器(TensorFlow Lite Converter):将模型转换成 TensorFlow Lite 文件格式的项目。
  • TensorFlow Lite 模型文件(TensorFlow Lite Model File):基于 FlatBuffers,适配最大速度和最小规模的模型。

6、移动端开发步骤:

Android Studio 3.0, SDK Version API26, NDK Version 14

步骤:

  1. 将此项目导入到Android Studio:
    https://github.com/tensorflow/tensorflow/tree/master/tensorflow/contrib/lite/java/demo

  2. 下载移动端的模型(model)和标签数据(lables):
    https://storage.googleapis.com/download.tensorflow.org/models/tflite/mobilenet_v1_224_android_quant_2017_11_08.zip

  3. 下载完成解压mobilenet_v1_224_android_quant_2017_11_08.zip文件得到一个xxx.tflite和labes.txt文件,分别是模型和标签文件,并且把这两个文件复制到assets文件夹下。

  4. 构建app,run……

17.7.9 TensorFlow Lite和TensorFlow Mobile的区别?

  • TensorFlow Lite是TensorFlow Mobile的进化版。
  • 在大多数情况下,TensorFlow Lite拥有跟小的二进制大小,更少的依赖以及更好的性能。
  • 相比TensorFlow Mobile是对完整TensorFlow的裁减,TensorFlow Lite基本就是重新实现了。从内部实现来说,在TensorFlow内核最基本的OP,Context等数据结构,都是新的。从外在表现来说,模型文件从PB格式改成了FlatBuffers格式,TensorFlow的size有大幅度优化,降至300K,然后提供一个converter将普通TensorFlow模型转化成TensorFlow Lite需要的格式。因此,无论从哪方面看,TensorFlow Lite都是一个新的实现方案。

17.10.9 PocketFlow

1、开源时间:2018年9月   

2、开源用户:腾讯   

3、GitHub地址:https://github.com/Tencent/PocketFlow

4、简介:

全球首个自动模型压缩框架

一款面向移动端AI开发者的自动模型压缩框架,集成了当前主流的模型压缩与训练算法,结合自研超参数优化组件实现了全程自动化托管式的模型压缩与加速。开发者无需了解具体算法细节,即可快速地将AI技术部署到移动端产品上,实现了自动托管式模型压缩与加速,实现用户数据的本地高效处理。

5、框架介绍

PocketFlow 框架主要由两部分组件构成,分别是模型压缩/加速算法组件和超参数优化组件,具体结构如下图所示。

​ 开发者将未压缩的原始模型作为 PocketFlow 框架的输入,同时指定期望的性能指标,例如模型的压缩和/或加速倍数;在每一轮迭代过程中,超参数优化组件选取一组超参数取值组合,之后模型压缩/加速算法组件基于该超参数取值组合,对原始模型进行压缩,得到一个压缩后的候选模型;基于对候选模型进行性能评估的结果,超参数优化组件调整自身的模型参数,并选取一组新的超参数取值组合,以开始下一轮迭代过程;当迭代终止时,PocketFlow 选取最优的超参数取值组合以及对应的候选模型,作为最终输出,返回给开发者用作移动端的模型部署。

6、PocketFlow如何实现模型压缩与加速?

​ 具体地,PocketFlow 通过下列各个算法组件的有效结合,实现了精度损失更小、自动化程度更高的深度学习模型的压缩与加速:

  • a) 通道剪枝(channel pruning)组件:在CNN网络中,通过对特征图中的通道维度进行剪枝,可以同时降低模型大小和计算复杂度,并且压缩后的模型可以直接基于现有的深度学习框架进行部署。在CIFAR-10图像分类任务中,通过对 ResNet-56 模型进行通道剪枝,可以实现2.5倍加速下分类精度损失0.4%,3.3倍加速下精度损失0.7%。

  • b) 权重稀疏化(weight sparsification)组件:通过对网络权重引入稀疏性约束,可以大幅度降低网络权重中的非零元素个数;压缩后模型的网络权重可以以稀疏矩阵的形式进行存储和传输,从而实现模型压缩。对于 MobileNet 图像分类模型,在删去50%网络权重后,在 ImageNet 数据集上的 Top-1 分类精度损失仅为0.6%。

  • c) 权重量化(weight quantization)组件:通过对网络权重引入量化约束,可以降低用于表示每个网络权重所需的比特数;团队同时提供了对于均匀和非均匀两大类量化算法的支持,可以充分利用 ARM 和 FPGA 等设备的硬件优化,以提升移动端的计算效率,并为未来的神经网络芯片设计提供软件支持。以用于 ImageNet 图像分类任务的 ResNet-18 模型为例,在8比特定点量化下可以实现精度无损的4倍压缩。

  • d)网络蒸馏(network distillation)组件:对于上述各种模型压缩组件,通过将未压缩的原始模型的输出作为额外的监督信息,指导压缩后模型的训练,在压缩/加速倍数不变的前提下均可以获得0.5%-2.0%不等的精度提升。

  • e) 多GPU训练(multi-GPU training)组件:深度学习模型训练过程对计算资源要求较高,单个GPU难以在短时间内完成模型训练,因此团队提供了对于多机多卡分布式训练的全面支持,以加快使用者的开发流程。无论是基于 ImageNet 数据的Resnet-50图像分类模型还是基于 WMT14 数据的 Transformer 机器翻译模型,均可以在一个小时内训练完毕。[1]

  • f) 超参数优化(hyper-parameter optimization)组件:多数开发者对模型压缩算法往往不甚了解,但超参数取值对最终结果往往有着巨大的影响,因此团队引入了超参数优化组件,采用了包括强化学习等算法以及 AI Lab 自研的 AutoML 自动超参数优化框架来根据具体性能需求,确定最优超参数取值组合。例如,对于通道剪枝算法,超参数优化组件可以自动地根据原始模型中各层的冗余程度,对各层采用不同的剪枝比例,在保证满足模型整体压缩倍数的前提下,实现压缩后模型识别精度的最大化。

7、PocketFlow 性能

​ 通过引入超参数优化组件,不仅避免了高门槛、繁琐的人工调参工作,同时也使得 PocketFlow 在各个压缩算法上全面超过了人工调参的效果。以图像分类任务为例,在 CIFAR-10 和 ImageNet 等数据集上,PocketFlow 对 ResNet 和 MobileNet 等多种 CNN 网络结构进行有效的模型压缩与加速。

​ 在 CIFAR-10 数据集上,PocketFlow 以 ResNet-56 作为基准模型进行通道剪枝,并加入了超参数优化和网络蒸馏等训练策略,实现了 2.5 倍加速下分类精度损失 0.4%,3.3 倍加速下精度损失 0.7%,且显著优于未压缩的 ResNet-44 模型; 在 ImageNet 数据集上,PocketFlow 可以对原本已经十分精简的 MobileNet 模型继续进行权重稀疏化,以更小的模型尺寸取得相似的分类精度;与 Inception-V1、ResNet-18 等模型相比,模型大小仅为后者的约 20~40%,但分类精度基本一致(甚至更高)。

相比于费时费力的人工调参,PocketFlow 框架中的 AutoML 自动超参数优化组件仅需 10
余次迭代就能达到与人工调参类似的性能,在经过 100 次迭代后搜索得到的超参数组合可以降低约 0.6%
的精度损失;通过使用超参数优化组件自动地确定网络中各层权重的量化比特数,PocketFlow 在对用于 ImageNet 图像分类任务的
ResNet-18 模型进行压缩时,取得了一致性的性能提升;当平均量化比特数为 4 比特时,超参数优化组件的引入可以将分类精度从 63.6%
提升至 68.1%(原始模型的分类精度为 70.3%)。

参考文献

[1] Zhuangwei Zhuang, Mingkui Tan, Bohan Zhuang, Jing Liu, Jiezhang Cao, Qingyao Wu, Junzhou Huang, Jinhui Zhu,「Discrimination-aware Channel Pruning for Deep Neural Networks”, In Proc. of the 32nd Annual Conference on Neural Information Processing Systems, NIPS ‘18, Montreal, Canada, December 2018.

[2] Jiaxiang Wu, Weidong Huang, Junzhou Huang, Tong Zhang,「Error Compensated Quantized SGD and its Applications to Large-scale Distributed Optimization」, In Proc. of the 35th International Conference on Machine Learning, ICML’18, Stockholm, Sweden, July 2018.

17.10.10 其他几款支持移动端深度学习的开源框架

https://blog.csdn.net/zchang81/article/details/74280019

17.10.11 MDL、NCNN和 TFLite比较

百度-MDL框架、腾讯-NCNN框架和谷歌TFLite框架比较。

MDL NCNN TFLite
代码质量 很高
跨平台
支持caffe模型 ×
支持TensorFlow模型 × ×
CPU NEON指令优化
GPU加速 × ×

相同点:

  • 只含推理(inference)功能,使用的模型文件需要通过离线的方式训练得到。
  • 最终生成的库尺寸较小,均小于500kB。
  • 为了提升执行速度,都使用了ARM NEON指令进行加速。
  • 跨平台,iOS和Android系统都支持。

不同点:

  • MDL和NCNN均是只支持Caffe框架生成的模型文件,而TfLite则毫无意外的只支持自家大哥TensorFlow框架生成的模型文件。
  • MDL支持利用iOS系统的Matal框架进行GPU加速,能够显著提升在iPhone上的运行速度,达到准实时的效果。而NCNN和TFLite还没有这个功能。

17.11 移动端开源框架部署

17.8.1 以NCNN为例

部署步骤

17.8.2 以QNNPACK为例

部署步骤

17.8.4 在Android手机上使用MACE实现图像分类

17.8.3 在Android手机上使用PaddleMobile实现图像分类

编译paddle-mobile库

1)编译Android能够使用的CPP库:编译Android的paddle-mobile库,可选择使用Docker编译和Ubuntu交叉编译,这里介绍使用Ubuntu交叉编译paddle-mobile库。

:在Android项目,Java代码调用CPP代码,CPP的函数需要遵循一定的命名规范,比如Java包名类名_对应的Java的方法名。

​ 目前官方提供了5个可以给Java调用的函数,该代码在:paddle-mobile/src/jni/paddle_mobile_jni.cpp,如果想要让这些函数能够在自己的包名下的类调用,就要修改CPP的函数名称修改如下:

1
2
3
4
5
6
JNIEXPORT jboolean JNICALL Java_com_baidu_paddle_PML_load(JNIEnv *env, 
jclass thiz,
jstring modelPath) {
ANDROIDLOGI("load invoked");
bool optimize = true;
return getPaddleMobileInstance()->Load(jstring2cppstring(env, modelPath), optimize); }

​ 笔者项目的包名为com.example.paddlemobile1,在这个包下有一个ImageRecognition.java的程序来对应这个CPP程序,那么修改load函数如下:

1
2
3
4
5
6
7
8
JNIEXPORT jboolean JNICALL Java_com_example_paddlemobile1_ImageRecognition_load(JNIEnv *env,
jclass thiz,
jstring modelPath) {
ANDROIDLOGI("load invoked");
bool optimize = true;
return getPaddleMobileInstance()->Load(jstring2cppstring(env, modelPath),
optimize);
}

使用Ubuntu交叉编译paddle-mobile库

1、下载和解压NDK。

1
2
wget https://dl.google.com/android/repository/android-ndk-r17b-linux-x86_64.zip
unzip android-ndk-r17b-linux-x86_64.zip

2、设置NDK环境变量,目录是NDK的解压目录。

1
export NDK_ROOT="/home/test/paddlepaddle/android-ndk-r17b"

设置好之后,可以使用以下的命令查看配置情况。

root@test:/home/test/paddlepaddle# echo $NDK_ROOT
/home/test/paddlepaddle/android-ndk-r17b

3、安装cmake,需要安装较高版本的,笔者的cmake版本是3.11.2。

下载cmake源码

1
wget https://cmake.org/files/v3.11/cmake-3.11.2.tar.gz

解压cmake源码

1
tar -zxvf cmake-3.11.2.tar.gz

进入到cmake源码根目录,并执行bootstrap。

1
2
cd cmake-3.11.2
./bootstrap

最后执行以下两条命令开始安装cmake。

1
2
make
make install

安装完成之后,可以使用cmake —version是否安装成功.

1
2
3
4
root@test:/home/test/paddlepaddle# cmake --version
cmake version 3.11.2

CMake suite maintained and supported by Kitware (kitware.com/cmake).

4、克隆paddle-mobile源码。

1
git clone https://github.com/PaddlePaddle/paddle-mobile.git

5、进入到paddle-mobile的tools目录下,执行编译。

1
2
cd paddle-mobile/tools/
sh build.sh android

(可选)如果想编译针对某一个网络编译更小的库时,可以在命令后面加上相应的参数,如下:

1
sh build.sh android googlenet

6、最后会在paddle-mobile/build/release/arm-v7a/build目录下生产paddle-mobile库。

1
2
root@test:/home/test/paddlepaddle/paddle-mobile/build/release/arm-v7a/build# ls
libpaddle-mobile.so

libpaddle-mobile.so就是我们在开发Android项目的时候使用到的paddle-mobile库。

创建Android项目

1、首先使用Android Studio创建一个普通的Android项目,包名为com.example.paddlemobile1

2、在main目录下创建l两个assets/paddle_models文件夹,这个文件夹存放PaddleFluid训练好的预测模型。PaddleMobile支持量化模型,使用模型量化可以把模型缩小至原来的四分之一,如果使用量化模型,那加载模型的接口也有修改一下,使用以下的接口加载模型:

1
public static native boolean loadQualified(String modelDir);

3、在main目录下创建一个jniLibs文件夹,这个文件夹是存放CPP编译库的,在本项目中就存放上一部分编译的libpaddle-mobile.so

4、在Android项目的配置文件夹中加上权限声明,因为我们要使用到读取相册和使用相机,所以加上以下的权限声明:

1
2
3
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

5、修改activity_main.xml界面,修改成如下:

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
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:id="@+id/btn_ll"
android:layout_alignParentBottom="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">

<Button
android:id="@+id/use_photo"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="相册" />

<Button
android:id="@+id/start_camera"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="拍照" />
</LinearLayout>

<TextView
android:layout_above="@id/btn_ll"
android:id="@+id/result_text"
android:textSize="16sp"
android:layout_width="match_parent"
android:hint="预测结果会在这里显示"
android:layout_height="100dp" />

<ImageView
android:layout_alignParentTop="true"
android:layout_above="@id/result_text"
android:id="@+id/show_image"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>

6、创建一个ImageRecognition.java的Java程序,这个程序的作用就是调用paddle-mobile/src/jni/paddle_mobile_jni.cpp的函数,对应的是里面的函数。目前支持一下几个接口。

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
package com.example.paddlemobile1;

public class ImageRecognition {
// set thread num
public static native void setThread(int threadCount);

//Load seperated parameters
public static native boolean load(String modelDir);

// load qualified model
public static native boolean loadQualified(String modelDir);

// Load combined parameters
public static native boolean loadCombined(String modelPath, String paramPath);

// load qualified model
public static native boolean loadCombinedQualified(String modelPath, String paramPath);

// object detection
public static native float[] predictImage(float[] buf, int[]ddims);

// predict yuv image
public static native float[] predictYuv(byte[] buf, int imgWidth, int imgHeight, int[] ddims, float[]meanValues);

// clear model
public static native void clear();
}

7、然后编写一个PhotoUtil.java的工具类。

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
package com.example.paddlemobile1;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.provider.MediaStore;
import android.support.v4.content.FileProvider;
import android.util.Log;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;

public class PhotoUtil {
// start camera
public static Uri start_camera(Activity activity, int requestCode) {
Uri imageUri;
// save image in cache path
File outputImage = new File(activity.getExternalCacheDir(), "out_image.jpg");
try {
if (outputImage.exists()) {
outputImage.delete();
}
outputImage.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
if (Build.VERSION.SDK_INT >= 24) {
// compatible with Android 7.0 or over
imageUri = FileProvider.getUriForFile(activity,
"com.example.paddlemobile1", outputImage);
} else {
imageUri = Uri.fromFile(outputImage);
}
// set system camera Action
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// set save photo path
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
// set photo quality, min is 0, max is 1
intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);
activity.startActivityForResult(intent, requestCode);
return imageUri;
}

// get picture in photo
public static void use_photo(Activity activity, int requestCode){
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType("image/*");
activity.startActivityForResult(intent, requestCode);
}

// get photo from Uri
public static String get_path_from_URI(Context context, Uri uri) {
String result;
Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
if (cursor == null) {
result = uri.getPath();
} else {
cursor.moveToFirst();
int idx = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
result = cursor.getString(idx);
cursor.close();
}
return result;
}

// Compress the image to the size of the training image,and change RGB
public static float[] getScaledMatrix(Bitmap bitmap, int desWidth,
int desHeight) {
float[] dataBuf = new float[3 * desWidth * desHeight];
int rIndex;
int gIndex;
int bIndex;
int[] pixels = new int[desWidth * desHeight];
Bitmap bm = Bitmap.createScaledBitmap(bitmap, desWidth, desHeight, false);
bm.getPixels(pixels, 0, desWidth, 0, 0, desWidth, desHeight);
int j = 0;
int k = 0;
for (int i = 0; i < pixels.length; i++) {
int clr = pixels[i];
j = i / desHeight;
k = i % desWidth;
rIndex = j * desWidth + k;
gIndex = rIndex + desHeight * desWidth;
bIndex = gIndex + desHeight * desWidth;
dataBuf[rIndex] = (float) ((clr & 0x00ff0000) >> 16) - 148;
dataBuf[gIndex] = (float) ((clr & 0x0000ff00) >> 8) - 148;
dataBuf[bIndex] = (float) ((clr & 0x000000ff)) - 148;

}
if (bm.isRecycled()) {
bm.recycle();
}
return dataBuf;
}

// compress picture
public static Bitmap getScaleBitmap(String filePath) {
BitmapFactory.Options opt = new BitmapFactory.Options();
opt.inJustDecodeBounds = true;
BitmapFactory.decodeFile(filePath, opt);

int bmpWidth = opt.outWidth;
int bmpHeight = opt.outHeight;

int maxSize = 500;

// compress picture with inSampleSize
opt.inSampleSize = 1;
while (true) {
if (bmpWidth / opt.inSampleSize < maxSize || bmpHeight / opt.inSampleSize < maxSize) {
break;
}
opt.inSampleSize *= 2;
}
opt.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(filePath, opt);
}
}
  • start_camera()方法是启动相机并返回图片的URI。
  • use_photo()方法是打开相册,获取到的图片URI在回到函数中获取。
  • get_path_from_URI()方法是把图片的URI转换成绝对路径。
  • getScaledMatrix()方法是把图片压缩成跟训练时的大小,并转换成预测需要用的数据格式浮点数组。
  • getScaleBitmap()方法是对图片进行等比例压缩,减少内存的支出。

8、最后修改MainActivity.java,修改如下:

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
package com.example.paddlemobile1;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.RequestOptions;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getName();
private static final int USE_PHOTO = 1001;
private static final int START_CAMERA = 1002;
private Uri image_uri;
private ImageView show_image;
private TextView result_text;
private String assets_path = "paddle_models";
private boolean load_result = false;
private int[] ddims = {1, 3, 224, 224};
private static final String[] PADDLE_MODEL = {
"lenet",
"alexnet",
"vgg16",
"resnet",
"googlenet",
"mobilenet_v1",
"mobilenet_v2",
"inception_v1",
"inception_v2",
"squeezenet"
};

// load paddle-mobile api
static {
try {
System.loadLibrary("paddle-mobile");

} catch (SecurityException e) {
e.printStackTrace();

} catch (UnsatisfiedLinkError e) {
e.printStackTrace();

} catch (NullPointerException e) {
e.printStackTrace();

}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

init();
}

// initialize view
private void init() {
request_permissions();
show_image = (ImageView) findViewById(R.id.show_image);
result_text = (TextView) findViewById(R.id.result_text);
Button use_photo = (Button) findViewById(R.id.use_photo);
Button start_photo = (Button) findViewById(R.id.start_camera);

// use photo click
use_photo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
PhotoUtil.use_photo(MainActivity.this, USE_PHOTO);
// load_model();
}
});
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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
    // start camera click
start_photo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
image_uri = PhotoUtil.start_camera(MainActivity.this, START_CAMERA);
}
});

// copy file from assets to sdcard
String sdcard_path = Environment.getExternalStorageDirectory()
+ File.separator + assets_path;
copy_file_from_asset(this, assets_path, sdcard_path);

// load model
load_model();
}

// load infer model
private void load_model() {
String model_path = Environment.getExternalStorageDirectory()
+ File.separator + assets_path + File.separator + PADDLE_MODEL[4];
Log.d(TAG, model_path);
load_result = ImageRecognition.load(model_path);
if (load_result) {
Log.d(TAG, "model load success");
} else {
Log.d(TAG, "model load fail");
}
}

// clear infer model
private void clear_model() {
ImageRecognition.clear();
Log.d(TAG, "model is clear");
}

// copy file from asset to sdcard
public void copy_file_from_asset(Context context, String oldPath, String newPath) {
try {
String[] fileNames = context.getAssets().list(oldPath);
if (fileNames.length > 0) {
// directory
File file = new File(newPath);
if (!file.exists()) {
file.mkdirs();
}
// copy recursivelyC
for (String fileName : fileNames) {
copy_file_from_asset(context, oldPath + "/" + fileName, newPath + "/" + fileName);
}
Log.d(TAG, "copy files finish");
} else {
// file
File file = new File(newPath);
// if file exists will never copy
if (file.exists()) {
return;
}

// copy file to new path
InputStream is = context.getAssets().open(oldPath);
FileOutputStream fos = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int byteCount;
while ((byteCount = is.read(buffer)) != -1) {
fos.write(buffer, 0, byteCount);
}
fos.flush();
is.close();
fos.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
String image_path;
RequestOptions options = new RequestOptions().skipMemoryCache(true).diskCacheStrategy(DiskCacheStrategy.NONE);
if (resultCode == Activity.RESULT_OK) {
switch (requestCode) {
case USE_PHOTO:
if (data == null) {
Log.w(TAG, "user photo data is null");
return;
}
image_uri = data.getData();
Glide.with(MainActivity.this).load(image_uri).apply(options).into(show_image);
// get image path from uri
image_path = PhotoUtil.get_path_from_URI(MainActivity.this, image_uri);
// show result
result_text.setText(image_path);
// predict image
predict_image(PhotoUtil.get_path_from_URI(MainActivity.this, image_uri));
break;
case START_CAMERA:
// show photo
Glide.with(MainActivity.this).load(image_uri).apply(options).into(show_image);
// get image path from uri
image_path = PhotoUtil.get_path_from_URI(MainActivity.this, image_uri);
// show result
result_text.setText(image_path);
// predict image
predict_image(PhotoUtil.get_path_from_URI(MainActivity.this, image_uri));
break;
}
}
}

@SuppressLint("SetTextI18n")
private void predict_image(String image_path) {
// picture to float array
Bitmap bmp = PhotoUtil.getScaleBitmap(image_path);
float[] inputData = PhotoUtil.getScaledMatrix(bmp, ddims[2], ddims[3]);
try {
long start = System.currentTimeMillis();
// get predict result
float[] result = ImageRecognition.predictImage(inputData, ddims);
Log.d(TAG, "origin predict result:" + Arrays.toString(result));
long end = System.currentTimeMillis();
long time = end - start;
Log.d("result length", String.valueOf(result.length));
// show predict result and time
int r = get_max_result(result);
String show_text = "result:" + r + "\nprobability:" + result[r] + "\ntime:" + time + "ms";
result_text.setText(show_text);
} catch (Exception e) {
e.printStackTrace();
}
}

private int get_max_result(float[] result) {
float probability = result[0];
int r = 0;
for (int i = 0; i < result.length; i++) {
if (probability < result[i]) {
probability = result[i];
r = i;
}
}
return r;
}

// request permissions
private void request_permissions() {

List<String> permissionList = new ArrayList<>();
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
permissionList.add(Manifest.permission.CAMERA);
}

if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
permissionList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
}

if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
permissionList.add(Manifest.permission.READ_EXTERNAL_STORAGE);
}

// if list is not empty will request permissions
if (!permissionList.isEmpty()) {
ActivityCompat.requestPermissions(this, permissionList.toArray(new String[permissionList.size()]), 1);
}
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case 1:
if (grantResults.length > 0) {
for (int i = 0; i < grantResults.length; i++) {

int grantResult = grantResults[i];
if (grantResult == PackageManager.PERMISSION_DENIED) {
String s = permissions[i];
Toast.makeText(this, s + " permission was denied", Toast.LENGTH_SHORT).show();
}
}
}
break;
}
}

@Override
protected void onDestroy() {
// clear model before destroy app
clear_model();
super.onDestroy();
}
}
  • load_model()方法是加载预测模型的。
  • clear_model()方法是清空预测模型的。
  • copy_file_from_asset()方法是把预测模型复制到内存卡上。
  • predict_image()方法是预测图片的。
  • get_max_result()方法是获取概率最大的预测结果。
  • request_permissions()方法是动态请求权限的。

因为使用到图像加载框架Glide,所以要在build.gradle加入以下的引用。

1
implementation 'com.github.bumptech.glide:glide:4.3.1'

8、最后运行项目,选择图片预测就会得到结果。

17.9 移动端开源框架部署疑难

增加常见的几个问题

知识蒸馏(Distillation)相关论文阅读(1)——Distilling the Knowledge in a Neural Network(以及代码复现)

参考文献

第五章 卷积神经网络(CNN)

​ 卷积神经网络是一种用来处理局部和整体相关性的计算网络结构,被应用在图像识别、自然语言处理甚至是语音识别领域,因为图像数据具有显著的局部与整体关系,其在图像识别领域的应用获得了巨大的成功。

5.1 卷积神经网络的组成层

​ 以图像分类任务为例,在表5.1所示卷积神经网络中,一般包含5种类型的网络层次结构:

​ 表5.1 卷积神经网络的组成

CNN层次结构 输出尺寸 作用
输入层 $W_1\times H_1\times 3$ 卷积网络的原始输入,可以是原始或预处理后的像素矩阵
卷积层 $W_1\times H_1\times K$ 参数共享、局部连接,利用平移不变性从全局特征图提取局部特征
激活层 $W_1\times H_1\times K$ 将卷积层的输出结果进行非线性映射
池化层 $W_2\times H_2\times K$ 进一步筛选特征,可以有效减少后续网络层次所需的参数量
全连接层 $(W_2 \cdot H_2 \cdot K)\times C$ 将多维特征展平为2维特征,通常低维度特征对应任务的学习目标(类别或回归值)

$W_1\times H_1\times 3$对应原始图像或经过预处理的像素值矩阵,3对应RGB图像的通道;$K$表示卷积层中卷积核(滤波器)的个数;$W_2\times H_2$ 为池化后特征图的尺度,在全局池化中尺度对应$1\times 1$;$(W_2 \cdot H_2 \cdot K)$是将多维特征压缩到1维之后的大小,$C$对应的则是图像类别个数。

5.1.1 输入层

​ 输入层(Input Layer)通常是输入卷积神经网络的原始数据或经过预处理的数据,可以是图像识别领域中原始三维的多彩图像,也可以是音频识别领域中经过傅利叶变换的二维波形数据,甚至是自然语言处理中一维表示的句子向量。以图像分类任务为例,输入层输入的图像一般包含RGB三个通道,是一个由长宽分别为$H$和$W$组成的3维像素值矩阵$H\times W \times 3$,卷积网络会将输入层的数据传递到一系列卷积、池化等操作进行特征提取和转化,最终由全连接层对特征进行汇总和结果输出。根据计算能力、存储大小和模型结构的不同,卷积神经网络每次可以批量处理的图像个数不尽相同,若指定输入层接收到的图像个数为$N$,则输入层的输出数据为$N\times H\times W\times 3$。

5.1.2 卷积层

​ 卷积层(Convolution Layer)通常用作对输入层输入数据进行特征提取,通过卷积核矩阵对原始数据中隐含关联性的一种抽象。卷积操作原理上其实是对两张像素矩阵进行点乘求和的数学操作,其中一个矩阵为输入的数据矩阵,另一个矩阵则为卷积核(滤波器或特征矩阵),求得的结果表示为原始图像中提取的特定局部特征。图5.1表示卷积操作过程中的不同填充策略,上半部分采用零填充,下半部分采用有效卷积(舍弃不能完整运算的边缘部分)。
conv-same
​ 图5.1 卷积操作示意图

5.1.3 激活层

​ 激活层(Activation Layer)负责对卷积层抽取的特征进行激活,由于卷积操作是由输入矩阵与卷积核矩阵进行相差的线性变化关系,需要激活层对其进行非线性的映射。激活层主要由激活函数组成,即在卷积层输出结果的基础上嵌套一个非线性函数,让输出的特征图具有非线性关系。卷积网络中通常采用ReLU来充当激活函数(还包括tanh和sigmoid等)ReLU的函数形式如公式(5-1)所示,能够限制小于0的值为0,同时大于等于0的值保持不变。

5.1.4 池化层

​ 池化层又称为降采样层(Downsampling Layer),作用是对感受域内的特征进行筛选,提取区域内最具代表性的特征,能够有效地降低输出特征尺度,进而减少模型所需要的参数量。按操作类型通常分为最大池化(Max Pooling)、平均池化(Average Pooling)和求和池化(Sum Pooling),它们分别提取感受域内最大、平均与总和的特征值作为输出,最常用的是最大池化。

5.1.5 全连接层

​ 全连接层(Full Connected Layer)负责对卷积神经网络学习提取到的特征进行汇总,将多维的特征输入映射为二维的特征输出,高维表示样本批次,低位常常对应任务目标。

5.2 卷积在图像中有什么直观作用

​ 在卷积神经网络中,卷积常用来提取图像的特征,但不同层次的卷积操作提取到的特征类型是不相同的,特征类型粗分如表5.2所示。
​ 表5.2 卷积提取的特征类型

卷积层次 特征类型
浅层卷积 边缘特征
中层卷积 局部特征
深层卷积 全局特征

图像与不同卷积核的卷积可以用来执行边缘检测、锐化和模糊等操作。表5.3显示了应用不同类型的卷积核(滤波器)后的各种卷积图像。
​ 表5.3 一些常见卷积核的作用

卷积作用 卷积核 卷积后图像
输出原图 $\begin{bmatrix} 0 & 0 & 0 \ 0 & 1 & 0 \ 0 & 0 & 0 \end{bmatrix}$ origin_img
边缘检测(突出边缘差异) $\begin{bmatrix} 1 & 0 & -1 \ 0 & 0 & 0 \ -1 & 0 & 1 \end{bmatrix}$ edgeDetect-1
边缘检测(突出中间值) $\begin{bmatrix} -1 & -1 & -1 \ -1 & 8 & -1 \ -1 & -1 & -1 \end{bmatrix}$ edgeDetect-2
图像锐化 $\begin{bmatrix} 0 & -1 & 0 \ -1 & 5 & -1 \ 0 & -1 & 0 \end{bmatrix}$ sharpen_img
方块模糊 $\begin{bmatrix} 1 & 1 & 1 \ 1 & 1 & 1 \ 1 & 1 & 1 \end{bmatrix} \times \frac{1}{9}$ box_blur
高斯模糊 $\begin{bmatrix} 1 & 2 & 1 \ 2 & 4 & 2 \ 1 & 2 & 1 \end{bmatrix} \times \frac{1}{16}$ gaussian_blur

5.3 卷积层有哪些基本参数?

​ 卷积层中需要用到卷积核(滤波器或特征检测器)与图像特征矩阵进行点乘运算,利用卷积核与对应的特征感受域进行划窗式运算时,需要设定卷积核对应的大小、步长、个数以及填充的方式,如表5.4所示。

​ 表5.4 卷积层的基本参数

参数名 作用 常见设置
卷积核大小 (Kernel Size) 卷积核的大小定义了卷积的感受野 在过去常设为5,如LeNet-5;现在多设为3,通过堆叠$3\times3$的卷积核来达到更大的感受域
卷积核步长 (Stride) 定义了卷积核在卷积过程中的步长 常见设置为1,表示滑窗距离为1,可以覆盖所有相邻位置特征的组合;当设置为更大值时相当于对特征组合降采样
填充方式 (Padding) 在卷积核尺寸不能完美匹配输入的图像矩阵时需要进行一定的填充策略 设置为’SAME’表示对不足卷积核大小的边界位置进行某种填充(通常零填充)以保证卷积输出维度与与输入维度一致;当设置为’VALID’时则对不足卷积尺寸的部分进行舍弃,输出维度就无法保证与输入维度一致
输入通道数 (In Channels) 指定卷积操作时卷积核的深度 默认与输入的特征矩阵通道数(深度)一致;在某些压缩模型中会采用通道分离的卷积方式
输出通道数 (Out Channels) 指定卷积核的个数 若设置为与输入通道数一样的大小,可以保持输入输出维度的一致性;若采用比输入通道数更小的值,则可以减少整体网络的参数量

卷积操作维度变换公式:

$Od =\begin{cases} \lceil \frac{(I_d - k{size})+ 1)}{s}\rceil ,& \text{padding=VALID}\ \lceil \frac{I_d}{s}\rceil,&\text{padding=SAME} \end{cases}$

其中,$Id$为输入维度,$O_d$为输出维度,$k{size}$为卷积核大小,$s$为步长

5.4 卷积核有什么类型?

​ 常见的卷积主要是由连续紧密的卷积核对输入的图像特征进行滑窗式点乘求和操作,除此之外还有其他类型的卷积核在不同的任务中会用到,具体分类如表5.5所示。
​ 表5.5 卷积核分类

卷积类别 示意图 作用
标准卷积 image 最常用的卷积核,连续紧密的矩阵形式可以提取图像区域中的相邻像素之间的关联关系,$3\times3$的卷积核可以获得$3\times3$像素范围的感受视野
扩张卷积(带孔卷积或空洞卷积) image 引入一个称作扩张率(Dilation Rate)的参数,使同样尺寸的卷积核可以获得更大的感受视野,相应的在相同感受视野的前提下比普通卷积采用更少的参数。同样是$3\times3$的卷积核尺寸,扩张卷积可以提取$5\times5$范围的区域特征,在实时图像分割领域广泛应用
转置卷积 image 先对原始特征矩阵进行填充使其维度扩大到适配卷积目标输出维度,然后进行普通的卷积操作的一个过程,其输入到输出的维度变换关系恰好与普通卷积的变换关系相反,但这个变换并不是真正的逆变换操作,通常称为转置卷积(Transpose Convolution)而不是反卷积(Deconvolution)。转置卷积常见于目标检测领域中对小目标的检测和图像分割领域还原输入图像尺度。
可分离卷积 image 标准的卷积操作是同时对原始图像$H\times W\times C$三个方向的卷积运算,假设有$K$个相同尺寸的卷积核,这样的卷积操作需要用到的参数为$H\times W\times C\times K$个;若将长宽与深度方向的卷积操作分离出变为$H\times W$与$C$的两步卷积操作,则同样的卷积核个数$K$,只需要$(H\times W + C)\times K$个参数,便可得到同样的输出尺度。可分离卷积(Seperable Convolution)通常应用在模型压缩或一些轻量的卷积神经网络中,如MobileNet$^{[1]}$、Xception$^{[2]}$等

5.5 二维卷积与三维卷积有什么区别?

  • 二维卷积
    二维卷积操作如图5.3所示,为了更直观的说明,分别展示在单通道和多通道输入中,对单个通道输出的卷积操作。在单通道输入的情况下,若输入卷积核尺寸为 $(k_h, k_w, 1)​$,卷积核在输入图像的空间维度上进行滑窗操作,每次滑窗和 $(k_h, k_w)​$窗口内的值进行卷积操作,得到输出图像中的一个值。在多通道输入的情况下,假定输入图像特征通道数为3,卷积核尺寸则为$(k_h, k_w, 3)​$,每次滑窗与3个通道上的$(k_h, k_w)​$窗口内的所有值进行卷积操作,得到输出图像中的一个值。

image

  • 三维卷积
    3D卷积操作如图所示,同样分为单通道和多通道,且假定只使用1个卷积核,即输出图像仅有一个通道。对于单通道输入,与2D卷积不同之处在于,输入图像多了一个深度(depth)维度,卷积核也多了一个$k_d​$维度,因此3D卷积核的尺寸为$(k_h, k_w, k_d)​$,每次滑窗与$(k_h, k_w, k_d)​$窗口内的值进行相关操作,得到输出3D图像中的一个值。对于多通道输入,则与2D卷积的操作一样,每次滑窗与3个channels上的$(k_h, k_w, k_d)​$窗口内的所有值进行相关操作,得到输出3D图像中的一个值。

image

5.7 有哪些池化方法?

​ 池化操作通常也叫做子采样(Subsampling)或降采样(Downsampling),在构建卷积神经网络时,往往会用在卷积层之后,通过池化来降低卷积层输出的特征维度,有效减少网络参数的同时还可以防止过拟合现象。池化操作可以降低图像维度的原因,本质上是因为图像具有一种“静态性”的属性,这个意思是说在一个图像区域有用的特征极有可能在另一个区域同样有用。因此,为了描述一个大的图像,很直观的想法就是对不同位置的特征进行聚合统计。例如,可以计算图像在固定区域上特征的平均值 (或最大值)来代表这个区域的特征。
​ 表5.6 池化分类

池化类型 示意图 作用
一般池化(General Pooling) max_pooling 通常包括最大池化(Max Pooling)和平均池化(Mean Pooling)。以最大池化为例,池化范围$(2\times2)$和滑窗步长$(stride=2)$ 相同,仅提取一次相同区域的范化特征。
重叠池化(Overlapping Pooling) overlap_pooling 与一般池化操作相同,但是池化范围$P{size}$与滑窗步长$stride$关系为$P{size}>stride$,同一区域内的像素特征可以参与多次滑窗提取,得到的特征表达能力更强,但计算量更大。
空间金字塔池化$^*$(Spatial Pyramid Pooling) spatial_pooling 在进行多尺度目标的训练时,卷积层允许输入的图像特征尺度是可变的,紧接的池化层若采用一般的池化方法会使得不同的输入特征输出相应变化尺度的特征,而卷积神经网络中最后的全连接层则无法对可变尺度进行运算,因此需要对不同尺度的输出特征采样到相同输出尺度。

SPPNet$^{[3]}$就引入了空间池化的组合,对不同输出尺度采用不同的滑窗大小和步长以确保输出尺度相同$(win_{size}=\lceil \frac{in}{out}\rceil; stride=\lfloor \frac{in}{out}\rfloor; )$,同时用如金字塔式叠加的多种池化尺度组合,以提取更加丰富的图像特征。常用于多尺度训练和目标检测中的区域提议网络(Region Proposal Network)的兴趣区域(Region of Interest)提取

5.8 $1\times1$卷积作用?

​ NIN(Network in Network)$^{[4]}​$是第一篇探索$1\times1​$卷积核的论文,这篇论文通过在卷积层中使用MLP替代传统线性的卷积核,使单层卷积层内具有非线性映射的能力,也因其网络结构中嵌套MLP子网络而得名NIN。NIN对不同通道的特征整合到MLP自网络中,让不同通道的特征能够交互整合,使通道之间的信息得以流通,其中的MLP子网络恰恰可以用$1\times1​$的卷积进行代替。

​ GoogLeNet$^{[5]}​$则采用$1\times1​$卷积核来减少模型的参数量。在原始版本的Inception模块中,由于每一层网络采用了更多的卷积核,大大增加了模型的参数量。此时在每一个较大卷积核的卷积层前引入$1\times1​$卷积,可以通过分离通道与宽高卷积来减少模型参数量。以图5.2为例,在不考虑参数偏置项的情况下,若输入和输出的通道数为$C_1=16​$,则左半边网络模块所需的参数为$(1\times1+3\times3+5\times5+0)\times C_1\times C_1=8960​$;假定右半边网络模块采用的$1\times1​$卷积通道数为$C_2=8​$$(满足C_1>C_2)​$,则右半部分的网络结构所需参数量为$(1\times1\times (3C_1+C_2)+3\times3\times C_2 +5\times5\times C_2)\times C_1=5248​$ ,可以在不改变模型表达能力的前提下大大减少所使用的参数量。

image

​ 图5.2 Inception模块

综上所述,$1\times 1​$卷积的作用主要为以下两点:

  • 实现信息的跨通道交互和整合。
  • 对卷积核通道数进行降维和升维,减小参数量。

5.9 卷积层和池化层有什么区别?

​ 卷积层核池化层在结构上具有一定的相似性,都是对感受域内的特征进行提取,并且根据步长设置获取到不同维度的输出,但是其内在操作是有本质区别的,如表5.7所示。

卷积层 池化层
结构 零填充时输出维度不变,而通道数改变 通常特征维度会降低,通道数不变
稳定性 输入特征发生细微改变时,输出结果会改变 感受域内的细微变化不影响输出结果
作用 感受域内提取局部关联特征 感受域内提取泛化特征,降低维度
参数量 与卷积核尺寸、卷积核个数相关 不引入额外参数

5.10 卷积核是否一定越大越好?

​ 在早期的卷积神经网络中(如LeNet-5、AlexNet),用到了一些较大的卷积核($11\times11$和$5\times 5$),受限于当时的计算能力和模型结构的设计,无法将网络叠加得很深,因此卷积网络中的卷积层需要设置较大的卷积核以获取更大的感受域。但是这种大卷积核反而会导致计算量大幅增加,不利于训练更深层的模型,相应的计算性能也会降低。后来的卷积神经网络(VGG、GoogLeNet等),发现通过堆叠2个$3\times 3$卷积核可以获得与$5\times 5$卷积核相同的感受视野,同时参数量会更少($3×3×2+1$ < $ 5×5×1+1$),$3\times 3$卷积核被广泛应用在许多卷积神经网络中。因此可以认为,在大多数情况下通过堆叠较小的卷积核比直接采用单个更大的卷积核会更加有效。

​ 但是,这并不是表示更大的卷积核就没有作用,在某些领域应用卷积神经网络时仍然可以采用较大的卷积核。譬如在自然语言处理领域,由于文本内容不像图像数据可以对特征进行很深层的抽象,往往在该领域的特征提取只需要较浅层的神经网络即可。在将卷积神经网络应用在自然语言处理领域时,通常都是较为浅层的卷积层组成,但是文本特征有时又需要有较广的感受域让模型能够组合更多的特征(如词组和字符),此时直接采用较大的卷积核将是更好的选择。

​ 综上所述,卷积核的大小并没有绝对的优劣,需要视具体的应用场景而定,但是极大和极小的卷积核都是不合适的,单独的$1\times 1$极小卷积核只能用作分离卷积而不能对输入的原始特征进行有效的组合,极大的卷积核通常会组合过多的无意义特征从而浪费了大量的计算资源。

5.11 每层卷积是否只能用一种尺寸的卷积核?

​ 经典的神经网络一般都属于层叠式网络,每层仅用一个尺寸的卷积核,如VGG结构中使用了大量的$3×3$卷积层。事实上,同一层特征图可以分别使用多个不同尺寸的卷积核,以获得不同尺度的特征,再把这些特征结合起来,得到的特征往往比使用单一卷积核的要好,如GoogLeNet、Inception系列的网络,均是每层使用了多个卷积核结构。如图5.3所示,输入的特征在同一层分别经过$1×1$、$3×3$和$5×5$三种不同尺寸的卷积核,再将分别得到的特征进行整合,得到的新特征可以看作不同感受域提取的特征组合,相比于单一卷积核会有更强的表达能力。

image

​ 图5.3 Inception模块结构

5.12 怎样才能减少卷积层参数量?

减少卷积层参数量的方法可以简要地归为以下几点:

  • 使用堆叠小卷积核代替大卷积核:VGG网络中2个$3\times 3$的卷积核可以代替1个$5\times 5$的卷积核
  • 使用分离卷积操作:将原本$K\times K\times C$的卷积操作分离为$K\times K\times 1$和$1\times1\times C$的两部分操作
  • 添加$1\times 1$的卷积操作:与分离卷积类似,但是通道数可变,在$K\times K\times C_1$卷积前添加$1\times1\times C_2$的卷积核(满足$C_2 <C_1$)
  • 在卷积层前使用池化操作:池化可以降低卷积层的输入特征维度

5.13 在进行卷积操作时,必须同时考虑通道和区域吗?

​ 标准卷积中,采用区域与通道同时处理的操作,如下图所示:

image

​ 这样做可以简化卷积层内部的结构,每一个输出的特征像素都由所有通道的同一个区域提取而来。

​ 但是这种方式缺乏灵活性,并且在深层的网络结构中使得运算变得相对低效,更为灵活的方式是使区域和通道的卷积分离开来,通道分离(深度分离)卷积网络由此诞生。如下图所示,Xception网络可解决上述问题。

image

​ 我们首先对每一个通道进行各自的卷积操作,有多少个通道就有多少个过滤器。得到新的通道特征矩阵之后,再对这批新通道特征进行标准的$1×1​$跨通道卷积操作。

5.14 采用宽卷积的好处有什么?

​ 宽卷积对应的是窄卷积,实际上并不是卷积操作的类型,指的是卷积过程中的填充方法,对应的是’SAME’填充和’VALID’填充。’SAME’填充通常采用零填充的方式对卷积核不满足整除条件的输入特征进行补全,以使卷积层的输出维度保持与输入特征维度一致;’VALID’填充的方式则相反,实际并不进行任何填充,在输入特征边缘位置若不足以进行卷积操作,则对边缘信息进行舍弃,因此在步长为1的情况下该填充方式的卷积层输出特征维度可能会略小于输入特征的维度。此外,由于前一种方式通过补零来进行完整的卷积操作,可以有效地保留原始的输入特征信息。

​ 比如下图左部分为窄卷积。注意到越在边缘的位置被卷积的次数越少。宽卷积可以看作在卷积之前在边缘用0补充,常见有两种情况,一个是全补充,如下图右部分,这样输出大于输入的维度。另一种常用的方法是补充一一部分0值,使得输出和输入的维度一致。

image

5.15 理解转置卷积与棋盘效应

5.15.1 标准卷积

在理解转置卷积之前,需要先理解标准卷积的运算方式。

首先给出一个输入输出结果

image

那是怎样计算的呢?

卷积的时候需要对卷积核进行180的旋转,同时卷积核中心与需计算的图像像素对齐,输出结构为中心对齐像素的一个新的像素值,计算例子如下:

image

这样计算出左上角(即第一行第一列)像素的卷积后像素值。

给出一个更直观的例子,从左到右看,原像素经过卷积由1变成-8。

image

通过滑动卷积核,就可以得到整张图片的卷积结果。

5.15.2 转置卷积

图像的deconvolution过程如下:

image

输入:2x2, 卷积核:4x4, 滑动步长:3, 输出:7x7

过程如下:

  1. 输入图片每个像素进行一次full卷积,根据full卷积大小计算可以知道每个像素的卷积后大小为 1+4-1=4, 即4x4大小的特征图,输入有4个像素所以4个4x4的特征图

  2. 将4个特征图进行步长为3的相加; 输出的位置和输入的位置相同。步长为3是指每隔3个像素进行相加,重叠部分进行相加,即输出的第1行第4列是由红色特阵图的第一行第四列与绿色特征图的第一行第一列相加得到,其他如此类推。

    可以看出翻卷积的大小是由卷积核大小与滑动步长决定, in是输入大小, k是卷积核大小, s是滑动步长, out是输出大小 得到 out = (in - 1) s + k 上图过程就是, (2 - 1) 3 + 4 = 7。

5.15.3 棋盘效应

5.16 卷积神经网络的参数设置

​ 卷积神经网络中常见的参数在其他类型的神经网络中也是类似的,但是参数的设置还得结合具体的任务才能设置在合理的范围,具体的参数列表如表XX所示。
​ 表XX 卷积神经网络常见参数

参数名 常见设置 参数说明
学习率(Learning Rate) $0-1$ 反向传播网络中更新权值矩阵的步长,在一些常见的网络中会在固定迭代次数或模型不再收敛后对学习率进行指数下降(如$lr=lr\times 0.1$)。当学习率越大计算误差对权值矩阵的影响越大,容易在某个局部最优解附近震荡;越小的学习率对网络权值的更新越精细,但是需要花费更多的时间去迭代
批次大小(Batch Size) $1-N$ 批次大小指定一次性流入模型的数据样本个数,根据任务和计算性能限制判断实际取值,在一些图像任务中往往由于计算性能和存储容量限制只能选取较小的值。在相同迭代次数的前提下,数值越大模型越稳定,泛化能力越强,损失值曲线越平滑,模型也更快地收敛,但是每次迭代需要花费更多的时间
数据轮次(Epoch) $1-N$ 数据轮次指定所有训练数据在模型中训练的次数,根据数据集规模和分布情况会设置为不同的值。当模型较为简单或训练数据规模较小时,通常轮次不宜过高,否则模型容易过拟合;模型较为复杂或训练数据规模足够大时,可适当提高数据的训练轮次。
权重衰减系数(Weight Decay) $0-0.001$ 模型训练过程中反向传播权值更新的权重衰减值

5.17 提高卷积神经网络的泛化能力

​ 卷积神经网络与其他类型的神经网络类似,在采用反向传播进行训练的过程中比较依赖输入的数据分布,当数据分布较为极端的情况下容易导致模型欠拟合或过拟合,表XX记录了提高卷积网络泛化能力的方法。
​ 表XX 提高卷积网络化能力的方法

方法 说明
使用更多数据 在有条件的前提下,尽可能多地获取训练数据是最理想的方法,更多的数据可以让模型得到充分的学习,也更容易提高泛化能力
使用更大批次 在相同迭代次数和学习率的条件下,每批次采用更多的数据将有助于模型更好的学习到正确的模式,模型输出结果也会更加稳定
调整数据分布 大多数场景下的数据分布是不均匀的,模型过多地学习某类数据容易导致其输出结果偏向于该类型的数据,此时通过调整输入的数据分布可以一定程度提高泛化能力
调整目标函数 在某些情况下,目标函数的选择会影响模型的泛化能力,如目标函数$f(y,y’)= y-y’ $在某类样本已经识别较为准确而其他样本误差较大的侵害概况下,不同类别在计算损失结果的时候距离权重是相同的,若将目标函数改成$f(y,y’)=(y-y’)^2$则可以使误差小的样本计算损失的梯度比误差大的样本更小,进而有效地平衡样本作用,提高模型泛化能力
调整网络结构 在浅层卷积神经网络中,参数量较少往往使模型的泛化能力不足而导致欠拟合,此时通过叠加卷积层可以有效地增加网络参数,提高模型表达能力;在深层卷积网络中,若没有充足的训练数据则容易导致模型过拟合,此时通过简化网络结构减少卷积层数可以起到提高模型泛化能力的作用
数据增强 数据增强又叫数据增广,在有限数据的前提下通过平移、旋转、加噪声等一些列变换来增加训练数据,同类数据的表现形式也变得更多样,有助于模型提高泛化能力,需要注意的是数据变化应尽可能不破坏元数数据的主体特征(如在图像分类任务中对图像进行裁剪时不能将分类主体目标裁出边界)。
权值正则化 权值正则化就是通常意义上的正则化,一般是在损失函数中添加一项权重矩阵的正则项作为惩罚项,用来惩罚损失值较小时网络权重过大的情况,此时往往是网络权值过拟合了数据样本(如$Loss=f(WX+b,y’)+\frac{\lambda}{\eta}\sum{ W }$)。
屏蔽网络节点 该方法可以认为是网络结构上的正则化,通过随机性地屏蔽某些神经元的输出让剩余激活的神经元作用,可以使模型的容错性更强。

对大多数神经网络模型同样通用

5.18 卷积神经网络在不同领域的应用

​ 卷积神经网络中的卷积操作是其关键组成,而卷积操作只是一种数学运算方式,实际上对不同类型的数值表示数据都是通用的,尽管这些数值可能表示的是图像像素值、文本序列中单个字符或是语音片段中单字的音频。只要使原始数据能够得到有效地数值化表示,卷积神经网络能够在不同的领域中得到应用,要关注的是如何将卷积的特性更好地在不同领域中应用,如表XX所示。
​ 表XX 卷积神经网络不同领域的应用
| 应用领域 | 输入数据图示 | 说明 |
| :——-: | :—————: | :— |
| 图像处理 | image_process | 卷积神经网络在图像处理领域有非常广泛的应用,这是因为图像数据本身具有的局部完整性非常 |
| 自然语言处理 | NLP | |
| 语音处理 | audio_process | |

5.18.1 联系

​ 自然语言处理是对一维信号(词序列)做操作。
​ 计算机视觉是对二维(图像)或三维(视频流)信号做操作。

5.18.2 区别

​ 自然语言处理的输入数据通常是离散取值(例如表示一个单词或字母通常表示为词典中的one hot向量),计算机视觉则是连续取值(比如归一化到0,1之间的灰度值)。CNN有两个主要特点,区域不变性(location invariance)和组合性(Compositionality)。

  1. 区域不变性:滤波器在每层的输入向量(图像)上滑动,检测的是局部信息,然后通过pooling取最大值或均值。pooling这步综合了局部特征,失去了每个特征的位置信息。这很适合基于图像的任务,比如要判断一幅图里有没有猫这种生物,你可能不会去关心这只猫出现在图像的哪个区域。但是在NLP里,词语在句子或是段落里出现的位置,顺序,都是很重要的信息。
  2. 局部组合性:CNN中,每个滤波器都把较低层的局部特征组合生成较高层的更全局化的特征。这在CV里很好理解,像素组合成边缘,边缘生成形状,最后把各种形状组合起来得到复杂的物体表达。在语言里,当然也有类似的组合关系,但是远不如图像来的直接。而且在图像里,相邻像素必须是相关的,相邻的词语却未必相关。

5.19 卷积神经网络凸显共性的方法?

5.19.1 局部连接

​ 我们首先了解一个概念,感受野,即每个神经元仅与输入神经元相连接的一块区域。
在图像卷积操作中,神经元在空间维度上是局部连接,但在深度上是全连接。局部连接的思想,是受启发于生物学里的视觉系统结构,视觉皮层的神经元就是仅用局部接受信息。对于二维图像,局部像素关联性较强。这种局部连接保证了训练后的滤波器能够对局部特征有最强的响应,使神经网络可以提取数据的局部特征;
下图是一个很经典的图示,左边是全连接,右边是局部连接。

image

对于一个1000 × 1000的输入图像而言,如果下一个隐藏层的神经元数目为10^6个,采用全连接则有1000 × 1000 × 10^6 = 10^12个权值参数,如此巨大的参数量几乎难以训练;而采用局部连接,隐藏层的每个神经元仅与图像中10 × 10的局部图像相连接,那么此时的权值参数数量为10 × 10 × 10^6 = 10^8,将直接减少4个数量级。

5.19.2 权值共享

​ 权值共享,即计算同一深度的神经元时采用的卷积核参数是共享的。权值共享在一定程度上讲是有意义的,是由于在神经网络中,提取的底层边缘特征与其在图中的位置无关。但是在另一些场景中是无意的,如在人脸识别任务,我们期望在不同的位置学到不同的特征。
需要注意的是,权重只是对于同一深度切片的神经元是共享的。在卷积层中,通常采用多组卷积核提取不同的特征,即对应的是不同深度切片的特征,而不同深度切片的神经元权重是不共享。相反,偏置这一权值对于同一深度切片的所有神经元都是共享的。
权值共享带来的好处是大大降低了网络的训练难度。如下图,假设在局部连接中隐藏层的每一个神经元连接的是一个10 × 10的局部图像,因此有10 × 10个权值参数,将这10 × 10个权值参数共享给剩下的神经元,也就是说隐藏层中10^6个神经元的权值参数相同,那么此时不管隐藏层神经元的数目是多少,需要训练的参数就是这 10 × 10个权值参数(也就是卷积核的大小)。

image

这里就体现了卷积神经网络的奇妙之处,使用少量的参数,却依然能有非常出色的性能。上述仅仅是提取图像一种特征的过程。如果要多提取出一些特征,可以增加多个卷积核,不同的卷积核能够得到图像不同尺度下的特征,称之为特征图(feature map)。

5.19.3 池化操作

池化操作与多层次结构一起,实现了数据的降维,将低层次的局部特征组合成为较高层次的特征,从而对整个图片进行表示。如下图:

image

5.20 全连接、局部连接、全卷积与局部卷积

​ 大多数神经网络中高层网络通常会采用全连接层(Global Connected Layer),通过多对多的连接方式对特征进行全局汇总,可以有效地提取全局信息。但是全连接的方式需要大量的参数,是神经网络中最占资源的部分之一,因此就需要由局部连接(Local Connected Layer),仅在局部区域范围内产生神经元连接,能够有效地减少参数量。根据卷积操作的作用范围可以分为全卷积(Global Convolution)和局部卷积(Local Convolution)。实际上这里所说的全卷积就是标准卷积,即在整个输入特征维度范围内采用相同的卷积核参数进行运算,全局共享参数的连接方式可以使神经元之间的连接参数大大减少;局部卷积又叫平铺卷积(Tiled Convolution)或非共享卷积(Unshared Convolution),是局部连接与全卷积的折衷。四者的比较如表XX所示。
​ 表XX 卷积网络中连接方式的对比

连接方式 示意图 说明
全连接 full-connected 层间神经元完全连接,每个输出神经元可以获取到所有输入神经元的信息,有利于信息汇总,常置于网络末层;连接与连接之间独立参数,大量的连接大大增加模型的参数规模。
局部连接 local-connected 层间神经元只有局部范围内的连接,在这个范围内采用全连接的方式,超过这个范围的神经元则没有连接;连接与连接之间独立参数,相比于全连接减少了感受域外的连接,有效减少参数规模
全卷积 convolution 层间神经元只有局部范围内的连接,在这个范围内采用全连接的方式,连接所采用的参数在不同感受域之间共享,有利于提取特定模式的特征;相比于局部连接,共用感受域之间的参数可以进一步减少参数量。
局部卷积 local-conv 层间神经元只有局部范围内的连接,感受域内采用全连接的方式,而感受域之间间隔采用局部连接与全卷积的连接方式;相比与全卷积成倍引入额外参数,但有更强的灵活性和表达能力;相比于局部连接,可以有效控制参数量

5.21 局部卷积的应用

并不是所有的卷积都会进行权重共享,在某些特定任务中,会使用不权重共享的卷积。下面通过人脸这一任务来进行讲解。在读人脸方向的一些paper时,会发现很多都会在最后加入一个Local Connected Conv,也就是不进行权重共享的卷积层。总的来说,这一步的作用就是使用3D模型来将人脸对齐,从而使CNN发挥最大的效果。
image

截取论文中的一部分图,经过3D对齐以后,形成的图像均是152×152,输入到上述的网络结构中。该结构的参数如下:

Conv:32个11×11×3的卷积核,

Max-pooling: 3×3,stride=2,

Conv: 16个9×9的卷积核,

Local-Conv: 16个9×9的卷积核,

Local-Conv: 16个7×7的卷积核,

Local-Conv: 16个5×5的卷积核,

Fully-connected: 4096维,

Softmax: 4030维。

前三层的目的在于提取低层次的特征,比如简单的边和纹理。其中Max-pooling层使得卷积的输出对微小的偏移情况更加鲁棒。但不能使用更多的Max-pooling层,因为太多的Max-pooling层会使得网络损失图像信息。全连接层将上一层的每个单元和本层的所有单元相连,用来捕捉人脸图像不同位置特征之间的相关性。最后使用softmax层用于人脸分类。
中间三层都是使用参数不共享的卷积核,之所以使用参数不共享,有如下原因:

(1)对齐的人脸图片中,不同的区域会有不同的统计特征,因此并不存在特征的局部稳定性,所以使用相同的卷积核会导致信息的丢失。

(2)不共享的卷积核并不增加inference时特征的计算量,仅会增加训练时的计算量。
使用不共享的卷积核,由于需要训练的参数量大大增加,因此往往需要通过其他方法增加数据量。

5.22 NetVLAD池化 (贡献者:熊楚原-中国人民大学)

NetVLAD是论文[15]提出的一个局部特征聚合的方法。

在传统的网络里面,例如VGG啊,最后一层卷积层输出的特征都是类似于Batchsize x 3 x 3 x 512的这种东西,然后会经过FC聚合,或者进行一个Global Average Pooling(NIN里的做法),或者怎么样,变成一个向量型的特征,然后进行Softmax or 其他的Loss。

这种方法说简单点也就是输入一个图片或者什么的结构性数据,然后经过特征提取得到一个长度固定的向量,之后可以用度量的方法去进行后续的操作,比如分类啊,检索啊,相似度对比等等。

那么NetVLAD考虑的主要是最后一层卷积层输出的特征这里,我们不想直接进行欠采样或者全局映射得到特征,对于最后一层输出的W x H x D,设计一个新的池化,去聚合一个“局部特征“,这即是NetVLAD的作用。

NetVLAD的一个输入是一个W x H x D的图像特征,例如VGG-Net最后的3 x 3 x 512这样的矩阵,在网络中还需加一个维度为Batchsize。

NetVLAD还需要另输入一个标量K即表示VLAD的聚类中心数量,它主要是来构成一个矩阵C,是通过原数据算出来的每一个$W \times H$特征的聚类中心,C的shape即$C: K \times D$,然后根据三个输入,VLAD是计算下式的V:

其中j表示维度,从1到D,可以看到V的j是和输入与c对应的,对每个类别k,都对所有的x进行了计算,如果$x_i$属于当前类别k,$a_k=1$,否则$a_k=0$,计算每一个x和它聚类中心的残差,然后把残差加起来,即是每个类别k的结果,最后分别L2正则后拉成一个长向量后再做L2正则,正则非常的重要,因为这样才能统一所有聚类算出来的值,而残差和的目的主要是消减不同聚类上的分布不均,两者共同作用才能得到最后正常的输出。

输入与输出如下图所示:

image

中间得到的K个D维向量即是对D个x都进行了与聚类中心计算残差和的过程,最终把K个D维向量合起来后进行即得到最终输出的$K \times D$长度的一维向量。

而VLAD本身是不可微的,因为上面的a要么是0要么是1,表示要么当前描述x是当前聚类,要么不是,是个离散的,NetVLAD为了能够在深度卷积网络里使用反向传播进行训练,对a进行了修正。

那么问题就是如何重构一个a,使其能够评估当前的这个x和各个聚类的关联程度?用softmax来得到:

将这个把上面的a替换后,即是NetVLAD的公式,可以进行反向传播更新参数。

所以一共有三个可训练参数,上式a中的$W: K \times D$,上式a中的$b: K \times 1$,聚类中心$c: K \times D$,而原始VLAD只有一个参数c。

最终池化得到的输出是一个恒定的K x D的一维向量(经过了L2正则),如果带Batchsize,输出即为Batchsize x (K x D)的二维矩阵。

NetVLAD作为池化层嵌入CNN网络即如下图所示,

image

原论文中采用将传统图像检索方法VLAD进行改进后应用在CNN的池化部分作为一种另类的局部特征池化,在场景检索上取得了很好的效果。

后续相继又提出了ActionVLAD、ghostVLAD等改进。

参考文献

[1] 卷积神经网络研究综述[J]. 计算机学报, 2017, 40(6):1229-1251.

[2] 常亮, 邓小明, 周明全,等. 图像理解中的卷积神经网络[J]. 自动化学报, 2016, 42(9):1300-1312.

[3] Chua L O. CNN: A Paradigm for Complexity[M]// CNN a paradigm for complexity /. 1998.

[4] He K, Gkioxari G, Dollar P, et al. Mask R-CNN[J]. IEEE Transactions on Pattern Analysis & Machine Intelligence, 2017, PP(99):1-1.

[5] Hoochang S, Roth H R, Gao M, et al. Deep Convolutional Neural Networks for Computer-Aided Detection: CNN Architectures, Dataset Characteristics and Transfer Learning[J]. IEEE Transactions on Medical Imaging, 2016, 35(5):1285-1298.

[6] 许可. 卷积神经网络在图像识别上的应用的研究[D]. 浙江大学, 2012.

[7] 陈先昌. 基于卷积神经网络的深度学习算法与应用研究[D]. 浙江工商大学, 2014.

[8] CS231n Convolutional Neural Networks for Visual Recognition, Stanford

[9] Machine Learning is Fun! Part 3: Deep Learning and Convolutional Neural Networks

[10] cs231n 动态卷积图:http://cs231n.github.io/assets/conv-demo/index.html

[11] Krizhevsky A, Sutskever I, Hinton G E. Imagenet classification with deep convolutional neural networks[C]//Advances in neural information processing systems. 2012: 1097-1105.

[12] Sun Y, Wang X, Tang X. Deep learning face representation from predicting 10,000 classes[C]//Computer Vision and Pattern Recognition (CVPR), 2014 IEEE Conference on. IEEE, 2014: 1891-1898.

[13] 魏秀参.解析深度学习——卷积神经网络原理与视觉实践[M].电子工业出版社, 2018

[14] Jianxin W U , Gao B B , Wei X S , et al. Resource-constrained deep learning: challenges and practices[J]. Scientia Sinica(Informationis), 2018.

[15] Arandjelovic R , Gronat P , Torii A , et al. [IEEE 2016 IEEE Conference on Computer Vision and Pattern Recognition (CVPR) - Las Vegas, NV, USA (2016.6.27-2016.6.30)] 2016 IEEE Conference on Computer Vision and Pattern Recognition (CVPR) - NetVLAD: CNN Architecture for Weakly Supervised Place Recognition[C]// 2016:5297-5307.