使用解码逻辑创建YOLO Core ML对象检测器(四)

目录

介绍

缩小模型

构建YOLO解码器

下一步


总目录 

将ONNX对象检测模型转换为iOS Core ML(一)

在本文中,我们将YOLO v2结果的解码包含到Core ML模型中。

介绍

本系列假定您熟悉PythonCondaONNX,并且具有使用Xcode开发iOS应用程序的经验。我们将使用macOS 10.15 +Xcode 11.7+iOS 13+运行代码。

缩小模型

为了节省iOS设备上的内存而不会对模型的性能造成负面影响,我们应该将其权重从32位精度降低到16位精度。请注意,当模型在iOS设备的GPU或神经引擎上执行时(应该如此),无论如何它总是以16位浮点数运行。只有在CPU上运行时,32位精度才能有所作为。

让我们开始吧:

import os
import coremltools as ct
import numpy as np

model_converted = ct.models.MLModel('./models/yolov2-coco-9.mlmodel')
model_converted = ct.models.neural_network.quantization_utils.quantize_weights(
    model_converted, 
    nbits=16, 
    quantization_mode='linear')
model_converted.save('./models/yolov2-16.mlmodel')

构建YOLO解码器

我们有两个选择:将解码器层添加到现有模型或创建单独的模型,然后使用管道将两者连接。让我们选择后一个选项。

我们将从创建一个新NeuralNetworkBuilder实例并映射新解码器模型的输入和输出开始:

from coremltools.models import datatypes

input_features = [ (spec.description.output[0].name, datatypes.Array(1, 425, 13, 13)) ]
output_features = [ ('all_scores', datatypes.Array(1, 845, 80)),
                    ('all_boxes', datatypes.Array(1, 845, 4)) ]

builder = ct.models.neural_network.NeuralNetworkBuilder(
    input_features, 
    output_features, 
    disable_rank5_shape_mapping=True
)

builder.spec.description.input[0].ParseFromString(spec.description.output[0].SerializeToString())

接下来,我们定义计算所需的常量:

GRID_SIZE = 13
CELL_SIZE = 1 / GRID_SIZE 
BOXES_PER_CELL = 5
NUM_CLASSES = 80

ANCHORS_W = np.array([0.57273, 1.87446, 3.33843, 7.88282, 9.77052]).reshape(1, 1, 5)
ANCHORS_H = np.array([0.677385, 2.06253, 5.47434, 3.52778, 9.16828]).reshape(1, 1, 5)

CX = np.tile(np.arange(GRID_SIZE), GRID_SIZE).reshape(1, 1, GRID_SIZE**2, 1)
CY = np.tile(np.arange(GRID_SIZE), GRID_SIZE).reshape(1, GRID_SIZE, GRID_SIZE).transpose()
CY = CY.reshape(1, 1, GRID_SIZE**2, 1)

注意上面的CELL_SIZE值。要将我们的模型与Vision框架一起使用,我们需要将包围盒坐标从图像像素缩放到[0-1]范围。

要将定义的常数用于计算,我们将它们添加到网络中:

builder.add_load_constant_nd('CX', output_name='CX', constant_value=CX, shape=CX.shape)
builder.add_load_constant_nd('CY', output_name='CY', constant_value=CY, shape=CY.shape)
builder.add_load_constant_nd('ANCHORS_W', output_name='ANCHORS_W', constant_value=ANCHORS_W, shape=ANCHORS_W.shape)
builder.add_load_constant_nd('ANCHORS_H', output_name='ANCHORS_H', constant_value=ANCHORS_H, shape=ANCHORS_H.shape)

现在,我们准备为我们的Core ML模型添加图层。在大多数情况下,这将直接转换上一篇文章中的代码,并尽可能使用相同的变量/节点名称。有时,Core ML怪癖会强制执行一些小的更改。请参阅代码下载以获取完整的解决方案,因为为了提高可读性,此处将不包含一些显而易见的代码序列。

我们从对应于先前(矢量化)实现的前两个转换的层开始:

