本文地址:http://blog.csdn.net/ismarvellous/article/details/79069661,转载请注明出处。
本文涉及的所有完整文件可在我的github下载。
与欧式距离(L2 Loss)相似,L1 Loss也是两个输入向量直接距离的一种度量。但L2 Loss的梯度在接近零点的时候梯度值也会接近于0,使学习进程变慢,而L1 Loss的梯度是一个常数,不存在这个问题。L1 Loss 和 L2 Loss 还有一些不同的特点,各有使用的场合,不过这不是本文的重点。本文主要关注如何在caffe中实现 L1 Loss。
L1 Loss的前向和后向都比较简单,下面简单概括一下。
L1 Loss的前向计算就是两个输入向量 x 1 , x 2 \mathbf x_1, \mathbf x_2 x1,x2的L1距离,具体地:
L = 1 N ∑ i N ∣ ∣ x 1 ( i ) − x 2 ( i ) ∣ ∣ 1 \mathcal L = \frac{1}{N} \sum_i^N ||\mathbf x_1^{(i)} - \mathbf x_2^{(i)}||_1 L=N1i∑N∣∣x1(i)−x2(i)∣∣1
这里,N代表输入样本对的数量。
L1 Loss本身没有参数,所以只需要计算对输入数据导数即可:
$$
\frac{\partial \mathcal L}{\partial \mathbf x_1^{(i)}} =
\begin{cases}
\frac{1}{N}, & x_1^{(i)} > x_2^{(i)} \
$$
\frac{\partial \mathcal L}{\partial \mathbf x_2^{(i)}} =
\begin{cases}
在caffe中添加层一般需要以下几个步骤:
include/caffe/layers/l1_loss_layer.hpp
中添加声明。src/caffe/layers/l1_loss_layer.cpp
中进行实现。src/caffe/layers/l1_loss_layer.cu
中进行实现。layer_factory.hpp
提供的宏实例化并注册新的层。假如新的层叫做L1LossLayer
:INSTANTIATE_CLASS(L1LossLayer);
REGISTER_LAYER_CLASS(L1Loss);
5.在src/caffe/test/test_l1_loss_layer.cpp
中写测试。
6. 编译:
make -j
make test -j
make runtest GTEST_FILTER='L1LossLayerTest/*'
前向计算主要是实现Forward_cpu和Forward_gpu两个函数。
CPU版本:
// src/caffe/layers/l1_loss_layer.cpp
template <typename Dtype>
void L1LossLayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top) {
int count = bottom[0]->count();
caffe_sub(
count,
bottom[0]->cpu_data(),
bottom[1]->cpu_data(),
diff_.mutable_cpu_data());
Dtype loss = caffe_cpu_asum(count, diff_.cpu_data()) / bottom[0]->num();
top[0]->mutable_cpu_data()[0] = loss;
}
GPU版本:
template <typename Dtype>
void L1LossLayer<Dtype>::Forward_gpu(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top) {
int count = bottom[0]->count();
caffe_gpu_sub(
count,
bottom[0]->gpu_data(),
bottom[1]->gpu_data(),
diff_.mutable_gpu_data());
Dtype asum;
caffe_gpu_asum(count, diff_.gpu_data(), &asum); // gpu函数,使用gpu_data()
Dtype loss = asum / bottom[0]->num();
top[0]->mutable_cpu_data()[0] = loss; // 这里没有使用gpu函数,是普通的cpu运算,所以使用cpu_data()
}
反向计算主要是实现Backward_cpu和Backward_gpu两个函数。
CPU版本:
template <typename Dtype>
void L1LossLayer<Dtype>::Backward_cpu(const vector<Blob<Dtype>*>& top,
const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom) {
for (int i = 0; i < 2; ++i) {
if (propagate_down[i]) {
const Dtype sign = (i == 0) ? 1 : -1; // 对两个输入的反向计算的差异仅是正负号,所以根据输入blob的序号确定一个符号即可
const Dtype alpha = sign * top[0]->cpu_diff()[0] / bottom[i]->num(); // alpha = 1/N. top[0]->cpu_diff()[0]是weight_loss
// 使用diff_的符号来判断两个输入blob哪个大
caffe_cpu_sign(bottom[i]->count(),
diff_.cpu_data(),
bottom[i]->mutable_cpu_diff());
// caffe_cpu_scale(n, alpha, x, y): y = alpha * x
caffe_cpu_scale(bottom[i]->count(),
alpha,
bottom[i]->cpu_diff(),
bottom[i]->mutable_cpu_diff());
}
}
}
解释一下上面函数中的top[0]->cpu_diff()[0]
。我们知道,每一层回传的梯度是由上一层传回来的梯度乘以本层的梯度得到的。但我们现在本来就是loss层了,后面没有层了,那这个top[0]->cpu_diff()[0]
是什么呢?注意,这里只是取了top[0]->cpu_diff()
的第一个元素,其实它就是我们在prototxt中定义的loss_weight
。
类似的,GPU版本:
template <typename Dtype>
void L1LossLayer<Dtype>::Backward_gpu(const vector<Blob<Dtype>*>& top,
const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom) {
for (int i = 0; i < 2; ++i) {
if (propagate_down[i]) {
const Dtype sign = (i == 0) ? 1 : -1;
const Dtype alpha = sign * top[0]->cpu_diff()[0] / bottom[i]->num(); // 这里是cpu运算,使用cpu_diff()
caffe_gpu_sign(bottom[i]->count(),
diff_.gpu_data(),
bottom[i]->mutable_gpu_diff());
caffe_gpu_scale(bottom[i]->count(),
alpha,
bottom[i]->gpu_diff(),
bottom[i]->mutable_gpu_diff());
}
}
}
测试文件是用来检查我们编写的层的前向和后向计算是否正确的。主要分为以下几个部分。
通过L1LossLayerTest类中的TestForward()成员来检查loss是否可以被loss weight正确放缩。
void TestForward() {
// 不指定loss weight,得到一个loss值loss_weight_1,相当于loss weight为1。
LayerParameter layer_param;
L1LossLayer<Dtype> layer_weight_1(layer_param);
layer_weight_1.SetUp(this->blob_bottom_vec_, this->blob_top_vec_);
const Dtype loss_weight_1 =
layer_weight_1.Forward(this->blob_bottom_vec_, this->blob_top_vec_);
// 指定一个特定的loss weight,再得到一个loss值loss_weight_2,
// 然后检查loss_weight_2是否被正确地放缩。
const Dtype kLossWeight = 3.7;
layer_param.add_loss_weight(kLossWeight);
L1LossLayer<Dtype> layer_weight_2(layer_param);
layer_weight_2.SetUp(this->blob_bottom_vec_, this->blob_top_vec_);
const Dtype loss_weight_2 =
layer_weight_2.Forward(this->blob_bottom_vec_, this->blob_top_vec_);
const Dtype kErrorMargin = 1e-5;
EXPECT_NEAR(loss_weight_1 * kLossWeight, loss_weight_2, kErrorMargin);
// 确保loss不会过小
const Dtype kNonTrivialAbsThresh = 1e-1;
EXPECT_GE(fabs(loss_weight_1), kNonTrivialAbsThresh);
}
利用数值方法计算梯度,然后和本层的梯度计算进行比较,检查是否正确。这是通过调用caffe提供的GradientChecker实现的。
// 通过和数值计算的梯度值对比,检查本层梯度计算是否正确。
TYPED_TEST(L1LossLayerTest, TestGradient) {
typedef typename TypeParam::Dtype Dtype;
LayerParameter layer_param;
const Dtype kLossWeight = 3.7;
layer_param.add_loss_weight(kLossWeight);
L1LossLayer<Dtype> layer(layer_param);
layer.SetUp(this->blob_bottom_vec_, this->blob_top_vec_);
GradientChecker<Dtype> checker(1e-4, 1e-2, 1701); // 1e-4为梯度数值计算的步长,1e-2为比较的阈值
checker.CheckGradientExhaustive(&layer, this->blob_bottom_vec_,
this->blob_top_vec_);
}
本文涉及的所有完整文件可在我的github下载。