在前面我们讲述了DNN的模型与前向反向传播算法。而在DNN大类中,卷积神经网络(Convolutional Neural Networks,以下简称CNN)是最为成功的DNN特例之一。CNN广泛的应用于图像识别,当然现在也应用于NLP等其他领域。

本文我们就对CNN的模型结构做一个总结。然后在此基础上,介绍CNN的前向传播算法和反向传播算法。

在学习CNN前,推荐大家先学习DNN的知识。如果不熟悉DNN而去直接学习CNN,难度会比较大。这是DNN的教程的链接:

深度学习(一):DNN前向传播算法和反向传播算法
深度学习(二):DNN损失函数和激活函数的选择
深度学习(三):DNN的正则化策略综述

文章目录

  • 一、卷积神经网络(CNN)模型结构
    • 1.1 CNN的基本结构
    • 1.2 CNN中的卷积层
    • 1.3 CNN中的池化层
  • 二、卷积神经网络(CNN)前向传播算法
    • 2.1 回顾CNN的结构
    • 2.2 CNN输入层前向传播到卷积层
    • 2.3 隐藏层前向传播到卷积层
    • 2.4 隐藏层前向传播到池化层
    • 2.5 隐藏层前向传播到全连接层
    • 2.6 CNN前向传播算法小结
  • 三、卷积神经网络(CNN)反向传播算法
    • 3.1 回顾DNN的反向传播算法
    • 3.2 CNN的反向传播算法思想
    • 3.3 已知池化层的δl\delta^{l}δl,推导上一隐藏层的δl−1\delta^{l-1}δl−1
    • 3.4 已知卷积层的δl\delta^{l}δl,推导上一隐藏层的δl−1\delta^{l-1}δl−1
    • 3.5 已知卷积层的δl\delta^lδl,推导该层的W,bW,bW,b的梯度
    • 3.6 CNN反向传播算法总结
  • 参考文献

一、卷积神经网络(CNN)模型结构

1.1 CNN的基本结构

首先我们来看看CNN的基本结构。一个常见的CNN例子如下图:

图中是一个图形识别的CNN模型。可以看出最左边的船的图像就是我们的输入层,计算机理解为输入若干个矩阵,这点和DNN基本相同。

接着是卷积层(Convolution Layer),这个是CNN特有的,我们后面专门来讲。卷积层的激活函数使用的是ReLU。我们在DNN中介绍过ReLU的激活函数,它其实很简单,就是ReLU(x)=max(0,x)ReLU(x)=max(0,x)ReLU(x)=max(0,x)。在卷积层后面是池化层(Pooling layer),这个也是CNN特有的,我们后面也会专门来讲。需要注意的是,池化层没有激活函数。

卷积层+池化层的组合可以在隐藏层出现很多次,上图中出现两次。而实际上这个次数是根据模型的需要而来的。当然我们也可以灵活使用使用卷积层+卷积层,或者卷积层+卷积层+池化层的组合,这些在构建模型的时候没有限制。但是最常见的CNN都是若干卷积层+池化层的组合,如上图中的CNN结构。

在若干卷积层+池化层后面是全连接层(Fully Connected Layer, 简称FC),全连接层其实就是我们前面讲的DNN结构,只是输出层使用了Softmax激活函数来做图像识别的分类,这点我们在DNN中也有讲述。

从上面CNN的模型描述可以看出,CNN相对于DNN,比较特殊的是卷积层和池化层,如果我们熟悉DNN,只要把卷积层和池化层的原理搞清楚了,那么搞清楚CNN就容易很多了。

1.2 CNN中的卷积层

CNN中的卷积,简单来说,就是对输入的图像的不同局部的矩阵和卷积核矩阵各个位置的元素相乘,然后相加得到。

举个例子如下,图中的输入是一个二维的3x4的矩阵,而卷积核是一个2x2的矩阵。这里我们假设卷积是一次移动一个像素来卷积的,那么首先我们对输入的左上角2x2局部和卷积核卷积,即各个位置的元素相乘再相加,得到的输出矩阵S的S00S_{00}S00​的元素,值为aw+bx+ey+fzaw+bx+ey+fzaw+bx+ey+fz。接着我们将输入的局部向右平移一个像素,现在是(b,c,f,g)(b,c,f,g)(b,c,f,g)四个元素构成的矩阵和卷积核来卷积,这样我们得到了输出矩阵SSS的S01S_{01}S01​的元素,同样的方法,我们可以得到输出矩阵SSS的S02S_{02}S02​,S10S_{10}S10​,S11S_{11}S11​,S12S_{12}S12​的元素。

最终我们得到卷积输出的矩阵为一个2x3的矩阵SSS。

再举一个动态的卷积过程的例子如下:

我们有下面这个绿色的5x5输入矩阵,卷积核是一个下面这个黄色的3x3的矩阵。卷积的步幅是一个像素。则卷积的过程如下面的动图。卷积的结果是一个3x3的矩阵。

上面举的例子都是二维的输入,卷积的过程比较简单,那么如果输入是多维的呢?比如在前面一组卷积层+池化层的输出是3个矩阵,这3个矩阵作为输入呢,那么我们怎么去卷积呢?又比如输入的是对应RGB的彩色图像,即是三个分布对应R,G和B的矩阵呢?

在斯坦福大学的cs231n的课程上,有一个动态的例子,链接在这。建议大家对照着例子中的动图看下面的讲解。

大家打开这个例子可以看到,这里面输入是3个7x7的矩阵。实际上原输入是3个5x5的矩阵。只是在原来的输入周围加上了1的padding,即将周围都填充一圈的0,变成了3个7x7的矩阵。

例子里面使用了两个卷积核,我们先关注于卷积核W0。和上面的例子相比,由于输入是3个7x7的矩阵,或者说是7x7x3的张量,则我们对应的卷积核W0也必须最后一维是3的张量,这里卷积核W0的单个子矩阵维度为3x3。那么卷积核W0实际上是一个3x3x3的张量。同时和上面的例子比,这里的步幅为2,也就是每次卷积后会移动2个像素的位置。

最终的卷积过程和上面的2维矩阵类似,上面是矩阵的卷积,即两个矩阵对应位置的元素相乘后相加。这里是张量的卷积,即两个张量的3个子矩阵卷积后,再把卷积的结果相加后再加上偏倚b。其实我们仔细分析,可以得到:一个卷积kernel后的值类比就是全连接层的神经元,kernel中的权重就是这个神经元的权重。

7x7x3的张量和3x3x3的卷积核张量W0卷积的结果是一个3x3的矩阵。由于我们有两个卷积核W0和W1,因此最后卷积的结果是两个3x3的矩阵。或者说卷积的结果是一个3x3x2的张量。

