天天看点

CoreML 的 C++部署 [2] 模型类抽象模型类的抽象

接上一篇

CoreML 的 C++部署 [1] 模型转换和预处理

再解决了预处理的问题后,部署部署还剩下模型类的抽象,主要包括初始化、推理以及获取输出。

模型类的抽象

什么是模型类?可以参考:

CoreML模型分析

我们是以MobileNetV2.mlmodel为例说明了mlmodel的结构。这里有一个预备知识,模型头文件中的类仅仅是对底层MLModel和MLFeatureProvider的封装。也就是说,每一个mlmodel,真正的实现是通过MLModel和MLFeatureProvider进行的,上层做什么封装都是可以的。

说的简单一点,我们可以把MobileNetV2换成任意名称,只要保持对MLModel和MLFeatureProvider的封装,都可以完成MLModel的初始化、推理以及获取输出。

比如我们取个名字叫 MyModel

@interface MyModel : NSObject
           

刚才已经说了,最重要的就是MLModel,所以我们也需要一个MLModel对象:

初始化,我们需要传入mlmodel的路径,以及输入输出的tensor的名称:

@param modelAssetPath 路径
@param outputfeatureNames 输出节点名称
@param inputFeatureName 输入节点名称
- (nullable instancetype)initWithModelAssetPath:(NSString *)modelAssetPath withOutputFeatureNames:(NSSet<NSString *> *)outputfeatureNames withInputFeatureName:(NSString *)inputFeatureName;
           

为什么我们要用NSSet<NSString *> * 类型的输出?先留个疑问,后面再解释。

有了初始化之后,我们就可以进行推理:

需要注意的是,我们一般只能拿到一个CVPixelBufferRef对象,而模型输入是MyModelInput *,所以还需要一个转换函数,输入CVPixelBufferRef对象,调用MyModelInput的初始化函数,然后再调用上面的predictionFromFeatures:

这样做的好处是,MyModelInput对外层是不可见的。

同样,我们也不希望直接操作MyModelOutput,因为对应用开发者来说,直接操作数组会简单的多,所以我们还需要一个从MyModelOutput转换为浮点数组的函数:

@interface MyModelOutput()
{
    NSSet<NSString *> *_featureNames;
}
@end

- (nullable MLMultiArray *)multiArrayForName:(NSString *)featureName {
    NSEnumerator *enumerator = [_featureNames objectEnumerator];
    NSString *object  = [enumerator nextObject];
    int index = 0;
    while (object != nil) {
        if ([featureName isEqualToString:object]) {
            return _mbox[index];
        }
        index++;
        object = [enumerator nextObject];
    }
    return nil;
}
           

到这里我们可以回答刚才的问题,为什么输出是NSSet<NSString *> *格式,为什么要用Set这种无序的结构来存储?

Core ML默认是支持多输出的。最简单的方式,我们将输出的featureNames保存成Vector,有序向量,然后输出也是保存成Vector<NSString >,理论上来说是完全可行的,而且输出的顺序与我们传入的featureNames一致,听起来是更方便的一种实现。但是可惜的是,OC是没有Vector结构的,所以只能用Set来保存,不过由于NSSet的底层是通过hash表实现,查询是O(1)的,所以通过Set来保存也没有什么问题。

关于输出还有一个最重要的提示,Core ML V1 版本的模型,输出的feature默认都是double浮点数,而到了Core ML

V4,默认都是float浮点数。而且V4的模型是只能在iOS 13及以上机型运行。关于模型转换可以参考coremltools。

通过multiArrayForName函数,我们就可以得到每一个输出节点的数据指针,就可以做后处理并得到最终结果了。

完整的代码:

MyModel:

@interface MyModel : NSObject
@property (readonly, nonatomic, nullable) MLModel * model;

@param modelAssetPath 路径
@param outputfeatureNames 输出节点名称
@param inputFeatureName 输入节点名称
- (nullable instancetype)initWithModelAssetPath:(NSString *)modelAssetPath withOutputFeatureNames:(NSSet<NSString *> *)outputfeatureNames withInputFeatureName:(NSString *)inputFeatureName;

@param input 经过初始化后的MyModelInput对象
- (nullable MyModelOutput *)predictionFromFeatures:(MyModelInput *)input error:(NSError * _Nullable __autoreleasing * _Nullable)error;

@param data CVPixelBufferRef图像
- (nullable MyModelOutput *)predictionFromData:(CVPixelBufferRef)data error:(NSError * _Nullable * _Nullable)error;
           

MyModelOutput :

@interface MyModelOutput()
{
    NSSet<NSString *> *_featureNames;
}
@end

- (nullable MLMultiArray *)multiArrayForName:(NSString *)featureName {
    NSEnumerator *enumerator = [_featureNames objectEnumerator];
    NSString *object  = [enumerator nextObject];
    int index = 0;
    while (object != nil) {
        if ([featureName isEqualToString:object]) {
            return _mbox[index];
        }
        index++;
        object = [enumerator nextObject];
    }
    return nil;
}
           

继续阅读