针对Caffe卷积滤波器中的输入查找梯度

37
我需要在卷积神经网络(CNN)中针对单个卷积过滤器找到相对于输入层的梯度,作为可视化滤波器的一种方式。
假设在Caffe的Python界面中有一个经过训练的网络,例如此示例,那么我该如何找到与输入层数据相关的卷积滤波器的梯度?

编辑:

根据cesans的答案,我添加了下面的代码。我的输入层维度为[8, 8, 7, 96]。我的第一个卷积层conv1有11个大小为1x5的滤波器,结果维度为[8, 11, 7, 92]

net = solver.net
diffs = net.backward(diffs=['data', 'conv1'])
print diffs.keys() # >> ['conv1', 'data']
print diffs['data'].shape # >> (8, 8, 7, 96)
print diffs['conv1'].shape # >> (8, 11, 7, 92)

如您所见,从net.backward()返回的数组的维度与我在Caffe中的层的维度相等。经过一些测试,我发现这个输出是损失相对于data层和conv1层的梯度。
然而,我的问题是如何找到单个卷积滤波器相对于输入层数据的梯度,这是另一回事。我该如何实现?
2个回答

29
Caffe的神经网络处理两种数字“流”。第一种是数据“流”,即图像和标签通过网络传输。当这些输入通过网络时,它们被转换成高级表示,并最终转换为类概率向量(在分类任务中)。第二个“流”保存不同层的参数,卷积的权重、偏差等。这些数字/权重在网络训练阶段中进行更改和学习。
尽管这两个“流”的作用完全不同,但Caffe仍然使用相同的数据结构“blob”来存储和管理它们。但是,对于每个层,有两个不同的blob向量,分别针对每个流。下面是一个希望能够澄清问题的示例:
import caffe
solver = caffe.SGDSolver( PATH_TO_SOLVER_PROTOTXT )
net = solver.net

如果您现在看一下

net.blobs

您会看到一个字典,其中存储了网络中每个层的“caffe blob”对象。每个blob都有储存数据和梯度的空间。

net.blobs['data'].data.shape    # >> (32, 3, 224, 224)
net.blobs['data'].diff.shape    # >> (32, 3, 224, 224)

对于卷积层:

net.blobs['conv1/7x7_s2'].data.shape    # >> (32, 64, 112, 112)
net.blobs['conv1/7x7_s2'].diff.shape    # >> (32, 64, 112, 112)

net.blobs保存了第一个数据流,其形状与输入图像相匹配,直到得到分类概率向量。

另一方面,您可以看到net的另一个成员。

net.layers

这是一个存储不同层参数的Caffe向量。
看第一层('data'层):
len(net.layers[0].blobs)    # >> 0

对于输入层,没有需要存储的参数。
另一方面,第一个卷积层需要存储以下参数

len(net.layers[1].blobs)    # >> 2

网络存储一个用于过滤权重的blob,另一个用于常量偏置。它们在这里。

net.layers[1].blobs[0].data.shape  # >> (64, 3, 7, 7)
net.layers[1].blobs[1].data.shape  # >> (64,)

正如您所看到的,这一层对3通道输入图像进行7x7卷积,并具有64个这样的滤波器。

那么,如何得到梯度?好吧,正如您注意到的那样。

diffs = net.backward(diffs=['data','conv1/7x7_s2'])

返回数据流的梯度。我们可以通过验证此操作。

np.all( diffs['data'] == net.blobs['data'].diff )  # >> True
np.all( diffs['conv1/7x7_s2'] == net.blobs['conv1/7x7_s2'].diff )  # >> True

(简要概述) 您想获取参数的梯度,这些梯度存储在net.layers和参数中:

net.layers[1].blobs[0].diff.shape # >> (64, 3, 7, 7)
net.layers[1].blobs[1].diff.shape # >> (64,)

为了帮助你将层的名称与它们在 net.layers 向量中的索引相互映射,你可以使用 net._layer_names


关于使用梯度可视化滤波器响应的更新:
梯度通常是针对一个标量函数定义的。损失是一个标量,因此您可以根据标量损失来计算像素/滤波器权重的梯度。这个梯度是每个像素/滤波器权重的单个数字。
如果您想获取导致特定内部隐藏节点最大激活的输入,则需要一个“辅助”网络,其损失恰好是要可视化的特定隐藏节点的激活度的度量。一旦您拥有了这个辅助网络,您可以从任意输入开始,并基于辅助损失到输入层的梯度更改该输入:


update = prev_in + lr * net.blobs['data'].diff

1
感谢您详细的回答,让我更深入地了解了blob的工作原理。然而,您是否仍然会考虑损失方面的梯度?滤波器可视化是通过优化输入以最大化滤波器激活来完成的。为此,我需要获取相对于输入的梯度,而不是相对于损失的梯度。基于这一点,似乎您的回答并没有真正反映出问题,但与cesans给出的答案类似。 - pir
数据流的梯度本质上是神经元中所有导致它的权重的累积梯度吗? - Alex
是的,我明白每个dE/dW都是关于权重的偏导数。但是net.backward(diffs=['data','conv1/7x7_s2'])返回的是该层中所有神经元的值,而不是卷积核中所有权重的值,我觉得有点困惑。 - Alex
是的,因为对于“损失”,i返回1,即dE / dE。 - Alex
1
没问题,我会在收到所有细节后把它们整合起来。 - Alex
显示剩余7条评论

10

当你运行backward()过程时,可以得到任何层的梯度。只需在调用函数时指定层列表即可。要显示关于数据层的梯度:

net.forward()
diffs = net.backward(diffs=['data', 'conv1'])`
data_point = 16
plt.imshow(diffs['data'][data_point].squeeze())

在某些情况下,您可能希望强制所有层执行反向传播,请查看模型的force_backward参数。

https://github.com/BVLC/caffe/blob/master/src/caffe/proto/caffe.proto


太好了,谢谢!我该如何获取与 SGD 使用的完全相同的梯度,以调整例如 conv1 中滤波器的参数?diffs = net.backward(diffs=['loss', 'conv1']) 是否能够准确地给我这个结果,或者 caffe 在进行误差表面下降之前对梯度进行某种操作? - pir
1
权重更新的计算取决于求解器。对于SGD,如果动量不为零,则在此时它包括先前的更新以及学习率和权重衰减。然而,根据这里的信息:http://caffe.berkeleyvision.org/tutorial/solver.html(更新参数)以及这里的代码:https://github.com/BVLC/caffe/blob/master/src/caffe/solver.cpp 我猜测存储在“diff”中的值是最终的权重更新。 - cesans
我目前正在使用solver.step(1)循环来训练我的CNN。我希望在每次迭代期间找到梯度,并且我猜想我可以简单地将diffs = net.backward(diffs=['loss', 'conv1'])添加到该循环中,因为求解器步自动进行一次前馈传递。您认为它可能会干扰培训吗? - pir
运行 net.backward() 只是计算梯度并储存它们,但不会更新参数,所以这不应该成为问题。尽管你会进行两次反向传播。 - cesans
据我所知,从Python中无法实现这一点。但是,您可以使用backward()计算的梯度编写自己的求解器来更新参数。如果您这样做,请检查diffs是否包括权重衰减、动量等。 - cesans
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接