Convolutional Neural Network(CNN) TUTORIAL - 学习笔记

catalogue

0. 引言
1. CNN简介
2. CNN的训练
3. CNN隐藏层训练可视化
4. CNN神经网络结构最佳实践
5. CNN概念的延伸扩展

 

0. 引言

0x1: 怎样理解卷积

卷积是通信领域的一个很常用的概念和计算方法,引用知乎上一个简明易懂的解释:已知x[0] = a, x[1] = b, x[2]=c,如图

已知y[0] = i, y[1] = j, y[2]=k,如图

下面通过演示求x[n] * y[n]的过程,揭示卷积的物理意义

1. 第一步:x[n]乘以y[0]并平移到位置0(注意这里随时间平移很重要)

这一步可以简单理解为x的当前状态值乘上了一个加权值y[0],接着移动到下一个时间响应点

2. 第二步:x[n]乘以y[1]并平移到位置1

3. 第三步:x[n]乘以y[2]并平移到位置2

4. 最后:把上面三个图叠加,就得到了x[n] * y[n]

直观上理解,一定是中间的时域的权重累乘和最多,而开始和结束阶段的累积和较小。卷积的重要的物理意义是:

一个函数(如:单位响应)在另一个函数(如:输入信号)上的加权叠加

这就是卷积的意义:加权叠加

1. 对于线性时不变系统,如果知道该系统的单位响应,那么将单位响应和输入信号求卷积,就相当于把输入信号的各个时间点的单位响应加权叠加,就直接得到了输出信号。
2. 注意!这里特别注意的一个概念是每个时间点输入信号只能输入一次,相当于给当前系统一次激励(加权乘),而这次激励的响应在之后的整个时间周期中逐渐消减
3. 通俗的说:在输入信号的每个位置,叠加一个单位响应,就得到了输出信号

Relevant Link:

https://www.zhihu.com/question/22298352

 

1. CNN简介

CNN最擅长的领域是计算机图像视觉处理,事实上,CNN就是在imageNet这个图像分类挑战赛的背景下"一战成名"的,当然,本质上CNN也能被用于其他领域

0x1: CNN的特性

卷积神经网络是一种特殊的深层的神经网络模型,它的特殊性体现在以下几个方面

1. 局部感受野(local receptive fields)

回想一下传统的全连接(full-connected) DNN,每一层的神经元都和上一层以及下一层的所有神经元建立了连接。但是CNN不这样做,我们以图像为例,CNN只会将隐藏层(CNN的hidden layer就是一层滤波器层/卷积层: N*N)和图像上相同大小的一个区域进行连接。这个输入图像的区域被称为隐藏神经元的局部感受野,它是输入像素上的一个小窗口。局部感受野中的单独每个连接学习一个权重w,同时隐藏神经元也学习一个总的偏置b。我们可以把这个特定的隐藏神经元看作是在学习分析它的局部感受野

局部感受野像一台扫描仪一样,每次按照一定的跨距(可以是1、2..)从左到右,从上到下移动

上图左:全连接网络。如果我们有1000x1000像素的图像,有1百万个隐层神经元,每个隐层神经元都连接图像的每一个像素点,就有1000x1000x1000000=10^12个连接,也就是10^12个权值参数
上图右:局部连接网络,每一个节点与上层节点同位置附件10x10的窗口相连接,则1百万个隐层神经元就只有((100w - 10) / 1 + 1) * (10 * 10),即10^8个参数。其权值连接个数比原来减少了四个数量级

2. 共享权重和偏置(shared weight)

共享权重和偏置指的是图像的所有局部感受野都使用同一份权重和偏置组成的卷积核窗口进行连接,例如对一张28*28的图像,使用5*5的局部感受野,步距1,则该CNN隐藏层总共由24*24个神经元组合,每个神经元都共享一份w向量(5*5个像素点,每个像素点和神经元建立的一个连接对应一个w)和偏置b

以5*5感受野为例,对第k,j个隐藏神经元,输出为

σ是神经元的激活函数(可以是sigmoid或者ReLU),在实践中ReLu被认为效果是最好的,我们本文接下来的讨论都会使用ReLu作为激活函数,b是偏置的共享值

是一个共享权重的5*5数组,表示位置(x,y)的输入激活值(对于第一层来说,就是像素强度值本身)

这意味着第一个隐藏层的所有神经元检测完全相同的特征(实际上后面每一层神经元都是共享权重和偏置的),只是在输入图像的不同位置。为了更好地理解这个概念,

