【darknet源碼解析-10】dropout.h 和 dropout.c 解析

本系列爲darknet源碼解析,本次解析src/dropout.h 與 src/dropout.c 兩個。

在神經網絡中應用dropout包括訓練和預測兩個階段,在訓練階段,dropout 以一定的概率p隨機的"捨棄"一部分神經元點,即這部分神經元節點暫時停止工作,如下圖所示。因此,對於一個包含N個節點的神經,在dropout的作用下可看作2^N個模型的集成。這2^N個模型可以看成原始網絡的子網絡,它們共享部分權值,並且具有相同的網絡層數,並且整體的參數數目保持不變,這就大大簡化了運算。減少了模型過擬合的風險,增強了模型的泛化能力。

在測試階段,在前向傳播的計算時,每個神經元的輸出結果要預先乘以概率1-p爲什麼要乘以p呢???因爲在訓練的時候只有佔比(1-p)的神經元節點參與了訓練,那麼在測試階段,所有的神經元節點都參與計算,則得到結果要比訓練時平均要大 1/(1-p), 爲了避免發生這種情況,就需要神經元輸出結果乘以1-p,使得下一層輸入規模保持不變。

在darknet源碼中採用了inverted dropout, 爲什麼要採用inverted dropout呢?其實就是爲了避免在測試階段,不需要做額外的操作。那麼怎麼解決這個問題,就是直接將dropout後每個神經元的輸出結果擴大1/(1-p)倍。從而可以保證dropout層輸出期望不會發生變化。

#ifndef DROPOUT_LAYER_H
#define DROPOUT_LAYER_H

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

typedef layer dropout_layer;

// 構建一個dropout層
dropout_layer make_dropout_layer(int batch, int inputs, float probability);
// dropout前向傳播
void forward_dropout_layer(dropout_layer l, network net);
// dropout反向傳播
void backward_dropout_layer(dropout_layer l, network net);
void resize_dropout_layer(dropout_layer *l, int inputs);

#ifdef GPU
void forward_dropout_layer_gpu(dropout_layer l, network net);
void backward_dropout_layer_gpu(dropout_layer l, network net);

#endif
#endif

dropout.c的詳細解釋如下:

#include "dropout_layer.h"
#include "utils.h"
#include "cuda.h"
#include <stdlib.h>
#include <stdio.h>

/**
 * 構建dropout層
 * @param batch // 一個batch中圖片張數
 * @param inputs // dropout層每張輸入圖片的元素個數
 * @param probability dropout概率,即某個輸入神經元被丟棄的概率,由配置文件指定;如果配置文件中未指定,則默認值爲0.5(參見parse_dropout_layer()函數)
 * @return dropout_layer
 *
 * 說明: dropout層的構建函數需要的輸入參數比較少,網絡輸入數據尺寸h,w,c也不需要;
 * 注意: dropout層有l.inputs = l.outputs; 另外此處實現使用了inverted dropout, 不是標準的dropout
 */
dropout_layer make_dropout_layer(int batch, int inputs, float probability)
{
    dropout_layer l = {0};
    l.type = DROPOUT;
    l.probability = probability; //丟棄概率 (1-probability 爲保留概率)
    l.inputs = inputs; // dropout層不會改變輸入輸出的個數,因此有 l.inputs == l.outputs
    l.outputs = inputs; // 雖然dropout會丟棄一些輸入神經元, 但這丟棄只是置該輸入元素值爲0, 並沒有刪除
    l.batch = batch; // 一個batch中圖片數量
    l.rand = calloc(inputs*batch, sizeof(float)); //動態分配內存,
    l.scale = 1./(1.-probability); //使用inverted dropout, scale取保留概率的倒數
    l.forward = forward_dropout_layer; //前向傳播
    l.backward = backward_dropout_layer; // 反向傳播
    #ifdef GPU
    l.forward_gpu = forward_dropout_layer_gpu;
    l.backward_gpu = backward_dropout_layer_gpu;
    l.rand_gpu = cuda_make_array(l.rand, inputs*batch);
    #endif
    fprintf(stderr, "dropout       p = %.2f               %4d  ->  %4d\n", probability, inputs, inputs);
    return l;
} 

