| 撰寫/攝影 | 曾吉弘 |
| 時間 | 1.5 – 2 hrs |
| 成本 | 具備webcam的筆記型電腦或桌上型電腦 |
| 難度 |
★★★☆☆ |
Lobe ai
Lobe ai 是微軟推出的離線版神經網路分類工具,相較於 Google Teachable Machine,離線就能訓練神經網路也許更適合教室使用。這股浪潮確實是資料驅動(data-driven)的時代,誰能掌握高品質的資料誰就能更快擁有合乎需求的神經網路。CAVEDU 2019 年所出版[實戰AI資料導向式學習]也有談到這個概念,使用 Raspberry Pi 搭配 Keras 訓練的視覺分類神經網路模型來實作路牌辨識機器人(讓我打一下書~嘛)
好,回歸正題,快開啟主頁面(https://lobe.ai)來看看!

根據主頁,目前可用的功能為影像分類,後續會推出物件偵測與資料分類等,非常令人期待。

下載與安裝
請由主頁右上角的 Download 選項來下載,安裝包大約 380MB。依照預設設定安裝完畢,開啟主畫面如下,相當清爽。
點選 New Project 來開啟新專案,目前只有影像分類可用囉

建立專案
空白專案畫面如下,我今天想要辨識兩種目標,都是玩具:薩克頭(zaku) 與 德姆頭 (DOM),因此總共需要三個類別,第三個類別是用來處理以上皆非的狀況,否則後續在辨識時就算看到別的東西,您的神經網路也只能薩克 / 德姆二選一。
Lobe 標榜每個類別只要5張照片就可以訓練,這真的是很厲害的數字啊!

點選右上角 Import,可以看到有三種建立資料集的方式,
Images: 從電腦端上傳照片
Camera: 使用 webcam 拍照
Dataset: 上傳已經整理好的資料集
在此使用 Camera 方便我們快速驗證。

建立資料集
在此總共建立三個類別:zaku、dom 與 other。
在上一步的 Import → Camera 之後,會出現即時的 webcam 畫面,點選中間的圓圈就會拍照,一直按著就會連拍。
本專案針對薩克與德姆各準備兩種不同顏色與略有不同的外型,希望能看看辨識的效果如何。可以看到左側 Unlabeled(未標註)的影片有 47張,後續再標註就好。
下圖左為沙漠型德姆,圖右為一般型德姆(在專業什麼..)
把想要被標誌為 dom 的照片全部選起來,點選任何一張照片左下角標籤名稱,就可以一次全部修改好。如果發現哪張照片糊了或是不滿意,都可以馬上刪除。

再來是薩克,一樣有藍色與紅色的薩克兩種。類別的照片數量請盡量保持差不多的數量。
最後是 other,這裡我放了人臉、水壺以及背景。但是請注意,可能會出現但不希望被辨識錯誤的項目才要放在 other 類別中,否則就不需要放進去喔。

訓練
發現了嗎?只要建立了第二個類別並標註之後,Lobe 就會自動開始訓練了。訓練速度和您的電腦規格有關,以我的筆記型電腦 (i7, 10GB ram),192 張照片大約10分鐘,相較於 Google Teachable Machine 的一分鐘之內來說這實在有點久,但離線執行確實有其方便之處,請您多多評估囉。
測試
測試分為 Images 與 Camera 兩個選項,前者讓您從電腦端上傳照片,可以看到以下這張照片被分類為 dom,分類錯誤!但您可以點選右下角的紅色禁止符號來告訴 Lobe 他做錯了並給予正確的標籤(zaku),之後再次訓練時就會更好喔!

接著是 Camera 即時預覽畫面,正確辨識為 zaku!您可以點選右下角的綠色勾勾將本張照片加入 zaku 類別,這時 lobe 也會馬上開始訓練。

多多調整,盡量讓每個類別的辨識結果都愈高愈好。

匯出
如果您滿意模型表現的話,就可以匯出(export)了,這也是這類工具最棒的地方。
請點選左上角的[三]圖示,開啟選單: Preferences → Project Settings,有兩個選項,分別是針對準確度最佳化以及針對速度最佳化。在此選擇後者,您可以兩個都比較看看。點選之後 Lobe 會需要一些時間完成最佳化。
接著就可以匯出了,請點選左上角的[三]圖示,開啟選單:Export,會看到 lobe 提供以下三個匯出選項:TensorFlow、TensorFlow Lite 與 Local API,請點選 TensorFlow Lite 就會開始匯出。
下載之後會出現一個與您專案同名並加上 TFLite 的資料夾(例如 DOM_ZAKU TFLite),會有 .example 資料夾,裡面有一個 python 程式可以跑跑看。另外就是 saved_model.tflite 與 signature.json 這兩個模型檔。

在此用 https://netron.app/ 這個神經網路視覺化工具來看看 .tflite 模型,層數約在80層左右,大量使用了 Conv2D 來進行卷積運算。

如果選擇 local API 會出現對應的語法與 parse result 說明

範例
馬上寫一個 python 來玩玩看。請先根據本篇文章在您的電腦上安裝好 Anaconda 與所需的套件。
執行語法:
[pastacode lang=”bash” manual=”python%20LOBE_WEBCAM_tflite.py%20–model%20%22DOM_ZAKU%20TFLite%22″ message=”” highlight=”” provider=”manual”/]
注意 LOBE_WEBCAM_tflite.py 要與 DOM_ZAKU TFLite 資料夾同一層。
LOBE_WEBCAM_tflite.py 內容:
[pastacode lang=”python” manual=”%23%0A%23%20%20————————————————————-%0A%23%20%20%20Copyright%20(c)%20CAVEDU.%20%20All%20rights%20reserved.%0A%23%20%20————————————————————-%0A%22%22%22%0ASkeleton%20code%20showing%20how%20to%20load%20and%20run%20the%20TensorFlow%20Lite%20export%20package%20from%20Lobe.%0A%22%22%22%0A%0Aimport%20argparse%0Aimport%20json%0Aimport%20os%0A%0Aimport%20numpy%20as%20np%0Afrom%20PIL%20import%20Image%0A%0Aimport%20cv2%0A%0Aimport%20tflite_runtime.interpreter%20as%20tflite%0A%0Adef%20get_prediction(image%2C%20interpreter%2C%20signature)%3A%0A%20%20%20%20%22%22%22%0A%20%20%20%20Predict%20with%20the%20TFLite%20interpreter!%0A%20%20%20%20%22%22%22%0A%20%20%20%20%23%20Combine%20the%20information%20about%20the%20inputs%20and%20outputs%20from%20the%20signature.json%20file%20with%20the%20Interpreter%20runtime%0A%20%20%20%20signature_inputs%20%3D%20signature.get(%22inputs%22)%0A%20%20%20%20input_details%20%3D%20%7Bdetail.get(%22name%22)%3A%20detail%20for%20detail%20in%20interpreter.get_input_details()%7D%0A%20%20%20%20model_inputs%20%3D%20%7Bkey%3A%20%7B**sig%2C%20**input_details.get(sig.get(%22name%22))%7D%20for%20key%2C%20sig%20in%20signature_inputs.items()%7D%0A%20%20%20%20signature_outputs%20%3D%20signature.get(%22outputs%22)%0A%20%20%20%20output_details%20%3D%20%7Bdetail.get(%22name%22)%3A%20detail%20for%20detail%20in%20interpreter.get_output_details()%7D%0A%20%20%20%20model_outputs%20%3D%20%7Bkey%3A%20%7B**sig%2C%20**output_details.get(sig.get(%22name%22))%7D%20for%20key%2C%20sig%20in%20signature_outputs.items()%7D%0A%0A%20%20%20%20%23%20process%20image%20to%20be%20compatible%20with%20the%20model%0A%20%20%20%20input_data%20%3D%20process_image(image%2C%20model_inputs.get(%22Image%22).get(%22shape%22))%0A%0A%20%20%20%20%23%20set%20the%20input%20to%20run%0A%20%20%20%20interpreter.set_tensor(model_inputs.get(%22Image%22).get(%22index%22)%2C%20input_data)%0A%20%20%20%20interpreter.invoke()%0A%0A%20%20%20%20%23%20grab%20our%20desired%20outputs%20from%20the%20interpreter!%0A%20%20%20%20%23%20un-batch%20since%20we%20ran%20an%20image%20with%20batch%20size%20of%201%2C%20and%20convert%20to%20normal%20python%20types%20with%20tolist()%0A%20%20%20%20outputs%20%3D%20%7Bkey%3A%20interpreter.get_tensor(value.get(%22index%22)).tolist()%5B0%5D%20for%20key%2C%20value%20in%20model_outputs.items()%7D%0A%20%20%20%20%23%20postprocessing!%20convert%20any%20byte%20strings%20to%20normal%20strings%20with%20.decode()%0A%20%20%20%20for%20key%2C%20val%20in%20outputs.items()%3A%0A%20%20%20%20%20%20%20%20if%20isinstance(val%2C%20bytes)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20outputs%5Bkey%5D%20%3D%20val.decode()%0A%0A%20%20%20%20return%20outputs%0A%0A%0Adef%20process_image(image%2C%20input_shape)%3A%0A%20%20%20%20%22%22%22%0A%20%20%20%20Given%20a%20PIL%20Image%2C%20center%20square%20crop%20and%20resize%20to%20fit%20the%20expected%20model%20input%2C%20and%20convert%20from%20%5B0%2C255%5D%20to%20%5B0%2C1%5D%20values.%0A%20%20%20%20%22%22%22%0A%20%20%20%20width%2C%20height%20%3D%20image.size%0A%0A%20%20%20%20%23%20ensure%20image%20type%20is%20compatible%20with%20model%20and%20convert%20if%20not%0A%20%20%20%20if%20image.mode%20!%3D%20%22RGB%22%3A%0A%20%20%20%20%20%20%20%20image%20%3D%20image.convert(%22RGB%22)%0A%0A%20%20%20%20%23%20center%20crop%20image%20(you%20can%20substitute%20any%20other%20method%20to%20make%20a%20square%20image%2C%20such%20as%20just%20resizing%20or%20padding%20edges%20with%200)%0A%20%20%20%20if%20width%20!%3D%20height%3A%0A%20%20%20%20%20%20%20%20square_size%20%3D%20min(width%2C%20height)%0A%20%20%20%20%20%20%20%20left%20%3D%20(width%20-%20square_size)%20%2F%202%0A%20%20%20%20%20%20%20%20top%20%3D%20(height%20-%20square_size)%20%2F%202%0A%20%20%20%20%20%20%20%20right%20%3D%20(width%20%2B%20square_size)%20%2F%202%0A%20%20%20%20%20%20%20%20bottom%20%3D%20(height%20%2B%20square_size)%20%2F%202%0A%20%20%20%20%20%20%20%20%23%20Crop%20the%20center%20of%20the%20image%0A%20%20%20%20%20%20%20%20image%20%3D%20image.crop((left%2C%20top%2C%20right%2C%20bottom))%0A%0A%20%20%20%20%23%20now%20the%20image%20is%20square%2C%20resize%20it%20to%20be%20the%20right%20shape%20for%20the%20model%20input%0A%20%20%20%20input_width%2C%20input_height%20%3D%20input_shape%5B1%3A3%5D%0A%20%20%20%20if%20image.width%20!%3D%20input_width%20or%20image.height%20!%3D%20input_height%3A%0A%20%20%20%20%20%20%20%20image%20%3D%20image.resize((input_width%2C%20input_height))%0A%0A%0A%20%20%20%20%23%20make%200-1%20float%20instead%20of%200-255%20int%20(that%20PIL%20Image%20loads%20by%20default)%0A%20%20%20%20image%20%3D%20np.asarray(image)%20%2F%20255.0%0A%0A%20%20%20%20%23%20format%20input%20as%20model%20expects%0A%20%20%20%20return%20image.reshape(input_shape).astype(np.float32)%0A%0A%0Adef%20main()%3A%0A%20%20%20%20%22%22%22%0A%20%20%20%20Load%20the%20model%20and%20signature%20files%2C%20start%20the%20TF%20Lite%20interpreter%2C%20and%20run%20prediction%20on%20the%20image.%0A%0A%20%20%20%20Output%20prediction%20will%20be%20a%20dictionary%20with%20the%20same%20keys%20as%20the%20outputs%20in%20the%20signature.json%20file.%0A%20%20%20%20%22%22%22%0A%20%20%20%20parser%20%3D%20argparse.ArgumentParser(%0A%20%20%20%20%20%20%20%20formatter_class%3Dargparse.ArgumentDefaultsHelpFormatter)%0A%20%20%20%20parser.add_argument(%0A%20%20%20%20%20%20%20%20′–model’%2C%20help%3D’Model%20path%20of%20.tflite%20file%20and%20.json%20file’%2C%20required%3DTrue)%0A%20%20%20%20args%20%3D%20parser.parse_args()%0A%20%20%20%20%0A%20%20%20%20with%20open(%20args.model%20%2B%20%22%2Fsignature.json%22%2C%20%22r%22)%20as%20f%3A%0A%20%20%20%20%20%20%20%20signature%20%3D%20json.load(f)%0A%0A%20%20%20%20model_file%20%3D%20signature.get(%22filename%22)%0A%0A%20%20%20%20interpreter%20%3D%20tflite.Interpreter(args.model%20%2B%20’%2F’%20%2B%20model_file)%0A%20%20%20%20interpreter.allocate_tensors()%0A%0A%20%20%20%20cap%20%3D%20cv2.VideoCapture(0)%0A%20%20%20%20%23%E6%93%B7%E5%8F%96%E7%95%AB%E9%9D%A2%20%E5%AF%AC%E5%BA%A6%20%E8%A8%AD%E5%AE%9A%E7%82%BA640%0A%20%20%20%20cap.set(cv2.CAP_PROP_FRAME_WIDTH%2C640)%0A%20%20%20%20%23%E6%93%B7%E5%8F%96%E7%95%AB%E9%9D%A2%20%E9%AB%98%E5%BA%A6%20%E8%A8%AD%E5%AE%9A%E7%82%BA480%0A%20%20%20%20cap.set(cv2.CAP_PROP_FRAME_HEIGHT%2C%20480)%0A%0A%20%20%20%20key_detect%20%3D%200%0A%20%20%20%20times%3D1%0A%20%20%20%20while%20(key_detect%3D%3D0)%3A%0A%20%20%20%20%20%20%20%20ret%2Cframe%20%3Dcap.read()%0A%0A%20%20%20%20%20%20%20%20image%20%3D%20Image.fromarray(cv2.cvtColor(frame%2Ccv2.COLOR_BGR2RGB))%0A%0A%20%20%20%20%20%20%20%20if%20(times%3D%3D1)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20prediction%20%3D%20get_prediction(image%2C%20interpreter%2C%20signature)%0A%0A%20%20%20%20%20%20%20%20print(‘Result%20%3D%20’%2B%20prediction%5B’Prediction’%5D)%0A%20%20%20%20%20%20%20%20print(‘Confidences%20%3D%20’%20%2B%20str(max(prediction%5B’Confidences’%5D))%20)%0A%20%0A%20%20%20%20%20%20%20%20cv2.putText(frame%2C%20prediction%5B’Prediction’%5D%20%2B%20%22%20%22%20%2B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20str(round(max(prediction%5B’Confidences’%5D)%2C3))%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20(5%2C30)%2C%20cv2.FONT_HERSHEY_SIMPLEX%2C%201%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20(0%2C0%2C255)%2C%201%2C%20cv2.LINE_AA)%0A%20%20%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20cv2.imshow(‘Detecting….’%2Cframe)%0A%0A%20%20%20%20%20%20%20%20times%3Dtimes%2B1%0A%20%20%20%20%20%20%20%20if%20(times%20%3E%3D%205)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20times%3D1%0A%0A%20%20%20%20%20%20%20%20read_key%20%3D%20cv2.waitKey(1)%0A%20%20%20%20%20%20%20%20if%20((read_key%20%26%200xFF%20%3D%3D%20ord(‘q’))%20or%20(read_key%20%3D%3D%2027)%20)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20key_detect%20%3D%201%0A%0A%20%20%20%20cap.release()%0A%20%20%20%20cv2.destroyAllWindows()%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20main()%0A” message=”” highlight=”” provider=”manual”/]執行效果
執行效果如下,會開啟一個預覽畫面,並把辨識結果(label)與信心指數顯示出來。
與 Google Teachable Machine 比較與展望
最後免不了要與Google Teachable Machine (簡稱TM)比較一下,暑期的 AIGO 高中職生扎根營隊我們大量使用 TM來匯出視覺分類模型,也可以放在 Raspberry Pi、Jetson Nano 上執行,或者連動 7697 等這類MCU板來做到各種有趣的 AIoT 應用。
但Lobe標榜離線運作的話,教室的網路壓力就不會這麼高了,很期待各位後續的意見分享喔!
| Lobe.ai | Teachable Machine | |
| 網路需求 | 下載安裝包才需要,之後都可離線使用 | 連線到網站使用,全程都須使用網路 |
| 訓練速度 | 於電腦端進行運算
根據該電腦等級而有明顯差異 200張約3-5分鐘(i7 / 16GB ram) |
於電腦端進行運算
幾乎都可在1分鐘內完成 |
| 支援之神經網路 | 視覺分類
物件辨識(尚未) 資料分類(尚未) |
視覺分類
聲音分類 姿勢分類 |
| 匯出模型格式 | TensorFlow
TensorFlow Lite |
TensorFlow
TensorFlow Lite TensorFlow js |
| TFLite 神經網路層數 | 79 | 74 |


















您好:測試上方LOBE_WEBCAM_tflite.py python程式,顯示 prediction[‘Prediction’] 名稱無法找到,請問要在哪裡修正,感謝
您好,請問是用這個語法執行嗎?
python LOBE_WEBCAM_tflite.py --model "DOM_ZAKU TFLite",DOM_ZAKU TFLite 是 LOBE 匯出的 tflite 資料夾,裡面有 .tflite / labels.txt,請注意 LOBE_WEBCAM_tflite.py 要與 DOM_ZAKU TFLite 資料夾同一層。發現是get_prediction 回傳變數沒有該欄位名稱,我自行新增後已可使用,感謝您的回覆。
您好,請問可以示範一下如何更正嗎?感謝您
請問您要更正什麼地方呢?
不好意思,經過嘗試之後會顯示下列的錯誤:
想請教一下是什麼原因呢?
Traceback (most recent call last):
File “LOBE_WEBCAM_tflite.py”, line 142, in
main()
File “LOBE_WEBCAM_tflite.py”, line 119, in main
print(‘Result = ‘+ prediction[‘Prediction’])
KeyError: ‘Prediction’
非常感謝您的協助,敬上最大感謝^_^