乾貨!caffe源碼深入學習9:caffe框架神經網絡反傳代碼解析(三)之contrastive_loss_layer源碼解析

caffe源碼深入學習9:caffe框架神經網絡反傳代碼解析(三)之contrastive_loss_layer源碼解析

寫在前面

本篇博客是Caffe源碼解析系列的第9篇,也是Caffe深度學習梯度反傳代碼解析的第3篇。在caffe源碼深入學習7和8中,筆者分別解析了Caffe框架中ReLU激活層和和池化層的反傳過程是如何實現的,在這兩個層中,既不包含可訓練參數,也沒有複雜的求導過程,因此代碼是相對簡單的。從本篇博客開始,在對Caffe框架中的反傳代碼進行解析時,筆者將解析更復雜的實現代碼,比如包含可訓練參數的層與複雜求導過程的層。

本篇博客作爲神經網絡反傳解析的第三講,筆者打算解析一下Caffe中的計算誤差的層,裏面往往都包含比較複雜的求導實現代碼。本想解析一下大家都熟悉,用的比較多的softmax_loss_layer,但是筆者發現博主王裏揚洛夫已經在原創博文CAFFE源碼學習筆記之softmax_layer中解析過了,寫得清晰詳細。因此,筆者就不打算重複解析softmax_loss_layer了。經過思考後,筆者打算解析一下對比損失,即contrastive_loss_layer,也是在人臉驗證,圖像檢索中使用的非常廣泛的一個層。廢話不多說,下面開始乾貨。

理論解析

Contrastive loss(對比損失)是由深度學習理論先行者Yann LeCun於2005年提出的,論文題目爲Dimensionality Reduction by Learning an Invariant Mapping,發表在CVPR 2006中。作用主要是區分樣本對是否相似,對於輸入的兩個向量abd表示a與b之間的距離(大多數時採用歐式距離)。contrastive loss可如下表述:

Losscontrastive=12Ni=1N(sim×d2+(1sim)×max(margind,0)2) Loss_{contrastive} = \frac{1}{2N}\sum_{i=1}^{N}(sim \times d^2 + (1-sim) \times max(margin-d, 0)^2)
在上式中,N表示樣本對總數,sim{0,1}sim\in\{0,1\}。如果sim取1,表示a與b是同類或者相似的樣本對,那麼Loss的目標是減少兩者的距離;相反,如果sim取0,表示a與b是不同類或者不相似的樣本對,那麼Loss的目標是拉大兩者的距離,使得兩者的距離至少要大於超參數margin

在Caffe框架實現contrastive loss時,我們可以先看看caffe.proto中是如何定義層參數的:

message ContrastiveLossParameter {
  // margin for dissimilar pair
  optional float margin = 1 [default = 1.0];
  // The first implementation of this cost did not exactly match the cost of
  // Hadsell et al 2006 -- using (margin - d^2) instead of (margin - d)^2.
  // legacy_version = false (the default) uses (margin - d)^2 as proposed in the
  // Hadsell paper. New models should probably use this version.
  // legacy_version = true uses (margin - d^2). This is kept to support /
  // reproduce existing models and results
  optional bool legacy_version = 2 [default = false];
}

大家可以看到,在層參數中設計中,除了上面公式中的margin,還有一個legacy_version,該參數在a與b不是同類或非相似樣本對時,對loss計算有一點小改動,可以用如下公式表示:

