【darknet源碼解析-12】connected_layer.h 和 connected_layer.c 解析

本系列爲darknet源碼解析,本次解析爲src/connect_layer.h 和 src/connect_layer.c 兩個,connect_layer主要是構建全連接網絡。

在閱讀本文之前請事先手推一遍BP算法,這樣有助於你對connect_layer源碼的理解。主要理解BP算法對隱藏層權重和偏置如何求偏導數,以及最輸出層求偏導數。

注意:在全連接網絡中的添加BN操作,

  1. W×X之後,進行BN,然後Activate。
  2. 還是先W×X+bias,在BN,最後Activate。

如果進行BN操作是不需要進行+bias,因爲這樣會造成功能重複!!!

connected_layer.h的解析如下:

#ifndef CONNECTED_LAYER_H
#define CONNECTED_LAYER_H

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

// 構造全連接層函數
layer make_connected_layer(int batch, int inputs, int outputs, ACTIVATION activation, int batch_normalize, int adam);

// 全連接層的前向傳播函數
void forward_connected_layer(layer l, network net);
// 全連接層的反向傳播函數
void backward_connected_layer(layer l, network net);
// 全連接層的更新函數
void update_connected_layer(layer l, update_args a);

#ifdef GPU
void forward_connected_layer_gpu(layer l, network net);
void backward_connected_layer_gpu(layer l, network net);
void update_connected_layer_gpu(layer l, update_args a);
void push_connected_layer(layer l);
void pull_connected_layer(layer l);
#endif

#endif

connected_layer.c 的解析如下:

#include "connected_layer.h"
#include "convolutional_layer.h"
#include "batchnorm_layer.h"
#include "utils.h"
#include "cuda.h"
#include "blas.h"
#include "gemm.h"

#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/**
 * 構建全連接層
 * @param batch 該層輸入中一個batch所含有的圖片張數,等於net.batch
 * @param inputs 全連接層每張輸入圖片的元素個數
 * @param outputs 全連接層輸出元素個數(由網絡配置文件指定,如果未指定,默認值爲1,在parse_connect()中賦值)
 * @param activation 激活函數類型
 * @param batch_normalize 是否進行BN
 * @param adam
 * @return 全連接層l
 */
