mini-caffe编译,用BLVC caffe编译的mnist模型进行测试
mini-caffe是最小化Caffe的运行版本,只用来forward,运算效率高、占用小,因此其极其适合用于在线测试。但是,如果你自己实现了非官方caffe的Layer,同样需要在mini-caffe中自己实现对应的计算代码。
这篇文章用VS2015编译mini-caffe项目,记载caffe训练mnist数据生成的模型和deploy.prototxt,对测试图片进行分类。
1、编译准备
下载mini-caffe项目,https://github.com/luoyetx/mini-caffe,解压到自己的存放目录即可。
另外,mini-caffe编译需要protobuf库,同样需要自己下载编译,下载地址https://github.com/google/protobuf。(建议下载release下的)
在我这里,mini-caffe存放地址如下;protobuf解压存放在mini-caffe-master\3rdparty\src\protobuf下。
2、编译protobuf
打开CMake工具,指定source目录为E:/Program Files/Tools/mini-caffe-master/3rdparty/src/protobuf/cmake,
build目录为E:/Program Files/Tools/mini-caffe-master/3rdparty/src/protobuf/cmake/build。
点击Configure,选择编译器Visual Studio 14 2015 Win64,取消勾选protobuf_BUILD_TESTS和protobuf_MSVC_STATIC_RUNTIME,再次点击Configure,再点击Generate,就会生成protobuf.sln解决方案。
同样也可以用cmake命令:
cd 3rdparty/src/protobuf/cmake mkdir build cd build cmake .. -Dprotobuf_BUILD_TESTS=OFF -Dprotobuf_MSVC_STATIC_RUNTIME=OFF -G "Visual Studio 14 2015 Win64"
打开protobuf.sln,编译生成Debug和Release版本,保存在protobuf/CMake/build/Debug和protobuf/CMake/build/Release文件夹下面。
有了这2个版本的库文件,就可以编译mini-caffe了。
3、编译mini-caffe
编译mini-cafe之前,有2个准备步骤。
(1)先要复制依赖文件到指定位置,大致过程为:
拷贝3rdparty\src\protobuf\src下的google文件夹到mini-caffe-master\3rdparty\include\下 (其实只需要.h文件);
拷贝protobuf\cmake\build\Debug\libprotobufd.lib 和 protobuf\cmake\build\Release\libprotobuf.lib到3rdparty\lib\下;
拷贝 protobuf\cmake\build\Release\protoc.exe 到 3rdparty\bin\下;
(2)生成caffe.pb.h和caffe.pb.cc
利用前面复制的操作,执行下面代码即可
"./3rdparty/bin/protoc" -I="./src/proto" --cpp_out="./src/proto" "./src/proto/caffe.proto"
mini-caffe已经做好了上述两个步骤步骤的脚本,可以分别点击mini-caffe-master目录下的两个批处理文件执行,分别为copydeps.bat 和 generatepb.bat。
完成上述两个准备步骤之后,就可以用CMake生成mini-caffe.sln了,命令行或者CMake的窗口工具都可以。(另外,还可以选择是否使用GPU)
mkdir build cd build cmake .. -G "Visual Studio 14 2015 Win64"
同样,编译可以生成Debug和Release两个版本的ceffe.lib和caffe.dll。后面就可以拿这个开始测试了。
4、测试mnist模型
在前面的博客中有用官方的classification.exe实现对mnist手写字符图像的识别(传送门),尽管改写后从篇幅上略有减小,但是整个网络中还有不少的东西可以去掉,例如网络反向计算的数据,在测试中不会用到的计算层等等。相较而言,mini-caffe只包含了forward计算,从网络Net上去掉了不需要的东西,并且在前向计算后清空了中间的所有层数据,极大的减小了空间的消耗,并且计算速度较快。
#include <caffe/caffe.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <chrono>
/*! \brief Timer */
class Timer {
using Clock = std::chrono::high_resolution_clock;
public:
/*! \brief start or restart timer */
inline void Tic() {
start_ = Clock::now();
}
/*! \brief stop timer */
inline void Toc() {
end_ = Clock::now();
}
/*! \brief return time in ms */
inline double Elasped() {
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_ - start_);
return duration.count();
}
private:
Clock::time_point start_, end_;
};
using namespace caffe; // NOLINT(build/namespaces)
using std::string;
static bool PairCompare(const std::pair<float, int>& lhs,
const std::pair<float, int>& rhs) {
return lhs.first > rhs.first;
}
void main()
{
string model_file = R"(E:\ProgramData\caffe-windows\data\mnist\windows\lenet.prototxt)";
string trained_file = R"(E:\ProgramData\caffe-windows\data\mnist\windows\snapshot_lenet_mean\_iter_10000.caffemodel)";
string mean_file = R"(E:\ProgramData\caffe-windows\data\mnist\windows\mean.binaryproto)";
string label_file = R"(E:\ProgramData\caffe-windows\data\mnist\windows\synset_words.txt)";
string file = R"(E:\ProgramData\caffe-windows\data\mnist\windows\3.bmp)";
//////////////////////////////////////////////////////////////////////////
//
// 读取网络Net, 指定数据类型是 float, 后面有关cv::Mat的type应该为 CV_32F
if (caffe::GPUAvailable())
caffe::SetMode(caffe::GPU, 0); // 若有GPU可用,设置为GPU模式
shared_ptr<caffe::Net> net_( new caffe::Net(model_file));
net_->CopyTrainedLayersFrom(trained_file);
//////////////////////////////////////////////////////////////////////////
//
// 标签数据(可不用)
cv::Size input_geometry_;
int num_channels_;
cv::Mat mean_;
std::vector<string> labels_ = { "zero","one","two","three","four","five","six","seven","Eight","Nine" };
//////////////////////////////////////////////////////////////////////////
//
// 输入层,只能通过blob_by_name获取,没有input_blobs()函数
//
shared_ptr<caffe::Blob> input_layer = net_->blob_by_name("data"); // 小写,尽管lenet.prototxt输入层name为"Data"
input_geometry_ = cv::Size(input_layer->width(), input_layer->height());
num_channels_ = input_layer->channels();
std::cout << "input shape: " << input_layer->shape_string() << std::endl;
std::cout << "input size: " << input_geometry_ << std::endl;
std::cout << "input channels: " << num_channels_ << std::endl;
//////////////////////////////////////////////////////////////////////////
//
// 从文件中读取均值图像数据
shared_ptr<Blob> mean_blob = ReadBlobFromFile(mean_file);
std::vector<cv::Mat> channels;
float* data = mean_blob->mutable_cpu_data();
//for (int i = 0; i < num_channels_; ++i) {
// cv::Mat channel(mean_blob->height(), mean_blob->width(), CV_32FC1, data);
// channels.push_back(channel);
// data += mean_blob->height() * mean_blob->width();
//}
//cv::Mat mean;
//cv::merge(channels, mean);
// 代替上面代码程序,这里已经明确是 1*1*28*28 的Blob, 输入Blob的shape一样
cv::Mat mean(mean_blob->height(), mean_blob->width(), CV_32FC1, data);
cv::Scalar channel_mean = cv::mean(mean); // 均值图像, 每个像素为 均值图像的平均亮度值
mean_ = cv::Mat(input_geometry_, mean.type(), channel_mean);
//////////////////////////////////////////////////////////////////////////
//
// 读入图像,并将图像数据写入到网络输入层Blob
cv::Mat img = cv::imread(file, -1);
if (!img.data) {
std::cout << "Unable to decode image.\nQuit." << std::endl;
system("pause");
return;
}
// 明确输入是 单通道数据 1*1*28*28
if (img.channels() == 3 && num_channels_ == 1) cv::cvtColor(img, img, cv::COLOR_BGR2GRAY);
else if (img.channels() == 4 && num_channels_ == 1) cv::cvtColor(img, img, cv::COLOR_BGRA2GRAY);
//else if (img.channels() == 4 && num_channels_ == 3) cv::cvtColor(img, img, cv::COLOR_BGRA2BGR);
//else if (img.channels() == 1 && num_channels_ == 3) cv::cvtColor(img, img, cv::COLOR_GRAY2BGR);
if (img.size() != input_geometry_)
cv::resize(img, img, input_geometry_);
img.convertTo(img, CV_32FC1);
//input_layer->Reshape({ 1,1,28,28 }); // 不需要执行,已经是确定的
float *dataInput = input_layer->mutable_cpu_data();
//img -= mean_; // 去均值
//img.copyTo(cv::Mat(input_geometry_, mean_.type(), dataInput)); // 去均值后图像数据拷贝到 data blob,方法1
//memcpy(dataInput, img.data, sizeof(float)*img.cols*img.rows); // 去均值后图像数据拷贝到 data blob,方法2
// 注释img -= mean_; 这两句代替上面的拷贝
cv::Mat dataImg(input_geometry_, CV_32FC1, dataInput); // 去均值后图像数据拷贝到 data blob,方法3
dataImg = img - mean_;
//////////////////////////////////////////////////////////////////////////
//
// 进行forward之后,仅保留第一层和最后一层的Blob数据, 也就是blob_by_name参数只能为"data"和"prob"
auto t1 = cv::getTickCount();
net_->Forward();
auto t2 = cv::getTickCount();
std::cout << " Forward time: " << (t2 - t1) / cv::getTickFrequency() * 1000 << " ms" << std::endl;
shared_ptr<caffe::Blob> output_layer = net_->blob_by_name("prob"); // InnerProduct 输出
std::cout <<" output layer shape: " << output_layer->shape_string() << std::endl;
const float* begin = output_layer->cpu_data();
const float* end = begin + /*output_layer->channels()*/output_layer->shape(1); // 只有2维,shape为(1,10)
std::vector<float> OutRes = std::vector<float>(begin, end); // channels()同shape(1), 也即N*C*W*H 的C, 尽管没有W和H
//////////////////////////////////////////////////////////////////////////
//
// 输出层的结果进行排序,打印输出
//
int N = 5;
std::vector<std::pair<float, int> > pairs;
for (size_t i = 0; i < OutRes.size(); ++i)
pairs.push_back(std::make_pair(OutRes[i], static_cast<int>(i)));
std::partial_sort(pairs.begin(), pairs.begin() + N, pairs.end(), PairCompare);
std::vector<std::pair<std::string, float> > result;
std::cout << "=========== Prediction ============" << std::endl;
for (int i = 0; i < N; ++i)
{
int idx = pairs[i].second;
//result.push_back(std::make_pair(labels_[idx], OutRes[idx]));
std::cout << std::fixed << OutRes[idx] << " " << labels_[idx]<< std::endl;
}
system("pause");
}
在训练生成模型时指定了均值文件,因此最好在测试时减去均值,使测试结果更加准确。
测试图片(放大后显示),减去均值测试结果,不减均值测试结果,依次如下图所示:
附件:VS2015编译的64位mini-caffe库下载地址http://download.csdn.net/download/wanggao_1990/10000792
5、可能遇到的错误
当使用cudnn版本不同,可能会遇到一些错误,例如使用cudnn6.0时,会提示"caffe cudnnSetConvolution2dDescriptor error: too few arguments in function call ...”的错误,查看cudnn发现需要dataType::type类型的参数。若用cudnn6,需要修改有关所有的调用地方的模板类型,包括头文件和源文件。建议用cudnn5.1。