我们把权重和偏置设想成隐藏神经元可以"挑选"的东西,例如一个局部感受野对90度角这个形状特别感兴趣,这种能力在图像的其他位置也可能是有用的,因此,在图像

中应用相同的特征检测器是非常有用的,用稍微更抽象一些的术语

卷积网络能很好地适应图像的平移不变性:例如稍微移动一幅猫的图像,CNN仍然能感知到这是一只猫

我们有时候把从输入层到隐藏层的映射称为一个特征映射。我们把定义特征映射的权重称为共享权重。我们把以这种方式定义特征映射的偏置称为共享偏置。共享权重和偏置合称为卷积核滤波器

值得注意的是,一个特征映射代表了一种"视角",为了完成图像细节识别和提取需要超过一个的特征映射

在上图中,有3个特征映射,每个特征映射定义为一个5*5共享权重和单个共享偏置的集合,其结果是网络能够检测3种不同的特征,每个特征都在整个图像中可检测

共享权重和偏置的一个很大的优点是,它大大减少了参与的卷积网络的参数,总共需要需要((输入图像size - (卷积核size - 1) / 跨距 + 1) * (卷积核size * 卷积核size),以本例为例就是(5*5) * 24 = 600

3. 混合层(池化层pooling)

卷积神经网络通常也包含混合层(polling layer),混合层通常紧跟在卷积层之后使用,它要做的是简化从卷积层输出的信息。一个混合层取得了从卷积层输出的每一个特征映射并且产生一个"凝缩映射",例如,混合层的每个单元可能概括了前一层的一个(2*2)的区域,凝缩的方法有很多种,例如

1. maxPool(最大值混合): 我们可以把最大值混合看作一种网络询问是否有一个给定的特征在一个图像区域中的哪个地方被发现的方式,然后它丢弃了确切的位置信息。直观上,一旦一个特征被发现,它的确切位置并不如它相对于其他特征的大概位置捉弄更要
2. averagePool(均值混合)
3. L2混合: 取区域中激活值的平方和的平方根,而不是最大激活值

同样可以看到,Pool池化技术减少了网络中超参数的个数

0x2: CNN的结构

卷积网络是为识别二维形状而特殊设计的一个多层感知器,这种网络结构对平移、比例缩放、倾斜或者共他形式的变形具有高度不变性。 这些良好的性能是网络在有监督方式下学会的,网络的结构主要有稀疏连接和权值共享两个特点,包括如下形式的约束

1. 特征提取: 每一个神经元从上一层的局部接受域得到突触输人,因而迫使它提取局部特征。一旦一个特征被提取出来, 只要它相对于其他特征的位置被近似地保留下来,它的精确位置就变得没有那么重要了 
2. 特征映射: 网络的每一个计算层都是由多个特征映射组成的,每个特征映射都是平面形式的。平面中单独的神经元在约束下共享 相同的突触权值集,这种结构形式具有如下的有益效果
    1) 平移不变性
    2) 自由参数数量的缩减(通过权值共享实现) 
3. 子抽样: 每个卷积层后面跟着一个实现局部平均和子抽样的计算层,由此特征映射的分辨率降低。这种操作具有使特征映射的输出对平移和其他形式的变形的敏感度下降的作用 

Relevant Link:

http://neuralnetworksanddeeplearning.com/chap6.html

 

2. CNN的训练

对于CNN的训练迭代过程(有监督学习),由于任一样本的类别是已知的,样本在空间的分布不再是依据其自然分布倾向来划分,而是要根据同类样本在空间的分布及不同类样本之间的分离程度找一种适当的空间划分方法,或者找到一个分类边界,使得不同类样本分别位于不同的区域内。这就需要一个长时间且复杂的学习过程,不断调整用以划分样本空间的分类边界的位置(滤波器本质上是一种像素模版),使尽可能少的样本被划分到非同类区域中
卷积网络在本质上是一种输入到输出的映射,它能够学习大量的输入与输出之间的映射关系,而不需要任何输入和输出之间的精确的数学表达式,只要用已知的模式对卷积网络加以训练,网络就具有输入输出对之间的映射能力。卷积网络执行的是有导师训练,所以其样本集是由形如: (输入向量,理想输出向量)的向量对构成的。所有这些向量对,都应该是来源于网络即将模拟的系统的实际"运行"结果。它们可以是从实际运行系统中采集来的
在开始训练前,所有的权都应该用一些不同的小随机数进行初始化

