forward_network(net)函数
void forward_network(network *netp)
{
#ifdef GPU//定义GPU使用下面代码
if(netp->gpu_index >= 0){
forward_network_gpu(netp); //前向网络训练
return;
}
#endif
network net = *netp;//使用CPU是使用下面代码训练
int i;
for(i = 0; i < net.n; ++i){//遍历所有网络层进行训练
net.index = i;
layer l = net.layers[i];//通过目录找寻每一层网络
if(l.delta){
fill_cpu(l.outputs * l.batch, 0, l.delta, 1);
/*
void fill_cpu(int N, float ALPHA, float *X, int INCX)
{
int i;
for(i = 0; i < N; ++i) X[i*INCX] = ALPHA;初始化l.delta中的值为0,l.delta内存大小为l.outputs * l.batch
}
*/
}
l.forward(l, net);//CPU前向网络训练
net.input = l.output;
if(l.truth) {
net.truth = l.output;
}
}
calc_network_cost(netp);
}
forward_network_gpu(netp);函数
GPU运行
void forward_network_gpu(network *netp)
{
network net = *netp;//将指针netp指向内存的内容深拷贝到结构体net中去
cuda_set_device(net.gpu_index);//配置cuda
cuda_push_array(net.input_gpu, net.input, net.inputs*net.batch);//将每次的min_batch训练数据导入GPU训练网络中去,即net.inputs*net.batch=602112 x min_batch;
/*
void cuda_push_array(float *x_gpu, float *x, size_t n)
{
size_t size = sizeof(float)*n;
cudaError_t status = cudaMemcpy(x_gpu, x, size, cudaMemcpyHostToDevice);
check_error(status);
}
*/
if(net.truth){
cuda_push_array(net.truth_gpu, net.truth, net.truths*net.batch);//将与之对应的标签导入GPU训练网络,这里的net.truths表示的是一幅图像上所有网格每个网格对应一个框且对应一个类别概率,即7*7*(4+1+20)=1225
}
int i;
for(i = 0; i < net.n; ++i){//遍历所有网络层进行训练
net.index = i;//网络层目录
layer l = net.layers[i];//通过目录找寻每一层网络
if(l.delta_gpu){
fill_gpu(l.outputs * l.batch, 0, l.delta_gpu, 1);//初始化GPU网络每一层输出的 l.delta_gpu为0;功能与fill_cpu一样l.delta内存大小为l.outputs * l.batch
}
l.forward_gpu(l, net);//GPU网络训练,对应了每一层网络的前向训练函数入口
net.input_gpu = l.output_gpu;
net.input = l.output;
if(l.truth) {
net.truth_gpu = l.output_gpu;
net.truth = l.output;
}
}
pull_network_output(netp);
calc_network_cost(netp);
}
1.前向卷积网络函数
GPU运行
void forward_convolutional_layer_gpu(convolutional_layer l, network net)
{
fill_gpu(l.outputs*l.batch, 0, l.output_gpu, 1);//初始化GPU网络每一层输出的 l.output_gpu为0;l.output = calloc(l.batch*l.outputs, sizeof(float));
if(l.binary){//是否进行权重二值化处理,这里若是加载了预训练模型一般不用
//这里的功能与CPU中的函数功能一样,为了方便理解,通过CPU下的函数讲解
binarize_weights_gpu(l.weights_gpu, l.n, l.c/l.groups*l.size*l.size, l.binary_weights_gpu);
swap_binary(&l);//交换l->weights与l->binary_weights的指针指向
}
if(l.xnor){//是否对权重以及输入进行二值化
binarize_weights_gpu(l.weights_gpu, l.n, l.c/l.groups*l.size*l.size, l.binary_weights_gpu);//同上
swap_binary(&l);//同上
binarize_gpu(net.input_gpu, l.c*l.h*l.w*l.batch, l.binary_input_gpu);//为了方便还是讲解CPU函数,与GPU功能一样
net.input_gpu = l.binary_input_gpu;//令net.input_gpu与l.binary_input_gpu指向同一内存地址
}
#ifdef CUDNN//用于加速cuda训练的,这里不再细讲,有兴趣的可以学习一下cuda编程
float one = 1;
cudnnConvolutionForward(cudnn_handle(),
&one,
l.srcTensorDesc,
net.input_gpu,
l.weightDesc,
l.weights_gpu,
l.convDesc,
l.fw_algo,
net.workspace,
l.workspace_size,
&one,
l.dstTensorDesc,
l.output_gpu);
#else//若是未使用CUDNN则运行下面代码,其实就是卷积操作,可以参照卷积公式来看,理解卷积运行过程
int i, j;
int m = l.n/l.groups;
int k = l.size*l.size*l.c/l.groups;
int n = l.out_w*l.out_h;
for(i = 0; i < l.batch; ++i){//遍历min_batch中的每一幅图像
for(j = 0; j < l.groups; ++j){//遍历groups
float *a = l.weights_gpu + j*l.nweights/l.groups;//l.nweights = c/groups*n*size*size;其实就是令指针指向下一幅图像的权重元素
float *b = net.workspace;//指向state.workspace这个工作空间,也就是把原始数据变成行向量放到工作空间里,然后进行卷积计算;l.out_h*l.out_w*l.size*l.size*l.c/l.groups*sizeof(float)
float *c = l.output_gpu + (i*l.groups + j)*n*m;//指向输出的下一幅图像特征图元素
float *im = net.input_gpu + (i*l.groups + j)*l.c/l.groups*l.h*l.w;//指向输入的下一幅图像元素
if (l.size == 1){//如果卷积核尺寸为1,则图像不变
b = im;
} else {
im2col_gpu(im, l.c/l.groups, l.h, l.w, l.size, l.stride, l.pad, b);//完成卷积操作
}
gemm_gpu(0,0,m,n,k,1,a,k,b,n,1,c,n);
}
}
#endif
if (l.batch_normalize) {//是否进行批标准化处理
forward_batchnorm_layer_gpu(l, net);//批标准化处理,具体函数不再赘述,可以参照公式查看
} else {
add_bias_gpu(l.output_gpu, l.biases_gpu, l.batch, l.n, l.out_w*l.out_h);
}
activate_array_gpu(l.output_gpu, l.outputs*l.batch, l.activation);//激活函数
if(l.binary || l.xnor) swap_binary(&l);
}
CPU运行的程序
void forward_convolutional_layer(convolutional_layer l, network net)
{
int i, j;
fill_cpu(l.outputs*l.batch, 0, l.output, 1);
/*
void fill_cpu(int N, float ALPHA, float *X, int INCX)
{
int i;
for(i = 0; i < N; ++i) X[i*INCX] = ALPHA;初始化l.output中的值为0
}
*/
if(l.xnor){//是否对权重以及输入进行二值化
binarize_weights(l.weights, l.n, l.c/l.groups*l.size*l.size, l.binary_weights);
/*
void binarize_weights(float *weights, int n, int size, float *binary)
{
int i, f;
for(f = 0; f < n; ++f){//遍历所有输出通道,即输出特征图数
float mean = 0;
for(i = 0; i < size; ++i){//遍历特征图上的所有权重元素值
mean += fabs(weights[f*size + i]);将特征图上的权重元素值的绝对值相加
}
mean = mean / size;//求得特征图上所有权重元素值的均值
for(i = 0; i < size; ++i){
binary[f*size + i] = (weights[f*size + i] > 0) ? mean : -mean;将权重值依次赋到binary中,当权重值大于0时赋值为mean,若小于0则赋-mean;将权重归一到了(-mean,mean)的范围内
}
}
}
*/
swap_binary(&l);
/*
void swap_binary(convolutional_layer *l)
{
float *swap = l->weights;定义指针swap并令其与l->weights指向同一内存地址
l->weights = l->binary_weights;令l->weights指向l->binary_weights指向的内存地址
l->binary_weights = swap;令l->binary_weights指向swap指向的内存地址,因为之前定义了swap的指向与l->weights相同,故l->binary_weights指向了之前l->weights指向的内存地址,两个指针完成了指向互换;
#ifdef GPU同上
swap = l->weights_gpu;
l->weights_gpu = l->binary_weights_gpu;
l->binary_weights_gpu = swap;
#endif
}
*/
binarize_cpu(net.input, l.c*l.h*l.w*l.batch, l.binary_input);
/*
void binarize_cpu(float *input, int n, float *binary)
{
int i;
for(i = 0; i < n; ++i){对输入进行归一化处理,使其取值范围在(-1,1)内;
binary[i] = (input[i] > 0) ? 1 : -1;
}
}
*/
net.input = l.binary_input;//令指针指向input指向binary_input指向的内存单元
}
int m = l.n/l.groups;//输出通道数
int k = l.size*l.size*l.c/l.groups;//每个卷积核的参数数量
int n = l.out_w*l.out_h;//输出特征图的大小
for(i = 0; i < l.batch; ++i){//遍历每幅图像
for(j = 0; j < l.groups; ++j){
float *a = l.weights + j*l.nweights/l.groups;//指向图像的权重,l.weights指向的空间大小为c/groups*n*size*size,l.nweights = c/groups*n*size*size;通过循环正好令其指向下一幅图像的权重
float *b = net.workspace;//用来保存卷积核每个元素所对应的原始图像的元素值,一个卷积元素对应的输入图像元素值为out_h*out_w,而卷积核有size*l.size*l.c个元素,故对应out_h*out_w*size*size*c个元素;
float *c = l.output + (i*l.groups + j)*n*m;//指向output指向的内存,通过循环可以令其指向下一幅图像的输出特征图空间
float *im = net.input + (i*l.groups + j)*l.c/l.groups*l.h*l.w;//指向input指向的内存,通过循环可以令其指向下一幅输入图像的空间
if (l.size == 1) {
b = im;
} else {
im2col_cpu(im, l.c/l.groups, l.h, l.w, l.size, l.stride, l.pad, b);
}
gemm(0,0,m,n,k,1,a,k,b,n,1,c,n);//卷积操作,最后将卷积结果保存到了c;
}
}
if(l.batch_normalize){//批标准化
forward_batchnorm_layer(l, net);
} else {
add_bias(l.output, l.biases, l.batch, l.n, l.out_h*l.out_w);//加上偏置项
}
activate_array(l.output, l.outputs*l.batch, l.activation);//激活函数
if(l.binary || l.xnor) swap_binary(&l);
}
(1)im2col_gpu(im, l.c/l.groups, l.h, l.w, l.size, l.stride, l.pad, b);卷积重要函数可以参考博客来进行理解
void im2col_cpu(float* data_im,int channels, int height, int width,int ksize, int stride, int pad, float* data_col)
{
int c,h,w;
int height_col = (height + 2*pad - ksize) / stride + 1;//卷积后的特征图高
int width_col = (width + 2*pad - ksize) / stride + 1;//卷积后的特征图宽
int channels_col = channels * ksize * ksize;//每个卷积核的参数个数
for (c = 0; c < channels_col; ++c) {//遍历卷积核的每个参数
int w_offset = c % ksize;//卷积核的行坐标例如(0,1,2,0,1,2,0,1,2,......)
int h_offset = (c / ksize) % ksize;//卷积核的列坐标,例如(0,0,0,1,1,1,2,2,2,......)
int c_im = c / ksize / ksize;//输入图像的第几个通道
//这两层循环是用卷积核把图像遍历一遍
for (h = 0; h < height_col; ++h)
{
for (w = 0; w < width_col; ++w)
{
//表示第c_im个卷积核上坐标为(h_offset,w_offset)的元素对应输入图像中元素的坐标(im_row,im_col);
int im_row = h_offset + h * stride;//每个卷积核元素对图像进行卷积时图像元素的行坐标
int im_col = w_offset + w * stride;//每个卷积核元素对图像进行卷积时图像元素的列坐标
//因为图像时按照一维格式保存的,这里为了便于对应原始图像元素,也使用了一维格式,最大目录为l.out_h*l.out_w*l.size*l.size*l.c
int col_index = (c * height_col + h) * width_col + w;
//将该目录下的所对应的原始图像元素导入到data_col内存中;其实存放还是很有规律的,首先存放第一个卷积核的第一个元素所对应的原始图像元素,又因为stride=2,故其对应元素的数量的height_col*width_col;之后依次存放。
data_col[col_index] = im2col_get_pixel(data_im, height, width, channels,im_row, im_col, c_im, pad);
}
}
}
}
float im2col_get_pixel(float *im, int height, int width, int channels,
int row, int col, int channel, int pad)
{
//行列坐标目录都减去pad值,去除不能卷积的边界
row -= pad;
col -= pad;
if (row < 0 || col < 0 ||
row >= height || col >= width) return 0;//防止越界
return im[col + width*(row + height*channel)];//返回卷积核每个元素所对应的原始图像元素
}
下面的图来帮助理解下im2col_cpu()这个函数,为了方便理解,这里假设图像尺寸是5*5, stride=2,kernel_size=3

