深度學習算法優化系列四 | 如何使用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
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容