tensorflow-综合学习系列实例之卷积神经网络

2022-11-19,,,,

在前面的文章学习过了神经网络,并且也可以做一些相关应用,但是神经网络的缺点,使很多场景都不能很好的发挥,所以我们需要再学习一种更好的网络,它就是cnn,在这之前我们还说先简单介绍一下相关网络结构,首先看一下传统的神经网络结构;

一般包含如下层次:

  • 输入层(Input layer),众多神经元(Neuron)接受大量非线形输入讯息。输入的讯息称为输入向量。
  • 输出层(Output layer),讯息在神经元链接中传输、分析、权衡,形成输出结果。输出的讯息称为输出向量。
  • 隐藏层(Hidden layer),简称“隐层”,是输入层和输出层之间众多神经元和链接组成的各个层面。如果有多个隐藏层,则意味着多个**函数。

从图中可以得知,如果当前输入的数据维度很高的话,那么对应的输入神经元和隐藏神经元的个数是不敢想象的,而且这种计算量对cpu和内存来说都是很难实现的,所以基于此一种更好的网络就诞生了,它就是cnn....

OK 那么我看CNN的网络结构,比如识别当前的图片是不是一辆车子,我们看看cnn的结构

卷积神经网络的层级结构
      • 数据输入层/ Input layer
  • 卷积计算层/ CONV layer
  • ReLU激励层 / ReLU layer
  • 池化层 / Pooling layer

  • 全连接层 / FC layer

1.数据输入层
该层要做的处理主要是对原始图像数据进行预处理,其中包括:
  • 去均值:把输入数据各个维度都中心化为0,如下图所示,其目的就是把样本的中心拉回到坐标系原点上。
  • 归一化:幅度归一化到同样的范围,如下所示,即减少各维度数据取值范围的差异而带来的干扰,比如,我们有两个维度的特征A和B,A范围是0到10,而B范围是0到10000,如果直接使用这两个特征是有问题的,好的做法就是归一化,即A和B的数据都变为0到1的范围。

  • PCA/白化:用PCA降维;白化是对数据各个特征轴上的幅度归一化

2.卷积计算层
这一层就是卷积神经网络最重要的一个层次,也是“卷积神经网络”的名字来源。
在这个卷积层,有两个关键操作:
  • 局部关联。每个神经元看做一个滤波器(filter)

  • 窗口(receptive field)滑动, filter对局部数据计算

3.激励层
把卷积层输出结果做非线性映射。

CNN采用的激励函数一般为ReLU(The Rectified Linear Unit/修正线性单元),它的特点是收敛快,求梯度简单,但较脆弱,图像如下。

 

激励层的实践经验:
  ①
不要用sigmoid!不要用sigmoid!不要用sigmoid!
  ② 首先试RELU,因为快,但要小心点
  ③ 如果2失效,请用Leaky ReLU或者Maxout

  ④ 某些情况下tanh倒是有不错的结果,但是很少

4.池化层
池化层夹在连续的卷积层中间, 用于压缩数据和参数的量,
减小过拟合。
简而言之,如果输入是图像的话,那么池化层的最主要作用就是压缩图像

 

这里再展开叙述池化层的具体作用。

1. 特征不变性,也就是我们在图像处理中经常提到的特征的尺度不变性,池化操作就是图像的resize,平时一张狗的图像被缩小了一倍我们还能认出这是一张狗的照片,这说明这张图像中仍保留着狗最重要的特征,我们一看就能判断图像中画的是一只狗,图像压缩时去掉的信息只是一些无关紧要的信息,而留下的信息则是具有尺度不变性的特征,是最能表达图像的特征。

2. 特征降维,我们知道一幅图像含有的信息是很大的,特征也很多,但是有些信息对于我们做图像任务时没有太多用途或者有重复,我们可以把这类冗余信息去除,把最重要的特征抽取出来,这也是池化操作的一大作用。

3. 在一定程度上防止过拟合,更方便优化。

5.全连接层

两层之间所有神经元都有权重连接,通常全连接层在卷积神经网络尾部。也就是跟传统的神经网络神经元的连接

核心知识点讲解:

& 卷积计算

