神经网络实战

参考资料:

说明:以下代码示例基于Python3.7和TensorFlow1.13.1

MNIST入门

手写数字识别MNIST是在机器学习领域中的一个经典问题。该问题解决的是把28x28像素的灰度手写数字图片识别为相应的数字,其中数字的范围从0到9.

MNIST Digits

接下来,我们将训练一个机器学习模型用于预测图片里面的数字。我们的目的是要介绍下如何使用TensorFlow,所以,我们这里会从一个很简单的数学模型开始,它叫做Softmax Regression, 即构建一个只有输入层和输出层的简单神经网络。

MNIST数据集

MNIST数据集的官网是Yann LeCun’s website。在这里,我们提供了一份python源代码用于自动下载和安装这个数据集。你可以下载这份代码,然后用下面的代码导入到你的项目里面,也可以直接复制粘贴到你的代码文件里面。

import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
文件内容
train-images-idx3-ubyte.gz训练集图片 - 55000 张 训练图片, 5000 张 验证图片
train-labels-idx1-ubyte.gz训练集图片对应的数字标签
t10k-images-idx3-ubyte.gz测试集图片 - 10000 张 图片
t10k-labels-idx1-ubyte.gz测试集图片对应的数字标签

下载下来的数据集被分成两部分:60000行的训练数据集(mnist.train)和10000行的测试数据集(mnist.test)。这样的切分很重要,在机器学习模型设计时必须有一个单独的测试数据集不用于训练而是用来评估这个模型的性能,从而更加容易把设计的模型推广到其他数据集上(泛化)。

每一个MNIST数据单元有两部分组成:一张包含手写数字的图片和一个对应的标签。我们把这些图片设为“xs”,把这些标签设为“ys”。训练数据集和测试数据集都包含xs和ys,比如训练数据集的图片是 mnist.train.images ,训练数据集的标签是 mnist.train.labels

每一张图片包含28像素X28像素。我们可以用一个数字数组来表示这张图片:

我们把这个数组展开成一个向量,长度是 28x28 = 784。如何展开这个数组(数字间的顺序)不重要,只要保持各个图片采用相同的方式展开。从这个角度来看,MNIST数据集的图片就是在784维向量空间里面的点, 并且拥有比较复杂的结构 (提醒: 此类数据的可视化是计算密集型的)。

展平图片的数字数组会丢失图片的二维结构信息。这显然是不理想的,最优秀的计算机视觉方法会挖掘并利用这些结构信息,我们会在后续教程中介绍。但是在这个教程中我们忽略这些结构,所介绍的简单数学模型,softmax回归(softmax regression),不会利用这些结构信息。

因此,在MNIST训练数据集中,mnist.train.images 是一个形状为 [60000, 784] 的张量,第一个维度数字用来索引图片,第二个维度数字用来索引每张图片中的像素点。在此张量里的每一个元素,都表示某张图片里的某个像素的强度值,值介于0和1之间。

相对应的MNIST数据集的标签是介于0到9的数字,用来描述给定图片里表示的数字。为了用于这个教程,我们使标签数据是"one-hot vectors",即用独热编码(One_Hot Encoding)的形式表示这些类标。 一个one-hot向量除了某一位的数字是1以外其余各维度数字都是0。所以在此教程中,数字n将表示成一个只有在第n维度(从0开始)数字为1的10维向量。比如,标签0将表示成([1,0,0,0,0,0,0,0,0,0,0])。因此, mnist.train.labels 是一个 [60000, 10] 的数字矩阵。

现在,我们准备好可以开始构建我们的模型啦!

Softmax回归介绍

我们知道MNIST的每一张图片都表示一个数字,从0到9。我们希望得到给定图片代表每个数字的概率。比如说,我们的模型可能推测一张包含9的图片代表数字9的概率是80%,但是判断它是8的概率是5%(因为8和9都有上半部分的小圆),然后给予它代表其他数字的概率更小的值。

这是一个使用softmax回归(softmax regression)模型的经典案例。softmax模型可以用来给不同的对象分配概率。即使在之后,我们训练更加精细的模型时,最后一步也需要用softmax来分配概率。

为了得到一张给定图片属于某个特定数字类的证据(evidence),我们对图片像素值进行加权求和。如果这个像素具有很强的证据说明这张图片不属于该类,那么相应的权值为负数,相反如果这个像素拥有有利的证据支持这张图片属于这个类,那么权值是正数。

下面的图片显示了一个模型学习到的图片上每个像素对于特定数字类的权值。红色代表负数权值,蓝色代表正数权值。

我们也需要加入一个额外的偏置量(bias),因为输入往往会带有一些无关的干扰量。因此对于给定的输入图片 x, 它代表的是数字 i 的证据可以表示为

其中 代表权重, 代表数字 i 类的偏置量,j 代表给定图片 x 的像素索引用于像素求和。然后用softmax函数可以把这些证据转换成概率 y

这里的softmax可以看成是一个激励(activation)函数或者链接(link)函数,把我们定义的线性函数的输出转换成我们想要的格式,也就是关于10个数字类的概率分布。因此,给定一张图片,它对于每一个数字的吻合度可以被softmax函数转换成为一个概率值。softmax函数可以定义为:

展开等式右边的子式,可以得到:

