我使用的是qqwweee/keras-yolo3的代码,代码地址
目前用于做某个设备的工业化检测,实现召回率98%以上,误检在2%左右,已满足应用需求。
解决了4个问题:
1.多GPU训练问题;
2.多GPU训练速度慢的问题;
3.单GPU无法加载多GPU跑出的加载权重问题;
4.Pretrain训练收敛速度慢的问题。
1.多GPU训练问题
参考的是这篇文章
①首先在train.py的def _main()里,加上parallel_model = multi_gpu_model(model, gpus=4),因为我的GPU为4个,所以gpus=4
def _main():
annotation_path = 'label-29669.txt'
log_dir = 'logs/000/'
classes_path = 'model_data/0classes4.txt'
anchors_path = 'model_data/yolo_anchors.txt'
class_names = get_classes(classes_path)
num_classes = len(class_names)
anchors = get_anchors(anchors_path)
input_shape = (640, 640) # multiple of 32, hw
model = create_model(input_shape, anchors, num_classes,
freeze_body=2, weights_path='model_data/train.h5')
parallel_model = multi_gpu_model(model, gpus=4)
然后把里面所有的model.compile、model.fit_generator都改成parallel_model.compile、parallel_model.generator,共改4处。
②qqwweee/keras-yolo3模型默认采用的是一块GPU,在直接使用model = multi_gpu_model(model,gpus=N)时,模型会报错tensorflow.python.framework.errors_impl.InvalidArgumentError: Can’t concatenate scalars (use tf.stack instead) for ‘yolo_loss_1/concat’ (op: ‘ConcatV2’) with input shapes: [], [], [], [].,这是因为该模型的设计的loss输出时一个标量,需要多输出进行修改,才能实现并行。具体修改方式如下:
yolo_loss函数中的
xy_loss = K.sum(xy_loss) / mf
wh_loss = K.sum(wh_loss) / mf
confidence_loss = K.sum(confidence_loss) / mf
class_loss = K.sum(class_loss) / mf
loss += xy_loss + wh_loss + confidence_loss +class_loss
替换成下面的代码
xy_loss = K.sum(K.sum(xy_loss,axis=[2,3,4]),1,keepdims=True)
wh_loss = K.sum(K.sum(wh_loss,axis=[2,3,4]),1,keepdims=True)
confidence_loss = K.sum(K.sum(confidence_loss,axis=[2,3,4]),1,keepdims=True)
class_loss = K.sum(K.sum(class_loss,axis=[2,3,4]),1,keepdims=True)
loss += xy_loss + wh_loss + confidence_loss +class_loss
即实现对loss的改进,保证输入N张图片,输出时(N,1)对应的时N张图片的loss
这个函数在yolo3文件夹model.py里。
③然后将编译语句model.compile(optimizer=Adam(lr=1e-3), loss={ ‘yolo_loss’: lambda y_true, y_pred: y_pred})改为
model.compile(optimizer=Adam(lr=1e-3), loss=totalloss),其中totalloss为下面定义的损失函数,下面这个定义要添加进train.py里。
def totalloss(y_true, y_pred):
return K.sum(y_pred)/K.cast(K.shape(y_pred)[0],K.dtype(y_pred))
在train.py里添加和修改,记住两个if True的model.compile都要改loss。
④再将model_loss = Lambda(yolo_loss, output_shape=(1,),name=‘yolo_loss’,arguments={‘anchors’: anchors, ‘num_classes’: num_classes, ‘ignore_thresh’: 0.5})( [*model_body.output, *y_true])中的, output_shape=(1,)去掉,
直接替换为model_loss = Lambda(yolo_loss,name=‘yolo_loss’,arguments={‘anchors’: anchors, ‘num_classes’: num_classes, ‘ignore_thresh’: 0.5})( [*model_body.output, *y_true])
model_loss在train.py的def create_model函数定义里。
修改完这4处之后,就直接可以进行多gpu训练了。
2.多GPU训练速度慢的问题
多GPU跑起来后,发现训练速度并没有变快。在代码中打了几个时间标签后发现yolo模块处理时间只占一个batch处理时间的10%左右,其余时间花在图像预处理上。图像预处理在train.py的data_generator函数中,其中:
image, box = get_random_data(annotation_lines[i], input_shape, random=True)
get_random_data在yolo3文件夹的utils.py中定义。如果random=True的话,要搞平移、翻转、扭曲一堆图像增强,还需要标定框跟随。如果random=False,也需要把原图转换成预先定义的(416,416)size,时间都费在这上面了!
解决这个问题的方法是:如果需要图像增强,事先人工把图像增强一下,存到数据集里。然后random定义为false,同时自己编个码,把图像size都归一化到预先定义的size里,保存到数据集。这样的好处是,如果有200个epoch,每张图片都需要resize200次,事先做好,训练时一次也不需要,节约大量训练时间。
对get_random_data进行改写如下:
def get_random_data(annotation_line, input_shape, random=True, max_boxes=20, jitter=.3, hue=.1, sat=1.5, val=1.5, proc_img=True):
'''random preprocessing for real-time data augmentation'''
line = annotation_line.split()
image = Image.open(line[0])
box = np.array([np.array(list(map(int, box.split(',')))) for box in line[1:]])
if not random:
image_data = np.array(image) / 255.
box_data = np.zeros((max_boxes, 5))
box_data[:len(box)] = box
return image_data, box_data
简单粗暴!图像处理时间大大压缩,加速效果也很明显,4个GPU都在满负荷工作了。
3.单GPU无法加载多GPU跑出的加载权重问题
参考文章
多GPU训练是通过multi_gpu_model把model分配到多块GPU上并行运算,所以训练出来的中间模型也只适用于训练它的机器。因为保存的方法是pararell_model.save,来看代码:
parallel_model.fit_generator(data_generator_wrapper(lines[:num_train], batch_size, input_shape, anchors, num_classes),
steps_per_epoch=max(1, num_train//batch_size),
validation_data=data_generator_wrapper(lines[num_train:], batch_size, input_shape, anchors, num_classes),
validation_steps=max(1, num_val//batch_size),
epochs=500,
initial_epoch=100,
callbacks=[logging, checkpoint, reduce_lr])
中间权重是通过回调checkpoint来保存的,checkpoint是ModelCheckpoint的返回值,对ModelCheckpoint类进行改写,用单GPU模型来保存权重文件,在callbacks.py里。
def __init__(self, filepath, monitor='val_loss', verbose=0,
save_best_only=False, save_weights_only=False,
mode='auto', period=1, save_model=0): # 增加save_mode参数,调用时用单GPU模型
super(ModelCheckpoint, self).__init__()
self.monitor = monitor
self.verbose = verbose
self.filepath = filepath
self.save_best_only = save_best_only
self.save_weights_only = save_weights_only
self.period = period
self.epochs_since_last_save = 0
self.save_model = save_model # 单GPU参数
if mode not in ['auto', 'min', 'max']:
warnings.warn('ModelCheckpoint mode %s is unknown, '
'fallback to auto mode.' % (mode),
RuntimeWarning)
mode = 'auto'
if mode == 'min':
self.monitor_op = np.less
self.best = np.Inf
elif mode == 'max':
self.monitor_op = np.greater
self.best = -np.Inf
else:
if 'acc' in self.monitor or self.monitor.startswith('fmeasure'):
self.monitor_op = np.greater
self.best = -np.Inf
else:
self.monitor_op = np.less
self.best = np.Inf
def on_epoch_end(self, epoch, logs=None):
logs = logs or {}
self.epochs_since_last_save += 1
if self.epochs_since_last_save >= self.period:
self.epochs_since_last_save = 0
filepath = self.filepath.format(epoch=epoch + 1, **logs)
if self.save_best_only:
current = logs.get(self.monitor)
if current is None:
warnings.warn('Can save best model only with %s available, '
'skipping.' % (self.monitor), RuntimeWarning)
else:
if self.monitor_op(current, self.best):
if self.verbose > 0:
print('\nEpoch %05d: %s improved from %0.5f to %0.5f,'
' saving model to %s'
% (epoch + 1, self.monitor, self.best,
current, filepath))
self.best = current
if self.save_weights_only:
# self.model.save_weights(filepath, overwrite=True)
self.save_model.save_weights(filepath) # 单GPU save model
else:
self.model.save(filepath, overwrite=True)
else:
if self.verbose > 0:
print('\nEpoch %05d: %s did not improve from %0.5f' %
(epoch + 1, self.monitor, self.best))
else:
if self.verbose > 0:
print('\nEpoch %05d: saving model to %s' % (epoch + 1, filepath))
if self.save_weights_only:
# self.model.save_weights(filepath, overwrite=True)
self.save_model.save_weights(filepath) # 单GPU save model
else:
self.model.save(filepath, overwrite=True)
保存的中间模型就可以用于单GPU机器加载了。
4.Pretrain训练收敛速度慢的问题
如果用预训练模型,在def create_model(input_shape, anchors, num_classes, load_pretrained=True, freeze_body=2,
weights_path='model_data/train.h5')定义中,如果load_pretrained=True,它认为你只需要训练最后2层全连接分类网络,前面的卷积神经网络就freeze了,所以训练速度快,收敛速度就慢了。如果要全网络训练,需屏蔽定义中的以下代码:
if load_pretrained:
model_body.load_weights(weights_path, by_name=True, skip_mismatch=True)
print('Load weights {}.'.format(weights_path))
# 如果加载预训练模型后还要训练卷积神经网络的话,需屏蔽以下代码
'''
if freeze_body in [1, 2]:
# Freeze darknet53 body or freeze all but 3 output layers.
num = (185, len(model_body.layers)-3)[freeze_body-1]
for i in range(num): model_body.layers[i].trainable = False
print('Freeze the first {} layers of total {} layers.'.format(num, len(model_body.layers)))
'''
问题解决!