有时遇到有的模型训练或测试脚本执行时遇到torch.autograd.gradcheck()抛出类似如下的错误:
有时报的是Jacobian mismatch for output 0 with respect to input 0,这个出错的原因都是一个:
torch.autograd.gradcheck()要求参数计算的PyTorch Tensor数据都是torch.DoubleTensor类型的(torch.float64),也就是双精度数据,而不是默认的torch.FloatTensor类型(torch.float32),参见torch.autograd.gradcheck — PyTorch master documentation:
一般人在实现网络和写训练或测试脚本时没注意到这个问题,使用的数值类的Tensor都是没有指定类型的,默认是float32而不是float64,精度不够参与运算的话很容易造成numerical gradients和analytical gradients的误差超出设定的允许范围而抛出异常导致程序终止。
另外要注意的是,当inputs里的Tensor数据是使用torch.rand()产生的随机数时,有时执行可能能正常完成,多数时候会报错。有的人提出的解决办法就是针对单精度数据将参数eps的默认值1e-6改为更大的1e-3或者1e-4,实际上这样做并不能完全保证触发抛出异常,只是减少了出错机会而已,但是异常还是会发生的,有时还是频繁抛出异常,所以根本不算好的解决办法。
另外,我看了一下源码/usr/local/lib/python3.6/dist-packages/torch/autograd/gradcheck.py:
for j, (a, n) in enumerate(zip(analytical, numerical)):
if a.numel() != 0 or n.numel() != 0:
if not torch.allclose(a, n, rtol, atol):
return fail_test('Jacobian mismatch for output %d with respect to input %d,\n'
'numerical:%s\nanalytical:%s\n' % (i, j, n, a))
实际上是在使用torch.allclose()比较analytical gradients和numerical gradients的误差是否在由atol和rtol设置的允许范围内,torch.allclose()实现的比较a和n是否接近的公式为
|a-n| <= atol+rtol*|n|
那这么看来,似乎调大atol的参数值似乎也能避免抛出异常啊,试了一下,还是只能保证部分时候能正常执行完,有时会报Backward is not reentrant,i.e., running backward with same input and grad_output multiple times gives different values, although analytical gradient matches numerical gradient的错误:
因为gradcheck()只是检查校验方向传播时使用链式法则算出来的analytical gradients和直接使用数值微分方式算出来的numerical gradients是否误差在允许的范围内,如果只是使用人家的模型,对这个训练或测试时的梯度检查结果不在意,完全可以把调用gradcheck()的语句给注释掉,或者设置raise_exception=False即可。
如果要根本解决这个问题,传入gradcheck()的inputs里tensor数据都必须转换成double类型,另外对于func(可以是继承自torch.nn.model或者torch.autograd.Function,前者是实现了后者的)的内部,也必须是使用的DoubleTensor类型tensor参与运算,对于使用pytorch实现的代码,很好修改,有的内部实现都是使用的参入参数inputs里的tensor,自己并没有创建tensor变量之类的,那就更好办根本不用修改,如果是为了性能考虑使用C++实现的网络再python包装实现的,那就得C++代码里要修改保证数据都是double类型的。
看个pytorch官网上别报告问题的简单例子以及怎么修改:Why is nn.Linear failing gradcheck? Is there a bug in gradcheck? - PyTorch Forums
x = torch.rand(256, 2, requires_grad=True)
y = torch.randint(0, 10, (256, ), requires_grad=True)
custom_op = nn.Linear(2, 10)
res = torch.autograd.gradcheck(custom_op, (x, ))
print(res)
上面这个代码执行时会抛出Jacobian mismatch的异常,那么改成下面的样子就不会抛出异常了:
x = torch.rand(256, 2, requires_grad=True).double()
y = torch.randint(0, 10, (256, ), requires_grad=True)
custom_op = nn.Linear(2, 10).double()
res = torch.autograd.gradcheck(custom_op, (x, ))
print(res)
自己弄着玩弄个完全没干啥的空壳func,这个只是演示gradcheck()的func大致该是什么样的:
class MF(Function):
@staticmethod
def forward(ctx, input):
return input
@staticmethod
def backward(ctx, grad):
return grad
mfa = MF.apply
x = dcn_v2_conv (input, offset, mask, weight, bias,
stride, padding, dilation, deformable_groups)
print('check_gradient_dconv: ',
gradcheck(mfa, (x.double(),), eps=1e-3, atol=1e-4, rtol=1e-2))
其实gradcheck()一般是用来校验模型网络的某层或者整个网络的反向传播链式梯度计算是否有误,校验的标准就是对照正常的数值微分计算出来的对应梯度值,既然是这样,一般传入gradcheck()的func就是model或者model里的某个layer就行了。
最后,再次说说什么是analytical gradients和numerical gradients。大学时学高数和数值分析等科目时我们都学了导数是什么东西、求导是怎么弄出来的,以及怎么使用小微量值求近似导数,正常的近似计算办法就是数值梯度法,这种办法计算得到的是numerical gradients,原理就是:
当delta->0时, df(x)/dx = lim( f(x + delta) - f(x) )/delta
数值分析中一般稍加改动,使用好看一点的中心差值公式(centered difference formula):
delta是个很小的数值,df(x)/dx = lim( f(x + delta) - f(x - delta))/(2*delta)
这里的delta其实就是gradcheck()中的eps参数,eps默认值是1e-06,所以gradcheck()里在使用数值微分方式计算x tensor的第i个分量对应的梯度值时应该是这样算的:
gradient[i] = (f(x + eps) - f( x -eps))/(2*eps)
从上面公式看出,eps的值设置会对致梯度值影响很大。按理说,计算数值梯度时,eps 越小,求得的值应该更接近于真实的梯度,可是实际中可能当eps值设置为 1e-3 时,func可以通过梯度检查,但 eps调小为1e-4 时,func反而都无法通过了!原因就是input tensor的默认类型为torch.float32, 在 eps 过小的情况下,与真实精度间产生了较大的精度误差(因为计算数值梯度时eps 作为被除数嘛)。可以以PyTorch内置的激活函数Sigmoid为例实验一下:
input = torch.randn(4, requires_grad=True)
torch.autograd.gradcheck(torch.sigmoid, (input,), eps=1e-3) # succeeded
torch.autograd.gradcheck(torch.sigmoid, (input,), eps=1e-4) # failed
将input的类型指定为torch.float64后,即使调小eps到1e-06都能通过校验成功:
input = torch.randn(4, requires_grad=True, dtype=torch.float64)
torch.autograd.gradcheck(torch.sigmoid, (input,), eps=1e-4) # succeeded
torch.autograd.gradcheck(torch.sigmoid, (input,), eps=1e-6) # succeeded
对于analytical gradients,其实就是反向传播(BP)时根据求导的链式法则进行从网络尾端向前端逐层反向计算各参数的梯度得到的,所谓求导的链式法则,举个简单例子:
df(g(z(x)))/dx = (df/dg)*(dg/dz)*(dz/dx)
链式法则求导非常好理解,网上也有很多解释就不多说了。