良心长文:深度学习框架的选择和有关tensorflow编程经验的分享

来源:互联网 发布:matlab画出网络拓扑图 编辑:程序博客网 时间:2024/05/03 20:34

   笔者接触深度学习领域已经一年了,在这一年的时间中,笔者通过自己摸索,算是进入了深度学习的大门(从白痴变成了菜鸟٩(๑>◡<๑)۶ ),然后在项目中,疯狂地经历了跳坑与出坑的过程,因此开这个经验分享的栏目,把经验同大家分享。

   在笔者最开始学习深度学习的将大半年中(研一期间),笔者主要使用的框架是caffe,使用这个框架主要是因为项目需求,因为笔者参与的项目大量需要在nvidia的嵌入式平台上完成,而在这样的嵌入式平台上运行程序,caffe无疑是首选,因为caffe是一个运行速度,并且轻量的框架。在笔者前期的博客中,也大量涉及了有关caffe以及nvidia嵌入式深度学习程序的内容,包括目前笔者正在更新的tensorflow2caffe系列,就正是在tensorflow上面做出了模型,然后取出数千万个模型参数,转换到caffe框架下,并在nvidia TX2上面运行。

   可是,caffe确实是一个运行速度快,并且轻量的框架。可是,caffe并不是一个高度支持自己定制的框架。请读者朋友们考虑下面这几个问题:

1. 如果用户需要在网络训练过程中,一个相同的网络结构需要搭配多套参数,在caffe下面应该怎么实现?

2. 在反传的过程中,需要对某些变量的梯度按照某些需求做出改变,在caffe下面应该怎么实现?

3. 需要自己实现一个简单的激活函数,在caffe下面应该怎么实现?

   对于第一个需求,在caffe下面已经超出了layer的范畴,是需要在net甚至solver以上的层次加以修改的(新手和菜鸟听到这个东西恐怕已经打算望而却步了)。其次,对于第二个需求,在caffe的layer级,可以在Backward_cpu和Backward_gpu程序中改变梯度的传播过程,可是caffe框架下反传程序是相对复杂的,这无疑增大了工作的复杂程度。最后,对于第三个需求,需要添加新的layer,不仅需要在solverparameter中修改,更需要自己通过hpp,cpp和cu程序去实现具体操作,这也无疑增加了工作的复杂程度与工作量。

   因此,在需要满足自定制比较高的需求时,tensorflow是一个比较好的选择。首先,tensorflow使用的语言基于python,这一门语言是一门很自由的语言,为了达到同一个目的可以有非常多种编程方法;同时,python的一些常用库也为编程提供了极大的方便;其次,tensorflow库中提供了非常多的规模宏大,功能完善的接口,并且使用相当方便;最后,tensorflow框架的机制是所有的数据在graph中得以呈示与检索,在编程建立graph的过程中,只需关注网络的前向传播过程,而网络反传的详细过程并不需要手动地去刻画,这就为科研提供了相当大的方便。相应的,对于前面三个问题,第一个问题,使用tensorflow库的接口可直接完成(几行代码的事);第二个问题,使用tensorflow库的接口同样可以完成(一行代码的事),第三个问题,使用python定义一个函数就行,无需关心层的反传工作。

   因此,从前几个月开始,由于工作与科研的需求,笔者对于自己定制网络层与网络架构的需求逐渐加深,因此,笔者更换了主研框架,从caffe变更到了tensorflow。在这个过程中,笔者认为对于caffe入门的研究者,转换到以python为主的tensorflow架构是相对轻松的。在caffe框架下工作的熟知c++的工程师对于python可以不加学习直接上手,并在使用中补缺式学习。同时,对于深度学习的入门选手,从caffe转换到tensorflow,也无需准备任何理论知识,只是编程的风格变换了而已。在笔者目前的项目中,训练程序往往使用的是tensorflow框架,然后若该程序需要转移到caffe上面去执行,则提取出训练参数,并且在caffe框架下恢复网络结构,填充参数,添加并执行网络的前传程序。

   那么,在使用tensorflow架构的时候,怎么去安排程序的架构呢?在这里笔者给出了一些接触tensorflow几个月后,自己的一些经验与思考。

   当然,笔者在深度学习领域还是相当naive的,因此,这些经验与思考权当抛砖引玉,如果各位读者朋友觉得有什么不妥,欢迎在评论区留言,笔者一定虚心接受。并且,由于笔者主要的研究方向是图像处理,因此在进行分享时,主要从处理图像的经验角度出发,不过笔者认为,这也能推广到其他的领域。

   下面,笔者就从几个角度阐述一下笔者常用的tensorflow程序架构。

   在使用tensorflow的时候,笔者工程文件下面的文件/文件夹往往分了几块:


(1) 训练与测试数据集文件夹datasets

