iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0
AI Engineering

30 天拆解 Wearable × AI:從穿戴裝置生理訊號到AI健康洞察系列 第 7

Day 07|Motion Artifact:為什麼穿戴式資料這麼難用?

  • 分享至 

  • xImage
  •  

今天為什麼研究這個?

之前我們提到各主流廠商的RHR或是HRV的數值大多是基於用戶在靜止或是睡眠狀態下的偵測。然而真實世界不可能都在這樣的狀態裡,但白天使用者在走路、跑步、打字、騎車、做家事,手腕隨時都在動作。感測器在手晃動時真正記錄到的是什麼?

Day 3 我們用 0.25–8 Hz 的帶通濾波器把基線漂移和高頻毛邊濾掉,難道手甩動的雜訊不能照樣濾掉?但手錶裡不是都內建了三軸加速度計嗎?如果手動得厲害、加速度能量高,演算法只要把動作成分扣掉,或者看到加速度大就乾脆標記放棄,問題不就解決了?

今天我們直面穿戴式生理訊號最棘手的痛點:Motion Artifact。看看手腕一動,波形到底被破壞成什麼樣子、為什麼古典的帶通濾波救不回來,以及那顆被寄予厚望的腕上加速度計,到底能不能當作合格的品質守門員。

Concept

1. 動作弄壞的是波形,不是數字

PPG 靠反射光的強弱推血容積變化。手一動,光路就變了,而且三件事同時變(Fine 2021):

  • 感測器與皮膚相對位移。 光走的路徑變了。位置越末梢、綁越鬆越嚴重;上臂最耐動,綠光又比紅外光耐動。手腕不是最好的量測位置,是最方便的位置。
  • 接觸壓力改變。 壓力直接決定 AC 振幅:太鬆訊號小,壓到血管閉合訊號就沒了,中間有一個最佳點。甩手、握拳、把手腕壓在桌上,都會離開那個點。
  • 環境光漏進來。 環境光可以比脈動成分大好幾個數量級,足以讓感測器飽和。手臂擺動時錶體與皮膚的縫隙一開一合,漏光跟著週期變化——和動作同步,不是隨機雜訊。

還有一點容易誤會:完全不動時基線也會漂,呼吸、體溫調節、交感神經活動都會造成低頻漂移。所以「靜態」不等於「乾淨」。PPG-DaLiA 把靜坐段叫 "motion-artefact-free baseline",那是實驗設計的意圖,不是量測結果。

2. 為什麼帶通濾波救不回來

濾波要有效,前提是訊號和雜訊落在不同頻率。心率是 1–4 Hz,動作橫跨 0.1–20 Hz,整個蓋過去(Fine 2021)。同頻就沒有判準——濾波器無從知道 2 Hz 那個成分是心跳還是腳步。

更麻煩的是週期性動作。走路、跑步、騎車的節律本身就像脈搏:振幅穩、週期規則,演算法會鎖到步頻上,把它當成心跳。Bent 2020 稱之為 signal crossover,六款裝置實測走路時誤差都顯著上升(Apple Watch 4 是唯一例外),而且有方向:走路讓心率報高,打字讓它報低。

所以濾完「看起來乾淨」不代表心率是對的,峰還在、只是那些峰不是心跳;而且這種錯有方向,取平均消不掉。

(邊界:強動作下並非追不到。Zhang 2015 的 TROIKA 在跑到 15 km/h 的資料上做到 MAE 2.34 bpm,但靠的是訊號分解與頻譜峰追蹤,不是一道帶通。)

3. 加速度計能不能當守門員

PPG-DaLiA 腕上除了 PPG 還錄了 32 Hz 三軸加速度,Reiss 2019 寫明用途是 "in order to facilitate motion artefact compensation"。兩種用法,代價差很多:

  • 當 reference input: 餵給 adaptive filter 把動作成分減掉、重建波形。輸出是估計值。
  • 當品質指標: 只判斷這段能不能用,不動波形。輸出是一個 flag。

