如果你一直有關注 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