但是更多的时候把softmax模型函数定义为前一种形式:把输入值当成幂指数求值,再正则化这些结果值。这个幂运算表示,更大的证据对应更大的假设模型(hypothesis)里面的乘数权重值。反之,拥有更少的证据意味着在假设模型里面拥有更小的乘数系数。假设模型里的权值不可以是0值或者负值。Softmax然后会正则化这些权重值,使它们的总和等于1,以此构造一个有效的概率分布。

对于softmax回归模型可以用下面的图解释,对于输入的xs加权求和,再分别加上一个偏置量,最后再输入到softmax函数中:

如果把它写成一个等式,我们可以得到:

我们也可以用向量表示这个计算过程:用矩阵乘法和向量相加。这有助于提高计算效率。(也是一种更有效的思考方式)

更进一步,可以写成更加紧凑的方式:

实现回归模型

为了用python实现高效的数值计算,我们通常会使用函数库,比如NumPy,会把类似矩阵乘法这样的复杂运算使用其他外部语言实现。不幸的是,从外部计算切换回Python的每一个操作,仍然是一个很大的开销。如果你用GPU来进行外部计算,这样的开销会更大。用分布式的计算方式,也会花费更多的资源用来传输数据。

TensorFlow也把复杂的计算放在python之外完成,但是为了避免前面说的那些开销,它做了进一步完善。Tensorflow不单独地运行单一的复杂计算,而是让我们可以先用图描述一系列可交互的计算操作,然后全部一起在Python之外运行。(这样类似的运行方式,可以在不少的机器学习库中看到。)

使用TensorFlow之前,首先导入它:

import tensorflow as tf

我们通过操作符号变量来描述这些可交互的操作单元,可以用下面的方式创建一个:

x = tf.placeholder("float", [None, 784])

x不是一个特定的值,而是一个占位符placeholder,我们在TensorFlow运行计算时输入这个值。我们希望能够输入任意数量的MNIST图像,每一张图展平成784维的向量。我们用2维的浮点数张量来表示这些图,这个张量的形状是[None,784 ]。(这里的None表示此张量的第一个维度可以是任何长度的。)

我们的模型也需要权重值和偏置量,当然我们可以把它们当做是另外的输入(使用占位符),但TensorFlow有一个更好的方法来表示它们:Variable

一个Variable代表一个可修改的张量,存在在TensorFlow的用于描述交互性操作的图中。它们可以用于计算输入值,也可以在计算中被修改。对于各种机器学习应用,一般都会有模型参数,可以用Variable表示。

W = tf.Variable(tf.zeros([784,10]))
b = tf.Variable(tf.zeros([10]))

我们赋予tf.Variable不同的初值来创建不同的Variable:在这里,我们都用全为零的张量来初始化Wb。因为我们要学习Wb的值,它们的初值可以随意设置。

注意,W的维度是[784,10],因为我们想要用784维的图片向量乘以它以得到一个10维的证据值向量,每一位对应不同数字类。b的形状是[10],所以我们可以直接把它加到输出上面。

现在,我们可以实现我们的模型啦。只需要一行代码!

y = tf.nn.softmax(tf.matmul(x,W) + b)

首先,我们用tf.matmul(​​X,W)表示x乘以W,对应之前等式里面的,这里x是一个2维张量拥有多个输入。然后再加上b,把和输入到tf.nn.softmax函数里面。

至此,我们先用了几行简短的代码来设置变量,然后只用了一行代码来定义我们的模型。TensorFlow不仅仅可以使softmax回归模型计算变得特别简单,它也用这种非常灵活的方式来描述其他各种数值计算,从机器学习模型对物理学模拟仿真模型。一旦被定义好之后,我们的模型就可以在不同的设备上运行:计算机的CPU,GPU,甚至是手机!

训练模型

为了训练我们的模型,我们首先需要定义一个指标来评估这个模型是好的。其实,在机器学习,我们通常定义指标来表示一个模型是坏的,这个指标称为成本(cost)或损失(loss),然后尽量最小化这个指标。但是,这两种方式是相同的。

一个非常常见的,非常漂亮的成本函数是“交叉熵”(cross-entropy)。交叉熵产生于信息论里面的信息压缩编码技术,但是它后来演变成为从博弈论到机器学习等其他领域里的重要技术手段。它的定义如下:

y 是我们预测的概率分布, y’ 是实际的分布(我们输入的one-hot vector)。比较粗糙的理解是,交叉熵是用来衡量我们的预测用于描述真相的低效性。

为了计算交叉熵,我们首先需要添加一个新的占位符用于输入正确值:

y_ = tf.placeholder("float", [None,10])

然后我们可以用 计算交叉熵:

cross_entropy = -tf.reduce_sum(y_*tf.log(y))

首先,用 tf.log 计算 y 的每个元素的对数。接下来,我们把 y_ 的每一个元素和 tf.log(y_) 的对应元素相乘。最后,用 tf.reduce_sum 计算张量的所有元素的总和。(注意,这里的交叉熵不仅仅用来衡量单一的一对预测和真实值,而是所有100幅图片的交叉熵的总和。对于100个数据点的预测表现比单一数据点的表现能更好地描述我们的模型的性能。

现在我们知道我们需要我们的模型做什么啦,用TensorFlow来训练它是非常容易的。因为TensorFlow拥有一张描述你各个计算单元的图,它可以自动地使用反向传播算法(backpropagation algorithm)来有效地确定你的变量是如何影响你想要最小化的那个成本值的。然后,TensorFlow会用你选择的优化算法来不断地修改变量以降低成本。

train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)

