1. 程式人生 > >caffe視覺化(權重、特徵圖和loss曲線)

caffe視覺化(權重、特徵圖和loss曲線)

由於要用到matlab介面來讀取網路,故在開始介紹caffe視覺化前,先看一下D:\caffe\caffe-master\matlab\+caffe\Net.m檔案裡定義的載入網路等函式,Net.m檔案大家可以自行閱讀,以下就摘抄幾個我們要用到的函式:

(1)Net(varargin)函式

   function self = Net(varargin)
      % decide whether to construct a net from model_file or handle
      if ~(nargin == 1 && isstruct(varargin{1}))
        % construct a net from model_file
        self = caffe.get_net(varargin{:});
        return
      end
      % construct a net from handle
      hNet_net = varargin{1};
      CHECK(is_valid_handle(hNet_net), 'invalid Net handle');
      
      % setup self handle and attributes
      self.hNet_self = hNet_net;
      self.attributes = caffe_('net_get_attr', self.hNet_self);
      
      % setup layer_vec
      self.layer_vec = caffe.Layer.empty();
      for n = 1:length(self.attributes.hLayer_layers)
        self.layer_vec(n) = caffe.Layer(self.attributes.hLayer_layers(n));
      end
      
      % setup blob_vec
      self.blob_vec = caffe.Blob.empty();
      for n = 1:length(self.attributes.hBlob_blobs);
        self.blob_vec(n) = caffe.Blob(self.attributes.hBlob_blobs(n));
      end
      
      % setup input and output blob and their names
      % note: add 1 to indices as matlab is 1-indexed while C++ is 0-indexed
      self.inputs = ...
        self.attributes.blob_names(self.attributes.input_blob_indices + 1);
      self.outputs = ...
        self.attributes.blob_names(self.attributes.output_blob_indices + 1);
      
      % create map objects to map from name to layers and blobs
      self.name2layer_index = containers.Map(self.attributes.layer_names, ...
        1:length(self.attributes.layer_names));
      self.name2blob_index = containers.Map(self.attributes.blob_names, ...
        1:length(self.attributes.blob_names));
      
      % expose layer_names and blob_names for public read access
      self.layer_names = self.attributes.layer_names;
      self.blob_names = self.attributes.blob_names;
            
      % expose bottom_id_vecs and top_id_vecs for public read access
      self.attributes.bottom_id_vecs = cellfun(@(x) x+1, self.attributes.bottom_id_vecs, 'UniformOutput', false);
      self.bottom_id_vecs = self.attributes.bottom_id_vecs;
      self.attributes.top_id_vecs = cellfun(@(x) x+1, self.attributes.top_id_vecs, 'UniformOutput', false);
      self.top_id_vecs = self.attributes.top_id_vecs;
   end

該函式實現載入網路模型和網路權重資訊,其中的輸入引數varargin需要有:網路模型檔案(如deploy.prototxt/train.prototxt等)、網路權重檔案(訓練時可忽略)和網路模式(TRAIN/TEST)。

(2)layers(self, layer_name)函式

   function layer = layers(self, layer_name)
      CHECK(ischar(layer_name), 'layer_name must be a string');
      layer = self.layer_vec(self.name2layer_index(layer_name));
   end

其中的引數self代表Net,即如果例項化了一個網路,例如net = caffe.Net(...),net就可以呼叫這些帶有self的函式,如net.layers(...)。

引數layer_name是該函式真正的輸入引數,代表網路某一層的名字,即網路模型檔案中的name關鍵字後的引數,如下面某一層中的name: "conv1",則要獲取該層,即可用net.layers('conv1')來實現:

layer {
  name: "conv1"
  type: "Convolution"
  bottom: "data"
  top: "conv1"
  convolution_param {
    num_output: 96
    kernel_size: 11
    stride: 4
  }
}

matlab介面獲取到的layers,裡面包含params資訊,即該層的權重和偏置資訊,如下圖:

我們可以通過以下程式碼來獲取該層的權重/偏置資料:

data=net.layers('conv1').params(1).get_data(); %權重
data=net.layers('conv1').params(2).get_data(); %偏置

(3)blobs(self, blob_name)函式

   function blob = blobs(self, blob_name)
      CHECK(ischar(blob_name), 'blob_name must be a string');
      blob = self.blob_vec(self.name2blob_index(blob_name));
   end

該函式實現根據blob名字(blob_name)來獲取相應的blob(blob中儲存著輸入輸出資料),例如:

layer {
  name: "conv1"
  type: "Convolution"
  bottom: "data"
  top: "conv1"
  convolution_param {
    num_output: 96
    kernel_size: 11
    stride: 4
  }
}
layer {
  name: "relu1"
  type: "ReLU"
  bottom: "conv1"
  top: "conv1"
}