今天做第二種。它的前提是「加速度大 → PPG 被污染」這個關聯夠強,而失效方向很明確:加速度安靜不代表 PPG 乾淨。打字、敲桌、握力變化這類微動作,手腕確實沒動,加速度計抓不到(Fine 2021);Bent 2020 實測打字的誤差常和走路一樣高。反過來也不保證:走路很規律,規律本身也可能讓演算法穩定地抓對。所以代理指標和真實誤差的關係要量出來,不是假設出來。不靠第二感測器的品質判斷留到 Day 11。

4. 判定壞了以後:修、標、丟

處置 做什麼 代價
adaptive filter 或波形重建 下游拿到的是估計值,而「修過」通常沒有傳下去
產生 quality flag,資料保留 下游必須真的會讀這個 flag,否則等於沒標
剔除該段 產生缺值,而且缺得不隨機——動作多的時段被系統性刪掉

第三條最容易被低估。剔除動作段之後,剩下的資料偏向靜態時刻;拿去算一天的平均心率或 HRV,算出來的不是「這一天」,是「這一天裡比較靜的那些時刻」。這個偏差會傳到 Day 12 的聚合與 Day 15 的 personal baseline,到那一層已經看不出它從哪裡來。

5. 先猜

三個問題:誤差隨能量變化的形狀、誤差的方向、各活動的能量排序跟誤差排序一不一樣。

  1. 形狀:斷崖。 微動時脈搏峰還大過雜訊,抓得到;一旦動作跨過某個程度(錶體滑移、漏光、血管受壓),脈動特徵被淹沒,演算法整個崩掉,誤差斷崖式噴發。
  2. 方向:偏高。 今天是時域找 local maxima,動作會疊出一堆假峰,數到的峰變多、間隔變短、心率就偏高;走路的 1.5–2.5 Hz 又落在心率帶裡,signal crossover 會把它拉到步頻。
  3. 活動別:兩個排序會脫節。 能量我猜 stairs / walking > table soccer > cycling > driving。誤差我猜 table soccer 最高(手腕快速轉動、接觸壓力劇變);driving 會比想像中高(握方向盤是微動作,加速度計抓不到,正是代理指標的盲區);walking 因為太規律,反而可能沒那麼失控。

Hands-on

Dataset

兩份資料,角色不同:

  • PPG-DaLiA S1、S2(主結果): 腕上 E4 的 BVP 64 Hz、三軸加速度 32 Hz(±2 g、8-bit),胸前 ECG 700 Hz 並附 R-peak。兩人各 153.5 與 136.8 分鐘,走完同一套流程:sitting、stairs、table soccer、cycling、driving、lunch break、walking、working,八個活動之間夾著標成 transient 的過場。本機只解壓了這兩位。
  • 合成 PPG(示範用): jittered_rr 產生 60 個間期,平均 1000 ms、sd 20 ms(seed=7),synthetic_ppg_from_rr 畫成 64 Hz 波形,再疊上 2.2 Hz 的正弦動作成分——2.2 Hz 等於 132 bpm,是走路的步頻,也落在心率帶裡。振幅取 0.5、1、2,以 PPG 主峰振幅為 1。

