分享一下我最近所经历的踩坑经历吧
这几天在实现一个语义分割的loss
该loss考虑了边缘,结果一致性等因素 如图
由于公式较为复杂, 决定用风格和numpy相似的pytorch来实现
再由于torch是动态图 而且 Python的for循环很慢, 所以打算全用Tensor操作。
想着应该和numpy差不多 难度中等 加上熟悉API的时间 一天足够了
开工前 准备了一组(image, ground truth, 及假装是分割网络结果的概率矩阵prob) 作为测试用例, 然后 正式开工!
写着写着 发现处理后的图片根本不对,可视化一下.发现 ground truth转化成`FloatTensor`后 就成了这个鬼样子
查明后 原因是: 对`torch.FloatTensor`传一个bool的numpy array , 即`torch.FloatTensor(Bool_Ndarray)` 就会成为上面这样的乱码
得先转化为float32 即 `torch.FloatTensor(np.float32(labels))`
继续写 咦 torch不支持[::-1]和flip?那自己写一个flip
又继续写, 全Tensor操作, 遇到复杂公式, 就意味着超多维度的select, index, 纬度变换,纬度匹配 ,若出了bug 分析起来特别麻烦
比如:
otherSideEdgeLossMap = -th.log(((probnb*gtind).sum(-3)*gtdf).sum(-3)/gtdf.sum(-3))
这么黑灯瞎火地写下去,调试又复杂又慢。
工欲善其事, 必先利其器
于是 我便先去改之前只支持numpy可视化的工具代码 yllab ,增加其对torch的支持。
配合`Ipython@Spyder` 调试效率提高不少 比`print xx.shape,xx.mean()` 不知高到哪里去了
最后, loss总算写出来了, 而且可视化出来的 loss map 符合预想效果,还很好看!
最后一到工序 将概率矩阵prob变成Variable 测试一下反向传播,我天真得以为 工作马上就要被完成了
改成Variable(prob)后, loss.backward()一下, 啥?Error?Variable 竟然不能和 Tensor 运算 ! 不用记录grad的Variable和 Tensor 有啥区别? 无语, 那全改为Variable 吧
嚓, Tensor和Variable部分api竟然不一样 比如(`.type`). 行, 为了兼容性 函数都加上判断是否为Variable, 并转化为Variable.data
继续吐槽一下 torch的类型系统 `Tensor/Variable * 8个数据类型` * `cuda/cpu` 共 `2*8*2=32` 类啊!! 而且 8个数据类型不支持类型自动转换,所以 这32个类型的数据 都是两两互斥
不同类型间 操作前都得转化, 可怕的是转换操作还有许多像上文提到的那种坑!
改好代码, 反向传播通了 赶忙可视化一下 prob.grad
毛线!! 全是白的 分析一下 grad 中 99.97%的是nan, 人家 loss 都好人一个 你梯度怎么就成了nan! 数学上不成立啊
遂开始了漫长的DEBUG之路, 终于 在不断地拆开loss
分别 Backpropagation 后 将凶手精准定位了导致nan的loss
进一步分析 果然是pyTroch的BUG 整理好BUG后 就提交到了pytorch 的 GitHub上了
x.grad should be 0 but get NaN after x/0 · Issue #…
BUG如下
Reproduction BUG code
x = Variable(torch.FloatTensor([1.,1]), requires_grad=True)
div = Variable(torch.FloatTensor([0.,1]))
y = x/div # => y is [inf, 1]
mask = (div!=0) # => mask is [0, 1]
loss = y[mask]
loss.backward()
print(x.grad) # grad is [nan, 1], but expected [0, 1]
由于被`mask`阻挡, `x[0]`根本就没在计算图中 所以`x[0]`梯度应该为0 却返回了`nan`
我还给出了BUG的解决方案:
Your code should't generat any inf in forward, which often produce by torch.log(0) and x/[0, ]
That means 0 should be filtered before do torch.log(x) and x/div
为避免这个BUG 代码变得更复杂了
variables
└─ /: 4
├── gtind: torch.Size([1, 2, 300, 400]) torch.cuda.FloatTensor
├── edge: torch.Size([1, 300, 400]) torch.cuda.ByteTensor
├── probnb: torch.Size([1, 8, 2, 300, 400]) torch.cuda.FloatTensor
└── gtdf: torch.Size([1, 8, 300, 400]) torch.cuda.FloatTensor
th = torch
tots = lambda x:x.data
code(before)
otherSideEdgeLossMap = -th.log(((probnb*gtind).sum(-3)*gtdf).sum(-3)/gtdf.sum(-3))
otherSideEdgeLossMap[~tots(edge)] = 0
code(after)
numerator = ((probnb*gtind).sum(-3)*gtdf).sum(-3)
numerator[tots(edge)] /= gtdf.sum(-3)[tots(edge)]
numerator[tots(edge)] = -th.log(numerator[tots(edge)])
otherSideEdgeLossMap = (numerator)
otherSideEdgeLossMap[~tots(edge)] = 0
最后 把代码适配成多batch版本加上分割网络 顺利跑通了 。 回想着一路下来 还好用的是动态图的pyTorch, 调试灵活 可视化方便 若是静态图 恐怕会调试得吐血,曾经就为了提取一个mxnet的featrue 麻烦得要死。
不过 换成静态图的话 可能就不会顾着效率,用那么多矩阵操作了,直接for循环定义网络结构 更简单直接 。
写于 2017.12
torch version : 0.3