仔细回味下卷积的过程,输入是7x7x3的张量,卷积核是两个3x3x3的张量。卷积步幅为2,最后得到了输出是3x3x2的张量。如果把上面的卷积过程用数学公式表达出来就是:
s(i,j)=(X∗W)(i,j)+b=∑k=1n_in(Xk∗Wk)(i,j)+bs(i,j)=(X*W)(i,j) + b = \sum\limits_{k=1}^{n\_in}(X_k*W_k)(i,j) +b s(i,j)=(X∗W)(i,j)+b=k=1∑n_in​(Xk​∗Wk​)(i,j)+b

其中,n_inn\_{in}n_in为输入矩阵的个数,或者是张量的最后一维的维数。XkX_kXk​代表第kkk个输入矩阵。WkW_kWk​代表卷积核的第kkk个子卷积核矩阵。s(i,j)s(i,j)s(i,j)即卷积核WWW对应的输出矩阵的对应位置元素的值。特别需要注意得是:∗*∗代表卷积,并非普通的乘法。这个在后面要反复用到。

通过上面的例子,相信大家对CNN的卷积层的卷积过程有了一定的了解。

对于卷积后的输出,一般会通过ReLU激活函数,将输出的张量中的小于0的位置对应的元素值都变为0

1.3 CNN中的池化层

相比卷积层的复杂,池化层则要简单的多,所谓的池化,个人理解就是对输入张量的各个子矩阵进行压缩。假如是2x2的池化,那么就将子矩阵的每2x2个元素变成一个元素,如果是3x3的池化,那么就将子矩阵的每3x3个元素变成一个元素,这样输入矩阵的维度就变小了。

要想将输入子矩阵的每nnnxnnn个元素变成一个元素,那么需要一个池化标准。常见的池化标准有2个,MAX或者是Average。即取对应区域的最大值或者平均值作为池化后的元素值。

下面这个例子采用取最大值的池化方法。同时采用的是2x2的池化。步幅为2。

首先对红色2x2区域进行池化,由于此2x2区域的最大值为6。那么对应的池化输出位置的值为6,由于步幅为2,此时移动到绿色的位置去进行池化,输出的最大值为8。同样的方法,可以得到黄色区域和蓝色区域的输出值。最终,我们的输入4x4的矩阵在池化后变成了2x2的矩阵。进行了压缩。

二、卷积神经网络(CNN)前向传播算法

2.1 回顾CNN的结构

在上一章里,我们已经讲到了CNN的结构,包括输出层,若干的卷积层+ReLU激活函数,若干的池化层,DNN全连接层,以及最后的用Softmax激活函数的输出层。这里我们用一个彩色的汽车样本的图像识别再从感官上回顾下CNN的结构。图中的CONV即为卷积层,POOL即为池化层,而FC即为DNN全连接层,包括了我们上面最后的用Softmax激活函数的输出层。

从上图可以看出,要理顺CNN的前向传播算法,重点是输入层的前向传播,卷积层的前向传播以及池化层的前向传播。而DNN全连接层和用Softmax激活函数的输出层的前向传播算法我们在讲DNN时已经讲到了。

2.2 CNN输入层前向传播到卷积层

输入层的前向传播是CNN前向传播算法的第一步。一般输入层对应的都是卷积层,因此我们标题是输入层前向传播到卷积层。

我们这里还是以图像识别为例。

先考虑最简单的,样本都是二维的黑白图片。这样输入层XXX就是一个矩阵,矩阵的值等于图片的各个像素位置的值。这时和卷积层相连的卷积核WWW就也是矩阵。

如果样本都是有RGB的彩色图片,这样输入XXX就是3个矩阵,即分别对应R,G和B的矩阵,或者说是一个张量。这时和卷积层相连的卷积核WWW就也是张量,对应的最后一维的维度为3。即每个卷积核都是3个子矩阵组成。

同样的方法,对于3D的彩色图片之类的样本,我们的输入XXX可以是4维,5维的张量,那么对应的卷积核WWW也是个高维的张量。

不管维度多高,对于我们的输入,前向传播的过程可以表示为:
a2=σ(z2)=σ(a1∗W2+b2)a^2= \sigma(z^2) = \sigma(a^1*W^2 +b^2) a2=σ(z2)=σ(a1∗W2+b2)

其中,上标代表层数,星号代表卷积,而bbb代表我们的偏倚, σ\sigmaσ为激活函数,这里一般都是ReLU。

和DNN的前向传播比较一下,其实形式非常的像,只是我们这儿是张量的卷积,而不是矩阵的乘法。同时由于WWW是张量,那么同样的位置,WWW参数的个数就比DNN多很多了。

为了简化我们的描述,本文后面如果没有特殊说明,我们都默认输入是3维的张量,即用RBG可以表示的彩色图片。

这里需要我们自己定义的CNN模型参数是:

1) 一般我们的卷积核不止一个,比如有KKK个,那么我们这一层卷积层的对应的输出的三维张量最后一维就为KKK。

2) 卷积核中每个子矩阵的的大小,一般我们都用子矩阵为方阵的卷积核,比如F×FF\times FF×F的子矩阵。

3) 填充padding(以下简称PPP),我们卷积的时候,为了可以更好的识别边缘,一般都会在输入矩阵在周围加上若干圈的0再进行卷积,加多少圈则PPP为多少。

4) 步幅stride(以下简称SSS),即在卷积过程中每次移动的像素距离大小。

2.3 隐藏层前向传播到卷积层

现在我们再来看普通隐藏层前向传播到卷积层时的前向传播算法。

假设隐藏层的输出是KKK个矩阵对应的三维张量(最后一维为KKK),则输出到卷积层的卷积核也是KKK个子矩阵对应的三维张量(最后一维为KKK)。这时表达式和输入层的很像,也是
al=σ(zl)=σ(al−1∗Wl+bl)a^l= \sigma(z^l) = \sigma(a^{l-1}*W^l +b^l) al=σ(zl)=σ(al−1∗Wl+bl)

其中,上标代表层数,星号代表卷积,而bbb代表我们的偏倚, σ\sigmaσ为激活函数,这里一般都是ReLU。

也可以写成KKK个子矩阵子矩阵卷积后对应位置相加的形式,即:
al=σ(zl)=σ(∑k=1Kzkl)=σ(∑k=1Kakl−1∗Wkl+bl)a^l= \sigma(z^l) = \sigma(\sum\limits_{k=1}^{K}z_k^l) = \sigma(\sum\limits_{k=1}^{K}a_k^{l-1}*W_k^l +b^l) al=σ(zl)=σ(k=1∑K​zkl​)=σ(k=1∑K​akl−1​∗Wkl​+bl)

