iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
AI Engineering

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

Day 11|Signal Quality:何時該相信資料,何時該沉默?

  • 分享至 

  • xImage
  •  

今天為什麼研究這個?

Day 8 已能把 PPG 轉成逐窗心率,但「算得出數字」不等於「數字可信」:cycling 的視窗全部有心率,對 ECG 的 MAE 仍達 17.30 bpm。Day 7 的 motion_flag 也只能說手腕有沒有動,不能直接宣判 PPG 乾淨;Day 6 發現只數峰的閘門會漏掉「多一個假峰、漏一個真峰」互相抵銷的情況,Day 8 的 30–220 bpm 間期閘門在四個注入情境擋掉 0 個間期(1000 ms 拆成兩個 500 ms 是 120 bpm,漏一拍是 2000 ms、30 bpm,都在範圍內)。品質不能再只是圖表旁的一句提醒,必須決定下游能不能使用這筆數字。

今天是設計加實驗:設計三個品質分項與「可用/保留/拒答」三態規則,再用 PPG-DaLiA 的活動片段檢查規則會放行哪些錯誤、又會犧牲多少 coverage。本機已有 S1、S2;每位都有 64 Hz 腕式 PPG、32 Hz 腕式加速度、700 Hz 胸帶 ECG 與活動標籤。ECG 用來評估規則,不進入實際的品質分數;否則分數只是在偷看答案。

Concept

1. 分數量的是證據,不是「正確機率」

同一窗先記下三種來自 PPG 的證據,視窗沿用 Day 8 的 8 s、hop 2 s。C 是通過既有 30–220 bpm 間期閘門的比例(有效間期數/全部間期數)。這道閘門在 Day 8 對假峰與漏拍擋掉 0 個間期,所以這兩類失敗的 C 會約等於 1,得靠 I 與 W 抓。I 是相鄰原始間期沒有超過 20% 跳變的比例,分母是相鄰間期對,用來看峰的位置是否連續。8 s 窗最少 4 個峰、3 個間期、2 對,I 只能是 0、0.5 或 1;以 75 bpm 估約 10 個峰、8 對,粒度約 1/8。W 是把窗內脈搏對齊、標準化後,與窗內中位脈搏模板的相關係數截到 0–1,再取中位數,避免只看間期而漏掉波形已變形的窗。

以脈搏模板判斷品質有研究先例(Lim et al., 2018),但 Lim 用的是預先建好、再逐拍更新的外部模板;這裡的模板取自同一窗,窗內大半被污染時模板也是髒的,W 會偏高。三項的組合方式與門檻是本篇的待測設計,不是文獻給出的標準。間期跳變也可能是真實心律變化:Day 10 的清理規則對每個早發拍剔掉 3 個間期(短、長、跳回來的那個),若三者都是跳變,約 8 對的窗裡 I 會降到 5/8,被降為保留。真實心律事件被降級,是這套規則的代價。

三項都落在 0–1,但不合成一個加權分數,三態怎麼判見第 2 節。motion_flag 和三個分項、峰數、有效間期數一起往下游送,但不直接把窗降級:Day 7 已看到動作窗仍可能有準確心率。規律的步頻甚至可能讓間期與波形都看起來漂亮,所以三項都通過只能說這幾項檢查通過,不能說心率必然準確,更不能保證同一串峰適合算 HRV。

2. 閾值要連同拒答代價一起驗證

三態由每個分項各自判定,一窗取最差的那一態。每一條規則對應一個原因碼;同一窗觸發幾條就回報幾個,不只報最嚴重的那條:

條件 結果 原因碼
峰數 < 4 或有效間期 < 3(即 Day 8 已不給數字的窗) 拒答 峰數不足/有效間期不足
C < 0.8 拒答 離譜間期
I < 0.5 拒答 間期不連續
0.5 ≤ I < 0.75 保留 間期不連續
W < w_low 拒答 波形變形
w_low ≤ W < w_high 保留 波形變形
峰數足夠,但已啟用的分項算不出來(NaN) 拒答 該分項的原因碼
以上都沒觸發 可用 —

