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

本系列为darknet源码解析,本次解析src/dropout.h 与 src/dropout.c 两个。

在神经网络中应用dropout包括训练和预测两个阶段,在训练阶段,dropout 以一定的概率p随机的"舍弃"一部分神经元点,即这部分神经元节点暂时停止工作,如下图所示。因此,对于一个包含N个节点的神经,在dropout的作用下可看作2^N个模型的集成。这2^N个模型可以看成原始网络的子网络,它们共享部分权值,并且具有相同的网络层数,并且整体的参数数目保持不变,这就大大简化了运算。减少了模型过拟合的风险,增强了模型的泛化能力。

在测试阶段,在前向传播的计算时,每个神经元的输出结果要预先乘以概率1-p为什么要乘以1-p呢???因为在训练的时候只有占比(1-p)的神经元节点参与了训练,那么在测试阶段,所有的神经元节点都参与计算,则得到结果要比训练时平均要大 1/(1-p), 为了避免发生这种情况,就需要神经元输出结果乘以1-p,使得下一层输入规模保持不变。

【darknet源码解析-10】dropout.h 和 dropout.c 解析_第1张图片

在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 
#include 

/**
 * 构建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;
    }
}

完,

你可能感兴趣的:(darknet源码解析)