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定义一个函数就行,无需关心层的反传工作。
在使用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的官方数据传送接口。
那么,怎么处理我们自己的数据传送接口呢?使用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)
在进行ImageReader定制的过程中,往往结合train_data.txt,从外存中读取数据,再送入网络进行迭代,用户可根据自己的需求进行各式各样的定制。
其次,对于(3),将网络定义文件单独罗列出来,为什么要这么做呢?因为训练/测试主控程序是一个层次较高的代码,在进行实验的时候应该是不关心神经网络的具体细节的,因此,网络定义的细节应该是定义在网络定义文件net.py中,在进行net.py的撰写中,网络底层代码与网络高层代码相互分离,网络高层代码调用网络底层代码。那么,何谓网络高层代码,何谓网络底层代码?网络高层代码协定了网络的设计架构,而网络底层代码则制约了网络底层(比如神经网络层)的规范。在训练主控代码中,程序只调用网络高层代码,对于高层代码与底层代码的封装,举个栗子:
- import numpy as np
- import tensorflow as tf
- import math
- def 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
这样做还有一个好处,就是在参数设置方面。注意到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()
对于测试的程序,与训练主程序不同的地方是,测试程序并不需要计算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
- 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)
当然对于辅助函数,完全可以按照用户的需求定制。