1. "小随机数"用来保证网络不会因权值过大而进入饱和状态,从而导致训练失败
2. "不同"用来保证网络可以正常地学习。实际上,如果用相同的数去初始化权矩阵,则网络无能力学习 

CNN训练算法与传统的DNN算法差不多,但还是存在一些差别。在一个卷积层,上一层的特征maps被一个可学习的卷积核进行卷积,然后通过一个激活函数,就可以得到输出特征map。每一个输出map可能是组合卷积多个输入maps的值

这里Wm|a表示输入maps的集合,在CNN自动训练过程中会自动选择需要组合的特征maps,但是对于一个特定的输出map,卷积每个输入maps的卷积核是不一样的(对应不同的w)。也就是说,如果输出特征map j和输出特征map k都是从输入map i中卷积求和得到,那么对应的卷积核是不一样的(因为同一份输入通过不同卷积核映射为了不同的输出特征)

0x1: 定义带正则化因子的损失函数

设有样本(xi, yi)共m个,CNN网络共有L层,中间包含若干个卷积层和pooling层,最后一层的输出为f(xi),则系统的loss表达式为(对权值进行了惩罚,一般分类都采用交叉熵形式)

0x2: CNN的反向传播算法

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

1. 池化层没有激活函数,我们可以令池化层的激活函数为σ(z)=zσ(z)=z,即激活后就是自己本身。这样池化层激活函数的导数为1 
2. 池化层在前向传播的时候,对输入进行了压缩,那么我们现在需要向前反向推导δl−1,这个推导方法和DNN完全不同 
3. 卷积层是通过张量卷积,或者说若干个矩阵(每个特征映射对应一个卷积矩阵)卷积求和而得的当前层的输出,这和DNN很不相同,DNN的全连接层是直接进行矩阵乘法得到当前层的输出。这样在卷积层反向传播的时候,上一层的δl−1递推计算方法肯定有所不同
4. 对于卷积层,由于W使用的运算是卷积,那么从δl推导出该层的所有卷积核的W,b的方式也不同

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

1. 知池化层的δl,推导上一隐藏层的δl−1

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

在反向传播时,我们首先会把δl的所有子矩阵矩阵大小还原成池化之前的大小

1. 如果是MAX,则把δl的所有子矩阵的各个池化局域的值放在之前做前向传播算法得到最大值的位置
2. 如果是Average,则把δl的所有子矩阵的各

这个过程一般叫做upsample(上采样)

假设我们的池化区域大小是2x2。δl的第k个子矩阵为

由于池化区域为2x2,我们先讲δlk做还原,即变成:

如果是MAX,假设我们之前在前向传播时记录的最大值位置分别是左上,右下,右上,左下,则转换后的矩阵为

如果是Average,则进行平均:反推转换后的矩阵为

这样我们就得到了上一层的值(针对当前层的误差上采(误差反向传递)样到上一层的W和b对激活值a的影响)

其中,upsample函数完成了池化误差矩阵放大与误差重新分配的逻辑(注意这里和DNN不同,DNN是直接全连接了,不存在误差重新分配的问题,DNN只有误差传递的问题)。我们概括下,对于张量δl1,我们有:

可以看到在池化层的误差反向传播上,CNN的误差传递还是和导数有关

2. 已知卷积层的δl,推导上一隐藏层的δl1

卷积层的前向传播公式 

我们知道δl1δl的递推关系为:

因此要推导出δl1δl的递推关系,必须计算的梯度表达式。注意到zlzl1的关系为:

因此我们有:

这里的式子其实和DNN的类似(大体上都和激活函数的导数有关),区别在于对于含有卷积的式子求导时,卷积核被旋转了180度。即式子中的rot180(),翻转180度的意思是上下翻转一次,接着左右翻转一次。我们从卷积矩阵的视角来解释一下这个公式的由来

假设我们l1层的输出al1是一个3x3矩阵,第l层的卷积核Wl是一个2x2矩阵,采用1像素的步幅,则输出zl是一个3x3的矩阵。我们简化bl0,则有

我们列出a,W,za,W,z的矩阵表达式如下:

利用卷积的定义,很容易得出:

接着我们模拟反向求导:

从上式可以看出,对于al1的梯度误差al1,等于第l层的梯度误差乘以zla,zla对应上面的例子中相关联的w的值。假设我们的z矩阵对应的反向传播误差

δ11,δ12,δ21,δ22组成的2x2矩阵,则利用上面梯度的式子和4个等式,我们可以分别写出al1的9个标量的梯度。比如对于a11的梯度,由于在4个等式中a11

