MNIST数字识别的问题
MNIST是一个非常有名的手写体数字识别数据集,MNIST是NIST数据集的一个子集,它包含了60000张图片作为训练数据,10000张图片作为测试数据;在MNIST中每一张图片都代表了0-9中的一个数字,图片的大小都是28*28;虽然这个数据集中只有训练集和测试集,但是为了测试的效果,我们一般会从训练数据集中划分出一部分作为验证数据集
from tensorflow.examples.tutorials.mnist import input_data
# 输入数据集
mnist = input_data.read_data_sets('/path/to/MNIST_DATA/', one_hot=True)
# 打印训练数据集
print(mnist.train.num_examples)
# 打印测试集数据
print(mnist.test.num_examples)
# 打印验证集的训练数据
print(mnist.validation.num_examples)
batch_size = 100
xs, ys = mnist.train.next_batch(batch_size)
print(xs.shape)
print(ys.shape)
运行结果:
Extracting /path/to/MNIST_DATA/train-images-idx3-ubyte.gz
Extracting /path/to/MNIST_DATA/train-labels-idx1-ubyte.gz
Extracting /path/to/MNIST_DATA/t10k-images-idx3-ubyte.gz
Extracting /path/to/MNIST_DATA/t10k-labels-idx1-ubyte.gz
55000
10000
5000
(100, 784)
(100, 10)
从上面代码中可以看出,通过input_data.read_data_sets函数生成的类自动将数据集分为train、test、validation三个数据集;处理后的数据输入都是长度为784的一维数组,因为神经网络的输入是一个特征向量,所以把二维图像的像素放到一个以为数组中可以方便tensorflow将图片的像素矩阵提供给神经网络的输入,像素矩阵中的元素的取值范围是[0,1],它代表了颜色的深浅,其中0表示白色背景,1表示黑色背景;为了方便使用SGD,input_data.read_data_sets还提供了mnist.train.next_batch函数,它可以从所有的训练数据中读取一小部分作为一个训练batch。
神经网络模型训练及不同模型结果对比
Tensorflow训练神经网络
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
# MNIST数据集相关的常数
INPUT_NODE = 784
OUTPUT_NODE = 10
LAYER1_NODE = 500
BATCH_SIZE = 100
LEARNING_RATE_BASE = 0.8
LEARNING_RATE_DECAY = 0.99
REGULARIZATION_RATE = 0.0001 # 描述模型复杂度的正则化在损失函数的系数
TRAINING_STEPS = 10000
MOVING_AVERAGE_DECAY = 0.99 # 滑动平均衰减率
# 一个辅助函数,给定神经网络的输入和所有参数,计算神经网络中的前向传播结果
def inference(input_tensor, avg_class, weights1, biases1, weights2, biases2):
# 当没有滑动平均类,直接使用参数当前的取值
if avg_class == None:
layer1 = tf.nn.relu(tf.matmul(input_tensor, weights1) + biases1)
pred = tf.matmul(layer1, weights2) + biases2
else:
layer1 = tf.nn.relu(tf.matmul(input_tensor, avg_class.average(weights1)) + avg_class.average(biases1))
pred = tf.matmul(layer1, avg_class.average(weights2)) + avg_class.average(biases2)
return pred
# 训练模型的过程
def train(mnist):
x = tf.placeholder(tf.float32, [None, INPUT_NODE], name='x_input')
y = tf.placeholder(tf.float32, [None, OUTPUT_NODE], name='y_output')
weights1 = tf.Variable(tf.truncated_normal([INPUT_NODE, LAYER1_NODE], stddev=0.1))
biases1 = tf.Variable(tf.constant(0.1, shape=[LAYER1_NODE]))
weights2 = tf.Variable(tf.truncated_normal([LAYER1_NODE, OUTPUT_NODE], stddev=0.1))
biases2 = tf.Variable(tf.constant(0.1, shape=[OUTPUT_NODE]))
# 计算在当前参数下神经网络前向传播的结果
pred = inference(x, None, weights1, biases1, weights2, biases2)
# 计算使用了滑动平均之后的前向传播结果
global_step = tf.Variable(0, trainable=False)
variable_average = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY, global_step)
# tf.trainable_variables函数返回的就是图上的所有变量的集合
average_op = variable_average.apply(tf.trainable_variables())
pred1 = inference(x, variable_average, weights1, biases1, weights2, biases2)
# 计算交叉熵作为刻画预测值和真实值之间差距的损失函数,这里使用sparse_softmax_cross_entropy_with_logits函数来计算交叉熵,当分类
# 问题只有一个正确答案的时候,可以使用这个函数来加速交叉熵的计算
# 这个函数的第一个参数是神经网络不包括softmax层的前向传播的结果,第二个是训练数据的正确答案
cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=pred, labels=tf.argmax(y, 1))
# 计算在当前batch中所有样例的交叉熵平均值
cross_entropy_mean = tf.reduce_mean(cross_entropy)
# 计算L2正则化损失函数
regularizer = tf.contrib.layers.l2_regularizer(REGULARIZATION_RATE)
# 计算模型的正则化损失,一般只计算神经网络边上的权重的正则化损失,而不使用偏执项
# 总损失等于交叉熵和正则化的损失的和
loss = cross_entropy_mean + regularizer(weights1) + regularizer(weights2)
learning_rate = tf.train.exponential_decay(
LEARNING_RATE_BASE, # 基础的学习率
global_step, # 当前迭代的轮数
mnist.train.num_examples / BATCH_SIZE, # 过完所有的训练数据集所需要的迭代次数
LEARNING_RATE_DECAY)
optimizer = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss, global_step=global_step)
# 在训练神经网络模型时,每过一遍数据既需要通过反向传播来更新神经网络中的参数,又要更新每一个参数的滑动平均值
# 为了一次完成多个操作,可以使用tf.group()来完成
train_op = tf.group(optimizer, average_op)
# 检验使用滑动平均模型的神经网络前向传播是否正确,tf.argmax(pred1, 1)计算每一个样例的答案
correct_prediction = tf.equal(tf.argmax(pred1, 1), tf.argmax(y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
# 初始会话并开始训练
with tf.Session() as sess:
tf.global_variables_initializer().run()
# 准备验证数据
validate_feed = {x:mnist.validation.images, y:mnist.validation.labels}
# 准备测试数据
test_feed = {x:mnist.test.images, y:mnist.test.labels}
for i in range(1, TRAINING_STEPS):
if i % 1000 == 0:
# 计算滑动平均模型在验证数据上的结果
# 当神经网络模型比较复杂或者验证数据集数据比较大的时候,太大的batch会导致时间过长甚至发生内存溢出的错误
validation_acc = sess.run(accuracy, feed_dict=validate_feed)
print('validation_acc:', validation_acc)
# 产生这一轮的batch数据,并进行训练
xs, ys = mnist.train.next_batch(BATCH_SIZE)
sess.run(train_op, feed_dict={x:xs, y:ys})
# 在训练结束之后,在测试数据上检测神经网络模型的最终正确率
test_acc = sess.run(accuracy, feed_dict=test_feed)
print('test_acc:', test_acc)
# 主程序入口
def main(argv=None):
# 声明处理MNIST数据集的类,这个类在初始化的时候会自动下载数据
mnist = input_data.read_data_sets('/path/to/MNIST_DATA/', one_hot=True)
train(mnist)
if __name__ == '__main__':
tf.app.run()
运行结果:
Extracting /path/to/MNIST_DATA/train-images-idx3-ubyte.gz
Extracting /path/to/MNIST_DATA/train-labels-idx1-ubyte.gz
Extracting /path/to/MNIST_DATA/t10k-images-idx3-ubyte.gz
Extracting /path/to/MNIST_DATA/t10k-labels-idx1-ubyte.gz
validation_acc: 0.9762
validation_acc: 0.9816
validation_acc: 0.9824
validation_acc: 0.9842
validation_acc: 0.9846
validation_acc: 0.9838
validation_acc: 0.9838
validation_acc: 0.9846
validation_acc: 0.9854
test_acc: 0.9839
使用验证数据集判断模型效果
- 参数的设置没有什么规律可循,配置神经网络的参数都是需要通过实验进行调整的;
- 一般来说,我们是通过在测试集上的效果来看最终的结果的,但是对于深度学习来说我们一般都会有大量的数据,而且测试集的数据非常宝贵,我们一般会将训练数据集分为训练集和测试集
- 除了使用验证数据集,我们还可以使用交叉验证来对模型进行评估
- 一般来说,模型在验证集上出现的结果和测试集上的结果大致一样,但是又不能一概而论,还要看验证集和测试集之间的数据分布情况
变量管理
从上面的代码中可以看到,定义inference函数的时候传入了很多的参数;然而当神经网络的结构更加复杂、参数更多的时候,就需要一个更好的方式来传递和管理神经网络中的参数了;tensorflow提供了通过变量名称来创建或者获取一个变量的机制。通过这个机制,在不同的函数中可以直接通过变量的名字来进行传递,就不需要将变量通过参数的形式到处传递
之前介绍了tf.Variable()函数来创建变量,除了这个函数,我们也可以用tf.get_variable()函数来创建或者获取变量;当tf.get_variable()用来创建变量的时候,这个函数的功能和tf.Variable基本上是等价的:
v = tf.get_variable('v', shape=[1], initializer=tf.constant_initializer(2.0))
tf.get_variable函数调用时需提供维度信息以及初始化方法
tensorflow中提供了七种不同的初始化方法:
tf.get_variable函数与tf.Variable函数最大的功能区别就是指定变量名称的参数,对于tf.Variable来说,变量名称是一个可选参数吗,但是对于tf.get_variable函数来说名称是一个必填的参数,tf.get_variable会根据这个名字去创建或者获取变量
- 如果创建过程中之前已经存在了这个名称的变量,那么get_variable创建变量就会报错
- 如果variable_scope中的参数reuse为True,那么如果变量名称不存在,那么get_variable就会报错
variable_scope创建一个上下文管理器,管理在这个里面所出现或待创建的一切变量
with tf.variable_scope('foo'):
v = tf.get_variable('v', shape=[1], initializer=tf.constant_initializer(1.0)) # 创建一个变量
# with tf.variable_scope('foo'):
# v = tf.get_variable('v', [1]) # 报错, 之前已经存在了一个变量名称为v,再次创建报错
with tf.variable_scope('foo', reuse=True):
v1 = tf.get_variable('v', [1])
print(v1==v)
with tf.variable_scope('bar', reuse=True):
v = tf.get_variable('v', [1]) # 报错,这个管理器里边没有这个变量
tf.variable_scope函数生成的上下文管理器也会创建一个tensorflow中的命名空间,在命名空间内创建的变量名称都会带上这个命名空间名作为前缀;所以tf.variable_scope函数除了可以控制tf.get_variable执行的功能,这个函数也提供了一个管理变量命名空间的方式,以下代码显示了如何通过tf.variabel_scope来管理变量的名称
import tensorflow as tf
v1 = tf.get_variable('v', [1])
print(v1.name)
with tf.variable_scope('foo'):
v2 = tf.get_variable('v', [1])
print(v2.name)
# 输出为foo/v:0 表示这是命名空间foo里边的变量
with tf.variable_scope('foo'):
with tf.variable_scope('bar'):
v3 = tf.get_variable('v', [1])
print(v3.name)
# foo/bar/v:0
with tf.variable_scope('', reuse=True):
v5 = tf.get_variable('foo/bar/v', [1])
# 可以直接通过命名空间名称的变量名来获得其他命名空间下的变量
tensorflow持久化
之前给出的样例就是样例代码在训练完后之后就直接退出了,并没有将训练得到的数据保存下来方便下次直接调用
持久化代码实现
tensorflow提供了一个非常简单的API来保存和还原一个神经网络模型,这个API就是tf.train.Saver类,看代码:
import tensorflow as tf
v1 = tf.Variable([1.0, 2.0])
v2 = tf.Variable(tf.constant([1.0, 3.], shape=[2]))
result = v1 + v2
# 声明tf.train.Saver类用于保存模型
saver = tf.train.Saver()
with tf.Session() as sess:
tf.global_variables_initializer().run()
saver.save(sess, '/path/to/model/model.ckpt')
以上代码实现了持久化一个简单的tensorflow模型的功能,在这段代码中saver.save函数将tensorflow模型保存在/path/to/model/model.ckpt文件中;tensorflow模型一般会存在后缀为.ckpt的文件中,虽然以上程序只指定了一个文件路径,但是在这个文件目录下会出现三个文件,这是因为tensorflow会将计算图的结构和图上的参数取值分开保存
import tensorflow as tf
v1 = tf.Variable([1.0, 2.0])
v2 = tf.Variable(tf.constant([1.0, 3.], shape=[2]))
result = v1 + v2
# 声明tf.train.Saver类用于保存模型
saver = tf.train.Saver()
with tf.Session() as sess:
# 加载已经保存的模型,并通过已经保存的模型中变量的值来计算加法
saver.restore(sess, '/path/to/model/model.ckpt')
print(sess.run(result))
saver.save函数将模型中的参数也一并保存了,在restore的时候也是用的保存的参数进行运算,也就是上面的restore之后如果要输出v1的值,那么输出的依然是保存时候的v1的值