在caffe中添加新层 L1 Loss layer

本文地址:http://blog.csdn.net/ismarvellous/article/details/79069661,转载请注明出处。

本文涉及的所有完整文件可在我的github下载。

1. L1 Loss的计算推导

与欧式距离(L2 Loss)相似,L1 Loss也是两个输入向量直接距离的一种度量。但L2 Loss的梯度在接近零点的时候梯度值也会接近于0,使学习进程变慢,而L1 Loss的梯度是一个常数,不存在这个问题。L1 Loss 和 L2 Loss 还有一些不同的特点,各有使用的场合,不过这不是本文的重点。本文主要关注如何在caffe中实现 L1 Loss。
L1 Loss的前向和后向都比较简单,下面简单概括一下。

1.1 前向计算

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=N1iNx1(i)x2(i)1
这里,N代表输入样本对的数量。

1.2 反向计算

L1 Loss本身没有参数,所以只需要计算对输入数据导数即可:
$$
\frac{\partial \mathcal L}{\partial \mathbf x_1^{(i)}} =
\begin{cases}
\frac{1}{N}, & x_1^{(i)} > x_2^{(i)} \

  • \frac{1}{N}, & x_1^{(i)} < x_2^{(i)}
    \end{cases}
    $$

$$
\frac{\partial \mathcal L}{\partial \mathbf x_2^{(i)}} =
\begin{cases}

  • \frac{1}{N}, & x_1^{(i)} > x_2^{(i)} \
    \frac{1}{N}, & x_1^{(i)} < x_2^{(i)}
    \end{cases}
    $$

2. caffe实现

在caffe中添加层一般需要以下几个步骤:

  1. include/caffe/layers/l1_loss_layer.hpp中添加声明。
  2. src/caffe/layers/l1_loss_layer.cpp中进行实现。
  3. 如果需要GPU版本,在src/caffe/layers/l1_loss_layer.cu中进行实现。
  4. 在cpp文件中用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/*'

2.1 前向计算

前向计算主要是实现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()
}

2.2 反向计算

反向计算主要是实现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());
    }
  }
}

2.3 测试文件编写

测试文件是用来检查我们编写的层的前向和后向计算是否正确的。主要分为以下几个部分。

通过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下载。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值