在这里,我们要求TensorFlow用梯度下降算法(gradient descent algorithm)以0.01的学习速率最小化交叉熵。梯度下降算法(gradient descent algorithm)是一个简单的学习过程,TensorFlow只需将每个变量一点点地往使成本不断降低的方向移动。当然TensorFlow也提供了其他许多优化算法:只要简单地调整一行代码就可以使用其他的算法。

TensorFlow在这里实际上所做的是,它会在后台给描述你的计算的那张图里面增加一系列新的计算操作单元用于实现反向传播算法和梯度下降算法。然后,它返回给你的只是一个单一的操作,当运行这个操作时,它用梯度下降算法训练你的模型,微调你的变量,不断减少成本。

现在,我们已经设置好了我们的模型。在运行计算之前,我们需要添加一个操作来初始化我们创建的变量:

init = tf.global_variables_initializer()

现在我们可以在一个Session里面启动我们的模型,并且初始化变量:

sess = tf.Session()
sess.run(init)

然后开始训练模型,这里我们让模型循环训练1000次!

for i in range(1000):
  batch_xs, batch_ys = mnist.train.next_batch(100)
  sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})

该循环的每个步骤中,我们都会随机抓取训练数据中的100个批处理数据点,然后我们用这些数据点作为参数替换之前的占位符来运行train_step

使用一小部分的随机数据来进行训练被称为随机训练(stochastic training)- 在这里更确切的说是随机梯度下降训练。在理想情况下,我们希望用我们所有的数据来进行每一步的训练,因为这能给我们更好的训练结果,但显然这需要很大的计算开销。所以,每一次训练我们可以使用不同的数据子集,这样做既可以减少计算开销,又可以最大化地学习到数据集的总体特性。

评估我们的模型

那么我们的模型性能如何呢?

首先让我们找出那些预测正确的标签。tf.argmax 是一个非常有用的函数,它能给出某个tensor对象在某一维上的其数据最大值所在的索引值。由于标签向量是由0,1组成,因此最大值1所在的索引位置就是类别标签,比如tf.argmax(y,1)返回的是模型对于任一输入x预测到的标签值,而 tf.argmax(y_,1) 代表正确的标签,我们可以用 tf.equal 来检测我们的预测是否真实标签匹配(索引位置一样表示匹配)。

correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(y_,1))

这行代码会给我们一组布尔值。为了确定正确预测项的比例,我们可以把布尔值转换成浮点数,然后取平均值。例如,[True, False, True, True] 会变成 [1,0,1,1] ,取平均值后得到 0.75.

accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))

最后,我们计算所学习到的模型在测试数据集上面的正确率。

print(sess.run(accuracy, feed_dict={x: mnist.test.images, y_: mnist.test.labels}))

这个最终结果值应该大约是91%。

这个结果好吗?嗯,并不太好。事实上,这个结果是很差的。这是因为我们仅仅使用了一个非常简单的模型。不过,做一些小小的改进,我们就可以得到97%的正确率。最好的模型甚至可以获得超过99.7%的准确率!(想了解更多信息,可以看看这个关于各种模型的性能对比列表。)

完整代码示例

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


mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)

# 定义一个占位符x
x = tf.placeholder(tf.float32, [None, 784])  # 张量的形状是[None, 784],None表第一个维度任意

# 定义变量W,b,是可以被修改的张量,用来存放机器学习模型参数
W = tf.Variable(tf.zeros([784, 10]))
b = tf.Variable(tf.zeros([10]))

# 实现模型, y是预测分布
y = tf.nn.softmax(tf.matmul(x, W) + b)

# 训练模型,y_是实际分布
y_ = tf.placeholder("float", [None, 10])
cross_entropy = -tf.reduce_sum(y_ * tf.log(y))  # 交叉嫡,cost function

# 使用梯度下降来降低cost,学习速率为0.01
train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)

# 初始化已经创建的变量
init = tf.global_variables_initializer()

# 在一个Session中启动模型,并初始化变量
sess = tf.Session()
sess.run(init)

# 训练模型,运行1000次,每次随机抽取100个
for i in range(1, 1000):
    batch_xs, batch_ys = mnist.train.next_batch(100)
    sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})

# 验证正确率
correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, 'float'))
print(sess.run(accuracy, feed_dict={x: mnist.test.images, y_: mnist.test.labels}))

# 输出
# 0.9172

构建一个多层的神经网络模型

构建一个多层的神经网络模型,使用交叉熵函数和梯度下降算法进行优化,添加Dropout防止过拟合;

模型结构如下:

代码如下:

import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
 
#载入数据集
mnist=input_data.read_data_sets('MNIST_data',one_hot=True)
 
#设置每个批次的大小
batch_size=100
#计算一共有多少个批次
n_batch=mnist.train.num_examples//batch_size
 
#定义三个placeholder
x=tf.placeholder(tf.float32,[None,784])
y=tf.placeholder(tf.float32,[None,10])
keep_prob=tf.placeholder(tf.float32)  #存放百分率
 