void resize_dropout_layer(dropout_layer *l, int inputs)
{
    l->rand = realloc(l->rand, l->inputs*l->batch*sizeof(float));
    #ifdef GPU
    cuda_free(l->rand_gpu);

    l->rand_gpu = cuda_make_array(l->rand, inputs*l->batch);
    #endif
}

/**
 * dropout層前向傳播函數
 * @param l 當前dropout層函數
 * @param net 整個網絡
 *
 * 說明:dropout層同樣沒有訓練參數,因此前向傳播函數比較簡單,只需要完成一件事: 按指定概率 l.probability
 * 丟棄輸入元素,並將保留下來的輸入元素乘以比例因子scale(採用inverted dropout, 這種凡是實現更爲方便,
 * 且代碼接口比較統一;如果採用標準的dropout, 則測試階段需要進入 forward_dropout_layer(),
 * 使每個輸入乘以保留概率,而使用inverted dropout, 測試階段就不需要進入到forward_dropout_layer())
 *
 * 說明: dropout層有l.inputs = l.outputs;
 */
void forward_dropout_layer(dropout_layer l, network net)
{
    int i;
    // 因爲使用inverted dropout,所以測試階段不需要進入forward_dropout_layer()
    if (!net.train) return;
    for(i = 0; i < l.batch * l.inputs; ++i){
        // 採樣一個0-1之間均勻分佈的隨機數
        float r = rand_uniform(0, 1);
        l.rand[i] = r; // 每一個隨機數都要保存到l.rand,之後反向傳播時候會用到
        if(r < l.probability) net.input[i] = 0; // 捨棄該元素,將其值置爲0, 所以這裏元素的總個數並沒有發生變化;
        else net.input[i] *= l.scale; //保留該輸入元素,並乘以比例因子scale
    }
}


// 進行隨機採樣操作,從區間[min, max]隨機返回一個實數
float rand_uniform(float min, float max)
{
    if(max < min){
        float swap = min;
        min = max;
        max = swap;
    }
    return ((float)rand()/RAND_MAX * (max - min)) + min;
}

/**
 * dropout層反向傳播函數
 * @param l 當前dropout層網絡
 * @param net 整個網絡
 *
 * 說明: dropout層的反向傳播相對簡單,因爲其本身沒有訓練參數,也沒有激活函數,或者說激活函數爲f(x) =x,
 * 也就是激活函數關於加權輸入的導數值爲1, 因此其自身的誤差項值以後由下一層網絡反向傳播時計算完了,
 * 沒有必要再曾以激活函數關於加權輸入的導數了.剩下要做的就是計算上一層的誤差項net.delta, 這個計算也很簡單;
 */
void backward_dropout_layer(dropout_layer l, network net)
{
    int i;
    // 如果net.delta爲空,則返回(net.delta爲空則說明已經反向傳播到第一層了,此處所指定的第一層,是net.layers[0]
    // 也就是與輸入層直接相連的第一層隱含層, 詳細見 network.c 中的 forward_network()h函數)
    if(!net.delta) return;

    // 由於當前dropout層與上一層之間的連接沒有權重,或者說連接權重爲0(對於捨棄的輸入)或固定的l.scale(保留的輸入,這個比例因子是固定的,
    // 不需要訓練),所以計算過程比較簡單,只需讓保留輸入對應輸出的誤差項值乘以l.scale, 其他輸入(輸入是針對當前dropout層而言,實際上爲上一層的輸出)
    // 的誤差項直接置爲0即可
    for(i = 0; i < l.batch * l.inputs; ++i){
        // 與前向過程 forward_dropout_layer 照應,根據l.rand指示,
        float r = l.rand[i];
        if(r < l.probability) net.delta[i] = 0;//如果r < probability,說明捨棄的輸入,其誤差項值爲0;
        else net.delta[i] *= l.scale; //保留下的輸入元素,其誤差項值爲當前層對應輸出的誤差項值乘以l.scale;
    }
}

完,

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