layer make_connected_layer(int batch, int inputs, int outputs, ACTIVATION activation, int batch_normalize, int adam)
{
    int i;
    layer l = {0};
    l.learning_rate_scale = 1;
    l.type = CONNECTED;

    l.inputs = inputs; // 全連接層一張輸入圖片的元素個數
    l.outputs = outputs; // 全連接層對應一張輸入圖片輸出元素個數
    l.batch=batch; // 一個batch中圖片的張數
    l.batch_normalize = batch_normalize; //是否進行BN
    l.h = 1; // 全連接層輸入圖片高爲1, 寬也爲1
    l.w = 1;
    l.c = inputs; // 全連接層的輸入通道數等於單張輸入圖片的元素個數
    l.out_h = 1; // 全連接輸入圖片高也爲1, 寬也爲1
    l.out_w = 1;
    l.out_c = outputs; // 全連接層輸出圖片的通道數等於一張輸入圖片對應的輸出元素個數

    l.output = calloc(batch*outputs, sizeof(float)); // 全連接層所有輸出(包含整個batch)
    l.delta = calloc(batch*outputs, sizeof(float)); // 全連接層誤差項(包含整個batch)

    // 由下面forward_connected_layer() 函數中調用的gemm()可以看出, l.weights_updates應該理解爲outputs行,input列
    l.weight_updates = calloc(inputs*outputs, sizeof(float)); // 全連接層權重係數更新值個數等於一張輸入圖片元素個數與對應輸出元素個數之積
    l.bias_updates = calloc(outputs, sizeof(float)); // 全連接層偏置更新值個數就等於一張輸入圖片的輸出元素個數

    // 由下面forward_connected_layer() 函數中調用的gemm()可以看出, l.weights_updates應該理解爲outputs行,input列
    l.weights = calloc(outputs*inputs, sizeof(float)); // 全連接層權重係數個數等於一張輸入圖片元素個數與對應輸出元素個數之積
    l.biases = calloc(outputs, sizeof(float)); // 全連接層偏置個數就等於一張輸入圖片的輸出元素個數

    l.forward = forward_connected_layer; // 全連接層前向傳播
    l.backward = backward_connected_layer; // 全連接層反向傳播
    l.update = update_connected_layer; // 全連接層更新


    // 全連接層權重初始化:
    // 使用He初始化方法, sqrt(2./inputs) * Uniform[-1, 1]
    // inputs 全連接層一張輸入圖片的元素個數 , 後面是一個[-1, 1] 均勻分佈
    //float scale = 1./sqrt(inputs);
    float scale = sqrt(2./inputs);
    for(i = 0; i < outputs*inputs; ++i){
        l.weights[i] = scale*rand_uniform(-1, 1);
    }

    // 全連接層偏置初始化: 全部初始化爲0
    for(i = 0; i < outputs; ++i){
        l.biases[i] = 0;
    }

    if(adam){ // 採用adam
        l.m = calloc(l.inputs*l.outputs, sizeof(float));
        l.v = calloc(l.inputs*l.outputs, sizeof(float));
        l.bias_m = calloc(l.outputs, sizeof(float));
        l.scale_m = calloc(l.outputs, sizeof(float));
        l.bias_v = calloc(l.outputs, sizeof(float));
        l.scale_v = calloc(l.outputs, sizeof(float));
    }

    if(batch_normalize){ // 採用BN時
        l.scales = calloc(outputs, sizeof(float));
        l.scale_updates = calloc(outputs, sizeof(float));
        for(i = 0; i < outputs; ++i){
            l.scales[i] = 1;
        }

        l.mean = calloc(outputs, sizeof(float));
        l.mean_delta = calloc(outputs, sizeof(float));
        l.variance = calloc(outputs, sizeof(float));
        l.variance_delta = calloc(outputs, sizeof(float));

        l.rolling_mean = calloc(outputs, sizeof(float));
        l.rolling_variance = calloc(outputs, sizeof(float));

        l.x = calloc(batch*outputs, sizeof(float));
        l.x_norm = calloc(batch*outputs, sizeof(float));
    }

#ifdef GPU
    l.forward_gpu = forward_connected_layer_gpu;
    l.backward_gpu = backward_connected_layer_gpu;
    l.update_gpu = update_connected_layer_gpu;

    l.weights_gpu = cuda_make_array(l.weights, outputs*inputs);
    l.biases_gpu = cuda_make_array(l.biases, outputs);

    l.weight_updates_gpu = cuda_make_array(l.weight_updates, outputs*inputs);
    l.bias_updates_gpu = cuda_make_array(l.bias_updates, outputs);

    l.output_gpu = cuda_make_array(l.output, outputs*batch);
    l.delta_gpu = cuda_make_array(l.delta, outputs*batch);
    if (adam) {
        l.m_gpu =       cuda_make_array(0, inputs*outputs);
        l.v_gpu =       cuda_make_array(0, inputs*outputs);
        l.bias_m_gpu =  cuda_make_array(0, outputs);
        l.bias_v_gpu =  cuda_make_array(0, outputs);
        l.scale_m_gpu = cuda_make_array(0, outputs);
        l.scale_v_gpu = cuda_make_array(0, outputs);
    }

    if(batch_normalize){
        l.mean_gpu = cuda_make_array(l.mean, outputs);
        l.variance_gpu = cuda_make_array(l.variance, outputs);

        l.rolling_mean_gpu = cuda_make_array(l.mean, outputs);
        l.rolling_variance_gpu = cuda_make_array(l.variance, outputs);

        l.mean_delta_gpu = cuda_make_array(l.mean, outputs);
        l.variance_delta_gpu = cuda_make_array(l.variance, outputs);

        l.scales_gpu = cuda_make_array(l.scales, outputs);
        l.scale_updates_gpu = cuda_make_array(l.scale_updates, outputs);

        l.x_gpu = cuda_make_array(l.output, l.batch*outputs);
        l.x_norm_gpu = cuda_make_array(l.output, l.batch*outputs);
#ifdef CUDNN
        cudnnCreateTensorDescriptor(&l.normTensorDesc);
        cudnnCreateTensorDescriptor(&l.dstTensorDesc);
        cudnnSetTensor4dDescriptor(l.dstTensorDesc, CUDNN_TENSOR_NCHW, CUDNN_DATA_FLOAT, l.batch, l.out_c, l.out_h, l.out_w); 
        cudnnSetTensor4dDescriptor(l.normTensorDesc, CUDNN_TENSOR_NCHW, CUDNN_DATA_FLOAT, 1, l.out_c, 1, 1); 
#endif
    }
#endif
    l.activation = activation;
    fprintf(stderr, "connected                            %4d  ->  %4d\n", inputs, outputs);
    return l;
}

