如果你一直有關注 Apple 去年所發佈的消息,就會知道他們在機器學習上投入了大量心力。自他們去年在 WWDC 2017 上推出 Core ML 以來,已經有大量結合機器學習技術的應用程式湧現。
但是,開發人員經常遇到的其中一個挑戰是:如何創建模型?幸運的是,Apple 在去年冬天宣布從 GraphLab 收購了 Turi Create,正正解決了我們的問題。Turi Create 是 Apple 的工具,可以幫助開發人員簡化創建客製化模型的步驟。使用 Turi Create,你可以建立自己的客製化機器學習模型。
Turi Create 快速入門
如果你有關注其他機器學習教學文章,你可能會覺得奇怪,「今年 Apple 不是有發佈一個叫 Create ML 的工具嗎?那相較於 Create ML 來說,Turi Create 有什麼優勢?」
雖然對於剛開始研究機器學習的人來說,Create ML 是一個很好的工具,但它在使用方面嚴重受到限制,例如只能使用文本或圖像數據。雖然這已經可以完成大多數的專案,但是對於稍微複雜的機器學習應用程式(例如風格轉換 (Style Transfer)), Create ML 就可能會變得毫無用處。
使用 Turi Create,你除了可以創建所有原本使用 Create ML 創建出的 Core ML 模型之外,更能創造更多不同類型的模型!由於 Turi Create 比 Create ML 複雜得多,因此它與其他機器學習工具如 Keras 和 TensorFlow 有高度的整合性。在我們的 CreateML 教學之中,你看到我們可以使用 Create ML 製作 Core ML 模型的類型。以下是你可以使用 Turi Create 製作的演算法類型:
你可以看到列表中包含了分類器與回歸器 (regressors),它們都可以使用 Create ML 或 Turi Create 來完成。Turi Create 提供了許多在 Create ML 缺少的高客製化程度,這也是更多有經驗的資料科學家會選用 Turi Create 的原因。
什麼是風格轉換?
現在你大致瞭解到甚麼是 Turi Create,那麼讓我們來看看甚麼是風格轉換。風格轉換是一種使用另一張圖像風格將圖像重新組合的技術,即是甚麼意思?看看下面利用 Prisma 創造出來的圖像:

如你所見,上面早餐餐盤的圖像風格轉換成漫畫了。由 Gatys 等人發表了一篇論文,描述如何使用捲積神經網路 (Convolutional Neural Networks, CNNs) 將一張圖像的美術風格轉換到另一張圖像,風格轉換就開始興起。
捲積神經網路是一種機器學習的神經網路,通常應用於圖像辨識及分類。它已經成功地解決電腦視覺方面的問題,例如:臉部辨識、物件辨識等。這是一個複雜的議題,所以我不會在這裡討論太多。
構建自己的風格轉換應用程式
現在你已經瞭解了本教學涵蓋到的工具和概念,我們終於可以開始了!我們將會利用 Turi Create 構建自己的風格轉換模型,並把它匯入 iOS 專案來看看效果!

首先,在這裡下載起始專案,在本次的教學中我們將會用到 Python 2、Jupyter Notebook 和 Xcode 9。
訓練風格轉換模型
Turi Create 是一個 Python 套件,但它並沒有內建在 macOS 裡面,所以讓我帶你快速安裝它。你的 macOS 應該已經安裝了 Python,若你的設備還沒有安裝 Python 或 pip ,你可以在這裡了解安裝流程。
安裝 Turi Create 及 Jupyter
打開終端機並輸入下列指令:
pip install turicreate==5.0b2
Python 套件安裝過程大約 1-2 分鐘。與此同時,我們可以下載 Jupyter Notebook。Jupyter Notebook 是一個供開發人員使用、支援許多語言的編譯器,它包含豐富和互動的輸出視覺效果。由於 Turi Create 僅支持 Python 2,因此請在終端機輸入以下命令以安裝適用於 Python 2 的 Jupyter Notebook。
python -m pip install --upgrade pip python -m pip install jupyter

當所有的套件都安裝好,就可以開始創造我們的演算法了!
使用 Turi Create 撰寫程式
我們即將構建的風格轉換模型會以梵谷的作品星夜 (Starry Night) 為基礎。簡單來說,我們創造的模型可以將任何圖像轉換成星夜 (Starry Night) 風格的複製品。

首先,下載訓練數據並解壓,裡面有一個 content 資料夾和一個 style 資料夾。打開 content 資料夾,你會看到大約有 70 張不同的圖片。這個資料夾包含了各式各樣的圖片,這樣就可以讓我們的演算法知道有甚麼類型的圖片需要做轉換。因為我們想要轉換所有圖像,我們就需要有多樣化的圖片才行。
而在 Style 資料夾中就很簡單地只有一張圖片:StarryNight.jpg 。這個資料夾包含了我們想要轉換的美術風格來源。
現在,讓我們打開 Jupyter Notebook 開始撰寫程式碼。輸入下列指令到終端機中:
jupyter notebook
這將會打開 Safari 並顯示這個頁面:

點擊 New 按鈕,然後按下 Python 2!
按下按鈕後,將會彈出一個新頁面,這就是我們要建立模型的地方。按下第一個 Cell,並匯入 Turi Create 套件:
import turicreate as tc
按下 SHIFT+Enter 來執行這一個 Cell 中的程式碼,等待套件匯入完成。下一步,來創建一個包含圖像資料夾的參考。請確認你已經把程式碼中的參數設為資料夾的路徑。
style = tc.load_images('/Path/To/Folder/style')
content = tc.load_images('/Path/To/Folder/content')
執行程式碼後,你應該會收到這樣輸出訊息:

不用太擔心這樣的警告。接下來,我們將輸入指令來創建風格轉換模型。強烈建議你一台在擁有 GPU 運算資源的 Mac上執行下列程式碼,像是最新的 MacBook Pro 或 iMac。如果你選擇在 MacBook Air 上執行,那麼程式將會透過 CPU 來運算,這可能會花上好幾天的時間。
model = tc.style_transfer.create(style, content)
執行程式碼,這可能因為你的設備而花上一段很長的時間才能完成,像我在 MacBook Air 上透過 CPU 運算就花了 3 天半才完成。如果你沒有足夠的時間,不用擔心,你可以在這裡下載最後的 Core ML 模型(CoreML 模型名為 “StarryStyle”)。然而,可以的話你還是試試執行整個程序,感受一下它是怎樣運作的!

你可以看到表格中包含了三個欄位: Iteration(疊代次數)、Loss(損失) 和 Elapsed Time(花費時間)。在機器學習之中,會有特定函數執行多次向前和向後運算。當函數向前運算就是 cost,往後運算就是 loss。每次執行函數時,目的是調整參數來減少 Loss。因此每次更改參數時,就會在增加一次 Iteration,目標是為了得到更少的 Loss。在訓練的過程中,你可以發現 Loss 會漸漸地變少。而 Elapsed time 指的就是運算所消耗的時間。
當模型已經完成訓練,只需要儲存它就可以了!這可以簡單地用一行程式碼來完成!
model.export_coreml("StarryStyle.mlmodel")

就這樣完成了,你可以到函式庫看看最終的模型!

Xcode 專案概覽
現在我們已經有了自己的模型,剩下來要做的就是將它匯入到 Xcode 專案之中。打開 Xcode 9 來看一下我們的專案。

構建並執行專案,這樣可以確認我們可以編譯此專案。應用程式目前還未能運作,當你按下Van Gogh!按鈕,你會發現甚麼事都沒發生!現在,輪到我們來撰寫程式碼了,讓我們開始吧!
實作機器學習
首先,將我們的模型檔案(即是 StarryStyle.mlmodel)拖曳到專案之中,請確保你有勾選 Copy Items If Needed,以及已經選了目標專案。

