All articles
Tutorials

RuView AI Model Training: WiFi CSI to Edge Deep Learning

A comprehensive guide on collecting raw Channel State Information datasets, performing PCA and Butterworth preprocessing, training PyTorch deep learning models, and compiling to ONNX for edge inference.

·22 min
RuView AI Model Training: WiFi CSI to Edge Deep Learning

Overview of the CSI Deep Learning Pipeline

Overview of the CSI Deep Learning Pipeline

Channel State Information (CSI) signals represent highly complex spatial-temporal maps of radio wave propagation. When a human moves through a wireless field, they introduce multipath scattering, causing fluctuations in the amplitude and phase of individual OFDM subcarriers.

To transform these raw radio fluctuations into high-accuracy actions—such as human presence detection, fall alerts, or respiratory rate monitoring—we must feed the cleaned subcarrier time-series data into a specialized deep learning model.

This tutorial covers the end-to-end pipeline: collecting raw training data from your ESP32 streams, applying digital signal filters (DSP), reducing noise with Principal Component Analysis (PCA), designing a custom Convolutional + LSTM neural network in PyTorch, and compiling the final weights to an optimized **ONNX** format for local deployment on your RuView Rust gateway.

Step 1: Raw CSI Data Collection & Labeling

Before we can train any deep learning model, we must compile a diverse, labeled dataset. The RuView Rust engine features a built-in logging tool that dumps raw incoming UDP CSI subcarrier matrices directly to timestamped CSV files.

To capture datasets for training, run the engine with the data logger flag enabled:

./ruview-engine --port 8082 --log-raw-csi --output-dir ./dataset

This logs a matrix of 52 columns (representing the 52 usable data subcarriers of a 20 MHz 802.11 OFDM channel) and thousands of rows (representing the time series at e.g., 80Hz packet transmission rate).

The Labeling Protocol:

You must systematically collect 5 to 10 minutes of data for each target activity class. Ensure you maintain strict labeling protocols:

  • Class 0: Empty Room (Baseline): Capture data with absolutely no movement in the room. This logs the static multipath scattering of walls, doors, and furniture.
  • Class 1: Human Walking: Have a volunteer walk in random patterns within the wireless Fresnel zones between the TX and RX nodes.
  • Class 2: Stationary / Sitting: Have a volunteer sit quietly in a chair, executing minimal micro-movements.
  • Class 3: Breathing / Vitals: Place the RX node close to a volunteer's chest while they sit/lie down, capturing micro-oscillations caused by respiration.

Step 2: Signal Preprocessing (Butterworth & PCA)

Step 2: Signal Preprocessing (Butterworth & PCA)

Raw CSI data is extremely noisy. High-frequency electrical interference from surrounding electronics, temperature-driven phase drift on the ESP32 transceiver, and ambient motion generate subcarrier noise. We must apply a preprocessing pipeline before passing data to the neural network.

1. High-Pass Butterworth Filtering:

Static objects like concrete walls, chairs, and tables reflect radio signals at a constant phase. This static multipath generates a large Direct Current (DC) offset in the time series. To remove these static baselines and isolate human motion, we apply a bandpass **Butterworth filter** with cutoff frequencies between 0.3 Hz and 15 Hz:

from scipy.signal import butter, filtfilt def butter_bandpass_filter(data, lowcut=0.3, highcut=15.0, fs=80.0, order=5): nyq = 0.5 * fs low = lowcut / nyq high = highcut / nyq b, a = butter(order, [low, high], btype='band') return filtfilt(b, a, data, axis=0)

2. Principal Component Analysis (PCA):

Streaming 52 raw subcarriers introduces high dimensionality, which increases computational load for edge inference. Because adjacent subcarriers are highly correlated, we can run **PCA** to project the 52 subcarriers down to the top **3 Principal Components** that capture 95% of signal variance caused by physical movement:

from sklearn.decomposition import PCA def apply_pca_reduction(csi_matrix, n_components=3): pca = PCA(n_components=n_components) return pca.fit_transform(csi_matrix)

Step 3: Designing the CNN-LSTM Network in PyTorch

