深度学习中的卷积和池化

  |     |  
阅读次数:

1. Convolution

卷积是什么?

卷积在数学上用通俗的话来说就是输入矩阵与卷积核(卷积核也是矩阵)进行对应元素相乘并求和,所以一次卷积的结果的输出是一个数,最后对整个输入输入矩阵进行遍历,最终得到一个结果矩阵,下面通过一个动画使其更直观。

  • 卷积动画演示
    • 卷积核
      卷积动画演示

卷积动画演示

卷积动画演示

在上面我们没有使用很专业的数学公式来表示,来解释卷积操作和相关操作,我结合我自己的理解,争取做到白话,及时没有数学基础,也能理解卷积核池化操作。

  • 卷积的目的

卷积在图像中的目的就是为了提取特征,我认为这就是深度学习的核心,因为有了卷积层,才避免了我们来手动提取图像的特征,让卷积层自动提取图像的高维度且有效的特征,虽然这没有手动提取特征比如Canny边缘,SIFT,HOG等的强大数学理论基础的支撑,但是卷积层提取的特征让最终的分类、识别结果往往非常的好。比如LeNet-5模型能在MNIST数据集上达到99%的识别率,一般来说网络结构越复杂,越深,往往最终的精确率会越高。


卷积分为许多种,下面将会一一介绍。

  • 符号约定

i: 输入大小表示为i x i

k: 卷积核大小表示为k x k

s: 步长

p: 填充

o: 输出表示为o*o

1.1 unit strides

卷积从大体上可以分为单位步长(unit strides)和非单位步长(Non-unit strides),还可以细分为有0填充和无0填充。

1.1.1 No zero padding, unit strides

figure2.1

无零填充 单位步长的卷积,蓝色矩阵是输入(4x4),深蓝色是卷积核(3x3),上方绿色是输出(2x2).输出矩阵大小的计算公式为:
figure2.1

动画演示
figure2.1

1.1.2 Zero padding, unit strides

figure2.2

有零填充(p=2) 单位步长的卷积,蓝色矩阵是输入(5x5),深蓝色是卷积核(3x3),上方绿色是输出(6x6).输出矩阵大小的计算公式为:
figure2.2

动画演示
figure2.2

1.1.2.1 Zero padding, unit strides - Half(Same) padding

这种情况叫Half Padding 也叫 Same Padding,因为它能保证输入和输出的尺寸是一致的
figure2.3

有零填充(p=1) 单位步长的卷积,蓝色矩阵是输入(5x5),深蓝色是卷积核(3x3),上方绿色是输出(5x5).输出矩阵大小的计算公式为:
figure2.3

动画演示
figure2.3

1.1.2.2 Zero padding, unit strides - Full padding

卷积操作产生的输出一般都会减少输入图片的尺寸,但有时候我们需要放大输入图片的尺寸,这个时候就需要使用到Full Padding。
figure2.4

有零填充(p=2) 单位步长的卷积,蓝色矩阵是输入(5x5),深蓝色是卷积核(3x3),上方绿色是输出(7x7).输出矩阵大小的计算公式为:
figure2.4

动画演示
figure2.4

1.2 Non-unit strides

接下来介绍非单位步长(Non-unit stride)的卷积操作,分为有零填充和无零填充。

1.2.1 No zero padding, non-unit strides

figure2.5

无零填充 非单位步长(s=2)的卷积,蓝色矩阵是输入(5x5),深蓝色是卷积核(3x3),上方绿色是输出(2x2).输出矩阵大小的计算公式为:

figure2.5

其中向下取整是为了避免(i-k)/s是小数的情况。

动画演示
figure2.5

1.2.2 Zero padding, non-unit strides

figure2.6

有零填充(p=1) 非单位步长(s=2)的卷积,蓝色矩阵是输入(5x5),深蓝色是卷积核(3x3),上方绿色是输出(3x3).输出矩阵大小的计算公式为:
figure2.6

其中向下取整是为了避免(i+2p-k)/s是小数的情况。

