1. 程式人生 > >caffe 原始碼分析【三】:Euclidean loss layer

caffe 原始碼分析【三】:Euclidean loss layer

以下是Euclidean loss layer的程式碼分析,轉自:

https://blog.csdn.net/seashell_9/article/details/68064294

一. 前向函式

template <typename Dtype>
void EuclideanLossLayer<Dtype>::Forward_gpu(const vector<Blob<Dtype>*>& bottom,
    const vector<Blob<Dtype>*>& top) {
  int count = bottom[0]->count(); //這裡的count就是你的batchsize的大小
  caffe_gpu_sub(
      count,                 
      bottom[0]->gpu_data(), //網路的輸出值
      bottom[1]->gpu_data(), //標籤值
      diff_.mutable_gpu_data());//儲存bottom[0] - bottom[1]
  Dtype dot;
  caffe_gpu_dot(count, diff_.gpu_data(), diff_.gpu_data(), &dot);//做點乘運算
  Dtype loss = dot / bottom[0]->num() / Dtype(2); //除以總數再除以2
  top[0]->mutable_cpu_data()[0] = loss; //將loss值賦給輸出
}

1.Euclidean loss函式

首先明確caffe中Euclidean loss函式:(多除了個2,方便後面求梯度時剛好約掉)

其次是程式碼裡面一些變數的意義:

count: count其實等於num*channels*height*width,也就是整個Blob元素的數量,但是因為此層中channels height width都為1,所以這裡的count()與num()實際上是相等的,都代表輸入圖片的數量,也就是batchsize的大小,也即公式裡的N

bottom[0]->gpu_data(): 網路的輸出值,注意這個變數並不只是單個的輸出值,而是包含整個batchsize每張圖片的輸出值,也就是一個含有N個元素的向量

bottom[1]->gpu_data(): 真實標籤值,也是個含有N個元素的向量

diff_.mutable_gpu_data(): 上述兩個向量做元素減法得到的向量。這裡說明一下為什麼是diff_.mutable_gpu_data()而不是diff_.gpu_data(),因為caffe定義了gpu_data()  為只讀變數,而mutable_gpu_data()為可變變數,也就是說讀操作用gpu_data(),寫操作用mutable_gpu_data()

dot: 對diff_.gpu_data()進行點乘運算得到的值(點乘運算得到的是一個數值)

top[0]->mutable_cpu_data()[0]:該層的輸出給下一層的變數。這裡top[0]的“0”指的是第一個輸出值(就像上面bottom[0]指第一個輸入值,bottom[1]指第二個輸入值),由於這個層只有一個輸出值,因此也就只有0這個索引

2.caffe_gpu_sub()函式

在math_function.cu裡可以查到caffe_gpu_sub()函式,如下:

template <>
void caffe_gpu_sub<float>(const int N, const float* a, const float* b,
    float* y) {
  // NOLINT_NEXT_LINE(whitespace/operators)
  sub_kernel<float><<<CAFFE_GET_BLOCKS(N), CAFFE_CUDA_NUM_THREADS>>>(
      N, a, b, y);
}

可以看到,該函式又呼叫了sub_kernel函式:

template <typename Dtype>
__global__ void sub_kernel(const int n, const Dtype* a,
    const Dtype* b, Dtype* y) {
  CUDA_KERNEL_LOOP(index, n) {
    y[index] = a[index] - b[index];
  }
}

從這個函式就可以明白了,caffe_gpu_sub()就是做了這樣一個運算:把a向量和b向量對應元素相減,然後賦給y向量。放到我們Euclidean loss程式碼裡面,也就是bottom[0]->gpu_data()(網路輸出值)和bottom[1]->gpu_data()(真實標籤值)做對應元素相減,然後賦給diff_.mutable_gpu_data()
3.caffe_gpu_dot()函式:

template <>
void caffe_gpu_dot<float>(const int n, const float* x, const float* y,
    float* out) {
  CUBLAS_CHECK(cublasSdot(Caffe::cublas_handle(), n, x, 1, y, 1, out));
}

可以看到呼叫了cublasSdot()函式,在cuda文件中看到該函式的作用:

即計算兩個輸入向量的點乘。最後就是除以2N了

二.反向函式

