目录
一、权重裁剪
1.Darknet裁剪方式
2.pytorch裁剪方式
二、数据集制作
1.数据集构建方式
2.DataLoader
三、准确率召回率计算
四、调参总结
1.配置中的数据
2.训练过程中的参数
导入的是官方给出的预训练参数,由于参数是在coco数据集上训练的,是80个类别,因此,如果想使用预训练参数来训练自己的数据集的话,需要在输出类别之前进行裁剪,只用前面的数据。
根据模型来看,第137个模块以后,就开始了第一个Yolo头的输出。那么我们就可以在输出之前,把模型参数拿过来当做预训练的参数来进行自己的训练。
但这样的话,如果我们的分类的类别和coco的数据集的不一样,那么效果可能就不好,因此,还可以在往前提,根据实际情况也可以在第104(只有backbone部分),116(backbone+spp)或者第126个模块的部分进行切割。
def load_weights(self, weightfile, pretrained=False, cut = 137):
print('starting load dark weights %s'%weightfile)
fp = open(weightfile, 'rb')
header = np.fromfile(fp, count=5, dtype=np.int32)
self.header = torch.from_numpy(header)
self.seen = self.header[3]
buf = np.fromfile(fp, dtype=np.float32)
fp.close()
start = 0
ind = -2
cutoff = None
if pretrained:
cutoff = cut+1
for block in self.blocks:
ind = ind + 1
if start >= buf.size:
print('weights at the end...')
break
if ind == cutoff:
print('weights at the cutoff...')
break
if block['type'] == 'net':
continue
elif block['type'] == 'convolutional':
model = self.models[ind]
batch_normalize = int(block['batch_normalize'])
if batch_normalize:
start = load_conv_bn(buf, start, model[0], model[1])
else:
start = load_conv(buf, start, model[0])
elif block['type'] == 'connected':
model = self.models[ind]
if block['activation'] != 'linear':
start = load_fc(buf, start, model[0])
else:
start = load_fc(buf, start, model)
elif block['type'] == 'maxpool':
pass
elif block['type'] == 'reorg':
pass
elif block['type'] == 'upsample':
pass
elif block['type'] == 'route':
pass
elif block['type'] == 'shortcut':
pass
elif block['type'] == 'region':
pass
elif block['type'] == 'yolo':
pass
elif block['type'] == 'avgpool':
pass
elif block['type'] == 'softmax':
pass
elif block['type'] == 'cost':
pass
else:
print('unknown type %s' % (block['type']))
print('load last layer number: ---> %d'%(ind-1))
也是导入权重,只不过是通过PyTorch的方式,由于模型的构造方式和darknet不一样,因此裁剪的方式也不一样。在PyTorch构建模型的时候,我们会定义每一部分的模型名字,那么构建完毕之后,通过model.state_dict()方法,就可以得到每一部分的名字,将其排序后,可以对照着darknet的结构,寻找到对应的模块所在位置的序号。在代码中已标注。
def load_model_pth(model, pth, cut=None):
print('Loading weights into state dict, name: %s'%(pth))
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model_dict = model.state_dict()
pretrained_dict = torch.load(pth, map_location=device)
#431: backbone
#467: backbone+SPP
#509: backbone+SPP+1_concat
#557: 对应darknet的137
# for i, key in enumerate(pretrained_dict):
# print('items:', i, key, np.shape(pretrained_dict[key]))
# assert 0
match_dict = {}
print_dict = {}
if cut== None: cut = (len(pretrained_dict) - 1)
try:
for i, (k, v) in enumerate(pretrained_dict.items()):
if i <= cut:
assert np.shape(model_dict[k]) == np.shape(v)
match_dict[k] = v
print_dict[k] = v
else:
print_dict[k] = '[NO USE]'
except:
print('different shape with:', np.shape(model_dict[k]), np.shape(v), ' name:', k)
assert 0
for i, key in enumerate(print_dict):
value = print_dict[key]
print('items:', i, key, np.shape(value) if type(value) != str else value)
model_dict.update(match_dict)
model.load_state_dict(model_dict)
print('Finished!')
return model
本项目中的数据集是网上下载的数据集,用来进行人和车的检测。数据集包含了已经标注好的框的左上角和右下角的数字,以及标签类别。
但是训练的时候,根据模型的需求,这里将每一行数据改成了如下的形式:
图片的绝对地址 810,568,1417,818,0 1374,481,1909,807,0
以空格分隔,第一部分是图片的绝对地址,后面就是这张图里的标签的四个数字以及类别数字,由于一张图里可以有多个物体,因此后面可以跟多个内容,都以空格分隔即可。
如果使用PyTorch自带的dataset的方式进行数据的构建的话,可以建一个继承自Dataset的类,定义图片变换、增强、随机化的方法对图片进行预处理,在__getitem__的时候,调用方法获取对应图片就可以。这样的方式速度快,但是当使用batch进行训练的时候,如果中间有一张图有问题,很难得知问题出在了哪里。因此可以使用自建的dataloader,先进行一次训练,确认没问题后,再使用PyTorch的dataloader。
class TrainGenerator(object):
def __init__(self, batch_size,
train_lines, image_size,
):
self.batch_size = batch_size
self.train_lines = train_lines
self.train_batches = len(train_lines)
self.image_size = image_size
self.test_time = time.time()
def get_random_data(self, annotation_line, input_shape, jitter=.3, hue=.1, sat=1.5, val=1.5):
'''r实时数据增强的随机预处理'''
line = annotation_line.split()
image = Image.open(line[0])
iw, ih = image.size
h, w = input_shape
box = np.array([np.array(list(map(float, box.split(',')))) for box in line[1:]])
# resize image
new_ar = w / h * rand(1 - jitter, 1 + jitter) / rand(1 - jitter, 1 + jitter)
scale = rand(.25, 2)
if new_ar < 1:
nh = int(scale * h)
nw = int(nh * new_ar)
else:
nw = int(scale * w)
nh = int(nw / new_ar)
image = image.resize((nw, nh), Image.BICUBIC)
# place image
dx = int(rand(0, w - nw))
dy = int(rand(0, h - nh))
new_image = Image.new('RGB', (w, h), (128, 128, 128))
new_image.paste(image, (dx, dy))
image = new_image
# flip image or not
flip = rand() < .5
if flip: image = image.transpose(Image.FLIP_LEFT_RIGHT)
# distort image
hue = rand(-hue, hue)
sat = rand(1, sat) if rand() < .5 else 1 / rand(1, sat)
val = rand(1, val) if rand() < .5 else 1 / rand(1, val)
x = cv2.cvtColor(np.array(image, np.float32) / 255, cv2.COLOR_RGB2HSV)
x[..., 0] += hue * 360
x[..., 0][x[..., 0] > 1] -= 1
x[..., 0][x[..., 0] < 0] += 1
x[..., 1] *= sat
x[..., 2] *= val
x[x[:, :, 0] > 360, 0] = 360
x[:, :, 1:][x[:, :, 1:] > 1] = 1
x[x < 0] = 0
image_data = cv2.cvtColor(x, cv2.COLOR_HSV2RGB) * 255
# correct boxes
box_data = np.zeros((len(box), 5))
if len(box) > 0:
np.random.shuffle(box)
box[:, [0, 2]] = box[:, [0, 2]] * nw / iw + dx
box[:, [1, 3]] = box[:, [1, 3]] * nh / ih + dy
if flip: box[:, [0, 2]] = w - box[:, [2, 0]]
box[:, 0:2][box[:, 0:2] < 0] = 0
box[:, 2][box[:, 2] > w] = w
box[:, 3][box[:, 3] > h] = h
box_w = box[:, 2] - box[:, 0]
box_h = box[:, 3] - box[:, 1]
box = box[np.logical_and(box_w > 1, box_h > 1)] # discard invalid box
box_data = np.zeros((len(box), 5))
box_data[:len(box)] = box
if len(box) == 0:
return image_data, []
if (box_data[:, :4] > 0).any():
return image_data, box_data
else:
return image_data, []
def get_random_data_with_Mosaic(self, annotation_line, input_shape, hue=.1, sat=1.5, val=1.5):
'''random preprocessing for real-time data augmentation'''
h, w = input_shape
min_offset_x = 0.4
min_offset_y = 0.4
scale_low = 1 - min(min_offset_x, min_offset_y)
scale_high = scale_low + 0.2
image_datas = []
box_datas = []
index = 0
place_x = [0, 0, int(w * min_offset_x), int(w * min_offset_x)]
place_y = [0, int(h * min_offset_y), int(w * min_offset_y), 0]
for line in annotation_line:
# 每一行进行分割
line_content = line.split()
# 打开图片
image = Image.open(line_content[0])
image = image.convert("RGB")
# 图片的大小
iw, ih = image.size
# 保存框的位置
box = np.array([np.array(list(map(float, box.split(',')))) for box in line_content[1:]])
# 是否翻转图片
flip = rand() < .5
if flip and len(box) > 0:
image = image.transpose(Image.FLIP_LEFT_RIGHT)
box[:, [0, 2]] = iw - box[:, [2, 0]]
# 对输入进来的图片进行缩放
new_ar = w / h
scale = rand(scale_low, scale_high)
if new_ar < 1:
nh = int(scale * h)
nw = int(nh * new_ar)
else:
nw = int(scale * w)
nh = int(nw / new_ar)
image = image.resize((nw, nh), Image.BICUBIC)
# 进行色域变换
hue = rand(-hue, hue)
sat = rand(1, sat) if rand() < .5 else 1 / rand(1, sat)
val = rand(1, val) if rand() < .5 else 1 / rand(1, val)
x = cv2.cvtColor(np.array(image, np.float32) / 255, cv2.COLOR_RGB2HSV)
x[..., 0] += hue * 360
x[..., 0][x[..., 0] > 1] -= 1
x[..., 0][x[..., 0] < 0] += 1
x[..., 1] *= sat
x[..., 2] *= val
x[x[:, :, 0] > 360, 0] = 360
x[:, :, 1:][x[:, :, 1:] > 1] = 1
x[x < 0] = 0
image = cv2.cvtColor(x, cv2.COLOR_HSV2RGB) # numpy array, 0 to 1
image = Image.fromarray((image * 255).astype(np.uint8))
# 将图片进行放置,分别对应四张分割图片的位置
dx = place_x[index]
dy = place_y[index]
new_image = Image.new('RGB', (w, h), (128, 128, 128))
new_image.paste(image, (dx, dy))
image_data = np.array(new_image)
index = index + 1
box_data = []
# 对box进行重新处理
if len(box) > 0:
np.random.shuffle(box)
box[:, [0, 2]] = box[:, [0, 2]] * nw / iw + dx
box[:, [1, 3]] = box[:, [1, 3]] * nh / ih + dy
box[:, 0:2][box[:, 0:2] < 0] = 0
box[:, 2][box[:, 2] > w] = w
box[:, 3][box[:, 3] > h] = h
box_w = box[:, 2] - box[:, 0]
box_h = box[:, 3] - box[:, 1]
box = box[np.logical_and(box_w > 1, box_h > 1)]
box_data = np.zeros((len(box), 5))
box_data[:len(box)] = box
image_datas.append(image_data)
box_datas.append(box_data)
# 将图片分割,放在一起
cutx = np.random.randint(int(w * min_offset_x), int(w * (1 - min_offset_x)))
cuty = np.random.randint(int(h * min_offset_y), int(h * (1 - min_offset_y)))
new_image = np.zeros([h, w, 3])
new_image[:cuty, :cutx, :] = image_datas[0][:cuty, :cutx, :]
new_image[cuty:, :cutx, :] = image_datas[1][cuty:, :cutx, :]
new_image[cuty:, cutx:, :] = image_datas[2][cuty:, cutx:, :]
new_image[:cuty, cutx:, :] = image_datas[3][:cuty, cutx:, :]
# 对框进行进一步的处理
new_boxes = np.array(merge_bboxes(box_datas, cutx, cuty))
if len(new_boxes) == 0:
return new_image, []
if (new_boxes[:, :4] > 0).any():
return new_image, new_boxes
else:
return new_image, []
def generate(self, train=True, mosaic=True):
while True:
shuffle(self.train_lines)
lines = self.train_lines
inputs = []
targets = []
flag = True
n = len(lines)
for i in range(len(lines)):
if mosaic == True:
if flag and (i + 4) < n:
img, y = self.get_random_data_with_Mosaic(lines[i:i + 4], self.image_size[0:2])
i = (i + 4) % n
else:
img, y = self.get_random_data(lines[i], self.image_size[0:2])
i = (i + 1) % n
flag = bool(1 - flag)
else:
img, y = self.get_random_data(lines[i], self.image_size[0:2])
i = (i + 1) % n
if len(y) != 0:
boxes = np.array(y[:, :4], dtype=np.float32)
boxes[:, 0] = boxes[:, 0] / self.image_size[1]
boxes[:, 1] = boxes[:, 1] / self.image_size[0]
boxes[:, 2] = boxes[:, 2] / self.image_size[1]
boxes[:, 3] = boxes[:, 3] / self.image_size[0]
boxes = np.maximum(np.minimum(boxes, 1), 0)
boxes[:, 2] = boxes[:, 2] - boxes[:, 0]
boxes[:, 3] = boxes[:, 3] - boxes[:, 1]
boxes[:, 0] = boxes[:, 0] + boxes[:, 2] / 2
boxes[:, 1] = boxes[:, 1] + boxes[:, 3] / 2
y = np.concatenate([boxes, y[:, -1:]], axis=-1)
img = np.array(img, dtype=np.float32)
inputs.append(np.transpose(img / 255.0, (2, 0, 1)))
targets.append(np.array(y, dtype=np.float32))
if len(targets) == self.batch_size:
tmp_inp = np.array(inputs)
tmp_targets = np.array(targets)
inputs = []
targets = []
# print('data load use time:', time.time()-self.test_time)
# self.test_time = time.time()
yield tmp_inp, tmp_targets
这里使用Evaluation工具来对预测进行计算。通过Evaluation.map_eval_pil.compute_map模块进行计算。计算之前,需要把经过前向传播的输出解码后的数据以及真实数据进行一个处理,把每张图片的每个预测,都处理成同一个格式:
1479504250862555460.jpg Out: car 0.99990356 [858.5215759277344, 587.5540852546692, 1011.3632583618164, 719.0725564956665]
图片名字,空格,Out:,\t,预测类别,空格,物体置信度,\t,画框的四个数据
之后再调节一下其他设定,就可以传入模块了。
def make_labels_and_compute_map(infos, classes, input_dir, save_err_miss=False):
out_lines,gt_lines = [],[]
out_path = 'Evaluation/out.txt'
gt_path = 'Evaluation/true.txt'
foutw = open(out_path, 'w')
fgtw = open(gt_path, 'w')
for info in infos:
out, gt, shapes = info
for i, images in enumerate(out):
for box in images:
bbx = [box[0]*shapes[i][1], box[1]*shapes[i][0], box[2]*shapes[i][1], box[3]*shapes[i][0]]
bbx = str(bbx)
cls = classes[int(box[6])]
prob = str(box[4])
img_name = os.path.split(shapes[i][2])[-1]
line = '\t'.join([img_name, 'Out:', cls, prob, bbx])+'\n'
out_lines.append(line)
for i, images in enumerate(gt):
for box in images:
bbx = str(box.tolist()[0:4])
cls = classes[int(box[4])]
img_name = os.path.split(shapes[i][2])[-1]
line = '\t'.join([img_name, 'Out:', cls, '1.0', bbx])+'\n'
gt_lines.append(line)
foutw.writelines(out_lines)
fgtw.writelines(gt_lines)
foutw.close()
fgtw.close()
args = EasyDict()
args.annotation_file = 'Evaluation/true.txt'
args.detection_file = 'Evaluation/out.txt'
args.detect_subclass = False
# 置信度大于设定的数值的数据,才会参与计算。
args.confidence = 0.2
# 真实框和预测框的IOU大于设定的数值,才会被认为是预测对了。
args.iou = 0.3
args.record_mistake = True
# 可以设定该参数用来保存预测错了的数据,并把框画出来,并保存图片。
args.draw_full_img = save_err_miss
args.draw_cut_box = False
args.input_dir = input_dir
args.out_dir = 'out_dir'
Map = compute_map(args)
return Map
①根据GPU大小,可调整subdivision,也就是一次输入GPU的数量。
②根据图片数据,可调整width和height,原来是608*608,只要是32的倍数即可,宽高越大精度越大,但速度会降低,反之精度低速度提升。
③图片的各种预处理如马赛克、色域变换之类的,无需调整。
④可调整burn in的值,比如burn in是1000,那么就是在前1000个batch学习率是增加的。
⑤根据类别数,可调整max batches也就是最大的训练的batch数量,比如是2类,那么可以训练8000个batches。
⑥如果训练数据没有很好地清洗,那么可以设置smooth_label为True,还可以有效防止过拟合。
①ciou损失的因子box_loss_scale
也就是根据当前的宽box_loss_scale_x和高box_loss_scale_y,计算所得的因子:box_loss_scale = 2 - box_loss_scale_x * box_loss_scale_y,当当前的宽高比较小的时候,这个值就会接近于2,那么就相当于提高了小物体的权重,当小目标比较多的时候可以用这个。
②三个损失的lambda因子
分类损失,回归损失,物体置信度损失的因子,默认是1.0,这里面前两个的因子可以调整,如果任务倾向于回归,也就是预测的框的准确性,那么可以把这个因子置为1,分类的相对小一些。当正样本很少的时候,可以在物体置信度损失那里乘上一个因子,比如2,5等。