动画演示
figure2.6

1.3 Convolution as a matrix operation

卷积操作也可以被表示为矩阵的形式,比如将1.1.1中的图转化为矩阵,如下图所示:

1.1.1中的图被表示为如下形式

figure2.6

矩阵表示的形式

figure2.6

我将上面的矩阵划分为了4行,每一行划分为了4列,表示此卷积操作需要进行16次,W0,0 W0,1 …… W2,2我在图中标注了出来。这个矩阵可以这样来看,按行来看,第一行对应于矩阵表示图的第一个图,第二行对应于矩阵表示图的第二个图,一次类推。

2. Pooling

池化操作是什么?

池化操作的过程和卷积很类似,但是卷积是用来提取特征的,池化层是用来减少卷积层提取的特征的个数的,可以理解为是为了增加特征的鲁棒性或者是降维。

池化操作是什么?

池化操作的过程和卷积很类似,但是卷积是用来提取特征的,池化层是用来减少卷积层提取的特征的个数的,可以理解为是为了增加特征的鲁棒性或者是降维。

池化分为平均值池化和最大值池化,下面会一一介绍。

2.1 Average Pooling

  • 平均值池化可以被表示为

figure1.5

  • 平均值池化的动画演示

figure1.6

可以看到池化操作也有一个类似于卷积的核,但是这个核不需要提供值,只是表示一个能作用于输入图片的窗口大小。

2.2 Max Pooling

  • 最大值池化可以被表示为

figure1.6

  • 最大值池化的动画演示

figure1.6

可以看到池化操作也有一个类似于卷积的核,但是这个核不需要提供值,只是表示一个能作用于输入图片的窗口大小。

3. 3D-Conv

3维的卷积,我个人的简单理解,就是在2维卷积的基础上加了一个深度的概念,如图。

figure1.6

输入是一个32x32x3的矩阵,卷积核假定是5x5x3,可以看到一次的卷积操作的结果就是一个带有深度的单位矩阵(2维的一次卷积操作的结果是深度为1的单位矩阵)。这里的深度可以自己指定。

为了更好的理解3维的卷积,这里引用斯坦福写的一篇博客里面的动画。http://cs231n.github.io/convolutional-networks/

<iframe
width=”100%”
height=”100%”
src=”http://cs231n.github.io/assets/conv-demo/index.html"
点击查看动画http://cs231n.github.io/assets/conv-demo/index.html

width=”750”
height=”720”
src=”https://cs231n.github.io/assets/conv-demo/index.html"
frameborder=”0”
allowfullscreen>

4. LeNet-5

这里介绍下LeNet-5模型,为了理解前面讲述的各种模型

Bibliography

这里介绍下LeNet-5模型,为了理解前面讲述的各种卷积和2种池化,下面具体介绍LeNet-5的每个层。

  • C1:
    • input: 32x32x1
    • conv: 5x5x1
    • padding: No Zero
    • strides: 1
    • output: 28x28x6
    • parmas: 5x5x1x6+6 = 156

第一层,卷积层

这一层的输入就是原始的图像像素32321。第一个卷积层过滤器尺寸为55,深度为6,不使用全0填充,步长为1。所以这一层的输出:28286,卷积层共有5516+6=156个参数。

  • S2:
    • input: 28x28x6
    • pool: 2x2
    • padding: No Zero
    • strides: 2
    • output: 14x14x6

第二层,池化层

这一层的输入为第一层的输出,是一个28286的节点矩阵。本层采用的过滤器大小为22,长和宽的步长均为2,所以本层的输出矩阵大小为1414*6。

  • C3:
    • input: 14x14x6
    • conv: 5x5x16
    • padding: No Zero
    • strides: 1
    • output: 10x10x16
    • params: 5x5x6x16+16=2416

第三层,卷积层