和上一节唯一的区别仅仅在于,这里的输入是隐藏层来的,而不是我们输入的原始图片样本形成的矩阵。

需要我们定义的CNN模型参数也和上一节一样,这里我们需要定义卷积核的个数KKK,卷积核子矩阵的维度FFF,填充大小PPP以及步幅SSS。

2.4 隐藏层前向传播到池化层

池化层的处理逻辑是比较简单的,我们的目的就是对输入的矩阵进行缩小概括。比如输入的若干矩阵是N×NN\times NN×N维的,而我们的池化大小是k×kk\times kk×k的区域,则输出的矩阵都是Nk×Nk\frac{N}{k} \times \frac{N}{k}kN​×kN​维的。

这里需要需要我们定义的CNN模型参数是:

1)池化区域的大小kkk

2)池化的标准,一般是MAX或者Average。

2.5 隐藏层前向传播到全连接层

由于全连接层就是普通的DNN模型结构,因此我们可以直接使用DNN的前向传播算法逻辑,即:
al=σ(zl)=σ(Wlal−1+bl)a^l = \sigma(z^l) = \sigma(W^la^{l-1} + b^l) al=σ(zl)=σ(Wlal−1+bl)

这里的激活函数一般是sigmoid或者tanh。

经过了若干全连接层之后,最后的一层为Softmax输出层。此时输出层和普通的全连接层唯一的区别是,激活函数是softmax函数。

这里需要需要我们定义的CNN模型参数是:

1)全连接层的激活函数

2)全连接层各层神经元的个数

2.6 CNN前向传播算法小结

有了上面的基础,我们现在总结下CNN的前向传播算法。

输入:1个图片样本,CNN模型的层数LLL和所有隐藏层的类型,对于卷积层,要定义卷积核的大小KKK,卷积核子矩阵的维度FFF,填充大小PPP,步幅SSS。对于池化层,要定义池化区域大小kkk和池化标准(MAX或Average),对于全连接层,要定义全连接层的激活函数(输出层除外)和各层的神经元个数。

输出:CNN模型的输出aLa^LaL

1)根据输入层的填充大小PPP,填充原始图片的边缘,得到输入张量a1a^1a1。

2)初始化所有隐藏层的参数W,bW,bW,b

3)for l=2l=2l=2 to L−1L−1L−1:

a) 如果第lll层是卷积层,则输出为

al=ReLU(zl)=ReLU(al−1∗Wl+bl)a^l= ReLU(z^l) = ReLU(a^{l-1}*W^l +b^l) al=ReLU(zl)=ReLU(al−1∗Wl+bl)

b) 如果第lll层是池化层,则输出为al=pool(al−1)a^l= pool(a^{l-1})al=pool(al−1), 这里的pool指按照池化区域大小kkk和池化标准将输入张量缩小的过程。

c) 如果第lll层是全连接层,则输出为 
      al=σ(zl)=σ(Wlal−1+bl)a^l= \sigma(z^l) = \sigma(W^la^{l-1} +b^l)             al=σ(zl)=σ(Wlal−1+bl)      

4)对于输出层第LLL层:
aL=softmax(zL)=softmax(WLaL−1+bL)a^L= softmax(z^L) = softmax(W^La^{L-1} +b^L) aL=softmax(zL)=softmax(WLaL−1+bL)

以上就是CNN前向传播算法的过程总结。有了CNN前向传播算法的基础,我们后面再来理解CNN的反向传播算法就简单多了。下一章我们来讨论CNN的反向传播算法。

三、卷积神经网络(CNN)反向传播算法

3.1 回顾DNN的反向传播算法

我们首先回顾DNN的反向传播算法。在DNN中,我们是首先计算出输出层的δL\delta^LδL:
δL=∂J(W,b)∂zL=∂J(W,b)∂aL⊙σ′(zL)(BP1)\delta^L = \frac{\partial J(W,b)}{\partial z^L} = \frac{\partial J(W,b)}{\partial a^L}\odot \sigma^{'}(z^L)\qquad \text{(BP1)} δL=∂zL∂J(W,b)​=∂aL∂J(W,b)​⊙σ′(zL)(BP1)

利用数学归纳法,用δl+1\delta^{l+1}δl+1的值一步步的向前求出第lll层的δl\delta^lδl,表达式为:
δl=δl+1∂zl+1∂zl=(Wl+1)Tδl+1⊙σ′(zl)(BP2)\delta^{l} = \delta^{l+1}\frac{\partial z^{l+1}}{\partial z^{l}} = (W^{l+1})^T\delta^{l+1}\odot \sigma^{'}(z^l)\qquad \text{(BP2)} δl=δl+1∂zl∂zl+1​=(Wl+1)Tδl+1⊙σ′(zl)(BP2)

有了δl\delta^lδl的表达式,从而求出W,bW,bW,b的梯度表达式:
∂J(W,b)∂Wl=∂J(W,b,x,y)∂zl∂zl∂Wl=δl(al−1)T(BP3)\frac{\partial J(W,b)}{\partial W^l} = \frac{\partial J(W,b,x,y)}{\partial z^l} \frac{\partial z^l}{\partial W^l} = \delta^{l}(a^{l-1})^T\qquad \text{(BP3)} ∂Wl∂J(W,b)​=∂zl∂J(W,b,x,y)​∂Wl∂zl​=δl(al−1)T(BP3)

∂J(W,b,x,y)∂bl=∂J(W,b)∂zl∂zl∂bl=δl(BP4)\frac{\partial J(W,b,x,y)}{\partial b^l} = \frac{\partial J(W,b)}{\partial z^l} \frac{\partial z^l}{\partial b^l} = \delta^{l}\qquad \text{(BP4)} ∂bl∂J(W,b,x,y)​=∂zl∂J(W,b)​∂bl∂zl​=δl(BP4)

有了W,bW,bW,b梯度表达式,就可以用梯度下降法来优化W,bW,bW,b,求出最终的所有W,bW,bW,b的值。

现在我们想把同样的思想用到CNN中,很明显,CNN有些不同的地方,不能直接去套用DNN的反向传播算法的公式。

3.2 CNN的反向传播算法思想

要套用DNN的反向传播算法到CNN,有几个问题需要解决:

1)池化层没有激活函数,这个问题倒比较好解决,我们可以令池化层的激活函数为σ(z)=z\sigma(z) = zσ(z)=z,即激活后就是自己本身。这样池化层激活函数的导数为1.

2)池化层在前向传播的时候,对输入进行了压缩,那么我们现在需要向前反向推导δl−1\delta^{l-1}δl−1,这个推导方法和DNN完全不同。

