目录
一、前言
二、yolov3.cfg 配置文件展示
文件信息
三、配置文件处理
1、parse_model_config 函数
(1)读取并预处理文件内容
(2)解析模块定义
2、parse_data_config 函数
(1)初始化默认选项
(2)读取并解析文件内容
四、定义模型
1、定义上采样层
2、定义占位类,用于处理路由层和残差层
3、定义Darknet层
4、定义yolo层
五、模型搭建
一、前言
在深度学习领域,目标检测一直是一个热门且极具挑战性的任务。而 YOLO(You Only Look Once)系列算法以其高效快速的检测性能备受关注,其中 YOLOv3 更是在诸多应用场景中展现出了强大的实力。今天,我们就来深入了解一下如何通过 yolov3.cfg 配置文件搭建其对应的网络模型。
yolov3.cfg 配置文件就像是搭建网络模型的蓝图,它详细地规定了网络的各个组成部分,包括每一层的类型、参数等信息。通过读取和解析这个配置文件,我们能够准确地复现 YOLOv3 的网络架构,为后续的训练和应用奠定基础。
二、yolov3.cfg 配置文件展示
文件信息
**模型基本信息**:包括输入数据的尺寸、批量大小等。
**卷积层和池化层**:YOLOv3模型主要由卷积层和池化层构成,这些层在配置文件中逐一列出。
**卷积层参数**:包括卷积核数量、大小、步长、填充等。
**池化层参数**:包括池化类型、大小、步长等。
**路由层和归一化层**:用于连接不同层的特征图。
**YOLO层**:负责检测物体的位置和类别。
[net]:定义了网络的基本信息,如输入尺寸(width、height)、通道数(channels)和批量大小(batch)。
[convolutional]:定义了卷积层的参数,包括卷积核大小(size)、步长(stride)、填充(pad)、卷积核数量(filters)和激活函数(activation)。
[maxpool]:定义了池化层的参数,包括池化大小(size)和步长(stride)。
[region]:定义了YOLO层的参数,包括类别数量(classes)、检测框数量(num)、抖动系数(jitter)、锚点(anchors)、logits等。
三、配置文件处理
1、parse_model_config 函数
(1)读取并预处理文件内容
**函数parse_model_config
以给定的路径path
打开 yolov3.cfg 文件,并将其内容读取为字符串列表lines
。在此过程中,它会进行一系列的预处理操作:
**去除空行:通过[x for x in lines if x and not x.startswith('#')]
这一语句,过滤掉了空行以及以#
开头的注释行,确保我们后续处理的都是有效信息。
(2)解析模块定义
经过预处理后,函数开始逐行解析配置文件内容以构建模块定义列表module_defs
。
**识别新模块块的开始:当遇到以[
开头的行时,意味着一个新的模块块开始了。此时,函数会在module_defs
列表中添加一个新的空字典,用于存储该模块的定义信息,并将该行中去除[
和]
后的字符串作为模块的类型type
存入字典。例如,若该行是[convolutional]
,则会在新添加的字典中设置type
为convolutional
。同时,对于convolutional
类型的模块,会默认设置batch_normalize
为0
。
**解析模块参数:对于非以[
开头的行,函数会将其按照=
进行拆分,得到参数名key
和参数值value
。然后去除参数名和参数值两端的空白字符,并将其存入当前正在构建的模块字典中。这样,通过逐行解析,就能完整地构建出每个模块的定义信息,最终module_defs
列表就包含了 yolov3.cfg 文件中所有模块的详细定义。
def parse_model_config(path):
"""Parses the yolo-v3 layer configuration file and returns module definitions"""
file = open(path, 'r')
lines = file.read().split('\n')
lines = [x for x in lines if x and not x.startswith('#')] #x.startswith('#')用于检查字符串变量x是否以#前缀开始。如果x以该前缀开头,该方法将返回一个布尔值,通常是True,否则返回False。
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()
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
2、parse_data_config 函数
(1)初始化默认选项
函数parse_data_config
首先初始化了一个字典options
,并设置了一些默认值,比如gpus
设置为'0,1,2,3'
,表示默认使用的 GPU 编号;num_workers
设置为'10'
,这通常涉及到数据加载时的工作线程数量。
(2)读取并解析文件内容
接着,函数以给定的路径打开数据配置文件,并逐行读取其内容到lines
列表中。对于每一行:
**跳过空行和注释行:通过if line == '' or line.startswith('#'):
判断,如果行是空行或者以#
开头的注释行,就直接跳过,不做处理。
**解析有效行:对于非空且非注释的行,函数会按照‘='
将其拆分得到参数名key
和参数值value
,然后去除参数名和参数值两端的空白字符,并将参数值存入options
字典中对应的参数名下。这样,经过对整个数据配置文件的逐行解析,options
字典就完整地记录了数据配置文件中的各项参数设置。
def parse_data_config(path):
"""Parses the data configuration file"""
options = dict()
options['gpus'] = '0,1,2,3'
options['num_workers'] = '10'
with open(path, 'r') as fp:
lines = fp.readlines()
for line in lines:
line = line.strip()
if line == '' or line.startswith('#'):#startswith()用于检查字符串是否以特定的子字符串开始。如果是,它将返回True,否则返回False。
continue
key, value = line.split('=')
options[key.strip()] = value.strip()
return options
四、定义模型
1、定义上采样层
class Upsample(nn.Module):
""" nn.Upsample is deprecated """
def __init__(self, scale_factor, mode="nearest"):
super(Upsample, self).__init__()
self.scale_factor = scale_factor
self.mode = mode
def forward(self, x):
# torch.nn.functional中的interpolate用于对输入的张量进行插值操作。它可以用于缩放、放大或者重新定义张量的空间尺寸。
# interpolate(input, size=None, scale_factor=None, mode='nearest', align_corners=False)
# 参数解释:
# input: 输入的张量。它应该是一个多维的张量,通常至少是二维的。
# size: 输出张量的大小。如果未指定,则默认使用scale_factor参数进行缩放。
# scale_factor: 缩放因子。它可以是单个数字,表示所有维度都按照相同的比例缩放;也可以是一个与输入张量维度相同的向量,表示每个维度分别按照不同的比例缩放。
# mode: 插值模式。它决定了在缩放过程中如何插值。不同的插值模式会得到不同的结果,常见的插值模式有'nearest'(最近邻插值)、'bilinear'(双线性插值)等。
# 函数返回一个与输入张量具有相同数据类型的输出张量,其空间尺寸通过指定的参数进行了缩放。
x = F.interpolate(x, scale_factor=self.scale_factor, mode=self.mode)
return x
2、定义占位类,用于处理路由层和残差层
class EmptyLayer(nn.Module):
"""Placeholder for 'route' and 'shortcut' layers"""
def __init__(self):
super(EmptyLayer, self).__init__()
3、定义Darknet层
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)
self.hyperparams, self.module_list = create_modules(self.module_defs) #创建出网络中需要的所有网络层,每一层需要哪些参数
self.yolo_layers = [layer[0] for layer in self.module_list if hasattr(layer[0], "metrics")] #hasattr()是Python的一个内置函数,它用于检查对象是否具有给定(metrics)的属性。
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": #为concat的操作,是数据的拼接
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": #包含3个yolo层,分别对应52*52,26*26,13*13层
x, layer_loss = module[0](x, targets, img_dim)
loss += layer_loss
yolo_outputs.append(x)
layer_outputs.append(x) #包含了按顺序执行后的每一层结果。后期route和shortcut需要使用
yolo_outputs = to_cpu(torch.cat(yolo_outputs, 1))
return yolo_outputs if targets is None else (loss, yolo_outputs)
def load_darknet_weights(self, weights_path):
"""Parses and loads the weights stored in 'weights_path'"""
# Open the weights file
with open(weights_path, "rb") as f:
header = np.fromfile(f, dtype=np.int32, count=5) # 前5个是开头值First five are header values
self.header_info = header # 保存权重时需要写入header
self.seen = header[3] # 训练期间看到的图像数量。number of images seen during training
weights = np.fromfile(f, dtype=np.float32) #读取剩余的权重数据 The rest are weights
# Establish cutoff for loading backbone weights
cutoff = None
if "darknet53.conv.74" in weights_path:
cutoff = 75
ptr = 0
for i, (module_def, module) in enumerate(zip(self.module_defs, self.module_list)):
if i == cutoff:
break
if module_def["type"] == "convolutional":
conv_layer = module[0]
if module_def["batch_normalize"]:
# Load BN bias, weights, running mean and running variance
bn_layer = module[1]
num_b = bn_layer.bias.numel() # Number of biases
# Bias
bn_b = torch.from_numpy(weights[ptr : ptr + num_b]).view_as(bn_layer.bias)
bn_layer.bias.data.copy_(bn_b)
ptr += num_b
# Weight
bn_w = torch.from_numpy(weights[ptr : ptr + num_b]).view_as(bn_layer.weight)
bn_layer.weight.data.copy_(bn_w)
ptr += num_b
# Running Mean
bn_rm = torch.from_numpy(weights[ptr : ptr + num_b]).view_as(bn_layer.running_mean)
bn_layer.running_mean.data.copy_(bn_rm)
ptr += num_b
# Running Var
bn_rv = torch.from_numpy(weights[ptr : ptr + num_b]).view_as(bn_layer.running_var)
bn_layer.running_var.data.copy_(bn_rv)
ptr += num_b
else:
# Load conv. bias
num_b = conv_layer.bias.numel()
conv_b = torch.from_numpy(weights[ptr : ptr + num_b]).view_as(conv_layer.bias)
conv_layer.bias.data.copy_(conv_b)
ptr += num_b
# Load conv. weights
num_w = conv_layer.weight.numel()
conv_w = torch.from_numpy(weights[ptr : ptr + num_w]).view_as(conv_layer.weight)
conv_layer.weight.data.copy_(conv_w)
ptr += num_w
def save_darknet_weights(self, path, cutoff=-1):
"""
@:param path - path of the new weights file
@:param cutoff - save layers between 0 and cutoff (cutoff = -1 -> all are saved)
"""
fp = open(path, "wb")
self.header_info[3] = self.seen
self.header_info.tofile(fp)
# Iterate through layers
for i, (module_def, module) in enumerate(zip(self.module_defs[:cutoff], self.module_list[:cutoff])):
if module_def["type"] == "convolutional":
conv_layer = module[0]
# If batch norm, load bn first
if module_def["batch_normalize"]:
bn_layer = module[1]
bn_layer.bias.data.cpu().numpy().tofile(fp)
bn_layer.weight.data.cpu().numpy().tofile(fp)
bn_layer.running_mean.data.cpu().numpy().tofile(fp)
bn_layer.running_var.data.cpu().numpy().tofile(fp)
# Load conv bias
else:
conv_layer.bias.data.cpu().numpy().tofile(fp)
# Load conv weights
conv_layer.weight.data.cpu().numpy().tofile(fp)
fp.close()
4、定义yolo层
class YOLOLayer(nn.Module):
"""Detection layer
YOLO
实现的,该卷积核对每个网格单元的先验框进行回归,以确定其大小和偏移。
关联:这个步骤将每个先验框与前面的卷积层输出的特征图进行关联。具体来说,对于每个先验框,它将特征图中的特征与该框进行匹配,并计算一个匹配得分。这个得分被用来确定该框是否应该保留下来作为检测框,还是应该被抑制掉。
非极大值抑制:这个步骤的目的是进一步优化最终的检测结果。它通过比较每个检测框与其相邻框的置信度得分,将低得分的框抑制掉,保留高得分的框。
YOLO层的操作都是在高层次的特征图上进行的,这使得YOLO算法能够在保持高准确性的同时,实现较高的运行速度。
"""
def __init__(self, anchors, num_classes, img_dim=416):
super(YOLOLayer, self).__init__()
self.anchors = anchors
self.num_anchors = len(anchors)
self.num_classes = num_classes
self.ignore_thres = 0.5 #阈值
self.mse_loss = nn.MSELoss() #nn.MSELoss()是用于计算均方误差(Mean Squared Error)的损失函数。均方误差常用于衡量预测值与真实值之间的差距,适用于回归问题。
self.bce_loss = nn.BCELoss() #nn.BCELoss()是用于二元分类问题的损失函数。全称是Binary Cross Entropy Loss,也就是二元交叉熵损失。它通常用于处理二分类问题。
self.obj_scale = 1
self.noobj_scale = 100
self.metrics = {}
self.img_dim = img_dim
self.grid_size = 0 # grid size
def compute_grid_offsets(self, grid_size, cuda=True):
self.grid_size = grid_size
g = self.grid_size
FloatTensor = torch.cuda.FloatTensor if cuda else torch.FloatTensor
self.stride = self.img_dim / self.grid_size #每一个网格占用的像素大小
# Calculate offsets for each grid
self.grid_x = torch.arange(g).repeat(g, 1).view([1, 1, g, g]).type(FloatTensor)
self.grid_y = torch.arange(g).repeat(g, 1).t().view([1, 1, g, g]).type(FloatTensor)
self.scaled_anchors = FloatTensor([(a_w / self.stride, a_h / self.stride) for a_w, a_h in self.anchors])
self.anchor_w = self.scaled_anchors[:, 0:1].view((1, self.num_anchors, 1, 1))
self.anchor_h = self.scaled_anchors[:, 1:2].view((1, self.num_anchors, 1, 1))
def forward(self, x, targets=None, img_dim=None):
# Tensors for cuda support
print (x.shape)
FloatTensor = torch.cuda.FloatTensor if x.is_cuda else torch.FloatTensor#根据是否支持GPU来选择不同的tensor处理方法
LongTensor = torch.cuda.LongTensor if x.is_cuda else torch.LongTensor
ByteTensor = torch.cuda.ByteTensor if x.is_cuda else torch.ByteTensor
self.img_dim = img_dim
num_samples = x.size(0)
grid_size = x.size(2)
prediction = (
x.view(num_samples, self.num_anchors, self.num_classes + 5, grid_size, grid_size)#改变一个张量(Tensor)的形状或者维度。这个方法并不会改变张量内部元素的排列顺序或者数据类型,只是改变了我们看待这个张量的方式。
.permute(0, 1, 3, 4, 2)#将张量的维度重新排列的函数。原始张量的维度顺序是(0, 1, 2, 3, 4),而重排后的维度顺序是(0, 1, 3, 4, 2)。这意味着原始张量的第一个维度(索引为0)保持不变,第二个维度(索引为1)保持不变,第三个维度(索引为3)移动到第四个位置,第四个维度(索引为4)移动到第五个位置,而原始张量的第五个维度(索引为2)移动到第三个位置。
.contiguous() #用于将张量在内存中重新排序,以确保在执行操作时没有发生内存错误。
) #排序后的结果:【1个batch数据的数量,每个框中候选框的个数,网格尺寸、网格尺寸、输出结果:分类的个数+(x,y,w,h,confidence)】
print (prediction.shape)
# Get outputs
x = torch.sigmoid(prediction[..., 0]) # Center x,...通常表示"展开"操作,特别是在多维数组或张量中。当你在一个数组或张量后面使用...,它表示将数组或张量的所有维度都展开。
y = torch.sigmoid(prediction[..., 1]) # Center y,是相对于每一个小的方格 的位置。
w = prediction[..., 2] # Width
h = prediction[..., 3] # Height
pred_conf = torch.sigmoid(prediction[..., 4]) # Conf
pred_cls = torch.sigmoid(prediction[..., 5:]) # Cls pred.
# If grid size does not match current we compute new offsets
if grid_size != self.grid_size:
self.compute_grid_offsets(grid_size, cuda=x.is_cuda) #相对位置得到对应的绝对位置比如之前的位置是0.5,0.5变为 11.5,11.5这样的
# Add offset and scale with anchors #特征图中的实际位置
pred_boxes = FloatTensor(prediction[..., :4].shape)
pred_boxes[..., 0] = x.data + self.grid_x
pred_boxes[..., 1] = y.data + self.grid_y
pred_boxes[..., 2] = torch.exp(w.data) * self.anchor_w #exp的原因:看ppt中bw和bh的计算方法
pred_boxes[..., 3] = torch.exp(h.data) * self.anchor_h
output = torch.cat( #用于将多个张量沿着指定维度进行拼接。
(
pred_boxes.view(num_samples, -1, 4) * self.stride, #还原到原始图中,
pred_conf.view(num_samples, -1, 1),
pred_cls.view(num_samples, -1, self.num_classes),
),
-1,
) #包含预测框,置信度、类别概率
if targets is None:
return output, 0
else:
iou_scores, class_mask, obj_mask, noobj_mask, tx, ty, tw, th, tcls, tconf = build_targets(
pred_boxes=pred_boxes,
pred_cls=pred_cls,
target=targets,
anchors=self.scaled_anchors,
ignore_thres=self.ignore_thres,
)
# iou_scores:真实值与最匹配的anchor的IOU得分值 class_mask:分类正确的索引 obj_mask:目标框所在位置的最好anchor置为1 noobj_mask obj_mask那里置0,还有计算的iou大于阈值的也置0,其他都为1 tx, ty, tw, th, 对应的对于该大小的特征图的xywh目标值也就是我们需要拟合的值 tconf 目标置信度
# Loss : Mask outputs to ignore non-existing objects (except with conf. loss)
loss_x = self.mse_loss(x[obj_mask], tx[obj_mask]) # 只计算有目标的
loss_y = self.mse_loss(y[obj_mask], ty[obj_mask])
loss_w = self.mse_loss(w[obj_mask], tw[obj_mask])
loss_h = self.mse_loss(h[obj_mask], th[obj_mask])
loss_conf_obj = self.bce_loss(pred_conf[obj_mask], tconf[obj_mask])
loss_conf_noobj = self.bce_loss(pred_conf[noobj_mask], tconf[noobj_mask])
loss_conf = self.obj_scale * loss_conf_obj + self.noobj_scale * loss_conf_noobj #有物体越接近1越好 没物体的越接近0越好
loss_cls = self.bce_loss(pred_cls[obj_mask], tcls[obj_mask]) #分类损失
total_loss = loss_x + loss_y + loss_w + loss_h + loss_conf + loss_cls #总损失
# Metrics
cls_acc = 100 * class_mask[obj_mask].mean()
conf_obj = pred_conf[obj_mask].mean()
conf_noobj = pred_conf[noobj_mask].mean()
conf50 = (pred_conf > 0.5).float()
iou50 = (iou_scores > 0.5).float()
iou75 = (iou_scores > 0.75).float()
detected_mask = conf50 * class_mask * tconf
precision = torch.sum(iou50 * detected_mask) / (conf50.sum() + 1e-16)
recall50 = torch.sum(iou50 * detected_mask) / (obj_mask.sum() + 1e-16)
recall75 = torch.sum(iou75 * detected_mask) / (obj_mask.sum() + 1e-16)
self.metrics = {
"loss": to_cpu(total_loss).item(),
"x": to_cpu(loss_x).item(),
"y": to_cpu(loss_y).item(),
"w": to_cpu(loss_w).item(),
"h": to_cpu(loss_h).item(),
"conf": to_cpu(loss_conf).item(),
"cls": to_cpu(loss_cls).item(),
"cls_acc": to_cpu(cls_acc).item(),
"recall50": to_cpu(recall50).item(),
"recall75": to_cpu(recall75).item(),
"precision": to_cpu(precision).item(),
"conf_obj": to_cpu(conf_obj).item(),
"conf_noobj": to_cpu(conf_noobj).item(),
"grid_size": grid_size,
}
return output, total_loss
五、模型搭建
结合parse_data_config
函数返回的options
字典中的参数,设置网络的输入输出属性、确定 GPU 的使用方式以及数据加载的工作线程数量等,确保网络模型能够在合适的环境下高效运行。
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"])] #输出特征图的个数,也是卷积核的个数
module_list = nn.ModuleList()
#nn.ModuleList()是PyTorch中的一个类,用于将一个或多个nn.Module的实例包装到一个可迭代对象中。这个类在定义神经网络模型时非常有用,因为它允许你将多个网络层(每个都是nn.Module的实例)组合在一起,并以列表的形式管理和使用。
#具体来说,nn.ModuleList()的工作方式类似于Python的列表。你可以像向普通列表添加元素一样向ModuleList添加层,然后可以通过索引访问它们,就像访问普通的列表一样。然而,与普通列表不同的是,ModuleList中的元素是nn.Module的实例,即神经网络层。
for module_i, module_def in enumerate(module_defs):
modules = nn.Sequential()#nn.Sequential()主要用于实现线性堆叠,内部实现了前向传播函数;而nn.ModuleList()则用于存储多个网络层,以便在需要时进行调用,并不实现任何前向传播功能。
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}", #'conv_0
nn.Conv2d(
in_channels=output_filters[-1],#输入特征图的数量,即输入的维度。
out_channels=filters, #输出特征图的数量,即输出的维度。
kernel_size=kernel_size, #卷积核的大小
stride=int(module_def["stride"]), #卷积核滑动的步长大小
padding=pad, #在输入数据边界上填充0的层数
bias=not bn, #是否在卷积后添加偏置项。做了batch normalize,很多情况就不用偏置项。
),
)
if bn:
# nn.BatchNorm2d的输入参数包括:
# num_features:输入图像的通道数量。
# eps:稳定系数,防止分母出现0。默认值为1e - 05。
# momentum:一个用于运行过程中均值和方差的一个估计参数。默认值为0.1。可用于控制批量归一化中移动平均和指数加权的因子。这意味着,随着时间的推移,momentum将推动均值和方差向新的数据进行调整。较小的momentum会导致较慢的调整速度,而较大的momentum则会加快调整速度。
# 这个参数的设置是有实际意义的。因为在深度神经网络训练过程中,数据分布会随时间变化。通过设置momentum参数,我们可以使批量归一化层的均值和方差能够跟随数据分布的变化而动态调整,从而更好地适应新的数据分布。
# 例如,如果我们有一个网络,其中第一个层使用了批量归一化,而第二个层没有使用批量归一化。在这种情况下,如果我们提高了第一个层的momentum值,那么随着时间的推移,第一个层的均值和方差将逐渐调整以适应新的数据分布。这将使得第二个层能够更好地处理经过归一化的输入数据,从而提高整个网络的性能。
# 总之,momentum参数在nn.BatchNorm2d中起到了推动批量归一化层的均值和方差向新数据进行调整的作用,有助于提高网络对数据分布变化的适应能力。
modules.add_module(f"batch_norm_{module_i}", nn.BatchNorm2d(filters, momentum=0.9))
if module_def["activation"] == "leaky":
# nn.LeakyReLU是PyTorch中的一个非线性激活函数,它是一种带有泄漏的线性整流激活函数(LeakyReLU)。与ReLU不同,Leaky
# ReLU在输入为负数时不是完全的截断,而是有一个小的负斜率。这意味着,对于负输入,LeakyReLU将不会输出0,而是逐渐增加的输出值。
# 这种设计有助于解决ReLU在负数区域的问题,特别是在深度神经网络中,ReLU的截断可能导致一些神经元无法激活,而LeakyReLU可以缓解这个问题。
modules.add_module(f"leaky_{module_i}", nn.LeakyReLU(0.1))
elif module_def["type"] == "maxpool": #darknet网络中没有池化层,因此本段代码不会执行
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")#yolov3.cfg文件中保存的参数为stride=2
modules.add_module(f"upsample_{module_i}", upsample)
elif module_def["type"] == "route": #为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": # #残差的链接,也就是进行数据的加法+运算
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(",")]#预选框的id
# 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