#创建一个多层神经网络模型
#第一个隐藏层
W1=tf.Variable(tf.truncated_normal([784,2000],stddev=0.1))
b1=tf.Variable(tf.zeros([2000])+0.1)
L1=tf.nn.tanh(tf.matmul(x,W1)+b1)
L1_drop=tf.nn.dropout(L1,keep_prob) #keep_prob设置工作状态神经元的百分率
#第二个隐藏层
W2=tf.Variable(tf.truncated_normal([2000,2000],stddev=0.1))
b2=tf.Variable(tf.zeros([2000])+0.1)
L2=tf.nn.tanh(tf.matmul(L1_drop,W2)+b2)
L2_drop=tf.nn.dropout(L2,keep_prob)
#第三个隐藏层
W3=tf.Variable(tf.truncated_normal([2000,1000],stddev=0.1))
b3=tf.Variable(tf.zeros([1000])+0.1)
L3=tf.nn.tanh(tf.matmul(L2_drop,W3)+b3)
L3_drop=tf.nn.dropout(L3,keep_prob)
#输出层
W4=tf.Variable(tf.truncated_normal([1000,10],stddev=0.1))
b4=tf.Variable(tf.zeros([10])+0.1)
prediction=tf.nn.softmax(tf.matmul(L3_drop,W4)+b4)
 
#定义交叉熵代价函数
loss=tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y,logits=prediction))
#定义反向传播算法(使用梯度下降算法)
train_step=tf.train.GradientDescentOptimizer(0.2).minimize(loss)
 
#结果存放在一个布尔型列表中(argmax函数返回一维张量中最大的值所在的位置)
correct_prediction=tf.equal(tf.argmax(y,1),tf.argmax(prediction,1))
 
#求准确率(tf.cast将布尔值转换为float型)
accuracy=tf.reduce_mean(tf.cast(correct_prediction,tf.float32))
 
#创建会话
with tf.Session() as sess:
    sess.run(tf.global_variables_initializer()) #初始化变量
    #训练次数
    for i in range(21):
        for batch in range(n_batch):
            batch_xs,batch_ys=mnist.train.next_batch(batch_size)
            sess.run(train_step,feed_dict={x:batch_xs,y:batch_ys,keep_prob:1.0})
        #测试数据计算出的准确率
        test_acc=sess.run(accuracy,feed_dict={x:mnist.test.images,y:mnist.test.labels,keep_prob:1.0})
        print("Iter"+str(i)+",Testing Accuracy"+str(test_acc))

结果为:

补充:

防止过拟合的常用方法

常用优化器

卷积神经网络

在这个小节里,我们将使用卷积神经网络模型来训练MNIST数据集。

权重初始化

为了创建这个模型,我们需要创建大量的权重和偏置项。这个模型中的权重在初始化时应该加入少量的噪声来打破对称性以及避免0梯度。由于我们使用的是ReLU神经元,因此比较好的做法是用一个较小的正数来初始化偏置项,以避免神经元节点输出恒为0的问题(dead neurons)。为了不在建立模型的时候反复做初始化操作,我们定义两个函数用于初始化。

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)

卷积和池化

TensorFlow在卷积和池化上有很强的灵活性。我们怎么处理边界?步长应该设多大?在这个实例里,我们会一直使用vanilla版本。我们的卷积使用1步长(stride size),0边距(padding size)的模板,保证输出和输入是同一个大小。我们的池化用简单传统的2x2大小的模板做max pooling。为了代码更简洁,我们把这部分抽象成一个函数。

def conv2d(x, W):
    return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')

def max_pool_2x2(x):
    return tf.nn.max_pool(x, ksize=[1, 2, 2, 1],
                         strides=[1, 2, 2, 1], padding='SAME')

第一层卷积

现在我们可以开始实现第一层了。它由一个卷积接一个max pooling完成。卷积在每个5x5的patch中算出32个特征。卷积的权重张量形状是[5, 5, 1, 32],前两个维度是patch的大小,接着是输入的通道数目,最后是输出的通道数目。 而对于每一个输出通道都有一个对应的偏置量。

W_conv1 = weight_variable([5, 5, 1, 32])
b_conv1 = bias_variable([32])

为了用这一层,我们把x变成一个4d向量,其第2、第3维对应图片的宽、高,最后一维代表图片的颜色通道数(因为是灰度图所以这里的通道数为1,如果是rgb彩色图,则为3)。

x_image = tf.reshape(x, [-1,28,28,1])

我们把x_image和权值向量进行卷积,加上偏置项,然后应用ReLU激活函数,最后进行max pooling。

h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)

第二层卷积

为了构建一个更深的网络,我们会把几个类似的层堆叠起来。第二层中,每个5x5的patch会得到64个特征。

W_conv2 = weight_variable([5, 5, 32, 64])
b_conv2 = bias_variable([64])

h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)

密集连接层

现在,图片尺寸减小到7x7,我们加入一个有1024个神经元的全连接层,用于处理整个图片。我们把池化层输出的张量reshape成一些向量,乘上权重矩阵,加上偏置,然后对其使用ReLU。

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

为了减少过拟合,我们在输出层之前加入dropout。我们用一个placeholder来代表一个神经元的输出在dropout中保持不变的概率。这样我们可以在训练过程中启用dropout,在测试过程中关闭dropout。

Dropout是在每次神经网络的训练过程中,使得部分神经元工作而另外一部分神经元不工作。而测试的时候激活所有神经元,用所有的神经元进行测试。这样便可以有效的缓解过拟合,提高模型的准确率。

TensorFlow的tf.nn.dropout操作除了可以屏蔽神经元的输出外,还会自动处理神经元输出值的scale。所以用dropout的时候可以不用考虑scale。

keep_prob = tf.placeholder("float")
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)

输出层

最后,我们添加一个softmax层,就像前面的单层softmax regression一样。

