深度学习入门(十七)阶段性总结1,数据读取相关、ResNet精简式写法

Apr 2, 2019


原创:岐山凤鸣,转载请注明本站域名

阶段性进行一些总结,这是总结的第一部分,包含三个内容,tf.data即读取数据相关内容并且进行了独立测速,以及和之前不同的一款ResNet的精简写法。

觉得不错不妨star/follow一下我的Github

数据读取相关

我写的测速项目地址在这:https://github.com/Ecohnoch/Test_Speed_Tensorflow

如需要引用,注明本站域名即可。

原始方法feed_dict

顾名思义,tf.data即和数据相关的方法,tf最原始的数据读入方法是feed_dict,形如下面的代码:

_, loss_val, acc_val = sess.run([train_op, loss, acc], feed_dict={x: batch_train, y: batch_label})

但是这样肯定是不好的,仅限于小规模的数据,一旦遇到大规模训练数据的时候很容易出问题,首要一点便是它的速度太慢。

在Voxceleb说话人识别的数据集上测试的Feed_dict直接读取速度如下所示:

Counter: 0 Step time: 6.295599699020386 Batch time: 2.5750744342803955
Counter: 1 Step time: 7.346299886703491 Batch time: 0.4196662902832031
Counter: 2 Step time: 7.89916205406189 Batch time: 0.42375802993774414
Counter: 3 Step time: 7.849005222320557 Batch time: 0.42288804054260254
Counter: 4 Step time: 7.825454950332642 Batch time: 0.42127156257629395
Counter: 5 Step time: 7.741421699523926 Batch time: 0.42023229598999023
Counter: 6 Step time: 7.170980215072632 Batch time: 0.3953855037689209
Counter: 7 Step time: 7.0572052001953125 Batch time: 0.39606690406799316
Counter: 8 Step time: 7.011394262313843 Batch time: 0.4239654541015625
Counter: 9 Step time: 7.550851821899414 Batch time: 0.41985249519348145
10 Batch time consumed:  73.74794006347656

多线程方法

第二种是使用多线程方法,这种需要用到tf.coordinator进行线程管理。

它总共分为三步:

Step1: 定义一个数据读取的Tensor,这个Tensor是一个队列,需要给开一个BufferSize并且形式定义,一个图像里的写法形如:

with tf.device('/cpu:0'):
	q = tf.FIFOQueue(BATCH_SIZE*3, [tf.float32, tf.int64], shapes=[[224, 224, 3], []])
    enqueue_op = q.enqueue_many([x, y])
    x_b, y_b = q.dequeue_many(BATCH_SIZE)

其中x和y分别是Placeholder,且之后不用x和y,而用x_b, y_b, 因为在计算图里一旦计算了enqueue_op,当前数据位置已经到了x_b, y_b

Step2: 定义一个多线程控制器,且初始化线程启动,并且要告诉线程每一步做什么

tf里的多线程控制器是tf.coordinator,可以和saver放到一起,定义如下:

coord = tf.coordinator()

之后启动Session后,需要在Session中启动线程,并且要告诉线程每步做什么

告诉线程每步做什么:

def enqueue_batches():
    while not coord.should_stop():
        global idx_thread_train
        global idx_thread_label
        batch_train, idx_train, end_epoch = get_batch(train_file_list, idx_thread_train, batch_size=BATCH_SIZE)
        batch_train_label, idx_train_label, end_epoch = get_label_batch(train_label_list, idx_thread_train, batch_size=BATCH_SIZE)
        batch_train = np.array(batch_train)
        sess.run(enqueue_op, feed_dict={x: batch_train, y: batch_train_label})
        if end_epoch:
            idx_thread_train = 0
			idx_thread_label = 0

可以看到每步线程都需要去跑计算图,跑什么呢,自然是跑enqueue_op,即我们Step1定义的Tensor,让数据入队

然后启动线程:

num_threads = 3
for j in range(num_threads):
	t = threading.Thread(target=enqueue_batches)
	t.setDaemon(True)
	t.start()

