从零开始Pytorch-YOLOv3【笔记】(二)解析配置文件
前言
上一篇:从零开始Pytorch-YOLOv3【笔记】(一)配置文件解读
下面是解析配置文件和生成model。对应从零开始PyTorch项目:YOLO v3目标检测实现中的第二部分 创建 YOLO 网络层级
解析配置文件
创建 Python 文件 darknet.py。darknet.py 是构建 YOLO 底层架构的环境,这个文件将包含实现 YOLO 网络的所有代码。
darknet.py中有如下函数和类:
def parse_cfg(cfgfile)
:解析配置文件。该函数使用配置文件的路径作为输入,返回一个blocks。
点击查看代码
def parse_cfg(cfgfile):
'''
Args:
cfgfile(str):cfg文件路径
Return:
blocks = [block, block, ..., block]
block = {
"type": "convolutional",
"batch_normalize": "1",
"filters": "32",
"size": "3",
"stride": "1",
"pad": "1",
"activation": "leaky"
}
'''
# 首先将配置文件内容保存在字符串列表中。下面的代码对该列表执行预处理:
file = open(cfgfile, 'r')
lines = file.read().split('\n')
lines = [x for x in lines if len(x) > 0]
lines = [x for x in lines if x[0] != '#']
lines = [x.strip() for x in lines] # 原代码是lines = [x.rstrip().lstrip() for x in lines],不知道为什么不直接用strip()
# 然后,我们遍历预处理后的列表,得到块。
block = {}
blocks = []
for line in lines:
if line[0] == "[": # This marks the start of a new block
if len(block) != 0: # If block is not empty, implies it is storing values of previous block.在读到下一个
blocks.append(block) # add it the blocks list
block = {} # re-init the block
block["type"] = line[1:-1].rstrip() # block = {}时,先写入type,
else:
key, value = line.split("=")
block[key.rstrip()] = value.lstrip()
blocks.append(block)
return blocks
def create_modules(blocks)
:根据parse_cfg
返回的blocks来构建pytorch模块。
列表中有 5 种类型的层。PyTorch 为 convolutional 和 upsample 提供预置层。我们将通过扩展 nn.Module 类为其余层写自己的模块。
点击查看代码
def create_modules(blocks):
'''
根据cfg返回的block来构建pytorch模块
'''
net_info = blocks[0] # 存储该网络的信息
module_list = nn.ModuleList()
prev_filters = 3 # 预定义卷积核的数量为3,因为初始输入图像为RGB图像,channel=3。卷积核的数量决定了这一层输出feature map的深度。
output_filters = [] # 将每个模块的输出卷积核数量添加到 output_filters 列表上。
'''
路由层(route layer)从前面层得到特征图(可能是拼接的)。如果在路由层之后有一个卷积层,那么卷积核将被应用到前面层的特征图上,精确来说是路由层得到的特征图。
因此,我们不仅需要追踪前一层的卷积核数量,还需要追踪之前每个层。随着不断地迭代,我们将每个模块的输出卷积核数量添加到 output_filters 列表上。
'''
for index, x in enumerate(blocks[1:]): # enumerate,将一组数据带索引[a,b,c] -> [(0,a), (1,b), (2,c)]
'''
遍历后续的模块([net]之后的block)
'''
module = nn.Sequential()
#check the type of block
#create a new module for the block
#append to module_list
# //---有以下几种block,依次添加到model中。
# if convolutional layer
# elif upsampling layer
# elif route layer
# elif shortcut corresponds to skip connection
# elif Yolo is the detection layer
# 在这个回路结束时,我们做了一些统计(bookkeeping.)。
module_list.append(module)
prev_filters = filters
output_filters.append(filters)
# 这总结了此回路的主体。在 create_modules 函数后,我们获得了包含 net_info 和 module_list 的元组。
return (net_info, module_list)
上面用到了两个pytorch方法module = nn.ModuleList()
和module.add_module("conv_{0}".format(index), conv)
我们使用 nn.Sequential 将这些层串联起来,得到 add_module 函数。
nn.Sequential
允许我们构建序列化的模块。就把Sequential当作list来看。也就是说用了Sequential的好处是我们可以通过数字访问第几层,可以通过parameters、weights等参数显示网络的参数和权重
卷积层
# convolutional layer
if (x["type"] == "convolutional"):
#Get the info about the layer
activation = x["activation"]
try:
batch_normalize = int(x["batch_normalize"])
bias = False
except:
batch_normalize = 0
bias = True
filters= int(x["filters"])
padding = int(x["pad"])
kernel_size = int(x["size"])
stride = int(x["stride"])
if padding: # 这里的padding在配置文件里有说明:pad=1时,pad = (kernel_size - 1) // 2,pad=0时,pad取默认值0
pad = (kernel_size - 1) // 2
else:
pad = 0
#Add the convolutional layer
conv = nn.Conv2d(prev_filters, filters, kernel_size, stride, pad, bias = bias)
module.add_module("conv_{0}".format(index), conv)
#Add the Batch Norm Layer
if batch_normalize:
bn = nn.BatchNorm2d(filters)
module.add_module("batch_norm_{0}".format(index), bn)
#Check the activation.
#It is either Linear or a Leaky ReLU for YOLO
if activation == "leaky":
activn = nn.LeakyReLU(0.1, inplace = True)
module.add_module("leaky_{0}".format(index), activn)
上采样层
对于backbone后面的分支,假设其网格为13×13,那么要和26×26的的分支进行级联,那么必须进行上采样,即Upsample操作。
# upsampling layer
#We use Bilinear2dUpsampling 这里注释说用的双线性上采样,意思应该是model = 'bilinear',但其实还是用的默认的'nearest'???
elif (x["type"] == "upsample"):
stride = int(x["stride"])
upsample = nn.Upsample(scale_factor = 2, mode = "nearest")
module.add_module("upsample_{}".format(index), upsample)
路由层
路由层的作用是获取之前层的拼接。
它的参数 layers 有一个或两个值。
当只有一个值时,它输出这一层通过该值索引的特征图。在我们的实验中设置为了-4,所以层级将输出路由层之前第四个层的特征图。
当层级有两个值时,它将返回由这两个值索引的拼接特征图。在我们的实验中为-1 和 61,因此该层级将输出由前一层级(-1)和第 61 层的特征图将它们按深度拼接后的特征图。
在路由层之后的卷积层会把它的卷积核应用到之前层的特征图(可能是拼接的)上。以下的代码更新了 filters 变量以保存路由层输出的卷积核数量。
这里
可能是拼接的
是指:layer只有一个值的时候就是获取之前的某一层输出的特征图,layer两个值时就是拼接的特征图。
#If it is a route layer
elif (x["type"] == "route"):
x["layers"] = x["layers"].split(',')
#Start of a route
start = int(x["layers"][0])
#end, if there exists one.
try:
end = int(x["layers"][1])
except:
end = 0
#Positive anotation
if start > 0:
start = start - index
if end > 0:
end = end - index
route = EmptyLayer()
module.add_module("route_{0}".format(index), route)
if end < 0:
filters = output_filters[index + start] + output_filters[index + end]
else:
filters= output_filters[index + start]
捷径层(跳跃连接)
捷径层执行一个非常简单的操作(加)。没必要更新 filters 变量,因为它只是将前一层的特征图添加到后面的层上而已。
#shortcut corresponds to skip connection
elif x["type"] == "shortcut":
shortcut = EmptyLayer()
module.add_module("shortcut_{}".format(index), shortcut)
EmptyLayer
Route layer,shortcut layer都使用了route = EmptyLayer()
TODO:...关于EmptyLayer的理解。
简要来说就是nn.Model_list()
对象中的元素必须为nn.Module的子类,而Route layer,shortcut layer操作都不是一个nn.Module,所以使用EmptyLayer来站位。
EmptyLayer,因为整个backbone模型被整合成了一个nn.Model_list()
对象,而route模块做的仅仅是级联(torch.cat操作),shortcut模块做的仅仅是将不同模块的结果进行相加,它们的共同特点是:未必需要上一个模块的输出,但需要上好几个模块的输出,至于需要的是第几个模块,一个模块还是多个模块,这个暂时无法知道,所以直接在create_modules
方法中,很难实习这个功能。
这个问题暂时先不处理,而是先定义一个空层来占位,由于nn.ModuleList中的元素必须为nn.Module的子类,所以在models.py中,可以这么定义EmptyLayer
class EmptyLayer(nn.Module):
def __init__(self):
super(EmptyLayer, self).__init__()
YOLO层
#Yolo is the detection layer
elif x["type"] == "yolo":
mask = x["mask"].split(",")
mask = [int(x) for x in mask]
anchors = x["anchors"].split(",")
anchors = [int(a) for a in anchors]
anchors = [(anchors[i], anchors[i+1]) for i in range(0, len(anchors),2)] # range(strat, end, stride),这代码写的,优雅!
anchors = [anchors[i] for i in mask]
detection = DetectionLayer(anchors)
module.add_module("Detection_{}".format(index), detection)
DetectionLayer
我们定义一个新的层 DetectionLayer 保存用于检测边界框的锚点。
检测层的定义如下:
class DetectionLayer(nn.Module):
def __init__(self, anchors):
super(DetectionLayer, self).__init__()
self.anchors = anchors
测试代码
你可以在 darknet.py 后通过输入以下命令行测试代码,运行文件。
blocks = parse_cfg("cfg/yolov3.cfg")
print(create_modules(blocks))
你会看到一个长列表(确切来说包含 106 条),其中元素看起来如下所示:
我测试输出是(51)条。不知道是不是我数的方式不对。
(9): Sequential(
(conv_9): Conv2d(128, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(batch_norm_9): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(leaky_9): LeakyReLU(negative_slope=0.1, inplace=True)
)
(10): Sequential(
(conv_10): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(batch_norm_10): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(leaky_10): LeakyReLU(negative_slope=0.1, inplace=True)
)
(11): Sequential(
(shortcut_11): EmptyLayer()
)