翻译自https://learnopencv.com/cnn-receptive-field-computation-using-backprop/
Introduction
在上一篇文章中,我们学习了如何对任意大小的图像进行分类,并可视化网络的响应图。
图1:“camel”类激活的边界框。该图来自我们之前的文章,标题是完全卷积图像分类。
在图1中,请注意骆驼的头部几乎没有高亮显示,而响应图包含大量的沙子纹理。边界框也明显关闭。
有点不对劲。
我们使用的ResNet18网络非常精确,事实上它能正确地对图像进行分类。看着这个边界框,你可能会觉得我们很幸运,分类是正确的,即使网络没有从图像中选择最好的信息。
但真的是这样吗?简而言之,答案是否定的。在上一篇文章中,我们使用了一种快速而不优雅的方法来找到感兴趣的领域。
在这篇文章中,我们将用正确的方法来理解神经网络中感受野的概念。
Neural Network Receptive Field
回想一下在上一篇文章中我们是如何找到骆驼周围感兴趣的区域和边界框的。我们对预测类的响应图进行了上采样,以匹配原始图像。
这种方法提供了某种观察的方法,但绝对不是正确的做法。
为了理解如何用正确的方法去做,我们需要理解一个叫做感受野的概念。对于网络内特征图中的一个像素,感受野代表先前特征图中影响其值的所有像素。
感受野是理解网络“看到”并且分析以什么标准来预测为“骆驼”类的合适工具,而我们在前一篇文章中看到的缩放响应图只是它的粗略近似值。
图2 逐层应用两个3×3卷积过后的感受野
我们来举个简单的例子。
图2展示了输入图像,然后是两层卷积神经网络(CNN)的输出。让我们调用第一层的输出FEATURE_MAP_1
,以及第二层的输出FEATURE_MAP_2
。
假设第1层和第2层的卷积核大小为3,那么,某一层之后的特征映射会受到第3层的影响×3个区域(即9个值)。那么,特征图经过一个特定层之后会受到前一个特征图中一个3×3区域(即9个values)的影响
接下来我们要找到特征图2的深蓝色像素的感受野。
此像素的值受以蓝色标记的FEATURE_MAP_1
中9个相应值的影响。反过来,这9个值受来自输入图像的相应像素的影响。
换句话说,FEATURE_MAP_2
中的像素受输入图像中5×5的patch影响(浅蓝色标记)。这些是深蓝色的像素点可以在输入图像上“看到”的像素。
注意,在输入图像中,像素有不同的蓝色阴影。这些阴影表示相应像素参与影响感兴趣的深蓝色像素的卷积的时间。外部像素只在计算中使用了一次。中心像素参与了每一次卷积,我们对其中的9个像素进行了特征映射计算。
这个简单的例子给了我们一个如何计算更复杂网络的感受野的想法。通过这种做法,我们可以了解输入图像的哪些像素可能会影响网络的结果!这样我们就可以对网络的输出结果有更深入的了解。
了解感受野的大小对于神经网络的调试非常有用,因为它可以让你深入了解网络是如何做出决定的
What does the Receptive Field Size Depend on?
输出像素的感受野大小通常相当大,通常有数百像素宽。此值取决于网络的深度、网络中卷积的大小、步长以及卷积滤波器中使用的填充。网络越深,每个像素在输入图像上“看到”的上下文就越多。
重要的是,感受野不受输入图像大小的影响。尽管完全卷积网络可以接受和处理任何大小的图像,但它们的感受野保持不变,因为深度保持不变。有时这意味着如果输入图像中的对象太大,网络的性能可能会很差——它们只是看不到足够的上下文来做出决定!
感受野大小同样也不受网络中权重的影响。事实上,对于同一体系结构的受过训练和未受过训练的网络来说,情况是一样的。
我们将用这点来计算感受野的大小。
Receptive Field Computation for Max Activated Pixel
让我们来讨论如何可视化一个像素的感受野。
主要有两种方式
- 对此像素运行反向传播。
- 解析计算感受野大小。
在本文中,我们将讨论第一种方法,并将在以后的文章中讨论后者。
再次输入图片并推理(infer)网络,得到最终的激活图(activation map)(我们在这里称之为“得分图(score map)”)。
# Load modified resnet18 model with pretrained ImageNet weights
model = FullyConvolutionalResnet18(pretrained=True).eval()
image = cv2.imread('camel.jpg')
# Perform the inference.
# Instead of a 1x1000 vector, we will get a
# 1x1000xnxm output ( i.e. a probability map
# of size n x m for each 1000 class,
# where n and m depend on the size of the image.)
preds = model(image)
preds = torch.softmax(preds, dim=1)
# Find the class with the maximum score in the n x m output map
pred, class_idx = torch.max(preds, dim=1)
row_max, row_idx = torch.max(pred, dim=1)
col_max, col_idx = torch.max(row_max, dim=1)
predicted_class = class_idx[0, row_idx[0, col_idx], col_idx]
我们的分数图有一个通道,因为我们已经从1000个初始通道中只提取了对应于所预测的类的通道。它有3行8列。
现在,让我们首先在网络输出中找到“camel”类值最高的像素。这是被激活最多的像素——让我们看看,它能“看到”图像的哪些部分。
scoremap_max_row_values, max_row_id = torch.max(scoremap, dim=1)
_, max_col_id = torch.max(scoremap_max_row_values, dim=1)
max_row_id = max_row_id[0, max_col_id]
print('Coords of the max activation:', max_row_id.item(), max_col_id.item())
在我们的图像中,激活度最高的像素位于第1行和第6列。
Use Backprop to Compute the Receptive Field 使用反传算法计算感受野
为了利用反向传播来计算感受野的大小,我们将利用网络的权值与计算感受野无关这个事实。
让我们看一下步骤
1. Load Model 读取模型
首先我们加载模型并进入训练模式。确保我们能够传递梯度。
# Initialize the model
model = FullyConvolutionalResnet18()
# model should be in the train mode to be able to pass the gradient
model = model.train()
2. Set Layer Parameters 设置层参数
如前所述,感受野不依赖于weights和biases。我们将利用这点来计算感受野。
卷积层有两个参数-权重和偏差。我们将每层的weight改为0.05,bias改为0。
BatchNorm层有四个参数:weight、bias、running_mean和running_var。我们将weight设置为0.05,bias设置为0,running_mean设置为0,running_var设置为1。
代码如下
for module in model.modules():
# skip errors on container modules, like nn.Sequential
try:
# Make all convolution weights equal.
# Set all biases to zero.
nn.init.constant_(module.weight, 0.05)
nn.init.zeros_(module.bias)
# Set BatchNorm means to zeros,
# variances - to 1.
nn.init.zeros_(module.running_mean)
nn.init.ones_(module.running_var)
except:
pass
3. Freeze BatchNorm Layers 冻结BN层
在BatchNorm层中,这四个参数中有两个是可学习的(权重和偏差),另外两个是在向前传递期间计算的统计信息。因此,它们改变了输入张量的值,但不会随着反向传播而更新。因此,尽管我们已经在上面的代码中初始化了这些参数,但它们将在前传期间更新,这将对我们想要的可视化产生不利影响。
所以,我们应该把它们切换到eval模式。这样,参数将不会在向前传递期间更新。
# Freeze the BatchNorm stats.
if isinstance(module, torch.nn.modules.BatchNorm2d):
module.eval()
4. Input a white image 输入一张白图像
我们希望创建一种情况,在这种情况下,模型输出处的梯度仅取决于像素的位置。所以,我们把一个白图像传给网络。
input = torch.ones_like(image, requires_grad=True)
out = model(input)
重要的是,我们想传播梯度信息回到图像上,看看哪些像素影响了最终的结果。因此,与普通的训练不同,我们将图像标记为PyTorch Autograd的可微性,设置requires_grad
为True。这样不仅可以计算网络权重的梯度,还可以计算图像本身的梯度。
5. Tweak output gradients and backpropagate调整输出梯度和反向传播
接下来我们将调整输出梯度,它将通过网络反向传播。我们只想计算最活跃像素的感受野,所以我们将相应的梯度值设置为1,其他的设置为0。
当我们把这个梯度反向传播到输入层时,感受野就会亮起来,其他的都会变暗。
现在让我们通过我们的合成网络来前传推理出这个合成图像。
# Set the gradient to 0.
# Only set the pixel of interest to 1.
grad = torch.zeros_like(out, requires_grad=True)
grad[0, 0, max_row_id, max_col_id] = 1
# Run the backprop.
out.backward(gradient=grad)
# Retrieve the gradient of the input image.
gradient_of_input = input.grad[0, 0].data.numpy()
# Normalize the gradient.
gradient_of_input = gradient_of_input / np.amax(gradient_of_input)
6. Visualize Results 可视化结果
最后一步是简单地归一化输入层上反向传播的梯度。归一化只需要减去最小值,然后除以最大值,这样归一化图像的值就会在0到1之间。
该归一化图像用作掩膜mask,并与原始图像相乘。
def normalize(activations):
# transform activations so that all the values be in range [0, 1]
activations = activations - np.min(activations[:])
activations = activations / np.max(activations[:])
return activations
def visualize_activations(image, activations):
activations = normalize(activations)
# replicate the activations to go from 1 channel to 3
# as we have colorful input image
# we could use cvtColor with GRAY2BGR flag here, but it is not
# safe - our values are floats, but cvtColor expects 8-bit or
# 16-bit inttegers
activations = np.stack([activations, activations, activations], axis=2)
masked_image = (image * activations).astype(np.uint8)
return masked image
receptive_field_mask = visualize_activations(image, gradient_of_input)
cv2.imshow("receiptive_field_max_activation", receptive_field_mask)
cv2.waitKey(0)
最活跃像素的感受野在骆驼头周围
感受野清楚地显示,网络的确最关注骆驼的头!这是个好消息——这意味着网络比我们以前看到的要聪明。
感受野的网络结构
What do we see the grids?
这个感受野的一个特殊细节是它的网格结构。ResNet的第一层的体系结构解释了这种结构。第一个block是对输入数据进行7×7卷积运算,然后快速对其进行下采样以减少计算量。这意味着,我们只观察到一次高质量的图像,然后再观察很多次被逐步下采样后的图像。就感受野而言,这意味着只参与第一次卷积,然后被下采样操作切断的区域只以一种微妙的方式影响结果——因此在这里被表示为暗网格线。
Receptive Field for the Net Prediction
让我们进一步分析“骆驼”类的整个网络特征图,而不是最活跃的像素。事实上,我们可以像对单个像素一样反向传播它——我们只需要把整个张量放到输出梯度上。通过这种方式,我们将了解输入图像中的哪些像素导致了camel类的整个最终得分图。
out = model(input)
grad = torch.zeros_like(out, requires_grad=True)
grad[0, predicted_class] = scoremap
out.backward(gradient=grad)
gradient_of_input = input.grad[0, 0].data.numpy()
gradient_of_input = gradient_of_input / np.amax(gradient_of_input)
结果图像显示输入图像的哪些区域影响网络的预测:
def find_rect(activations):
# Dilate and erode the activations to remove grid-like artifacts
kernel = np.ones((5, 5), np.uint8)
activations = cv2.dilate(activations, kernel=kernel)
activations = cv2.erode(activations, kernel=kernel)
# Binarize the activations
_, activations = cv2.threshold(activations, 0.25, 1, type=cv2.THRESH_BINARY)
activations = activations.astype(np.uint8).copy()
# Find the countour of the binary blob
contours, _ = cv2.findContours(activations, mode=cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_SIMPLE)
# Find bounding box around the object.
rect = cv2.boundingRect(contours[0])
return Rect(rect[0], rect[1], rect[0] + rect[2], rect[1] + rect[3])
rect = find_rect(gradient_of_input)
receptive_field_mask = visualize_activations(image, gradient_of_input)
cv2.rectangle(receptive_field_mask, (rect.x1, rect.y1), (rect.x2, rect.y2), color=(0, 0, 255), thickness=2)
cv2.imshow("receiptive_field", receptive_field_mask)
cv2.waitKey(0)
camel的感受野的边界框
请注意,在这个feature map的某个地方有非零值并不意味着网络预测该位置的camel类-其他类的激活可能要高得多。
让我们把这张图片和我们之前用来粗略近似感受野的缩放分数图进行比较
骆驼感受野的边界框:激活放大的前一个版本
现在我们看到了好消息。首先,与骆驼相比,这里的边界框更紧,因此我们的网络实际上比我们之前想象的更适合作为一个对象检测器。其次,影响正确预测的区域在感受野可视化中似乎比在近似分数图中更相关。