Grad-CAM与T-SNE 可视化代码实现
1.T-SNE
t-Distributed Stochastic Neighbor Embedding (t-SNE)是一种降维技术,用于在二维或三维的低维空间中表示高维数据集,从而使其可视化。在sklearn库的代码实现中,首先会用PCA算法进行降维,再用t-sne算法进一步把维度降至为2,这样可以提高运行速度。对数据集或者特征图进行降维可视化可以帮助我们更好地分析数据集和模型。对mnist手写数据集的降维效果如图1。
我在Imagenet数据集中选择了其中5个类的数据进行测试,图2为对这5个类的图片降维可视化的结果。
除了对数据集进行降维以外,t-sne还被经常用来对特征图进行降维。以resnet为例layer4是骨干网络的最后一层,每个样本对应特征图的输出尺寸为(512,7,7),也就是说特征图的维度是512x7x7=25088。我用resnet34的预训练权重加载模型,以Imagenet中的5个子类作为测试数据,对layer4输出的特征图进行降维后的可视化结果如图3。可以看到模型已经可以很好的从图片中提取到区分不同类样本的特征了。
一点思考:深度学习的本质是特征学习(表示学习),我们希望深度学习模型能自动学习到可以有效区分不同类别的特征,把人们从费时费力地手工选取特征的工作中解放出来。如果选取的特征足够好,那么根据这个特征应该可以很容易的区分出不同类别的样本,很显然resnet34的预训练模型已经很好的学习到了足以区分这5个类样本的特征了,因为通过降维可以看到,同一个类的样本对于这种特征的输出值十分相似,不同类的样本通过这种特征输出的值可以被明显的分辨出来。于是我们就可以很容易训练一个线性分类头来对这些特征值进行分类了。假如模型没有很好的学习到特征,也就是说模型学习到的特征不具有很强的区分性,那么不同类的样本通过这样的特征得到的输出值就不具有区分度,如图4。降维后可以看到不同类别的样本会混淆在一起,分类头将无法学习到一个可以很好区分各个类样本的决策边界。
T-SNE代码实现如下:
from sklearn.manifold import TSNE
from time import time
import torchvision
from torchvision import transforms
from torch.utils.data import DataLoader
import torch
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
class T_sne_visual():
def __init__(self, model, dataset, dataloader):
self.model = model
self.dataset = dataset
self.dataloader = dataloader
self.class_list=dataset.classes
def visual_dataset(self):
imgs = []
labels = []
for img, label in self.dataset:
imgs.append(np.array(img).transpose((2, 1, 0)).reshape(-1))
tag = self.class_list[label]
labels.append(tag)
self.t_sne(np.array(imgs), labels,title=f'Dataset visualize result\n')
def visual_feature_map(self, layer):
self.model.eval()
with torch.no_grad():
self.feature_map_list = []
labels = []
getattr(self.model, layer).register_forward_hook(self.forward_hook)
for img, label in self.dataloader:
img=img.cuda()
self.model(img)
for i in label.tolist():
tag=self.class_list[i]
labels.append(tag)
self.feature_map_list = torch.cat(self.feature_map_list,dim=0)
self.feature_map_list=torch.flatten(self.feature_map_list,start_dim=1)
self.t_sne(np.array(self.feature_map_list.cpu()), np.array(labels),title=f'{layer} resnet feature map\n')
def forward_hook(self, model, input, output):
self.feature_map_list.append(output)
def set_plt(self, start_time, end_time,title):
plt.title(f'{title} time consume:{end_time - start_time:.3f} s')
plt.legend(title='')
plt.ylabel('')
plt.xlabel('')
plt.xticks([])
plt.yticks([])
def t_sne(self, data, label,title):
# t-sne处理
print('starting T-SNE process')
start_time = time()
data = TSNE(n_components=2, learning_rate='auto', init='pca').fit_transform(data)
x_min, x_max = np.min(data, 0), np.max(data, 0)
data = (data - x_min) / (x_max - x_min)
df = pd.DataFrame(data, columns=['x', 'y']) # 转换成df表
df.insert(loc=1, column='label', value=label)
end_time = time()
print('Finished')
sns.scatterplot(x='x', y='y', hue='label', s=3, palette="Set2", data=df)
self.set_plt(start_time, end_time, title)
plt.savefig('1.jpg', dpi=400)
plt.show()
trans=transforms.Compose([transforms.Resize((224,224)),transforms.ToTensor(),transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])
imgset=torchvision.datasets.ImageFolder(root=r'C:\Users\Administrator\Desktop\imageset',transform=trans)
img_loader=DataLoader(imgset,batch_size=16,shuffle=True)
net=torchvision.models.resnet34(pretrained=False).cuda()
t = T_sne_visual(net, imgset,img_loader)
t.visual_dataset()
t.visual_feature_map('layer4')
我数据集的目录结构为:
2.Grad-CAM
Grad-CAM(Gradient-weighted Class Activation Mapping)可以对输入网络的每张图片生成一个热力图,假设网络输出这张图片的类别为'A',那么这个热力图就会呈现图片中每一个位置与'A'这个类别的相似程度,有助于了解原始图像的哪一个局部位置让网络做出了最终的分类决策。还是以预训练的resnet34模型为例,效果如图5和图6所示。图片中的区域越红,代表该区域对模型决策分类的重要程度越高。
Grad-CAM代码实现如下:
import cv2
import numpy as np
import torch
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import torchvision
class GradCAM:
def __init__(self, model, target_layer, size=(224, 224), num_cls=1000, mean=None, std=None):
self.model = model
self.model.eval()
getattr(self.model, target_layer).register_forward_hook(self.__forward_hook)
getattr(self.model, target_layer).register_full_backward_hook(self.__backward_hook)
self.size = size
self.num_cls = num_cls
self.mean, self.std = mean, std
self.ann = {}
with open(r'D:\jupyter\imagenet_ann.txt', 'r') as f:
file = f.readlines()
for line in file:
key,_ ,value=line.split(' ',2)
self.ann[key] = value
def forward(self, path, show=True, write=False):
# 读取图片
origin_img = cv2.imread(path)
origin_size = (origin_img.shape[1], origin_img.shape[0]) # [H, W, C]
transform = transforms.Compose([
transforms.ToPILImage(),
transforms.Resize(self.size),
transforms.ToTensor(),
transforms.Normalize(self.mean, self.std)])
img = transform(origin_img[:, :, ::-1]).unsqueeze(0)
# 输入模型以获取特征图和梯度
output = self.model(img)
self.model.zero_grad()
loss,index = torch.max(output,dim=1) # 这个output的下标就是模型预测的label
print('预测label为:',index.item())
print('对应类别为:', self.ann[str(index.item())])
loss.backward()
# 计算cam图片
cam = np.zeros(self.fmaps.shape[1:], dtype=np.float32)
alpha = np.mean(self.grads, axis=(1, 2))
for k, ak in enumerate(alpha):
cam += ak * self.fmaps[k] # linear combination
cam[cam < 0] = 0
cam = cv2.resize(np.array(cam), origin_size)
cam /= np.max(cam)
# 把cam图变成热力图,再与原图相加
cam= cv2.applyColorMap(np.uint8(255 * cam), cv2.COLORMAP_JET)
cam = np.float32(cam) + np.float32(origin_img)
# 两幅图的色彩相加后范围要回归到(0,1)之间,再乘以255
cam = np.uint8(255 * cam / np.max(cam))
if write:
cv2.imwrite("camcam.jpg", cam)
if show:
# 要显示RGB的图片,如果是BGR的 热力图是反过来的
plt.imshow(cam[:, :, ::-1])
plt.show()
def __backward_hook(self, module, grad_in, grad_out):
self.grads = np.array(grad_out[0].detach().squeeze())
def __forward_hook(self, module, input, output):
self.fmaps = np.array(output.detach().squeeze())