天天看點

【darknet源碼解析-25】local_layer.h 和 local_layer.c 解析

本系列為darknet源碼解析,本次解析為src/local_layer.h 和 src/local_layer.c 兩個,local_layer主要是建構局部連接配接層。local_layer 與conv_layer 工作方式相同,除了權值不共享外, 也就是說,在輸入的每個不同部分應用不同的一組過濾器。local_layer僅用于yolo v1中;

局部連接配接層與卷積層有什麼異同:

如下圖所示:在卷積操作中,X[:,:,0]通道上的卷積滑動的每一處位置,一共有9個,權重W0[:,:,0]各處都是一樣的。而在局部連接配接層中是不共享的,也就是9處都是不一樣權重;

【darknet源碼解析-25】local_layer.h 和 local_layer.c 解析
  • 局部連接配接層前向傳播

局部連接配接操作如下圖所示:在這裡batch=1,輸入圖檔h*w為5*5,通道數c為3;卷積模闆數n為2,局部連接配接視窗大小size為3*3,卷積步幅stride為1,補零個數pad為1;

1. 計算局部後特征圖的大小?

如果pad不為0,則 out_h = (h -1)/stride + 1,out_w = (w -1)/stride + 1;

如果pad為0,則 out_h = (h - size)/stride + 1,out_w = (w -size)/stride + 1;

2. 局部連接配接層的權重值有多少個?n個卷積模闆,每個模闆權重參數個數 c*size*size*out_h*out_w;

3. 局部連接配接層有多少偏置值?out_w*out_h*c;

4. im2col(), gemm()在前向計算怎麼使用的?im2col()将每張圖檔重排為B=[c*size*size, out_w*out_h],n個卷積組成A=[n, c*size*size], gemm()完成卷積元素(實際上就是矩陣乘法)卷積後輸出結果C=A*B=[n, out_w*out_h];

  • 局部連接配接層反向傳播 

與卷積層一緻,這裡不展開,詳細見源碼解釋;

local_layer.h的聲明如下:

#ifndef LOCAL_LAYER_H
#define LOCAL_LAYER_H

#include "cuda.h"
#include "image.h"
#include "activations.h"
#include "layer.h"
#include "network.h"

typedef layer local_layer;

#ifdef GPU
void forward_local_layer_gpu(local_layer layer, network net);
void backward_local_layer_gpu(local_layer layer, network net);
void update_local_layer_gpu(local_layer layer, update_args a);

void push_local_layer(local_layer layer);
void pull_local_layer(local_layer layer);
#endif

// 構造local_layer層
local_layer make_local_layer(int batch, int h, int w, int c, int n, int size, int stride, int pad, ACTIVATION activation);

// local_layer層的正向,反向,更新函數
void forward_local_layer(const local_layer layer, network net);
void backward_local_layer(local_layer layer, network net);
void update_local_layer(local_layer layer, update_args a);

void bias_output(float *output, float *biases, int batch, int n, int size);
void backward_bias(float *bias_updates, float *delta, int batch, int n, int size);

#endif
           

local_layer.c 詳細解析如下:

#include "local_layer.h"
#include "utils.h"
#include "im2col.h"
#include "col2im.h"
#include "blas.h"
#include "gemm.h"
#include <stdio.h>
#include <time.h>

// 計算local層輸出特征圖的高度
int local_out_height(local_layer l)
{
    int h = l.h;
    if (!l.pad) h -= l.size;
    else h -= 1;
    return h/l.stride + 1;
}

// 計算local層輸出特征圖的寬度
int local_out_width(local_layer l)
{
    int w = l.w;
    if (!l.pad) w -= l.size;
    else w -= 1;
    return w/l.stride + 1;
}

/**
 * 建構yolo v1 local層
 * @param batch 每個batch含有圖檔的張數
 * @param h 輸入圖檔的高度
 * @param w 寬度
 * @param c 通道數
 * @param n 卷積核的個數
 * @param size 卷積核的大小
 * @param stride 卷積核的步幅
 * @param pad 補0的個數
 * @param activation 激活函數類型
 * @return
 */
