Keras搭建M2Det目标检测平台示例

什么是M2det目标检测算法

一起来看看M2det的keras实现吧,顺便训练一下自己的数据。

 

常见的特征提取方法如图所示有SSD形,FPN形,STDN形:

SSD型:使用了主干网络的最后两层,再加上4个使用stride=2卷积的下采样层构成;

FPN型:也称为U型网络,经过上采样操作,然后对应融合相同的scale;

STDN型:基于DenseNet的最后一个dense block,通过池化和scale-transfer操作来构建;

这三者有一定的缺点:

一是均基于分类网络作为主干提取,对目标检测任务而言特征表示可能不够;

二是每个feature map仅由主干网络的single level给出,不够全面

M2det论文新提出MLFPN型,整体思想是Multi-level&Multi-scale。是一种更加有效的适合于检测的特征金字塔结构。

Keras搭建M2Det目标检测平台示例_第1张图片

 

M2det实现思路

一、预测部分

1、主干网络介绍

Keras搭建M2Det目标检测平台示例_第2张图片


M2det采用可以采用VGG和ResNet101作为主干特征提取网络,上图的backbone network指的就是VGG和Resnet101,本文以VGG为例介绍。

M2DET采用的主干网络是VGG网络,关于VGG的介绍大家可以看我的另外一篇博客

https://blog.csdn.net/weixin_44791964/article/details/102779878。

在m2det中,我们去掉了全部的全连接层,只保留了卷积层和最大池化层,即Conv1到Conv5。

1、一张原始图片被resize到(320,320,3)。

2、conv1两次[3,3]卷积网络,输出的特征层为64,输出为(320,320,64),再2X2最大池化,输出net为(160,160,64)。

3、conv2两次[3,3]卷积网络,输出的特征层为128,输出net为(160,160,128),再2X2最大池化,输出net为(80,80,128)。

4、conv3三次[3,3]卷积网络,输出的特征层为256,输出net为(80,80,256),再2X2最大池化,输出net为(40,40,256)。

5、conv4三次[3,3]卷积网络,输出的特征层为512,输出net为(40,40,512),再2X2最大池化,此时不进行池化,输出net为(40,40,512)。conv4-3的结果会进入FFM1进行特征的融合。

6、conv5三次[3,3]卷积网络,输出的特征层为1024,输出net为(40,40,1024),再2X2最大池化,输出net为(20,20,1024)。池化后的结果会进入FFM1进行特征的融合。

from keras import Model
from keras.layers import Conv2D, MaxPooling2D

