CoreML尝鲜:将自己训练的 caffe 模型移植到 IOS 上

导语 : 自从苹果6月5日在WWDC 2017上show出自己在计算机视觉和AI领域又一重磅新科技——CoreML后,我们真是喜忧参半,喜的是Core ML等SDK技术的出现加速了深度学习在移动端的落地,忧的是对于正在研究CNN模型小型化与加速的我们来说,如果苹果的SDK做的足够好,那么还需要我们做什么呢?『思考』『思考』

俗话说的好,知己知彼,百战不殆。为了找出对手的优缺点,研究对手的技术核心,在WWDC召开近20多天后,终于有时间静下心来研究一下苹果这个机器学习SDK——CoreML。

CoreML的官网主页如下:https://developer.apple.com/machine-learning/

主页上对CoreML的核心定位是:CoreML能够方便地将机器学习模型移植到移动端APP中,即下图所示:

CoreML有其自定义的模型类型.mlmodel,并且支持目前几种主流模型到mlmodel的转换,包括Caffe、Keras 1.2.2+、scikit-learn等。 苹果在主页给出了几种现成的mlmodel,包括Resnet-50、GoogLeNet、Inception-v3和VGG-16四种。网上关于直接利用这几种模型进行图像分类的参考例程已经很多了,所以这里主要讲一下如何转换自己的训练模型并进行应用的参考过程。

一、软件准备

由于CoreML目前仅支持iOS11和Xcode9,因此需要先对移动设备升级到iOS11,并且下载Xcode9 beta版本。 戳这里下载 xcode9 beta http://www.cnblogs.com/daxueba-ITdaren/p/6955925.html

二、下载转换工具

苹果提供了开源的转换工具CoreML Tools,https://pypi.python.org/pypi/coremltools 在安装coremltools前,需要安装基本依赖包(numpy (1.12.1+),protobuf (3.1.0+)),如果需要转换Keras、Xgboost、scikit-learn、libSVM等,还需安装对应的依赖包(Keras (1.2.2+, 2.0.4+) with Tensorflow (1.0.x, 1.1.x)、Xgboost (0.6+)、scikit-learn (0.15+)、libSVM)等 使用pip可以很方便地安装依赖包和coremltools pip install -U coremltools 安装完成后就可以使用了。

三、转换模型

