深度学习算法优化系列四 | 如何使用OpenVINO部署以Mobilenet做Backbone的YOLOv3模型?

前言

因为最近在和计算棒打交道,自然存在一个模型转换问题,如果说YOLOv3或者YOLOV3-tiny怎么进一步压缩,我想大多数人都会想到将标准卷积改为深度可分离卷积结构?而当前很多人都是基于DarkNet框架训练目标检测模型,并且github也有开源一个Darknet转到OpenVINO推理框架的工具,地址见附录。而要说明的是,github上的开源工具只是支持了原生的YOLOv3和YOLOV3-tiny模型转到tensorflow的pb模型,然后再由pb模型转换到IR模型执行在神经棒的推理。因此,我写了一个脚本可以将带深度可分离卷积的YOLOv3或YOLOV3-tiny转换到pb模型并转换到IR模型,且测试无误。就奉献一下啦。

项目配置

  • Tensorflow 1.8.0
  • python3

工具搭建

此工具基于github上mystic123darknet模型转pb模型的工具tensorflow-yolo-v3,具体见附录。我这里以修改一下YOLOV3-tiny里面的有1024个通道的标准卷积为深度可分离卷积为例来介绍。下图是YOLOv3-tiny的网络结构,我们考虑如何把1024个通道的标准卷积改造成深度可分离卷积的形式即可。其他卷积类似操作即可。

在这里插入图片描述

  • 步骤一:修改YOLOv3-tiny的cfg文件,1024个输出通道的卷积层输入通道数512,卷积核尺寸为3x3,因此对应到深度可分离卷积的结构就是[512,512,3,3]的分组卷积核[512,1024,1,1]的点卷积(也是标准的1x1)卷积。所以我们将1024个输出通道的卷积层替换为这两个层即可,这里使用AlexAB版本的Darknet进行训练,链接也在附录,注意要使用groups分组卷积这个参数,需要用cudnn7以上的版本编译DarkNet。然后我们修改cfg文件夹下面的yolov3-tiny.cfg,把其中的1024通道的卷积换成深度可分离卷积,如下图所示。注意是groups而不是group
在这里插入图片描述
  • 步骤二:训练好模型,并使用DarkNet测试一下模型是否表现正常。
  • 步骤三:克隆tensorflow-yolo-v3工程,链接见附录。
  • 步骤四:用我的工具转换训练出来的darknet模型到tensorflowpb模型,这一步骤的具体操作为用下面我提供的脚本替换一下tensorflow-yolo-v3工程中的yolov3-tiny.py即可,注意是全部替换。我的脚本具体代码如下:
# -*- coding: utf-8 -*-

import numpy as np
import tensorflow as tf
from yolo_v3 import _conv2d_fixed_padding, _fixed_padding, _get_size, \
    _detection_layer, _upsample

slim = tf.contrib.slim

_BATCH_NORM_DECAY = 0.9
_BATCH_NORM_EPSILON = 1e-05
_LEAKY_RELU = 0.1

_ANCHORS = [(10, 14),  (23, 27),  (37, 58),
            (81, 82),  (135, 169),  (344, 319)]