def VGG16(inputs):
    net = {} 
    #------------------------#
    #   输入默认为320,320,3
    #------------------------#
    net['input'] = inputs

    #------------------------------------------------#
    #   第一个卷积部分  320,320,3 -> 160,160,64
    #------------------------------------------------#
    net['conv1_1'] = Conv2D(64, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv1_1')(net['input'])
    net['conv1_2'] = Conv2D(64, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv1_2')(net['conv1_1'])
    net['pool1'] = MaxPooling2D((2, 2), strides=(2, 2), padding='same',
                                name='pool1')(net['conv1_2'])
    
    #------------------------------------------------#
    #   第二个卷积部分  160,160,64 -> 80,80,128
    #------------------------------------------------#
    net['conv2_1'] = Conv2D(128, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv2_1')(net['pool1'])
    net['conv2_2'] = Conv2D(128, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv2_2')(net['conv2_1'])
    net['pool2'] = MaxPooling2D((2, 2), strides=(2, 2), padding='same',
                                name='pool2')(net['conv2_2'])
    y0 = net['pool2']

    #------------------------------------------------#
    #   第三个卷积部分  80,80,128 -> 40,40,256
    #------------------------------------------------#
    net['conv3_1'] = Conv2D(256, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv3_1')(net['pool2'])
    net['conv3_2'] = Conv2D(256, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv3_2')(net['conv3_1'])
    net['conv3_3'] = Conv2D(256, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv3_3')(net['conv3_2'])
    net['pool3'] = MaxPooling2D((2, 2), strides=(2, 2), padding='same',
                                name='pool3')(net['conv3_3'])
    y1 = net['pool3']

    #------------------------------------------------#
    #   第四个卷积部分  40,40,256 -> 40,40,512
    #------------------------------------------------#
    net['conv4_1'] = Conv2D(512, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv4_1')(net['pool3'])
    net['conv4_2'] = Conv2D(512, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv4_2')(net['conv4_1'])
    net['conv4_3'] = Conv2D(512, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv4_3')(net['conv4_2'])
    # net['pool4'] = MaxPooling2D((2, 2), strides=(2, 2), padding='same',
    #                             name='block4_pool')(net['conv4_3'])
    y2 = net['conv4_3']
    
    #------------------------------------------------#
    #   第五个卷积部分  40,40,512 -> 20,20,1024
    #------------------------------------------------#
    net['conv5_1'] = Conv2D(1024, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv5_1')(net['conv4_3'])
    net['conv5_2'] = Conv2D(1024, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv5_2')(net['conv5_1'])
    net['conv5_3'] = Conv2D(1024, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv5_3')(net['conv5_2'])
    net['pool5'] = MaxPooling2D((3, 3), strides=(2, 2), padding='same',
                                name='pool5')(net['conv5_3'])
    y3 = net['pool5']

    model = Model(inputs, [y0,y1,y2,y3], name='vgg16')
    return model

2、FFM1特征初步融合

Keras搭建M2Det目标检测平台示例_第3张图片

FFM1具体的结构如下:

Keras搭建M2Det目标检测平台示例_第4张图片


FFM1会对VGG提取到的特征进行初步融合。

在利用VGG进行特征提取的时候,我们会取出shape为(40,40,512)、(20,20,1024)的特征层进行下一步的操作。

在FFM1中,其会对(20,20,1024)的特征层进行进行一个通道数为512、卷积核大小为3x3、步长为1x1的卷积,然后再进行上采样,使其Shape变为(40,40,512);

同时会对(40,40,512)的特征层进行进行一个通道数为256、卷积核大小为1x1,步长为1x1的卷积,使其Shape变为(40,40,256);

然后将两个卷积后的结果进行堆叠,变成一个(40,40,768)的初步融合特征层

实现代码为:

def FFMv1(C4, C5, feature_size_1=256, feature_size_2=512, name='FFMv1'):
    #------------------------------------------------#
    #   C4特征层      40,40,512
    #   C5特征层      20,20,1024
    #------------------------------------------------#

    # 40,40,512 -> 40,40,256
    F4 = conv2d(C4, filters=feature_size_1, kernel_size=(3, 3), strides=(1, 1), padding='same', name='F4')

    # 20,20,1024 -> 20,20,512
    F5 = conv2d(C5, filters=feature_size_2, kernel_size=(1, 1), strides=(1, 1), padding='same', name='F5')
    # 20,20,512 -> 40,40,512
    F5 = keras.layers.UpSampling2D(size=(2, 2), name='F5_Up')(F5)

    # 40,40,256 + 40,40,512 -> 40,40,768
    outputs = keras.layers.Concatenate(name=name)([F4, F5])
    return outputs

3、细化U型模块TUM

Keras搭建M2Det目标检测平台示例_第5张图片


Tum的结构具体如下:

Keras搭建M2Det目标检测平台示例_第6张图片


当我们给Tum输入一个(40,40,256)的有效特征层之后,Tum会对输入进来的特征层进行U型的特征提取,这里的结构比较类似特征金字塔的结构,先对特征层进行不断的特征压缩,然后再不断的上采样进行特征融合,利用Tum我们可以获得6个有效特征层,大小分别是(40,40,128)、(20,20,128)、(10,10,128)、(5,5,128)、(3,3,128)、(1,1,128)。

def TUM(stage, inputs, feature_size=256, name="TUM"): #---------------------------------# # 进行下采样的部分 #---------------------------------# # 40,40,256 f1 = inputs # 40,40,256 -> 20,20,256 f2 = conv2d(f1, filters=feature_size, kernel_size=(3, 3), strides=(2, 2), padding='same',name=name + "_" + str(stage) + '_f2') # 20,20,256 -> 10,10,256 f3 = conv2d(f2, filters=feature_size, kernel_size=(3, 3), strides=(2, 2), padding='same',name=name + "_" + str(stage) + '_f3') # 10,10,256 -> 5,5,256 f4 = conv2d(f3, filters=feature_size, kernel_size=(3, 3), strides=(2, 2), padding='same',name=name + "_" + str(stage) + '_f4') # 5,5,256 -> 3,3,256 f5 = conv2d(f4, filters=feature_size, kernel_size=(3, 3), strides=(2, 2), padding='same',name=name + "_" + str(stage) + '_f5') # 3,3,256 -> 1,1,256 f6 = conv2d(f5, filters=feature_size, kernel_size=(3, 3), strides=(2, 2), padding='valid',name=name + "_" + str(stage) + '_f6') size_buffer = [] # 40,40 size_buffer.append([int(f1.shape[1]), int(f1.shape[2])]) # 20,20 size_buffer.append([int(f2.shape[1]), int(f2.shape[2])]) # 10,10 size_buffer.append([int(f3.shape[1]), int(f3.shape[2])]) # 5,5 size_buffer.append([int(f4.shape[1]), int(f4.shape[2])]) # 3,3 size_buffer.append([int(f5.shape[1]), int(f5.shape[2])]) #---------------------------------# # 进行上采样与特征融合的部分 #---------------------------------# c6 = f6 # 1,1,256 -> 1,1,256 c5 = conv2d(c6, filters=feature_size, kernel_size=(3, 3), strides=(1, 1), padding='same',name=name + "_" + str(stage) + '_c5') # 1,1,256 -> 3,3,256 c5 = keras.layers.Lambda(lambda x: tf.image.resize_bilinear(x, size=size_buffer[4]), name=name + "_" + str(stage) + '_upsample_add5')(c5) c5 = keras.layers.Add()([c5, f5]) # 3,3,256 -> 3,3,256 c4 = conv2d(c5, filters=feature_size, kernel_size=(3, 3), strides=(1, 1), padding='same', name=name + "_" + str(stage) + '_c4') # 3,3,256 -> 5,5,256 c4 = keras.layers.Lambda(lambda x: tf.image.resize_bilinear(x, size=size_buffer[3]), name=name + "_" + str(stage) + '_upsample_add4')(c4) c4 = keras.layers.Add()([c4, f4]) # 5,5,256 -> 5,5,256 c3 = conv2d(c4, filters=feature_size, kernel_size=(3, 3), strides=(1, 1), padding='same', name=name + "_" + str(stage) + '_c3') # 5,5,256 -> 10,10,256 c3 = keras.layers.Lambda(lambda x: tf.image.resize_bilinear(x, size=size_buffer[2]), name=name + "_" + str(stage) + '_upsample_add3')(c3) c3 = keras.layers.Add()([c3, f3]) # 10,10,256 -> 10,10,256 c2 = conv2d(c3, filters=feature_size, kernel_size=(3, 3), strides=(1, 1), padding='same', name=name + "_" + str(stage) + '_c2') # 10,10,256 -> 20,20,256 c2 = keras.layers.Lambda(lambda x: tf.image.resize_bilinear(x, size=size_buffer[1]), name=name + "_" + str(stage) + '_upsample_add2')(c2) c2 = keras.layers.Add()([c2, f2]) # 20,20,256 -> 20,20,256 c1 = conv2d(c2, filters=feature_size, kernel_size=(3, 3), strides=(1, 1), padding='same', name=name + "_" + str(stage) + '_c1') # 20,20,256 -> 40,40,256 c1 = keras.layers.Lambda(lambda x: tf.image.resize_bilinear(x, size=size_buffer[0]), name=name + "_" + str(stage) + '_upsample_add1')(c1) c1 = keras.layers.Add()([c1, f1]) #---------------------------------# # 利用1x1卷积调整通道数后输出 #---------------------------------# output_features = feature_size // 2 # 40,40,256 -> 40,40,128 o1 = conv2d(c1, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o1') # 20,20,256 -> 20,20,128 o2 = conv2d(c2, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o2') # 10,10,256 -> 10,10,128 o3 = conv2d(c3, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o3') # 5,5,256 -> 5,5,128 o4 = conv2d(c4, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o4') # 3,3,256 -> 3,3,128 o5 = conv2d(c5, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o5') # 1,1,256 -> 1,1,128 o6 = conv2d(c6, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o6') outputs = [o1, o2, o3, o4, o5, o6] return outputs
def TUM(stage, inputs, feature_size=256, name="TUM"):
    #---------------------------------#
    #   进行下采样的部分
    #---------------------------------#
    # 40,40,256
    f1 = inputs
    # 40,40,256 -> 20,20,256
    f2 = conv2d(f1, filters=feature_size, kernel_size=(3, 3), strides=(2, 2), padding='same',name=name + "_" + str(stage) + '_f2')
    # 20,20,256 -> 10,10,256
    f3 = conv2d(f2, filters=feature_size, kernel_size=(3, 3), strides=(2, 2), padding='same',name=name + "_" + str(stage) + '_f3')
    # 10,10,256 -> 5,5,256   
    f4 = conv2d(f3, filters=feature_size, kernel_size=(3, 3), strides=(2, 2), padding='same',name=name + "_" + str(stage) + '_f4')
    # 5,5,256 -> 3,3,256
    f5 = conv2d(f4, filters=feature_size, kernel_size=(3, 3), strides=(2, 2), padding='same',name=name + "_" + str(stage) + '_f5')
    # 3,3,256 -> 1,1,256
    f6 = conv2d(f5, filters=feature_size, kernel_size=(3, 3), strides=(2, 2), padding='valid',name=name + "_" + str(stage) + '_f6')

    size_buffer = []
    # 40,40
    size_buffer.append([int(f1.shape[1]), int(f1.shape[2])])
    # 20,20
    size_buffer.append([int(f2.shape[1]), int(f2.shape[2])])
    # 10,10
    size_buffer.append([int(f3.shape[1]), int(f3.shape[2])])
    # 5,5
    size_buffer.append([int(f4.shape[1]), int(f4.shape[2])])
    # 3,3
    size_buffer.append([int(f5.shape[1]), int(f5.shape[2])])
    
    #---------------------------------#
    #   进行上采样与特征融合的部分
    #---------------------------------#
    c6 = f6
    # 1,1,256 -> 1,1,256
    c5 = conv2d(c6, filters=feature_size, kernel_size=(3, 3), strides=(1, 1), padding='same',name=name + "_" + str(stage) + '_c5')
    # 1,1,256 -> 3,3,256
    c5 = keras.layers.Lambda(lambda x: tf.image.resize_bilinear(x, size=size_buffer[4]), name=name + "_" + str(stage) + '_upsample_add5')(c5)
    c5 = keras.layers.Add()([c5, f5])
 
    # 3,3,256 -> 3,3,256
    c4 = conv2d(c5, filters=feature_size, kernel_size=(3, 3), strides=(1, 1), padding='same', name=name + "_" + str(stage) + '_c4')
    # 3,3,256 -> 5,5,256
    c4 = keras.layers.Lambda(lambda x: tf.image.resize_bilinear(x, size=size_buffer[3]), name=name + "_" + str(stage) + '_upsample_add4')(c4)
    c4 = keras.layers.Add()([c4, f4])

    # 5,5,256 -> 5,5,256
    c3 = conv2d(c4, filters=feature_size, kernel_size=(3, 3), strides=(1, 1), padding='same', name=name + "_" + str(stage) + '_c3')
    # 5,5,256 -> 10,10,256
    c3 = keras.layers.Lambda(lambda x: tf.image.resize_bilinear(x, size=size_buffer[2]), name=name + "_" + str(stage) + '_upsample_add3')(c3)
    c3 = keras.layers.Add()([c3, f3])

    # 10,10,256 -> 10,10,256
    c2 = conv2d(c3, filters=feature_size, kernel_size=(3, 3), strides=(1, 1), padding='same', name=name + "_" + str(stage) + '_c2')
    # 10,10,256 -> 20,20,256
    c2 = keras.layers.Lambda(lambda x: tf.image.resize_bilinear(x, size=size_buffer[1]), name=name + "_" + str(stage) + '_upsample_add2')(c2)
    c2 = keras.layers.Add()([c2, f2])

    # 20,20,256 -> 20,20,256
    c1 = conv2d(c2, filters=feature_size, kernel_size=(3, 3), strides=(1, 1), padding='same', name=name + "_" + str(stage) + '_c1')
    # 20,20,256 -> 40,40,256
    c1 = keras.layers.Lambda(lambda x: tf.image.resize_bilinear(x, size=size_buffer[0]), name=name + "_" + str(stage) + '_upsample_add1')(c1)
    c1 = keras.layers.Add()([c1, f1])

    #---------------------------------#
    #   利用1x1卷积调整通道数后输出
    #---------------------------------#
    output_features = feature_size // 2
    # 40,40,256 -> 40,40,128 
    o1 = conv2d(c1, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o1')
    # 20,20,256 -> 20,20,128
    o2 = conv2d(c2, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o2')
    # 10,10,256 -> 10,10,128
    o3 = conv2d(c3, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o3')
    # 5,5,256 -> 5,5,128
    o4 = conv2d(c4, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o4')
    # 3,3,256 -> 3,3,128
    o5 = conv2d(c5, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o5')
    # 1,1,256 -> 1,1,128
    o6 = conv2d(c6, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o6')

    outputs = [o1, o2, o3, o4, o5, o6]

    return outputs

4、FFM2特征加强融合

通过TUM,我们可以获得六个有效特征层,为了进一步加强网络的特征提取能力,M2det将6个有效特征层中的(40,40,128)特征层取出,和FFM1提取出来的初步融合特征层进行加强融合,再次输出一个(40,40,256)的加强融合的特征层。

Keras搭建M2Det目标检测平台示例_第7张图片


此时FFM2输出的加强融合特征层可以再一次传入到TUM中进行U形特征提取。

Keras搭建M2Det目标检测平台示例_第8张图片


如上图所示,我们可以进一步利用多个TUM模块进行特征提取,利用多个TUM模块我们可以获得多次有效特征层。

TUM模块的数量我们可以根据自身需要进行修改,本文使用4次TUM模块,可以分别获得四次(40,40,128)、(20,20,128)、(10,10,128)、(5,5,128)、(3,3,128)、(1,1,128)的有效特征层。(论文中做了实验,用8次TUM模块会有比较好的效果)。

我们可以将获得的有效特征层,按照shape进行堆叠,最终获得(40,40,512)、(20,20,512)、(10,10,512)、(5,5,512)、(3,3,512)、(1,1,512)六个有效特征层。

def FFMv2(stage, base, tum, base_size=(40,40,768), tum_size=(40,40,128), feature_size=128, name='FFMv2'):
    # 40,40,128
    outputs = conv2d(base, filters=feature_size, kernel_size=(1, 1), strides=(1, 1), padding='same', name=name+"_"+str(stage) + '_base_feature')
    outputs = keras.layers.Concatenate(name=name+"_"+str(stage))([outputs, tum])
    # 40,40,256
    return outputs

def _create_feature_pyramid(base_feature, stage=8):
    features = [[],[],[],[],[],[]]
    # 将输入进来的
    inputs = keras.layers.Conv2D(filters=256, kernel_size=1, strides=1, padding='same')(base_feature)
    # 第一个TUM模块
    outputs = TUM(1,inputs)
    max_output = outputs[0]
    for j in range(len(features)):
        features[j].append(outputs[j])

    # 第2,3,4个TUM模块,需要将上一个Tum模块输出的40x40x128的内容,传入到下一个Tum模块中
    for i in range(2, stage+1):
        # 将Tum模块的输出和基础特征层传入到FFmv2层当中
        # 输入为base_feature 40x40x768,max_output 40x40x128
        # 输出为40x40x256
        inputs = FFMv2(i - 1,base_feature, max_output)
        # 输出为40x40x128、20x20x128、10x10x128、5x5x128、3x3x128、1x1x128
        outputs = TUM(i,inputs)

        max_output = outputs[0]
        for j in range(len(features)):
            features[j].append(outputs[j])
    # 进行了4次TUM
    # 将获得的同样大小的特征层堆叠到一起
    concatenate_features = []
    for feature in features:
        concat = keras.layers.Concatenate()([f for f in feature])
        concatenate_features.append(concat)
    return concatenate_features

5、注意力机制模块SFAM

Keras搭建M2Det目标检测平台示例_第9张图片


注意力机制模块如下:

Keras搭建M2Det目标检测平台示例_第10张图片


其会对上一步获得的(40,40,512)、(20,20,512)、(10,10,512)、(5,5,512)、(3,3,512)、(1,1,512)六个有效特征层。进行各个通道的注意力机制调整,判断每一个通道数应该有的权重。

# 注意力机制
def SE_block(inputs, input_size, compress_ratio=16, name='SE_block'):
    pool = keras.layers.GlobalAveragePooling2D()(inputs)
    reshape = keras.layers.Reshape((1, 1, input_size[2]))(pool)

    fc1 = keras.layers.Conv2D(filters=input_size[2] // compress_ratio, kernel_size=1, strides=1, padding='valid',
                              activation='relu', name=name+'_fc1')(reshape)
    fc2 = keras.layers.Conv2D(filters=input_size[2], kernel_size=1, strides=1, padding='valid', activation='sigmoid',
                              name=name+'_fc2')(fc1)

    reweight = keras.layers.Multiply(name=name+'_reweight')([inputs, fc2])

    return reweight


def SFAM(feature_pyramid,input_sizes, compress_ratio=16, name='SFAM'):
    outputs = []
    for i in range(len(input_sizes)):
        input_size = input_sizes[i]
        _input = feature_pyramid[i]
        _output = SE_block(_input, input_size, compress_ratio=compress_ratio, name='SE_block_' + str(i))

        outputs.append(_output)
    return outputs

6、从特征获取预测结果

通过第五步,我们获取了6个融合了注意力机制的有效特征层。

对获取到的每一个有效特征层,我们分别对其进行一次num_anchors x 4的卷积、一次num_anchors x num_classes的卷积、并需要计算每一个有效特征层对应的先验框。而num_anchors指的是该特征层所拥有的先验框数量。

其中:
 

num_anchors x 4的卷积 用于预测 该特征层上 每一个网格点上 每一个先验框的变化情况。(为什么说是变化情况呢,这是因为M2DET的预测结果需要结合先验框获得预测框,预测结果就是先验框的变化情况。)

num_anchors x num_classes的卷积 用于预测 该特征层上 每一个网格点上 每一个预测框对应的种类。

每一个有效特征层对应的先验框对应着该特征层上 每一个网格点上 预先设定好的六个框。

所有的特征层对应的预测结果的shape如下:

Keras搭建M2Det目标检测平台示例_第11张图片


实现代码为:

def m2det(input_shape, num_classes=21, num_anchors = 6):
    inputs = keras.layers.Input(shape=input_shape)
    #------------------------------------------------#
    #   利用主干特征提取网络获得两个有效特征层
    #   分别是C4      40,40,512
    #   分别是C5      20,20,1024
    #------------------------------------------------#
    C4, C5 = VGG16(inputs).outputs[2:]

    # base_feature的shape为40,40,768
    base_feature = FFMv1(C4, C5, feature_size_1=256, feature_size_2=512)

    #---------------------------------------------------------------------------------------------------#
    #   在_create_feature_pyramid函数里,我们会使用TUM模块对输入进来的特征层进行特征提取
    #   最终输出的特征层有六个,由于进行了四次的TUM模块,所以六个有效特征层由4次TUM模块的输出堆叠而成
    #   o1      40,40,128*4         40,40,512
    #   o2      20,20,128*4         20,20,512
    #   o3      10,10,128*4         10,10,512
    #   o4      5,5,128*4           5,5,512
    #   o5      3,3,128*4           3,3,512
    #   o6      1,1,128*4           1,1,512
    #---------------------------------------------------------------------------------------------------#
    feature_pyramid = _create_feature_pyramid(base_feature, stage=4)

    #-------------------------------------------------#
    #   给合并后的特征层添加上注意力机制
    #-------------------------------------------------#
    outputs = SFAM(feature_pyramid)

    #-------------------------------------------------#
    #   将有效特征层转换成输出结果
    #-------------------------------------------------#
    classifications = []
    regressions = []
    for feature in outputs:
        classification = keras.layers.Conv2D(filters = num_anchors * num_classes, kernel_size=3, strides=1, padding='same')(feature)
        classification = keras.layers.Reshape((-1, num_classes))(classification)
        classification = keras.layers.Activation('softmax')(classification)

        regression = keras.layers.Conv2D(filters = num_anchors * 4, kernel_size=3, strides=1, padding='same')(feature)
        regression = keras.layers.Reshape((-1, 4))(regression)

        classifications.append(classification)
        regressions.append(regression)
    
    classifications = keras.layers.Concatenate(axis=1, name="classification")(classifications)
    regressions     = keras.layers.Concatenate(axis=1, name="regression")(regressions)

    pyramids        = keras.layers.Concatenate(axis=-1, name="out")([regressions, classifications])
    return keras.models.Model(inputs=inputs, outputs=pyramids)

7、预测结果的解码

我们通过对每一个特征层的处理,可以获得两个内容,分别是:

num_anchors x 4的卷积 用于预测 该特征层上 每一个网格点上 每一个先验框的变化情况。

num_anchors x num_classes的卷积 用于预测 该特征层上 每一个网格点上 每一个预测框对应的种类。

每一个有效特征层对应的先验框对应着该特征层上 每一个网格点上 预先设定好的六个框。

我们利用 num_anchors x 4的卷积每一个有效特征层对应的先验框 获得框的真实位置。

每一个有效特征层对应的先验框就是,如图所示的作用:

每一个有效特征层将整个图片分成与其长宽对应的网格,如conv4-3和fl7组合成的特征层就是将整个图像分成38x38个网格;然后从每个网格中心建立多个先验框,如conv4-3和fl7组合成的有效特征层就是建立了6个先验框;对于conv4-3和fl7组合成的特征层来讲,整个图片被分成38x38个网格,每个网格中心对应6个先验框,一共包含了,38x38x6个,8664个先验框。

Keras搭建M2Det目标检测平台示例_第12张图片


先验框虽然可以代表一定的框的位置信息与框的大小信息,但是其是有限的,无法表示任意情况,因此还需要调整,RFBnet利用num_anchors x 4的卷积的结果对先验框进行调整。

num_anchors x 4中的num_anchors表示了这个网格点所包含的先验框数量,其中的4表示了x_offset、y_offset、h和w的调整情况。

x_offset与y_offset代表了真实框距离先验框中心的xy轴偏移情况。
h和w代表了真实框的宽与高相对于先验框的变化情况。

RFBnet解码过程就是将每个网格的中心点加上它对应的x_offset和y_offset,加完后的结果就是预测框的中心,然后再利用 先验框和h、w结合 计算出预测框的长和宽。这样就能得到整个预测框的位置了。

当然得到最终的预测结构后还要进行得分排序与非极大抑制筛选这一部分基本上是所有目标检测通用的部分。
 

1、取出每一类得分大于self.obj_threshold的框和得分。
 

2、利用框的位置和得分进行非极大抑制。

实现代码如下:

import numpy as np
import tensorflow as tf
import keras.backend as K

class BBoxUtility(object):
    def __init__(self, num_classes, nms_thresh=0.45, top_k=300):
        self.num_classes    = num_classes
        self._nms_thresh    = nms_thresh
        self._top_k         = top_k
        self.boxes          = K.placeholder(dtype='float32', shape=(None, 4))
        self.scores         = K.placeholder(dtype='float32', shape=(None,))
        self.nms            = tf.image.non_max_suppression(self.boxes, self.scores, self._top_k, iou_threshold=self._nms_thresh)
        self.sess           = K.get_session()

    def ssd_correct_boxes(self, box_xy, box_wh, input_shape, image_shape, letterbox_image):
        #-----------------------------------------------------------------#
        #   把y轴放前面是因为方便预测框和图像的宽高进行相乘
        #-----------------------------------------------------------------#
        box_yx = box_xy[..., ::-1]
        box_hw = box_wh[..., ::-1]
        input_shape = np.array(input_shape)
        image_shape = np.array(image_shape)

        if letterbox_image:
            #-----------------------------------------------------------------#
            #   这里求出来的offset是图像有效区域相对于图像左上角的偏移情况
            #   new_shape指的是宽高缩放情况
            #-----------------------------------------------------------------#
            new_shape = np.round(image_shape * np.min(input_shape/image_shape))
            offset  = (input_shape - new_shape)/2./input_shape
            scale   = input_shape/new_shape

            box_yx  = (box_yx - offset) * scale
            box_hw *= scale

        box_mins    = box_yx - (box_hw / 2.)
        box_maxes   = box_yx + (box_hw / 2.)
        boxes  = np.concatenate([box_mins[..., 0:1], box_mins[..., 1:2], box_maxes[..., 0:1], box_maxes[..., 1:2]], axis=-1)
        boxes *= np.concatenate([image_shape, image_shape], axis=-1)
        return boxes

    def decode_boxes(self, mbox_loc, anchors, variances):
        # 获得先验框的宽与高
        anchor_width     = anchors[:, 2] - anchors[:, 0]
        anchor_height    = anchors[:, 3] - anchors[:, 1]
        # 获得先验框的中心点
        anchor_center_x  = 0.5 * (anchors[:, 2] + anchors[:, 0])
        anchor_center_y  = 0.5 * (anchors[:, 3] + anchors[:, 1])

        # 真实框距离先验框中心的xy轴偏移情况
        decode_bbox_center_x = mbox_loc[:, 0] * anchor_width * variances[0]
        decode_bbox_center_x += anchor_center_x
        decode_bbox_center_y = mbox_loc[:, 1] * anchor_height * variances[1]
        decode_bbox_center_y += anchor_center_y
        
        # 真实框的宽与高的求取
        decode_bbox_width   = np.exp(mbox_loc[:, 2] * variances[2])
        decode_bbox_width   *= anchor_width
        decode_bbox_height  = np.exp(mbox_loc[:, 3] * variances[3])
        decode_bbox_height  *= anchor_height

        # 获取真实框的左上角与右下角
        decode_bbox_xmin = decode_bbox_center_x - 0.5 * decode_bbox_width
        decode_bbox_ymin = decode_bbox_center_y - 0.5 * decode_bbox_height
        decode_bbox_xmax = decode_bbox_center_x + 0.5 * decode_bbox_width
        decode_bbox_ymax = decode_bbox_center_y + 0.5 * decode_bbox_height

        # 真实框的左上角与右下角进行堆叠
        decode_bbox = np.concatenate((decode_bbox_xmin[:, None],
                                      decode_bbox_ymin[:, None],
                                      decode_bbox_xmax[:, None],
                                      decode_bbox_ymax[:, None]), axis=-1)
        # 防止超出0与1
        decode_bbox = np.minimum(np.maximum(decode_bbox, 0.0), 1.0)
        return decode_bbox

    def decode_box(self, predictions, anchors, image_shape, input_shape, letterbox_image, variances = [0.1, 0.1, 0.2, 0.2], confidence=0.5):
        #---------------------------------------------------#
        #   :4是回归预测结果
        #---------------------------------------------------#
        mbox_loc        = predictions[:, :, :4]
        #---------------------------------------------------#
        #   获得种类的置信度
        #---------------------------------------------------#
        mbox_conf       = predictions[:, :, 4:]

        results = []
        #----------------------------------------------------------------------------------------------------------------#
        #   对每一张图片进行处理,由于在predict.py的时候,我们只输入一张图片,所以for i in range(len(mbox_loc))只进行一次
        #----------------------------------------------------------------------------------------------------------------#
        for i in range(len(mbox_loc)):
            results.append([])
            #--------------------------------#
            #   利用回归结果对先验框进行解码
            #--------------------------------#
            decode_bbox = self.decode_boxes(mbox_loc[i], anchors, variances)

            for c in range(1, self.num_classes):
                #--------------------------------#
                #   取出属于该类的所有框的置信度
                #   判断是否大于门限
                #--------------------------------#
                c_confs     = mbox_conf[i, :, c]
                c_confs_m   = c_confs > confidence
                if len(c_confs[c_confs_m]) > 0:
                    #-----------------------------------------#
                    #   取出得分高于confidence的框
                    #-----------------------------------------#
                    boxes_to_process = decode_bbox[c_confs_m]
                    confs_to_process = c_confs[c_confs_m]
                    #-----------------------------------------#
                    #   进行iou的非极大抑制
                    #-----------------------------------------#
                    idx         = self.sess.run(self.nms, feed_dict={self.boxes: boxes_to_process, self.scores: confs_to_process})
                    #-----------------------------------------#
                    #   取出在非极大抑制中效果较好的内容
                    #-----------------------------------------#
                    good_boxes  = boxes_to_process[idx]
                    confs       = confs_to_process[idx][:, None]
                    labels      = (c - 1) * np.ones((len(idx), 1))
                    #-----------------------------------------#
                    #   将label、置信度、框的位置进行堆叠。
                    #-----------------------------------------#
                    c_pred      = np.concatenate((good_boxes, labels, confs), axis=1)
                    # 添加进result里
                    results[-1].extend(c_pred)

            if len(results[-1]) > 0:
                results[-1] = np.array(results[-1])
                box_xy, box_wh = (results[-1][:, 0:2] + results[-1][:, 2:4])/2, results[-1][:, 2:4] - results[-1][:, 0:2]
                results[-1][:, :4] = self.ssd_correct_boxes(box_xy, box_wh, input_shape, image_shape, letterbox_image)

        return results

8、在原图上进行绘制

通过第三步,我们可以获得预测框在原图上的位置,而且这些预测框都是经过筛选的。这些筛选后的框可以直接绘制在图片上,就可以获得结果了。

二、训练部分

1、真实框的处理

从预测部分我们知道,每个特征层的预测结果,num_anchors x 4的卷积 用于预测 该特征层上 每一个网格点上 每一个先验框的变化情况。

也就是说,我们直接利用M2DET网络预测到的结果,并不是预测框在图片上的真实位置,需要解码才能得到真实位置。

而在训练的时候,我们需要计算loss函数,这个loss函数是相对于M2DET网络的预测结果的。我们需要把图片输入到当前的M2DET网络中,得到预测结果;同时还需要把真实框的信息,进行编码,这个编码是把真实框的位置信息格式转化为M2DET预测结果的格式信息

也就是,我们需要找到 每一张用于训练的图片每一个真实框对应的先验框,并求出如果想要得到这样一个真实框,我们的预测结果应该是怎么样的。

从预测结果获得真实框的过程被称作解码,而从真实框获得预测结果的过程就是编码的过程。

因此我们只需要将解码过程逆过来就是编码过程了。

实现代码如下:

def iou(self, box):
    #---------------------------------------------#
    #   计算出每个真实框与所有的先验框的iou
    #   判断真实框与先验框的重合情况
    #---------------------------------------------#
    inter_upleft    = np.maximum(self.anchors[:, :2], box[:2])
    inter_botright  = np.minimum(self.anchors[:, 2:4], box[2:])

    inter_wh    = inter_botright - inter_upleft
    inter_wh    = np.maximum(inter_wh, 0)
    inter       = inter_wh[:, 0] * inter_wh[:, 1]
    #---------------------------------------------# 
    #   真实框的面积
    #---------------------------------------------#
    area_true = (box[2] - box[0]) * (box[3] - box[1])
    #---------------------------------------------#
    #   先验框的面积
    #---------------------------------------------#
    area_gt = (self.anchors[:, 2] - self.anchors[:, 0])*(self.anchors[:, 3] - self.anchors[:, 1])
    #---------------------------------------------#
    #   计算iou
    #---------------------------------------------#
    union = area_true + area_gt - inter

    iou = inter / union
    return iou

def encode_box(self, box, return_iou=True, variances = [0.1, 0.1, 0.2, 0.2]):
    #---------------------------------------------#
    #   计算当前真实框和先验框的重合情况
    #   iou [self.num_anchors]
    #   encoded_box [self.num_anchors, 5]
    #---------------------------------------------#
    iou = self.iou(box)
    encoded_box = np.zeros((self.num_anchors, 4 + return_iou))
    
    #---------------------------------------------#
    #   找到每一个真实框,重合程度较高的先验框
    #   真实框可以由这个先验框来负责预测
    #---------------------------------------------#
    assign_mask = iou > self.overlap_threshold

    #---------------------------------------------#
    #   如果没有一个先验框重合度大于self.overlap_threshold
    #   则选择重合度最大的为正样本
    #---------------------------------------------#
    if not assign_mask.any():
        assign_mask[iou.argmax()] = True
    
    #---------------------------------------------#
    #   利用iou进行赋值 
    #---------------------------------------------#
    if return_iou:
        encoded_box[:, -1][assign_mask] = iou[assign_mask]
    
    #---------------------------------------------#
    #   找到对应的先验框
    #---------------------------------------------#
    assigned_anchors = self.anchors[assign_mask]

    #---------------------------------------------#
    #   逆向编码,将真实框转化为M2det预测结果的格式
    #   先计算真实框的中心与长宽
    #---------------------------------------------#
    box_center  = 0.5 * (box[:2] + box[2:])
    box_wh      = box[2:] - box[:2]
    #---------------------------------------------#
    #   再计算重合度较高的先验框的中心与长宽
    #---------------------------------------------#
    assigned_anchors_center = (assigned_anchors[:, 0:2] + assigned_anchors[:, 2:4]) * 0.5
    assigned_anchors_wh     = (assigned_anchors[:, 2:4] - assigned_anchors[:, 0:2])
    
    #------------------------------------------------#
    #   逆向求取M2det应该有的预测结果
    #   先求取中心的预测结果,再求取宽高的预测结果
    #   存在改变数量级的参数,默认为[0.1,0.1,0.2,0.2]
    #------------------------------------------------#
    encoded_box[:, :2][assign_mask] = box_center - assigned_anchors_center
    encoded_box[:, :2][assign_mask] /= assigned_anchors_wh
    encoded_box[:, :2][assign_mask] /= np.array(variances)[:2]

    encoded_box[:, 2:4][assign_mask] = np.log(box_wh / assigned_anchors_wh)
    encoded_box[:, 2:4][assign_mask] /= np.array(variances)[2:4]
    return encoded_box.ravel()

利用上述代码我们可以获得,真实框对应的所有的iou较大先验框,并计算了真实框对应的所有iou较大的先验框应该有的预测结果。

在训练的时候我们只需要选择iou最大的先验框就行了,这个iou最大的先验框就是我们用来预测这个真实框所用的先验框。

因此我们还要经过一次筛选,将上述代码获得的真实框对应的所有的iou较大先验框的预测结果中,iou最大的那个筛选出来。

通过assign_boxes我们就获得了,输入进来的这张图片,应该有的预测结果是什么样子的。

实现代码如下:

def assign_boxes(self, boxes):
    #---------------------------------------------------#
    #   assignment分为3个部分
    #   :4      的内容为网络应该有的回归预测结果
    #   4:-1    的内容为先验框所对应的种类,默认为背景
    #   -1      的内容为当前先验框是否包含目标
    #---------------------------------------------------#
    assignment          = np.zeros((self.num_anchors, 4 + self.num_classes + 1))
    assignment[:, 4]    = 1.0
    if len(boxes) == 0:
        return assignment

    # 对每一个真实框都进行iou计算
    encoded_boxes   = np.apply_along_axis(self.encode_box, 1, boxes[:, :4])
    #---------------------------------------------------#
    #   在reshape后,获得的encoded_boxes的shape为:
    #   [num_true_box, num_anchors, 4 + 1]
    #   4是编码后的结果,1为iou
    #---------------------------------------------------#
    encoded_boxes   = encoded_boxes.reshape(-1, self.num_anchors, 5)
    
    #---------------------------------------------------#
    #   [num_anchors]求取每一个先验框重合度最大的真实框
    #---------------------------------------------------#
    best_iou        = encoded_boxes[:, :, -1].max(axis=0)
    best_iou_idx    = encoded_boxes[:, :, -1].argmax(axis=0)
    best_iou_mask   = best_iou > 0
    best_iou_idx    = best_iou_idx[best_iou_mask]
    
    #---------------------------------------------------#
    #   计算一共有多少先验框满足需求
    #---------------------------------------------------#
    assign_num      = len(best_iou_idx)

    # 将编码后的真实框取出
    encoded_boxes   = encoded_boxes[:, best_iou_mask, :]
    #---------------------------------------------------#
    #   编码后的真实框的赋值
    #---------------------------------------------------#
    assignment[:, :4][best_iou_mask] = encoded_boxes[best_iou_idx,np.arange(assign_num),:4]
    #----------------------------------------------------------#
    #   4代表为背景的概率,设定为0,因为这些先验框有对应的物体
    #----------------------------------------------------------#
    assignment[:, 4][best_iou_mask]     = 0
    assignment[:, 5:-1][best_iou_mask]  = boxes[best_iou_idx, 4:]
    #----------------------------------------------------------#
    #   -1表示先验框是否有对应的物体
    #----------------------------------------------------------#
    assignment[:, -1][best_iou_mask]    = 1
    # 通过assign_boxes我们就获得了,输入进来的这张图片,应该有的预测结果是什么样子的
    return assignment

2、利用处理完的真实框与对应图片的预测结果计算loss

loss的计算分为三个部分:

1、获取所有正标签的框的预测结果的回归loss。

2、获取所有正标签的种类的预测结果的交叉熵loss。

3、获取一定负标签的种类的预测结果的交叉熵loss。

由于在M2DET的训练过程中,正负样本极其不平衡,即 存在对应真实框的先验框可能只有十来个,但是不存在对应真实框的负样本却有几千个,这就会导致负样本的loss值极大,因此我们可以考虑减少负样本的选取,对于M2DET的训练来讲,常见的情况是取三倍正样本数量的负样本用于训练。这个三倍呢,也可以修改,调整成自己喜欢的数字。

实现代码如下:

import tensorflow as tf

class MultiboxLoss(object):
    def __init__(self, num_classes, alpha=1.0, neg_pos_ratio=3.0,
                 background_label_id=0, negatives_for_hard=100.0):
        self.num_classes = num_classes
        self.alpha = alpha
        self.neg_pos_ratio = neg_pos_ratio
        if background_label_id != 0:
            raise Exception('Only 0 as background label id is supported')
        self.background_label_id = background_label_id
        self.negatives_for_hard = negatives_for_hard

    def _l1_smooth_loss(self, y_true, y_pred):
        abs_loss = tf.abs(y_true - y_pred)
        sq_loss = 0.5 * (y_true - y_pred)**2
        l1_loss = tf.where(tf.less(abs_loss, 1.0), sq_loss, abs_loss - 0.5)
        return tf.reduce_sum(l1_loss, -1)

    def _softmax_loss(self, y_true, y_pred):
        y_pred = tf.maximum(y_pred, 1e-7)
        softmax_loss = -tf.reduce_sum(y_true * tf.log(y_pred),
                                      axis=-1)
        return softmax_loss

    def compute_loss(self, y_true, y_pred):
        num_boxes = tf.to_float(tf.shape(y_true)[1])

        # --------------------------------------------- #
        #   分类的loss
        #   batch_size,8732,21 -> batch_size,8732
        # --------------------------------------------- #
        conf_loss = self._softmax_loss(y_true[:, :, 4:-1],
                                       y_pred[:, :, 4:])
        # --------------------------------------------- #
        #   框的位置的loss
        #   batch_size,8732,4 -> batch_size,8732
        # --------------------------------------------- #
        loc_loss = self._l1_smooth_loss(y_true[:, :, :4],
                                        y_pred[:, :, :4])
                                        
        # --------------------------------------------- #
        #   获取所有的正标签的loss
        # --------------------------------------------- #
        pos_loc_loss = tf.reduce_sum(loc_loss * y_true[:, :, -1],
                                     axis=1)
        pos_conf_loss = tf.reduce_sum(conf_loss * y_true[:, :, -1],
                                      axis=1)

        # --------------------------------------------- #
        #   每一张图的正样本的个数
        #   batch_size,
        # --------------------------------------------- #
        num_pos = tf.reduce_sum(y_true[:, :, -1], axis=-1)

        # --------------------------------------------- #
        #   每一张图的负样本的个数
        #   batch_size,
        # --------------------------------------------- #
        num_neg = tf.minimum(self.neg_pos_ratio * num_pos, num_boxes - num_pos)
        # 找到了哪些值是大于0的
        pos_num_neg_mask = tf.greater(num_neg, 0)
        # --------------------------------------------- #
        #   如果所有的图,正样本的数量均为0
        #   那么则默认选取100个先验框作为负样本
        # --------------------------------------------- #
        has_min = tf.to_float(tf.reduce_any(pos_num_neg_mask))
        num_neg = tf.concat(axis=0, values=[num_neg, [(1 - has_min) * self.negatives_for_hard]])
        
        # --------------------------------------------- #
        #   从这里往后,与视频中看到的代码有些许不同。
        #   由于以前的负样本选取方式存在一些问题,
        #   我对该部分代码进行重构。
        #   求整个batch应该的负样本数量总和
        # --------------------------------------------- #
        num_neg_batch = tf.reduce_sum(tf.boolean_mask(num_neg, tf.greater(num_neg, 0)))
        num_neg_batch = tf.to_int32(num_neg_batch)

        # --------------------------------------------- #
        #   对预测结果进行判断,如果该先验框没有包含物体
        #   那么它的不属于背景的预测概率过大的话
        #   就是难分类样本
        # --------------------------------------------- #
        confs_start = 4 + self.background_label_id + 1
        confs_end   = confs_start + self.num_classes - 1

        # --------------------------------------------- #
        #   batch_size,8732
        #   把不是背景的概率求和,求和后的概率越大
        #   代表越难分类。
        # --------------------------------------------- #
        max_confs = tf.reduce_sum(y_pred[:, :, confs_start:confs_end], axis=2)

        # --------------------------------------------------- #
        #   只有没有包含物体的先验框才得到保留
        #   我们在整个batch里面选取最难分类的num_neg_batch个
        #   先验框作为负样本。
        # --------------------------------------------------- #
        max_confs   = tf.reshape(max_confs * (1 - y_true[:, :, -1]), [-1])
        _, indices  = tf.nn.top_k(max_confs, k=num_neg_batch)

        neg_conf_loss = tf.gather(tf.reshape(conf_loss, [-1]), indices)

        # 进行归一化
        num_pos     = tf.where(tf.not_equal(num_pos, 0), num_pos, tf.ones_like(num_pos))
        total_loss  = tf.reduce_sum(pos_conf_loss) + tf.reduce_sum(neg_conf_loss) + tf.reduce_sum(self.alpha * pos_loc_loss)
        total_loss /= tf.reduce_sum(num_pos)
        return total_loss

训练自己的M2Det模型

首先前往Github下载对应的仓库,下载完后利用解压软件解压,之后用编程软件打开文件夹。
 

注意打开的根目录必须正确,否则相对目录不正确的情况下,代码将无法运行。

一定要注意打开后的根目录是文件存放的目录。

Keras搭建M2Det目标检测平台示例_第13张图片

一、数据集的准备

本文使用VOC格式进行训练,训练前需要自己制作好数据集,如果没有自己的数据集,可以通过Github连接下载VOC12+07的数据集尝试下。
训练前将标签文件放在VOCdevkit文件夹下的VOC2007文件夹下的Annotation中。

Keras搭建M2Det目标检测平台示例_第14张图片


训练前将图片文件放在VOCdevkit文件夹下的VOC2007文件夹下的JPEGImages中。

Keras搭建M2Det目标检测平台示例_第15张图片


此时数据集的摆放已经结束。

二、数据集的处理

在完成数据集的摆放之后,我们需要对数据集进行下一步的处理,目的是获得训练用的2007_train.txt以及2007_val.txt,需要用到根目录下的voc_annotation.py。

voc_annotation.py里面有一些参数需要设置。

分别是annotation_mode、classes_path、trainval_percent、train_percent、VOCdevkit_path,第一次训练可以仅修改classes_path

'''
annotation_mode用于指定该文件运行时计算的内容
annotation_mode为0代表整个标签处理过程,包括获得VOCdevkit/VOC2007/ImageSets里面的txt以及训练用的2007_train.txt、2007_val.txt
annotation_mode为1代表获得VOCdevkit/VOC2007/ImageSets里面的txt
annotation_mode为2代表获得训练用的2007_train.txt、2007_val.txt
'''
annotation_mode     = 0
'''
必须要修改,用于生成2007_train.txt、2007_val.txt的目标信息
与训练和预测所用的classes_path一致即可
如果生成的2007_train.txt里面没有目标信息
那么就是因为classes没有设定正确
仅在annotation_mode为0和2的时候有效
'''
classes_path        = 'model_data/voc_classes.txt'
'''
trainval_percent用于指定(训练集+验证集)与测试集的比例,默认情况下 (训练集+验证集):测试集 = 9:1
train_percent用于指定(训练集+验证集)中训练集与验证集的比例,默认情况下 训练集:验证集 = 9:1
仅在annotation_mode为0和1的时候有效
'''
trainval_percent    = 0.9
train_percent       = 0.9
'''
指向VOC数据集所在的文件夹
默认指向根目录下的VOC数据集
'''
VOCdevkit_path  = 'VOCdevkit'

classes_path用于指向检测类别所对应的txt,以voc数据集为例,我们用的txt为:

Keras搭建M2Det目标检测平台示例_第16张图片

训练自己的数据集时,可以自己建立一个cls_classes.txt,里面写自己所需要区分的类别。

三、开始网络训练

通过voc_annotation.py我们已经生成了2007_train.txt以及2007_val.txt,此时我们可以开始训练了。
 

训练的参数较多,大家可以在下载库后仔细看注释,其中最重要的部分依然是train.py里的classes_path。

classes_path用于指向检测类别所对应的txt,这个txt和voc_annotation.py里面的txt一样!训练自己的数据集必须要修改!

Keras搭建M2Det目标检测平台示例_第17张图片


修改完classes_path后就可以运行train.py开始训练了,在训练多个epoch后,权值会生成在logs文件夹中。

其它参数的作用如下:

#--------------------------------------------------------#
#   训练前一定要修改classes_path,使其对应自己的数据集
#--------------------------------------------------------#
classes_path    = 'model_data/voc_classes.txt'
#----------------------------------------------------------------------------------------------------------------------------#
#   权值文件请看README,百度网盘下载。数据的预训练权重对不同数据集是通用的,因为特征是通用的。
#   预训练权重对于99%的情况都必须要用,不用的话权值太过随机,特征提取效果不明显,网络训练的结果也不会好。
#   训练自己的数据集时提示维度不匹配正常,预测的东西都不一样了自然维度不匹配
#
#   如果想要断点续练就将model_path设置成logs文件夹下已经训练的权值文件。 
#   当model_path = ''的时候不加载整个模型的权值。
#
#   此处使用的是整个模型的权重,因此是在train.py进行加载的。
#   如果想要让模型从主干的预训练权值开始训练,则设置model_path为主干网络的权值,此时仅加载主干。
#   如果想要让模型从0开始训练,则设置model_path = '',Freeze_Train = Fasle,此时从0开始训练,且没有冻结主干的过程。
#   一般来讲,从0开始训练效果会很差,因为权值太过随机,特征提取效果不明显。
#----------------------------------------------------------------------------------------------------------------------------#
model_path      = 'model_data/M2det_weights.h5'
#------------------------------------------------------#
#   输入的shape大小,32的倍数
#------------------------------------------------------#
input_shape     = [320, 320]
#----------------------------------------------------#
#   可用于设定先验框的大小,默认的anchors_size
#   是根据voc数据集设定的,大多数情况下都是通用的!
#   如果想要检测小物体,可以修改anchors_size
#   一般调小浅层先验框的大小就行了!因为浅层负责小物体检测!
#   比如anchors_size = [21, 45, 99, 153, 207, 261, 315]
#----------------------------------------------------#
anchors_size    = [26, 48, 106, 163, 221, 278, 336]

#----------------------------------------------------#
#   训练分为两个阶段,分别是冻结阶段和解冻阶段。
#   显存不足与数据集大小无关,提示显存不足请调小batch_size。
#   受到BatchNorm层影响,batch_size最小为2,不能为1。
#----------------------------------------------------#
#----------------------------------------------------#
#   冻结阶段训练参数
#   此时模型的主干被冻结了,特征提取网络不发生改变
#   占用的显存较小,仅对网络进行微调
#----------------------------------------------------#
Init_Epoch          = 0
Freeze_Epoch        = 50
Freeze_batch_size   = 8
Freeze_lr           = 5e-4
#----------------------------------------------------#
#   解冻阶段训练参数
#   此时模型的主干不被冻结了,特征提取网络会发生改变
#   占用的显存较大,网络所有的参数都会发生改变
#----------------------------------------------------#
UnFreeze_Epoch      = 100
Unfreeze_batch_size = 4
Unfreeze_lr         = 1e-4
#------------------------------------------------------#
#   是否进行冻结训练,默认先冻结主干训练后解冻训练。
#------------------------------------------------------#
Freeze_Train        = True
#------------------------------------------------------#
#   用于设置是否使用多线程读取数据,0代表关闭多线程
#   开启后会加快数据读取速度,但是会占用更多内存
#   keras里开启多线程有些时候速度反而慢了许多
#   在IO为瓶颈的时候再开启多线程,即GPU运算速度远大于读取图片的速度。
#------------------------------------------------------#
num_workers         = 0
#----------------------------------------------------#
#   获得图片路径和标签
#----------------------------------------------------#
train_annotation_path   = '2007_train.txt'
val_annotation_path     = '2007_val.txt'

四、训练结果预测

训练结果预测需要用到两个文件,分别是yolo.py和predict.py。

我们首先需要去yolo.py里面修改model_path以及classes_path,这两个参数必须要修改。

model_path指向训练好的权值文件,在logs文件夹里。
 

classes_path指向检测类别所对应的txt。

Keras搭建M2Det目标检测平台示例_第18张图片


完成修改后就可以运行predict.py进行检测了。运行后输入图片路径即可检测。

以上就是Keras搭建M2Det目标检测平台示例的详细内容,更多关于Keras M2Det目标检测的资料请关注脚本之家其它相关文章!

你可能感兴趣的:(Keras搭建M2Det目标检测平台示例)