这是一篇译文
原文地址Introduction to Core ML: Building a Simple Image Recognition App
在2017年WWDC上,Apple发行了一系列振奋人心的 frameworks 和 APIs 给我们开发者使用。在所有新的 frameworks 里面最受欢迎的莫过于 Core ML.Core ML 是一款可以用来在你的APP中融入机器学习模型的 framework. Core ML最伟大的地方就是你不需要具备全面的神经网络或者机器学习的知识。而他额外的特性就是你只要转换成 Core ML的模型,就可以使用预先训练过的数据模型。为了演示的目的,我们将使用 Apple 开发者网站提供的 Core ML模型。话不多说,让我们开始学习 Core ML 吧!
Note: 以下课程你需要 Xcode 9 bate 版,而且你需要一台 运行 iOS 11 bate 版系统的设备来测试这个课程中介绍的一些特性。尽管 Xcode 9 bate 同时支持 Swift 3.2 和 4.0,但我们所有的代码都是用 Swift 4.0。
Core ML 是什么
Core ML lets you integrate a broad variety of machine learning model types into your app. In addition to supporting extensive deep learning with over 30 layer types, it also supports standard models such as tree ensembles, SVMs, and generalized linear models. Because it’s built on top of low level technologies like Metal and Accelerate, Core ML seamlessly takes advantage of the CPU and GPU to provide maximum performance and efficiency. You can run machine learning models on the device so data doesn’t need to leave the device to be analyzed.
-- Apple’s official documentation about Core ML
Core ML 在今年的 WWDC 中被宣称是机器学习 framework 中的一个新类型,它将和iOS 11 一起到来。有了 Core ML,你可以将机器学习模型融入你的 App.稍等一下,什么是机器学习?简单来说,机器学习就是一个应用在不需要一个明确的程序的情况下,可以赋予电脑学习的能力。一个机器学习的算法和一些列受训的数据结合在一起的结果就是一个受训过的模型。
作为一个应用的开发者,我们主要关心的是我们应该怎样应用这些模型到我们的 app,并做一些有意思的事情。非常幸运的是,有了Core ML, Apple 已经把它做到非常简单来让我们把机器学习模型加入我们自己的应用。这给开发者开发一些特性,如图像识别、自然语言处理(NLP)、文本预测等带带来更多的可能性。
现在你可能很想知道,把这种类型的 AI 加入到你的应用是不是很困难。Core ML使用起来非常简单这一点就是它最伟大的一点。在这节课程中,你将会看到仅仅用了10行代码,就可以把 Core ML 用到你的 app 里了。
很酷,对吧?让我们开始吧!
Demo app 预览
我们即将写的这个demo相当简单。我们的应用可以让用户选择是拍摄某样东西或者是直接从相册选择。之后,机器学习算法会尝试去预测图像中的对象是什么。结果可能不完美,但是你可以从中学习到把 Core ML 应用到你的 app 的一些想法。
开始写代码
首先,我们用Xcode 9创建一个新的工程。选择 single-view app 作为我们的工程模板,并确保我们的语言选择的是 Swift.
搭建用户界面
作者的建议:如果你想要跳过搭建UI的部分,你可以下载 The starter project,然后直接跳转到 Core ML那一段去.
接下来开始吧!首先到你的Main.stroyboard
里面去添加一些 UI 元素到 view 上。选择 View controller, 然后到 Xcode 的菜单栏,点击Editor-> Embed In-> Navigation Controller
,之后你就会看见一个导航栏出现在 view 的顶部。给导航栏起一个名字叫 Core ML(或者你自己觉得舒服的名字)。
之后拖拽两个 bar button item:两个分别在导航栏的两侧。左边的一个,到
Attributes Inspector
改变System Item
为Camera
.右边的一个,起名为Library
。这另个 button可以让用户选择是从相册选择一张图片还是用相机拍摄一张。
最后你需要加的两个对象是UILabel
和UIImageView
。把UIImageView
放在view的正中间,改变它的宽和高为 299 * 299
,让它变成一个方形。对于 UILabel
,把它放到 view 的下面,并且拉伸它到两边边缘。这就是整个 UI 界面了。
当我还没有谈及怎么去给这些view设置 Auto Layout的时候,我强烈建议你去完成它,避免一些view不能完全显示。如果你不能完成这个,那就在你将要测试的设备上跑一下这个app.
实现拍照和相册的功能
现在你已经设计好了UI, 让我们转到实现,这一节我们将要实现相册和拍照按钮的功能。在ViewController.swift
,首先遵守UINavigationControllerDelegate
协议,这个协议是UIImagePickerController
类的 delegate 必须遵守的。
class ViewController: UIViewController, UINavigationControllerDelegate
之后添加两个 outlet 给 label 和 imageView。 为了方便,我给UIImageView
起名 imageView, 给 UILabel
起名 classifier. 你到代码看起来长这样:
import UIKit
class ViewController: UIViewController, UINavigationControllerDelegate {
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var classifier: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
接下来,你需要创建各自的 action 给 bar button item 的点击事件。现在插入下面的 action 方法到 UIViewController
类:
@IBAction func camera(_ sender: Any) {
if !UIImagePickerController.isSourceTypeAvailable(.camera) {
return
}
let cameraPicker = UIImagePickerController()
cameraPicker.delegate = self
cameraPicker.sourceType = .camera
cameraPicker.allowsEditing = false
present(cameraPicker, animated: true)
}
@IBAction func openLibrary(_ sender: Any) {
let picker = UIImagePickerController()
picker.allowsEditing = false
picker.delegate = self
picker.sourceType = .photoLibrary
present(picker, animated: true)
}
总结一下我们再各 action 里面所做的事。我们创建了一个UIImagePickerController
的常量,之后我们确保用户不能编辑图片(不管是拍照还是从相册选取的)。之后我们设置代理为 self, 最后我们展示UIImagePickerController
给用户。
因为我们没有添加UIImagePickerControllerDelegate
的方法到ViewController.swift
里, 所以我们会出现一个error, 我们将用一个类扩展来遵守这个协议:
extension ViewController: UIImagePickerControllerDelegate {
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
dismiss(animated: true, completion: nil)
}
}
这一行代码处理了用户点击取消拍照的情况。也实现了UIImagePickerControllerDelegate
的代理方法。最终你的代码长这样:
import UIKit
class ViewController: UIViewController, UINavigationControllerDelegate {
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var classifier: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
@IBAction func camera(_ sender: Any) {
if !UIImagePickerController.isSourceTypeAvailable(.camera) {
return
}
let cameraPicker = UIImagePickerController()
cameraPicker.delegate = self
cameraPicker.sourceType = .camera
cameraPicker.allowsEditing = false
present(cameraPicker, animated: true)
}
@IBAction func openLibrary(_ sender: Any) {
let picker = UIImagePickerController()
picker.allowsEditing = false
picker.delegate = self
picker.sourceType = .photoLibrary
present(picker, animated: true)
}
}
extension ViewController: UIImagePickerControllerDelegate {
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
dismiss(animated: true, completion: nil)
}
}
确保你所有的 outlet 变量和 action 方法都和 storyboard 连接了。
要进到拍照和相册,这里还有最后一件事你需要做。到你的Info.plist
加入两行:Privacy – Camera Usage Description 和 Privacy – Photo Library Usage Description。从iOS 10开始,你需要说明你的 app 需要打开相机和相册的原因。
OK! 就是这样了,现在你将要转到我们课程最重要的部分了。再一次提醒,如果你想绕过这些UI的搭建部分,可以下载 The starter project。
融入 Core ML 数据模型
现在,让我们切换档位,将Core ML 数据模型融入到我们的 app 里面。 就像我之前提到的,我们需要一个预先训练过的模型来和 Core ML 一起工作。你可以创建你自己的模型,倒是对于这个demo, 我们将使用 Apple 开发者网站提供的 预先训练的模型。
到 Apple 开发者网站的Machine Learning页面,滚动到页面的底部,你可以看到4个预先训练的 Core ML 模型。
在这节课程里面,我们使用Inception v3 模型,但是请随意尝试其他3个模型。一旦你下载了Inception v3 模型,就把它加到你的项目里面,看一下它所展示的内容。
Note:确保你选择了此工程的Target Membership,否则的话,你将没有权限去到这个文件里面。
在这个界面,你可以看到数据模型的类型是一个自然网络类别。另外一个信息你需要记一下的是这个模型的评估参数。它告诉你这个模型可以接受的输入以及他返回的输出。这里他接收一个 299 * 299的图片,并返回一个最相近的类别,加上每个类别的概率。
另一件你需要关注的是 模型类, 这是从机器学习模型中产生的模型类(Inceptionv3
),所以我们可以直接用在我们的代码中。如果你点击Inceptionv3
旁边的小箭头,你可以看到这个类的源码。
现在,我们把这个模型加到我们的代码里。返回到
ViewController.swift
,首先,引入 CoreML framework:
import CoreML
之后,声明一个model
变量是Inceptionv3
类型,并在viewWillAppear()
方法中初始化它:
var model: Inceptionv3!
override func viewWillAppear(_ animated: Bool) {
model = Inceptionv3()
}
我知道你可能在想:“为什么我们不更早一点初始化这个 model?” "我们为什么要在viewWillAppear
方法中定义它 ?" 。 Well, 亲爱的朋友们,原因就是当你的app尝试去识别图片中的对象的时候,它将会更快一点。
回到Inceptionv3.mlmodel
,我们发现这个模型接收的输入就是一个尺寸为299 * 299
的图片。因此我们怎么转换这个图片到这个尺寸。这就是接下来我们要做的。
转换图片
在ViewController.swift
的类扩展里面,像下面一样👇更新代码。我们实现了imagePickerController(_:didFinishPickingMediaWithInfo)
方法来处理选中的图片:
extension ViewController: UIImagePickerControllerDelegate {
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
dismiss(animated: true, completion: nil)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
picker.dismiss(animated: true)
classifier.text = "Analyzing Image..."
guard let image = info["UIImagePickerControllerOriginalImage"] as? UIImage else {
return
}
UIGraphicsBeginImageContextWithOptions(CGSize(width: 299, height: 299), true, 2.0)
image.draw(in: CGRect(x: 0, y: 0, width: 299, height: 299))
let newImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
let attrs = [kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue, kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue] as CFDictionary
var pixelBuffer : CVPixelBuffer?
let status = CVPixelBufferCreate(kCFAllocatorDefault, Int(newImage.size.width), Int(newImage.size.height), kCVPixelFormatType_32ARGB, attrs, &pixelBuffer)
guard (status == kCVReturnSuccess) else {
return
}
CVPixelBufferLockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0))
let pixelData = CVPixelBufferGetBaseAddress(pixelBuffer!)
let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
let context = CGContext(data: pixelData, width: Int(newImage.size.width), height: Int(newImage.size.height), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer!), space: rgbColorSpace, bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue) //3
context?.translateBy(x: 0, y: newImage.size.height)
context?.scaleBy(x: 1.0, y: -1.0)
UIGraphicsPushContext(context!)
newImage.draw(in: CGRect(x: 0, y: 0, width: newImage.size.width, height: newImage.size.height))
UIGraphicsPopContext()
CVPixelBufferUnlockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0))
imageView.image = newImage
}
}
上面的代码做了以下事情:
1.#7-11行:在这个方法最开始几行,我们从
info
字典中通过UIImagePickerControllerOriginalImage
key拿到选择的图片.当图片选好的时候我们也把UIImagePickerController
不显示。2.#13-16行:因为我们的模型仅能接收
299*299
尺寸的图片,所以我们转换图片成为一个方形,然后我们把它赋予另外一个常量newImage
3.#18-23行:现在,我们转换
newImage
成为一个CVPixelBuffer
.如果你不熟悉CVPixelBuffer
,从本质上来说它是保存主存储器中像素的图像缓冲器。你可以在这里找到更多关于CVPixelBuffer
的内容。4.#31-32行:我们将图像中存在的所有像素转换成与设备相关的RGB颜色,然后,通过将所有这些数据创建到
CGContext
中我们可以在需要渲染或更改其中一些基础属性时轻松调用它。这就是我们在转换和缩放图片后两行所做的。5.#34-38行:最后,我们将图像上下文作为当前上下文,渲染图片,将上下文从顶部栈移除,并设置
imageView.image
为newImage
.
现在,如果你还是不太明白上面的代码,不用担心,这就是一些高级的 Core Image
的代码,他们超出了本节课程的范围。你需要知道的就是我们把拍摄的图片转换成了我们模型所能接收的类型。我会建议你去玩数字并观察结果来更好的理解它。
使用 Core ML
不管怎样,让我们转换注意力到 Core ML,我们使用 Inceptionv3 模型来展示对象识别。有了 Core ML,要完成这个,只需要几行代码。粘贴下列代码到imageView.image = newImage
行的下面。
guard let prediction = try? model.prediction(image: pixelBuffer!) else {
return
}
classifier.text = "I think this is a \(prediction.classLabel)."
就是它了!这个Inceptionv3
类有一个创建的方法叫prediction(image:)
,它被用来预测所给图片的对象。我们传给这个方法一个pixelBuffer
变量--一个重新调整大小的图片。只要预测的结果返回,我们就会更新classifier
label 去设置它的文字来展示所识别的内容。
是时候测试一下这个 app 了!编译并运行这个app 到模拟器或者是你的设备(安装了iOS 11 bate系统)。从你的相册选择一张照片或者用相机拍摄一张照片。这个 app 就会告诉你这个图片展示的是什么。
测试的时候,你可能会发现这个app不能准确的识别图片的内容。这不是你代码的原因,而是这个模型的原因。
提要
我希望现在你已经知道了怎么把 Core ML 用到你自己的app. 这只是一篇开场的课程,如果你对转换你训练的Caffe, Keras, or SciKit 模型到 Core ML 模型有兴趣,请继续关注我们Core ML系列的下一个教程,我将会教你怎么把一个模型转换为 Core ML 模型。
demo 地址:complete project on GitHub
更多关于 Core ML framework的细节,你可以查阅官方Core ML 文档.你也可以参考 Apple 在 WWDC 2017上的内容:
你对于Core ML的想法是什么?请留下你的评论并让我知道!