《TensorFlow实战Google深度学习框架第2版》教材中的样例代码,由于tensorflow版本、运行过程的修改、敲码过程中的失误、教材样例自带的错误等原因,可能会导致代码与教材不一致或者不能运行成功,仅供参考。
第6章 图像识别与卷积神经网络
- 6..3 卷积层和池化层的前向传播过程
#!/usr/bin/python
# -*- coding: utf-8 -*-
import tensorflow as tf
from chapter06.lenet5 import mnist_inference
BATCH_SIZE = 100
# 通过tf.get_variable的方式创建过滤器的权重变量和偏置项变量
# 上面介绍了卷积层的参敖个数只和过滤器的尺寸、深度以及当前层节点矩阵的深度有关,所以这里声明的参数变量是一个四维矩阵
# 前面两个维度代表了过滤器的尺寸,第三个维度表示当前层的深度,第四个维度表示过滤撼的深度
filter_weight = tf.get_variable('weights', [5, 5, 3, 16],
initializer=tf.truncated_normal_initializer(stddev=0.1))
# 和卷积层的权重类似,当前层矩阵上不同位置的偏置项也是共辜的,所以总共有下-层深度个不同的偏置项
# 样例代码中16为过滤器的深度,也是神经网络中下一层节点矩阵的深度
biases = tf.get_variable('biases', [16], initializer=tf.constant_initializer(0.1))
# tf.nn.conv2d提供了一个非常方便的函数来实现卷积层前向传播的算法
# 这个函数的第一个输入为当前层的节点矩阵,注意这个矩阵是一个四维矩阵,后面三个维度对应一个节点矩阵,第一维对应一个输入batch
# 比如在输入层,input[O , :, :, :]表示第一张图片,input[1, :, :, :]表示第二张图片,以此类推
# tf.nn.conv2d第二个参数提供了卷积层的权重
# 第三个参数为不同维度上的步长,虽然第三个参数提供的是一个长度为4的数组,但是第一维和最后一维的数字要求一定是1,这是因为卷积层的步长只对矩阵的长和宽有效
# 最后一个参数是填充(padding)的方法,TensorFlow中提供SAME或是VALID两种选择,其中SAME表示添加全0填充,VALID表示不添加
conv = tf.nn.conv2d(input, filter_weight, strides=[1, 1, 1, 1], padding='SAME')
# tf.nn.bias_add提供了一个方便的函数给每一个节点加上偏置顷
# 注意这里不能直接使用加法,因为矩阵上不同位置上的节点都需要加上同样的偏置项
bias = tf.nn.bias_add(conv, biases)
# 将计算结果通过ReLU激活函数完成去线性化
actived_conv = tf.nn.relu(bias)
# tf.nn.max_pool实现了最大池化层的前向传播过程,它的参数和tf.nn.conv2d函数类似
# ksize提供了过滤器的尺寸,strides提供了步长信息,padding提供了是否使用全0填充
pool = tf.nn.max_pool(actived_conv, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME')
- 6.4 实现类似LeNet-5模型结构的前向传播过程的mnist_ inference.py程序
#!/usr/bin/python
# -*- coding: utf-8 -*-
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):
# 声明第一层卷积层的变量并实现前向传播过程
# 通过使用不同的命名空间来隔离不同层的变量,这可以让每一层中的变量命名只需要考虑在当前层的作用,而不需要担心重名的问题
# 和标准LeNet-5模型不大一样,这里定义的卷积层输入为28x28x1的原始MNIST图片像素
# 因为卷积层中使用了全0填充,所以输出为28x28x32的矩阵
with tf.variable_scope('layer1-conv1'):
conv1_weights = tf.get_variable(
"weight", [CONV1_SIZE, CONV1_SIZE, NUM_CHANNELS, CONV1_DEEP],
initializer=tf.truncated_normal_initializer(stddev=0.1))
conv1_biases = tf.get_variable("bias", [CONV1_DEEP], initializer=tf.constant_initializer(0.0))
# 使用边长为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
# 这一层的输入是上一层的输出,也就是28x28×32的矩阵,输出为14x14×32的矩阵
with tf.name_scope("layer2-pool1"):
pool1 = tf.nn.max_pool(relu1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding="SAME")
# 声明第三层卷积层的变量并实现前向传播过程
# 这一层的输入为14×14x32的矩阵,输出为14x14x64的矩阵
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.0))
# 使用边长为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))
# 实现第四层池化层的前向传播过程
# 这一层和第二层的结构是一样的,这一层的输入为14x14x64的矩阵,输出为7x7x64的矩阵
with tf.name_scope("layer4-pool2"):
pool2 = tf.nn.max_pool(relu2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
# 将第四层池化层的输出转化为第五层全连接层的输入格式
# 第四层的输出为7x7x64的矩阵,然而第五层全连接层需要的输入格式为向量,所以在这里面要将这个7x7x64的矩阵拉直成一个向量
# pool2.get_shape函数可以得到第四层输出矩阵的维度而不需要手工计算
# 注意,因为每一层神经网络的输入输出都为一个batch的矩阵,所以这里得到的维度也包含了一个batch中数据的个数
pool_shape = pool2.get_shape().as_list()
# 计算将矩阵拉直成向量之后的长度,这个长度就是矩阵长宽及深度的乘积
# 注意这里pool_shape[O]为一个batch中数据的个数
nodes = pool_shape[1] * pool_shape[2] * pool_shape[3]
# 通过tf.reshape函数将第四层的输出变成一个batch的向量
reshaped = tf.reshape(pool2, [pool_shape[0], nodes])
# 声明第五层全连接层的变量并实现前向传播过程
# 这一层的输入是拉直之后的一组向盘,向量长度为3136,输出是一组长度为512的向量
# 这一层引入了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 != None: 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, 0.5)
# 声明第六层全连接层的变量并实现前向传播过程
# 这一层的输入为一组长度为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 != None: 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
- 6.4 mnist_train.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
import numpy as np
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
from chapter06.lenet5 import mnist_inference
# 配置神经网络的参数
BATCH_SIZE = 100
LEARNING_RATE_BASE = 0.01
LEARNING_RATE_DECAY = 0.99
REGULARIZATION_RATE = 0.0001
TRAINING_STEPS = 6000
MOVING_AVERAGE_DECAY = 0.99
# 模型保存的路径和文件名
MODEL_SAVE_PATH = "/path/to/MNIST_model/"
MODEL_NAME = "mnist_model"
def train(mnist):
# 定义输入输出placeholder
x = tf.placeholder(tf.float32, [BATCH_SIZE, mnist_inference.IMAGE_SIZE, mnist_inference.IMAGE_SIZE,
mnist_inference.NUM_CHANNELS], name='x-input')
y_ = tf.placeholder(tf.float32, [None, mnist_inference.OUTPUT_NODE], name='y-input')
regularizer = tf.contrib.layers.l2_regularizer(REGULARIZATION_RATE)
# 直接使用mnist_inference.py中定义的前向传播过程
y = mnist_inference.inference(x, False, regularizer)
global_step = tf.Variable(0, trainable=False)
# 定义损失函数/学习率/滑动平均操作以及训练过程
variable_averages = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY, global_step)
variables_averages_op = variable_averages.apply(tf.trainable_variables())
cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=y, labels=tf.argmax(y_, 1))
cross_entropy_mean = tf.reduce_mean(cross_entropy)
loss = cross_entropy_mean + tf.add_n(tf.get_collection('losses'))
learning_rate = tf.train.exponential_decay(
LEARNING_RATE_BASE,
global_step,
mnist.train.num_examples / BATCH_SIZE,
LEARNING_RATE_DECAY,
staircase=True)
train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss, global_step=global_step)
with tf.control_dependencies([train_step, variables_averages_op]):
train_op = tf.no_op(name='train')
# 初始化TensorFlow持久化类
saver = tf.train.Saver()
with tf.Session() as sess:
tf.global_variables_initializer().run()
for i in range(TRAINING_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))
_, 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))
def main(argv=None):
mnist = input_data.read_data_sets("/path/to/MNIST_data", one_hot=True)
train(mnist)
if __name__ == '__main__':
tf.app.run()
- 6.4 Inception模块
#!/usr/bin/python
# -*- coding: utf-8 -*-
import tensorflow as tf
slim = tf.contrib.slim
# slim.arg_scope函数可以用于设置默认的参数取值
# slim.arg_scope函数的第一个参数是一个函数列表,在这个列表中的函数将使用默认的参数取值
# 如果在函数调用时指定了stride,那么这里设置的默认值就不会再使用
# 通过这种方式可以进一步减少冗余的代码
with slim.arg_scope([slim.conv2d, slim.max_pool2d, slim.avg_pool2d],
stride=1, padding='VALID'):
# 假设输入图片经过之前的神经网络前向传播的结果保存在变量net中
net = '上一层的的输出节点矩阵'
# 为一个Inception模块声明一个统一的变量命名空间
with tf.variable_scope('Mixed_7c'):
# 给Inception模块中每一条路径声明一个命名空间
with tf.variable_scope('Branch_0'):
# 实现一个过滤器边长为1,深度为320的卷积层
branch_0 = slim.conv2d(net, 320, [1, 1], scope='Conv2d_Oa_1x1')
# Inception模块中第二条路径,这条计算路径上的结构本身也是一个Inception结构
with tf.variable_scope('Branch_1'):
branch_1 = slim.conv2d(net, 384, [1, 1], scope='Conv2d_Oa_1x1')
# tf.concat函数可以将多个矩阵拼接起来
# tf.concat 函数的第一个参数指定了拼接的维度,这里给出的"3"代表了矩阵是在深度这个维度上进行拼接
branch_1 = tf.concat(3, [
slim.conv2d(branch_1, 384, [1, 3], scope='Conv2d_Ob_1x3'),
slim.conv2d(branch_1, 384, [3, 1], scope='Conv2d_Oc_3x1')])
# Inception模块中第三条路径,此计算路径也是一个Inception结构
with tf.variable_scope('Branch_2'):
branch_2 = slim.conv2d(
net, 448, [1, 1], scope='Conv2d_Oa_1x1')
branch_2 = slim.conv2d(
branch_2, 384, [3, 3], scope=' Conv2d_Ob_3x3')
branch_2 = tf.concat(3, [
slim.conv2d(branch_2, 384,
[1, 3], scope='Conv2d_Oc_1x3'),
slim.conv2d(branch_2, 384,
[3, 1], scope='Conv2d_Od 3x1')])
# Inception模块中第四条路径
with tf.variable_scope('Branch_3'):
branch_3 = slim.avg_pool2d(
net, [3, 3], scope='AvgPool_Oa_3x3')
branch_3 = slim.conv2d(
branch_3, 192, [1, 1], scope='Conv2d_Ob_1x1')
# 当前Inception模块的最后输出是由上面4个计算结果拼攘得到的
net = tf.concat(3, [branch_0, branch_1, branch_2, branch_3])
- 6.5 将原始的图像数据整理成模型需要的输入数据
#!/usr/bin/python
# -*- coding: utf-8 -*-
import glob
import os.path
import numpy as np
import tensorflow as tf
from tensorflow.python.platform import gfile
INPUT_DATA = '/path/to/flower_photos'
OUTPUT_FILE = '/path/to/flower_processed_data.npy' # 输出文件地址,这里先通过numpy来保存
# 测试数据和验证数据比例
VALIDATION_PERCENTAGE = 10
TEST_PERCENTAGE = 10
# 读取数据并将数据分割成训练数据、验证数据和测试数据
def create_image_lists(sess, testing_percentage, validation_percentage):
sub_dirs = [x[0] for x in os.walk(INPUT_DATA)]
is_root_dir = True
# 初始化各个数据集
training_images = []
training_labels = []
testing_images = []
testing_labels = []
validation_images = []
validation_labels = []
current_label = 0
# 读取所有的子目录
for sub_dir in sub_dirs:
if is_root_dir:
is_root_dir = False
continue
# 获取一个子目录中所有的图片文件
extensions = ['jpg', 'jpeg', 'JPG', 'JPEG']
file_list = []
dir_name = os.path.basename(sub_dir)
for extension in extensions:
file_glob = os.path.join(INPUT_DATA, dir_name, '*.' + extension)
file_list.extend(glob.glob(file_glob))
if not file_list: continue
# 处理图片数据
for file_name in file_list:
# 读取并解析图片,将图片转化为299×299以便inception-v3模型来处理
image_raw_data = gfile.FastGFile(file_name, 'rb').read()
image = tf.image.decode_jpeg(image_raw_data)
if image.dtype != tf.float32:
image = tf.image.convert_image_dtype(image, dtype=tf.float32)
image = tf.image.resize_images(image, (299, 299))
image_value = sess.run(image)
# 随机划分数据集
chance = np.random.randint(100)
if chance < validation_percentage:
validation_images.append(image_value)
validation_labels.append(current_label)
elif chance < (testing_percentage + validation_percentage):
testing_images.append(image_value)
testing_labels.append(current_label)
else:
training_images.append(image_value)
training_labels.append(current_label)
current_label += 1
# 将训练数据随机打乱以获得更好的训练效果
state = np.random.get_state()
np.random.shuffle(training_images)
np.random.set_state(state)
np.random.shuffle(training_labels)
return np.asarray([training_images, training_labels,
validation_images, validation_labels,
testing_images, testing_labels])
# 数据整理主函数
def main():
with tf.Session() as sess:
processed_data = create_image_lists(
sess, TEST_PERCENTAGE, VALIDATION_PERCENTAGE)
# 通过numpy格式保存处理后的数据
np.save(OUTPUT_FILE, processed_data)
if __name__ == '__main__':
main()
- 6.5 迁移学习过程
#!/usr/bin/python
# -*- coding: utf-8 -*-
import glob
import os.path
import numpy as np
import tensorflow as tf
from tensorflow.python.platform import gfile
import tensorflow.contrib.slim as slim
# 加载通过TensorFlow-Slim定义好的inception_v3模型
import tensorflow.contrib.slim.python.slim.nets.inception_v3 as inception_v3
# 处理好之后的数据文件
INPUT_DATA = '/path/to/flower_processed_data.npy'
# 保存训练好的模型的路径,这里可以将使用新数据训练得到的完整模型保存下来
# 如果计算资源充足,还可以在训练完最后的全连接层之后再训练所有网络层
# 这样可以使得新模型更加贴近新数据
TRAIN_FILE = '/path/to/save_model'
# 谷歌提供的训练好的模型文件地址
CKPT_FILE = '/path/to/inception_v3.ckpt'
# 定义训练中使用的参数
LEARNING_RATE = 0.0001
STEPS = 300
BATCH = 32
N_CLASSES = 5
# 不需要从谷歌训练好的模型中加载的参数
# 这里就是最后的全连接层,因为在新的问题中要重新训练这一层中的参数
# 这里给出的是参数的前缀
CHECKPOINT_EXCLUDE_SCOPES = 'InceptionV3/Logits,InceptionV3/AuxLogits'
# 需要训练的网络层参数名称,在fine-tuning的过程中就是最后的全连接层
# 这里给出的是参数的前缀
TRAINABLE_SCOPES = 'InceptionV3/Logits,InceptionV3/AuxLogits'
# 获取所有需要从谷歌训练好的模型中加载的参数
def get_tuned_variables():
exclusions = [scope.strip() for scope in CHECKPOINT_EXCLUDE_SCOPES.split(',')]
variables_to_restore = []
# 枚举inception-v3模型中所有的参数,然后判断是否需要从加载列表中移除
for var in slim.get_model_variables():
excluded = False
for exclusion in exclusions:
if var.op.ηame.startswith(exclusion):
excluded = True
break
if not excluded:
variables_to_restore.append(var)
return variables_to_restore
# 获取所有需要训练的变量列表
def get_trainable_variables():
scopes = [scope.strip() for scope in TRAINABLE_SCOPES.split(',')]
variables_to_train = []
# 枚举所有需要训练的参数前缀,并通过这些前缀找到所有的参数
for scope in scopes:
variables = tf.get_collection(
tf.GraphKeys.TRAINABLE_VARIABLES, scope)
variables_to_train.extend(variables)
return variables_to_train
def main():
# 加载预处理好的数据
processed_data = np.load(INPUT_DATA)
training_images = processed_data[0]
n_training_example = len(training_images)
training_labels = processed_data[1]
validation_images = processed_data[2]
validation_labels = processed_data[3]
testing_images = processed_data[4]
testing_labels = processed_data[5]
print("%d training examples, %d validation examples and %d "
"testing examples." % (n_training_example, len(validation_labels), len(testing_labels)))
# 定义inception-v3的输入,images为输入图片,labels为每一张图片对应的标签
images = tf.placeholder(tf.float32, [None, 299, 299, 3], name='input_images')
labels = tf.placeholder(tf.int64, [None], name='labels')
# 定义inception-v3模型
# 因为谷歌给出的只有模型参数取值,所以这里需要在这个代码中定义inception-v3的模型结构
# 虽然理论上需要区分训练和测试中使用的模型,也就是说在测试时应该使用is_training = False
# 但是因为预先训练好的inception-v3模型中使用的batch normalization参数与新的数据会有差异,导致结果很差
# 所以这里直接使用同一个模型来进行测试
with slim.arg_scope(inception_v3.inception_v3_arg_scope()):
logits, _ = inception_v3.inception_v3(images, num_classes=N_CLASSES)
# 获取需要训练的变量
trainable_variables = get_trainable_variables()
# 定义交叉熵损失,注意在模型定义的时候己经将正则化损失加入损失集合了
tf.losses.softmax_cross_entropy(tf.one_hot(labels, N_CLASSES), logits, weights=1.0)
# 定义训练过程,这里minimize的过程中指定了需要优化的变量集合
train_step = tf.train.RMSPropOptimizer(LEARNING_RATE).minimize(tf.losses.get_total_loss())
# 计算正确率
with tf.name_scope('evaluation'):
correct_prediction = tf.equal(tf.argmax(logits, 1), labels)
evaluation_step = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
# 定义加载模型的函数
load_fn = slim.assign_from_checkpoint_fn(CKPT_FILE, get_tuned_variables(), ignore_missing_vars=True)
# 定义保存新的训练好的模型的函数
saver = tf.train.Saver()
with tf.Session() as sess:
# 初始化没有加载进来的变量,注意这个过程一定要在模型加载之前,否则初始化过程会将已经加载好的变量重新赋值
init = tf.global_variables_initializer()
sess.run(init)
# 加载谷歌已经训练好的模型
print('Loading tuned variables from %s ' % CKPT_FILE)
load_fn(sess)
start = 0
end = BATCH
for i in range(STEPS):
# 运行训练过程,这里不会更新全部的参数,只会更新指定的部分参数
sess.run(train_step, feed_dict={images: training_images[start:end], labels: training_labels[start:end]})
# 输出日志
if i % 30 == 0 or i + 1 == STEPS:
saver.save(sess, TRAIN_FILE, global_step=i)
validation_accuracy = sess.run(evaluation_step, feed_dict={
images: validation_images, labels: validation_labels})
print('Step % d: Validation accuracy = %.1f%%' % (i, validation_accuracy * 100.0))
# 因为在数据预处理的时候已经做过了打乱数据的操作,所以这里只需要顺序使用训练数据就好
start = end
if start == n_training_example:
start = 0
end = start + BATCH
if end > n_training_example:
end = n_training_example
# 在最后的测试数据上测试正确率
test_accuracy = sess.run(evaluation_step, feed_dict={images: testing_images, labels: testing_labels})
print('Final test accuracy = %.1f%%' % (test_accuracy * 100))
if __name__ == '__main__':
tf.app.run()