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