(2) 数据传送接口image_reader.py

(3) 网络定义文件net.py

(4) 训练主控文件train.py

(5) 测试主控文件evaluate.py

(6) 辅助文件utils.py

   首先,对于(1),数据集肯定是需要的,笔者往往自己处理好训练集与测试集,然后放到数据集文件夹下。示例如下:

|-datasets

  |-train_dataset

  |-test-dataset

  train_data.txt

  test_data.txt

   首先train_dataset和test_dataset记录了训练与测试图片,然后两个txt文档内记录了对于所有训练与测试图片的索引,作用是给数据传送接口调用。对于txt文件可以使用python编写(比如笔者就是用上图中的write_txt.py和write_test_txt.py撰写训练/测试数据集索引),索引的内容也可自己定制。笔者一般直接添加图片和对应标签的名字,使用空格分离。

   然后,对于(2),训练数据传送接口。笔者在这里多讲几句,在进行数据读写的过程中,数据读取的方式大概分成两种,第一种是使用TensorFlow的官方的一些接口,举个栗子:

path_queue = tf.train.string_input_producer(input_paths, shuffle = True)reader = tf.WholeFileReader()paths, contents = reader.read(path_queues)src_image = tf.image,decode_jpeg(contents)input = tf.identity(src_image)input_batch = tf.train.batch([input], batch_size)
   大家可以看如上的代码,这只是笔者举的一个例子,这就是使用的tensorflow的官方接口去定制的数据读取,首先使用string_input_producer将输入文件名转化成string类型的queue,然后,数据内容在最后tf.Session().run的时候会按照需求出队。在这里,读者朋友可以发现,在数据接口中,tensorflow已经在做一些对用户透明的处理了,比如说在底层新建队列,并且通过一些多线程的机制在队列滚动的时候送出batch大小的数据,通过几行代码就可以轻松实现,并且能够非常简便地设置batch的规模。可是,有大量的操作对用户来说是完全透明的,我们并不知道队列在怎么处理,并且,tensorflow提供的数据处理接口是相当有限的。

   因此,在对数据接口进行自定制的过程中,笔者并不提倡使用tensorflow的官方数据传送接口。

   那么,怎么处理我们自己的数据传送接口呢?笔者的方法是使用placeholder-feed_dict机制,简要的来说就是下面这样:

image = tf.placeholder(tf.float32, shape=(batch_size, height, width, channels), name="image_name")src = ImageReader(args)'''# handles'''feed_dict = {image : src}sess.run([loss, accu, res, ], feed_dict = feed_dict)
   上面的示意代码描述了大意,由于tensor是一个占位符,因此先用placeholder把tensor的位置占住。然后在程序中正常安排tensor的处理逻辑,而最后需要给tensor送值的时候,通过自己定制的ImageReader送值给tensor。这样做的好处就是,可以非常自由地定制程序架构,尤其在数据读取方面。在自定制的ImageReader中,不仅可以自由地安排数据读取库(比如说由于入坑的原因,笔者就很爱使用cv2库做数据接口,有一些小伙伴喜欢使用PIL,或者scipy),又可以随意地在数据接口中做一些preprocess,并且该过程对于用户而言不是透明的,用户能够把握数据送入的绝大部分过程。因此,对于自定制数据接口而言,使用placeholder-feed_dict机制是不错的选择。

   在进行ImageReader定制的过程中,笔者往往结合train_data.txt,从外存中读取数据,再送入网络进行迭代,用户可根据自己的需求进行各式各样的定制。

   其次,对于(3),笔者一向是将网络定义文件单独罗列出来,为什么要这么做呢?因为训练/测试主控程序是一个层次较高的代码,在进行实验的时候应该是不关心神经网络的具体细节的,因此,网络定义的细节应该是定义在网络定义文件net.py中,在进行net.py的撰写中,笔者的习惯是网络底层代码与网络高层代码相互分离,网络高层代码调用网络底层代码。那么,何谓网络高层代码,何谓网络底层代码?笔者认为,网络高层代码协定了网络的设计架构,而网络底层代码则制约了网络底层(比如神经网络层)的规范。在训练主控代码中,程序只调用网络高层代码,对于高层代码与底层代码的封装,举个栗子:

import numpy as npimport tensorflow as tfimport mathdef make_var(name, shape, trainable = True):    return tf.get_variable(name, shape, trainable = trainable)#底层部分def conv2d(input_, output_dim, kernel_size, stride, padding = "SAME", name = "conv2d", biased = False):    input_dim = input_.get_shape()[-1]    with tf.variable_scope(name):        kernel = make_var(name = 'weights', shape=[kernel_size, kernel_size, input_dim, output_dim])        output = tf.nn.conv2d(input_, kernel, [1, stride, stride, 1], padding = padding)        if biased:            biases = make_var(name = 'biases', shape = [output_dim])            output = tf.nn.bias_add(output, biases)        return output#高层部分def net(image, gf_dim=64, reuse=False, name="net"):      input_dim = image.get_shape()[-1]    with tf.variable_scope(name):        if reuse:            tf.get_variable_scope().reuse_variables()        else:            assert tf.get_variable_scope().reuse is False         c0 = conv2d(input_ = image, output_dim = gf_dim, kernel_size = 3, stride = 2, name = 'c0')c1 = conv2d(input_ = c0, output_dim = gf_dim * 2, kernel_size = 3, stride = 2, name = 'c1')c2 = conv2d(input_ = c1, output_dim = gf_dim * 2, kernel_size = 3, stride = 2, name = 'c2')c3 = conv2d(input_ = c2, output_dim = gf_dim * 4, kernel_size = 3, stride = 1, name = 'c3')c4 = conv2d(input_ = c3, output_dim = gf_dim * 8, kernel_size = 3, stride = 1, name = 'c4')        output = tf.nn.tanh(c4)        return output

   在上述代码的网络底层代码中,规定了卷积操作的定义,里面使用了tf.nn.conv2d接口,而网络高层代码看不见tensorflow的网络层接口,在高层代码中,制定了网络的具体架构,如上所示,笔者定义了一个5层的全卷积神经网络,而训练主控程序就调用net函数。从net中也可以方便地看出网络架构,并使得训练主控程序简洁明了。

   这样做还有一个好处,就是在参数设置方面。不知读者朋友们注意到tensorflow的权重参数名称设置没有,在构造训练代码中,tf.get_variable(name, )和with tf.variable_scope(name)中的name是相当重要的。因为在tensorflow中,参数名称是按照类似堆栈的架构一级一级由底往上堆叠的,而每一次添加scope中的name就组成了堆栈的一部分。因此,如果说要满足同一个网络有两套不同的参数,又不需重新定义网络结构,应该怎么做呢?就将参数名称堆栈的栈顶换掉就行了,而栈顶以下的部分是不需要改动的。在网络定义文件中,网络高层代码和网络底层代码共同完成了对参数名称堆栈的除栈顶以外部分的定义与约束,而参数的栈顶部分则可以在训练主控程序中定制,这样就可以实现同一网络结构配置多套参数。并且,代码更加层次分明!

   对于(4)和(5),首先笔者先提一下训练和测试主控代码需要做什么。

   对于训练的主控程序,首先往往需要在其中定义训练参数,这样方便在代码中修改。然后,可能需要一些输入占位符定义,然后最主要的是进行网络前传得到训练结果,并根据这个结果计算loss,计算loss之后,需要使用训练器对网络进行反传并更新参数梯度。最后,根据需要导入fine-tune的参数,进行初始化,然后就是不停地送入数据并且进行网络的训练。

   简而言之,训练的主控程序逻辑如下:

获取用户定义训练程序参数->置占位符(placeholder)->进行网络前传->计算loss->设置训练器并且进行网络反传->保存sammary(可选)->进行fine-tune参数的导入->初始化训练过程->送数据进行网络训练

   根据上面的流程,训练程序可以按照如下示意执行。

