PyTorch 深度学习开发填坑日记

转载自:https://zhuanlan.zhihu.com/p/374843205  侵删

  1. 背景
  • 今天因为一场意外发现了深埋在自己代码中将近半年的bug,解完之后觉得整个人都神清气爽了很多.自从读研之后,coding的重心从工程实现偏向了科研探索,已经很久没有感受到由它带给我的强烈的成就感和乐趣了。趁着今天的热情以及发现了神奇bug,决定从工程师角度写一写我最近两年做深度学习开发遇到的千奇百怪的bug(不会报错,但是却会影响你的训练,验证或者测试表现的bug)和一些开发感悟。希望能有一些帮助,如果大家比较感兴趣我会再补上部分我记录直接报错的bug和相应的分析和解决。

2.关于transform

    • 所有尺寸相关的输入,一定要输入完整的目标尺寸。
      • 比如transform.Resize((224,224)),而不要只是输入transform.Resize(224)。我最开始看到很多代码都是只有一个224输入,所以我也直接这样用了,但是实际上这个是有问题的。因为底层并不是给你resize到(224,224),而是根据图像的长宽比计算另一个值,也就是实际是(224, 224*h/w)。
      • 但是我们一个数据集的数据通常长宽比是一样的,特别是现在网络最后通常接的都是GAP,所以就算是只是输入一个224,代码也不会报错,甚至是你跨数据集测试都不会有报错,但是实际上输入网络的图像尺寸可能已经发生了变化(如果两个数据集的图像长宽比不一样的话),有可能会影响测试准确率。
      • 那么这个问题是怎么发现的?因为有一次使用自己搜集一些数据做测试,图像长宽比不一样,运行到中间的突然报错,具体错误是什么当时没有记录,但是原因是因为一个batch的内图像尺寸不一样。我当时想了很久没有想明白我都resize了怎么还会不一样,后来一层层抽丝剥茧才发现了这个bug。因为长宽比不一样,所以resize结果不一样,导致了尺寸不一样。
    • 所有的尺寸输入要用括号!!
      • 这个很简单,如果不是括号的话,比如transform.Resize(224,224),那么你的参数size得到只是一个参数224,同样的,因为上一个问题,不会报错,所以也发现不了。
      • 你可能觉得resize没什么,至多只是尺寸偏差。但是在这两个问题对于randomcrop也同样存在啊,而且random会把另一个参数当做pading的参数,然后你就会发现你randomcrop出来的结果可能就是一片黑色区域,这就是你错误参数传递导致的错误。后果就是你的数据增强越用效果越差。
      • 这个发现的原因很巧合,因为是我其实很少用这个增强的方式,并且也不是没次使用都没有使用括号。有一次是自己写了一个transform想要看看运行处理效果,而且刚好那次用了这个增强同时没有用了错误输入(应该不能更巧了),然后才发现看这个错误。
    • 如果是自己写transform,要么统一使用opencv,要么统一pil,如果用来回切二者进行处理,一定要注意转换通道,但是时间开销会有些大
      • pytorch默认使用的pil的图像格式,图像是RGB顺序,而opencv是BGR,如果不保证图像通道顺序一致,并不会报错,但是你的结果可能会非常差。
      • 说“可能”是因为,如果你使用了某些像素相关的预处理,就会导致这个问题,但是其他时候可能就不会。比如你之前是pil图像,转成array之后直接使用opencv处理,那么效果和你直接写函数,用opencv读取和处理的结果是不一样的,可能就不是你需要的预处理。
      • 推荐是尽量都是pil格式的处理,很多opencv的处理,pil图像也可以,而且这样不易切换格式的话处理时间会更快
    • 将一切随机增强之外的图像变换都拿出transform外提前完成。
      • 这个目的是为了尽可能降低transform的复杂度,提高训练速度。否则你会发现你的GPU利用率忽高忽低而且高的时间占比很小,训练速度提不上去。这个是因为transform的处理是在cpu上处理的,而这时候瓶颈在于CPU处理的速度跟不上GPU的处理速度。
      • 类似的有人脸检测,统一的图像增强等
      • 其实,理论上所有的操作,包括随机操作,都可以从前往后都能拿出来先完成,然后把数据保存下来打上对应的标签,之后训练的时候直接导入数据,相当于离线完成了数据增强和增广。但是这样有些麻烦,所以看个人的需求决定。
    • 最后,如果不确定自己写的transform或者感觉使用效果不好,可以自己用几张图可视化试一下transform处理结果。
      • 千言万语不及一张图。

3. 关于dropout无效的问题

    • dropout有两种使用方式,nn.Dropout 和F.dropout,前者是类,后者是方法。如果使用前者,万事大吉,啥事没有,但是使用后者,一定要加上self.training的参数,控制dropout的开关,不然默认是关闭状态。也就是不会起作用
    • 这里额外补一句,使用pytorch搭建模型的时候最好都是使用类对象,因为参数都被封装在里面了不要自己考虑,使用函数的话一定要把相关的参数都仔细看一下
    • 发现这个也是一个意外,之前在解决一个训练过拟合的问题,自己本想加dropout,想缓解过拟合,原代码是直接F.relu,我就直接在里面补了一层F.dropout,加了之后开始训练,回寝睡觉等着第二天起床看结果。结果发现,,一切并没有什么不同。开始以为是这次训练有问题,于是又训练了几次还是不行,折腾了很久才发现是这个问题。

