Caffe训练代码解析

疑问

  • Caffe 中不同的Solver 是如何生效的?

Caffe 编译

1
2
3
4
5
6
7
# Caffe -extend 
# https://github.com/BVLC/caffe/issues/5262
cmake .. \
-DCMAKE_BUILD_TYPE=Release \
-DCUDA_PROPAGATE_HOST_FLAGS=off \ # 为了使用c++11 但是cuda不是
-DCMAKE_CXX_FLAGS="-std=c++11”
make -j8

Caffe 是如何训练的?

想要理解Caffe是如何训练的, 首先得掌握Caffe的基本数据结构, 可以通过文末的参考文献, 过一遍就好了. 最关键的一点是, 知道网络计算过程中参数和层的输入输出分别放在哪里, 又是由谁在管理的?

Caffe 各个层对象管理自己参数, 而层的输入输出是由 Net 对象管理的, 明白这一点就够了.

Caffe以层为单位的抽象已经过时了, 这个粒度太大, 不够自由, 表达能力有限, 现在主流是用操作符和计算图抽象整个网络, 这个抽象更贴近, 网络实际上是一个”函数”这一事实. 

训练迭代 Solver->Step(Iters)

去除一些网络初始化, 加载训练中间过程相关的操作, Caffe 训练过程只要看 Solver.Solve()Solver.Step(iters) 两个操作. 而Step 是最核心的训练过程.

整个网络训练过程的主要代码如下(只留下训练相关的骨干代码).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template <typename Dtype>
void Solver<Dtype>::Step(int iters) {
while (iter_ < stop_iter) {
// 梯度置零, 后续Backward将对梯度进行累加
net_->ClearParamDiffs();
Dtype loss = 0;
for (int i = 0; i < param_.iter_size(); ++i) {
// Caffe 累计梯度包括 iter_size x batch_size
// loss 主要是为了打印信息, 与参数更新无关
// 更新参数, 策略因不同的 Sovler 而不同
loss += net_->ForwardBackward();
}
ApplyUpdate();

// Increment the internal iter_ counter -- its value should always indicate
// the number of times the weights have been updated.
++iter_;

// 存储快照, 提前退出等操作.
}
}

在阅读上面的代码的时候, 我最大的疑问是, 为啥 Net->ForwardBackward() 可以连续调用 iter_size 次, 迭代过程不会冲掉之前迭代的梯度吗? 答案是, Caffe 对 $iter\_size \times batch\_size$ 范围内的梯度进行了累计, 如下面的 proto 注释. iter_size 这个参数默认值是 1, 也就是说, 如果你不特意设置, Caffe 值对单个 Batch 的梯度进行累加.

1
2
// accumulate gradients over `iter_size` x `batch_size` instances
optional int32 iter_size = 36 [default = 1];

前后向计算 ForwardBackward()

这个应该是训练过程中最为关键的调用. 这个调用对当前 Batch 的样本进行前向计算, 然后进行反向传播. 反向传播对梯度进行回传, 并对参数的梯度进行累加. 本文的重点是洞悉 Caffe 是如何训练的, 对于Net是如何前向, 反向传播又是怎么做的, 超出了本文的范围, 再次省略.

前后向Caffe是如何掌握网络的拓扑结构的?: 看前向传播和反向传播代码的时候, 我一直又一个疑问, Caffe 是如何掌握和表示, 网络这个有向无环图的. 我一直以为, caffe 应该有个 Graph 有关的数据结构去表示这么个东西, 翻来覆去, 没有找到. Caffe 不管是前向还是, 反向都只是一个简单的线性循环, 这是为什么呢? 这个主要原因是, 我们在定义网络的时候, 实际上已经通过proto对网络进行了拓扑排序; 换句话说, 如果你的网络中先计算的层放在了后计算的层之后, Caffe 解析的时候纠错了, Caffe只接受拓扑排序好的网络定义.

既然已经拓扑排序好了网络, 那么, 我只要把层与层之间的输入输出线性拍起来(vector), 然后辅以它们对应的名称, 供查找就行了. 接着前向就只需顺序进行各个层的前向就行了, 因为这个表实际上是拓扑排序号的网络节点; 反向传播反着来一遍就好了, 就是这么简单.

更新参数

更新参数有不同的策略, 对应不同的Solver, 例如SGD, AdaDelta, Adam 等等, 默认是SGD. 想要彻底了解梯度下降的各个变种, 参考这篇博客. 下面我们看下SGD 的更新过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void SGDSolver<Dtype>::ApplyUpdate() {
Dtype rate = GetLearningRate();
ClipGradients();
for (int param_id = 0; param_id < this->net_->learnable_params().size();
++param_id) {
Normalize(param_id); //
Regularize(param_id); // 正则
ComputeUpdateValue(param_id, rate);
}
// Sovler 只负责更具策略计算出用于更新的梯度值
// 最后的更新则有网络对象自己来
// 更新只需要简单地用当前的权重减去更新的值就好了
this->net_->Update();
}

参考