『PyTorch × TensorFlow』第十七弹_ResNet快速实现

要点

  • 神经网络逐层加深有Degradiation问题,准确率先上升到饱和,再加深会下降,这不是过拟合,是测试集和训练集同时下降的
  • 提出了残差结构,这个结构解决了深层网络训练误差反而提升的情况,使得网络理论上可以无限深
  • bottleneck网络结构,注意Channel维度变化: ,宛如一个中间细两端粗的瓶颈,所以称为“bottleneck”。这种结构相比VGG,早已经被证明是非常效的,能够更好的提取图像特征。

  

残差结构

截取代码如下,

  1. @slim.add_arg_scope
  2. def bottleneck(inputs,depth,depth_bottleneck,stride,
  3. outputs_collections=None,scope=None):
  4. """
  5. 核心残差学习单元
  6. 输入tensor给出一个直连部分和残差部分加和的输出tensor
  7. :param inputs: 输入tensor
  8. :param depth: Block类参数,输出tensor通道
  9. :param depth_bottleneck: Block类参数,中间卷积层通道数
  10. :param stride: Block类参数,降采样步长
  11. 3个卷积只有中间层采用非1步长去降采样。
  12. :param outputs_collections: 节点容器collection
  13. :return: 输出tensor
  14. """
  15. with tf.variable_scope(scope,'bottleneck_v2',[inputs]) as sc:
  16. # 获取输入tensor的最后一个维度(通道)
  17. depth_in = slim.utils.last_dimension(inputs.get_shape(),min_rank=4)
  18. # 对输入正则化处理,并激活
  19. preact = slim.batch_norm(inputs,activation_fn=tf.nn.relu,scope='preact')
  20.  
  21. 'shortcut直连部分'
  22. if depth == depth_in:
  23. # 如果输入tensor通道数等于输出tensor通道数
  24. # 降采样输入tensor使之宽高等于输出tensor
  25. shortcut = subsample(inputs,stride,'shortcut')
  26. else:
  27. # 否则,使用尺寸为1*1的卷积核改变其通道数,
  28. # 同时调整宽高匹配输出tensor
  29. shortcut = slim.conv2d(preact,depth,[1,1],stride,
  30. normalizer_fn=None,activation_fn=None,
  31. scope='shortcut')
  32. 'residual残差部分'
  33. residual = slim.conv2d(preact,depth_bottleneck,[1,1],stride=1,scope='conv1')
  34. residual = slim.conv2d(residual,depth_bottleneck,3,stride=stride,scope='conv2')
  35. residual = slim.conv2d(residual,depth,[1,1],stride=1,scope='conv3')
  36.  
  37. output = shortcut + residual
  38.  
  39. return slim.utils.collect_named_outputs(outputs_collections,sc.name,output)

逻辑流程如下,

批正则化数据

shortcut分量处理:调整输入tensor使之和输出tensor深度一致,宽高一致

residual分量处理:1*1/1卷积->3*3/自定步长(所以上面需要调整shortcut宽高)卷积->1*1/1卷积

shortcut + residual 作为最终输出,注意是加,不是concat。

代码

之前一直对变量作用域的实际功效比较不解(虽然介绍文章看了很多)特别是reuse属性搭配上后,这节网络结构较为复杂,图结构生成方式很混乱,也和自己粗心有关,运行时出了一些变量重复的错,修改过程中对变量作用域的理解深入了。所谓的reuse或者重名什么的和变量生成函数没有关系,只要同一个生成函数不同生成次数时指定不同的name就完全没问题,重点是在计算图的位置要不一样。如果是同一个计算图位置重复生成(初始化)的话要注明reuse=True。

实际例子中作者采用了一些写起来显得麻烦但是提高了效率的架构方式,比如conv2d_same的实现,和网络结束部分的全局平均池化,都为了效率放弃了最简单的写法。