其实就是根据卷积核每个元素对应输入图像元素值放入了一个暂存内存中,因为输入图像和卷积核在内存中存放都是一维连续存放的,所以需要通过上面的操作来找到各自的位置坐标,float *b指向state.workspace这个工作空间,也就是把原始数据变成行向量放到工作空间里,然后进行卷积计算;
(2)gemm(0,0,m,n,k,1,a,k,b,n,1,c,n);
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)
{
gemm_cpu( TA, TB, M, N, K, ALPHA,A,lda, B, ldb,BETA,C,ldc);
}
void gemm_cpu(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)
{
//printf("cpu: %d %d %d %d %d %f %d %d %f %d\n",TA, TB, M, N, K, ALPHA, lda, ldb, BETA, ldc);
int i, j;
for(i = 0; i < M; ++i){//M就是输出通道的数量,即输出特征图数,这里将遍历每幅特征图
for(j = 0; j < N; ++j){//N就是输出特征图的参数数量,这里将遍历特征图上的每一个参数
C[i*ldc + j] *= BETA;//初始化每个通道特征图上的每个元素;这里由于BETA=1,故初始化为1;
}
}
/*根据指定的TA和TB来选择不同的矩阵乘法方法:
当TA=0,TB=0时,我们进行的是C = ALPHA * A * B + BETA * C操作
当TA=1,TB=0时,进行的是C = ALPHA * A' * B + BETA * C操作
当TA=0,TB=1时,进行的是C = ALPHA * A * B' + BETA * C操作
当TA=1,TB=1时,进行的时C = ALPHA * A' * B' + BETA * C操作
*/
if(!TA && !TB)
gemm_nn(M, N, K, ALPHA,A,lda, B, ldb,C,ldc);//卷积操作
else if(TA && !TB)
gemm_tn(M, N, K, ALPHA,A,lda, B, ldb,C,ldc);
else if(!TA && TB)
gemm_nt(M, N, K, ALPHA,A,lda, B, ldb,C,ldc);
else
gemm_tt(M, N, K, ALPHA,A,lda, B, ldb,C,ldc);
}
**(3)gemm_nn(M, N, K, ALPHA,A,lda, B, ldb,C,ldc);卷积函数 **
void gemm_nn(int M, int N, int K, float ALPHA,
float *A, int lda,
float *B, int ldb,
float *C, int ldc)
{
int i,j,k;
#pragma omp parallel for//表示接下来的for循环将被多线程执行,另外每次循环之间不能有关系。
for(i = 0; i < M; ++i){//遍历所有的卷积核,同时也是遍历输出特征图,因为输出通道和卷积核个数相等
for(k = 0; k < K; ++k){//遍历每个卷积核的每个元素
register float A_PART = ALPHA*A[i*lda+k];//将每个卷积核内的每个元素依次赋给寄存器变量
for(j = 0; j < N; ++j){//遍历每个输出特征图的元素
C[i*ldc+j] += A_PART*B[k*ldb+j];//令与给通道对应的卷积核内的元素依次与其对应位置的图像像素卷积,然后将结果赋给对应位置的输出特征图;
}
}
}
}
这个函数的将卷积后的值放入c所指向的内存中(最终生成number of kernel个2*2的feature map) ,下图只是当只有一个输出特征图的时候;函数结束后,开始循环每一个batch,卷积计算结果依次向后放
(4)forward_batchnorm_layer(l, net);
void forward_batchnorm_layer(layer l, network net)
{
if(l.type == BATCHNORM) copy_cpu(l.outputs*l.batch, net.input, 1, l.output, 1);
copy_cpu(l.outputs*l.batch, l.output, 1, l.x, 1);
if(net.train){
mean_cpu(l.output, l.batch, l.out_c, l.out_h*l.out_w, l.mean);
variance_cpu(l.output, l.mean, l.batch, l.out_c, l.out_h*l.out_w, l.variance);
scal_cpu(l.out_c, .99, l.rolling_mean, 1);
axpy_cpu(l.out_c, .01, l.mean, 1, l.rolling_mean, 1);
scal_cpu(l.out_c, .99, l.rolling_variance, 1);
axpy_cpu(l.out_c, .01, l.variance, 1, l.rolling_variance, 1);
normalize_cpu(l.output, l.mean, l.variance, l.batch, l.out_c, l.out_h*l.out_w);
copy_cpu(l.outputs*l.batch, l.output, 1, l.x_norm, 1);
} else {
normalize_cpu(l.output, l.rolling_mean, l.rolling_variance, l.batch, l.out_c, l.out_h*l.out_w);
}
scale_bias(l.output, l.scales, l.batch, l.out_c, l.out_h*l.out_w);
add_bias(l.output, l.biases, l.batch, l.out_c, l.out_h*l.out_w);
}
对应公式即可理解函数
//blas.c
//计算均值
void mean_cpu(float *x, int batch, int filters, int spatial, float *mean)
{
float scale = 1./(batch * spatial);
int i,j,k;
//注意,这里的均值是不同batch的同一维度的feature的均值
for(i = 0; i < filters; ++i){
mean[i] = 0;
for(j = 0; j < batch; ++j){
for(k = 0; k < spatial; ++k){
int index = j*filters*spatial + i*spatial + k;
mean[i] += x[index];
}
}
mean[i] *= scale;
}
}
//计算方差
void variance_cpu(float *x, float *mean, int batch, int filters, int spatial, float *variance)
{
float scale = 1./(batch * spatial - 1);
int i,j,k;
for(i = 0; i < filters; ++i){
variance[i] = 0;
for(j = 0; j < batch; ++j){
for(k = 0; k < spatial; ++k){
int index = j*filters*spatial + i*spatial + k;
variance[i] += pow((x[index] - mean[i]), 2);
}
}
variance[i] *= scale;
}
}
//归一化
void normalize_cpu(float *x, float *mean, float *variance, int batch, int filters, int spatial)
{
int b, f, i;
for(b = 0; b < batch; ++b){
for(f = 0; f < filters; ++f){
for(i = 0; i < spatial; ++i){
int index = b*filters*spatial + f*spatial + i;
//公式中的ε=.000001f
x[index] = (x[index] - mean[f])/(sqrt(variance[f]) + .000001f);
}
}
}
}
//convolutional_layer.c
void scale_bias(float *output, float *scales, 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){
//scales就是创建convolutional_layer时分配的l.scales,值全是1
output[(b*n + i)*size + j] *= scales[i];
}
}
}
}
2.最大池化层前向网络函数
void forward_maxpool_layer(const maxpool_layer l, network net)
{
int b,i,j,k,m,n;
int w_offset = -l.pad/2;
int h_offset = -l.pad/2;
int h = l.out_h;
int w = l.out_w;
int c = l.c;
for(b = 0; b < l.batch; ++b){//遍历batch中的每个特征图
for(k = 0; k < c; ++k){//遍历所有的特征图
//注意这里的h和w是maxpooling层输出的高度和宽度
for(i = 0; i < h; ++i){
for(j = 0; j < w; ++j){
int out_index = j + w*(i + h*(k + c*b));//因为内存是一维连续存放,所以需要定位输出特征图元素位置
float max = -FLT_MAX;
int max_i = -1;
//寻找最大值
//遍历池化滤波器
for(n = 0; n < l.size; ++n){
for(m = 0; m < l.size; ++m){
//获取当前滤波器对应的输入特征图元素位置
int cur_h = h_offset + i*l.stride + n;
int cur_w = w_offset + j*l.stride + m;
//转化为一维连续存放格式坐标
int index = cur_w + l.w*(cur_h + l.h*(k + b*l.c));
int valid = (cur_h >= 0 && cur_h < l.h &&cur_w >= 0 && cur_w < l.w);
//开始寻找一个滤波器对应元素中的最大值
float val = (valid != 0) ? net.input[index] : -FLT_MAX;
max_i = (val > max) ? index : max_i;
max = (val > max) ? val : max;
}
}
//使用最大值替代之前的值
l.output[out_index] = max;
l.indexes[out_index] = max_i;
}
}
}
}
}
3.定位层前向网络函数
void forward_local_layer(const local_layer l, network net)
{
//输出后的特征图高宽
int out_h = local_out_height(l);
int out_w = local_out_width(l);
/*out_h= (h + 2*pad - ksize) / stride + 1;
int local_out_height(local_layer l)
{
int h = l.h;//输入特征图高
if (!l.pad) h -= l.size;//判断pad是否为0,若是为0则令h=h-l.size,否则h=h-1
else h -= 1;
return h/l.stride + 1;//返回
}
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;
}
*/
int i, j;
int locations = out_h * out_w;//输出特征图的尺寸
//将一个batch中的l.biases中的值复制到l.output的内存中
for(i = 0; i < l.batch; ++i){
copy_cpu(l.outputs, l.biases, 1, l.output + i*l.outputs, 1);
}
/*
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];
}
*/
for(i = 0; i < l.batch; ++i){
//将输入特种图数据导入到net.workspace中去
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;//指向每个输出特征图的起始位置
for(j = 0; j < locations; ++j){//遍历输出特征图的每一个元素;locations = out_h * out_w,即对不同特征图相同位置每个元素都进行local操作
float *a = l.weights + j*l.size*l.size*l.c*l.n;//指向每个权重文件的起始位置,
/*这里可以看出生成的每个特征图上的元素所对应的滤波器都是不同的,卷积时如果是生成一个特征图的话,只需要一个卷积核大小为l.size*l.size*l.c的卷积核即可,对于输出的同一个特征图上所有的元素其卷积核参数是相同的,输出n个特征图则需要n个卷积核;但是在定位层里面就不同了,对应输出的同一个特征图上每个元素的滤波器都是不同的,对与输出一个特征图时,需要out_h*out_w个滤波器,输出n个特征图则需要n*out_h*out_w个卷积核;*/
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(0,0,m,n,k,1,a,k,b,locations,1,c,locations);//进行local操作
}
}
activate_array(l.output, l.outputs*l.batch, l.activation);//激活函数
}
4.dropout前向网络函数
void forward_dropout_layer(dropout_layer l, network_state state)
{
int i;
//dropout层只在训练阶段有效
if (!state.train) return;
for(i = 0; i < l.batch * l.inputs; ++i){
//产生0~1之间的随机数
float r = rand_uniform(0, 1);
l.rand[i] = r;
//如果小于给定概率值,就把相应的输入项赋0
if(r < l.probability) state.input[i] = 0;
else state.input[i] *= l.scale;
}
}
5.全连接层前向网络函数
void forward_connected_layer(connected_layer l, network_state state)
{
int i;
fill_cpu(l.outputs*l.batch, 0, l.output, 1);
int m = l.batch;
int k = l.inputs;
int n = l.outputs;
float *a = state.input;
float *b = l.weights;
float *c = l.output;
//注意这里的TB=1了,所以调用了gemm_tn()这个函数,下面会有介绍
gemm(0,1,m,n,k,1,a,k,b,k,1,c,n);
if(l.batch_normalize){
if(state.train){
mean_cpu(l.output, l.batch, l.outputs, 1, l.mean);
variance_cpu(l.output, l.mean, l.batch, l.outputs, 1, l.variance);
//将l.rolling_mean所有值赋0.95(移动平均什么意思呢?自己百度吧,数据分析用的很多~)
scal_cpu(l.outputs, .95, l.rolling_mean, 1);
//将l.rolling_mean的值加上0.5*l.mean
axpy_cpu(l.outputs, .05, l.mean, 1, l.rolling_mean, 1);
//将l.rolling_variance所有值赋0.95
scal_cpu(l.outputs, .95, l.rolling_variance, 1);
//将l.rolling_variance的值加上0.5*l.variance
axpy_cpu(l.outputs, .05, l.variance, 1, l.rolling_variance, 1);
//将l.output的值赋值到l.x,此时l.x是没有经过BN的
copy_cpu(l.outputs*l.batch, l.output, 1, l.x, 1);
//BN
normalize_cpu(l.output, l.mean, l.variance, l.batch, l.outputs, 1);
//将l.output的值赋值到l.x_norm,此时l.x_norm是经过BN之后的数据
copy_cpu(l.outputs*l.batch, l.output, 1, l.x_norm, 1);
} else {
normalize_cpu(l.output, l.rolling_mean, l.rolling_variance, l.batch, l.outputs, 1);
}
scale_bias(l.output, l.scales, l.batch, l.outputs, 1);
}
for(i = 0; i < l.batch; ++i){
axpy_cpu(l.outputs, 1, l.biases, 1, l.output + i*l.outputs, 1);
}
//线性变换返回值不变
activate_array(l.output, l.outputs*l.batch, l.activation);
}
//gemm.c
void gemm_nt(int M, int N, int K, float ALPHA,
float *A, int lda,
float *B, int ldb,
float *C, int ldc)
{
int i,j,k;
//M=batch,每个样本有N(yolo.train.cfg中是1715=S×S×(B∗5+C))个输出
for(i = 0; i < M; ++i){
for(j = 0; j < N; ++j){
register float sum = 0;
//K是inputs,即输入个数,就是得到一个输出时遍历的权重,如下图所示
for(k = 0; k < K; ++k){
//输入项 和权重项对应相乘相加
sum += ALPHA*A[i*lda+k]*B[j*ldb + k];
}
C[i*ldc+j] += sum;
}
}
}
如上图所示就是得到一个输出时进行的操作,而权重大小为 l.inputs* l.outputs,每次使用inputs个权重与输入进行计算得到一个输出,共有outputs个inputs的权重,故会得到outputs个输出值。这里没有使用偏置值,会在之后的计算中加入;
6.检测层前向网络函数
void forward_detection_layer(const detection_layer l, network net)
{
int locations = l.side*l.side;
int i,j;
memcpy(l.output, net.input, l.outputs*l.batch*sizeof(float));
//if(l.reorg) reorg(l.output, l.w*l.h, size*l.n, l.batch, 1);
int b;
//这里的softmax=0,所以最后竟然都没有softmax层……
if (l.softmax){
for(b = 0; b < l.batch; ++b){
int index = b*l.inputs;
for (i = 0; i < locations; ++i) {
int offset = i*l.classes;
softmax(l.output + index + offset, l.classes, 1, 1,
l.output + index + offset);
}
}
}
//训练的时候才需要cost function
if(net.train){
float avg_iou = 0;
float avg_cat = 0;
float avg_allcat = 0;
float avg_obj = 0;
float avg_anyobj = 0;
int count = 0;
*(l.cost) = 0;
int size = l.inputs * l.batch;
/*void *memset(void *s, int ch, size_t n);
memset是计算机中C/C++语言函数。将s所指向的某一块内存中的前n个 字节的内容全部设置为ch指定的ASCII值*/
memset(l.delta, 0, size * sizeof(float));//将l.delta指向的内存单元数据初始化为0,之后l.delta存放loss function的每一项,最后就是使用它来计算的损失值
for (b = 0; b < l.batch; ++b){//遍历每一个图像
int index = b*l.inputs;//输入图像目录,表示第几个图
//locations = 7*7,49个grid cell,
for (i = 0; i < locations; ++i) {//遍历每一个grid cell
//真实标签索引,指向每个grid cell标签的第一个数,即置信度,其每个grid cell的标签存放格式为(1+20+4),即(置信度,类别概率,真实边界框位置),其中真实标签的置信度只有0和1,0代表该grid cell中没有目标对象,1代表存在目标对象;其类别概率也是hot格式的;
int truth_index = (b*locations+i)*(1+l.coords+l.classes);
//将真实标签中的置信度值赋给is_obj,使得is_obj判断该grid cell是否存在目标
int is_obj = net.truth[truth_index];
//遍历grid cell中的每一个预测框,论文中是2个,计算置信度的损失
for (j = 0; j < l.n; ++j) {
//p_index是预测值的置信度索引,每个网格预测l.n个框,这里l.n=3(cfg文件中的num值),论文中是2;预测标签的存放方式有些不同,在l.output内存中存放格式为(sxsxclass + sxsxnumx1+sxsxnumxcorrd),即(7x7x20+7x7x2x1+7x7x2x4)),将分类与位置预测分开保存了;故此索引表示该grid cell每个预测框的置信度索引
int p_index = index + locations*l.classes+i*l.n + j;
//注意这个l.delta,去格式大小都和l.output相同,其作用是为了保存损失值,之后会用于反向传播;当然在这个函数里面还有的用途就是用于最后的loss计算,这里没有进行平方,会在最后使用;
l.delta[p_index]=l.noobject_scale*(0- l.output[p_index]);
//对应论文公式,这里先假设B个框中都没有物体
*(l.cost) += l.noobject_scale*pow(l.output[p_index], 2);
avg_anyobj += l.output[p_index];//将所有预测框的预测置信度累加,方便一会计算预测置信度平均值
}
int best_index = -1;
float best_iou = 0;
float best_rmse = 20;
//若是is_obj=0,则说明该grid cell没有目标,也就不需要计算其他损失了,直接继续下一个;
if (!is_obj){
continue;
}
int class_index = index + i*l.classes;//类别索引
for(j = 0; j < l.classes; ++j) {//遍历每一种类别
//计算类别的损失
//继续保存类别误差,这里依然没有使用平方
l.delta[class_index+j] = l.class_scale * (net.truth[truth_index+1+j] - l.output[class_index+j]);
*(l.cost) += l.class_scale * pow(net.truth[truth_index+1+j] - l.output[class_index+j], 2);//和论文公式一样
if(net.truth[truth_index + 1 + j]) avg_cat += l.output[class_index+j];//将真实类别的概率类别累加
avg_allcat += l.output[class_index+j];//将所有的概率类别进行累加
}
//计算位置信息的损失
box truth = float_to_box(net.truth + truth_index + 1 + l.classes, 1);//将真实标签位置坐标信息保存到truth
/*
box float_to_box(float *f, int stride)
{
box b = {0};
b.x = f[0];
b.y = f[1*stride];
b.w = f[2*stride];
b.h = f[3*stride];
return b;
}
*/
//将真实坐标进行转化,首先先了解标签坐标的格式,其(x,y,w,h)都是对原图进行了压缩,比如一幅(500, 403)的图像,其中一个框的位置坐标为(245.0 166.0 406.0 232.0),通过对其进行缩放可得(245/500,166/403,406/500,232/403),即(0.49 0.4119106699751861 0.812 0.575682382133995),之后装载时会将其中心坐标转化为相对于grid cell的位置;这里又除以l.side可能是为了令数据变得更小精度更高还有就是为了和预测通过grid建立联系;
truth.x /= l.side;
truth.y /= l.side;
for(j = 0; j < l.n; ++j){
//grid cell中每个预测边界框的位置信息索引
int box_index = index + locations*(l.classes + l.n) + (i*l.n + j) * l.coords;
//将预测位置信息保存到out;
box out = float_to_box(l.output + box_index, 1);
//将中心坐标转化为相对于grid cell的位置,找到与真实标签的关联
out.x /= l.side;
out.y /= l.side;
//是否使用平方,可能是防止负值,这样预测值其实就是根号下的高和宽
if (l.sqrt){
out.w = out.w*out.w;
out.h = out.h*out.h;
}
//计算iou的值
float iou = box_iou(out, truth);
//iou = 0;
//计算均方根误差(root-mean-square error)
float rmse = box_rmse(out, truth);
/*
float box_rmse(box a, box b)
{
return sqrt(pow(a.x-b.x, 2) +
pow(a.y-b.y, 2) +
pow(a.w-b.w, 2) +
pow(a.h-b.h, 2));
}
*/
//选出iou最大或者均方根误差最小的那个框作为该grid cell的预测框
if(best_iou > 0 || iou > 0){
if(iou > best_iou){
best_iou = iou;
best_index = j;
}
}else{
if(rmse < best_rmse){
best_rmse = rmse;
best_index = j;
}
}
}
//如果无法通过上面方法选择出结果则通过下面方法
//强制确定一个最后预测框,默认为0,即默认随机
if(l.forced){
if(truth.w*truth.h < .1){
best_index = 1;
}else{
best_index = 0;
}
}
//随机确定一个最后预测框~
if(l.random && *(net.seen) < 64000){
best_index = rand()%l.n;
}
//网格中最终用来检测的那个预测框坐标索引
int box_index = index + locations*(l.classes + l.n) + (i*l.n + best_index) * l.coords;
//真实标签框坐标索引
int tbox_index = truth_index + 1 + l.classes;
//将最终确认负责grid cell的那个预测框的位置信息赋给out,前面的操作就是为了确定最终的负责grid cell的那个预测框;
box out = float_to_box(l.output + box_index, 1);
out.x /= l.side;
out.y /= l.side;
if (l.sqrt) {
out.w = out.w*out.w;
out.h = out.h*out.h;
}
//获取预测框与真实框交并比
float iou = box_iou(out, truth);
//printf("%d,", best_index);
//负责该grid的那个预测框的置信度索引
int p_index = index + locations*l.classes + i*l.n + best_index;
//之前计算的计算的没有目标的置信度损失是假设了所有的预测框都没有目标,现在需要减去含有目标的置信度损失值
*(l.cost) -= l.noobject_scale * pow(l.output[p_index], 2);
//计算含有目标的预测框置信度损失
*(l.cost) += l.object_scale * pow(1-l.output[p_index], 2);
avg_obj += l.output[p_index];//将所有的含有目标置信度相加
//这里保存了含有目标的预测框置信度误差
l.delta[p_index] = l.object_scale * (1.-l.output[p_index]);
//暂理解为一个开关,非0时通过重打分来调整l.delta(预测值与真实值的差)
if(l.rescore){
//修改调整的误差值
l.delta[p_index] = l.object_scale * (iou - l.output[p_index]);
}
//保存所有的定位误差
l.delta[box_index+0] = l.coord_scale*(net.truth[tbox_index + 0] - l.output[box_index + 0]);
l.delta[box_index+1] = l.coord_scale*(net.truth[tbox_index + 1] - l.output[box_index + 1]);
l.delta[box_index+2] = l.coord_scale*(net.truth[tbox_index + 2] - l.output[box_index + 2]);
l.delta[box_index+3] = l.coord_scale*(net.truth[tbox_index + 3] - l.output[box_index + 3]);
//这里使用了公式中的内容,使用了根号计算
if(l.sqrt){
//修改保存的高宽误差,这里和论文中保持了一致,减少了小框的误差
l.delta[box_index+2] = l.coord_scale*(sqrt(net.truth[tbox_index + 2]) - l.output[box_index + 2]);
l.delta[box_index+3] = l.coord_scale*(sqrt(net.truth[tbox_index + 3]) - l.output[box_index + 3]);
}
//把iou作为损失,这包含了x,y,w,h四个参数,其实后来没用iou来计算损失,而是论文中给的公式
*(l.cost) += pow(1-iou, 2);
avg_iou += iou;
++count;//目标对象计数
}
}
if(0){//未使用
float *costs = calloc(l.batch*locations*l.n, sizeof(float));
for (b = 0; b < l.batch; ++b) {
int index = b*l.inputs;
for (i = 0; i < locations; ++i) {
for (j = 0; j < l.n; ++j) {
int p_index = index + locations*l.classes + i*l.n + j;
costs[b*locations*l.n + i*l.n + j] = l.delta[p_index]*l.delta[p_index];
}
}
}
int indexes[100];
top_k(costs, l.batch*locations*l.n, 100, indexes);
float cutoff = costs[indexes[99]];
for (b = 0; b < l.batch; ++b) {
int index = b*l.inputs;
for (i = 0; i < locations; ++i) {
for (j = 0; j < l.n; ++j) {
int p_index = index + locations*l.classes + i*l.n + j;
if (l.delta[p_index]*l.delta[p_index] < cutoff) l.delta[p_index] = 0;
}
}
}
free(costs);//释放了costs,也就是说前面的计算没有用到,tmd搞了半天不用,作者很考验人的耐性
}
//前面的*(l.cost)其实可以注释掉了,因为前面都没用,到这里才计算loss,直接计算一个min_batch的损失值;不过使用delta保存的误差值计算与论文一样;
*(l.cost) = pow(mag_array(l.delta, l.outputs * l.batch), 2);
/*
float mag_array(float *a, int n)
{
int i;
float sum = 0;
for(i = 0; i < n; ++i){
sum += a[i]*a[i]; //因为之前保存的误差都没有进行平方运算,这里补上
}
return sqrt(sum);//这里返回根号下的值,之后还要在平方,感觉多此一举;
}
*/
printf("Detection Avg IOU: %f, Pos Cat: %f, All Cat: %f, Pos Obj: %f, Any Obj: %f, count: %d\n", avg_iou/count, avg_cat/count, avg_allcat/(count*l.classes), avg_obj/count, avg_anyobj/(l.batch*locations*l.n), count);
//if(l.reorg) reorg(l.delta, l.w*l.h, size*l.n, l.batch, 0);
}
}