线程启动后就会自己开始跑了,等你的计算图需要用到数据的时候,线程会自动出队一个数据。

Step3: 全部结束后控制线程停止

注:多线程方法进行数据训练的时候是不需要填充feed_dict的,直接sess.run(train_op)即可。

程序最后需要停止线程,即:

coord.request_stop()
coord.join()

最后测试的时间为:

Counter: 0 Step time: 6.949103832244873 Batch time: 6.949103832244873
Counter: 1 Step time: 0.4178884029388428 Batch time: 0.41788816452026367
Counter: 2 Step time: 0.41602659225463867 Batch time: 0.41602659225463867
Counter: 3 Step time: 1.6344246864318848 Batch time: 1.6344244480133057
Counter: 4 Step time: 0.4172630310058594 Batch time: 0.4172627925872803
Counter: 5 Step time: 0.4188086986541748 Batch time: 0.4188082218170166
Counter: 6 Step time: 3.5055649280548096 Batch time: 3.5055646896362305
Counter: 7 Step time: 0.41728734970092773 Batch time: 0.41728711128234863
Counter: 8 Step time: 0.4174153804779053 Batch time: 0.41741514205932617
Counter: 9 Step time: 3.524942398071289 Batch time: 3.52494215965271
10 Batch time consumed:  18.121190071105957

可以看到每三个Batch都会有一个多出来的时间,是因为我的Queue Buffer设置为3,三个线程去填充。

这个方法比原始方法快了很多,大概4倍左右。

TFRecord

这是tf自带的一种数据方法,专门用来处理大规模数据的。

我们之前的逻辑是每轮训练生成一个Batch,拿Batch去跑计算图。而我们现在不这样做。

我们直接先把所有的Batch跑一遍,但不去算计算图,而是把这过程的数据以二进制形式写进另一个文件,即TFRecord文件。

之后我们训练计算图的时候,只要跟着这个TFRecord文件出来的数据跑就可以了。

所以很明显有一个劣势,那就是你原始数据如果有100G,那这个TFRecord文件也差不多是100G,需要生成很久。

好处是只要生成了这个文件,读取数据就再也不用愁了,它也会自动给你一个Batch一个Batch的出来,很方便。

Step1: 跑一遍所有的原始数据,生成TFRecord文件

def generate_tfrecord(output_path):
    train_file_list, train_label_list = audio_data_extracted(SPLIT_FILE, WAV_DIR)
    writer = tf.python_io.TFRecordWriter(os.path.join(output_path, 'tran.tfrecords'))
    for ind, (file, label) in enumerate(zip(train_file_list, train_label_list)):
        audio = load_data(file)
        audio_raw = audio.tobytes()
        label = int(label)
        example = tf.train.Example(features=tf.train.Features(feature={
                'audio_raw': tf.train.Feature(bytes_list=tf.train.BytesList(value=[audio_raw])),
                'label':     tf.train.Feature(int64_list=tf.train.Int64List(value=[label]))
            }))
        writer.write(example.SerializeToString())
        if i != 0 and ind % 1000 == 0:
            print("%d num audios processed" % ind)
	writer.close()

可以看到这里要使用tf.train.Example去生成一个example,然后使用writer每轮写进一个数据,每轮写进的数据包含两个部分,即一个个单独的音频文件(这里我用的Voxceleb数据集),和对应的标签,把所有的数据全部跑一遍,比如这里就要跑153000轮才能跑完,以后有15万的数据。

Step2: 写一个函数去读取TFRecord文件

有了TFRecord基本就已经高枕无忧了,我们需要用一个解码器去对example进行解码。

def parse_function(example_proto):
    features = {
        'audio_raw': tf.FixedLenFeature([], tf.string),
        'label':     tf.FixedLenFeature([], tf.int64)
    }
    features = tf.parse_single_example(example_proto, features)
    audio_file = tf.decode_raw(features['audio_raw'], tf.float32)
    audio_file = tf.reshape(audio_file, [257, 250, 1])
    label      = tf.cast(features['label'], tf.int64)

	return audio_file, label

可以看到解码器的功能就只是对单独的一个example解码,从二进制到俩个东西,即单个音频文件和对应标签。