#按需导入库import ...#用户自定义训练参数parser = argparse.ArgumentParser(description='')parser.add_argument("--arg_name", type = int, default=default_num, help="helps")#可加入更多参数args = parser.parse_args()#训练主函数def main():    #置占位符    image = tf.placeholder(tf.float32,shape=[1, image_height, image_height, image_channels],name='image')    label = tf.placeholder(tf.float32,shape=[1, label_height, label_height, 1],name='label')    #进行网络前传    net_output = net(image=image, reuse=False, name='net')    #计算loss    loss = comput_loss(image, label)        #得到loss_summary(可选)    loss_sum = tf.summary.scalar("loss", loss)    summary_writer = tf.summary.FileWriter(args.snapshot_dir, graph=tf.get_default_graph())    #设置训练器并进行网络训练,这里使用了一个Adam优化器    vars = [v for v in tf.trainable_variables() if 'net' in v.name]    optim = tf.train.AdamOptimizer(learning_rate)    grads_and_vars = optim.compute_gradients(loss, var_list=vars)    train_op = optim.apply_gradients(grads_and_vars)    #设置tensorflow会话层    sess = tf.Session()    #进行fine-tune参数导入    restore_vars = [v for v in tf.trainable_variables() if ...]#设置需要导入的参数    loader = tf.train.Saver(var_list = restore_vars)    loader.restore(sess, 'load parameters path')    #初始化训练过程    init = tf.global_variables_initializer()    sess.run(init)    #保存器    saver = tf.train.Saver(var_list=tf.global_variables(), max_to_keep=max_to_keep_nums)    for step in range(training_steps):#进行网络训练load_image, load_label = ImageReader(image_path, step, ...)#按需传入读取图片的参数        feed_dict = {image : load_image, label : load_label}        oss_value, loss_sum_value, _= sess,run([loss, loss_sum, train_op], feed_dict = feed_dict)if step % add_summary_per_step == 0:#按需更新summary            summary_writer.add_summary(loss_sum_value, loss)if step % save_per_step == 0:#按需保存模型参数    saver.save(sess, 'save_path', step)        print('...')#按需打印loss等    if __name__ == '__main__':    main()
   在进行训练程序的编写时,笔者想提示一点,就是在进行训练器设置并让网络自动求解梯度更新训练参数时,使用了两个接口,一个是compute_gradients,一个是apply_gradients,为什么要这么做呢?笔者认为,在进行参数更新的时候,先求解出参数和对应的梯度,然后可以按需对梯度做出转换,最后再利用改变的梯度来更新参数,这样可以有效地满足一些用户的自定制需求,笔者也会在之后的博客中为大家解答如何改变参数的梯度,这里先埋个伏笔。

   对于测试的程序,与训练主程序不同的地方是,测试程序并不需要计算loss和设置训练器进行网络参数的训练,当然也不需要保存summary,可是,测试程序重要的是需要导入所需要的网络的全部参数,这样网络才能完成前传过程。并且按需进行对结果的处理,测试精度,得到可视化效果等。

   简而言之,测试的主控程序逻辑如下:

获取用户定义测试程序参数->置占位符(placeholder)->进行网络前传得到网络前传结果->进行前传所需参数的导入->送数据进网络->进行结果后处理

   根据上面的流程,测试程序可以按照如下示意执行。

#按需导入库import ...#用户自定义测试参数parser = argparse.ArgumentParser(description='')parser.add_argument("--arg_name", type = int, default=default_num, help="helps")#可加入更多参数args = parser.parse_args()#测试主函数def main():    #置占位符    image = tf.placeholder(tf.float32,shape=[1, image_height, image_height, image_channels],name='image')    #进行网络前传    net_output = net(image=image, reuse=False, name='net')    #设置tensorflow会话层    sess = tf.Session()    #进行测试所需参数导入    restore_vars = [v for v in tf.global_variables() if ...]#设置测试需要导入的参数    loader = tf.train.Saver(var_list = restore_vars)    loader.restore(sess, 'load parameters' path')    for step in range(testing_steps):#送入测试样本load_image, load_label = ImageReader(image_path, step, ...)#按需传入读取图片的参数feed_dict = {image : load_image, label : load_label}net_output = sess,run(net_output, feed_dict = feed_dict)result = postprocess(net_output)#进行后处理(可选)accuracy = compute_accuracy(result, label)#进行精度计算(可选)        print('...')#按需打印信息等    if __name__ == '__main__':    main()

   最后,对于辅助文件utils.py,往往就是一些中小型的tools,比如,笔者就经常将训练图像的前处理,后处理,数据转换等一些小型函数放在utils.py里面,起到一些辅助与查缺补漏的作用,举个栗子,比如说,笔者就喜欢在在utils.py中放一些小的tools。

   比如说,读txt文件的接口:

def get_data_lists(data_path):    f = open(data_path, 'r')    datas=[]    for line in f:        data = line.strip("\n")        datas.append(data)    return datas
   又比如说计算l2_loss:

def comput_l2loss(src, dst):    return tf.reduce_mean((src-dst)**2)
   再比如说对模型参数的保存:

def save(saver, sess, logdir, step):   model_name = 'model'   checkpoint_path = os.path.join(logdir, model_name)   if not os.path.exists(logdir):      os.makedirs(logdir)   saver.save(sess, checkpoint_path, global_step=step)

   当然对于辅助函数,完全可以按照用户的需求定制。

   到这里,笔者对于tensorflow编程的经验介绍就接近尾声了。不过话说回来,条条大路通罗马,笔者这只小菜鸟只是给出了自己编程做科研中的一些经验,不同的人有不同的工程习惯,比如有些科研者喜欢使用类,有些喜欢将训练与测试程序融在一起,并在训练的时候测试精度,都不失为一些很好的方法,笔者也要虚心学习。在此,笔者也诚心希望本文只是抛砖引玉,希望有更多的经验丰富的科研大师在评论区分享自己的深度学习工程架构,也欢迎大家热烈讨论。对于笔者的一些疏漏与不足,欢迎大家批评指正!

   欢迎阅读笔者后续博客,各位读者朋友的支持与鼓励是我最大的动力!


written by jiong

如果觉得生活不顺利,说明你在走上坡路!

阅读全文
4 0
原创粉丝点击