3) 卷积层是通过张量卷积,或者说若干个矩阵卷积求和而得的当前层的输出,这和DNN很不相同,DNN的全连接层是直接进行矩阵乘法得到当前层的输出。这样在卷积层反向传播的时候,上一层的δl−1\delta^{l-1}δl−1递推计算方法肯定有所不同。

4)对于卷积层,由于WWW使用的运算是卷积,那么从δl\delta^{l}δl推导出该层的所有卷积核的W,bW,bW,b的方式也不同。

从上面可以看出,问题1比较好解决,但是问题2,3,4就需要好好的动一番脑筋了,而问题2,3,4也是解决CNN反向传播算法的关键所在。另外大家要注意到的是,DNN中的al,zla^l,z^lal,zl都只是一个向量,而我们CNN中的al,zla^l,z^lal,zl都是一个张量,这个张量是三维的,即由若干个输入的子矩阵组成。

下面我们就针对问题2,3,4来一步步研究CNN的反向传播算法。

在研究过程中,需要注意的是,由于卷积层可以有多个卷积核,各个卷积核的处理方法是完全相同且独立的,为了简化算法公式的复杂度,我们下面提到卷积核都是卷积层中若干卷积核中的一个。

3.3 已知池化层的δl\delta^{l}δl,推导上一隐藏层的δl−1\delta^{l-1}δl−1

我们首先解决上面的问题2,如果已知池化层的δl\delta^{l}δl,推导出上一隐藏层的δl−1\delta^{l-1}δl−1。

在前向传播算法时,池化层一般我们会用MAX或者Average对输入进行池化,池化的区域大小已知。现在我们反过来,要从缩小后的误差或者梯度δl\delta^{l}δl,还原前一次较大区域对应的误差。

在反向传播时,我们首先会把δl\delta^{l}δl的所有子矩阵矩阵大小还原成池化之前的大小。有一个原则就是需要保证传递的loss(或者梯度)总和不变。根据这条原则,average pooling和max pooling的反向传播也是不同的。然后如果是MAX,则把δl\delta^{l}δl的所有子矩阵的各个池化局域的值放在之前做前向传播算法得到最大值的位置。如果是Average,则把δl\delta^{l}δl的所有子矩阵的各个池化局域的值取平均后放在还原后的子矩阵位置。这个过程一般叫做upsample

用一个例子可以很方便的表示:假设我们的池化区域大小是2x2。δl\delta^{l}δl的第kkk个子矩阵为:
δkl=(2846)\delta_k^l = \left( \begin{array}{ccc} 2& 8 \\ 4& 6 \end{array} \right) δkl​=(24​86​)

由于池化区域为2x2,我们先对δkl\delta_k^lδkl​做还原,即变成:
(0000028004600000)\left( \begin{array}{ccc} 0&0&0&0 \\ 0&2& 8&0 \\ 0&4&6&0 \\ 0&0&0&0 \end{array} \right) ⎝⎜⎜⎛​0000​0240​0860​0000​⎠⎟⎟⎞​

如果是MAX,假设我们之前在前向传播时记录的最大值位置分别是左上,右下,右上,左下,则转换后的矩阵为:
(2000000804000060)\left( \begin{array}{ccc} 2&0&0&0 \\ 0&0& 0&8 \\ 0&4&0&0 \\ 0&0&6&0 \end{array} \right) ⎝⎜⎜⎛​2000​0040​0006​0800​⎠⎟⎟⎞​

如果是Average,则进行平均:转换后的矩阵为:
(0.50.5220.50.522111.51.5111.51.5)\left( \begin{array}{ccc} 0.5&0.5&2&2 \\ 0.5&0.5&2&2 \\ 1&1&1.5&1.5 \\ 1&1&1.5&1.5 \end{array} \right) ⎝⎜⎜⎛​0.50.511​0.50.511​221.51.5​221.51.5​⎠⎟⎟⎞​

注:Average pooling比较容易让人理解错的地方就是会简单的认为直接把梯度复制NNN遍之后直接反向传播回去,但是这样会造成loss之和变为原来的NNN倍,网络是会产生梯度爆炸的

