1. 程式人生 > >YOLOv2原始碼分析(四)

YOLOv2原始碼分析(四)

文章全部YOLOv2原始碼分析

0x01 backward_convolutional_layer

void backward_convolutional_layer(convolutional_layer l, network net)
{
    int i, j;
    int m = l.n/l.groups;               //每組卷積核的個數
    int n = l.size*l.size*l.c/l.groups; //每組卷積核的元素個數
    int k = l.out_w*l.out_h;            //輸出影象的元素個數
gradient_array(l.output, l.outputs*l.batch, l.activation, l.delta);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

這裡出現了一個gradient_array函式,我們看看這個函式有什麼作用。

float gradient(float x, ACTIVATION a)
{
    switch(a){
        case LINEAR:
            return linear_gradient(x);
        case LOGISTIC:
            return
logistic_gradient(x); case LOGGY: return loggy_gradient(x); case RELU: return relu_gradient(x); case ELU: return elu_gradient(x); case RELIE: return relie_gradient(x); case RAMP: return ramp_gradient(x); case
LEAKY: return leaky_gradient(x); case TANH: return tanh_gradient(x); case PLSE: return plse_gradient(x); case STAIR: return stair_gradient(x); case HARDTAN: return hardtan_gradient(x); case LHTAN: return lhtan_gradient(x); } return 0; } void gradient_array(const float *x, const int n, const ACTIVATION a, float *delta) { int i; for(i = 0; i < n; ++i){ delta[i] *= gradient(x[i], a); } } static inline float linear_gradient(float x){return 1;} static inline float logistic_gradient(float x){return (1-x)*x;} static inline float relu_gradient(float x){return (x>0);} static inline float elu_gradient(float x){return (x >= 0) + (x < 0)*(x + 1);} static inline float relie_gradient(float x){return (x>0) ? 1 : .01;} static inline float ramp_gradient(float x){return (x>0)+.1;} static inline float leaky_gradient(float x){return (x>0) ? 1 : .1;} static inline float tanh_gradient(float x){return 1-x*x;} static inline float plse_gradient(float x){return (x < 0 || x > 1) ? .01 : .125;}
  • 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

這個函式的作用很明顯,就是將layer的輸出影象,輸入到相應的梯度下降演算法(計算每一個元素對應啟用函式的導數),最後將delta的每個元素乘以啟用函式的導數,結果送到delta指向的記憶體中。

舉個例子

m=2 n=3x3=9 k=3x3=9
x [95 107 107 95]
relu啟用
delta[0]=95 delta[1]=107 delta[2]=107 delta[3]=105
  
  • 1
  • 2
  • 3
  • 4

接著往後

    if(l.batch_normalize){
        backward_batchnorm_layer(l, net);
  
  • 1
  • 2

出現這個backward_batchnorm_layer函式

0x0101 backward_batchnorm_layer

void backward_batchnorm_layer(layer l, network net)
{
    if(!net.train){
        l.mean = l.rolling_mean;
        l.variance = l.rolling_variance;
    }
    backward_bias(l.bias_updates, l.delta, l.batch, l.out_c, l.out_w*l.out_h);
    backward_scale_cpu(l.x_norm, l.delta, l.batch, l.out_c, l.out_w*l.out_h, l.scale_updates);

    scale_bias(l.delta, l.scales, l.batch, l.out_c, l.out_h*l.out_w);

    mean_delta_cpu(l.delta, l.variance, l.batch, l.out_c, l.out_w*l.out_h, l.mean_delta);
    variance_delta_cpu(l.x, l.delta, l.mean, l.variance, l.batch, l.out_c, l.out_w*l.out_h, l.variance_delta);
    normalize_delta_cpu(l.x, l.mean, l.variance, l.mean_delta, l.variance_delta, l.batch, l.out_c, l.out_w*l.out_h, l.delta);
    if(l.type == BATCHNORM) copy_cpu(l.outputs*l.batch, l.delta, 1, net.delta, 1);
}
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

這裡面也有很多函式,我們一一分析

void backward_bias(float *bias_updates, float *delta, int batch, int n, int size)
{
    int i,b;
    for(b = 0; b < batch; ++b){
        for(i = 0; i < n; ++i){
            bias_updates[i] += sum_array(delta+size*(i+b*n), size);
        }
    }
}
float sum_array(float *a, int n)//計算輸入陣列的和
{
    int i;
    float sum = 0;
    for(i = 0; i < n; ++i) sum += a[i];
    return sum;
}
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • bias_updates:指向權重更新的指標
  • delta:指向前面梯度下降得到的累乘值
  • batch:batch大小
  • n:layer的輸出通道數目
  • size:輸出影象的元素個數

這個函式就是計算偏置的更新值(誤差函式對偏置的導數),將delta對應同一個卷積核的項相加。

這個函式想要描述的就是這個公式

  • Lβ=m0Lyi∂L∂β=∑0m∂L∂yi
void backward_scale_cpu(float *x_norm, float *delta, int batch, int n, int size, float *scale_updates)
{
    int i,b,f;
    for(f = 0; f < n; ++f){
        float sum = 0;
        for(b = 0; b < batch; ++b){
            for(i = 0; i < size; ++i){
                int index = i + size*(f + n*b);
                sum += delta[index] * x_norm[index];
            }
        }
        scale_updates[f] += sum;
    }
}
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

這個函式想要描述的就是這個公式

  • Lγ=m0lyixi^∂L∂γ=∑0m∂l∂yixi^
void mean_delta_cpu(float *delta, float *variance, int batch, int filters, int spatial, float *mean_delta)
{

    int i,j,k;
    for(i = 0; i < filters; ++i){
        mean_delta[i] = 0;
        for (j = 0; j < batch; ++j) {
            for (k = 0; k < spatial; ++k) {
                int index = j*filters*spatial + i*spatial + k;
                mean_delta[i] += delta[index];
            }
        }
        mean_delta[i] *= (-1./sqrt(variance[i] + .00001f));
    }
}
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

這個函式想要描述的就是這個公式

  • Lmean=m0Lyiyimean=m0Lyi1var+eps∂L∂mean=∑0m∂L∂yi∂yi∂mean=∑0m∂L∂yi−1var+eps
void  variance_delta_cpu(float *x, float *delta, float *mean, float *variance, int batch, int filters, int spatial, float *variance_delta)
{

    int i,j,k;
    for(i = 0; i < filters; ++i){
        variance_delta[i] = 0;
        for(j = 0; j < batch; ++j){
            for(k = 0; k < spatial; ++k){
                int index = j*filters*spatial + i*spatial + k;
                variance_delta[i] += delta[index]*(x[index] - mean[i]);
            }
        }
        variance_delta[i] *= -.5 * pow(variance[i] + .00001f, (float)(-3./2.));
    }
}
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

這個函式想要描述的就是這個公式

  • Lvar=m0Lyi(ximean)(12)(var+eps)32∂L∂var=∑0m∂L∂yi(xi−mean)(−12)(var+eps)−32
void normalize_delta_cpu(float *x, float *mean, float *variance, float *mean_delta, float *variance_delta, int batch, int filters, int spatial, float *delta)
{
    int f, j, k;
    for(j = 0; j < batch; ++j){
        for(f = 0; f < filters; ++f){
            for(k = 0; k < spatial; ++k){
                int index = j*filters*spatial + f*spatial + k;
                delta[index] = delta[index] * 1./(sqrt(variance[f] + .00001f)) + variance_delta[f] * 2. * (x[index] - mean[f]) / (spatial * batch) + mean_delta[f]/(spatial*batch);
            }
        }
    }
}
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

這個函式想要描述的就是這個公式

  • Lxi=Lyi1var+eps+Lvarvarxi+Lmeanmeanxi=Lyi1var+eps+Lvar2m(ximean)+Lmean1m∂L∂xi=∂L∂yi1var+eps+∂L∂var∂var∂xi+∂L∂mean∂mean∂xi=∂L∂yi1var+eps+∂L∂var2m(xi−mean)+∂L∂mean1m

回到backward_convolutional_layer函式

//backward_convolutional_layer  
    if(l.batch_normalize){
        backward_batchnorm_layer(l, net);
    } else {
        backward_bias(l.bias_updates, l.delta, l.batch, l.n, k);
    }    
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

如果沒有定義batch_normalize的話,直接更新bias就完事了。

接著往後

    //backward_convolutional_layer
    for(i = 0; i < l.batch; ++i){
        for(j = 0; j < l.groups; ++j){
            float *a = l.delta + (i*l.groups + j)*m*k;
            float *b = net.workspace;
            float *c = l.weight_updates + j*l.nweights/l.groups;

            float *im = net.input+(i*l.groups + j)*l.c/l.groups*l.h*l.w;

            im2col_cpu(im, l.c/l.groups, l.h, l.w, 
                    l.size, l.stride, l.pad, b);
            gemm(0,1,m,n,k,1,a,k,b,k,1,c,n);

            if(net.delta){
                a = l.weights + j*l.nweights/l.groups;
                b = l.delta + (i*l.groups + j)*m*k;
                c = net.workspace;

                gemm(1,0,n,k,m,1,a,n,b,k,0,c,k);

                col2im_cpu(net.workspace, l.c/l.groups, l.h, l.w, l.size, l.stride, 
                    l.pad, net.delta + (i*l.groups + j)*l.c/l.groups*l.h*l.w);
            }
        }
    }
}
  
  • 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

首先我們這裡先說明一個問題,細心的同學會發現,這裡使用的是gemm(0,1...),也就是我在(三)中說過的,我這裡對B進行了轉置操作。為什麼要這樣做呢?

先看看引數是什麼意思。

int m = l.n/l.groups;               //每組卷積核的個數
int n = l.size*l.size*l.c/l.groups; //每組卷積核的元素個數
int k = l.out_w*l.out_h;            //輸出影象的元素個數
  
  • 1
  • 2
  • 3

a指向一個groupl.delta的一行,元素個數為(l.out_c)*(l.out_h*l.out_w)b指向儲存結果的記憶體,大小是(l.c*l.size*l.size)*(l.out_h*l.out_w)c指向一個groupweight的一行,大小是(l.n)*(l.c*l.size*l.size)gemm描述的是這樣一種運算a*b+c。所以根據矩陣運算的原理,這裡的b要進行轉置操作。

那麼這裡的卷積作用也就非常明顯了,就是就算當前層的權重更新c=alpha*a*b+beta*c

和之前的forward_convolutional_layer函式引數對比

int m = l.n/l.groups;//一個group的卷積核個數
int k = l.size*l.size*l.c/l.groups;//一個group的卷積核元素個數
int n = l.out_w*l.out_h;//一個輸出影象的元素個數
float *a = l.weights + j*l.nweights/l.groups;
float *b = net.workspace;
float *c = l.output + (i*l.groups + j)*n*m;
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

接著看後面這個判斷語句中的內容

//backward_convolutional_layer
if(net.delta){
    a = l.weights + j*l.nweights/l.groups;//注意此時權重沒有更新,我們上面算的是放在了weight_updates裡面
    b = l.delta + (i*l.groups + j)*m*k;
    c = net.workspace;

    gemm(1,0,n,k,m,1,a,n,b,k,0,c,k);

    col2im_cpu(net.workspace, l.c/l.groups, l.h, l.w, l.size, l.stride, 
               l.pad, net.delta + (i*l.groups + j)*l.c/l.groups*l.h*l.w);
}
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

我們看看這裡的gemm和前面的有什麼區別,首先看引數。這裡的a對應上面的cb對應上面的ac對應上面的b

a指向weight,大小是(l.n)*(l.c*l.size*l.size);b指向delta,大小是(l.out_c)*(l.out_h*l*out_w)c指向輸出儲存空間,大小是(l.c*l.size*l.size)*(l.out_h*l.out_w)。那麼這裡呼叫gemm(1,0...)就很好理解了,最後完成c=alpha*a*b+beta*c操作,也就是更新workspace的工作。

那麼這裡這個函式到底有什麼意義呢?

通過上面那個c=alpha*a*b+beta*c給我們的直觀感受,就是將當前層的delta和上一層weights進行卷積操作,這個得到的結果意義不是很大,但是他經過之前說的gradient_array操作後就有了非常重要的意義,就是上一層的delta。為什麼?這就要從bp演算法開始說了

0x02 卷積層誤差傳遞

首先舉個例子



我們假設輸入是A,卷積核是W,輸出是C,啟用函式是f,偏向是B,我們可以知道

  • out=WA+Bout=W∗A+B

我們假設損失函式是L,那麼可以計算出誤差項ΔΔ

  • Δ=LoutΔ=∂L∂out

好的現在我們要求解l-1層的ΔΔ​

  • Δl1=Loutl1=LCl1Cl1outl1=LCl1f(outl1)Δl−1=∂L∂outl−1=∂L∂Cl−1∂Cl−1∂outl−1=∂L∂Cl−1f′(outl−1)



由上面這個圖不難看出

  • LCl1=ΔlW∂L∂Cl−1=Δl∗W

所以

  • Δl1=ΔlWf(outl1)Δl−1=Δl∗W∘f′(outl−1)

回到backward_convolutional_layer這個函式

    //backward_convolutional_layer
    col2im_cpu(net.workspace, l.c/l.groups, l.h, l.w, l.size, l.stride, 
               l.pad, net.delta + (i*l.groups + j)*l.c/l.groups*l.h*l.w);
  
  • 1
  • 2
  • 3

最後這個函式col2im_cpu的作用就是將net.workspace重排,類似於(三)中的im2col_cpu,只是這裡反過來了。

0x03 update_convolutional_layer

void update_convolutional_layer(convolutional_layer l, update_args a)
{
    float learning_rate = a.learning_rate*l.learning_rate_scale;
    float momentum = a.momentum;
    float decay = a.decay;
    int batch = a.batch;

    axpy_cpu(l.n, learning_rate/batch, l.bias_updates, 1, l.biases, 1);
    scal_cpu(l.n, momentum, l.bias_updates, 1);

    if(l.scales){
        axpy_cpu(l.n, learni