近年來所謂的「大數據」(Big Data)、「AI 人工智慧」、「機器學習」(Machine Learning)、「深度學習」(Deep Learning)突然變得很夯,可說是目前最火熱、最具爆炸性發展的當紅顯學。我沒有在豪洨,熱門的程度,可觀察這個現象:在台南的市立圖書館與其他分館中,Python 的書籍根本是一書難求,而且持續很長一段時間。Python 本身除了用途廣泛之外,在機械學習領域更是入門標準,無法規避不學的程式語言,不會 Python 就等於做不成機械學習專家。在多年前我就對這個領域很感興趣,也持續 Google 搜到不少教材,不過坦白講這類學問的技術理論實在是深不見底、高不可攀,找到的教學常常需要參考某某博士論文,或是先弄懂某某數學演算法,這實非我等江湖術士能力所及,所以這條學習之路不論從哪開始下手,沒多久就一定會碰到鐵釘子,走得跌跌撞撞,始終得不到入門的要領。
直到去年(2017 年)蘋果公司於 WWDC 2017 發表了 Core ML 套件,我這條機械學習的黑暗之路終於見到曙光。蘋果宣稱 Core ML 能輕易的將「機械學習模型」(Machine Learning Model)整合到 iOS 裝置的應用程式內,並在 Developer 官網上釋出幾個實用的模型(副檔名為 mimodel)。到了今年 WWDC 2018,蘋果進一步釋出 Create ML 工具,號稱「不需具備機械學習專家技能,也能在 Mac 上輕鬆使用機械學習技術」,並且自動啟用可用的 GPU 加速訓練過程。
當然了,以黑蘋果技術帶入 Mac,這句話也可改寫成:
「不需具備機械學習專家技能,也能在個人電腦上輕鬆使用機械學習技術」
照這篇官方教學(網址),我終於如願完成人生第一個機械學習模型,讓手機也能認識新事物!
感恩 Apple!讚嘆 Apple!讓艱澀難走的機械學習之路,突然變成一條平坦的康莊大道。(希望啦!)
這篇文章,就是簡單紀錄一下如何自行創建訓練導出機械學習模型(.mlmode),並應用在 iOS App 上面。步驟很簡單,真的很簡單!
準備工作:
- 一台安裝 macOS 10.14 Mojave 作業系統的個人電腦(安裝說明)
- 安裝 Xcode 10 (下載) 的 macOS 10.14 Mojave 的作業系統,需 Apple 開發者帳號權限。
不過目前的 macOS 10.14 Mojave Beta 2 有 BUG,會讓訓練的過程中發生錯誤而無法完成。(2018.07.10:BUG 已在 Beta3 解決。)
iOS App 使用 .mlmodel 模型
這個部分在去年的 iOS 11 就有了,所以只需要 Xcode 9 就行了。教學很多,在官方強勢主導下,找到的大多是 swift 的範例。網路上當然也有 Object-C 的範例就是了。本文內容將使用少數幾行 Object-C 語言,搭配官網提供的 MobileNet.mlmodel 作為示範。
(1) 下載 MobileNet.mlmodel (官網下載),並拉進 iOS 專案。
(2) 點擊 MobileNet.mlmodel 並查看資訊:
.mlmodel 是去年(2017)由蘋果公司發表對應自家 Core ML 模型的檔案格式。上圖中點擊專案內的 MobileNet.mlmodel 項目後,在 Model Evaluation Parameter 這裏可查看模型的輸入輸出的項目。
- 輸入項目限定在 224x224 尺寸的影像(Image)。
- 輸出的項目有字串(String)與字典(Dictionary,string -> double)兩個。
點擊 Model Class 內 MobileNet 右邊的箭頭(紅框處),則會依照專案使用的語言(Object-C 或 swift)出現可用的 API 列表。這說明了 .mimodel 其實是一種封裝。
雖然有很多個,不過最基本只需要兩個就能運作:
- (instancetype)init NS_UNAVAILABLE;
- (nullable MobileNetOutput *)predictionFromImage:(CVPixelBufferRef)image error:(NSError * _Nullable * _Nullable)error;
也就是最少只需兩行就能跑了(當然實際上不可能這麼偷懶)。輸入的 Image 並不是常見的 UIImage 類,而是影片處理中常見的 CVPixelBufferRef 類,且大小限定在 224x224 。所以在取得 UIImage 類別的物件後,需轉換成 224x224 大小的 CVPixelBufferRef 類型物件。這部分也有人把 Object-C 程式碼寫好了。
參考網址:https://github.com/happymanx/CoreMLTest/
- (UIImage *)imageResize:(UIImage*)img toSize:(CGSize)newSize;
- (CVPixelBufferRef)pixelBufferFromCGImage:(CGImageRef)image;
用這兩個方法就能轉換輸入 Image 的格式問題。
至於模型拉進專案之後,撰寫應用程式時只要在前面加入 #import "xxxxxx.h" (Object-C 語法)就可以使用模型提供的 API 了。以這個範例使用的 MobileNet.mlmodel 來說,前面加入 #import "MobileNet.h" 就行了。
用最簡單的程式碼來檢驗運作的方式與結果。
// 用 Google 搜尋 "狗"圖片,隨機挑一張圖片,取用圖的網址,做成 UIImage 物件 img
UIImage *img = [UIImage imageWithData:
[NSData dataWithContentsOfURL:
[NSURL URLWithString:@"https://www.teepr.com/wp-content/uploads/2014/08/%E9%96%8B%E5%BF%83%E7%9A%84%E7%8B%97%E7%8B%97.jpg"]]];
// 圖片內容顯示在 App 畫面上的 UIImageView(名稱為 imageView)
imageView.image = img;
// 辨識前先縮圖
UIImage *img224x224 = [self imageResize:img toSize:CGSizeMake(224, 224)];
// 轉換成 CVPixelBufferRef 型態
CVPixelBufferRef buffer = [self pixelBufferFromCGImage:img224x224.CGImage];
// 開始影像辨識,載入模型
MobileNet *mobileNet = [[MobileNet alloc] init];
// 輸入 CVPixelBufferRef 進行辨識,辨識輸出結果為 MobileNetOutput 類別物件。
NSError *error = nil; // 接收辨識程序的錯誤內容
MobileNetOutput *output = [mobileNet predictionFromImage:buffer error:&error];
// 檢視輸出結果:字串部分為 classLabel
NSLog(@"output.classLabel = %@", output.classLabel);
// 檢視輸出結果:字典部分為 classLabel
NSLog(@"output.classLabelProbs = %@", output.classLabelProbs);
// 將輸出結果的字串部分,顯示在 App 畫面中的 UILabel(名稱 classifier)
classifier.text = output.classLabel;
App 的畫面結果:
終端視窗的回應訊息:
MobileNetProject[1004:38219] output.classLabel = Pembroke, Pembroke Welsh corgi
MobileNetProject[1004:38219] output.classLabelProbs = {
"Afghan hound, Afghan" = "6.213400904897526e-09";
"African chameleon, Chamaeleo chamaeleon" = "2.19332374484793e-09";
"African crocodile, Nile crocodile, Crocodylus niloticus" = "8.66392568799057e-10";
"African elephant, Loxodonta africana" = "1.9315593569047e-09";
"African grey, African gray, Psittacus erithacus" = "1.536224836229394e-08";
"African hunting dog, hyena dog, Cape hunting dog, Lycaon pictus" = "1.789419101783096e-08";
"Airedale, Airedale terrier" = "1.449278741461058e-08";
"American Staffordshire terrier, Staffordshire terrier, American pit bull terrier, pit bull terrier" = "6.084317760723934e-08";
"American alligator, Alligator mississipiensis" = "2.423576617172074e-10";
"American black bear, black bear, Ursus americanus, Euarctos americanus" = "2.094742157510154e-08";
"American chameleon, anole, Anolis carolinensis" = "9.495959574223889e-09";
"American coot, marsh hen, mud hen, water hen, Fulica americana" = "2.884459615604129e-10";
"American egret, great white heron, Egretta albus" = "3.334652831643581e-10";
"American lobster, Northern lobster, Maine lobster, Homarus americanus" = "1.485787919364157e-07";
"Angora, Angora rabbit" = "2.548045685557554e-08";
回應的文字部分是「Pembroke, Pembroke Welsh corgi」,字典的部分有幾百行的訊息。搜尋字典中「Pembroke, Pembroke Welsh corgi」這行:
"Pembroke, Pembroke Welsh corgi" = "0.8785513043403625";
辨識結果不難理解,字典的部分前面的字串代表機械學習模型可辨識出來的物品名稱,後面的數字則是「該筆結果的準確率」。這些物品名稱中,「Pembroke, Pembroke Welsh corgi」所顯示的數字結果最大,大約是 87%(0.87),其他物品名稱都沒有比這個數字還高,大多只有 <1 % 的準確率。
至於「Pembroke, Pembroke Welsh corgi」是尛東西?丟給 Google 查詢結果:
Google:潘布魯克威爾斯柯基犬,就是俗稱的「柯基犬」。
極簡化的說法:MobileNet 覺得這張狗圖,跟柯基犬有 87% 像。
App 使用 .mlmodel 模型的方法真的是非常簡單,輸入的 Image 類型是 CVPixelBufferRef 而非 UIImage 類別的最大原因,我想應該是影片處理程序的過程中可隨時提取 CVPixelBufferRef 來源,應用的層面更廣。
使用 Vision Framework 的另一種寫法:
NSError *err = nil;
MobileNet *mobileNet = [[MobileNet alloc] init];
//將模型轉換為 VNCoreMLModel 類,#import <Vision/Vision.h>
VNCoreMLModel *vnModel=[VNCoreMLModel modelForMLModel:mobileNet.model error:&err]; //轉換為 VNCoreMLModel
//建立一個 VNCoreMLRequest,傳入 VNCoreMLModel,並處理辨識結果於 completionHandler 區段
VNCoreMLRequest *request=[[VNCoreMLRequest alloc] initWithModel:vnModel completionHandler:^(VNRequest * _Nonnull request, NSError * _Nullable error) {
CGFloat confidence = 0.0f;
VNClassificationObservation *tempClassification = nil;
for (VNClassificationObservation *classification in request.results) {
//NSLog(@"%@ %.2f%%",classification.identifier, 100 * classification.confidence); //查看辨識率
if (classification.confidence > confidence)
{
confidence = classification.confidence;
tempClassification = classification;
}
}
classifier.text = tempClassification.identifier;
}] ;
request.imageCropAndScaleOption = VNImageCropAndScaleOptionCenterCrop;
UIImage *img = [UIImageimageWithData:[NSDatadataWithContentsOfURL:[NSURLURLWithString:imgurl]]];
imageView.image = img;
//建立一個 VNImageRequestHandler,將圖片傳入
VNImageRequestHandler *handler = [[VNImageRequestHandleralloc] initWithCGImage:img.CGImageoptions:nil];
NSError *error = nil;
// VNImageRequestHandler 運行 VNCoreMLRequest 類
[handler performRequests:[NSArrayarrayWithObject:request] error:&error];
if (error) {
NSLog(@"%@",error.localizedDescription);
}
這種寫法雖然比較囉唆,但是可統一 .mlmodel 的輸出接口,而且不用另外處理圖片縮圖/轉換的問題。兩種寫法的辨識結果相同,但辨識率數字有些許差異。
使用 Create ML 自行訓練產生機械學習模型(.mlmodel)
上面的例子是使用蘋果提供已經做好的 .mlmodel 模型,能辨識的影像也是生活常見的事物,不過在商業實務上,不太可能引用這些定義攏統的識別結果,例如識別某廠商特定產品做相關應用,得針對特定目的去產生訓練模型。在今年 WWDC 2018 以前,必須使用另外的機械學習套件產生模型之後,再透過「Core ML Tools」工具把模型轉換成 .mlmodel 格式,操作者必須自帶機械學習專業知識與技能,得熟悉 Python 程式語言與一定程度的數理知識,有一定的技術門檻。
不過從今年的 Xcode 10 開始,我們可以用很簡單的方式,直接在 Xcode 內自行訓練產生 .mlmodel 模型,不需額外的學習套件了。蘋果的官方開發者網站日前已經釋出教學,以下是依照教學練習的操作過程,憑個人直覺就能完成訓練,很簡單,一點都不難。
Creating an Image Classifier Model
英文不懂沒關係,AppCoda 中文網站也有中文教學了(網址)。不過萬事起頭難,要怎麼蒐集到海量又分類好的訓練素材就是個問題。我在之前學習的過程中,有找到的一組花卉的海量圖集,剛好可以拿來作訓練 .mlmodel 的範例。
(1) 下載圖集,並且解壓縮。(花卉圖集下載,約 218MB)
查看了一下,圖集底下有五個目錄「daisy(雛菊),dandelion(浦公英),roses(玫瑰),sunflower(向日葵),tulips(鬱金香)」,每個目錄底下各有數百張圖片。
根據蘋果的官方教學,將這些照片以檔案數量分配出 80% 的「Training Data」和 20% 的「Testing Data」,大約這個比例就行了( LICENSE.txt 可刪除)。
(2) 執行 Xcode 10,File -> New -> Playground,選 macOS - Blank,按 Next。
決定 Playground 檔名之後按 Create 即可。
(2) 點選視窗右上方「Show the Assistant Editor」(即兩個圓圈扣在一起的圖示按鈕),並在左邊的編輯區輸入三行程式碼,輸入完按下最後一行的三角形執行鈕(下圖的紅框處)。
import CreateMLUI
let builder = MLImageClassifierBuilder()
builder.showInLiveView()
大約等個幾秒鐘,右邊會出現訓練模型的操作介面。介面非常簡單,只有一個訓練模型的預設標題「ImageClassifier」,和底下有一個「Drop images To Begin Training」的區域(紅框部分)。
我們要做的訓練只有兩件事:
- 滑鼠操作,先將「Training Data」的圖片拖進區域
- 滑鼠操作,再將「Testing Data」的圖片拖進區域
真是超級簡單的訓練機械學習模型方法!比起其他的三方套件得搞一堆演算參數和撰寫神經網路程式碼,這簡直是蘋果德政!
用滑鼠點選「Training Data」目錄項目,從 Finder 整個拖進區域後,訓練就開始了。下方會出現訓練進度和訊息。黑蘋果的 GPU (R9-280X)溫度也跟著上升,CPU 溫度跟頻率倒是沒什麼變化。
大約花了兩分半鐘,學習了約三千多張圖片,並出現 Training 跟 Validation 兩個數字結果。
接著把「Testing Data」從 Finder 拖進區域,最後會得到第三個數字 Evaluation。
Testing Data 的資料量只占了約 20% ,所以這個步驟約 40 秒就完成了。三個數字結果代表什麼意思,老實說我也不太清楚。
補充:以相同的訓練資料,個別以 R9-280X 和 GTX-780 各自重新訓練一次,所需時間如下:
R9-280X | GTX-780 | CPU ( i7-3770) | |
Training Data | 2 分 23 秒 | 1 分 40 秒 | 11 分 43 秒 |
Testing Data | 39 秒 | 22 秒 | 3 分 8 秒 |
溫度 (°C) | 約 68 °C | 約 67 °C | --( CPU 溫度沒有明顯升高) |
使用 GPU 就已經比 CPU 節省了 80% 時間,而 GTX-780 又比 R9-280X 訓練速度快約 40%,所以想玩 Machine Learning 的人,好的獨立顯卡是必要的,運行時的 GPU 溫度也比較低(溫度爬升較慢)。
就這樣,我們就完成了機械學習認識五種花卉的訓練過程,操作超級簡單!程式碼只需三行,叫出訓練介面就行了。在標題「ImageClassfier」右邊的往下箭頭點一下,填寫一下作者大名跟資訊,取個好聽一點的名字,按個 Save 就可以把訓練好的 .mlmodel 模型存擋了。
最終得到的這個 .mlmodel 模型,可用來辨識 daisy(雛菊),dandelion(浦公英),roses(玫瑰),sunflower(向日葵),tulips(鬱金香)五種花卉的圖片。
驗證模型運作準確度
照上述 iOS App 使用 .mlmodel 模型的方式依樣畫葫蘆,把自己訓練出來的 .mlmodel 拉進 iOS 專案來驗證訓練成果。先使用相同的科基犬圖,得到的結果如下:
daisy = 0.06090744495831847
roses = 0.1396115153932302
sunflowers = 0.003177831752012689
tulips = 0.7923442192467409
dandelion = 0.003958988649697627
雖然柯基犬被辨識為 tulips(鬱金香),不過準確度只有 73%。辨識為 rose(玫瑰)的準確度為 13%,daisy(雛菊)也有 6%。簡單的說,如果把準確度拉高到九成以上,柯基犬會被排除在五種辨識結果之外。
上網隨機找一張鬱金香的圖片來識別看看:
https://i2.wp.com/travels.media/tc/wp-content/uploads/2015/04/鬱金香.jpg
結果如下:
2018-06-24 19:21:31.421823+0800 MobileNetProject[2283:175536] daisy = 3.277897492815316e-06
2018-06-24 19:21:31.421907+0800 MobileNetProject[2283:175536] roses = 2.371430734334716e-07
2018-06-24 19:21:31.422006+0800 MobileNetProject[2283:175536] sunflowers = 7.708879521718365e-08
2018-06-24 19:21:31.422100+0800 MobileNetProject[2283:175536] tulips = 0.9999963913991989
2018-06-24 19:21:31.422209+0800 MobileNetProject[2283:175536] dandelion = 1.647143954869135e-08
辨識為 tulips(鬱金香)的準確度高達 99.999%, 其他花卉的機率低到小數點以下六位。
菊花的種類與顏色更多,隨機找一張菊花的圖片試試看。
http://pic.pimg.tw/herballey/4bb1c168b3a59.jpg
daisy = 0.9937612288437867
roses = 3.80718888536895e-05
sunflowers = 0.001653476840197138
tulips = 0.0009781564784137448
dandelion = 0.003569065948748827
準確率 99.3% 也很好,看來訓練出來的模型辨識度很不錯。
找一個也是花圖,但不在辨識名稱內的的照片。Google 搜尋「百合花」:
http://img.ltn.com.tw/Upload/liveNews/BigPic/600_2354058_2.jpg
結果如下:
daisy = 0.00254776680123029;
dandelion = 8.846607084706478e-06;
roses = 0.001350513187249112;
sunflowers = 0.0001463019971201222;
tulips = 0.9959465714073158;
結果顯示為鬱金香,準確度還高達 99%。看來這個模型不認識百合花之後,一股腦把結果往鬱金香身上堆。
daisy = "0.1995546049451405";
dandelion = "0.622240376836122";
roses = "0.0250135171787896";
sunflowers = "0.1017718509332739";
tulips = "0.05141965010667407";
辨識為 dandelion (浦公英),不過準確度只有 62%,辨識為 daisy(雛菊)的準確度為 19.9%,與柯基圖的結果類似。
看來百合的生長姿態與鬱金香很雷同,而黃花風鈴木的姿態皆不同於五種可辨識的花種,所以機率不高。得到這樣的邏輯之後,後續可以用程式去繞開機率過低的結果。
用 Xcode 10 Beta 訓練出來的模型,經測試只能用在 iOS 12。在 iOS 11 裝置上雖然不會報錯,但讀進來的模型檔變成 (null) ,所以也不能用。希望蘋果在 Xcode 10 能處理掉這個問題。
模擬情境:訓練辨識新的事物 - 「Gogoro 換電站」
上面提到的花卉圖集已經是有人整理過,是絕佳的機械學習教學範例圖檔,歷經無數的教學驗證,所以不論用哪一種訓練方式,辨識度當然一定有很不錯的成績。但實際上機械學習往往都是有特定的需求,第一步該如何收集到大量的學習圖庫、正確的分類整理,對個人練習來說,就是個很頭痛的問題。模擬一個訓練模型的操作流程,首先想到要收集的對象,是正在生活中發生的新事物,或是有強烈特定主題的商品主體,才會更貼近商業上的實務操作。
Gogoro 電動機車在去年中發表第 2 型廉價版之後才逐步打開市場,獨有的「換電生態」與逐步建設中的換電站,在最近幾個月才在六大都會區逐漸佈局,很多人看到 Gogoro 換電站也不知道是什麼玩意(不少人以為是販賣機)。而換電站廣布台灣西岸都會區,如果要收集到海量的換電站照片作為訓練資料,挑戰並不小。
(圖片來源:SUPERMOTO8)
所以我就用「訓練機械學習辨識 Gogoro 換電站」為情境,嘗試練習如何自行收集海量的換電站圖片,作為訓練機械學習的素材。Google 圖片搜尋的確是不錯的方式,不過最近幾個月 Google 搜尋圖片的網頁變更,已經不太容易從 Google 圖片使用「一鍵下載」的功能了,試了幾個 Chrome 外掛都失敗,目前大概只能一張一張手動下載。
在 Gogoro 官網有一個「查詢換電站」的網頁(網址),點擊地圖上的換電站圖示之後,網頁會顯示該換電站的外觀照片。除了 Google 圖片之外,這應該是海量收集換電站照片的最佳入口了。然而官網的圖片有版權限制,所以我不能教大家如何一次抓取全部的換電站照片。
但我相信對於熟悉解析網頁內容的人來說,看到網頁就知道用什麼方法「一次海量下載」,並非難事。
解析這個網頁內容後,我們一次就取得了 917 張每個換電站的外觀圖片。
接著按照上面的方式,在原來的花卉圖集的目錄中,加入「gogorostation」目錄作為辨識標籤,並依照 80% 「Training Data」與 20% 「Testing Data」分配圖檔。
中間的訓練過程快轉,並改寫了一下程式,只要輸出結果不是換電站的,通通顯示「不是換電站」的結果標籤。跳轉到驗證模型...結果悲劇了!網路上隨便找一張有建築物的街景圖,通通判定為「gogorostation」,準確率還高到嚇人,幾乎都有 99% 以上。
檢查拿來訓練的圖檔內容,我大概知道原因在哪了。
有些訓練圖檔照片的換電站主體比例太小,小到看不到換電站在哪,有些照片周遭有太多「不是換電站該有的東西」,能一眼看出「是換電站主體照片」的圖檔不到一半,這些一股腦全丟進去訓練,當然會干擾學習的效果,只好再花了不少時間把一些「看起來有問題」的訓練照片刪除,再訓練,效果不佳再刪除,再訓練、再刪除、再訓練(GPU 快燒起來了)...反覆幾次之後,勉強有了「稍微比較滿意」的辨識結果。
然後找幾張日前另一家機車大廠發表的電動機車換電站照片,來考驗一下模型:
看起來很成功啊~是嗎?老實講,這兩張機車廠的換電站照片,準確率還有 98% 與 97 %,所以判定標準設定在 99% 以上才讓它「看起來有效果」。而這麼高標準,當然也會造成錯誤的結果。
這張是官方的換電站宣傳圖,準確率 97.8%。
不知為何這張只被判定 94.5 %,準確度比競爭對手還低。
所以大概算了一下,以 99% 準確度為標準,辨識成功大約只有一半...(哭...),所以嚴格來講,這個「辨識 Gogoro 換電站」的模型沒有練好,不過這成績也只花了短短一個晚上,比我寫這邊文章的時間還短。
延伸閱讀官網文件:提升模型的準確度。
後記
近幾年來一直嘗試踏入機械學習的坑,也一路走得跌跌撞撞,直到蘋果去年發表 Core ML 與今年的 Create ML,將機械學習的門檻一下子從天際拉到了腳邊,可想而知,不久的將來「機械學習」將是一門人人可觸及、實作的議題。然而蘋果拉低門檻也只是個開端,也不代表所有的機械學習基礎都能拋開艱澀的數理知識與資料收集,現在可以訓練機械學會認識換電站,那麼接下來,要怎麼訓練從圖片裡明確標示出換電站在圖片中哪個位置,找到換電站後如何從外觀辨識機器有幾個充電孔,有哪幾個充電孔是可用的?後面還有一大堆的學習挖坑,永無止境的機械學習過程,延伸至深度學習、人工智慧的領域。
再者,訓練機械學習的演算理論、程式撰寫功力似乎不太是重點,真正的重點在於:準備、標註資料、整理資料,如何利用演算法的特性提升準確度,否則機械學習到的也是不可信任,很難發揮真正的用處。
不論如何,還是很感恩如今有了簡單的訓練機械方法,至少在建立某些機械學習模型時,有機會繞過如博士論文般艱深難懂的演算邏輯,建立符合個人目的應用,或許有朝一日,每個人都可以透過如教育人類般的互動過程,訓練出完全屬於自己使用、符合自己生活習慣的人工機械助理,期待這天能在有生之日實現。