史上最詳細的Pytorch版yolov3程式碼中文註釋詳解(一)
阿新 • • 發佈:2018-12-21
有了上面這些教程,我這個教程自然不會重複之前的工作,而是給出每個程式每行程式碼最詳細全面的小白入門註釋,不論基礎多差都能看懂,註釋到每個語句每個變數是什麼意思,只有把工作做細到這個程度,才是真正對我們這些小白有利(大神們請忽略,這只是給我們小白們看的。)
本篇是系列教程的第一篇,詳細闡述程式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 了。