W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])

y_conv=tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)

训练和评估模型

为了进行训练和评估,我们使用与之前简单的单层SoftMax神经网络模型几乎相同的一套代码,只是我们会用更加复杂的ADAM优化器来做梯度最速下降,在feed_dict中加入额外的参数keep_prob来控制dropout比例。然后每100次迭代输出一次日志。

cross_entropy = -tf.reduce_sum(y_*tf.log(y_conv))
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(y_conv,1), tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
sess.run(tf.global_variables_initializer())
for i in range(20000):
    batch = mnist.train.next_batch(50)
    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))
    train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})

print("test accuracy %g"%accuracy.eval(feed_dict={
 x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))

以上代码,在最终测试集上的准确率大概是99.2%。

目前为止,我们已经学会了用TensorFlow快捷地搭建、训练和评估一个复杂一点儿的深度学习模型。

完整代码示例

# -*- coding: utf-8 -*-

import tensorflow as tf

#导入input_data用于自动下载和安装MNIST数据集
#from tensorflow.examples.tutorials.mnist import input_data
#mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)

#如果自动下载不成功,可以手动下载数据集(MNIST_data包含4个gz文件),然后用下面进行本地加载数据
from tensorflow.examples.tutorials.mnist import input_data
#路径要写全
MNIST_data_folder="D:\\TestTensorflow\\……\\MNIST_data"
mnist=input_data.read_data_sets(MNIST_data_folder,one_hot=True)

#创建一个交互式Session
sess = tf.InteractiveSession()

#创建两个占位符,x为输入网络的图像,y_为输入网络的图像类别
x = tf.placeholder("float", shape=[None, 784])
y_ = tf.placeholder("float", shape=[None, 10])

#权重初始化函数
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)

#创建卷积op
#x 是一个4维张量,shape为[batch,height,width,channels]
#卷积核移动步长为1。填充类型为SAME,可以不丢弃任何像素点
def conv2d(x, W):
    return tf.nn.conv2d(x, W, strides=[1,1,1,1], padding="SAME")

#创建池化op
#采用最大池化,也就是取窗口中的最大值作为结果
#x 是一个4维张量,shape为[batch,height,width,channels]
#ksize表示pool窗口大小为2x2,也就是高2,宽2
#strides,表示在height和width维度上的步长都为2
def max_pool_2x2(x):
    return tf.nn.max_pool(x, ksize=[1,2,2,1],
                          strides=[1,2,2,1], padding="SAME")


#第1层,卷积层
#初始化W为[5,5,1,32]的张量,表示卷积核大小为5*5,第一层网络的输入和输出神经元个数分别为1和32
W_conv1 = weight_variable([5,5,1,32])
#初始化b为[32],即输出大小
b_conv1 = bias_variable([32])

#把输入x(二维张量,shape为[batch, 784])变成4d的x_image,x_image的shape应该是[batch,28,28,1]
#-1表示自动推测这个维度的size
x_image = tf.reshape(x, [-1,28,28,1])

#把x_image和权重进行卷积,加上偏置项,然后应用ReLU激活函数,最后进行max_pooling
#h_pool1的输出即为第一层网络输出,shape为[batch,14,14,1]
h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)

#第2层,卷积层
#卷积核大小依然是5*5,这层的输入和输出神经元个数为32和64
W_conv2 = weight_variable([5,5,32,64])
b_conv2 = weight_variable([64])

#h_pool2即为第二层网络输出,shape为[batch,7,7,1]
h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)

#第3层, 全连接层
#这层是拥有1024个神经元的全连接层
#W的第1维size为7*7*64,7*7是h_pool2输出的size,64是第2层输出神经元个数
W_fc1 = weight_variable([7*7*64, 1024])
b_fc1 = bias_variable([1024])

#计算前需要把第2层的输出reshape成[batch, 7*7*64]的张量
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层
#为了减少过拟合,在输出层前加入dropout
keep_prob = tf.placeholder("float")
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)

#输出层
#最后,添加一个softmax层
#可以理解为另一个全连接层,只不过输出时使用softmax将网络输出值转换成了概率
W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])

y_conv = tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)

#预测值和真实值之间的交叉墒
cross_entropy = -tf.reduce_sum(y_ * tf.log(y_conv))

#train op, 使用ADAM优化器来做梯度下降。学习率为0.0001
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)

#评估模型,tf.argmax能给出某个tensor对象在某一维上数据最大值的索引。
#因为标签是由0,1组成了one-hot vector,返回的索引就是数值为1的位置
correct_predict = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_, 1))

#计算正确预测项的比例,因为tf.equal返回的是布尔值,
#使用tf.cast把布尔值转换成浮点数,然后用tf.reduce_mean求平均值
accuracy = tf.reduce_mean(tf.cast(correct_predict, "float"))

#初始化变量
sess.run(tf.global_variables_initializer())

#开始训练模型,循环20000次,每次随机从训练集中抓取50幅图像
for i in range(20000):
    batch = mnist.train.next_batch(50)
    if i%100 == 0:
        #每100次输出一次日志
        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))

    train_step.run(feed_dict={x:batch[0], y_:batch[1], keep_prob:0.5})

print("test accuracy %g"%accuracy.eval(feed_dict={
x:mnist.test.images, y_:mnist.test.labels, keep_prob:1.0}))

RNN循环神经网络

简单示例

