Part1. models.py文件里的模型創建
1.如何更方便的准備debug環境?
我們選取的源碼是github上5.7k star的 pytorch implementation
項目源碼地址
下面我們從models.py文件入手。在講源碼的過程中采用了debug模式,這樣可以更為深入的分析整個tensor數據流的變化。默認的數據集是coco數據集,完整下載要十幾G,但是作者也留下了一個小的入口,方面大家debug。
這個入口是一張圖片,需要修改train.py文件里面的一行內容:
parser.add_argument("--data_config", type=str, default="config/custom.data", help="path to data config file")
也就是把coco數據集改為custom數據集,custom數據集里只包含一張圖片,對應的GT labels里面只包含一個target。為了看清楚target的變換,我們再多加一個target,只需修改一下custom/labels里面的train.txt。
D:\pyprojects\object-detection-code-debug\PyTorch-YOLOv3\data\custom\labels
我多加了一個target,所以改成了:

同時大家也可能看到里面的數值是5列,分別對應了target的class,x, y中心坐標, target高寬h, w,注意這里都是對應於原圖進行的歸一化。
修改完之后就可以跑train.py了。我們在models.py里面增加debug斷點,便於分析源碼,觀察數據變化。
2.parse_model_config()函數是如何加載配置文件的?
首先最外層是models.py里面的Darknet Class。重點的代碼是下面這段:
class Darknet(nn.Module):
"""YOLOv3 object detection model"""
def __init__(self, config_path, img_size=416):
super(Darknet, self).__init__()
self.module_defs = parse_model_config(config_path) # 得到list[dict()]類型的model配置信息表
self.hyperparams, self.module_list = create_modules(self.module_defs)
self.yolo_layers = [layer[0] for layer in self.module_list if isinstance(layer[0], YOLOLayer)]
self.img_size = img_size
self.seen = 0
self.header_info = np.array([0, 0, 0, self.seen, 0], dtype=np.int32)
def forward(self, x, targets=None):
img_dim = x.shape[2] # 圖像的尺寸
loss = 0
layer_outputs, yolo_outputs = [], []
for i, (module_def, module) in enumerate(zip(self.module_defs, self.module_list)):
if module_def["type"] in ["convolutional", "upsample", "maxpool"]:
x = module(x)
elif module_def["type"] == "route":
# torch.cat 對單個tensor相當於保持原樣
x = torch.cat([layer_outputs[int(layer_i)] for layer_i in module_def["layers"].split(",")], 1)
elif module_def["type"] == "shortcut":
layer_i = int(module_def["from"])
x = layer_outputs[-1] + layer_outputs[layer_i]
elif module_def["type"] == "yolo":
x, layer_loss = module[0](x, targets, img_dim)
loss += layer_loss
yolo_outputs.append(x)
layer_outputs.append(x)
yolo_outputs = to_cpu(torch.cat(yolo_outputs, 1))
return yolo_outputs if targets is None else (loss, yolo_outputs)
我們從init函數看起,首先是:
self.module_defs = parse_model_config(config_path)
這句調用了parse_model_config函數,那我們首先看一下這個函數:
def parse_model_config(path):
"""通過cfg文件加載yolov3的配置,並存儲到list[dict()]的結構中
每一層以“[”作為標記的開始
"""
file = open(path, 'r', encoding="utf-8")
lines = file.read().split('\n')
lines = [x for x in lines if x and not x.startswith('#')]
lines = [x.rstrip().lstrip() for x in lines] # get rid of fringe whitespaces
module_defs = []
for line in lines:
if line.startswith('['): # This marks the start of a new block
module_defs.append({})
module_defs[-1]['type'] = line[1:-1].rstrip()
# 初始化batch-normal參數 配置文件里還是加載為1
if module_defs[-1]['type'] == 'convolutional':
module_defs[-1]['batch_normalize'] = 0
else:
key, value = line.split("=")
value = value.strip()
module_defs[-1][key.rstrip()] = value.strip()
return module_defs
這個函數的作用很明顯,就是加載配置文件,然后把配置文件里面的模型結構解析出來。配置文件的形式大家在項目里也是可以找到的,結尾是cfg的文件。
打開yolov3.cfg,再結合剛才的源碼,很容易知道每個[]代表一個module的開始,每個module以一個dict()的形式去存放相關的參數,仔細觀察cfg配置文件可以知曉module分為以下幾類:
[net]
# Training
batch=16
subdivisions=1
width=416
...
[net]:模型的參數以及一些可配置的學習策略參數
[convolutional]
batch_normalize=1
filters=32
size=3
stride=1
pad=1
activation=leaky
[convolution]:卷積層,參數指定了常見的有卷積核的kernel size、padding、stride之類的。以及是否跟隨BN層,是否跟隨激活層。可以注意到默認的激活函數是leaky-relu。
[shortcut]
from=-3
activation=linear
[shortcut]:這個對應的是殘差結構,from=-3指代的是從當前結果和哪個層的輸出進行殘差結構。-3就是從當前的結果回溯三層的輸出。
[yolo]
mask = 6,7,8
anchors = 10,13, 16,30, 33,23, 30,61, 62,45, 59,119, 116,90, 156,198, 373,326
classes=80
num=9
jitter=.3
ignore_thresh = .7
truth_thresh = 1
random=1
[yolo]:yolo對應的是一個模型的最終輸出,因為yolov3采用的類似FPN結構的輸出,所以有3個yolo layer。
[route]
layers = -4
[route]:層指代的來自不同層的特征融合。是tensor在channel維度的疊加。因為存在FPN結構,所以yolov3 在多個地方進行了上采樣+特征融合。
[upsample]
stride=2
[unsample]:就比較簡單了,就是2倍上采樣。默認的是通過雙線性插值的方式來進行的。
3.怎樣更方便的debug yolov3模型結構?
這部分最好的debug方式就是調用parse_model_config()函數去把模型的每一層打印出來,我們在parse_config.py里面添加一個main函數,解析yolov3.cfg文件:
if __name__ == '__main__':
path = "../config/yolov3.cfg"
res = parse_model_config(path)
for i in range(len(res)):
print(f"###layer{i}###")
print(res[i])
觀察打印出的模型結構:
###layer0###
{'type': 'net', 'batch': '16', 'subdivisions': '1', 'width': '416', 'height': '416', 'channels': '3', 'momentum': '0.9', 'decay': '0.0005', 'angle': '0', 'saturation': '1.5', 'exposure': '1.5', 'hue': '.1', 'learning_rate': '0.001', 'burn_in': '1000', 'max_batches': '500200', 'policy': 'steps', 'steps': '400000,450000', 'scales': '.1,.1'}
###layer1###
{'type': 'convolutional', 'batch_normalize': '1', 'filters': '32', 'size': '3', 'stride': '1', 'pad': '1', 'activation': 'leaky'}
###layer2###
{'type': 'convolutional', 'batch_normalize': '1', 'filters': '64', 'size': '3', 'stride': '2', 'pad': '1', 'activation': 'leaky'}
###layer3###
{'type': 'convolutional', 'batch_normalize': '1', 'filters': '32', 'size': '1', 'stride': '1', 'pad': '1', 'activation': 'leaky'}
###layer4###
{'type': 'convolutional', 'batch_normalize': '1', 'filters': '64', 'size': '3', 'stride': '1', 'pad': '1', 'activation': 'leaky'}
...
...
...
###layer107###
{'type': 'yolo', 'mask': '0,1,2', 'anchors': '10,13, 16,30, 33,23, 30,61, 62,45, 59,119, 116,90, 156,198, 373,326', 'classes': '80', 'num': '9', 'jitter': '.3', 'ignore_thresh': '.7', 'truth_thresh': '1', 'random': '1'}
可以看出總共有107個配置信息,第一個是模型的參數,里面有3個yolo layer的信息。這個配置文件不僅包含生成Darknet53的信息,其實包含了生成整個yolov3的信息。這種配置文件生成模型的方式可以快速調整模型結構,並且十分的清晰,值得學習。
這部分很簡單,作者想表達的意思是如果想快速並細致的觀測模型結構,就必須把模型結構打印出來,並且對照模型結構圖逐步觀察,這樣基本上一次就可以完全熟悉模型結構。下面是yolov3的結構圖:
這個圖用來配合剛剛打印出來的模型配置信息是非常好的,可以清晰的明白哪里是route,哪里是普通卷積層,哪里是yolo layer。
至此我們拿到了配置文件中每一個module的配置參數,這些module串聯起來就可以生成darknet53的的結構,parse_model_config()函數最終得到的是list[dict()]類型的model配置信息表。下一步代碼執行了如下內容:
self.hyperparams, self.module_list = create_modules(self.module_defs)
下一步我們就來解析create_modules()函數。
4.create_modules()函數如何構建模型?
首先上這部分的代碼:
def create_modules(module_defs):
"""
Constructs module list of layer blocks from module configuration in module_defs
"""
hyperparams = module_defs.pop(0) # 配置信息第一項是超參數
output_filters = [int(hyperparams["channels"])] #記錄當前操作輸出channel 后面輸入channel根據output_filters[-1]取
module_list = nn.ModuleList()
for module_i, module_def in enumerate(module_defs):
modules = nn.Sequential() # 每一層構建單獨的nn.Sequential() 最后再統一加到nn.MuduleList()里
if module_def["type"] == "convolutional":
bn = int(module_def["batch_normalize"])
filters = int(module_def["filters"])
kernel_size = int(module_def["size"])
pad = (kernel_size - 1) // 2
modules.add_module(
f"conv_{module_i}",
nn.Conv2d(
in_channels=output_filters[-1],
out_channels=filters,
kernel_size=kernel_size,
stride=int(module_def["stride"]),
padding=pad,
bias=not bn,
),
)
if bn:
modules.add_module(f"batch_norm_{module_i}", nn.BatchNorm2d(filters, momentum=0.9, eps=1e-5))
if module_def["activation"] == "leaky":
modules.add_module(f"leaky_{module_i}", nn.LeakyReLU(0.1))
elif module_def["type"] == "maxpool":
kernel_size = int(module_def["size"])
stride = int(module_def["stride"])
if kernel_size == 2 and stride == 1:
modules.add_module(f"_debug_padding_{module_i}", nn.ZeroPad2d((0, 1, 0, 1)))
maxpool = nn.MaxPool2d(kernel_size=kernel_size, stride=stride, padding=int((kernel_size - 1) // 2))
modules.add_module(f"maxpool_{module_i}", maxpool)
elif module_def["type"] == "upsample":
upsample = Upsample(scale_factor=int(module_def["stride"]), mode="nearest")
modules.add_module(f"upsample_{module_i}", upsample)
elif module_def["type"] == "route": # 對應feature maps間特征融合的結構 方式是concat
layers = [int(x) for x in module_def["layers"].split(",")]
filters = sum([output_filters[1:][i] for i in layers])
modules.add_module(f"route_{module_i}", EmptyLayer())
elif module_def["type"] == "shortcut": # 對應residual的結構 方式是add
filters = output_filters[1:][int(module_def["from"])]
modules.add_module(f"shortcut_{module_i}", EmptyLayer())
elif module_def["type"] == "yolo":
anchor_idxs = [int(x) for x in module_def["mask"].split(",")]
# Extract anchors
anchors = [int(x) for x in module_def["anchors"].split(",")]
anchors = [(anchors[i], anchors[i + 1]) for i in range(0, len(anchors), 2)]
anchors = [anchors[i] for i in anchor_idxs]
num_classes = int(module_def["classes"])
img_size = int(hyperparams["height"])
# Define detection layer
yolo_layer = YOLOLayer(anchors, num_classes, img_size)
modules.add_module(f"yolo_{module_i}", yolo_layer)
# Register module list and number of output filters
module_list.append(modules)
output_filters.append(filters)
return hyperparams, module_list
這部分還是不難理解的。
hyperparams = module_defs.pop(0)
這里拿到了模型和學習策略的配置信息。
for module_i, module_def in enumerate(module_defs):
...
這里開始循環加載模型結構。
if module_def["type"] == "convolutional":
bn = int(module_def["batch_normalize"])
filters = int(module_def["filters"])
kernel_size = int(module_def["size"])
pad = (kernel_size - 1) // 2
modules.add_module(
f"conv_{module_i}",
nn.Conv2d(
in_channels=output_filters[-1],
out_channels=filters,
kernel_size=kernel_size,
stride=int(module_def["stride"]),
padding=pad,
bias=not bn,
),
)
if bn:
modules.add_module(f"batch_norm_{module_i}", nn.BatchNorm2d(filters, momentum=0.9, eps=1e-5))
if module_def["activation"] == "leaky":
modules.add_module(f"leaky_{module_i}", nn.LeakyReLU(0.1))
conv層都默認加了BN,激活函數默認是leaky-relu。這些都是可以調整的,作者在很多競賽中發現leaky-relu不一定效果會比普通的relu好。
elif module_def["type"] == "maxpool":棗庄人流醫院哪家好 http://mobile.0632-3679999.com/
kernel_size = int(module_def["size"])
stride = int(module_def["stride"])
if kernel_size == 2 and stride == 1:
modules.add_module(f"_debug_padding_{module_i}", nn.ZeroPad2d((0, 1, 0, 1)))
maxpool = nn.MaxPool2d(kernel_size=kernel_size, stride=stride, padding=int((kernel_size - 1) // 2))
modules.add_module(f"maxpool_{module_i}", maxpool)
elif module_def["type"] == "upsample":
upsample = Upsample(scale_factor=int(module_def["stride"]), mode="nearest")
modules.add_module(f"upsample_{module_i}", upsample)
maxpool是下采樣,upsample是上采樣,這里都是2倍。但是實際上yolov3里面下采樣用的是stride=2的卷積並沒有使用maxpool。stride=2的卷積相對於maxpool保留了更多的信息,在一些論文里都有替代maxpool的作用,另外用stride conv下采樣的時候也可以考慮大一點的卷積核,但這些都不是本文重點,提一句就此略過。
elif module_def["type"] == "route": # 對應feature maps間特征融合的結構 方式是concat
layers = [int(x) for x in module_def["layers"].split(",")]
filters = sum([output_filters[1:][i] for i in layers])
modules.add_module(f"route_{module_i}", EmptyLayer())
elif module_def["type"] == "shortcut": # 對應residual的結構 方式是add
filters = output_filters[1:][int(module_def["from"])]
modules.add_module(f"shortcut_{module_i}", EmptyLayer())
route和shortcut對應的分別是tensor的拼接和殘差結構。EmptyLayer()是一個空層,啥也不做,具體的操作都是在Darknet的forward()函數里才去執行具體的操作。
filters = sum([output_filters[1:][i] for i in layers])
filters = output_filters[1:][int(module_def["from"])]
filters都是在計算相應操作之后的channel數量。所以一個是sum所有的channel的concat,一個是channel保持不變的element-wise 加法。
elif module_def["type"] == "yolo":
anchor_idxs = [int(x) for x in module_def["mask"].split(",")]
# Extract anchors
anchors = [int(x) for x in module_def["anchors"].split(",")]
anchors = [(anchors[i], anchors[i + 1]) for i in range(0, len(anchors), 2)]
anchors = [anchors[i] for i in anchor_idxs]
num_classes = int(module_def["classes"])
img_size = int(hyperparams["height"])
# Define detection layer
yolo_layer = YOLOLayer(anchors, num_classes, img_size)
modules.add_module(f"yolo_{module_i}", yolo_layer)
yolo layer會加載YOLOLayer() module。在觀察一下cfg文件里的配置信息:
[yolo]
mask = 0,1,2
anchors = 10,13, 16,30, 33,23, 30,61, 62,45, 59,119, 116,90, 156,198, 373,326
classes=80
num=9
jitter=.3
ignore_thresh = .7
truth_thresh = 1
random=1
mask代表的是第幾個anchor,比如對於這個yolo head就只采用(10,13)、(16,30)、(33,23)這三個anchor信息。我們都知道yolov3采用FPN結構作為輸出的結果,擁有三個yolo head,每個head預先設置了3個不同大小的anchor,三個head分別負責大、中、小三類物體。更為具體的信息我們會在后面解析yolo layer源碼的時候再分享。
5.Darknet的前向傳播過程?
講完create_modules()函數,我們再回到Darknet Class,剩余的__init__()里面的部分:
def __init__(self, config_path, img_size=416):
super(Darknet, self).__init__()
self.module_defs = parse_model_config(config_path) # 得到list[dict()]類型的model配置信息表
self.hyperparams, self.module_list = create_modules(self.module_defs)
self.yolo_layers = [layer[0] for layer in self.module_list if isinstance(layer[0], YOLOLayer)]
self.img_size = img_size
self.seen = 0
self.header_info = np.array([0, 0, 0, self.seen, 0], dtype=np.int32)
其中第三句:
self.yolo_layers = [layer[0] for layer in self.module_list if isinstance(layer[0], YOLOLayer)]
self.module_list對應的是一個nn.ModuleList(),layer對應的是一個nn.Sequential(),layer(0)對應的是nn.Sequential()里面的第一個module。
下面來看下forward()部分:
def forward(self, x, targets=None):
img_dim = x.shape[2] # 圖像的尺寸
loss = 0
layer_outputs, yolo_outputs = [], []
for i, (module_def, module) in enumerate(zip(self.module_defs, self.module_list)):
if module_def["type"] in ["convolutional", "upsample", "maxpool"]:
x = module(x)
elif module_def["type"] == "route":
# torch.cat 對單個tensor相當於保持原樣
x = torch.cat([layer_outputs[int(layer_i)] for layer_i in module_def["layers"].split(",")], 1)
elif module_def["type"] == "shortcut":
layer_i = int(module_def["from"])
x = layer_outputs[-1] + layer_outputs[layer_i]
elif module_def["type"] == "yolo":
x, layer_loss = module[0](x, targets, img_dim)
loss += layer_loss
yolo_outputs.append(x)
layer_outputs.append(x)
yolo_outputs = to_cpu(torch.cat(yolo_outputs, 1))
return yolo_outputs if targets is None else (loss, yolo_outputs)
如果是conv、upsample、maxpool就直接執行;如果是route,就根據從cfg拿到的layer編號進行concat;如果是yolo,對應着輸出就進行loss的計算。
layer_outputs.append(x)
這里每一層(對應一個create_modules()函數的nn.Sequential()),記錄每一層的輸出,可以方便的進行route和residual,這種寫法是很經典的。