1. 程式人生 > >caffe for windows的matlab介面(三):權重和特徵圖的視覺化

caffe for windows的matlab介面(三):權重和特徵圖的視覺化

模型讀取
讀取bvlc_reference_caffenet 的模型結構以及訓練好的引數,注意此處的模型結構為deploy,而非train時候的。

caffe.set_mode_cpu();%設定CPU模式  
model = '../../models/bvlc_reference_caffenet/deploy.prototxt';%模型  
weights = '../../models/bvlc_reference_caffenet/bvlc_reference_caffenet.caffemodel';%引數  
net=caffe.Net(model,'test');%測試  
net.copy_from
(weights); %得到訓練好的權重引數 net %顯示net的結構

執行我們就可以看到模型的結構了:
這裡寫圖片描述
這裡額外提一下,net 通過”.” 能顯示的東西,除了上面輸出的這些properties外,還有Net.m中定義的函式:

function self = Net(varargin)  
function layer = layers(self, layer_name)  
function blob = blobs(self, blob_name)  
function blob = params(self, layer_name, blob_index)  
function
forward_prefilled(self)
function backward_prefilled(self) function res = forward(self, input_data) function res = backward(self, output_diff) function copy_from(self, weights_file) function reshape(self) function save(self, weights_file)

輸入資料整理
下面這張圖片藏在E:\CaffeDev\caffe-master\examples\images\cat.jpg
①先把均值讀進來

d = load('../+caffe/imagenet/ilsvrc_2012_mean.mat');  
mean_data = d.mean_data;

②讀取圖片

im = imread('../../examples/images/cat.jpg');%讀取圖片  
IMAGE_DIM = 256;%影象將要resize的大小,建議resize為影象最小的那個維度  
CROPPED_DIM = 227;%待會需要把一張圖片crops成十塊,最終softmax求出每一塊可能的標籤 

設定在輸入網路之前需要將圖片resize的大小,一般我們會取圖片長寬最小的那個,其次需要設定的是輸入網路的圖片的大小,注意與deploy.prototxt的輸入一致,比如

name: "CaffeNet"  
layer {  
  name: "data"  
  type: "Input"  
  top: "data"  
  input_param { shape: { dim: 10 dim: 3 dim: 227 dim: 227 } }  
}  

這裡關注一下input_param,代表一次輸入十張圖片,每張圖片三通道,每張圖片大小是227*227。此外注意一下,在opencv中,彩色影象按照BGR儲存,而matlab中讀取的順序一般是RGB。所以對這隻貓需要進行如下處理:

im_data = im(:, :, [3, 2, 1]);  %matlab按照RGB讀取圖片,opencv是BGR,所以需要轉換順序為opencv處理格式  
im_data = permute(im_data, [2, 1, 3]);  % 原始影象m*n*channels,現在permute為n*m*channels大小  
im_data = single(im_data);  % 強制轉換資料為single型別  
im_data = imresize(im_data, [IMAGE_DIM IMAGE_DIM], 'bilinear');  % 線性插值resize影象

注意一下你在訓練的train.prototxt中的預處理部分

transform_param {  
   mirror: true  
   crop_size: 227  
   mean_file: "data/ilsvrc12/imagenet_mean.binaryproto"  
 }  

這裡最後一行代表進行了零均值處理,關於這一部分引數,可以看我前面用classification,exe手寫識別時候提到的那個部落格,介紹瞭如何看這一部分進行了怎樣的預處理。

先零均值化一下,然後按照deploy和train的prototxt,將這隻貓crop(分成)十塊,採用的是classification.demo的分割方法,分別取貓的上下左右四個角以及中心的大小為deploy中提到的227*227大小。這是五個,然後再對圖片翻轉180°;合起來就是代表這隻貓的十張圖片:

im_data = im_data - mean_data;  % 零均值  
crops_data = zeros(CROPPED_DIM, CROPPED_DIM, 3, 10, 'single');%注意此處是因為prototxt的輸入大小為寬*高*通道數*10  
indices = [0 IMAGE_DIM-CROPPED_DIM] + 1;%獲得十塊每一塊大小與原始影象大小差距,便於crops  
%下面就是如何將一張圖片crops成十塊  
n = 1;  
%此處兩個for迴圈並非是1:indices,而是第一次取indices(1),然後是indices(2),每一層迴圈兩次  
%分別讀取圖片四個角大小為CROPPED_DIM*CROPPED_DIM的圖片  
for i = indices  
    for j = indices  
        crops_data(:, :, :, n) = im_data(i:i+CROPPED_DIM-1, j:j+CROPPED_DIM-1, :);%產生四個角的cropdata,1 2 3 4  
        crops_data(:, :, :, n+5) = crops_data(end:-1:1, :, :, n);%翻轉180°來一次,產生四個角的翻轉cropdata,6 7 8 9  
        n = n + 1;  
    end  
end  
center = floor(indices(2) / 2) + 1;  
%以中心為crop_data左上角頂點,讀取CROPPED_DIM*CROPPED_DIM的塊  
crops_data(:,:,:,5) = ...  
    im_data(center:center+CROPPED_DIM-1,center:center+CROPPED_DIM-1,:);  
%與for迴圈裡面一樣,翻轉180°,繞左邊界翻轉  
crops_data(:,:,:,10) = crops_data(end:-1:1, :, :, 5);  

視覺化提取出的這十個影象:

clear  
clc  
close all  
%caffenet的解讀:http://www.2cto.com/kf/201606/515700.html  
%% 設定網路  
addpath('..') %加入+caffe路徑  
caffe.set_mode_cpu();%設定CPU模式  
model = '../../models/bvlc_reference_caffenet/deploy.prototxt';%模型  
weights = '../../models/bvlc_reference_caffenet/bvlc_reference_caffenet.caffemodel';%引數  
net=caffe.Net(model,'test');%測試  
net.copy_from(weights); %得到訓練好的權重引數  
net %顯示net的結構  

%% 預處理  
d = load('../+caffe/imagenet/ilsvrc_2012_mean.mat');  
mean_data = d.mean_data;%讀取均值  
im = imread('../../examples/images/cat.jpg');%讀取圖片  
IMAGE_DIM = 256;%影象將要resize的大小,建議resize為影象最小的那個維度  
CROPPED_DIM = 227;%最終需要把一張圖片crops成十塊,最終求出每一塊可能的標籤  
im_data=im;  
im_data = im(:, :, [3, 2, 1]);  %matlab按照RGB讀取圖片,opencv是BGR,所以需要轉換順序為opencv處理格式  
im_data = permute(im_data, [2, 1, 3]);  % 原始影象m*n*channels,現在permute為n*m*channels大小  
im_data = single(im_data);  % 強制轉換資料為single型別  
im_data = imresize(im_data, [IMAGE_DIM IMAGE_DIM], 'bilinear');  % 線性插值resize影象  
% im_data = im_data - mean_data;  % 零均值  
crops_data = zeros(CROPPED_DIM, CROPPED_DIM, 3, 10, 'single');%注意此處是因為prototxt的輸入大小為寬*高*通道數*10  
indices = [0 IMAGE_DIM-CROPPED_DIM] + 1;%獲得十塊每一塊大小與原始影象大小差距,便於crops  
%下面就是如何將一張圖片crops成十塊  
n = 1;  
%此處兩個for迴圈並非是1:indices,而是第一次取indices(1),然後是indices(2),每一層迴圈兩次  
%分別讀取圖片四個角大小為CROPPED_DIM*CROPPED_DIM的圖片  
for i = indices  
    for j = indices  
        crops_data(:, :, :, n) = im_data(i:i+CROPPED_DIM-1, j:j+CROPPED_DIM-1, :);%產生四個角的cropdata,1 2 3 4  
        crops_data(:, :, :, n+5) = crops_data(end:-1:1, :, :, n);%翻轉180°來一次,產生四個角的翻轉cropdata,6 7 8 9  
        n = n + 1;  
    end  