在TensorFlow里,我们可以借助MultiRNNCell这个类来实现深度循环神经网络,下面我们用一个具体的例子来演示TensorFlow中多层循环神经网络的实现。

示例:航班乘客人数预测

本示例所使用的数据来自DataMarket:https://datamarket.com/data/set/22u3/international-airline-passengers-monthly-totals-in-thousands-jan-49-dec-60#!ds=22u3&display=line。该数据集包含了从1949年到1960年共144个月的乘客总人数的数据,每个月的乘客人数是一条记录,共144条记录。其中部分数据显示如下:

时间乘客人数(单位:千人)
“1949-01”112
“1949-02”118
“1949-03”132
“1949-04”129
“1949-05”121

我们将所有数据可视化显示:

从可视化的效果可以看到数据的变化呈现周期性,对于RNN来说,学习这种很明显的序列关系并非难事。我们定义一个多层循环神经网络:

def get_cell(self):  
    return rnn.BasicRNNCell(self.hidden_dim)  

def model(self):  
    # 定义一个多层循环神经网络  
    cells = rnn.MultiRNNCell([self.get_cell() for _ in range(3)], state_is_tuple=True)  
    # 用cells构造完整的循环神经网络  
    outputs, states = tf.nn.dynamic_rnn(cells, self.x, dtype=tf.float32)  

    num_examples = tf.shape(self.x)[0]  
    # 初始化输出层权重矩阵  
    W_out = tf.tile(tf.expand_dims(self.W_out, 0), [num_examples, 1, 1])  
    out = tf.matmul(outputs, W_out) + self.b_out  # 输出层的输出  
    out = tf.squeeze(out)  # 删除大小为1的维度  
    tf.add_to_collection('model', out)  
    return out

在第1行代码中定义了一个方法用来返回单层的cell,在第6行代码中,我们使用MultiRNNCell类生成了一个3层的循环神经网络。最终的预测结果如下图左侧所示:

多层循环神经网络的预测结果

上图中,蓝色线条是原始数据,绿色线条是从原始数据中划分出来的测试数据,红色线条是在测试数据上的预测效果,左侧是多层循环神经网络的预测结果,右侧是单层循环神经网络的预测结果(除网络层数不同外,其它参数均相同)。可以看到两个预测结果都几乎和真实数据重合,仔细比较会发现,多层循环神经网络的拟合效果更好一些。

进阶

我们将用TensorFlow实现简单的RNN,并且用来解决时序数据的预测问题,看一看RNN究竟能达到什么样的效果,具体又是如何实现的。

在这个演示项目里,我们使用随机生成的方式生成一个数据集(由0和1组成的二进制序列),然后人为的增加一些数据间的关系。最后我们把这个数据集放进RNN里,让RNN去学习其中的关系,实现二进制序列的预测。数据生成的方式如下:

循环生成规模为五十万的数据集,每次产生的数据为0或1的概率均为0.5。如果连续生成了两个1(或两个0)的话,则下一个数据强制为0(或1)。

1、 我们首先导入需要的Python模块:

#!/usr/bin/python  
# -_- coding: UTF-8 -_-  
import numpy as np  
import tensorflow as tf  
import matplotlib.pyplot as plt  
from tensorflow.contrib import rnn

2、 定义一个Data类,用来产生数据:

class Data:  
    def __init__(self, data_size, num_batch, batch_size, time_step):
        self.data_size = data_size      # 数据集的大小  
        self.batch_size = batch_size    # 一个batch的大小  
        self.num_batch = num_batch   # batch的数目(num_batch=data_size//batch_size)  
        self.time_step = time_step     # RNN的时间步  
        self.data_without_rel = []      # 保存随机生成的数据,数据间没有联系  
        self.data_with_rel = []         # 保存有时序关系的数据 

3、 在构造方法“init”中,我们初始化了数据集的大小“data_size”、一个batch的大小“batch_size”、一个epoch中的batch数目“num_batch”以及RNN的时间步“time_step”。接下来我们定义一个“generate_data”方法:

def generate_data(self):  
    # 随机生成数据  
    self.data_without_rel = np.array(np.random.choice(2, size=(self.data_size,)))  
    for i in range(self.data_size):  
        if self.data_without_rel[i-1] == 1 and self.data_without_rel[i-2] == 1:  
            # 之前连续出现两个1,当前数据设为0  
            self.data_with_rel.append(0)  
            continue  
        elif self.data_without_rel[i-1] == 0 and self.data_without_rel[i-2] == 0:  
            # 之前连续出现两个0,当前数据设为1  
            self.data_with_rel.append(1)  
            continue  
        # np.random.rand()产生的随机数范围:[0,1]  
        else:  
            if np.random.rand() >= 0.5:  
                self.data_with_rel.append(1)  
            else:  
                self.data_with_rel.append(0)  
    return self.data_without_rel, self.data_with_rel

在第11行代码中,我们用了 “np.random.choice”函数生成的由0和1组成的长串数据。接下来我们用了一个for循环,在“data_without_rel”保存的数据的基础上重新生成了一组数据,并保存在“data_with_rel”数组中。为了使生成的数据间具有一定的序列关系,我们使用了前面介绍的很简单的数据生成方式:以“data_without_rel”中的数据为参照,如果出现了连续两个1(或0)则生成一个0(或1),其它情况则以相等概率随机生成0或1。

