iOS中的人工智能(Core ML)
前言
大概在一个多月前笔者参加了一场线上“IT技术人成长经验交流会”,实际上这场大会的参加者主要是以iOS技术人员为主。小猿搜题技术负责人唐巧大神也参加了这场大会,给在场所有人做了一场分享。印象最为深刻的一个大神李嘉璇(《TensorFlow技术解析于实战》作者),当时做了一个让在场几乎所有人都懵逼的技术分享,技术分享的主题是和TensorFlow相关的人工智能。参加交流会的多为iOS开发者,接触人工智能的少之又少,所以在场的90%以上的人听得一脸懵也是正常情况。所以今天笔者想简单的总结下所谓的人工智能以及苹果最近推出的Core ML框架。附上上次技术交流会的几张图,巧神、李嘉璇以及我露面的镜头😀。
唐巧大神 李嘉璇大神 露面的我什么是人工智能?
人工智能总体介绍
人工智能简称AI。提及这个词汇的时候,通常大数据、机器学习、神经网络等词汇也会与之一块出来。接下来笔者带领大家一一认识这几个词汇。
所谓的人工智能,实际就是机器依靠数据的内在逻辑自己定义方法,通过机器模拟人类大脑思考过程,进而定义方法。机器学习实际就是实现人工智能的一种方法,而方法的定义是以大数据为依靠。
试想一位儿童心理学家在做一些心理学实验。这个实验大概可以分为三步:
- 1、尽可能收集多的数据,该数据实际就类似大数据
- 2、分析数据。分析数据的过程实际上就类似机器学习的过程。
- 3、得出结论。
为了更好的理解这个过程,下图简单的对比了下。人的大脑学习过程和机器学习的对比。
但为什么人工智能智能会比人类学习更智能?因为数据和人相比人脑,更准确更快。人类在思考问题的过程中,会因为某些极端条件、前后因果、以及一些细节问题没考虑进去等而导致一些问题,然而机器学习可以依据大量的数据为食物,不断的填充自己的肚子,从而可以考虑到很多极端情况以及一些细节问题等。除此之外,计算机的运行速度是人类大脑无法相比的。吴军博士在《智能时代》一书中对大数据的优势进行了以下总结:“在无法确定因果关系时,数据为我们提供了解决问题的新方法,数据中所包含的信息可以帮助我们消除不确定性,而数据之间的相关性在某种程度上可以取代原来的因果关系,帮助我们得到想要的答案,这便是大数据的核心。”
再简单说下神经网络。可以简单理解成神经元是神经网络的成员,每个神经元都有自己的功能。如在花和草之间,前一个神经元识别出花,后一个神经元在花中识别出玫瑰花。前一个神经元的识别结果再传递个后一个神经元。前者是后者的输出,这就是神经分层的大致比喻。围棋AlphaGo理论上就是一个大型的神经网络,在围棋比赛中,它能预测到接下来的若干种结果,这种预测就是基于神经分层的原理。
机器学习
机器学习就是通过对经验、数据进行分析,来改进现有的计算机算法,优化现有的程序性能。简单说就是:
数据->算法->模型; 需要判断的数据->模型->给出预测
机器学习有三个要素:
- 数据:数据就是机器学习的样本。比如在多种花中识别这些花,这些花本身的一些特征就是数据,区分这些花,主要是依照它们自身的特性不同。
- 学习算法:神经网络、逻辑回归、随机森林等都是机器的学习算法,所谓iOS开发工程师的我们,无需深刻理解这些算法,Core ML框架中就已经为我们做了这些。
- 模型:所谓的模型就是机器从样本数据中找出的规律。根据这些规律,面对新的数据,模型就能做出相应的判断。实际和人类学习是很相像的。
Core ML基本介绍
Core ML支持 iOS、MacOS、tvOS和 watchOS。由4部分组成。
Core ML构造- 该结构最底层是有由 Acccelerate 和 Metal Performance Shaders 组成。前者用于图形学以及数学上的大规模计算,后者用于优化加速图形渲染。
- Core ML主要有两个职责:导入机器学习模型;生成对应的OC或Swift代码。
- Vision主要用于图片分析。NLP主要用于自然语义分析。
- 最上层是应用层,有了下面三层的基础,应用层就可以做很多事情,如人脸识别、手写文字理解、文字情感分析、自动翻译等。
Core ML应用步骤分析
1、拿到模型
最简单的获取方式是在苹果官网下载,具体是在Model模块中,该模块下有Places205-GoogLeNet、ResNet50、Inception V3、 VGG6四个模型。当然也可以自己训练模型。另外苹果也提供了转换器(Core ML Tools),该转换器是基于Python实现的,可用它把训练出来的模型转为适配Core ML的模型。在文章的最后我会介绍如何使用Core ML Tools进行模型转换。
模型转换适配Core ML框架的过程2、模型导入到项目
将模型导入到项目中。然后点击会出现下图所示状态。大小(Size)是 App 性能的一个重要指标,输入(Input)输出(Output)决定了如何使用这个模型。下图的输入是一张图片,输出有两个值,一个是最有可能的图片物体结果,为 String 类型;另一个是所有可能的物体类型以及对应的可能性,为 String 对应 Dobule 的 Dictionary 类型。点击下图的Resret50字样可以跳转到生成的代码链接中。
模型信息
3、生成高级代码并编程
这一步骤请具体看下面示例的代码。
Core ML实战(基于ResNet50模型照片识别)
这个示例中我选择苹果官网提供的图像识别ResNet50作为模型。照片的选择主要是通过UIImagePickerController这个类实现。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.imagePickerController = [[UIImagePickerController alloc] init];
self.imagePickerController.delegate = self;
self.imagePickerController.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
self.imagePickerController.allowsEditing = YES;
self.imagePickerController.sourceType = UIImagePickerControllerSourceTypeCamera;
self.imagePickerController.mediaTypes = @[(NSString *)kUTTypeImage];
self.imagePickerController.cameraCaptureMode = UIImagePickerControllerCameraCaptureModePhoto;
[self.navigationController presentViewController:self.imagePickerController
animated:YES
completion:nil];
}
在UIImagePickerControllerDelegate的代理方法下,实现了如下代码。
- (void)imagePickerController:(UIImagePickerController *)picker
didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info {
NSString *mediaType=[info objectForKey:UIImagePickerControllerMediaType];
if ([mediaType isEqualToString:(NSString *)kUTTypeImage]){
CGSize thesize = CGSizeMake(224, 224);
UIImage *theimage = [self image:info[UIImagePickerControllerEditedImage] scaleToSize:thesize];
self.imageView.image = theimage;
CVPixelBufferRef imageRef = [self pixelBufferFromCGImage:theimage.CGImage];
Resnet50 *resnet50Model = [[Resnet50 alloc] init];
NSError *error = nil;
Resnet50Output *output = [resnet50Model predictionFromImage:imageRef
error:&error];
if (error == nil) {
self.photoNameLabel.text = output.classLabel;
} else {
NSLog(@"Error is %@", error.localizedDescription);
}
}
UIImagePickerController *imagePickerVC = picker;
[imagePickerVC dismissViewControllerAnimated:YES completion:^{
}];
}
在上面Core ML应用步骤分析中,导入模型那一步骤我们知道,ResNet50这个模型需要输入的是一个 224 * 224 的Image 图片模型;输出则是预测归类标签等信息。上面方法中的 imageRef以及output.classLabel都是基于此模型定义的。
如果不是很理解,可以点击模型,然后再点击Model Class -> 模型名称,可以查看模型生成的代码。下面一段代码便是此模型生成的代码。
//
// Resnet50.h
//
// This file was automatically generated and should not be edited.
//
#import <Foundation/Foundation.h>
#import <CoreML/CoreML.h>
#include <stdint.h>
NS_ASSUME_NONNULL_BEGIN
/// Model Prediction Input Type
@interface Resnet50Input : NSObject<MLFeatureProvider>
/// Input image of scene to be classified as BGR image buffer, 224 pixels wide by 224 pixels high
@property (readwrite, nonatomic) CVPixelBufferRef image;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithImage:(CVPixelBufferRef)image;
@end
/// Model Prediction Output Type
@interface Resnet50Output : NSObject<MLFeatureProvider>
/// Probability of each category as dictionary of strings to doubles
@property (readwrite, nonatomic) NSDictionary<NSString *, NSNumber *> * classLabelProbs;
/// Most likely image category as string value
@property (readwrite, nonatomic) NSString * classLabel;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithClassLabelProbs:(NSDictionary<NSString *, NSNumber *> *)classLabelProbs classLabel:(NSString *)classLabel;
@end
/// Class for model loading and prediction
@interface Resnet50 : NSObject
@property (readonly, nonatomic, nullable) MLModel * model;
- (nullable instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError * _Nullable * _Nullable)error;
/// Make a prediction using the standard interface
/// @param input an instance of Resnet50Input to predict from
/// @param error If an error occurs, upon return contains an NSError object that describes the problem. If you are not interested in possible errors, pass in NULL.
/// @return the prediction as Resnet50Output
- (nullable Resnet50Output *)predictionFromFeatures:(Resnet50Input *)input error:(NSError * _Nullable * _Nullable)error;
/// Make a prediction using the convenience interface
/// @param image Input image of scene to be classified as BGR image buffer, 224 pixels wide by 224 pixels high:
/// @param error If an error occurs, upon return contains an NSError object that describes the problem. If you are not interested in possible errors, pass in NULL.
/// @return the prediction as Resnet50Output
- (nullable Resnet50Output *)predictionFromImage:(CVPixelBufferRef)image error:(NSError * _Nullable * _Nullable)error;
@end
NS_ASSUME_NONNULL_END
这样就基本完成了主要代码的编写。不过代码的实现中还有这样两个方法:
- (CVPixelBufferRef) pixelBufferFromCGImage: (CGImageRef) image {
NSDictionary *options = @{
(NSString*)kCVPixelBufferCGImageCompatibilityKey : @YES,
(NSString*)kCVPixelBufferCGBitmapContextCompatibilityKey : @YES,
};
CVPixelBufferRef pxbuffer = NULL;
CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, CGImageGetWidth(image),
CGImageGetHeight(image), kCVPixelFormatType_32ARGB, (__bridge CFDictionaryRef) options,
&pxbuffer);
if (status!=kCVReturnSuccess) {
NSLog(@"Operation failed");
}
NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);
CVPixelBufferLockBaseAddress(pxbuffer, 0);
void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);
CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(pxdata, CGImageGetWidth(image),
CGImageGetHeight(image), 8, 4*CGImageGetWidth(image), rgbColorSpace,
kCGImageAlphaNoneSkipFirst);
NSParameterAssert(context);
CGContextConcatCTM(context, CGAffineTransformMakeRotation(0));
CGAffineTransform flipVertical = CGAffineTransformMake( 1, 0, 0, -1, 0, CGImageGetHeight(image) );
CGContextConcatCTM(context, flipVertical);
CGAffineTransform flipHorizontal = CGAffineTransformMake( -1.0, 0.0, 0.0, 1.0, CGImageGetWidth(image), 0.0 );
CGContextConcatCTM(context, flipHorizontal);
CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image),
CGImageGetHeight(image)), image);
CGColorSpaceRelease(rgbColorSpace);
CGContextRelease(context);
CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
return pxbuffer;
}
- (UIImage*)image:(UIImage *)image scaleToSize:(CGSize)size{
UIGraphicsBeginImageContext(size);
[image drawInRect:CGRectMake(0, 0, size.width, size.height)];
UIImage* scaledImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return scaledImage;
}
对于第一个方法,CVPixelBufferRef这种图像格式的处理与UIImage, CGImageRef的处理需小心,容易造成内存泄漏。对于第二个方法,是因为模型的Input Image是有宽和高限制的,因此输入时,需要转换为224 * 224大小才能够正确识别。
这个Demo到此就完成了,如果此时拍一张你敲代码的键盘,就能识别出图片中显示的是键盘。
这里顺便在说一下文字感情分析类实现过程,有兴趣的可以自行研究下。文字感情分析总共分为四个过程:1、用自然语义处理API统计输入文字的单词频率。2、将单词频率输入到 Core ML 的模型中。3、Core ML 模型根据单词频率判断内容为正面或负面情绪。4、根据情绪内容更新 UI。其实说白了基本上和图片识别的实现过程实现一致。
Core ML Tool 模型转化工具介绍
之前说过了,模型可以在苹果官网下载,可以自己训练模型,也可以借助Core ML Tool将Caffee,Keras,LIBSVM,scikit-learn,xgboot等开源机器学习框架训练出的模型转换为 Core ML 对应的模型。Core ML Tool模型转换工具是基于Python实现的,我们可以定制转换器以及转换模型的参数。
#######1、安转Core ML Tool 模型转换工具
如果电脑没有安装Python,请先执行:
brew install python
然后直接输入以下命令:
pip install -U coremltools
2、转换训练好的模型
假如模型是用 caffe 训练的,即现在有一个 .caffemodel 文件,以下步骤可以将其转化为苹果支持的 .mlmodel:
import coremltools
// 利用 core ml 中对应的 caffee 转化器处理 .caffemodel 模型
coreml_model = coremltools.converters.caffe.convert('XXX.caffemodel')
// 将转化好的模型存储为 .mlmodel 文件
coreml_model.save('XXX.mlmodel')
确定转化的模型是否正常(检测模型能否识别一张狗的图片),可以直接运行如下命令。如果能正确输出结果,预测结果应含有 dog,并且预测的正确可能性比较高,则说明模型转换没问题。
XXX.mlmodel.predict('data': myTestData)
3、定制化转化的模型
定制转化模型的参数,我们一般用 label.txt 文件来定义,直接传入转化中即可。
// 自定义模型的接口参数
labels = 'labels.txt'
// 将 labels 设为转换的模型参数
coreml_model = coremltools.converters.caffe.convert('XXX.caffemodel', class_labels='labels')
定制转化的输入数据 data 为 image 类型:
coreml_model = coremltools.converters.caffe.convert('XXX.caffemodel', class_labels='labels', image_input_name = 'data')
指定转换模型的描述型参数(metadata),其他参数的设置类似:
// 指定作者信息
coreml_model.author = 'Apple Papa'
// 指定许可证
coreml_model.license = 'MIT'
// 指定输入('data')描述
coreml_model.input_description['data'] = 'An image of flower'
结语
整片文章中我们认识了什么人工智能、机器学习,知道了Core ML框架结构以及应用步骤,并实现了一个简单的照片识别实例,最后还介绍了Core ML模型转换工具。但是这一切的一切只是简单的入门,同上次参加经验交流会《TensorFlow技术解析于实战》作者李嘉璇所讲的那些技术相比,连九牛一毛都不算,毕竟人家讲的都是一些很高深的底层实现以及高数中那些复杂计算公式。可能有些人会认为人工智能只是一个噱头,实际人工智能已经渗入到我们生活的很多方面,希望这篇文章能引起更多iOS开发者对人工智能领域的关注。