caffe之网络权重可视化(C++实现)
在上一篇博客中,已经介绍了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);
![](https://i-blog.csdnimg.cn/blog_migrate/0f4c76d35a2b9fa37f96d4dcd88ac256.jpeg)
此时,第一个卷积层的96个权重图如上所示,可以看出,这个跟用python或者matlab接口可视化的权重是一样的。