这里以caffe模型SqueezeNet v1.1为例(浏览这里https://github.com/DeepScale/SqueezeNet 下载caffemodel) 3.1进入python命令行 3.2引入coremltools


>>> import coremltools

3.3调用转换函数进行模型转换

>>> model = coremltools.converters.caffe.convert(('/Users/xxx/cnn-project/coreml-model/squeezeNet/squeezeNet_v1.1.caffemodel',
'/Users/xxx/cnn-project/coreml-model/squeezeNet/deploy.prototxt',
'/Users/xxx/cnn-project/coreml-model/squeezeNet/mean.binaryproto'),
image_input_names='data',is_bgr=True,class_labels = '/Users/xxx/cnn-project/coreml-model/squeezeNet/imagent1000-labels.txt' )

针对以上调用参数,有几点进行说明: 如果打开coremltools安装目录下的源码site-packages/coremltools/converters/caffe/_caffe_converter.py,可以看到convert的函数的定义:


def convert(model, image_input_names=[], is_bgr=False,
            red_bias=0.0, blue_bias=0.0, green_bias=0.0, gray_bias=0.0,
            image_scale=1.0, class_labels=None, predicted_feature_name=None):

其中, model: 即原始caffe 模型,是一个元组,可以有三种形式: i ‘squeezeNetv1.1.caffemodel’ 只包含原始模型 ii (‘squeezeNetv1.1.caffemodel’, ‘deploy.prototxt’)包含原始模型与模型结构prototxt文件 iii (‘squeezeNetv1.1.caffemodel’, ‘deploy.prototxt’,’mean.binaryproto’)包含原始模型、prototxt文件和均值文件binaryproto 一般需要同时提供模型和prototxt文件,否则转换程序无法找到输入的维度定义。同时,一般在prototxt文件开头会指定输入的名称和维度,如下形式: input: “my_image_input” input_dim: 1 input_dim: 3 input_dim: 227 input_dim: 227 这与caffe默认的deploy形式是一致的,所以我们无需再做任何修改。 对于需要做均值减除操作的模型,需要同时提供均值文件。需要注意的是,对于三通道彩色图像,均值文件需与输入图像通道顺序一致。 剩下的参数根据模型自身特点进行设置: image_input_names: 这个参数可以不用设置,但如果采用上述形式的deploy.prototxt,转换后的模型经Xcode解析后,会将输入解析成MLMultiArray<>形式,对于输入UIimage的话还需要进行转换,不够灵活方便,因此强烈建议对该参数进行设置,而设置也很简单,只要将其设为deploy.prototxt输入层的名称即可,如我的prototxt中输入名为data,则令image_input_names=’data’即可。设置此项参数后,转换后的模型经Xcode解析,输入就变成了Image<>类型,可以方便地与UIimage进行转换。 is_bgr: 这个参数很直观,也很重要,用于标明输入彩色图像的顺序。通常情况下,caffe模型由于采用opencv做为读取图像的接口,因此,输入的图像均为BGR顺序,因此需要将此参数设置为true。 class_labels: 这个参数和predicted_feature_name参数一样,都是为分类模型设计的,对图像分类很有用。class_labels允许开发者提供一个包含所有类名的文件,每类一行,用以将分类预测的结果映射到类名中,从而可以直接输出human readable的直观分类结果。如果设置了该项参数,模型经过Xcode解析后,输出就包含了两部分,如下

原本网络输出N维softmax概率值,这里被进一步加工成top1对应的classLabel和由每一类及其概率组成的字典型结构。 而相比之下,如果不设置该参数,则输出即被解析为数组形式,需要开发者自己完成后续计算和类别映射:

predicted_feature_name: 用于对Core ML模型输出类别名称进行重命名,默认为上面图中的“classLabel”,开发者根据自身喜好和方便设置即可。

剩下几项参数由于没有用到,故不再赘述,大家可参考函数定义,苹果写的还是很详细的。 3.4保存转换模型


>>> model.save('squeezeNet.mlmodel')

至此模型就转换完毕了。

四、将模型应用到app中

4.1 打开Xcode 9 beta ,新建一个Xcode工程,语言我选择的是Objective-C 4.2 将第三步生成好的模型放在工程目录下,同时,将模型拖入到左侧工程导航栏中。 点击该模型,会出现相关信息,如下图

可以看到模型的输入和输出定义。这里我的模型输入是Image 类型,即大小为227x227的BGR三通道图像,输出则是包含Dictionary和String的结构体,对应每个类的类名和其概率,以及top1的类名。模型对应的接口文件可以在Model Class中名称右侧的小箭头点击查看。 这里出现了一个小插曲。正常情况下,将mlmodel拖入工程后,Xcode会自动解析并生成对应的接口文件,但是最初我的模型接口文件一直无法生成,谷歌后发现,不知道是Xcode9的Bug还是设置问题,拖入到工程中的文件,还需手动勾选target membership,在界面右侧导航栏,勾选后就能生成对应的接口文件了。 4.3 编写处理接口 在生成的对应接口文件中,可以了解对应的模型类名称和接口函数

- (void) predictImageScene:(UIImage *)image {  //主处理函数
    squeezeNet *model = [[squeezeNet alloc] init];//首先定义并初始化模型
    NSError *error; //定义返回错误
    UIImage *scaledImage = [image scaleToSize:CGSizeMake(259, 259)]; //将输入图像scale到259*259
    UIImage *cropImage = [scaledImage cropToSize:CGSizeMake(227, 227)] ; //crop图像得到227*227,此即模型输入大小
    CVPixelBufferRef buffer = [image pixelBufferFromCGImage:cropImage]; //将uiimage转到CVPixelBufferRef
    squeezeNetOutput* output = [model predictionFromData:buffer error:&error]; //前向计算得到输出
    if (error != nil) {

            NSLog(@"Error is %@", error.localizedDescription);
            return ;
    }


     NSString *label = output.classLabel ;
     NSLog(@"label:%@", label); //输出top1类名

}

- (UIImage *)scaleToSize:(CGSize)size {
    UIGraphicsBeginImageContext(size);
    [self drawInRect:CGRectMake(0, 0, size.width, size.height)];
    UIImage* scaledImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return scaledImage;
}

- (UIImage *)cropToSize:(CGSize)size{
    if(size.width > self.size.width || size.height > self.size.height)
    {
        NSLog(@"crop size must be smaller than original size");
        return self ;
    }
    CGRect cropRect = CGRectMake((self.size.width-size.width)/2, (self.size.height-size.height)/2, size.width, size.height) ;
    CGImageRef imageRef = CGImageCreateWithImageInRect([self CGImage], cropRect);

    UIImage *cropImage = [UIImage imageWithCGImage:imageRef];
    CGImageRelease(imageRef);

    return cropImage;
}
- (CVPixelBufferRef)pixelBufferFromCGImage:(UIImage *)originImage  {
    CGImageRef image = originImage.CGImage;
    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                             [NSNumber numberWithBool:YES], kCVPixelBufferCGImageCompatibilityKey,
                             [NSNumber numberWithBool:YES], kCVPixelBufferCGBitmapContextCompatibilityKey,
                             nil];

    CVPixelBufferRef pxbuffer = NULL;

    CGFloat frameWidth = CGImageGetWidth(image);
    CGFloat frameHeight = CGImageGetHeight(image);


    CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault,
                                          frameWidth,
                                          frameHeight,
                                          kCVPixelFormatType_32ARGB,
                                          (__bridge CFDictionaryRef) options,
                                          &pxbuffer);

    NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);

    CVPixelBufferLockBaseAddress(pxbuffer, 0);
    void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);
    NSParameterAssert(pxdata != NULL);

    CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();

    CGContextRef context = CGBitmapContextCreate(pxdata,
                                                 frameWidth,
                                                 frameHeight,
                                                 8,
                                                 CVPixelBufferGetBytesPerRow(pxbuffer),
                                                 rgbColorSpace,
                                                 (CGBitmapInfo)kCGImageAlphaNoneSkipFirst);
    NSParameterAssert(context);
    CGContextConcatCTM(context, CGAffineTransformIdentity);
    CGContextDrawImage(context, CGRectMake(0,
                                           0,
                                           frameWidth,
                                           frameHeight),
                       image);
    CGColorSpaceRelease(rgbColorSpace);
    CGContextRelease(context);

    CVPixelBufferUnlockBaseAddress(pxbuffer, 0);

    return pxbuffer;
}

