姓名:成杰? ? ?學(xué)號:21021210653? ? 學(xué)院:電子工程學(xué)院
轉(zhuǎn)自:https://zhuanlan.zhihu.com/p/509645247
【嵌牛導(dǎo)讀】
CAM(Class Activation Mapping):將CNN在分類時使用的分類依據(jù)(圖中對應(yīng)的類別特征)在原圖的位置進(jìn)行可視化,并繪制成熱圖驹针,以此作為物體定位的依據(jù)的方法烘挫。
【嵌牛鼻子】
神經(jīng)網(wǎng)絡(luò)、可視化
【嵌牛提問】
CAM方法的原理柬甥?
【嵌牛正文】?
1. 整體思路
CNN網(wǎng)絡(luò)做分類時之所以丟失了物體的位置信息饮六,是因?yàn)榫W(wǎng)絡(luò)末端使用了全連接層,通過使用GAP替代全連接層苛蒲,從而使卷積網(wǎng)絡(luò)的定位能力能延續(xù)到網(wǎng)絡(luò)的最后一層卤橄,故保持經(jīng)典網(wǎng)絡(luò)(如VGGnet窟扑、Alexnet和GoogleNet)的卷積部分,只在輸出層前(用于分類的softmax)將全連接層替換為GAP橘霎,并將它們作為得出分類的特征。通過這樣的連接結(jié)構(gòu)阔蛉,可以把圖片中重要的區(qū)域用輸出層權(quán)重映射回卷積層特征的方式標(biāo)記出來,這種方法稱為類別激活映射或類別激活圖损搬。
2. 全局平均池化層(GAP)和全局最大池化層(GMP)對比
GAP和GMP都是全局池化的方法嵌灰,也有學(xué)者在做弱監(jiān)督物體定位時采用了這兩種方法沽瞭,而文章之所以選擇GAP有以下兩個原因:
(1) GMP希望網(wǎng)絡(luò)更關(guān)注物體的1個discriminaltive part,更關(guān)注物體的邊緣識別豌鹤,取最大的部分,而GAP則更希望網(wǎng)絡(luò)識別物體的整個范圍灵临。在求平均值時儒溉,GAP可以綜合并找到所有discriminaltive part來得到最大激活赶诊,對于低激活的區(qū)域就會減少特定輸出舔痪,即GAP相對于GMP來說識別這個物體辨別性區(qū)域的損失更小。
(2) GMP由于只取了區(qū)域最大值滋捶,所以其他低分的區(qū)域?qū)ψ罱K分類的得分都不會有影響,GMP的分類性能和GAP相當(dāng)巡扇,但GAP的定位能力強(qiáng)于GMP。
3. 原理
CAM激活圖由兩部分加權(quán)構(gòu)成:原圖+特征圖刀闷。其中特征圖是通過:最后全連接層的參數(shù)(權(quán)重矩陣W)與最后輸出的特征圖集合對應(yīng)相乘再相加(重疊)而形成,即顯示模型是依據(jù)特征圖進(jìn)行決策分類的筒扒。
(1)輸入:訓(xùn)練集或測試集圖片澄步,大小為[224,224,3]
(2) VGG16預(yù)訓(xùn)練卷積層最后一層的特征圖大小為7*7村缸,共512張仇箱,所以大小為[7,7,512]
(3)經(jīng)過全局平均池化(GAP)剂桥,512張?zhí)卣鲌D被降維為長度512的特征向量
(4)最后特征向量與權(quán)重矩陣W點(diǎn)積,再經(jīng)過softmax函數(shù)壓縮為[0,1]區(qū)間內(nèi)的概率
由于采用遷移學(xué)習(xí)的方式斟薇,卷積層凍結(jié)固定,所以對于同一張圖片袱箱,卷積層的輸出特征始終不變,模型的分類概率只隨著全連接層的權(quán)重矩陣W發(fā)生變化筐咧,這就是模型的學(xué)習(xí)更新過程。
權(quán)重矩陣W可以理解為對長度為512的特征向量的加權(quán)残炮,畢竟特征向量是由特征圖全局平局池化GAP所得。歸根到底是對特征圖集合的加權(quán)苞冯,所以利用特征圖集合與權(quán)重矩陣相乘司忱,再重疊為一張?zhí)卣鲌D,就可以模擬模型分類過程中,是依據(jù)哪部分區(qū)域做出判斷的老翘。
假設(shè)初始模型在剛開始訓(xùn)練時,利用某個特征圖作為判斷的依據(jù)卫键,并計(jì)算正確率莉炉,此時損失值loss很大钓账,則在反向傳播過程中,權(quán)重矩陣W會不斷更新絮宁,在損失函數(shù)約束下梆暮,找到有效的特征圖作為判斷的依據(jù),那么當(dāng)loss小到一定程度绍昂,或預(yù)測的準(zhǔn)確率上升到一定程度啦粹,那么此時的模型便學(xué)會了判別,有了正確分類的能力唠椭。
計(jì)算方法如下圖所示敌厘。對于一個CNN模型,對其最后一個feature map做全局平均池化(GAP)計(jì)算各通道均值活合,然后通過全連接層等映射到class score,找出argmax隆判,計(jì)算最大的那一類的輸出相對于最后一個feature map的梯度,再把這個梯度可視化到原圖上即可照筑。直觀來說冬念,就是看一下網(wǎng)絡(luò)抽取到的高層特征的哪部分對最終的classifier影響更大。
GAP操作后輸出最后一個卷積層每個單元feature-map的平均值,之后再接一個softmax層用于分類色乾,而該層的所有參數(shù)作為權(quán)重wck澎办,對前方的GAP得到的feature-map做加權(quán)總和得到最后的輸出县貌,即CAM輸出楞泼。此時CAM的輸出尺寸和feature-map大小一致,故需要通過上采樣方式還原疊加到原圖中。
CAM的可視化是通過fk激活值實(shí)現(xiàn)的,激活值越大的地方說明該區(qū)域越有可能屬于對應(yīng)某個分類,通過改變圖像尺寸,將激活圖還原成原圖大小的圖片校套,即可得到該分類對應(yīng)在原圖的位置价脾,加權(quán)越多的區(qū)域顏色亮度越大,在通過設(shè)置閾值即可畫出覆蓋該區(qū)域的BBox笛匙,從而得到物體在圖片中的定位邊框侨把。
該方法的缺點(diǎn)是只能適用于最后一層特征圖和全連接之間是GAP操作。如果不是妹孙,就需要用戶修改網(wǎng)絡(luò)并重新訓(xùn)練(或 fine-tune)秋柄。修改網(wǎng)絡(luò)全連接為GAP形式,利用GAP層與全連接的權(quán)重作為特征融合權(quán)重蠢正,對特征圖進(jìn)行線性融合獲取CAM骇笔。
CAM 的實(shí)現(xiàn)依賴于全局平均池化層,通過全局平均池化得到 feature map 每一個通道的權(quán)重嚣崭,然后線性加權(quán)求和得到網(wǎng)絡(luò)關(guān)注區(qū)域的熱力圖笨触。因此對于很多網(wǎng)絡(luò)都不能直接使用,需要把網(wǎng)絡(luò)后面的全連接層改為全局平均池化雹舀。CAM雖然簡單芦劣,但是它要求修改原模型的結(jié)構(gòu),導(dǎo)致需要重新訓(xùn)練該模型说榆,這大大限制了它的使用場景虚吟。如果模型已經(jīng)上線了寸认,或著訓(xùn)練的成本非常高,我們幾乎是不可能為了它重新訓(xùn)練的串慰。
4. 代碼實(shí)現(xiàn)
fromPILimportImagefromtorchvisionimportmodels,transformsfromtorch.autogradimportVariablefromtorch.nnimportfunctionalasFimportnumpyasnpimportcv2importjsonLABELS_URL='https://s3.amazonaws.com/outcome-blog/imagenet/labels.json'# 下載label# 使用本地的圖片和下載到本地的labels.json文件LABELS_PATH="labels.json"# networks such as googlenet, resnet, densenet already use global average pooling at the end, so CAM could be used directly.model_id=2# 選擇使用的網(wǎng)絡(luò)ifmodel_id==1:net=models.squeezenet1_1(pretrained=True)finalconv_name='features'# this is the last conv layer of the networkelifmodel_id==2:net=models.resnet18(pretrained=True)finalconv_name='layer4'elifmodel_id==3:net=models.densenet161(pretrained=True)finalconv_name='features'net.eval()print(net)# 獲取特定層的feature map# hook the feature extractorfeatures_blobs=[]defhook_feature(module,input,output):# input是注冊層的輸入 output是注冊層的輸出print("hook input",input[0].shape)features_blobs.append(output.data.cpu().numpy())# 對layer4層注冊偏塞,把layer4層的輸出append到features里面net._modules.get(finalconv_name).register_forward_hook(hook_feature)# 注冊到finalconv_name,如果執(zhí)行net()的時候,# 會對注冊的鉤子也執(zhí)行邦鲫,這里就是執(zhí)行了 layer4()print(net._modules)# 這里是利用鉤子函數(shù)來獲取最后的feature map灸叼,# _modules.get()方法會返回指定層的網(wǎng)絡(luò)結(jié)構(gòu),即layer4的結(jié)構(gòu)掂碱,# 然后使用register_forward_hook在前向傳播的過程中為layer4注冊hook函數(shù)怜姿,# 接下來會在前向傳播的過程中截取layer4的輸入和輸出,# 在hook_feature函數(shù)中實(shí)現(xiàn)對于輸入和輸出的操作疼燥。# 得到softmax weightparams=list(net.parameters())# 將參數(shù)變換為列表 按照weights bias 排列 池化無參數(shù)weight_softmax=np.squeeze(params[-2].data.numpy())# 提取softmax 層的參數(shù) (weights,-1是bias)# 打印一下網(wǎng)絡(luò)的參數(shù)蚁堤,可以得到網(wǎng)絡(luò)的最后兩個參數(shù)是fc.weight和fc.biasforname,datainnet.named_parameters():print(name,":",data.size())# 生成CAM圖的函數(shù)醉者,完成權(quán)重和feature相乘操作,最后resize成上采樣defreturnCAM(feature_conv,weight_softmax,class_idx):# generate the class activation maps upsample to 256x256size_upsample=(256,256)bz,nc,h,w=feature_conv.shape# 獲取feature_conv特征的尺寸output_cam=[]# class_idx為預(yù)測分值較大的類別的數(shù)字表示的數(shù)組披诗,一張圖片中有N類物體則數(shù)組中N個元素foridxinclass_idx:# weight_softmax中預(yù)測為第idx類的參數(shù)w乘以feature_map(為了相乘撬即,故reshape了map的形狀)# w1*c1 + w2*c2+ .. -> (w1,w2..) * (c1,c2..)^T -> (w1,w2...)*((c11,c12,c13..),(c21,c22,c23..))# weight_softmax[idx]的含義:我們之前取到的fc.weight是一個(1000,512)的矩陣,# 因?yàn)閷τ贗mageNet來說是一個1000分類的問題呈队,那么weight_softmax的第i行就代表第i類的權(quán)重剥槐,# 傳入的第三個參數(shù)class_idx是一個列表,里面有若干值,# 代表我們想得到對于一張圖片來說被分成不同的類映射回原圖分別是原圖的哪一部分在起作用宪摧。# 對于最后的特征圖做一個reshape粒竖,feature_conv.reshape((nc, h*w)將特征圖變成每一行代表原特征圖的一個通道,# 然后對于權(quán)重和reshape之后的圖像做一個矩陣乘法就完成了對于feature map每個加權(quán)通道的求和几于。cam=weight_softmax[idx].dot(feature_conv.reshape((nc,h*w)))# 把原來的相乘再相加轉(zhuǎn)化為矩陣# 將feature_map的形狀reshape回去cam=cam.reshape(h,w)# 歸一化操作(最小的值為0蕊苗,最大的為1)cam=cam-np.min(cam)cam_img=cam/np.max(cam)# 轉(zhuǎn)換為圖片的255的數(shù)據(jù)cam_img=np.uint8(255*cam_img)# resize 圖片尺寸與輸入圖片一致output_cam.append(cv2.resize(cam_img,size_upsample))returnoutput_cam# 數(shù)據(jù)處理,先縮放尺寸到(256*256)沿彭,再變換數(shù)據(jù)類型為tensor,最后normalizenormalize=transforms.Normalize(mean=[0.485,0.456,0.406],std=[0.229,0.224,0.225])preprocess=transforms.Compose([transforms.Resize((256,256)),transforms.ToTensor(),normalize])img_pil=Image.open('2103.jpg')img_tensor=preprocess(img_pil)# 處理圖片為Variable數(shù)據(jù)img_variable=Variable(img_tensor.unsqueeze(0))# 將圖片輸入網(wǎng)絡(luò)得到預(yù)測類別分值logit=net(img_variable)# 使用本地的 LABELS_PATHwithopen(LABELS_PATH)asf:data=json.load(f).items()classes={int(key):valuefor(key,value)indata}# 使用softmax打分h_x=F.softmax(logit,dim=1).data.squeeze()# 分類分值# 對分類的預(yù)測類別分值排序朽砰,輸出預(yù)測值和在列表中的位置probs,idx=h_x.sort(0,True)# 轉(zhuǎn)換數(shù)據(jù)類型probs=probs.numpy()idx=idx.numpy()# output the predictionforiinrange(0,5):print('{:.3f} -> {}'.format(probs[i],classes[idx[i]]))# generate class activation mapping for the top1 predictionCAMs=returnCAM(features_blobs[0],weight_softmax,[idx[0]])# render the CAM and outputprint('output CAM.jpg for the top1 prediction: %s'%classes[idx[0]])img=cv2.imread('2103.jpg')height,width,_=img.shapeheatmap=cv2.applyColorMap(cv2.resize(CAMs[0],(width,height)),cv2.COLORMAP_JET)result=heatmap*0.3+img*0.5cv2.imwrite('CAM.jpg',result)