
Caffe
用 Caffe 编写 prototxt:数据层、视觉层与激活层配置
Caffe 的模型全部由 prototxt 描述,能否熟练使用它,关键就在于会不会写层配置。本文从数据层入手,逐类讲解 Data、MemoryData、HDF5Data、ImageData、WindowData 的必填与可选参数,再讲 Convolution、Pooling、LRN、im2col 等视觉层,以及 Sigmoid、ReLU、TanH、Power、BNLL 等激活层和 SoftmaxWithLoss、InnerProduct 等常用层,并给出一个可跑通的最小示例。
Caffe 用一份纯文本的模型描述文件(prototxt)来定义整个网络:模型由若干层(layer)组成,每一层又由一组参数构成,所有可用参数都定义在 caffe.proto 里。层与层之间的数据以 Blob 的形式流动,每一层用 bottom 接收输入、用 top 输出结果。也就是说,想用 Caffe 训练或推理,第一步不是写代码,而是把 prototxt 写对。本文面向已经装好 Caffe、准备自己搭网络或改别人网络的读者,按数据层、视觉层、激活层、其它常用层的顺序,把每一类层的必填参数、可选参数、默认值和典型写法讲清楚。如果你更习惯用图形化的方式核对张量形状与卷积输出尺寸,可以配合 Caffe 相关工具页一起看,但配置本身仍然要落在 prototxt 文本上。
准备工作
在动手写 prototxt 之前,需要确认几件事已经就位。
- Caffe 已编译可用。 训练入口是
caffe train,推理入口是caffe test,两者都依赖编译出的可执行文件与链接库。具体编译方式与依赖版本请以 Caffe 官方当前信息为准。 - 一份 solver 配置文件。 层里的
lr_mult只是系数,真正的学习率来自 solver.prototxt 中的base_lr,最终学习率等于lr_mult乘以base_lr。所以写层参数时,心里要有一份对应的 solver。 - 数据已经准备好。 常见做法是把图片转成 LMDB 或 LevelDB;小规模试验也可以直接用图片列表文件,或者用 HDF5 存特征。数据放在哪个目录、列表文件怎么写,决定了数据层里
source填什么。 - 均值文件(可选)。 如果要做减均值,需要提前生成
mean.binaryproto,并在transform_param里用mean_file指向它。 - 一个文本编辑器。 prototxt 是纯文本,缩进不影响语义,但建议保持统一缩进,方便排查括号不匹配的问题。
另外要建立一个基本认识:训练阶段和测试阶段的网络结构往往不同(比如训练时要做随机裁剪、测试时做中心裁剪),这靠层里的 include 参数区分。没有写 include 的层,在训练模型和测试模型里都会存在。
操作步骤
第一步:理解层的通用字段
任何一层都共享同一组外层字段,先把它们记住,后面看具体层就不会乱。
name:层名称,可以随意取,但同一份 prototxt 内建议唯一,便于看日志。type:层类型,决定这一层做什么运算,也决定它下面挂哪个*_param块。bottom:输入 Blob 名称。可以没有,也可以有多个。top:输出 Blob 名称。可以没有,也可以有多个。只有 top 没有 bottom,说明这层只有输出没有输入;反之亦然。include:指定该层属于哪个阶段,常见写法是include { phase: TRAIN }或phase: TEST。param:按顺序为权值和偏置设置lr_mult。写两个param块时,第一个对应权值,第二个对应偏置;通常偏置的学习率系数取权值的两倍。
在数据层里还有一条约定:至少要有一个名为 data 的 top;如果有第二个 top,一般命名为 label。这种 (data, label) 配对是分类模型所必需的。
第二步:写数据层
数据层是模型的最底层,既是入口,也承担预处理。所有数据层共有的预处理参数都写在 transform_param 里:
transform_param {
scale: 0.00390625 # 即 1/255,把 0-255 归一化到 0-1
mean_file: "examples/cifar10/mean.binaryproto"
mirror: 1 # 1 开启镜像,0 关闭,也可写 true / false
crop_size: 227 # 训练阶段随机裁剪,测试阶段从中间裁剪
}
下面按数据来源分别说明。
1. 数据来自数据库(LevelDB / LMDB)
层类型写 Data。必填参数两个:source 是包含数据库的目录名,例如 examples/mnist/mnist_train_lmdb;batch_size 是每次处理的数据个数。可选参数有 rand_skip(开始时跳过若干条输入,对异步 SGD 有用)和 backend(选 LevelDB 还是 LMDB,默认是 LevelDB)。
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
}
}
2. 数据来自内存
层类型写 MemoryData,四个必填参数:batch_size、channels、height、width。
layer {
name: "memory_data"
type: "MemoryData"
top: "data"
top: "label"
memory_data_param {
batch_size: 2
channels: 1
height: 100
width: 100
}
transform_param {
scale: 0.0078125
mean_file: "mean.proto"
mirror: false
}
}
3. 数据来自 HDF5
层类型写 HDF5Data,必填 source(读取的文件名)与 batch_size。
layer {
name: "data"
type: "HDF5Data"
top: "data"
top: "label"
hdf5_data_param {
source: "examples/hdf5_classification/data/train.txt"
batch_size: 10
}
}
4. 数据来自图片
层类型写 ImageData。必填 source(一个文本文件,每行给出一个图片文件名和标签)与 batch_size。可选参数包括 rand_skip、shuffle(是否随机打乱,默认 false)、new_height 与 new_width(设置后会对图片做 resize)。
layer {
name: "data"
type: "ImageData"
top: "data"
top: "label"
transform_param {
mirror: false
crop_size: 227
mean_file: "data/ilsvrc12/imagenet_mean.binaryproto"
}
image_data_param {
source: "examples/_temp/file_list.txt"
batch_size: 50
new_height: 256
new_width: 256
}
}
5. 数据来自 WindowData
层类型写 WindowData,必填 source 与 batch_size,其余是检测任务常用的阈值与裁剪方式参数。
layer {
name: "data"
type: "WindowData"
top: "data"
top: "label"
include { phase: TRAIN }
transform_param {
mirror: true
crop_size: 227
mean_file: "data/ilsvrc12/imagenet_mean.binaryproto"
}
window_data_param {
source: "examples/finetune_pascal_detection/window_file_2007_trainval.txt"
batch_size: 128
fg_threshold: 0.5
bg_threshold: 0.5
fg_fraction: 0.25
context_pad: 16
crop_mode: "warp"
}
}
第三步:写视觉层
视觉层包括 Convolution、Pooling、LRN、im2col 等,是卷积网络的主体。
Convolution 卷积层
必填参数是 num_output(卷积核个数)和 kernel_size(卷积核大小;长宽不等时用 kernel_h 与 kernel_w 分别设定)。其余常用参数:
stride:步长,默认 1,也可用stride_h/stride_w。pad:边缘扩充,默认 0。扩充是上下左右对称的,卷积核为 5×5 时设pad: 2,宽高各扩 4 个像素,输出特征图尺寸不变。也可用pad_h/pad_w。weight_filler:权值初始化,默认constant(全 0),常用xavier,也可设gaussian。bias_filler:偏置初始化,一般设constant(全 0)。bias_term:是否启用偏置,默认 true。group:分组,默认 1。大于 1 时把卷积连接限制在子集内;按通道分组时,第 i 个输出分组只与第 i 个输入分组相连。
输出尺寸按下面两式计算,其中 c1 就是 num_output:
w1 = (w0 + 2*pad - kernel_size) / stride + 1
h1 = (h0 + 2*pad - kernel_size) / stride + 1
由此可以推出两条常用经验:stride 为 1 时前后两次卷积存在重叠;pad 取 (kernel_size-1)/2 时,运算后宽高不变。
layer {
name: "conv1"
type: "Convolution"
bottom: "data"
top: "conv1"
param { lr_mult: 1 }
param { lr_mult: 2 }
convolution_param {
num_output: 20
kernel_size: 5
stride: 1
weight_filler { type: "xavier" }
bias_filler { type: "constant" }
}
}
Pooling 池化层
必填 kernel_size(也可用 kernel_h / kernel_w)。可选参数:pool 指定池化方法,默认 MAX,可选 MAX、AVE、STOCHASTIC;pad 与卷积层含义相同,默认 0;stride 默认 1,通常设为 2 表示不重叠。
layer {
name: "pool1"
type: "Pooling"
bottom: "conv1"
top: "pool1"
pooling_param {
pool: MAX
kernel_size: 3
stride: 2
}
}
池化的计算方式与卷积基本一致,区别是通道数 c 保持不变:
w1 = (w0 + 2*pad - kernel_size) / stride + 1
h1 = (h0 + 2*pad - kernel_size) / stride + 1
例如 100×100 的特征图,stride 为 2 时池化后变成 50×50。
LRN 局部响应归一化层
对一个输入的局部区域做归一化,达到「侧抑制」效果,AlexNet 与 GoogLeNet 中都用到了它。层类型写 LRN,参数全部可选:
local_size:默认 5。跨通道 LRN 时表示参与求和的通道数;通道内 LRN 时表示求和的正方形区域边长。alpha:默认 1。beta:默认 5。norm_region:默认ACROSS_CHANNELS,表示在相邻通道间求和归一化;另一个取值WITHIN_CHANNEL表示在单个通道内部的特定区域求和。它与local_size的含义相对应。
归一化公式为:每个输入除以 (1 + (alpha/n) * Σx_i²)^(beta),得到输出。
layers {
name: "norm1"
type: LRN
bottom: "pool1"
top: "norm1"
lrn_param {
local_size: 5
alpha: 0.0001
beta: 0.75
}
}
im2col 层
它的作用是把一个大矩阵重叠地划分成多个子矩阵,每个子矩阵序列化成向量,最终得到另一个矩阵。Caffe 中的卷积运算就是先做 im2col,再做内积运算,这样比原始卷积操作更快。这一层通常由框架内部使用,手工配置网络时一般不需要显式写出。
第四步:写激活层
激活层对输入逐元素做函数变换,从 bottom 取一个 Blob,运算后从 top 输出一个 Blob。它不改变数据大小,输入输出都是 n×c×(h*w)。
Sigmoid:没有额外参数,层类型写 Sigmoid。
layer {
name: "encode1neuron"
bottom: "encode1"
top: "encode1neuron"
type: "Sigmoid"
}
ReLU:目前使用最多的激活函数,收敛更快且效果相当。标准形式为 max(x, 0)。可选参数 negative_slope 默认 0;设置后负数不再直接置 0,而是乘以该系数(即 Leaky-ReLU)。ReLU 层支持 in-place 计算,bottom 与 top 同名即可省去一份内存。
layer {
name: "relu1"
type: "ReLU"
bottom: "pool1"
top: "pool1"
}
TanH:双曲正切变换,无额外参数。
layer {
name: "layer"
bottom: "in"
top: "out"
type: "TanH"
}
Absolute Value:逐元素取绝对值,层类型 AbsVal。
layer {
name: "layer"
bottom: "in"
top: "out"
type: "AbsVal"
}
Power:逐元素幂运算,公式为 (shift + scale*x)^(power)。三个可选参数 power、scale、shift 的默认值分别是 1、1、0。
layer {
name: "layer"
bottom: "in"
top: "out"
type: "Power"
power_param {
power: 2
scale: 1
shift: 0
}
}
BNLL:binomial normal log likelihood 的简称,函数为 log(1 + exp(x))。
layer {
name: "layer"
bottom: "in"
top: "out"
type: "BNLL"
}
第五步:写 SoftmaxWithLoss 与 InnerProduct
Softmax 与 SoftmaxWithLoss 的区别:Softmax 是分类器,输出各类别的概率似然;SoftmaxWithLoss 在此基础上再算损失。两者都没有参数,只是层类型不同。之所以拆成两层,是为了灵活:如果最终只要概率值,用 Softmax 就够;如果已经通过别的方式拿到概率、只想做最大似然估计,那就只需要 SoftmaxWithLoss。
Softmax 的概率公式为 P_j = e^(o_j) / Σ e^(o_k);SoftmaxWithLoss 的损失为 L = -Σ y_j log p_j。
layer {
name: "loss"
type: "SoftmaxWithLoss"
bottom: "ip1"
bottom: "label"
top: "loss"
}
layers {
bottom: "cls3_fc"
top: "prob"
name: "prob"
type: "Softmax"
}
InnerProduct 全连接层:把输入当作一个向量,输出也是一个简单向量,即把输入的 width 和 height 都变为 1。输入为 n×c0×(h*w),输出为 n×c1×1×1。它本质上也是一种卷积层,只是卷积核大小与原数据大小一致,因此参数写法与卷积层基本相同,同样用 param 块设置 lr_mult,第一个对应权值、第二个对应偏置。
一个完整示例
下面把前面几类层串起来,构成一个能跑通的最小分类网络。数据用 LMDB,输入 28×28 单通道,经过一次卷积、一次池化、一次 ReLU,再全连接到 10 类,最后接 SoftmaxWithLoss。
name: "tiny_cls"
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
}
}
layer {
name: "conv1"
type: "Convolution"
bottom: "data"
top: "conv1"
param { lr_mult: 1 }
param { lr_mult: 2 }
convolution_param {
num_output: 20
kernel_size: 5
stride: 1
pad: 2
weight_filler { type: "xavier" }
bias_filler { type: "constant" }
}
}
layer {
name: "relu1"
type: "ReLU"
bottom: "conv1"
top: "conv1"
}
layer {
name: "pool1"
type: "Pooling"
bottom: "conv1"
top: "pool1"
pooling_param {
pool: MAX
kernel_size: 2
stride: 2
}
}
layer {
name: "ip1"
type: "InnerProduct"
bottom: "pool1"
top: "ip1"
param { lr_mult: 1 }
param { lr_mult: 2 }
inner_product_param {
num_output: 10
weight_filler { type: "xavier" }
bias_filler { type: "constant" }
}
}
layer {
name: "loss"
type: "SoftmaxWithLoss"
bottom: "ip1"
bottom: "label"
top: "loss"
}
配套的 solver 里需要给出 base_lr,层中的 lr_mult 才会生效。训练时执行:
caffe train --solver=solver.prototxt
如果要从已有权重继续训练,加上 --weights 指向 caffemodel 即可。推理或评估时执行:
caffe test --model=train_val.prototxt --weights=tiny_cls_iter_10000.caffemodel
注意示例中两个数据层同名但 include 不同,一个属于 TRAIN 阶段、一个属于 TEST 阶段,这样同一份 prototxt 可以同时被训练和测试流程使用。实际文件名、迭代次数与权重路径请按自己的目录调整。
注意事项
- 默认值容易踩坑。
weight_filler默认是constant,也就是全 0 初始化,直接拿来训练通常学不动,卷积层一般显式写xavier。backend默认是 LevelDB,如果你的数据是 LMDB 却没写backend: LMDB,会读不到数据。 - 学习率是乘积关系。 层里的
lr_mult只是系数,最终学习率等于它乘以 solver 中的base_lr。改学习率时先确认改的是哪一处。 - 输出尺寸要自己算。 卷积和池化的输出尺寸都按 (w0 + 2*pad - kernel_size)/stride + 1 计算,池化时通道数不变。写完全连接层之前,务必确认上一层输出的实际维度,否则 InnerProduct 的输入维度会对不上。
- in-place 计算要同名。 ReLU 支持 in-place,前提是 bottom 与 top 写成同一个名字;写成不同名字会多占一份内存,但语义上仍然正确。
- include 决定层的归属。 不写
include的层在训练和测试模型里都存在。数据层通常需要写两份、分别标注 TRAIN 与 TEST,否则测试阶段会复用训练数据。 - 数据层至少要有 data。 分类模型还需要第二个 top 作为 label,命名习惯上就叫
label。 - 预处理参数写在 transform_param。 归一化(
scale)、减均值(mean_file)、镜像(mirror)、裁剪(crop_size)都在这里设置,不要写到data_param里。 - 版本与可用性以官方为准。 各层支持的参数集合、默认值以及编译依赖会随版本变化,本文涉及的参数含义与默认值请以 Caffe 官方当前信息为准。