typedef struct{
    int batch;
    float learning_rate;
    float momentum;
    float decay;
    int adam;
    float B1;
    float B2;
    float eps;
    int t;
} update_args;

// Y += alpha * X
void axpy_cpu(int N, float ALPHA, float *X, int INCX, float *Y, int INCY)
{
    int i;
    for(i = 0; i < N; ++i) Y[i*INCY] += ALPHA*X[i*INCX];
}
// X *= alpha
void scal_cpu(int N, float ALPHA, float *X, int INCX)
{
    int i;
    for(i = 0; i < N; ++i) X[i*INCX] *= ALPHA;
}

/**
 * 全連接層更新函數
 * @param l 當前層
 * @param a 優化參數
 */
void update_connected_layer(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;
    // 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 計算下次梯度需要的偏置的動量, 引入動量的參數更新: vt = momentum*vt-1
    scal_cpu(l.outputs, momentum, l.bias_updates, 1);

    if(l.batch_normalize){
        // l.scales += learning_rate/batch * l.scale_updates  同理在BN層上更新
        axpy_cpu(l.outputs, learning_rate/batch, l.scale_updates, 1, l.scales, 1);
        // l.scale_updates *= momentum
        scal_cpu(l.outputs, momentum, l.scale_updates, 1);
    }

    // l.weight_updates += (-decay*batch)*l.weights 計算權重衰減,
    axpy_cpu(l.inputs*l.outputs, -decay*batch, l.weights, 1, l.weight_updates, 1);
    //  l.weights += learning_rate/batch * l.weight_updates // 更新權重
    axpy_cpu(l.inputs*l.outputs, learning_rate/batch, l.weight_updates, 1, l.weights, 1);
    // l.weight_updates *= momentum //計算下次梯度需要的權重的動量
    scal_cpu(l.inputs*l.outputs, momentum, l.weight_updates, 1);
}

/**
 * 全連接層前向傳播函數
 * @param l 當前全連接層
 * @param net 整個網絡
 */
