定义一个新的 layer 主要分四个步骤,其实就是实现四个方法
- 初始化方法
- 前向
- 后向
- 更新参数
对应到全连接层就是下面四个方法
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 的变量名来写, output
和 input
权重weight
都可理解为二维矩阵,之所以只是二维,是因为它们的一行会对应一个样本,相当于把一张图片的RGB 值按行优先拉成一个向量bias
是一个向量,
$$ output = input * weight + bias; $$
这里有个比较关键的问题,均值乘法如何实现的。下面函数中gemm
就是完成矩阵计算的, 的定义在 gemm.h
和 gemm.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$, 简单说就是把 A
的 ALPHA
倍和 B
做矩阵乘法然后加上 C
的 BETA
倍放到 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 的矩阵计算是怎么实现的, 以及参数的传递方式, layer
和 net
, layer
和 layer
之间的参数是怎么传递的。后面的卷积层也是再此基础上实现的。