本系列为darknet源码解析,本次解析为src/local_layer.h 和 src/local_layer.c 两个,local_layer主要是构建局部连接层。local_layer 与conv_layer 工作方式相同,除了权值不共享外, 也就是说,在输入的每个不同部分应用不同的一组过滤器。local_layer仅用于yolo v1中;
局部连接层与卷积层有什么异同:
如下图所示:在卷积操作中,X[:,:,0]通道上的卷积滑动的每一处位置,一共有9个,权重W0[:,:,0]各处都是一样的。而在局部连接层中是不共享的,也就是9处都是不一样权重;
- 局部连接层前向传播
局部连接操作如下图所示:在这里batch=1,输入图片h*w为5*5,通道数c为3;卷积模板数n为2,局部连接窗口大小size为3*3,卷积步幅stride为1,补零个数pad为1;
1. 计算局部后特征图的大小?
如果pad不为0,则 out_h = (h -1)/stride + 1,out_w = (w -1)/stride + 1;
如果pad为0,则 out_h = (h - size)/stride + 1,out_w = (w -size)/stride + 1;
2. 局部连接层的权重值有多少个?n个卷积模板,每个模板权重参数个数 c*size*size*out_h*out_w;
3. 局部连接层有多少偏置值?out_w*out_h*c;
4. im2col(), gemm()在前向计算怎么使用的?im2col()将每张图片重排为B=[c*size*size, out_w*out_h],n个卷积组成A=[n, c*size*size], gemm()完成卷积元素(实际上就是矩阵乘法)卷积后输出结果C=A*B=[n, out_w*out_h];
- 局部连接层反向传播
与卷积层一致,这里不展开,详细见源码解释;
local_layer.h的声明如下:
#ifndef LOCAL_LAYER_H
#define LOCAL_LAYER_H
#include "cuda.h"
#include "image.h"
#include "activations.h"
#include "layer.h"
#include "network.h"
typedef layer local_layer;
#ifdef GPU
void forward_local_layer_gpu(local_layer layer, network net);
void backward_local_layer_gpu(local_layer layer, network net);
void update_local_layer_gpu(local_layer layer, update_args a);
void push_local_layer(local_layer layer);
void pull_local_layer(local_layer layer);
#endif
// 构造local_layer层
local_layer make_local_layer(int batch, int h, int w, int c, int n, int size, int stride, int pad, ACTIVATION activation);
// local_layer层的正向,反向,更新函数
void forward_local_layer(const local_layer layer, network net);
void backward_local_layer(local_layer layer, network net);
void update_local_layer(local_layer layer, update_args a);
void bias_output(float *output, float *biases, int batch, int n, int size);
void backward_bias(float *bias_updates, float *delta, int batch, int n, int size);
#endif
local_layer.c 详细解析如下:
#include "local_layer.h"
#include "utils.h"
#include "im2col.h"
#include "col2im.h"
#include "blas.h"
#include "gemm.h"
#include <stdio.h>
#include <time.h>
// 计算local层输出特征图的高度
int local_out_height(local_layer l)
{
int h = l.h;
if (!l.pad) h -= l.size;
else h -= 1;
return h/l.stride + 1;
}
// 计算local层输出特征图的宽度
int local_out_width(local_layer l)
{
int w = l.w;
if (!l.pad) w -= l.size;
else w -= 1;
return w/l.stride + 1;
}
/**
* 构建yolo v1 local层
* @param batch 每个batch含有图片的张数
* @param h 输入图片的高度
* @param w 宽度
* @param c 通道数
* @param n 卷积核的个数
* @param size 卷积核的大小
* @param stride 卷积核的步幅
* @param pad 补0的个数
* @param activation 激活函数类型
* @return
*/
local_layer make_local_layer(int batch, int h, int w, int c, int n, int size, int stride, int pad, ACTIVATION activation)
{
int i;
local_layer l = {0};
l.type = LOCAL; //层类别
l.h = h; // 输入图片的高度
l.w = w; // 输入图片的宽度
l.c = c; // 输入图片的通道数
l.n = n; // 卷积模板数
l.batch = batch; // 一个batch中包含图片的张数
l.stride = stride; // 卷积步幅
l.size = size; // 卷积核大小
l.pad = pad; // 补0的个数
int out_h = local_out_height(l); // 计算local层输出的特征图的高度
int out_w = local_out_width(l); // 计算local层输出的特征图的宽度
int locations = out_h*out_w; // 一个通道输出特征图包含的元素个数
l.out_h = out_h; // 输出特征图的高度
l.out_w = out_w; // 输出特征图的宽度
l.out_c = n; // 输出特征图的通道数
l.outputs = l.out_h * l.out_w * l.out_c; // local层对应一张输入图片输出元素个数
l.inputs = l.w * l.h * l.c; // local层一张输入图片的元素个数
l.weights = calloc(c*n*size*size*locations, sizeof(float)); // local层的权重个数, 此处就是比传统conv多locations倍
l.weight_updates = calloc(c*n*size*size*locations, sizeof(float)); // local层的权重更新值
l.biases = calloc(l.outputs, sizeof(float)); // local层偏置的个数,输出特征每个元素,其实都是卷积计算的结果;所有偏置有这么多
l.bias_updates = calloc(l.outputs, sizeof(float)); // local层的偏置更新值
// float scale = 1./sqrt(size*size*c);
float scale = sqrt(2./(size*size*c));
for(i = 0; i < c*n*size*size; ++i) l.weights[i] = scale*rand_uniform(-1,1);
l.output = calloc(l.batch*out_h * out_w * n, sizeof(float)); // local层所有输出(包含整个batch的)
l.delta = calloc(l.batch*out_h * out_w * n, sizeof(float)); // local层误差项(包含整个batch的)
l.workspace_size = out_h*out_w*size*size*c;
// local层的前向,反向,更新
l.forward = forward_local_layer;
l.backward = backward_local_layer;
l.update = update_local_layer;
#ifdef GPU
l.forward_gpu = forward_local_layer_gpu;
l.backward_gpu = backward_local_layer_gpu;
l.update_gpu = update_local_layer_gpu;
l.weights_gpu = cuda_make_array(l.weights, c*n*size*size*locations);
l.weight_updates_gpu = cuda_make_array(l.weight_updates, c*n*size*size*locations);
l.biases_gpu = cuda_make_array(l.biases, l.outputs);
l.bias_updates_gpu = cuda_make_array(l.bias_updates, l.outputs);
l.delta_gpu = cuda_make_array(l.delta, l.batch*out_h*out_w*n);
l.output_gpu = cuda_make_array(l.output, l.batch*out_h*out_w*n);
#endif
l.activation = activation; // 激活函数类型
fprintf(stderr, "Local Layer: %d x %d x %d image, %d filters -> %d x %d x %d image\n", h,w,c,n, out_h, out_w, n);
return l;
}
void copy_cpu(int N, float *X, int INCX, float *Y, int INCY)
{
int i;
for(i = 0; i < N; ++i) Y[i*INCY] = X[i*INCX];
}
/**
* local层的前向传播函数
* @param l 当前local层
* @param net 整个网络
*/
void forward_local_layer(const local_layer l, network net)
{
int out_h = local_out_height(l);
int out_w = local_out_width(l);
int i, j;
int locations = out_h * out_w;
for(i = 0; i < l.batch; ++i){
// l.output = l.biases
copy_cpu(l.outputs, l.biases, 1, l.output + i*l.outputs, 1);
}
for(i = 0; i < l.batch; ++i){
float *input = net.input + i*l.w*l.h*l.c; // 计算输入图片的起始位置
im2col_cpu(input, l.c, l.h, l.w,
l.size, l.stride, l.pad, net.workspace); // 对输入图片进行重排, 重排后的大小[c*size*size, out_h*out_w]
float *output = l.output + i*l.outputs; //计算输出图片的起始位置 l.outputs = out_w * out_h * n
for(j = 0; j < locations; ++j){ // locations = out_h * out_w, 这tm是一个一个的计算
float *a = l.weights + j*l.size*l.size*l.c*l.n;
float *b = net.workspace + j;// net
float *c = output + j;
int m = l.n;
int n = 1;
int k = l.size*l.size*l.c;
gemm(0,0,m,n,k,1,a, k,b, locations,1, c,locations);
//gemm_nn(m, n, k, 1,
// a, k,
// b, locations,
// c, locations);
}
}
activate_array(l.output, l.outputs*l.batch, l.activation);
}
/**
* local层的反向传播函数
* @param l
* @param net
*/
void backward_local_layer(local_layer l, network net)
{
int i, j;
int locations = l.out_w*l.out_h; // 每张输出特征图的元素个数
// 计算当前层激活函数对加权输入的导数并乘以l.delta相应元素,从而完成当前层的误差项l.delta计算
// l.output存储了该层网络的所有输出:该层网络接收一个batch的输入图片,其中每张图片经过local层处理后得到的尺寸为: l.out_w, l.out_h
// 该层local共有l.n个卷积核,因此一张输入图片共输出l.n张宽高为l.out_w, l.out_h的特征图(l.outputs为一张图片所有输出特征图的总元素个数)
// 所以所有输入图片也即 l.output中的总元素个数为: l.n * l.out_w * l.out_h * l.batch; `
gradient_array(l.output, l.outputs*l.batch, l.activation, l.delta);
// 计算偏置层的更新值
// 每一个卷积核都有一个偏置,偏置的更新值也即误差函数对偏置的导数,这个导数的计算很简单,实际所有的导数已经求完,都存储在l.delta中
// 接下来只需要把l.delta中对用同一个卷积核的项加起来就可以了。其实local层的每个偏置重复出现batch次,
// local层共有 l.outputs = l.out_h * l.out_w * l.n 个偏置, 要对整个batch进行求和
for(i = 0; i < l.batch; ++i){
// l.bias_updates += l.delta + i*l.outputs
axpy_cpu(l.outputs, 1, l.delta + i*l.outputs, 1, l.bias_updates, 1);
}
// 遍历batch中每张图片,对于l.delta来说,每张图片是分开存的,因此维度会达到 l.batch * out_h * out_w * n
// 对于l.weights, l.weight_updates 以及上面提到的l.bias, l.bias_updates
for(i = 0; i < l.batch; ++i){
float *input = net.input + i*l.w*l.h*l.c;
im2col_cpu(input, l.c, l.h, l.w,
l.size, l.stride, l.pad, net.workspace);
for(j = 0; j < locations; ++j){
float *a = l.delta + i*l.outputs + j;
// net.workspace的元素个数为所有层中最大的l.workspace_size(在make_convolution_layer() 计算得到network_size大小,
// 在parse_network_cfg()中动态分配内存, 此值对应未使用GPU时的情况),
// net.workspace充当一个临时工作空间的作用,存储时所需要的计算参数, 比如没成单张图片的重排后的结果(这些参数马上会参与卷积计算)
// 一旦用完, 就会被马上更新(因此该遍历的情况更新频率比较大)
float *b = net.workspace + j;
float *c = l.weight_updates + j*l.size*l.size*l.c*l.n;
int m = l.n;
int n = l.size*l.size*l.c;
int k = 1;
// 计算local层的权重更新值
// 所谓权重更新就是 weight = weight - alpha * weight_update中的weight_update
// 权重更新值等于当前层误差项中每个元素乘以相应的像素值,因为一个权重跟当前层多个输出关联(权值共享,即卷积核在图像中跨步移动做卷积,每个位置卷积得到的值
// 都与该权值相关), 所以对每一权重更新值来说,需要在l.delta中找出所有与之相关的误差项,乘以相应的像素值,再求和,具体的实现的方式im2col_cpu()与gemm_nt()完成
// 当前全连接的误差项乘以当前层的输入即可得到当前卷积层的权重更新值
// 此处在im2col_cpu操作基础上,利用矩阵乘法 c = alpha*a*b + beta*c完成对图像卷积的操作;
// 0表示不对输入a进行转置, 1表示对输入b进行转置;
// m是输入a,c的行数,具体含义为卷积核的个数(l.n);
// n是输入b和c的列数,具体含义为每个卷积核元素个数乘以输入图像的通道数(l.size*l.size*l.c)
// k是输入a的列数也是b的行数, 具体含义为每个输出特征图的元素个数 (l.out_h * l.out_w)
// a,b,c即为三个参与运算的矩阵(用一维数据存储), alpha=beta=1为常系数
// a为l.delta的一大行, l.delta为本层所有输出(包含整个batch中每张图片的所有特征图)关于加权输入的导数(即激活函数的导数值)集合;
// 元素的个数为 l.batch * l.out_h * l.out_w * l.out_c (l.out_c = l.n), 按行存储, 共有l.batch行, l.out_h * l.out_w * l.out_c列
// 即l.delta中每行包含一张图的所有输出图,故这么一大行,又可以视作有 l.out_c(l.out_c = l.n)个小行, l.out_h * l.out_w小列,而一次循环就是处理
// l.delta的一大行, 故可以视a作为l.out_c行, l.out_h*l.out_w列的矩阵;
// b 为单张输入图像经过im2col_cpu重排后的图像;
// c 为输出,按存储,可视作有l.n行, l.c*size*size列(l.c是输入图像的通道数,l.n是卷积核个数)
// 即c就是所谓的误差项(输出关于加权输入的导数)或者误差项 (一个卷积核有l.c*l.size*l.size个权重,共有l.n个核)
// 由上可知:
// a: (l.out_c) * (1)
// b: (l.c * l.size * l.size) * (1)
// c: (l.n) * (l.c * l.size * l.size) (注意:l.n = l.out_c)
// 故要进行a * b + c计算,必须对b进行转置(否则行列不匹配), 因此调用gemm_nt()函数;
gemm(0,1,m,n,k,1,a,locations,b,locations,1,c,n);
}
// 由当前local层计算上一层的误差项
if(net.delta){
for(j = 0; j < locations; ++j){
float *a = l.weights + j*l.size*l.size*l.c*l.n;
float *b = l.delta + i*l.outputs + j;
float *c = net.workspace + j;
int m = l.size*l.size*l.c;
int n = 1;
int k = l.n;
gemm(1,0,m,n,k,1,a,m,b,locations,0,c,locations);
}
col2im_cpu(net.workspace, l.c, l.h, l.w, l.size, l.stride, l.pad, net.delta+i*l.c*l.h*l.w);
}
}
}
/**
* local层的权重更新函数
* @param l 当前局部连接层
* @param a
*/
void update_local_layer(local_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;
int locations = l.out_w*l.out_h;
int size = l.size*l.size*l.c*l.n*locations;
// 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; 计算下一次梯度需要偏置的动量;
scal_cpu(l.outputs, momentum, l.bias_updates, 1);
// l.weight_updates += -decay*batch * l.weights 计算权重衰减
axpy_cpu(size, -decay*batch, l.weights, 1, l.weight_updates, 1);
// l.weights += learning_rate/batch * l.weight_updates 更新权重
axpy_cpu(size, learning_rate/batch, l.weight_updates, 1, l.weights, 1);
// l.weight_updates *= momentum 计算下次梯度需要的权重的动量
scal_cpu(size, momentum, l.weight_updates, 1);
}
#ifdef GPU
void forward_local_layer_gpu(const local_layer l, network net)
{
int out_h = local_out_height(l);
int out_w = local_out_width(l);
int i, j;
int locations = out_h * out_w;
for(i = 0; i < l.batch; ++i){
copy_gpu(l.outputs, l.biases_gpu, 1, l.output_gpu + i*l.outputs, 1);
}
for(i = 0; i < l.batch; ++i){
float *input = net.input_gpu + i*l.w*l.h*l.c;
im2col_gpu(input, l.c, l.h, l.w,
l.size, l.stride, l.pad, net.workspace);
float *output = l.output_gpu + i*l.outputs;
for(j = 0; j < locations; ++j){
float *a = l.weights_gpu + j*l.size*l.size*l.c*l.n;
float *b = net.workspace + j;
float *c = output + j;
int m = l.n;
int n = 1;
int k = l.size*l.size*l.c;
gemm_gpu(0,0,m,n,k,1,a,k,b,locations,1,c,locations);
}
}
activate_array_gpu(l.output_gpu, l.outputs*l.batch, l.activation);
}
void backward_local_layer_gpu(local_layer l, network net)
{
int i, j;
int locations = l.out_w*l.out_h;
gradient_array_gpu(l.output_gpu, l.outputs*l.batch, l.activation, l.delta_gpu);
for(i = 0; i < l.batch; ++i){
axpy_gpu(l.outputs, 1, l.delta_gpu + i*l.outputs, 1, l.bias_updates_gpu, 1);
}
for(i = 0; i < l.batch; ++i){
float *input = net.input_gpu + i*l.w*l.h*l.c;
im2col_gpu(input, l.c, l.h, l.w,
l.size, l.stride, l.pad, net.workspace);
for(j = 0; j < locations; ++j){
float *a = l.delta_gpu + i*l.outputs + j;
float *b = net.workspace + j;
float *c = l.weight_updates_gpu + j*l.size*l.size*l.c*l.n;
int m = l.n;
int n = l.size*l.size*l.c;
int k = 1;
gemm_gpu(0,1,m,n,k,1,a,locations,b,locations,1,c,n);
}
if(net.delta_gpu){
for(j = 0; j < locations; ++j){
float *a = l.weights_gpu + j*l.size*l.size*l.c*l.n;
float *b = l.delta_gpu + i*l.outputs + j;
float *c = net.workspace + j;
int m = l.size*l.size*l.c;
int n = 1;
int k = l.n;
gemm_gpu(1,0,m,n,k,1,a,m,b,locations,0,c,locations);
}
col2im_gpu(net.workspace, l.c, l.h, l.w, l.size, l.stride, l.pad, net.delta_gpu+i*l.c*l.h*l.w);
}
}
}
void update_local_layer_gpu(local_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;
int locations = l.out_w*l.out_h;
int size = l.size*l.size*l.c*l.n*locations;
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);
axpy_gpu(size, -decay*batch, l.weights_gpu, 1, l.weight_updates_gpu, 1);
axpy_gpu(size, learning_rate/batch, l.weight_updates_gpu, 1, l.weights_gpu, 1);
scal_gpu(size, momentum, l.weight_updates_gpu, 1);
}
void pull_local_layer(local_layer l)
{
int locations = l.out_w*l.out_h;
int size = l.size*l.size*l.c*l.n*locations;
cuda_pull_array(l.weights_gpu, l.weights, size);
cuda_pull_array(l.biases_gpu, l.biases, l.outputs);
}
void push_local_layer(local_layer l)
{
int locations = l.out_w*l.out_h;
int size = l.size*l.size*l.c*l.n*locations;
cuda_push_array(l.weights_gpu, l.weights, size);
cuda_push_array(l.biases_gpu, l.biases, l.outputs);
}
#endif