李宏毅 机器学习(4)- CNN介绍、基本理解、code
1、为什么用CNN做影像识别?
那现在的问题是这样子:当我们直接用一般的fully connected的feedforward network来做图像处理的时候,往往会需要太多的参数 举例来说,假设这是一张100100的彩色图片,它的分辨率才100100,那这已经是很小张的image了,然后你需要把它拉成一个vector,总共有1001003个pixel(如果是彩色的图的话,每个pixel其实需要3个value,即RGB值来描述它的),把这些加起来input vectot就已经有三万维了;如果input vector是三万维,又假设hidden layer有1000个neuron,那仅仅是第一层hidden layer的参数就已经有30000*1000个了,这样就太多了
CNN做的事情其实是,来简化这个neural network的架构,我们根据自己的知识和对图像处理的理解,一开始就把某些实际上用不到的参数给过滤掉. CNN看起来,它的运作比较复杂,但事实上,它的模型比DNN还要更简单,我们就是用prior knowledge,去把原来fully connected的layer里面的一些参数拿掉,就变成CNN
2、CNN的架构基础
1 Some patterns are much smaller than the whole image
在影像处理里面,如果在network的第一层hidden layer里,那些neuron要做的事情是侦测有没有一种东西、一种pattern(图案样式)出现,那大部分的pattern其实是比整张image要小的,所以对一个neuron来说,想要侦测有没有某一个pattern出现,它其实并不需要看整张image,只需要看这张image的一小部分,就可以决定这件事情了
对鸟来说,那第一层hidden layer的某一个neuron的工作是,检测有没有鸟嘴的存在(你可能还有一些neuron侦测有没有鸟嘴的存在、有一些neuron侦测有没有爪子的存在、有一些neuron侦测有没有翅膀的存在、有没有尾巴的存在,之后合起来,就可以侦测,图片中有没有一只鸟),那它其实并不需要看整张图,因为,其实我们只要给neuron看这个小的红色杠杠里面的区域,它其实就可以知道说,这是不是一个鸟嘴,对人来说也是一样,只要看这个小的区域你就会知道说这是鸟嘴,所以,每一个neuron其实只要连接到一个小块的区域就好,它不需要连接到整张完整的图,因此也对应着更少的参数
2 The same patterns appear in different regions
举例来说,上图中分别有一个处于左上角的鸟嘴和一个处于中央的鸟嘴,但你并不需要训练两个不同的detector去专门侦测左上角有没有鸟嘴和中央有没有鸟嘴这两件事情,这样做太冗余了,我们要cost down(降低成本),我们并不需要有两个neuron、两组不同的参数来做duplicate(重复一样)的事情,所以我们可以要求这些功能几乎一致的neuron共用一组参数,它们share同一组参数就可以帮助减少总参数的量
3 Subsampling the pixels will not change the object
我们可以对一张image做subsampling(二次抽样),假如你把它奇数行、偶数列的pixel拿掉,image就可以变成原来的十分之一大小,而且并不会影响人对这张image的理解,对你来说,下面两张大小不一的image看起来不会有什么太大的区别,你都可以识别里面有什么物件,因此subsampling对图像辨识来说,可能是没有太大的影响的 所以,我们可以利用subsampling这个概念把image变小,从而减少需要用到的参数量
整体架构
首先,input一张image以后,它会先通过Convolution的layer,接下来做Max Pooling这件事,然后再去做Convolution,再做Max Pooling...,这个process可以反复进行多次(重复次数需要事先决定),这就是network的架构,就好像network有几层一样,你要做几次convolution,做几次Max Pooling,在定这个network的架构时就要事先决定好
当你做完先前决定的convolution和max pooling的次数后,你要做的事情是Flatten,做完flatten以后,你就把Flatten output丢到一般的Fully connected network里面去,最终得到影像辨识的结果
CNN这样的架构,第一个是要侦测一个pattern,你不需要看整张image,只要看image的一个小部分;第二个是同样的pattern会出现在一张图片的不同区域;第三个是我们可以对整张image做subsampling
那前面这两个property,是用convolution的layer来处理的;最后这个property,是用max pooling来处理的
3、详细cnn介绍-convolution
convolution怎么做性质一:pattern 小于整张图片大小
假设现在我们network的input是一张6*6的image,图像是黑白的,因此每个pixel只需要用一个value来表示,而在convolution layer里面,有一堆Filter,这边的每一个Filter,其实就等同于是Fully connected layer里的一个neuron
每一个Filter其实就是一个matrix,这个matrix里面每一个element的值,就跟那些neuron的weight和bias一样,是network的parameter,它们具体的值都是通过Training data学出来的,而不是人去设计的 所以,每个Filter里面的值是什么,要做什么事情,都是自动学习出来的,上图中每一个filter是3✖️3的size,意味着它就是在侦测一个3✖️3的pattern,当它侦测的时候,并不会去看整张image,它只看一个3✖️3范围内的pixel,就可以判断某一个pattern有没有出现,这就考虑了property 1
convolution怎么做性质二 : pattern重复
这个filter是从image的左上角开始,做一个slide window,每次向右挪动一定的距离,这个距离就叫做stride,由你自己设定,每次filter停下的时候就跟image中对应的3✖️3的matrix做一个内积(相同位置的值相乘并累计求和),这里假设stride=1,那么我们的filter每次移动一格,当它碰到image最右边的时候,就从下一行的最左边开始重复进行上述操作,经过一整个convolution的process,最终得到下图所示的红色的4✖️4 matrix
观察上图中的Filter1,它斜对角的地方是1,1,1,所以它的工作就是detect有没有连续的从左上角到右下角的1,1,1出现在这个image里面,检测到的结果已在上图中用蓝线标识出来,此时filter得到的卷积结果的左上和左下得到了最大的值,这就代表说,该filter所要侦测的pattern出现在image的左上角和左下角
同一个pattern出现在image左上角的位置和左下角的位置,并不需要用到不同的filter,我们用filter1就可以侦测出来,这就考虑了property 2
Feature Map
在一个convolution的layer里面,它会有一打filter,不一样的filter会有不一样的参数,但是这些filter做卷积的过程都是一模一样的,你把filter2跟image做完convolution以后,你就会得到另外一个蓝色的4✖️4 matrix,那这个蓝色的4✖️4 matrix跟之前红色的4✖️4matrix合起来,就叫做Feature Map(特征映射),有多少个filter,对应就有多少个映射后的image
局限性:不同scale的相同pattern的处理
CNN对不同scale的相同pattern的处理上存在一定的困难,由于现在每一个filter size都是一样的,这意味着,如果你今天有同一个pattern,它有不同的size,有大的鸟嘴,也有小的鸟嘴,CNN并不能够自动处理这个问题;DeepMind曾经发过一篇paper,上面提到了当你input一张image的时候,它在CNN前面,再接另外一个network,这个network做的事情是,它会output一些scalar,告诉你说,它要把这个image的里面的哪些位置做旋转、缩放,然后,再丢到CNN里面,这样你其实会得到比较好的performance
彩色的怎么做
彩色的image就是由RGB组成的,所以一个彩色的image,它就是好几个matrix叠在一起,是一个立方体,如果我今天要处理彩色的image,要怎么做呢?
这个时候你的filter就不再是一个matrix了,它也会是一个立方体,如果你今天是RGB这三个颜色来表示一个pixel的话,那你的input就是3*6*6,你的filter就是3*3*3,你的filter的高就是3,你在做convolution的时候,就是把这个filter的9个值跟这个image里面的9个值做内积,可以想象成filter的每一层都分别跟image的三层做内积,得到的也是一个三层的output,每一个filter同时就考虑了不同颜色所代表的channel
如何理解convolution和之前普通的dnn里面neuron的关系
filter是特殊的”neuron“
convolution这件事情,其实就是fully connected的layer把一些weight拿掉而已,下图中绿色方框标识出的feature map的output,其实就是hidden layer的neuron的output
老师的详细解释
如下图所示,我们在做convolution的时候,把filter放在image的左上角,然后再去做inner product,得到一个值3;这件事情等同于,我们现在把这个image的6*6的matrix拉直变成右边这个用于input的vector,然后,你有一个红色的neuron,这些input经过这个neuron之后,得到的output是3
作为对比,Fully connected的neuron是必须连接到所有36个input上的,但是,我们现在只用连接9个input,因为我们知道要detect一个pattern,不需要看整张image,看9个input pixel就够了,所以当我们这么做的时候,就用了比较少的参数
“neuron”之间共享参数,weight share
当我们把filter做stride = 1的移动的时候,会发生什么事呢?此时我们通过filter和image matrix的内积得到另外一个output值-1,我们假设这个-1是另外一个neuron的output,那这个neuron会连接到哪些input呢?下图中这个框起来的地方正好就对应到pixel 2,3,4,pixel 8,9,10跟pixel 14,15,16 你会发现output为3和-1的这两个neuron,它们分别去检测在image的两个不同位置上是否存在某个pattern,因此在Fully connected layer里它们做的是两件不同的事情,每一个neuron应该有自己独立的weight 但是,当我们做这个convolution的时候,首先我们把每一个neuron前面连接的weight减少了,然后我们强迫某些neuron(比如上图中output为3和-1的两个neuron),它们一定要共享一组weight,虽然这两个neuron连接到的pixel对象各不相同,但它们用的weight都必须是一样的,等于filter里面的元素值,这件事情就叫做weight share,当我们做这件事情的时候,用的参数,又会比原来更少
总结
因此我们可以这样想,有这样一些特殊的neuron,它们只连接着9条带weight的线(9=3*3对应着filter的元素个数,这些weight也就是filter内部的元素值,上图中圆圈的颜色与连线的颜色一一对应)
当filter在image matrix上移动做convolution的时候,每次移动做的事情实际上是去检测这个地方有没有某一种pattern,对于Fully connected layer来说,它是对整张image做detection的,因此每次去检测image上不同地方有没有pattern其实是不同的事情,所以这些neuron都必须连接到整张image的所有pixel上,并且不同neuron的连线上的weight都是相互独立的
那对于convolution layer来说,首先它是对image的一部分做detection的,因此它的neuron只需要连接到image的部分pixel上,对应连线所需要的weight参数就会减少;其次由于是用同一个filter去检测不同位置的pattern,所以这对convolution layer来说,其实是同一件事情,因此不同的neuron,虽然连接到的pixel对象各不相同,但是在“做同一件事情”的前提下,也就是用同一个filter的前提下,这些neuron所使用的weight参数都是相同的,通过这样一张weight share的方式,再次减少network所需要用到的weight参数
CNN的本质,就是减少参数的过程
怎么train:怎么让某些neuron的weight值永远都是一样呢?你就用一般的Backpropagation的方法,对每个weight都去算出gradient,再把本来要tight在一起、要share weight的那些weight的gradient平均,然后,让他们update同样值就ok
4、详细cnn介绍-maxpooling
相较于convolution,max pooling是比较简单的,它就是做subsampling,根据filter 1,我们得到一个4*4的matrix,根据filter 2,你得到另外一个4*4的matrix,接下来,我们要做什么事呢?
我们把output四个分为一组,每一组里面通过选取平均值或最大值的方式,把原来4个value合成一个 value,这件事情相当于在image每相邻的四块区域内都挑出一块来检测,这种subsampling的方式就可以让你的image缩小!
做完一次convolution加一次max pooling,我们就把原来6*6的image,变成了一个2*2的image;至于这个2*2的image,它每一个pixel的深度,也就是每一个pixel用几个value来表示,就取决于你有几个filter,如果你有50个filter,就是50维,像下图中是两个filter,对应的深度就是两维
所以,这是一个新的比较小的image,它表示的是不同区域上提取到的特征,实际上不同的filter检测的是该image同一区域上的不同特征属性,所以每一层channel(通道)代表的是一种属性,一块区域有几种不同的属性,就有几层不同的channel,对应的就会有几个不同的filter对其进行convolution操作
这件事情可以repeat很多次,你可以把得到的这个比较小的image,再次进行convolution和max pooling的操作,得到一个更小的image,依次类推 有这样一个问题:假设我第一个convolution有25个filter,通过这些filter得到25个feature map,然后repeat的时候第二个convolution也有25个filter,那这样做完,我是不是会得到25^2个feature map?
其实不是这样的,你这边做完一次convolution,得到25个feature map之后再做一次convolution,还是会得到25个feature map,因为convolution在考虑input的时候,是会考虑深度的,它并不是每一个channel分开考虑,而是一次考虑所有的channel,所以,你convolution这边有多少个filter,再次output的时候就会有多少个channel
因此你这边有25个channel,经过含有25个filter的convolution之后output还会是25个channel,只是这边的每一个channel,它都是一个cubic(立方体),它的高有25个value那么高
5、flatten
做完convolution和max pooling之后,就是FLatten和Fully connected Feedforward network的部分
Flatten的意思是,把左边的feature map拉直,然后把它丢进一个Fully connected Feedforward network,然后就结束了,也就是说,我们之前通过CNN提取出了image的feature,它相较于原先一整个image的vetor,少了很大一部分内容,因此需要的参数也大幅度地减少了,但最终,也还是要丢到一个Fully connected的network中去做最后的分类工作
6、keras中的cnn
如何用Keras来implement CNN,实际上在compile、training和fitting的部分,内容跟DNN是一模一样的,对CNN来说,唯一需要改变的是network structure,以及input的format
本来在DNN里,input是一个由image拉直展开而成的vector,但现在如果是CNN的话,它是会考虑input image的几何空间的,所以不能直接input一个vector,而是要input一个tensor给它(tensor就是高维的vector),这里你要给它一个三维的vector,一个image的长宽各是一维,如果它是彩色的话,RGB就是第三维,所以你要assign一个三维的matrix,这个高维的matrix就叫做tensor 还是用model.add增加CNN的layer,将原先的Dense改成Convolution2D,参数25代表你有25个filter,参数3,3代表你的filter都是3*3的matrix,此外你还需要告诉model,你input的image的shape是什么样子的,假设我现在要做手写数字识别,input就是28*28的image,又因为它的每一个pixel都只有单一颜色,因此input_shape的值就是(28,28,1),如果是RGB的话,1就要改成3
model2.add( Convolution2D(25,3,3, input_shape=(28,28,1)) )
model2.add( MaxPooling2D(2,2) )
# 这里参数(2,2)指的是,我们把通过convolution得到的feature map
# 按照2*2的方式分割成一个个区域,每次选取最大的那个值,并将这些值组成一个新的比较小的image,作为subsampling的结果
在第一个convolution里面,每一个filter都有9个参数,它就是一个3*3的matrix;但是在第二个convolution layer里面,虽然每一个filter都是3*3,但它其实不是3*3个参数,因为它的input是一个25*13*13的cubic,这个cubic的channel有25个,所以要用同样高度的cubic filter对它进行卷积,于是我们的filter实际上是一个25*3*3的cubic,所以这边每个filter共有225个参数 通过两次convolution和max pooling的组合,最终的image变成了50*5*5的size,然后使用Flatten将这个image拉直,变成一个1250维的vector,再把它丢到一个Fully Connected Feedforward network里面,network structure就搭建完成了
一个重要的问题
看到这里,你可能会有一个疑惑,第二次convolution的input是25*13*13的cubic,用50个3*3的filter卷积后,得到的输出时应该是50个cubic,且每个cubic的尺寸为25*11*11,那么max pooling把长宽各砍掉一半后就是50层25*5*5的cubic,那flatten后不应该就是50*25*5*5吗?
其实不是这样的,在第二次做convolution的时候,我们是用25*3*3的cubic filter对25*13*13的cubic input进行卷积操作的,filter的每一层和input cubic中对应的每一层(也就是每一个channel),它们进行内积后,还要把cubic的25个channel的内积值进行求和,作为这个“neuron”的output,它是一个scalar,这个cubic filter对整个cubic input做完一遍卷积操作后,得到的是一层scalar,然后有50个cubic filter,对应着50层scalar,因此最终得到的output是一个50*11*11的cubic!
这里的关键是filter和image都是cubic,每个cubic filter有25层高,它和同样有25层高的cubic image做卷积,并不是单单把每个cubic对应的channel进行内积,还会把这些内积求和!!!最终变为1层,因此两个矩阵或者tensor做了卷积后,不管之前的维数如何,都会变为一个scalor!,故如果有50个Filter,无论input是什么样子的,最终的output还会是50层
7、示例code
cnn 0.98 dnn 0.96
import numpy as np
from keras.models import Sequential
from keras.layers import Convolution2D, MaxPooling2D, Flatten, Conv2D
from keras.layers.core import Dense, Dropout, Activation
from keras.optimizers import SGD, Adam
from keras.utils import np_utils
from keras.datasets import mnist
# categorical_crossentropy
def load_mnist_data(number):
# the data, shuffled and split between train and test sets
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train = x_train[0:number]
y_train = y_train[0:number]
x_train = x_train.reshape(number, 784)
x_test = x_test.reshape(10000, 784)
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
# convert class vectors to binary class matrices
y_train = np_utils.to_categorical(y_train, 10)
y_test = np_utils.to_categorical(y_test, 10)
x_train = x_train / 255
x_test = x_test / 255
return (x_train, y_train), (x_test, y_test)
if __name__ == '__main__':
(x_train, y_train), (x_test, y_test) = load_mnist_data(10000)
# do DNN
model = Sequential()
model.add(Dense(input_dim=28 * 28, units=500, activation='relu'))
model.add(Dense(units=500, activation='relu'))
model.add(Dense(units=500, activation='relu'))
model.add(Dense(units=10, activation='softmax'))
model.summary()
model.compile(loss='categorical_crossentropy',
optimizer='adam', metrics=['accuracy'])
model.fit(x_train, y_train, batch_size=100, epochs=20)
result_train = model.evaluate(x_train, y_train)
print('\nTrain Acc:\n', result_train[1])
result_test = model.evaluate(x_test, y_test)
print('\nTest Acc:\n', result_test[1])
# do CNN
x_train = x_train.reshape(x_train.shape[0], 1, 28, 28)
x_test = x_test.reshape(x_test.shape[0], 1, 28, 28)
model2 = Sequential()
model2.add(Conv2D(25, (3, 3), input_shape=(
1, 28, 28), data_format='channels_first'))
model2.add(MaxPooling2D((2, 2)))
model2.add(Conv2D(50, (3, 3)))
model2.add(MaxPooling2D((2, 2)))
model2.add(Flatten())
model2.add(Dense(units=100, activation='relu'))
model2.add(Dense(units=10, activation='softmax'))
model2.summary()
model2.compile(loss='categorical_crossentropy',
optimizer='adam', metrics=['accuracy'])
model2.fit(x_train, y_train, batch_size=100, epochs=20)
result_train = model2.evaluate(x_train, y_train)
print('\nTrain CNN Acc:\n', result_train[1])
result_test = model2.evaluate(x_test, y_test)
print('\nTest CNN Acc:\n', result_test[1])