AB
AiBoss站
教程

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 官方当前信息为准。