# prepare_data.py — Documentation This document downloads the Banking77 dataset, partitions it across 10 simulated clients (IID and Non-IID), and assigns realistic hardware resource profiles to each client — matching the experimental setup in the HAFLQ paper --- ## Quick Start ```bash pip install datasets numpy pandas python datasets/prepare_data.py ``` Expected output: ``` Loading Banking77 dataset... Cleaning data... Remaining samples: 9993 Cleaning data... Remaining samples: 3076 == IID Partition === Client 0: 1000 samples (IID) Client 1: 1000 samples (IID) Client 2: 1000 samples (IID) Client 3: 999 samples (IID) ... == Non-IID Partition === Client 0: 1334 samples, labels: [13, 32, 54, 59, 73]... Client 1: 1216 samples, labels: [9, 10, 23, 51, 58]... ... Validating IID partition... [OK] All 10 client files exist and are well-formed. [OK] Total samples allocated across all clients: 9993 [OK] Validation for IID successful! Validating NONIID partition... [OK] All 10 client files exist and are well-formed. [OK] Total samples allocated across all clients: 12886 [OK] Clients have diverse, distinct label sets (Non-IID check passed). [OK] Validation for NONIID successful! == Saving Summary & Global Test Set === Saved dataset summary to .../data/dataset_summary.json Done! Data ready for federated training. ``` > [!NOTE] > Banking77 training data has exactly 10,003 samples. Dividing this among 10 clients results in an uneven split of 1001 samples for clients 0-2 and 1000 samples for the remaining clients. --- ## What This Script Produces ``` data/ ├── clients/ │ ├── iid/ │ │ ├── client_0.json │ │ ├── client_1.json │ │ └── ... (10 files) │ └── noniid/ │ ├── client_0.json │ ├── client_1.json │ └── ... (10 files) └── dataset_summary.json ``` Each `client_X.json` file contains everything a federated client needs: its local dataset AND its hardware resource profile. Example: ```json { "client_id": "client_0", "split_type": "noniid", "resources": { "tier": "low", "lora_rank": 2, "freeze_ratio": 0.75, "distance_m": 1100, "max_bits_mb": 10.0 }, "data": { "texts": ["I want to check my balance", "Was my transfer sent?", "..."], "labels": [3, 45, 12], "size": 847 } } ``` --- ## Dataset: Banking77 | Property | Value | |-----------------|------------------------------------| | Source | HuggingFace — `PolyAI/banking77` | | Task | Text classification (intent detection) | | Training samples | 10,003 | | Test samples | 3,080 | | Number of labels | 77 banking intent categories | | Example labels | `balance_inquiry`, `card_stolen`, `transfer_abroad` | | Paper reference | Used directly in HAFLQ (Section VII) | Why Banking77? The HAFLQ paper evaluated on this exact dataset, so our simulated results are directly comparable to the paper's reported numbers. --- ## Configuration Constants These are at the top of the file. Change them to adjust the simulation. | Constant | Default | Meaning | |---------------------------|---------|----------------------------------------------| | `NUM_CLIENTS` | `10` | Number of simulated federated clients | | `LABELS_PER_CLIENT_NONIID`| `20` | How many of 77 labels each client sees (HAFLQ convergence standard) | | `SAMPLES_PER_LABEL_RATIO` | `0.5` | Fraction of samples per label assigned to a client | | `OUTPUT_DIR` | `data/clients` | Where JSON files are saved | | `SEED` | `42` | Random seed — keeps results reproducible | --- ## IID vs Non-IID Partitioning ### IID (Independent and Identically Distributed) Every client gets a random, equal slice of all 77 labels. Unrealistic but used as a baseline comparison. ``` Client 0: 1000 samples — ~13 samples per label — all labels represented Client 1: 1000 samples — ~13 samples per label — all labels represented ... ``` **How it works in code:** ```python indices = np.random.permutation(len(data)) # shuffle all 10,003 indices splits = np.array_split(indices, num_clients) # cut into 10 equal piles ``` Each pile is random so label distribution is roughly equal across clients. --- ### Non-IID (Real World Scenario) Each client only sees data from 20 out of 77 labels. This simulates reality — different bank branches serve different customer types. ``` Client 0: 2315 samples — only sees labels [2, 5, 11, 14, 22, 27, 34, 39, 41, ...] Client 1: 2190 samples — only sees labels [8, 19, 23, 31, 38, 45, 50, 55, 60, ...] ... ``` **How it works in code:** ```python # Step 1: Group all samples by their label label_to_indices = defaultdict(list) for idx, item in enumerate(data): label_to_indices[item["label"]].append(idx) # Step 2: Shuffle the label list so assignment is random all_labels = list(label_to_indices.keys()) np.random.shuffle(all_labels) # Step 3: Give each client a sliding window of 8 labels start = (i * labels_per_client) % len(all_labels) assigned_labels = [all_labels[(start + j) % len(all_labels)] for j in range(labels_per_client)] ``` Each client then receives the first 50% of samples for each of its assigned labels (so labels can overlap between clients — realistic). **Why non-IID makes federated learning harder:** When clients train on different distributions, their model updates point in different directions. Averaging them causes "client drift" — the global model gets pulled in conflicting directions and converges slowly or not at all. This is the core problem that HAFLQ and AFLoRA are designed to solve. --- ## Client Resource Profiles This is where the federated learning research connects to the data setup. Each client is assigned a resource profile matching the HAFLQ paper (Section VII, Table II). ### The Three Tiers | Tier | Clients | LoRA Rank | Freeze Ratio | Distance | |--------|---------|-----------|--------------|---------------| | Low | 0, 1, 2 | 2 | 0.50 | 1100–1300 m | | Medium | 3, 4, 5 | 4 | 0.50 | 1400–1600 m | | High | 6, 7, 8, 9 | 8 | 0.00 | 1700–2000 m | --- ### Field-by-Field Explanation #### `tier` — Compute Category Think of this as the class of hardware: - `"low"` — weak laptop, old GPU, very limited memory - `"medium"` — decent desktop, mid-range GPU - `"high"` — powerful workstation, modern GPU server This is the root from which all other resource values derive. --- #### `lora_rank` — LoRA Adapter Size LoRA replaces full model fine-tuning with two small matrices B and A: ``` ΔW = B × A ``` The rank `r` controls how big these matrices are. If the original weight matrix W is 1000×1000, then: - Rank 2 → B is 1000×2, A is 2×1000 → 4,000 trainable parameters - Rank 4 → B is 1000×4, A is 4×1000 → 8,000 trainable parameters - Rank 8 → B is 1000×8, A is 8×1000 → 16,000 trainable parameters Higher rank = more expressive = better accuracy = more compute required. **Why different ranks across clients?** Forcing all clients to use rank 2 (to match the weakest) wastes the potential of powerful clients. Our system lets each client use the rank appropriate for its hardware — this is the heterogeneity problem that HETLoRA, HAFLQ, and AFLoRA all address. | Client Tier | LoRA Rank | Parameters Trained | |-------------|-----------|-------------------| | Low | 2 | ~4,000 | | Medium | 4 | ~8,000 | | High | 8 | ~16,000 | --- #### `freeze_ratio` — Fraction of Parameters Frozen This comes directly from the **importance-based parameter freezing scheme** in HAFLQ (Section IV-C). With rank 8, there are 8 "rank-1 matrices" (components) in the LoRA adapter. A weak client cannot compute gradients for all 8. Two options: **Option A — Truncation (bad):** Give the weak client only 2 rank-1 matrices. The global model loses information about the other 6 dimensions permanently. **Option B — Freezing (good, what we use):** Give the weak client all 8 rank-1 matrices, but freeze 6 of them (lock their values). Only update the 2 most important ones. The global model retains all 8 dimensions — nothing is lost. ``` freeze_ratio = 0.50 → 50% of rank-1 matrices are frozen → for rank 2: freeze 1, train 1 → for rank 4: freeze 2, train 2 freeze_ratio = 0.00 → freeze nothing, train all 8 ``` The server tells clients which rank-1 matrices are most important (using importance scores) so clients always freeze the least important ones. Low-tier clients use `lora_rank = 2` with `freeze_ratio = 0.5` to train exactly 1 rank-1 component. Trainable component count is calculated as `max(1, int(round((1 - freeze_ratio) * lora_rank)))`. --- #### `distance_m` — Distance From Base Station (metres) The HAFLQ paper models wireless uplink communication between clients and a base station (like a 5G tower). Distance directly affects signal quality and therefore how much data a client can transmit per round. ``` Client 0: 1100 m ← closest, strongest signal Client 1: 1200 m Client 2: 1300 m ... Client 9: 2000 m ← furthest, weakest signal ``` This matches the exact setup stated in the paper: > "clients are positioned at increasing distances from the base station, > ranging from 1100 meters to 2000 meters in increments of 100 meters" --- #### `max_bits_mb` — Maximum Uploadable Data Per Round (MB) Derived from distance using a simplified version of the Shannon capacity formula used in HAFLQ (Equation 11–12): ```python max_bits_mb = round(10.0 * (2000 - distance_m) / 900, 2) ``` | Client | Distance | Max Upload | |--------|----------|------------| | 0 | 1100 m | 10.00 MB | | 3 | 1400 m | 6.67 MB | | 6 | 1700 m | 3.33 MB | | 9 | 2000 m | 0.50 MB | > [!NOTE] > Client 9's bandwidth is bound by a minimum communication floor of 0.5 MB so it can still upload updates. This value is used in `run_mvp.py` to simulate the importance-aware bandwidth-adaptive quantization: clients with low bandwidth must compress their updates more aggressively to fit within this limit. --- ## Functions Reference ### `load_banking77()` Downloads and returns the Banking77 dataset from HuggingFace. ```python train_data, test_data = load_banking77() # train_data: 10,003 samples ``` No parameters. Requires internet connection on first run. Cached locally after first download. --- ### `iid_partition(data, num_clients)` Randomly splits data equally across clients. Each client gets `len(data) / num_clients` samples with similar label distribution. | Parameter | Type | Description | |--------------|---------|--------------------------------| | `data` | Dataset | HuggingFace dataset object | | `num_clients`| int | Number of clients to split into| Returns `dict` — keys are `"client_0"` through `"client_9"`, values are dicts with `texts`, `labels`, `size`. --- ### `noniid_partition(data, num_clients, labels_per_client=8)` Splits data so each client only sees a subset of labels. Simulates real-world data heterogeneity. | Parameter | Type | Description | |---------------------|---------|--------------------------------------| | `data` | Dataset | HuggingFace dataset object | | `num_clients` | int | Number of clients | | `labels_per_client` | int | How many labels each client receives | Returns same format as `iid_partition`. Clients with overlapping label assignments receive 50% of that label's samples. --- ### `assign_client_resources()` Creates hardware resource profiles for all clients matching the HAFLQ paper's experimental setup (Section VII). No parameters. Returns `dict` — keys are `"client_0"` through `"client_9"`, values are resource profile dicts with tier, lora_rank, freeze_ratio, distance_m, max_bits_mb. --- ### `save_client_data(client_data, resources, split_type)` Merges data and resource profiles and saves one JSON file per client. | Parameter | Type | Description | |---------------|--------|---------------------------------------| | `client_data` | dict | Output from iid/noniid partition | | `resources` | dict | Output from assign_client_resources() | | `split_type` | str | `"iid"` or `"noniid"` | Saves to `data/clients/{split_type}/client_{i}.json`. --- ### `save_summary(train_data, test_data, resources)` Saves a human-readable summary of the entire data setup to `data/dataset_summary.json`. Useful for documentation and double-checking the setup is correct. --- ## Paper References | Concept | Paper | Section | |---------------------|---------------|----------------| | LoRA rank heterogeneity | HAFLQ | Section IV | | Parameter freezing scheme | HAFLQ | Section IV-C | | Wireless channel model | HAFLQ | Section II-C | | Client distance setup | HAFLQ | Section VII | | Non-IID partitioning | AFLoRA | Section VI-A | | Banking77 dataset | HAFLQ | Section VII | | FedAvg baseline | DFL Survey | Section II-A | --- ## What This Script Does NOT Do - **Tokenization:** Raw sentences are stored as strings in JSON. Client trainers (e.g., `local_trainer.py`) must tokenize them at runtime using their model's specific tokenizer (e.g., `AutoTokenizer.from_pretrained(...)`), applying appropriate BOS tokens, padding, truncation, and attention masks. - **Label Alignment/Head Mapping:** Labels are saved as raw integer indices (0-76). The local training loop must choose whether to use sequence classification (adding a classification head with `AutoModelForSequenceClassification`) or generative classification (prompting the model and parsing the generated output). ## Common Errors | Error | Cause | Fix | |-------|-------|-----| | `ModuleNotFoundError: datasets` | Library not installed | `pip install datasets` | | `ConnectionError` | No internet | Run on a machine with internet access for first download | | `FileNotFoundError: data/clients` | Output dir missing | Script creates it automatically — check write permissions | | Different sample counts each run | Seed not set | `SEED = 42` is set at top — do not remove `np.random.seed(SEED)` | | `KeyError: 'text'` | Wrong dataset field name | Banking77 uses `"text"` and `"label"` — do not rename | --- ## Dependencies ``` datasets>=2.0.0 # HuggingFace datasets library numpy>=1.21.0 # Array operations and random shuffling ``` Install with: ```bash pip install datasets numpy ``` --- *This file is part of the AMD Hackathon project on efficient federated fine-tuning of LLMs. See `docs/README.md` for the full project overview.*