PyTorch实现yolov3代码详细解密(一)

yolo系列是目标识别的重头戏,为了更好的理解掌握它,我们必须从源码出发深刻理解代码。下面我们来讲解pytorch实现的yolov3源码。在讲解之前,大家应该具备相应的原理知识yolov1,yolov2,yolov3。
大部分同学在看论文时并不能把所有的知识全部掌握。我们必须结合代码(代码将理论变成实践),它是百分百还原理论的,也只有在掌握代码以及理论后,我们才能推陈出新有所收获,所以大家平时一定多接触代码,这里我们会结合yolov3的理论知识让大家真正在代码中理解思想。
yolov3的inference部分我们主要分为四个部分去分析:
PyTorch实现yolov3代码详细解密(一)

PyTorch实现yolov3代码详细解密(二)

PyTorch实现yolov3代码详细解密(三)

PyTorch实现yolov3代码详细解密(四)

yolov3的train部分我们主要分为两个部分去分析:
Pytorch实现yolov3(train)训练代码详解(一)
Pytorch实现yolov3(train)训练代码详解(二)

创建YOLO网络

首先我们知道yolov3将resnet改造变成了具有更好性能的Darknet作为它的backbone,称为darknet。

配置文件
官方代码(authored in C)使用一个配置文件来构建网络,即 cfg 文件一块块地描述了网络架构。我们开始要做的就是用pytorch来读取网络结构形成自己的module进行前向与反向传播。
PyTorch实现yolov3代码详细解密(一)_第1张图片
我们看到图中有两个卷积层与一个用于残差相加的跳转连接,下面我们逐一讲解darknet中所有的层级。

1.卷积层
PyTorch实现yolov3代码详细解密(一)_第2张图片
这个很简单,就是普通的卷积操作,filters其实就是输出的通道数,激活函数用的是leaky,这里batch_normalize=1 相当于一个flag并不是bn的参数。
PyTorch实现yolov3代码详细解密(一)_第3张图片
详细解释一下pytorch中的bn,如图BN这个类需要一些参数,num_features是feature map的通道数,因为bn最终优化的参数是γ和β,这两个参数是相对于每个维度来讲的,即一个维度对应一组参数。其他的参数不是很重要,想了解的话可以去官网查看。
在这里插入图片描述
2.跳转连接
在这里插入图片描述
它于残差网络中使用的结构类似,参数 from 为-3 表明该层的输出为前一层的输出加上前三层的输出(要求维度相同,两组输出直接相加)。

3.上采样
在这里插入图片描述
通过参数 stride 在前面层级中双线性上采样特征图。

4.路由层(Route)
PyTorch实现yolov3代码详细解密(一)_第4张图片
它的参数 layers 有一个或两个值。当只有一个值时,它输出这一层通过该值索引的特征图。在我们的实验中设置为了-4,所以层级将输出路由层之前第四个层的特征图。

当层级有两个值时,它将返回由这两个值索引的拼接特征图。在我们的实验中为-1 和 61,因此该层级将输出从前一层级(-1)到第 61 层的特征图,并将它们按深度拼接。

5.YOLO
PyTorch实现yolov3代码详细解密(一)_第5张图片
这里的9个anchor是论文中提出用k-means求出来的,yolov3有三条预测之路(多尺度由此而来)mask=0,1,2代表第一条支路,分别对应其anchors。
还有一个net,其描述的不是层的信息,而是训练相关的参数。

解析配置文件

def parse_cfg(cfgfile):
    """
    Takes a configuration file

    Returns a list of blocks. Each blocks describes a block in the neural
    network to be built. Block is represented as a dictionary in the list

    """
    file = open(cfgfile, 'r')
    lines = file.read().split('\n')  # store the lines in a list
    lines = [x for x in lines if len(x) > 0]  # get read of the empty lines
    lines = [x for x in lines if x[0] != '#']
    lines = [x.rstrip().lstrip() for x in lines]

    block = {}
    blocks = []

    for line in lines:
        if line[0] == "[":  # This marks the start of a new block
            if len(block) != 0:
                blocks.append(block)
                block = {}
            block["type"] = line[1:-1].rstrip()
        else:
            key, value = line.split("=")
            block[key.rstrip()] = value.lstrip()
    blocks.append(block)

    return blocks