4. 相同模型,相同参数,相同数据,固定随机种子,但是多次测试结果还是不一样

    • 首先 model.eval 有没有打开,这个应该是很基础的东西,大部分人应该也不会忘,不过有时候自己写部署代码的时候却还是可能会错,比如我前不久....。
    • 这个时候可以检查一下是否使用了cuda进行测试,如果是,别忘了cuda的随机数种子。这里还需要用到torch.backends.cudnn.deterministic. 将这个 flag 置为True的话,每次返回的卷积算法将是确定的,即默认算法.否则做前向传播过程中,卷积运算的底层其实是有多种运算方式的,每种卷积算法,都有其特有的一些优势,比如有的算法在卷积核大的情况下,速度很快;比如有的算法在某些情况下内存使用比较小。不限制,就可以会随机选择算法实现卷积,因而会有些许差异。
    • 或者最简单的,看看只是在CPU上测试是否也不一样,如果CPU上测试结果一样,但是GPU的结果不一样的话就是这个问题。

5.关于随机种子的固定

    • 大家为了保证可复现性一般会固定随机种子,但是里面有大坑啊!!!,当你固定随机种子之后,运行固定随机种子的代码之后所有代码的随机操作都会固定到一个的操作,所有!!!
      • 也就是你的随机裁剪,随机旋转,随机翻转,你的dropout都固定要一个具体的状态!!!
    • 这就是我今天发现的致命bug, 我之前都只想着固定网络的参数初始化,却忽略了这样,而且一直也没出什么大问题,因为常用的数据增强是随机翻转和旋转,顶多加上随机裁剪,所以说就算是固定也不会有很大问题,至多是数据增强无效(其实也还是有效的,虽然是固定随机,但是终究会和验证集有效差异,导致如果最终优化的时候,相同验证准确率在测试上可能加了增强的会有更好的效果)。
    • 这次是因为,训练一个数据集的时候过拟合实在太严重,于是尽可能加入以前没试过的数据增强手段,结果开始几个epoch有些作用,但是之后却还是一样快速的过拟合,还是没什么效果。于是想着自己写几个其他数据增强,用到了随机处理的时候才突然意识到要是我固定随机种子了,本来随机的增强是不是也会被固定,于是试验发现了这个。
    • 同样进一步分析了其他相关的随机操作,发现dropout也会被固定住
    • 更可怕的是,即使你是在随机操作之后再进行种子的固定,如果这个操作是在代码中多次进行的,你第一次是真正的随机,但是第二次的时候还是会被固定住,因为之前运行过一次固定随机种子的函数了。比如每次数据加载都会有随机增强,
    • 所以如果又想固定初始化参数,又想使用随机旋转之类的增强,就需要在每个循环的时候更换随机种子。

6. 相同的环境配置,为什么我另外一个电脑能用,这个电脑就不能用?或者之前能用,现在不能用

    • 很大的概率是你可能装了一样的包,但是包的版本不一样了,导致了整个环境其实还是发生了变化。
    • 所以大家在进行环境迁移的时候,一定要具体到对应的版本,不然出现 这种问题还真不是很好解决,几乎只能把各个版本都试一遍。
      • 比如我就试过几次的skimage的版本.....一把辛酸泪

