与你共享小喵的心得与感悟

0%

基于Caffe的Large Margin Softmax Loss的实现(一)

小喵的唠叨话:在写完上一次的博客之后,已经过去了2个月的时间,小喵在此期间,做了大量的实验工作,最终在使用的DeepID2的方法之后,取得了很不错的结果。这次呢,主要讲述一个比较新的论文中的方法,L-Softmax,据说单model在LFW上能达到98.71%的等错误率。更重要的是,小喵觉得这个方法和DeepID2并不冲突,如果二者可以互补,或许单model达到99%+将不是梦想。

和上一篇博客一样,小喵对读者做了如下的假定:

  1. 了解Deep Learning的基本知识。
  2. 仔细阅读过L-Softmax的论文,了解其中的数学推导。
  3. 使用Caffe作为训练框架。
  4. 即使不满足上述3条,也能持之以恒的学习。

L-Softmax的论文:Large-Margin Softmax Loss for Convolutional Neutral Networks Google 一下,第一条应该就是论文的地址,鉴于大家时间有限,小喵把原文地址也贴出来了,但不保证长期有效。http://jmlr.org/proceedings/papers/v48/liud16.pdf 这里我们也将整个系列分几部分来讲。

一、margin与lambda

margin和lambda这两个参数是我们这篇博客的重点。也是整篇论文的重点。对于分类的任务,每个样本都会有N的输出的分数(N的类别),如果在训练中,人为的使正确类别的得分变小,也就是说加大了区分正确类别的难度,那么网络就会学习出更有区分能力的特征,并且加大类间的距离。作者选用的加大难度的方式就是改变最后一个FC层中的weight和特征之间的角度值,角度增大的倍数就是margin,从而使特定类别的得分变小。而第二个参数lambda是为了避免网络不收敛而设定的,我们之后会讲到。

为了实现这个效果,我们需要设计一个新的层,large_margin_inner_product_layer。这个层和一般的 inner_product_layer 很相似,但是多了特定类别削弱的功能。

考虑到这个层是有参数的,我们需要在 caffe.protocaffe_home/src/caffe/proto/caffe.proto)中做一些修改。这里的定义是按照 protobuf 的语法写的,简单的修改只要照着其他的参数来改写就好。

首先定义我们的这个层的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
message LargeMarginInnerProductParameter {
  optional uint32 num_output = 1; // The number of outputs for the layer
  optional bool bias_term = 2 [default = true]; // whether to have bias terms
  optional FillerParameter weight_filler = 3; // The filler for the weight
  optional FillerParameter bias_filler = 4; // The filler for the bias

  // The first axis to be lumped into a single inner product computation;
  // all preceding axes are retained in the output.
  // May be negative to index from the end (e.g., -1 for the last axis).
  optional int32 axis = 5 [default = 1];
  // Specify whether to transpose the weight matrix or not.
  // If transpose == true, any operations will be performed on the transpose
  // of the weight matrix. The weight matrix itself is not going to be transposed
  // but rather the transfer flag of operations will be toggled accordingly.
  optional bool transpose = 6 [default = false];
  optional uint32 margin = 7 [default = 1];
optional float lambda = 8 [default = 0];
}

参数的定义和 InnerProductParameter 非常相似,只是多了两个参数 marginlambda

之后在 LayerParameter 添加一个可选参数(照着 InnerProductParameter 写就好)。

1
optional LargeMarginInnerProductParameter large_margin_inner_product_param = 147;

这时,喵粉可能很在意这个147是怎么回事。其实呢,在protobuf中,每个结构中的变量都需要一个id,只要保证不重复即可。我们在LayerParameter的最开始可以看到这么一行注释:

next-availabel-layer-id

说明下一个有效的id是147。这里我们新加的参数就果断占用了这个id。修改之后,建议把注释改一下(不要人为的挖坑):

1
LayerParameter next available layer-specific ID: 148 (last added: large_margin_inner_product_param)

避免之后再新加层的时候出问题。 工作完毕,我们就可以在 train_val.prototxt 中用这种方式使用这个新层了(具体的使用,后面再说):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
layer {
name: "fc2"
type: "LargeMarginInnerProduct"
bottom: "fc1"
bottom: "label"
top: "fc2"
param {
lr_mult: 1
decay_mult: 1
}
param {
lr_mult: 0
decay_mult: 0
}
large_margin_inner_product_param {
num_output: 10000
margin: 2
lambda: 0
weight_filler {
type: "xavier"
}
}
}

二,运筹帷幄之成员变量

我们刚刚在 caffe.proto 中,添加了新参数的定义。而事实上,我们还没有这个层的具体实现。这部分,主要介绍我们需要的临时变量。