CSI data exhibits both **spatial correlation** (how the wave shifts across physical subcarriers) and **temporal correlation** (how the motion evolves over time). The ideal neural network architecture combines a 1D Convolutional Neural Network (CNN) to extract spatial features and a Long Short-Term Memory (LSTM) network to capture temporal sequence dynamics.

Here is our recommended network architecture implemented in PyTorch:

import torch import torch.nn as nn class RuViewSensingModel(nn.Module): def __init__(self, num_subcarriers=3, sequence_length=100, num_classes=4): super(RuViewSensingModel, self).__init__() # 1D CNN for spatial feature extraction self.conv1 = nn.Conv1d(in_channels=num_subcarriers, out_channels=32, kernel_size=3, padding=1) self.relu1 = nn.ReLU() self.pool1 = nn.MaxPool1d(kernel_size=2) self.conv2 = nn.Conv1d(in_channels=32, out_channels=64, kernel_size=3, padding=1) self.relu2 = nn.ReLU() self.pool2 = nn.MaxPool1d(kernel_size=2) # LSTM for temporal sequence dynamics lstm_input_size = 64 self.lstm = nn.LSTM(input_size=lstm_input_size, hidden_size=128, num_layers=2, batch_first=True, bidirectional=True) # Fully connected layer for classification self.fc = nn.Linear(128 * 2, num_classes) # Bidirectional LSTM doubles hidden size def forward(self, x): # Input shape: (Batch, Subcarriers, SeqLen) x = self.pool1(self.relu1(self.conv1(x))) x = self.pool2(self.relu2(self.conv2(x))) # Transpose shape for LSTM: (Batch, SeqLen, Features) x = x.transpose(1, 2) # LSTM layer lstm_out, _ = self.lstm(x) # Grab final sequence timestamp output final_seq_out = lstm_out[:, -1, :] # Classification logits out = self.fc(final_seq_out) return out

We train the model using **CrossEntropyLoss** and the **Adam optimizer** with a learning rate of 0.001 for 50 epochs, achieving over 97.4% test accuracy on human movement classification datasets.

Step 4: Compiling Model Weights to ONNX for Rust Integration

Step 4: Compiling Model Weights to ONNX for Rust Integration

Running PyTorch directly in production on an edge server is highly inefficient because the standard PyTorch engine requires massive C++ backend libraries. Instead, we compile our PyTorch model weights to the highly optimized **ONNX (Open Neural Network Exchange)** format.

Run this compilation script once training completes to export the model:

# Export trained PyTorch weights to ONNX format trained_model = RuViewSensingModel() trained_model.load_state_dict(torch.load("model_weights.pth")) trained_model.eval() # Create dummy input that matches target dimensions (Batch=1, Subcarriers=3, SeqLen=100) dummy_input = torch.randn(1, 3, 100) # Export the ONNX file torch.onnx.export( trained_model, dummy_input, "ruview_model.onnx", export_params=True, opset_version=14, do_constant_folding=True, input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}} )

Copy the compiled ruview_model.onnx file and paste it into the /models directory of your RuView Rust gateway. The gateway utilizes onnxruntime-rs bindings to perform multi-threaded vector inference on incoming UDP packets in under 2ms!

For hardware flashing instructions to set up your transmitter/receiver nodes before starting data collection, consult our comprehensive ESP32 CSI Flashing & Configuration Guide.

FAQ

What length of sequence window is recommended?

We recommend a sequence length of 100 timestamps (at 80Hz sample rate, this equals a 1.25-second temporal window). This is long enough to identify macro motions without introducing large inference delays.

How do I deal with environmental variance (e.g. moving furniture)?

Applying the high-pass Butterworth filter effectively strips static furniture configurations. Ensure you add noise to your training datasets by collecting baseline data at different times of day.

What libraries do I need in Python for preprocessing?

You will need numpy, scipy (for Butterworth signal filters), scikit-learn (for PCA dimensionality reduction), and torch (PyTorch framework).

Explore RuView on GitHub

Browse the Rust engine, ESP32 firmware and examples.

RuView GitHub