对图像(不同的数据窗口数据)和滤波矩阵(一组固定的权重:因为每个神经元的权重固定,所以又可以看做一个恒定的滤波器filter)做内积(逐个元素相乘再求和)的操作就是所谓的『卷积』操作,也是卷积神经网络的名字来源。
    比如下图中,图中左边部分是原始输入数据,图中中间部分是滤波器filter,图中右边是输出的新的二维数据。

中间滤波器filter与数据窗口做内积,其具体计算过程则是:4*0 + 0*0 + 0*0 + 0*0 + 0*1 + 0*1 + 0*0 + 0*1 + -4*2 = -8

接下来看一个动态卷积计算的过程:


卷积计算完了,就需要进行结果的选择了,一般有两种方式就是最大值和平均值实际中一般都是使用最大值,这也是池化层中使用的,那什么是最大值呢,很简单就是选择结果中的最大的一个元素,比如,如果一个卷积计算结果如下所示,


& 参数共享机制

计算同一个深度切片的神经元时采用的滤波器是共享的。例如图4中计算o[:,:,0]的每个每个神经元的滤波器均相同,都为W0,这样可以很大程度上减少参数。共享权重在一定程度上讲是有意义的,例如图片的底层边缘特征与特征在图中的具体位置无关。但是在一些场景中是无意的,比如输入的图片是人脸,眼睛和头发位于不同的位置,希望在不同的位置学到不同的特征 (参考斯坦福大学公开课)。请注意权重只是对于同一深度切片的神经元是共享的,在卷积层,通常采用多组卷积核提取不同特征,即对应不同深度切片的特征,不同深度切片的神经元权重是不共享。另外,偏重对同一深度切片的所有神经元都是共享的。

通过介绍卷积计算过程及其特性,可以看出卷积是线性操作,并具有平移不变性(shift-invariant),平移不变性即在图像每个位置执行相同的操作。卷积层的局部连接和权重共享使得需要学习的参数大大减小,这样也有利于训练较大卷积神经网络。

& 局部连接

每个神经元仅与输入神经元的一块区域连接,这块局部区域称作感受野(receptive field)。在图像卷积操作中,即神经元在空间维度(spatial dimension,即上图示例H和W所在的平面)是局部连接,但在深度上是全部连接。对于二维图像本身而言,也是局部像素关联较强。这种局部连接保证了学习后的过滤器能够对于局部的输入特征有最强的响应。局部连接的思想,也是受启发于生物学里面的视觉系统结构,视觉皮层的神经元就是局部接受信息的。

cnn的优点和缺点:

优点
  • 共享卷积核,对高维数据处理无压力
  • 无需手动选取特征,训练好权重,即得特征分类效果好
缺点
  • 需要调参,需要大样本量,训练***要GPU
  • 物理含义不明确

到此和cnn相关的基础知识就讲完了,接下来我们就是tf实现一个cnn来进行minist的训练,看看精度是不是要比传统的神经网络要高?

我们看看需要那几步?

1 创建权重 weight

2 创建偏值 bias

3 创建卷积层 conv2d

4 创建池化层 maxpooling

5 创建**函数

6 创建全连接层

7 计算损失函数 loss

8 计算梯度更新 gradient

9 计算精度 accuracy

10 打印loss

接下来的代码基本上是按照这个步骤来编写的:

# this is use python script!
# -*- coding: UTF-8 -*-
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data

# 远程下载手写数据到本地目录
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)

# 创建权重
def weight_variable(shape):
    initial = tf.truncated_normal(shape, stddev=0.1)
    return tf.Variable(initial)

# 创建偏置
def bias_variable(shape):
    initial = tf.constant(0.1, shape=shape)
    return tf.Variable(initial)


# 创建卷积层
def conv2d(x, W):
    # x --图像输入
    # w --权重
    # strides --代表步长信息(开头和结尾必须都是1,倒数第二位是卷积的间隔)
    # padding --填充方式(same:代笔输入和输出是一样的大小,VALID:代表输入和输出不完全一致)
    # same 在数据不够的情况下 会自动填充 默认用0补充
    # valid 靠最右和最下的不够一个卷积覆盖的丢掉
    return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')

# 创建池化层
def max_pool_2x2(x):
    # ksize --卷积核大小(滤波器 2*2)
    return tf.nn.max_pool(x, ksize=[1,2,2,1], strides=[1,2,2,1], padding='SAME')

