内容提要

  1. MNIST数据集简介
  2. 神经元常用函数
  3. 深度神经网络
  4. 卷积神经网络介绍
  5. 循环神经网络

一、MNIST数据集简介

数据集(Dataset)是一类数据的集合。传统的数据集通常表现为表格或者文档形式,每个数值被称为数据资料。

MNIST数据集是一个含有手写数字的大型数据集,包含0~9共10个数字,通常用于训练图像处理系统。

MNIST数据集包含60000个训练图像和10000个测试图像。

MNIST数据集共有4个文件,分别是训练集数据、训练集标签以及测试集数据、测试集标签。MNIST数据集的图像以字节的形式进行存储,每幅图像都为单通道图像,由28×28个像素点构成。

MNIST数据集的测试集样本图像如图所示:

不同分类器使用MNIST数据集的错误率如表所示:

二、神经元常用函数

2.1 激活函数

1、sigmoid()函数

在之前的一段时间内,sigmoid()函数是非常常用的,因为它对神经元的激活有很好的解释,且它本身为单调连续,非常适合作为输出层。

缺陷:

第一,当输入稍微远离了坐标原点,函数的梯度就变得很小了,几乎为0,这会导致在反向传播过程中,梯度很小的时候接近0,神经网络无法更新参数,这个问题称为梯度饱和,也可以称为梯度弥散。

第二,sigmoid()函数的输出不是零中心的,这会影响梯度下降的运作。

2、tanh()函数

tanh()函数的数学公式为

,tanh()函数图像如图所示。

tanh()函数的定义域是实数域 ,值域是(-1,1)。其输入如果是很大的负数,其值就会无限接近于-1。

输入如果是很大的正数,其值就会无限接近于1。

和sigmoid()函数一样,tanh()也存在梯度饱和问题,但是tanh()的输出是零中心的,所以实际使用更多一些。

3、ReLU函数

ReLU函数的数学公式为

,ReLU函数图像如图所示。

ReLU函数的定义域是实数 ,值域是[0, +∞] 。这个激活函数可以理解为一个关于0的阈值。

2.2 池化函数

池化是用一个矩阵窗口在张量上进行扫描,通过取最大值或者平均值等方式来减少参数数量的方法。最大值池化和平均值池化的过程如图所示。

池化层主要对输入的图像进行降采样(Subsample),池化并不改变深度或维度,只改变大小。池化函数有平均值池化函数和最大值池化函数等。

1、平均值池化函数

TensorFlow函数:tf.nn.avg_pool(value,ksize,strides,padding,name=None),该函数计算的是池化区域的平均值。

value:需要池化的输入,一般池化层接在卷积层后面,这是一个四维的张量,4个维度为[batch,height,width,channels]。

ksize:池化窗口的大小,是一个四维向量,一般是[1,height,width,1]。

strides:和卷积类似,是窗口在每一个维度上滑动的步长,一般是[1,stride,stride,1]。

padding:和卷积类似,可以取“VALID”或“SAME”。

name:该池化的名称。

2、最大值池化函数

TensorFlow函数:tf.nn.max_pool(value,ksize,strides,padding,name=None),该函数计算的是池化区域的最大值。

在池化时一般选择最大值池化。

2.3 损失函数

1、hinge损失函数

hinge损失函数源自支持向量机,在支持向量机中,最终的目的是最大化分类间隔,减少错误分类的样本数目。

损失函数公式为

,其中,

, 是一个超参数,设置为1.0时,在绝大多数情况下都是安全的。

2、square损失函数

损失函数公式为

,大多用在线性回归中。

3、log损失函数

在使用似然函数最大化时,其形式是进行连乘,但是为了便于处理,一般会加上log,这样便可以将连乘转换为求和。

由于log函数是单调递增函数,因此不会改变优化结果。log类型的损失函数也是一种常见的损失函数。

4、交叉熵

如何判断输出的结果与期望结果有多接近呢?

交叉熵(Cross Entropy)是常用的方法之一。

交叉熵的公式为

, 是分类问题的真实分布概率, 是分类问题的预测分布概率,也就是说,交叉熵的输入是概率,范围是[0,1]。

交叉熵得到的值越小,真实分布概率和预测分布概率越接近,预测的结果就越真实。

由于网络的输出是任意的,如ReLU函数的值域为 ,所以在进行交叉熵计算之前还需要将输出结果转换为概率分布。

softmax()函数就是一个常用的方法,它的公式为

,该函数所有输出值的和为1,输入的负数会变成正数,然后外面嵌套 函数,就可以得到优化结果。

三、深度神经网络

