这篇博客记录一下自己学习实践CNN的一些知识。可能东西会比较碎。
关于CNN的基本原理,请参看《深度学习(四):卷积神经网络(CNN)模型结构,前向传播算法和反向传播算法介绍。》
一、卷积操作和池化操作
卷积操作和池化操作是CNN的核心操作。卷积操作在局部相关的数据中通过权重共享获得更好的表示,池化的基本作用是假设了图像的平移不变性,提高了网络的统计效率。
我们尝试讨论以下2个问题:
- ①在参数数量不变的前提下,使用卷积能否提高图像识别的准确率?
- ②卷积一定要配合池化使用么?
我们使用经典的数据集MNIST来说明这2个问题。
1.1 卷积提高图片识别性能
对于第1个问题,我们使用两种模型,即仅使用全连接层的传统神经网络和卷积神经网络。为了公平对比,需要保证两种模型的可训练参数大致相同,如果需要证明卷积真的有效,最好是卷积模型的参数小于普通模型的参数,但是性能还会比普通模型更优。
1.2 池化操作的用处
对于第2个问题,我们同样使用两种模型,即带最大池化的卷积模型和平均池化的卷积模型。我们的问题是,加入池化后,会不会使得CNN表现更加优异呢?另外,平均池化和最大池化效果是否会相同?
1.3 代码
下面的代码完整地说明了问题1和问题2。一些细节,我在代码中加了注释。代码来自参考文献【2】。
from keras.layers import Input
from keras.datasets import mnist
import matplotlib.pyplot as plt
from keras.models import Model
from keras.layers import Conv2D,MaxPooling2D,AveragePooling2D
from keras.layers import Dense,Activation,Flatten
from keras.utils import to_categorical
from keras.layers import BatchNormalization as BN
# 这里为了快速说明问题,将测试集的10000个样本作为训练样本了。
# 数据集链接:https://keras-cn.readthedocs.io/en/latest/other/datasets/
(_,_),(X_train,y_train)=mnist.load_data()
train_labels = to_categorical(y_train)
X_train_normal = X_train.reshape(10000,28*28)
X_train_normal = X_train_normal.astype('float32') / 255
X_train_conv = X_train.reshape(10000,28,28,1)
X_train_conv = X_train_conv.astype('float32') / 255
# filters卷积核的个数,(length, width)卷积核的大小。
def Conv2D_bn(x,filters,length,width,padding='same',strides=(1, 1),dilation_rate=1):
x = Conv2D(filters, (length, width),strides=strides,padding=padding)(x)
# Batch Normalization
x = BN()(x)
x = Activation('relu')(x)
return x
def Dense_bn(x,units):
# units输出后的维度
x = Dense(units)(x)
x =BN()(x)
x =Activation('relu')(x)
return x
def normal_model():
x= Input(shape=(28*28,))
x2= Dense_bn(x,1000)
x3=Dense_bn(x2,512)
x4=Dense_bn(x3,256)
y=Dense(10,activation='softmax')(x4)
model = Model(inputs=x, outputs=y)
model.compile(optimizer='SGD',loss='categorical_crossentropy',\
metrics=['accuracy'])
return(model)
def conv_model():
x= Input(shape=(28,28,1))
x2= Conv2D_bn(x,64,3,3)
x3= Conv2D_bn(x2,32,3,3)
x4=Flatten()(x3)
x5=Dense_bn(x4,32)
y=Dense(10,activation='softmax')(x5)
model = Model(inputs=x, outputs=y)
model.compile(optimizer='SGD',\
loss='categorical_crossentropy',\
metrics=['accuracy'])
return(model)
def conv_model_max():
x= Input(shape=(28,28,1))
x2= Conv2D_bn(x,64,3,3)
x3=MaxPooling2D(2)(x2)
x4= Conv2D_bn(x3,32,3,3)
x5=Flatten()(x4)
x6=Dense_bn(x5,32)
y=Dense(10,activation='softmax')(x6)
model = Model(inputs=x, outputs=y)
model.compile(optimizer='SGD',loss='categorical_crossentropy',metrics=['accuracy'])
return(model)
def conv_model_mean():
x= Input(shape=(28,28,1))
x2= Conv2D_bn(x,64,3,3)
x3=AveragePooling2D(2)(x2)
x4= Conv2D_bn(x3,32,3,3)
x5=Flatten()(x4)
x6=Dense_bn(x5,32)
y=Dense(10,activation='softmax')(x6)
model = Model(inputs=x, outputs=y)
model.compile(optimizer='SGD',loss='categorical_crossentropy',metrics=['accuracy'])
return(model)
# ①只有全连接层的传统网络
model_1=normal_model()
model_1.summary()
his1=model_1.fit(X_train_normal,train_labels,batch_size=128,validation_split=0.3,verbose=1,epochs=10)
'''
Total params: 1,438,482
Trainable params: 1,434,946
Non-trainable params: 3,536
'''
# ②带卷积
model_2=conv_model()
model_2.summary()
his2=model_2.fit(X_train_conv,train_labels,batch_size=128,validation_split=0.3,verbose=1,epochs=10)
'''
Total params: 822,794
Trainable params: 822,538
Non-trainable params: 256
'''
# ③卷积+最大池化
model_3=conv_model_max()
model_3.summary()
his3=model_3.fit(X_train_conv,train_labels,batch_size=128,validation_split=0.3,verbose=1,epochs=10)
'''
Total params: 220,682
Trainable params: 220,426
Non-trainable params: 256
'''
# ④卷积+平均池化
model_4=conv_model_mean()
model_4.summary()
his4=model_4.fit(X_train_conv,train_labels,batch_size=128,validation_split=0.3,verbose=1,epochs=10)
'''
Total params: 220,682
Trainable params: 220,426
Non-trainable params: 256
'''
w1=his1.history
w2=his2.history
w3=his3.history
w4=his4.history
import seaborn as sns
sns.set(style='whitegrid')
plt.plot(range(10),w1['acc'],'b-.',label='Normal train')
plt.plot(range(10),w2['acc'],'r-.',label='CNN train')
plt.plot(range(10),w1['val_acc'],'b-',label='Normal test')
plt.plot(range(10),w2['val_acc'],'r-',label='CNN test')
plt.plot(range(10),w3['acc'],'k-.',label='CNN+Maxpooling train')
plt.plot(range(10),w3['val_acc'],'k-',label='CNN+Maxpooling test')
plt.plot(range(10),w4['acc'],'g-.',label='CNN+Meanpooling train')
plt.plot(range(10),w4['val_acc'],'g-',label='CNN+Meanpooling test')
plt.xlabel('epochs')
plt.ylabel('accuracy')
plt.legend()
结果为:
结论如下:
- 对于问题1,我们发现:即便普通全连接模型参数数目远大于CNN,但几乎在每一个epochs上,训练和测试准确率均不如CNN表现优异,平均看来,CNN的准确率要比全连接模型高出三个百分点,我们已经有理由说,CNN是比全连接神经网络在MNIST上更为优异的模型。
- 对于问题2,我们发现:加入池化操作后的卷积神经网络的参数数目明显小于不加池化的。然而,准确率没有提升。这说明了,池化使得模型的参数数量大大减少,而对于准确率不一定会有提升。
参数数量的减少主要来源于最后全连接层时的参数减少,卷积操作时参数并不会减少(因为卷积操作的参数是跟卷积核有关的,只要卷积核不变,参数数目就不会变。)
二、CNN的一些知识点Q&A
Q:如果输入图片的尺寸为,经过
个
的卷积核,步长(stride)为
,zero-padding为
,那么经过该卷积层输出图像的尺寸是多少?不考虑偏置的话,涉及到多少个参数?
A:经过这样的操作,输出图片的长为 ,宽为
,深为K。
参数数量:总共涉及到K个卷积核,每个卷积核的权重参数个数为SSC。
Q: Tensorflow或者Keras中卷积函数Conv2D中padding参数为‘valid’和‘same’,解释一下两者的区别。
A: 这是2个不同的padding方式。VALID是采用丢弃的方式,比如下图中的例子,VALID只允许滑动2次,多余的元素全部丢掉。而SAME的方式,采用的是补全的方式,对于上述的情况,允许滑动3次,但是需要补3个元素,左奇右偶,在左边补一个0,右边补2个0
整理一下,对于VALID,输出的形状计算如下:
对于SAME,输出的形状计算如下:
其中,为输入的size,
为filter的size,
为步长,
为向上取整符号。
Q: Tensorflow或者Keras中卷积函数Conv2D中参数dilation_rate有什么作用?如何理解空洞卷积?
dilation_rate是空洞卷积的参数,控制其其膨胀率。空洞卷积指的是,在不增加参数的前提下,将原有的卷积核扩大。原本的卷积操作是的卷积核作用在
的区域内,而设置dilation_rate=2后,空洞卷积则会作用在
的区域内,一定程度上扩大了特征范围。
下图是个例子,
关于空洞卷积的介绍请看这里。