通過呼叫net.blobs('data').get_data()可以獲取conv1層的輸入,通過net.blobs('conv1').get_data()可以獲取relu1層的輸出(也即通過conv1層(包含啟用函式)後的特徵圖資料,可用於視覺化特徵圖)。

注:這裡無法獲取經過啟用函式ReLU之前的conv1層的輸出資料,因為conv1層和relu1層採用的同一個名字的輸出blob(同一名字預設是同一個blob,所以經過ReLU後會覆蓋conv1層產生的輸出blob;ReLU這種輸入輸出同名的方式也叫in-place,可以節省內參/視訊記憶體)

(4)params(self, layer_name, blob_index)函式

   function blob = params(self, layer_name, blob_index)
      CHECK(ischar(layer_name), 'layer_name must be a string');
      CHECK(isscalar(blob_index), 'blob_index must be a scalar');
      blob = self.layer_vec(self.name2layer_index(layer_name)).params(blob_index);
   end

該函式也能實現獲取某一層的權重和偏置資料,只是需要兩個輸入引數layer_name和blob_index,其中blob_index = 1表示權重,blob_index = 2表示偏置。例如net.params('conv1',1).get_data()可以獲取conv1層的權重。

(5)forward(self, input_data)函式

   function res = forward(self, input_data)
      CHECK(iscell(input_data), 'input_data must be a cell array');
      CHECK(length(input_data) == length(self.inputs), ...
        'input data cell length must match input blob number');
      % copy data to input blobs
      for n = 1:length(self.inputs)
        if isempty(input_data{n})
          continue;
        end
        self.blobs(self.inputs{n}).set_data(input_data{n});
      end
      self.forward_prefilled();
      % retrieve data from output blobs
      res = cell(length(self.outputs), 1);
      for n = 1:length(self.outputs)
        res{n} = self.blobs(self.outputs{n}).get_data();
      end
   end

該函式用於實現整個網路的前向傳播,只需要輸入input_data(即data層的輸入資料即可);該函式輸出的是輸出層的blob資料。

注:這裡的input_data需要是cell array格式(即元胞陣列)。

1.視覺化權重

網路的權重資料可以通過matlab介面或python介面(python介面可以參見我寫的另一篇部落格)來讀取,以下網路權重視覺化過程採用matlab介面來實現。

主函式程式碼如下:

clear;
clc;
close all;
addpath('D:\caffe\caffe-master\Build\x64\Release\matcaffe'); %matcaffe路徑(windows下,如果為linux,則修改為根目錄下的matlab資料夾路徑)
caffe.set_mode_cpu(); 
%以test模式載入網路和權重
net=caffe.Net('D:\caffe\caffe-master\models\bvlc_reference_caffenet\deploy.prototxt',...
    'D:\caffe\caffe-master\models\bvlc_reference_caffenet\bvlc_reference_caffenet.caffemodel','test');
%視覺化conv1的權重
conv=net.params('conv1',1);  %1為權重,2為偏置(按名字索引)
blob=conv.get_data(); %得到權重資料
visualize(blob,1); %呼叫視覺化函式

其中的visualize()函式如下:

%引數w:權重資訊
%引數s:黑邊大小
function [] = visualize(w,s)
width = size(w,1);
height = size(w,2);
gw = width+s;  %拓展寬,以便視覺化更美觀(其實就是每張影象之間新增黑邊隔開而已)
gh = height+s;
%歸一化
w = w - min(min(min(min(w))));
w = w / max(max(max(max(w)))) * 255;
w = uint8(w);

W=zeros(gh * size(w,3),gw * size(w,4));
for u = 1:size(w,3)
    for v = 1:size(w,4)
        %caffe中的matlab介面的blob是按(width,height,channel,num)順序儲存的
        W(gh * (u - 1) + (1:height), gw * (v - 1) + (1:width)) = w(:,:,u,v)'; %需要轉置為正常影象的位置索引
    end
end
W=uint8(W);
figure;
imshow(W);
end

拿conv1層舉例,其權重視覺化結果如下(由於conv1層有96組卷積核(每組有3個卷積核),故圖中的行數為3行,列數為96列,詳見models\bvlc_reference_caffenet\deploy.prototxt檔案):

2.視覺化特徵圖

主函式程式碼如下:

clear;
clc;
close all;
addpath('D:\caffe\caffe-master\Build\x64\Release\matcaffe'); %matcaffe路徑(windows下,如果為linux,則修改為根目錄下的matlab資料夾路徑)
caffe.set_mode_cpu(); 
%以test模式載入網路和權重
net=caffe.Net('D:\caffe\caffe-master\models\bvlc_reference_caffenet\deploy.prototxt',...
    'D:\caffe\caffe-master\models\bvlc_reference_caffenet\bvlc_reference_caffenet.caffemodel','test');
image=imread('D:\caffe\caffe-master\examples\images\cat.jpg'); %載入輸入影象
figure;
imshow(image);
title('原始影象');