深度神经网络(Deep Neural Networks,DNN)可以理解为有很多隐藏层的神经网络。

具有一层隐藏层的神经网络如图所示。

神经网络某个节点的计算公式是

,n为网络的第n层,

是权重矩阵,

是第一层的偏置矩阵,f()是激活函数,它会作用到每个元素。经过这些激活函数的转换后,由于每个节点不再是线性变换的,所以整个神经网络就成为非线性的了。

反向传播过程:如果有表达式

,现在需要求出

,先将这个复合函数的表达式分解为

,根据链式求导法则可知

,所以需要分别求出

,可以求得

,故得到

。前向传播与反向传播过程如图所示。

四、卷积神经网络介绍

卷积神经网络(Convolutional Neural Networks,CNN)是一类深度神经网络,最常用于分析视觉图像。

一个简单的卷积神经网络是由各种层按一定顺序排列的。

卷积神经网络主要由卷积层(Convolutional Layers)、池化层(Pooling Layers)、全连接层(Fully Connected Layers,FC Layers)构成。将这些层按一定的顺序排列,就可以搭建一个卷积神经网络。

4.1 LeNet-5模型及其实现

LeNet-5模型是由杨立昆(YannLeCun)教授于1998年在论文Gradient-Based Learning Applied to Document Recognition中提出的,是一种用于手写体字符识别的非常高效的卷积神经网络

网络实现过程如图。

卷积层是一个将卷积运算和加法运算组合在一起的隐藏层,在图像识别里提到的卷积是二维卷积,即离散二维滤波器(也称作卷积核)与二维图像做卷积操作。

简单地讲是二维滤波器滑动到二维图像上的所有位置,并在每个位置上与该像素点及其领域像素点做内积,卷积运算过程如图所示:

1、LeNet-5实现的过程

(1) 输入层

(2)卷积层(第一层)

(3)池化层(第二层)

(4)卷积层(第三层)

(5)池化层(第四层)

(6)全连接层(第五层)

(7)全连接层(第六层)

(8)全连接层,输出层(第七层)

2、欠拟合

欠拟合(Under Fitting)是由于特征维度过少,模型过于简单,使神经网络没办法完全满足数据集的特征提取要求,体现在训练以及预测时表现不佳、成功率低。欠拟合解决方法如下。

(1)将模型复杂化。

(2)增加更多特征,使输入数据的特征更明显。

(3)调整超参数。

(4)减弱正则化约束或者去掉正则化约束。

3、过拟合

过拟合是指模型在训练集上表现很好,但在验证和测试阶段效果比较差,即模型的泛化能力很差。过拟合的解决方法如下。

(1)增加训练数据量。

(2)减少数据特征,去掉数据中非共性的特征。

(3)调整超参数。

(4)使用正则化约束或者增强正则化约束。

(5)降低模型的复杂度。

(6)使用Dropout。

(7)Early Stopping,即提前结束训练。

4、卷积神经网络的TensorFlow实现

通过TensorFlow框架实现一个类似于LeNet-5的神经网络,来解决MNIST数据集上的数字识别问题。

本网络与原LeNet-5的区别是:卷积核的个数不同;激活函数不同,

此处用的是ReLU,输出层选择softmax()函数,二者的整体过程是一致的。

案例1:在TensorFlow目录下新建文件,命名为LeNet-5.py,利用TensorFlow解决类似于LeNet-5在MNIST数据集上进行数字识别的问题,在PyCharm中编写以下代码。