CoreML能够为每个模型生成对应的接口函数,极大程度的减少了调用成本,除去图像处理步骤,核心代码只有两句: 定义模型:squeezeNet model = [[squeezeNet alloc] init]; 前向计算:squeezeNetOutput output = [model predictionFromData:buffer error:&error]; 是不是很意外?很惊喜?如此一来调用起来简直快捷方便,为开发者提供了极大的便利。

体验下来发现,CoreML精度基本与原始caffemodel无损,速度由于目前只在iphone5s上进行了测试,squeezeNet模型处理耗时约120ms,可以大概确定的是,苹果内部应该没有对模型参数进行量化等操作,主要应该还是只对原始浮点型运算进行了相应的硬件加速,正在研究如何设置开启多核和使用GPU,但仅若是单核CPU,此处理速度也算不上是特别惊艳,也许苹果还有所保留,估计会逐步开放提升其前向运算能力。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏醉程序

DES算法中子密钥的产生

1083
来自专栏逍遥剑客的游戏开发

球面环境映射实现高光效果

1212
来自专栏Y大宽

金黄葡萄球菌RNA-seq数据分析

这里出现问题了,突变株的比对率太低,不到1%,这是不可能的,怀疑样品污染,然后随机挑选了5条序列blast了下,发现应该是被溶血葡萄球菌污染。

802
来自专栏我杨某人的青春满是悔恨

Learning From Data Note 1

周末上了一节 Yaser 的网上公开课,教授的发音虽然有点奇怪,但是为人风趣,循循善诱,课程内容也是深入浅出,既有干货又不至于太过枯燥乏味。看完之后记了一点笔记...

692
来自专栏计算机视觉与深度学习基础

Leetcode 5 Longest Palindromic Substring

Given a string S, find the longest palindromic substring in S. You may assume ...

1605
来自专栏本立2道生

Matlab图像处理常用基本函数

之前用Matlab做图像处理工作时,用到什么函数就查什么函数,从没做过系统的总结,再做的时候又要去查,所以总结还是有必要的~

612
来自专栏CreateAMind

(Keras)基于DDPG用300行Python代码玩转TORCS(开放赛车模拟器)-教程及代码

视频地址 http://weibo.com/3164120327/EcF8g6jdw

1563
来自专栏用户2442861的专栏

FAST Algorithm for Corner Detection

We saw several feature detectors and many of them are really good. But when look...

551
来自专栏本立2道生

伪随机数生成算法

伪随机数生成算法在计算机科学领域应用广泛,比如枪击游戏里子弹命中扰动、数据科学里对样本进行随机采样、密码设计、仿真领域等等,背后都会用到伪随机数生成算法。

22912
来自专栏PPV课数据科学社区

用R进行文本挖掘与分析:分词、画词云

要分析文本内容,最常见的分析方法是提取文本中的词语,并统计频率。频率能反映词语在文本中的重要性,一般越重要的词语,在文本中出现的次数就会越多。词语提取后,还可以...

3414

扫码关注云+社区