W 未校準時整條不啟用,是 NaN 也不影響判定。原因碼同時指「低於門檻」與「算不出來」:「波形變形」也可能只是窗內切不出兩個完整脈搏,缺證據,不是真的觀察到變形。

本篇的「保留」指中間態,和 Day 6、7、9 的「保留窗」(通過閘門的窗)不同。保留態可以存數字供查核,但下游不把它寫成確定的健康敘述;拒答態只回報原因碼,不補值。

不用加權和,是因為加權和允許分項互相補分。C 在真實資料上幾乎是常數:Day 9 的 PPG 間期只有 0.68% 超出生理範圍。放進 0.4C + 0.3I + 0.3W 這類加權和,C 等於固定加 0.4 分,拒答幾乎只能靠硬條件觸發。所以 C 只當閘門,I 與 W 也不互補。代價是任一項不及格就降級,coverage 會更低。

I 的門檻不看資料就能定:0.75 表示 8 對裡最多 2 對跳變仍算可用,0.5 表示一半的相鄰對都在跳;4 個峰的最小窗(2 對)一次跳變就是 0.5,落到保留。W 的門檻必須看資料,所以用 S1 的 W 分布與 ECG 心率誤差定下 w_low、w_high 後凍結。Day 9 的 PPG 間期有 45.2% 被同一條 20% 跳變規則剔除,若 8 s 窗也接近這個比例,大部分窗會落在保留或拒答;那就是結果,不回頭放寬門檻。這些數字是本篇要檢驗的政策閾值,不是生理安全界線。

驗證時按活動報三態比例與各態的 ECG 心率誤差,不能只報可用窗的 MAE:品質規則可能只是挑走較靜的片段,讓彙總心率產生選擇偏差。

調參與驗證要分開:W 的門檻只用 S1 定,S2 只報告、不回頭改。三個分項也各自報偵測力:C、I 用 Day 10 已知位置的峰時間注入;峰時間層的注入改不了波形,W 改用 Day 7 的 add_motion_to_ppg。偵測力以事件窗為分母:每種事件注入 43 個,但重疊窗讓一個事件落進數個窗,共 130–172 個事件窗,所以這是窗級比例,不是事件級的 recall。

Hands-on

Dataset

PPG-DaLiA 的 S1、S2:腕式 PPG 64 Hz、胸帶 ECG 的 R-peak(700 Hz)與 4 Hz 活動標籤。窗沿用 Day 8 的 8 s/2 s,丟掉跨活動交界的窗後,S1 有 4,551 窗、S2 有 4,039 窗。

另有兩組已知答案的合成測試資料。峰時間層用 Day 10 的注入函式,在 100、75、60 bpm 的 900 s 序列上各注入 43 個漏拍、假峰或早發拍,另加 10–40 ms 的峰定位抖動。波形層用 synthetic_ppg_from_rr 產生 75 bpm 的 PPG,在 40–80 s 疊上 Day 7 的 add_motion_to_ppg:步頻 2.2 Hz 與 1.25 Hz(後者剛好等於心率),動作振幅是主峰的 0–2 倍。

Method

  1. 逐窗算 C、I、W,窗與峰和 Day 8 的心率是同一組。先核對九種活動的窗數、MAE 與 bias 都和 Day 8 逐列相同(cycling 393 窗、17.30 bpm),確認兩天用的是同一組窗。
  2. 在合成資料上各自量分項的偵測力。「事件窗」是一個事件弄壞的間期全在窗內的窗,被窗邊界切開的不計;另外看沒有事件的窗有多少被降級(誤降級)。
  3. W 的門檻只用 S1,而且只在峰數、C、I 都已放行的 849 窗上選,避免重複懲罰 I 已抓到的窗。w_low = 0.87 約在這些窗的 10 分位(0.874,以下的 MAE 10.2 bpm);w_high = 0.96 是 30 分位,以上的 MAE 1.72 bpm、誤差超過 10 bpm 的占 1.52%。選定後凍結,S2 沒有參與。
  4. S1、S2 都套三態,按活動報三態比例、各態 MAE、誤放行(可用但 |PPG − ECG| > 10 bpm),以及全部窗與可用窗的 ECG 平均心率。

