专业做公司网站,网站制作建设公司推荐,网页设计于制作课程标准,怎么提高网站的转化率前言
从去年十一月份开始学习yolo神经网络用于目标识别的硬件实现#xff0c;到现在已经六个月了。一个硬件工程师#xff0c;C/C基础都差劲的很#xff0c;对照着darknet作者的源码和网上东拼西凑的原理讲解#xff0c;一点一点地摸索。刚开始进度很慢#xff0c;每天都…前言
从去年十一月份开始学习yolo神经网络用于目标识别的硬件实现到现在已经六个月了。一个硬件工程师C/C基础都差劲的很对照着darknet作者的源码和网上东拼西凑的原理讲解一点一点地摸索。刚开始进度很慢每天都感觉学习不了几行代码到后来慢慢的入了门每周都有不菲的收获和重大的进展。总结一下自己这大半年的学习记录一下心路历程也为躬耕于此的有缘人提供哪怕一点点的帮助吧。
相关推荐
网上的资源很多也有很多博主的原理讲解。在这里推荐几个资源 darknet官网https://pjreddie.com/darknet/yolo/ linux系统 github上基于VS的工程https://github.com/AlexeyAB/darknet github上带有注释的工程https://github.com/hgpvision/darknet
yolov3-tiny 原理
Yolo算法采用一个单独的CNN模型实现end-to-end的目标检测首先将输入图片resize到448x448然后送入CNN网络最后处理网络预测结果得到检测的目标。 YOLO 的核心思想就是利用整张图作为网络的输入直接在输出层回归 bounding box边界框 的位置及其所属的类别。将一幅图像分成 SxS 个网格grid cell如果某个 object 的中心落在这个网格中则这个网格就负责预测这个 object。 每个 bounding box 要预测 (x, y, w, h) 和 confidence 共5个值每个网格还要预测一个类别信息记为 C 类。则 SxS个 网格每个网格要预测 B 个 bounding box 每个box中都有 C 个 classes对应的概率值。输出就是 S x S x B x(5C) 的一个 tensor。
注意 class 信息是针对每个网格的confidence 信息是针对每个 bounding box 的。
yolov3-tiny中共有两个输出层yolo层分别为13x13和26x26每个网格可以预测3个bounding box共有80个分类数。所以最后的yolo层的尺寸为13x13x255和26x26x255。 yolov3-tiny网络层结构如下 更直观的一个模型图: 可以看出yolov3-tiny共有23层网络其中包含五种不同的网络层卷积层convolutional(13个)池化层maxpool(6个)路由层route(2个)上采样层upsample(1个)输出层yolo(2个)。Yolov3-tiny中除了Yolo层之前的那个卷积层每个卷积层之后都有BN层,且每个卷积层之后都有激活函数LEAKYyolo层之前是linear。
yolov3-tiny 源码分析
1. 配置网络结构
yolov3-tiny前向传播主要在detector.c中的test_detector函数中完成
/** 本函数是检测模型的一个前向推理测试函数.
* param datacfg 数据集信息文件路径也即cfg/*.data文件文件中包含有关数据集的信息比如cfg/coco.data
* param cfgfile 网络配置文件路径也即cfg/*.cfg文件包含一个网络所有的结构参数比如cfg/yolo.cfg
* param weightfile 已经训练好的网络权重文件路径比如darknet网站上下载的yolo.weights文件
* param filename 待进行检测的图片路径单张图片
* param thresh 阈值类别检测概率大于该阈值才认为其检测结果有效
* param hier_thresh
* param outfile
* param fullscreen
* details 该函数为一个前向推理测试函数不包括训练过程因此如果要使用该函数必须提前训练好网络并加载训练好的网络参数文件
* 这些文件可以在作者网站上根据作者的提示下载到。本函数由darknet.c中的主函数调用严格来说本文件不应纳入darknet网络结构文件夹中
* 其只是一个测试文件或者说是一个example应该放入到example文件夹中新版的darknet已经这样做了可以在github上查看。
* 本函数的流程为.
*/
void test_detector(char *datacfg, char *cfgfile, char *weightfile, char *filename, float thresh,float hier_thresh, int dont_show, int ext_output, int save_labels, char *outfile, int letter_box, int benchmark_layers)
{// 从指定数据文件datacfg.data文件中读入数据信息测试、训练数据信息到options中// options是list类型数据其中的node包含的void指针具体是kvp数据类型具有键值和值类似C中的Maplist *options read_data_cfg(datacfg);// 获取数据集的名称包括路径第二个参数names表明要从options中获取所用数据集的名称信息如names data/coco.nameschar *name_list option_find_str(options, names, data/names.list);int names_size 0;// 从data/**.names中读取物体名称/标签信息char **names get_labels_custom(name_list, names_size); //get_labels(name_list);// 加载data/labels/文件夹中所有的字符标签图片image **alphabet load_alphabet();network net parse_network_cfg_custom(cfgfile, 1, 1); // set batch1 配置各网络层参数重要
在parser.c中的parse_network_cfg_custom函数中根据yolov3-tiny.cfg文件对网络结构进行配置明确各层网络的类型、输入输出通道数、图像尺寸、卷积核大小等。
//配置各网络层参数
network parse_network_cfg_custom(char *filename, int batch, int time_steps)
{// 从神经网络结构参数文件中读入所有神经网络层的结构参数存储到sections中// sections的每个node包含一层神经网络的所有结构参数list *sections read_cfg(filename);// 获取sections的第一个节点可以查看一下cfg/***.cfg文件其实第一块参数以[net]开头不是某层神经网络的参数// 而是关于整个网络的一些通用参数比如学习率衰减率输入图像宽高batch大小等// 具体的关于某个网络层的参数是从第二块开始的如[convolutional],[maxpool]...// 这些层并没有编号只说明了层的属性但层的参数都是按顺序在文件中排好的读入时// sections链表上的顺序就是文件中的排列顺序。node *n sections-front;if(!n) error(Config file has no sections);// 创建网络结构并动态分配内存输入网络层数为sections-size - 1sections的第一段不是网络层而是通用网络参数network net make_network(sections-size - 1);// 所用显卡的卡号gpu_index在cuda.c中用extern关键字声明// 在调用parse_network_cfg()之前使用了cuda_set_device()设置了gpu_index的值号为当前活跃GPU卡号net.gpu_index gpu_index;// size_params结构体元素不含指针变量size_params params;if (batch 0) params.train 0; // allocates memory for Detection onlyelse params.train 1; // allocates memory for Detection Trainingsection *s (section *)n-val;list *options s-options;if(!is_network(s)) error(First section must be [net] or [network]);parse_net_options(options, net);#ifdef GPUprintf(net.optimized_memory %d \n, net.optimized_memory);if (net.optimized_memory 2 params.train) {pre_allocate_pinned_memory((size_t)1024 * 1024 * 1024 * 8); // pre-allocate 8 GB CPU-RAM for pinned memory}
#endif // GPUparams.h net.h;params.w net.w;params.c net.c;params.inputs net.inputs;if (batch 0) net.batch batch;if (time_steps 0) net.time_steps time_steps;if (net.batch 1) net.batch 1;if (net.time_steps 1) net.time_steps 1;if (net.batch net.time_steps) net.batch net.time_steps;params.batch net.batch;params.time_steps net.time_steps;params.net net;printf(mini_batch %d, batch %d, time_steps %d, train %d \n, net.batch, net.batch * net.subdivisions, net.time_steps, params.train);int avg_outputs 0;float bflops 0;size_t workspace_size 0;size_t max_inputs 0;size_t max_outputs 0;n n-next;int count 0;free_section(s);// 此处stderr不是错误提示而是输出结果提示提示网络结构fprintf(stderr, layer filters size/strd(dil) input output\n);while(n){params.index count;fprintf(stderr, %4d , count);s (section *)n-val;options s-options;// 定义网络层layer l { (LAYER_TYPE)0 };// 获取网络层的类别LAYER_TYPE lt string_to_layer_type(s-type);//通过读取网络类型从而配置各网络层的参数if(lt CONVOLUTIONAL){//yolov3-tiny 卷积层 13层l parse_convolutional(options, params);}else if(lt LOCAL){l parse_local(options, params);}else if(lt ACTIVE){l parse_activation(options, params);}else if(lt RNN){l parse_rnn(options, params);}else if(lt GRU){l parse_gru(options, params);}else if(lt LSTM){l parse_lstm(options, params);}else if (lt CONV_LSTM) {l parse_conv_lstm(options, params);}else if(lt CRNN){l parse_crnn(options, params);}else if(lt CONNECTED){l parse_connected(options, params);}else if(lt CROP){l parse_crop(options, params);}else if(lt COST){l parse_cost(options, params);l.keep_delta_gpu 1;}else if(lt REGION){l parse_region(options, params);l.keep_delta_gpu 1;}else if (lt YOLO) {//yolov3-tiny YOLO层 两层l parse_yolo(options, params);l.keep_delta_gpu 1;}else if (lt GAUSSIAN_YOLO) {l parse_gaussian_yolo(options, params);l.keep_delta_gpu 1;}else if(lt DETECTION){l parse_detection(options, params);}else if(lt SOFTMAX){l parse_softmax(options, params);net.hierarchy l.softmax_tree;l.keep_delta_gpu 1;}else if(lt NORMALIZATION){l parse_normalization(options, params);}else if(lt BATCHNORM){l parse_batchnorm(options, params);}else if(lt MAXPOOL){//yolov3-tiny 池化层 maxpool 6层l parse_maxpool(options, params);}else if (lt LOCAL_AVGPOOL) {l parse_local_avgpool(options, params);}else if(lt REORG){l parse_reorg(options, params); }else if (lt REORG_OLD) {l parse_reorg_old(options, params);}else if(lt AVGPOOL){l parse_avgpool(options, params);}else if(lt ROUTE){//yolov3-tiny 路由层 2层l parse_route(options, params);int k;for (k 0; k l.n; k) {net.layers[l.input_layers[k]].use_bin_output 0;net.layers[l.input_layers[k]].keep_delta_gpu 1;}}else if (lt UPSAMPLE) {//yolov3-tiny 上采样层 1层l parse_upsample(options, params, net);}else if(lt SHORTCUT){l parse_shortcut(options, params, net);net.layers[count - 1].use_bin_output 0;net.layers[l.index].use_bin_output 0;net.layers[l.index].keep_delta_gpu 1;}else if (lt SCALE_CHANNELS) {l parse_scale_channels(options, params, net);net.layers[count - 1].use_bin_output 0;net.layers[l.index].use_bin_output 0;net.layers[l.index].keep_delta_gpu 1;}else if (lt SAM) {l parse_sam(options, params, net);net.layers[count - 1].use_bin_output 0;net.layers[l.index].use_bin_output 0;net.layers[l.index].keep_delta_gpu 1;}else if(lt DROPOUT){l parse_dropout(options, params);l.output net.layers[count-1].output;l.delta net.layers[count-1].delta;.........
2. 下载权重文件
在parser.c的load_weights_upto中根据卷积层的网络配置开始下载读取各层的权重文件。
//读取权重文件函数
void load_weights_upto(network *net, char *filename, int cutoff)//cutoff net-n
{
#ifdef GPUif(net-gpu_index 0){cuda_set_device(net-gpu_index);}
#endiffprintf(stderr, Loading weights from %s...\n, filename);fflush(stdout);FILE *fp fopen(filename, rb);if(!fp) file_error(filename);int major;int minor;int revision;fread(major, sizeof(int), 1, fp);//读取一个4字节的数据fread(minor, sizeof(int), 1, fp);//读取一个4字节的数据fread(revision, sizeof(int), 1, fp);//读取一个4字节的数据printf(the size of int in x64 is %d bytes,attention!!!\n, sizeof(int));//x86 x64: 4printf(major ,minor,revision of weight is %d, %d ,%d\n, major, minor, revision);//0.2.0if ((major * 10 minor) 2) {//运行这一部分printf(\n seen 64);uint64_t iseen 0;fread(iseen, sizeof(uint64_t), 1, fp);//读取一个8字节的数据printf(the size of uint64_t is %d\n, sizeof(uint64_t));*net-seen iseen;}else {printf(\n seen 32);uint32_t iseen 0;fread(iseen, sizeof(uint32_t), 1, fp);*net-seen iseen;}*net-cur_iteration get_current_batch(*net);printf(, trained: %.0f K-images (%.0f Kilo-batches_64) \n, (float)(*net-seen / 1000), (float)(*net-seen / 64000));int transpose (major 1000) || (minor 1000);int i;for(i 0; i net-n i cutoff; i){//cutoff net-nlayer l net-layers[i];if (l.dontload) continue;//always 0 跳过之后的循环体直接运行iif(l.type CONVOLUTIONAL l.share_layer NULL){ //只运行这一个分支的代码load_convolutional_weights(l, fp);//printf(network layer [%d] is CONVOLUTIONAL \n,i);}.......
在读取yolov3-tiny各层权重文件前先读取4个和训练有关的参数majorminor, revision和iseen。在前向传播的工程当中并没有实际的应用。
parser.c中的load_convolutional_weights函数具体执行对yolov3-tiny权重文件的下载包括节点参数weight偏置参数bias和批量归一化参数BN。
void load_convolutional_weights(layer l, FILE *fp)
{static int flipped_num;if(l.binary){//load_convolutional_weights_binary(l, fp);//return;}int num l.nweights;//int num l.n*l.c*l.size*l.size;//l.n 输出的层数 l.c输入的层数 int read_bytes;read_bytes fread(l.biases, sizeof(float), l.n, fp);//读取偏置参数 l.n个float数据if (read_bytes 0 read_bytes l.n) printf(\n Warning: Unexpected end of wights-file! l.biases - l.index %d \n, l.index);//fread(l.weights, sizeof(float), num, fp); // as in connected layerif (l.batch_normalize (!l.dontloadscales)){read_bytes fread(l.scales, sizeof(float), l.n, fp);//读取batch normalize 参数 l.n个float数据if (read_bytes 0 read_bytes l.n) printf(\n Warning: Unexpected end of wights-file! l.scales - l.index %d \n, l.index);read_bytes fread(l.rolling_mean, sizeof(float), l.n, fp);//读取batch normalize 参数 l.n个float数据if (read_bytes 0 read_bytes l.n) printf(\n Warning: Unexpected end of wights-file! l.rolling_mean - l.index %d \n, l.index);read_bytes fread(l.rolling_variance, sizeof(float), l.n, fp);//读取batch normalize 参数 l.n个float数据if (read_bytes 0 read_bytes l.n) printf(\n Warning: Unexpected end of wights-file! l.rolling_variance - l.index %d \n, l.index);
将权重参数批量归一化
yolov3-tiny每个卷积层之后激活函数之前都要对结果进行Batch Normalization 由于BN层和卷积操作都是线性的将权重文件进行批量归一化可以代替卷积层之后的BN层 在network.c的fuse_conv_batchnorm函数中实现权重文件和BN层的合并。
void fuse_conv_batchnorm(network net)
{int j;for (j 0; j net.n; j) {layer *l net.layers[j];// printf(the %d layer batch_normalize is %d, groups is %d \n, j, l-batch_normalize, l-groups);if (l-type CONVOLUTIONAL) { //只运行这一分支 合并卷积层和batch_normal//printf( Merges Convolutional-%d and batch_norm \n, j);if (l-share_layer ! NULL) {//l-share_layer always is 0,不运行这个分支l-batch_normalize 0;}if (l-batch_normalize) {//#15,22层卷积卷积之后没有batch normalize其他都要运行这一分支int f;for (f 0; f l-n; f)//该层神经网络 1-n 个输出层权重{l-biases[f] l-biases[f] - (double)l-scales[f] * l-rolling_mean[f] / (sqrt((double)l-rolling_variance[f] .00001));const size_t filter_size l-size*l-size*l-c / l-groups;//kernel_size * kernel_size * c/分组 l-groups存在于卷积层always is 1int i;for (i 0; i filter_size; i) {int w_index f*filter_size i;l-weights[w_index] (double)l-weights[w_index] * l-scales[f] / (sqrt((double)l-rolling_variance[f] .00001));}}free_convolutional_batchnorm(l);//no usel-batch_normalize 0;......
3. 输入图像
yolov3-tiny输入神经网络的图像尺寸为416x416对不符合该尺寸的图像要进行裁剪。在image.c的resize_image函数中完成。这个可以说是整个yolo算法对输入图像唯一进行预处理的地方了。这也是yolo算法在工程应用中极好的地方没有那么多类似于降噪、滤波之类的预处理直接送到网络里就完事了。
//im输入图片 w:416 h:416
//函数作用将输入图片热size到416x416的尺寸基本按照缩放/扩大的策略
image resize_image(image im, int w, int h)
{if (im.w w im.h h) return copy_image(im);image resized make_image(w, h, im.c);//416 x 416 x 3空的地址空间image part make_image(w, im.h, im.c);//416 x im.h x im.c空的地址空间int r, c, k;float w_scale (float)(im.w - 1) / (w - 1);//宽度缩放因子float h_scale (float)(im.h - 1) / (h - 1);//高度缩放因子for(k 0; k im.c; k){for(r 0; r im.h; r){for(c 0; c w; c){//416float val 0;if(c w-1 || im.w 1){//c 415 最后一列val get_pixel(im, im.w-1, r, k);//取原图片最后一列的像素} else {float sx c*w_scale;int ix (int) sx;float dx sx - ix;val (1 - dx) * get_pixel(im, ix, r, k) dx * get_pixel(im, ix1, r, k);}set_pixel(part, c, r, k, val);}}}for(k 0; k im.c; k){for(r 0; r h; r){float sy r*h_scale;int iy (int) sy;float dy sy - iy;for(c 0; c w; c){float val (1-dy) * get_pixel(part, c, iy, k);set_pixel(resized, c, r, k, val);}if(r h-1 || im.h 1) continue;for(c 0; c w; c){float val dy * get_pixel(part, c, iy1, k);add_pixel(resized, c, r, k, val);}}}free_image(part);return resized;
}
4. 前向传播网络
network.c中的forward_network函数是整个神经网络的核心部分各层的网络都在函数指针l.forward(l, state)中完成。
void forward_network(network net, network_state state)
{state.workspace net.workspace;int i;/// 遍历所有层从第一层到最后一层逐层进行前向传播网络总共有net.n层for(i 0; i net.n; i){ state.index i;/// 置网络当前活跃层为当前层即第i层 layer l net.layers[i];/// 获取当前层 if(l.delta state.train){//不执行此分支的代码/// 如果当前层的l.delta已经动态分配了内存则调用fill_cpu()函数将其所有元素的值初始化为0 scal_cpu(l.outputs * l.batch, 0, l.delta, 1);/// 第一个参数为l.delta的元素个数第二个参数为初始化值为0printf(forward_network scal_cpu of %d layer done!\n , i);}//double time get_time_point();l.forward(l, state);//进行卷积运算激活函数池化运算///if layer_type convolutional ; l.forward forward_convolutional_layer;//if layer_type maxpool l.forward forward_maxpool_layer;//if layer_type yolo l.forward forward_yolo_layer;//if layer_type ROUTE l.forward forward_route_layer;其实就是数据的复制和搬移//if layer_type upsample l.forward forward_upsample_layer;; //printf(%d - Predicted in %lf milli-seconds.\n, i, ((double)get_time_point() - time) / 1000);/// 完成某一层的推理时置网络的输入为当前层的输出这将成为下一层网络的输入要注意的是此处是直接更改指针变量net.input本身的值/// 也就是此处是通过改变指针net.input所指的地址来改变其中所存内容的值并不是直接改变其所指的内容而指针所指的地址没变/// 所以在退出forward_network()函数后其对net.input的改变都将失效net.input将回到进入forward_network()之前时的值。 ......
5. 卷积层[convolution]
卷积层在convolutional_layer.c中的forward_convolutional_layer函数实现。
void forward_convolutional_layer(convolutional_layer l, network_state state)
{int out_h convolutional_out_height(l);//获得本层卷积层输出特征图的高、宽int out_w convolutional_out_width(l);int i, j;// l.outputs l.out_h * l.out_w * l.out_c在make各网络层函数中赋值比如make_convolutional_layer()// 对应每张输入图片的所有输出特征图的总元素个数每张输入图片会得到n也即l.out_c张特征图// 初始化输出l.output全为0.0输入l.outputs*l.batch为输出的总元素个数其中l.outputs为batch// 中一个输入对应的输出的所有元素的个数l.batch为一个batch输入包含的图片张数0表示初始化所有输出为0fill_cpu(l.outputs*l.batch, 0, l.output, 1);//将地址l.outputl.outputs*l.batch个float地址空间的数据初始化0.......
作者在进行卷积运算前将输入特征图进行重新排序 c
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;// 计算该层神经网络的输出图像尺寸其实没有必要再次计算的因为在构建卷积层时make_convolutional_layer()函数// 已经调用convolutional_out_width()convolutional_out_height()函数求取了这两个参数// 此处直接使用l.out_h,l.out_w即可函数参数只要传入该层网络指针就可了没必要弄这么多参数int height_col (height 2*pad - ksize) / stride 1;int width_col (width 2*pad - ksize) / stride 1;/// 卷积核大小ksize*ksize是一个卷积核的大小之所以乘以通道数channels是因为输入图像有多通道每个卷积核在做卷积时/// 是同时对同一位置处多通道的图像进行卷积运算这里为了实现这一目的将三通道上的卷积核并在一起以便进行计算因此卷积核/// 实际上并不是二维的而是三维的比如对于3通道图像卷积核尺寸为3*3该卷积核将同时作用于三通道图像上这样并起来就得/// 到含有27个元素的卷积核且这27个元素都是独立的需要训练的参数。所以在计算训练参数个数时一定要注意每一个卷积核的实际/// 训练参数需要乘以输入通道数。int channels_col channels * ksize * ksize;//输入通道// 外循环次数为一个卷积核的尺寸数循环次数即为最终得到的data_col的总行数for (c 0; c channels_col; c) {//行列偏置都是对应着本次循环要操作的输出位置的像素而言的通道偏置是该位置像素所在的输出通道的绝对位置通道数// 列偏移卷积核是一个二维矩阵并按行存储在一维数组中利用求余运算获取对应在卷积核中的列数比如对于// 3*3的卷积核3通道当c0时显然在第一列当c5时显然在第2列当c9时在第二通道上的卷积核的第一列// 当c26时在第三列第三输入通道上int w_offset c % ksize;//0,1,2// 行偏移卷积核是一个二维的矩阵且是按行卷积核所有行并成一行存储在一维数组中的// 比如对于3*3的卷积核处理3通道的图像那么一个卷积核具有27个元素每9个元素对应一个通道上的卷积核互为一样// 每当c为3的倍数就意味着卷积核换了一行h_offset取值为0,1,2对应3*3卷积核中的第1, 2, 3行int h_offset (c / ksize) % ksize;//0,1,2// 通道偏移channels_col是多通道的卷积核并在一起的比如对于3通道3*3卷积核每过9个元素就要换一通道数// 当c0~8时c_im0;c9~17时c_im1;c18~26时c_im2,操作对象是排序后的像素位置int c_im c / ksize / ksize;// 中循环次数等于该层输出图像行数height_col说明data_col中的每一行存储了一张特征图这张特征图又是按行存储在data_col中的某行中for (h 0; h height_col; h) {// 内循环等于该层输出图像列数width_col说明最终得到的data_col总有channels_col行height_col*width_col列for (w 0; w width_col; w) {// 由上面可知对于3*3的卷积核行偏置h_offset取值为0,1,2,当h_offset0时会提取出所有与卷积核第一行元素进行运算的像素// 依次类推加上h*stride是对卷积核进行行移位操作比如卷积核从图像(0,0)位置开始做卷积那么最先开始涉及(0,0)~(3,3)// 之间的像素值若stride2那么卷积核进行一次行移位时下一行的卷积操作是从元素(2,0)2为图像行号0为列号开始int im_row h_offset h * stride;//yolov3-tiny stride 1// 对于3*3的卷积核w_offset取值也为0,1,2当w_offset取1时会提取出所有与卷积核中第2列元素进行运算的像素// 实际在做卷积操作时卷积核对图像逐行扫描做卷积加上w*stride就是为了做列移位// 比如前一次卷积其实像素元素为(0,0)若stride2,那么下次卷积元素起始像素位置为(0,2)0为行号2为列号int im_col w_offset w * stride;// col_index为重排后图像中的像素索引等于c * height_col * width_col h * width_col w还是按行存储所有通道再并成一行// 对应第c通道h行w列的元素int col_index (c * height_col h) * width_col w;//将重排后的图片像素按照左上-右下的顺序计算一维索引//im_col width*im_row width*height*channel 重排前的特征图在内存中的位置索引// im2col_get_pixel函数获取输入图像data_im中第c_im通道im_row,im_col的像素值并赋值给重排后的图像// height和width为输入图像data_im的真实高、宽pad为四周补0的长度注意im_row,im_col是补0之后的行列号// 不是真实输入图像中的行列号因此需要减去pad获取真实的行列号data_col[col_index] im2col_get_pixel(data_im, height, width, channels,im_row, im_col, c_im, pad);// return data_im[im_col width*im_row width*height*channel)];}}}
}
通过gemm进行卷积乘加操作通过add_bias添加偏置。
//进行卷积的乘加运算没有bias偏置参数参与运算
gemm(0, 0, m, n, k, 1, a, k, b, n, 1, c, n);add_bias(l.output, l.biases, l.batch, l.n, out_h*out_w);//每个输出特征图的元素都加上对应通道的偏置参数
6. 池化层[maxpool]
maxpool_layer.c中的forward_maxpool_layer函数完成池化操作。yolov3-tiny保留了池化层并使用最大值池化将尺寸为2x2的核中最大值保留下来。
void forward_maxpool_layer_avx(float *src, float *dst, int *indexes, int size, int w, int h, int out_w, int out_h, int c,int pad, int stride, int batch)
{const int w_offset -pad / 2;const int h_offset -pad / 2;int b, k;for (b 0; b batch; b) {// 对于每张输入图片将得到通道数一样的输出图以输出图为基准按输出图通道行列依次遍历// 这对应图像在l.output的存储方式每张图片按行铺排成一大行然后图片与图片之间再并成一行。// 以输出图为基准进行遍历最终循环的总次数刚好覆盖池化核在输入图片不同位置进行池化操作。#pragma omp parallel forfor (k 0; k c; k) {int i, j, m, n;for (i 0; i out_h; i) {//for (j 0; j out_w; j) {j 0;for (; j out_w; j) {// out_index为输出图中的索引int out_index j out_w*(i out_h*(k c*b));//j out_w * i out_w * iout_h * kfloat max -FLT_MAX;// FLT_MAX为c语言中float.h定义的对大浮点数此处初始化最大元素值为最小浮点数int max_i -1;// 最大元素值的索引初始化为-1// 下面两个循环回到了输入图片计算得到的cur_h以及cur_w都是在当前层所有输入元素的索引内外循环的目的是找寻输入图像中// 以(h_offset i*l.stride, w_offset j*l.stride)为左上起点尺寸为l.size池化区域中的最大元素值max及其在所有输入元素中的索引max_ifor (n 0; n size; n) {//2for (m 0; m size; m) {//2// cur_hcur_w是在所有输入图像中第k通道中的cur_h行与cur_w列index是在所有输入图像元素中的总索引。// 为什么这里少一层对输入通道数的遍历循环呢因为对于最大池化层来说输入与输出通道数是一样的并在上面的通道数循环了int cur_h h_offset i*stride n;int cur_w w_offset j*stride m;int index cur_w w*(cur_h h*(k b*c));// 边界检查正常情况下是不会越界的但是如果有补0操作就会越界了这里的处理方式是直接让这些元素值为-FLT_MAX// 注意虽然称之为补0操作但实际不是补0总之这些补的元素永远不会充当最大元素值。int valid (cur_h 0 cur_h h cur_w 0 cur_w w);float val (valid ! 0) ? src[index] : -FLT_MAX;// 记录这个池化区域中的最大的元素值及其在所有输入元素中的总索引max_i (val max) ? index : max_i;max (val max) ? val : max;}}// 由此得到最大池化层每一个输出元素值及其在所有输入元素中的总索引。// 为什么需要记录每个输出元素值对应在输入元素中的总索引呢因为在下面的反向过程中需要用到在计算当前最大池化层上一层网络的敏感度时// 需要该索引明确当前层的每个元素究竟是取上一层输出也即上前层输入的哪一个元素的值具体见下面backward_maxpool_layer()函数的注释。dst[out_index] max;if (indexes) indexes[out_index] max_i;}}}}
}
7. 路由层[route]
yolov3-tiny中共有两层路由层。第17层路由层从0层开始其实直接将第13层网络的输出结果输入。第20层路由层将第19层和第8层网络结果合并在一起19层在前8层在后。在route_layer.c中的forward_route_layer函数中实现。
void forward_route_layer(const route_layer l, network_state state)
{int i, j;int offset 0;for(i 0; i l.n; i){//l.n 卷积层输出特征图通道数 路由层有几层网络层输入本层 17层1路由第13层 202路由第19、8层int index l.input_layers[i];//输入本网络层的网络层的索引如13198float *input state.net.layers[index].output;//输入等于 之前网络层索引值得输出.outputint input_size l.input_sizes[i];//输入的网络层的数据量int part_input_size input_size / l.groups;//未分组for(j 0; j l.batch; j){//copy_cpu(input_size, input j*input_size, 1, l.output offset j*l.outputs, 1);//从首地址input处复制input_size 个数据到 l.output中copy_cpu(part_input_size, input j*input_size part_input_size*l.group_id, 1, l.output offset j*l.outputs, 1);//l.group_id 0//其实就是copy_cpu(part_input_size, input, 1, l.output offset, 1);}//offset input_size;offset part_input_size;}
}
8. 上采样层[upsample]
yolov3-tiny中第19层是上采样层将18层13x13x128的输入特征图转变为26x26x128的输出特征图。在upsample_layer.c中的forward_upsample_layer函数中完成。
void upsample_cpu(float *in, int w, int h, int c, int batch, int stride, int forward, float scale, float *out)
{int i, j, k, b;for (b 0; b batch; b) {for (k 0; k c; k) {for (j 0; j h*stride; j) {for (i 0; i w*stride; i) {int in_index b*w*h*c k*w*h (j / stride)*w i / stride;int out_index b*w*h*c*stride*stride k*w*h*stride*stride j*w*stride i;if (forward) out[out_index] scale*in[in_index];else in[in_index] scale*out[out_index];}}}}
}
效果
9. 输出层[yolo]
yolo层完成了对13x13x255和26x26x255输入特诊图的logistic逻辑回归计算。每个box的预测宽度和高度不参与逻辑回归在yolo_layer.c中的forward_yolo_layer函数中完成。
//两个yolo层 只对数据进行了logistic处理并没有预测box的位置
//将0-1通道x,y 4-84(confidenceclass)计算logistic三个prior(预测框都是这样)
void forward_yolo_layer(const layer l, network_state state)
{int i, j, b, t, n;//从state.input复制数据到l.outputmemcpy(l.output, state.input, l.outputs*l.batch * sizeof(float));#ifndef GPUprintf(yolo v3 tiny l.n and l.batch of yolo layer is %d and %d \n ,l.n,l.batch);for (b 0; b l.batch; b) {//l.batch 1for (n 0; n l.n; n) {//l.n3yolo层mask 0,1,2 表示每个网络单元预测三个box?//printf(l.coords is %d in yolov3 tiny yolo layer ,l.scale_x_y is %f \n, l.coords, l.scale_x_y);// l.coords 坐标0 l.classes分类数量80 l.scale_x_y:1//l.w:输入特征图宽度 l.h输出特征图高度 int index entry_index(l, b, n*l.w*l.h, 0);//index n*l.w*l.h*(4 l.classes 1)//起始地址为l.output index 个数为2 * l.w*l.h 计算逻辑回归值并保存activate_array(l.output index, 2 * l.w*l.h, LOGISTIC); // x,y,//起始地址为l.output index 个数为2 * l.w*l.h 计算方式为x x*l.scale_x_y -0.5*(l.scale_x_y - 1) 简化后x x//yolov3-tiny l.scale_x_y 1 实际上该函数没有参与任何的运算 scal_add_cpuscal_add_cpu(2 * l.w*l.h, l.scale_x_y, -0.5*(l.scale_x_y - 1), l.output index, 1); // scale x,y//index entry_index(l, b, n*l.w*l.h, 4);//index n*l.w*l.h*(4 l.classes 1) 4*l.w*l.h//起始地址为l.output index,个数为180*l.w*l.h 计算器其逻辑回归值activate_array(l.output index, (1 l.classes)*l.w*l.h, LOGISTIC);}}
10. 预测结果统计[detection ] //w:输入图像宽度640,不一定是416 h:输入图像高度424,不一定是416 thresh:图像置信度阈值0.25 hier:0.5
//map:0 relative:1 num:0 letter:0
//函数作用统计两个yolo层中 置信度大于阈值的box个数并对这个box初始化一段地址空间 dets
//根据网络来填充该地址空间dets
//根据yolo层 计算满足置信度阈值要求的box相对的预测坐标、宽度和高度并将结果保存在dets[count].bbox结构体中
//每个box有80个类别有一个置信度该类别对应的可能性probclass概率*置信度
///舍弃prob小于阈值0.25的box
//将满足阈值的box个数保存到num中
detection *get_network_boxes(network *net, int w, int h, float thresh, float hier, int *map, int relative, int *num, int letter)
{//printf(w、h、thresh、hier and letter is %d 、%d 、%f 、%f and %d\n, w, h, thresh, hier, letter);//函数作用统计两个yolo层中 置信度大于阈值的box个数并对这个box初始化一段地址空间 dets//将满足阈值的box个数保存到num中detection *dets make_network_boxes(net, thresh, num);//根据网络来填充该地址空间dets//根据yolo层 计算满足置信度阈值要求的box相对的预测坐标、宽度和高度并将结果保存在dets[count].bbox结构体中//每个box有80个类别有一个置信度该类别对应的可能性probclass概率*置信度///舍弃prob小于阈值0.25的boxfill_network_boxes(net, w, h, thresh, hier, map, relative, dets, letter);return dets;
}
使用make_network_boxes来创建预测信息的指针变量
// thresh: 置信度阈值
//num: 0
//函数作用统计置信度大于阈值的box个数并对这个box初始化一段地址空间
detection *make_network_boxes(network *net, float thresh, int *num)
{layer l net-layers[net-n - 1];//应该是神经网络最后一层 net-n:24 最后一层yolo层//printf( net-n of network is %d\n ,(net-n));int i;// -thresh 0.25//yolo层yolov3-tiny中共有两层//三个prior预测框对每个预测框中置信度大于thresh 0.25记为一次将次数进行累加并输出//nboxes:即为要保留的box的个数 两个yolo层中的置信度个数一起累加int nboxes num_detections(net, thresh);//-thresh 0.25if (num) {printf(nbox %d \n, num);*num nboxes;//不执行该语句}//申请内存个数为nboxes每个内存大小为sizeof(detection)detection* dets (detection*)xcalloc(nboxes, sizeof(detection));//遍历每个box,每个dets.prob申请80个float类型的内存//dets.uc申请4个float类型的空间位置信息for (i 0; i nboxes; i) {dets[i].prob (float*)xcalloc(l.classes, sizeof(float));// tx,ty,tw,th uncertaintydets[i].uc (float*)xcalloc(4, sizeof(float)); // Gaussian_YOLOv3if (l.coords 4) {//不执行这个分支 l.coords0dets[i].mask (float*)xcalloc(l.coords - 4, sizeof(float));}}return dets;
}
使用get_yolo_detections来统计两层yolo层的预测信息
//w,h:640,424 netw, neth:416,416 thresh:图像置信度阈值0.25 hier:0.5
//map:0 relative:1 letter:0
//根据yolo层 计算满足置信度阈值要求的box相对的预测坐标、宽度和高度并将结果保存在dets[count].bbox结构体中
//每个box有80个类别有一个置信度该类别对应的可能性probclass概率*置信度
///舍弃prob小于阈值0.25的box
int get_yolo_detections(layer l, int w, int h, int netw, int neth, float thresh, int *map, int relative, detection *dets, int letter)
{printf(\n l.batch %d, l.w %d, l.h %d, l.n %d ,netw %d, neth %d \n, l.batch, l.w, l.h, l.n, netw, neth);int i,j,n;float *predictions l.output;//yolo层的输出// This snippet below is not necessary// Need to comment it in order to batch processing 2 images//if (l.batch 2) avg_flipped_yolo(l);int count 0;//printf(yolo layer l.mask[0] is %d, l.mask[1] is %d, l.mask[2] is %d\n, l.mask[0], l.mask[1], l.mask[2]);//printf(yolo layer l.biases[l.mask[0]*2] is %f, l.biases[l.mask[1]*2] is %f, l.biases[l.mask[2]*2] is %f\n, l.biases[l.mask[0] * 2], l.biases[l.mask[1] * 2], l.biases[l.mask[2] * 2]);//遍历yolo层for (i 0; i l.w*l.h; i){//该yolo层输出特征图的宽度、高度13x13 26x26int row i / l.w;int col i % l.w;for(n 0; n l.n; n){//yolo层l.n 3//obj_index:置信度层索引int obj_index entry_index(l, 0, n*l.w*l.h i, 4);//obj_index n*l.w*l.h*(4l.classes1) 4*l.w*l.h i;float objectness predictions[obj_index];//获得对应的置信度//if(objectness thresh) continue; // incorrect behavior for Nan valuesif (objectness thresh) {//只有置信度大于阈值才开始执行该分支//printf(\n objectness %f, thresh %f, i %d, n %d \n, objectness, thresh, i, n);//box_index:yolo层每个像素点有三个box,表示每个box的索引值int box_index entry_index(l, 0, n*l.w*l.h i, 0);//box_index n*l.w*l.h*(4l.classes1) i;//l.biases-偏置参数起始地址 l.mask[n]分别为345012biases偏置参数偏移量//根据yolo层 计算满足置信度阈值要求的box相对的预测坐标、宽度和高度并将结果保存在dets[count].bbox结构体中dets[count].bbox get_yolo_box(predictions, l.biases, l.mask[n], box_index, col, row, l.w, l.h, netw, neth, l.w*l.h);//获取对应的置信度该置信度经过了logisticdets[count].objectness objectness;//获得分类数80int类型dets[count].classes l.classes;for (j 0; j l.classes; j) {//80个类别每个类别对应的概率class_index为其所在层的索引int class_index entry_index(l, 0, n*l.w*l.h i, 4 1 j);//class_index n*l.w*l.h*(4l.classes1) 41j*l.w*l.h i;//每个box有80个类别有一个置信度该类别对应的可能性probclass概率*置信度float prob objectness*predictions[class_index];//舍弃prob小于阈值0.25的boxdets[count].prob[j] (prob thresh) ? prob : 0;}count;}}}correct_yolo_boxes(dets, count, w, h, netw, neth, relative, letter);return count;
}
11. 非极大值抑制[NMS]
//dets:box结构体 nboxes:满足阈值的box个数 l.classe:80 thresh0.45f
//两个box,同一类别进行非极大值抑制遍历
void do_nms_sort(detection *dets, int total, int classes, float thresh)
{int i, j, k;k total - 1;for (i 0; i k; i) {//box个数if (dets[i].objectness 0) {//置信度0 不执行该分支理论上没有objectness 0printf(there is no objectness 0 !!! \n);detection swap dets[i];dets[i] dets[k];dets[k] swap;--k;--i;}}total k 1;//同一类别进行比较for (k 0; k classes; k) {//80个 //box预测的类别for (i 0; i total; i) {//box个数dets[i].sort_class k;}//函数作用将prob较大的box排列到前面qsort(dets, total, sizeof(detection), nms_comparator_v3);for (i 0; i total; i) {//两个box,同一类别进行非极大值抑制//printf( k %d, \t i %d \n, k, i);if (dets[i].prob[k] 0) continue;box a dets[i].bbox;for (j i 1; j total;j){box b dets[j].bbox;if( box_iou(a, b) thresh) dets[j].prob[k] 0;}}}
}
Bonus有用的参考
https://github.com/bobo0810/PytorchNetHub/tree/master/Yolov3_pytorch
https://github.com/HulkMaker/pytorch-yolov3
https://github.com/Peterisfar/YOLOV3