load('D:\caffe\caffe-master\matlab\+caffe\imagenet\ilsvrc_2012_mean.mat'); %載入均值檔案mean_data(均值檔案大小為256*256*3)
%%
%以下將輸入的影象格式改為matcaffe的輸入格式,即(width,height,channel,num)
%先進性RGB轉為BGR(適合opencv的格式)
image=image(:,:,[3,2,1]);
%再將width和heigth互換
image=permute(image,[2,1,3]);
image=single(image); %轉化為單精度,以便減去有小數的均值
%將圖片縮放到均值檔案的大小
image=imresize(image,[size(mean_data,1),size(mean_data,2)],'bilinear'); %採用雙線性插值
image=image-mean_data; %減去均值
image=imresize(image,[227,227],'bilinear'); %網路輸入為10*3*227*227(詳見deploy.prototxt)
%由於每次輸入網路的num=10,故複製9遍image當做最終的輸入(在視覺化的時候只取num中第一維,其餘9維均一樣)
kimage=cat(4,image,image,image,image,image); %按照num這一維度合併
pimage=cat(4,kimage,kimage);
input={pimage}; %matcaffe中的forward函式的輸入為cell array(元胞陣列),故轉化為cell array

%%
%前向傳播
score=net.forward(input);

%%
%獲取最終的識別結果
score=score{1,1}; %提取出元胞陣列中是資料為一般的矩陣
score=mean(score,2); %由於前面輸入的num=10是10張一模一樣的圖,故score的大小為1000*10,且每列都是一樣的,故直接按列求均值
[~,maxlabel]=max(score); %得到的maxlabel=282,表示是貓

%%
%以下視覺化特徵圖
data=net.blobs('conv1').get_data();
visualize_feature(data,1);

注:主函式中的去均值操作和是否或者輸入資料,都需要根據自己的需求進行修改(即輸入資料部分的程式碼需要自行修改);獲取最終識別結果的程式碼可有可無,也需要根據自己的需求進行修改。

其中的visualize_feature()函式如下:

%引數w:權重資訊
%引數s:黑邊大小
function visualize_feature(w,s)
width = size(w,1);
height = size(w,2);
gw = width+s;  %拓展寬,以便視覺化更美觀(其實就是每張影象之間新增黑邊隔開而已)
gh = height+s;

channel=size(w,3);
cq=ceil(sqrt(channel));
W=zeros(gh*cq,gw*cq);
for u=1:cq
    for v=1:cq
        temp=zeros(height,width);
        if(((u-1)*cq+v)<=channel)
            temp=w(:,:,(u-1)*cq+v,1)';
            temp=temp-min(min(temp));
            temp=temp/max(max(temp))*255;
        end
        W(gh*(u-1)+(1:height),gw*(v-1)+(1:width))=temp;
    end   
end
W=uint8(W);
figure;
imshow(W);
end

視覺化結果舉例如下(輸入資料是examples\images\cat.jpg;視覺化輸出資料conv1 blob,即relu1層的輸出特徵圖):

3.視覺化loss曲線

caffe無法直接實時繪製loss曲線(需要進一步用python介面或matlab介面程式設計實現實時繪製),但可以在訓練完後根據儲存的日誌檔案來繪製loss曲線。這裡只介紹後者,實時繪製待以後有時間了研究下。

caffe的tools\extra資料夾裡有自帶的繪製一系列曲線的檔案plot_training_log.py.example,但該工具只能一幅影象上繪製一條曲線,無法將訓練和測試過程中的兩條loss曲線繪製在同一幅圖上,這樣就導致無法很直觀判斷訓練過程是否出現過擬合和欠擬合,所以這次介紹利用matlab介面來在同一幅影象上繪製兩條loss曲線。

由於這段時間放假回家了,但自己的筆記本上沒裝linux系統,所以採用windows下的caffe版本進行講解,當然會嵌入linux下的程式碼(只是這些程式碼沒有經過測試,大家要是發現有錯誤或執行報錯的話,及時告知哈,等回去了,我再用實驗室的機子測試一下)。

這裡拿caffe中的mnist例子進行說明如何視覺化loss曲線,首先需要執行訓練過程來得到訓練日誌,大家根據自己編譯的caffe位置進行修改下面執行訓練程式碼中的路徑(mnist資料集下載,已經轉為lmdb格式):

d:
cd ./caffe/caffe-master
D:\caffe\caffe-master\Build\x64\Release\caffe.exe train --solver=./examples/mnist/lenet_solver.prototxt >mnist.log 2>&1 &

如下圖:

程式碼中的>是windows下的重定向(是1>的簡略版,功能是一樣的),2>&1表示將標準錯誤重定向到標準輸出,最後的&表示訓練過程在後臺執行,所以如上圖所示是不會顯示訓練過程中的各種輸出的。

