前言
大概在一個多月前筆者參加了一場線上“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部分組成。
- 該結構最底層是有由 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進行模型轉換。
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開發者對人工智能領域的關注。