很多人使用 Caffe 配置神经网络的时候,习惯于撰写两个配置文件,一个叫 train_val.prototxt,在训练的时候使用;一个叫 deploy.prototxt,在预测的时候使用。这两个文件的本质区别,往往在输入、输出层不同。train_val.prototxt 里包含 train/test 的输入数据和标签,但出于效率考虑,train/test 都是分 batch 进行的,而输出的往往是 acc/loss;deploy.prototxt 里只包含 test 的输入,而且一般是每次输入一个数据(没有标签),输出的也不是 acc/loss,而是预测值(Top N 类别或者预测概率)。可以把 deploy.prototxt 看成可以往线上部署的网络配置文件,来一个用户请求,执行 network 的 forward,预测返回给用户结果。
这样做没什么不可以,而且很多开源的例子都是这么做的。但实际操作中,有一个很麻烦的地方是,当你在频繁调整模型的时候,每次修改隐层都要同时修改两个 .prototxt 让人很烦恼。Caffe 的配置文件不像 Keras 那样,每层就是简单的一行代码,而是一个 Protobuf 的 txt message,有很多行,这样电脑的一屏显示不全,就需要花精力去仔细 diff 两个文件。
其实我们有更好的办法,使用 Caffe 的 proto 协议实现 All in one network。那就是充分利用 NetStateRule 这个结构,结合 phase 和 stage/not_stage,实现不同场合下 layer 的过滤。
message NetStateRule {
// Set phase to require the NetState have a particular phase (TRAIN or TEST)
// to meet this rule.
optional Phase phase = 1;
// Set the minimum and/or maximum levels in which the layer should be used.
// Leave undefined to meet the rule regardless of level.
optional int32 min_level = 2;
optional int32 max_level = 3;
// Customizable sets of stages to include or exclude.
// The net must have ALL of the specified stages and NONE of the specified
// "not_stage"s to meet the rule.
// (Use multiple NetStateRules to specify conjunctions of stages.)
repeated string stage = 4;
repeated string not_stage = 5;
}
以 Caffe 里的 example/minist/lenet_train_test.prototxt 为例 ,那怎么把它改成 all in one 的 prototxt 呢?
name: "LeNet"
layer {
name: "mnist"
type: "Data"
top: "data"
top: "label"
include {
phase: TRAIN
}
transform_param {
scale: 0.00390625
}
data_param {
source: "examples/mnist/mnist_train_lmdb"
batch_size: 64
backend: LMDB
}
}
layer {
name: "mnist"
type: "Data"
top: "data"
top: "label"
include {
phase: TEST
}
transform_param {
scale: 0.00390625
}
data_param {
source: "examples/mnist/mnist_test_lmdb"
batch_size: 100
backend: LMDB
}
}
首先,我们要明确解决的是 TEST phase 的冲突(验证集和测试集的 input/output 不同),不用去管 TRAIN phase。而为了解决 TEST phase 的冲突,就需要通过为 NetStateRule 增加参数来实现。min_level/max_level 和 stage/not_stage 都可以做这个事情,但我习惯用 stage,因为文字看起来比数字更直观一些。所以我会在原来的 train_val.prototxt 里再增加一个 TEST 输入层,通过 stage 区分不同的应用场景,如下所示:
layer {
name: "mnist"
type: "Data"
top: "data"
top: "label"
include {
phase: TEST
not_stage: "predict" # 在 predict 时过滤掉这一层
}
transform_param {
scale: 0.00390625
}
data_param {
source: "examples/mnist/mnist_test_lmdb"
batch_size: 100
backend: LMDB
}
}
# 增加 deploy 的输入层
layer {
name: "data"
type: "Input"
top: "data"
input_param { shape: { dim: 1 dim: 1 dim: 28 dim: 28 } }
include {
phase: TEST
stage: "predict" # 在 predict 时加上这一层
}
}
在 caffe.bin train 时,由于 solver.prototxt 没有提供特殊的参数,所以只包含 batch_size 100 的 TEST 输入层;在预测的时候,设置 stage='predict' 参数(设置方式下文有介绍),网络的输入层就变成了 dim: 1 的 TEST 输入层了。
同理,对输出层也是一样,在 loss layer 加上 exclude stage: "predict" 的参数,预测时就无需提供 label 和计算 loss 了,如下所示:
layer {
name: "accuracy"
type: "Accuracy"
bottom: "ip2"
bottom: "label"
top: "accuracy"
include { #
phase: TEST #
not_stage: "predict" # 在 predict 时过滤掉 accuracy 层
} #
}
layer {
name: "loss"
type: "SoftmaxWithLoss"
bottom: "ip2"
bottom: "label"
top: "loss"
exclude { # 注意是 exclude
phase: TEST #
stage: "predict" # 在 predict 时过滤掉 loss 层
} #
}
这样,你就能得到一个 all in one 的网络配置 lenet_train_val_deploy.prototxt,可以统一用它进行训练和预测,修改隐层再也不用拷贝来拷贝去了。其实使用 NetStateRule 可以进行各种组合,其它的参数组合也能实现 all in one 的网络设置,但我上面介绍的这种配置方法有个好处是完全不用修改原来的 solver.prototxt。也就是 default 走 non-predict,显式走 predict。
那怎样显式提供 stage='predict' 参数呢?在 caffe.bin 命令行可以使用:
$ caffe.bin test --stage="predict" --model="train_val_deploy.prototxt" \
--weights="iter_N.caffemodel"
当然,这时候输入层可能要换成其它的类型,不能是 Input 类型,不然 caffe 没法读取数据。使用 Input 类型时,就得用 Python/C++ 来加载数据。使用 stage="predict" 初始化 Python 和 C++ 的方法如下:
Python:
net = caffe.Net("train_val_deploy.prototxt", caffe.TEST, stages=['predict'],
weights="iter_N.caffemodel")
C++:
caffe::vector<caffe::string> stages;
stages.push_back("predict");
caffe::Net *net = new caffe::Net("train_val_deploy.prototxt", caffe::TEST, 0, &stages);