Code for Meta-Transfer Learning-Based Cross-Domain Gesture Recognition Using WiFi Channel State Information, Penglin Dai, Junfei Zhou, Jialong Ma, Hao Zhang, Xiao Wu. IEEE Transactions on Consumer Electronics, 71(2): 2530-2543, 2025.
WiFi CSI changes with location, orientation, user and environment, so a gesture classifier trained in one domain degrades in another. This code trains in two stages:
- Transfer learning (unsupervised domain adaptation). A ResNet feature extractor is trained adversarially against a domain discriminator, so that its features describe the gesture rather than the domain.
- Meta-learning (MAML). With the feature extractor frozen, the gesture classifier is meta-trained so that a few labelled samples from a new domain are enough to adapt it (6-way 1-shot / 5-shot).
Evaluation uses Widar3.0 (in-domain, cross-location, cross-orientation, cross-user, cross-environment, multi-domain) and a self-collected real-world set.
raw CSI ─► CSI ratio + Savitzky-Golay ─► heatmap .jpg (Sec. 3.1-3.2, done offline)
Stage 1 train_uda.py (Sec. 3.3-3.4, Algorithm 1)
source (labelled) ─┐ ┌─► G_y gesture classifier ───────► L_y (Eq. 13)
├─► G_f ResNet-18+SE ─┤
target (unlabelled) ─┘ └─► GRL R ─► G_d domain discr. ─► L_d (Eq. 14)
L = L_y + L_d (Eq. 15)
Stage 2 train_meta.py, G_f frozen (Sec. 3.5, Algorithm 2)
meta-train on source tasks: inner loop on the support set (Eq. 16), outer loop on the query set (Eq. 17)
meta-test on target tasks: adapt on the k-shot support set, classify the query set
| Paper | Code |
|---|---|
| Feature extractor G_f (Fig. 6, Eq. 7) | metatrans/models/uda.py · FeatureExtractor |
| Gesture classifier G_y (Fig. 8, Eq. 9) | metatrans/models/uda.py · GestureClassifier |
| Domain discriminator G_d (Fig. 9, Eq. 10) | metatrans/models/uda.py · DomainDiscriminator |
| Gradient reversal layer (Eq. 11-12) | metatrans/models/uda.py · GradientReversalFn |
| Stage-1 training (Eq. 13-15, Algorithm 1) | train_uda.py |
| MAML classifier, inner loop (Eq. 16) | metatrans/models/maml.py · MAML.adapt |
| Task sampling, outer loop (Eq. 17, Algorithm 2) | metatrans/data/episodic.py, train_meta.py |
train_uda.py Stage 1: feature extractor with UDA (or without, `uda: false`)
train_meta.py Stage 2: MAML gesture classifier + few-shot evaluation
metatrans/
data/csi_dataset.py heatmap dataset for Stage 1
data/episodic.py N-way K-shot episode sampling for Stage 2
models/uda.py G_f, G_y, G_d, gradient reversal
models/maml.py fast-weight layers, meta classifier, MAML inner loop
utils.py config loading, log folders, confusion-matrix I/O
configs/
uda/ Stage 1 with UDA, one file per experiment
no_uda/ Stage 1 ablation without UDA
meta/ Stage 2
aril/ ARIL dataset (cross-location), not in the paper
scripts/ launchers grouped by experiment
tools/ preprocessing, plots, model complexity / FPS
docs/ result tables used for figures
pip install -r requirements.txtRun every command from the repository root: the data and log paths in the configs are relative to it.
The loaders read CSI heatmaps that are already rendered (CSI ratio between two antennas, Savitzky-Golay smoothing, colour-mapped to an image, paper Sec. 3.2). This rendering step for Widar3.0 is not part of this repository.
data/
env/ Widar3.0 heatmaps: {user}-{gesture}-{location}-{orientation}-{repetition}.jpg
our_env/ self-collected set: me-{gesture}-1-1-{repetition}.jpg, repetitions 1..230
aril/amp/ ARIL heatmaps written by tools/preprocess_aril.py
- Gestures 1-6: Push&Pull, Sweep, Clap, Slide, Draw-O (Horizontal), Draw-Zigzag (Horizontal).
- Widar3.0 domains: locations 1-5, orientations 1-5, repetitions 1-5.
- User groups used in the configs:
env1_users= user5, 10-15, 17;env2_users= user1, 2, 3, 6;env3_users= user33, 7, 8, 9.
python train_uda.py --hypes_yaml configs/uda/cross_env1_adam.yamlA config lists three domain splits, each given as data_list + users / ges / pos / ori / rep:
source_domain_data (labelled), target_domain_data (used without labels by the domain
discriminator) and test_data. Outputs go to logs/<name>_<timestamp>/:
| File | Content |
|---|---|
epoch_<e>_featureExtractor.pth, epoch_<e>_model.pth |
saved whenever test accuracy improves |
loss1.txt, loss2.txt |
L_y and L_d per iteration (loss2.txt only with UDA) |
acc.txt |
test accuracy per epoch |
best_confusion_matrix.txt |
row-normalised confusion matrix of the best epoch |
Point feature_extractor_pth in the config at a Stage-1 checkpoint, or pass it on the command line:
python train_meta.py --hypes_yaml configs/meta/cross_env1.yaml \
--feature_extractor_pth logs/<stage1_run>/epoch_<e>_featureExtractor.pthmeta_learning sets shot (1 or 5), the inner loop (inner_steps, inner_lr), the outer loop
(meta_lr, tasks_per_update) and the number of training / testing episodes. Each class in an
episode draws n_fea domain cells (user × location × orientation) × 5 samples; the first shot
samples form the support set and the rest the query set. Outputs: acc_training.txt and
acc_testing.txt (running accuracy per episode), best_confusion_matrix_testing.txt,
meta_classifier.pth.
Ablations:
python train_meta.py --hypes_yaml configs/meta/cross_env1.yaml --reinit_before_test # w/o meta-training
python train_meta.py --hypes_yaml configs/meta/cross_env1.yaml --test_noise_mean 0.4 --test_noise_std 0.1 # noisy target inputs
python train_uda.py --hypes_yaml configs/no_uda/cross_env1.yaml # w/o UDA| Paper section | Config | Setting |
|---|---|---|
| 4.2.5 Cross-environment | configs/uda/cross_env1_adam.yaml, cross_env2.yaml, cross_env3.yaml |
target = env1 / env2 / env3, source = the other two |
configs/uda/cross_env1.yaml |
target env1, source = all 16 users (env1 users included) | |
| 4.2.7 Multiple domains | configs/uda/multicross_lo.yaml, multicross_lop.yaml, multicross_lope.yaml |
train on locations/orientations {1,2,4,5}, test on location 3 + orientation 3; LOP adds unseen env1 users, LOPE unseen users from all three environments |
| 4.2.11 Real-world test | configs/uda/cross_our_env.yaml |
source = env1 users, target = data/our_env |
| Stage 2 | configs/meta/cross_env1.yaml |
6-way 5-shot, target env1 |
| Ablation w/o UDA | configs/no_uda/cross_env1.yaml, cross_our_env.yaml |
|
| ARIL (extra) | configs/aril/cross_loc_uda.yaml, cross_loc_meta.yaml |
cross-location |
The in-domain, cross-location, cross-orientation and cross-user settings (Sec. 4.2.1-4.2.4) use the
same configs with different pos / ori / users lists, e.g. cross-location trains on
pos: [1, 2, 3, 4] and adapts to / tests on pos: [5].
scripts/ holds launchers for these groups (cross_environment.sh, multi_domain.sh,
real_world.sh, ablation.sh); set CUDA_VISIBLE_DEVICES to pick a GPU.
Run as modules from the repository root, e.g. python -m tools.plot_curves uda logs/<run>.
| Script | Purpose |
|---|---|
tools/preprocess_aril.py |
render the ARIL .mat files into heatmaps named like Widar3.0 |
tools/plot_csi.py |
plot the raw CSI time series of .mat files |
tools/csi_kurtosis.py |
mean kurtosis of the CSI series per gesture |
tools/plot_curves.py |
Stage-1 loss/accuracy curves; Stage-2 accuracy with vs. without meta-training |
tools/plot_confusion_matrix.py |
camera-ready confusion matrix from a *confusion_matrix*.txt |
tools/complexity.py |
parameter count and GFLOPs |
tools/benchmark_fps.py |
inference throughput |
tools/clean_logs.py |
delete log folders without checkpoints (--dry_run to preview) |
docs/realworld_confusion_matrix.txt is the confusion matrix of the real-world test (Fig. 14b).
Details of the code that the paper does not spell out:
- G_f is the ImageNet-pretrained ResNet-18 trunk followed by a squeeze-and-excitation block on the 512-channel output.
- Stage-1 G_y is Linear(512, 128) - BatchNorm - ReLU - Linear(128, 6), trained with cross-entropy on logits. G_d is a 512-512-512-1 MLP trained with binary cross-entropy on a single source/target logit.
- The GRL coefficient ramps from 0 to 1 as
2 / (1 + exp(-10 p)) - 1over training (Ganin et al., JMLR 2016). Each config chooses SGD or Adam. - MAML is first-order: the inner-loop gradients are detached. The meta classifier's BatchNorm always normalises with the current batch statistics.
- The learning-rate scheduler is stepped with
scheduler.step(epoch)as in the original runs, so each decay takes effect one epoch after its milestone. - Checkpoints from the first release contain unused
lstm.*/wparameters;load_feature_extractordrops them, so those checkpoints still load.
@article{dai2025meta,
title={Meta-transfer learning-based cross-domain gesture recognition using WiFi channel state information},
author={Dai, Penglin and Zhou, Junfei and Ma, Jialong and Zhang, Hao and Wu, Xiao},
journal={IEEE Transactions on Consumer Electronics},
volume={71},
number={2},
pages={2530--2543},
year={2025},
publisher={IEEE}
}