1. 程式人生 > >史上最詳細的Pytorch版yolov3程式碼中文註釋詳解(一)

史上最詳細的Pytorch版yolov3程式碼中文註釋詳解(一)

有了上面這些教程,我這個教程自然不會重複之前的工作,而是給出每個程式每行程式碼最詳細全面的小白入門註釋,不論基礎多差都能看懂,註釋到每個語句每個變數是什麼意思,只有把工作做細到這個程度,才是真正對我們這些小白有利(大神們請忽略,這只是給我們小白們看的。)

本篇是系列教程的第一篇,詳細闡述程式darknet.py。下面幾篇地址如下:

話不多說,先看darknet.py程式碼的超詳細註釋。

from __future__ import division

import torch 
import torch.nn as nn
import torch.nn.functional as F 
from torch.autograd import Variable
import numpy as np
from util import * 


def get_test_input():
    img = cv2.imread("dog-cycle-car.png")
    img = cv2.resize(img, (416,416))          #Resize to the input dimension
    img_ =  img[:,:,::-1].transpose((2,0,1))  #img是【h,w,channel】,這裡的img[:,:,::-1]是將第三個維度channel從opencv的BGR轉化為pytorch的RGB,然後transpose((2,0,1))的意思是將[height,width,channel]->[channel,height,width]
    img_ = img_[np.newaxis,:,:,:]/255.0       #Add a channel at 0 (for batch) | Normalise
    img_ = torch.from_numpy(img_).float()     #Convert to float
    img_ = Variable(img_)                     # Convert to Variable
    return img_

def parse_cfg(cfgfile):
    """
    輸入: 配置檔案路徑
    返回值: 列表物件,其中每一個元素為一個字典型別對應於一個要建立的神經網路模組(層)
    
    """
    # 載入檔案並過濾掉文字中多餘內容
    file = open(cfgfile, 'r')
    lines = file.read().split('\n')                        # store the lines in a list等價於readlines
    lines = [x for x in lines if len(x) > 0]               # 去掉空行
    lines = [x for x in lines if x[0] != '#']              # 去掉以#開頭的註釋行
    lines = [x.rstrip().lstrip() for x in lines]           # 去掉左右兩邊的空格(rstricp是去掉右邊的空格,lstrip是去掉左邊的空格)
    # cfg檔案中的每個塊用[]括起來最後組成一個列表,一個block儲存一個塊的內容,即每個層用一個字典block儲存。
    block = {}
    blocks = []
    
    for line in lines:
        if line[0] == "[":               # 這是cfg檔案中一個層(塊)的開始           
            if len(block) != 0:          # 如果塊內已經存了資訊, 說明是上一個塊的資訊還沒有儲存
                blocks.append(block)     # 那麼這個塊(字典)加入到blocks列表中去
                block = {}               # 覆蓋掉已儲存的block,新建一個空白塊儲存描述下一個塊的資訊(block是字典)
            block["type"] = line[1:-1].rstrip()  # 把cfg的[]中的塊名作為鍵type的值   
        else:
            key,value = line.split("=") #按等號分割
            block[key.rstrip()] = value.lstrip()#左邊是key(去掉右空格),右邊是value(去掉左空格),形成一個block字典的鍵值對
    blocks.append(block) # 退出迴圈,將最後一個未加入的block加進去
    # print('\n\n'.join([repr(x) for x in blocks]))
    return blocks

# 配置檔案定義了6種不同type
# 'net': 相當於超引數,網路全域性配置的相關引數
# {'convolutional', 'net', 'route', 'shortcut', 'upsample', 'yolo'}

# cfg = parse_cfg("cfg/yolov3.cfg")
# print(cfg)



class EmptyLayer(nn.Module):
    """
    為shortcut layer / route layer 準備, 具體功能不在此實現,在Darknet類的forward函式中有體現
    """
    def __init__(self):
        super(EmptyLayer, self).__init__()
        

