原因与问题
上周科研时,需求核算一个CNN参数的Hessian矩阵,即二阶导数。核算Hessian矩阵的完成思路想起来很简单:
- 关于模型参数,求两次导。第一次求导时运用loss关于参数求导
- 运用核算出来的梯度关于参数再次求导,就能够求出Hessian矩阵/二阶导数。
也便是只需求调用两次autogard.gard()就能够完成核算。此外,pytorch也供给了直接能够用于核算Hessian矩阵的api:autograd.functional.hessian()。但在调pytorch的api去完成的时分,却发现了如下问题:
pytorch的autogard.gard只能求标量关于向量的导数
假设咱们的模型参数有N维,则loss函数关于参数的一阶导数为:
而二阶导数为:
\frac{\partial^2 l o s s}{\partial w_1^2} & \frac{\partial^2 \text { loss }}{\partial w_1 \partial w_2} & \ldots & \frac{\partial^2 \text { loss }}{\partial w_1 \partial w_n} \\
\frac{\partial^2 l o s s}{\partial w_2 \partial w_1} & \frac{\partial^2 l o s s}{\partial w_2^2} & \ldots & \frac{\partial^2 \text { loss }}{\partial w_2 \partial w_n} \\
\vdots & \vdots & \vdots & \vdots \\
\frac{\partial^2 l o s s}{\partial w_n \partial w_1} & \frac{\partial^2 \operatorname{loss}}{\partial w_n \partial w_2} & \ldots & \frac{\partial^2 \operatorname{loss}}{\partial w_n^2}
\end{array}\right]
通过autogard.gard()能够求出loss的一阶导数,可是二阶导数的核算需求关于一阶导数求导,autogard.gard()无法直接关于向量求导。更确切的说,torch.autograd.grad()函数常用的输入参数有以下三个:
- outputs(Tensor) – 一个可微函数的输出
- inputs(Tensor) – 需求被求导的参数
- grad_outputs(Tensor) – 通常是size与outputs相同的tensor
当output为一个长度大于一的向量时,就需求输入一个grad_outputs向量,其size与outputs相同。该参数相当于和向量做一个点乘,换句话说其将outputs向量转变为一个加权和的方式,从而将向量转化为标量,再进行求导。
因此,假如调用这个函数对梯度进行求导,最终得到一个长度为N的向量,而不会得到NxN大小的矩阵,该向量中每一个元素代表Hessian矩阵的某一行的和。即如下所示:
torch.autograd.functional.hessian无法主动求参数的Hession矩阵
pytorch中供给了一个专门用于核算Hessian矩阵的API:torch.autograd.functional.hessian(),可是在我尝试运用这个API去核算参数的Hession矩阵时却发现了一个问题:这个API无法主动关于参数进行求导。
torch.autograd.functional.hessian的主要参数如下所示:
- func(function) – 一个可微函数,其输入一个tensor,输出一个标量
- inputs(tuple of Tensors or Tensor) – 一个tensor或许tensor的tuple,其作为func的输入.
这儿能够与gard()的参数进行比较,gard函数输入的是一个标量和一个tensor,其会基于标量的核算进程构建出核算图,核算出与输入的tensor相关的导数,也便是说,这个api是基于核算结果核算某一个参数的导数。可是torch.autograd.functional.hessian其输入的是一个函数和一个函数的输入。然后他会核算出这个函数关于这个输入的Hession矩阵。假设咱们的func代表某一个模型,那么Hessian函数核算的则是这个模型输入针关于输入数据的Hession矩阵,而不是关于模型参数的Hessian矩阵。
现有的几种核算Hessian矩阵的完成办法
运用autogard.gard关于一阶梯度循环核算
以下方代码为例:
#定义函数
x = torch.tensor([0., 0, 0], requires_grad=True)
b = torch.tensor([1., 3, 5])
A = torch.tensor([[-5, -3, -0.5], [-3, -2, 0], [-0.5, 0, -0.5]])
y = b@x + 0.5*x@A@x
#核算一阶导数,因为咱们需求继续核算二阶导数,所以创建并保存核算图
grad = torch.autograd.grad(y, x, retain_graph=True, create_graph=True)
#定义Print数组,为输出和进一步利用Hessian矩阵作准备
Print = torch.tensor([])
for anygrad in grad[0]: #torch.autograd.grad回来的是元组
Print = torch.cat((Print, torch.autograd.grad(anygrad, x, retain_graph=True)[0]))
print(Print.view(x.size()[0], -1))
实验方案十分简单,如上所示,假设要关于一个线性模型的参数求二阶导数,首先调用gard求出y关于x的一阶导数,得到一个tensor,然后遍历tensor中的每一个元素,核算该标量关于参数x的导数,并存储结果。就能够得到一个NxN的矩阵,该矩阵即为Hessian矩阵。
调用hessian函数并对model进行封装
如下代码所示:
import torch
import numpy as np
from torch.nn import Module
import torch.nn.functional as F
class Net(Module):
def __init__(self, h, w):
super(Net, self).__init__()
self.c1 = torch.nn.Conv2d(1, 32, 3, 1, 1)
self.f2 = torch.nn.Linear(32 * h * w, 5)
def forward(self, x):
x = self.c1(x)
x = x.view(x.size(0), -1)
x = self.f2(x)
return x
def forward_loss(a, b, c, d):
p = [a.view(32, 1, 3, 3), b, c.view(5, 32 * 12 * 12), d]
x = torch.randn(size=[8, 1, 12, 12], dtype=torch.float32)
y = torch.randint(0, 5, [8])
x = F.conv2d(x, p[0], p[1], 1, 1)
x = x.view(x.size(0), -1)
x = F.linear(x, p[2], p[3])
loss = F.cross_entropy(x, y)
return loss
if __name__ == '__main__':
net = Net(12, 12)
h = torch.autograd.functional.hessian(forward_loss, tuple([_.view(-1) for _ in net.parameters()]))
与调用gard函数时不同,因为hessian函数的输入有必要是一个可调用函数,而且函数的输入与被求导的量一致。因为咱们核算的通常是loss关于参数的导数∂loss∂w\frac{\partial \operatorname{loss}}{\partial w},而不是输出值对参数的导数∂y∂w\frac{\partial \operatorname{y}}{\partial w}因此,在运用此函数核算Hessian矩阵的时分,有必要要写好一个前向传达核算loss的函数,而且该函数的输入为模型的参数。
此外,loss核算函数在调用时,其输入的参数需求reshape成一维向量并包装成tuple,然后在函数内部再reshape为数组。
参考文献
- www.cnblogs.com/chester-cs/…
- stackoverflow.com/questions/6…