卷积神经网络的组成
卷积神经网络由一个或多个卷积层、池化层以及全连接层等组成。与其他的深度学习结构相比,卷积神经网络在图像等方面能够给出更好的结果。这一模型也可以使用反向传播算法(back-propagation)进行训练。相比其他浅层神经网络和深度神经网络,卷积神经网络需要考量的参数更少。
LeNet5作为经典的卷积神经网络之一,推动了深度学习的发展,先看下LeNet5的网络结构:
该网络中主要包含了两个结构:
- 卷积层(convolution layer):通过在原始图像上使用过滤器(也叫卷积核)平移来提取特征后的结果就是特征图(feature map)。
- 池化层(pooling layer):又叫子采样层。通过特征后稀疏参数来减少学习的参数,降低网络的复杂度。池化的方法有最大池化和平均池化。
- 全连接层:前面的卷积层和池化层相当于做特征工程,全连接层则相当于做特征加权,起到“分类器”的作用。
卷积层
卷积的运算过程
以一张单通道图片的大小为 5 ∗ 5 5∗5 5∗5,以步长为1、卷积核大小为 3 ∗ 3 3∗3 3∗3进行运算,经过卷积核平移以后得到 3 ∗ 3 3*3 3∗3的运算结果。
通过上图运算可以发现,进行卷积之后的图片变小了。假设N为图片大小,F为卷积核大小,那么输出大小公式为:
(
N
−
F
+
1
)
∗
(
N
−
F
+
1
)
=
(
5
−
3
+
1
)
∗
(
5
−
3
+
1
)
=
3
∗
3
\left(N-F+1\right)*\left(N-F+1\right)⁼\left(5-3+1\right)*\left(5-3+1\right)=3*3
(N−F+1)∗(N−F+1)=(5−3+1)∗(5−3+1)=3∗3
如果我们换一个卷积核大小或者加入很多层卷积之后,经过内积计算后图像可能最后就变成了1 X 1 大小,这不是我们希望看到的结果。并且对于原始图片当中的边缘像素来说,只计算了一遍,而中间的像素会有很多次卷积核与之计算,这样导致对边缘信息的丢失。那么如何解决这样的问题?
零填充
在图片像素的最外层加上P层0值,因为0在权重乘积和运算中对最终结果不造成影响,也就避免了图片增加了额外的干扰信息。由于移动步长不一定能够整除整张图的像素宽度,导致图片的边缘信息丢,这就是零填充的动机所在。
在上图中,假设还是跟之前一样图片大小为
5
∗
5
5*5
5∗5、卷积核大小为
3
∗
3
3*3
3∗3、步长为1,填充1层,那么可以使用该公式计算输出图片大小:
N
+
2
P
−
F
+
1
=
5
+
2
∗
1
−
3
+
1
=
5
N+2P-F+1=5+2*1-3+1=5
N+2P−F+1=5+2∗1−3+1=5;这只是填充一层的结果,实际上可以填充更过层,如果填充层数为2,则输出图片的大小比原图还要大,那么对于零填充层数该怎么选择?在CNN中,有两种填充方式:
- VALID填充:不越过边缘取样,取样的面积小于输入的图像的像素宽度。最终输出大小为: ( N − F + 1 ) ∗ ( N − F + 1 ) (N−F+1)∗(N−F+1) (N−F+1)∗(N−F+1)
- SAME填充:越过边缘取样,取样的面积和输入图像的宽度一致。输出大小还是原来图片大小: ( N + 2 P − F + 1 ) ∗ ( N + 2 P − F + 1 ) (N+2P−F+1)∗(N+2P−F+1) (N+2P−F+1)∗(N+2P−F+1)
那么问题又来了,在以上公式中,如果卷积核F的选择不是奇数而是偶数,那么计算的结果则不为整数,导致填充不均匀,所以,一般都是选择奇数维度的卷积核大小。
步长
在上面的例子中,我们都是选择1为步长进行计算的结果,当步长设置为2时,结果又如何?
如果按照之前的输出公式计算,那么结果为:
N
+
2
P
−
F
+
1
=
6
+
2
∗
0
−
3
+
1
=
4
N+2P−F+1=6+2*0−3+1=4
N+2P−F+1=6+2∗0−3+1=4每次移动两个像素才得到一个计算的结果,所以公式变为:
(
N
+
2
P
−
F
)
S
+
1
=
1.5
+
1
=
2.5
\frac {(N+2P-F)}{S}+1 = 1.5+1 = 2.5
S(N+2P−F)+1=1.5+1=2.5结果不为整数时做法是向下取整,为2。所以最终的公式为:
(
(
N
+
2
P
−
F
)
S
+
1
)
∗
(
(
N
+
2
P
−
F
)
S
+
1
)
\left(\frac {(N+2P-F)}{S}+1 \right)∗\left(\frac {(N+2P-F)}{S}+1 \right)
(S(N+2P−F)+1)∗(S(N+2P−F)+1)
多通道图片卷积
当输入有多个通道(channel)时(例如图片可以有 RGB 三个通道),卷积核需要拥有相同的channel数,每个卷积核 channel 与输入层的对应 channel 进行卷积,将每个 channel 的卷积结果按位相加得到最终的feature map。卷积核数量可以有多个,多个卷积核经过内积后输出多张 feature map。对应的,feature map的通道数和卷积核的数量一致。
池化层
池化层主要对卷积层学习到的特征进行子采样(subsampling)处理,通过去掉feature map中不重要的样本,进一步减少参数数量,缩减模型大小,提高模型计算速度和feature map的鲁棒性,防止过拟合。
主要有两种:
- 最大池化(max pooling):取卷积核内的最大值作为输出。是最常用的一种池化方法。
- 平均池化(avg pooling):取卷积核内的所有值的均值作为输出。
全连接层
卷积层+激活层+池化层可以看成是CNN的特征学习/特征提取层,而学习到的feature map最终应用于模型任务(分类、回归):
- 先对所有 feature map 进行扁平化(flatten, 即 reshape 成 1 x N 向量)
- 再接一个或多个全连接层,进行模型学习
代码实现
# 定义一个初始化权重的函数
def weight_variables(shape):
w = tf.Variable(tf.random_normal(shape=shape, mean=0.0, stddev=1.0))
return w
# 定义一个初始化偏置的函数
def bias_variables(shape):
b = tf.Variable(tf.constant(0.0, shape=shape))
return b
def conv_fc():
"""自定义卷积模型"""
with tf.variable_scope("plshd"):
# 准备数据的占位符
x = tf.placeholder(tf.float32, [None, 784])
y_true = tf.placeholder(tf.int32, [None, 10])
# 随机初始化权重、偏置
with tf.variable_scope('first_layer_conv'):
# 5x5: filter的大小 32: filter数量
# w = tf.Variable(tf.random_normal(shape=[5, 5, 1, 32], mean=0.0, stddev=1.0))
# b = tf.Variable(tf.constant(0.0, shape=[32]))
# w参数设置: 过滤器大小(5x5)*图片通道数*过滤器数量
w = weight_variables([5, 5, 1, 32])
b = bias_variables([32])
# 卷积
# 改变x的形状 会生成一个新的张量
reshaped_x = tf.reshape(x, [-1, 28, 28, 1])
conv_ret = tf.nn.conv2d(reshaped_x, filters=w, strides=[1, 1, 1, 1], padding='SAME') + b
# 激活
x_relu1 = tf.nn.relu(conv_ret)
# 池化
x_pooling1 = tf.nn.max_pool(x_relu1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding="SAME")
with tf.variable_scope("second_layer_conv"):
# 可以定义第二层卷积
# 随机初始化权重w和偏置b
# w_conv2 = tf.Variable(tf.random_normal(shape=[5, 5, 32, 64], mean=0.0, stddev=1.0))
# b_conv2 = tf.Variable(tf.constant(0.0, shape=[64]))
# 32:上层池化结果
w_conv2 = weight_variables([5, 5, 32, 64])
b_conv2 = bias_variables([64])
# 卷积
conv2_ret = tf.nn.conv2d(x_pooling1, w_conv2, strides=[1, 1, 1, 1], padding='SAME') + b_conv2
# 激活
x_relu2 = tf.nn.relu(conv2_ret)
# 池化
x_pooling2 = tf.nn.max_pool(x_relu2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding="SAME") + b_conv2
# 全连接层
with tf.variable_scope('fc'):
# 随机初始化一个权重w和偏置b
# w_fc = tf.Variable(tf.random_normal(shape=[7*7*64, 10], mean=0.0, stddev=1.0))
# b_fc = tf.Variable(tf.constant(0.0, shape=[10]))
b_fc = bias_variables([10])
w_fc = weight_variables([7 * 7 * 64, 10])
# 进行回归运算
reshaped_x_fc = tf.reshape(x_pooling2, [-1, 7 * 7 * 64])
y_predict = tf.matmul(reshaped_x_fc, w_fc) + b_fc
# with tf.Session() as sess:
# sess.run(print(x_pooling1))
return x, y_true, y_predict
def train_conv():
# 加载数据
mnist = input_data.read_data_sets(train_dir="./data/MNIST_data", one_hot=True)
# 定义模型 得到输出
x, y_true, y_predict = conv_fc()
# 3、求出所有样本的损失,然后求平均值
with tf.variable_scope("soft_cross"):
# 求平均交叉熵损失
# tf.reduce_mean()
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y_true, logits=y_predict))
# 4、梯度下降求出损失
with tf.variable_scope("optimizer"):
train_op = tf.train.GradientDescentOptimizer(0.0001).minimize(loss)
# 5、计算准确率
with tf.variable_scope("acc"):
equal_list = tf.equal(tf.argmax(y_true, 1), tf.argmax(y_predict, 1))
# equal_list None个样本 [1, 0, 1, 0, 1, 1,..........]
accuracy = tf.reduce_mean(tf.cast(equal_list, tf.float32))
# 初始化变量
init_var = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init_var)
for i in range(1000):
# 取出真实的特征值和目标值进行训练
mnist_x, mnist_y = mnist.train.next_batch(50)
sess.run(train_op, feed_dict={x: mnist_x, y_true: mnist_y})
if i % 100 == 0:
print("训练第%d步,准确率为:%f" % (i, sess.run(accuracy, feed_dict={x: mnist_x, y_true: mnist_y})))
if __name__ == '__main__':
train_conv()
tensorflow 2.x
# encoding=utf-8
import numpy as np
import tensorflow as tf
from tensorflow.keras.datasets import mnist
from matplotlib import pyplot as plt
(train_imgs, train_labels), (test_imgs, test_labels) = mnist.load_data(
path=r'datasets\mnist.npz')
#
# 构建网络
resnet = tf.keras.applications.resnet50.ResNet50(include_top=False, input_shape=(224, 224, 3))
for layers in resnet.layers:
layers.trainable = False
net = tf.keras.Sequential()
net.add(resnet)
net.add(tf.keras.layers.Flatten())
net.add(tf.keras.layers.Dense(units=10, activation='softmax'))
# 定义损失函数 优化器
optimizer = tf.keras.optimizers.Adam(learning_rate=0.01)
# 定义训练函数
@tf.function
def train_step(x, y):
with tf.GradientTape() as gt:
predictions = net(x, y)
# 计算损失
# loss = tf.keras.losses.categorical_crossentropy(y, predictions)
loss = tf.reduce_mean(tf.keras.losses.sparse_categorical_crossentropy(y, predictions, from_logits=True))
# 反向传播
grads = gt.gradient(loss, net.trainable_variables)
# 更新参数
optimizer.apply_gradients(zip(grads, net.trainable_variables))
return loss
# 模型训练
def data_loader(batch_size, imgs, labels):
# 设置网络要求维度 b h w c
batch_imgs = np.reshape(imgs,
(imgs.shape[0], imgs.shape[1], imgs.shape[2], 1))
# 单通道转三通道
batch_imgs = np.concatenate((batch_imgs, batch_imgs, batch_imgs), axis=-1)
def _batch_generator(batch_size, imgs, labels):
for batch in range(0, imgs.shape[0], batch_size):
# 设置网络要求输入尺寸
index = np.random.randint(0, np.shape(imgs)[0], batch + batch_size)
resized_imgs = tf.image.resize_with_pad(batch_imgs[index], 224, 224)
# 标签值
batch_labels = np.array([labels[index]])
yield (resized_imgs.numpy(), batch_labels)
return _batch_generator(batch_size, imgs, labels), imgs.shape[0]
# def train(train_data):
# train_running_loss = 0.0
# train_running_accuracy = 0.0
# for train_tensor, train_label in train_data:
# optimizer.zero_grad()
# train_predict = net(train_tensor)
# train_loss = criterion(train_predict, train_label)
# train_running_loss += train_loss.item()
# # 反向传播
# train_loss.backward()
# # 更新参数
# optimizer.step()
# # 计算准去率
# train_running_accuracy += (train_predict.argmax(1) == train_label).sum().item()
#
# return train_running_loss, train_running_accuracy
if __name__ == '__main__':
batch_size = 16
# batch_train_data = data_loader(batch_size, train_imgs, train_labels)
# batch_test_data = data_loader(batch_size, test_imgs, test_labels)
# 画图看看img和label是否对应
# img = next(batch_train_data)
# plt.figure(figsize=(10, 10))
# for i in range(16):
# plt.subplot(4, 4, i+1)
# plt.xticks([])
# plt.yticks([])
# plt.grid(False)
# plt.imshow(img[0][i], cmap=plt.cm.binary)
# plt.xlabel(img[1][i])
# plt.show()
# 模型训练
# criterion = tf.keras.losses.categorical_crossentropy
epochs = 1
for epoch in range(epochs):
batch_train_data, len_imgs = data_loader(batch_size, train_imgs, train_labels)
for img in batch_train_data:
loss = train_step(img[0], img[1])
print(f'epoch: {epoch} | train_avg_loss: {loss}')