1 tensorflow中数据加载的通常方式
1.1 feed_dict方式
这种方式是网络上介绍得最多的方式,也是目前很多厂商技术人员经常使用的方式。它先使用tf.placeholder
定位数据占位符,在Graph中读取数据,数据直接嵌入到Graph中,在会话中运行该Graph,使用feed_dict方式喂数据。这种方式在数据量较小,或预处理数据不那么复杂时时可以接受的,当数据量较大,或者预处理数据较为复杂耗时时,Graph的传输就会遇到效率低下的问题。因此这种方式并不推荐。
1.2 队列
与feed_dict的单线程相比,队列的实现方式利用了多线程思想,减少了GPU等待数据的时间,使用了两个线程(文件名队列和内存队列)分别执行数据的读入和数据计算。文件名队列源源不断的将硬盘中的数据读取内存,内存队列负责给GPU传输数据,所需数据直接从内存队列中获取。两个线程互不干扰,同时运行。
但这种方式对于不同类型的数据处理方式或提供的API都不一样,如二进制文件一种API,图像数据一种API,文本数据一种API,并且需要做线程管理,稍显麻烦。
1.3 tf.data.Dataset方式
tensorflow官方推荐使用次方式,它使用多线程/进程和数据预加载的实现方式,让GPU不再为等待数据而空闲。
2 为什么要解决数据加载问题
上一章已经交代了目前大多数的任务都使用的时feed_dict方式,而这种方式是最慢的,最浪费计算资源的一种方式,下图就是使用feed_dict方式时的CPU和GPU之间的协作状态。
众所周知GPU极擅长矩阵运算,就上图而言,其实GPU处理的时间,也就是training的时间,非常短,大部分的时间都花在了CPU的预处理/加载批数据上面了,这就直接导致CPU利用率超高,GPU利用率极低,造成了GPU资源浪费。
而tf.data
方式利用多线程/进程和预加载技术可以完美解决这一问题,下图为使用tf.data
方式的CPU和GPU之间的协作状态图。
如此,让GPU只用等待一次数据加载的时间,往后GPU和CPU同步处理,互不干扰。这时候CPU利用率也是超高,但GPU利用率通常会提升数倍到数十倍。在我们的实际使用中,发现相比使用feed_dict方式,GPU利用率始终在个位数徘徊,使用了tf.data
方式后GPU使用率在整个训练过程中都维持在70%-85%之间。
3 tf.data.Dataset的使用
使用tf.data
方式大致有以下步骤:
- 创建
tf.data.Dataset
对象 tf.data.Dataset
对象配置- 迭代器创建
- 会话
3.1 创建Dataset对象
tensorflow提供了多种创建Dataset对象的方式,可以通过numpy数组、文本数据、CSV文件、tensor等创建。
3.1.1 from_tensor_slices
接受单个或多个numpy数组或tensor对象
import numpy as np
# 数据准备
data = np.arange(10)
# 创建
dataset = tf.data.Dataset.from_tensor_slices(data)
3.1.2 from_tensors
同样接受单个或多个numpy数组或tensor对象
data = tf.arange(10)
dataset = tf.data.Dataset.from_tensors(data)
3.1.3 from_generator
从generator创建
def generator():
for i in range(10):
yield 2*i
dataset = tf.data.Dataset.from_generator(generator, (tf.int32))
3.2 Dataset对象配置
3.2.1 Batches
在之前为了避免将数据全部读入造成内存溢出,我们一般给模型喂数据时都是一个batch一个batch的喂,这里也不例外。
data = np.arange(10,40)
dataset = tf.data.Dataset.from_tensor_slices(data)
# 配置batch为10
dataset = dataset.batch(10)
3.2.2 Zip
和python的内置函数zip一样,这里的作用是粘合两个dataset对象。适用于标签和数据分离的场景。
datax = np.arange(10,20)
datay = np.arange(11,21)
datasetx = tf.data.Dataset.from_tensor_slices(datax)
datasety = tf.data.Dataset.from_tensor_slices(datay)
# 粘合
dcombined = tf.data.Dataset.zip((datasetx, datasety))
3.2.3 Repeat
重复使用数据
dataset = tf.data.Dataset.from_tensor_slices(tf.range(10))
dataset = dataset.repeat(count=2)
3.2.4 Map
同样的,对比python的内置函数map,它是对Dataset对象中的每一个元素进行操作。一般是在将数据喂入模型之前使用。
def map_fnc(x):
return x*2;
data = np.arange(10)
dataset = tf.data.Dataset.from_tensor_slices(data)
dataset = dataset.map(map_fnc)
3.3 创建迭代器
创建迭代器的目的就是为了使用我们的数据,可以想象Dataset就是一个水库,数据就是水库中的水,迭代器就是一根管子,通过管子数据才能被不断取出。迭代器拥有get_next
方法,可以在计算图中创建一个OP节点,在会话中运行后就会返回实际的数据。当Dataset被读完后,自动抛出tf.errors.OutOfRangeError
异常。Tensorflow提供多种创建迭代器的方式(对比多种规格的水管),下面一一介绍。
3.3.1 One-shot迭代器
单次迭代器,就像是一次性水管,不需要任何初始化。
data = np.arange(10,15)
# 创建dataset
dataset = tf.data.Dataset.from_tensor_slices(data)
# 创建迭代器
iterator = dataset.make_one_shot_iterator()
3.3.2 Initializable迭代器
可初始化的迭代器,使用该迭代器必须在会话中对其初始化,可以通过palceholder占位符向它传入初始化数据。
# 定义两个表示最小值和最大值的占位符
min_val = tf.placeholder(tf.int32, shape=[])
max_val = tf.placeholder(tf.int32, shape=[])
data = tf.range(min_val, max_val)
dataset = tf.data.Dataset.from_tensor_slices(data)
iterator = dataset.make_initializable_iterator()
next_ele = iterator.get_next()
# 创建会话
with tf.Session() as sess:
# 初始化迭代器,初始化数据为{min_val:10, max_val:15}
sess.run(iterator.initializer, feed_dict={min_val:10, max_val:15})
try:
while True:
val = sess.run(next_ele)
print(val)
except tf.errors.OutOfRangeError:
pass
# 初始化迭代器,初始化数据为{min_val:1, max_val:10}
sess.run(iterator.initializer, feed_dict={min_val:1, max_val:10})
try:
while True:
val = sess.run(next_ele)
print(val)
except tf.errors.OutOfRangeError:
pass
3.3.3 Reinitializable迭代器
可重复初始化的迭代器,即可看作可以从不同水库抽水的管子。这种迭代器可以从不同的Dataset对象创建并初始化(注意:Dataset要有相同结构),比如训练过程中的训练集和验证集。
def map_fnc(ele):
return ele*2
# 定义两个表示最小值和最大值的占位符
min_val = tf.placeholder(tf.int32, shape=[])
max_val = tf.placeholder(tf.int32, shape=[])
data = tf.range(min_val, max_val)
# 训练集和验证集的Dataset对象创建
train_dataset = tf.data.Dataset.from_tensor_slices(data)
val_dataset = tf.data.Dataset.from_tensor_slices(data).map(map_fnc)
# 迭代器创建
iterator=tf.data.Iterator.from_structure(train_dataset.output_types,train_dataset.output_shapes)
# 准备初始化迭代器
train_initializer = iterator.make_initializer(train_dataset)
val_initializer = iterator.make_initializer(val_dataset)
next_ele = iterator.get_next()
# 创建会话
with tf.Session() as sess:
# 初始化迭代器
sess.run(train_initializer, feed_dict={min_val:10, max_val:15})
try:
while True:
val = sess.run(next_ele)
print(val)
except tf.errors.OutOfRangeError:
pass
# 再初始化迭代器, 注意initializer是不一样的,这里连其他水库去了
sess.run(val_initializer, feed_dict={min_val:1, max_val:10})
try:
while True:
val = sess.run(next_ele)
print(val)
except tf.errors.OutOfRangeError:
pass
3.3.4 Feedable迭代器
tensorflow中最美妙的就是feeding机制了,它决定了一些东西可以事先在计算图中定义好,在会话运行时动态填充,当然这就包括了迭代器。其思想就是不同的Dataset对象用不同的迭代器,可以通过feeding机制动态的来决定。
通常无论是在机器学习还是深度学习当中,训练集、验证集、测试集是大家绕不开的话题,但偏偏它们要分离开来,偏偏它们的数据类型又一致,因此经常我们要写同样的重复的代码,而这一机制就是解决重复代码的问题,同时又将这些数据集操作分离得很清晰。就像不同的水库使用不同规格的水管抽水,不再使用同一根了。在具体实现上与reinitilizable iterator 类似,并且在切换数据集的时候不需要在开始的时候初始化iterator。
def map_fnc(ele):
return ele*2
# 定义两个表示最小值和最大值的占位符
min_val = tf.placeholder(tf.int32, shape=[])
max_val = tf.placeholder(tf.int32, shape=[])
data = tf.range(min_val, max_val)
handle = tf.placeholder(tf.string, shape=[])
# Dataset对象创建
train_dataset = tf.data.Dataset.from_tensor_slices(data)
val_dataset = tf.data.Dataset.from_tensor_slices(data).map(map_fnc)
test_dataset = tf.data.Dataset.from_tensor_slices(tf.range(10,15))
# 迭代器创建,训练集和验证集具有相同的结构
train_val_iterator = tf.data.Iterator.from_structure(train_dataset.output_types , train_dataset.output_shapes)
# 测试集一般没有标签,因此直接使用一次性的就可以
test_iterator = test_dataset.make_one_shot_iterator()
# 定义feedable迭代器,达到数据集切换的目的
iterator = tf.data.Iterator.from_string_handle(handle, train_dataset.output_types, train_dataset.output_shapes)
# 准备初始化迭代器
train_initializer = train_val_iterator.make_initializer(train_dataset)
val_initializer = train_val_iterator.make_initializer(val_dataset)
next_ele = iterator.get_next()
with tf.Session() as sess:
# 上面定义了iterator, 这里要让程序自己决定在运行中使用哪一个Dataset
train_val_handle = sess.run(train_val_iterator.string_handle())
test_handle = sess.run(test_iterator.string_handle())
# 初始化训练的迭代器
sess.run(train_initializer, feed_dict={min_val:10, max_val:15})
try:
while True:
# runnext_ele, 喂的是handle,有些不一样
val = sess.run(next_ele, feed_dict={handle:train_val_handle})
print(val)
except tf.errors.OutOfRangeError:
pass
# 初始化验证的迭代器
sess.run(val_initializer, feed_dict={min_val:1, max_val:10})
try:
while True:
# runnext_ele, 喂的是handle,有些不一样
val = sess.run(next_ele, feed_dict={handle:train_val_handle})
print(val)
except tf.errors.OutOfRangeError:
pass
# 测试,一次性的迭代器不需要初始化
try:
while True:
# runnext_ele, 喂的是handle,有些不一样
val = sess.run(next_ele, feed_dict={handle:test_handle})
print(val)
except tf.errors.OutOfRangeError:
pass
4 示例
在这里我们定义一个模型,以LeNet-5模型为例,这里仅仅是一个示例。
from tensorflow.examples.tutorials.mnist import input_data
# MNIST数据集包含了60000张训练图像和10000张测试图像,每一张图像大小为28×28,为单通道图像
mnist = input_data.read_data_sets("MNIST_data/", reshape=False, one_hot = True)
X_train, y_train = mnist.train.images, mnist.train.labels
X_val, y_val = mnist.validation.images, mnist.validation.labels
X_test, y_test = mnist.test.images, mnist.test.labels
X_train = np.pad(X_train, ((0,0), (2,2), (2,2), (0,0)), 'constant')
X_val = np.pad(X_val, ((0,0), (2,2), (2,2), (0,0)), 'constant')
X_test = np.pad(X_test, ((0,0), (2,2), (2,2), (0,0)), 'constant')
def forward_pass(X):
W1 = tf.get_variable("W1", [5,5,1,6], initializer = tf.contrib.layers.xavier_initializer(seed=0))
# for conv layer2
W2 = tf.get_variable("W2", [5,5,6,16], initializer = tf.contrib.layers.xavier_initializer(seed=0))
Z1 = tf.nn.conv2d(X, W1, strides = [1,1,1,1], padding='VALID')
A1 = tf.nn.relu(Z1)
P1 = tf.nn.max_pool(A1, ksize = [1,2,2,1], strides = [1,2,2,1], padding='VALID')
Z2 = tf.nn.conv2d(P1, W2, strides = [1,1,1,1], padding='VALID')
A2= tf.nn.relu(Z2)
P2= tf.nn.max_pool(A2, ksize = [1,2,2,1], strides=[1,2,2,1], padding='VALID')
P2 = tf.contrib.layers.flatten(P2)
Z3 = tf.contrib.layers.fully_connected(P2, 120)
Z4 = tf.contrib.layers.fully_connected(Z3, 84)
Z5 = tf.contrib.layers.fully_connected(Z4,10, activation_fn= None)
return Z5
def model(X,Y):
logits = forward_pass(X)
cost = tf.reduce_mean( tf.nn.softmax_cross_entropy_with_logits_v2(logits=logits, labels=Y))
optimizer = tf.train.AdamOptimizer(learning_rate=0.0009)
learner = optimizer.minimize(cost)
correct_predictions = tf.equal(tf.argmax(logits,1), tf.argmax(Y,1))
accuracy = tf.reduce_mean(tf.cast(correct_predictions, tf.float32))
return (learner, accuracy)
上面的代码已经将模型搭建好了,结合之前对tf.data
和迭代器的介绍,我们采用feedable迭代器消费数据。
epochs = 10
batch_size = 64
tf.reset_default_graph()
# 定义数据 将图像resize成32×32
X_data = tf.placeholder(tf.float32, [None, 32,32,1])
Y_data = tf.placeholder(tf.float32, [None, 10])
# 创建Dataset对象,并定义batch size
train_dataset = tf.data.Dataset.from_tensor_slices((X_data, Y_data)).batch(batch_size)
val_dataset = tf.data.Dataset.from_tensor_slices((X_data, Y_data)).batch(batch_size)
test_dataset = tf.data.Dataset.from_tensor_slices((X_test, y_test.astype(np.float32)).batch(batch_size)
# 定义handle, 让会话运行后程序自动判断使用哪一个Dataset
handle = tf.placeholder(tf.string, shape=[])
iterator = tf.data.Iterator.from_string_handle(handle, train_dataset.output_types, train_dataset.output_shapes)
X_batch , Y_batch = iterator.get_next()
# 定义模型, 返回的是优化器和精度
(learner, accuracy) = model(X_batch, Y_batch)
# 创建迭代器, 训练集和验证集拥有相同的数据结构
train_val_iterator = tf.data.Iterator.from_structure(train_dataset.output_types, train_dataset.output_shapes)
train_iterator = train_val_iterator.make_initializer(train_dataset)
# 准备初始化,虽然切换数据时不需要初始化,但还是得初始化训练集、验证集的迭代器,以及在会话中决定他们如何切换
val_iterator = train_val_iterator.make_initializer(val_dataset)
test_iterator = test_dataset.make_one_shot_iterator()
# 创建会话
with tf.Session() as sess:
# 需要初始化全局参数,这是必须的
sess.run(tf.global_variables_initializer())
# 程序自己决定在运行中使用哪一个Dataset
train_val_string_handle = sess.run(train_val_iterator.string_handle())
test_string_handle = sess.run(test_iterator.string_handle())
for epoch in range(epochs):
# 模型训练
# 初始化Reinitializable迭代器
sess.run(train_iterator, feed_dict={X_data:X_train, Y_data:y_train})
total_train_accuracy = 0
no_train_examples = len(y_train)
try:
while True:
temp_train_accuracy, _ = sess.run([accuracy, learner], feed_dict={handle:train_val_string_handle})
total_train_accuracy += temp_train_accuracy*batch_size
except tf.errors.OutOfRangeError:
pass
# 模型验证
# 初始化Reinitializable迭代器
sess.run(val_iterator, feed_dict={X_data:X_val, Y_data:y_val})
total_val_accuracy = 0
no_val_examples = len(y_val)
try:
while True:
temp_val_accuracy, _ = sess.run([accuracy, learner], feed_dict={handle:train_val_string_handle})
total_val_accuracy += temp_val_accuracy*batch_size
except tf.errors.OutOfRangeError:
pass
print('Epoch %d' % (epoch+1))
print("---------------------------")
print('Training accuracy is {}'.format(total_train_accuracy/no_train_examples))
print('Validation accuracy is {}'.format(total_val_accuracy/no_val_examples))
print("Testing the model --------")
total_test_accuracy = 0
no_test_examples = len(y_test)
try:
while True:
temp_test_accuracy, _ = sess.run([accuracy, learner], feed_dict={handle:test_string_handle})
total_test_accuracy += temp_test_accuracy*batch_size
except tf.errors.OutOfRangeError:
pass
print('Testing accuracy is {}'.format(total_test_accuracy/no_test_examples))
5 名词解释
5.1 计算图
计算图是tensorflow中的表达指令依赖关系的一种方式,它是由数据(tensor)和操作(OP节点)组成的。
5.2 Tensor
中文名叫张量是tensorflow中重要的数据形式。可以对比数组的定义,常量对比tensorflow中就称为纯量,二阶数组对比tensorflow就称为二阶张量,依次类推。
5.3 会话
tensorflow的底层是由C++开发的,提供python接口供上层开发使用,可以理解其目的就是为python和C++搭建资源沟通的桥梁。实际上会话之前定义的一些参数都是在即一个计算图中定义的,只有在会话中使用run或eval函数才能真正让这些数据流动起来,让程序运行起来,它是程序获得计算资源的地方,因此使用后一定记得删除会话。