7.一些开发的经验

    • 写在前面
      • 很多人可能会觉得自己有着丰富的工程开发经验,就可以不要担心这个问题,但是事实是当我们的目标从工程开发转到科研探索的时候,根本思维的转变会导致很多之前工程的习惯很难直接被顺利的转变到科研中辅助自己的进行科研。我就是血淋淋的例子。
      • 研一刚进来的时候,觉得自己厉害的不行,给我一个问题,我一定能给你解决掉。后来,后来的故事我在另一篇回到里面有简单提过,惨不忍睹。原因我后来分析有二。一是因为我的问题,在角色切换的时候,我切换的太过决绝,把科研和工程分得太清楚,潜意识里就把他们断开,也就不会想着迁移一些工程开发的习惯到科研上。另一个就是二者本身之间就存在差异的问题,工程开发的时候我会在开始确定好大致框架,后来基本只是在框架内部做调整。但是科研没有这个东西,我没有办法了解到全貌, 知识更新太快(也是我当初轻视了这个的难度)可能就是不断阅读,不断尝试,不断修改,经常是新建一个工程本想做a,后来发现b也不错,直接就把b也加进来了,又或者做a需要bcd三个步骤,每一个又都是一个研究点,很多方法,于是就都加进来,然后代码开始变得冗杂,主要精力被我花在思考怎么解决问题,对代码的要求就慢慢降低了。最后......
    • 尽量重用封装好函数或者数据增强的代码,不要不同函数不同的训练,或者训练测试你都整一个。不然开发到后期你会发现各种奇奇怪怪的问题。
      • 包括但不限于怎么我服务器训练测试时候效果是A本地测试的时候,就变成了B,或者多次测试准确率不一样,或者自己写部署代码的时候发现怎么都达不到训练时候测试的效果,等等
      • 因为你同一个功能和变换如果是以复制粘贴的形式放到各个代码里面。很可能你调参或者做优化的时候, 对某个代码的处理进行了修改,但是其他地方却没有更新,导致了这个问题.
      • 虽然说起来很简单,但是真的非常非常非常常见!!!
    • 关于参数管理
      • 把配置参数独立到一个单独的配置文件不要和main函数或者其他函数混合,方便修改
      • 所有需要修改的参数,甚至代码段(比如随机增强,模型结构)都要加到配置文件中。特别是一开始没有,后面你突发奇想想到了,然后在代码中加入的参数。
        • 所有的函数都提供args接口,把所有需要修改的参数都通过args实现。
        • 否则你会发现你后面想重现你之前的效果的时候,可能就完全重现不出来。然后气急败坏狂按键盘。
      • 把args记录到日志的开头
        • 很多人还有记录日志的习惯,但是多数都是记录一下loss,accuracy等。但是很少会有人把参数也一起记录了。
        • 然后就会发现,啊,当初训练的这个模型很不错,怎么就训不出来了,怎么就训不出来了....然后气急败坏狂按键盘。
        • 结果很重要,但是更重要的是结果怎么来的。
    • 关于版本管理
      • 参数管理和记录是是为了方便复现,而最好的解决办法还是版本管理。
      • 不说你增加一个功能就备份一次吧,你调出一个结果觉得很好,心里很高兴,觉得终于向发论文靠近了一步的时候你怎么也得备份一次吧。
        • 曾经有一份完美的代码呈现在我面前我却不知道备份,后来失去了才知道珍惜,如果时间能从来,我会对它说,git commit -m , git push。如果需要加上以次数的话,我希望是一万次。
      • commit一定写的详细一点,详细一点,详细一点,重要的事情说三遍
        • 我年轻的时候(大二大三的时候),写commit就是解决了xxx问题,修正了xxx bug,至多加上一个实现了怎样的效果。心想和其他开发者来说,我应该算是不错了。后来有一次有想回去重构我之前写的代码,看着我的git log,心中就在想,我当初写的是啥....我完全不知道每一个commit背后我到底做了什么修改。也不知道回退到哪一个版本可以找到我需要的代码。
        • 所以写commit的时候,要把问题是什么,为什么需要解决,怎么解决的都要说清楚。大阶段的commit的话,需要把效果和方法读记录上。
    • 解耦解耦解耦!尽量把代码解耦
      • 最开始看到深度学习代码的时候,就是一个主文件,然后就没了....那时候对深度学习一窍不通,所以写的时候也是一样模仿着写。但是后代码一朵就把握不住了。发现要增加或者修改某个功能需要修改的地方太多。而这时候你入股连参数管理和版本管理又没有做好,可能改着改着,效果没改出来,之前实现的效果也怎么都复现不出来了。而且复用性极差,每个新的工程都需要把相同功能的代码重写一遍,费时费力。
      • 后来。。你以为我就开始解耦了吗?没有,我想既然删除会有问题,那么我就加开关控制,不删除总可以了吧,结果就是代码越写越大,越写越大,几周之后重新回去看之前的代码.....emmmm,什么时候我的代码竟然也开始写的像屎一样了。
      • 所以最后开始尝试解耦代码。一路上也走了很多弯路,根据自己的需求修修改改不断纠正封装的原则,最后发现,还是pytorch本身的层次分的最为清楚。大家解耦的时候可以参见pytorch代码层次。
        • 具体实现上:方法--类 分开
        • 功能上:dataset,datalaoder, preprocessing(transform), model struture,training,test and deploy. 基本我们常用的也就是这些功能,如果自己需要定制也跟这个一样。
      • 可以参见我针对任务写的开发模板,用在其他任务上应该能很快迁移
    • 明确自己的最终目标,功能优先,性能后优化。
      • 大家会习惯把问题拆分成几个模块解决,这个是没问题的。然后问题在于有时候我们会止不住在解决某个模块的时候就想着拼命去提高它的效果,并且坚定的相信这个模块的性能更好,必然能提升我整体的性能,然后在子问题上花了很长时间。最后导致进展缓慢,可能并没有实现自己在模块上的优化目标,或者更可怕的是你实现了自己的优化目标,但是发现对自己最终要做的事情提升甚微......。
      • 这个其实是工程上一个很基本的常识,但是做科研的时候很容易会被淡化掉。为什么呢?因为科研本身就是要打破边界,在一个地方花时间去优化提升就是我们要做的事情。可是啊,要明确你最终的目标,不要把力用错了地方。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值