# 定义输入(784=28*28,None 代表可以输入任何)
xs = tf.placeholder(tf.float32, [None, 784]) # 28x28
# 定义输出(10--0 1 2 3....9)
ys = tf.placeholder(tf.float32, [None, 10])

# 定义dropout(防止过拟合)
keep_prob = tf.placeholder(tf.float32)

# 把图片转换为28*28的像素(-1 代表可以取任何值) --图片拉平
x_image = tf.reshape(xs, [-1, 28, 28, 1])

# 创建第一个卷积层
W_conv1 = weight_variable([5,5, 1,32]) # patch 5x5, in size 1, out size 32
b_conv1 = bias_variable([32])

# 激励函数
h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1) # output size 28x28x32
# 创建池化层
h_pool1 = max_pool_2x2(h_conv1)  # output size 14x14x32

# 创建第二个卷积层
W_conv2 = weight_variable([5,5, 32, 64]) # patch 5x5, in size 32, out size 64
b_conv2 = bias_variable([64])
# 激励函数
h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2) # output size 14x14x64
# 创建第二个池化层
h_pool2 = max_pool_2x2(h_conv2) # output size 7x7x64

# 创建一个全连接层
# 这里为什么是7*7? 因为一开始是28*28 经过两次池化每次都缩小为原来的1/2
W_fc1 = weight_variable([7*7*64, 1024])
b_fc1 = bias_variable([1024])

h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])
# 创建一个全连接层
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)

# 对全连接层进行dropout
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)

## 创建一个输出层
W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])
# sofxmax回归分类-每个输入是属于每个输出的概率大小
prediction = tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)

# 计算期望值和实际值之间的误差(交熵函数)
cross_entropy = tf.reduce_mean(-tf.reduce_sum(ys * tf.log(prediction),reduction_indices=[1]))# loss
# 指定算法优化器(神经网络使用的梯度下降,卷积神经网络使用的是adam算法)
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)

# 计算模型准确度(模型评估)
def compute_accuracy(v_xs, v_ys):
    global prediction

    y_pre = sess.run(prediction, feed_dict={xs: v_xs, keep_prob: 1})

    # tf.argmax(input, axis=None, name=None, dimension=None)
    # 此函数是对矩阵按行或列计算最大值
    # 参数 input:输入Tensor
    # axis:0 表示按列,1 表示按行
    # name:名称  dimension:和axis功能一样,默认axis取值优先。新加的字段
    # tf.equal 来检测我们的预测是否真实标签匹配
    correct_prediction = tf.equal(tf.argmax(y_pre,1), tf.argmax(v_ys,1))
    # 求平均精确度
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
    result = sess.run(accuracy, feed_dict={xs: v_xs, ys: v_ys, keep_prob: 1})
    return result

# 创建图会话 并且**变量 这个一定要放在所有变量使用之后 如果放在之前的话 会出现无法**变量的
sess = tf.Session()
inital_variables=tf.global_variables_initializer()
sess.run(inital_variables)

#训练算法 10000-97%,20000-99.2%
for i in range(10000):
    # 输入数据和输出的标签
    batch_xs, batch_ys = mnist.train.next_batch(100)
    sess.run(train_step, feed_dict={xs: batch_xs, ys: batch_ys, keep_prob: 0.5})
    if i % 100 == 0:
        print(compute_accuracy(
            mnist.test.images, mnist.test.labels))

ok,代码就写完了,刚刚上面还少了一个很重要的参数没有说到就是,dropout,在实际使用中一定要配上这个参数,它的作用就是为了避免计算所有的神经元,它的值是百分比,比如如果dropout=0.5,那就代表有一半的神经元不参与计算,那么这个计算量肯定少很多,当然

这个值不是随便去的,一般0.1到0.3之间,它的核心作用可以减少一定的过拟合风险

好,我们看一下最终的程序运行结果部分展示:



看还少很不错的,几乎都达到了99的精度了

最后总结,上面介绍的只是最简单的cnn,在实际应用场景中,我们会使用变种的cnn,比如深度cnn(dnn),当然我们真正开发的时候并不需要从头到尾来编写代码,要学会利用一些已有的网络,比如alexnet,vggnet,google net,rnsnet等,其本质就是要学会迁移学习,这个很重要,复用别人已经训练好的模型的参数,然后在这个基础之上进行参数调优或者二次开发