有了数据我们接下来要用RNN去学习这些数据,看看它能不能学习到我们产生这些数据时使用的策略,即数据间的联系。评判RNN是否学习到规律以及学习的效果如何的依据,使用的是交叉熵损失函数。根据我们生成数据的规则,如果RNN没有学习到规则,那么它预测正确的概率就是0.5,否则它预测正确的概率为:0.75(在“data_without_rel”中,连续出现的两个数字的组合为:00、01、10和11。00和11出现的总概率占0.5,在这种情况下,如果RNN学习到了规律,那么一定能预测出下一个数字,00对应1,11对应0。而如果出现的是01或10的话,RNN预测正确的概率就只有0.5,所以综合起来就是0.75)。

根据交叉熵损失函数,在没有学习到规律的时候,其交叉熵损失为:

loss = – (0.5 * np.log(0.5) + 0.5 * np.log(0.5)) = 0.6931471805599453

在学习到规律的时候,其交叉熵损失为:

Loss = -0.5*(0.5 * np.log(0.5) + np.log(0.5))=-0.25 * (1 * np.log(1) ) – 0.25 * (1 * np.log(1))=0.34657359027997264

4、 我们定义“generate_epochs”方法处理生成的数据:

def generate_epochs(self):  
    # 生成数据  
    self.generate_data()  
    data_x = np.zeros([self.num_batch, self.batch_size], dtype=np.int32)
    data_y = np.zeros([self.num_batch, self.batch_size], dtype=np.int32)  
    # 将数据划分成num_batch组  
    for i in range(self.num_batch):  
        data_x[i] = self.data_without_rel[self.batch_size * i:self.batch_size * (i + 1)]  
        data_y[i] = self.data_with_rel[self.batch_size * i:self.batch_size * (i + 1)]  
        # 将每个batch的数据按time_step进行切分  
        epoch_size = self.batch_size // self.time_step  
      
        # 返回最终的数据  
        for i in range(epoch_size):  
            x = data_x[:, self.time_step * i:self.time_step * (i + 1)]  
            y = data_y[:, self.time_step * i:self.time_step * (i + 1)]  
            yield (x, y)

5、 接下来实现RNN部分:

class Model:  
    def init(self, data_size, batch_size, time_step, state_size):  
        self.data_size = data_size  
        self.batch_size = batch_size  
        self.num_batch = self.data_size // self.batch_size  
        self.time_step = time_step  
        self.state_size = state_size  
        
        # 输入数据的占位符  
        self.x = tf.placeholder(tf.int32, [self.num_batch, self.time_step], name='input_placeholder')  
        self.y = tf.placeholder(tf.int32, [self.num_batch, self.time_step], name='labels_placeholder')  
        
        # 记忆单元的占位符  
        self.init_state = tf.zeros([self.num_batch, self.state_size])  
        
        # 将输入数据进行one-hot编码  
        self.rnn_inputs = tf.one_hot(self.x, 2)  
        
        # 隐藏层的权重矩阵和偏置项  
        self.W = tf.get_variable('W', [self.state_size, 2])  
        self.b = tf.get_variable('b', [2], initializer=tf.constant_initializer(0.0))  
        
        # RNN隐藏层的输出  
        self.rnn_outputs, self.final_state = self.model()  
        
        # 计算输出层的输出  
        logits = tf.reshape( tf.matmul(tf.reshape(self.rnn_outputs, [-1, self.state_size]), self.W) + self.b, [self.num_batch, self.time_step, 2])  
        
        self.losses = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=self.y, logits=logits)  
        self.total_loss = tf.reduce_mean(self.losses)  
        self.train_step = tf.train.AdagradOptimizer(0.1).minimize(self.total_loss) 

6、 定义RNN模型:

def model(self):  
    cell = rnn.BasicRNNCell(self.state_size)  
    rnn_outputs, final_state = tf.nn.dynamic_rnn(cell, self.rnn_inputs,  
    initial_state=self.init_state)  
    return rnn_outputs, final_state 

这里我们使用了“dynamic_rnn”,因此每次会同时处理所有batch的第一组数据,总共处理的次数为:batch_size / time_step。

def train(self):  
    with tf.Session() as sess:  
        sess.run(tf.global_variables_initializer())  
        training_losses = []  
        d = Data(self.data_size, self.num_batch, self.batch_size, self.time_step)  
        training_loss = 0  
        training_state = np.zeros((self.num_batch, self.state_size))  
        for step, (X, Y) in enumerate(d.generate_epoch()):  
            tr_losses, training_loss_, training_state, _ = \  
            sess.run([self.losses, self.total_loss, self.final_state, self.train_step],  
            feed_dict={self.x: X, self.y: Y, self.init_state: training_state})  
            training_loss += training_loss_  
            if step % 20 == 0 and step > 0:  
                training_losses.append(training_loss/20)  
                training_loss = 0  
    return training_losses

7、 到这里,我们已经实现了整个RNN模型,接下来初始化相关数据,看看RNN的学习效果如何:

if __name__ == '__main__':  
    data_size = 500000  
    batch_size = 2000  
    time_step = 5  
    state_size = 6  
    
    m = Model(data_size, batch_size, time_step, state_size)  
    training_losses = m.train()  
    plt.plot(training_losses)  
    plt.show()  

定义数据集的大小为500000,每个batch的大小为2000,RNN的“时间步”设为5,隐藏层的神经元数目为6。将训练过程中的loss可视化,结果如下图中的左侧图像所示:

图 二进制序列数据训练的loss曲线