Step3: 建立一个Tensor去自动跑每一个Batch

有了编码器和解码器,接下来构造一个Tensor自动去跑出来Batch,非常的优雅。

dataset = tf.data.TFRecordDataset(trans_file)
dataset = dataset.map(parse_function)
dataset = dataset.shuffle(buffer_size=10000)
dataset = dataset.batch(BATCH_SIZE)
iterator= dataset.make_initializable_iterator()
next_element = iterator.get_next()

这个trans_file就是TFRecord的文件路径,最后的next_element就是一个Batch的Tensor,我们随时都可以跑一下这个Tensor,都能返回一个Batch的数据。

要注意这个next_element是按照顺序跑的,按照顺序和Batch_Size去跑dataset,那么最后它把dataset跑完的时候会throw一个tf.errors.OutOFRangeError,我们在不停的训练数据时,如果遇到了这个error,说明一个Epoch已经跑完了,可以进行下一个Epoch了!

训练时我们这样写就ok了:

for i in range(epoch_times):
	sess.run(iterator.initializer)   # 重置iterator
	while True:
		try:
			train, train_label = sess.run(next_element)  # 自动获得一个Batch
			# Do something

		except tf.errors.OutOfRangeError:
			print("End epoch %d" % i)
			break

While True是让next_element不停的跑,直到把dataset跑完,会throw 一个error自动结束本轮训练,开始下一轮运算,是不是很优雅。

TFRecord的测试时间如下,可以看到相比多线程,速度更快了一步,读取数据的速度几乎全部为0:

Counter: 0 Step time: 6.16588568687439 Batch time: 2.5156548023223877
Counter: 1 Step time: 0.4680795669555664 Batch time: 0.4195573329925537
Counter: 2 Step time: 0.4180431365966797 Batch time: 0.3949098587036133
Counter: 3 Step time: 0.4179658889770508 Batch time: 0.3945798873901367
Counter: 4 Step time: 0.417874813079834 Batch time: 0.39447832107543945
Counter: 5 Step time: 0.4183065891265869 Batch time: 0.39487552642822266
Counter: 6 Step time: 0.4181385040283203 Batch time: 0.3947765827178955
Counter: 7 Step time: 0.4182932376861572 Batch time: 0.39474034309387207
Counter: 8 Step time: 0.44022130966186523 Batch time: 0.3950035572052002
Counter: 9 Step time: 0.4184234142303467 Batch time: 0.3947596549987793
10 Batch time consumed:  10.002059936523438

精简式ResNet写法

在我的第13篇深度学习入门的文章中,介绍了一种tensorflow的ResNet50的写法,经过这么长时间学习,我也用过了很多ResNet,但这种写法并不容易理解也不容易维护,于是我提供一个精简式方便维护,一眼就能看懂的ResNet50的写法。我上面的进行速度评测,也是使用的这种写法的ResNet50。

https://github.com/Ecohnoch/Test_Speed_Tensorflow/blob/master/thin_resnet.py

重点部件就是:

普通残差块:

def identity_block(input_tensor, kernel_sizes, filters, names, is_training, reuse):
'''
三连conv->bn->relu
最后带上input_tensor做加法
'''

过渡残差块:

def conv_block(input_tensor, kernel_sizes, filters, names, is_training, reuse):
'''
对input_tensor做个conv->bn->relu来进行一波上采样,否则满足不了最后加法的shape
三连conv->bn->relu
最后带上第一步的上采样结果做加法
'''

实现ResNet34(伪代码):

x = 预处理

# Block1
x = conv_block()
x = identity_block()

x = conv_block()
x = identity_block()
x = identity_block()

x = conv_block()
x = identity_block()
x = identity_block()
x = identity_block()

x = conv_block()
x = identity_block()
x = identity_block()
x = 特征聚合

反正比较方便写也比较方便维护,resnet50, 152也就是多复制几行就可以了,Tensor名字比较关键,要写清楚。

后续还有一系列的阶段性总结,先写这么多吧。

Reference

InsightFace_TF