Download tutorial.py from wi-lab/lwm-cde: direct link, hf CLI and curl.
- Browser
- Download file 14.8 kB
-
https://huggingface.co/wi-lab/lwm-cde/resolve/main/tutorial.py
- Command line
-
hf download hf://wi-lab/lwm-cde/tutorial.py
-
curl -L -o tutorial.py https://huggingface.co/wi-lab/lwm-cde/resolve/main/tutorial.py
14.8 kB
| """ | |
| Dataset Distancing Tutorial | |
| ============================ | |
| End-to-end: generate datasets from DeepMIMO, split into train/val/test, | |
| compute distances and transfer performance, measure correlation. | |
| Usage: | |
| python tutorial.py | |
| python tutorial.py --trials 5 | |
| python tutorial.py --task beam-pred --n_beams 8 --epochs 150 --batch_size 512 | |
| """ | |
| import argparse, logging, sys, os | |
| parser = argparse.ArgumentParser(description="Dataset Distancing Tutorial") | |
| # paths | |
| parser.add_argument("--lwm_path", type=str, default="third_party/lwm-1.1") | |
| parser.add_argument("--weights_file", type=str, default=None, help="alternative checkpoint (e.g. CDE-finetuned)") | |
| parser.add_argument("--cache_dir", type=str, default="cache/tutorial") | |
| parser.add_argument("--gpu", type=int, default=1) | |
| # data splits | |
| parser.add_argument("--n_train", type=int, default=3000) | |
| parser.add_argument("--n_val", type=int, default=2000) | |
| parser.add_argument("--n_test", type=int, default=2000) | |
| parser.add_argument("--seed", type=int, default=42) | |
| parser.add_argument("--trials", type=int, default=1, help="number of trials with different split seeds") | |
| # scenario generation | |
| parser.add_argument("--task", type=str, default="los-nlos", | |
| choices=["los-nlos", "beam-pred"]) | |
| parser.add_argument("--n_ant", type=int, default=32) | |
| parser.add_argument("--n_sub", type=int, default=32) | |
| parser.add_argument("--n_beams", type=int, default=32, help="codebook size for beam-pred") | |
| # distance computation | |
| parser.add_argument("--distance", type=str, default="swd", choices=["swd", "centroid", "cosine"]) | |
| parser.add_argument("--embed_space", type=str, default="lwm", choices=["lwm", "raw", "umap"], | |
| help="embedding space for distance computation") | |
| parser.add_argument("--umap_dim", type=int, default=2, help="UMAP target dimension") | |
| parser.add_argument("--label_aware", action="store_true", default=True) | |
| parser.add_argument("--no_label_aware", action="store_true") | |
| parser.add_argument("--sw_projections", type=int, default=64) | |
| parser.add_argument("--asymmetric", action="store_true", help="directed SWD + unsymmetrized P") | |
| parser.add_argument("--raw_perf", action="store_true", help="skip P symmetrization (keep symmetric D)") | |
| parser.add_argument("--asym_alpha", type=float, default=1.0, help="strength of asymmetry") | |
| # CDE fine-tuning | |
| parser.add_argument("--cde", action="store_true", help="train CDE before computing distances") | |
| parser.add_argument("--cde_epochs", type=int, default=40) | |
| parser.add_argument("--cde_lr", type=float, default=5e-4) | |
| parser.add_argument("--cde_save", type=str, default=None, help="save CDE checkpoint to this dir") | |
| # performance matrix | |
| parser.add_argument("--epochs", type=int, default=15) | |
| parser.add_argument("--lr", type=float, default=1e-3) | |
| parser.add_argument("--batch_size", type=int, default=128) | |
| # label robustness | |
| parser.add_argument("--label_noise", type=float, default=0.0, help="fraction of labels to flip randomly") | |
| parser.add_argument("--label_drop", type=float, default=0.0, help="fraction of labels to drop (β label-agnostic D)") | |
| parser.add_argument("--log_dir", type=str, default="logs", help="directory for log files") | |
| args = parser.parse_args() | |
| if args.no_label_aware: | |
| args.label_aware = False | |
| os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu) | |
| # ββ Logging setup ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| os.makedirs(args.log_dir, exist_ok=True) | |
| from datetime import datetime | |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| log_file = os.path.join(args.log_dir, f"{args.task}_{timestamp}.log") | |
| log = logging.getLogger("tutorial") | |
| log.setLevel(logging.INFO) | |
| log.handlers.clear() | |
| # File handler | |
| fh = logging.FileHandler(log_file) | |
| fh.setFormatter(logging.Formatter("%(asctime)s %(message)s", datefmt="%H:%M:%S")) | |
| log.addHandler(fh) | |
| # Console handler | |
| ch = logging.StreamHandler() | |
| ch.setFormatter(logging.Formatter("%(message)s")) | |
| log.addHandler(ch) | |
| log.info(f"Command: {' '.join(sys.argv)}") | |
| log.info(f"Args: {vars(args)}") | |
| log.info(f"Log file: {log_file}") | |
| import torch, numpy as np | |
| from dataset_distancing import ( | |
| DatasetCollection, | |
| compute_distances, | |
| compute_performance_matrix, | |
| correlate, | |
| DistanceConfig, | |
| TransferConfig, | |
| ) | |
| from dataset_distancing.embeddings import register_lwm_path, load_runner | |
| register_lwm_path(args.lwm_path) | |
| # ββ Step 1: Generate datasets (once, shared across trials) βββββββββββββββββββ | |
| import deepmimo as dm | |
| scenarios = ( | |
| [{"name": s, "tx_set": tx} | |
| for s in dm.search()[1:5] | |
| for tx in [1]] | |
| + dm.search()[60:65] | |
| ) | |
| full = DatasetCollection.from_scenarios( | |
| scenarios, | |
| task=args.task, | |
| n_ant=args.n_ant, | |
| n_sub=args.n_sub, | |
| n_beams=args.n_beams, | |
| cache_dir=args.cache_dir, | |
| ) | |
| def _print_matrix(name, M): | |
| log.info(f"\n{name}:") | |
| for row in M: | |
| log.info(" " + " ".join(f"{v:.4f}" for v in row)) | |
| def _perturb_labels(collection, noise_frac, drop_frac, seed): | |
| """Flip and/or drop a fraction of labels in each dataset.""" | |
| from dataset_distancing.data import WirelessDataset | |
| g = torch.Generator().manual_seed(seed) | |
| new_ds = [] | |
| for ds in collection.datasets: | |
| if ds.labels is None: | |
| new_ds.append(ds) | |
| continue | |
| labels = ds.labels.clone() | |
| n = len(labels) | |
| # Flip labels | |
| if noise_frac > 0: | |
| n_flip = int(noise_frac * n) | |
| flip_idx = torch.randperm(n, generator=g)[:n_flip] | |
| n_classes = int(labels.max().item()) + 1 | |
| labels[flip_idx] = torch.randint(0, n_classes, (n_flip,), generator=g) | |
| # Drop labels | |
| if drop_frac > 0: | |
| n_drop = int(drop_frac * n) | |
| drop_idx = torch.randperm(n, generator=g)[:n_drop] | |
| labels[drop_idx] = -1 # sentinel | |
| # Keep only samples with valid labels | |
| keep = labels >= 0 | |
| new_ds.append(WirelessDataset( | |
| name=ds.name, | |
| channels=ds.channels[keep], | |
| labels=labels[keep], | |
| targets=ds.targets[keep] if ds.targets is not None else None, | |
| )) | |
| continue | |
| new_ds.append(WirelessDataset( | |
| name=ds.name, channels=ds.channels, labels=labels, | |
| targets=ds.targets, | |
| )) | |
| return DatasetCollection(datasets=new_ds) | |
| # ββ Run trials βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| all_pearson, all_spearman = [], [] | |
| for trial in range(args.trials): | |
| split_seed = args.seed + trial | |
| torch.manual_seed(split_seed) | |
| np.random.seed(split_seed) | |
| # ββ Split ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| train_ds, val_ds, test_ds = [], [], [] | |
| for ds in full: | |
| tr, va, te = ds.split( | |
| train=args.n_train, val=args.n_val, test=args.n_test, seed=split_seed, | |
| ) | |
| train_ds.append(tr) | |
| val_ds.append(va) | |
| test_ds.append(te) | |
| train_col = DatasetCollection(datasets=train_ds) | |
| val_col = DatasetCollection(datasets=val_ds) | |
| test_col = DatasetCollection(datasets=test_ds) | |
| # ββ Perturb labels for distance computation only ββββββββββββββββββ | |
| # Keep clean labels for P (ground-truth transfer performance), | |
| # perturb only the copies used for label-aware D. | |
| if args.label_noise > 0 or args.label_drop > 0: | |
| train_col_noisy = _perturb_labels(train_col, args.label_noise, args.label_drop, split_seed) | |
| else: | |
| train_col_noisy = train_col | |
| if trial == 0: | |
| log.info(f"\n{'='*60}") | |
| log.info(f"Generated {len(full)} datasets " | |
| f"(train/val/test = {args.n_train}/{args.n_val}/{args.n_test}):") | |
| for tr, te in zip(train_col, test_col): | |
| n0 = (tr.labels == 0).sum().item() | |
| log.info(f" {tr.name}: train={len(tr)}, test={len(te)}, " | |
| f"LoS%={100*n0/max(len(tr),1):.0f}%") | |
| # ββ Load runner ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| import io, contextlib | |
| with contextlib.redirect_stdout(io.StringIO()): | |
| runner = load_runner(args.lwm_path, weights_file=args.weights_file) | |
| # ββ P matrix βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| verbose = (args.trials == 1) | |
| P = compute_performance_matrix( | |
| train_col, | |
| cfg=TransferConfig( | |
| task=args.task, | |
| epochs=args.epochs, | |
| lr=args.lr, | |
| batch_size=args.batch_size, | |
| n_beams=args.n_beams, | |
| seed=split_seed, | |
| symmetrize=not (args.asymmetric or args.raw_perf), | |
| verbose=verbose, | |
| ), | |
| val_collection=val_col, | |
| test_collection=test_col, | |
| ) | |
| # ββ CDE (optional) βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if args.cde: | |
| from dataset_distancing.cde import CDETrainer | |
| from dataset_distancing import CDEConfig | |
| cde_cfg = CDEConfig( | |
| epochs=args.cde_epochs, | |
| lr=args.cde_lr, | |
| n_per_dataset=64, | |
| n_per_dataset_eval=128, | |
| corr_n_per_dataset=64, | |
| asymmetric=args.asymmetric, | |
| distance=DistanceConfig( | |
| sw_projections=args.sw_projections, | |
| label_aware=args.label_aware, | |
| asymmetric=args.asymmetric, | |
| ), | |
| ) | |
| trainer = CDETrainer(runner, cde_cfg) | |
| trainer.fit(train_col, P, eval_collection=train_col, perf_eval=P, verbose=verbose) | |
| if args.cde_save and trial == 0: | |
| trainer.save(args.cde_save) | |
| # ββ D matrix βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if args.embed_space == "lwm": | |
| D = compute_distances(train_col_noisy, runner, cfg=DistanceConfig( | |
| distance=args.distance, | |
| label_aware=args.label_aware, | |
| n_per_dataset=len(train_col[0]), | |
| sw_projections=args.sw_projections, | |
| asymmetric=args.asymmetric, | |
| asymmetric_alpha=args.asym_alpha, | |
| )) | |
| else: | |
| # Raw or UMAP: build EmbeddingResult manually | |
| from dataset_distancing.embeddings import EmbeddingResult, channels_to_patches | |
| from dataset_distancing.distances import compute_distances_from_embeddings | |
| # Vectorize channels: (N, 1, ant, sub) complex β (N, 2*ant*sub) float | |
| # Subsample to common size across datasets | |
| n_per = min(len(ds) for ds in train_col_noisy.datasets) | |
| all_embs, all_labels = [], [] | |
| for ds in train_col_noisy.datasets: | |
| idx = torch.randperm(len(ds))[:n_per] | |
| ch = ds.channels[idx] | |
| labels = ds.labels[idx] if ds.labels is not None else None | |
| if ch.is_complex(): | |
| flat = torch.cat([ch.real.reshape(n_per, -1), | |
| ch.imag.reshape(n_per, -1)], dim=1).float() | |
| else: | |
| flat = ch.reshape(n_per, -1).float() | |
| if args.embed_space == "umap": | |
| import warnings, umap | |
| from sklearn.preprocessing import StandardScaler | |
| with warnings.catch_warnings(): | |
| warnings.simplefilter("ignore") | |
| scaled = StandardScaler().fit_transform(flat.numpy()) | |
| reducer = umap.UMAP(n_components=args.umap_dim, n_neighbors=16, | |
| min_dist=0.1, random_state=split_seed) | |
| flat = torch.from_numpy(reducer.fit_transform(scaled)).float() | |
| all_embs.append(flat.unsqueeze(0)) | |
| all_labels.append(labels) | |
| emb_result = EmbeddingResult( | |
| embeddings=torch.cat(all_embs, dim=0), | |
| labels=all_labels, | |
| dataset_names=train_col_noisy.names(), | |
| ) | |
| D = compute_distances_from_embeddings(emb_result, cfg=DistanceConfig( | |
| distance=args.distance, | |
| label_aware=args.label_aware, | |
| sw_projections=args.sw_projections, | |
| asymmetric=args.asymmetric, | |
| asymmetric_alpha=args.asym_alpha, | |
| )) | |
| # ββ Correlate ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| stats = correlate(D, P, symmetric=not (args.asymmetric or args.raw_perf)) | |
| all_pearson.append(stats['pearson']) | |
| all_spearman.append(stats['spearman']) | |
| if args.trials == 1: | |
| _print_matrix("Distance matrix D", D.numpy()) | |
| _print_matrix("Performance matrix P", P.numpy()) | |
| log.info(f"\n{'='*60}") | |
| log.info(f"Pearson = {stats['pearson']:.4f}") | |
| log.info(f"Spearman = {stats['spearman']:.4f}") | |
| log.info(f"{'='*60}") | |
| else: | |
| log.info(f" Trial {trial+1}/{args.trials} (seed={split_seed}): " | |
| f"r={stats['pearson']:.4f}, Ο={stats['spearman']:.4f}") | |
| # ββ Summary ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if args.trials > 1: | |
| p = np.array(all_pearson) | |
| s = np.array(all_spearman) | |
| log.info(f"\n{'='*60}") | |
| log.info(f"Over {args.trials} trials:") | |
| log.info(f" Pearson = {p.mean():.4f} Β± {p.std():.4f}") | |
| log.info(f" Spearman = {s.mean():.4f} Β± {s.std():.4f}") | |
| log.info(f"{'='*60}") | |
| # Examples, from the repository root: | |
| # python tutorial.py --task los-nlos --n_train 400 --n_test 400 --epochs 200 --batch_size 32 --seed 0 --trials 20 | |
| # python tutorial.py --task beam-pred --n_beams 32 --n_train 2000 --n_test 2000 --epochs 200 --batch_size 512 --seed 0 --trials 20 | |
| # python tutorial.py --task los-nlos --embed_space raw # the same on raw channels | |
| # python tutorial.py --task los-nlos --cde --cde_save ckpt # fine-tune with the LWM-CDE objective first | |