local_layer make_local_layer(int batch, int h, int w, int c, int n, int size, int stride, int pad, ACTIVATION activation)
{
    int i;
    local_layer l = {0};
    l.type = LOCAL; //層類别

    l.h = h; // 輸入圖檔的高度
    l.w = w; // 輸入圖檔的寬度
    l.c = c; // 輸入圖檔的通道數
    l.n = n; // 卷積模闆數
    l.batch = batch; // 一個batch中包含圖檔的張數
    l.stride = stride; // 卷積步幅
    l.size = size; // 卷積核大小
    l.pad = pad; // 補0的個數

    int out_h = local_out_height(l); // 計算local層輸出的特征圖的高度
    int out_w = local_out_width(l);  // 計算local層輸出的特征圖的寬度
    int locations = out_h*out_w; // 一個通道輸出特征圖包含的元素個數
    l.out_h = out_h; // 輸出特征圖的高度
    l.out_w = out_w; // 輸出特征圖的寬度
    l.out_c = n; // 輸出特征圖的通道數
    l.outputs = l.out_h * l.out_w * l.out_c; // local層對應一張輸入圖檔輸出元素個數
    l.inputs = l.w * l.h * l.c; // local層一張輸入圖檔的元素個數

    l.weights = calloc(c*n*size*size*locations, sizeof(float)); // local層的權重個數, 此處就是比傳統conv多locations倍
    l.weight_updates = calloc(c*n*size*size*locations, sizeof(float)); // local層的權重更新值

    l.biases = calloc(l.outputs, sizeof(float)); // local層偏置的個數,輸出特征每個元素,其實都是卷積計算的結果;所有偏置有這麼多
    l.bias_updates = calloc(l.outputs, sizeof(float)); // local層的偏置更新值

    // float scale = 1./sqrt(size*size*c);
    float scale = sqrt(2./(size*size*c));
    for(i = 0; i < c*n*size*size; ++i) l.weights[i] = scale*rand_uniform(-1,1);

    l.output = calloc(l.batch*out_h * out_w * n, sizeof(float)); // local層所有輸出(包含整個batch的)
    l.delta  = calloc(l.batch*out_h * out_w * n, sizeof(float)); // local層誤差項(包含整個batch的)

    l.workspace_size = out_h*out_w*size*size*c;

    // local層的前向,反向,更新
    l.forward = forward_local_layer;
    l.backward = backward_local_layer;
    l.update = update_local_layer;

#ifdef GPU
    l.forward_gpu = forward_local_layer_gpu;
    l.backward_gpu = backward_local_layer_gpu;
    l.update_gpu = update_local_layer_gpu;

    l.weights_gpu = cuda_make_array(l.weights, c*n*size*size*locations);
    l.weight_updates_gpu = cuda_make_array(l.weight_updates, c*n*size*size*locations);

    l.biases_gpu = cuda_make_array(l.biases, l.outputs);
    l.bias_updates_gpu = cuda_make_array(l.bias_updates, l.outputs);

    l.delta_gpu = cuda_make_array(l.delta, l.batch*out_h*out_w*n);
    l.output_gpu = cuda_make_array(l.output, l.batch*out_h*out_w*n);

#endif
    l.activation = activation; // 激活函數類型

    fprintf(stderr, "Local Layer: %d x %d x %d image, %d filters -> %d x %d x %d image\n", h,w,c,n, out_h, out_w, n);

    return l;
}

void copy_cpu(int N, float *X, int INCX, float *Y, int INCY)
{
    int i;
    for(i = 0; i < N; ++i) Y[i*INCY] = X[i*INCX];
}

/**
 * local層的前向傳播函數
 * @param l 目前local層
 * @param net 整個網絡
 */