只和z11有乘积关系,从而我们有:

对于a12的梯度,由于在4个等式中a12z12z11有乘积关系,从而我们有:

同样的道理我们得到:

这上面9个式子其实可以用一个矩阵卷积的形式表示,即:

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

3. 已知卷积层的δl,推导该层的W,b的梯度

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

只有卷积层的W,b需要求出。 注意到卷积层zW,b的关系为:

因此我们有:

而对于b,则稍微有些特殊,因为δl是三维张量,而b只是一个向量,不能像DNN那样直接和δl相等。通常的做法是将δl的各个子矩阵的项分别求和,得到一个误差向量,

即为b的梯度

0x3: CNN反向传播算法总结

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

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

Relevant Link:

http://cogprints.org/5869/1/cnn_tutorial.pdf
http://www.cnblogs.com/nsnow/p/4562363.html
http://www.cnblogs.com/tornadomeet/p/3468450.html
http://www.cnblogs.com/pinard/p/6494810.html
http://www.cnblogs.com/pinard/p/6494810.html

 

3. CNN隐藏层训练可视化

我们知道,不论是使用线性回归还是神经网络的方式对图像进行卷积操作,本质上,CNN是在训练一些"像素模版",这个像素模版很像一个像素过滤器,即只放过神经网络感兴趣的像素形状,而阻断不感兴趣的像素,这样说可能有些抽象,本小节通过可视化的方式来深入看看在训练过程中CNN的每一层究竟长什么样

0x1: 使用Keras探索卷积网络的滤波器

我们将利用Keras观察CNN到底在学些什么,它是如何理解我们送入的训练图片的。我们将使用Keras来对滤波器的激活值进行可视化。本文使用的神经网络是VGG-16,数据集为ImageNet

1. 在Keras中定义VGG网络的结构

from keras.models import Sequential
from keras.layers import Convolution2D, ZeroPadding2D, MaxPooling2D

img_width, img_height = 128, 128

# build the VGG16 network
model = Sequential()
model.add(ZeroPadding2D((1, 1), batch_input_shape=(1, 3, img_width, img_height)))
first_layer = model.layers[-1]
# this is a placeholder tensor that will contain our generated images
input_img = first_layer.input

# build the rest of the network
model.add(Convolution2D(64, 3, 3, activation='relu', name='conv1_1'))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(64, 3, 3, activation='relu', name='conv1_2'))
model.add(MaxPooling2D((2, 2), strides=(2, 2)))

model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(128, 3, 3, activation='relu', name='conv2_1'))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(128, 3, 3, activation='relu', name='conv2_2'))
model.add(MaxPooling2D((2, 2), strides=(2, 2)))

model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(256, 3, 3, activation='relu', name='conv3_1'))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(256, 3, 3, activation='relu', name='conv3_2'))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(256, 3, 3, activation='relu', name='conv3_3'))
model.add(MaxPooling2D((2, 2), strides=(2, 2)))

model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(512, 3, 3, activation='relu', name='conv4_1'))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(512, 3, 3, activation='relu', name='conv4_2'))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(512, 3, 3, activation='relu', name='conv4_3'))
model.add(MaxPooling2D((2, 2), strides=(2, 2)))

model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(512, 3, 3, activation='relu', name='conv5_1'))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(512, 3, 3, activation='relu', name='conv5_2'))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(512, 3, 3, activation='relu', name='conv5_3'))
model.add(MaxPooling2D((2, 2), strides=(2, 2)))

# get the symbolic outputs of each "key" layer (we gave them unique names).
layer_dict = dict([(layer.name, layer) for layer in model.layers])

注意我们不需要全连接层,所以网络就定义到最后一个卷积层为止

2. 将预训练好的权重载入模型

这么做的目的是让我们更直观地观察到卷积网络的模版,因为随着CNN的训练逐渐拟合,CNN各层的w和b会使得各层的神经元逐渐有意义,即表现出逐层提取图像特征的特性,跳过训练过程,直接观察一个已经训练好的模型,能够让我们从直观上更加理解CNN的卷积神经元究竟意味着什么

# -*- coding: utf-8 -*-

from __future__ import print_function

from scipy.misc import imsave
import numpy as np
import time
from keras.applications import vgg16
from keras import backend as K

# dimensions of the generated pictures for each filter.
img_width = 128
img_height = 128

