YOLO的原作者使用了自己开发的Darknet框架,而没有选取当前流行的其他深度学习框架实现算法,所以有必要对其网络模型的参数解析与存储方式做一了解,方便阅读源码和在其他流行的框架下的算法移植。
YOLO网络结构定义的CFG文件
YOLO中的网络定义采用和Caffe类似的方式,都是通过一个顺序堆叠layer来对神经网络结构进行定义的文件来描述。不同的地方在于,Caffe中使用了Google家出品的protobuf,省时省力,无需自己实现解析文件的功能,但是也使得Caffe对第三方库的依赖更加严重。相信很多人在编译Caffe的时候都出现过无法链接等蛋疼无比的问题。而YOLO的作者则是使用了自己定义的一种CFG文件格式,需要自己实现解析功能。
CFG文件的格式可以归纳如下(可以打开某个CFG文件进行对照):1
2
3
4
5
6
7
8
9[net]
# 这里会对net的参数进行配置
# 同时YOLO将对net的求解器的参数也放在了这里
[conv]
# 一些conv层的参数描述
[maxpool]
# 一些池化层的参数描述
# 顺序堆叠的其他layer描述
在Darknet的代码中,将每个[]
符号导引的参数列表叫做section。
网络结构解析器 Parser
具体的解析实现参见parser.c文件。我们先以convolutional_layer parse_convolutional(list *options, size_params params)
函数为例,看一下Darknet是如何完成对卷积层参数的解析的。
从函数签名可以看出,这个函数接受一个list
的变量(Darknet中将堆叠起来的这些层描述抽象成链表),而size_params
类型的变量params
指示了该层上一层的参数情况,其具体定义如下:1
2
3
4
5
6
7
8
9
10typedef struct size_params{
int batch;
int inputs;
int h;
int w;
int c;
int index;
int time_steps;
network net;
} size_params;
这样,在构建该层卷积层的时候,我们就能够知道上一层的输入维度等信息,方便做一些参数检查和layer初始化等的工作。
进入函数内部,会发现频繁出现option_find_int
这个函数。从函数名字面意义看,应该是要解析字符串中的整型数。
我们首先来看一下这个函数的定义吧~这个函数并不在parser.c
中,而是在option_list.c 文件中。
1 | // l: data pointer to the list |
而其中的option_find
函数则是逐项顺序查找,匹配字符串来实现的。
1 | char *option_find(list *l, char *key) |
构建conv层
由此,我们可以通过CFG文件得到卷积层的参数了。接下来需要调用其初始化函数,进行构建。
1 | // 首先得到参数 |
所以,如果在阅读源码时候,对layer的某个成员变量不知道什么意思的话,可以参考此文件,看一下原始解析对应的字符串是什么,一般这个字符串描述是比较具体的。
构建网络
有了各个layer的解析方法,接下来就可以逐层读取参数信息并构建网络了。
Darknet中对应的函数为network parse_network_cfg(char *filename)
,这个函数接受文件名为参数,进行网络结构的解析。
首先,调用read_cfg(filename)
得到CFG文件的一个层次链表,接着只要对这个链表进行解析就好了。不过对第一个section,也就是[net]
section,要特殊对待。这里不再多说了。
保存参数信息
Darknet中保存带参数的layer的信息是直接写入二进制文件。仍然以卷积层为例,其保存代码如下所示:
1 | void save_convolutional_weights(layer l, FILE *fp) |
在保存整个网络的参数信息的时候,同样逐层保存到同一个二进制文件中就好了。