网络结构如下,

  1. # Author : Hellcat
  2. # Time : 2017/12/14
  3.  
  4. import math
  5. import time
  6. from datetime import datetime
  7. import collections
  8. import tensorflow as tf
  9. slim = tf.contrib.slim
  10.  
  11. class Block(collections.namedtuple('Block',['scope','unit_fn','args'])):
  12. """
  13. 使用collections.namedtuple设计ResNet基本模块组的name tuple,并用它创建Block的类
  14. 只包含数据结构,不包含具体方法。
  15. 定义一个典型的Block,需要输入三个参数:
  16. scope:Block的名称
  17. unit_fn:ResNet V2中的残差学习单元生成函数
  18. args:Block的args(输出深度,瓶颈深度,瓶颈步长)
  19. """
  20.  
  21. def subsample(inputs,factor,scope=None):
  22. """
  23. 如果factor为1,则不做修改直接返回inputs;如果不为1,则使用
  24. slim.max_pool2d最大池化来实现,通过1*1的池化尺寸,factor作步长,实
  25. 现降采样。
  26. :param inputs: A 4-D tensor of size [batch, height_in, width_in, channels]
  27. :param factor: 采样因子
  28. :param scope: 域名
  29. :return: 采样结果
  30. """
  31. if factor == 1:
  32. return inputs
  33. else:
  34. return slim.max_pool2d(inputs,[1,1],stride=factor,scope=scope)
  35.  
  36. def conv2d_same(inputs,num_outputs,kernel_size,stride,scope=None):
  37. """
  38. 卷积层实现,有更简单的写法,这样做其实是为了提高效率
  39. :param inputs: 输入tensor
  40. :param num_outputs: 输出通道
  41. :param kernel_size: 卷积核尺寸
  42. :param stride: 卷积步长
  43. :param scope: 节点名称
  44. :return: 输出tensor
  45. """
  46. if stride == 1:
  47. return slim.conv2d(inputs,num_outputs,kernel_size,stride=1,padding='SAME',scope=scope)
  48. else:
  49. pad_total = kernel_size - 1
  50. pad_beg = pad_total // 2
  51. pad_end = pad_total - pad_beg
  52. inputs = tf.pad(inputs,[[0,0],[pad_beg,pad_end],
  53. [pad_beg,pad_end],[0,0]])
  54. return slim.conv2d(inputs,num_outputs,kernel_size,stride=stride,
  55. padding='VALID',scope=scope)
  56.  
  57. @slim.add_arg_scope
  58. def stack_block_dense(net,blocks,outputs_collections=None):
  59. """
  60. 示例,Block('block1',bottleneck,[(256,64,1)]*2 + [(256,64,2)])
  61. :param net: A `Tensor` of size [batch, height, width, channels].
  62. :param blocks: 是之前定义的Block的class的列表
  63. :param outputs_collections: 收集各个end_points的collections
  64. :return: Output tensor
  65. """
  66. for block in blocks:
  67. with tf.variable_scope(block.scope,'block',[net]) as sc:
  68. for i,unit in enumerate(block.args):
  69. with tf.variable_scope('unit_%d' % (i+1), values=[net]):
  70. # 示例:(256,64,1)
  71. unit_depth,unit_depth_bottleneck,unit_stride = unit
  72. net = block.unit_fn(net,
  73. depth=unit_depth,
  74. depth_bottleneck=unit_depth_bottleneck,
  75. stride=unit_stride)
  76. net = slim.utils.collect_named_outputs(outputs_collections,sc.name,net)
  77. '''
  78. 这个方法会返回本次添加的tensor对象,
  79. 意义是为tensor添加一个别名,并收集进collections中
  80. 实现如下
  81. if collections:
  82. append_tensor_alias(outputs,alias)
  83. ops.add_to_collections(collections,outputs)
  84. return outputs
  85.  
  86. 据说本方法位置已经被转移到这里了,
  87. from tensorflow.contrib.layers.python.layers import utils
  88. utils.collect_named_outputs()
  89. '''
  90. return net
  91. @slim.add_arg_scope
  92. def bottleneck(inputs,depth,depth_bottleneck,stride,
  93. outputs_collections=None,scope=None):
  94. """
  95. 核心残差学习单元
  96. 输入tensor给出一个直连部分和残差部分加和的输出tensor
  97. :param inputs: 输入tensor
  98. :param depth: Block类参数,输出tensor通道
  99. :param depth_bottleneck: Block类参数,中间卷积层通道数
  100. :param stride: Block类参数,降采样步长
  101. 3个卷积只有中间层采用非1步长去降采样。
  102. :param outputs_collections: 节点容器collection
  103. :return: 输出tensor
  104. """
  105. with tf.variable_scope(scope,'bottleneck_v2',[inputs]) as sc:
  106. # 获取输入tensor的最后一个维度(通道)
  107. depth_in = slim.utils.last_dimension(inputs.get_shape(),min_rank=4)
  108. # 对输入正则化处理,并激活
  109. preact = slim.batch_norm(inputs,activation_fn=tf.nn.relu,scope='preact')
  110.  
  111. 'shortcut直连部分'
  112. if depth == depth_in:
  113. # 如果输入tensor通道数等于输出tensor通道数
  114. # 降采样输入tensor使之宽高等于输出tensor
  115. shortcut = subsample(inputs,stride,'shortcut')
  116. else:
  117. # 否则,使用尺寸为1*1的卷积核改变其通道数,
  118. # 同时调整宽高匹配输出tensor
  119. shortcut = slim.conv2d(preact,depth,[1,1],stride,
  120. normalizer_fn=None,activation_fn=None,
  121. scope='shortcut')
  122. 'residual残差部分'
  123. residual = slim.conv2d(preact,depth_bottleneck,[1,1],stride=1,scope='conv1')
  124. residual = slim.conv2d(residual,depth_bottleneck,3,stride=stride,scope='conv2')
  125. residual = slim.conv2d(residual,depth,[1,1],stride=1,scope='conv3')
  126.  
  127. output = shortcut + residual
  128.  
  129. return slim.utils.collect_named_outputs(outputs_collections,sc.name,output)
  130.  
  131. def resnet_arg_scope(is_training=True,
  132. weight_decay=0.0001, # L2权重衰减速率
  133. batch_norm_decay=0.997, # BN的衰减速率
  134. batch_norm_epsilon=1e-5, # BN的epsilon默认1e-5
  135. batch_norm_scale=True): # BN的scale默认值
  136.  
  137. batch_norm_params = { # 定义batch normalization(标准化)的参数字典
  138. 'is_training': is_training,
  139. # 是否是在训练模式,如果是在训练阶段,将会使用指数衰减函数(衰减系数为指定的decay),
  140. # 对moving_mean和moving_variance进行统计特性的动量更新,也就是进行使用指数衰减函数对均值和方
  141. # 差进行更新,而如果是在测试阶段,均值和方差就是固定不变的,是在训练阶段就求好的,在训练阶段,
  142. # 每个批的均值和方差的更新是加上了一个指数衰减函数,而最后求得的整个训练样本的均值和方差就是所
  143. # 有批的均值的均值,和所有批的方差的无偏估计
  144.  
  145. 'zero_debias_moving_mean': True,
  146. # 如果为True,将会创建一个新的变量对 'moving_mean/biased' and 'moving_mean/local_step',
  147. # 默认设置为False,将其设为True可以增加稳定性
  148.  
  149. 'decay': batch_norm_decay, # Decay for the moving averages.
  150. # 该参数能够衡量使用指数衰减函数更新均值方差时,更新的速度,取值通常在0.999-0.99-0.9之间,值
  151. # 越小,代表更新速度越快,而值太大的话,有可能会导致均值方差更新太慢,而最后变成一个常量1,而
  152. # 这个值会导致模型性能较低很多.另外,如果出现过拟合时,也可以考虑增加均值和方差的更新速度,也
  153. # 就是减小decay
  154.  
  155. 'epsilon': batch_norm_epsilon, # 就是在归一化时,除以方差时,防止方差为0而加上的一个数
  156. 'scale': batch_norm_scale,
  157. 'updates_collections': tf.GraphKeys.UPDATE_OPS,
  158. # force in-place updates of mean and variance estimates
  159. # 该参数有一个默认值,ops.GraphKeys.UPDATE_OPS,当取默认值时,slim会在当前批训练完成后再更新均
  160. # 值和方差,这样会存在一个问题,就是当前批数据使用的均值和方差总是慢一拍,最后导致训练出来的模
  161. # 型性能较差。所以,一般需要将该值设为None,这样slim进行批处理时,会对均值和方差进行即时更新,
  162. # 批处理使用的就是最新的均值和方差。
  163. #
  164. # 另外,不论是即使更新还是一步训练后再对所有均值方差一起更新,对测试数据是没有影响的,即测试数
  165. # 据使用的都是保存的模型中的均值方差数据,但是如果你在训练中需要测试,而忘了将is_training这个值
  166. # 改成false,那么这批测试数据将会综合当前批数据的均值方差和训练数据的均值方差。而这样做应该是不
  167. # 正确的。
  168. }
  169.  
  170. with slim.arg_scope(
  171. [slim.conv2d],
  172. weights_regularizer=slim.l2_regularizer(weight_decay), # 权重正则器设置为L2正则
  173. weights_initializer=slim.variance_scaling_initializer(),
  174. activation_fn=tf.nn.relu,
  175. normalizer_fn=slim.batch_norm, # 标准化器设置为BN
  176. normalizer_params=batch_norm_params):
  177. with slim.arg_scope([slim.batch_norm],**batch_norm_params):
  178. with slim.arg_scope([slim.max_pool2d],padding='SAME') as arg_sc:
  179. return arg_sc
  180.  
  181. def resnet_v2(inputs,
  182. blocks,
  183. num_classes=None,
  184. global_pool=True,
  185. include_root_block=True,
  186. reuse=None,
  187. scope=None):
  188. """
  189. 网络结构主函数
  190. :param inputs: 输入tensor
  191. :param blocks: Block类列表
  192. :param num_classes: 输出类别数
  193. :param global_pool: 是否最后一层全局平均池化
  194. :param include_root_block: 是否最前方添加7*7卷积和最大池化
  195. :param reuse: 是否重用
  196. :param scope: 整个网络名称
  197. :return:
  198. """
  199. with tf.variable_scope(scope,'resnet_v2',[inputs],reuse=reuse) as sc:
  200. # 字符串,用于命名collection名字
  201. end_points_collecion = sc.original_name_scope + 'end_points'
  202. print(end_points_collecion)
  203. with slim.arg_scope([slim.conv2d,bottleneck,stack_block_dense],
  204. # 为新的收集器取名
  205. outputs_collections=end_points_collecion):
  206. net = inputs
  207. if include_root_block:
  208. with slim.arg_scope([slim.conv2d],
  209. activation_fn=None,
  210. normalizer_fn=None):
  211. # 卷积:2步长,7*7核,64通道
  212. net = conv2d_same(net,64,7,stride=2,scope='conv1')
  213. # 池化:2步长,3*3核
  214. net = slim.max_pool2d(net,[3,3],stride=2,scope='pool1')
  215. # 至此图片缩小为1/4
  216. # 读取blocks数据结构,生成残差结构
  217. net = stack_block_dense(net,blocks)
  218. net = slim.batch_norm(net,
  219. activation_fn=tf.nn.relu,
  220. scope='postnorm')
  221. if global_pool:
  222. # 全局平均池化,效率比avg_pool更高
  223. # 即对每个feature做出平均池化,使每个feature输出一个值
  224. net = tf.reduce_mean(net,[1,2],name='pool5',keep_dims=True)
  225. if num_classes is not None:
  226. net = slim.conv2d(net,num_classes,[1,1],
  227. activation_fn=None,
  228. normalizer_fn=None,
  229. scope='logits')
  230. # 将collection转化为dict
  231. end_points = slim.utils.convert_collection_to_dict(end_points_collecion)
  232. if num_classes is not None:
  233. # 为dict添加节点
  234. end_points['predictions'] = slim.softmax(net,scope='predictions')
  235. return net, end_points
  236.  
  237. def resnet_v2_152(inputs,
  238. num_classes=None,
  239. global_pool=True,
  240. reuse=None,
  241. scope='resnet_v2_152'):
  242. blocks = [
  243. # 输出深度,瓶颈深度,瓶颈步长
  244. Block('block1',bottleneck,[(256,64,1)]*2 + [(256,64,2)]),
  245. Block('block2',bottleneck,[(512,128,1)]*7 + [(512,128,2)]),
  246. Block('block3',bottleneck,[(1024,256,1)]*35 + [(1024,256,2)]),
  247. Block('block4',bottleneck,[(2048,512,1)]*3)
  248. ]
  249. return resnet_v2(inputs,blocks,num_classes,global_pool,
  250. include_root_block=True,reuse=reuse,scope=scope)
  251.  
  252. #-------------------评测函数---------------------------------
  253.  
  254. # 测试152层深的ResNet的forward性能
  255. def time_tensorflow_run(session, target, info_string):
  256. num_steps_burn_in = 10
  257. total_duration = 0.0
  258. total_duration_squared = 0.0
  259. for i in range(num_batches + num_steps_burn_in):
  260. start_time = time.time()
  261. _ = session.run(target)
  262. duration = time.time() - start_time
  263. if i >= num_steps_burn_in:
  264. if not i % 10:
  265. print ('%s: step %d, duration = %.3f' %
  266. (datetime.now(), i - num_steps_burn_in, duration))
  267. total_duration += duration
  268. total_duration_squared += duration * duration
  269. mn = total_duration / num_batches
  270. vr = total_duration_squared / num_batches - mn * mn
  271. sd = math.sqrt(vr)
  272. print('%s: %s across %d steps, %.3f +/- %.3f sec / batch' %
  273. (datetime.now(), info_string, num_batches, mn, sd))
  274.  
  275. batch_size = 32
  276. height,width = 224,224
  277. inputs = tf.random_uniform([batch_size,height,width,3])
  278. with slim.arg_scope(resnet_arg_scope(is_training=False)):
  279. # 1000分类
  280. net,end_points = resnet_v2_152(inputs,1000)
  281.  
  282. init = tf.global_variables_initializer()
  283. sess = tf.Session()
  284. sess.run(init)
  285. num_batches = 100
  286. time_tensorflow_run(sess,net,'Forward')
  287. # forward计算耗时相比VGGNet和Inception V3大概只增加了50%,是一个实用的卷积神经网络。