def yolo_v3_tiny(inputs, num_classes, is_training=False, data_format='NCHW', reuse=False):
    """
    Creates YOLO v3 tiny model.
    :param inputs: a 4-D tensor of size [batch_size, height, width, channels].
        Dimension batch_size may be undefined. The channel order is RGB.
    :param num_classes: number of predicted classes.
    :param is_training: whether is training or not.
    :param data_format: data format NCHW or NHWC.
    :param reuse: whether or not the network and its variables should be reused.
    :return:
    """
    # it will be needed later on
    img_size = inputs.get_shape().as_list()[1:3]

    # transpose the inputs to NCHW
    if data_format == 'NCHW':
        inputs = tf.transpose(inputs, [0, 3, 1, 2])

    # normalize values to range [0..1]
    inputs = inputs / 255

    # set batch norm params
    batch_norm_params = {
        'decay': _BATCH_NORM_DECAY,
        'epsilon': _BATCH_NORM_EPSILON,
        'scale': True,
        'is_training': is_training,
        'fused': None,  # Use fused batch norm if possible.
    }

    with tf.variable_scope('yolo-v3-tiny'):
        for i in range(6):
            inputs = slim.conv2d(inputs, 16 * pow(2, i), 3, 1, padding='SAME', biases_initializer=None,
                                     activation_fn=lambda x: tf.nn.leaky_relu(x, alpha=_LEAKY_RELU),
                                     normalizer_fn=slim.batch_norm, normalizer_params=batch_norm_params)

            if i == 4:
                route_1 = inputs

            if i == 5:
                inputs = slim.max_pool2d(
                    inputs, [2, 2], stride=1, padding="SAME", scope='pool2')
            else:
                inputs = slim.max_pool2d(
                    inputs, [2, 2], scope='pool2')

        # inputs = _conv2d_fixed_padding(inputs, 1024, 3)
        inputs = slim.separable_conv2d(inputs, num_outputs=None, kernel_size=3, depth_multiplier=1, stride=1, biases_initializer=None,
                                               activation_fn=lambda x: tf.nn.leaky_relu(x, alpha=_LEAKY_RELU),
                                               normalizer_fn=slim.batch_norm, normalizer_params=batch_norm_params,
                                               padding='SAME')

        inputs = slim.conv2d(inputs, 1024, 1, 1, biases_initializer=None,
                             activation_fn=lambda x: tf.nn.leaky_relu(x, alpha=_LEAKY_RELU),
                             normalizer_fn=slim.batch_norm, normalizer_params=batch_norm_params, padding='VALID')

        inputs = slim.conv2d(inputs, 256, 1, 1, padding='SAME', biases_initializer=None,
                             activation_fn=lambda x: tf.nn.leaky_relu(x, alpha=_LEAKY_RELU),
                             normalizer_fn=slim.batch_norm, normalizer_params=batch_norm_params)
        route_2 = inputs

        inputs = slim.conv2d(inputs, 512, 3, 1, padding='SAME', biases_initializer=None,
                             activation_fn=lambda x: tf.nn.leaky_relu(x, alpha=_LEAKY_RELU),
                             normalizer_fn=slim.batch_norm, normalizer_params=batch_norm_params)
        # inputs = _conv2d_fixed_padding(inputs, 255, 1)

        detect_1 = _detection_layer(
            inputs, num_classes, _ANCHORS[3:6], img_size, data_format)
        detect_1 = tf.identity(detect_1, name='detect_1')

        inputs = slim.conv2d(route_2, 128, 1, 1, padding='SAME', biases_initializer=None,
                             activation_fn=lambda x: tf.nn.leaky_relu(x, alpha=_LEAKY_RELU),
                             normalizer_fn=slim.batch_norm, normalizer_params=batch_norm_params)
        upsample_size = route_1.get_shape().as_list()
        inputs = _upsample(inputs, upsample_size, data_format)

        inputs = tf.concat([inputs, route_1],
                           axis=1 if data_format == 'NCHW' else 3)

        inputs = slim.conv2d(inputs, 256, 3, 1, padding='SAME', biases_initializer=None,
                             activation_fn=lambda x: tf.nn.leaky_relu(x, alpha=_LEAKY_RELU),
                             normalizer_fn=slim.batch_norm, normalizer_params=batch_norm_params)
        # inputs = _conv2d_fixed_padding(inputs, 255, 1)

        detect_2 = _detection_layer(
            inputs, num_classes, _ANCHORS[0:3], img_size, data_format)
        detect_2 = tf.identity(detect_2, name='detect_2')

        detections = tf.concat([detect_1, detect_2], axis=1)
        detections = tf.identity(detections, name='detections')
        return detections

可以看到我仍然使用了tensorflow的slim模块搭建整个框架,和原始的yolov3-tiny的区别就在:

 # inputs = _conv2d_fixed_padding(inputs, 1024, 3)
inputs = slim.separable_conv2d(inputs, num_outputs=None, kernel_size=3, depth_multiplier=1, stride=1, biases_initializer=None,
                                               activation_fn=lambda x: tf.nn.leaky_relu(x, alpha=_LEAKY_RELU),
                                               normalizer_fn=slim.batch_norm, normalizer_params=batch_norm_params,
                                               padding='SAME')

inputs = slim.conv2d(inputs, 1024, 1, 1, biases_initializer=None,
                             activation_fn=lambda x: tf.nn.leaky_relu(x, alpha=_LEAKY_RELU),
                             normalizer_fn=slim.batch_norm, normalizer_params=batch_norm_params, padding='VALID')

需要进一步注意的是slim.separable_conv2d深度可分离卷积的参数传递方式,我们来看一下这个函数的参数列表:

