本系列爲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;
}
}
完,