和上节同理,不贴时耗。

『TensorFlow』读书笔记_ResNet_V2的更多相关文章

  1. 『TensorFlow』读书笔记_降噪自编码器

    『TensorFlow』降噪自编码器设计  之前学习过的代码,又敲了一遍,新的收获也还是有的,因为这次注释写的比较详尽,所以再次记录一下,具体的相关知识查阅之前写的文章即可(见上面链接). # Aut ...

  2. 『TensorFlow』读书笔记_VGGNet

    VGGNet网络介绍 VGG系列结构图, 『cs231n』卷积神经网络工程实践技巧_下 1,全部使用3*3的卷积核和2*2的池化核,通过不断加深网络结构来提升性能. 所有卷积层都是同样大小的filte ...

  3. 『TensorFlow』读书笔记_SoftMax分类器

    开坑之前 今年3.4月份的时候就买了这本书,同时还买了另外一本更为浅显的书,当时读不懂这本,所以一度以为这本书很一般,前些日子看见知乎有人推荐它,也就拿出来翻翻看,发现写的的确蛮好,只是稍微深一点,当 ...

  4. 『TensorFlow』读书笔记_多层感知机

    多层感知机 输入->线性变换->Relu激活->线性变换->Softmax分类 多层感知机将mnist的结果提升到了98%左右的水平 知识点 过拟合:采用dropout解决,本 ...

  5. 『TensorFlow』读书笔记_简单卷积神经网络

    如果你可视化CNN的各层级结构,你会发现里面的每一层神经元的激活态都对应了一种特定的信息,越是底层的,就越接近画面的纹理信息,如同物品的材质. 越是上层的,就越接近实际内容(能说出来是个什么东西的那些 ...

  6. 『TensorFlow』读书笔记_进阶卷积神经网络_分类cifar10_上

    完整项目见:Github 完整项目中最终使用了ResNet进行分类,而卷积版本较本篇中结构为了提升训练效果也略有改动 本节主要介绍进阶的卷积神经网络设计相关,数据读入以及增强在下一节再与介绍 网络相关 ...

  7. 『TensorFlow』读书笔记_进阶卷积神经网络_分类cifar10_下

    数据读取部分实现 文中采用了tensorflow的从文件直接读取数据的方式,逻辑流程如下, 实现如下, # Author : Hellcat # Time : 2017/12/9 import os ...

  8. 『TensorFlow』读书笔记_AlexNet

    网络结构 创新点 Relu激活函数:效果好于sigmoid,且解决了梯度弥散问题 Dropout层:Alexnet验证了dropout层的效果 重叠的最大池化:此前以平均池化为主,最大池化避免了平均池 ...

  9. 『TensorFlow』读书笔记_Inception_V3_下

    极为庞大的网络结构,不过下一节的ResNet也不小 线性的组成,结构大体如下: 常规卷积部分->Inception模块组1->Inception模块组2->Inception模块组3 ...

随机推荐

  1. java JFR

    1. 参数: -XX:+UnlockCommercialFeatures -XX:+FlightRecorder 2. 运行命令: jcmd <PID> JFR.start name=te ...

  2. 从Windows到linux小记

    从Windows到linux小记 年后疯狂加班,趁着喘息的时间,更新一下安装linux的艰辛路程. 周四晚上,公司举办活动,好不容易从加班的节奏暂时脱离出来,我这人就是不能闲,只要一闲下来就会做die ...

  3. git 用远程覆盖本地

    git 用远程覆盖本地   git fetch --all git reset --hard origin/master

  4. jQuery学习--Code Organization Concepts

    jQuery官方文档:  http://learn.jquery.com/code-organization/concepts/ Code Organization Concepts(代码组织概念) ...

  5. 公网k8s

    dm :32750/swagger/ 统一在   cd /opt/iot 删除容器,自动创建容器 dm 更新dm和acl包  dm源文件chart包   cd /var/lib/helmrepo/ h ...

  6. 8 . IO类-标准IO、文件IO、stringIO

    8.1 IO类 #include <iostream> //标准IO头文件  8.2 文件输入输出流 #include <fstream> //读写文件头文件 std::fst ...

  7. Java读取resource文件/路径的几种方式

    方式一: String fileName = this.getClass().getClassLoader().getResource("文件名").getPath();//获取文 ...

  8. Electron把网页打包成桌面应用并进行源码加密

    前言 最近想把自己用html+css+js做的网页界面打包成桌面应用,网上一搜,发现Electron是一个不错的选择,试了试,发现效果真的不错.这里记录一下打包过程以作记录,便于自己以后查看学习. 一 ...

  9. HTML5中 audio标签的样式修改

    由于html5的流行,现在移动端大多数的需求都可以使用audio来播放音频,但您可能只是需要很简单的播放/停止效果,但不同的浏览器上的audio样式却不尽人意,那么要怎么改变这个样式呢,其实它的原理比 ...

  10. MySQL驱动和数据库字符集设置不搭配

    刚才控制台又报这个错,这是代表MySQL驱动和数据库字符集设置不搭配: 错误: "...Initial client character set can be forced via the ...