自打我稀里糊涂把自己的专业变成:人工智能以来,已经过去了快三年。有很长一段时间,都是用PyTorch, matlab这种解释型语言。已经很久很久没有像我的一个朋友一样,怼着几百个头文件嗯看了。
实际上,在很早以前。我其实巧妙地误入过一次“歧途”,就是在大一有个院里的小比赛里。要往一个小车里部署目标检测。当时由于啥也不会,各种版本的问题没有装上torch。误打误撞的装上了DarkNet框架下的YOLOV3。
一个好处就是,DarkNet是纯C语言编写的,而且相对简单小巧,有很好的学习意义。基本可以获得如下的收获:
- 对optim.step(), loss.backward(), compution graph有个更实际的认识
- 掌握大型项目的调试(gdb)
- 复习一下C语言
“现在现在向下穿越 向下穿越 ”
进入项目&命令行传参
因为,我已经很久没有写过C语言了。所以我可能会同时记录一些很细节的事情,因为我可能确实不知道。
我们可以从DarkNet的repo直接git下来。如果你和我一样,只是想在Windows下来看源码,建议用Cygwin来编译,至少我这样就不会报错了。
darknet提供了Python的接口,至于,这个操作的具体原理我后面再看,现在看来就像魔法。总之我们可以一般通过的运行:
1 | ./darknet detector test cfg/coco.data cfg/yolov3.cfg yolov3.weights data/dog.jpg |
这个预测的指令,是用来作出那个经典的狗狗图的预测的。我们就拿它来一步步进入代码里吧。
对于每一个C语言的项目,我们都最好先找到它的入口,我们可以在./examples/darknet.c
里找到main函数:
1 | int main(int argc, char **argv) |
在这里(至少在我这里,Windows下),./darknet
其实是一个可执行文件darknet.exe,所以命令行输入的第一个参数也正是程序本身的名字。test
可以猜出来是决定模式的字符串。后面那几个路径决定了一些杂七杂八的事情(用哪个数据集cfg,用哪个权重…)。
我们关注argv[1] == detector时,接下来我们会进入run_detector():(我们只保留我们需要阅读的部分了,不然太长了。)
1 | void run_detector(int argc, char **argv) |
在我们进一步进入test_detector()之前,我们可以停一下来看一下在run_detector()和main()中时不时出现的,帮我们解析命令行参数的辅助函数们(我来通过他们复习一下指针的知识):
1 | void del_arg(int argc, char **argv, int index) |
实际上,为了夯实一下我不怎么存在的基础,我仔细探究了一下这一过程。首先我们要仔细考察一下char argv[]和char * argv。如果只是作为函数传参,那么这二者是等价的。就像find_arg()和find_int_arg()的传参一样,是没有区别的。
但是,char argv[]实际上是声明了一个数组argv,该数组保存多个指向char类型的指针。char *argv是声明argv是指向“指向char类型”的指针。前者会声明一个数组,所以会在内存中分配连续的一段空间。例如:
1 |
|
可以看到,我们打印了出了strings[i]所指向的地址,这是连续的。strings[0]的首地址是4000, 而strings[1]是4008,这是因为strings[0]由7个char(1个字节1个char)组成,然后被程序自己追加了一个\Null以表分割。所以是8个字节。同理,4008+5=400D,也就得到了strings[2]的开头。
然而,如果我们想用char **argv来办到这点,我们需要用malloc()手动分配空间:
1 |
|
此时分配的并不连续,而是隔了一个固定的间隔。这是为了内存对齐。所以实际上,当我们通过终端把那些字符输入进去。是操作系统帮我们创建了指针char **argv,并且根据输入给他们自动分配空间。
注意,这两种方式开辟的指针数组,他们本身其实都是连续的。所以我们都可以用argv[1]这样的方式来索引它。它们两两之间都隔着8位,因为char*是8位的。
所以,回过头来看那些解析命令行参数的工具函数,它们的原理就是把命令行参数的字符数组传入,如果匹配到需要的,就返回它,同时用后面的字符串覆盖它,最后空出来的那个置零。
注意那些试图匹配特殊的一些输入时用到的find_int_arg(), find_float_arg(), *find_char_arg(),我们发现它调用了两次del_arg(),这是为了同时删除选项,例如float thresh = find_float_arg(argc, argv, “-thresh”, .5);中,命令行一定是以这样的形式输入的:xxx -thresh 0.8 xxxx
而此处预设的0.5,实际上对应find_float_arg()里默认的def参数,效果和我们在python里用argparse,设置default是一样的。
数据结构
现在我们进入test_detector()函数:
1 | void test_detector(char *datacfg, char *cfgfile, char *weightfile, char *filename, float thresh, float hier_thresh, char *outfile, int fullscreen) |
现在,信息量略微大了一些。第一行里突然出现了个”list”,这可不是python,list是需要自己定义了。这里的list是一个双向链表,在darknet.h
里定义了:
1 | typedef struct node{ |
node结构体下的数据类型是任意的,因为其是void型指针。在list.c中,定义了初始化这样双向链表的函数:
1 | list *make_list() |
我们通过读命令行,可以知道在这里read_data_cfg()传入的是字符数组cfg/coco.data
,而这个最为我们遇到的第一个读取配置文件的操作,我们当然需要进一步探究一下:
1 | list *read_data_cfg(char *filename) |
这个函数的输入大概是内容为这样的文件:
1 | classes= 80 |
所以read_data_cfg()会返回一个双向链表的指针,switch-case中会检查是否是空行,注释行。如果不是,就用read_option