[Darknet源码]4. 全连接层的实现

正午 2020-07-06 AM 181℃ 0条

定义一个新的 layer 主要分四个步骤,其实就是实现四个方法

  1. 初始化方法
  2. 前向
  3. 后向
  4. 更新参数
    截屏2020-07-06 上午11.41.56.png

对应到全连接层就是下面四个方法

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);

初始化

初始化的方法主要做一件事情分配内存,初始化参数。这里比较关心权重初始化的值是多少,因为会影响到模型训练。固定写死的方法,都是 [-scale, scale]的均匀分布, scale 是根据输入大小计算的,在这里的输入大小就是 batch 的大小。具体公式见下面的代码

l.weights = calloc(outputs*inputs, sizeof(float));
l.biases = calloc(outputs, sizeof(float));
float scale = sqrt(2./inputs);
    for(i = 0; i < outputs*inputs; ++i){
        l.weights[i] = scale*rand_uniform(-1, 1);
    }

前向

全连接层的公式我简单描述如下,主要都使用 darknet 的变量名来写, outputinput 权重weight都可理解为二维矩阵,之所以只是二维,是因为它们的一行会对应一个样本,相当于把一张图片的RGB 值按行优先拉成一个向量bias 是一个向量,

$$ output = input * weight + bias; $$

这里有个比较关键的问题,均值乘法如何实现的。下面函数中gemm就是完成矩阵计算的, 的定义在 gemm.hgemm.c


void forward_connected_layer(layer l, network net)
{
    fill_cpu(l.outputs*l.batch, 0, l.output, 1);
    int m = l.batch;
    int k = l.inputs;
    int n = l.outputs;
    float *a = net.input;
    float *b = l.weights;
    float *c = l.output;
    gemm(0,1,m,n,k,1,a,k,b,k,1,c,n);
    if(l.batch_normalize){
        forward_batchnorm_layer(l, net);
    } else {
        add_bias(l.output, l.biases, l.batch, l.outputs, 1);
    }
    activate_array(l.output, l.outputs*l.batch, l.activation);
}

gemm 的参数数量多达 13 个,这是不是很难接受,除非参数有啥些规律,不然真的很难记住。这个函数的目的是计算 $ C = ALPHA * AB + BETA * C$, 简单说就是把 AALPHA倍和 B做矩阵乘法然后加上 CBETA倍放到 C里。 下面的注释里解释了每个参数的含义。这个解释也有点儿复杂,总之记住一个事情, 最终逻辑上的矩阵乘法都是 一个 M X K 的矩阵 乘上 K X N 的矩阵, 得到一个 M X N 的矩阵。 TA, TB 都是为了让物理存储结构和逻辑结构保持一致。不过这样解释好像还是不够清楚,这可能就是不使用 Tensor 这种抽象带来的问题吧,感觉具体细节复杂无比。

// TA:0或1 表示 A 是否需要转置
// TB: 0或1 表示 B 是否需要转置
// M: 如果TA=0 就是 A 的行数,TA!=0 就是 A 的列数
// N: 如果 TB = 0 就是 B 的列数,TB != 0 就是 B 的行数
// K: 如果TA=0 就是 A 的列数 如果 TB = 0 就是 B 的列数, 如果 TA!=0 就是 A 的行数, 如果 TB!=0 就是 B 的行数
// lda: A 一行的元素数量
// ldb: B 一行的元素数量
// ldc: C 一行的元素数量

void gemm(int TA, int TB, int M, int N, int K, float ALPHA, 
        float *A, int lda, 
        float *B, int ldb,
        float BETA,
        float *C, int ldc)

后向

为了说清楚后向的逻辑,我先简单定义几个量,考虑第 l
$ L $ 模型的损失函数
$ dL / d(output) $ 损失函数对地 l 层的梯度,这个相当于是 l+1 层的输入的梯度,会在 l+1层计算好。
$ dL / d(weight) $ 损失函数对 l 层权重的梯度
$ dL / d(input) $ 损失函数对 l 层输入的梯度

如果忽略 bias 主要计算后两个梯度, 计算之前,需要对激活函数求导

gradient_array(l.output, l.outputs*l.batch, l.activation, l.delta);

如果有做 batch normal 也要先求导


    if(l.batch_normalize){
        backward_batchnorm_layer(l, net);
    } else {
        backward_bias(l.bias_updates, l.delta, l.batch, l.outputs, 1);
    }

然后对权重的求导,和对输入的求导就是两个矩阵运算了

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

对输入的求导会存放在 net传递到下一层

参数更新

参数更新就是根据更新策略计算好新的权重,当然如果有用动量等,也需要计算这些中间值,存储在 layer里方便下次使用


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;
    axpy_cpu(l.outputs, learning_rate/batch, l.bias_updates, 1, l.biases, 1);
    scal_cpu(l.outputs, momentum, l.bias_updates, 1);

    if(l.batch_normalize){
        axpy_cpu(l.outputs, learning_rate/batch, l.scale_updates, 1, l.scales, 1);
        scal_cpu(l.outputs, momentum, l.scale_updates, 1);
    }

    axpy_cpu(l.inputs*l.outputs, -decay*batch, l.weights, 1, l.weight_updates, 1);
    axpy_cpu(l.inputs*l.outputs, learning_rate/batch, l.weight_updates, 1, l.weights, 1);
    scal_cpu(l.inputs*l.outputs, momentum, l.weight_updates, 1);
}

总结下: 全连接层的重点是理解 Darknet 的矩阵计算是怎么实现的, 以及参数的传递方式, layernet, layerlayer之间的参数是怎么传递的。后面的卷积层也是再此基础上实现的。

标签: none

非特殊说明,本博所有文章均为博主原创。

评论