void forward_local_layer(const local_layer l, network net)
{
    int out_h = local_out_height(l);
    int out_w = local_out_width(l);
    int i, j;
    int locations = out_h * out_w;

    for(i = 0; i < l.batch; ++i){
        // l.output = l.biases
        copy_cpu(l.outputs, l.biases, 1, l.output + i*l.outputs, 1);
    }

    for(i = 0; i < l.batch; ++i){
        float *input = net.input + i*l.w*l.h*l.c; // 計算輸入圖檔的起始位置
        im2col_cpu(input, l.c, l.h, l.w, 
                l.size, l.stride, l.pad, net.workspace); // 對輸入圖檔進行重排, 重排後的大小[c*size*size, out_h*out_w]
        float *output = l.output + i*l.outputs; //計算輸出圖檔的起始位置 l.outputs = out_w * out_h * n
        for(j = 0; j < locations; ++j){ // locations = out_h * out_w, 這tm是一個一個的計算
            float *a = l.weights + j*l.size*l.size*l.c*l.n;
            float *b = net.workspace + j;// net

            float *c = output + j;

            int m = l.n;
            int n = 1;
            int k = l.size*l.size*l.c;

            gemm(0,0,m,n,k,1,a, k,b, locations,1, c,locations);
            //gemm_nn(m, n, k, 1,
            // a, k,
            // b, locations,
            // c, locations);
        }
    }
    activate_array(l.output, l.outputs*l.batch, l.activation);
}

/**
 * local層的反向傳播函數
 * @param l
 * @param net
 */