我们定义一个函数 parse_cfg,该函数使用配置文件的路径作为输入。这个函数是将cfg中的网络结构读取出来并将属性和键值存储到词典block中,最后将这些block添加到列表blocks中由函数返还。lines以list形式将cfg按照‘/n’分隔开并存储,然后删除空行,注释,以及每行的空格。
for line in lines:遍历list中每个元素,按照要求将key和value组成block字典存入blocks中。

构建块
现在我们将使用上面 parse_cfg 返回的列表来构建 PyTorch 模块。列表中有 5 种类型的层。PyTorch 为 convolutional 和 upsample 提供预置层。我们将通过扩展 nn.Module 类为其余层写自己的模块。

def create_modules(blocks):
    net_info = blocks[0]  # Captures the information about the input and pre-processing
    module_list = nn.ModuleList()
    index = 0  # indexing blocks helps with implementing route  layers (skip connections)
    prev_filters = 3
    output_filters = []

我们知道blocks[0]是net中的信息,我们将它于层分离, prev_filters = 3因为RGB图输入通道数为3,output_filters = []我们初始化输出通道数。

nn.ModuleList
nn.Sequential它是Module的子类,在构建数个网络层之后会自动调用forward()方法,从而有网络模型生成。而nn.ModuleList仅仅类似于pytho中的list类型,只是将一系列层装入列表,并没有实现forward()方法,因此也不会有网络模型产生的副作用。

路由层(route layer)从前面层得到特征图(可能是拼接的)。如果在路由层之后有一个卷积层,那么卷积核将被应用到前面层的特征图上,精确来说是路由层得到的特征图。因此,我们不仅需要追踪前一层的卷积核数量,还需要追踪之前每个层。随着不断地迭代,我们将每个模块的输出卷积核数量添加到 output_filters 列表上。

    for x in blocks:
        module = nn.Sequential()

现在,我们的思路是迭代模块的列表,并为每个模块创建一个 PyTorch 模块。nn.Sequential 类被用于按顺序地执行 nn.Module 对象,一个 convolutional 类型的模块有一个批量归一化层、一个 leaky ReLU 激活层以及一个卷积层。我们使用 nn.Sequential 将这些层串联起来,得到 add_module 函数。

        if (x["type"] == "net"):
            continue

        # If it's a 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:
                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)



        # If it's an upsampling layer
        # We use Bilinear2dUpsampling

        elif (x["type"] == "upsample"):
            stride = int(x["stride"])
            #            upsample = Upsample(stride)
            upsample = nn.Upsample(scale_factor=2, mode="nearest")
            module.add_module("upsample_{}".format(index), upsample)

接下来,我们看看路由层于跳转连接层

#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]
                        
            
        
        #shortcut corresponds to skip connection
        elif x["type"] == "shortcut":
            from_ = int(x["from"])
            shortcut = EmptyLayer()
            module.add_module("shortcut_{}".format(index), shortcut)

因为路由层layers会出现两个数,即两层concat,所以开始用‘,’将数据隔开,一个用start表示,一个用end表示,并将输出的通道数记录以便后面使用。
这里出现了EmptyLayer()

class EmptyLayer(nn.Module):
    def __init__(self):
        super(EmptyLayer, self).__init__()

顾名思义,就是空的层。因为Route layer,shortcut layer执行的操作很简单一个是相加,一个是拼接(在特征图上调用 torch.cat)像上述过程那样设计一个层将导致不必要的抽象,增加样板代码。取而代之,我们可以将一个假的层置于之前提出的路由层的位置上,然后直接在代表 darknet 的 nn.Module 对象的 forward 函数中执行拼接运算。

YOLO 层

最后,我们将编写创建 YOLO 层的代码:

        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)]
            anchors = [anchors[i] for i in mask]
            
            detection = DetectionLayer(anchors)
            module.add_module("Detection_{}".format(index), detection)

x[“mask”]这个键值里面存放的是mask的值,我们用DetectionLayer保存用于检测边界框的锚点。
PyTorch实现yolov3代码详细解密(一)_第6张图片
最后,我们看到由106个sequential组成了ModuleList这个列表。这一部分我们仅仅完成了ModuleList列表的构建,即我们把darknet的框架搭建完成了,但是还缺少网络的前向传播,下面一章我们来完成前向传播。

你可能感兴趣的:(深度学习)