class DetectionLayer(nn.Module):
    '''yolo 檢測層的具體實現, 在特徵圖上使用錨點預測目標區域和類別, 功能函式在predict_transform中'''
    def __init__(self, anchors):
        super(DetectionLayer, self).__init__()
        self.anchors = anchors



def create_modules(blocks):
    net_info = blocks[0]     # blocks[0]儲存了cfg中[net]的資訊,它是一個字典,獲取網路輸入和預處理相關資訊    
    module_list = nn.ModuleList() # module_list用於儲存每個block,每個block對應cfg檔案中一個塊,類似[convolutional]裡面就對應一個卷積塊
    prev_filters = 3   #初始值對應於輸入資料3通道,用來儲存我們需要持續追蹤被應用卷積層的卷積核數量(上一層的卷積核數量(或特徵圖深度))
    output_filters = []   #我們不僅需要追蹤前一層的卷積核數量,還需要追蹤之前每個層。隨著不斷地迭代,我們將每個模組的輸出卷積核數量新增到 output_filters 列表上。
    
    for index, x in enumerate(blocks[1:]): #這裡,我們迭代block[1:] 而不是blocks,因為blocks的第一個元素是一個net塊,它不屬於前向傳播。
        module = nn.Sequential()# 這裡每個塊用nn.sequential()建立為了一個module,一個module有多個層
    
        #check the type of block
        #create a new module for the block
        #append to module_list
        
        if (x["type"] == "convolutional"):
            ''' 1. 卷積層 '''
            # 獲取啟用函式/批歸一化/卷積層引數(通過字典的鍵獲取值)
            activation = x["activation"]
            try:
                batch_normalize = int(x["batch_normalize"])
                bias = False#卷積層後接BN就不需要bias
            except:
                batch_normalize = 0
                bias = True #卷積層後無BN層就需要bias
        
            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
            # nn.Conv2d(self, in_channels, out_channels, kernel_size, stride=1, padding=0, bias=True)
            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
            # 給定引數負軸系數0.1
            if activation == "leaky":
                activn = nn.LeakyReLU(0.1, inplace = True)
                module.add_module("leaky_{0}".format(index), activn)
                   
        elif (x["type"] == "upsample"):
            '''
            2. upsampling layer
            沒有使用 Bilinear2dUpsampling
            實際使用的為最近鄰插值
            '''
            stride = int(x["stride"])#這個stride在cfg中就是2,所以下面的scale_factor寫2或者stride是等價的
            upsample = nn.Upsample(scale_factor = 2, mode = "nearest")
            module.add_module("upsample_{}".format(index), upsample)
                
        # route layer -> Empty layer
        # route層的作用:當layer取值為正時,輸出這個正數對應的層的特徵,如果layer取值為負數,輸出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>0,由於end= end - index,再執行index + end輸出的還是第end層的特徵
                end = end - index
            route = EmptyLayer()
            module.add_module("route_{0}".format(index), route)
            if end < 0: #若end<0,則end還是end,輸出index+end(而end<0)故index向後退end層的特徵。
                filters = output_filters[index + start] + output_filters[index + end]
            else: #如果沒有第二個引數,end=0,則對應下面的公式,此時若start>0,由於start = start - index,再執行index + start輸出的還是第start層的特徵;若start<0,則start還是start,輸出index+start(而start<0)故index向後退start層的特徵。
                filters= output_filters[index + start]
    
        #shortcut corresponds to skip connection
        elif x["type"] == "shortcut":
            shortcut = EmptyLayer() #使用空的層,因為它還要執行一個非常簡單的操作(加)。沒必要更新 filters 變數,因為它只是將前一層的特徵圖新增到後面的層上而已。
            module.add_module("shortcut_{}".format(index), shortcut)
            
        #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)]
            anchors = [anchors[i] for i in mask]
    
            detection = DetectionLayer(anchors)# 錨點,檢測,位置迴歸,分類,這個類見predict_transform中
            module.add_module("Detection_{}".format(index), detection)
                              
        module_list.append(module)
        prev_filters = filters
        output_filters.append(filters)
        
    return (net_info, module_list)