Method

  1. 切窗。 8 秒、不重疊(8 秒取自資料集自己算 hr_label 的視窗長度)。跨在活動交界上的窗丟掉(S1 14 窗、S2 15 窗),剩 1137 + 1010 = 2147 窗。transient 的 586 窗留著——它佔了 27%,拿掉等於偷偷挑資料。
  2. 心率與誤差。 PPG 用 Day 3 的 find_pulse_peaks(0.25–8 Hz 帶通、prominence 取該窗振幅 15%),ECG 用資料集附的 R-peak 當對照真值,兩邊都取平均間期,一窗至少 4 個峰才算。誤差 = PPG − ECG,2147 窗裡有 2118 窗兩邊都算得出來。
  3. 加速度能量。 沒有標準算法,三軸怎麼合併、要不要去重力、用變異數還是 RMS,各家不同,所以這個定義是我自己選的:每窗先扣掉各軸平均(去掉重力與手腕姿勢),再取三軸平方和的均方根,單位 g。等價於三軸變異數和的平方根。選它的理由是換一個手腕姿勢不會改變輸出。
  4. 門檻。 靜坐段(activity 1,160 窗)的能量分位數是 50% 0.0097、90% 0.3459、100% 0.636 g,取 0.35 g 當門檻:超過就標記,嚴格大於,不要求連續幾窗。另外掃了靜坐段的 50/75/90/95/99/100 分位當對照。

Code

import numpy as np
from wearable_ai.datasets import load_subject
from wearable_ai.signal_processing import find_pulse_peaks
from wearable_ai.signal_processing.motion import acc_energy, motion_flags

data = load_subject(1)
t0_s, energy = acc_energy(data.acc_wrist, 32.0, window_s=8.0)     # 去平均後三軸 RMS,單位 g
flagged = motion_flags(energy, threshold=0.35, min_consecutive=1)  # True = 疑似被動作污染

seg = data.ppg[int(150 * 64) : int(158 * 64)]                      # 靜坐段的一窗
peaks_s = find_pulse_peaks(seg, 64.0) / 64.0
60.0 / np.mean(np.diff(peaks_s))                                   # 42.8 bpm;同窗 ECG 43.7

一動就變形的不只是雜訊

https://ithelp.ithome.com.tw/upload/images/20260920/20184206AWSOwTEoQD.png
S1 的 sitting 與 walking 各 10 秒,上排 PPG 與偵測到的峰、下排腕式三軸加速度;sitting 的三軸幾乎是直線,walking 三軸都在 ±1 g 擺動

左邊是靜坐的 10 秒:加速度三軸幾乎是三條直線,PPG 每一拍的形狀都一樣,峰偵測 43.4 bpm、ECG 43.5 bpm。右邊是走路的 10 秒,加速度三軸都在 ±1 g 之間擺動,PPG 的每一拍長得都不一樣——主峰有高有低,中間多出不屬於脈搏的起伏。這一段剛好對得不錯(80.6 對 80.3 bpm),但整個 walking 的 179 窗,|誤差| 中位數是 12.4 bpm。

帶通濾波沒有可依據的判準

https://ithelp.ithome.com.tw/upload/images/20260920/20184206lMzYnwgBb6.png
合成 60 bpm 的 PPG 疊上 2.2 Hz 動作、經 0.25–8 Hz 帶通之後的四段波形,動作振幅 0、0.5、1、2,偵測到的峰逐漸鎖到 132 bpm

帶通留下了 99.7–99.8% 的動作成分。這不是濾波器沒做好,是 2.2 Hz 就在通帶正中央,和心跳同頻,濾波器沒有依據把兩者分開。沒有動作時峰偵測報 60.3 bpm(真值 60);動作振幅 0.5 報 73.5 bpm;振幅到 1 以上時找到 133 個峰(真實 61 個),報出的 132.1 與 132.0 bpm 剛好是步頻——錯誤不是隨機的,是鎖到了另一個週期訊號上。

加速度能量與誤差:整體有關係,分活動就散了

https://ithelp.ithome.com.tw/upload/images/20260920/20184206KFNjCEy3Pu.png
九個活動各一格的散點圖,x 是加速度能量、y 是心率誤差的絕對值,橘線是各格內依能量分組的中位數;table soccer 與 cycling 的橘線往下走

把 2147 窗依能量等量分成五組,|誤差| 中位數是 1.31、5.30、8.87、11.4、11.5 bpm,超過 10 bpm 的比例從 6.8% 升到 57.4%,全體的 Spearman ρ = +0.453(n = 2118)。關係存在,但不是斷崖:最大的一跳在第一組到第二組之間(能量 0.046 g 這條線附近),第四組到第五組幾乎持平。