这样我们就得到了上一层∂J(W,b)∂akl−1\frac{\partial J(W,b)}{\partial a_k^{l-1}}∂akl−1​∂J(W,b)​的值(为什么上面的矩阵的值是∂J(W,b)∂akl−1\frac{\partial J(W,b)}{\partial a_k^{l-1}}∂akl−1​∂J(W,b)​?可以这么认为:传入到lll池化层的是akl−1a_k^{l-1}akl−1​,那么反向传播时 upsample(δl)upsample(\delta^l)upsample(δl)的误差自然是∂J(W,b)∂akl−1\frac{\partial J(W,b)}{\partial a_k^{l-1}}∂akl−1​∂J(W,b)​),要得到δkl−1\delta_k^{l-1}δkl−1​:
δkl−1=∂J(W,b)∂akl−1∂akl−1∂zkl−1=upsample(δkl)⊙σ′(zkl−1)\delta_k^{l-1} = \frac{\partial J(W,b)}{\partial a_k^{l-1}} \frac{\partial a_k^{l-1}}{\partial z_k^{l-1}} = upsample(\delta_k^l) \odot \sigma^{'}(z_k^{l-1}) δkl−1​=∂akl−1​∂J(W,b)​∂zkl−1​∂akl−1​​=upsample(δkl​)⊙σ′(zkl−1​)

其中,upsample函数完成了池化误差矩阵放大与误差重新分配的逻辑。

我们概括下,对于张量δl−1\delta^{l-1}δl−1,我们有:
δl−1=upsample(δl)⊙σ′(zl−1)(BP2*)\delta^{l-1} = upsample(\delta^l) \odot \sigma^{'}(z^{l-1})\qquad \text{(BP2*)} δl−1=upsample(δl)⊙σ′(zl−1)(BP2*)

上式和普通网络的反向推导误差很类似:
δl=(Wl+1)Tδl+1⊙σ′(zl)(BP2)\delta^{l} = (W^{l+1})^T\delta^{l+1}\odot \sigma^{'}(z^l)\qquad \text{(BP2)} δl=(Wl+1)Tδl+1⊙σ′(zl)(BP2)

3.4 已知卷积层的δl\delta^{l}δl,推导上一隐藏层的δl−1\delta^{l-1}δl−1

对于卷积层的反向传播,我们首先回忆下卷积层的前向传播公式:
al=σ(zl)=σ(al−1∗Wl+bl)a^l= \sigma(z^l) = \sigma(a^{l-1}*W^l +b^l) al=σ(zl)=σ(al−1∗Wl+bl)

在DNN中,我们知道δl−1\delta^{l-1}δl−1和δl\delta^{l}δl的递推关系为:
δl=∂J(W,b)∂zl=∂J(W,b)∂zl+1∂zl+1∂zl=δl+1∂zl+1∂zl\delta^{l} = \frac{\partial J(W,b)}{\partial z^l} = \frac{\partial J(W,b)}{\partial z^{l+1}}\frac{\partial z^{l+1}}{\partial z^{l}} = \delta^{l+1}\frac{\partial z^{l+1}}{\partial z^{l}} δl=∂zl∂J(W,b)​=∂zl+1∂J(W,b)​∂zl∂zl+1​=δl+1∂zl∂zl+1​

因此要推导出δl−1\delta^{l-1}δl−1和δl\delta^{l}δl的递推关系,必须计算∂zl∂zl−1\frac{\partial z^{l}}{\partial z^{l-1}}∂zl−1∂zl​的梯度表达式。

注意到zlz^lzl和zl−1z^{l−1}zl−1的关系为:
zl=al−1∗Wl+bl=σ(zl−1)∗Wl+blz^l = a^{l-1}*W^l +b^l =\sigma(z^{l-1})*W^l +b^l zl=al−1∗Wl+bl=σ(zl−1)∗Wl+bl

因此我们有:
δl−1=δl∂zl∂zl−1=δl∗rot180(Wl)⊙σ′(zl−1)(BP2*)\delta^{l-1} = \delta^{l}\frac{\partial z^{l}}{\partial z^{l-1}} = \delta^{l}*rot180(W^{l}) \odot \sigma^{'}(z^{l-1})\qquad \text{(BP2*)} δl−1=δl∂zl−1∂zl​=δl∗rot180(Wl)⊙σ′(zl−1)(BP2*)

这里的式子其实和DNN的类似,区别在于对于含有卷积的式子求导时,卷积核被旋转了180度。即式子中的rot180(),翻转180度的意思是上下翻转一次,接着左右翻转一次。在DNN中这里只是矩阵的转置。那么为什么呢?由于这里都是张量,直接推演参数太多了。我们以一个简单的例子说明为啥这里求导后卷积核要翻转。

假设我们l−1l−1l−1层的输出al−1a^{l−1}al−1是一个3x3矩阵,第lll层的卷积核WlW^lWl是一个2x2矩阵,采用1像素的步幅,则输出zlz^lzl是一个2x2的矩阵。我们简化blb^lbl都是0,则有
al−1∗Wl=zla^{l-1}*W^l = z^{l} al−1∗Wl=zl

我们列出a,W,za,W,za,W,z的矩阵表达式如下:
(a11a12a13a21a22a23a31a32a33)∗(w11w12w21w22)=(z11z12z21z22)\left( \begin{array}{ccc} a_{11}&a_{12}&a_{13} \\ a_{21}&a_{22}&a_{23}\\ a_{31}&a_{32}&a_{33} \end{array} \right) * \left( \begin{array}{ccc} w_{11}&w_{12}\\ w_{21}&w_{22} \end{array} \right) = \left( \begin{array}{ccc} z_{11}&z_{12}\\ z_{21}&z_{22} \end{array} \right) ⎝⎛​a11​a21​a31​​a12​a22​a32​​a13​a23​a33​​⎠⎞​∗(w11​w21​​w12​w22​​)=(z11​z21​​z12​z22​​)

利用卷积的定义,很容易得出:
z11=a11w11+a12w12+a21w21+a22w22z_{11} = a_{11}w_{11} + a_{12}w_{12} + a_{21}w_{21} + a_{22}w_{22} z11​=a11​w11​+a12​w12​+a21​w21​+a22​w22​

z12=a12w11+a13w12+a22w21+a23w22z_{12} = a_{12}w_{11} + a_{13}w_{12} + a_{22}w_{21} + a_{23}w_{22} z12​=a12​w11​+a13​w12​+a22​w21​+a23​w22​

z21=a21w11+a22w12+a31w21+a32w22z_{21} = a_{21}w_{11} + a_{22}w_{12} + a_{31}w_{21} + a_{32}w_{22}z21​=a21​w11​+a22​w12​+a31​w21​+a32​w22​

z22=a22w11+a23w12+a32w21+a33w22z_{22} = a_{22}w_{11} + a_{23}w_{12} + a_{32}w_{21} + a_{33}w_{22} z22​=a22​w11​+a23​w12​+a32​w21​+a33​w22​

接着我们模拟反向求导:
∇al−1=∂J(W,b)∂al−1=∂J(W,b)∂zl∂zl∂al−1=δl∂zl∂al−1\nabla a^{l-1} = \frac{\partial J(W,b)}{\partial a^{l-1}} = \frac{\partial J(W,b)}{\partial z^{l}} \frac{\partial z^{l}}{\partial a^{l-1}} = \delta^{l} \frac{\partial z^{l}}{\partial a^{l-1}} ∇al−1=∂al−1∂J(W,b)​=∂zl∂J(W,b)​∂al−1∂zl​=δl∂al−1∂zl​

从上式可以看出,对于al−1a^{l-1}al−1的梯度误差∇al−1\nabla a^{l-1}∇al−1,等于第lll层的梯度误差乘以∂zl∂al−1\frac{\partial z^{l}}{\partial a^{l-1}}∂al−1∂zl​,而∂zl∂al−1\frac{\partial z^{l}}{\partial a^{l-1}}∂al−1∂zl​对应上面的例子中相关联的www的值。假设我们的zzz矩阵对应的反向传播误差是δ11,δ12,δ21,δ22\delta_{11}, \delta_{12}, \delta_{21}, \delta_{22}δ11​,δ12​,δ21​,δ22​组成的2x2矩阵(即,δl=∂J(W,b)∂zl\delta^{l} = \frac{\partial J(W,b)}{\partial z^l}δl=∂zl∂J(W,b)​),则利用上面梯度的式子和4个等式,我们可以分别写出∇al−1\nabla a^{l-1}∇al−1的9个标量的梯度。

比如对于a11a_{11}a11​的梯度,由于在4个等式中a11a_{11}a11​只和z11z_{11}z11​有乘积关系,从而我们有:
∇a11=δ11w11\nabla a_{11} = \delta_{11}w_{11} ∇a11​=δ11​w11​

对于a12a_{12}a12​的梯度,由于在4个等式中a12a_{12}a12​和z12z_{12}z12​,z11z_{11}z11​有乘积关系,从而我们有:
∇a12=δ11w12+δ12w11\nabla a_{12} = \delta_{11}w_{12} + \delta_{12}w_{11} ∇a12​=δ11​w12​+δ12​w11​

同样的道理我们得到:
∇a13=δ12w12\nabla a_{13} = \delta_{12}w_{12} ∇a13​=δ12​w12​

∇a21=δ11w21+δ21w11\nabla a_{21} = \delta_{11}w_{21} + \delta_{21}w_{11} ∇a21​=δ11​w21​+δ21​w11​

∇a22=δ11w22+δ12w21+δ21w12+δ22w11\nabla a_{22} = \delta_{11}w_{22} + \delta_{12}w_{21} + \delta_{21}w_{12} + \delta_{22}w_{11} ∇a22​=δ11​w22​+δ12​w21​+δ21​w12​+δ22​w11​

∇a23=δ12w22+δ22w12\nabla a_{23} = \delta_{12}w_{22} + \delta_{22}w_{12} ∇a23​=δ12​w22​+δ22​w12​

∇a31=δ21w21\nabla a_{31} = \delta_{21}w_{21} ∇a31​=δ21​w21​

∇a32=δ21w22+δ22w21\nabla a_{32} = \delta_{21}w_{22} + \delta_{22}w_{21} ∇a32​=δ21​w22​+δ22​w21​

∇a33=δ22w22\nabla a_{33} = \delta_{22}w_{22} ∇a33​=δ22​w22​

这上面9个式子其实可以用一个矩阵卷积的形式表示,即:
(00000δ11δ1200δ21δ2200000)∗(w22w21w12w11)=(∇a11∇a12∇a13∇a21∇a22∇a23∇a31∇a32∇a33)\left( \begin{array}{ccc} 0&0&0&0 \\ 0&\delta_{11}& \delta_{12}&0 \\ 0&\delta_{21}&\delta_{22}&0 \\ 0&0&0&0 \end{array} \right) * \left( \begin{array}{ccc} w_{22}&w_{21}\\ w_{12}&w_{11} \end{array} \right) = \left( \begin{array}{ccc} \nabla a_{11}&\nabla a_{12}&\nabla a_{13} \\ \nabla a_{21}&\nabla a_{22}&\nabla a_{23}\\ \nabla a_{31}&\nabla a_{32}&\nabla a_{33} \end{array} \right) ⎝⎜⎜⎛​0000​0δ11​δ21​0​0δ12​δ22​0​0000​⎠⎟⎟⎞​∗(w22​w12​​w21​w11​​)=⎝⎛​∇a11​∇a21​∇a31​​∇a12​∇a22​∇a32​​∇a13​∇a23​∇a33​​⎠⎞​

为了符合梯度计算,我们在误差矩阵周围填充了一圈0,此时我们将卷积核翻转后和反向传播的梯度误差进行卷积,就得到了前一次的梯度误差。这个例子直观的介绍了为什么对含有卷积的式子反向传播时,卷积核要翻转180度的原因。

以上就是卷积层的误差反向传播过程。

3.5 已知卷积层的δl\delta^lδl,推导该层的W,bW,bW,b的梯度

好了,我们现在已经可以递推出每一层的梯度误差δl\delta^lδl了,对于全连接层,可以按DNN的反向传播算法求该层W,bW,bW,b的梯度,而池化层并没有W,bW,bW,b,也不用求W,bW,bW,b的梯度。只有卷积层的W,bW,bW,b需要求出。

注意到卷积层zzz和W,bW,bW,b的关系为:
zl=al−1∗Wl+bz^l = a^{l-1}*W^l +b zl=al−1∗Wl+b

因此我们有:
∂J(W,b)∂Wl=∂J(W,b)∂zl∂zl∂Wl=al−1∗δl(BP3*)\frac{\partial J(W,b)}{\partial W^{l}} = \frac{\partial J(W,b)}{\partial z^{l}}\frac{\partial z^{l}}{\partial W^{l}} =a^{l-1} *\delta^l \qquad \text{(BP3*)} ∂Wl∂J(W,b)​=∂zl∂J(W,b)​∂Wl∂zl​=al−1∗δl(BP3*)

注意到此时卷积核并没有反转,主要是此时是层内的求导,而不是反向传播到上一层的求导。具体过程我们可以分析一下。

和第3.4节一样的一个简化的例子,这里输入是矩阵,不是张量,那么对于第lll层,某个个卷积核矩阵W的导数可以表示如下:
∂J(W,b)∂Wpql=∑i∑j(δijlai+p−1,j+q−1l−1)\frac{\partial J(W,b)}{\partial W_{pq}^{l}} = \sum\limits_i\sum\limits_j(\delta_{ij}^la_{i+p-1,j+q-1}^{l-1}) ∂Wpql​∂J(W,b)​=i∑​j∑​(δijl​ai+p−1,j+q−1l−1​)

假设我们输入al−1a^{l-1}al−1是4x4的矩阵,卷积核WlW^lWl是3x3的矩阵,输出zlz^lzl是2x2的矩阵,那么反向传播的zlz^lzl的梯度误差δl\delta^lδl也是2x2的矩阵。

即:
(a11a12a13a14a21a22a23a24a31a32a33a34a41a42a43a44)∗(w11w12w13w21w22w23w31w32w33)=(z11z12z21z22)\left( \begin{array}{ccc} a_{11}& a_{12}& a_{13}& a_{14} \\ a_{21}& a_{22}& a_{23}& a_{24} \\ a_{31}& a_{32}&a_{33}& a_{34} \\ a_{41}& a_{42}& a_{43}& a_{44} \end{array} \right) * \left( \begin{array}{ccc} w_{11}&w_{12}&w_{13} \\w_{21}&w_{22}&w_{23}\\ w_{31}&w_{32}&w_{33} \end{array} \right)=\left( \begin{array}{ccc} z_{11}&z_{12}\\ z_{21}&z_{22} \end{array} \right) ⎝⎜⎜⎛​a11​a21​a31​a41​​a12​a22​a32​a42​​a13​a23​a33​a43​​a14​a24​a34​a44​​⎠⎟⎟⎞​∗⎝⎛​w11​w21​w31​​w12​w22​w32​​w13​w23​w33​​⎠⎞​=(z11​z21​​z12​z22​​)

那么根据上面的式子,我们有:
∂J(W,b)∂W11l=a11δ11+a12δ12+a21δ21+a22δ22\frac{\partial J(W,b)}{\partial W_{11}^{l}} = a_{11}\delta_{11} + a_{12}\delta_{12} + a_{21}\delta_{21} + a_{22}\delta_{22} ∂W11l​∂J(W,b)​=a11​δ11​+a12​δ12​+a21​δ21​+a22​δ22​

∂J(W,b)∂W12l=a12δ11+a13δ12+a22δ21+a23δ22\frac{\partial J(W,b)}{\partial W_{12}^{l}} = a_{12}\delta_{11} + a_{13}\delta_{12} + a_{22}\delta_{21} + a_{23}\delta_{22} ∂W12l​∂J(W,b)​=a12​δ11​+a13​δ12​+a22​δ21​+a23​δ22​

∂J(W,b)∂W13l=a13δ11+a14δ12+a23δ21+a24δ22\frac{\partial J(W,b)}{\partial W_{13}^{l}} = a_{13}\delta_{11} + a_{14}\delta_{12} + a_{23}\delta_{21} + a_{24}\delta_{22} ∂W13l​∂J(W,b)​=a13​δ11​+a14​δ12​+a23​δ21​+a24​δ22​

∂J(W,b)∂W21l=a21δ11+a22δ12+a31δ21+a32δ22\frac{\partial J(W,b)}{\partial W_{21}^{l}} = a_{21}\delta_{11} + a_{22}\delta_{12} + a_{31}\delta_{21} + a_{32}\delta_{22} ∂W21l​∂J(W,b)​=a21​δ11​+a22​δ12​+a31​δ21​+a32​δ22​

................ ........

最终我们可以一共得到9个式子。整理成矩阵形式后可得:
∂J(W,b)∂Wl=(a11a12a13a14a21a22a23a24a31a32a33a34a41a42a43a44)∗(δ11δ12δ21δ22)\frac{\partial J(W,b)}{\partial W^{l}} =\left( \begin{array}{ccc} a_{11}&a_{12}&a_{13}&a_{14} \\ a_{21}&a_{22}&a_{23}&a_{24} \\ a_{31}&a_{32}&a_{33}&a_{34} \\ a_{41}&a_{42}&a_{43}&a_{44} \end{array} \right) * \left( \begin{array}{ccc} \delta_{11}& \delta_{12} \\ \delta_{21}&\delta_{22} \end{array} \right) ∂Wl∂J(W,b)​=⎝⎜⎜⎛​a11​a21​a31​a41​​a12​a22​a32​a42​​a13​a23​a33​a43​​a14​a24​a34​a44​​⎠⎟⎟⎞​∗(δ11​δ21​​δ12​δ22​​)

从而可以清楚的看到这次我们为什么没有反转的原因。

而对于bbb,则稍微有些特殊,因为δl\delta^lδl是三维张量,而bbb只是一个向量,不能像DNN那样直接和δl\delta^lδl相等。通常的做法是将δl\delta^lδl的各个子矩阵的项分别求和,得到一个误差向量,即为bbb的梯度:
∂J(W,b)∂bl=∑u,v(δl)u,v(BP4*)\frac{\partial J(W,b)}{\partial b^{l}} = \sum\limits_{u,v}(\delta^l)_{u,v} \qquad \text{(BP4*)} ∂bl∂J(W,b)​=u,v∑​(δl)u,v​(BP4*)

3.6 CNN反向传播算法总结

现在我们总结下CNN的反向传播算法,以最基本的批量梯度下降法为例来描述反向传播算法。

输入:mmm个图片样本,CNN模型的层数LLL和所有隐藏层的类型,对于卷积层,要定义卷积核的大小KKK,卷积核子矩阵的维度FFF,填充大小PPP,步幅SSS。对于池化层,要定义池化区域大小kkk和池化标准(MAX或Average),对于全连接层,要定义全连接层的激活函数(输出层除外)和各层的神经元个数。梯度迭代参数迭代步长α\alphaα,最大迭代次数MAX与停止迭代阈值ϵ\epsilonϵ

输出:CNN模型各隐藏层与输出层的W,bW,bW,b

1)初始化各隐藏层与输出层的各W,bW,bW,b的值为一个随机值。

2)for iter to 1 to MAX:

2-1) for i=1i =1i=1 to mmm:

a) 将CNN输入a1a^1a1设置为xix_ixi​对应的张量

