
Caffe
用 Caffe C++ 接口加载模型并提取中间层特征
面向需要在 C++ 程序里直接调用 Caffe 的开发者,讲清从初始化网络、加载 caffemodel、读取均值、前向传播,到按名字定位 Blob 与 Layer、读取特征图与权重、修改权重并另存模型的完整流程,附可直接编译的最小示例与常见坑。
Caffe 的 Python 接口用起来方便,但当你要把推理嵌进一个已有的 C++ 工程、或者要在不落盘的情况下直接拿到某一层的特征图时,走 C++ 接口是更直接的路子。这篇教程讲的就是这件事:用 Caffe 的 C++ API 加载一个已经训练好的模型,喂入预处理好的数据做前向传播,然后按名字把中间某一层(比如 conv1)的输出取出来,顺带也讲怎么读某一层的权重、怎么改权重、怎么把改过的网络另存成新的 caffemodel。适合已经会用 Caffe 训练模型、现在需要在自己程序里调用它的开发者。文中涉及的版本、编译选项与依赖以 Caffe 项目当前信息为准。
如果你更习惯在命令行或脚本里跑推理,而不是写 C++,可以先看看站内的 Caffe 相关工具页,确认自己要走哪条路径再往下读。
准备工作
在写第一行代码之前,需要把下面几样东西备齐。缺任何一样,后面的代码都跑不起来。
- 可用的 Caffe 库与头文件:源码编译安装或使用预编译包均可,关键是要有
caffe/caffe.hpp以及链接时能找到libcaffe。Caffe 依赖的 BLAS、protobuf、glog、gflags 等也要一并就位。 - 一个 deploy 用的 prototxt:这是推理用的网络定义,不含训练阶段的 data 层与 loss 层,输入形状固定。素材示例用的是 CaffeNet 的
deploy.prototxt。 - 一个训练好的 caffemodel:与上面的 prototxt 结构严格对应,素材示例用的是
bvlc_reference_caffenet.caffemodel。 - 均值文件:通常是
imagenet_mean.binaryproto,用于把输入图像减去数据集均值。如果你的预处理在别处已经做完,这一步可以省,但要注意输入数据的数值范围必须和训练时一致。 - 编译环境:能编译 C++ 的编译器、CMake 或 Makefile,以及 CUDA 工具链(仅当你打算用 GPU 模式)。
关于路径:素材里的示例用的是 Windows 风格的反斜杠路径(如 H:\\Models\\Caffe\\deploy.prototxt)。在 Linux 或 macOS 上要换成对应的正斜杠路径,并且注意 C++ 字符串里的反斜杠本身需要转义,写成双反斜杠。这一点在跨平台迁移代码时最容易出错。
操作步骤
第一步:初始化网络
先引入头文件,然后构造一个 Net 对象。构造时传入 prototxt 路径和阶段(TEST 或 TRAIN),推理场景用 TEST。同时要显式指定运行模式:CPU 还是 GPU。如果选 GPU,还要指定设备号。
#include "caffe/caffe.hpp"
#include <string>
#include <vector>
using namespace caffe;
char* proto = "H:\\Models\\Caffe\\deploy.prototxt";
Phase phase = TEST; // 推理用 TEST,训练用 TRAIN
Caffe::set_mode(Caffe::CPU);
// Caffe::set_mode(Caffe::GPU);
// Caffe::SetDevice(0);
boost::shared_ptr<Net<float>> net(new caffe::Net<float>(proto, phase));
后面所有操作都围绕这个 net 展开。注意 set_mode 是全局状态,一旦设定,之后所有 Blob 的 cpu_data() / gpu_data() 行为都跟着变,所以不要在程序中途随意切换。
第二步:加载已训练好的模型
网络结构有了,权重还是随机的。用 CopyTrainedLayersFrom 把 caffemodel 里的参数按层名拷进网络。层名对不上的部分会被跳过,所以如果发现某层权重没生效,先检查 prototxt 与 caffemodel 的层命名是否一致。
char* model = "H:\\Models\\Caffe\\bvlc_reference_caffenet.caffemodel";
net->CopyTrainedLayersFrom(model);
第三步:读取图像均值
均值文件是二进制 protobuf 格式,需要先读成 BlobProto,再转成 Blob<float>,最后拿到 CPU 上的数据指针。以 CaffeNet 为例,均值 Blob 的形状是 1×3×256×256,元素总数 196608。
char* mean_file = "H:\\Models\\Caffe\\imagenet_mean.binaryproto";
Blob<float> image_mean;
BlobProto blob_proto;
const float* mean_ptr;
unsigned int num_pixel;
bool succeed = ReadProtoFromBinaryFile(mean_file, &blob_proto);
if (succeed) {
image_mean.FromProto(blob_proto);
num_pixel = image_mean.count(); // NCHW = 1x3x256x256 = 196608
mean_ptr = (const float*)image_mean.cpu_data();
}
拿到 mean_ptr 之后,减均值的动作由调用方自己完成——Caffe 的 C++ 接口不会替你处理。减完均值的数据要符合网络输入层的长宽和 batch size。
第四步:前向传播
把预处理好的数据写进网络的第一个输入 Blob,然后调用 ForwardPrefilled()。写入方式随 CPU / GPU 模式不同:CPU 用 memcpy 到 mutable_cpu_data(),GPU 用 cudaMemcpy 到 mutable_gpu_data()。
void caffe_forward(boost::shared_ptr<Net<float>>& net, float* data_ptr) {
Blob<float>* input_blobs = net->input_blobs()[0];
switch (Caffe::mode()) {
case Caffe::CPU:
memcpy(input_blobs->mutable_cpu_data(), data_ptr,
sizeof(float) * input_blobs->count());
break;
case Caffe::GPU:
cudaMemcpy(input_blobs->mutable_gpu_data(), data_ptr,
sizeof(float) * input_blobs->count(),
cudaMemcpyHostToDevice);
break;
default:
LOG(FATAL) << "Unknown Caffe mode.";
}
net->ForwardPrefilled();
}
这里 data_ptr 指向的数据必须是已经处理好、去均值、形状与网络输入一致的连续内存。长度用 input_blobs->count() 来校验,不要凭感觉写死。
第五步:按名字找到 Blob 的索引
Caffe 的 Net 暴露的 Blob 是每一层的输出,也就是 Feature Maps。想取某一层输出,先遍历 net->blob_names() 找到名字对应的下标。找不到时用 LOG(FATAL) 直接终止,避免拿到越界下标。
unsigned int get_blob_index(boost::shared_ptr<Net<float>>& net,
char* query_blob_name) {
std::string str_query(query_blob_name);
vector<string> const& blob_names = net->blob_names();
for (unsigned int i = 0; i != blob_names.size(); ++i) {
if (str_query == blob_names[i]) {
return i;
}
}
LOG(FATAL) << "Unknown blob name: " << str_query;
}
第六步:读取指定 Feature 层的数据
拿到下标后,从 net->blobs() 取出对应 Blob,再用 cpu_data() 拿只读指针。CaffeNet 的 deploy 网络共有 15 个 Blob,从 data 一直到 prob,常见的名字有 data、conv1、pool1、norm1、fc6、prob 等。以 conv1 为例,输出形状是 10×96×55×55(batch 为 10 时)。
char* query_blob_name = "conv1";
unsigned int blob_id = get_blob_index(net, query_blob_name);
boost::shared_ptr<Blob<float>> blob = net->blobs()[blob_id];
unsigned int num_data = blob->count(); // NCHW = 10x96x55x55
const float* blob_ptr = (const float*)blob->cpu_data();
注意 blob_ptr 指向的是网络内部的内存,下一次前向传播就会被覆盖。要保留数据必须自己拷贝出来。
第七步:按名字找到 Layer 的索引
Layer 和 Blob 是两套东西:Layer 是网络里的层,CaffeNet 一共 23 层;Blob 是层与层之间流动的数据。要读权重就得走 Layer 这条路。同样用名字遍历 net->layer_names()。
unsigned int get_layer_index(boost::shared_ptr<Net<float>>& net,
char* query_layer_name) {
std::string str_query(query_layer_name);
vector<string> const& layer_names = net->layer_names();
for (unsigned int i = 0; i != layer_names.size(); ++i) {
if (str_query == layer_names[i]) {
return i;
}
}
LOG(FATAL) << "Unknown layer name: " << str_query;
}
第八步:读取指定 Layer 的权重
Layer 的 blobs() 里装的是该层的参数,卷积层和全连接层通常是两个:blobs[0] 是权重,blobs[1] 是偏置。池化、激活这类无参数层返回空 vector,所以取之前要先判断 blobs.size() > 0。
char* query_layer_name = "conv1";
const float *weight_ptr, *bias_ptr;
unsigned int layer_id = get_layer_index(net, query_layer_name);
boost::shared_ptr<Layer<float>> layer = net->layers()[layer_id];
std::vector<boost::shared_ptr<Blob<float>>> blobs = layer->blobs();
if (blobs.size() > 0) {
weight_ptr = (const float*)blobs[0]->cpu_data();
bias_ptr = (const float*)blobs[1]->cpu_data();
}
如果是在训练模式下想读梯度,做法完全一样,唯一区别是把 cpu_data 换成 cpu_diff。
第九步:修改某层的权重
写权重需要可写指针,所以用 mutable_cpu_data() 或 mutable_gpu_data()。写入前用 CHECK 校验数据量等于该 Blob 的元素数,然后用 caffe_copy 完成拷贝。
const float* data_ptr; // 源数据
float* weight_ptr = NULL; // 目标:网络中某层权重
unsigned int data_size; // 待写入的数据量
char* layer_name = "conv1";
unsigned int layer_id = get_layer_index(net, layer_name);
boost::shared_ptr<Blob<float>> blob = net->layers()[layer_id]->blobs()[0];
CHECK(data_size == blob->count());
switch (Caffe::mode()) {
case Caffe::CPU:
weight_ptr = blob->mutable_cpu_data();
break;
case Caffe::GPU:
weight_ptr = blob->mutable_gpu_data();
break;
default:
LOG(FATAL) << "Unknown Caffe mode";
}
caffe_copy(blob->count(), data_ptr, weight_ptr);
训练模式下手动改梯度同理,把 mutable_cpu_data 换成 mutable_cpu_diff、mutable_gpu_data 换成 mutable_gpu_diff 即可。
第十步:保存新的模型
改完权重之后,用 ToProto 把网络序列化成 NetParameter,再写成二进制文件。第二个参数传 false 表示不写入共享数据。
char* weights_file = "bvlc_reference_caffenet_new.caffemodel";
NetParameter net_param;
net->ToProto(&net_param, false);
WriteProtoToBinaryFile(net_param, weights_file);
一个完整示例
下面把上面的步骤串成一个可编译的最小程序:加载 CaffeNet,读均值,构造一份全零输入(仅用于演示流程,实际使用要换成真实预处理数据),前向传播,取出 conv1 的输出并打印元素个数,最后把网络另存一份。
#include "caffe/caffe.hpp"
#include <cstring>
#include <string>
#include <vector>
using namespace caffe;
unsigned int get_blob_index(boost::shared_ptr<Net<float>>& net,
const std::string& name) {
vector<string> const& names = net->blob_names();
for (unsigned int i = 0; i != names.size(); ++i)
if (name == names[i]) return i;
LOG(FATAL) << "Unknown blob name: " << name;
}
int main() {
const char* proto = "deploy.prototxt";
const char* model = "bvlc_reference_caffenet.caffemodel";
const char* mean_file = "imagenet_mean.binaryproto";
Caffe::set_mode(Caffe::CPU);
boost::shared_ptr<Net<float>> net(new Net<float>(proto, TEST));
net->CopyTrainedLayersFrom(model);
Blob<float> image_mean;
BlobProto blob_proto;
if (ReadProtoFromBinaryFile(mean_file, &blob_proto)) {
image_mean.FromProto(blob_proto);
}
Blob<float>* input = net->input_blobs()[0];
unsigned int n = input->count();
std::vector<float> data(n, 0.0f); // 演示用,实际应填入预处理后的数据
memcpy(input->mutable_cpu_data(), &data[0], sizeof(float) * n);
net->ForwardPrefilled();
unsigned int id = get_blob_index(net, "conv1");
boost::shared_ptr<Blob<float>> blob = net->blobs()[id];
LOG(INFO) << "conv1 count = " << blob->count();
NetParameter net_param;
net->ToProto(&net_param, false);
WriteProtoToBinaryFile(net_param, "caffenet_new.caffemodel");
return 0;
}
编译时把 Caffe 的头文件目录加进 include 路径,链接 libcaffe 及其依赖。如果链接阶段报 protobuf 或 glog 的符号缺失,说明依赖库没链全,按 Caffe 的构建配置补齐即可。
批量提取特征的做法
如果目标不是单张图,而是给整个数据集抽特征,思路是把上面的流程包一层循环,但有三处需要额外准备。
- 生成文件列表:格式与训练时用的列表类似,每行一个图像,写全路径、一个空格、标签;没有标签就置 0。
- 改写 prototxt:以 train_val 或 deploy 的 prototxt 为基础,把输入层改成
image_data层,并在末尾加上prob和argmax,这样输出里既有概率,也有 Top-1 / Top-5 的预测标签。 - 运行程序:按指定参数跑完后会生成若干个二进制文件,可以再用 MATLAB 等工具读入做后续分析。
批量场景下要特别注意 batch size 与列表长度的关系:最后一批不足一个 batch 时,要么补齐,要么单独处理,否则会读到无效数据。
注意事项
- Blob 与 Layer 别混用:
net->blobs()拿到的是各层输出的 Feature Maps,net->layers()[i]->blobs()拿到的是该层的权重与偏置。两者名字空间不同,索引也不能互换。 - 数据指针的生命周期:
cpu_data()返回的是网络内部内存,下一次ForwardPrefilled()会覆盖。需要留存就立即拷贝。 - 模式是全局的:
Caffe::set_mode影响整个进程,切换模式后之前拿到的 GPU 指针不能继续当 CPU 指针用。 - 名字找不到会直接终止:示例里的查找函数用
LOG(FATAL)处理未命中,生产代码里可以改成返回错误码,避免整个进程退出。 - 层名必须与 caffemodel 对应:
CopyTrainedLayersFrom按名字匹配,prototxt 里改了层名而 caffemodel 没跟着改,权重就会静默丢失。 - 均值与输入形状要匹配:减均值前确认输入的长宽、通道顺序(NCHW)与训练时一致,否则结果会明显偏差。
- 路径转义:Windows 路径写进 C++ 字符串要双写反斜杠;跨平台迁移时统一改成正斜杠更省事。
- 版本与依赖:Caffe 的编译选项、CUDA 版本兼容性、预编译包可用性都会随时间变化,具体以项目官网当前信息为准。