分活動看就散了:

活動 窗數 能量中位數 誤差中位數(帶號) |誤差| 中位數 >10 bpm ρ
stairs 67 0.452 −27.2 28.3 89.6% +0.11
table soccer 79 0.336 −20.7 21.4 82.4% −0.39
cycling 98 0.628 −13.3 15.7 63.3% −0.36
walking 179 0.476 +8.07 12.4 58.1% +0.15
sitting 160 0.00971 +0.41 1.48 15.4% +0.50

另外四個活動的 |誤差| 中位數是 transient 7.97、driving 6.23、lunch break 5.23、working 2.51 bpm(完整的九列在 days/day07/results.md)。能量最高的是 cycling,誤差最高的是 stairs;八個具名活動(不含 transient)的兩個排序,後四名(driving、lunch break、working、sitting)一致,前四名是同一組、次序重排。

活動之內,關係有時是反的:table soccer ρ = −0.39、cycling ρ = −0.36,圖上那兩格的橘線往下走。方向也分活動:stairs 有 94% 的窗比 ECG 低 5 bpm 以上,walking 反過來,58% 的窗高出 5 bpm 以上。全部 2118 窗合起來誤差為正的占 49.1%——整體看不出方向,是兩邊抵銷掉了。

兩人也不一樣:table soccer 的誤差中位數 S1 是 −9.11、S2 是 −32.2 bpm;S2 的 cycling 有 5.85% 的樣本撞到 ±2 g 的量程上限(S1 1.44%),撞軌的窗真實加速度比記到的大。

當 quality gate 用:留下什麼、丟掉什麼

https://ithelp.ithome.com.tw/upload/images/20260920/2018420691r9DE4VLi.png
門檻掃描的三格圖:保留率、保留與丟掉視窗的誤差中位數、平均心率偏差,x 是門檻、虛線標在 0.35

門檻 0.35 g 保留 60.5% 的窗,保留窗的 |誤差| 中位數 3.86 bpm、丟掉的 11.5 bpm——gate 確實把誤差大的挑出來了。但兩個方向都有漏:

  • 通過門檻、|誤差| 仍大於 10 bpm 的有 353 窗,占保留窗的 27.5%。能量最低的那 429 窗裡也還有 29 窗(6.8%),來自 transient 10、lunch break 8、sitting 5、working 5、table soccer 1 窗。
  • 被丟掉、但 |誤差| 其實 ≤ 5 bpm 的有 199 窗,占丟掉的 23.9%。

保留率按活動拆開,兩端差很遠:sitting 90%、working 84.4%,另一頭是 walking 29.1%、stairs 5.97%。stairs 的 67 窗只剩 4 窗,而那 4 窗的 |誤差| 中位數是 42.8 bpm。

代價在第三格:用 ECG 的心率算(與 PPG 準不準無關),全部窗的平均是 78.0 bpm,門檻 0.35 只留下的那些窗平均 73.4 bpm,低了 4.68 bpm。門檻再嚴一點,0.0097 g(靜坐段的能量中位數)只留 7.87% 的窗,平均心率掉到 60.2 bpm——低了 17.8 bpm。剔除動作段之後算出來的「平均心率」,量的不是這段時間,是這段時間裡比較靜的那些時刻。

結果與意外