# -×- coding: utf-8 -×-
# 载入MINIST数据需要的库
from tensorflow.examples.tutorials.mnist import input_data
# 保存模型需要的库
from tensorflow.python.framework.graph_util import convert_variables_to_constants
from tensorflow.python.framework import graph_util
# 导入其他库
import tensorflow as tf
import time
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
# 获取MINIST数据
mnist = input_data.read_data_sets("./MNIST_data", one_hot=True)
# 占位符
# x是特征值,也就是像素
# 使用一个28×28=784列的数据来表示一个图像的构成
# 每一个点都是这个图像的一个特征
# 因为每一个点都会对图像的外观和表达的含义有影响,只是影响的大小不同而已
x = tf.placeholder("float", shape=[None, 784], name="Mul")  # 输入28×28=784
# y_是真实数据[0,0,0,0,1,0,0,0,0],为4
y_ = tf.placeholder("float", shape=[None, 10], name="y_")  # 输出
# 变量 784×10的矩阵
# W表示每一个特征值(像素点)影响结果的权重
# 这个值很重要,因为深度学习的过程就是发现特征
# 经过一系列训练,得出每一个特征值对结果影响的权重
W = tf.Variable(tf.zeros([784, 10]), name='x')
b = tf.Variable(tf.zeros([10]), 'y_') 
# 权重
def weight_variable(shape):
      # 生成的值服从具有指定平均值和标准偏差的正态分布
    # 如果生成的值大于平均值的两个标准偏差的值,则丢弃重新选择
      initial = tf.truncated_normal(shape, stddev=0.1)  # 标准差为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指需要做卷积的输入图像,要求它是一个Tensor
      # 具有[batch, in_height, in_width, in_channels]这样的shape
      # 具体含义是[训练时一个batch的图像数量, 图像高度, 图像宽度, 图像通道数] 
      # 注意这是一个4维的Tensor,batch和in_channels在卷积层中通常设为1
      # 参数W相当于CNN中的卷积核,要求它是一个Tensor
      # 具有[filter_height, filter_width, in_channels, out_channels]这样的shape
     # 具体含义是[卷积核的高度,卷积核的宽度,图像通道数,卷积核个数]
      # 注意,第三维in_channels就是参数x的第四维
      return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='VALID')
      # 参数strides:卷积时在图像每一维的步长,这是一个一维的向量,长度为4
      # 参数padding:string类型的量,只能是“VALID”,不补零
# 最大池化
def max_pool_2x2(x):
      # x:input
          # ksize:filter,滤波器大小为2×2
      # strides:步长,2×2,表示filter窗口每次水平移动两格,每次垂直移动两格
      # padding:填充方式,补零
      return tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
# 第一层卷积
# 权重+偏置+激活+池化
# patch为5×5;in_size为1,即图像的厚度,如果是彩色的,则为3;32个卷积核(滤波器)
W_conv1 = weight_variable([5, 5, 1, 32])
b_conv1 = bias_variable([32])
# 对数据进行重新排列,形成图像
x_image = tf.reshape(x, [-1, 28, 28, 1])
# print("x",x)
# print("x_image",x_image)
# ReLU操作,输出大小为28×28×32
h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
# Pooling操作,输出大小为14×14×32
h_pool1 = max_pool_2x2(h_conv1)
# 第二层卷积
# 权重+偏置+激活+池化
# patch为5×5;in_size为32,即图像的厚度;out_size是64,即输出的大小
W_conv2 = weight_variable([5, 5, 32, 64])
b_conv2 = bias_variable([64])
# ReLU操作,输出大小为14×14×64
h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
# Pooling操作,输出大小为7×7×64
h_pool2 = max_pool_2x2(h_conv2)
# 全连接一
W_fc1 = weight_variable([7 × 7 × 64, 1024])
b_fc1 = bias_variable([1024])
# 全连接二
W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])
# 输入数据变换
# 变换为m×n,列n为7×7×64
h_pool2_flat = tf.reshape(h_pool2, [-1, 7 × 7 × 64])
# 进行全连接操作
# tf.nn.relu()函数可将大于0的数保持不变,将小于0的数置为0
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
# Dropout可防止过拟合,它一般用在全连接层,训练用,测试不用
# Dropout就是在不同的训练过程中随机扔掉一部分神经元
# Dropout可以让某个神经元的激活值以一定的概率p停止工作
# 参数keep_prob:设置神经元被选中的概率,在初始化时keep_prob是一个占位符
# TensorFlow在运行时设置keep_prob具体的值,如keep_prob: 0.5
keep_prob = tf.placeholder("float", name='rob')
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)
# 用于训练的softmax()函数将所有数据归一化到0~1之间,大的数据特征更明显
y_conv = tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2, name='res')
# 训练完成后,进行测试用的softmax()函数
y_conv2 = tf.nn.softmax(tf.matmul(h_fc1, W_fc2) + b_fc2, name="final_result")
# 交叉熵的计算,返回包含了损失值/误差的Tensor
# 熵是衡量事物混乱程度的一个值
cross_entropy = -tf.reduce_sum(y_ × tf.log(y_conv))
# 优化器,负责最小化交叉熵
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
# tf.argmax():取出该数组最大值的下角标
correct_prediction = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_, 1))
# 计算准确率
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
# 创建会话
with tf.Session() as sess:
      time_begin = time.time()
      # 初始化所有变量
      sess.run(tf.global_variables_initializer())
      # print(sess.run(W_conv1))
      # 保存输入/输出,可以在之后用
      tf.add_to_collection('res', y_conv)
      tf.add_to_collection('output', y_conv2)
      tf.add_to_collection('x', x)
    ### 训练开始 ###
      for i in range(10000):
             # 取出MNIST数据集中的50个数据
             batch = mnist.train.next_batch(50)
             # run()可以看作输入相关值到函数中的占位符,然后计算出结果
             # 这里将batch[0]给x,将batch[1]给y_
             # 执行训练过程并传入真实数据
             train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})
             if i % 100 == 0:
                   train_accuracy = accuracy.eval(feed_dict={x: batch[0], y_: batch[1], keep_prob: 1.0})
                   print("step %d, training accuracy %g" % (i, train_accuracy))
      time_elapsed = time.time() - time_begin
      print("训练所用时间:%d秒" % time_elapsed)
      # 用saver 保存模型
      saver = tf.train.Saver()
      saver.save(sess, "model_data/model")

