1.卷积神经网络介绍
**卷积神经网络(Convolutional Neural Network,CNN)**是一种前馈神经网络,它的人工神经元可以响应一部分覆盖范围内的周围单元,对于大型图像处理有出色表现。
虽然上图中显示的全连接神经网络结构和卷积神经网络的结构直观上差异比较大,但他们的整体架构是非常相似的。从上图可以看出,卷积神经网络也是通过一层一层的节点组织起来的。和全连接神经网络一样,卷积神经网络中的每一个节点都是一个神经元。在全链接神经网络中,每相邻两层之间的节点都全部相连,于是一般会将每一层全连接层中的节点组织成一列,这样方便显示连接结构。而对于卷积神经网络,相邻两层之间只有部分相连接,为了展示每一层的神经元的维度,一般会将每一层卷积层的节点组织成一个三维矩阵。
除了结构相似,卷积神经网络的输入和输出以及训练流程与全连接神经网络也基本一致。以图象为例,卷积神经网络的输入层就是图像的原始像素,而输出层的每一个节点代表了不同类别的可信度。这和全链接神经网络的输入输出是一致的。卷积神经网络和全连接神经网络的的唯一区别就在于神经网络中相邻两层的连接方式。
上图给出了一个更加具体的卷积神经网络架构图。
在神经网络前几层中,每一层的节点都被组织成一个三维矩阵,从图中可以看出卷积神经网络前几层中每一个节点之和上一层中部分节点相连。一个卷积神经网络主要由以下5种结构组成。
- 输入层。输入层是整个神经网络的输入,在处理图像的卷积神经网络中,它一般代表了一张图片的像素矩阵。
- 卷积层。从名字就可以看出,卷积层是一个卷积神经网络中最重要的部分。和传统的全连接层不容,卷积层中每一个介蒂安的输入只是上一层神经网络的一小块。这个小块常用的大小为33或者5。卷积层试图将神经网络中每一个小块进行更加深入的分析从而得到抽象程度更高的特征。一般来说,通过卷积层处理过的节点矩阵会变得更深。
- 池化层(Pooling)。池化层神经网络不会改变三维矩阵的深度,但是它可以缩小矩阵的大小。池化操作可以认为使将一张分辨率较高的图片转换为一张分辨率较低的图片。通过池化层,可以进一步缩小最后全连接层结点的个数,从而达到减少整个神经网络中参数的目的。
- 全连接层。在经过多轮卷积层和池化层的处理之后,在卷积神经网络的最后一般是由一到两个全连接层来给出最后的分类结果。经过几轮卷积层和池化层的处理之后,可以认为图像中的信息已经被抽象成了信息含量更高的特征。我们可以将卷积层和池化层看成自动图像特征提取的过程。在特质提取完成之后,仍然需要使用全连接层来完成分类任务。
- Softmax层。Softmax主要用于分类问题。通过Softmax层,可以得到当前样例属于不同种类的概率分布情况。
2. 过滤器
(1)分解图像
CNN的第一步是将图像分解成较小的碎片。我们通过选择定义过滤器的宽度和高度来做到这一点。
然后,我们可以简单地水平或垂直滑动此滤镜以聚焦在另一幅图像上。
过滤器滑动的量称为“步幅”。跨度是我们可以调整的超参数。通过减少每一层观察到的总面片数量,增加步幅可减小模型的大小。但是,这通常会降低准确性。
让我们看一个例子。在这张放大的狗的图像中,我们首先以红色概述的补丁开始。滤镜的宽度和高度定义了该正方形的大小。
然后,我们将正方形向右移动给定的步幅(在本例中为2)以获得另一个图片。
(2)过滤器深度
通常有多个过滤器。不同的过滤器选择不同质量的部分。例如,一个过滤器可能查找特定的颜色,而另一个可能查找特定形状的对象。卷积层中过滤器的数量称为 过滤器深度 。
过滤器尺寸是指的过滤器的输入节点矩阵的大小,深度指的hi输出单位节点矩阵的深度。
假设使用
w
x
,
y
,
z
i
w_{x,y,z}^i
wx,y,zi来表示对于输出单位节点矩阵中的第
i
i
i个节点,过滤器输入节点
(
x
,
y
,
z
)
( x,y,z)
(x,y,z) 的权重,使用
b
i
b^i
bi表示第
i
i
i个输出节点对应的偏置项参数,那么单位矩阵中的第
i
i
i个节点的取值
g
(
i
)
g(i)
g(i)为:
g
(
i
)
=
f
(
∑
x
=
1
2
∑
y
=
1
2
∑
z
=
1
3
a
x
,
y
,
z
×
w
x
,
y
,
z
i
+
b
i
)
g(i)=f(\sum_{x=1}^{2}\sum_{y=1}^{2}\sum_{z=1}^{3}a_{x,y,z}\times w_{x,y,z}^{i}+b^i)
g(i)=f(x=1∑2y=1∑2z=1∑3ax,y,z×wx,y,zi+bi)
其中 a x , y , z a_{x,y,z} ax,y,z为过滤器中节点 ( x , y , z ) (x,y,z) (x,y,z)的取值, f f f为激活函数。下图展示了在给定 a , w 0 a, w^0 a,w0和 b 0 b^0 b0的情况下,使用ReLU 作为激活函数时 g ( 0 ) g(0) g(0)的计算过程。在图6-9 的左侧给出了 a , w 0 a, w^0 a,w0的取值,这里通过3 个二维矩阵来表示一个三维矩阵的取值,其中每一个二维矩阵表示三维矩阵在某一个深度上的取值。下图中 ⋅ · ⋅ 符号表示点积,也就是矩阵中对应元素乘积的和。图6-9 的右侧显示了 g ( 0 ) g(0 ) g(0)的计算过程。如果给出 w 1 w^1 w1到 w 4 w^4 w4 和 b 1 b^1 b1 到 b 4 b^4 b4 , 那么也可以类似地计算出 g ( 1 ) g(1) g(1)到 g ( 4 ) g(4) g(4)的取值。如果将 a a a 和 w i w^i wi组织成两个向量,那么一个过滤器的计算过程完全可以通过向量乘法来完成。
当过滤器的大小不为l × l 时, 卷积层前向传播得到的矩阵的尺寸要小于当前层矩阵的尺寸。如下图所示, 当前层矩阵的大小为3 × 3 (左侧矩阵〉,而通过卷积层前向传播算法之后,得到的矩阵大小为2 × 2 (右侧矩阵〉。为了避免尺寸的变化,可以在当前层矩阵的边界上加入全0填充(zero -padding)。这样可以使得卷积层前向传播结果矩阵的大小和当前层矩阵保持一致。
以下公式给出了同时使用全0填充的结果矩阵的大小。
o
u
t
l
e
n
g
t
h
=
[
i
n
l
e
n
g
t
h
/
s
r
t
i
d
e
l
e
n
g
t
h
]
out_{length}=[in_{length}/srtide_{length}]
outlength=[inlength/srtidelength]
o
u
t
w
i
d
t
h
=
[
i
n
w
i
d
t
h
/
s
r
t
i
d
e
w
i
d
t
h
]
out_{width}=[in_{width}/srtide_{width}]
outwidth=[inwidth/srtidewidth]
如果不使用全0填充,以下公式给出了结果矩阵的大小。
o
u
t
l
e
n
g
t
h
=
[
(
i
n
l
e
n
g
t
h
−
f
i
l
t
e
r
l
e
n
g
t
h
+
1
)
/
s
r
t
i
d
e
l
e
n
g
t
h
]
out_{length}=[(in_{length}-filter_{length}+1)/srtide_{length}]
outlength=[(inlength−filterlength+1)/srtidelength]
o
u
t
w
i
d
t
h
=
[
(
i
n
w
i
d
t
h
−
f
i
l
t
e
r
w
i
d
t
h
+
1
)
/
s
r
t
i
d
e
w
i
d
t
h
]
out_{width}=[(in_{width}-filter_{width}+1)/srtide_{width}]
outwidth=[(inwidth−filterwidth+1)/srtidewidth]
TensorFlow 对卷积神经网络提供了非常好的支持,以下程序实现了一个卷积层的前向传播过程。从以下代码可以看出,通过TensorFlow 实现卷积层是非常方便的。
filter_weight = tf.get_variable ('weights',[5, 5, 3, 16], initializer=tf .truncated_normal_initializer(stddev=0.1))
biases = tf.get_variable('biases', [16], initializer=tf.constant_initializer ( 0 .1) )
conv = tf.nn.conv2d(input , filter_weight, strides=[1, 1, 1, 1], padding ='SAME')
bias= tf.nn.bias add(conv, biases)
actived_conv = tf.nn.relu(bias)
3.卷积层
TensorFlow提供tf.nn.conv2d()
和tf.nn.bias_add()
功能来创建卷积层。
# 输出深度
k_output = 64
# 图像属性
image_width = 10
image_height = 10
color_channels = 3
# 卷积过滤器
filter_size_width = 5
filter_size_height = 5
# 输入图片
input = tf.placeholder(
tf.float32,
shape=[None, image_height, image_width, color_channels])
# 权重和偏置
weight = tf.Variable(tf.truncated_normal([filter_size_height, filter_size_width, color_channels, k_output]))
bias = tf.Variable(tf.zeros(k_output))
# 应用卷积
conv_layer = tf.nn.conv2d(input, weight, strides=[1, 2, 2, 1], padding='SAME')
# 添加偏置
conv_layer = tf.nn.bias_add(conv_layer, bias)
# 应用激活函数
conv_layer = tf.nn.relu(conv_layer)
上面的代码使用tf.nn.conv2d()
函数来计算以权重为过滤器的卷积,并使用[1,2,2,1]来计算步幅。TensorFlow对每个输入维度使用一个stride
, [batch, input_height, input_width, input_channels]
。我们通常总是将batch
和input_channels
(即strides
组中的第一个和第四个元素)的步长设置为1。
更改input_height
和input_width
,同时将batch
和input_channels
设置为1。input_height
和input_width
strides用于跨越input
的过滤器。此示例代码在输入上使用了带有5x5过滤器的2步。
该tf.nn.bias_add()
函数在矩阵的最后一个维度上添加一维偏置。
例如,卷积第三层输出:
4.池化层
在卷积层之间往往会加上一个池化层( pooling layer )。池化层可以非常有效地缩小矩阵的尺寸 ,从而减少最后全连接层中的参数。使用池化层既可以加快计算速度也有防止过拟合问题的作用。
池化层前向传播的过程也是通过移动一个类似过滤器的结构完成的。不过池化层过滤器中的计算不是节点的加权和,而是采用更加简单的最大值或者平均值运算。使用最大值操作的池化层被称之为最大池化层( max pooling ), 这是被使用得最多的池化层结构。使用平均值操作的池化层被称之为平均池化层(average pooling )。
与卷积层的过滤器类似,池化层的过滤器也需要人工设定过滤器的尺寸、是否使用全0填充以及过滤器移动的步长等设置,而且这些设置的意义也是一样的。卷积层和池化层中过滤器移动的方式是相似的,唯一的区别在于卷积层使用的过滤器是横跨整个深度的,而池化层使用的过滤器只影响一个深度上的节点。所以池化层的过滤器除了在长和宽两个维度移动,它还需要在深度这个维度移动。
不同颜色或者不同线段(虚线或者实线)代表了不同的池化层过滤器。从上图中可以轩出,池化层的过滤器除了在长和宽的维度上移动,它还需要在深度的维
度上移动。以下TensorFlow 程序实现了最大池化层的前向传播算法。
# tf.nn.max pool 实现了最大池化层的前向传播过程,它的参数和tf.nn.conv2d 函数类似。
# ksize 提供了过滤器的尺寸, strides 提供了步长信息, padding 提供了是否使用全0 填充。
pool= tf.nn.max pool(actived_conv, ksize=[1, 3 , 3, 1], strides=[l , 2, 2, 1), padding='SAME')
对比池化层和卷积层前向传播在TensorFlow 中的实现,可以发现函数的参数形式是相似的。在tf.nn.max_pool
函数中, 首先需要传入当前层的节点矩阵,这个矩阵是一个四维矩阵,格式和tf.nn.conv2d
函数中的第一个参数一致。第二个参数为过滤器的尺寸。虽然给出的是一个长度为4 的一维数组,但是这个数组的第一个和最后一个数必须为1 。这意味着池化层的过滤器是不可以跨不同输入样例或者节点矩阵深度的。在实际应用中使用得最多的池化层过滤器尺寸为
[
1
,
2
,
2
,
1
]
[ 1, 2, 2 ,1]
[1,2,2,1]或者
[
1
,
3
,
3
,
1
]
[ 1, 3, 3, 1]
[1,3,3,1]。
tf.nn.max pool
函数的第三个参数为步长,它和tf.nn.conv2d
函数中步长的意义是一样的,而且第一维和最后一维也只能为1。这意味着在Tensor Flow 中,池化层不能减少节点矩阵的深度或者输入样例的个数。tf.nn.max_pool
函数的最后一个参数指定了是否使用全0填充。这个参数也凡有两种取值一VALID 或者SAME ,其中VALID 表示不使用全0 填充,SAME 表示使用全0 填充。TensorFlow 还提供了tf.nn.avg_pool
来实现平均池化层。tf.nn.avg_pool
函数的调用格式和tf.nn.max_pool
函数是一致的。
最近,池层已不再受欢迎。原因如下:
- 最近的数据集如此庞大和复杂,我们更加担心拟合不足。
- dropout是一个更好的正则化器。
- 合并会导致信息丢失。以最大池化操作为例。我们只保留n个数字中的最大值,从而完全忽略n-1个数字。
5.程序
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets(".", one_hot=True, reshape=False)
import tensorflow as tf
# gpu参数
gpu_options = tf.GPUOptions(per_process_gpu_memory_fraction=0.333)
# 参数
learning_rate = 0.00001
epochs = 10
batch_size = 128
# 计算验证和准确性样本的数量
# 如果计算精度的内存不足,请减少此值
test_valid_size = 256
# 网络参数
n_classes = 10 # MNIST的全部类别 (0-9 digits)
dropout = 0.75 # Dropout, 保持单位的概率
# 存储层的权重和偏置
weights = {
'wc1': tf.Variable(tf.random_normal([5, 5, 1, 32])),
'wc2': tf.Variable(tf.random_normal([5, 5, 32, 64])),
'wd1': tf.Variable(tf.random_normal([7*7*64, 1024])),
'out': tf.Variable(tf.random_normal([1024, n_classes]))}
biases = {
'bc1': tf.Variable(tf.random_normal([32])),
'bc2': tf.Variable(tf.random_normal([64])),
'bd1': tf.Variable(tf.random_normal([1024])),
'out': tf.Variable(tf.random_normal([n_classes]))}
def conv2d(x, W, b, strides=1):
x = tf.nn.conv2d(x, W, strides=[1, strides, strides, 1], padding='SAME')
x = tf.nn.bias_add(x, b)
return tf.nn.relu(x)
def maxpool2d(x, k=2):
return tf.nn.max_pool(x, ksize=[1, k, k, 1], strides=[1, k, k, 1], padding='SAME')
def conv_net(x, weights, biases, dropout):
# Layer 1 - 28*28*1 to 14*14*32
conv1 = conv2d(x, weights['wc1'], biases['bc1'])
conv1 = maxpool2d(conv1, k=2)
# Layer 2 - 14*14*32 to 7*7*64
conv2 = conv2d(conv1, weights['wc2'], biases['bc2'])
conv2 = maxpool2d(conv2, k=2)
# Fully connected layer - 7*7*64 to 1024
fc1 = tf.reshape(conv2, [-1, weights['wd1'].get_shape().as_list()[0]])
fc1 = tf.add(tf.matmul(fc1, weights['wd1']), biases['bd1'])
fc1 = tf.nn.relu(fc1)
fc1 = tf.nn.dropout(fc1, dropout)
# Output Layer - class prediction - 1024 to 10
out = tf.add(tf.matmul(fc1, weights['out']), biases['out'])
return out
# tf 计算图输入
x = tf.placeholder(tf.float32, [None, 28, 28, 1])
y = tf.placeholder(tf.float32, [None, n_classes])
keep_prob = tf.placeholder(tf.float32)
# 模型
logits = conv_net(x, weights, biases, keep_prob)
# 定义损失和优化
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=y))
optimizer = tf.train.GradientDescentOptimizer(learning_rate=learning_rate).minimize(cost)
# 准确率
correct_pred = tf.equal(tf.argmax(logits, 1), tf.argmax(y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))
# 初始化变量
init = tf.global_variables_initializer()
# 启动计算图
with tf.Session(config=tf.ConfigProto(gpu_options=gpu_options)) as sess:
sess.run(init)
for epoch in range(epochs):
for batch in range(mnist.train.num_examples//batch_size):
batch_x, batch_y = mnist.train.next_batch(batch_size)
sess.run(optimizer, feed_dict={x: batch_x, y: batch_y, keep_prob: dropout})
# 计算一个batch的损失和准确性
loss = sess.run(cost, feed_dict={x: batch_x, y: batch_y, keep_prob: 1.})
valid_acc = sess.run(accuracy, feed_dict={
x: mnist.validation.images[:test_valid_size],
y: mnist.validation.labels[:test_valid_size],
keep_prob: 1.})
print('Epoch {:>2}, Batch {:>3} - Loss: {:>10.4f} Validation Accuracy: {:.6f}'.format(
epoch + 1,
batch + 1,
loss,
valid_acc))
# 计算测试精度
test_acc = sess.run(accuracy, feed_dict={
x: mnist.test.images[:test_valid_size],
y: mnist.test.labels[:test_valid_size],
keep_prob: 1.})
print('Testing Accuracy: {}'.format(test_acc))
输出如下:
Epoch 1, Batch 1 - Loss: 42201.4062 Validation Accuracy: 0.113281
Epoch 1, Batch 2 - Loss: 38468.5820 Validation Accuracy: 0.125000
...
Epoch 10, Batch 428 - Loss: 177.6433 Validation Accuracy: 0.835938
Epoch 10, Batch 429 - Loss: 194.3704 Validation Accuracy: 0.835938
Testing Accuracy: 0.84765625
其中:
# gpu参数
gpu_options = tf.GPUOptions(per_process_gpu_memory_fraction=0.333)
...
with tf.Session(config=tf.ConfigProto(gpu_options=gpu_options)) as sess:
...
这两行是因为我使用了gpu训练模型才加上的,如果不使用gpu可以不加。
在下个笔记将介绍Fashion-MNIST,将使用卷积神经网络对十种不同得服饰进行区分。