接下來,我們需要在 ViewController.swift 加入程式碼來處理機器學習流程,大部分的程式碼會在 transformImage() 函數中撰寫。讓我們從匯入 Core ML 套件並呼叫模型開始吧!
import CoreML
...
@IBAction func transformImage(_ sender: Any) {
    // Style Transfer Here
    let model = StarryStyle()
}
這行程式碼簡單地將 Core ML 模型指定為叫做 model 的常數。
圖像轉換
下一步,我們需要將使用者所選取的圖像轉換成可讀數據。再看看 StarryStyle.mlmodel 檔案,你就會發現它接受的圖像尺寸是 256×256,因此我們必須執行轉換。在我們的 transformImage() 函數下方加入一個新的函數。
func pixelBuffer(from image: UIImage) -> CVPixelBuffer? {
    // 1
    UIGraphicsBeginImageContextWithOptions(CGSize(width: 256, height: 256), true, 2.0)
    image.draw(in: CGRect(x: 0, y: 0, width: 256, height: 256))
    let newImage = UIGraphicsGetImageFromCurrentImageContext()!
    UIGraphicsEndImageContext()
    // 2
    let attrs = [kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue, kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue] as CFDictionary
    var pixelBuffer : CVPixelBuffer?
    let status = CVPixelBufferCreate(kCFAllocatorDefault, 256, 256, kCVPixelFormatType_32ARGB, attrs, &pixelBuffer)
    guard (status == kCVReturnSuccess) else {
        return nil
    }
    // 3
    CVPixelBufferLockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0))
    let pixelData = CVPixelBufferGetBaseAddress(pixelBuffer!)
    // 4
    let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
    let context = CGContext(data: pixelData, width: 256, height: 256, bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer!), space: rgbColorSpace, bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue)
    // 5
    context?.translateBy(x: 0, y: 256)
    context?.scaleBy(x: 1.0, y: -1.0)
    // 6
    UIGraphicsPushContext(context!)
    image.draw(in: CGRect(x: 0, y: 0, width: 256, height: 256))
    UIGraphicsPopContext()
    CVPixelBufferUnlockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0))
    return pixelBuffer
}
這是一個補助函數 (Helper Function),與我們之前 Core ML 教學文章所使用的函數有點相似。如果你已經忘了,別擔心,讓我一步一步解釋這個函數。
- 因為我們的模型只能接受尺寸為 256 x 256的圖像,所以我們將圖片轉換為正方形,接著將正方形圖像指定到另一個newImage的常數。
- 現在,我們將 newImage轉換成為CVPixelBuffer。如果你對CVPixelBuffer不熟悉,它基本上是一個圖像緩衝區,用來將像素存於主要記憶體中。你可以在這裡瞭解更多關於CVPixelBuffers的資訊。
- 取得圖片中的所有像素後,我們將它轉換成設備所對應的 RGB 色彩空間。接著,將所有數據創建為 CGContext,當我們需要渲染(或改變)某些底層的屬性時,就可以簡單地呼叫它,這是我們在下列兩行程式碼中透過轉化及縮放圖像所做的事。
- 最後,我們將圖像內容放入當前內容中,渲染圖像,並移除堆疊最上層的內容。當這些變更都完成後,回傳像素緩衝器。
這其實是一些非常進階的Core Image程式碼,已經超出了本篇教學文章的範圍。如果有某些部分不瞭解其實不用擔心。整段程式碼的主要目的,是藉由轉換一張圖像為像素緩衝器來提取它的數據,讓 Core ML 可以更方便地讀取它。
將風格轉換應用於圖像
現在我們有了 Core ML 補助函數,讓我們回到 transformImage() 並實作程式碼。在我們宣告 model 常數的那行下面,輸入下列程式碼:
let styleArray = try? MLMultiArray(shape: [1] as [NSNumber], dataType: .double)
styleArray?[0] = 1.0
Turi Create 允許你將多於一種「風格」打包到模型之中,雖然這次的專案只有一種風格,就是 Starry Night。如果你想添加更多種風格,你可以加入更多圖片到 style 資料夾中。我們將 styleArray 宣告為 MLMultiArray,這是一種被 Core ML 所使用來作模型輸入及輸出的陣列型態。由於我們只有一種風格,所以只有一種形狀及數據元素,因此我們將 styleArray 的數據元素設為 1。

最後,只需要利用我們的模型進行預測,並將結果設置為 imageView。
if let image = pixelBuffer(from: imageView.image!) {
    do {
        let predictionOutput = try model.prediction(image: image, index: styleArray!)
        let ciImage = CIImage(cvPixelBuffer: predictionOutput.stylizedImage)
        let tempContext = CIContext(options: nil)
        let tempImage = tempContext.createCGImage(ciImage, from: CGRect(x: 0, y: 0, width: CVPixelBufferGetWidth(predictionOutput.stylizedImage), height: CVPixelBufferGetHeight(predictionOutput.stylizedImage)))
        imageView.image = UIImage(cgImage: tempImage!)
    } catch let error as NSError {
        print("CoreML Model Error: \(error)")
    }
}
這個函數首先檢查 imageView 之中是否有圖像。在這段程式碼中,我們先定義了 predictionOutput 用來儲存模型預測的輸出結果。我們以使用者的影像以及風格陣列作為參數,呼叫模型的 prediction 方法。預測的結果是像素緩衝器,但是我們無法將像素緩衝器設定為 UIImageView,因此我們想出了一個非常有創意的方法來實現。
首先,我們將像素緩衝器 predictionOutput.stylizedImage 設置為 CIImage 類型的圖像。然後,創建一個 tempContext 變數,它是 CIContext 的實例。我們呼叫 context 的內建函數(也就是createCGImage),它從 ciImage 產生 CGImage。最後,我們可以將 imageView 設置為 tempImage。這樣就完成了!如果有任何錯誤,我們可以將錯誤印出來好好處理。
構建並執行專案。你可以從圖庫中選一張圖片,然後測試應用程式!

你可能會注意到模型的輸出結果看起來不太接近原本的 Starry Night,而這種情況可以有很多原因。可能我們需要更多的訓練數據?或是我們訓練數據時需要更多次的疊代次數?我強烈的建議你回到前面幾個步驟,再玩玩這些參數,直到你滿意輸出結果為止!
總結
教學文章就到此為止了!我已經向你介紹了 Turi Create,並創造了你自己的風格轉換模型,如果是在 5 年前,一個人定必無法完成。你也學習到了如何將 Core ML 模型匯入 iOS 應用程式中,並有創意地應用它!
但是,風格轉換只是一個開始。如我在前文提過,Turi Create 可以用來創造各類型的應用程式,下面是一些幫助你更進一步的資源:
- Apple’s Gitbook on Turi Create Applications
- A Guide to Turi Create – WWDC 2018
- Turi Create Repository
如果需要完整的專案,請到 GitHub 下載。如果你有任何意見或問題,請在下面留言,與我分享你的想法。
原文:Creating a Prisma-like App with Core ML, Style Transfer and Turi Create
 
