前情提要:教你的 iPhone 認識 Gogoro 換電站(Part 1)- 用黑蘋果電腦玩轉最夯的機械學習。
在前一篇文章中,第一次嘗試從無到有完成整個機械學習影像辨識的練習過程,最終得到一個「必須以半作弊的方式得到貌似可用的訓練模型」這種不太滿意的結果,後續也留下許多問題留待後續探討。接下來這段時間裡,我一直不斷嘗試各種手段,增加辨識成功的機率,不過終究效果有限,誤判的原因也越來越難深究,以至於教學課程訓練好的範例資料可用,自己訓練出來資料卻不能用。才正要開心的向前踏出一步,接著又卡關了,實在很灰心!
於是又再度上谷歌大海中漂流,反覆嘗試搜尋自己可能遺漏了什麼關鍵字,一次又一次的搜尋,一次又一次的觸碰艱深難懂也始終搞不懂的演算法議題。在某一次的資料搜尋中,搜到一張圖片:
一目瞭然...這不就是我想要的辨識結果嗎?在上次的操作練習中,是針對整張圖的辨識,也導致「Gogoro 換電站」的訓練資料中,周遭伴隨一堆地景地物而干擾學習結果,我總算意識到自己忽略的地方。順著這張圖的線索,找到圖片的來源:
「Turi Create - Object detection」
在這裡出現了新的關鍵字「Object Detection」(物體偵測),我終於搞懂卡關的原因了!原來我一直把「影像辨識」(Image Classification)與「物體偵測」(Object Detection)兩者混為一談了,機械學習的應用方式錯誤,所以始終擺脫不了辨識結果遭誤判的命運。來源網址給出另一個關鍵字「Turi Create」,也找到了「Turi Create」專案的 Github 主頁:
蛇嘛?!蝦毀?這是蘋果在 GitHub 上的專案?什麼時候蘋果給出這玩意但我不知道,我意識到自己肯定又忽略了什麼,再度回到蘋果的開發者網頁,重新瀏覽了一遍,才發現蘋果早在「Machine Learning - Working with Core ML Models」這個專題頁面中,就提過這玩意兒。這網頁我不知刷了多少回都沒注意到,正所謂「直覺往往是盲點」,以為又是難搞的第三方學習套件,所以就自動忽略。
好吧!就承認自己沒把書讀好,繞了太多冤枉路,發現錯誤也是學習的過程。
既然「Turi Create」是蘋果在 GitHub 上發表的專案,應該要好好搞懂「Turi Create」是怎麼一回事,希望這玩意兒如同蘋果自己在專案頁上宣稱「 You don’t have to be a machine learning expert to...」(您不需具備機械學習專家知識就能 ooxx ...)。
終於,透過 Turi Create 的 用戶指南網頁(User Guide),我總算完成第一個,從無到有的資料收集、整理,最終自己也覺得滿意的訓練模型,讓 iPhone 能「真正認識」 Gogoro 換電站惹!
這篇文章是簡單紀錄我的「Turi Create - Object Detection」學習過程。先說結論:與前一篇文章中使用 Xcode 內建的「Create ML」的無腦訓練方式相比, Turi Create 還是有一定程度的難度,不能像 Create ML 滑鼠拖拉簡單操作的方式。難度主要有兩個:
- 具備基本的 Python 語言基礎:毫無疑問,目前 Python 語言是機械學習入門基礎。過程中無法避免需要撰寫 Python 程式。
- 資料標註(Annotations)作業:這是真正的苦工,也直接影響模型的學習成果。
首先,當然是先把 Turi Create 裝起來,並順著 Turi Create 的 Object-Detection User Guide 文件,一步一步的做下去...
準備工作:
- 一台運作 macOS 10.13 以上,或 Ubuntu 16.04 的電腦。強烈推薦 macOS 10.14,因為 Turi Create 在 5 版以後,在 macOS 10.14 會自動以可用的 GPU 加速運算,訓練出來的模型也能直接用 iOS App 測試,本文以 macOS 10.14 為例。
- 安裝 Xcode 10 (beta) 。
這兩個條件和前一篇文章幾乎一樣,如何安裝 macOS 10.14 Mojave 與 Xcode,請參閱前一篇文章「教你的 iPhone 認識 Gogoro 換電站(Part 1) - 用黑蘋果電腦玩轉最夯的機械學習」。
另外,強烈推薦安裝一張 macOS 原生支援的獨立顯卡,經實測運行 Object Detect 模型所消耗的時間與資源,比 Image Classification 還要高出許多,若只用 CPU 運算,真的會跑到地老天荒。
安裝 Turi Create
Turi Create 是 Python 的模組,所以只需把 Python 跟 pip 裝起來之後,用 pip 來安裝即可。
macOS 安裝 Python:
雖然 macOS 已經內建 Python,但內建的是給 macOS 系統用的,也不能任意更動,否則 macOS 系統會出現一堆不能執行的內建程式。用戶使用的 Python 通常會另外安裝,安裝方式有兩種:Python 官網與 HomeBrew 安裝,擇一即可,我用的是官網安裝方式。
安裝方式很簡單,下載後執行它的 .pkg 依指示下一步就行了。安裝 3.6.x 或 2.7 皆可,也可兩種版本都安裝。不要安裝 3.7 版,因為目前 Turi Create 尚未支援。
安裝官網版之後,需再手動執行 Python 安裝目錄下的兩個 Command ,免得後面一堆問題。
pip 安裝 Turi Create
終端機視窗,一行就搞定:
$ pip install turicreate
不過目前這行指令下載的是 4 版,並不支援 macOS 10.14 環境下的 GPU 加速運算,所以建議安裝 5.0 beta 版:
$ pip install "turicreate==5.0b2"
第一次大概跑個幾分鐘之後,TuriCreate 就安裝完成了。若是使用 Python 3.6 的話,要用 pip3 指令:
$ pip3 install "turicreate==5.0b2"
這樣就完成了安裝,簡單執行一下看有沒有安裝完成:
沒什麼問題之後,順著「Object Detection」章節的教學範例操作一次。
使用 Turi Create 進行 物體識別(Object Detection)項目
(1) 首先,當然是下載訓練機器的範例資料。依照 Object Detect 教學文件(網址)中「Data Prepare」這節內容的指示,下載範例檔:
教學文件中要我們選擇 ig02-v1.0-bikes.zip 和 ig02-v1.0-cars 兩個就行了,目的很明顯的是要訓練出能識別「腳踏車(bikes)」和「汽車(cars)」主體的模型。下載後解壓縮,建一個目錄 ig02 後,把下載檔案連同目錄放進去,整理如下:
(2) 產生訓練模型
開啟一終端機視窗,先移到與 ig02 同一層的目錄下,再執行 python:
接著就依照教學文件中的指令照 Key 就行了。要特別注意每一行前面是否有空格,這個是撰寫 python 的特例,空格代表程式可執行的區段,空格沒對齊,後續程式就會出問題。
import turicreate as tc
import os
# Change if applicable
ig02_path = '~/Desktop/ig02'
# Load all images in random order
raw_sf = tc.image_analysis.load_images(ig02_path, recursive=True,
random_order=True)
# Split file names so that we can determine what kind of image each row is
# E.g. bike_005.mask.0.png -> ['bike_005', 'mask']
info = raw_sf['path'].apply(lambda path: os.path.basename(path).split('.')[:2])
# Rename columns to 'name' and 'type'
info = info.unpack().rename({'X.0': 'name', 'X.1': 'type'})
# Add to our main SFrame
raw_sf = raw_sf.add_columns(info)
# Extract label (e.g. 'bike') from name (e.g. 'bike_003')
raw_sf['label'] = raw_sf['name'].apply(lambda name: name.split('_')[0])
# Original path no longer needed
del raw_sf['path']
# Split into images and masks
sf_images = raw_sf[raw_sf['type'] == 'image']
sf_masks = raw_sf[raw_sf['type'] == 'mask']
def mask_to_bbox_coordinates(img):
"""
Takes a tc.Image of a mask and returns a dictionary representing bounding
box coordinates: e.g. {'x': 100, 'y': 120, 'width': 80, 'height': 120}
"""
import numpy as np
mask = img.pixel_data
if mask.max() == 0:
return None
# Take max along both x and y axis, and find first and last non-zero value
x0, x1 = np.where(mask.max(0))[0][[0, -1]]
y0, y1 = np.where(mask.max(1))[0][[0, -1]]
return {'x': (x0 + x1) / 2, 'width': (x1 - x0),
'y': (y0 + y1) / 2, 'height': (y1 - y0)}
# Convert masks to bounding boxes (drop masks that did not contain bounding box)
sf_masks['coordinates'] = sf_masks['image'].apply(mask_to_bbox_coordinates)
# There can be empty masks (which returns None), so let's get rid of those
sf_masks = sf_masks.dropna('coordinates')
# Combine label and coordinates into a bounding box dictionary
sf_masks = sf_masks.pack_columns(['label', 'coordinates'],
new_column_name='bbox', dtype=dict)
# Combine bounding boxes of the same 'name' into lists
sf_annotations = sf_masks.groupby('name',
{'annotations': tc.aggregate.CONCAT('bbox')})
# Join annotations with the images. Note, some images do not have annotations,
# but we still want to keep them in the dataset. This is why it is important to
# a LEFT join.
sf = sf_images.join(sf_annotations, on='name', how='left')
# The LEFT join fills missing matches with None, so we replace these with empty
# lists instead using fillna.
sf['annotations'] = sf['annotations'].fillna([])
# Remove unnecessary columns
del sf['type']
# Save SFrame
sf.save('ig02.sframe')
然後另外開一個終端機視窗,一樣是移到與 ig02 同一層的目錄,執行 python:
開始訓練模型:
import turicreate as tc
# Load the data
data = tc.SFrame('ig02.sframe')
# Make a train-test split
train_data, test_data = data.random_split(0.8)
# Create a model, 這裡加上 max_iterations = 500 縮短訓練時間,先把流程跑完
# 加上 _advanced_parameters={'batch_size':24} 則是解決 GPU 記憶體 < 4GB 導致 Loss 出現 nan 的問題
model = tc.object_detector.create(train_data,max_iterations = 500, _advanced_parameters={'batch_size':24})
# Save predictions to an SArray
predictions = model.predict(test_data)
# Evaluate the model and save the results into a dictionary
metrics = model.evaluate(test_data)
# Save the model for later use in Turi Create
model.save('mymodel.model')
# Export for use in Core ML
model.export_coreml('MyCustomObjectDetector.mlmodel')
這時候可聽到顯示卡風扇起飛的聲音,R9-280X 的溫度大約在 79~81 度。如果再跑久一點(max_iterations 數字加大),還可以隱約聞到房間空氣中一股清淡的塑膠味...
若 Loss 的值隨著 Iteration 進度而越來越小,表示訓練有正常進行。若 Loss 值越來越大,或是出現 nan 值,則表示訓練失敗,有很多種可能,得上網查一下原因。這次訓練在 Iterations = 500 的參數下,耗時大約六分鐘(如果是 CPU 大概要 1~2 個小時,你不會想等的),最後我們得到一個「MyCustomObjectDetector.mlmodel」的 Core ML 模型。可以將 python 命令中的「predictions」與「metrics」的內容印出來看看,評估這組模型的可信度如何。
如果把顯卡換成 GTX-780 進行訓練, 得到差異很大的結果:
batch_size = 24
batch_size = 16
相同訓練資料與參數下(batch_size = 24, max_iterations = 500),GTX-780 使用了比 R9-280X 更多的時間(557 秒 vs 342 秒),模型信任度也比較低。將 batch_size 下修到 16 時訓練時間有加快,但訓練出來的信任度更低,模型可用性就呵呵了。
在 Turi Create - Object Detector 的案例中,R9-280X 顯卡比 GTX-780 更適合拿來訓練,與前一篇 Xcode 10 的影像辨識是完全相反的結果。至於純 CPU 訓練所花的時間,初估約 R9-280X 的 9 ~ 10 倍,沒有等它跑完,因為實在太久了,相同的訓練參數下預估要一個多小時。
2019-01-26 補充:Radeon Vega 56 8G HMB2 的訓練成績
去年從蝦皮購入一張 Radeon Vega 56 礦卡後,滿心期待它的訓練表現,沒想到 macOS Mojave 10.14 系統竟然有 BUG 導致訓練失敗,直到這兩天 10.14.4 Beta 釋出後才解決這個問題。Radeon Vega 56 訓練結果如下:
batch_size = 24
相同的訓練集和參數,Radeon Vega 56 只花了 261 秒解決,比 Radeon R9-280X 快了約 30%。溫度的部分大約在 68~78 度之間徘徊。
有關 BUG 詳情請閱讀「黑蘋果安裝 AMD Radeon RX Vega 56」文章。
發佈至 iOS App
接著用 iOS 程式去實際驗證模型的使用方式和「信任度」(confidence)。
教學文件也提供了 swift 版的 iOS 程式範例,所幸對應 iOS 12 的程式不複雜,可以很快的改寫成熟悉的 Object-C 程式。取得的結果中,座標系統中的 Y 軸必須經過轉換,程式細節不多談,程式的重點如下:
MyDetector *mlmodel = [[MyDetector alloc] init];
// featureImage 是被偵測的圖片,UIImage 物件
CGSize featureImageSize = featureImage.size;
VNCoreMLModel *visionModel = [VNCoreMLModel modelForMLModel:mlmodel.model error:nil];
VNCoreMLRequest *objectRecognition = [[VNCoreMLRequest alloc] initWithModel:visionModel completionHandler:^(VNRequest * _Nonnull request, NSError * _Nullable error) {
if (request.results == nil) {
return;
}
if (request.results.count <= 0 ) {
return;
}
for (VNRecognizedObjectObservation *foundObject in request.results) {
VNClassificationObservation *bestLabel = [foundObject.labels firstObject];
CGRect objectBounds = foundObject.boundingBox;
//發現 Y 軸怪怪的,做修正
CGRect fixBouns = CGRectMake(objectBounds.origin.x,
1.0f - (objectBounds.origin.y + objectBounds.size.height),
objectBounds.size.width, objectBounds.size.height);
CGRect rect = CGRectMake(fixBouns.origin.x * featureImageSize.width,
fixBouns.origin.y * featureImageSize.height,
fixBouns.size.width * featureImageSize.width,
fixBouns.size.height * featureImageSize.height);
NSLog(@"%@ (%.2f %%) ==> %f,%f,%f,%f",bestLabel.identifier, bestLabel.confidence * 100 , rect.origin.x, rect.origin.y, rect.size.width, rect.size.height);
}
}];
objectRecognition.imageCropAndScaleOption = VNImageCropAndScaleOptionScaleFill;
VNImageRequestHandler *requestHandler = [[VNImageRequestHandleralloc] initWithCGImage:featureImage.CGImageoptions:@{}];
NSError *error = nil;
[requestHandler performRequests:@[objectRecognition] error:&error];
if (error) {
NSLog(@"VNImageRequestHandler:performRequests Error: %@",error.localizedDescription);
}
如果產生的模型要給 iOS 11 使用的話,model.export_coreml 需加上
include_non_maximum_suppression = False
參數:
model.export_coreml('MyCustomObjectDetector.mlmodel', include_non_maximum_suppression=False)
不過對應 iOS11 的程式會有個麻煩。必須經過一堆闕值過濾、抑制過濾、矩陣處理與轉換,這個部分教學指南仍然是 swift 版的範例,裡面有一段矩陣的寫法實在是看不太懂。還好有個對岸的同胞寫出對應 Object-C 的程式,兩相對照之下也弄懂了。而對應 iOS11 的 Core ML 模型,輸出的座標 Y 軸並不需要轉換,檢測的信任度值也比較低。
剛好搜尋到一張車子載著腳踏車的圖片,結果:
這張明確標示出汽車與單車。
這張只標示出單車,汽車的標示應該是被抑制掉了。
隨機上網搜尋腳踏車或汽車的圖片:
辨識度相當的不錯,不過對數量的判定有時會有誤差,會把一個物體標示成兩個以上。 iOS 12 的程式還沒研究如何手動調整「抑制度」,它是根據模型內的 Metadata 自動設置(預設值為 0.45,也就是兩塊區域交疊超過 45% 時會抑制其中一塊不顯示)。
從結果看來,範例的操作與訓練是成功的,不過訓練模型的資料是經過人為介入挖掘彙整,並經歷各種 AI 演算法的千錘百鍊之後,才成為教學範例。然而如同上一篇文章中所言,機械學習真正的價值,在於如何針對特定需求,去收集、挖掘出可用的訓練資料,否則即使會寫演算、精通程式,卻挖掘不到可用的訓練資料,一切也都是枉然。
所以再次用上次不算成功的「認識 Gogoro 換電站」為例,藉由這次學到的新訓練方式,嘗試看看結果如何。
再一次模擬情境:訓練辨識新的事物 - 「Gogoro 換電站」
初始資料收集就如同上次提到的,從官網的網頁中,撈取所有的換電站資料。這段時間又有幾個新開的站點,所以撈到的換電站圖片來到 955 張。然而當我再回到「cars & bikes」範例資料,試著從裡面找出彙整的邏輯,目睹的這一幕,讓全國兩千三百萬人都驚呆了...
範例使用的資料集,是將訓練資料標註為「image」與「mask」兩種類型,換言之,每一張拿來訓練的 image 原始資料,都得對應一張以上的 mask.(x) 圖片。 從訓練資料網站上的這段說明:
Details
Our team re-annotated cars, bicycles and people on the original set of images. Only some...
標註資料的工作是一整個團隊在執行,並非只有一人,每一張的 mask 都是經過人為標註與檢驗,仔細的把每一張訓練圖片,將物體的輪廓區域(紅色)和被遮蔽區(綠色)畫出來。
看到這個情況,再看到抓取的 955 張 Gogoro 換電站的原始圖片,我的眼眶濕了,兩行清淚不禁潸然落下....
如果依照上面的成功範例,那我得逐一標註出每張煥電站的 mask 圖片,對個人來說,這簡直是鉅大的工程。難道我...又再度卡關了嗎?接下來的幾天,再度陷入一連串的苦思:
「人為作業下,如何用最快速、最間單的方法,正確的標註訓練資料?」
為了尋找問題的答案,線索再回到練習的第一段 Python 程式,去研讀每一段 Python 到底在做什麼事情,是如何彙整 cars 和 bikes 當中個別標示為 image 與 mask 資料,最後又傳了哪些資料給機械去學習。經過很多天的摸索,研讀 Turi Create API 文件後,答案就在 python 程式的最後,我加上一行指令:
# 瀏覽訓練資料集內容
sf.explore()
終於又弄懂了,第一段 python 程式中,mark 圖片經過一連串高深莫測的 python 程序後,最終也只是產生 annonation 標註。每一個標註區域,也只是描述物體最大輪廓的矩形範圍和中心點座標罷了。大概像下圖這樣:
換言之,只要找出一個快速標註圖片的方法,產生對應且正確的 annonations 文字資料就行了,並不需要每張訓練圖片都得弄一張標註物體完整輪廓邊緣的 mask 圖片,畢竟那是很龐大的工程。
所以,與其要一張一張標出 mask 消耗大量時間的工作,不如再花個幾天時間,自行開發出一支可直覺、快速標註訓練圖庫的工具程式,對我反而還更容易些。
這支程式經過不斷的優化操作介面之後,標註一張換電站照片的時間,縮短到 10 ~ 20 秒。標註完成後自動生成 Python 程式碼,內容包含訓練與產生 .mlmodel 模型的指令,以及「一鍵執行訓練(Run Command)」功能。以後每次的訓練時,不需再寫任何 Python 程式了。
驗證模型
從 955 張 Gogoro 換電站圖片中快速挑了約 30 張,大約只花了十分鐘標註完成,加上 R9-280X 顯卡,只需五分鐘的訓練時間。
2019-01-26 補充:Radeon Vega 56 訓練自製 Gogoro 換電站資料集的成績:
batch_size = 24
batch_size = 32
速度比 Radeon R9-280X 快了約 34 ~ 80%。
<補充結束>
接下來是驗收成果的時候了。
底下這張也能辨識成功!
當然也找來競爭對手的假換電站,模型沒有被騙倒。
這個模型的信任度,達到了預期的目標。這還是只有 30 張圖的訓練成績,若能繼續加入更多種換電站外觀,以及各種情況下拍攝的訓練素材,相信會更加準確。
2019-10-24 補充:使用 Create ML 實施 Object Detector
從 Xcode 11 起,Create ML 終於有自己專屬的全新面貌,不再需要透過 Playground 下指令啟動了。可以在 Xcode 11 功能選單 Developer Tools 項目找到 Create ML,裡面有 8 種模型訓練,其中也包含了 Object Detector 訓練。
操作很簡單,快速說明一下。詳細的說明可參考 WWDC2019。
1. 將訓練素材(圖片)與標註資料( .json)放在同一個目錄。
該目錄包含多張圖檔與一個 json 格式的標註檔。根據蘋果的說明, json 的內容大概長得像這樣:
2. 開啟 Xcode,功能表 Xcode -> Open Developer Tool -> Create ML。
3. 選擇 Object Detector,
4. 填寫資訊
5. 選擇 1)訓練素材目錄 2)開始訓練。按照蘋果的說法,它會自動優先使用 Mac 電腦上獨立顯卡 GPU 來進行訓練,若沒有 GPU 的話則使用 CPU。訓練速度 GPU 比 CPU 快 9~13 倍。
使用一樣的 Gogoro 換電站訓練圖庫,Iterations 一樣設定在 500,Radeon VEGA 56 只花了 1 分 56 秒(116 秒)就完成了,比之前使用 Turi Create 的 250 秒足足快上一倍。
訓練完成之後,在 Output 按鈕位置會生成一個檔案圖示。用滑屬拖曳檔案圖示,就能把訓練好的模型擷取出來。Output 這頁還提供簡易的測試,直接把圖片拉進去就能看結果。
上面這張是網路搜尋的圖片,可信度 100%。
他廠的(假)換電站,依然逃不過法眼,沒有被騙倒。
Create ML 的 Object Detector 工具可節省撰寫 Python 程式碼的工作,並提供測試介面,使用上很方便。可惜的是,標註工作仍然得自行完成,所以使用額外的標註工具是必要的,先前為自行開發的快速標註工具,依然派得上用場。
心得後記 & 黑蘋果用於機械學習:
回想起這趟 Machine Learning 自學之路走得跌跌撞撞,前前後後也花費鉅大的學習時間,也多虧有蘋果另外提供的機械學習工具 Turi Create,大幅降低了門檻,如今總算是達標了,也學到了如何快速的挖掘與標註資料。從標註 Gogoro 換電站圖片開始,到模型的產出,前後只花了半個小時就有滿意的結果,對於不需特別嚴謹的應用,已經是足夠了。背後所代表的一股腦傻勁與心力付出,和學習過程中不斷的自我反省,可說是獲得難能寶貴的一課。
「黑蘋果」在 Core ML 領域有絕對的優勢。搭配 VEGA 10 GPU 的 iMac Pro 價格竟高達 15 萬 9 千 9 百元,搭配獨立顯卡的 MacBook Pro 一台也要七萬多且顯卡性能中下。而搭配高性能顯卡的黑蘋果電腦具備更好的訓練效率與更低廉的採購成本,Turi Create 適合在 macOS 環境中運行的特性,目前又是門檻最低的機械學習套件,讓安裝黑蘋果的理由又多了這一項。
隨著 iOS 12 與 macOS 10.14 Mojave 正式版即將於九月中旬釋出,除了更完整支援 Core ML 訓練與開發環境,尤其是 iOS 12 號稱可大幅改善目前所使用的手機 iPhone 5s 效能,使得今年的九月,更加令人期待。
留言列表