void forward_connected_layer(layer l, network net)
{
    // 將全連接層所有輸出(包含所有batch)元素置爲0
    fill_cpu(l.outputs*l.batch, 0, l.output, 1);
    int m = l.batch; //   m: 全連接層接收的一個batch中圖片張數
    int k = l.inputs; //  k: 全連接層單張輸入圖片元素個數
    int n = l.outputs; // n: 全連接層對應單張輸入圖片的輸出元素個數
    float *a = net.input; // a: 全連接層的輸入數據, 維度爲l.batch * l.inputs(包含整個batch的輸入), 可視爲l.batch行, l.inputs列, 每行就是一張圖片
    float *b = l.weights; // b: 全連接層的所有權重, 維度爲l.outputs * l.inputs (存儲空間申請見make_connected_layer())
    float *c = l.output; //  c: 全連接層的所有輸出, 維度爲l.batch * l.outputs(包含整個batch的輸出)


    // 根據維度的匹配規則,顯然需要對b進行轉置,故而調用gemm_nt()函數,最終計算得到c的維度爲 l.batch * l.outputs
    // 全連接層的輸出很好計算,直接矩陣相乘就可以;
    gemm(0,1,m,n,k,1,a,k,b,k,1,c,n);
    if(l.batch_normalize){
        forward_batchnorm_layer(l, net); //有BN就不需要加上偏置了;
    } else {
        add_bias(l.output, l.biases, l.batch, l.outputs, 1); // 無BN則需要加上偏置
    }
    // 前向傳播最後一步: 這裏利用激活函數處理
    activate_array(l.output, l.outputs*l.batch, l.activation);
}

void activate_array(float *x, const int n, const ACTIVATION a)
{
    int i;
    for(i = 0; i < n; ++i){
        x[i] = activate(x[i], a);
    }
}

void add_bias(float *output, float *biases, int batch, int n, int size)
{
    int i,j,b;
    for(b = 0; b < batch; ++b){
        for(i = 0; i < n; ++i){
            for(j = 0; j < size; ++j){
                output[(b*n + i)*size + j] += biases[i];
            }
        }
    }
}


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); //gradient f'(x) -> 激活函數求導
    }
}

//  計算每個卷積核的偏置更新值,所謂偏置,就是bias= bias - alpha * bias_update中的bias_update
void backward_bias(float *bias_updates, float *delta, int batch, int n, int size)
{
    int i,b;
    // 遍歷batch中每張輸入圖片
    // 注意,最後的偏置更新值是所有輸入圖片的總和(多張圖片無非就是重複一張圖片的操作,求和即可)。
    // 總之:一個卷積覈對應一個偏置更新值,該偏置更新值等於batch所有圖片累積的偏置更新值
    // 而每張圖片也需要進行偏置更新值求和(因爲每個卷積核在每張圖片多個位置做卷積運算,這都對偏置更新值有貢獻)
    // 以得到每張圖片的總偏置更新值。
    for(b = 0; b < batch; ++b){
        // 求和得到一張輸入圖片的總偏置更新值
        for(i = 0; i < n; ++i){
            //l.n = n; 卷積核的個數
            bias_updates[i] += sum_array(delta+size*(i+b*n), size);
        }
    }
}

/**
 * 全連接層反向傳播函數
 * @param l 當前全連接層
 * @param net 整個網絡
 */

