在上一篇文章中,我们分析了darknet的网络结构与初始化过程,在源码中是load_network函数。接下来继续分析数据加载过程。数据加载过程中涉及的函数有load_data、load_threads、load_data_in_thread、load_thread。
数据加载过程如下图所示。
(1)load_data中首先启动一个线程调用load_threads,load_threads是实际加载数据的线程,再load_threads加载完成前,主线程会等待。
(2)在load_threads通过循环方式,共调用load_data_in_thread函数64次。数据加载过程在load_data_in_thread函数中完成。
以下几行代码需要注意:
data *buffers = calloc(args.threads, sizeof(data));
pthread_t *threads = calloc(args.threads, sizeof(pthread_t));
for(i = 0; i < args.threads; ++i){
args.d = buffers + i;
args.n = (i+1) * total/args.threads - i * total/args.threads;
threads[i] = load_data_in_thread(args);
}
共申请了64个data类型空间与64个pthread_t类型空间,在循环中分别调用load_data_in_thread,每次args.d的地址向后移动一个data类型空间,args.n的值一直为1。
(3)load_data_in_thread函数中启动线程,并调用load_thread函数,主线程返回load_threads中等待。
(4)load_thread函数调用load_data_detection函数加载数据。
下面对load_data_detection进行详细分析。load_data_detection函数每次仅加载一张图片,因此一个batch共加载64张图片。其中有3个函数需要重点关注。
place_image(orig, nw, nh, dx, dy, sized);
random_distort_image(sized, hue, saturation, exposure);
fill_truth_detection(random_paths[i], boxes, d.y.vals[i], classes, flip, -dx/w, -dy/h, nw/w, nh/h);
以上三个函数的功能是来自于我的推测,还没有详细验证,place_image的功能是将原图(orig)的一部分放入输入图片中,random_distort_image是随机扭曲图片,fill_truth_detection的功能是填充bounding_box,但具体如何变换还没有搞清楚。
至此darknet的数据加载流程就已经搞清楚了,还有另一条线需要明确一下那就是数据是如何返回的。
1)首先是在detector.c中的train_detector函数中有以下几行代码:
data train, buffer;
args.d = &buffer;
通过以上两行代码可以知道,现在args.d指向buffer的地址。
2)进入load_data函数(data.c):
struct load_args *ptr = calloc(1, sizeof(struct load_args));
*ptr = args;
现在ptr是args的拷贝,二者使用不同的内存空间,但由于ptr是args的拷贝,因此ptr.d同样也指向buffer。
3)进入load_threads函数(data.c):
load_args args = *(load_args *)ptr;
data *out = args.d;
free(ptr);
data *buffers = calloc(args.threads, sizeof(data));
相同的套路在此处又出现了一次,此时args是ptr的拷贝,args.d同样指向buffer。然后令out同样指向buffer。接下来free ptr,也即在load_data中申请的内存在load_threads中释放。此处申请的buffers是用来实际存放训练数据的。
args.d = buffers + i;
此处实际上是args的复用。由于args不是指针,是变量,因此分配有内存空间。
4)进入load_data_in_thread函数(data.c):
struct load_args *ptr = calloc(1, sizeof(struct load_args));
*ptr = args;
又是相同的套路,ptr复制args的数据,此时ptr.d指向的是buffers对应的内存地址,注意此处是buffers,不是buffer。
5)进入load_thread函数(data.c):
load_args a = *(struct load_args*)ptr;
*a.d = load_data_detection(a.n, a.paths, a.m, a.w, a.h, a.num_boxes, a.classes, a.jitter, a.hue, a.saturation, a.exposure);
free(ptr);
此处a是ptr的拷贝,a.d指向的是buffers的地址空间,所以不需要再额外调用calloc函数,就可以直接使用*a.d。free ptr后,在load_data_in_thread中申请的内存空间也就被释放了。同时load_thread函数返回后,a变量占用的栈空间也会被释放。
6)数据回传
通过对函数的返回过程进行分析,同时分析数据的回传过程。
(1)在load_thread函数中训练数据已经被存储到buffers指向的内存空间中。同时在load_data_in_thread中申请的空间也被释放了,不存在内存泄漏的问题。
(2)回到load_threads函数中。
*out = concat_datas(buffers, args.threads);
buffers中的数据都被concat在一起,同时复制到out执行的内存空间,而out指向的内存空间恰好是buffer的内存空间,由于已经申请过内存空间了,因此也不需要再calloc了。
for(i = 0; i < args.threads; ++i){
buffers[i].shallow = 1;
free_data(buffers[i]);
}
free(buffers);
free(threads);
通过上述过程彻底释放在load_threads中分配的内存空间。同样没有内存泄漏的问题。
(3)回到load_data函数中:
在load_data函数申请的ptr空间在load_threads中释放了,同样没有内存泄漏问题,同时训练数据也被传回到train_detector中。
还是记录一下当前的问题:
1)Box–Muller transform
2)load_data_detection的详细分析