class Darknet(nn.Module):
    def __init__(self, cfgfile):
        super(Darknet, self).__init__()
        self.blocks = parse_cfg(cfgfile) #呼叫parse_cfg函式
        self.net_info, self.module_list = create_modules(self.blocks)#呼叫create_modules函式
        
    def forward(self, x, CUDA):
        modules = self.blocks[1:] # 除了net塊之外的所有,forward這裡用的是blocks列表中的各個block塊字典
        outputs = {}   #We cache the outputs for the route layer
        
        write = 0#write表示我們是否遇到第一個檢測。write=0,則收集器尚未初始化,write=1,則收集器已經初始化,我們只需要將檢測圖與收集器級聯起來即可。
        for i, module in enumerate(modules):        
            module_type = (module["type"])
            
            if module_type == "convolutional" or module_type == "upsample":
                x = self.module_list[i](x)
    
            elif module_type == "route":
                layers = module["layers"]
                layers = [int(a) for a in layers]
    
                if (layers[0]) > 0:
                    layers[0] = layers[0] - i
                # 如果只有一層時。從前面的if (layers[0]) > 0:語句中可知,如果layer[0]>0,則輸出的就是當前layer[0]這一層的特徵,如果layer[0]<0,輸出就是從route層(第i層)向後退layer[0]層那一層得到的特徵 
                if len(layers) == 1:
                    x = outputs[i + (layers[0])]
                #第二個元素同理 
                else:
                    if (layers[1]) > 0:
                        layers[1] = layers[1] - i
    
                    map1 = outputs[i + layers[0]]
                    map2 = outputs[i + layers[1]]
                    x = torch.cat((map1, map2), 1)#第二個引數設為 1,這是因為我們希望將特徵圖沿anchor數量的維度級聯起來。
                
    
            elif  module_type == "shortcut":
                from_ = int(module["from"])
                x = outputs[i-1] + outputs[i+from_] # 求和運算,它只是將前一層的特徵圖新增到後面的層上而已
            
            elif module_type == 'yolo':        
                anchors = self.module_list[i][0].anchors
                #從net_info(實際就是blocks[0],即[net])中get the input dimensions
                inp_dim = int (self.net_info["height"])
        
                #Get the number of classes
                num_classes = int (module["classes"])
        
                #Transform 
                x = x.data # 這裡得到的是預測的yolo層feature map
                # 在util.py中的predict_transform()函式利用x(是傳入yolo層的feature map),得到每個格子所對應的anchor最終得到的目標
                # 座標與寬高,以及出現目標的得分與每種類別的得分。經過predict_transform變換後的x的維度是(batch_size, grid_size*grid_size*num_anchors, 5+類別數量)
                x = predict_transform(x, inp_dim, anchors, num_classes, CUDA)
                 
                if not write:              #if no collector has been intialised. 因為一個空的tensor無法與一個有資料的tensor進行concatenate操作,
                    detections = x #所以detections的初始化在有預測值出來時才進行,
                    write = 1   #用write = 1標記,當後面的分數出來後,直接concatenate操作即可。
        
                else:  
                    '''
                    變換後x的維度是(batch_size, grid_size*grid_size*num_anchors, 5+類別數量),這裡是在維度1上進行concatenate,即按照
                    anchor數量的維度進行連線,對應教程part3中的Bounding Box attributes圖的行進行連線。yolov3中有3個yolo層,所以
                    對於每個yolo層的輸出先用predict_transform()變成每行為一個anchor對應的預測值的形式(不看batch_size這個維度,x剩下的
                    維度可以看成一個二維tensor),這樣3個yolo層的預測值按照每個方框對應的行的維度進行連線。得到了這張圖處所有anchor的預測值,後面的NMS等操作可以一次完成
                    '''
                    detections = torch.cat((detections, x), 1)# 將在3個不同level的feature map上檢測結果儲存在 detections 裡
        
            outputs[i] = x
        
        return detections