將任務放入後臺有如下好處:

(1)有意或無意關閉終端時,不會停止訓練(對linux而言是這樣的,但windows下仍舊會停止訓練)

(2)可以用一個終端幹更多的事(windows仍舊需要等訓練結束了,才能接著輸入其他命令)

(3)可以在其他終端(遠端)檢視訓練進度(實時檢視),而不必要回到當前的終端

上面的第三條優點的實現需要在另外的終端輸入以下程式碼檢視訓練過程(windows下和linux下一樣的命令):

tail -f mnist.log

如下圖所示(圖上的訓練內容是實時顯示的):

如果顯示tail不是內部或外部命令,也不是可執行的程式或批處理檔案。則需要下載tail.exe,然後解壓將裡面的tail.exe放在C:\Windows\System32下即可。

如果是linux的話,在cd到相應目錄下後,只需要輸入以下程式碼:

./build/tools/caffe train --solver=./examples/mnist/lenet_solver.prototxt 2>&1 mnist.log &

注:日誌檔案儲存在當前終端上顯示的目錄,比如上圖中的D:\caffe\caffe-master下。

在得到訓練日誌檔案後,我們利用matlab來實現loss曲線的視覺化,程式碼如下:

clear;
clc;
close all;
trainlog = 'D:\caffe\caffe-master\mnist.log'; %載入日誌檔案
train_interval = 100; %對應於solver.prototxt中的display
test_interval = 500; %對應於solver.prototxt中的test_interval
%%
%提取loss資料
%先提取訓練過程的loss
[~,soutput] = dos(['type ',trainlog,' | grep ','"Train net output #0"',' | awk ','"{print $11}"']);

train_loss = str2num(soutput); %字串轉化為數字
nn = 1:length(train_loss);
train_index = (nn - 1) * train_interval;
%再提取測試過程的loss
[~,soutput] = dos(['type ',trainlog,' | grep ','"Test net output #1"',' | awk ','"{print $11}"']);
test_loss = str2num(soutput);
mm = 1:length(test_loss);
test_index = (mm - 1) * test_interval;

%%
%繪製loss曲線
figure;
plot(train_index,train_loss); %繪製訓練過程隨著迭代次數的loss曲線
hold on
plot(test_index,test_loss); %繪製測試過程隨迭代次數的loss曲線

grid on
xlabel('迭代次數');
ylabel('loss');
title('loss曲線');
legend('訓練loss','測試loss');

程式碼中的dos()是matlab呼叫dos命令的函式,裡面的['type ',trainlog,' | grep ','"Test net output #1"',' | awk ','"{print $11}"']是字串拼接操作,注意在windows下使用awk的命令為awk "{print $11}",不是linux下的awk '{print $11}',這個區別一定要注意(血一樣的教訓,調這個不是bug的bug花了好長時間)。

如果出現awk或grep不是內部或外部命令,也不是可執行的程式或批處理檔案。請下載awk,解壓後將bin資料夾下的awk.exe放入C:\Windows\System32目錄下;請下載grep,並按照,按照路徑隨意(比如我是預設安裝的,即C:\Program Files (x86)\GnuWin32路徑),安裝完畢後,將C:\Program Files (x86)\GnuWin32\bin路徑(修改自己的對應路徑)加入系統的path,如下圖所示:

然後重啟電腦即可。

當然在linux下無需安裝上述軟體,可以直接執行,但需要修改一下下面兩句程式碼:

(1)

[~,soutput] = dos(['type ',trainlog,' | grep ','"Train net output #0"',' | awk ','"{print $11}"']);

改為

[~,soutput] = dos(['cat ',trainlog,' | grep ','"Train net output #0"',' | awk ',''{print $11}'']);

(2)

[~,soutput] = dos(['type ',trainlog,' | grep ','"Test net output #1"',' | awk ','"{print $11}"']);

改為

[~,soutput] = dos(['cat ',trainlog,' | grep ','"Test net output #1"',' | awk ',''{print $11}'']);

其中的$11中的數字11是由於loss= 2.3165中的2.3165(我們要提取的資料)是該行的第11個字串(字串間用空格隔開),舉個例子如下(大家可以自行數一下2.3165在那行的第幾個字串處):

I0811 20:57:26.300038  8380 solver.cpp:404]     Test net output #0: accuracy = 0.1116
I0811 20:57:26.300038  8380 solver.cpp:404]     Test net output #1: loss = 2.35696 (* 1 = 2.35696 loss)
I0811 20:57:26.300038  8380 solver.cpp:228] Iteration 0, loss = 2.3165
I0811 20:57:26.300038  8380 solver.cpp:244]     Train net output #0: loss = 2.3165 (* 1 = 2.3165 loss)

執行matlab,結果如下圖:

注:如需轉載,請註明本部落格連結。