template <typename Dtype>
void EuclideanLossLayer<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]) {    //對於輸入的第i個Blob propagate_dowm 為1(該變數即為該Blob輸入後是否要向前面的層提供反向傳播的梯度)
      const Dtype sign = (i == 0) ? 1 : -1; 
      const Dtype alpha = sign * top[0]->cpu_diff()[0] / bottom[i]->num();
      caffe_gpu_axpby(
          bottom[i]->count(),              // count
          alpha,                              // alpha
          diff_.gpu_data(),                   // a
          Dtype(0),                           // beta
          bottom[i]->mutable_gpu_diff());  // b
    }
  }
}

1.因為loss層沒有引數,所以求導時是對兩個輸入求偏導,即:

Derive (X1) = [(x11-x21)+…+(x1n-x2n)]/N

Derive (X2) = -[(x11-x21)+…+(x1n-x2n)]/N(注意前面有個負號)

所以程式碼中for迴圈兩次就是分別對X1和X2求偏導

2.propagate_down[i]:這裡兩個propagate_down都是1的。如果要了解propagate_down的含義,可以看來自百度的一個描述:

caffe中怎麼固定前面的網路引數,訓練後面層的引數? 
這裡面就用到了propagate_down, 有兩種情況:比如有4個全連線層A->B->C->D 
a. 你希望C層的引數不會改變,C前面的AB層的引數也不會改變,這種情況也就是D層的梯度不往前反向傳播到D層的輸入blob(也就是C層的輸出blob 沒有得到梯度),你可以通過設定D層的propagate_down為false來做到。 

propagate_down的數量與輸入blob的數量相同,假如你某個層有2個輸入blob,那麼你應該在該layer的Param裡面寫上兩行: 

propagate_down : 0 # 第1個輸入blob不會得到反向傳播的梯度 
propagate_down : 0 # 第2個輸入blob不會得到反向傳播的梯度 
這樣的話,你這個layer的梯度就不會反向傳播啦,前面的所有layer的引數也就不會改變了 
b. 你希望C層的引數不會改變,但是C前面的AB層的引數會改變,這種情況,只是固定了C層的引數,C層得到的梯度依然會反向傳播給前面的B層。只需要將對應的引數blob的學習率調整為0: 

layer { 
type: "InnerProduct" 
    param { # 對應第1個引數blob的配置,也就是全連線層的引數矩陣的配置 
         lr_mult: 0 # 學習率為0,其他引數可以看caffe.proto裡面的ParamSpec這個型別 
    } 
    param { # 對應第2個引數blob的配置,也就是全連線層的偏置項的配置 
        lr_mult: 0 # 學習率為0 
    } 
} 

3.sign的作用:

   第一次求偏導是對X1,前面不需要加負號;第二次求偏導是對X2,前面需要乘-1

4.top[0]->cpu_diff()[0]:

在反向傳播中,top代表從高一層反向傳過來的變數,所以top[0]->cpu_diff()表示從高一層傳過來的error。但問題來了,這明明是loss層,也就是最後一層,為什麼還有所謂的再高一層呢?其實大家可以發現,這裡用的是top[0]->cpu_diff()[0],而不是top[0]->cpu_diff()。caffe中反向傳給低層error時其實使用者還可以給這個error乘以一個倍數,這個倍數就儲存在top[0]->cpu_diff()的第一個元素,也就是top[0]->cpu_diff()[0]。而使用者設定這個倍數則是通過在layer引數中新增loss_weight引數

layer {
  name: "loss"
  type: "SoftmaxWithLoss"
  bottom: "pred"
  bottom: "label"
  top: "loss"
  loss_weight: 1
}

預設的loss_weight都是1

參見:http://stackoverflow.com/questions/31099233/euclidean-loss-layer-in-caffe

5.caffe_gpu_axpby()函式:

template <>
void caffe_gpu_axpby<float>(const int N, const float alpha, const float* X,
    const float beta, float* Y) {
  caffe_gpu_scal<float>(N, beta, Y); // Y = beta*Y
  caffe_gpu_axpy<float>(N, alpha, X, Y);// Y = Y + alpha*X
}

做的運算其實就是bottom[i]->mutable_gpu_diff() = alpha*diff.gpu_data() + beta*bottom[i]->mutable_gpu_diff() = alpha*diff.gpu_data() (因為beta = 0)