end  
center = floor(indices(2) / 2) + 1;  
%以中心為crop_data左上角頂點,讀取CROPPED_DIM*CROPPED_DIM的塊  
crops_data(:,:,:,5) = ...  
    im_data(center:center+CROPPED_DIM-1,center:center+CROPPED_DIM-1,:);  
%與for迴圈裡面一樣,翻轉180°,繞左邊界翻轉  
crops_data(:,:,:,10) = crops_data(end:-1:1, :, :, 5);  


cat_map=zeros(CROPPED_DIM*2,CROPPED_DIM*5,3);%兩行五列展示  
cat_num=0;  
for i=0:1  
    for j=0:4  
        cat_num=cat_num+1  
        cat_map(CROPPED_DIM*i+1:(i+1)*CROPPED_DIM,CROPPED_DIM*j+1:(j+1)*CROPPED_DIM,:)=crops_data(:,:,:,cat_num);  
    end  
end  
imshow(uint8(cat_map))  

這裡寫圖片描述
看著比較怪的原因在於,中間經過了matlab處理方式到opencv處理方式的轉變,但是我們依舊用matlab輸出了。

前向計算

res=net.forward({crops_data});  
prob=res{1};  
prob1 = mean(prob, 2);  
[~, maxlabel] = max(prob1); 

這一步完畢以後,整個網路就會充滿引數了,權重,特徵圖均生成完畢,接下來視覺化它們。

特徵圖視覺化
特徵圖提取方法
說一下步驟,首先利用net 中的blob_name函式取出與deploy.prototxt對應的 top 名字,顯示一下看看
這裡寫圖片描述
然後利用blob呼叫get_data()函式獲取我們需要的特徵圖的值。注意,每一層的特徵圖是四維,看看前三層的特徵圖大小:
這裡寫圖片描述
結合deploy中每一層的卷積核大小以及步長,利用 (當前層特徵圖大小 - 卷積核大小) / 步長+1=下一層特徵圖大小,可以推匯出每一個featuremap 的前兩維,第三個維度代表的是卷積核個數,featuremap {2}到featuremap {3}是池化了。第四個維度代表最開始輸入了十張圖

部分視覺化方法

這一部分針對指定的第crop_num張影象在第map_num層進行視覺化。注意,這一部分的視覺化包含池化層等。

function [  ] = feature_partvisual( net,mapnum,crop_num )  
names=net.blob_names;  
featuremap=net.blobs(names{mapnum}).get_data();%獲取指定層的特徵圖  
[m_size,n_size,num,crop]=size(featuremap);%獲取特徵圖大小,長*寬*卷積核個數*通道數  
row=ceil(sqrt(num));%行數  
col=row;%列數  
feature_map=zeros(m_size*row,n_size*col);  
cout_map=1;  
for i=0:row-1  
    for j=0:col-1  
        if cout_map<=num  
            feature_map(i*m_size+1:(i+1)*m_size,j*n_size+1:(j+1)*n_size)=(mapminmax(featuremap(:,:,cout_map,crop_num),0,1)*255)';  
            cout_map=cout_map+1;  
        end  
    end  
end  
imshow(uint8(feature_map))  
str=strcat('feature map num:',num2str(cout_map-1));  
title(str)  
end  

呼叫方法:

mapnum=1;%第幾層的feature☆☆☆☆☆☆☆☆  
crop_num=1;%第幾個crop的特徵圖☆☆☆☆☆☆☆☆  
feature_partvisual( net,mapnum,crop_num )  

中間有個處理細節是歸一化然後乘以255,是避免featuremap的數值過小,或者有負數,導致特徵圖一片漆黑;在下面的權重視覺化方法採取的是另一種處理。