首先,我们要理清整个计算的流程。

先看前馈。

第一步,需要求出W和x的夹角的余弦值:

\[ cos(\theta_j)=\frac{W_j^Tx_i}{\|W_j\|\|x_i\|} \]

第二步,计算m倍角度的余弦值:

\[ \cos(m\theta_i)=\sum_n(-1)^n{C_m^{2n}\cos^{m-2n}(\theta_i)\cdot(1-\cos(\theta_i)^2)^n}, (2n\leq m) \]

第三步,计算前馈:

\[ f_{y_{i}}=(-1)^k\cdot\|W_{y_{i}}\|\|x_{i}\|\cos(m\theta_i)-2k\cdot\|W_{y_i}\|\|x_i\| \]

k是根据 \(\cos(\theta)\) 的取值决定的。

后馈比前馈要复杂一些,不过使用的变量也是一样的。因此我们可以编写自己的头文件了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#ifndef CAFFE_LARGE_MARGIN_INNER_PRODUCT_LAYER_HPP_
#define CAFFE_LARGE_MARGIN_INNER_PRODUCT_LAYER_HPP_

#include <vector>

#include "caffe/blob.hpp"
#include "caffe/layer.hpp"
#include "caffe/proto/caffe.pb.h"

namespace caffe {

template <typename Dtype>
class LargeMarginInnerProductLayer : public Layer<Dtype> {
public:
explicit LargeMarginInnerProductLayer(const LayerParameter& param)
: Layer<Dtype>(param) {}
virtual void LayerSetUp(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top);
virtual void Reshape(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top);

virtual inline const char* type() const { return "LargeMarginInnerProduct"; }
// edited by miao
// LM_FC层有两个bottom
virtual inline int ExactNumBottomBlobs() const { return 2; }
// end edited
virtual inline int ExactNumTopBlobs() const { return 1; }

protected:
virtual void Forward_cpu(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top);
virtual void Forward_gpu(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top);
virtual void Backward_cpu(const vector<Blob<Dtype>*>& top,
const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom);
virtual void Backward_gpu(const vector<Blob<Dtype>*>& top,
const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom);

int M_;
int K_;
int N_;
bool bias_term_;
Blob<Dtype> bias_multiplier_;
bool transpose_; ///< if true, assume transposed weights

// added by miao

// 一些常数
Blob<Dtype> cos_theta_bound_; // 区间边界的cos值
Blob<int> k_; // 当前角度theta所在的区间的位置
Blob<int> C_M_N_; // 组合数
unsigned int margin; // margin
float lambda; // lambda

Blob<Dtype> wx_; // wjT * xi
Blob<Dtype> abs_w_; // ||wj||
Blob<Dtype> abs_x_; // ||xi||
Blob<Dtype> cos_t_; // cos(theta)
Blob<Dtype> cos_mt_; // cos(margin * theta)

Blob<Dtype> dydw_; // 输出对w的导数
Blob<Dtype> dydx_; // 输出对x的导数
// end added
};

} // namespace caffe

#endif // CAFFE_LARGE_MARGIN_INNER_PRODUCT_LAYER_HPP_

这里主要是复制了 inner_product_layer.hpp,然后做了一点修改。具体是增加了几个成员变量,同时改了 ExactNumBottomBlobs 的返回值,因为我们的这个层磁带 bottom 需要两个,前一层的 feature 和样本的 label

三、内存和常量的初始化

这部分,主要给我们的各个成员变量分配内存,同时给几个常量进行初始化。这里也是照着 inner_product_layer.cpp 来写的,在 setup 的时候,增加了一些用于初始化的代码,并删除了 forward_cpubackwark_cpu 的具体实现。

修改之后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
#include <vector>
#include <cmath>

#include "caffe/filler.hpp"
#include "caffe/layers/large_margin_inner_product_layer.hpp"
#include "caffe/util/math_functions.hpp"

#define PI 3.14159265