本层的输入矩阵大小为14146,使用的过滤器大小为55,深度为16.本层不使用全0填充,步长为1。本层的输出矩阵大小为101016。本层有55616+16=2416个参数。

  • S4:
    • input: 10x10x16
    • pool: 2x2
    • padding: No Zero
    • strides: 2
    • output: 5x5x16

第四层,池化层

本层的输入矩阵大小101016。本层采用的过滤器大小为22,长和宽的步长均为2,所以本层的输出矩阵大小为55*16。

  • C5:
    • input: 5x5x16
    • conv: 5x5
    • padding: No Zero
    • strides: 1
    • output: 120
    • params: 5x5x16x120 + 120=48120

第五层,全连接层(卷积层)

本层的输入矩阵大小为5516,在LeNet-5论文中将这一层成为卷积层,但是因为过滤器的大小就是55,所以和全连接层没有区别。如果将5516矩阵中的节点拉成一个向量,那么这一层和全连接层就一样了。本层的输出节点个数为120,总共有5516120+120=48120个参数。

  • F6:
    • input: 120
    • output: 84
    • params: 120x84+84 = 10164

第六层,全连接层

本层的输入节点个数为120个,输出节点个数为84个,总共参数为120*84+84=10164个。

  • Output:
    • input: 84
    • output: 10
    • parmas: 84x10 + 10 = 850

第七层,全连接层

本层的输入节点个数为84个,输出节点个数为10个,总共参数为84*10+10=850

Code

  • lenet_train.py

训练代码

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2018/2/21 16:08
# @update  : 2018年4月19日21:38:34
# @Author  : Jasontang
# @Site    : 
# @File    : lenet_train.py
# @ToDo    : 使用LeNet-5模型。定义了神经网络的训练过程

import os

import tensorflow as tf
import numpy as np
from tensorflow.examples.tutorials.mnist import input_data

import neural_network_learning.cnn.lenet.mnist_inference as mnist_inference

# 配置神经网络的参数
BATCH_SIZE = 100
# LEARNING_REATE_BASE = 0.8  # 0.8的学习率导致准确率不高。明显看出不收敛,准确率跟瞎猜差不多。
LEARNING_REATE_BASE = 0.01  # 降低学习率
LEARNING_RATE_DECAY = 0.99
REGULARAZTION_RATE = 0.0001
TRAING_STEPS = 30000
MOVING_AVERAGE_DECAY = 0.99
# 模型保存的路径和文件名
MODEL_SAVE_PATH = "./model/"
MODEL_NAME = "model.ckpt"


def train(mnist):
# 定义输入输出placeholder
x = tf.placeholder(tf.float32,
                   [BATCH_SIZE,
                    mnist_inference.IMAGE_SIZE,
                    mnist_inference.IMAGE_SIZE,
                    mnist_inference.NUM_CHANNELS], name="input-x")
y_ = tf.placeholder(tf.float32, [None, mnist_inference.OUTPUT_NODE], name="input-y")

regularizer = tf.contrib.layers.l2_regularizer(REGULARAZTION_RATE)
y = mnist_inference.inference(x, True, regularizer)
global_step = tf.Variable(0, trainable=False)

variable_averages = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY, global_step)
variables_average_op = variable_averages.apply(tf.trainable_variables())
cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=tf.argmax(y_, 1), logits=y)
cross_entropy_mean = tf.reduce_mean(cross_entropy)

# loss = tf.get_collection("losses") 返回一个列表
# loss_add = tf.add_n(loss) 将列表元素进行相加
loss = cross_entropy_mean + tf.add_n(tf.get_collection("losses"))

learing_rate = tf.train.exponential_decay(LEARNING_REATE_BASE,
                                          global_step,
                                          mnist.train.num_examples / BATCH_SIZE,
                                          LEARNING_RATE_DECAY)
train_step = tf.train.GradientDescentOptimizer(learing_rate).minimize(loss, global_step)

with tf.control_dependencies([train_step, variables_average_op]):
    train_op = tf.no_op(name="train")

