本文给出完整代码实现CNN特征的可视化输入图像,也就是简单的deep dream图,有助于更好的理解CNN工作原理,并掌握用梯度上升法生成满足要求输入图像的技术。更清晰美观的deep dream图需要加入一些其他技巧,可以参考我另一篇文章。
1,原理
深度网络的常规工作过程是,给定输入样本及标签,前向处理后的输出结果和标签计算出某种损失函数,再由损失函数反向传播求网络各参数的梯度,根据此梯度更新参数,以使损失函数减小,逐渐训练得到一个逼近标签的网络。
有趣的是,我们也可以反向思维,固定网络的参数不变,而是优化输入图像。根据某项指标求得输入图像的梯度,再根据此梯度优化输入,就可以得到满足要求的输入图像。由于往往需要最大化某种指标,与通常最小化损失函数的梯度下降法不同,所以这种方法也被称为梯度上升法。在程序实现上,为了利用SGD,Adam等成熟的梯度下降优化算法,我们只需要给指标加个负号,也就变成和梯度下降一样了。
如果我们把特征图的某个部分的均值作为最大化的指标,此时就可以得到使指定特征图部分最大响应的输入图,也就能够直观的看出指定部分的特征图到底处理的是什么类型的特征。这个指定的特征图部分可以是某个层,也可以是某个层的某个通道,甚至也可以是某个通道上某个元素。我们先来看单个元素的情况:
2,指定特征图单个元素的最大响应输入图像可视化
import torch
import torchvision.models as models
import cv2
import time
t0 = time.time()
model = models.resnet18(pretrained=True).cuda()
batch_size = 1
for params in model.parameters():
params.requires_grad = False
model.eval()
def hook(module,inp,out):
global features
features = out
data = torch.rand(batch_size,3,224,224).cuda()
data.requires_grad=True
mu = torch.Tensor([0.485, 0.456, 0.406]).unsqueeze(-1).unsqueeze(-1).cuda()
std = torch.Tensor([0.229, 0.224, 0.225]).unsqueeze(-1).unsqueeze(-1).cuda()
unnormalize = lambda x: x*std + mu
normalize = lambda x: (x-mu)/std
#optimizer = torch.optim.SGD([data], lr=1, momentum=0.99) #参数需根据实际情况再调
optimizer = torch.optim.Adam([data], lr=0.1, weight_decay=1e-6)
myhook = model.layer2.register_forward_hook(hook)
n,h,w = 0,3,8
for i in range(4001):
x = (data - data.min()) / (data.max() - data.min())
x = normalize(x)
_ = model(x)
loss = - features[:,n,h,w].mean() #指定元素
#loss = - features[:,n,:,:].mean() #指定通道
#loss = - features.mean() #指定层
optimizer.zero_grad()
loss.backward()
optimizer.step()
if i%100==0:
print('data.abs().mean():',data.abs().mean().item())
print('loss:',loss.item())
print('time: %.2f'%(time.time()-t0))
myhook.remove()
data_i = data[0]
data_i = (data_i - data_i.min()) / (data_i.max() - data_i.min())
data_i = data_i.permute(1,2,0).data.cpu().numpy()*255
data_i = data_i[...,::-1].astype('uint8') #注意cv2使用BGR顺序
cv2.imwrite('./feature_visual/layer2_filter%d.png'%n,data_i)
完整代码如上,我们以torchvision中的预训练resnet18为例,按照大的模块划分,resnet18中有4个layer,画出每个layer中某个元素对应的输入特征图如下:
从图中还可以看出,每个特征元素在输入图像上的感受野有多大,显然越靠后层元素的感受野越大。
3,指定通道
可以看出,前序层的输入响应图都是一些均匀的纹理图,显然是图像的更基础的组成要素;随着网络越深,输入响应图则逐渐呈现出更加全局性的一些图像概念,以及更加高级和接近实物的一些图像概念,有些似乎是某种实物的形状特征。
我们还可以看出,单个通道的响应中可以看到多个类别的图像的“影子”,所以通道并不是类别依赖的。
4,指定层
5,其他一些网络
可以看出,不同网络的风格还是很不相同的,其中densenet不像别的那么恶心,还挺好看。