https://ithelp.ithome.com.tw/upload/images/20260924/20184206lLcTeS2R11.png
S1:每窗的 W 對 PPG 心率誤差,折線是十分位分箱的 MAE,虛線是 w_low 與 w_high

這張圖畫的是 S1 全部有心率的 4,448 窗,門檻則依前述 849 窗的子集選定。在全部窗上,W 前七個十分位箱的 MAE 都在 9.77–12.2 bpm,第八箱 8.77,最後兩箱才降到 4.44 與 1.36 bpm。

Code

三態的判定寫成「觸發了哪些規則、各自判成什麼態」,最後取最差的那一態:

triggered = {}
enough_peaks = n_peaks >= thresholds.min_peaks
if not enough_peaks:
    triggered["too_few_peaks"] = "abstain"
# ...C 同理...
if (enough_peaks and not np.isfinite(i)) or i < thresholds.i_abstain:
    triggered["discontinuous"] = "abstain"
elif i < thresholds.i_hold:
    triggered["discontinuous"] = "hold"
# ...W 同理,未校準時整條不啟用...
reasons = tuple(code for code in REASON_CODES if code in triggered)
state = max(triggered.values(), key=STATES.index, default="usable")

not np.isfinite 那半句是必要的。NaN 與任何數比較都是 False:一窗切不出兩個完整脈搏時 W 是 NaN,只寫 w < w_low 的話,W 這條規則等於沒跑,其他規則都放行時這一窗就默默判成可用。

結果與意外

原本以為 實際發現
原本以為 C 至少能攔下部分假峰或漏拍。 C 在合成事件窗上的拒答比例全部是 0%,漏拍與假峰都擋不到。真實資料上 C < 0.8 大多在 0–4.79% 的窗,只有 table soccer 到 17.8%(S1)與 20.9%(S2)。
原本以為間期一短一長,I 就會穩定抓到假峰與漏拍。 I 的偵測力綁在心率上。對半切的假峰(開頭 Day 8 的例子)在 100、75 bpm 被降級 0%,60 bpm 是 90.9%;漏拍在 100 bpm 只有 0.585%。沒有任何一個單事件窗被 I 拒答。乾淨窗的誤降級只出現在 40 ms 峰定位抖動:100 bpm 13.2%、75 bpm 0.895%。
原本以為一個早發拍造成三次跳變,I 應該會把事件窗降級。 早發拍在 75 bpm 的事件窗 I 中位數是 5/8,但只有 63.2% 被降為保留;100 bpm 是 33.9%,60 bpm 是 100%。要看得到三次跳變,早發拍前後的正常間期都得在窗內;只看到兩次時,8 對的窗 I = 0.75,仍算可用。
原本以為動作振幅越大,W 就會越低,規律的步頻也能靠 I 辨出。 2.2 Hz 步頻下,W 中位數從 1 隨振幅降到 0.828,振幅 2 倍時回升到 0.948。這時峰偵測改鎖步頻,PPG 心率中位數 131.9 bpm(真值 75),I 中位數 1,MAE 57.2 bpm;15 窗裡 14 窗判保留、1 窗判可用。步頻等於心率時,W ≥ 0.998、MAE ≤ 0.47 bpm。
原本以為三項各自判定後,仍會有不少活動窗可用。 可用窗只有 S1 13.0%、S2 12.7%。stairs、table soccer、walking 兩人都沒有可用窗,S2 的 cycling 也沒有。約 73% 的窗同時觸發 I 與 W。
原本以為只要可用窗的平均誤差夠低,就能放心使用每個可用窗。 可用窗的 MAE 兩人都是 1.72 bpm,全部窗是 9.20/10.23 bpm;誤放行 9 窗(1.52%)/2 窗(0.39%),其中 S1 cycling 的 13 個可用窗有 3 個超過 10 bpm。W 把 256/164 窗從可用降級,這些窗的 MAE 是 6.98/7.55 bpm。
原本以為篩出可信的窗後,這些窗還能代表整段活動的心率分布。 全部窗的 ECG 平均心率是 74.4/82.2 bpm,可用窗只有 63.0/72.1 bpm。motion_flag 不參與判定,但帶旗標的窗只有 0.6%/0.9% 可用,沒旗標的是 22.9%/18.7%。

