GAM/Grad_GAM即为热力图,可以很直观的看到模型关注的重点区域,如图所示。这种可视化方法有很多帖子可参考,因此不再赘述,放上大佬改好的开源代码链接https://github.com/IDayday/YOLOv4_CAM。
对于YOLOv4的GAM可视化作者源码,同学在使用过程中出现了一些BUG,现在进行一下说明,帮助大家避坑。
源码使用方法如下:
1、在yolo_cam.py里面将model_path、classes_path、path改为自己的训练好的模型权重、目标类别、输入图片路径(相对位置)
2、直接运行或调试yolo_cam.py
同学在使用过程中出现的报错如下:
这是因为对张量f进行reshape操作时出现了维度不匹配的情况。让我们从第221行代码进行分析,首先通过赋值过的path读取图片,将图片存入image中,然后调用detect_image()函数,将图片输入进行检测后,将检测结果返回到output_list这个张量中,output_list是一个存了3个tensor的列表,其shape为[[1,507,6],[1,2028,6],[1,8112,6]]。报错的第223行代码通过枚举函数和for语句将output_list中的tensor和对应索引依次取出,进行reshape后加入ret列表中。这里取出第一个tensor处理时就出现了报错,其shape为[1,507,6],总的维度数量为1x507x6=3042。而此时reshape函数中的参数为(1,3,13,13,10),总的维度数量为1x3x13x13x10=5070,总的维度数量不等,所以报错。
解决方案: 将224行ret.append(f.reshape(1,3,stride[i],stride[i],10))
中的10改为6,将156行def show_CAM(image_path, feature_maps, class_id, all_ids=10, show_one_layer=True):
函数定义处的 all_ids改为6。最后将230行show_CAM(path, ret, 1)
中的1改为想要可视化类别的索引即可,但是要注意这里的索引值从0开始,并且不能超过classes.txt的维度大小。比如classes.txt中只有1个类别,索引设为了1,必然报错(应设置为0)。
特征图可视化需要对特征图进行通道分离,将tensor展开后以图片的形式保存。为直观展示效果,从YOLOv4主干网络CSPdarknet53中分别提取shape为(416,416,32)、(208,208,64)、(104,104,128)的特征进行可视化。在CSPDarkNet类中的forward(self, x)函数进行修改,调用模型即可,使用前要先在项目目录下分别创建名为“new_feature_picture0”、“new_feature_picture1”、“new_feature_picture2”的文件夹,代码如下:
def forward(self, x):
x = self.conv1(x)
out0 = x
x = self.stages[0](x)
out1 = x
x = self.stages[1](x)
out2 = x
out3 = self.stages[2](x)
out4 = self.stages[3](out3)
out5 = self.stages[4](out4)
#-----------------------------------特征可视化---------------------------------#
image0s = out0
image1s = out1
image2s = out2
#--------通道分离tensor,并可视化每个维度的特征图-------#
#先清空文件夹中的内容
shutil.rmtree('new_feature_picture0/')
os.mkdir('new_feature_picture0/')
image0s=torch.split(image0s,1,dim=0) #dim=0是通道的索引,代表image0的第一个通道。'1'表示分离成单个维度,此处分离后image0为装有单通道tensor的元组。
k=0
for image0 in image0s:
q=0
image0=torch.split(image0,1,dim=1)
for i in image0:
t='backbone-32-416-416-picture'+str(k)+'-channel-'+str(q)+'.jpg'
save_image(i,os.path.join('new_feature_picture0',t))
q=q+1
# feature_map_sum = sum(ele for ele in image0)
# save_image(feature_map_sum,os.path.join('new_feature_picture0','feature_map_sum'+'_'+str(k)+'.png'))
k=k+1
#--------通道分离tensor,并可视化每个维度的特征图-------#
#先清空文件夹中的内容
shutil.rmtree('new_feature_picture1/')
os.mkdir('new_feature_picture1/')
image1s=torch.split(image1s,1,dim=0) #dim=0是通道的索引,代表image0的第一个通道。'1'表示分离成单个维度,此处分离后image0为装有单通道tensor的元组。
k=0
for image1 in image1s:
q=0
image1=torch.split(image1,1,dim=1)
for i in image1:
t='backbone-64-208-208-picture'+str(k)+'-channel-'+str(q)+'.jpg'
save_image(i,os.path.join('new_feature_picture1',t))
q=q+1
# feature_map_sum = sum(ele for ele in image1)
# save_image(feature_map_sum,os.path.join('new_feature_picture1','feature_map_sum'+'_'+str(k)+'.png'))
k=k+1
#--------通道分离tensor,并可视化每个维度的特征图-------#
#先清空文件夹中的内容
shutil.rmtree('new_feature_picture2/')
os.mkdir('new_feature_picture2/')
image2s=torch.split(image2s,1,dim=0) #dim=0是通道的索引,代表image0的第一个通道。'1'表示分离成单个维度,此处分离后image0为装有单通道tensor的元组。
k=0
for image2 in image2s:
q=0
image2=torch.split(image2,1,dim=1)
for i in image2:
t='backbone-128-104-104-picture'+str(k)+'-channel-'+str(q)+'.jpg'
save_image(i,os.path.join('new_feature_picture2',t))
q=q+1
# feature_map_sum = sum(ele for ele in image2)
# save_image(feature_map_sum,os.path.join('new_feature_picture2','feature_map_sum'+'_'+str(k)+'.png'))
k=k+1
return out3, out4, out5
可视化完成后,如需要对特征图进行拼接,可参考我的上一篇博客https://blog.csdn.net/weixin_44944382/article/details/123829981?spm=1001.2014.3001.5501,最终效果如下:
原图
(416,416,32),32通道,所以一共由32张图片拼接而成。
(208,208,64)
(104,104,128)