# 初始化持久化类
saver = tf.train.Saver()
with tf.device("/gpu:0"):
    session_conf = tf.ConfigProto(allow_soft_placement=True)
    with tf.Session(config=session_conf) as sess:
        tf.global_variables_initializer().run()

        for i in range(TRAING_STEPS):
            xs, ys = mnist.train.next_batch(BATCH_SIZE)
            reshaped_xs = np.reshape(xs, [BATCH_SIZE,
                                          mnist_inference.IMAGE_SIZE,
                                          mnist_inference.IMAGE_SIZE,
                                          mnist_inference.NUM_CHANNELS])
            # print(type(xs))
            # print(type(reshaped_xs))
            _, loss_value, step = sess.run([train_op, loss, global_step], feed_dict={x: reshaped_xs, y_: ys})

            if i % 1000 == 0:
                print("After %d training step(s), loss on training batch is %g." % (step, loss_value))

                saver.save(sess, os.path.join(MODEL_SAVE_PATH, MODEL_NAME), global_step=global_step)


def main(argv=None):
    # 存放目录为当前工程目录下的MNIST_data目录
    mnist = input_data.read_data_sets("./MNIST_data", one_hot=True)
    train(mnist)


if __name__ == '__main__':
    tf.app.run()
  • lenet_inference

计算代码

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2018/2/20 19:43
# @update  : 2018年4月19日21:38:34
# @Author  : Jasontang
# @Site    : 
# @File    : lent_inference.py
# @ToDo    : 使用LeNet-5模型。定义了前向传播的过程及神经网络的参数


import tensorflow as tf

# 定义神经网络结构相关的参数
INPUT_NODE = 784
OUTPUT_NODE = 10

IMAGE_SIZE = 28
NUM_CHANNELS = 1
NUM_LABELS = 10

# 第一层卷积层的尺寸和深度
CONV1_DEEP = 32
CONV1_SIZE = 5
# 第二层卷积层的尺寸和深度
CONV2_DEEP = 64
CONV2_SIZE = 5
# 全连接层的结点个数
FC_SIZE = 512

