中图杯师兄给了去年的r3net代码,backbone用的是resnet,一堆堆代码垒起来了,假期还有时间,就慢慢看,看代码过程中,顺便把tf巩固一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 def train (): with tf.Graph().as_default(), tf.device('/cpu:0' ): num_gpu = len (cfgs.GPU_GROUP.strip().split(',' )) global_step = slim.get_or_create_global_step() lr = warmup_lr(cfgs.LR, global_step, cfgs.WARM_SETP, num_gpu) tf.summary.scalar('lr' , lr) with tf.name_scope('get_batch' ): if cfgs.IMAGE_PYRAMID: shortside_len_list = tf.constant(cfgs.IMG_SHORT_SIDE_LEN) shortside_len = tf.random_shuffle(shortside_len_list)[0 ] else : shortside_len = cfgs.IMG_SHORT_SIDE_LEN img_name_batch, img_batch, gtboxes_and_label_batch, num_objects_batch, img_h_batch, img_w_batch = \ next_batch(dataset_name=cfgs.DATASET_NAME, batch_size=cfgs.BATCH_SIZE * num_gpu, shortside_len=shortside_len, is_training=True ) optimizer = tf.train.MomentumOptimizer(lr, momentum=cfgs.MOMENTUM) r3det_plusplus = build_whole_network_r3det_plusplus.DetectionNetwork( base_network_name=cfgs.NET_NAME, is_training=True ) inputs_list = [] for i in range (num_gpu): img = tf.expand_dims(img_batch[i], axis=0 ) if cfgs.NET_NAME in ['resnet152_v1d' , 'resnet101_v1d' , 'resnet50_v1d' ]: img = img / tf.constant([cfgs.PIXEL_STD]) gtboxes_and_label_r = tf.py_func(backward_convert, inp=[gtboxes_and_label_batch[i]], Tout=tf.float32) gtboxes_and_label_r = tf.reshape(gtboxes_and_label_r, [-1 , 6 ]) gtboxes_and_label_h = get_horizen_minAreaRectangle(gtboxes_and_label_batch[i]) gtboxes_and_label_h = tf.reshape(gtboxes_and_label_h, [-1 , 5 ]) num_objects = num_objects_batch[i] num_objects = tf.cast(tf.reshape(num_objects, [-1 , ]), tf.float32) img_h = img_h_batch[i] img_w = img_w_batch[i] inputs_list.append([img, gtboxes_and_label_h, gtboxes_and_label_r, num_objects, img_h, img_w]) tower_grads = [] biases_regularizer = tf.no_regularizer weights_regularizer = tf.keras.regularizers.l2(cfgs.WEIGHT_DECAY) total_loss_dict = { 'cls_loss' : tf.constant(0. , tf.float32), 'reg_loss' : tf.constant(0. , tf.float32), 'refine_cls_loss' : tf.constant(0. , tf.float32), 'refine_reg_loss' : tf.constant(0. , tf.float32), 'refine_cls_loss_stage3' : tf.constant(0. , tf.float32), 'refine_reg_loss_stage3' : tf.constant(0. , tf.float32), 'total_losses' : tf.constant(0. , tf.float32), } if cfgs.USE_SUPERVISED_MASK: total_loss_dict['mask_loss' ] = tf.constant(0. , tf.float32) with tf.variable_scope(tf.get_variable_scope()): for i in range (num_gpu): with tf.device('/gpu:%d' % i): with tf.name_scope('tower_%d' % i): with slim.arg_scope( [slim.model_variable, slim.variable], device='/device:CPU:0' ): with slim.arg_scope([slim.conv2d, slim.conv2d_in_plane, slim.conv2d_transpose, slim.separable_conv2d, slim.fully_connected], weights_regularizer=weights_regularizer, biases_regularizer=biases_regularizer, biases_initializer=tf.constant_initializer(0.0 )): gtboxes_and_label_h, gtboxes_and_label_r = tf.py_func(get_gtboxes_and_label, inp=[inputs_list[i][1 ], inputs_list[i][2 ], inputs_list[i][3 ]], Tout=[tf.float32, tf.float32]) gtboxes_and_label_h = tf.reshape(gtboxes_and_label_h, [-1 , 5 ]) gtboxes_and_label_r = tf.reshape(gtboxes_and_label_r, [-1 , 6 ]) img = inputs_list[i][0 ] img_shape = inputs_list[i][-2 :] img = tf.image.crop_to_bounding_box(image=img, offset_height=0 , offset_width=0 , target_height=tf.cast(img_shape[0 ], tf.int32), target_width=tf.cast(img_shape[1 ], tf.int32)) outputs = r3det_plusplus.build_whole_detection_network(input_img_batch=img, gtboxes_batch_h=gtboxes_and_label_h, gtboxes_batch_r=gtboxes_and_label_r, gpu_id=i) gtboxes_in_img_h = draw_boxes_with_categories(img_batch=img, boxes=gtboxes_and_label_h[:, :-1 ], labels=gtboxes_and_label_h[:, -1 ], method=0 ) gtboxes_in_img_r = draw_boxes_with_categories(img_batch=img, boxes=gtboxes_and_label_r[:, :-1 ], labels=gtboxes_and_label_r[:, -1 ], method=1 ) tf.summary.image('Compare/gtboxes_h_gpu:%d' % i, gtboxes_in_img_h) tf.summary.image('Compare/gtboxes_r_gpu:%d' % i, gtboxes_in_img_r) if cfgs.ADD_BOX_IN_TENSORBOARD: detections_in_img = draw_boxes_with_categories_and_scores( img_batch=img, boxes=outputs[0 ], scores=outputs[1 ], labels=outputs[2 ], method=1 ) tf.summary.image('Compare/final_detection_gpu:%d' % i, detections_in_img) loss_dict = outputs[-1 ] total_losses = 0.0 for k in loss_dict.keys(): total_losses += loss_dict[k] total_loss_dict[k] += loss_dict[k] / num_gpu total_losses = total_losses / num_gpu total_loss_dict['total_losses' ] += total_losses if i == num_gpu - 1 : regularization_losses = tf.get_collection( tf.GraphKeys.REGULARIZATION_LOSSES) total_losses = total_losses + tf.add_n(regularization_losses) tf.get_variable_scope().reuse_variables() grads = optimizer.compute_gradients(total_losses) if cfgs.GRADIENT_CLIPPING_BY_NORM is not None : grads = slim.learning.clip_gradient_norms(grads, cfgs.GRADIENT_CLIPPING_BY_NORM) tower_grads.append(grads) for k in total_loss_dict.keys(): tf.summary.scalar('{}/{}' .format (k.split('_' )[0 ], k), total_loss_dict[k]) if len (tower_grads) > 1 : grads = sum_gradients(tower_grads) else : grads = tower_grads[0 ] if cfgs.MUTILPY_BIAS_GRADIENT is not None : final_gvs = [] with tf.variable_scope('Gradient_Mult' ): for grad, var in grads: scale = 1. if '/biases:' in var.name: scale *= cfgs.MUTILPY_BIAS_GRADIENT if 'conv_new' in var.name: scale *= 3. if not np.allclose(scale, 1.0 ): grad = tf.multiply(grad, scale) final_gvs.append((grad, var)) apply_gradient_op = optimizer.apply_gradients(final_gvs, global_step=global_step) else : apply_gradient_op = optimizer.apply_gradients(grads, global_step=global_step) variable_averages = tf.train.ExponentialMovingAverage(0.9999 , global_step) variables_averages_op = variable_averages.apply(tf.trainable_variables()) train_op = tf.group(apply_gradient_op, variables_averages_op) summary_op = tf.summary.merge_all() restorer, restore_ckpt = r3det_plusplus.get_restorer() saver = tf.train.Saver(max_to_keep=20 ) init_op = tf.group( tf.global_variables_initializer(), tf.local_variables_initializer() ) tfconfig = tf.ConfigProto( allow_soft_placement=True , log_device_placement=False ) tfconfig.gpu_options.allow_growth = True with tf.Session(config=tfconfig) as sess: sess.run(init_op) coord = tf.train.Coordinator() threads = tf.train.start_queue_runners(coord=coord, sess=sess) summary_path = os.path.join(cfgs.SUMMARY_PATH, cfgs.VERSION) tools.mkdir(summary_path) summary_writer = tf.summary.FileWriter(summary_path, graph=sess.graph) if not restorer is None : restorer.restore(sess, restore_ckpt) print('restore model' ) for step in range (cfgs.MAX_ITERATION // num_gpu): training_time = time.strftime('%Y-%m-%d %H:%M:%S' , time.localtime(time.time())) if step % cfgs.SHOW_TRAIN_INFO_INTE != 0 and step % cfgs.SMRY_ITER != 0 : _, global_stepnp = sess.run([train_op, global_step]) else : if step % cfgs.SHOW_TRAIN_INFO_INTE == 0 and step % cfgs.SMRY_ITER != 0 : start = time.time() _, global_stepnp, total_loss_dict_ = \ sess.run([train_op, global_step, total_loss_dict]) end = time.time() print('***' *20 ) print("""%s: global_step:%d current_step:%d""" % (training_time, (global_stepnp-1 )*num_gpu, step*num_gpu)) print("""per_cost_time:%.3fs""" % ((end - start) / num_gpu)) loss_str = '' for k in total_loss_dict_.keys(): loss_str += '%s:%.3f\n' % (k, total_loss_dict_[k]) print(loss_str) if np.isnan(total_loss_dict_['total_losses' ]): sys.exit(0 ) else : if step % cfgs.SMRY_ITER == 0 : _, global_stepnp, summary_str = sess.run([train_op, global_step, summary_op]) summary_writer.add_summary(summary_str, (global_stepnp-1 )*num_gpu) summary_writer.flush() if (step > 0 and step % (cfgs.SAVE_WEIGHTS_INTE // num_gpu) == 0 ) or (step >= cfgs.MAX_ITERATION // num_gpu - 1 ): save_dir = os.path.join(cfgs.TRAINED_CKPT, cfgs.VERSION) if not os.path.exists(save_dir): os.mkdir(save_dir) save_ckpt = os.path.join(save_dir, '{}_' .format (cfgs.DATASET_NAME) + str ((global_stepnp-1 )*num_gpu) + 'model.ckpt' ) saver.save(sess, save_ckpt) print(' weights had been saved' ) coord.request_stop() coord.join(threads)
下面开始逐步解释:
1、
1 with tf.Graph().as_default(), tf.device('/cpu:0' ):
这里解释一下数据流图,学习神经网络的时候,经常会有绘制的多层感知机原理图,线代表数据流向,而圆圈就代表神经元,也就是运算单元,将数据流传输过去后,在神经元中得到运算。而tf中的数据流图和这一类的原理图颇为类似,在tf中,
TensorFlow计算,表示为数据流图。 TensorFlow 中的所有计算都会被转化为计算图上的节点。 TensorFlow 是一个通过计算图的形式来表述计算的编程系统。 TensorFlow中的每个计算都是计算图的一个节点,而节点之间的边描述了计算之间的依赖关系。 TensorFlow 的计算模型是有向图,采用数据流图 (Data Flow Graphs),其中每个节点代表一些函数或计算,而边代表了数值、矩阵或张量
这样看来,是不是很像多层感知机的原理模型。
在模型任务开始的时候,系统就会指定一个数据流图作为默认图,而 tf.Graph()会返回一个新的数据流图,tf.Graph().as_default 这句话的含义是指定一个数据流图(a=tf.Graph(),a.as_default())作为当前上下文管理器中的默认图,上下文管理器就是with,其作用是即使出现错误异常也会进行退出操作,使得图间的运算分离,互不干扰,在with中的都认为在当前上下文管理之中,即运算会映射到当前设定的图中,如果不指定默认的数据流图,则会将所有操作都映射到系统的初始的默认图中。as_default的意义何在?as_default返回一个上下文管理器,with可以将内部内容作为上下文,但with只是指定当前内容作为其内容,with中的回话(即session,tf1.0中常用)只有在with中有效(with tf.session啥的),一旦调用了函数跳出了with,会话自动关闭,只有指定了作为as_default,在跳出是才可以继续使用run和eval(run是tf1.0中手动运算需要的,1.0中定义操作和执行操作是分开的,而tf2.0中已经可以自动更新了)。tf.device是用于指定使用的运算设备。补充:tensor.graph可以得到该tensor使用的数据流图,tf.compat.v1.get_default_graph()可以获得当前的默认图(有compat.v1是为了tf1.0兼容tf2.0版本,该语句是2.0版本中的,等价于tf1.0版本中的tf.get_default_graph())。
参考:
tensorflow基础知识(二) Graph计算图的创建和使用
[ tf.Graph().as_default() 详解
2、
1 num_gpu = len (cfgs.GPU_GROUP.strip().split(',' ))
这个cfgs库好像是自定义库(from libs.configs import cfgs),跳转查看GPU_GROUP可以看到其是一个常数:GPU_GROUP = “2,3”,这里可以视自己服务器上GPU数量来改,如果服务器上有四个GPU,且我们想全用,可以改为:GPU_GROUP = “0,1,2,3”,而strip函数是用来去掉字符串两端的空格的,split(‘,’)用于分割字符串,一个逗号进行分割,返回的是列表,列表的元素就是分割开的字符串。
3、
1 global_step = slim.get_or_create_global_step()
get_or_create_global_step是用于创建全局阶跃张量,简单来说主要是用于自动计算步长的,而不需要人为来纪步长。2.3版本中打印函数说明(即slim.get_or_creat_global_step?),说明中说slim的这个函数未来会弃用,建议使用的tf.train.get_or_create_global_step,但是在tf2.0中改函数已经被丢弃了,为了兼容tf1.0,将其放入了compat.v1中了,可以看出函数说明文档没有更新,还是用slim.get_or_creat_global_step吧。这个函数用于纪录步长,global_step 在训练中是计数的作用,每训练一个batch就加1,可以传入参数,默认参数graph=None,即默认缺失,则会使用系统的图作为被纪录对象。纪录的全局步数很有意义,其相当于一个时钟表,方便控制程序还何时做何事,比如滑动平均、优化器、指数衰减学习率等方面都有用到,一些学习率大小是随步数来变的越来越小的,那么这个参数就很有意义了。
参考了篇博客,测试一下这个全局步数量的自加的原理(注:这里没有用get_or_create_global_step函数):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import tensorflow.compat.v1 as tfimport numpy as np x = tf.placeholder(tf.float32, shape=[None , 1 ], name='x' ) y = tf.placeholder(tf.float32, shape=[None , 1 ], name='y' ) w = tf.Variable(tf.constant(0.0 )) global_steps = tf.Variable(0 , trainable=False ) learning_rate = tf.train.exponential_decay(0.1 , global_steps, 10 , 2 , staircase=False ) loss = tf.pow (w*x-y, 2 ) train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss,global_step=global_steps) with tf.Session() as sess: sess.run(tf.global_variables_initializer()) for i in range (10 ): sess.run(train_step, feed_dict={x:np.linspace(1 ,2 ,10 ).reshape([10 ,1 ]), y:np.linspace(1 ,2 ,10 ).reshape([10 ,1 ])}) print(sess.run(learning_rate)) print(sess.run(global_steps))
由于这个代码是tf1.0的代码,所以我在此基础上改了改,tf1.0的静态操作都被放入了tensorflow.compat.v1库中了(tf.train.get_or_create_global_step还有session也以及session需要的全局初始化tf.global_variables_initializer()都放入了v1库中了),所以导入时候特地用了tensorflow.compat.v1 as tf,使得tf1.0的代码可以在tf2.0中运行(除此以外可能还需要其他操作,比如禁止动态更新,后续讲),代码中with 的含义是tf1.0中为了把with中的上下文都加入到一个会话(session)中,这样上下文都在sess的作用域下,可以通过sess.run进行更新。
输出如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 0.10717734 1 0.11486983 2 0.123114444 3 0.1319508 4 0.14142136 5 0.15157166 6 0.16245048 7 0.17411011 8 0.1866066 9 0.2 10
其实可以看出,global_step初始化为1,但其是作为参数传入给minimize的,所以更新应该是在该函数中进行的,minimize进行求导是对一个batch来的,然后梯度下降优化器进行更新参数,所以一个batch全局步长自加一是合理的,而learning_rate再通过global_step进行改变自身。若从简单的参数来说,函数传递过去的应该是形参,不会改变global_step,但是这里的变量是tf自定义的Variable,大胆的猜测一下,这里传过去的应该是类似于引用的方法,直接改变图中的原值。
参考链接:
参考:
TensorFlow中global_step的简单分析
slim.get_or_create_global_step()
4、
1 lr = warmup_lr(cfgs.LR, global_step, cfgs.WARM_SETP, num_gpu)
这里的warmup_lr是一个函数,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def warmup_lr (init_lr, global_step, warmup_step, num_gpu ): def warmup (end_lr, global_step, warmup_step ): start_lr = end_lr * 0.1 global_step = tf.cast(global_step, tf.float32) return start_lr + (end_lr - start_lr) * global_step / warmup_step def decay (start_lr, global_step, num_gpu ): lr = tf.train.piecewise_constant(global_step, boundaries=[np.int64(cfgs.DECAY_STEP[0 ] // num_gpu), np.int64(cfgs.DECAY_STEP[1 ] // num_gpu), np.int64(cfgs.DECAY_STEP[2 ] // num_gpu)], values=[start_lr, start_lr / 10. , start_lr / 100. , start_lr / 1000. ]) return lr return tf.cond(tf.less_equal(global_step, warmup_step), true_fn=lambda : warmup(init_lr, global_step, warmup_step), false_fn=lambda : decay(init_lr, global_step, num_gpu))
函数传入的clsg.LR是5e-4,全局步长(之前也说过了,全局步长是在和梯度、优化器一起变化的),cfgs.WARM_SETP以及调用的GPU数量。进入cfgs中看一下WARM_SETP,可以看到WARM_SETP = int(1.0 / 4.0 * SAVE_WEIGHTS_INTE),其中设置了SAVE_WEIGHTS_INTE = 20000(至于为啥乘1/4不太了解了)。
那么来看一下这个函数,函数里就一句return,直接看return就好了,tf.cond啥意思呢?在交互式窗口输入tf.cond?看看(注意,tf2.0中有cond函数,所以可以import tensorflow as tf,但是为了兼容tf1.0,tf1.0的所有函数方法都放入了tf.compat.v1中,所以tf.compat.v1.cond就好,tf1.0在2.0环境中运行时候,直接import tensorflow.compat.v1 as tf即可保证原来的代码都完好运行),函数返回如下(tf1.0下的返回,即2.0环境中tf.compat.v1.cond?):
1 2 3 4 5 6 7 8 9 10 11 tf.compat.v1.cond( pred, true_fn=None , false_fn=None , strict=False , name=None , fn1=None , fn2=None , ) Docstring: Return `true_fn()` if the predicate `pred` is true else `false_fn()`. (deprecated arguments)
可以看出这个函数功能和if else是类似的,当pred为真的时候,返回true_fn后面的语句,否则返回false_fn的。举个例子如下:
1 2 3 4 5 6 7 8 9 10 11 import tensorflow.compat.v1 as tfx=tf.constant([1 ],dtype=tf.float32) y=tf.constant([2 ],dtype=tf.float32) a=tf.constant([2 ],dtype=tf.float32) b=tf.constant([5 ],dtype=tf.float32) with tf.Session() as sess: sess.run(tf.global_variables_initializer()) z = tf.multiply(a, b) print(sess.run(z)) result = tf.cond(x < y, lambda : tf.add(x, z), lambda : tf.square(y)) print(sess.run(result))
输入如下:
例子中都统一设定dtype为float32了,因为tf不会自动类型转换,挺傻瓜式的,至于为什么加会话即tf.Session(),是因为tf1.0中运算定义,运算执行是分开的,在一个会话内,初始化后,然后用run执行,否则print得不到想看到的结果。
cond解释完毕,回到模型中的函数,如果global_step(全局步数,一个batch自加一,因为训练也是以batch为单位的,其自动更新)小于等于 warmup_step的时候,返回warmup(升温)函数,否则返回decay(衰弱)函数。看升温函数,其升温初始值为传入学习率的0.1,升温至传入的学习率位置,此阶段为升温阶段;后面开始衰弱阶段,看一下decay函数,我们来看一下tf.train.piecewise_constant()函数,如下:
1 2 import tensorflow as tftf.compat.v1.train.piecewise_constant?
输出如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Signature: tf.compat.v1.train.piecewise_constant(x, boundaries, values, name=None ) Docstring: Piecewise constant from boundaries and interval values. Example: use a learning rate that's 1.0 for the first 100001 steps, 0.5 for the next 10000 steps, and 0.1 for any additional steps. ```python global_step = tf.Variable(0, trainable=False) boundaries = [100000, 110000] values = [1.0, 0.5, 0.1] learning_rate = tf.compat.v1.train.piecewise_constant(global_step, boundaries, values) # Later, whenever we perform an optimization step, we increment global_step. ```
这个函数的描述是:边界和区间值的分段常数。看函数说明文档的例子,可以看出,前100001步(global_step)用学习率1.0,前10000步是指0-100000闭区间正好100001步;然后100001步到110000的10000步用0.5,后面global_step再增加的时候,学习率用0.1,这是一种明显的逐渐逼近最优解的方式,在刚开始远离最优解位置的时候,学习调大一点,使得快速接近最优解点,在靠近后,调小学习率,防止跨度太大,跳过了该最优解到其他地方了。
那么回到模型函数中,显然第一个参数是global_step,第二个参数是步数边界,跳入cfgs看参数,可以看到如下:
1 DECAY_STEP = [SAVE_WEIGHTS_INTE*12 , SAVE_WEIGHTS_INTE*16 , SAVE_WEIGHTS_INTE*20 ]
SAVE_WEIGHTS_INTE之前说过,设置成20000了,这里乘12、16、20应该是可以自行修改的边界,而decay中//num_gpu应该是因为多GPU并行运算,所以步数需要除以gpu数量,//代表取除法结果的整数部分,等价于np.floor(a/b),而学习率是从初始传入的值,变为十分之一,再百分之一,最后的是千分之一。
5、
1 tf.summary.scalar('lr' , lr)
我们来看一下这个函数的说明文档:
1 2 import tensorflow as tftf.compat.v1.summary.scalar?
输出如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Signature: tf.compat.v1.summary.scalar(name, tensor, collections=None , family=None ) Docstring: Outputs a `Summary` protocol buffer containing a single scalar value. The generated Summary has a Tensor.proto containing the input Tensor. Args: name: A name for the generated node. Will also serve as the series name in TensorBoard. tensor: A real numeric Tensor containing a single value. collections: Optional list of graph collections keys. The new summary op is added to these collections. Defaults to `[GraphKeys.SUMMARIES]`. family: Optional; if provided, used as the prefix of the summary tag name, which controls the tab name used for display on Tensorboard. Returns: A scalar `Tensor` of type `string`. Which contains a `Summary` protobuf.
可以看出name是当做图中的生成的节点名称,也作为tensorboard的名称。而tensor就是一个包含单个值的张量。该函数返回一个协议缓冲区(用于保存数据),即该函数主要用来显示标量的信息,一般在画loss,accuary 时会用到这个函数(比如tensorboard中绘制可视化图)。
6、
1 2 3 4 5 6 7 8 9 10 11 12 13 with tf.name_scope('get_batch' ): if cfgs.IMAGE_PYRAMID: shortside_len_list = tf.constant(cfgs.IMG_SHORT_SIDE_LEN) shortside_len = tf.random_shuffle(shortside_len_list)[0 ] else : shortside_len = cfgs.IMG_SHORT_SIDE_LEN img_name_batch, img_batch, gtboxes_and_label_batch, num_objects_batch, img_h_batch, img_w_batch = \ next_batch(dataset_name=cfgs.DATASET_NAME, batch_size=cfgs.BATCH_SIZE * num_gpu, shortside_len=shortside_len, is_training=True )
这里顺便解释一下tf.name_scope和tf.variable_scope以及tensorboard。tf.name_scope是为了给一些Variable变量起一个区域名字,即使得数据流图更有条理性,而variable_scope也是给区域取个名字,但是其是结合tf.get_variable使用的,get_variable和普通的tf.Variable的区别在于tf.Variable总会创建一个新的变量(name属性是图中的名字,我们给的a=tf.Variable中的a是普通变量名),如果name相同了,系统会自动修改一下这个name,进行创建;而tf.get_Variable则是可以进行共享的,如果name相同,则返回图中的那个变量,具有共享功能(比如a=tf.get_variable([1,2,3],name=”a”),b=tf.get_variable([1,2,3],name=”a”),那么a和b是共享内存的,可以理解为引用),值得注意的是,想要进行共享,需要把共享功能设置为true,如下,这样就可以参数共享了,有些地方没有开启也可以进行共享,往往是使用的语句内部已经使用了reuse_variables。
1 2 3 4 5 with tf.variable_scope("image_filters") as scope: scope.reuse_variables() #或者如下也可以: with tf.variable_scope("image_filters", reuse=True): a=tf.get_variable([1,2,3],name="a")
tensorboard是一个用来可视化的工具,可以看scalar(标量)的变化曲线,也可以看模型的数据流图,通过各类scope使得可视化效果更清晰,下面看个例子(来源网络),下述就是将一个scalar保存到本地:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 def learn_tensor_board_2 (): with tf.name_scope('data' ): x_data = np.random.rand(100 ).astype(np.float32) y_data = 0.3 * x_data + 0.1 with tf.name_scope('parameters' ): with tf.name_scope('weights' ): weight = tf.Variable(tf.random_uniform([1 ], -1.0 , 1.0 )) tf.summary.histogram('weight' , weight) with tf.name_scope('biases' ): bias = tf.Variable(tf.zeros([1 ])) tf.summary.histogram('bias' , bias) with tf.name_scope('y_prediction' ): y_prediction = weight * x_data + bias with tf.name_scope('loss' ): loss = tf.reduce_mean(tf.square(y_data - y_prediction)) tf.summary.scalar('loss' , loss) optimizer = tf.train.GradientDescentOptimizer(0.5 ) with tf.name_scope('train' ): train = optimizer.minimize(loss) with tf.name_scope('init' ): init = tf.global_variables_initializer() sess = tf.Session() merged = tf.summary.merge_all() writer = tf.summary.FileWriter("tensorboard/loss-2" , sess.graph) sess.run(init) for step in range (201 ): sess.run(train) rs = sess.run(merged) writer.add_summary(rs, step) if __name__ == '__main__' : learn_tensor_board_2()
运行上述命令后,在命令行窗口输入如下命令tensorboard打开保存的scalar图:
1 tensorboard --logdir=C:\Users\21311 \machine_learning\tensorflow\tensorboard\loss-2
然后在浏览器中输入如下:
这里的localhost其实就是本地ip,所以使用本地ip是一样的,6006是端口号,就像jupyter使用的是8888端口似的。
这样就可以打开tensorboard进行查看数据流图和scalar等等了。
注意:variable变量都是数据流图的量,和普通的变量不一样,普通的量在函数调用退出时内存销毁,然而variable会一直在图中保留,所以不要认为调用的函数中的variable变量会消失。
参考文章:
:
TensorFlow入门(七) 充分理解 name / variable_scope
TensorFlow中的name_scope和variable_scope的使用
什么是TensorBoard?
继续回到模型程序中,第一个if cfgs.IMAGE_PYRAMID,跳转进入可以看到IMAGE_PYRAMID是False,这里应该是用于决定是否使用图像金字塔模型的。else中,cfgs.IMG_SHORT_SIDE_LEN=800,代表图像短的一边设置为800。
然后我们转入batch生成的函数内看一看,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 def next_batch (dataset_name, batch_size, shortside_len, is_training ): ''' :return: img_name_batch: shape(1, 1) img_batch: shape:(1, new_imgH, new_imgW, C) gtboxes_and_label_batch: shape(1, Num_Of_objects, 5] .each row is [x1, y1, x2, y2, label] ''' valid_dataset= ['DOTA1.5' , 'ICDAR2015' , 'pascal' , 'coco' , 'bdd100k' , 'DOTA' , 'DOTA800' , 'DOTA600' , 'HRSC2016' , 'UCAS-AOD' , 'OHD-SJTU' , 'OHD-SJTU-600' , 'OHD-SJTU-ALL-600' , 'DOTATrain' ] if dataset_name not in valid_dataset: raise ValueError('dataSet name must be in {}' .format (valid_dataset)) if is_training: pattern = os.path.join('../data/tfrecord' , dataset_name + '_train*' ) else : pattern = os.path.join('../data/tfrecord' , dataset_name + '_test*' ) print('tfrecord path is -->' , os.path.abspath(pattern)) filename_tensorlist = tf.train.match_filenames_once(pattern) filename_queue = tf.train.string_input_producer(filename_tensorlist) img_name, img, gtboxes_and_label, num_obs, img_h, img_w = read_and_prepocess_single_img(filename_queue, shortside_len, is_training=is_training) img_name_batch, img_batch, gtboxes_and_label_batch, num_obs_batch, img_h_batch, img_w_batch = \ tf.train.batch( [img_name, img, gtboxes_and_label, num_obs, img_h, img_w], batch_size=batch_size, capacity=16 , num_threads=16 , dynamic_pad=True ) return img_name_batch, img_batch, gtboxes_and_label_batch, num_obs_batch, img_h_batch, img_w_batch
可以看到valid_dataset中有很多著名的数据集,若使用自己的数据集,封装好后,把名字加入该列表中即可,若不加,则后面的if判断后会抛出错误。后面的if&else是判断是训练模式or推理模式。os库常用于管理路径文件等等,os.path.join函数用于连接两个或更多的路径名组件,功能如下:
1.如果各组件名首字母不包含’/’,则函数会自动加上
2.如果有一个组件是一个绝对路径,则在它之前的所有组件均会被舍弃
3.如果最后一个组件为空,则生成的路径以一个’/’分隔符结尾
示例如下:
1 2 3 4 5 6 7 8 9 10 import osPath1 = 'home' Path2 = 'develop' Path3 = 'code' Path10 = Path1 + Path2 + Path3 Path20 = os.path.join(Path1,Path2,Path3) print ('Path10 = ' ,Path10)print ('Path20 = ' ,Path20)
输出如下:
1 2 Path10 = homedevelopcode Path20 = home\develop\code
那么os.path.abspath()啥功能呢,从字面上也可以看出来,其是绝对路径的意思,若当前路径是A,你传入路径参数B,返回A/B,测试一下,如下:
1 print(os.path.abspath(Path20))
输出如下:
1 C:\Users\21311 \machine_learning\tensorflow\home\develop\code
而 filename_tensorlist = tf.train.match_filenames_once(pattern)这句话,是类似于正则表达式,在当前路径下找匹配的文件,’_train*’中的星号就是啥都行的意思。也就是说会匹配到/data/tfrecord/DOTA_train.tfrecord文件。(r3det中测试没测试好,竟然是空[],也就是没检测到)
如下看一下这个函数的测试案例:
1 2 3 4 5 6 7 8 9 10 import tensorflow.compat.v1 as tf directory = "*.*" file_names = tf.train.match_filenames_once(directory) init = (tf.global_variables_initializer(), tf.local_variables_initializer()) with tf.Session() as sess: sess.run(init) print(sess.run(file_names))
结果如下:
1 2 3 4 5 [b'.\\.ipynb_checkpoints' b'.\\.ipynb_checkpoints\\Untitled-checkpoint.ipynb' b'.\\Untitled.ipynb' b'.\\tensorboard' b'.\\tensorboard\\loss-2' b'.\\tensorboard\\loss-2\\events.out.tfevents.1628397138.DESKTOP-P038UOU' b'.\\tensorboard\\loss-2\\events.out.tfevents.1628397214.DESKTOP-P038UOU' ]
参考链接如下:
tf.train.match_filenames_once如何验证文件是否正确读取?
对于 filename_queue = tf.train.string_input_producer(filename_tensorlist)这句话,这个函数的描述说明如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 Signature: tf.train.string_input_producer( string_tensor, num_epochs=None , shuffle=True , seed=None , capacity=32 , shared_name=None , name=None , cancel_op=None , ) Docstring: Output strings (e.g. filenames) to a queue for an input pipeline. (deprecated)
其本质就是生成一个文件名队列(其实就是pipeline流水线),但是tf1.0定义和执行是分开的,需要 tf.train.start_queue_runners函数才能真的把文件加载到文件队列中,而将文件队列中的文件加载到内存中还需要reader = tf.WholeFileReader()和key, value = reader.read(filename_queue)这两句函数。
参考链接如下:
详解TensorFlow数据读取机制(tf.train.string_input_producer)
然后read_and_prepocess_single_img函数,这个函数对读出的sample进行预处理。来看一下该函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 def read_and_prepocess_single_img (filename_queue, shortside_len, is_training ): img_name, img, gtboxes_and_label, num_objects = read_single_example_and_decode(filename_queue) img = tf.cast(img, tf.float32) if is_training: if cfgs.RGB2GRAY: img = image_preprocess.random_rgb2gray(img_tensor=img, gtboxes_and_label=gtboxes_and_label) if cfgs.IMG_ROTATE: img, gtboxes_and_label = image_preprocess.random_rotate_img(img_tensor=img, gtboxes_and_label=gtboxes_and_label) img, gtboxes_and_label, img_h, img_w = image_preprocess.short_side_resize(img_tensor=img, gtboxes_and_label=gtboxes_and_label, target_shortside_len=shortside_len, length_limitation=cfgs.IMG_MAX_LENGTH) if cfgs.HORIZONTAL_FLIP: img, gtboxes_and_label = image_preprocess.random_flip_left_right(img_tensor=img, gtboxes_and_label=gtboxes_and_label) if cfgs.VERTICAL_FLIP: img, gtboxes_and_label = image_preprocess.random_flip_up_down(img_tensor=img, gtboxes_and_label=gtboxes_and_label) else : img, gtboxes_and_label, img_h, img_w = image_preprocess.short_side_resize(img_tensor=img, gtboxes_and_label=gtboxes_and_label, target_shortside_len=shortside_len, length_limitation=cfgs.IMG_MAX_LENGTH) if cfgs.NET_NAME in ['resnet152_v1d' , 'resnet101_v1d' , 'resnet50_v1d' ]: img = img / 255 - tf.constant([[cfgs.PIXEL_MEAN_]]) else : img = img - tf.constant([[cfgs.PIXEL_MEAN]]) return img_name, img, gtboxes_and_label, num_objects, img_h, img_w
这个函数中的第一句使用了read_single_example_and_decode函数,这个函数功能其实是读取出一个sample,由于sample的数据格式是特定的,所以需要进行解析,这个函数就是对其进行解析的。来看一下这个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 def read_single_example_and_decode (filename_queue ): reader = tf.TFRecordReader() _, serialized_example = reader.read(filename_queue) features = tf.parse_single_example( serialized=serialized_example, features={ 'img_name' : tf.FixedLenFeature([], tf.string), 'img_height' : tf.FixedLenFeature([], tf.int64), 'img_width' : tf.FixedLenFeature([], tf.int64), 'img' : tf.FixedLenFeature([], tf.string), 'gtboxes_and_label' : tf.FixedLenFeature([], tf.string), 'num_objects' : tf.FixedLenFeature([], tf.int64) } ) img_name = features['img_name' ] img_height = tf.cast(features['img_height' ], tf.int32) img_width = tf.cast(features['img_width' ], tf.int32) img = tf.decode_raw(features['img' ], tf.uint8) img = tf.reshape(img, shape=[img_height, img_width, 3 ]) gtboxes_and_label = tf.decode_raw(features['gtboxes_and_label' ], tf.int32) gtboxes_and_label = tf.reshape(gtboxes_and_label, [-1 , 9 ]) num_objects = tf.cast(features['num_objects' ], tf.int32) return img_name, img, gtboxes_and_label, num_objects
第一句是为了可以读取文件名队列中文件到内存中,reader.read就是读到内存中,tf.parse_single_example就是对其进行解析,其返回的是一个字典类型,我们可以打印一下看看:
结果如下:
解析后将值赋值给img_name等等即可,由于img解析的时候用的是str格式,需要继续解码用tf.decode_raw,这个函数可以将原始字节存储变为tensor,这个函数的描述如下:
1 2 3 4 5 6 7 8 9 10 Signature: tf.decode_raw( input_bytes=None , out_type=None , little_endian=True , name=None , bytes =None , ) Docstring: Convert raw byte strings into tensors. (deprecated arguments)
注意这个函数的输出形式需要指定,示例如下:
1 2 3 4 5 import tensorflow.compat.v1 as tfwith tf.Session() as sess: sess.run(tf.global_variables_initializer()) a=tf.decode_raw(str (0 ),tf.uint8) print(sess.run(a))
输出结果如下,48就是字符0的ascii码.
后面又对img进行了reshape处理,reshape处理成的格式是为了和模型输入相匹配,但是这样变是否会改变数据结构呢??比如本来是[3,img_wight,img_height],这样reshape后数据应该是拉跨了的。后续又对gtbox进行处理,至于为啥一个框是9个坐标呢??应该是四个角的坐标+1个label或者是三个角坐标+中心坐标+1个label。
最终函数返回了目标名,图像,gtbox和label以及目标数量。
现在返回read_and_prepocess_single_img函数。
第一个if判断是否需要灰度化,cfgs里该参数设定为了False不用管,第二个if判断图像是否rotate即旋转了,设定为True,那么if里的函数就是以0.5的概率进行旋转,旋转的角度在(-90,96)之间,跨度为15度??传入原始图和gtbox_and_label后返回旋转后的图像和gtbox_and_label。
image_preprocess.short_side_resize这个函数用于尺寸限制,不过有点奇怪,传入的最短边在cfgs中设置为800,cfgs.IMG_MAX_LENGTH这个最大值限制也是800??迷惑。
后面的两个if判断是否水平翻转和垂直翻转,cfgs中设置的都是True,然后以概率来进行翻转。
如果不是训练模式,而是推理模式,那么直接进行resize到规定的尺寸就好了。
cfgs.NET_NAME在cfgs中设定的是resnet152,执行的除以255应该是类似于归一化的,至于为啥减去那个参数:有三个参数(广播机制会进行使每个像素点RGB都减去这个值)。
最终这个函数返回预处理后的图像名,图像,图像的gtbox和label,图像数量以及图像的高和宽。
这个函数也解读完毕,再返回到next_batch函数中,到了最后一句的batch函数,这个函数将第一个参数进行入队,再以batch_size大小进行读出,具体含义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 tf.train.batch( tensors, batch_size, num_threads=1 , capacity=32 , enqueue_many=False , shapes=None , dynamic_pad=False , allow_smaller_final_batch=False , shared_name=None , name=None )
函数功能:利用一个tensor的列表或字典来获取一个batch数据
参数介绍:
tensors:一个列表或字典的tensor用来进行入队 batch_size:设置每次从队列中获取出队数据的数量 num_threads:用来控制入队tensors线程的数量,如果num_threads大于1,则batch操作将是非确定性的,输出的batch可能会乱序 capacity:一个整数,用来设置队列中元素的最大数量 enqueue_many:在tensors中的tensor是否是单个样本 shapes:可选,每个样本的shape,默认是tensors的shape dynamic_pad:Boolean值.允许输入变量的shape,出队后会自动填补维度,来保持与batch内的shapes相同 allow_samller_final_batch:可选,Boolean值,如果为True队列中的样本数量小于batch_size时,出队的数量会以最终遗留下来的样本进行出队,如果为Flalse,小于batch_size的样本不会做出队处理 shared_name:可选,通过设置该参数,可以对多个会话共享队列 name:可选,操作的名字 但是这里略有疑问,我们之前获取文件名(该文件包含了所有的训练集数据),但读取的是全读出来了??还是只读了一个,若是全读出来了,入队倒是可以理解,然后以batch_size=16进行读出,但是数据处理shape那里又感觉颇有问题;如果是只读了一个数据??那么感觉如队的时候就有问题。
参考链接如下:
tensorflow中的tf.train.batch详解
- - - - - - - - - - - - - - 本文结束啦,感谢您的观看 - - - - - - - - - - - - - -