掌握一个深度学习框架的用法,从训练一个模型的流程来看,需要掌握以下几个步骤:
1. 数据的处理,包括训练数据转成网络的输入,模型参数的存储与读取
2. 网络结构的定义,包括网络主体的搭建以及loss的定义
3. solver的定义,也就是如何对网络进行优化
4. 模型评估的定义,也就是对模型训练结果进行评测
这篇博文主要介绍第一部分,数据处理中的训练数据读取部分
tensorflow当中读取数据的方式一共有三种:
1. 供给数据(feeding):在定义graph的时候,可以用placeholder来表示其中的某些节点,在运行的时候,通过向这些节点填入数据来运行整个graph
2. 预加载数据:仅仅适用于可以完全加载到存储器中的小的数据集
3. 从文件中加载数据:在图运行的时候,通过一个pipeline从文件中读取数据,作为网络的输入
前面两种方法并不适用于平时的大数据集处理的情况,因此这里我只介绍第三种方法:从文件中加载数据,重点介绍通过tensorflow特有的tfrecords进行存储和读取的方法
tfrecords是一种二进制文件,我们可以实现把image文件夹里面的图片和label文件夹里面的标签读进来制作成tfrecords文件,之后就都使用tfrecords进行数据的读取,能够更好的利用内存,更方便复制和移动,类似caffe里面的hdf5,下面我们将详细介绍如果制作tfrecords文件,以及如何从tfrecords文件里面读取数据
每一个训练样例在tfrecords里面叫一个example,tensorflow使用名为tf.train.Example的协议来存储训练样例,一个example就类似一个词典(dict),它的key名为feature,每一个feature对应的值是tensorflow预定义好的Feature message(必须要是ByteList,FloatList以及Int64List中的一种数据类型),这样的话一个训练样例就可以表示为很多键-值对的组合。
example通过SerializeToString()方法将样例序列化成字符串存储,tensorflow通过TFRecordWriter将这些序列化之后的字符串存成tfrecord形式
下面表示一个制作tfrecords的例子
#image_set可以选择'训练'或者'测试',images表示所有的图片数据,labels表示所有的标签,包括训练和测试
def convert(self, image_set, images, labels):
filename = os.path.join(self._image_set_path, 'dianli_{:s}.tfrecords'.format(image_set))
print('writing ', filename)
#创建一个tfrecords的writer
writer = tf.python_io.TFRecordWriter(filename)
if image_set == 'train':
names = self.train_list
elif image_set == 'test':
names = self.test_list
else:
raise ValueError
#max_object表示一张图最多有多少个object,设置这个参数是为了保证写入时所有label项的形状都是固定的
max_object_num = cfg.TRAIN.MAX_OBJECT
for ix, name in enumerate(names):
image_raw = images[ix].tostring()
single_label = labels[ix]
if single_label.shape[0] > max_object_num:
raise ValueError('number of gt object is {:d}, more than max object num!'.format(single_label.shape[0]))
#加0确保label维度固定,这样后面就可以使用tf.train.shuffle_batch
non_obj_label = np.zeros((max_object_num - single_label.shape[0], 6))
comp_label = np.vstack((single_label, non_obj_label))
#这里用(cls,xc,yc,xita,w,h)表示一个斜框标签,cls表示框里面目标的类别
cls = comp_label[:, 0].tolist()
xc = comp_label[:, 1].tolist()
yc = comp_label[:, 2].tolist()
xita = comp_label[:, 3].tolist()
w = comp_label[:, 4].tolist()
h = comp_label[:, 5].tolist()
#image_name和image序列化成bytelist,其他的按照本来的数据类型选择序列化成float或者int64
example = tf.train.Example(features=tf.train.Features(feature={
'image_name': self._bytes_feature(names[ix].encode('utf8')),
'height': self._int64_feature(images[ix].shape[0]),
'width': self._int64_feature(images[ix].shape[1]),
'depth': self._int64_feature(images[ix].shape[2]),
'xc': self._float_feature(xc),
'yc': self._float_feature(yc),
'xita': self._float_feature(xita),
'w': self._float_feature(w),
'h': self._float_feature(h),
'cls': self._int64_feature([int(c) for c in cls]),
'image_raw': self._bytes_feature(image_raw)
}))
#存储一个example
writer.write(example.SerializeToString())
writer.close()
print('finish writing ', filename)
为了验证我们的tfrecords文件是否写入正确,我们可以写一个检测的测试程序,读取里面的example看看
#使用tf_record_iterator来遍历每一个example
with tf.Session() as sess:
example = tf.train.Example()
#train_record表示训练的tfrecords文件的路径
record_iterator = tf.python_io.tf_record_iterator(path=train_record)
for record in record_iterator:
example.ParseFromString(record)
f = example.features.feature
#解析一个example
image_name = f['image_name'].bytes_list.value[0]
image_raw = f['image_raw'].bytes_list.value[0]
xc = np.array(f['xc'].float_list.value)[:, np.newaxis]
yc = np.array(f['yc'].float_list.value)[:, np.newaxis]
xita = np.array(f['xita'].float_list.value)[:, np.newaxis]
w = np.array(f['w'].float_list.value)[:, np.newaxis]
h = np.array(f['h'].float_list.value)[:, np.newaxis]
label = np.hstack((xc, yc, xita, w, h))
#将label画在image上,同时打印image_name,查看三者是不是对应上的
print(image_name.encode('utf-8'))
img_1d = np.fromstring(image_raw, dtype=np.uint8)
img_2d = img_1d.reshape((480, 480, -1))
draw_bboxes(img_2d, label)
我们其实可以直接用上面测试的程序来一个一个读取tfrecords文件里面的example,作为网络的输入,但是这样一来,网络训练一个batch的时间就包括从tfrecords里面读取一个batch数据的时间,以及这个batch做前向反向的时间,显然数据读取的时间是可以避免的。
最好的情况是,网络每次做完一次前向反向之后,不需要等待数据的读取,也就是说我们可以开两个线程,一个线程在进行网络的前向反向计算,另外一个线程专门进行数据的读取,这样的话两个线程都不会存在所谓的等待状态。
下面将分别介绍tfrecords文件的解析以及如何高效得进行数据的读取
解析部分代码如下所示:
#filename_queue表示一个文件名队列,后面会讲到,比如我需要解析train.tfrecords文件的话,传入的就应该是训练的文件名队列
def read_and_decode(self, filename_queue):
"""
read and decode
"""
#创建一个tfrecords文件读取器并读取文件
reader = tf.TFRecordReader()
_, serialized_example = reader.read(filename_queue)
#tensorflow使用parse_single_example来解析example
features = tf.parse_single_example(
serialized_example,
features={
#对于单个元素的变量,我们使用FixlenFeature来读取,需要指明变量存储的数据类型
# meta data
'image_name': tf.FixedLenFeature([], tf.string),
'height': tf.FixedLenFeature([], tf.int64),
'width': tf.FixedLenFeature([], tf.int64),
'depth': tf.FixedLenFeature([], tf.int64),
#对于list类型的变量,我们使用VarLenFeature来读取,同样需要指明读取变量的类型
# label part
'xc': tf.VarLenFeature(tf.float32),
'yc': tf.VarLenFeature(tf.float32),
'xita': tf.VarLenFeature(tf.float32),
'w': tf.VarLenFeature(tf.float32),
'h': tf.VarLenFeature(tf.float32),
'cls': tf.VarLenFeature(tf.int64),
# dense data
'image_raw': tf.FixedLenFeature([], tf.string),
}
)
# meta data
image_name = tf.cast(features['image_name'], tf.string)
height = tf.cast(features['height'], tf.int32)
width = tf.cast(features['width'], tf.int32)
depth = tf.cast(features['depth'], tf.int32)
#VarLenFeature得到的是sparse tensor需要转换一下
# label part
xc = tf.sparse_tensor_to_dense(features['xc'])
yc = tf.sparse_tensor_to_dense(features['yc'])
xita = tf.sparse_tensor_to_dense(features['xita'])
w = tf.sparse_tensor_to_dense(features['w'])
h = tf.sparse_tensor_to_dense(features['h'])
cls = tf.sparse_tensor_to_dense(features['cls'])
xc = tf.reshape(xc, shape=[cfg.TRAIN.MAX_OBJECT, 1])
yc = tf.reshape(yc, shape=[cfg.TRAIN.MAX_OBJECT, 1])
xita = tf.reshape(xita, shape=[cfg.TRAIN.MAX_OBJECT, 1])
w = tf.reshape(w, shape=[cfg.TRAIN.MAX_OBJECT, 1])
h = tf.reshape(h, shape=[cfg.TRAIN.MAX_OBJECT, 1])
cls = tf.reshape(cls, shape=[cfg.TRAIN.MAX_OBJECT, 1])
cls = tf.cast(cls, tf.float32)
label = tf.concat(values=[cls, xc, yc, xita, w, h], axis=1)
#set_shape是为了固定label的维度,方便后面使用tf.train.shuffle_batch
label.set_shape([cfg.TRAIN.MAX_OBJECT, 6])
#对于图像数据需要使用decode_raw解码,同样也需要set_shape
# dense data
image = tf.decode_raw(features['image_raw'], tf.uint8)
image_shape = tf.stack([height, width, 3])
image = tf.reshape(image, image_shape)
image.set_shape([cfg.TRAIN.IMG_SIZE[0], cfg.TRAIN.IMG_SIZE[1], 3])
return image, label
下面再详细介绍一下高效读取的方法
前面说了,最高效的读取方法应该是一个线程专门读取数据,一个线程专门做训练(前向反向传播),读取数据的线程应该维护一个队列(queue),不断得读取数据,压入队列,tensorflow里面常用的是FIFO queue。训练的线程每次从这个队列里面读取一个batch的训练样例用来训练。
tensorflow里面用的方法比上面的方法要稍微复杂一点,区别在于其维护了两个队列,第一个队列存的是训练样例的文件名,第二个队列存的才是真正的训练样例。整个读取的流程如下图所示:
我们可以使用tf.string_input_produce创建上述的文件名队列(filename queue),在创建的时候,我们可以指定参数num_epoch的值,这个值显然是控制文件名队列里的文件名一共用来训练几个epoch,然后我们可以通过上面read_and_decode函数读取文件名队列的文件名,进行解析,将解析得到的训练样例压入训练样例队列(example queue)。最后我们进行训练的时候每次可以从训练样例队列里面读取一个batch,可以使用tf.train.shuffle_batch来获取一个随机打乱顺序的batch,代码样例如下:
def inputs(dataset_, image_set, batch_size, num_epochs):
"""
read batch of data fro tf-record files for epoch
:param dataset_: constructed dataset
:param image_set:train or test
:param batch_size:
:param num_epochs:
:return:images and labels for a single batch
"""
train_record = os.path.join(dataset_.image_set_path, 'dianli_train.tfrecords')
test_record = os.path.join(dataset_.image_set_path, 'dianli_test.tfrecords')
if not (os.path.exists(train_record) and os.path.exists(test_record)):
dataset_.create_tfrecord()
filename = train_record if image_set == 'train' else test_record
filename_queue = tf.train.string_input_producer([filename], num_epochs=num_epochs)
image, label = dataset_.read_and_decode(filename_queue)
#num_thread可以选择用几个线程同时读取example queue,min_after_dequeue表示读取一次之后队列至少需要剩下的样例数目,capacity表示队列的容量
images, sparse_labels = tf.train.shuffle_batch([image, label], batch_size=batch_size, num_threads=1, capacity=1000 + 3 * batch_size, min_after_dequeue=1000)
return images, sparse_labels
if __name__ == '__main__':
image_path = '/home/user/myDocuments/tf-yolo/data/image/'
label_path = '/home/user/myDocuments/tf-yolo/data/annotation/'
image_set_path = '/home/user/myDocuments/tf-yolo/data/'
d = Dianli(image_path, label_path, image_set_path)
image, label = inputs(d, 'train', batch_size=1, num_epochs=2)
# de_file = inputs(d, 'train', batch_size=1, num_epochs=1)
init_op = tf.group(tf.global_variables_initializer(), tf.local_variables_initializer())
sess = tf.Session()
sess.run(init_op)
#要让程序运行起来,一定要先start_queue_runners向队列里面填入数据
coord = tf.train.Coordinator()
threads = tf.train.start_queue_runners(sess=sess, coord=coord)
try:
step = 0
while not coord.should_stop():
images, labels = sess.run([image, label])
images = np.squeeze(images, axis=0)
labels = np.squeeze(labels, axis=0)
ind = np.where(np.sum(labels, axis=1) != 0)[0]
labels = labels[ind, 1:]
draw_bboxes(images, labels)
except tf.errors.OutOfRangeError:
print('done')
finally:
coord.request_stop()
#join表示等待各个线程关闭
coord.join(threads)
sess.close()
在上面的例子当中,我们使用了tf.Coordinator来协调各个线程,各个线程正常情况下完成各自的任务,如果出现异常则向coordinator报告异常(should_stop==1),这个时候coordinator会将所有的线程关闭(request_stop==1)
注意在训练开始之前,一定要调用start_queue_runners来开启各个队列的线程,否则队列的内容一直为空,训练的进程会一直挂着无法运行
以上就是整个从文件数据制作tfrecords,再到tfrecords读取以及训练样例的输入的整个过程