本系列为darknet源码解析,本次解析为src/connect_layer.h 和 src/connect_layer.c 两个,connect_layer主要是构建全连接网络。
在阅读本文之前请事先手推一遍BP算法,这样有助于你对connect_layer源码的理解。主要理解BP算法对隐藏层权重和偏置如何求偏导数,以及最输出层求偏导数。
注意:在全连接网络中的添加BN操作,
- W×X之后,进行BN,然后Activate。
- 还是先W×X+bias,在BN,最后Activate。
如果进行BN操作是不需要进行+bias,因为这样会造成功能重复!!!
connected_layer.h的解析如下:
#ifndef CONNECTED_LAYER_H
#define CONNECTED_LAYER_H
#include "activations.h"
#include "layer.h"
#include "network.h"
// 构造全连接层函数
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);
#ifdef GPU
void forward_connected_layer_gpu(layer l, network net);
void backward_connected_layer_gpu(layer l, network net);
void update_connected_layer_gpu(layer l, update_args a);
void push_connected_layer(layer l);
void pull_connected_layer(layer l);
#endif
#endif
connected_layer.c 的解析如下:
#include "connected_layer.h"
#include "convolutional_layer.h"
#include "batchnorm_layer.h"
#include "utils.h"
#include "cuda.h"
#include "blas.h"
#include "gemm.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/**
* 构建全连接层
* @param batch 该层输入中一个batch所含有的图片张数,等于net.batch
* @param inputs 全连接层每张输入图片的元素个数
* @param outputs 全连接层输出元素个数(由网络配置文件指定,如果未指定,默认值为1,在parse_connect()中赋值)
* @param activation 激活函数类型
* @param batch_normalize 是否进行BN
* @param adam
* @return 全连接层l
*/
layer make_connected_layer(int batch, int inputs, int outputs, ACTIVATION activation, int batch_normalize, int adam)
{
int i;
layer l = {0};
l.learning_rate_scale = 1;
l.type = CONNECTED;
l.inputs = inputs; // 全连接层一张输入图片的元素个数
l.outputs = outputs; // 全连接层对应一张输入图片输出元素个数
l.batch=batch; // 一个batch中图片的张数
l.batch_normalize = batch_normalize; //是否进行BN
l.h = 1; // 全连接层输入图片高为1, 宽也为1
l.w = 1;
l.c = inputs; // 全连接层的输入通道数等于单张输入图片的元素个数
l.out_h = 1; // 全连接输入图片高也为1, 宽也为1
l.out_w = 1;
l.out_c = outputs; // 全连接层输出图片的通道数等于一张输入图片对应的输出元素个数
l.output = calloc(batch*outputs, sizeof(float)); // 全连接层所有输出(包含整个batch)
l.delta = calloc(batch*outputs, sizeof(float)); // 全连接层误差项(包含整个batch)
// 由下面forward_connected_layer() 函数中调用的gemm()可以看出, l.weights_updates应该理解为outputs行,input列
l.weight_updates = calloc(inputs*outputs, sizeof(float)); // 全连接层权重系数更新值个数等于一张输入图片元素个数与对应输出元素个数之积
l.bias_updates = calloc(outputs, sizeof(float)); // 全连接层偏置更新值个数就等于一张输入图片的输出元素个数
// 由下面forward_connected_layer() 函数中调用的gemm()可以看出, l.weights_updates应该理解为outputs行,input列
l.weights = calloc(outputs*inputs, sizeof(float)); // 全连接层权重系数个数等于一张输入图片元素个数与对应输出元素个数之积
l.biases = calloc(outputs, sizeof(float)); // 全连接层偏置个数就等于一张输入图片的输出元素个数
l.forward = forward_connected_layer; // 全连接层前向传播
l.backward = backward_connected_layer; // 全连接层反向传播
l.update = update_connected_layer; // 全连接层更新
// 全连接层权重初始化:
// 使用He初始化方法, sqrt(2./inputs) * Uniform[-1, 1]
// inputs 全连接层一张输入图片的张数 , 后面是一个[-1, 1] 均匀分布
//float scale = 1./sqrt(inputs);
float scale = sqrt(2./inputs);
for(i = 0; i < outputs*inputs; ++i){
l.weights[i] = scale*rand_uniform(-1, 1);
}
// 全连接层偏置初始化: 全部初始化为0
for(i = 0; i < outputs; ++i){
l.biases[i] = 0;
}
if(adam){ // 采用adam
l.m = calloc(l.inputs*l.outputs, sizeof(float));
l.v = calloc(l.inputs*l.outputs, sizeof(float));
l.bias_m = calloc(l.outputs, sizeof(float));
l.scale_m = calloc(l.outputs, sizeof(float));
l.bias_v = calloc(l.outputs, sizeof(float));
l.scale_v = calloc(l.outputs, sizeof(float));
}
if(batch_normalize){ // 采用BN时
l.scales = calloc(outputs, sizeof(float));
l.scale_updates = calloc(outputs, sizeof(float));
for(i = 0; i < outputs; ++i){
l.scales[i] = 1;
}
l.mean = calloc(outputs, sizeof(float));
l.mean_delta = calloc(outputs, sizeof(float));
l.variance = calloc(outputs, sizeof(float));
l.variance_delta = calloc(outputs, sizeof(float));
l.rolling_mean = calloc(outputs, sizeof(float));
l.rolling_variance = calloc(outputs, sizeof(float));
l.x = calloc(batch*outputs, sizeof(float));
l.x_norm = calloc(batch*outputs, sizeof(float));
}
#ifdef GPU
l.forward_gpu = forward_connected_layer_gpu;
l.backward_gpu = backward_connected_layer_gpu;
l.update_gpu = update_connected_layer_gpu;
l.weights_gpu = cuda_make_array(l.weights, outputs*inputs);
l.biases_gpu = cuda_make_array(l.biases, outputs);
l.weight_updates_gpu = cuda_make_array(l.weight_updates, outputs*inputs);
l.bias_updates_gpu = cuda_make_array(l.bias_updates, outputs);
l.output_gpu = cuda_make_array(l.output, outputs*batch);
l.delta_gpu = cuda_make_array(l.delta, outputs*batch);
if (adam) {
l.m_gpu = cuda_make_array(0, inputs*outputs);
l.v_gpu = cuda_make_array(0, inputs*outputs);
l.bias_m_gpu = cuda_make_array(0, outputs);
l.bias_v_gpu = cuda_make_array(0, outputs);
l.scale_m_gpu = cuda_make_array(0, outputs);
l.scale_v_gpu = cuda_make_array(0, outputs);
}
if(batch_normalize){
l.mean_gpu = cuda_make_array(l.mean, outputs);
l.variance_gpu = cuda_make_array(l.variance, outputs);
l.rolling_mean_gpu = cuda_make_array(l.mean, outputs);
l.rolling_variance_gpu = cuda_make_array(l.variance, outputs);
l.mean_delta_gpu = cuda_make_array(l.mean, outputs);
l.variance_delta_gpu = cuda_make_array(l.variance, outputs);
l.scales_gpu = cuda_make_array(l.scales, outputs);
l.scale_updates_gpu = cuda_make_array(l.scale_updates, outputs);
l.x_gpu = cuda_make_array(l.output, l.batch*outputs);
l.x_norm_gpu = cuda_make_array(l.output, l.batch*outputs);
#ifdef CUDNN
cudnnCreateTensorDescriptor(&l.normTensorDesc);
cudnnCreateTensorDescriptor(&l.dstTensorDesc);
cudnnSetTensor4dDescriptor(l.dstTensorDesc, CUDNN_TENSOR_NCHW, CUDNN_DATA_FLOAT, l.batch, l.out_c, l.out_h, l.out_w);
cudnnSetTensor4dDescriptor(l.normTensorDesc, CUDNN_TENSOR_NCHW, CUDNN_DATA_FLOAT, 1, l.out_c, 1, 1);
#endif
}
#endif
l.activation = activation;
fprintf(stderr, "connected %4d -> %4d\n", inputs, outputs);
return l;
}
typedef struct{
int batch;
float learning_rate;
float momentum;
float decay;
int adam;
float B1;
float B2;
float eps;
int t;
} update_args;
// Y += alpha * X
void axpy_cpu(int N, float ALPHA, float *X, int INCX, float *Y, int INCY)
{
int i;
for(i = 0; i < N; ++i) Y[i*INCY] += ALPHA*X[i*INCX];
}
// X *= alpha
void scal_cpu(int N, float ALPHA, float *X, int INCX)
{
int i;
for(i = 0; i < N; ++i) X[i*INCX] *= ALPHA;
}
/**
* 全连接层更新函数
* @param l 当前层
* @param a 优化参数
*/
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;
// l.biases += learning_rate/batch * l.bias_updates 这里需要除以batch,应该等效于batch个梯度的平均值
axpy_cpu(l.outputs, learning_rate/batch, l.bias_updates, 1, l.biases, 1);
// l.bias_updates *= momentum 计算下次梯度需要的偏置的动量, 引入动量的参数更新: vt = momentum*vt-1
scal_cpu(l.outputs, momentum, l.bias_updates, 1);
if(l.batch_normalize){
// l.scales += learning_rate/batch * l.scale_updates 同理在BN层上更新
axpy_cpu(l.outputs, learning_rate/batch, l.scale_updates, 1, l.scales, 1);
// l.scale_updates *= momentum
scal_cpu(l.outputs, momentum, l.scale_updates, 1);
}
// l.weight_updates += (-decay*batch)*l.weights 计算权重衰减,
axpy_cpu(l.inputs*l.outputs, -decay*batch, l.weights, 1, l.weight_updates, 1);
// l.weights += learning_rate/batch * l.weight_updates // 更新权重
axpy_cpu(l.inputs*l.outputs, learning_rate/batch, l.weight_updates, 1, l.weights, 1);
// l.weight_updates *= momentum //计算下次梯度需要的权重的动量
scal_cpu(l.inputs*l.outputs, momentum, l.weight_updates, 1);
}
/**
* 全连接层前向传播函数
* @param l 当前全连接层
* @param net 整个网络
*/
void forward_connected_layer(layer l, network net)
{
// 将全连接层所有输出(包含所有batch)元素置为0
fill_cpu(l.outputs*l.batch, 0, l.output, 1);
int m = l.batch; // m: 全连接层接收的一个batch中图片张数
int k = l.inputs; // k: 全连接层单张输入图片元素个数
int n = l.outputs; // n: 全连接层对应单张输入图片的输出元素个数
float *a = net.input; // a: 全连接层的输入数据, 维度为l.batch * l.inputs(包含整个batch的输入), 可视为l.batch行, l.inputs列, 每行就是一张图片
float *b = l.weights; // b: 全连接层的所有权重, 维度为l.outputs * l.inputs (存储空间申请见make_connected_layer())
float *c = l.output; // c: 全连接层的所有输出, 维度为l.batch * l.outputs(包含整个batch的输出)
// 根据维度的匹配规则,显然需要对b进行转置,故而调用gemm_nt()函数,最终计算得到c的维度为 l.batch * l.outputs
// 全连接层的输出很好计算,直接矩阵相乘就可以;
gemm(0,1,m,n,k,1,a,k,b,k,1,c,n);
if(l.batch_normalize){
forward_batchnorm_layer(l, net); //有BN就不需要加上偏置了;
} else {
add_bias(l.output, l.biases, l.batch, l.outputs, 1); // 无BN则需要加上偏置
}
// 前向传播最后一步: 这里利用激活函数处理
activate_array(l.output, l.outputs*l.batch, l.activation);
}
void activate_array(float *x, const int n, const ACTIVATION a)
{
int i;
for(i = 0; i < n; ++i){
x[i] = activate(x[i], a);
}
}
void add_bias(float *output, float *biases, int batch, int n, int size)
{
int i,j,b;
for(b = 0; b < batch; ++b){
for(i = 0; i < n; ++i){
for(j = 0; j < size; ++j){
output[(b*n + i)*size + j] += biases[i];
}
}
}
}
void gradient_array(const float *x, const int n, const ACTIVATION a, float *delta)
{
int i;
for(i = 0; i < n; ++i){
delta[i] *= gradient(x[i], a); //gradient f'(x) -> 激活函数求导
}
}
// 计算每个卷积核的偏置更新值,所谓偏置,就是bias= bias - alpha * bias_update中的bias_update
void backward_bias(float *bias_updates, float *delta, int batch, int n, int size)
{
int i,b;
// 遍历batch中每张输入图片
// 注意,最后的偏置更新值是所有输入图片的总和(多张图片无非就是重复一张图片的操作,求和即可)。
// 总之:一个卷积核对应一个偏置更新值,该偏置更新值等于batch所有图片累积的偏置更新值
// 而每张图片也需要进行偏置更新值求和(因为每个卷积核在每张图片多个位置做卷积运算,这都对偏置更新值有贡献)
// 以得到每张图片的总偏置更新值。
for(b = 0; b < batch; ++b){
// 求和得到一张输入图片的总偏置更新值
for(i = 0; i < n; ++i){
//l.n = n; 卷积核的个数
bias_updates[i] += sum_array(delta+size*(i+b*n), size);
}
}
}
/**
* 全连接层反向传播函数
* @param l 当前全连接层
* @param net 整个网络
*/
void backward_connected_layer(layer l, network net)
{
// gradient_array()函数主要完成激活函数对加权输入的导数,并乘以之前得到的l.delta,得到当前层最终的l.delta(误差函数对加权输入的导数)
gradient_array(l.output, l.outputs*l.batch, l.activation, l.delta);
if(l.batch_normalize){
backward_batchnorm_layer(l, net);
} else {
// 计算当前全连接层的偏置的梯度值
// 误差函数对偏置的偏导数实际上就等于以上刚求完的l.delta, 因为一个batch中不只有一张图片,所有将进行效果叠加.
// 不同于卷积层每个卷积核采有一个偏置参数, 全连接层是每个输出元素就对应一个偏置参数,共有l.outputs个,
// 每次循环求完一张图片输出的偏置梯度值.
// 最终会把每一张图的偏置更新叠加,因此,最终l.bias_updates中每一个元素的值是batch中所有图片对应输出元素偏置更新值的叠加.
backward_bias(l.bias_updates, l.delta, l.batch, l.outputs, 1);
}
// 计算当前全连接层权重更新值
int m = l.outputs; // m: 是a^T的行数或者a的列数, 含义为每张图片输出的元素个数
int k = l.batch; // k: 是a^T的列数或者a的行数, b的行数,含义为一个batch中含有图片张数
int n = l.inputs; // n: b的列数, 含义为每张输入图片的元素个数
float *a = l.delta; // a:当前全连接层误差项, 维度 l.batch * l.outputs
float *b = net.input; // b:当前全连接层所有输入, 维度 l.batch * l.inputs
float *c = l.weight_updates; // c:当前全连接层权重更新值, 维度 l.outputs * l.inputs (权重个数)
// 由行列匹配规则可知, 需要将a转置, 故而调用gemm_tn() 函数,转置a实际上是想把batch中所有图片的影响叠加
// 全连接层的权重更新值的计算也相对简单,简单的矩阵乘法可完成.
// 当前全连接的误差项乘以当前层的输入即可得到当前全连接层的权重更新值 [建议看推导一遍BP算法,再来看会容易很多]
gemm(1,0,m,n,k,1,a,m,b,n,1,c,n);
// 由当前全连接层计算上一层的误差项(完成绝大部分计算[因为少了乘以前一层激活函数f'(x), 所以只能说完成绝大部分]: 当前全连接层误差项乘以当前还未更新的权重,)
m = l.batch; // a: a的行数, c的行数, 含义一个batch中含有图片的数量
k = l.outputs; // k: a的列数, b的行数, 含义每张输出图片的元素个数
n = l.inputs; // n: b的列数, c的列数, 含义每张输入图片的元素个数
a = l.delta; // a: 当前全连接层误差项,维度为 l.batch * l.outputs
b = l.weights; // b: 当前层权重(连接当前层与上一层) l.outputs * l.inputs
c = net.delta; // c: 上一层误差项(包含整个batch), 维度为l.batch * l.inputs
// 一定注意此时的c等于net.delta,已经在network.c中backward_network()函数中赋值给上一层的delta
// 由行列匹配规则可知,不需要转置. 由全连接层误差项计算上一层的误差项也是很简单的,直接利用矩阵的相乘,将当前l.delta与当前层权重相乘就可以了.
if(c) gemm(0,0,m,n,k,1,a,k,b,n,1,c,n);
}
void denormalize_connected_layer(layer l)
{
int i, j;
for(i = 0; i < l.outputs; ++i){
float scale = l.scales[i]/sqrt(l.rolling_variance[i] + .000001);
for(j = 0; j < l.inputs; ++j){
l.weights[i*l.inputs + j] *= scale;
}
l.biases[i] -= l.rolling_mean[i] * scale;
l.scales[i] = 1;
l.rolling_mean[i] = 0;
l.rolling_variance[i] = 1;
}
}
void statistics_connected_layer(layer l)
{
if(l.batch_normalize){
printf("Scales ");
print_statistics(l.scales, l.outputs);
/*
printf("Rolling Mean ");
print_statistics(l.rolling_mean, l.outputs);
printf("Rolling Variance ");
print_statistics(l.rolling_variance, l.outputs);
*/
}
printf("Biases ");
print_statistics(l.biases, l.outputs);
printf("Weights ");
print_statistics(l.weights, l.outputs);
}
#ifdef GPU
void pull_connected_layer(layer l)
{
cuda_pull_array(l.weights_gpu, l.weights, l.inputs*l.outputs);
cuda_pull_array(l.biases_gpu, l.biases, l.outputs);
cuda_pull_array(l.weight_updates_gpu, l.weight_updates, l.inputs*l.outputs);
cuda_pull_array(l.bias_updates_gpu, l.bias_updates, l.outputs);
if (l.batch_normalize){
cuda_pull_array(l.scales_gpu, l.scales, l.outputs);
cuda_pull_array(l.rolling_mean_gpu, l.rolling_mean, l.outputs);
cuda_pull_array(l.rolling_variance_gpu, l.rolling_variance, l.outputs);
}
}
void push_connected_layer(layer l)
{
cuda_push_array(l.weights_gpu, l.weights, l.inputs*l.outputs);
cuda_push_array(l.biases_gpu, l.biases, l.outputs);
cuda_push_array(l.weight_updates_gpu, l.weight_updates, l.inputs*l.outputs);
cuda_push_array(l.bias_updates_gpu, l.bias_updates, l.outputs);
if (l.batch_normalize){
cuda_push_array(l.scales_gpu, l.scales, l.outputs);
cuda_push_array(l.rolling_mean_gpu, l.rolling_mean, l.outputs);
cuda_push_array(l.rolling_variance_gpu, l.rolling_variance, l.outputs);
}
}
void update_connected_layer_gpu(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;
if(a.adam){ //Adam更新只用在GPU版本
adam_update_gpu(l.weights_gpu, l.weight_updates_gpu, l.m_gpu, l.v_gpu, a.B1, a.B2, a.eps, decay, learning_rate, l.inputs*l.outputs, batch, a.t);
adam_update_gpu(l.biases_gpu, l.bias_updates_gpu, l.bias_m_gpu, l.bias_v_gpu, a.B1, a.B2, a.eps, decay, learning_rate, l.outputs, batch, a.t);
if(l.scales_gpu){
adam_update_gpu(l.scales_gpu, l.scale_updates_gpu, l.scale_m_gpu, l.scale_v_gpu, a.B1, a.B2, a.eps, decay, learning_rate, l.outputs, batch, a.t);
}
}else{
axpy_gpu(l.outputs, learning_rate/batch, l.bias_updates_gpu, 1, l.biases_gpu, 1);
scal_gpu(l.outputs, momentum, l.bias_updates_gpu, 1);
if(l.batch_normalize){
axpy_gpu(l.outputs, learning_rate/batch, l.scale_updates_gpu, 1, l.scales_gpu, 1);
scal_gpu(l.outputs, momentum, l.scale_updates_gpu, 1);
}
axpy_gpu(l.inputs*l.outputs, -decay*batch, l.weights_gpu, 1, l.weight_updates_gpu, 1);
axpy_gpu(l.inputs*l.outputs, learning_rate/batch, l.weight_updates_gpu, 1, l.weights_gpu, 1);
scal_gpu(l.inputs*l.outputs, momentum, l.weight_updates_gpu, 1);
}
}
void forward_connected_layer_gpu(layer l, network net)
{
fill_gpu(l.outputs*l.batch, 0, l.output_gpu, 1);
int m = l.batch;
int k = l.inputs;
int n = l.outputs;
float * a = net.input_gpu;
float * b = l.weights_gpu;
float * c = l.output_gpu;
gemm_gpu(0,1,m,n,k,1,a,k,b,k,1,c,n);
if (l.batch_normalize) {
forward_batchnorm_layer_gpu(l, net);
} else {
add_bias_gpu(l.output_gpu, l.biases_gpu, l.batch, l.outputs, 1);
}
activate_array_gpu(l.output_gpu, l.outputs*l.batch, l.activation);
}
void backward_connected_layer_gpu(layer l, network net)
{
constrain_gpu(l.outputs*l.batch, 1, l.delta_gpu, 1);
gradient_array_gpu(l.output_gpu, l.outputs*l.batch, l.activation, l.delta_gpu);
if(l.batch_normalize){
backward_batchnorm_layer_gpu(l, net);
} else {
backward_bias_gpu(l.bias_updates_gpu, l.delta_gpu, l.batch, l.outputs, 1);
}
int m = l.outputs;
int k = l.batch;
int n = l.inputs;
float * a = l.delta_gpu;
float * b = net.input_gpu;
float * c = l.weight_updates_gpu;
gemm_gpu(1,0,m,n,k,1,a,m,b,n,1,c,n);
m = l.batch;
k = l.outputs;
n = l.inputs;
a = l.delta_gpu;
b = l.weights_gpu;
c = net.delta_gpu;
if(c) gemm_gpu(0,0,m,n,k,1,a,k,b,n,1,c,n);
}
#endif
完,