卷积神经网络是一种特殊类型的人工神经网络,广泛应用于图像识别。这种架构的成功始于 2015 年,当时凭借这种方法赢得了 ImageNet 图像分类挑战。 这些方法非常强大并且能够很好地进行预测,但同时它们也难以解释。因此,它们也被称为黑盒模型。 肯定是可用的与模型无关的方法,如LIME和部分依赖图,可以应用于任何模型。但在这种情况下,应用专为神经网络开发的可解释方法更有意义。与 ML 模型不同,卷积神经网络从原始图像像素中学习抽象特征 [1]。 在这篇文章中,我将重点介绍卷积神经网络如何学习特征。这可以通过逐步学习的特征的可视化来实现。在展示 Pythorch 的实现之前,我将解释 CNN 的工作原理,然后我将可视化为分类任务训练的 CNN 学习的特征图和感受野。 本文内容:
什么是CNNcnn由构建模块组成:卷积层、池化层和全连接层。卷积层的主要功能是提取特征或所谓的特征映射。它是如何做到的呢?它使用来自数据集[2]的多个过滤器。 之后,通过池化层将卷积运算得到的特征映射降维。最常用的池化操作是Maxpooling,它选择feature map中每个filter patch中最显著的像素值。因此,这两种类型的层对于进行特征提取是很有用的。 与卷积层和池化层不同的是,全连接层将提取的特征映射到最终的输出中,例如MNIST图像的分类为10位数字之一。 在数字图像中,一个二维网格存储像素值。它可以看作是一组数字。内核是一个小网格,通常大小为3x3,应用于图像的每个位置。当你深入到更深的层次时,这些特性会变得越来越复杂。 模型的性能是通过一个损失函数,即输出和目标标签之间的差异,通过在训练集上的前向传播,并通过梯度下降算法的反向传播更新参数,如权值和偏差。 定义和训练CNN的MNIST数据集让我们首先导入库和数据集。Torch是为一维或多维张量提供数据结构的模块。 几个重要的包: Torchvision:在我们的例子中,它为我们提供了MNIST数据集。 torch.nn:包含帮助你构建卷积神经网络的类和函数。 torch.optim:提供了所有的优化器,比如Adam。 torch.nn.functional:用于导入函数,如dropout、convolution、pooling、非线性激活函数和loss函数等。 我们下载训练和测试数据集,并将图像数据集转换为张量。我们不需要对图像进行归一化,因为数据集已经包含了灰度图像。将训练数据集划分为训练集和验证集。random_split为这两个集合提供了一个随机分区。DataLoader用于为训练、验证和测试集创建数据加载器,这些数据加载器被划分为小批。batchsize是模型训练过程中一次迭代中使用的样本数量。 import matplotlib.pyplot as plt import numpy as np import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import Dataset, DataLoader,TensorDataset,random_split,SubsetRandomSampler, ConcatDataset from torch.nn import functional as F import torchvision from torchvision import datasets,transforms import torchvision.transforms as transforms train_dataset = torchvision.datasets.MNIST('classifier_data', train=True, download=True) test_dataset = torchvision.datasets.MNIST('classifier_data', train=False, download=True) transform = torchvision.transforms.Compose([ torchvision.transforms.ToTensor() ]) train_dataset.transform=transform test_dataset.transform=transform m=len(train_dataset) #random_split randomly split a dataset into non-overlapping new datasets of given lengths train_data, val_data = random_split(train_dataset, [int(m-m*0.2), int(m*0.2)]) batch_size=128 # The dataloaders handle shuffling, batching, etc... train_loader = torch.utils.data.DataLoader(train_data, batch_size=batch_size) valid_loader = torch.utils.data.DataLoader(val_data, batch_size=batch_size) test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=batch_size,shuffle=True) print('Batches in Train Loader: {}'.format(len(train_loader))) print('Batches in Valid Loader: {}'.format(len(valid_loader))) print('Batches in Test Loader: {}'.format(len(test_loader))) print('Examples in Train Loader: {}'.format(len(train_loader.sampler))) print('Examples in Valid Loader: {}'.format(len(valid_loader.sampler))) print('Examples in Test Loader: {}'.format(len(test_loader.sampler))) 我们定义了CNN架构。 class ConvNet(nn.Module): 我们可以很容易地打印架构来快速了解一下: 您可以看到有两个卷积层和两个完全连接的层。每个卷积层之后是ReLU激活函数和maxpooling层。视图函数将数据重塑为一维数组,并将其传递给线性层。第二个完全连接的层,也称为输出层,将图像分类为10位数字之一。 我们定义构建块,将用于训练CNN:
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu') torch.manual_seed(0) model = ConvNet() model.to(device) optimizer = optim.Adam(model.parameters(), lr=0.002) criterion= nn.CrossEntropyLoss() 现在,我们可以在训练集中训练网络,并在验证集中对其进行评估: history={'train_loss':[],'valid_loss':[],'train_acc':[],'valid_acc':[]} 训练代码可以分为两部分。 向前传播:
反向传播:
对训练集和验证集都计算损失函数和准确性。 在测试集上评估模型模型训练完成后,我们可以对测试集的性能进行评估: correct = 0 total = 0 test_loss = 0 with torch.no_grad(): for images, labels in test_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) loss = criterion(outputs,labels) test_loss += loss.item() * images.size(0) _, predicted = torch.max(outputs.data, 1) total += labels.size(0) correct += (predicted == labels).sum().item() test_loss = test_loss / len(test_loader.sampler) print('Accuracy of the network on the test images: {:.2f} %%'.format(100 * correct / total)) print('Loss of the network on the test images: {:.4f}'.format(test_loss)) 让我们把测试代码分解成小块: torch.no_grad()用于禁用梯度跟踪,我们不再需要计算梯度,因为模型已经训练。 将输入图像传递给网络
可视化过滤器我们可以将学习到的过滤器可视化,CNN使用它来卷积feature map,其中包含从上一层提取的特征。可以通过遍历模型的所有层,list(model.children())来获得这些过滤器。如果层是卷积的,我们可以将权重存储在model_weights列表中,该列表将包含两个卷积层中使用的过滤器。 model_weights = [] 下面是我找到的过滤器的形状。 for weight, conv in zip(model_weights, conv_layers): # print(f'WEIGHT: {weight} \nSHAPE: {weight.shape}') print(f'CONV: {conv} ====> SHAPE: {weight.shape}') 现在,我们可以可视化学习到的第一个卷积层的滤波器: # visualize the first conv layer filters 现在,轮到可视化第二层卷积层的滤波器了。 # visualize the second conv layer filters plt.figure(figsize=(20, 17)) for i, filter in enumerate(model_weights[1]): plt.subplot(6, 6, i+1) # (8, 8) because in conv0 we have 5x5 filters and total of 64 (see printed shapes) plt.imshow(filter[0, :, :].detach().cpu().numpy(), cmap='viridis') plt.axis('off') plt.savefig('filter2.png') plt.show() 可视化特征图Feature Map,又称Activation Map,是通过卷积运算得到的,通过filter/kernel将其应用到输入数据上。下面,我们定义一个函数来提取应用激活函数后获得的特征。 activation = {} 从训练数据集中,我们取一个代表数字9的图像。因此,我们将在第一个卷积层中可视化该图像获得的特征映射。 model.conv1.register_forward_hook(get_activation('conv1')) data, _ = train_dataset[4] data=data.to(device) data.unsqueeze_(0) output = model(data) k=0 act = activation['conv1'].squeeze() fig,ax = plt.subplots(4,4,figsize=(12, 15)) for i in range(act.size(0)//4): for j in range(act.size(0)//4): ax[i,j].imshow(act[k].detach().cpu().numpy()) k+=1 plt.savefig('fm1.png') 现在,我们在第二层卷积中将同一幅图像获得的特征映射可视化。 model.conv2.register_forward_hook(get_activation('conv2')) 最后总结恭喜你!您已经学会了用Pytorch将CNN学到的特征可视化。网络在其卷积层中学习新的、日益复杂的特征。从第一个卷积层到第二个卷积层,您可以看到这些特征的差异。在卷积层中走得越远,特征就越抽象。 感谢你的阅读。祝你过得愉快。 作者:Eugenia Anello |
|