CoreML是苹果在iOS11上新推出的机器学习SDK库。
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
这里出现了一个小插曲。正常情况下,将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,此处理速度也算不上是特别惊艳,也许苹果还有所保留,估计会逐步开放提升其前向运算能力。
本文代码和模型在 https://github.com/may0324/CoreMLDemo