执行该程序,如果计算机安装的是CPU版本的TensorFlow,建议将训练步数调少,否则会训练非常久。

训练完成后的准确率如图所示。

生成的模型在TensorFlow目录下的model_data目录中,模型文件如图所示。

提示:

LeNet-5模型的结构比较清晰,即输入层→卷积层(池化层)→全连接层→输出层,但是有些网络中是没有池化层的。

4.2 AlexNet介绍

AlexNet模型是2012年大规模视觉识别挑战赛中的冠军模型,AlexNet将LeNet的思想发扬光大,把CNN的基本原理应用到了更深、更宽的网络结构中。

另外,AlexNet模型首次在CNN中采取了ReLU激活函数、Dropout防止过拟合、GPU加速训练等技术,16层的VGGNet网络结构如图所示。

4.3 Inception模型及其实现

1、Inception模型介绍

Inception模型是通过串联+并联的方式将卷积层连接起来的。

Inception模型是对输入图像并行地执行多个卷积运算或池化操作,并将所有输出结果拼接为一个非常深的特征图,且不同大小卷积核的卷积运算可以得到图像中的不同信息,处理获取到的图像中的不同信息可以得到更好的图像特征。

Inception模型的主要成员包括:Inception v1、Inception v2、Inception v3、Inception v4和Inception-ResNet。

这里给出了Inception模块的一个单元结构示意图。

由于一个Inception模块要进行多次运算,所以需要耗费大量的计算资源。为了实现降维,降低运算成本,在3×3、5×5的卷积运算前,在最大值池化的运算后,加入1×1的卷积核。

如下为降维的Inception模块的一个单元结构示意图。

2、GoogLeNet实现的过程

这里以Inception模块构建的GoogLeNet(Inception v1)为例,描述GoogLeNet的实现过程。

(1)输入层:原始输入图像大小为224×224×3,为三通道RGB图像。

(2)卷积层:接收224×224×3的矩阵数据,与64个大小为7×7的卷积核(步长为2,padding为“SAME”)做运算,输出为112×112×64,经过3×3的最大值池化(步长为2),输出矩阵为56×56×64。

(3)卷积层:接收56×56×64的矩阵数据,与192个大小为3×3的卷积核(步长为1,padding为“SAME”)做运算,输出为56×56×192,经过3×3的最大值池化(步长为2),输出矩阵为28×28×192。

(4)Inception 3a层:接收28×28×192的矩阵数据,共4个分支,采用不同尺度的卷积核运算,4个分支步长都为1。

(5)之后的层:以后的层数都以此类推,GoogLeNet模型有9个堆叠的Inception模块,共有22层(如果包括池化层,则是27层)。

五、循环神经网络RNN

循环神经网络(Recurrent Neural Network, RNN)是一类以序列(sequence)数据为输入,在序列的演进方向进行递归(recursion)且所有节点(循环单元)按链式连接的递归神经网络(recursive neural network)。

时间序列数据是序列数据中最常见的一种。时间序列数据(Time Series Data)是在不同时间上收集到的数据,用于描述现象随时间变化的情况。这类数据反映了某一事物、现象等随时间的变化状态或程度。

在全连接的神经网络以及卷积神经网络中有输入层、隐藏层、输出层,层与层之间通过学习到的权重进行连接,在同一层中,节点与节点之间是不连接的。

1、RNN基本结构

从RNN基本结构可以看出,RNN的输入和输出是等长的。图1中有4个序列,但实际上序列数是不定的,假设有不定个序列,可以组成RNN不展开表达样式,如图2所示,其中A为某特殊序列。

2、RNN结构分析

RNN常用的结构有3种,分别是Vector-to-Sequence结构、Sequence-to-Vector结构、Encoder-Decoder结构。

(1)Vector-to-Sequence结构

假设一个问题的输入是单独的值,输出是一个序列,则可以建立Vector-to-Sequence结构模型,将输入放到某一个序列进行计算(如图1所示),也可以将输入放到全部序列中进行计算(如图2所示)。

