前言
在 2022 年上線的「BlueTrend 海洋生物資料庫」很順利的穩定運作中,截止目前為止共累積了 8,122 位會員、81 位社群分類員,以及蒐集到超過一萬兩千張的海洋生物照片,這個數字放眼全球的生態資料庫應該都是非常亮眼的成績,等未來拓展到海外勢必會有更驚人的資料量。
但從數字中可以發現已上傳的一萬多張照片中,已經辨識的照片數量還不到一半,沒有辨識的照片就無法得知該物種的相關資訊,像是地理位置、水下深度、水溫等等,這對於生態維護與周邊海域的效益就非常有限。
期間 Bluetrend 的 Spark 團隊也試過許多活動以及跟學術單位合作來提升照片的辨識率,不然已辨識照片的數量可能還會更低,正當大家苦思該如何提升辨識率的同時,AI 技術的突飛猛進提供我們一個契機:使用機器圖像辨識來分類物種。
確定這個方向後,我們從 2023 年底開始著手進行,期間歷經了無數的討論、爬了一堆文件、不停的修改程式碼,剩下的就是測試再測試,而該專案也榮獲了台灣大哥大基金會 Tech The Dreamers 科技夢想+ 第三屆的團隊,並於 2024 四月正式完成上線:https://www.cna.com.tw/postwrite/chi/367792
在開發 AI 圖像辨識之前,我是機器學習小白無誤,我完全沒有這方面的相關知識,每天只會用 ChatGPT 問些低級問題,所以當被告知要從蒐集圖片到訓練模型,甚至要串接辨識結果到網站中時,當下除了興奮外,更是佩服&感謝 Spark 團隊願意信任我這個 AI 零經驗的工程師。
這過程中學到超級多,也踩了不少雷,更是遇到許多前輩的熱心指導,這篇文章紀錄了我的學習跟開發過程,希望能讓有興趣投入機器辨識的朋友少走一些冤枉路。先來看一下我們成果:
僅有的三個畫面,背後花了超過三個月XD
一、使用情境
在使用者上傳照片選擇照片分類時,系統會根據該分類是否有提供 AI 辨識功能來進行判斷,如果有的話會詢問使用者是否要進行機器辨識,確認後就會開始等待辨識結果,辨識完成後會顯示信心度超過 N 分的的結果,最後再透過人為的判斷來決定是那個分類,
理論上大家應該都會選擇信心度最高的結果,可能會有人問說為何不自動取信心度最高的結果來進行分類,然而實際情況是有些照片辨識出來的結果信心度非常接近,這時候就是還是需要人為介入來進行判斷,整個過程可以想成是電影賭俠裡面海珊弄小刀的經典場景XD:
至於後台的部分有紀錄每次辨識的信心度,之所以要記錄是因為模型有可能因為某些原因失真,譬如說把鹿辨識成馬,這個時候透過信心度的資料與人為的判斷就能知道這個模型中猴了,就需要再次訓練或是打掉重練。
重新訓練的部分透過前輩得知也能做到全自動化,也就是先設好條件讓系統自行判斷何時該重新訓練,譬如連續辨識 N 次的結果都沒有被使用者採納,當滿足條件後就會透過 API 自動觸發重新訓練的流程,這是 MLOps 的領域,這專案還沒搞剛到這種程度,有興趣的朋友可以自行研究~
二、技術選擇
既有圖像辨識模型
在實作這專案時剛好遇到 OpenAI 推出他們的圖像辨識服務 Vision,透過 API 就能進行圖像辨識,實測結果 10 次裡面會辨識正確 2~3 次,如果是個人興趣使用或許還行,而當時 Vision 尚未提供自行訓練模型的功能,以此在這專案中就不適用。
另外一個評估的模型是桃園的 Xpark 海洋水族館,之前去逛的時候有使用他們名為 「LINNÉ LENS」的手機 App,安裝後可以透過它即時辨識眼前看的海洋生物是什麼物種:
研究發現這間日本的公司有提供技術給全球的海洋水族館作物種辨識,雖然官網沒寫但感覺一定有 API 可以串接,因此本想說直接寫信跟他們合作,就能省下自己訓練模型的成本,但礙於本專案的性質以及考量到資料的完整度,最後還是以自行訓練為優先選擇。
公有雲的選擇
因為沒有機器學習相關的知識,在技術選擇上以三大公有雲主機提供的機器學習服務為尋找方向,而網路上查到的資料與相關文件以 Google 雲比較完整,再加上有前輩可以請教,最後就以 Google 的 Vertex AI 作為我們的訓練&辨識模型。
訓練成本
Vertex AI 的收費點主要是儲存空間、訓練時間以及辨識模型 API。要使用 Vertex AI 進行圖像模型訓練一定要把要訓練資料上傳 Google Cloud Storage,因此會有相關費用的產生,不同地區有不同價格,詳細可以參考官方說明:https://cloud.google.com/storage/pricing?hl=zh-tw
訓練圖像辨識模型要新增儲存空間,首先要建立 Bucket ( 值區 ) 也就是一個容器的概念,撰文當下要做模型訓練只有北美機房支援,因此我們選擇 us-central1,並且使用單一地區,另外根據儲存資料的用途不同還可以選擇空間級別,我們選擇的是 Standard,以下是我們儲放訓練資資料的值區設定方式:
目前我們的訓練照片資料量大概是 45 GB 左右,每個月的儲存費用還不到 1 塊美金,只有在訓練時有資料傳輸才會有額外的費用產生,除非是要上到 TB 等級的訓練資料量,否則儲存的成本低到幾乎可以忽略。
其次是訓練的費用,是依照訓練時數與節點數量來計算,時間的部分就是訓練時間跑多久就算多久,以訓練 3,395 張照片為例,訓練完成花了 1 小時 33 分,如果想要快一點的話,可以用多個節點下去跑,但節點越多總費用就越高。
另外模型還有分線上或是邊緣部署,前者直接會把訓練好的模型放在 Google 上,要用的時候以 API 請求即可,邊緣部署則是把模型下載下來放在自己的主機上,要用的時候需要自己開發 API 讓網站存取。
有了以上概念對於 Vertex AI 的訓練費用就會比較有概念:
最後是部署的費用,前面提到我們可以把訓練好的模型放在 Google 的主機上,並用他們內建的 API 來進行圖像辨識的操作,可以省去我們自行準備機器以及開發 API 的時間,以上圖的價目表來看,只要模型放在 Google 就會計費,並且實際進行辨識的讀取時間也會算錢。
然而我們在開發時忽略了這件事,當時訓練完後想說就用他們的機器去部署並測試辨識結果,但測試完後忘記把機器關掉,導致整個服務都還沒上線就噴了超多錢 > <,於是後來我們改用邊緣部署的方式來做,不然這筆開銷很容易造成營運的負擔。
三、模型架構
確定了技術之後,接下來我們進一步思考一個模型到底要放多少資料?資料太大會不會導致辨識速度慢、資料太少一次辨識存取多個模型會不會更花時間?在完全沒有概念的情況下只能透過不斷的測試來尋找最佳解。
這邊先簡單介紹一下我們的資料結構,物種的層級有四層關聯,分別是祖分類 > 父分類 > 子分類 > 孫分類,我們的目標是當上傳照片時選擇祖分類,就會開始進行圖像辨識,辨識完的結果是孫分類:
第一個想法是一個孫分類是一個模型,原因是一個孫分類一個模型要重新訓練比較方便,辨識的時間理論上也會比較短,但對網站來說就比較不方便使用,由於上傳照片時的欄位只能選擇祖分類,因此我要撈出祖分類底下所有的孫分類模型,並且去迴圈判斷當 A 孫分類模型辨識結果都沒有超過一定的信心度時,再去請求 B 孫分類模型的結果。
以此類推,如果 B 也沒有就必須 C → D → E 一個一個送照片去辨識是否有符合的結果,當時在思考時就覺得重複 API 請求一定會花不少時間,對使用者來說感受上可能會不太好。第二個想法是是從父或子分類的層級來訓練模型,但依舊跑不掉迴圈判斷的邏輯。
最後我們的做法是訓練祖分類這個大模型,把屬於同一個祖分類的孫分類全部都塞進同一個模型之中,實際測試的結果還不錯,只有第一次請求的時候約需要十秒左右的等待,同一個模型第二次請求就可以秒辨識,而且不管是線上部署還是邊緣部署時間都差不多,這樣的測試結果讓我們非常滿意!
四、訓練模型
依照 Google 官方的建議,一個辨識結果最好要有 3000 張的照片來訓練才會得到比較準確的預測,實測結果如果待辨識的照片品質很好,像是被攝主體的輪廓、角度、清晰度都不錯的話,大概使用 200~500 張照片訓練的模型就能取得理想的結果,但如果是長得太像的物種就會不準。
像是以海龜來說,就算是不同種類但他們的外型都很接近,差別可能只是殼的花紋或是體型的大小,因此要取得更準確的結果就需要更精細的訓練資料,像是只拍攝針對龜殼花紋的大量照片才有可能取得比較好的辨識。
所以假設今天要能夠辨識出「冠刺棘海膽」這個物種,訓練模型的步驟如下:
(一)蒐集 3000 張冠刺棘海膽的照片
盡可能蒐集到該物種各個角度的照片,多虧海洋生物資料庫已經取得許多辨識過的照片,在基礎資料的蒐集上就有很好的開始,不足的部分再去網路上找圖,把單一物種可能呈現的樣貌都囊括其中。
(二)在 GCS ( Google Cloud Storage ) 建立冠刺棘海膽的值區
上述提及訓練資料必須放在 Google 的 Storage,因此當照片蒐集完成後必須要把他們上傳到 GCS,Vertex AI 才能讀取到這些照片,值區內可以建立不同資料夾來區分物種。
(三)彙整資料集 Dataset
資料集是訓練資料的集合,要訓練模型必須要透過資料集來訓練,我們可以在 Vertex AI 後台建立它,我們的作法是一個資料集對應一個模型,如果一個模型用多個資料集進行訓練也是可以:
接下來就要將資料集的內容整理好後進行上傳,資料集定義了照片的路徑與對應的標籤(物種),使用 csv 格式來彙整,長得像這樣:
gs://echinothrix/diadema/10559679065_50d2b16f6d.jpg,echinothrix_diadema
gs://echinothrix/diadema/10828951106_c3cd47983f.jpg,echinothrix_diadema
...
一行一個 GCS 圖片路徑與標籤名稱,echinothrix_diadema 是冠刺棘海膽的學名,所以一個有 3000 張照片的資料集就是一個有 3000 行的 csv 檔,在這之前還要先上傳照片以及將照片路徑整理出來,這要人工整理會瘋掉,因此這樣的作法比較適合用程式處理。
另外一種整理方式可以直接在 Vertex AI 的進行操作,在建立好資料集後點擊進入,可以直接在介面中上傳圖片到 GCS:
上傳完成後切換到「瀏覽」頁籤,先新增物種的標籤,接著全選照片並指派標籤,就可以批次完成資料集的整理:
(四)訓練模型
當所有照片都有指派好標籤後,就可以在資料集的右上角看到訓練新模型的按鈕:
點擊後會出現模型訓練的確認視窗,比較需要注意的有以下兩個地方:
首先是模型的使用位置,上述提到為了節省模型的部署成本,我們選擇使用 Edge 也就是邊緣部署的方式將模型放在自己的主機上來使用,雖然要自行開發 API 以及確保模型的可用率,但實際比較之後辨識結果沒有差太多。
其次是關係到訓練費用的節點數:
訓練費用是以節點數與訓練時間來計算,以我們使用的 Edge 邊緣部署為例,定價為每節點時數價格 18 美元,也就是說我用 1 個節點訓練 1 小時,費用是 1 節點 x 1 小時 x 18 美元 = 18 美元,公式是:
總費用 = 節點數量 × 訓練時間(小時)× 每節點時數價格
由於訓練時數會跟資料集的大小有關,很難預估大概會跑多久,因此為了避免荷包爆炸,這邊可以設定預算,這邊輸入的是小時節點數,也就是如果我們希望預算控制在 100 美元以內並只跑一小時的話,節點數上限為 100 元 / (1 個節點 x 每小時 18 元) = 5.6 取整數 5,公式是:
節點數上限 = 金額預算 / ( 所需節點數 x 節點數每小時單價)
因此在相同 100 元的預算下,如果不希望花費太多時間可以增加節點數,100/(2×18) = 2.7 取整數 2,但萬一模型太大就會導致預算花完了但模型還沒訓練完,因此下方的「啟用提早中止訓練功能」一定要記得勾,可以減少訓練的花費。
訓練完成後就能實際上傳照片來測試模型的準確度,但前提是要先把模型發佈到端點,也就是將模型部署在 Google 的主機上,由於一部署就要計費了,因此我們採取把模型載回來部署在自己主機上的方式來測試,並於網站後台紀錄每次辨識的信心度,再由專家判斷該模型是否失真:
五、邊緣部署
接著說明如何取得訓練好的模型並上傳到主機。首先進入要匯出的模型找到上方的匯出按鈕,並以 TensorFlow.js 的格式匯出,要以 Edge 模式訓練的模型才有辦法匯出:
匯出後它不會給你一個檔案直接下載,而是會儲存到 GCS 裡面,因此模型匯出後我們要去匯出時指定的 GCS 路徑找到模型:
匯出的資料夾名稱會以 model- 加一串數字開頭,點進去後會是一個 tf-js 的資料夾,而我們需要將這資料夾下載到我們電腦,所以勾選資料夾後點選右上角的下載,就會出現 gsutil 的指令,這是 Google 專用的終端機指令,我們必須先在電腦安裝好。
具體的安裝方式可以參考官方說明:https://cloud.google.com/storage/docs/gsutil_install#install
裝好後開啟終端機,首先執行 gcloud init
指令來進行授權,授權的過程會需要進行 Google 登入並選擇需要授權的專案,找到有啟用 Vertex AI 的專案名稱後,用數字進行選擇,就能完成本機的授權作業。
接下來回到 GCS 的網頁複製下載的指令並於本機執行,就能將我們所需的模型下載回來,打開模型資料夾後會發現以下的檔案:
model.json 是讀取模型的引入檔,以 group 開頭的 .bin 檔則是模型本身,根據模型的大小數量不一定,dict.txt 是所有的標籤,因此只要這些檔案上傳到主機上,並透過前端來讀取 model.json 就能載入所需的模型。要注意的地方伺服器的設定要放行 .bin 不然會讀取不到。
六、取得辨識結果
透過 TensorFlow.js 讓我們可以很方便的在前端取得辨識結果,首先引入相關的 JS,可以透過 NPM 安裝或是直接引入:
<script src="https://unpkg.com/@tensorflow/tfjs"></script>
<script src="https://unpkg.com/@tensorflow/tfjs-automl"></script>
接下來就可以用 tf automl 物件的 loadImageClassification 方法來讀取模型的引入檔 model.json:
const model = await tf.automl.loadImageClassification('/model.json');
然後使用 classify 方法就可以取得辨識結果:
const predictions = await model.classify(image);
classify 帶的參數 image 是需要透過 tf 的 fromPixels 方法來轉換格式,如果你的圖片是放在 id 名為 img 的 <img> 標籤裡面,可以這樣取得 image:
const imgElement = document.getElementById('img');
const image = tf.browser.fromPixels(imgElement);
由於該專案上傳的圖片都是放在第三方圖床,因此需要從外部網址來取得 image:
async function loadImageFromUrl(url) {
return new Promise((resolve, reject) => {
const image = new Image();
image.crossOrigin = "anonymous";
image.onload = () => {
const tensor = tf.browser.fromPixels(image);
resolve(tensor);
};
image.onerror = reject;
image.src = url;
});
},
loadImageFromUrl 帶入一個網址參數就可以回傳 tf 所需的 image 格式,這邊以圖片在 <img> 標籤內為範例,重新整理上述的程式碼:
<script src="https://unpkg.com/@tensorflow/tfjs"></script>
<script src="https://unpkg.com/@tensorflow/tfjs-automl"></script>
<script>
const imgElement = document.getElementById('img');
const image = tf.browser.fromPixels(imgElement);
const model = await tf.automl.loadImageClassification('/model.json');
const predictions = await model.classify(image);
</script>
predictions 回傳的範例如下:
[
{
"label": "echinothrix_diadema",
"prob": 0.834123
},
{
"label": "echinothrix_calamaris",
"prob": 0.54839
},
{
"label": "Tripneustes_gratilla",
"prob": 0.014233
}
]
label 是標籤名稱,prob 是信心度,取得這些資料後就可以判斷當辨識完成後取 prob 高於 0.8 的 label 做顯示,或是將 prob 超過 0.5 的 label 全部列出來讓使用者決定哪個才是正確答案,這部分就看遊戲規則怎麼設定。
結論
不得不說有了 Vertex AI 的協助讓完全沒有機器學習知識的我也能夠開發出圖像辨識這種強大的功能,花比較多時間的地方在於熟悉 Vertex AI 的操作介面與專有名詞,至於在開發的體驗上覺得 Google 的文件真的好多,多到都不知道要看哪個才是正確的 > <
但如果是用線上部署的話 API 用法會直接附在後台的側邊欄,只能說 Google 為了賺這一條錢設計了很多貼心的地方讓開發者容易入坑XD
希望這篇整理能幫助到也想入坑圖像辨識的你!有相關問題都可以來信跟我討論~
參考資料
Vertex AI 訓練資料 API 文件
https://cloud.google.com/vertex-ai/docs/reference/rest
https://cloud.google.com/vertex-ai/docs/training-overview
Vertex AI 設定教學
https://dotblogs.com.tw/jakeuj/2023/08/21/Google-Cloud-Vertex-AI
PHP SDK
https://cloud.google.com/php/docs/reference/cloud-ai-platform/latest
邊緣部署教學
https://cloud.google.com/vision/automl/docs/tensorflow-js-tutorial