b) for l=2l=2l=2 to L−1L-1L−1,根据下面3种情况进行前向传播算法计算:

b-1) 如果第lll层是卷积层,则输出为ai,l=σ(zi,l)=σ(Wl∗ai,l−1+bl)a^{i,l} = \sigma(z^{i,l}) = \sigma(W^l*a^{i,l-1} + b^{l})ai,l=σ(zi,l)=σ(Wl∗ai,l−1+bl)

b-2) 如果第lll层是池化层,则输出为ai,l=pool(ai,l−1)a^{i,l}= pool(a^{i,l-1})ai,l=pool(ai,l−1), 这里的pool指按照池化区域大小kkk和池化标准将输入张量缩小的过程。

b-3) 如果第lll层是全连接层,则输出为 ai,l=σ(zi,l)=σ(Wlai,l−1+bl)a^{i,l} = \sigma(z^{i,l}) = \sigma(W^la^{i,l-1} + b^{l})ai,l=σ(zi,l)=σ(Wlai,l−1+bl)

c) 对于输出层第LLL层:ai,L=softmax(zi,L)=softmax(WLai,L−1+bL)a^{i,L}= softmax(z^{i,L}) = softmax(W^{L}a^{i,L-1} +b^{L})ai,L=softmax(zi,L)=softmax(WLai,L−1+bL)

