首页 >> 中医诊断

管理从一次CycleGAN实现聊聊TF

中医诊断  2020年09月19日  浏览:6 次

从一次 CycleGAN 实现聊聊 TF

AI科技评论按,本文作者Coldwings,该文首发于知乎专栏为爱写程序,(公众号:) AI科技评论获其授权转载。以下为原文内容,有删减。

CycleGAN是个很有趣的想法(Unpaired Image-to-Image Translationusing Cycle-Consistent Adversarial Networks []),看完这篇论文之后,隐隐地觉得,这后面有更多的内容可以挖,我尽我所能做出了各种尝试,努力发掘更多的可能性。

实现过程中可以说还是略微纠结的,最初是用Keras快速实践了一下,然而其实并不『快速』,后来反倒是用TensorFlow重写以及尝试各种意外想法时才感觉,当需要处理一些比较复杂的络结构、训练流程甚至op时,TF提供的可以细化到每个操作的体验实际上要比各种上层API都来得更好,而结合TensorBoard,可视化的训练将取得更好的效果。当然,我对Torch无感,或许用Torch能有更好的体验,但我不擅长这个;Chainer(A flexible framework for neural networks)讲道理写出来的代码会更好看,但是似乎身边用的人并不多,姑且放过。

这篇文章倒不是来介绍什么是CycleGAN的,若是不甚了解,我妻子将会将她的发表整理一下再发布出来(CycleGAN(以及DiscoGAN和DualGAN)简介 - 知乎专栏)。这一阵的尝试中,我自己也对GAN,对Generator中的图像甚至其它东西的生成,以及单纯从写代码角度来看,怎么管理TF里的变量,怎么把代码写得好看,怎么更好地利用TensorBoard都有了更多地理解在我们看来,算是不小的提高吧……

所以这里也就大概提一提一些实现中需要注意的小技巧吧。(虽然我觉得大概大多数真正拿着TF搞DL研究的人都不需要研究这篇文章)

CycleGAN比较麻烦的地方其实CycleGAN麻烦的地方不少,这是一个挺复合的模型:两个Generator,两个Discriminator,这已经是四个比较简单的络了(是的,考虑到所有可能性,Generator和Discriminator完全可以各自都有两种不同的结构);一组Generator+Discriminator复合成一个GAN,又一层复合模型,并且GAN的训练还得控制,由于G和D的损失相反,训练G时需要控制D的变量让其不可训练;我们还要让Cycle loss作为模型loss的一部分,这个更高一层的复合模型由两个GAN组成……

良好的代码结构TensorFlow的自由度挺高的,类比的话,有那么点DL框架里的C++的意思;Python的语言灵活度也是高得不行,两个很灵活的玩意放一起,写个简单模型自然想怎么玩就怎么玩,写个复杂一些的模型,为了保证写着方便,用着方便,改起来方便

,还是需要比较好的代码结构的。

如果翻翻GitHub上一些比较热的用TF写的模型,通常都会发现大家比较习惯于把代码分成op、module和model三个部分。

op里是一些通用层或者运算的简化定义,例如写个卷积层,总是包含定义变量和定义运算。习惯于Keras这样不需要自己定义变量的玩意当然不会太纠结,但用TF时,若是写两行定义一下变量总是挺让人伤神的。

如果参照Keras的实现,通过写个类来定制op,变量管理看起来方便一点,未免太过繁琐。实际上TF提供的variable scope已经非常方便了,这一部分写成这样似乎也不错

def conv2d(input, filter, kernel, strides=1, stddev=0.02, name=conv2d):

with riable_scope(name):

w = t_variable(

w,

(kernel, kernel, t_shape()[-1], filter),

initializer=uncated_normal_initializer(stddev=stddev)

)

conv = nv2d(input, w, strides=[1, strides, strides, 1], padding=VALID)

b = t_variable(

b,

[filter],

initializer=nstant_initializer(0.0)

)

conv = shape(as_add(conv, b), ape(conv))

return conv

这样定义几个op之后,写起代码来就更有点类似于mxnet那样的感觉了。

特别的,有些时候有些简单结构,例如ResNet中的一个block这样的玩意,我们也可以用类似的方式,用一个简单函数包装起来

def res_block(x, dim, name=res_block):

with riable_scope(name):

y = reflect_pad(x, name=rp1)

y = conv2d(y, dim, 3, name=conv1)

y = lrelu(y)

y = reflect_pad(y, name=rp2)

y = conv2d(y, dim, 3, name=conv2)

y = lrelu(y)

return d(x, y)

对于重复的模块,这样的包装也方便多次使用。

这些是很常见的做法。同时我们也发现了,几乎每个这样的函数里都少不了一个variable scope的使用专家介绍,一方面避免定义变量时名字的重复以及训练时变量的管理,另一方面也方便TensorBoard画图的时候能把有用的东西放到一起。但这样每个函数里带个name参数的做法写多了也会烦,加上奇怪的缩进……我会更倾向于用一个装饰器来解决这样的问题,同时也能减少『忘了用variable scope』的情况。