Losscontrastive={12Ni=1Nd2sim=112Ni=1Nmax(margind,0)2sim=0andlegacy_version=false12Ni=1Nmax(margind2,0)sim=0andlegacy_version=true Loss_{contrastive} = \begin{cases} \frac{1}{2N}\sum_{i=1}^{N}d^2 & sim=1 \\ \frac{1}{2N}\sum_{i=1}^{N}max(margin-d, 0)^2 & sim=0 \quad and \quad legacy\_version=false \\ \frac{1}{2N}\sum_{i=1}^{N}max(margin-d^2, 0) & sim=0 \quad and \quad legacy\_version=true \end{cases}
假設d爲歐氏距離,即d=i=1n(aibi)2d=\sqrt{\sum_{i=1}^{n}(a_i-b_i)^2}。那麼,在訓練時進行反傳的時候,在一個容量爲N的batch中,對於頂層的梯度,經過鏈式求導,那麼aia_i對應的梯度可由下述公式表示:

Lossai={dN×dai×top_diffsim=1margindN×dai×top_diffsim=0andlegacy_version=falseandmargind>0dN×dai×top_diffsim=0andlegacy_version=trueandmargind2>00sim=0andlegacy_version=falseandmargind00sim=0andlegacy_version=trueandmargind20 \frac{\partial_{Loss}}{\partial_{a_i}} = \begin{cases} \frac{d}{N} \times \frac{\partial_{d}}{\partial_{a_i}} \times top\_diff & sim=1 \\ -\frac{margin-d}{N} \times \frac{\partial_{d}}{\partial_{a_i}} \times top\_diff & sim=0 \quad and \quad legacy\_version=false \quad and \quad margin-d>0\\ -\frac{d}{N} \times \frac{\partial_{d}}{\partial_{a_i}} \times top\_diff & sim=0 \quad and \quad legacy\_version=true \quad and \quad margin-d^2>0\\ 0 & sim=0 \quad and \quad legacy\_version=false \quad and \quad margin-d\leq0\\ 0 & sim=0 \quad and \quad legacy\_version=true \quad and \quad margin-d^2\leq0 \end{cases}
在上述公式中,有一個dai\frac{\partial_{d}}{\partial_{a_i}},由於d=i=1n(aibi)2d=\sqrt{\sum_{i=1}^{n}(a_i-b_i)^2}。那麼,對於任意的iidai\frac{\partial_{d}}{\partial_{a_i}}可由下述公示表示:
dai=aibid \frac{\partial_{d}}{\partial_{a_i}}=\frac{a_i-b_i}{d}
在求得dai\frac{\partial_{d}}{\partial_{a_i}}後,Lossai\frac{\partial_{Loss}}{\partial_{a_i}}可表示爲:
Lossai={aibiN×top_diffsim=1margindN×aibid×top_diffsim=0andlegacy_version=falseandmargind>0aibiN×top_diffsim=0andlegacy_version=trueandmargind2>00sim=0andlegacy_version=falseandmargind00sim=0andlegacy_version=trueandmargind20 \frac{\partial_{Loss}}{\partial_{a_i}} = \begin{cases} \frac{a_i-b_i}{N} \times top\_diff & sim=1 \\ -\frac{margin-d}{N} \times \frac{a_i - b_i}{d} \times top\_diff & sim=0 \quad and \quad legacy\_version=false \quad and \quad margin-d>0\\ -\frac{a_i-b_i}{N} \times top\_diff & sim=0 \quad and \quad legacy\_version=true \quad and \quad margin-d^2>0\\ 0 & sim=0 \quad and \quad legacy\_version=false \quad and \quad margin-d\leq0\\ 0 & sim=0 \quad and \quad legacy\_version=true \quad and \quad margin-d^2\leq0 \end{cases}
如上所示,就能在bottom[0],即a上進行每一個aia_i的梯度反傳了。同理,對於任意的iibib_i的梯度,只需要在aia_i的梯度上取相反數就行了。因爲
dbi=aibid \frac{\partial_{d}}{\partial_{b_i}}=-\frac{a_i-b_i}{d}
原理解析清楚了,下面放出源碼與註釋。

源碼及註釋

首先還是contrastive_loss_layer.hpp的源碼:

#ifndef CAFFE_CONTRASTIVE_LOSS_LAYER_HPP_
#define CAFFE_CONTRASTIVE_LOSS_LAYER_HPP_

#include <vector>

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

#include "caffe/layers/loss_layer.hpp"

namespace caffe {

/**
 * @brief Computes the contrastive loss @f$
 *          E = \frac{1}{2N} \sum\limits_{n=1}^N \left(y\right) d^2 +
 *              \left(1-y\right) \max \left(margin-d, 0\right)^2
 *          @f$ where @f$
 *          d = \left| \left| a_n - b_n \right| \right|_2 @f$. This can be
 *          used to train siamese networks.
 *
 * @param bottom input Blob vector (length 3)
 *   -# @f$ (N \times C \times 1 \times 1) @f$
 *      the features @f$ a \in [-\infty, +\infty]@f$
 *   -# @f$ (N \times C \times 1 \times 1) @f$
 *      the features @f$ b \in [-\infty, +\infty]@f$
 *   -# @f$ (N \times 1 \times 1 \times 1) @f$
 *      the binary similarity @f$ s \in [0, 1]@f$
 * @param top output Blob vector (length 1)
 *   -# @f$ (1 \times 1 \times 1 \times 1) @f$
 *      the computed contrastive loss: @f$ E =
 *          \frac{1}{2N} \sum\limits_{n=1}^N \left(y\right) d^2 +
 *          \left(1-y\right) \max \left(margin-d, 0\right)^2
 *          @f$ where @f$
 *          d = \left| \left| a_n - b_n \right| \right|_2 @f$.
 * This can be used to train siamese networks.
 */
template <typename Dtype>
class ContrastiveLossLayer : public LossLayer<Dtype> {
 public:
  explicit ContrastiveLossLayer(const LayerParameter& param)
      : LossLayer<Dtype>(param), diff_() {} //空的構造函數
  virtual void LayerSetUp(const vector<Blob<Dtype>*>& bottom,
      const vector<Blob<Dtype>*>& top); //LayerSetUp函數

  virtual inline int ExactNumBottomBlobs() const { return 3; } //輸入必須是3個Blob,即a, b和sim
  virtual inline const char* type() const { return "ContrastiveLoss"; }
  /**
   * Unlike most loss layers, in the ContrastiveLossLayer we can backpropagate
   * to the first two inputs.
   */
  virtual inline bool AllowForceBackward(const int bottom_index) const {
    return bottom_index != 2;
  } //允許在第0個和第1個輸入Blob上進行強制反傳

 protected:
  /// @copydoc ContrastiveLossLayer
  virtual void Forward_cpu(const vector<Blob<Dtype>*>& bottom,
      const vector<Blob<Dtype>*>& top); //cpu前傳
  virtual void Forward_gpu(const vector<Blob<Dtype>*>& bottom,
      const vector<Blob<Dtype>*>& top); //gpu前傳

  /**
   * @brief Computes the Contrastive error gradient w.r.t. the inputs.
   *
   * Computes the gradients with respect to the two input vectors (bottom[0] and
   * bottom[1]), but not the similarity label (bottom[2]).
   *
   * @param top output Blob vector (length 1), providing the error gradient with
   *      respect to the outputs
   *   -# @f$ (1 \times 1 \times 1 \times 1) @f$
   *      This Blob's diff will simply contain the loss_weight* @f$ \lambda @f$,
   *      as @f$ \lambda @f$ is the coefficient of this layer's output
   *      @f$\ell_i@f$ in the overall Net loss
   *      @f$ E = \lambda_i \ell_i + \mbox{other loss terms}@f$; hence
   *      @f$ \frac{\partial E}{\partial \ell_i} = \lambda_i @f$.
   *      (*Assuming that this top Blob is not used as a bottom (input) by any
   *      other layer of the Net.)
   * @param propagate_down see Layer::Backward.
   * @param bottom input Blob vector (length 2)
   *   -# @f$ (N \times C \times 1 \times 1) @f$
   *      the features @f$a@f$; Backward fills their diff with
   *      gradients if propagate_down[0]
   *   -# @f$ (N \times C \times 1 \times 1) @f$
   *      the features @f$b@f$; Backward fills their diff with gradients if
   *      propagate_down[1]
   */
  virtual void Backward_cpu(const vector<Blob<Dtype>*>& top,
      const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom); //cpu反傳
  virtual void Backward_gpu(const vector<Blob<Dtype>*>& top,
      const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom); //gpu反傳

  Blob<Dtype> diff_;  // cached for backward pass 反傳時使用的存儲a和b差值的Blob
  Blob<Dtype> dist_sq_;  // cached for backward pass 反傳時使用的存儲a和b歐式距離平方的Blob
  Blob<Dtype> diff_sq_;  // tmp storage for gpu forward pass gpu前傳時所需的暫存Blob
  Blob<Dtype> summer_vec_;  // tmp storage for gpu forward pass gpu前傳時所需的暫存Blob
};

}  // namespace caffe

#endif  // CAFFE_CONTRASTIVE_LOSS_LAYER_HPP_

}

然後是contrastive_loss_layer.cpp的源碼:

#include <algorithm>
#include <vector>

#include "caffe/layers/contrastive_loss_layer.hpp"
#include "caffe/util/math_functions.hpp"

namespace caffe {

template <typename Dtype>
void ContrastiveLossLayer<Dtype>::LayerSetUp( //LayerSetUp函數,進行部分初始化工作
  const vector<Blob<Dtype>*>& bottom, const vector<Blob<Dtype>*>& top) {
  LossLayer<Dtype>::LayerSetUp(bottom, top);
  CHECK_EQ(bottom[0]->channels(), bottom[1]->channels()); //檢查輸入數據a和b的通道是否相同,注意沒有保證a和b的個數是否相同
  CHECK_EQ(bottom[0]->height(), 1); //height和width的檢查確保a是一個向量
  CHECK_EQ(bottom[0]->width(), 1);
  CHECK_EQ(bottom[1]->height(), 1); //height和width的檢查確保b是一個向量
  CHECK_EQ(bottom[1]->width(), 1);
  CHECK_EQ(bottom[2]->channels(), 1); //channel,height和width的檢查確保sim是一個值,0表示data0和data1不同類,1表示同類
  CHECK_EQ(bottom[2]->height(), 1);
  CHECK_EQ(bottom[2]->width(), 1);
  diff_.Reshape(bottom[0]->num(), bottom[0]->channels(), 1, 1); //diff_ Blob形狀初始化爲(n, c, 1, 1)
  diff_sq_.Reshape(bottom[0]->num(), bottom[0]->channels(), 1, 1); //diff_sq_ Blob形狀同樣初始化爲(n, c, 1, 1)
  dist_sq_.Reshape(bottom[0]->num(), 1, 1, 1); //dist_sq_ Blob形狀初始化爲(n, 1, 1, 1),用來記錄距離
  // vector of ones used to sum along channels
  summer_vec_.Reshape(bottom[0]->channels(), 1, 1, 1); //summer_vec_ Blob形狀初始化爲(n, 1, 1, 1)
  for (int i = 0; i < bottom[0]->channels(); ++i)
    summer_vec_.mutable_cpu_data()[i] = Dtype(1); //初始化一下summer_vec_中的值,全部初始化爲1
}

template <typename Dtype>
void ContrastiveLossLayer<Dtype>::Forward_cpu(
    const vector<Blob<Dtype>*>& bottom,
    const vector<Blob<Dtype>*>& top) { //對比損失計算前傳函數
  int count = bottom[0]->count(); //首先取得a和b的數據量
  caffe_sub(
      count,
      bottom[0]->cpu_data(),  // a
      bottom[1]->cpu_data(),  // b
      diff_.mutable_cpu_data());  // a_i-b_i 對a和b逐元相減,並將結果存儲在diff_ Blob中
  const int channels = bottom[0]->channels(); //取得a的通道數
  Dtype margin = this->layer_param_.contrastive_loss_param().margin(); //取得層設置文件中的margin參數
  bool legacy_version =
      this->layer_param_.contrastive_loss_param().legacy_version(); //取得層設置文件中的legacy_version參數
  Dtype loss(0.0); //初始化loss爲0
  for (int i = 0; i < bottom[0]->num(); ++i) { //在一個batch中逐對進行計算
    dist_sq_.mutable_cpu_data()[i] = caffe_cpu_dot(channels,
        diff_.cpu_data() + (i*channels), diff_.cpu_data() + (i*channels)); //首先計算a和b之差的二範數的平方,也稱爲a和b歐式距離(d)的平方,記爲d^2
    if (static_cast<int>(bottom[2]->cpu_data()[i])) {  // similar pairs //如果sim爲1,表示a和b相同
      loss += dist_sq_.cpu_data()[i]; //直接在loss上加上d^2
    } else {  // dissimilar pairs //如果sim爲0,表示a和b不同
      if (legacy_version) { //如果legacy_version參數等於true
        loss += std::max(margin - dist_sq_.cpu_data()[i], Dtype(0.0)); //loss直接加上max{margin-d^2, 0}
      } else { //如果legacy_version參數等於false
        Dtype dist = std::max<Dtype>(margin - sqrt(dist_sq_.cpu_data()[i]), //計算max{margin-d, 0}
          Dtype(0.0));
        loss += dist*dist; //loss直接加上上式中最大值的平方
      }
    }
  }
  loss = loss / static_cast<Dtype>(bottom[0]->num()) / Dtype(2); //loss值除以2n
  top[0]->mutable_cpu_data()[0] = loss; //將loss值賦予top[0]
}

template <typename Dtype>
void ContrastiveLossLayer<Dtype>::Backward_cpu(const vector<Blob<Dtype>*>& top,
    const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom) { //對比損失計算反傳函數
  Dtype margin = this->layer_param_.contrastive_loss_param().margin(); //取得層設置文件中的margin參數
  bool legacy_version =
      this->layer_param_.contrastive_loss_param().legacy_version(); //取得層設置文件中的legacy_version參數
  for (int i = 0; i < 2; ++i) { //在a和b上分別進行反傳
    if (propagate_down[i]) { //如果梯度需要反傳到該Blob上
      const Dtype sign = (i == 0) ? 1 : -1; //如果是計算反傳到a的梯度,sign爲1,;如果是計算反傳到b的梯度,sign爲-1
      const Dtype alpha = sign * top[0]->cpu_diff()[0] /
          static_cast<Dtype>(bottom[i]->num()); //計算sign * top_diff / n,存儲在alpha中
      int num = bottom[i]->num(); //獲得該Blob的n(一個batch中的前傳向量個數)
      int channels = bottom[i]->channels(); //獲得該Blob的c(通道數)
      for (int j = 0; j < num; ++j) { //在batch中一個一個來計算
        Dtype* bout = bottom[i]->mutable_cpu_diff(); //bout存儲梯度計算的結果
        if (static_cast<int>(bottom[2]->cpu_data()[j])) {  // similar pairs 如果是同類的圖像對
          caffe_cpu_axpby(
              channels,
              alpha,
              diff_.cpu_data() + (j*channels),
              Dtype(0.0),
              bout + (j*channels)); //計算sign * diff_ * top_diff / n,並存儲在bout中
        } else {  // dissimilar pairs 如果是不同的圖像對
          Dtype mdist(0.0); //初始化mdist爲0
          Dtype beta(0.0); //初始化beta爲0
          if (legacy_version) { //如果legacy_version參數等於true
            mdist = margin - dist_sq_.cpu_data()[j]; //mdist爲margin-d^2
            beta = -alpha; //beta爲-sign * top_diff / n
          } else { //如果legacy_version參數等於false
            Dtype dist = sqrt(dist_sq_.cpu_data()[j]); //dist爲d
            mdist = margin - dist; //mdist爲margin-d
            beta = -alpha * mdist / (dist + Dtype(1e-4)); //beta爲-sign * top_diff * (margin-d) / (n * d)
          }
          if (mdist > Dtype(0.0)) { //如果mdist參數大於0
            caffe_cpu_axpby(
                channels,
                beta,
                diff_.cpu_data() + (j*channels),
                Dtype(0.0),
                bout + (j*channels)); //計算beta * diff_,並存儲在bout中
          } else {
            caffe_set(channels, Dtype(0), bout + (j*channels)); //如果前傳時loss輸出0,那麼梯度直接置0
          }
        }
      }
    }
  }
}

#ifdef CPU_ONLY
STUB_GPU(ContrastiveLossLayer);
#endif

INSTANTIATE_CLASS(ContrastiveLossLayer);
REGISTER_LAYER_CLASS(ContrastiveLoss);

}  // namespace caffe

}

源碼分析

由於前傳和反傳的原理在源碼之上已經給出了,在這裏筆者就給出相對簡略的分析了。

首先在前傳部分,在contrastive_loss_layer.cpp的Forward_cpu函數中,將a與b歐式距離的平方存儲在了dist_sq_中,sim則表示爲static_cast(bottom[2]->cpu_data()[i]),按照sim區分進行損失值計算。當sim不爲0時,直接在loss上加上dist_sq_;否則區分legacy_version分別進行loss的計算。

然後,在反傳部分,在contrastive_loss_layer.cpp的Backward_cpu函數中,通過sign來區分給a和b反傳的梯度。接着,首先計算好alpha=sign * top_diff / N,alpha在之後計算梯度中各種情況都會使用到。然後還是先按照sim的值區分梯度進行計算,如果sim不爲0,那麼直接通過caffe_cpu_axpby函數計算出梯度值,注意,diff_指的就是公式中的aibia_i-b_i

if (static_cast<int>(bottom[2]->cpu_data()[j])) {  // similar pairs 如果是同類的圖像對
          caffe_cpu_axpby(
              channels,
              alpha,
              diff_.cpu_data() + (j*channels),
              Dtype(0.0),
              bout + (j*channels)); //計算sign * diff_ * top_diff / n,並存儲在bout中
        }

如果sim不爲0,那麼就根據legacy_version進行判斷,分別按照公式輸出相應的梯度值,

if (legacy_version) { //如果legacy_version參數等於true
            mdist = margin - dist_sq_.cpu_data()[j]; //mdist爲margin-d^2
            beta = -alpha; //beta爲-sign * top_diff / n
          } else { //如果legacy_version參數等於false
            Dtype dist = sqrt(dist_sq_.cpu_data()[j]); //dist爲d
            mdist = margin - dist; //mdist爲margin-d
            beta = -alpha * mdist / (dist + Dtype(1e-4)); //beta爲-sign * top_diff * (margin-d) / (n * d)
          }
          if (mdist > Dtype(0.0)) { //如果mdist參數大於0
            caffe_cpu_axpby(
                channels,
                beta,
                diff_.cpu_data() + (j*channels),
                Dtype(0.0),
                bout + (j*channels)); //計算beta * diff_,並存儲在bout中
          }

最後,在前傳時loss輸出0時,反傳的梯度也置0。

else {
            caffe_set(channels, Dtype(0), bout + (j*channels)); //如果前傳時loss輸出0,那麼梯度直接置0
          }

各位讀者朋友可以對照公式看看,求導結果和代碼實現一模一樣

寫在最後

到這裏,整篇博文就接近尾聲了。

本篇博文是筆者第一次解析深度學習中相對複雜的包含求導邏輯的梯度反傳代碼。整體解析下來,筆者自己感覺收穫非常大。隨着深度學習的快速發展,TensorFlow,PyTorch等高層包裝,上手速度快的不需要用戶實現梯度反傳的框架能解決我們很多科研與項目問題,但是作爲深度學習研究者和工程師,很有必要懂得梯度反傳代碼的邏輯與實現細節。這樣,纔不會囿於框架內部。尤其是在進行科學研究與算法探索時,更應該對代碼底層實現與數學原理加深理解。

歡迎閱讀筆者後續博客,各位讀者朋友的支持與鼓勵是我最大的動力!

written by jiong
爲有犧牲多壯志,敢教日月換新天

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章