讀者可以更改”☆”標誌的行中的數值去提取不同crop影象的不同層特徵圖。

第一層特徵圖:
這裡寫圖片描述

第二層featuremap:
這裡寫圖片描述

全部視覺化
這一部分視覺化每一張輸入圖片在指定卷積層的特徵圖,按照每一行為儲存圖片的特徵圖為圖例。

function [  ] = feature_fullvisual( net,mapnum )  
names=net.blob_names;  
featuremap=net.blobs(names{mapnum}).get_data();%獲取指定層的特徵圖  
[m_size,n_size,num,crop]=size(featuremap)%獲取特徵圖大小,長*寬*卷積核個數*圖片個數  
row=crop;%行數  
col=num;%列數  
feature_map=zeros(m_size*row,n_size*col);  
for i=0:row-1  
    for j=0:col-1  
        feature_map(i*m_size+1:(i+1)*m_size,j*n_size+1:(j+1)*n_size)=(mapminmax(featuremap(:,:,j+1,i+1),0,1)*255)';  
    end  
end  
figure  
imshow(uint8(feature_map))  
str=strcat('feature map num:',num2str(row*col));  
title(str)  
end  

呼叫方法:

mapnum=2;%第幾層的feature☆☆☆☆☆☆☆☆  
feature_fullvisual( net,mapnum )  

這裡寫圖片描述

**

卷積核可視化

**
權重提取方法
先建議參考多通道卷積的概念:http://blog.csdn.net/u014114990/article/details/51125776,不看也行,注意這句話,每一個通道的卷積核是不一樣的,同一個卷積核只在同一個特徵圖中共享,應該理解的沒錯吧o(╯□╰)o。(後續學習這篇,記得在人臉識別中,每個通道的卷積核是不一樣的,然後進行重疊,即一個相加的過程,最後再加上偏置項。)

通過net 的layer_names 函式能夠獲取deploy.txt 對應的name 的名稱,每一個name的blob對應兩個值,分別是權重和偏置,提取方法如下:

layers=net.layer_names;  
convlayer=[];  
for i=1:length(layers)  
    if strcmp(layers{i}(1:3),'con')%僅僅卷積核能獲取到權重  
        convlayer=[convlayer;layers{i}];  
    end  
end  
w=cell(1,length(convlayer));%儲存權重  
b=cell(1,length(convlayer));%儲存偏置  
for i=1:length(convlayer)  
    w{i}=net.layers(convlayer(i,:)).params(1).get_data();  
    b{i}=net.layers(convlayer(i,:)).params(2).get_data();  
end  

提取完畢以後觀察一下每一層的權重維度,發現也是四維,顯示一下前三個卷積核的維度:

size(w{1})= 11    11     3    96  
size(w{2})= 5     5    48   256  
size(w{3})= 3     3   256   384  

前兩個維度不說了,卷積核的大小,第三個維度代表卷積核的左邊,也就是上一層的特徵圖的個數(對應前面說的每一個通道對應不同卷積核),第四個維度代表每一個通道對應的卷積核個數(也就是卷積核右邊下一層的特徵圖的個數)。

部分視覺化的方法

那麼我們視覺化也是可選的,需要選擇哪一個特徵圖對應的卷積核,視覺化方法如下:

function [  ] = weight_partvisual( net,layer_num ,channels_num )  
layers=net.layer_names;  
convlayer=[];  
for i=1:length(layers)  
    if strcmp(layers{i}(1:3),'con')  
        convlayer=[convlayer;layers{i}];  
    end  
end  
w=net.layers(convlayer(layer_num,:)).params(1).get_data();  
b=net.layers(convlayer(layer_num,:)).params(2).get_data();  
w=w-min(min(min(min(w))));  
w=w/max(max(max(max(w))))*255;  

