yolov3-tiny原理解析及代码分析

前言

从去年十一月份开始学习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(5+C) 的一个 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层,且每个卷积层之后都有激活函数LEAKY(yolo层之前是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++中的Map)
    list *options = read_data_cfg(datacfg);
	// 获取数据集的名称(包括路径),第二个参数"names"表明要从options中获取所用数据集的名称信息(如names = data/coco.names)
    char *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 batch=1  配置各网络层参数,重要

在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 - 1,sections的第一段不是网络层,而是通用网络参数
    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 only
    else params.train = 1;              // allocates memory for Detection & Training

    section *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 GPU
    printf("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  // GPU

    params.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 GPU
    if(net->gpu_index >= 0){
        cuda_set_device(net->gpu_index);
    }
#endif
    fprintf(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: 4
	printf("major ,minor,revision of weight is %d, %d ,%d\n", major, minor, revision);//0.2.0
    if ((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->n
        layer l = net->layers[i];
        if (l.dontload) continue;//always 0		跳过之后的循环体,直接运行++i
        if(l.type == CONVOLUTIONAL && l.share_layer == NULL){ //只运行这一个分支的代码
            load_convolutional_weights(l, fp);
			//printf("network layer [%d] is CONVOLUTIONAL \n",i);
        }
        .......

在读取yolov3-tiny各层权重文件前,先读取4个和训练有关的参数:major,minor, 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 layer
    if (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 1
                    int 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 use
                l->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){//416
                float 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, ix+1, 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, iy+1, 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的元素个数,第二个参数为初始化值,为0
			printf("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表示初始化所有输出为0;
    fill_cpu(l.outputs*l.batch, 0, l.output, 1);//将地址l.output,l.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通道),当c=0时,显然在第一列,当c=5时,显然在第2列,当c=9时,在第二通道上的卷积核的第一列,
		// 当c=26时,在第三列(第三输入通道上)
        int w_offset = c % ksize;//0,1,2
		// 行偏移,卷积核是一个二维的矩阵,且是按行(卷积核所有行并成一行)存储在一维数组中的,
		// 比如对于3*3的卷积核,处理3通道的图像,那么一个卷积核具有27个元素,每9个元素对应一个通道上的卷积核(互为一样),
		// 每当c为3的倍数,就意味着卷积核换了一行,h_offset取值为0,1,2,对应3*3卷积核中的第1, 2, 3int h_offset = (c / ksize) % ksize;//0,1,2
		// 通道偏移,channels_col是多通道的卷积核并在一起的,比如对于3通道,3*3卷积核,每过9个元素就要换一通道数,
		// 当c=0~8时,c_im=0;c=9~17时,c_im=1;c=18~26时,c_im=2,操作对象是排序后的像素位置
        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_offset=0时,会提取出所有与卷积核第一行元素进行运算的像素,
				// 依次类推;加上h*stride是对卷积核进行行移位操作,比如卷积核从图像(0,0)位置开始做卷积,那么最先开始涉及(0,0)~(3,3)
				// 之间的像素值,若stride=2,那么卷积核进行一次行移位时,下一行的卷积操作是从元素(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),若stride=2,那么下次卷积元素起始像素位置为(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 for
        for (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 * k
                    float 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_i
                    for (n = 0; n < size; ++n) {//2
                        for (m = 0; m < size; ++m) {//2
                            // cur_h,cur_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(路由第198层)
        int index = l.input_layers[i];//输入本网络层的网络层的索引:如13198
        float *input = state.net.layers[index].output;//输入等于 之前网络层索引值得输出(.output)
        int 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(confidence+class)计算logistic,三个prior(预测框都是这样)
void forward_yolo_layer(const layer l, network_state state)
{
    int i, j, b, t, n;
	//从state.input复制数据到l.output
    memcpy(l.output, state.input, l.outputs*l.batch * sizeof(float));

#ifndef GPU
	printf("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 = 1
        for (n = 0; n < l.n; ++n) {//l.n:3(yolo层)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_cpu
            scal_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,个数为:(1+80*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个类别,有一个置信度,该类别对应的可能性prob:class概率*置信度
///舍弃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个类别,有一个置信度,该类别对应的可能性prob:class概率*置信度
	///舍弃prob小于阈值0.25的box
    fill_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.25

	if (num) {
		printf("nbox = %d \n", num);
		*num = nboxes;//不执行该语句
	}
    //申请内存,个数为nboxes,每个内存大小为:sizeof(detection)
    detection* dets = (detection*)xcalloc(nboxes, sizeof(detection));

	//遍历每个box,每个dets.prob申请80float类型的内存:
	//dets.uc,申请4float类型的空间:位置信息
    for (i = 0; i < nboxes; ++i) {
        dets[i].prob = (float*)xcalloc(l.classes, sizeof(float));
        // tx,ty,tw,th uncertainty
        dets[i].uc = (float*)xcalloc(4, sizeof(float)); // Gaussian_YOLOv3
        
		if (l.coords > 4) {//不执行这个分支 l.coords:0
            dets[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个类别,有一个置信度,该类别对应的可能性prob:class概率*置信度
///舍弃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 26x26
        int 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*(4+l.classes+1) + 4*l.w*l.h + i;
            float objectness = predictions[obj_index];//获得对应的置信度
            //if(objectness <= thresh) continue;    // incorrect behavior for Nan values
            
			if (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*(4+l.classes+1)+ i;

				//l.biases->偏置参数起始地址    l.mask[n]:分别为345012,biases偏置参数偏移量
				//根据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);

				//获取对应的置信度,该置信度经过了logistic
                dets[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*(4+l.classes+1) +4+1+j)*l.w*l.h + i;
                    //每个box有80个类别,有一个置信度,该类别对应的可能性prob:class概率*置信度
					float prob = objectness*predictions[class_index];
					
					//舍弃prob小于阈值0.25的box
                    dets[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    thresh=0.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 = 0
			printf("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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/54435.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

pytorch学习——模型选择

一.概念 模型选择是机器学习中的重要环节&#xff0c;它涉及到从各种统计&#xff0c;机器学习或深度学习模型中选取最佳模型的过程。这涉及到许多关键概念&#xff0c;包括偏差与方差&#xff0c;过拟合与欠拟合&#xff0c;训练误差和泛化误差&#xff0c;交叉验证&#xff0…

【计算机网络】传输层协议 -- TCP协议

文章目录 1. TCP协议的引入2. TCP协议的特点3. TCP协议格式3.1 序号与确认序号3.2 发送缓冲区与接收缓冲区3.3 窗口大小3.4 六个标志位 4. 确认应答机制5. 超时重传机制6. 连接管理机制6.1 三次握手6.2 四次挥手 7. 流量控制8. 滑动窗口9. 拥塞控制10. 延迟应答11. 捎带应答12.…

Inkscape 1.3 版开放源代码 SVG 编辑器发布,新增形状生成器工具和许多更改

导读Inkscape 是功能强大的开源、跨平台、免费 SVG&#xff08;可缩放矢量图形&#xff09;编辑器&#xff0c;今天已更新到稳定的 1.3 版&#xff0c;这是一个引入新功能和许多改进的重要版本。 Inkscape 1.3 是在 Inkscape 1.2 发布一年零两个月后推出的&#xff0c;它引入了…

python-网络爬虫.regular

regular 正则表达式 (regular expression) 正则表达式(regular expression)描述了一种字符串匹配的模式 &#xff08;pattern&#xff09;&#xff0c; 可以用来检查一个串是否含有某种子串、将匹配的子串替换或者从某个串 中取出符合某个条件的子串等。 正则表达式是由普通…

学C的第三十一天【通讯录的实现】

相关代码gitee自取&#xff1a;C语言学习日记: 加油努力 (gitee.com) 接上期&#xff1a; 学C的第三十天【自定义类型&#xff1a;结构体、枚举、联合】_高高的胖子的博客-CSDN博客 通讯录需求&#xff1a; 实现一个通讯录&#xff0c; 通讯录中存放保存人的信息&#xff1…

SpringBoot中MongoDB的使用

SpringBoot中MongoDB的使用 MongoDB 是最早热门非关系数据库的之一&#xff0c;使用也比较普遍&#xff0c;一般会用做离线数据分析来使用&#xff0c;放到内网的居 多。由于很多公司使用了云服务&#xff0c;服务器默认都开放了外网地址&#xff0c;导致前一阵子大批 MongoD…

P1535 [USACO08MAR] Cow Travelling S(dfs+剪枝 or 记忆化搜索)

1:本题暴力做法简单&#xff0c;重点在于我们如何剪枝&#xff1a; :《曼哈顿距离》我们每走一个点就判断&#xff0c;当前点到终点的最短步数是不是小于当前剩余的步数&#xff0c; 如果大于就肯定不符合直接return,或者当步数为0时&#xff0c;当还没到达终点&#xff0c;那…

springSecurity自定义过滤器不生效问题排查

在使用springSecurity过滤器的过程中&#xff0c;由于需要自定义一个过滤器处理数据问题。代码如下&#xff1a; 过滤器定义&#xff1a; public class AuthRequestParamFiler extends GenericFilterBean {private static final CoreLogger LOGGER CoreLoggerFactory.getLog…

Flink - souce算子

水善利万物而不争&#xff0c;处众人之所恶&#xff0c;故几于道&#x1f4a6; 目录 1. 从Java的集合中读取数据 2. 从本地文件中读取数据 3. 从HDFS中读取数据 4. 从Socket中读取数据 5. 从Kafka中读取数据 6. 自定义Source 官方文档 - Flink1.13 1. 从Java的集合中读取数据 …

【python】使用Selenium和Chrome WebDriver来获取 【腾讯云 Cloud Studio 实战训练营】中的文章信息

文章目录 前言导入依赖库设置ChromeDriver的路径创建Chrome WebDriver对象打开网页找到结果元素创建一个空列表用于存储数据遍历结果元素并提取数据提取标题、作者、发布时间等信息判断是否为目标文章提取目标文章的描述、阅读数量、点赞数量、评论数量等信息将提取的数据存储为…

【外卖系统】菜品信息分页查询

需求分析 当菜品数据很多时&#xff0c;用分页的形式来展示列表数据 代码开发 页面发送ajax请求&#xff0c;将分页查询参数提交到服务端&#xff0c;获取分页数据页面发送请求&#xff0c;请求服务端进行图片下载&#xff0c;用于页面图片展示 构造分页 注意&#xff1a;…

Java入门指南:Java语言优势及其特点

目录 1. Java语言简介及发展概述 2. Java语言的优势 2.1 可移植性 2.2 面向对象 2.3 安全性 2.4 大量类库 3. Java语言与C/C的区别 4. 初识Java程序入口之main方法 5. 注释、标识符、关键字 5.1 注释 5.2 标识符 5.3 关键字 1. Java语言简介及发展概述 Java是一种面…

iphone备份用什么软件?好用的苹果数据备份工具推荐!

众所周知&#xff0c;如果要将iPhone的数据跟电脑进行传输备份的话&#xff0c;我们需要用到iTunes这个pc工具。但是对于iTunes&#xff0c;不少人都反映这个软件比较难用&#xff0c;用不习惯。于是&#xff0c;顺应时代命运的iPhone备份同步工具就出现了。那iphone备份用什么…

[css]margin-top不起作用问题(外边距合并)

在初学css时&#xff0c;会遇到突然间margin-top不起作用的情况。如下面&#xff1a; 情况一&#xff1a; 代码&#xff1a; <html> <head><style type"text/css"> * {margin:0;padding:0;border:0; }#outer {width:300px;height:300px;backgroun…

数据库—数据库备份(三十四)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 一、概述 二、数据备份的重要性 三、造成数据丢失的原因 四、备份类型 4.1物理与逻辑角度 4.2数据库备份策略角度 五、常见的备份方法 5.1 物理备份 5.2 使用专用备…

短视频平台视频怎么去掉水印?

短视频怎么去水印&#xff0c;困扰很多人&#xff0c;例如&#xff0c;有些logo水印&#xff0c;动态水印等等&#xff0c;分享操作经验&#xff1a; 抖音作为中国最受欢迎的社交娱乐应用程序之一&#xff0c;已成为许多人日常生活中不可或缺的一部分。在使用抖音过程中&#x…

融合大数据、物联网和人工智能的智慧校园云平台源码 智慧学校源码

电子班牌系统用以展示各个班级的考勤信息、授课信息、精品课程、德育宣传、班级荣誉、校园电视台、考场信息、校园通知、班级风采&#xff0c;是智慧校园和智慧教室的对外呈现窗口&#xff0c;也是学校校园文化宣传和各种信息展示的重要载体。将大数据、物联网和人工智能等新兴…

tinkerCAD案例:24. Ruler - Measuring Lengths 标尺 -量勺

tinkerCAD案例&#xff1a;24. Ruler - Measuring Lengths 标尺 - 测量长度 Project Overview: 项目概况&#xff1a; A machine shop, where any idea can become a reality, can cost millions and million of dollars. Still, the most important tool in the shop is the…

ELK高级搜索(一)

文章目录 ELK搜索1&#xff0e;简介1.1 内容1.2 面向 2&#xff0e;Elastic Stack2.1 简介2.2 特色2.3 组件介绍 3&#xff0e;Elasticsearch3.1 搜索是什么3.2 数据库搜索3.3 全文检索3.4 倒排索引3.5 Lucene3.6 Elasticsearch3.6.1 Elasticsearch的功能3.6.2 Elasticsearch使…

Patchwork 黑客组织瞄准我国大学和研究机构

据知道创宇404高级威胁情报团队近期发现&#xff0c;名为“Patchwork”的黑客组织正以中国的大学和研究机构为目标进行活动&#xff0c;部署名为EyeShell的后门。 Patchwork也被称为“Operation Hangover”和“Zinc Emerson”&#xff0c;被怀疑是来自印度的APT组织。该组织发起…