从左侧loss曲线可以看到,loss最终稳定在了0.35左右,这与我们之前的计算结果一致,说明RNN学习到了序列数据中的规则。右侧的loss曲线是在调整了序列关系的时间间隔后(此时的time_step过小,导致RNN无法学习到序列数据的规则)的结果,此时loss稳定在0.69左右,与之前的计算也吻合。

TensorFlow实现LSTM和GRU

这一节里我们使用TensorFlow来实现一个LSTM模型。为了方便,这里我们使用前面介绍过的mnist数据集。可能读者对于在循环神经网络中使用图像数据会有一点疑惑,因为通常情况下图像数据一般都是使用卷积神经网络来训练。事实的确是这样,由于卷积神经网络和循环神经网络的结构不同,也就使得它们各自有不同的适用场景,但这不代表卷积神经网络只能用来处理时序数据,同样也不能认为循环神经网络不能用来处理图像数据,只要在输入数据的格式上稍作调整即可。

from tensorflow.examples.tutorials.mnist import input_data  
import tensorflow as tf  
from tensorflow.contrib import rnn  

batch_size = 100      # batch的大小  
time_step = 28        # LSTM网络中的时间步(每个时间步处理图像的一行)  
data_length = 28      # 每个时间步输入数据的长度(这里就是图像的宽度)  
learning_rate = 0.01  # 学习率

我们首先导入需要的包,然后定义了神经网络中的一些相关参数。其中第6行代码定义了LSTM中的时间步的长度,由于我们mnist数据集的图像大小为28X28,所以我们将一行像素作为一个输入,这样我们就需要有28个时间步。第7行代码定义了每个时间步输入数据的长度(每个时间步的输入是一个向量),即一行像素的长度。

# 下载mnist数据集,当前目录有已下载的数据集的话,就直接读取  
mnist = input_data.read_data_sets("data", one_hot=True, reshape=False, validation_size=1000)  

# 定义相关数据的占位符  
X_ = tf.placeholder(tf.float32, [None, 28, 28, 1])  # 输入数据  
Y_ = tf.placeholder(tf.int32, [None, 10])          # mnist数据集的类标  
# dynamic_rnn的输入数据(batch_size, max_time, ...)  
inputs = tf.reshape(X_, [-1, time_step, data_length])  
# 验证集  
validate_data = {X_: mnist.validation.images, Y_: mnist.validation.labels}  
# 测试集  
test_data = {X_: mnist.test.images, Y_: mnist.test.labels}`

第10行代码用来加载mnist数据集,并通过参数“validation_size”指定了验证集的大小。第16行代码用来将mnist数据集的格式转换成“dynamic_rnn”函数接受的数据格式“[batch_size, max_time, data_length]”。

# 定义一个两层的LSTM模型  
lstm_layers = rnn.MultiRNNCell([rnn.BasicLSTMCell(num_units=num)  
for num in [100, 100]], state_is_tuple=True)  
# 定义一个两层的GRU模型  
# gru_layers = rnn.MultiRNNCell([rnn.GRUCell(num_units=num) for num in [100, 100]], state_is_tuple=True)
outputs, h_ = tf.nn.dynamic_rnn(lstm_layers, inputs, dtype=tf.float32)  
# outputs, h_ = tf.nn.dynamic_rnn(gru_layers, inputs, dtype=tf.float32)  

output = tf.layers.dense(outputs[:, -1, :], 10)  # 获取LSTM网络的最后输出状态  

# 定义交叉熵损失函数和优化器  
loss = tf.losses.softmax_cross_entropy(onehot_labels=Y_, logits=output)  # compute cost  
train_op = tf.train.AdamOptimizer(learning_rate).minimize(loss)  

# 计算准确率  
accuracy = tf.metrics.accuracy(  
labels=tf.argmax(Y_, axis=1), predictions=tf.argmax(output, axis=1))[1]  

# 初始化变量  
sess = tf.Session()  
init = tf.group(tf.global_variables_initializer(), tf.local_variables_initializer())  
sess.run(init)

在上面的代码中,我们定义了一个两层的LSTM网络结构,并使用了交叉熵损失函数和“Adam”优化器。LSTM多层网络结构的定义和我们前面使用过的多层神经网络的定义方法一样,只是将“BasicRNNCell”类换成了“BasicLSTMCel”类。

for step in range(3000):  
    # 获取一个batch的训练数据  
    train_x, train_y = mnist.train.next_batch(batch_size)  
    _, loss_ = sess.run([train_op, loss], {X_: train_x, Y_: train_y})  
    
    # 在验证集上计算准确率  
    if step % 50 == 0:  
        val_acc = sess.run(accuracy, feed_dict=validate_data)  
        print('train loss: %.4f' % loss_, '| val accuracy: %.2f' % val_acc)  
        
        # 计算测试集上的准确率  
        test_acc = sess.run(accuracy, feed_dict=test_data)  
        print('test loss: %.4f' % test_acc)

在上面的整个代码中,我们使用的参数都是比较随意的进行选择的,没有进行任何的优化,最终在测试集上的结果能达到96%左右,当然这肯定不是LSTM网络处理mnist数据集所能达到的最好的效果,有兴趣的读者可以试着去调整网络的结构和参数,看是否能达到更高的准确率。

TensorFlow中实现LSTM和GRU的切换非常简单,在上面的代码中,将第22和26行代码注释掉,然后取消第24和27行代码的注释,实现的就是GRU。

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