# 卷积神经网络的前向传播过程
# 添加一个新的参数train,用于区分训练过程和测试过程。
# 在这个程序中将用到dropout方法,dropout可以进一步提升模型可靠性并防止
# 过拟合。dropout过程只在训练时使用。
def inference(input_tensor, train, regularizer, dropout=0.5):
    # 声明第一层神经网络的变量并完成前向传播过程
    # 和标准的LeNet-5模型不大一样,这里定义的卷积层输入为28*28*1的原始MNIST图片像素。
    # 因为卷积层使用了全0填充,所以输出为28*28*32的矩阵。
    with tf.variable_scope("layer1-conv1"):
        conv1_weights = tf.get_variable("weights", [CONV1_SIZE, CONV1_SIZE, NUM_CHANNELS, CONV1_DEEP],
                                        initializer=tf.truncated_normal_initializer(stddev=0.1))
        conv1_biases = tf.get_variable("biases", [CONV1_DEEP], initializer=tf.constant_initializer(0.1))
        # 使用变长为5,深度为32的过滤器,过滤器移动的步长为1,且使用全0填充。
        conv1 = tf.nn.conv2d(input_tensor, conv1_weights, strides=[1, 1, 1, 1], padding="SAME")
        relu1 = tf.nn.relu(tf.nn.bias_add(conv1, conv1_biases))

    # 实现第二层池化层的前向传播过程,这里选择最大池化层,池化层过滤器的变长为2
    # 使用全0填充且移动的步长为2,这一层的输入是上一层的输出,也就是28*28*32的矩阵。
    # 输出为14*14*32的矩阵
    with tf.variable_scope("layer2-pool1"):
        pool1 = tf.nn.max_pool(relu1,
                               ksize=[1, 2, 2, 1],
                               strides=[1, 2, 2, 1],
                               padding="SAME")

    # 声明第三层卷积层的变量并实现前向传播过程,这一层的输入为14*14*32的矩阵。
    # 输出为14*14*64的矩阵。
    with tf.variable_scope("layer3-conv2"):
        conv2_weights = tf.get_variable("weight",
                                        [CONV2_SIZE, CONV2_SIZE, CONV1_DEEP, CONV2_DEEP],
                                        initializer=tf.truncated_normal_initializer(stddev=0.1))
        conv2_biases = tf.get_variable("bias", [CONV2_DEEP], initializer=tf.constant_initializer(0.1))

        # 使用边长为5,深度为64的过滤器,过滤器移动的步长为1,且使用全0填充。
        conv2 = tf.nn.conv2d(pool1, conv2_weights, strides=[1, 1, 1, 1], padding="SAME")
        relu2 = tf.nn.relu(tf.nn.bias_add(conv2, conv2_biases))

    # 实现第四层池化层的前向传播过程。这一层和第二层的记过是一样的,这一层的输入为14*14*64的矩阵,
    # 输出位7*7*64的矩阵。
    with tf.name_scope("layer4-pool2"):
        pool2 = tf.nn.max_pool(relu2,
                               ksize=[1, 2, 2, 1],
                               strides=[1, 2, 2, 1],
                               padding="SAME")

    # 将第四层池化层的输出转化为第五层全连接层的输入格式。
    # 第四层的输出为7*7*64的矩阵,然后第五层全连接层需要的输入格式为向量
    # 所以在这里需要将这个7*7*64的矩阵拉直成一个向量。pool2.get_shape函数可以得到
    # 第四层输出矩阵的维度而不需要手工计算。注意因为每一层神经网络的输入输出都为一个batch的矩阵,
    # 所以这里得到的维度也包含了一个batch的数据的个数。
    pool_shape = pool2.get_shape().as_list()

    # 计算将矩阵拉直成向量之后的长度,这个长度就是矩阵长宽及深度的乘积。
    # 这里pool_shape[0]为一个batch中数据的个数
    nodes = pool_shape[1] * pool_shape[2] * pool_shape[3]

    # 通过tf.reshape函数将第四层的输出变成一个batch的向量
    reshaped = tf.reshape(pool2, [pool_shape[0], nodes])

    # 声明第五层全连接层的变量并实现前向传播过程,这一层的输入是拉直之后的一组向量,
    # 向量长度为3136,输出是一组长度为512的向量。这一层和之前在重构MNIST数据集的代码基本一致,
    # 唯一的区别就是引入了dropout的概念。dropout在训练时会随机将部分结点的输出改为0。
    # dropout可以避免过拟合问题,从而使得模型在测试数据上的效果更好。
    # dropout一般只在全连接层而不是卷积层或者池化层使用。
    with tf.variable_scope("layer5-fc1"):
        fc1_weights = tf.get_variable("weight",
                                      [nodes, FC_SIZE],
                                      initializer=tf.truncated_normal_initializer(stddev=0.1))
        # 只要全连接层的权重需要加入正则化
        if regularizer is not None:
            # 当使用正则化生成函数时,当前变量的正则化损失加入名字为losses的集合.
            tf.add_to_collection("losses", regularizer(fc1_weights))
        fc1_biases = tf.get_variable("bias",
                                     [FC_SIZE],
                                     initializer=tf.constant_initializer(0.1))
        fc1 = tf.nn.relu(tf.matmul(reshaped, fc1_weights) + fc1_biases)
        if train:
            fc1 = tf.nn.dropout(fc1, dropout)

    # 声明第六层全连接层的变量并实现前向传播过程。
    # 这一层的输入为一组长度为512的向量,输出为一组长度为10的向量。
    # 这一层的输出通过Softmax之后就得到了最后的分类结果。
    with tf.variable_scope("layer6-fc2"):
        fc2_weights = tf.get_variable("weight",
                                      [FC_SIZE, NUM_LABELS],
                                      initializer=tf.truncated_normal_initializer(stddev=0.1))
        if regularizer is not None:
            # 当使用正则化生成函数时,当前变量的正则化损失加入名字为losses的集合.
            tf.add_to_collection("losses", regularizer(fc2_weights))
        fc2_biases = tf.get_variable("bias",
                                     [NUM_LABELS],
                                     initializer=tf.constant_initializer(0.1))
        logit = tf.matmul(fc1, fc2_weights) + fc2_biases

    # 返回第六层的输出
    return logit
  • lenet_eval.py

