CNN采坑
在cnn学习过程中,因为理论中的cnn和实际keras使用中的Convolution layers有一定的差异,造成了不少的困惑,所以在这里做个记录。
理论中的CNN
理论中的CNN,输入一般是二维的图像矩阵,使用一个二维的卷积核,从图像矩阵的左上角开始,取出和卷积核相同大小的矩阵做内积。卷积核在图像中做一定stride的滑动,最后输出的结果也是一个二维的矩阵。使用下面这样图来示例。

在邱锡鹏的书中,这个被称为二维卷积,输入是二维,卷积核是二维,输出是二维。
假设输入图片大小为W*W,卷积核大小F*F,步长S,padding的像素数P,那么可以得出输出的图片长度N:
卷积层的参数个数为:
同时也有一维卷积。输入是一维,卷积核是一维,输出是一维。

keras中的CNN
打开keras的api文档,可以看到keras对于卷积层,有具体一下几种实现:
- Conv1D layer
- Conv2D layer
- Conv3D layer
- SeparableConv1D layer
- SeparableConv2D layer
- DepthwiseConv2D layer
- Conv2DTranspose layer
- Conv3DTranspose layer
这里主要讲Conv1D和Conv2D,从名字上看,很容易把这两个和上面的一维卷积和二维卷积联系在一起,但事实上是有一些差别的,这也是给我造成困惑的地方。
Conv2D
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| >>> import tensorflow as tf >>> import numpy as np
>>> conv2d = tf.keras.layers.Conv2D(5,3)
>>> image = np.random.normal(10,size=(1,10,10,3))
>>> r = conv2d(image)
>>> r.shape TensorShape([1, 8, 8, 5])
>>> kernel,bias=conv2d.weights >>> kernel.shape TensorShape([3, 3, 3, 5]) >>> bias.shape TensorShape([5])
|
可以看到,输入是(10,10,3)的三维张量。
根据公式,输出的尺寸N=(10-3)/1+1 = 8。卷积之后输出的结果为(8,8,5)的三维张量。
打印卷积核,输出尺寸为(3,3,3)的三维张量。
从上面可以看出,keras里面的Conv2D从输入到输出到卷积核,其实都是三维的张量,和前面介绍的二维卷积完全不一样。这也是造成我困惑的主要原因。
所以总结一下,
输入尺寸(weight,height,channel)
卷积核尺寸(kernel_size,kernel_size,channel),kernel_size需要传参,channel会根据输入自动设定。
输出尺寸(N,N,filters) ,其中N = (W-F+2P)/S+1
。
卷积层参数计算:(kernel_size*kernel_size+bias)*filters
。
附一段通过ConvD来进行mnist分类的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import tensorflow as tf
(train_images,train_labels),(test_images,test_labels) = tf.keras.datasets.mnist.load_data() train_labels = tf.one_hot(train_labels, 10, 1,0) test_labels = tf.one_hot(test_labels, 10, 1,0)
train_images = train_images.reshape(-1,28,28,1) test_images = test_images.reshape(-1,28,28,1)
model = tf.keras.models.Sequential() model.add(tf.keras.layers.Conv2D(3,3, input_shape=(28,28,1))) model.add(tf.keras.layers.MaxPool2D()) model.add(tf.keras.layers.Conv2D(5,3)) model.add(tf.keras.layers.MaxPool2D()) model.add(tf.keras.layers.Flatten()) model.add(tf.keras.layers.Dense(10,activation="softmax"))
model.summary()
model.compile(optimizer=tf.keras.optimizers.Adam(),loss=tf.keras.losses.categorical_crossentropy,metrics=tf.keras.metrics.categorical_accuracy)
model.fit(train_images,train_labels,epochs=10,batch_size=10,validation_data=(test_images,test_labels))
|
Conv1D
Conv2D实际比二维卷积要多出一维,那么Conv1D是否就是理论上的二维卷积呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| >>> import tensorflow as tf >>> import numpy as np
>>> data = np.random.normal(10,size=(1,10,10))
>>> conv1d = tf.keras.layers.Conv1D(5,2)
>>> conv1d(data) >>> r = conv1d(data)
>>> r.shape TensorShape([1, 9, 5])
>>> kernel,bias=conv1d.weights >>> kernel.shape TensorShape([2, 10, 5]) >>> bias.shape TensorShape([5])
|
可以看到输入是(10,10),卷积核是(2,10),输出是(9,5)。
卷积核的大小设定是2,但为什么实际是(2,10),而不是(2,2)呢?
虽然所有尺寸都是二维的,但是可以看到不管卷积核的大小,还是输出的大小,和 二维卷积、甚至一维卷积都不一样。
这里引用别人的图来说明一下Conv1D的工作原理。

输入( word_size, embedding_size ),卷积核大小(kernel_size, embedding_size)。
我们设定的kernel_size表示要同时计算几个word embedding,而卷积核的长度随着word embedding的尺寸而随之变化。
卷积核自上而下根据stride来扫描,进行内积的计算。所以这里卷积核不是正方形的。
所以可以总结如下:
输入(word_size, embedding_size)
卷积核(kernel_size, embedding_size)
输出(n, filters), 其中N = (W-F+2P)/S+1
最后附上使用Conv1D来做imdb分类的代码:
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
| import tensorflow as tf from tensorflow import keras
import numpy as np
print(tf.__version__)
imdb = keras.datasets.imdb
(train_data, train_labels), (test_data, test_labels) = imdb.load_data(num_words=10000)
word_index = imdb.get_word_index()
word_index = {k:(v+3) for k,v in word_index.items()} word_index["<PAD>"] = 0 word_index["<START>"] = 1 word_index["<UNK>"] = 2 word_index["<UNUSED>"] = 3
train_data = keras.preprocessing.sequence.pad_sequences(train_data, value=word_index["<PAD>"],padding='post',maxlen=256)
test_data = keras.preprocessing.sequence.pad_sequences(test_data,value=word_index["<PAD>"],padding='post',maxlen=256)
x_val = train_data[:10000] partial_x_train = train_data[10000:]
y_val = train_labels[:10000] partial_y_train = train_labels[10000:]
vocab_size = 10000
model = keras.Sequential() model.add(keras.layers.Embedding(vocab_size, 16)) model.add(keras.layers.Dropout(0.7)) model.add(keras.layers.Conv1D(128, 7, padding="valid", activation="relu", strides=3)) model.add(keras.layers.Conv1D(128, 7, padding="valid", activation="relu", strides=3)) model.add(keras.layers.GlobalAveragePooling1D())
model.add(keras.layers.Dense(128, activation="relu",kernel_regularizer=keras.regularizers.L2(1))) model.add(keras.layers.Dropout(0.7))
model.add(keras.layers.Dense(2, activation='softmax'))
model.summary()
model.compile(optimizer='adam', loss=keras.losses.SparseCategoricalCrossentropy(), metrics=['accuracy'])
history = model.fit(partial_x_train, partial_y_train, epochs=20, batch_size=512, validation_data=(x_val, y_val), verbose=1)
|