六件實際發現,對照的是 Concept 第 5 節那三個先猜的問題:

  • 形狀不是斷崖。 能量五等分的 |誤差| 中位數是 1.31 → 5.30 → 8.87 → 11.4 → 11.5 bpm,最大的一跳在第一到第二組,第四到第五組持平。全體 ρ = +0.453。
  • 整體沒有方向。 2118 窗裡誤差為正的占 49.1%。有方向的是個別活動,而且多半往低:stairs −27.2、table soccer −20.7、cycling −13.3 bpm;只有 walking 是 +8.07。
  • 兩個排序沒有脫節,但也沒對齊。 八個具名活動的能量排序與誤差排序,後四名一致、前四名是同一組但次序重排。能量最高的是 cycling,誤差最高的是 stairs;driving 的誤差(6.23 bpm)比猜的溫和,排在中間。
  • 帶通濾波濾不掉同頻的動作。 0.25–8 Hz 的帶通對 2.2 Hz 的動作成分留下 99.7–99.8%。動作振幅 ≥ 1 時,峰偵測報 132 bpm,就是步頻本身。
  • 加速度計當守門員是單向的。 兩個漏:通過門檻但 |誤差| > 10 bpm 的 353 窗(保留窗的 27.5%),被丟掉但 |誤差| ≤ 5 bpm 的 199 窗(丟掉窗的 23.9%)。
  • 剔除不是隨機缺值。 門檻 0.35 把 ECG 平均心率從 78.0 拉到 73.4 bpm;stairs 只留 5.97%、walking 29.1%、cycling 31.6%。

Limitations

  1. 此實驗只有 2 人,table soccer 的誤差中位數 S1 −9.11、S2 −32.2 bpm,單一活動的結論撐不住。
  2. 對照真值是資料集附的 ECG R-peak,沒有自己重跑 R-peak 偵測;動作也會污染胸帶。
  3. transient 佔 586 窗(27%),資料集沒寫裡面在做什麼,它的能量中位數 0.337 g 與誤差 7.97 bpm 都混了多種情境。
  4. PPG 峰偵測是 Day 3 的版本(時域 prominence),不是消費裝置用的頻域追蹤;TROIKA 那類方法在強動作下的誤差遠低於今天的數字,所以這裡量到的是「這條 pipeline 的脆弱度」,不是 PPG 的上限。

這對 AI Engineering 的意義

quality gate 的輸出要保留它的單向性。 今天量到的兩個漏——27.5% 的保留窗誤差仍大於 10 bpm、23.9% 的丟掉窗其實是好的——說明加速度能量不是「PPG 乾不乾淨」的量測,只是「加速度計有沒有看到動作」。所以這個欄位的名字不該叫 clean,該叫 motion_flag,而且要連定義一起往下游送:能量怎麼算、門檻多少、幾秒一窗。Day 19 的 feature contract 要放得下這種「有條件的證據」,不是一個 bool。

覆蓋率與偏差要和數字一起走。 門檻 0.35 下,一天裡 60.5% 的窗留下來,平均心率被拉低 4.68 bpm;門檻收到靜坐段的中位數,剩 7.87% 的窗、心率低 17.8 bpm。這個偏差與 PPG 準不準無關,它是剔除本身造成的——用 ECG 的心率算也一樣。下游若只收到一個平均值,這一層資訊就消失了;到 Day 12 的聚合、Day 15 的 personal baseline,已經看不出「今天的基準是 60% 的時間算出來的,而且偏靜」。所以聚合結果要帶 coverage 與被丟掉的時段分布。

錯誤有方向,取平均消不掉。 stairs、table soccer、cycling 系統性偏低(−13 到 −27 bpm),walking 偏高(+8 bpm)。這不是零均值雜訊,多平均幾窗不會變好。對 LLM 尤其危險:一個「運動時心率偏低」的序列,讀起來像體能進步的證據,而它其實是 signal crossover 與漏拍的產物。所以 Day 17 的 deviation detection 要先問「這段時間的量測可信嗎」,再問「有沒有偏離基準」;能回答第一個問題的,是 deterministic 這一層算出來的 flag 與覆蓋率,不是語言模型的推理。


上一篇
Day 06|Sampling Rate:取樣頻率為何決定你能相信什麼?
下一篇
Day 08|實作:從 PPG 波形萃取心率
系列文
30 天拆解 Wearable × AI:從穿戴裝置生理訊號到AI健康洞察10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言