# blocks = parse_cfg('cfg/yolov3.cfg')
# x,y = create_modules(blocks)
# print(y)

    def load_weights(self, weightfile):
        #Open the weights file
        fp = open(weightfile, "rb")
    
        #The first 5 values are header information 
        # 1. Major version number
        # 2. Minor Version Number
        # 3. Subversion number 
        # 4,5. Images seen by the network (during training)
        header = np.fromfile(fp, dtype = np.int32, count = 5)# 這裡讀取first 5 values權重
        self.header = torch.from_numpy(header)
        self.seen = self.header[3]   
        
        weights = np.fromfile(fp, dtype = np.float32)#載入 np.ndarray 中的剩餘權重,權重是以float32型別儲存的
        
        ptr = 0
        for i in range(len(self.module_list)):
            module_type = self.blocks[i + 1]["type"] # blocks中的第一個元素是網路引數和影象的描述,所以從blocks[1]開始讀入
    
            #If module_type is convolutional load weights
            #Otherwise ignore.
            
            if module_type == "convolutional":
                model = self.module_list[i]
                try:
                    batch_normalize = int(self.blocks[i+1]["batch_normalize"]) # 當有bn層時,"batch_normalize"對應值為1
                except:
                    batch_normalize = 0
            
                conv = model[0]
                
                
                if (batch_normalize):
                    bn = model[1]
        
                    #Get the number of weights of Batch Norm Layer
                    num_bn_biases = bn.bias.numel()
        
                    #Load the weights
                    bn_biases = torch.from_numpy(weights[ptr:ptr + num_bn_biases])
                    ptr += num_bn_biases
        
                    bn_weights = torch.from_numpy(weights[ptr: ptr + num_bn_biases])
                    ptr  += num_bn_biases
        
                    bn_running_mean = torch.from_numpy(weights[ptr: ptr + num_bn_biases])
                    ptr  += num_bn_biases
        
                    bn_running_var = torch.from_numpy(weights[ptr: ptr + num_bn_biases])
                    ptr  += num_bn_biases
        
                    #Cast the loaded weights into dims of model weights. 
                    bn_biases = bn_biases.view_as(bn.bias.data)
                    bn_weights = bn_weights.view_as(bn.weight.data)
                    bn_running_mean = bn_running_mean.view_as(bn.running_mean)
                    bn_running_var = bn_running_var.view_as(bn.running_var)
        
                    #Copy the data to model 將從weights檔案中得到的權重bn_biases複製到model中(bn.bias.data)
                    bn.bias.data.copy_(bn_biases)
                    bn.weight.data.copy_(bn_weights)
                    bn.running_mean.copy_(bn_running_mean)
                    bn.running_var.copy_(bn_running_var)
                
                else:#如果 batch_normalize 的檢查結果不是 True,只需要載入卷積層的偏置項
                    #Number of biases
                    num_biases = conv.bias.numel()
                
                    #Load the weights
                    conv_biases = torch.from_numpy(weights[ptr: ptr + num_biases])
                    ptr = ptr + num_biases
                
                    #reshape the loaded weights according to the dims of the model weights
                    conv_biases = conv_biases.view_as(conv.bias.data)
                
                    #Finally copy the data
                    conv.bias.data.copy_(conv_biases)
                    
                #Let us load the weights for the Convolutional layers
                num_weights = conv.weight.numel()
                
                #Do the same as above for weights
                conv_weights = torch.from_numpy(weights[ptr:ptr+num_weights])
                ptr = ptr + num_weights
                
                conv_weights = conv_weights.view_as(conv.weight.data)
                conv.weight.data.copy_(conv_weights)


總的來說,darknet.py程式包含函式parse_cfg輸入 配置檔案路徑返回一個列表,其中每一個元素為一個字典型別對應於一個要建立的神經網路模組(層),而函式create_modules用來建立網路層級,而Darknet類的forward函式就是實現網路前向傳播函數了,還有個load_weights用來匯入預訓練的網路權重引數。當然,forward函式中需要產生需要的預測輸出形式,因此需要變換輸出即函式 predict_transform 在檔案 util.py 中,我們在 Darknet 類別的 forward 中使用該函式時,將匯入該函式。下一篇就要詳細註釋util.py 了。