def separable_convolution2d(
    inputs,
    num_outputs,
    kernel_size,
    depth_multiplier=1,
    stride=1,
    padding='SAME',
    data_format=DATA_FORMAT_NHWC,
    rate=1,
    activation_fn=nn.relu,
    normalizer_fn=None,
    normalizer_params=None,
    weights_initializer=initializers.xavier_initializer(),
    pointwise_initializer=None,
    weights_regularizer=None,
    biases_initializer=init_ops.zeros_initializer(),
    biases_regularizer=None,
    reuse=None,
    variables_collections=None,
    outputs_collections=None,
    trainable=True,
    scope=None):
  """一个2维的可分离卷积,可以选择是否增加BN层。
  这个操作首先执行逐通道的卷积(每个通道分别执行卷积),创建一个称为depthwise_weights的变量。如果num_outputs
不为空,它将增加一个pointwise的卷积(混合通道间的信息),创建一个称为pointwise_weights的变量。如果
normalizer_fn为空,它将给结果加上一个偏置,并且创建一个为biases的变量,如果不为空,那么归一化函数将被调用。
最后再调用一个激活函数然后得到最终的结果。
  Args:
    inputs: 一个形状为[batch_size, height, width, channels]的tensor
    num_outputs: pointwise 卷积的卷积核个数,如果为空,将跳过pointwise卷积的步骤.
    kernel_size: 卷积核的尺寸:[kernel_height, kernel_width],如果两个的值相同,则可以为一个整数。
    depth_multiplier: 卷积乘子,即每个输入通道经过卷积后的输出通道数。总共的输出通道数将为:
num_filters_in * depth_multiplier。
    stride:卷积步长,[stride_height, stride_width],如果两个值相同的话,为一个整数值。
    padding:  填充方式,'VALID' 或者 'SAME'.
    data_format:数据格式, `NHWC` (默认) 和 `NCHW` 
    rate: 空洞卷积的膨胀率:[rate_height, rate_width],如果两个值相同的话,可以为整数值。如果这两个值
任意一个大于1,那么stride的值必须为1.     
    activation_fn: 激活函数,默认为ReLU。如果设置为None,将跳过。
    normalizer_fn: 归一化函数,用来替代biase。如果归一化函数不为空,那么biases_initializer
和biases_regularizer将被忽略。 biases将不会被创建。如果设为None,将不会有归一化。
    normalizer_params: 归一化函数的参数。
    weights_initializer: depthwise卷积的权重初始化器
    pointwise_initializer: pointwise卷积的权重初始化器。如果设为None,将使用weights_initializer。
    weights_regularizer: (可选)权重正则化器。
    biases_initializer: 偏置初始化器,如果为None,将跳过偏置。
    biases_regularizer: (可选)偏置正则化器。
    reuse: 网络层和它的变量是否可以被重用,为了重用,网络层的scope必须被提供。
    variables_collections: (可选)所有变量的collection列表,或者是一个关键字为变量值为collection的字典。
    outputs_collections: 输出被添加的collection.
    trainable: 变量是否可以被训练
    scope: (可选)变量的命名空间。
  Returns:
    代表这个操作的输出的一个tensor
  • 步骤四:执行下面的模型转换命令,就可以把带深度可分离卷积的yolov3-tiny模型转到tensorflowpb模型了。
python3 convert_weights_pb.py \
--class_names coco.names \
--weights_file weights/yolov3-tiny.weights \
--data_format NHWC \
--tiny \
--output_graph pbmodels/frozen_tiny_yolo_v3.pb
  • 步骤五:接下来就是把pb模型转为IR模型,在Intel神经棒上进行推理,这一部分之前的推文已经详细说过了,这里就不再赘述了。想详细了解请看之前的推文,地址如下:YOLOv3-tiny在VS2015上使用Openvino部署

测试结果

1024个输出通道的卷积核替换为深度可分离卷积之后,模型从34M压缩到了18M,并且在我的数据集上精度没有显著下降(这个需要自己评判了,因为我的数据自然是没有VOC或者COCO数据集那么复杂的),并且速度也获得了提升。

后记

这个工具可以为大家提供了一个花式将Darknet转换为pb模型的一个BaseLine,DarkNet下面的MobileNet-YOLO自然比Caffe的MobileNet-YOLO更容易获得,因为动手改几个groups参数就可以啦。所以我觉得这件事对于使用DarkNet同时玩一下计算棒的同学是有一点意义的,我把我修改后的工程放在github了,地址见附录。

附录

原始的darknet转pb模型工程:https://github.com/mystic123/tensorflow-yolo-v3

支持深度可分离卷积的darknet转pb模型工程:https://github.com/BBuf/cv_tools

AlexAB版Darknet:https://github.com/AlexeyAB/darknet


欢迎关注我的微信公众号GiantPandaCV,期待和你一起交流机器学习,深度学习,图像算法,优化技术,比赛及日常生活等。


图片.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容