测试代码

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2018年4月19日21:38:34
# @Author  : Jasontang
# @Site    : 
# @File    : mnist_eval.py
# @ToDo    : 测试过程


import time
import tensorflow as tf
import numpy as np
from tensorflow.examples.tutorials.mnist import input_data

import neural_network_learning.cnn.lenet.mnist_inference as mnist_inference
import neural_network_learning.cnn.lenet.mnist_train as mnist_train

# 每10s加载一次最新模型,并在测试数据上测试最新模型的正确率
EVAL_INTERVAL_SECS = 10


def evaluate(mnist):
    x = tf.placeholder(tf.float32,
                       [mnist.validation.images.shape[0],
                        mnist_inference.IMAGE_SIZE,
                        mnist_inference.IMAGE_SIZE,
                        mnist_inference.NUM_CHANNELS], name="input-x")
    y_ = tf.placeholder(tf.float32, [None, mnist_inference.OUTPUT_NODE], name="input-y")

    y = mnist_inference.inference(x, False, None)

    correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

    variable_averages = tf.train.ExponentialMovingAverage(mnist_train.MOVING_AVERAGE_DECAY)
    variables_to_restore = variable_averages.variables_to_restore()
    saver = tf.train.Saver(variables_to_restore)

    # 每隔EVAL_INTERVAL_SECS秒调用一次计算正确率的过程以检测训练过程中正确率的变化
    stop_count = 0
    while True:
        with tf.Session() as sess:
            # tf.train.get_checkpoint_state函数会通过checkpoint文件自动找刀目录中最新模型的文件名
            ckpt = tf.train.get_checkpoint_state(mnist_train.MODEL_SAVE_PATH)
            # 停止条件 #
            stop_count += 1
            if stop_count > 5:
                return
            # 停止条件 #
            if ckpt and ckpt.model_checkpoint_path:
                # 加载模型
                saver.restore(sess, ckpt.model_checkpoint_path)
                # 通过文件名得到模型保存时迭代的轮数
                # 输出./model/model.ckpt-29001
                print(ckpt.model_checkpoint_path)
                global_step = ckpt.model_checkpoint_path.split("/")[-1].split("-")[-1]
                validate_feed = {x: mnist.validation.images,
                                 y_: mnist.validation.labels}

                # print(validate_feed[x])
                reshaped_x = np.reshape(validate_feed[x],
                                        [validate_feed[x].shape[0],
                                         mnist_inference.IMAGE_SIZE,
                                         mnist_inference.IMAGE_SIZE,
                                         mnist_inference.NUM_CHANNELS])
                validate_feed[x] = reshaped_x
                accuracy_score = sess.run(accuracy, feed_dict=validate_feed)
                print("After %s training step(s), validation accuracy is %g" % (global_step, accuracy_score))
            else:
                print("No checkpoint file found")
                return
        time.sleep(EVAL_INTERVAL_SECS)


def main(argv=None):
    # 存放目录为当前工程目录下的MNIST_data目录
    mnist = input_data.read_data_sets("./MNIST_data", one_hot=True)
    evaluate(mnist)


if __name__ == '__main__':
    tf.app.run()

MNIST数据集

AI Live

测试结果

AI Live

最终正确率可以达到99%

Bibliography

  1. A guide to convolution arithmetic for deep learninghttps://arxiv.org/pdf/1603.07285.pdf
  2. TensforFlow 实战Google深度学习框架

我参与举办了一个小团体,主要是技术分享,这篇是第三期的分享内容。下面是我们的公众号:

AI Live

Join Us

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器