weight=w(:,:,channels_num,:);%四維,核長*核寬*核左邊輸入*核右邊輸出(核個數)  
[kernel_r,kernel_c,input_num,kernel_num]=size(w);  
map_row=ceil(sqrt(kernel_num));%行數  
map_col=map_row;%列數  
weight_map=zeros(kernel_r*map_row,kernel_c*map_col);  
kernelcout_map=1;  
for i=0:map_row-1  
    for j=0:map_col-1  
        if kernelcout_map<=kernel_num  
            weight_map(i*kernel_r+1+i:(i+1)*kernel_r+i,j*kernel_c+1+j:(j+1)*kernel_c+j)=weight(:,:,:,kernelcout_map);  
            kernelcout_map=kernelcout_map+1;  
        end  
    end  
end  
figure  
imshow(uint8(weight_map))  
str1=strcat('weight num:',num2str(kernelcout_map-1));  
title(str1)  

end  

呼叫方法:

layer_num=1;%想看哪一個卷積核對應的權重☆☆☆☆☆☆☆☆☆☆  
channels_num=1;%想看第幾個通道對應的卷積核  
weight_partvisual( net,layer_num ,channels_num )  

第一個卷積層的卷積核:

第二個卷積層的卷積核:

全部視覺化
將指定卷積層對應的每一個特徵圖的全部卷積核畫出

function [  ] = weight_fullvisual( net,layer_num  )  
layers=net.layer_names;  
convlayer=[];  
for i=1:length(layers)  
    if strcmp(layers{i}(1:3),'con')  
        convlayer=[convlayer;layers{i}];  
    end  
end  

weight=net.layers(convlayer(layer_num,:)).params(1).get_data();%四維,核長*核寬*核左邊輸入*核右邊輸出(核個數)  
b=net.layers(convlayer(layer_num,:)).params(2).get_data();  
weight=weight-min(min(min(min(weight))));  
weight=weight/max(max(max(max(weight))))*255;  

[kernel_r,kernel_c,input_num,kernel_num]=size(weight);  
map_row=input_num;%行數  
map_col=kernel_num;%列數  
weight_map=zeros(kernel_r*map_row,kernel_c*map_col);  
for i=0:map_row-1  
    for j=0:map_col-1  
        weight_map(i*kernel_r+1+i:(i+1)*kernel_r+i,j*kernel_c+1+j:(j+1)*kernel_c+j)=weight(:,:,i+1,j+1);  
    end  
end  
figure  
imshow(uint8(weight_map))  
str1=strcat('weight num:',num2str(map_row*map_col));  
title(str1)  

end  

第一層:

第二層
**

全連線探討:

**
這裡初步探索一下CaffeNet 的最後一個池化層pool5到第一個全連線層fc6的連線,我最開始的理解是直接把pool層所有的單元拉成一個列向量,不過分析以後,感覺應該是類似BP,pool5先被拉成一個一維向量,然後利用權重連線到fc6層的所有單元上,類似二部圖的連線方法。

實驗過程如下:
①首先提取出pool5的特徵圖大小:

>> A=net.blobs('pool5').get_data();  
>> size(A)  
ans =  

     6     6   256    10  

可以發現對於每一個輸入圖片(總共十張)都有256個6*6大小的特徵圖。預先計算一下256*6*6=9216
②然後提取出fc6的特徵圖大小:

>>B=net.blobs('fc6').get_data();  
>> size(B) 
ans =  

        4096          10  

然後發現pool5到fc6的連線並不是簡單的拉成一維向量,而是利用了一個9216*4096的權重去將pool5的特徵對映到fc6的單元中。

③驗證一下是否如所想的對映方法, 只需要看看pool5到fc6的權重大小即可:

>>C=net.layers('fc6').params(1).get_data();  
>> size(C)  

ans =  

        9216        4096  

發現果真如此,所以池化層到全連線層的確是用了一次對映而非簡單的拉成向量。