赞
踩
low level feature指的是角点,边缘信息,是浅层卷积核学习的,即一开始的卷积核学习的。high level feature指的是纹理等抽象信息,是深层卷积核学习的,即越后面的卷积核学习的越抽象。
CNN具有比较强的可解释性,为什么呢?有个很大的原因就在于它的中间特征矩阵是可以可视化的,像Attention的中间特征矩阵就不好可视化。
下面要讲的获得CNN中间特征图的方法大致分为两种,一种是在model中直接定义一个list用于保存中间特征矩阵,然后返回,简称为“重写model”方法;一种是使用pytorch自定义的注册hook进行输出中间特征图,简称为“注册hook”方法。前者适用于网络较为复杂,如采用多层递归等等,但是需要重写forward函数,用于保存你需要的特征矩阵;后者简洁,且不需要重写模型中的forward函数。
重写model方法中,即使用一个list变量存储你需要特征矩阵并在forward函数中,最终return你的特征矩阵。
import torch.nn as nn import torch class AlexNet(nn.Module): def __init__(self, num_classes=1000, init_weights=False): super(AlexNet, self).__init__() self.features = nn.Sequential( nn.Conv2d(3, 48, kernel_size=11, stride=4, padding=2), # input[3, 224, 224] output[48, 55, 55] nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), # output[48, 27, 27] nn.Conv2d(48, 128, kernel_size=5, padding=2), # output[128, 27, 27] nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), # output[128, 13, 13] nn.Conv2d(128, 192, kernel_size=3, padding=1), # output[192, 13, 13] nn.ReLU(inplace=True), nn.Conv2d(192, 192, kernel_size=3, padding=1), # output[192, 13, 13] nn.ReLU(inplace=True), nn.Conv2d(192, 128, kernel_size=3, padding=1), # output[128, 13, 13] nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), # output[128, 6, 6] ) self.classifier = nn.Sequential( nn.Dropout(p=0.5), nn.Linear(128 * 6 * 6, 2048), nn.ReLU(inplace=True), nn.Dropout(p=0.5), nn.Linear(2048, 2048), nn.ReLU(inplace=True), nn.Linear(2048, num_classes), ) if init_weights: self._initialize_weights() def forward(self, x): outputs = [] for name, module in self.features.named_children(): x = module(x) if name in ["0", "3", "6"]: outputs.append(x) return outputs def _initialize_weights(self): for m in self.modules(): if isinstance(m, nn.Conv2d): nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu') if m.bias is not None: nn.init.constant_(m.bias, 0) elif isinstance(m, nn.Linear): nn.init.normal_(m.weight, 0, 0.01) nn.init.constant_(m.bias, 0)
在PyTorch中,可以使用hook函数来获取神经网络中某一层的输出(也就是所谓的“特征图”或“feature map”),以便进行后续的分析或处理。下面是一个示例代码,展示了如何使用hook函数获取某一层的输出:
import torch import torch.nn as nn class MyModel(nn.Module): def __init__(self): super(MyModel, self).__init__() self.conv1 = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, stride=1, padding=1) self.conv2 = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=3, stride=1, padding=1) self.conv3 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, stride=1, padding=1) self.relu = nn.ReLU(inplace=True) def forward(self, x): x = self.conv1(x) x = self.relu(x) x = self.conv2(x) x = self.relu(x) x = self.conv3(x) x = self.relu(x) return x # 定义一个hook函数,用于获取某一层的输出 def get_feature_map(module, input, output): print('Feature map size:', output.size()) # 创建一个模型实例,并注册hook函数 model = MyModel() hook_handle = model.conv2.register_forward_hook(get_feature_map) # 创建一个输入张量,并通过模型进行前向传播 x = torch.randn((1, 3, 32, 32)) output = model(x) # 打印输出结果 print('Model output size:', output.size()) # 移除hook函数 hook_handle.remove()
在上述示例中,我们定义了一个MyModel类,该类包含了三个卷积层和一个ReLU激活函数。然后,我们使用register_forward_hook方法注册了一个hook函数,用于获取第二个卷积层的输出。最后,我们传入一个输入张量x,并通过模型进行前向传播,同时在hook函数中获取了第二个卷积层的输出大小。
需要注意的是,在hook函数中我们可以获取到模块的输入、输出以及模块自身,这些参数都可以在后续的分析或处理中使用。在本例中,我们只是简单地打印了特征图的大小,但实际上可以根据需要对特征图进行进一步的操作,比如可视化或保存到文件中等。
进行可视化即更改hook函数。
对于上述示例中获取到的特征图,可以使用Matplotlib库将其可视化,或使用PIL库将其保存到文件中。下面是示例代码:
# 使用Matplotlib将特征图可视化 import matplotlib.pyplot as plt import numpy as np def visualize_feature_map(module, input, output): # 将Tensor转换为NumPy数组,并取第一个样本 feature_map = output.detach().numpy()[0] # 获取特征图的通道数 num_channels = feature_map.shape[0] # 创建一个网格图,每行显示8个特征图 fig, axs = plt.subplots(nrows=int(np.ceil(num_channels/8)), ncols=8, figsize=(12, 6)) for i in range(num_channels): # 获取第i个特征图,并将其归一化到0-1之间 channel_map = feature_map[i, ...] channel_map -= np.min(channel_map) channel_map /= np.max(channel_map) # 在网格图中显示第i个特征图 row = i // 8 col = i % 8 axs[row][col].imshow(channel_map, cmap='gray') axs[row][col].set_xticks([]) axs[row][col].set_yticks([]) plt.show() # 注册hook函数并进行前向传播 hook_handle = model.conv2.register_forward_hook(visualize_feature_map) output = model(x) # 移除hook函数 hook_handle.remove()
在上述代码中,我们定义了一个新的hook函数visualize_feature_map,用于将特征图可视化。在该函数中,我们首先将输出张量转换为NumPy数组,并取出第一个样本的特征图。然后,我们使用Matplotlib库创建一个网格图,并将每个特征图归一化到0-1之间后在网格图中显示。最后,我们通过调用plt.show()方法显示可视化结果。
除了将特征图可视化外,还可以使用PIL库将其保存到文件中,如下所示:
# 使用PIL将特征图保存到文件中 from PIL import Image def save_feature_map(module, input, output): # 将Tensor转换为PIL图像,并保存到文件中 feature_map = output.detach().numpy()[0] for i in range(feature_map.shape[0]): channel_map = feature_map[i, ...] channel_map -= np.min(channel_map) channel_map /= np.max(channel_map) channel_map = (channel_map * 255).astype(np.uint8) image = Image.fromarray(channel_map) image.save(f'channel_{i}.png') # 注册hook函数并进行前向传播 hook_handle = model.conv2.register_forward_hook(save_feature_map) output = model(x) # 移除hook函数 hook_handle.remove()
在上述代码中,我们定义了一个新的hook函数save_feature_map,用于将特征图保存到文件中。在该函数中,我们首先将输出张量转换为NumPy数组,并对每个特征图进行归一化和类型转换,然后使用PIL库创建一个图像对象,并将其保存。
torch.nn.Sequential
的特征图可视化如果网络结构是使用nn.Sequential进行书写的,那么可以通过指定Sequential中子模块的名称或者索引来获取特定层的特征矩阵。
例如,假设您的网络结构如下:
import torch.nn as nn model = nn.Sequential( nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1), nn.ReLU(), nn.MaxPool2d(kernel_size=2, stride=2), nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1), nn.ReLU(), nn.MaxPool2d(kernel_size=2, stride=2), nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1), nn.ReLU(), nn.MaxPool2d(kernel_size=2, stride=2), nn.Flatten(), nn.Linear(256 * 4 * 4, 1024), nn.ReLU(), nn.Linear(1024, 10) )
要获取第2个卷积层(即nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1))的输出特征矩阵,可以通过以下代码实现:
hook_handle = model[3].register_forward_hook(hook_fn)
其中,model[3]表示获取Sequential中的第4个子模块(由于Python中的索引从0开始计数,所以model[3]实际上是第4个子模块),并注册hook_fn作为其forward hook函数。在这个hook函数中,您可以像之前的示例一样获取和处理特征矩阵。
如果您想通过子模块名称来获取特定层的特征矩阵,则可以使用nn.Sequential.named_modules()方法。例如,要获取第2个卷积层的输出特征矩阵,可以使用以下代码:
for name, module in model.named_modules():
if name == '3':
hook_handle = module.register_forward_hook(hook_fn)
其中,name表示子模块的名称(由于Sequential中子模块没有名称,因此默认情况下,它们的名称为其在Sequential中的索引),module表示子模块本身。在这个循环中,我们检查每个子模块的名称,找到名称为’3’的子模块(即第2个卷积层),并注册hook_fn作为其forward hook函数。注意,named_modules()方法会递归地遍历Sequential中的所有子模块,因此如果您的网络结构比较复杂,可能需要在循环中添加额外的逻辑来确保正确地获取到特定层。
如果模型中嵌套了一个小模型,也是继承自nn.Module的,要获取该小模型中某一特定层的输出特征矩阵,可以使用类似于前面的示例的方法。假设您的模型结构如下:
import torch.nn as nn class SmallModel(nn.Module): def __init__(self): super(SmallModel, self).__init__() self.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1) self.relu = nn.ReLU() self.conv2 = nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1) self.maxpool = nn.MaxPool2d(kernel_size=2, stride=2) def forward(self, x): x = self.conv1(x) x = self.relu(x) x = self.conv2(x) x = self.relu(x) x = self.maxpool(x) return x class MyModel(nn.Module): def __init__(self): super(MyModel, self).__init__() self.small_model = SmallModel() self.conv3 = nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1) self.relu = nn.ReLU() self.maxpool = nn.MaxPool2d(kernel_size=2, stride=2) self.flatten = nn.Flatten() self.fc1 = nn.Linear(256 * 4 * 4, 1024) self.fc2 = nn.Linear(1024, 10) def forward(self, x): x = self.small_model(x) x = self.conv3(x) x = self.relu(x) x = self.maxpool(x) x = self.flatten(x) x = self.fc1(x) x = self.relu(x) x = self.fc2(x) return x
要获取SmallModel中的第2个卷积层(即nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1))的输出特征矩阵,可以使用以下代码:
hook_handle = model.small_model.conv2.register_forward_hook(hook_fn)
其中,model.small_model.conv2表示获取SmallModel中的conv2子模块(即第2个卷积层),并注册hook_fn作为其forward hook函数。在这个hook函数中,您可以像之前的示例一样获取和处理特征矩阵。
需要注意的是,您需要在SmallModel的forward方法中将每个子模块逐个调用,以便正确计算输出特征矩阵。在这个例子中,由于SmallModel的forward方法中已经将conv1、relu、conv2和maxpool子模块都调用了一遍,因此在获取SmallModel中的第2个卷积层的输出特征矩阵时,我们只需要关注SmallModel中的conv2子模块。
如果模型是递归定义的,可以使用递归函数来访问特定层的输出特征矩阵。在递归函数中,您需要检查当前层是否是目标层,如果是,则注册hook并获取其输出特征矩阵;如果不是,则继续递归进入下一层。以下是一个示例:
import torch.nn as nn class RecursiveModel(nn.Module): def __init__(self, in_channels, out_channels): super(RecursiveModel, self).__init__() self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=1, padding=1) self.relu = nn.ReLU() self.recursive = None if out_channels > 1: self.recursive = RecursiveModel(out_channels, out_channels // 2) def forward(self, x): x = self.conv1(x) x = self.relu(x) if self.recursive: x = self.recursive(x) x = self.conv1(x) x = self.relu(x) return x def hook_fn(module, input, output): # process the output feature map print(output.shape) def recursive_hook(model, layer_name, current_depth, target_depth): for name, module in model._modules.items(): if isinstance(module, nn.Module): if current_depth == target_depth and name == layer_name: # if this is the target layer, register the hook hook_handle = module.register_forward_hook(hook_fn) hooks.append(hook_handle) elif current_depth < target_depth: # if we haven't reached the target depth, keep going recursively recursive_hook(module, layer_name, current_depth + 1, target_depth) model = RecursiveModel(3, 16) hooks = [] recursive_hook(model, "conv1", 1, 2)
在这个例子中,我们定义了一个递归模型RecursiveModel,它包含一个卷积层和一个递归子模块。在递归函数recursive_hook中,我们遍历模型中的所有子模块,并检查当前深度是否等于目标深度,以及当前子模块是否是目标层。如果当前深度等于目标深度且当前子模块是目标层,我们就注册forward hook并将其句柄添加到hooks列表中。如果当前深度小于目标深度,我们就递归进入下一层。
在本例中,我们希望获取第2层递归中的第1个卷积层(即模型中的第二个卷积层)。因此,我们在recursive_hook中设置目标深度为2,目标层名称为"conv1"。当我们注册forward hook时,它将捕获目标层的输出特征矩阵。
所以尽量使用顺序的,多嵌套的代码来实现你的模型。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。