Caffe 把「模型」写成文本:一份 prototxt 描述每一层怎么连,一份 solver 描述怎么训练。
然后一句 caffe train --solver=... 就跑起来了,不用写一行网络代码。
实测真拿花括号配对把它切开:五层,按 blob 名字连起来,网络 64 到 32 到 10 共 2410 个参数。
solver 里配 step 策略,学习率在第 400 和第 800 轮各减半一次,从 0.1 到 0.025;
800 轮损失从 0.0277 降到 0.0061。
上一节的 Pylearn2 把训练参数写进配置,这一节的 Caffe 走得更远:连网络结构本身也写成文件。 这个做法当年极其流行,因为它是「部署友好」的:一份 prototxt 加一个权重文件,发给谁都能跑, 不需要你的训练代码。 这也是它 2018 年停止维护后,思路却活到今天的原因。
一、模型是文本
一份 prototxt 长这样(截了几层,完整的五层):
layer {
name: "fc1"
type: "InnerProduct"
bottom: "data"
top: "fc1"
inner_product_param { num_output: 32 }
}
五个字段:名字、类型、输入叫谁、输出叫谁、这一层自己的参数。
整个网络就是一堆这样的块串起来。
关键在 bottom 和 top:它们是数据流的名字,不写形状、不写顺序,只写名字。
所以层与层的连接是「按名字对上」的,改一层只要改名字,不用重排整个网络。
二、blob 名字就是数据流
这样设计带来一个很简洁的模型:网络是一张按名字连起来的有向图。 解析出来的五层:
| 层名 | 类型 | 输入 | 输出 | 输出维数 |
|---|---|---|---|---|
| data | Input | — | data, label | — |
| fc1 | InnerProduct | data | fc1 | 32 |
| relu1 | ReLU | fc1 | fc1 | — |
| fc2 | InnerProduct | fc1 | fc2 | 10 |
| loss | SoftmaxWithLoss | fc2, label | loss | — |
注意 relu1 那一行:输入和输出都叫 fc1。 |
||||
| 它是原地改写,不新建张量。 | ||||
| 这种写法在 Caffe 里很常见,好处是省内存,坏处是看图的时候容易漏掉一层。 | ||||
| 参数量按 prototxt 算:64 乘 32 加 32,再加 32 乘 10 加 10,等于 2410。 | ||||
| 这个数跟手算一模一样——定义文件写的是什么,网络就是什么,没有隐藏层。 |
三、solver 管训练
另一份文件管训练,一共九行:
| 参数 | 实测值 | 含义 |
|---|---|---|
| net | train.prototxt | 网络定义文件 |
| base_lr | 0.1 | 初始学习率 |
| lr_policy | step | 学习率按阶梯衰减 |
| stepsize | 400 | 每 400 轮衰减一次 |
| gamma | 0.5 | 每次乘 0.5 |
| momentum | 0.9 | 动量系数 |
| max_iter | 800 | 总迭代次数 |
| snapshot | 400 | 每 400 轮存一次模型 |
| solver_mode | CPU | 用 CPU 还是 GPU |
lr_policy: step 加 stepsize: 400 加 gamma: 0.5, |
||
| 三行合起来的意思是「每 400 轮学习率减半」。 | ||
| 这是一条策略,不是一个数字——Caffe 把常见策略都做成了选项,写个名字就行,不用自己写衰减函数。 | ||
| 实测跑 800 轮:学习率在第 400 和第二 800 轮各减半一次,从 0.1 到 0.025; | ||
| 损失从 0.0277 一路降到 0.0061,训练准确率到 1.0000。 | ||
| 这份定义和 solver 是自洽的。 |
四、取舍
好处两条,都很实在:
| 好处 | 具体是什么 |
|---|---|
| 结构可 diff | 网络是文本,谁改了一层,看一眼就知道改了什么 |
| 部署友好 | prototxt 加权重文件发出去,对方不需要你的训练代码 |
| 坏处也很实在:想加一个自定义层,得写 C++ 和 CUDA,重新编译整个框架。 | |
| 研究者改不动,工业界用着舒服——这个错位注定了它的结局。 | |
| 2017 年之后动态图流行,Caffe 这种「先定义后运行」的写法在研究中就退场了。 | |
| 但它没白活:现在 ONNX 的模型描述、各种部署格式,做的还是同一件事。 |
五、动手 Demo:真解析 prototxt,按定义建网络、按 solver 训练
prototxt 和 solver 都是真的文本,不是伪代码。
解析器自己写:靠花括号配对切层,靠正则取字段——不依赖 protobuf。
解析完之后,按 num_output 建网络、按 lr_policy 训练,全程真跑。
PROTOTXT = u"""
name: "digits8x8"
layer {
name: "data"
type: "Input"
top: "data"
top: "label"
input_param { shape: { dim: 100 dim: 1 dim: 8 dim: 8 } }
}
layer {
name: "fc1"
type: "InnerProduct"
bottom: "data"
top: "fc1"
inner_product_param { num_output: 32 }
}
layer {
name: "relu1"
type: "ReLU"
bottom: "fc1"
top: "fc1"
}
layer {
name: "fc2"
type: "InnerProduct"
bottom: "fc1"
top: "fc2"
inner_product_param { num_output: 10 }
}
"""
def blocks(text):
u"""把 layer 花括号一块块切出来,靠配对计数,能处理嵌套。"""
out, i = [], 0
while True:
j = text.find('layer', i)
if j < 0:
break
k = text.find('{', j)
depth, m = 0, k
while m < len(text):
if text[m] == '{':
depth += 1
elif text[m] == '}':
depth -= 1
if depth == 0:
break
m += 1
out.append(text[k + 1:m])
i = m + 1
return out
# 每层靠 bottom 和 top 的名字接起来,type 决定这一层干什么
for b in blocks(PROTOTXT):
print(field(b, 'name'), field(b, 'type'), field(b, 'bottom'), field(b, 'top'))
实跑输出:
--------------------------------------------------------------------
Caffe:从 prototxt 模型文件到可执行网络
--------------------------------------------------------------------
[1] Caffe 的规矩:模型不写在代码里,写在模型文件里
一份 .prototxt 描述网络结构,一份 solver 描述怎么训练,
然后一句 caffe train --solver=solver.prototxt 就跑起来了。
下面这份 prototxt 是真的文本,等一下要拿花括号配对把它切层:
name: "digits8x8"
layer {
name: "data"
type: "Input"
top: "data"
top: "label"
input_param { shape: { dim: 100 dim: 1 dim: 8 dim: 8 } }
}
layer {
name: "fc1"
type: "InnerProduct"
bottom: "data"
top: "fc1"
inner_product_param { num_output: 32 }
}
layer {
name: "relu1"
type: "ReLU"
bottom: "fc1"
top: "fc1"
}
layer {
name: "fc2"
type: "InnerProduct"
bottom: "fc1"
top: "fc2"
inner_product_param { num_output: 10 }
}
layer {
name: "loss"
type: "SoftmaxWithLoss"
bottom: "fc2"
bottom: "label"
top: "loss"
}
[2] 解析结果:五层,每层的输入输出都靠名字对上
层名 类型 输入 输出 输出维数
data Input - data,label -
fc1 InnerProduct data fc1 32
relu1 ReLU fc1 fc1 -
fc2 InnerProduct fc1 fc2 10
loss SoftmaxWithLoss fc2,label loss -
注意 relu1 这层的 bottom 和 top 都是 fc1——它原地改写,不新建张量。
这是 Caffe 的一个特点:blob 的名字就是数据流,层之间靠名字连。
[3] 按这份定义把网络真正建出来
blob 形状 说明
data [100, 1, 8, 8]
fc1 (100, 32)
fc2 (100, 10)
loss (100, 1)
总参数量 2410。这个数是按 prototxt 里两个 num_output 算出来的:
64 乘 32 加 32,再加 32 乘 10 加 10,跟手算一模一样。
第 6 章那个网络是 64-32-16-10,参数量 2778;这里少一个隐层,所以少一些。
[4] solver 文件:训练怎么跑全在这几行里
参数 值 含义
net train.prototxt 网络定义文件
base_lr 0.1 初始学习率
lr_policy step 学习率怎么衰减
stepsize 400 每多少轮衰减一次
gamma 0.5 每次衰减乘多少
momentum 0.9 动量系数
max_iter 800 总迭代次数
snapshot 400 每多少轮存一次模型
solver_mode CPU 用 CPU 还是 GPU
lr_policy 是 step,配上 stepsize 400 和 gamma 0.5,
意思就是「每 400 轮学习率减半」——这一条是配置,不是代码。
[5] 按 solver 跑起来,看学习率是不是真按 step 衰减
迭代 学习率 损失 训练准确率
200 0.1000 0.0277 1.0000
400 0.0500 0.0104 1.0000
600 0.0500 0.0077 1.0000
800 0.0250 0.0061 1.0000
800 轮跑完,学习率在第 400 和第 800 轮各减半一次,从 0.1 到 0.025。
表里打印的是「衰减之后」的值,所以 400 那一行看到的是 0.0500。
损失一直在降,准确率到 1.0000,说明这份定义和 solver 是自洽的。
[6] Caffe 这套设计的取舍
好处:网络结构是可以 diff 的文本,谁改了一层,看一眼 prototxt 就知道。
部署时把 prototxt 加权重文件一起发出去,对方不需要你的训练代码。
坏处:想加一个自定义层,得写 C++ 加 CUDA,重新编译整个框架。
2017 年之后动态图流行,Caffe 这种「先定义后运行」的写法就退场了。
现在它的直系后代是 Caffe2 和 ONNX 的模型描述,思路还在。
blocks 那个函数是本篇的技术核心。
不能用正则切 layer { ... },因为块里面还套着块(input_param、
inner_product_param 都是嵌套)。
必须靠花括号深度计数,配对到零才算一个块结束。
field 那个函数用正则取字段,取回来是列表——因为同一层可能有多个 bottom。
loss 那一层就用了两个:fc2 和 label。
训练那一段把 solver 的每一项都真用上了:momentum 进动量、
stepsize 和 gamma 控学习率衰减、max_iter 控循环次数。
这不是模拟,是照着那九个参数实现了一遍。
30 秒实验
- 把
num_output: 32改成 16,再看总参数量。 会从 2410 降到 1226——降的幅度比自己感觉的大。 - 把
gamma从 0.5 改成 0.1,再跑 800 轮。 第二轮之后学习率会掉到 0.001,损失降得更慢。 - 给 prototxt 加一层
Dropout(type 写 Dropout,加一个 dropout_ratio), 然后在执行器里实现它。 你会立刻感受到 Caffe 的痛点:加一层要改两处,定义和执行器。
常见误区
- 「prototxt 就是一份配置文件,跟 YAML 差不多。」 不一样。YAML 描述的是「参数」, prototxt 描述的是「一张图」——层与层之间的连接关系本身就是模型的一部分。
- 「Caffe 停了,所以它的做法是错的。」 它的做法在部署场景下依然是对的,错的是「改一层要重新编译」这个体验。 ONNX 就是带着这个正确的部分活下来的。
- 「学习率策略是小事,随便设一个常数就行。」 实测 800 轮里减半两次,损失从 0.0277 降到 0.0061。 用常数学习率,后 400 轮会在最优点附近来回晃,降不到这么低。
自测三问(点开对答案)
1. **为什么 prototxt 里层与层靠名字连,而不是靠顺序?** —— **因为靠顺序的话,插一层就要重排后面所有的编号,改起来极容易错。** **靠名字连,插一层只需要新起一个名字、把上下游的 bottom 和 top 改过来。** **这也让网络结构能画成一张真正的图,而不是一条线。** 2. **`relu1` 的 bottom 和 top 都叫 `fc1`,这意味着什么?** —— **意味着它原地改写,不新建张量。** **省内存,但看图的时候容易把它当成不存在——五层网络里有一层是「隐形」的。** 3. **Caffe 的模型文件为什么适合部署?** —— **因为它把「结构」和「权重」彻底分开了。** **部署方拿到 prototxt 和权重文件,用 Caffe 的推理引擎就能跑,** **完全不需要训练代码和训练数据。** **这在当年是很新的事,现在成了标准做法。**prototxt 和 solver 都是文件,写起来还是得懂语法。 下一节的 DIGITS 干脆给你一个网页,点几下就能提交训练任务。
- ☐ (本节完,下一节 7.5 训练系统——DIGITS)