void backward_connected_layer(layer l, network net)
{
    // gradient_array()函數主要完成激活函數對加權輸入的導數,並乘以之前得到的l.delta,得到當前層最終的l.delta(誤差函數對加權輸入的導數)
    gradient_array(l.output, l.outputs*l.batch, l.activation, l.delta);

    if(l.batch_normalize){
        backward_batchnorm_layer(l, net);
    } else {
        // 計算當前全連接層的偏置的梯度值
        // 誤差函數對偏置的偏導數實際上就等於以上剛求完的l.delta, 因爲一個batch中不只有一張圖片,所有將進行效果疊加.
        // 不同於卷積層每個卷積核採有一個偏置參數, 全連接層是每個輸出元素就對應一個偏置參數,共有l.outputs個,
        // 每次循環求完一張圖片輸出的偏置梯度值.
        // 最終會把每一張圖的偏置更新疊加,因此,最終l.bias_updates中每一個元素的值是batch中所有圖片對應輸出元素偏置更新值的疊加.
        backward_bias(l.bias_updates, l.delta, l.batch, l.outputs, 1);
    }
    // 計算當前全連接層權重更新值
    int m = l.outputs;               // m: 是a^T的行數或者a的列數, 含義爲每張圖片輸出的元素個數
    int k = l.batch;                 // k: 是a^T的列數或者a的行數, b的行數,含義爲一個batch中含有圖片張數
    int n = l.inputs;                // n: b的列數, 含義爲每張輸入圖片的元素個數
    float *a = l.delta;              // a:當前全連接層誤差項, 維度 l.batch * l.outputs
    float *b = net.input;            // b:當前全連接層所有輸入, 維度 l.batch * l.inputs
    float *c = l.weight_updates;     // c:當前全連接層權重更新值, 維度 l.outputs * l.inputs (權重個數)

    // 由行列匹配規則可知, 需要將a轉置, 故而調用gemm_tn() 函數,轉置a實際上是想把batch中所有圖片的影響疊加
    // 全連接層的權重更新值的計算也相對簡單,簡單的矩陣乘法可完成.
    // 當前全連接的誤差項乘以當前層的輸入即可得到當前全連接層的權重更新值 [建議看推導一遍BP算法,再來看會容易很多]
    gemm(1,0,m,n,k,1,a,m,b,n,1,c,n);



    // 由當前全連接層計算上一層的誤差項(完成絕大部分計算[因爲少了乘以前一層激活函數f'(x), 所以只能說完成絕大部分]: 當前全連接層誤差項乘以當前還未更新的權重,)
    m = l.batch;   // a: a的行數, c的行數, 含義一個batch中含有圖片的數量
    k = l.outputs; // k: a的列數, b的行數, 含義每張輸出圖片的元素個數
    n = l.inputs;  // n: b的列數, c的列數, 含義每張輸入圖片的元素個數

    a = l.delta;   // a: 當前全連接層誤差項,維度爲 l.batch * l.outputs
    b = l.weights; // b: 當前層權重(連接當前層與上一層) l.outputs * l.inputs
    c = net.delta; // c: 上一層誤差項(包含整個batch), 維度爲l.batch * l.inputs
                    // 一定注意此時的c等於net.delta,已經在network.c中backward_network()函數中賦值給上一層的delta

    // 由行列匹配規則可知,不需要轉置. 由全連接層誤差項計算上一層的誤差項也是很簡單的,直接利用矩陣的相乘,將當前l.delta與當前層權重相乘就可以了.
    if(c) gemm(0,0,m,n,k,1,a,k,b,n,1,c,n);
}


void denormalize_connected_layer(layer l)
{
    int i, j;
    for(i = 0; i < l.outputs; ++i){
        float scale = l.scales[i]/sqrt(l.rolling_variance[i] + .000001);
        for(j = 0; j < l.inputs; ++j){
            l.weights[i*l.inputs + j] *= scale;
        }
        l.biases[i] -= l.rolling_mean[i] * scale;
        l.scales[i] = 1;
        l.rolling_mean[i] = 0;
        l.rolling_variance[i] = 1;
    }
}


void statistics_connected_layer(layer l)
{
    if(l.batch_normalize){
        printf("Scales ");
        print_statistics(l.scales, l.outputs);
        /*
           printf("Rolling Mean ");
           print_statistics(l.rolling_mean, l.outputs);
           printf("Rolling Variance ");
           print_statistics(l.rolling_variance, l.outputs);
         */
    }
    printf("Biases ");
    print_statistics(l.biases, l.outputs);
    printf("Weights ");
    print_statistics(l.weights, l.outputs);
}

#ifdef GPU

void pull_connected_layer(layer l)
{
    cuda_pull_array(l.weights_gpu, l.weights, l.inputs*l.outputs);
    cuda_pull_array(l.biases_gpu, l.biases, l.outputs);
    cuda_pull_array(l.weight_updates_gpu, l.weight_updates, l.inputs*l.outputs);
    cuda_pull_array(l.bias_updates_gpu, l.bias_updates, l.outputs);
    if (l.batch_normalize){
        cuda_pull_array(l.scales_gpu, l.scales, l.outputs);
        cuda_pull_array(l.rolling_mean_gpu, l.rolling_mean, l.outputs);
        cuda_pull_array(l.rolling_variance_gpu, l.rolling_variance, l.outputs);
    }
}