(2)Sequence-to-Vector结构

假设一个问题的输入是一个序列,输出是一个单独的值,一般会在最后一个序列上进行输出变换,可以建立Sequence-to-Vector结构,如图5所示。

(3)Encoder-Decoder结构

有时也会将Encoder-Decoder结构称为Sequence-to-Sequence结构,该结构的具体过程就是编码及解码。

首先将输入的数据编码成一个上下文向量c,这个过程称为Encoder,得到c后,用另一个RNN网络进行解码,这个过程称为Decoder。c作为新的RNN的h0时,结构如图5-21所示;c作为新的输入时,结构如图所示。

3、长短期记忆网络结构

RNN可以做到在时间序列上记忆,但是对于时间序列上较远的点,记起来比较困难,因为两个节点距离较远时,会涉及多次的雅可比矩阵相乘,导致梯度消失或者梯度膨胀,长短期记忆网络(Long Short-Term Memory,LSTM)结构可以很好地解决这个问题。

在标准的RNN结构中,会重复一些简单的结构,如tanh层,简化后的RNN标准模型如图所示。

虽然LSTM与RNN的大体结构相同,但是LSTM在重复模块中拥有一个不同的结构,LSTM结构如图。

LSTM靠门结构有选择性地处理信息,每一个门包含一个sigmoid神经网络层和一个pointwise乘法操作。

LSTM共拥有3个门,分别是遗忘门、输入门和输出门,用来保护和控制状态。遗忘门将状态中的信息选择性地遗忘,输入门将新的信息选择性地记录下来,输出门确定输出什么值。

案例2:在TensorFlow目录下新建文件,命名为LSTM.py,利用TensorFlow解决类似于LSTM在MNIST数据集上进行数字识别的问题,在PyCharm中编写以下代码。

import tensorflow as tf
from tensorflow.contrib import rnn
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
# 导入MNIST数据集
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("./MNIST_data/", one_hot=True)
# 设置全局变量
learning_rate = 0.001
training_steps = 10000
batch_size = 128
display_step = 200
num_input = 28 # 输入向量的维度
timesteps = 28 # 循环层长度
num_hidden = 128 # 隐藏层的特征数
num_classes = 10 # 0~9
# tf Graph 输入
X = tf.placeholder("float", [None, timesteps, num_input])
Y = tf.placeholder("float", [None, num_classes])
# 定义权重和偏置
weights = {
     'out': tf.Variable(tf.random_normal([num_hidden, num_classes]))
}
biases = {
     'out': tf.Variable(tf.random_normal([num_classes]))
}
def RNN(x, weights, biases):
      x = tf.unstack(x, timesteps, 1)
      # 初始的biases=1,不希望遗忘任何信息
      lstm_cell = rnn.BasicLSTMCell(num_hidden, forget_bias=1.0)
      outputs, states = rnn.static_rnn(lstm_cell, x, dtype=tf.float32)
      # 选择最后一个output与输出的全连接weights相乘,再加上biases
      return tf.matmul(outputs[-1], weights['out']) + biases['out']
logits = RNN(X, weights, biases)
prediction = tf.nn.softmax(logits)
# 定义损失和优化
loss_op = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=Y))
optimizer = tf.train.GradientDescentOptimizer(learning_rate=learning_rate)
train_op = optimizer.minimize(loss_op)
correct_pred = tf.equal(tf.argmax(prediction, 1), tf.argmax(Y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))
init = tf.global_variables_initializer()

with tf.Session() as sess:
      sess.run(init)
      for step in range(1, training_steps+1):
             # 随机抽出这一次迭代训练时用的数据
             batch_x, batch_y = mnist.train.next_batch(batch_size)
             # 对数据进行处理,使得其符合输入
             batch_x = batch_x.reshape((batch_size, timesteps, num_input))
             # 迭代
             sess.run(train_op, feed_dict={X: batch_x, Y: batch_y})
             if step % display_step == 0 or step == 1:
                    # 计算损失
                    loss, acc = sess.run([loss_op, accuracy], feed_dict={X: batch_x, Y: batch_y})
                    print("Step " + str(step) + ", Minibatch Loss= " +  "{:.4f}".format(loss) + ", Training Accuracy= " +  "{:.3f}".format(acc))
      print("优化完成!")
      # 计算128个测试的准确率
      test_len = 128
      test_data = mnist.test.images[:test_len].reshape((-1, timesteps, num_input))
      test_label = mnist.test.labels[:test_len]
      print("测试准确率:", sess.run(accuracy, feed_dict={X: test_data, Y: test_label}))

更多精彩内容请继续关注本站!

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