caffe之網路權重視覺化(C++實現),分析網路功能 caffe之網路權重視覺化(C++實現)
caffe之網路權重視覺化(C++實現)
2017年05月05日 11:28:12 真小假 閱讀數:3083<span class="tags-box artic-tag-box"> <span class="label">標籤:</span> <a data-track-click="{"mod":"popu_626","con":"CNN"}" class="tag-link" href="http://so.csdn.net/so/search/s.do?q=CNN&t=blog" target="_blank">CNN </a><a data-track-click="{"mod":"popu_626","con":"c++"}" class="tag-link" href="http://so.csdn.net/so/search/s.do?q=c++&t=blog" target="_blank">c++ </a><a data-track-click="{"mod":"popu_626","con":"caffe"}" class="tag-link" href="http://so.csdn.net/so/search/s.do?q=caffe&t=blog" target="_blank">caffe </a><a data-track-click="{"mod":"popu_626","con":"opencv"}" class="tag-link" href="http://so.csdn.net/so/search/s.do?q=opencv&t=blog" target="_blank">opencv </a> <span class="article_info_click">更多</span></span> <div class="tags-box space"> <span class="label">個人分類:</span> <a class="tag-link" href="https://blog.csdn.net/zxj942405301/article/category/6900848" target="_blank">Caffe </a> </div> </div> <div class="operating"> </div> </div> </div> </div> <article class="baidu_pl"> <div id="article_content" class="article_content clearfix csdn-tracking-statistics" data-pid="blog" data-mod="popu_307" data-dsm="post"> <div class="article-copyright"> 版權宣告:本文為博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/zxj942405301/article/details/71194359 </div> <link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/ck_htmledit_views-f76675cdea.css"> <div class="htmledit_views">
在上一篇部落格中,已經介紹了caffe用訓練好的model對一副測試圖片進行分類的c++實現過程。
今天,我們來看對一個訓練好的model,用c++進行model的網路權值的視覺化,比如看看網路中的第一個卷積層conv1、第一個卷積層pool1到底學到的是什麼特徵。
正如上一篇部落格結尾處說的,網路權值的視覺化,是不需要網路的前向傳播就能看到的,因為學到的引數已經事實存在了,只要載入這個model,把相關的引數提取出來就ok啦,所以,為了簡單起見,這裡就不在classification.cpp裡去新增程式碼實現,而是在一個新的cpp檔案裡實現這個功能(當然,最好是可以將權值視覺化功能封裝在classification.cpp這個檔案裡,這樣子的話,該檔案就提供了分類、視覺化等功能,同理,還可以將提取某一層特徵向量,特徵圖視覺化也整合在這個檔案裡,這樣的話對以後的需求就能方便的使用了,比如講第一個全連線層的特徵向量提取出來,再送進更精準的分類器(如SVM)去分類,而只把cnn當成特徵提取器);
不失一般性,我們借用caffe中已經訓練好的模型caffenet(2012年的alexnet的修改版),對該model進行視覺化
// File: test.cpp
#include <caffe/caffe.hpp> //為了能正常編譯,需要引入caffe的標頭檔案 #include <opencv2/core/core.hpp> //這三行是為了引用opencv #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> #include <algorithm> #include <iosfwd> #include <memory> //使用c++智慧指標,必須包含該目錄 #include <string> #include <utility> #include <vector>
using namespace caffe; // NOLINT(build/namespaces)
using namespace cv;
using namespace std;
int main(){
//初始化一個網路,網路結構從caffenet_deploy.prototxt檔案中讀取,TEST表示是測試階段
Net<float> net(“caffenet_deploy.prototxt”,TEST);
net.CopyTrainedLayersFrom(“caffenet.caffemodel”); //讀取已經訓練好的model的引數
vector<shared_ptr<Blob<float> > > params=net.params(); //獲取網路的各個層學習到的引數(權值+偏置)
//打印出該model學到的各層的引數的維度資訊
cout<<“各層引數的維度資訊為:\n”;
for(int i=0;i<params.size();++i)
cout<<params[i]->shape_string()<<endl;
return 0;
}
此時,我們需要編譯該檔案,假設該檔案取名test.cpp,則執行以後命令編譯該檔案
g++ -o netapp.bin test.cpp `pkg-config --libs --cflags opencv` -I ~/caffe/include/ -I ~/caffe/src/ -I ~/caffe/build/src/ -I /usr/local/cuda-8.0/include/ -L ~/caffe/build/lib/ -lprotobuf -lcaffe -lglog -lboost_system
解釋下這條指令,前面幾個是g++編譯cpp檔案的命令,`pkg-config --libs --cflags` 是引用opencv庫,前三個
-I(大寫的i)表示指定三個包含目錄(因為會引用很多caffe裡的檔案),第四個-I,是指定用cuda的包含目錄,-L表示該檔案需要引用的庫目錄,而接下來的四個-l 則表示具體連結的四個動態庫了。(如果你沒有GPU,只要用cpu來執行的話,則刪除第四個-I,以及在命令中,加一條 -D CPU_ONLY就可以正常編譯了)
好了,此時已經編譯成功,生成了netapp.bin可執行檔案,我們再把caffenet的deploy.prototxt和caffenet.model檔案放在這個目錄下,執行 ./netapp.bin,打印出一系列blog後,會輸出以下資訊:
96 3 11 11 (34848)
96 (96)
256 48 5 5 (307200)
256 (256)
384 256 3 3 (884736)
384 (384)
384 192 3 3 (663552)
384 (384)
256 192 3 3 (442368)
256 (256)
4096 9216 (37748736)
4096 (4096)
4096 4096 (16777216)
4096 (4096)
1000 4096 (4096000)
1000 (1000)
我們都知道alexnet模型的網路結構,由資料層、5個卷積層+池化層+ReLU層交叉、3個全連線層組成,而這個網路需要學習的引數(權重+偏置),則只有5個卷積層+3個全連線層(池化層和Relu層是不用學習權重引數的),從列印的資訊來看,分別是第一個卷積層權重引數的維度,第一個卷積層偏置引數的維度;第一個卷積層權重引數的維度,第一個卷積層偏置引數的維度;.......以此類推,剛好16行,沒層輸出2行,所以一共8層(5卷積+3全連線)。
所以,我們可以對某一個層的權重進行可視化了,繼續往test.cpp檔案中新增程式碼
//對第一個卷積層進行視覺化,第一個卷積層"conv1"的維度資訊是96*3*11*11,即96個卷積核,每個卷積核是3通道的,每個卷積核尺寸為11*11
//故我們可以認為,該卷積層有96個圖,每個圖是11*11的三通道BGR影象
int ii=0; //我們提前第1層的引數,此時為conv1層
int width=params[ii]->shape(2); //寬度,第一個卷積層為11
int height=params[ii]->shape(3); //高度,第一個卷積層為11
int num=params[ii]->shape(0); //卷積核的個數,第一個卷積層為96
//我們將num個圖,放在同一張大圖上進行顯示,此時用OpenCV進行視覺化,宣告一個大尺寸的圖片,使之能容納所有的卷積核圖
int imgHeight=(int)(1+sqrt(num))*height; //大圖的尺寸
int imgWidth=(int)(1+sqrt(num))*width;
Mat img1(imgHeight,imgWidth,CV_8UC3,Scalar(0,0,0));
//同時,我們注意到各層的權值,是一個可正可負的實數,而在OpenCV裡的一般圖片,每個畫素的值在0~255之間
//因為我們還需要對權值進行歸一化到0~255才能正常顯示
float maxValue=-1000,minValue=10000;
const float* tmpValue=params[ii]->cpu_data(); //獲取該層的引數,實際上是一個一維陣列
for(int i=0;i<params[ii]->count();i++){ //求出最大最小值
maxValue=std::max(maxValue,tmpValue[i]);
minValue=std::min(minValue,tmpValue[i]);
}
//對最終顯示的大尺寸圖片,進行逐個畫素賦值
int kk=0; //此時在畫第kk個卷積核
for(int y=0;y<imgHeight;y+=height){
for(int x=0;x<imgWidth;x+=width){
if(kk>=num)
continue;
Mat roi=img1(Rect(x,y,width,height));
for(int i=0;i<height;i++){
for(int j=0;j<width;j++){
for(int k=0;k<3;k++){
float value=params[ii]->data_at(kk,k,i,j);
roi.at<Vec3b>(i,j)[k]=(value-minValue)/(maxValue-minValue)255; //歸一化到0~255
}
}
}
++kk;
}
}
resize(img1,img1,Size(500,500)); //將顯示的大圖,調整為500500尺寸
imshow(“conv1”,img1); //顯示
waitKey(0);
此時,第一個卷積層的96個權重圖如上所示,可以看出,這個跟用python或者matlab介面視覺化的權重是一樣的。