https://ithelp.ithome.com.tw/upload/images/20260924/20184206JcaX1FClBM.png
合成 PPG:動作段內的 W 中位數對動作振幅

https://ithelp.ithome.com.tw/upload/images/20260924/201842060EpmrSEZXZ.png
S1 與 S2 各活動的可用、保留、拒答窗數比例

S1 用來定 W 的門檻,S2 沒有參與;兩人的可用比例差不多,但分布在不同活動:S1 sitting 可用 59%,S2 是 34%;S2 的 lunch break 17%,S1 只有 3%。

https://ithelp.ithome.com.tw/upload/images/20260924/20184206y2r1g53azi.png
各活動三態各自的 MAE,窗數少於 10 的點標出 n

多數活動的誤差依可用、保留、拒答遞增;walking 兩人都是保留高於拒答,其餘例外的保留窗只有 2–6 窗。

Limitations

這次只有 S1、S2 兩位受試者;W 門檻用 S1 的 ECG 心率誤差選定,S1 的成績因此不能當獨立驗證。S2 沒參與調參,但一人也不足以推論門檻可套用到其他人。8 s 視窗每 2 s 移動一次,相鄰窗大量重疊;文中的窗數與比例描述的是這次資料,不能把每窗當成獨立樣本,或從 9、2 個誤放行窗推算一般使用者的錯誤率。

三個分項各有盲點。W 用同一窗的脈搏建立模板,窗內大半被污染時,模板也會跟著變形;合成實驗在 2.2 Hz、振幅 2 倍時就看到 W 回升、I 中位數為 1,仍放行了 1 個心率誤差 56.69 bpm 的窗。I 的分母是窗內相鄰間期對,同樣的跳變數在不同心率下會得到不同分數;它也可能把真實早發拍當作品質問題。合成測試每窗至多一個事件,同一窗有多個事件的情形沒有測到。

ECG R-peak 是比較心率的參考,並非無誤的生理真值;這裡也只驗證逐窗心率,不能據此宣稱放行的峰序列適合計算 HRV。拒答窗有些根本算不出 PPG 心率,拒答態的 MAE 只用其中有心率的 S1 3,014/3,117 窗、S2 2,792/2,916 窗計算,不能把它解讀為所有拒答窗的誤差。

這對 AI Engineering 的意義

在這兩位受試者身上,拒答是常態,不是例外:S1、S2 各有 68.5%、72.2% 的窗拒答,可用只有約 13%。這不能當作要回頭放寬門檻的理由,而是下游要照這個比例設計。餵給 LLM 的不能只有可用窗算出的心率,還要有三態比例、原因碼與被排除時段的活動分布。S1 的可用窗約七成來自 sitting 與 working,可用窗的 ECG 平均心率比全部窗低 10–11 bpm;只給可用窗的平均,「今天的心率」就悄悄變成「今天較靜時段的心率」,模型無從察覺。

「可用」的意思是這幾項檢查都通過,不是這個數字正確。C 看不到 Day 8 的假峰,I 的偵測力隨心率變,W 在步頻鎖定時回升到 0.948,那些窗的心率誤差卻約 57 bpm。每個分項的盲點都有已知答案的案例可以重現,這些案例應該留在 regression test 裡,規則改了才看得出放行了什麼。Day 19 的 feature contract 要放得下原因碼清單,而不是一個 bool 或一個分數。


上一篇
Day 10|故意把訊號弄壞:Artifact 注入實驗
下一篇
Day 12|Time Alignment 與 Rolling Aggregation:多種指標如何對齊成同一天?
系列文
30 天拆解 Wearable × AI:從穿戴裝置生理訊號到AI健康洞察 共 13 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言