春学期开学至今这一个多月里,除了有序推进自己的科研任务,非专业的我还和协会志同道合的小伙伴跟着罗老师以参加比赛的方式在深度学习计算机视觉领域里进行实战演练。我们参加的是天池FashionAI服饰关键点定位全球挑战赛,今天(4月21日)是初赛截止的日子,凭借有限的精力和资源我们最终以test_b数据集不包含遮挡点NE(Normalized Error)7.49%的成绩排名70/2321,顺利进入复赛。虽然在复赛队伍里,我们的成绩并不拔尖,我也知道未来我们要做的还有很多很多,但是这是我第一次参赛,CPM(Convolutional Pose Machines)也是我第一个通篇读懂、调试、修改、完善的深度学习模型。和本科时候第一次参加电子设计大赛一样,第一次永远是最痛苦最煎熬的,但也是收获最大进步最显著的。从当初面试时的什么也不懂,到如今能够独当一面成为一名还算不错的深度学习炼丹师,碰了不少壁,踩了不少坑,但是我很享受这种钻研进步的过程。罗老师说,任重而道远,是的,还有很多要做,我也还有很多要学。那么就先写博客做一个阶段性的总结吧。
论文传送门:《Convolutional Pose Machines》。
作者源码传送门:https://github.com/shihenw/convolutional-pose-machines-release。
我们在比赛中用到的tensorflow实现的源码传送门:https://github.com/timctho/convolutional-pose-machines-tensorflow。
关于CPM的机理,这篇博客讲的已经很好了,CPM的结构简单,结构上复用很多,配合代码很快就能看懂,虽然我这个小白还是看了半个月,一句一句地推敲才读通了网络。在这里就不再多说原理,我想说一下我通过实践对CPM的一些理解,可能不准确,仅代表个人意见,欢迎各位指正。
优点:
缺点:
综上所述,CPM具有很好域迁移的鲁棒性,baseline挺高,但是它的缺点也决定了仅仅依靠CPM的baseline很难达到非常好的效果,即使加入完善的数据增广、加入检测给预测增加一个很强的constrain,也只能是涨一些点,要想跻身排行榜前列,需要结合一些更先进的算法,关于这一点,我们也在仔细研究。
深度学习炼丹过程是痛苦的,一种trick的效果验证的等待周期很长,先要代码实现,然后放到GPU上训练,等很长的时间一个模型训练完,然后做出结果,虽然有可视化的图片给出直观的感觉,但是还是需要提交一波结果让官方审核才放心,于是又得等待一天只有两次的测评。关键的是,我是个非常非常非常明显的急性子,等得我真的是......当然,我目前盲目的训练方法也存在问题,有很多弯路是完全可以规避的,这个后面会说。
不过,炼丹的过程却又是很有趣的,当结果显示一个小trick的加入让网络涨了哪怕只有半个百分点的时候,别提多高兴了。很多个trick都是之前从论文里看到的,知道机理却从没实践过,现在通过第一次尝试,对一些trick也有了更深的了解。后面就大致说一下我训练CPM baseline的过程,多少也能够表明训练一个网络都要做哪些事。有一点我是确信的,就是下一个网络,我绝不会再花这么久才做出第一个结果了(我的第一次结果提交是在4月4号,也就是说我差不多花了一个月)。
1、Datagenerator——数据生成器
我的师兄经常说数据预处理是训练一个网络的过程中最麻烦的,而且这个过程针对每个数据集的操作还不一样,因此针对不同网络需要写不同的数据生成器,所以,会做数据预处理,会写数据生成器,是深度学习实践的第一步。因此深度学习行业里有个岗位叫“Data provider”,它是专门做数据预处理的一个岗位。
将图片Feed进网络之前,一般需要做如下几个步骤数据预处理、数据增广、编写数据迭代器。我在CPM的实践过程中的数据生成器是团队小伙伴写的,大致做了如下操作:
综上所述,一个简单的random batch的datagenerator的函数体大概如下:
def generator(self, batch_size=16, normalize=False, sample_set='train'):
""" Auxiliary Generator
"""
while True:
train_img = np.zeros((batch_size, 256, 256, 3), dtype=np.float32)
i = 0
while i < batch_size:
if sample_set == 'train':
name = random.choice(self.train_set)
elif sample_set == 'valid':
name = random.choice(self.valid_set)
img = self.open_img(name)
......
data processing
......
......
data augmentation
......
train_img[i] = img.astype(np.float32)
i = i + 1
yield train_img
在主函数调用的时候用:
generator = dataset.generator(batchSize=FLAGS.batch_size, norm=False, sample='train')
batch_img_np = next(generator)
2、learning rate decay存在的问题
大家可以先看这篇干货:https://zhuanlan.zhihu.com/p/33107481?utm_medium=social&utm_source=wechat_session&wechatShare=1。谷歌爸爸都已经证明了增加Batch Size的同时保持学习率的策略,既可以保证不掉点,还可以减少参数更新的次数。当然,我们的硬件资源有限,显存就那么点,没办法把batch size设置成那么大,没办法还是在用decayed learning rate。
关于learning rate decay网络上博客很多,这里就不再赘述了。learning rate decay设计的初衷就是为了让网络在开始训练的时候可以用一个较大的学习率迅速调节网络参数,之后再网络的后期通过一个小的学习率fine tune网络参数,使网络下降到全局或者局部最优,解决了固定学习率网络训练后期在全局或局部最优点附近震荡等问题。但是learning rate decay的参数很难调,decay rate和decay step很难调到和网络相匹配,decay周期长了,网络后期还在以一个较大的值在持续训练,可能出现过拟合甚至跳出全局/局部最优点造成loss回升;decay周期短了,网络还没有收敛完全,learning rate就非常非常小了,网络参数就更新不动了。这两种情况在实践过程中我都出现过。所以说,learning rate decay存在很严重的网络匹配问题。
同样的原因,我们的解决方法时机成熟再和大家分享。
3、适当的fine tune很重要
fine tune就是在网络训练的后期以一个合适的小学习率更新网络参数,使网络更好地拟合训练集,在全局/局部最优点处收敛得更好。实践证明,我们通过fine tune让模型在test a上增长了一个多点。
4、理解tensorflow命名空间
和小伙伴在写BN代码的时候总是会出现命名空间复用的问题,那时候不懂命名空间,一气之下干脆把bug相关的变量定义从tf.get_variable全部替换成了tf.Variable,并且名字都设置成了None,最后辛辛苦苦训练了一天多的网络保存的checkpoint在load进test网络的时候发生变量不存在的错误,训好的网络参数一点用都没有了,白白浪费了很多时间。
因此,以后管理变量尽量都用scope管理好,在不同的代码块用不同的scope管理代码。比如CPM中每个stage都定义一个单独的scope。并且用tf.get_variable定义时,每个变量定义一个自己唯一的名字,这样保存的checkpoint才能根据变量名成功地加载参数,也规范了网络的变量管理。
5、切忌“盲训”,要建立自己的validation机制
我们在训练的时候没有根据大赛官方的评价标准建立自己的验证机制,每做一次新的尝试,都要等待经过漫长的等待时间获得官方给出的test结果,这种盲目的时间浪费了我们大量的时间。如果在训练的过程中就建立和大赛官方标准一样的NE评价机制,就能直接在训练过程中通过观察验证集的NE值得到新的trick的性能,这样能减少浪费的时间,并且有据可依地尝试各种方法。做比赛嘛,要过拟合,也要在官方评价指标过拟合。
上述是我自己在训练CPM的基础模型的过程中遇到的一些问题,对于大神来说可能不算什么,但对于第一次训练完整深度学习模型的我来说,都是第一次遇见。过程中确实走了不少弯路、碰了不少壁、踩了不少坑。在此总结一番,便于自己今后学习翻阅,也和大家一起交流分享。还有一些trick我会继续总结好再和大家一起分享,代码我也会在后面开源。未来还有很多要学,那就继续学下去吧。
最后,对于文中的错误,还请大家指正。
-------------------------------------------
Youzhi Gu, master student
Foresight Control Center
College of Control Science & Engineering
Zhejiang University
Email: [email protected]