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