CNN采坑

在cnn学习过程中,因为理论中的cnn和实际keras使用中的Convolution layers有一定的差异,造成了不少的困惑,所以在这里做个记录。

理论中的CNN

理论中的CNN,输入一般是二维的图像矩阵,使用一个二维的卷积核,从图像矩阵的左上角开始,取出和卷积核相同大小的矩阵做内积。卷积核在图像中做一定stride的滑动,最后输出的结果也是一个二维的矩阵。使用下面这样图来示例。

在邱锡鹏的书中,这个被称为二维卷积,输入是二维,卷积核是二维,输出是二维。

假设输入图片大小为W*W,卷积核大小F*F,步长S,padding的像素数P,那么可以得出输出的图片长度N:

1
N = (W-F+2P)/S+1

卷积层的参数个数为:

1
(F*F+ 1个bias)* filter数量

同时也有一维卷积。输入是一维,卷积核是一维,输出是一维。

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层,其中filters=5,kernel_size=3
>>> conv2d = tf.keras.layers.Conv2D(5,3)
#创建一个10*10*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)

#mnist只有weight和height,所以这里通过reshape增加一个channel
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
#创建10*10的数据
>>> data = np.random.normal(10,size=(1,10,10))
#创建kernel_size=2,filters=5的Conv1D层
>>> 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 # unknown
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:]

# 输入形状是用于电影评论的词汇数目(10,000 词)
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)