在看过 caffe 代码的三个核心部分 blob、layer、net 之后,陷入了不知道以什么顺序继续看的困境。
blob、layer、net 只是三个最基本的概念,关键还是在于各个 layer。但是 layer 这么多,要怎么看呢?想了一下,决定把相同作用的 layer 放在一起分析。今天先分析一下激活函数。
sigmoid#

表达式为 \( f(t) = 1/(1+e^{-t}) \)。
caffe 的 GPU 实现非常直接
1
2template <typename Dtype>
3__global__ void SigmoidForward(const int n, const Dtype* in, Dtype* out) {
4 CUDA_KERNEL_LOOP(index, n) {
5 out[index] = 1. / (1. + exp(-in[index]));
6 }
7}sigmoid 激活函数的一大优点是求导非常容易,因此 backward 函数也很简单。
1
2template <typename Dtype>
3__global__ void SigmoidBackward(const int n, const Dtype* in_diff,
4 const Dtype* out_data, Dtype* out_diff) {
5 CUDA_KERNEL_LOOP(index, n) {
6 const Dtype sigmoid_x = out_data[index];
7 out_diff[index] = in_diff[index] * sigmoid_x * (1 - sigmoid_x);
8 }
9}然后 proto 里面也没什么内容,因为 sigmoid 函数没什么参数。
1
2message SigmoidParameter {
3 enum Engine {
4 DEFAULT = 0;
5 CAFFE = 1;
6 CUDNN = 2;
7 }
8 optional Engine engine = 1 [default = DEFAULT];
9}sigmoid 源码里还有一段注释值得留意:
/** @brief Sigmoid function non-linearity @f$ y = (1 + \exp(-x))^{-1} @f$, a classic choice in neural networks.
Note that the gradient vanishes as the values move away from 0. The ReLULayer is often a better choice for this reason.
sigmoid 函数大概是早期的一个比较常用的选择,但是它有几个缺点:
- 梯度弥散(除了中间的位置,其他位置的梯度都接近 0)
- sigmoid 函数的输出不是 0 均值的,导致权重的梯度全部为正或者为负,只能往一个方向更新,学习效率比较低。
- 算 exp 比较慢
因此现在已经几乎不会用 sigmoid 来做激活函数了。
但是 sigmoid 函数其实还有其他用途,比如对于一个多标签的分类任务,常常在 fc 后面接 sigmoid 作为神经网络的输出,来判断是否包含这些标签中的一个或者几个。
(多标签和多分类任务的区别在于,多分类任务通常只有一个标签,一个物体属于这个类别就不会属于另外的类别。)
tanh#
和 sigmoid 比较相似,比起 sigmoid 的优点是值域在 [-1,1]、均值为 0,梯度更新的效率好一些。
没什么可说的,直接放代码吧。
工业界用得也不太多
1
2template <typename Dtype>
3void TanHLayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom,
4 const vector<Blob<Dtype>*>& top) {
5 const Dtype* bottom_data = bottom[0]->cpu_data();
6 Dtype* top_data = top[0]->mutable_cpu_data();
7 const int count = bottom[0]->count();
8 for (int i = 0; i < count; ++i) {
9 top_data[i] = tanh(bottom_data[i]);
10 }
11}
12
13template <typename Dtype>
14void TanHLayer<Dtype>::Backward_cpu(const vector<Blob<Dtype>*>& top,
15 const vector<bool>& propagate_down,
16 const vector<Blob<Dtype>*>& bottom) {
17 if (propagate_down[0]) {
18 const Dtype* top_data = top[0]->cpu_data();
19 const Dtype* top_diff = top[0]->cpu_diff();
20 Dtype* bottom_diff = bottom[0]->mutable_cpu_diff();
21 const int count = bottom[0]->count();
22 Dtype tanhx;
23 for (int i = 0; i < count; ++i) {
24 tanhx = top_data[i];
25 bottom_diff[i] = top_diff[i] * (1 - tanhx * tanhx);
26 }
27 }
28}relu 及其变种#
表达式为 \( f(x) = \max(0, x) \)。

我们看caffe的proto,发现relu和leaky relu是在一起实现的,因此干脆一起说了。
1
2// Message that stores parameters used by ReLULayer
3message ReLUParameter {
4 // Allow non-zero slope for negative inputs to speed up optimization
5 // Described in:
6 // Maas, A. L., Hannun, A. Y., & Ng, A. Y. (2013). Rectifier nonlinearities
7 // improve neural network acoustic models. In ICML Workshop on Deep Learning
8 // for Audio, Speech, and Language Processing.
9 optional float negative_slope = 1 [default = 0];
10 enum Engine {
11 DEFAULT = 0;
12 CAFFE = 1;
13 CUDNN = 2;
14 }
15 optional Engine engine = 2 [default = DEFAULT];
16}leaky relu是relu的改进,表达式为 \( f(x)={\begin{cases}x&{\text{if }}x>0\\\lambda x&{\text{if }}x\leq 0\end{cases}} \)
其中lambda是一个用户设定的超参,就是上面的negative_slope
看一下代码. backward部分也很简单,就一起看一下。
1
2template <typename Dtype>
3void ReLULayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom,
4 const vector<Blob<Dtype>*>& top) {
5 const Dtype* bottom_data = bottom[0]->cpu_data();
6 Dtype* top_data = top[0]->mutable_cpu_data();
7 const int count = bottom[0]->count();
8 Dtype negative_slope = this->layer_param_.relu_param().negative_slope();
9 for (int i = 0; i < count; ++i) {
10 top_data[i] = std::max(bottom_data[i], Dtype(0))
11 + negative_slope * std::min(bottom_data[i], Dtype(0));
12 }
13}
14
15template <typename Dtype>
16void ReLULayer<Dtype>::Backward_cpu(const vector<Blob<Dtype>*>& top,
17 const vector<bool>& propagate_down,
18 const vector<Blob<Dtype>*>& bottom) {
19 if (propagate_down[0]) {
20 const Dtype* bottom_data = bottom[0]->cpu_data();
21 const Dtype* top_diff = top[0]->cpu_diff();
22 Dtype* bottom_diff = bottom[0]->mutable_cpu_diff();
23 const int count = bottom[0]->count();
24 Dtype negative_slope = this->layer_param_.relu_param().negative_slope();
25 for (int i = 0; i < count; ++i) {
26 bottom_diff[i] = top_diff[i] * ((bottom_data[i] > 0)
27 + negative_slope * (bottom_data[i] <= 0));
28 }
29 }
30}relu 激活函数应该是目前业界默认的激活函数,简单,效果也挺不错。优点是:
- 收敛速度快
- 在 x>0 的区域,不会出现梯度饱和和梯度消失的情况。
- 计算复杂度低,不需要进行指数运算,只要一个阈值就可以得到激活值。
当然也有一些缺点:
- ReLU 的输出不是 0 均值的。
- Dead ReLU Problem(神经元坏死现象):ReLU 在负数区域被 kill 的现象叫做 dead relu。ReLU 在训练时很“脆弱”,在 x<0 时梯度为 0,这个神经元及之后的神经元梯度永远为 0,不再对任何数据有所响应,导致相应参数永远不会被更新。 产生这种现象的两个原因:参数初始化问题;learning rate 太高导致在训练过程中参数更新太大。 解决方法:采用 Xavier 初始化方法,以及避免将 learning rate 设置太大或使用 adagrad 等自动调节 learning rate 的算法。
leaky relu 主要是为了解决 dead relu 现象提出来的,避免出现激活值总为 0 的问题。
但是在实际应用中,它并不明显比 relu 效果好,因此人们还是经常用 relu。
leaky relu 中的 lambda 是一个用户设定的超参,如果不手动设定,而是把这个参数通过数据学习出来,就是 prelu(p for Parametric)。
具体的区别在于:
- negative slope 是通过数据学习得到的。
- 每个 channel 可以学到不同的 negative slope(也可以设置所有 channel 的 negative slope 统一)
@brief Parameterized Rectified Linear Unit non-linearity @f$ y_i = \max(0, x_i) + a_i \min(0, x_i) @f$. The differences from ReLULayer are 1) negative slopes are learnable though backprop and 2) negative slopes can vary across channels. The number of axes of input blob should be greater than or equal to 2. The 1st axis (0-based) is seen as channels.
我们先看一下proto
1
2message PReLUParameter {
3 // Parametric ReLU described in K. He et al, Delving Deep into Rectifiers:
4 // Surpassing Human-Level Performance on ImageNet Classification, 2015.
5
6 // Initial value of a_i. Default is a_i=0.25 for all i.
7 optional FillerParameter filler = 1;
8 // Whether or not slope paramters are shared across channels.
9 optional bool channel_shared = 2 [default = false];
10}这里面的filler的作用是决定每个channel的lambda的初始值
forward函数和之前比较相似,重点关注一下 slope_data
1
2template <typename Dtype>
3void PReLULayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom,
4 const vector<Blob<Dtype>*>& top) {
5 const Dtype* bottom_data = bottom[0]->cpu_data();
6 Dtype* top_data = top[0]->mutable_cpu_data();
7 const int count = bottom[0]->count();
8 const int dim = bottom[0]->count(2);
9 const int channels = bottom[0]->channels();
10 const Dtype* slope_data = this->blobs_[0]->cpu_data();
11
12 // For in-place computation
13 if (bottom[0] == top[0]) {
14 caffe_copy(count, bottom_data, bottom_memory_.mutable_cpu_data());
15 }
16
17 // if channel_shared, channel index in the following computation becomes
18 // always zero.
19 const int div_factor = channel_shared_ ? channels : 1;
20 for (int i = 0; i < count; ++i) {
21 int c = (i / dim) % channels / div_factor;
22 top_data[i] = std::max(bottom_data[i], Dtype(0))
23 + slope_data[c] * std::min(bottom_data[i], Dtype(0));
24 }
25}然后在backward部分,我们可以看到 slope_data是通过BP学习得到的。
1
2template <typename Dtype>
3void PReLULayer<Dtype>::Backward_cpu(const vector<Blob<Dtype>*>& top,
4 const vector<bool>& propagate_down,
5 const vector<Blob<Dtype>*>& bottom) {
6 const Dtype* bottom_data = bottom[0]->cpu_data();
7 const Dtype* slope_data = this->blobs_[0]->cpu_data();
8 const Dtype* top_diff = top[0]->cpu_diff();
9 const int count = bottom[0]->count();
10 const int dim = bottom[0]->count(2);
11 const int channels = bottom[0]->channels();
12
13 // For in-place computation
14 if (top[0] == bottom[0]) {
15 bottom_data = bottom_memory_.cpu_data();
16 }
17
18 // if channel_shared, channel index in the following computation becomes
19 // always zero.
20 const int div_factor = channel_shared_ ? channels : 1;
21
22 // Propagte to param
23 // Since to write bottom diff will affect top diff if top and bottom blobs
24 // are identical (in-place computaion), we first compute param backward to
25 // keep top_diff unchanged.
26 if (this->param_propagate_down_[0]) {
27 Dtype* slope_diff = this->blobs_[0]->mutable_cpu_diff();
28 for (int i = 0; i < count; ++i) {
29 int c = (i / dim) % channels / div_factor;
30 slope_diff[c] += top_diff[i] * bottom_data[i] * (bottom_data[i] <= 0);
31 }
32 }
33 // Propagate to bottom
34 if (propagate_down[0]) {
35 Dtype* bottom_diff = bottom[0]->mutable_cpu_diff();
36 for (int i = 0; i < count; ++i) {
37 int c = (i / dim) % channels / div_factor;
38 bottom_diff[i] = top_diff[i] * ((bottom_data[i] > 0)
39 + slope_data[c] * (bottom_data[i] <= 0));
40 }
41 }
42}这个东西的优点基本上是 leaky relu 的优点,再加上参数可以通过数据学习,更加鲁棒。
然而在工业界,这东西用得很少,再加上 TensorRT5 现在还不支持 prelu(似乎是 caffe parser 的锅)。
还有个类似的叫 elu。
$$ f(z)=\left\{\begin{array}{ll}{z} & {z>0} \\ {\alpha(\exp (z)-1)} & {z \leq 0}\end{array}\right. $$和 leaky relu 其实非常类似,没什么好说的。
效果并不确定完全比 relu 好,而且还要做 exp 运算,差评。
1
2
3template <typename Dtype>
4void ELULayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom,
5 const vector<Blob<Dtype>*>& top) {
6 const Dtype* bottom_data = bottom[0]->cpu_data();
7 Dtype* top_data = top[0]->mutable_cpu_data();
8 const int count = bottom[0]->count();
9 Dtype alpha = this->layer_param_.elu_param().alpha();
10 for (int i = 0; i < count; ++i) {
11 top_data[i] = std::max(bottom_data[i], Dtype(0))
12 + alpha * (exp(std::min(bottom_data[i], Dtype(0))) - Dtype(1));
13 }
14}
15
16template <typename Dtype>
17void ELULayer<Dtype>::Backward_cpu(const vector<Blob<Dtype>*>& top,
18 const vector<bool>& propagate_down,
19 const vector<Blob<Dtype>*>& bottom) {
20 if (propagate_down[0]) {
21 const Dtype* bottom_data = bottom[0]->cpu_data();
22 const Dtype* top_data = top[0]->cpu_data();
23 const Dtype* top_diff = top[0]->cpu_diff();
24 Dtype* bottom_diff = bottom[0]->mutable_cpu_diff();
25 const int count = bottom[0]->count();
26 Dtype alpha = this->layer_param_.elu_param().alpha();
27 for (int i = 0; i < count; ++i) {
28 bottom_diff[i] = top_diff[i] * ((bottom_data[i] > 0)
29 + (alpha + top_data[i]) * (bottom_data[i] <= 0));
30 }
31 }
32}总结#
无脑上relu一般效果不会太差。