d) 通过损失函数计算输出层的δi,L\delta^{i,L}δi,L

e) for l=L−1l= L-1l=L−1 to 2, 根据下面3种情况进行进行反向传播算法计算:

e-1) 如果当前是全连接层:δi,l=(Wl+1)Tδi,l+1⊙σ′(zi,l)\delta^{i,l} = (W^{l+1})^T\delta^{i,l+1}\odot \sigma^{'}(z^{i,l})δi,l=(Wl+1)Tδi,l+1⊙σ′(zi,l)

e-2) 如果当前是卷积层:δi,l=δi,l+1∗rot180(Wl+1)⊙σ′(zi,l)\delta^{i,l} = \delta^{i,l+1}*rot180(W^{l+1}) \odot \sigma^{'}(z^{i,l})δi,l=δi,l+1∗rot180(Wl+1)⊙σ′(zi,l)

e-3) 如果当前是池化层:δi,l=upsample(δi,l+1)⊙σ′(zi,l)\delta^{i,l} = upsample(\delta^{i,l+1}) \odot \sigma^{'}(z^{i,l})δi,l=upsample(δi,l+1)⊙σ′(zi,l)

2-2) for l=2l = 2l=2 to LLL,根据下面2种情况更新第lll层的Wl,blW^l,b^lWl,bl:

2-2-1) 如果当前是全连接层:Wl=Wl−α∑i=1mδi,l(ai,l−1)TW^l = W^l -\alpha \sum\limits_{i=1}^m \delta^{i,l}(a^{i, l-1})^TWl=Wl−αi=1∑m​δi,l(ai,l−1)T, bl=bl−α∑i=1mδi,lb^l = b^l -\alpha \sum\limits_{i=1}^m \delta^{i,l}bl=bl−αi=1∑m​δi,l

2-2-2) 如果当前是卷积层,对于每一个卷积核有:Wl=Wl−α∑i=1mδi,l∗rot180(ai,l−1)W^l = W^l -\alpha \sum\limits_{i=1}^m \delta^{i,l}*rot180(a^{i, l-1})Wl=Wl−αi=1∑m​δi,l∗rot180(ai,l−1), bl=bl−α∑i=1m∑u,v(δi,l)u,vb^l = b^l -\alpha \sum\limits_{i=1}^m \sum\limits_{u,v}(\delta^{i,l})_{u,v}bl=bl−αi=1∑m​u,v∑​(δi,l)u,v​

2-3) 如果所有W,bW,bW,b的变化值都小于停止迭代阈值ϵ\epsilonϵ,则跳出迭代循环到步骤3。

3) 输出各隐藏层与输出层的线性关系系数矩阵WWW和偏倚向量bbb。

参考文献

【1】卷积神经网络(CNN)模型结构

【2】卷积神经网络(CNN)前向传播算法

【3】卷积神经网络(CNN)反向传播算法

本文转自参考文献1-3

深度学习(四):卷积神经网络(CNN)模型结构,前向传播算法和反向传播算法介绍。相关推荐

  1. 深度学习~卷积神经网络(CNN)概述

    目录​​​​​​​ 1. 卷积神经网络的形成和演变 1.1 卷积神经网络结构 1.2 卷积神经网络的应用和影响 1.3 卷积神经网络的缺陷和视图 1.3.1 缺陷:可能错分 1.3.2 解决方法:视图 ...

  2. 深度学习之卷积神经网络CNN

    转自:https://blog.csdn.net/cxmscb/article/details/71023576 一.CNN的引入 在人工的全连接神经网络中,每相邻两层之间的每个神经元之间都是有边相连 ...

  3. 干货 | 深度学习之卷积神经网络(CNN)的模型结构

    微信公众号 关键字全网搜索最新排名 [机器学习算法]:排名第一 [机器学习]:排名第一 [Python]:排名第三 [算法]:排名第四 前言 在前面我们讲述了DNN的模型与前向反向传播算法.而在DNN ...

  4. 深度学习之卷积神经网络CNN 常用的几个模型

    LeNet5 论文:http://yann.lecun.com/exdb/publis/pdf/lecun-01a.pdf LeNet-5:是Yann LeCun在1998年设计的用于手写数字识别的卷 ...

  5. 深度学习:卷积神经网络CNN入门

    作者:机器之心 链接:https://www.zhihu.com/question/52668301/answer/131573702 来源:知乎 著作权归作者所有.商业转载请联系作者获得授权,非商业 ...

  6. 卷积神经网络(CNN)模型结构

    在前面我们讲述了DNN的模型与前向反向传播算法.而在DNN大类中,卷积神经网络(Convolutional Neural Networks,以下简称CNN)是最为成功的DNN特例之一.CNN广泛的应用 ...

  7. 深度学习之卷积神经网络CNN理论与实践详解

    六月 北京 | 高性能计算之GPU CUDA培训 6月22-24日三天密集式学习  快速带你入门阅读全文> 正文共1416个字,6张图,预计阅读时间6分钟. 概括 大体上简单的卷积神经网络是下面 ...

  8. 深度学习笔记-卷积神经网络CNN与循环神经网络RNN有什么区别?

    转载 https://blog.csdn.net/weixin_35227692/article/details/79223536 转载于:https://www.cnblogs.com/USTBlx ...

  9. 【深度学习】卷积神经网络-图片分类案例(pytorch实现)

    前言 前文已经介绍过卷积神经网络的基本概念[深度学习]卷积神经网络-CNN简单理论介绍.下面开始动手实践吧.本文任务描述如下: 从公开数据集CIFAR10中创建训练集.测试集数据,使用Pytorch构 ...

最新文章

  1. theano 安装杂记
  2. 一文看懂软件测试方法和规范
  3. 模拟浏览器自动化测试工具Selenium之五Centos系统命令行下部署selenium环境试验
  4. python 实现简单查询页面_python web 实现简易天气查询
  5. UNITY UI字体模糊的原因
  6. Wannafly挑战赛22 B 字符路径 ( 拓扑排序+dp )
  7. 【agc004d】Teleporter
  8. NPM私有服务器搭建方法——sinopia
  9. Tech.Ed 2011微软技术大会(二)之专题讲座
  10. java ocx调用_Javascript调用OCX控件
  11. Seasonal-ARIMA模型
  12. Arcmap坐标系转换通用教程【简单明了】
  13. Java将byte流转换成zip文件_java zip文件的压缩与解压
  14. 手机端html只允许竖屏,关于移动端页面强制竖屏的方法
  15. 微信小程序全栈开发实践 第一章 重新认识微信小程序、及新功能介绍
  16. 一个简单的音乐播放器(实现上一曲下一曲,和自动播放)
  17. JAVA广度优先搜索---寻找从A点到B点最短路径
  18. Beta冲刺总结随笔
  19. php 调用 C++
  20. 推荐12个国外免费自助建站网站

热门文章

  1. 传说中的移动员工工资表,也不知是真是假
  2. html自动请求favicon,浏览器默认请求的favicon.ico文件,可能带来的问题
  3. 关于前端在vue中实现‘距离某个时间点的倒计时’问题:设置了间隔计算时间,刚开始有停顿。
  4. VMware认证专家(VCP)详细介绍
  5. gb2312的字符串(包括中午)转16进制字符串以及反转义原始字符
  6. [笑语天下]风景、照片与评论古今
  7. 手机cpu性能天梯图2022
  8. 计算机EV录屏培训体会,ev录屏不能录制声音怎么办?对照原因进行解决
  9. cocos creator动态加载DragonBones
  10. JVM参数-XXMatespaceSize的含义