def scope(default_name):

def deco(fn):

def wrapper(*args, **kwargs):

if name in kwargs:

name = kwargs[name]

p(name)

else:

name = default_name

with riable_scope(name):

return fn(*args, **kwargs)

return wrapper

return deco@scope(conv2d)def conv2d(input, filter, kernel, strides=1, stddev=0.02):

w = t_variable(

w,

(kernel, kernel, t_shape()[-1], filter),

initializer=uncated_normal_initializer(stddev=stddev)

)

conv = nv2d(input, w, strides=[1, strides, strides, 1], padding=VALID)

b = t_variable(

b,

[filter],

initializer=nstant_initializer(0.0)

)

conv = shape(as_add(conv, b), ape(conv))

return conv

至于module,也就是一些稍微复杂的成型结构,例如GAN里的Discriminator和Generator,讲道理这玩意其实和op大体上是类似的,就不多说了。

最后是model。通常大家都是用类来做,因为model中往往还包含了输入数据用的placeholder、训练用的op,甚至一些具体的方法等等内容。这一块的代码建议,只不过是最好先写一个抽象类,把需要的几个接口给定义一下,然后让实际的model类继承,代码会漂亮很多,也更便于利用诸如PyCharm这样的IDE来提示你哪些东西该做而没有做。

关于config/options上常见的代码里,模型的一些参数信息大都设计成用命令行参数来传入,更多是直接使用ags来处理。但无论如何,我仍然觉得定义一个config类来管理参数是有一定必要性的,直接使用ags主要是是有大段FINE_xxx,不好看,也不方便直观地反应默认参数。相对的,如果定义一个参数类,在__init__里写下默认参数,然后写个小方法自动地根据dir来添加这些ags会漂亮许多。但这个只是个人观点,似乎并没有具体的优劣之分。

关于TensorBoard不得不说TensorBoard作为TF自带的配套可视化工具,只要你不是太在意刷新频率的问题(通常不会有人在意这个吧……),用起来实在太方便。加上能够自动生成运算的各个符号的结构图,哪怕不说训练,就是检查模型结构是否符合自己所想都是个非常好用的工具。比如封面图,生成出来用来检查代码的模型逻辑,还可以根据需要点选观察依赖关系。

amp;lt;img src=amp;quot; data-rawwidth=amp;quot;2660amp;quot; data-rawheight=amp;quot;1220amp;quot; class=amp;quot;origin_image zh-lightbox-thumbamp;quot; width=amp;quot;2660amp;quot; data-original=amp;quot; _src=amp;quot;顺带一提,如果生成的模型图长得非常奇怪,八成是代码有问题……

不过要用好TensorBoard,有几个小小的要点:首先是,至少,你的各个op和module里,得用上variable scope或者name scope。对于一个scope,在TensorBoard的Graph里会将其聚集成一个小块,内部结构可以展开观察,而如果不用scope,你会看到满眼都是一堆一堆的基本op,当模型复杂时,图基本没法看……

此外,对于图片处理,用好TensorBoard的ImageSummary当然是很不错的选择。但是记得一定要为添加图片的summary op定义一个喂数据的placeholder。

self.p_img = aceholder(oat32, shape=[1, 256 * 6, 256 * 4, 3])

g_op = age(sample, self.p_img)

……

img = ray([img])

s_img = n(g_op, feed_dict={self.p_img: img})

d_summary(s_img, count)

这样才是正确的。上有些材料里告诉你可以直接用age(tag, data)来生成图片summary,这样其实每次都会构造一个新的summary,不便于图片归类,但更大的问题是,这样做会使得每次都申请一个新的变量(用来装你的图片数据),倘若你有定周期存储训练权重的习惯,会发现没几个小时就会因为权重变量总量超过2GB而使得程序跑崩……想想看晚上跑着训练的代码想着可以回家休息了,结果前脚刚进家门,程序就罢工了,大好的训练时间就给直接浪费了。

另外,这里的图片可以是重新归为0~255的整形的数据,也可以直接给浮点数据[-1, 1]。更不错的想法是,先使用matplotlib/pil/numpy来合成、拼凑甚至生成图像,然后再来添加,会让效果更令人满意,比如这样:

amp;lt;img src=amp;quot; data-rawwidth=amp;quot;1682amp;quot; data-rawheight=amp;quot;900amp;quot; class=amp;quot;origin_image zh-lightbox-thumbamp;quot; width=amp;quot;1682amp;quot; data-original=amp;quot; _src=amp;quot;最后补充一句……双显示器确实有利于提高写代码、改代码以及码字的效率……

版权文章,未经授权禁止转载。详情见转载须知。



先声药业创新
大连白癜风专科医院
得了灰指甲马上用亮甲有用吗
友情链接