# the name of the layer we want to visualize
# (see model definition at keras/applications/vgg16.py)
layer_name = 'block5_conv1'

# util function to convert a tensor into a valid image


def deprocess_image(x):
    # normalize tensor: center on 0., ensure std is 0.1
    x -= x.mean()
    x /= (x.std() + 1e-5)
    x *= 0.1

    # clip to [0, 1]
    x += 0.5
    x = np.clip(x, 0, 1)

    # convert to RGB array
    x *= 255
    if K.image_data_format() == 'channels_first':
        x = x.transpose((1, 2, 0))
    x = np.clip(x, 0, 255).astype('uint8')
    return x

# build the VGG16 network with ImageNet weights
model = vgg16.VGG16(weights='imagenet', include_top=False)
print('Model loaded.')

model.summary()

# this is the placeholder for the input images
input_img = model.input

# get the symbolic outputs of each "key" layer (we gave them unique names).
layer_dict = dict([(layer.name, layer) for layer in model.layers[1:]])


def normalize(x):
    # utility function to normalize a tensor by its L2 norm
    return x / (K.sqrt(K.mean(K.square(x))) + 1e-5)


kept_filters = []
for filter_index in range(0, 200):
    # we only scan through the first 200 filters,
    # but there are actually 512 of them
    print('Processing filter %d' % filter_index)
    start_time = time.time()

    # we build a loss function that maximizes the activation
    # of the nth filter of the layer considered
    # 定义一个损失函数,这个损失函数将用于最大化某个指定滤波器的激活值。以该函数为优化目标优化后,我们可以真正看一下使得这个滤波器激活的究竟是些什么东西
    layer_output = layer_dict[layer_name].output
    if K.image_data_format() == 'channels_first':
        loss = K.mean(layer_output[:, filter_index, :, :])
    else:
        loss = K.mean(layer_output[:, :, :, filter_index])

    # we compute the gradient of the input picture wrt this loss
    grads = K.gradients(loss, input_img)[0]

    # normalization trick: we normalize the gradient
    # 计算出来的梯度进行了正规化,使得梯度不会过小或过大。这种正规化能够使梯度上升的过程平滑进行
    grads = normalize(grads)

    # this function returns the loss and grads given the input picture
    iterate = K.function([input_img], [loss, grads])

    # step size for gradient ascent
    step = 1.

    # we start from a gray image with some random noise
    if K.image_data_format() == 'channels_first':
        input_img_data = np.random.random((1, 3, img_width, img_height))
    else:
        input_img_data = np.random.random((1, img_width, img_height, 3))
    input_img_data = (input_img_data - 0.5) * 20 + 128

    # we run gradient ascent for 20 steps
    # 根据刚刚定义的函数,现在可以对某个滤波器的激活值进行梯度上升,这里是梯度下降的逆向应用,即将当前图像像素点朝着梯度的方向去"增强",让图像的像素点反过来和梯度方向去拟合
    for i in range(20):
        loss_value, grads_value = iterate([input_img_data])
        input_img_data += grads_value * step

        print('Current loss value:', loss_value)
        if loss_value <= 0.:
            # some filters get stuck to 0, we can skip them
            break

    # decode the resulting input image
    if loss_value > 0:
        img = deprocess_image(input_img_data[0])
        kept_filters.append((img, loss_value))
    end_time = time.time()
    print('Filter %d processed in %ds' % (filter_index, end_time - start_time))

# we will stich the best 64 filters on a 8 x 8 grid.
n = 8

# the filters that have the highest loss are assumed to be better-looking.
# we will only keep the top 64 filters.
kept_filters.sort(key=lambda x: x[1], reverse=True)
kept_filters = kept_filters[:n * n]

# build a black picture with enough space for
# our 8 x 8 filters of size 128 x 128, with a 5px margin in between
margin = 5
width = n * img_width + (n - 1) * margin
height = n * img_height + (n - 1) * margin
stitched_filters = np.zeros((width, height, 3))

# fill the picture with our saved filters
for i in range(n):
    for j in range(n):
        img, loss = kept_filters[i * n + j]
        stitched_filters[(img_width + margin) * i: (img_width + margin) * i + img_width,
                         (img_height + margin) * j: (img_height + margin) * j + img_height, :] = img

# save the result to disk
imsave('stitched_filters_%dx%d.png' % (n, n), stitched_filters)

3. 可视化每一层的滤波器

下面我们系统的可视化一下各个层的各个滤波器结果,看看CNN是如何对输入进行逐层分解的