手勢(shì)識(shí)別:從數(shù)據(jù)采集到實(shí)時(shí)部署避坑指南)
簡(jiǎn)介這套基于sEMG表面肌電與IMU慣性測(cè)量單元的手語(yǔ)手勢(shì)識(shí)別項(xiàng)目面向從事人機(jī)交互、康復(fù)工程或移動(dòng)感知研究的學(xué)生與開(kāi)發(fā)者覆蓋從數(shù)據(jù)采集到實(shí)時(shí)識(shí)別的完整流程。包內(nèi)共59個(gè)文件包含Python預(yù)處理與訓(xùn)練腳本、C/MATLAB源碼、TensorFlow模型文件checkpoint、meta、index等、CSV數(shù)據(jù)文件及Visual Studio工程文件便于直接查看與二次開(kāi)發(fā)整體壓縮包約39.91MB。項(xiàng)目附帶單、雙手?jǐn)?shù)據(jù)采集程序數(shù)據(jù)預(yù)處理環(huán)節(jié)集成去噪、特征提取與分割并采用WMA與小波變換處理信號(hào)核心部分提供了RNN、CNN及MYO_RNN2s_v1模型和圖文件可對(duì)照理解網(wǎng)絡(luò)結(jié)構(gòu)設(shè)計(jì)便于調(diào)整參數(shù)與訓(xùn)練策略。實(shí)時(shí)識(shí)別模塊與模型文件相互配套適合希望復(fù)現(xiàn)sEMGIMU手勢(shì)識(shí)別實(shí)驗(yàn)、快速搭建原型或?qū)W習(xí)神經(jīng)網(wǎng)絡(luò)在該領(lǐng)域應(yīng)用的讀者也有助于在此基礎(chǔ)上開(kāi)展動(dòng)作分類與人機(jī)交互研究。資源已有678人學(xué)習(xí)下載具備一定的參考與復(fù)用價(jià)值。1. 基于sEMG和IMU的手語(yǔ)手勢(shì)識(shí)別為什么要把肌電和慣性數(shù)據(jù)放在一起做手語(yǔ)手勢(shì)識(shí)別在可穿戴設(shè)備上從來(lái)不是熱門賽道但真到需要脫離攝像頭、在移動(dòng)場(chǎng)景里理解手語(yǔ)時(shí)基于sEMG和IMU的手語(yǔ)手勢(shì)識(shí)別反而是少數(shù)能穩(wěn)定落地的方案。表面肌電sEMG能直接反映前臂肌肉的發(fā)力模式手指和手腕的細(xì)小動(dòng)作都會(huì)在肌電信號(hào)里留下痕跡IMU則負(fù)責(zé)手臂整體的姿態(tài)和運(yùn)動(dòng)軌跡正好補(bǔ)上肌電“能看出發(fā)力看不出位移”的短板。兩者融合后數(shù)據(jù)收集、去噪、特征提取、手勢(shì)分割、神經(jīng)網(wǎng)絡(luò)搭建到實(shí)時(shí)識(shí)別是一條完整鏈路。這篇文章寫(xiě)給正在做穿戴式手勢(shì)交互、無(wú)障礙翻譯或人機(jī)接口的工程師和學(xué)生目標(biāo)是讓你照著我給的流程和參數(shù)自己搭出一套能跑、能量化評(píng)估的手語(yǔ)識(shí)別系統(tǒng)并避開(kāi)我已經(jīng)踩過(guò)的坑。2. 數(shù)據(jù)收集sEMG與IMU同步采集的硬件選型與標(biāo)注方案2.1 硬件選型與通道配置先定同步方式再定傳感器手語(yǔ)識(shí)別數(shù)據(jù)質(zhì)量的一半取決于傳感器同步方式。sEMG和IMU本來(lái)就是兩類頻率特性完全不同的信號(hào)肌電的典型有效頻段在20Hz到450Hz之間采樣率通常要做到500Hz以上IMU描述的剛體運(yùn)動(dòng)頻率低得多100Hz到200Hz就能覆蓋絕大多數(shù)手部動(dòng)作。兩者直接拼接會(huì)帶來(lái)兩個(gè)問(wèn)題一是時(shí)間基準(zhǔn)不同二是空間坐標(biāo)系不同。所以硬件選型的第一步不是堆通道數(shù)而是想清楚這兩個(gè)問(wèn)題怎么解。最常見(jiàn)的配置是8通道sEMG加6軸或9軸IMU。sEMG電極貼在前臂的指屈肌群和指伸肌群上沿著肌腹走向布置IMU放在腕背或靠近手背的位置用來(lái)測(cè)量手臂旋轉(zhuǎn)和位移。如果選擇分體式模塊sEMG采集板和IMU模塊各有各的晶振時(shí)間戳天然不同步如果選擇腕帶式集成方案很多產(chǎn)品把電極和IMU做在同一塊柔性電路上硬件層已經(jīng)統(tǒng)一時(shí)基能省掉后面不少事但通道數(shù)通常固定在8到16路不方便按前臂長(zhǎng)度調(diào)整電極位置。傳感器典型采樣率通道數(shù)放置位置用途sEMG500Hz-1kHz8前臂屈肌群/伸肌群手指、手腕發(fā)力模式IMU加速度100Hz-200Hz3腕背手勢(shì)整體加速度與傾斜IMU陀螺儀100Hz-200Hz3腕背手部旋轉(zhuǎn)、角速度IMU磁力計(jì)50Hz-100Hz3腕背航向參考室內(nèi)易受磁干擾關(guān)于磁力計(jì)我的建議是謹(jǐn)慎使用。手語(yǔ)識(shí)別大多在室內(nèi)環(huán)境進(jìn)行辦公桌、顯示器、金屬扶手都會(huì)造成磁場(chǎng)畸變磁力計(jì)輸出一個(gè)不穩(wěn)定的航向角會(huì)拖累姿態(tài)解算。如果不是必須知道絕對(duì)朝向直接用加速度和陀螺儀做6軸姿態(tài)估計(jì)就夠。關(guān)節(jié)角度、手臂指向這些特征靠線性加速度和角速度在重力方向的投影也能恢復(fù)大部分。通道數(shù)為什么定8路而不是16路因?yàn)槭终Z(yǔ)詞匯雖然復(fù)雜但大部分由手指屈伸、手腕翻轉(zhuǎn)和前臂旋轉(zhuǎn)組合而成前臂淺層能穩(wěn)定采集到信號(hào)的肌群就那么幾個(gè)。8路電極覆蓋指淺屈肌、指深屈肌、橈側(cè)腕屈肌、尺側(cè)腕屈肌、指伸肌群等主要發(fā)力點(diǎn)已經(jīng)能把大部分手形區(qū)分開(kāi)。16路能拿到更細(xì)的空間分布但電極間距變小、串?dāng)_變大線纜和佩戴成本也翻倍。對(duì)這個(gè)應(yīng)用場(chǎng)景性價(jià)比最高的起點(diǎn)是8路模型效果不夠再考慮提高通道密度。2.2 同步采集與數(shù)據(jù)標(biāo)注時(shí)間戳對(duì)齊與標(biāo)簽協(xié)議如果你用的是分體式模塊同步采集是第一個(gè)真正會(huì)翻車的環(huán)節(jié)。常見(jiàn)做法是在兩個(gè)采集線程里分別打上主機(jī)接收時(shí)間戳數(shù)據(jù)包內(nèi)部再帶一個(gè)包序號(hào)。主機(jī)收到任意一個(gè)串口的數(shù)據(jù)后記錄time.time()并寫(xiě)入幀頭之后離線對(duì)齊時(shí)用這個(gè)時(shí)間戳找對(duì)應(yīng)關(guān)系。IMU采樣率低一般以sEMG時(shí)間戳為基準(zhǔn)用線性插值把IMU插到相同的采樣時(shí)刻。import pandas as pd semg_df pd.read_csv(raw_semg.csv) # 列: timestamp_ms, ch1..ch8 imu_df pd.read_csv(raw_imu.csv) # 列: timestamp_ms, acc_x, acc_y, acc_z, gyro_x, gyro_y, gyro_z aligned pd.merge_asof( semg_df.sort_values(timestamp_ms), imu_df.sort_values(timestamp_ms), ontimestamp_ms, directionnearest, tolerance5 ).dropna(subset[acc_x])這段代碼的邏輯是以sEMG時(shí)間戳為主鍵為每一個(gè)sEMG采樣點(diǎn)找時(shí)間上最接近的IMU樣本容差設(shè)為5ms。超過(guò)5ms的直接丟棄因?yàn)閮蓚€(gè)傳感器時(shí)間差超過(guò)一個(gè)IMU采樣周期時(shí)插值已經(jīng)沒(méi)有意義。注意在調(diào)用merge_asof前兩個(gè)DataFrame必須按時(shí)間排序否則會(huì)報(bào)錯(cuò)或得到亂序結(jié)果。實(shí)時(shí)推理時(shí)不能等離線對(duì)齊需要維護(hù)兩個(gè)環(huán)形緩沖區(qū)在采集線程里用同樣的插值思路做在線對(duì)齊。這個(gè)細(xì)節(jié)后面避坑章節(jié)會(huì)展開(kāi)。標(biāo)注協(xié)議是另一個(gè)容易低估的環(huán)節(jié)。手語(yǔ)詞匯沒(méi)有一個(gè)固定的“標(biāo)準(zhǔn)時(shí)長(zhǎng)”同一個(gè)詞在不同語(yǔ)境下的語(yǔ)速差別很大所以不能用固定窗口去切。推薦的做法是讓受試者按自然手語(yǔ)速度重復(fù)手勢(shì)標(biāo)注者把每個(gè)手勢(shì)的起止時(shí)間寫(xiě)入annot.csv每一行包含start_ms, end_ms, label_id, label_name。注意sEMG的激活通常比可見(jiàn)動(dòng)作早20ms到50ms肌肉先發(fā)力手才開(kāi)始動(dòng)所以標(biāo)簽起點(diǎn)可以對(duì)齊到肌電能量上升沿而不是視頻里的可見(jiàn)動(dòng)作起點(diǎn)。50ms內(nèi)的偏差可以接受超過(guò)100ms就會(huì)影響模型對(duì)動(dòng)作邊界的判斷。另外要專門錄兩類特殊的標(biāo)簽一類是“空狀態(tài)”也就是手自然放松、不做任何手勢(shì)另一類是“過(guò)渡動(dòng)作”比如手從桌面移到胸前、兩指準(zhǔn)備動(dòng)作等。只錄目標(biāo)詞匯的常見(jiàn)后果是模型在實(shí)時(shí)識(shí)別時(shí)對(duì)空狀態(tài)不停輸出某個(gè)詞因?yàn)橛?xùn)練時(shí)模型從沒(méi)見(jiàn)過(guò)“什么都不做”的輸入分布。加這兩類會(huì)讓類別不平衡嚴(yán)重一些但實(shí)時(shí)體驗(yàn)會(huì)好非常多。2.3 采集流程與文件組織按被試、會(huì)話、手勢(shì)編號(hào)存盤數(shù)據(jù)文件組織直接決定后面能不能做留一被試驗(yàn)證。我一般按這個(gè)目錄結(jié)構(gòu)存原始數(shù)據(jù)dataset/ subject_01/ session_1/ raw_semg.csv raw_imu.csv annot.csv video.mp4 session_2/ raw_semg.csv raw_imu.csv annot.csv subject_02/ session_1/raw_semg.csv的列是timestamp_ms, ch1, ch2, ..., ch8raw_imu.csv的列是timestamp_ms, acc_x, acc_y, acc_z, gyro_x, gyro_y, gyro_z。按被試和會(huì)話分開(kāi)而不是全部堆在一起是為了后續(xù)做跨受試者評(píng)估。同一個(gè)被試在同一個(gè)session里的數(shù)據(jù)高度相關(guān)如果訓(xùn)練集和驗(yàn)證集混著切模型會(huì)記住該受試者的肌肉發(fā)力習(xí)慣而不是真正學(xué)到手勢(shì)的通用表示。采集流程上我建議一個(gè)session不要超過(guò)15分鐘中間至少休息5分鐘。肌肉疲勞之后sEMG的幅度和頻譜會(huì)明顯變化同一個(gè)手勢(shì)在session前段和后段的特征漂移很大模型會(huì)學(xué)到疲勞偽影。每類手勢(shì)重復(fù)20到30遍順序隨機(jī)打亂不要按固定列表循環(huán)否則模型會(huì)學(xué)會(huì)“下一個(gè)手勢(shì)是什么”。標(biāo)注時(shí)最好有視頻參照用手機(jī)攝像頭對(duì)著手部拍然后用一個(gè)明顯的加速度沖擊比如用手拍一下桌面把傳感器時(shí)間軸和視頻時(shí)間軸對(duì)齊這個(gè)動(dòng)作在后期復(fù)核標(biāo)簽時(shí)非常救命。3. 數(shù)據(jù)預(yù)處理去噪、手勢(shì)分割與特征提取的落地順序3.1 sEMG去噪帶通濾波、陷波與因果濾波的取舍表面肌電信號(hào)幅度在微伏到毫伏量級(jí)非常容易被工頻干擾和運(yùn)動(dòng)偽跡污染。國(guó)內(nèi)最常見(jiàn)的干擾是50Hz工頻國(guó)外實(shí)驗(yàn)室代碼里經(jīng)常寫(xiě)60Hz如果你的系統(tǒng)有可能跨地域使用陷波頻率要做成配置項(xiàng)而不是寫(xiě)死。sEMG的有效頻段通常認(rèn)為是20Hz到450Hz低于20Hz的直流偏置和運(yùn)動(dòng)偽跡要濾掉高于450Hz的噪聲也要濾掉。順序上先陷波再帶通也可以把陷波和帶通串成級(jí)聯(lián)濾波器。import numpy as np from scipy.signal import butter, iirnotch, filtfilt, sosfiltfilt def preprocess_semg_offline(raw, fs1000, notch_freq50): # 先陷波再帶通filtfilt 是零相位濾波只用于離線數(shù)據(jù) b, a iirnotch(notch_freq, Q30, fsfs) raw filtfilt(b, a, raw, axis0) sos butter(4, [20, 450], btypeband, fsfs, outputsos) return sosfiltfilt(sos, raw, axis0)這段代碼里filtfilt和sosfiltfilt都是零相位濾波意思是對(duì)信號(hào)做正向?yàn)V波再反向?yàn)V波整體不產(chǎn)生相位偏移非常適合離線處理。但在實(shí)時(shí)識(shí)別里不能用這種濾波器因?yàn)樗盐磥?lái)的數(shù)據(jù)算進(jìn)了當(dāng)前結(jié)果。實(shí)時(shí)端要改用因果濾波器比如lfilter或sosfilt然后接受幾毫秒的群延遲。群延遲對(duì)識(shí)別的影響遠(yuǎn)遠(yuǎn)小于時(shí)間不同步不要為了那幾毫秒去犧牲濾波質(zhì)量。IMU信號(hào)去噪相對(duì)簡(jiǎn)單加速度和陀螺儀各做一個(gè)低通濾波就夠了。但這里有一個(gè)真正需要處理的問(wèn)題重力加速度補(bǔ)償。加速度計(jì)測(cè)到的值是“慣性加速度重力加速度”的合成手語(yǔ)動(dòng)作里手臂一翻轉(zhuǎn)重力在三個(gè)軸上的投影就劇烈變化。如果直接把原始加速度送進(jìn)模型模型很可能學(xué)的是重力方向而不是手勢(shì)運(yùn)動(dòng)。常見(jiàn)做法是先做姿態(tài)解算得到四元數(shù)再把加速度旋轉(zhuǎn)到世界坐標(biāo)系然后減去重力分量[0, 0, 1]。這一步對(duì)手勢(shì)方向泛化至關(guān)重要我在避坑章里會(huì)專門再講。注意濾波參數(shù)不要照搬別人的設(shè)置。sEMG電極類型不同、貼放位置不同、皮膚阻抗不同最優(yōu)高通截止頻率會(huì)有差異。先錄一段靜息和一段握拳看看頻譜再?zèng)Q定。3.2 手勢(shì)分割基于能量閾值和最小持續(xù)時(shí)間的滑窗檢測(cè)分割的作用是把連續(xù)數(shù)據(jù)流切成一個(gè)個(gè)手勢(shì)樣本。離線階段可以用標(biāo)注文件直接切但自動(dòng)分割仍然要做一是用來(lái)校核標(biāo)注二是實(shí)時(shí)識(shí)別時(shí)根本等不到動(dòng)作結(jié)束才去識(shí)別。自動(dòng)分割的核心思路是計(jì)算sEMG的短期能量包絡(luò)和IMU的角速度模值兩者超過(guò)閾值就認(rèn)為是動(dòng)作開(kāi)始。我常用的參數(shù)是RMS滑動(dòng)窗長(zhǎng)度25ms步長(zhǎng)10ms閾值取靜息段能量的均值加3倍標(biāo)準(zhǔn)差活動(dòng)段最短持續(xù)200ms靜默段最短150ms。靜息段的選取要單獨(dú)錄2秒“手放松垂在身側(cè)”的數(shù)據(jù)用前2秒做基線而不是用整段數(shù)據(jù)均值。因?yàn)檎螖?shù)據(jù)里大部分是手勢(shì)動(dòng)作均值會(huì)被拉高最終閾值太高弱手勢(shì)直接漏檢。import numpy as np from scipy.ndimage import binary_closing, binary_opening, label def segment_by_energy(rms_energy, gyro_norm, fs_frame, thr_semg, thr_imu): active (rms_energy thr_semg) | (gyro_norm thr_imu) structure np.ones(int(0.15 * fs_frame), dtypebool) # 150ms 的形態(tài)學(xué)結(jié)構(gòu) active binary_closing(active, structure) # 合并相近碎片 active binary_opening(active, structure) # 去除短促毛刺 labeled, n_seg label(active) segments [] for seg_id in range(1, n_seg 1): idx np.where(labeled seg_id)[0] start_ms idx[0] / fs_frame * 1000 end_ms idx[-1] / fs_frame * 1000 segments.append((start_ms, end_ms)) return segments這段代碼里fs_frame是包絡(luò)幀率不是原始采樣率。如果RMS窗是25ms、步長(zhǎng)10ms那么fs_frame約為100Hz。binary_closing先做閉運(yùn)算把兩個(gè)相隔不到150ms的動(dòng)作碎片連成一個(gè)再做開(kāi)運(yùn)算把不足150ms的噪聲毛刺刪掉。這樣處理之后活動(dòng)段邊界基本就是手勢(shì)的起止位置。如果發(fā)現(xiàn)某些詞匯的起止總是偏長(zhǎng)或偏短優(yōu)先檢查RMS窗長(zhǎng)和形態(tài)學(xué)結(jié)構(gòu)長(zhǎng)度不要盲目改閾值。3.3 特征提取時(shí)域、頻域與IMU姿態(tài)特征的表頭設(shè)計(jì)特征提取在深度學(xué)習(xí)路線里看起來(lái)有點(diǎn)過(guò)時(shí)但它仍然有不可替代的作用手工特征可以用來(lái)訓(xùn)練隨機(jī)森林或前饋神經(jīng)網(wǎng)絡(luò)作為深度學(xué)習(xí)模型的基線當(dāng)深度模型效果不好時(shí)對(duì)比手工特征能快速定位是模型容量不夠還是輸入信息不足。所以我保留了手工特征作為參考。特征組特征名維度每通道說(shuō)明sEMG時(shí)域RMS1反映發(fā)力強(qiáng)度sEMG時(shí)域MAV1平均絕對(duì)值比RMS計(jì)算量小sEMG時(shí)域ZC1過(guò)零率反映信號(hào)頻率變化sEMG時(shí)域WL1波形長(zhǎng)度對(duì)幅度和頻率都敏感sEMG頻域MNF/MDF1平均頻率/中值頻率反映肌肉疲勞IMU線性加速度3去重力后的加速度IMU角速度模值1手部旋轉(zhuǎn)活躍度IMU姿態(tài)角3歐拉角或四元數(shù)def extract_handcrafted_features(semg_seg, imu_seg): feats [] for ch in range(semg_seg.shape[1]): x semg_seg[:, ch] - np.mean(semg_seg[:, ch]) feats.append(np.sqrt(np.mean(x ** 2))) # RMS feats.append(np.mean(np.abs(x))) # MAV feats.append(np.sum(np.abs(np.diff(x 0)))) # ZC feats.append(np.sum(np.abs(np.diff(x)))) # WL for i in range(imu_seg.shape[1]): feats.append(np.mean(imu_seg[:, i])) feats.append(np.std(imu_seg[:, i])) return np.array(feats)需要說(shuō)明的是過(guò)零率不能直接在原始信號(hào)上計(jì)算要先減掉直流分量否則一個(gè)基線漂移就會(huì)讓過(guò)零率失真。WL的計(jì)算量很小但對(duì)sEMG的幅度和頻率變化都很敏感適合作為輕量分類器的輸入。如果后面要直接把原始波形喂給神經(jīng)網(wǎng)絡(luò)手工特征可以不做只需要做標(biāo)準(zhǔn)化。但保留一份特征基線能幫你判斷模型是不是真的學(xué)到了超越手工特征的信息。4. 神經(jīng)網(wǎng)絡(luò)搭建一維卷積與BiLSTM融合的模型結(jié)構(gòu)與訓(xùn)練策略4.1 輸入組織把sEMG和IMU拼成多通道時(shí)間序列神經(jīng)網(wǎng)絡(luò)的輸入最好是固定長(zhǎng)度的滑窗樣本。窗口長(zhǎng)度我通常取500ms因?yàn)槭终Z(yǔ)中一個(gè)手指動(dòng)作從發(fā)力到結(jié)束大多在300ms到800ms之間500ms能覆蓋大部分信息。sEMG按1kHz采樣就是500個(gè)點(diǎn)IMU原本可能只有100Hz需要通過(guò)線性插值補(bǔ)到500個(gè)點(diǎn)。插值之后IMU的高頻細(xì)節(jié)本來(lái)就少模型能學(xué)到的只是低頻姿態(tài)信息不用擔(dān)心插值引入額外的虛假信息。拼合方式是把sEMG的8個(gè)通道和IMU的6個(gè)通道acc、gyro按時(shí)間軸拼成一個(gè)17通道的二維數(shù)組形狀是(17, 500)。這個(gè)數(shù)組作為模型輸入。更精細(xì)的做法是sEMG和IMU分別用小網(wǎng)絡(luò)編碼后再融合但工程上第一種方式更容易跑通而且在這個(gè)任務(wù)上已經(jīng)夠用。通道標(biāo)準(zhǔn)化要非常小心在訓(xùn)練集上逐通道計(jì)算均值和標(biāo)準(zhǔn)差驗(yàn)證集和測(cè)試集必須用訓(xùn)練集存下來(lái)的統(tǒng)計(jì)量不能重新計(jì)算否則等于把驗(yàn)證集信息泄漏進(jìn)訓(xùn)練過(guò)程。標(biāo)準(zhǔn)化參數(shù)保存成JSON推理階段直接加載。4.2 模型結(jié)構(gòu)一維卷積神經(jīng)網(wǎng)絡(luò)提取局部特征BiLSTM建模時(shí)序依賴純前饋神經(jīng)網(wǎng)絡(luò)在這個(gè)任務(wù)上很難出效果因?yàn)槭终Z(yǔ)手勢(shì)是一個(gè)時(shí)間序列同一時(shí)刻的肌肉和姿態(tài)信息不足必須依賴前幾百毫秒的上下文。純CNN也能做但需要堆很多層才能覆蓋足夠大的感受野。我常用的結(jié)構(gòu)是一維卷積神經(jīng)網(wǎng)絡(luò)加雙向LSTM卷積層負(fù)責(zé)提取局部肌電爆發(fā)特征和IMU波形片段特征LSTM負(fù)責(zé)把不同時(shí)間步的信息關(guān)聯(lián)起來(lái)。import torch import torch.nn as nn class SemgImuGestureNet(nn.Module): def __init__(self, n_classes20): super().__init__() self.encoder nn.Sequential( nn.Conv1d(17, 32, kernel_size9, padding4), # 1700ms 局部窗口 nn.BatchNorm1d(32), nn.ReLU(), nn.Conv1d(32, 64, kernel_size5, padding2), nn.BatchNorm1d(64), nn.ReLU(), nn.Dropout(0.3) ) self.lstm nn.LSTM(64, 64, bidirectionalTrue, batch_firstTrue) self.head nn.Sequential( nn.Linear(128, 64), nn.ReLU(), nn.Linear(64, n_classes) ) def forward(self, x): # x: (batch, 17, 500) x self.encoder(x) # (batch, 64, 500) x x.transpose(1, 2) # (batch, 500, 64) out, _ self.lstm(x) # (batch, 500, 128) out out.mean(dim1) # (batch, 128) return self.head(out)第一層卷積核大小9在1kHz采樣率下相當(dāng)于9ms的窗口足夠捕獲單個(gè)肌電動(dòng)作電位波形第二層卷積核大小5相當(dāng)于5ms。兩層卷積都不做池化保留完整時(shí)間分辨率讓LSTM能看到每一步的細(xì)節(jié)。LSTM用雙向結(jié)構(gòu)是因?yàn)殡x線訓(xùn)練時(shí)一個(gè)樣本的完整窗口已經(jīng)拿到前后文都可用實(shí)時(shí)推理時(shí)因?yàn)榇翱谑枪潭ǖ碾p向LSTM在窗口內(nèi)仍然可以做因果處理只是最后一個(gè)時(shí)間步的輸出會(huì)隱含整個(gè)窗口的信息。mean(dim1)對(duì)所有時(shí)間步取平均比只取最后一個(gè)時(shí)間步更穩(wěn)定模型不依賴特定時(shí)間步的位置。這個(gè)模型參數(shù)量很小大約幾十萬(wàn)在幾千個(gè)樣本的小數(shù)據(jù)集上不容易過(guò)擬合。如果你的數(shù)據(jù)量更大可以加大LSTM隱藏層到128或者加一層卷積數(shù)據(jù)量小的時(shí)候增加參數(shù)只會(huì)讓驗(yàn)證指標(biāo)越來(lái)越難看。4.3 訓(xùn)練策略留一被試交叉驗(yàn)證、類別不平衡與數(shù)據(jù)增強(qiáng)訓(xùn)練集和驗(yàn)證集劃分是手語(yǔ)識(shí)別最容易自欺欺人的地方。同一個(gè)受試者的數(shù)據(jù)絕不能同時(shí)出現(xiàn)在訓(xùn)練集和驗(yàn)證集中否則模型記住了這個(gè)人的肌肉發(fā)力習(xí)慣換個(gè)人就崩。我一般用留一被試交叉驗(yàn)證有10個(gè)受試者就訓(xùn)練10折每折拿1個(gè)人做驗(yàn)證其余9個(gè)人做訓(xùn)練最終報(bào)告10折的平均準(zhǔn)確率。這種評(píng)估方式才是真實(shí)穿戴場(chǎng)景的性能下限。類別不平衡在加入“空狀態(tài)”和“過(guò)渡動(dòng)作”后必然出現(xiàn)。最簡(jiǎn)單的處理是用compute_class_weight計(jì)算每個(gè)類別的權(quán)重然后給損失函數(shù)傳入weight。也可以對(duì)空狀態(tài)做欠采樣。我傾向保留全部空狀態(tài)數(shù)據(jù)用加權(quán)損失因?yàn)閷?shí)時(shí)識(shí)別最怕誤觸發(fā)空狀態(tài)樣本越多模型對(duì)靜息輸入的抑制越強(qiáng)。from sklearn.utils.class_weight import compute_class_weight import numpy as np classes np.unique(all_labels) weights compute_class_weight(balanced, classesclasses, yall_labels) class_weights torch.tensor(weights, dtypetorch.float32).to(device) criterion nn.CrossEntropyLoss(weightclass_weights)數(shù)據(jù)增強(qiáng)方面sEMG加5%的高斯噪聲時(shí)間軸隨機(jī)拉伸0.9到1.1倍IMU的三維向量繞重力方向隨機(jī)旋轉(zhuǎn)一個(gè)小角度。這些增強(qiáng)模擬的是佩戴位置偏移和手部朝向變化。增強(qiáng)幅度不能太大sEMG加噪超過(guò)10%會(huì)把肌電信號(hào)淹沒(méi)時(shí)間拉伸超過(guò)1.1倍會(huì)讓手語(yǔ)節(jié)奏失真旋轉(zhuǎn)角度超過(guò)20度會(huì)改變手勢(shì)的實(shí)際語(yǔ)義方向有可能制造錯(cuò)誤樣本。訓(xùn)練配置不需要花哨AdamW優(yōu)化器學(xué)習(xí)率1e-3權(quán)重衰減1e-4批次大小64最大訓(xùn)練60個(gè)epoch早停patience設(shè)為10。每輪記錄驗(yàn)證集損失連續(xù)10輪不下降就恢復(fù)最佳模型權(quán)重。如果驗(yàn)證集損失在訓(xùn)練早期就停住先檢查輸入標(biāo)準(zhǔn)化是否是按訓(xùn)練集統(tǒng)計(jì)量做的如果準(zhǔn)確率始終只有隨機(jī)水平檢查標(biāo)簽和分割窗口的對(duì)應(yīng)關(guān)系這兩處是出現(xiàn)“訓(xùn)練了但學(xué)不到”最常見(jiàn)的原因。5. 實(shí)時(shí)識(shí)別與部署的避坑清單時(shí)間同步、延遲、重力補(bǔ)償與采樣率5.1 坑一sEMG和IMU時(shí)間不同步離線訓(xùn)練和實(shí)時(shí)結(jié)果差距巨大現(xiàn)象離線訓(xùn)練準(zhǔn)確率能做到95%部署到實(shí)時(shí)識(shí)別只有60%同一個(gè)動(dòng)作第一次識(shí)別對(duì)第二次就錯(cuò)。原因離線數(shù)據(jù)處理時(shí)用merge_asof把兩個(gè)傳感器按時(shí)間戳對(duì)齊了實(shí)時(shí)代碼卻只是簡(jiǎn)單地從兩個(gè)串口讀到原始數(shù)據(jù)后直接拼接。兩個(gè)模塊的晶振不同時(shí)鐘漂移造成幾十毫秒的錯(cuò)位在快速手勢(shì)里幾十毫秒足夠讓sEMG的發(fā)力峰值和IMU的角速度峰值錯(cuò)開(kāi)模型看到的“肌肉發(fā)力對(duì)應(yīng)姿態(tài)變化”關(guān)系是亂的。解決在線程里為每個(gè)數(shù)據(jù)包打上主機(jī)接收時(shí)間戳并維護(hù)最近100對(duì)的平均時(shí)間差。如果時(shí)間差超過(guò)5ms丟棄當(dāng)前包或重新插值。更穩(wěn)妥的做法是建立兩個(gè)環(huán)形緩沖區(qū)把IMU樣本按時(shí)間戳插值到sEMG采樣點(diǎn)上每收到一個(gè)sEMG數(shù)據(jù)包就觸發(fā)一次對(duì)齊和推理。不要等到兩個(gè)流都攢滿再處理那樣延遲會(huì)疊加。5.2 坑二把離線分割邏輯搬到實(shí)時(shí)識(shí)別延遲高到?jīng)]法用現(xiàn)象手語(yǔ)動(dòng)作已經(jīng)做完了界面上的識(shí)別詞要等500ms才出來(lái)或者手勢(shì)還沒(méi)結(jié)束就提前識(shí)別成另一個(gè)詞。原因離線預(yù)處理用了filtfilt雙向?yàn)V波整個(gè)手勢(shì)段完成后才能計(jì)算離線分割又要求“檢測(cè)到動(dòng)作結(jié)束”才知道這是一個(gè)完整手勢(shì)。這兩件事在實(shí)時(shí)系統(tǒng)里都不能直接做。實(shí)時(shí)系統(tǒng)只能看到當(dāng)前時(shí)刻之前的數(shù)據(jù)任何雙向?yàn)V波都會(huì)引入等于窗口長(zhǎng)度的延遲任何“等結(jié)束再識(shí)別”的策略都會(huì)讓輸出晚一拍。解決實(shí)時(shí)端改用因果濾波直接用lfilter群延遲只有幾毫秒識(shí)別端改為滑動(dòng)窗口持續(xù)推理用輸出平滑器抑制抖動(dòng)。我常用的參數(shù)是窗口500ms步長(zhǎng)50ms模型每50ms給一個(gè)預(yù)測(cè)再用一個(gè)長(zhǎng)度為5的滑動(dòng)投票器連續(xù)3次投同一個(gè)類別才輸出。這樣輸出延遲約150ms對(duì)實(shí)時(shí)手語(yǔ)識(shí)別來(lái)說(shuō)可接受。from collections import deque votes deque(maxlen5) def online_infer(frame_tensor): pred model(frame_tensor).argmax().item() votes.append(pred) if votes.count(votes[-1]) 3: return votes[-1] return -1 # -1 表示保持靜默5.3 坑三IMU原始加速度里帶重力模型方向泛化差現(xiàn)象訓(xùn)練集里“水平向左”的手勢(shì)識(shí)別得很好“水平向右”總是錯(cuò)受試者手臂稍微傾斜一點(diǎn)就崩潰。原因加速度計(jì)輸出的是“慣性加速度重力加速度”的混合體重力約1g而手部手勢(shì)產(chǎn)生的加速度往往只有0.1g到0.5g。手臂一傾斜重力在三個(gè)軸上的投影大幅變化原始加速度分布完全變樣模型學(xué)到的是重力方向而不是手勢(shì)運(yùn)動(dòng)方向。解決必須做IMU重力加速度補(bǔ)償。常見(jiàn)做法是用Madgwick或互補(bǔ)濾波算法由陀螺儀和加速度計(jì)估計(jì)姿態(tài)四元數(shù)再把加速度從機(jī)體坐標(biāo)系旋轉(zhuǎn)到世界坐標(biāo)系最后減去重力向量[0,0,1]。不需要絕對(duì)航向只要重力方向估計(jì)準(zhǔn)線性加速度就能反映真實(shí)手勢(shì)運(yùn)動(dòng)。姿態(tài)解算頻率和IMU采樣率保持一致先解算再插值對(duì)齊順序不能反。5.4 坑四為了省電把采樣率降到25Hz快速手勢(shì)全被吞掉現(xiàn)象桌面慢速演示時(shí)一切正常戴到手腕上做快速拍手、手指輕彈等動(dòng)作時(shí)識(shí)別率明顯下降模型輸出來(lái)回跳。原因sEMG的有效頻段最高到450HzIMU也跟不上快速翻轉(zhuǎn)25Hz采樣會(huì)把高頻肌電折疊到低頻快速手勢(shì)的細(xì)節(jié)被徹底混疊。有些工程師為了在嵌入式板子上省電把采樣率降得很低這是性價(jià)比最低的優(yōu)化方式。解決sEMG采樣率至少保持500HzIMU至少100Hz。算力不夠就在軟件端降采樣不要在硬件端降采樣。更合理的低功耗方案是讓系統(tǒng)在空閑時(shí)進(jìn)入低功耗待機(jī)用sEMG能量閾值檢測(cè)有效肌電激活后再把采集和推理升到全速手勢(shì)結(jié)束后再恢復(fù)待機(jī)。這樣省電省的是待機(jī)時(shí)間而不是采樣頻率。6. 驗(yàn)證方法留一交叉驗(yàn)證、流式回放與誤觸發(fā)統(tǒng)計(jì)模型訓(xùn)練完之后我至少會(huì)跑三層驗(yàn)證缺一層都會(huì)在真人測(cè)試時(shí)被發(fā)現(xiàn)。第一層是離線留一被試交叉驗(yàn)證畫(huà)出混淆矩陣逐類計(jì)算精確率和召回率。重點(diǎn)看兩類錯(cuò)誤一是“同軌跡不同手形”混淆比如手腕轉(zhuǎn)動(dòng)方向相同但手指屈伸不同二是“同手形不同軌跡”混淆比如手指彎的弧度相同但手臂移動(dòng)方向不同。這兩種混淆說(shuō)明模型依賴的信號(hào)源不對(duì)前者要多靠sEMG區(qū)分后者要多靠IMU區(qū)分。第二層是流式回放。把采集時(shí)存儲(chǔ)的原始數(shù)據(jù)包按實(shí)時(shí)識(shí)別流程逐包讀入讓模型在一個(gè)模擬的流式環(huán)境里輸出預(yù)測(cè)再把預(yù)測(cè)結(jié)果和離線標(biāo)簽做時(shí)間對(duì)齊。這一步能發(fā)現(xiàn)延遲、濾波參數(shù)和滑動(dòng)投票器的問(wèn)題。我會(huì)統(tǒng)計(jì)從真實(shí)手勢(shì)開(kāi)始到系統(tǒng)首次輸出正確類別的時(shí)間差目標(biāo)控制在300ms以內(nèi)如果超過(guò)500ms實(shí)時(shí)體驗(yàn)就會(huì)顯得反應(yīng)遲鈍。第三層是真實(shí)佩戴測(cè)試但只測(cè)誤觸發(fā)率還不夠。我自己的習(xí)慣是錄一段10分鐘的自由手語(yǔ)對(duì)話里面包含大量空狀態(tài)和過(guò)渡動(dòng)作統(tǒng)計(jì)系統(tǒng)在這段時(shí)間里輸出了幾次錯(cuò)誤手勢(shì)詞。誤觸發(fā)每10分鐘超過(guò)3次就說(shuō)明空狀態(tài)抑制不夠需要調(diào)大投票器閾值或增加空狀態(tài)樣本權(quán)重。這里給你一個(gè)流式回放的骨架邏輯加載原始數(shù)據(jù)包按時(shí)間戳順序逐包交給同步模塊和模型記錄每次推理輸出最后與標(biāo)簽對(duì)齊。這個(gè)腳本會(huì)非??菰锏看文P偷家芤槐楹碗x線訓(xùn)練同樣重要。我的教訓(xùn)是再漂亮的離線指標(biāo)到了真人的手腕上都會(huì)因?yàn)榕宕魑恢闷坪蛣?dòng)作速度波動(dòng)掉一截。所以每次改完網(wǎng)絡(luò)或預(yù)處理參數(shù)我都會(huì)先跑一遍流式回放再安排新受試者做一輪快速體驗(yàn)把混淆矩陣和誤觸發(fā)次數(shù)抄進(jìn)實(shí)驗(yàn)記錄本作為下一個(gè)版本的基準(zhǔn)。這套驗(yàn)證閉環(huán)雖然沒(méi)有花哨的可視化卻是讓系統(tǒng)真正可用的鑰匙。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取