void backward_local_layer(local_layer l, network net)
{
    int i, j;
    int locations = l.out_w*l.out_h; // 每張輸出特征圖的元素個數

    // 計算目前層激活函數對權重輸入的導數并乘以l.delta相應元素,進而完成目前層的誤差項l.delta計算
    // l.output存儲了該層網絡的所有輸出:該層網絡接收一個batch的輸入圖檔,其中每張圖檔經過local層處理後得到的尺寸為: l.out_w, l.out_h
    // 該層local共有l.n個卷積核,是以一張輸入圖檔共輸出l.n張寬高為l.out_w, l.out_h的特征圖(l.outputs為一張圖檔所有輸出特征圖的總元素個數)
    // 是以所有輸入圖檔也即 l.output中的總元素個數為: l.n * l.out_w * l.out_h * l.batch;                                                                                                                      `
    gradient_array(l.output, l.outputs*l.batch, l.activation, l.delta);

    // 計算偏置層的更新值
    // 每一個卷積核都有一個偏置,偏置的更新值也即誤差函數對偏置的導數,這個導數的計算很簡單,實際所有的導數已經求完,都存儲在l.delta中
    // 接下來隻需要把l.delta中對用同一個卷積核的項加起來就可以了。其實local層的每個偏置重複出現batch次,
    // local層共有 l.outputs = l.out_h * l.out_w * l.n 個偏置, 要對整個batch進行求和
    for(i = 0; i < l.batch; ++i){
        // l.bias_updates += l.delta + i*l.outputs
        axpy_cpu(l.outputs, 1, l.delta + i*l.outputs, 1, l.bias_updates, 1);
    }


    // 周遊batch中每張圖檔,對于l.delta來說,每張圖檔是分開存的,是以次元會達到 l.batch * out_h * out_w * n
    // 對于l.weights, l.weight_updates 以及上面提到的l.bias, l.bias_updates
    for(i = 0; i < l.batch; ++i){
        float *input = net.input + i*l.w*l.h*l.c;
        im2col_cpu(input, l.c, l.h, l.w, 
                l.size, l.stride, l.pad, net.workspace);

        for(j = 0; j < locations; ++j){ 
            float *a = l.delta + i*l.outputs + j;
            // net.workspace的元素個數為所有層中最大的l.workspace_size(在make_convolution_layer() 計算得到network_size大小,
            // 在parse_network_cfg()中動态配置設定記憶體, 此值對應未使用GPU時的情況),
            // net.workspace充當一個臨時工作空間的作用,存儲時所需要的計算參數, 比如沒成單張圖檔的重排後的結果(這些參數馬上會參與卷積計算)
            // 一旦用完, 就會被馬上更新(是以該周遊的情況更新頻率比較大)
            float *b = net.workspace + j;
            float *c = l.weight_updates + j*l.size*l.size*l.c*l.n;

            int m = l.n;
            int n = l.size*l.size*l.c;
            int k = 1;

            // 計算local層的權重更新值
            // 所謂權重更新就是 weight = weight - alpha * weight_update中的weight_update
            // 權重更新值等于目前層誤差項中每個元素乘以相應的像素值,因為一個權重跟目前層多個輸出關聯(權值共享,即卷積核在圖像中跨步移動做卷積,每個位置卷積得到的值
            // 都與該權值相關), 是以對每一權重更新值來說,需要在l.delta中找出所有與之相關的誤差項,乘以相應的像素值,再求和,具體的實作的方式im2col_cpu()與gemm_nt()完成
            // 目前全連接配接的誤差項乘以目前層的輸入即可得到目前卷積層的權重更新值

            // 此處在im2col_cpu操作基礎上,利用矩陣乘法 c = alpha*a*b + beta*c完成對圖像卷積的操作;
            // 0表示不對輸入a進行轉置, 1表示對輸入b進行轉置;
            // m是輸入a,c的行數,具體含義為卷積核的個數(l.n);
            // n是輸入b和c的列數,具體含義為每個卷積核元素個數乘以輸入圖像的通道數(l.size*l.size*l.c)
            // k是輸入a的列數也是b的行數, 具體含義為每個輸出特征圖的元素個數 (l.out_h * l.out_w)
            // a,b,c即為三個參與運算的矩陣(用一維資料存儲), alpha=beta=1為常系數

            // a為l.delta的一大行, l.delta為本層所有輸出(包含整個batch中每張圖檔的所有特征圖)關于權重輸入的導數(即激活函數的導數值)集合;
            // 元素的個數為 l.batch * l.out_h * l.out_w * l.out_c (l.out_c = l.n),  按行存儲, 共有l.batch行, l.out_h * l.out_w * l.out_c列
            // 即l.delta中每行包含一張圖的所有輸出圖,故這麼一大行,又可以視作有 l.out_c(l.out_c = l.n)個小行, l.out_h * l.out_w小列,而一次循環就是處理
            // l.delta的一大行, 故可以視a作為l.out_c行, l.out_h*l.out_w列的矩陣;
            // b 為單張輸入圖像經過im2col_cpu重排後的圖像;
            // c 為輸出,按存儲,可視作有l.n行, l.c*size*size列(l.c是輸入圖像的通道數,l.n是卷積核個數)
            // 即c就是所謂的誤差項(輸出關于權重輸入的導數)或者誤差項 (一個卷積核有l.c*l.size*l.size個權重,共有l.n個核)

            // 由上可知:
            // a: (l.out_c) * (1)
            // b: (l.c * l.size * l.size) * (1)
            // c: (l.n) * (l.c * l.size * l.size) (注意:l.n = l.out_c)
            // 故要進行a * b + c計算,必須對b進行轉置(否則行列不比對), 是以調用gemm_nt()函數;
            gemm(0,1,m,n,k,1,a,locations,b,locations,1,c,n);
        }

        // 由目前local層計算上一層的誤差項
        if(net.delta){
            for(j = 0; j < locations; ++j){ 
                float *a = l.weights + j*l.size*l.size*l.c*l.n;
                float *b = l.delta + i*l.outputs + j;
                float *c = net.workspace + j;

                int m = l.size*l.size*l.c;
                int n = 1;
                int k = l.n;

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

            col2im_cpu(net.workspace, l.c,  l.h,  l.w,  l.size,  l.stride, l.pad, net.delta+i*l.c*l.h*l.w);
        }
    }
}

/**
 * local層的權重更新函數
 * @param l 目前局部連接配接層
 * @param a
 */
void update_local_layer(local_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;

    int locations = l.out_w*l.out_h;
    int size = l.size*l.size*l.c*l.n*locations;
    // l.biases += learning_rate/batch * l.bias_updates 更新偏置,這裡學習率要除以batch,整個batch的梯度平均值
    axpy_cpu(l.outputs, learning_rate/batch, l.bias_updates, 1, l.biases, 1);
    // l.bias_updates *= momentum; 計算下一次梯度需要偏置的動量;
    scal_cpu(l.outputs, momentum, l.bias_updates, 1);
    
    // l.weight_updates += -decay*batch * l.weights 計算權重衰減
    axpy_cpu(size, -decay*batch, l.weights, 1, l.weight_updates, 1);
    // l.weights += learning_rate/batch * l.weight_updates 更新權重
    axpy_cpu(size, learning_rate/batch, l.weight_updates, 1, l.weights, 1);
    // l.weight_updates *= momentum 計算下次梯度需要的權重的動量
    scal_cpu(size, momentum, l.weight_updates, 1);
}

#ifdef GPU

void forward_local_layer_gpu(const local_layer l, network net)
{
    int out_h = local_out_height(l);
    int out_w = local_out_width(l);
    int i, j;
    int locations = out_h * out_w;

    for(i = 0; i < l.batch; ++i){
        copy_gpu(l.outputs, l.biases_gpu, 1, l.output_gpu + i*l.outputs, 1);
    }

    for(i = 0; i < l.batch; ++i){
        float *input = net.input_gpu + i*l.w*l.h*l.c;
        im2col_gpu(input, l.c, l.h, l.w, 
                l.size, l.stride, l.pad, net.workspace);
        float *output = l.output_gpu + i*l.outputs;
        for(j = 0; j < locations; ++j){
            float *a = l.weights_gpu + j*l.size*l.size*l.c*l.n;
            float *b = net.workspace + j;
            float *c = output + j;

            int m = l.n;
            int n = 1;
            int k = l.size*l.size*l.c;

            gemm_gpu(0,0,m,n,k,1,a,k,b,locations,1,c,locations);
        }
    }
    activate_array_gpu(l.output_gpu, l.outputs*l.batch, l.activation);
}

void backward_local_layer_gpu(local_layer l, network net)
{
    int i, j;
    int locations = l.out_w*l.out_h;

    gradient_array_gpu(l.output_gpu, l.outputs*l.batch, l.activation, l.delta_gpu);
    for(i = 0; i < l.batch; ++i){
        axpy_gpu(l.outputs, 1, l.delta_gpu + i*l.outputs, 1, l.bias_updates_gpu, 1);
    }

    for(i = 0; i < l.batch; ++i){
        float *input = net.input_gpu + i*l.w*l.h*l.c;
        im2col_gpu(input, l.c, l.h, l.w, 
                l.size, l.stride, l.pad, net.workspace);

        for(j = 0; j < locations; ++j){ 
            float *a = l.delta_gpu + i*l.outputs + j;
            float *b = net.workspace + j;
            float *c = l.weight_updates_gpu + j*l.size*l.size*l.c*l.n;
            int m = l.n;
            int n = l.size*l.size*l.c;
            int k = 1;

            gemm_gpu(0,1,m,n,k,1,a,locations,b,locations,1,c,n);
        }

        if(net.delta_gpu){
            for(j = 0; j < locations; ++j){ 
                float *a = l.weights_gpu + j*l.size*l.size*l.c*l.n;
                float *b = l.delta_gpu + i*l.outputs + j;
                float *c = net.workspace + j;

                int m = l.size*l.size*l.c;
                int n = 1;
                int k = l.n;

                gemm_gpu(1,0,m,n,k,1,a,m,b,locations,0,c,locations);
            }

            col2im_gpu(net.workspace, l.c,  l.h,  l.w,  l.size,  l.stride, l.pad, net.delta_gpu+i*l.c*l.h*l.w);
        }
    }
}

void update_local_layer_gpu(local_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;

    int locations = l.out_w*l.out_h;
    int size = l.size*l.size*l.c*l.n*locations;
    axpy_gpu(l.outputs, learning_rate/batch, l.bias_updates_gpu, 1, l.biases_gpu, 1);
    scal_gpu(l.outputs, momentum, l.bias_updates_gpu, 1);

    axpy_gpu(size, -decay*batch, l.weights_gpu, 1, l.weight_updates_gpu, 1);
    axpy_gpu(size, learning_rate/batch, l.weight_updates_gpu, 1, l.weights_gpu, 1);
    scal_gpu(size, momentum, l.weight_updates_gpu, 1);
}

void pull_local_layer(local_layer l)
{
    int locations = l.out_w*l.out_h;
    int size = l.size*l.size*l.c*l.n*locations;
    cuda_pull_array(l.weights_gpu, l.weights, size);
    cuda_pull_array(l.biases_gpu, l.biases, l.outputs);
}

void push_local_layer(local_layer l)
{
    int locations = l.out_w*l.out_h;
    int size = l.size*l.size*l.c*l.n*locations;
    cuda_push_array(l.weights_gpu, l.weights, size);
    cuda_push_array(l.biases_gpu, l.biases, l.outputs);
}
#endif