最近在想一個問題... 你覺得,只靠一個比指甲還小的感測器,有可能準確知道你現在是在深蹲、弓箭步,還是只是躺在沙發上耍廢嗎?
聽起來有點像科幻片對吧?但這就是我前陣子在弄的一個專案,叫做「人體活動辨識」(Human Activity Recognition, HAR)。老實說,一開始拿到這個題目,我跟我的夥伴 [Gesina] 都有點傻眼,因為限制是:只能用「單一」一個三軸加速規。市面上的運動手錶哪個不是塞滿了各種感測器,我們只有一個,這能玩嗎?
但這個限制反而...嗯...蠻酷的。它逼我們不能無腦地堆硬體,而是得從演算法跟數據處理下手。這整個過程踩了不少坑,但也學到很多,想說來分享一下。
所以,我們到底做了什麼?
簡單講,我們的目標就是做一個小盒子,你把它佩戴在身上,它就能即時猜出你在做什麼運動。整個系統的核心,其實就三樣東西:
- 感測器:我們用的是 ADXL343 加速規。你可以把它想像成一個小小的方塊,裡面有三個軸(X, Y, Z),專門偵測往前、往旁、往上的加速度變化。
- 大腦:一塊 Raspberry Pi(樹莓派)。所有的數據都送到這裡來處理、分析。
- 演算法:這才是靈魂。我們用機器學習模型來「教」電腦認識不同動作的數據長什麼樣子。
我自己是負責硬體跟收數據的部分啦,所以先把東西兜起來。這個 ADXL343 加速規,在國外像是 Adafruit 這種 DIY 電子零件網站超級熱門,教學文件也寫得很好。不過呢,這東西在台灣也超好找,去趟光華商場或上網拍隨便搜,基本上都有,而且很多社群也都在玩,不怕卡關。
硬體接線不難,就是把感測器用 I2C 介面跟 Raspberry Pi 連起來。比較麻煩的是要怎麼讓它穩定工作。我們還加了兩顆 LED 燈,綠燈代表正常、紅燈代表出錯,還有一個按鈕用來開始跟停止記錄。千萬別小看這兩顆燈,它在除錯的時候真的救了我好幾次... 至少讓我知道機器是活著的。
# 初始化 I2C 跟 ADXL343
try:
i2c = busio.I2C(board.SCL, board.SDA)
accelerometer = adafruit_adxl34x.ADXL343(i2c)
# 成功的話,綠燈亮一下
green_led.on()
time.sleep(1)
green_led.off()
except Exception as e:
# 失敗就直接亮紅燈,然後閃退
print(f"加速規設定出錯: {e}")
red_led.on()
exit()
對了,還有一個關鍵決定:感測器要放哪?我們最後選了「髖部」,大概就是褲子側邊口袋的位置。因為髖部接近人體的重心,不管你做什麼動作,這裡的數據變化都會很明顯,CP值最高。
怎麼「教」電腦認識動作?
硬體搞定後,接下來就是最累也最重要的一步:收集訓練資料。這部分說真的...有點蠢。我就把那個小盒子固定在腰上,然後開始做各種運動。
我們錄了像是深蹲、弓箭步、髖部繞環、俄羅斯轉體、側抬腿、走路、坐下、躺下...等等九種動作。每做一個動作,就按一下按鈕開始記錄,做完再按一下停止。如果綠燈有規律地閃爍,就代表數據有成功上傳到我們的資料庫(MongoDB)。
這邊有個小技巧。我們不是每收到一筆數據就馬上上傳,那樣太耗電也太占網路資源。我們用了一個「微批次」的方法,每收集到 60 筆樣本(大概是 3 秒的活動量),再一次打包上傳。算是一種效能跟即時性之間的平衡吧。
這個過程也讓我體會到,訓練資料的「一致性」和「多樣性」有多重要。有時候我只是稍微改變一下深蹲的姿勢,數據看起來就差很多。所以後來我們每個動作都錄了好幾組,盡量讓模型看過各種奇奇怪怪的、不標準的動作,這樣它的判斷才會更準、更貼近真實情況。
讓魔法發生的演算法
好,原始數據只是一堆數字,電腦看不懂。所以我們得做「特徵提取」(Feature Extraction),把這些數字變成電腦能理解的「特徵」。
我們從每 3 秒的數據窗口中,提煉出 15 個特徵,例如:
- 平均值 (Mean): 每個軸向的平均加速度,可以看出動作的大致方向。
- 範圍 (Range): 每個軸向的最大值減最小值,可以看出動作的幅度大不大。
- 均方根 (RMS): 測量整體的平均強度,動作越劇烈,這個值就越高。
- 相關性 (Correlation): 不同軸向之間的關聯性。 مثلاً,走路時,往前(X軸)和上下(Z軸)的晃動就會有很強的關聯。
有了這些特徵之後,就可以把它們餵給不同的機器學習模型去訓練了。我們試了好幾種,結果真的差蠻多的。
哪個模型才是真正的MVP?
我們把同樣的資料餵給了幾種常見的模型,想看看誰的表現最好。這過程就像一場選秀會,每個模型都有它的脾氣。
| 模型 | 準確率 | 我的主觀評價 |
|---|---|---|
| Random Forest (隨機森林) | 大概 95.2% | 根本是這次的MVP。速度快、效果又好,不太需要什麼複雜的調整,很穩定。 |
| Support Vector Machine (SVM) | 差不多 91% | 也還不錯啦,算是資優生,但有時候有點死板,對某些模糊的動作判斷力稍弱。 |
| K-Nearest Neighbors (KNN) | 大概 88% | 很直覺的模型,但腦筋不太好... 很容易被一些奇怪的數據點誤導,準確率不太穩定。 |
| Decision Trees (決策樹) | ~85% | 有點太「一根筋」了,很容易過度擬合,在沒看過的數據上表現很差。基本上被隨機森林完虐。 |
結果很明顯,Random Forest (隨機森林) 完勝,準確率衝到超過九成五。你看它的成績單:
側抬腿(side-leg-raises)和坐下(sitting)幾乎是 100% 辨識成功,超猛。躺下(lying-down)和踏步(step-ups)也接近完美。比較容易搞混的是深蹲(squats)和弓箭步(lunges),準確率大概在八成八到八成九之間... 這其實也可以理解啦,畢竟這兩個動作對髖部的運動模式真的有點像。
最爽的瞬間,就是看著它即時運作。當我一邊做著深蹲,螢幕上就同步跳出「Predicted Activity: squats」的時候,真的很有成就感。雖然它偶爾還是會出錯,但考慮到我們只用了一個這麼陽春的感測器,這成果已經很驚人了。
我們踩過的那些坑(以及怎麼爬出來的)
當然啦,過程不可能一帆風順。我們也遇到了不少鳥事。
第一個就是前面提到的數據不一致。解決方法就是... 認命,多錄幾次,讓數據庫更多樣化。
第二個是 Raspberry Pi 的效能限制。畢竟它不是專門用來跑機器學習的。所以我們才選擇了對計算資源要求不高的 Random Forest 模型,而且用了批次上傳的方式來減輕負擔。
第三個是最怪的,一個數學上的錯誤。在算「相關性」的時候,如果某個動作在某個軸向上「完全沒有變化」(例如你完美地水平移動,Z軸標準差是0),程式就會因為除以零而崩潰。後來我們加了一段程式碼,判斷如果標準差是零,就直接當作相關性是 0,才解決這個問題。
# 處理除以零的 bug (當標準差為0時)
if np.std(x) == 0 or np.std(y) == 0 or np.std(z) == 0:
# 如果其中一個軸完全沒動,相關性就直接給 0
return [0, 0, 0]
最後是網路穩定性。因為數據要上傳到雲端資料庫,如果網路一不穩,程式就會卡住。所以我們在 `upload_data` 的函式裡也加了錯誤處理 (`try...except`),至少讓它在網路斷線時不會整個掛掉,而是亮起紅燈警告。
所以,我學到了什麼?
這個專案搞下來,除了寫程式的技巧,我自己是覺得有幾個想法蠻值得記下來的:
- 少,真的可以是多:一開始我也覺得一個感測器根本不夠。但後來發現,只要演算法和特徵提取做得夠聰明,就算硬體很陽春,也能榨出驚人的效果。重點在於最大化數據的價值,而不是感測器的數量。
- 那兩顆 LED 燈,比想像中重要:使用者體驗真的很重要。就是那兩顆會閃的燈,給了我最直接的回饋,讓我知道系統到底有沒有在正常工作。一個好的互動設計,哪怕再簡單,都能讓整個體驗好上十倍。
- 91% 跟 95% 的差距,比數字上看起來更大:一開始看報告,覺得 SVM 的 91% 好像也不差啊?但實際用起來,那 4% 的差距非常有感。特別是在分辨相似動作時,模型的「信心」完全不同。
總之,這個專案真的很好玩。它證明了複雜的人體活動辨識,不一定需要昂貴複雜的硬體。未來如果能把模型直接部署到裝置上做「邊緣運算」(Edge Computing),擺脫對網路的依賴,再做個手機 App 來顯示結果,感覺會是個超實用的個人健身教練!
對了,如果你也有一個這樣的裝置,你最想用它來辨識什麼奇怪的、日常生活中根本沒人會想到的動作?在下面留言分享一下吧!搞不好會是下一個專案的靈感。😂