void push_connected_layer(layer l)
{
    cuda_push_array(l.weights_gpu, l.weights, l.inputs*l.outputs);
    cuda_push_array(l.biases_gpu, l.biases, l.outputs);
    cuda_push_array(l.weight_updates_gpu, l.weight_updates, l.inputs*l.outputs);
    cuda_push_array(l.bias_updates_gpu, l.bias_updates, l.outputs);
    if (l.batch_normalize){
        cuda_push_array(l.scales_gpu, l.scales, l.outputs);
        cuda_push_array(l.rolling_mean_gpu, l.rolling_mean, l.outputs);
        cuda_push_array(l.rolling_variance_gpu, l.rolling_variance, l.outputs);
    }
}

void update_connected_layer_gpu(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;
    if(a.adam){ //Adam更新只用在GPU版本
        adam_update_gpu(l.weights_gpu, l.weight_updates_gpu, l.m_gpu, l.v_gpu, a.B1, a.B2, a.eps, decay, learning_rate, l.inputs*l.outputs, batch, a.t);
        adam_update_gpu(l.biases_gpu, l.bias_updates_gpu, l.bias_m_gpu, l.bias_v_gpu, a.B1, a.B2, a.eps, decay, learning_rate, l.outputs, batch, a.t);
        if(l.scales_gpu){
            adam_update_gpu(l.scales_gpu, l.scale_updates_gpu, l.scale_m_gpu, l.scale_v_gpu, a.B1, a.B2, a.eps, decay, learning_rate, l.outputs, batch, a.t);
        }
    }else{
        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);

        if(l.batch_normalize){
            axpy_gpu(l.outputs, learning_rate/batch, l.scale_updates_gpu, 1, l.scales_gpu, 1);
            scal_gpu(l.outputs, momentum, l.scale_updates_gpu, 1);
        }

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

void forward_connected_layer_gpu(layer l, network net)
{
    fill_gpu(l.outputs*l.batch, 0, l.output_gpu, 1);

    int m = l.batch;
    int k = l.inputs;
    int n = l.outputs;
    float * a = net.input_gpu;
    float * b = l.weights_gpu;
    float * c = l.output_gpu;
    gemm_gpu(0,1,m,n,k,1,a,k,b,k,1,c,n);

    if (l.batch_normalize) {
        forward_batchnorm_layer_gpu(l, net);
    } else {
        add_bias_gpu(l.output_gpu, l.biases_gpu, l.batch, l.outputs, 1);
    }
    activate_array_gpu(l.output_gpu, l.outputs*l.batch, l.activation);
}

void backward_connected_layer_gpu(layer l, network net)
{
    constrain_gpu(l.outputs*l.batch, 1, l.delta_gpu, 1);
    gradient_array_gpu(l.output_gpu, l.outputs*l.batch, l.activation, l.delta_gpu);
    if(l.batch_normalize){
        backward_batchnorm_layer_gpu(l, net);
    } else {
        backward_bias_gpu(l.bias_updates_gpu, l.delta_gpu, l.batch, l.outputs, 1);
    }

    int m = l.outputs;
    int k = l.batch;
    int n = l.inputs;
    float * a = l.delta_gpu;
    float * b = net.input_gpu;
    float * c = l.weight_updates_gpu;
    gemm_gpu(1,0,m,n,k,1,a,m,b,n,1,c,n);

    m = l.batch;
    k = l.outputs;
    n = l.inputs;

    a = l.delta_gpu;
    b = l.weights_gpu;
    c = net.delta_gpu;

    if(c) gemm_gpu(0,0,m,n,k,1,a,k,b,n,1,c,n);
}
#endif

完,

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