namespace caffe {

int factorial(int n) {
if (0 == n) return 1;
int f = 1;
while (n) {
f *= n;
-- n;
}
return f;
}

template <typename Dtype>
void LargeMarginInnerProductLayer<Dtype>::LayerSetUp(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top) {

const int axis = bottom[0]->CanonicalAxisIndex(
this->layer_param_.large_margin_inner_product_param().axis());
// added by miao
std::vector<int> wx_shape(1);
wx_shape[0] = bottom[0]->shape(0);
this->wx_.Reshape(wx_shape);
this->abs_w_.Reshape(wx_shape);
this->abs_x_.Reshape(wx_shape);
this->k_.Reshape(wx_shape);
this->cos_t_.Reshape(wx_shape);
this->cos_mt_.Reshape(wx_shape);

std::vector<int> cos_theta_bound_shape(1);
this->margin = static_cast<unsigned int>(this->layer_param_.large_margin_inner_product_param().margin());
cos_theta_bound_shape[0] = this->margin + 1;
this->cos_theta_bound_.Reshape(cos_theta_bound_shape);
for (int k = 0; k <= this->margin; ++ k) {
this->cos_theta_bound_.mutable_cpu_data()[k] = std::cos(PI * k / this->margin);
}
this->C_M_N_.Reshape(cos_theta_bound_shape);
for (int n = 0; n <= this->margin; ++ n) {
this->C_M_N_.mutable_cpu_data()[n] = factorial(this->margin) / factorial(this->margin - n) / factorial(n);
}

// d size
std::vector<int> d_shape(2);
d_shape[0] = bottom[0]->shape(0);
d_shape[1] = bottom[0]->count(axis);
this->dydw_.Reshape(d_shape);
this->dydx_.Reshape(d_shape);

this->lambda = this->layer_param_.large_margin_inner_product_param().lambda();
// end added

transpose_ = false; // 坚决不转置!

const int num_output = this->layer_param_.large_margin_inner_product_param().num_output();
bias_term_ = this->layer_param_.large_marin_inner_product_param().bias_term();
N_ = num_output;

// Dimensions starting from "axis" are "flattened" into a single
// length K_ vector. For example, if bottom[0]'s shape is (N, C, H, W),
// and axis == 1, N inner products with dimension CHW are performed.
K_ = bottom[0]->count(axis);
// Check if we need to set up the weights
if (this->blobs_.size() > 0) {
LOG(INFO) << "Skipping parameter initialization";
} else {
if (bias_term_) {
this->blobs_.resize(2);
} else {
this->blobs_.resize(1);
}
// Initialize the weights
vector<int> weight_shape(2);
if (transpose_) {
weight_shape[0] = K_;
weight_shape[1] = N_;
} else {
weight_shape[0] = N_;
weight_shape[1] = K_;
}
this->blobs_[0].reset(new Blob<Dtype>(weight_shape));
// fill the weights
shared_ptr<Filler<Dtype> > weight_filler(GetFiller<Dtype>(
this->layer_param_.large_margin_inner_product_param().weight_filler()));
weight_filler->Fill(this->blobs_[0].get());
// If necessary, intiialize and fill the bias term
if (bias_term_) {
vector<int> bias_shape(1, N_);
this->blobs_[1].reset(new Blob<Dtype>(bias_shape));
shared_ptr<Filler<Dtype> > bias_filler(GetFiller<Dtype>(
this->layer_param_.large_margin_inner_product_param().bias_filler()));
bias_filler->Fill(this->blobs_[1].get());
}

} // parameter initialization
this->param_propagate_down_.resize(this->blobs_.size(), true);
}

template <typename Dtype>
void LargeMarginInnerProductLayer<Dtype>::Reshape(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top) {
// Figure out the dimensions
const int axis = bottom[0]->CanonicalAxisIndex(
this->layer_param_.large_margin_inner_product_param().axis());
const int new_K = bottom[0]->count(axis);
CHECK_EQ(K_, new_K)
<< "Input size incompatible with large margin inner product parameters.";
// The first "axis" dimensions are independent inner products; the total
// number of these is M_, the product over these dimensions.
M_ = bottom[0]->count(0, axis);
// The top shape will be the bottom shape with the flattened axes dropped,
// and replaced by a single axis with dimension num_output (N_).
vector<int> top_shape = bottom[0]->shape();
top_shape.resize(axis + 1);
top_shape[axis] = N_;
top[0]->Reshape(top_shape);
}

template <typename Dtype>
void LargeMarginInnerProductLayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top) {
// not implement
}

template <typename Dtype>
void LargeMarginInnerProductLayer<Dtype>::Backward_cpu(const vector<Blob<Dtype>*>& top,
const vector<bool>& propagate_down,
const vector<Blob<Dtype>*>& bottom) {
// not implement
}

#ifdef CPU_ONLY
STUB_GPU(LargeMarginInnerProductLayer);
#endif

INSTANTIATE_CLASS(LargeMarginInnerProductLayer);
REGISTER_LAYER_CLASS(LargeMarginInnerProduct);

} // namespace caffe

至此,large_margin_inner_product_layer 的准备工作就做完了。

下一篇博客,我们来详细的讨论前馈的具体实现。

如果您觉得本文对您有帮助,

转载 请注明出处~

一杯奶茶也是心意~