builder.add_transpose(
    'yolo_trans_node', 
    axes=(0,2,3,1), 
    input_name='218', 
    output_name=‘yolo_transp')

builder.add_reshape_static(
    'yolo_reshap', 
    input_name='yolo_transp',
    output_name='yolo_reshap',
    output_shape=(1, GRID_SIZE**2, BOXES_PER_CELL, NUM_CLASSES + 5)
)

当我们使用NeuralNetworkBuilder实例创建一个新层时,我们需要为该节点及其节点指定一个唯一的名称output_name(分别在上面的第一个操作中为“yolo_trans_node”“yolo_transp”)。该input_name值必须与现有output_name值相对应(在这种情况下为“218”,这是我们转换后的YOLO v2模型的输出)。

要提取编码的框和置信度值,我们需要分割输入数组:

builder.add_split_nd(
    'split_boxes_node', 
    input_name='yolo_reshap',
    output_names=['tx', 'ty', 'tw', 'th', 'tc', 'classes_raw'],    
    axis=3,
    split_sizes=[1, 1, 1, 1, 1, 80])

该操作将raw_preds数组切片为上一篇文章中的txtytwthtcclasses_raw数组。

不幸的是,代码的其余部分将更加冗长,因为每个基本算术运算都需要一个单独的节点。这导致了来自矢量化解码器的简单行的情况:

x = ((CX + sigmoid(tx)) * CELL_SIZE).reshape(-1)

变成:

builder.add_reshape_static('tx:1', input_name='tx', output_name='tx:1', output_shape=(1,169,5))
builder.add_activation('tx:1_sigm', non_linearity='SIGMOID', input_name='tx:1', output_name='tx:1_sigm')
builder.add_add_broadcastable('tx:1_add', input_names=['CX', 'tx:1_sigm'], output_name='tx:1_add')
builder.add_elementwise('x', input_names=['tx:1_add'], output_name='x', mode='MULTIPLY', alpha=CELL_SIZE)

请注意,为了使代码更短、更易读,我们在输出shape参数中使用显式值“169”代替GRID_SIZE**2“5”代替 BOXES_PER_CELL。这同样适用于“80”,而不是其他一些地方的NUM_CLASSES字面量。当然,在适当而灵活的解决方案中,我们应该坚持使用字面量。

计算y需要相同的运算。然后,我们有一个非常相似的代码来计算边界框的宽度(w):

builder.add_reshape_static('tw:1', input_name='tw', output_name='tw:1', output_shape=(1,169,5))
builder.add_unary('tw:1_exp', input_name='tw:1', output_name='tw:1_exp', mode='exp')
builder.add_multiply_broadcastable('tw:1_mul', input_names=['tw:1_exp', 'ANCHORS_W'], output_name='tw:1_mul')
builder.add_elementwise('w', input_names=['tw:1_mul'], output_name='w', mode='MULTIPLY', alpha=CELL_SIZE)

h的后续的计算非常相似(除了使用ANCHORS_H而不是ANCHORS_W常量之外)。

最后,我们解码box_confidenceclasses_confidence值:

builder.add_reshape_static('tc:1', input_name='tc', output_name='tc:1', output_shape=(1,169*5,1))
builder.add_activation('box_confidence', non_linearity='SIGMOID', input_name='tc:1', output_name='box_confidence')
builder.add_reshape_static('classes_raw:1', input_name='classes_raw', output_name='classes_raw:1', output_shape=(1,169*5,80))
builder.add_softmax_nd('classes_confidence', input_name='classes_raw:1', output_name='classes_confidence', axis=-1)

在先前文章中描述的YOLO v2预测解码中,我们为每个框返回了一个最可能的类。Vision框架希望我们为每个盒子返回80个类别中每个类别的置信度:

builder.add_multiply_broadcastable(
    'combined_classes_confidence', 
    input_names=['box_confidence', 'classes_confidence'],
    output_name=‘combined_classes_confidence')

现在,我们拥有了所需的所有值。接下来,让我们将Vision框架的这些值格式化为两个数组:一个数组包含所有边界框的坐标(每个盒子四列),第二个数组具有针对每个盒子/类组合计算的置信度(包含80列)每盒)。

这不是一项艰巨的任务,但是因为我们需要将每个转换作为单独的操作来处理,所以它又导致了冗长的代码:

builder.add_reshape_static('x:1', input_name='x', output_name='x:1', output_shape=(1,169*5,1))
builder.add_reshape_static('y:1', input_name='y', output_name='y:1', output_shape=(1,169*5,1))
builder.add_reshape_static('w:1', input_name='w', output_name='w:1', output_shape=(1,169*5,1))
builder.add_reshape_static('h:1', input_name='h', output_name='h:1', output_shape=(1,169*5,1))

builder.add_stack(
    'all_boxes:0', 
    input_names=['x:1', 'y:1', 'w:1', 'h:1'], 
    output_name='all_boxes:0', 
    axis=2)

builder.add_reshape_static(
    'all_boxes', 
    input_name='all_boxes:0', 
    output_name='all_boxes',
    output_shape=(1,169*5, 4))

builder.add_reshape_static(
    'all_scores', 
    input_name='combined_classes_confidence', 
    output_name='all_scores',
    output_shape=(1,169*5, 80))

格式化了all_scoresall_boxes数组后,我们可以映射这些数组模型的输出和保存模型本身:

builder.set_output(
    output_names= ['all_scores', 'all_boxes'],
    output_dims= [(845,80), (845,4)])

model_decoder = ct.models.MLModel(builder.spec)
model_decoder.save('./models/yolov2-decoder.mlmodel')

下一步

有很多代码,但我们最终做到了。现在,我们有了一个可以解码YOLO v2预测的Core ML模型。但是,没有链接到YOLO的输出,我们将无法使用它。在接下来的文章中,我们将创建一个Core ML管道是我们的终端到终端的模式。

https://www.codeproject.com/Articles/5286802/Creating-a-YOLO-Core-ML-Object-Detector-with-Decod

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值