5G Signal Modulation Classifier

Train a PyTorch CNN to classify radio signals and deploy it on Hugging Face.

Introduction

30 Second Summary

Every time you stream a video or send a text, your phone decodes a radio signal. The type of modulation on that signal determines how data is encoded in the wave. Identifying modulation types automatically is a core challenge in modern 5G systems.

In this project, you will train a deep learning model in PyTorch that classifies 11 radio modulation types from raw signal data, then deploy it to Hugging Face so anyone can try it in their browser.

What You'll Build

You'll paste raw signal data into a live web interface and watch your model identify whether it's QPSK, QAM16, QAM64, or one of eight other modulation schemes, complete with confidence scores.

By the end of this project, you'll have:

  • A trained 1D CNN that takes raw IQ signal samples and predicts the modulation type with high accuracy at practical signal strengths used in real 5G systems.
  • A published Hugging Face model that anyone can load with two lines of Python using from_pretrained, plus a live Gradio demo where you paste IQ samples and get back predictions instantly.
  • A GitHub repository with a professional README showcasing your results in 3 clear quantitative points, linking to your live demo and model.
  • Secret Mission: Improve your model's performance on noisy signals by adding noise augmentation during training.

Are there any prerequisites?

You should be comfortable with basic Python programming. You should also understand what a neural network does at a high level. You do not need to have built one in PyTorch before.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up Your Environment

Training a deep learning model requires serious compute power. A GPU can process thousands of signal samples in parallel, turning hours of training into minutes.

In this step, you will set up Google Colab with a free T4 GPU and connect it to Hugging Face Hub so you can later publish your trained model for anyone to use.

In this step, get ready to:
  • Set up a Google Colab notebook with T4 GPU acceleration.
  • Create a Hugging Face account and authenticate from Colab.
  • Verify PyTorch GPU access and confirm library versions.
Open Google Colab and enable the GPU runtime

Google Colab gives you free access to cloud GPUs directly in your browser. You need to switch the default CPU runtime to a T4 GPU before running any training code.

  • Open your browser and navigate to colab.research.google.com.
  • Sign in with your Google account if prompted.
  • Click New notebook to create a fresh notebook.
  • Click Runtime in the top menu bar.
  • Click Change runtime type.
  • Under Hardware accelerator, select T4 GPU.
  • Click Save.

What is a T4 GPU?

The NVIDIA T4 is a GPU designed for machine learning inference and training. Colab provides it for free, giving you 16 GB of GPU memory to train neural networks much faster than on a CPU.

Don't see T4 GPU as an option?

If the T4 option is greyed out or unavailable, Colab may be at capacity. Wait a few minutes and try again. You can also try disconnecting your current runtime first by clicking Runtime then Disconnect and delete runtime, and re-selecting the GPU type.

help me set up a T4 GPU runtime in Google Colab

Create a Hugging Face account and authenticate

Hugging Face Hub is where you will publish your trained model so others can load it with two lines of code. You need a free account and a write-access token to push models from Colab.

  • Open a new browser tab and go to huggingface.co/join.
  • Fill in your details and create your free account.
  • Once signed in, navigate to huggingface.co/settings/tokens.
  • Click Create new token.
  • Give it a name (e.g. colab-project) and select Write access.
  • Click Create token and copy the token to your clipboard.

Keep your token safe

This token gives write access to your Hugging Face account. Do not share it or paste it anywhere public. You will paste it into a secure prompt inside Colab in the next step.

Now install the required libraries in your Colab notebook. PyTorch is already pre-installed on the GPU runtime, but you need huggingface_hub and gradio.

  • Install the Hugging Face Hub library and Gradio by running this in a new Colab cell:
!pip install huggingface_hub gradio

You will see installation output scrolling past. Once it finishes, you are ready to authenticate.

  • Authenticate with Hugging Face by running this in a new cell:
from huggingface_hub import notebook_login

# Authenticate with your Hugging Face write-access token
notebook_login()

What happens when you run this?

A text input widget appears in the cell output asking for your token. Paste the write-access token you copied from Hugging Face and press Enter. After a moment, you will see a confirmation that login was successful.

Login not working?

Make sure you copied the full token string (it starts with hf_). If you accidentally created a read-only token, go back to your Hugging Face tokens page and create a new one with Write access.

help me authenticate to Hugging Face from Google Colab

Verify PyTorch GPU access

Before you start training, you need to confirm that PyTorch can see the GPU and that all libraries installed correctly.

  • Verify your environment by running this in a new cell:
import torch
import huggingface_hub

# Set up the compute device (GPU if available, otherwise CPU)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

# Print versions to confirm everything is working
print(torch.__version__, torch.cuda.is_available())
print(huggingface_hub.__version__)

Before you run the cell, what do you expect to see for torch.cuda.is_available()? Think about whether you successfully switched to a GPU runtime earlier.

  • Run the cell now.

You should see three lines of output. The first prints Using device: cuda. The second prints a PyTorch version number followed by True. The third prints the huggingface_hub version number.

What do these outputs mean?

The cuda device means PyTorch found your T4 GPU and will use it for tensor operations. True confirms CUDA (the GPU compute framework) is available. The version numbers confirm your libraries are installed and ready.

Seeing False instead of True?

This means Colab is not running on a GPU. Go back to Runtime then Change runtime type and confirm T4 GPU is selected. Click Save, then re-run your cells from the top.

help me get GPU access working in Google Colab

✔️ Awesome, I've got everything!

Great. Double check you see cuda and True in your output, then move on to the next step.

ⓧ I'd like to double check the full code

Here is all the Python code you should have across your Colab cells (excluding the !pip install cell which is a shell command):

import torch
import huggingface_hub
from huggingface_hub import notebook_login

notebook_login()
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")
print(torch.__version__, torch.cuda.is_available())
print(huggingface_hub.__version__)

Your environment is locked in. You have a GPU-powered notebook connected to Hugging Face Hub. Next up, you will load a real radio signal dataset and see what modulation patterns look like.

Load the RadioML Dataset and Visualize Signals

In the last step, you set up your Colab notebook with GPU access and authenticated to Hugging Face. Now it's time to meet your data.

Before building any model, you need to see what the data looks like. Raw IQ samples are just numbers in an array. Plotting them reveals how different modulation types produce visually distinct signal patterns, which is exactly what the CNN will learn to distinguish.

In this step, get ready to:
  • Download and load the RadioML 2016.10a dataset into a Python dictionary.
  • Convert the dictionary into numpy arrays ready for PyTorch.
  • Plot IQ waveforms for different modulation types and see how they differ visually.
Update your imports and download the dataset

The RadioML 2016.10a dataset is a benchmark collection of 220,000 synthetic radio signals covering 11 modulation types at 20 different noise levels. You'll download it from Zenodo (about 213 MB) and load it into a Python dictionary.

  • In your Colab notebook, replace the code in your first cell (the imports and setup cell) with the following expanded version:
import pickle
import tarfile
import numpy as np
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
from huggingface_hub import PyTorchModelHubMixin, notebook_login

# === Setup ===
notebook_login()
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

What does this code do?

This cell imports all the libraries you'll use throughout the entire project. You won't use all of them in this step, but having them ready avoids import errors later.

  • pickle and tarfile handle the dataset file (compressed archive containing a pickled dictionary).
  • numpy and matplotlib handle array math and plotting.
  • torch modules provide the neural network building blocks for later steps.
  • sklearn provides data splitting and evaluation metrics.
  • PyTorchModelHubMixin will let you push your trained model to Hugging Face Hub later.
  • Run the cell. You'll be prompted to authenticate to Hugging Face again (since this replaces the previous login call). Paste your token and press Enter.
  • You should see Using device: cuda printed below the cell.

Now add a new cell below and download the dataset by running this code:

# === Download and Load Dataset ===
!wget -q "https://zenodo.org/records/18397070/files/RML2016.10a.tar.bz2" -O RML2016.10a.tar.bz2

with tarfile.open("RML2016.10a.tar.bz2", "r:bz2") as tar:
    tar.extractall()

with open("RML2016.10a_dict.pkl", "rb") as f:
    data_dict = pickle.load(f, encoding="latin1")

What does this code do?

  • wget -q quietly downloads the 213 MB compressed archive from Zenodo.
  • tarfile.open extracts the .tar.bz2 archive, producing RML2016.10a_dict.pkl.
  • pickle.load reads the pickle file into data_dict. The keys are tuples like ("QPSK", 18) representing (modulation type, SNR in dB). Each value is a numpy array of shape (1000, 2, 128).
  • Run this cell. The download takes about 10-20 seconds depending on your connection. The cell completes silently when finished (no text output is expected from this cell).

Cell taking too long or failing?

  • If you see a network error, check that your Colab runtime is still connected (green checkmark in the top right). Reconnect and re-run if needed.
  • If you see a file-not-found error on the pickle load, the extracted filename may differ slightly. Run !ls *.pkl to see the actual filename and adjust accordingly.

Still stuck? Help me troubleshoot my RadioML dataset download in Google Colab.

Convert the dictionary into numpy arrays

The raw dictionary groups samples by modulation type and SNR level. To train a neural network, you need flat arrays: one for the signal data (X), one for the labels (Y), and one tracking the SNR of each sample.

  • Create a new cell below and run this code to reshape the data:
# === Prepare Data ===
modulation_types = sorted(list(set(k[0] for k in data_dict.keys())))
snr_levels = sorted(list(set(k[1] for k in data_dict.keys())))

print(f"Modulation types ({len(modulation_types)}): {modulation_types}")
print(f"SNR levels ({len(snr_levels)}): {snr_levels}")

X = []
Y = []
SNR = []

for (mod, snr), samples in data_dict.items():
    X.append(samples)
    Y.extend([modulation_types.index(mod)] * samples.shape[0])
    SNR.extend([snr] * samples.shape[0])

X = np.vstack(X)
Y = np.array(Y)
SNR = np.array(SNR)

print(f"Dataset shape: {X.shape} | Labels: {Y.shape} | SNR: {SNR.shape}")

What does this code do?

  • modulation_types extracts and sorts the 11 unique modulation names from the dictionary keys.
  • snr_levels extracts and sorts the 20 unique SNR values (-20dB to +18dB in 2dB steps).
  • The loop stacks all 220 sample arrays into one large X array, assigns numeric labels to Y, and tracks each sample's noise level in SNR.
  • np.vstack combines the list of arrays into one contiguous array of shape (220000, 2, 128). The 2 channels are I (in-phase) and Q (quadrature) components of each signal.
  • Run this cell. You should see output listing all 11 modulation types, all 20 SNR levels, and the final dataset shape:
Modulation types (11): ['8PSK', 'AM-DSB', 'AM-SSB', 'BPSK', 'CPFSK', 'GFSK', 'PAM4', 'QAM16', 'QAM64', 'QPSK', 'WBFM']
SNR levels (20): [-20, -18, -16, -14, -12, -10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
Dataset shape: (220000, 2, 128) | Labels: (220000,) | SNR: (220000,)

What is IQ data?

Every radio signal can be represented as two waveforms: the I (in-phase) component and the Q (quadrature) component. Together, these two channels fully describe the signal's amplitude and phase at each time step.

Each sample in this dataset is 2 channels by 128 time steps. Think of it like a tiny stereo audio clip, except it encodes radio information instead of sound.

Seeing a different shape or missing modulation types?

  • If the dataset shape is not (220000, 2, 128), make sure you loaded the correct pickle file. Run len(data_dict) in a new cell. It should return 220 (11 modulations x 20 SNR levels).
  • If you see a NameError on data_dict, make sure you ran the download cell above first.

Need more help? Help me fix my RadioML data preparation code.

Plot IQ waveforms for different modulation types

Numbers in an array don't tell you much. Plotting the I and Q channels reveals that each modulation type has a distinct visual signature. This is what the CNN will learn to recognize.

  • Before you run the next cell, predict: do you think BPSK (2 symbols) and QAM64 (64 symbols) will look similar or different when plotted as waveforms?
  • Create a new cell and run this code to plot six modulation types at high SNR:
# === Visualize Signals ===
fig, axes = plt.subplots(2, 3, figsize=(12, 6))
sample_mods = ["BPSK", "QPSK", "QAM16", "QAM64", "8PSK", "CPFSK"]

for ax, mod_name in zip(axes.flat, sample_mods):
    mod_idx = modulation_types.index(mod_name)
    high_snr_mask = (Y == mod_idx) & (SNR == 18)
    sample = X[high_snr_mask][0]
    ax.plot(sample[0], label="I", alpha=0.8)
    ax.plot(sample[1], label="Q", alpha=0.8)
    ax.set_title(mod_name)
    ax.legend(fontsize=8)
    ax.set_xlabel("Sample")

plt.tight_layout()
plt.savefig("signal_examples.png", dpi=100)
plt.show()

What does this code do?

  • For each of the 6 modulation types, it filters for samples at SNR = +18dB (the cleanest signals with least noise).
  • It plots the I channel (blue) and Q channel (orange) as line graphs across 128 time samples.
  • plt.savefig saves the plot as signal_examples.png for later use in your GitHub README.
  • Run the cell. You should see a 2x3 grid of plots, each labeled with its modulation type.

Notice how BPSK shows clear jumps between two amplitude levels. QPSK has more complex phase transitions. QAM64 looks almost noisy because it packs 64 different symbol states into the waveform.

Plots not showing?

  • If you see a blank output, make sure plt.show() is at the end of your cell. Colab requires this to render inline plots.
  • If you get an IndexError on X[high_snr_mask][0], check that SNR == 18 matches a value in your snr_levels list (the highest SNR in the dataset is 18dB).

Still stuck? Help me debug my matplotlib visualization in Colab.

✔️ Awesome, I've got everything!

Great. Make sure you've saved your notebook (Ctrl+S) before moving on.

ⓧ I'd like to double check the full code

Your notebook should now have 4 cells, in this order:

  • Cell 1: All imports plus the setup section (notebook_login() and device assignment).
  • Cell 2: Download and Load Dataset (wget, tarfile.extractall(), pickle.load).
  • Cell 3: Prepare Data (modulation_types, snr_levels, the loop building X/Y/SNR, and the shape print).
  • Cell 4: Visualize Signals (the subplot loop and plt.show()).

Scroll through each cell and confirm the code matches what you pasted above. If any cell is missing, add it and re-run from top to bottom.

You've loaded 220,000 radio signal samples and visualized how different modulation types look as raw waveforms. Next up, you'll build a 1D CNN that learns to classify these patterns automatically.

Build and Train the 1D CNN

You have 220,000 labeled IQ signal samples loaded and visualized. Now you need a model that can learn the patterns hidden in those waveforms and distinguish between 11 modulation types automatically.

A 1D Convolutional Neural Network is the standard architecture for sequential signal data. It slides learned filters across the 128-sample IQ waveform to detect modulation-specific patterns. In this step, you'll split your data, define the CNN, train it, and watch the loss decrease over 30 epochs.

In this step, get ready to:
  • Split the dataset into training and test sets with PyTorch DataLoaders.
  • Define a 1D CNN model class that integrates with Hugging Face Hub.
  • Train the model for 30 epochs and plot the loss curve.
Split the data into training and test sets

Before training, you need to hold back 20% of the data so you can measure how well the model performs on signals it has never seen.

  • Create a new code cell in your Colab notebook and paste the following code to split the data and create DataLoaders:
# === Train/Test Split ===
X_train, X_test, Y_train, Y_test, SNR_train, SNR_test = train_test_split(
    X, Y, SNR, test_size=0.2, stratify=Y, random_state=42
)

# Convert numpy arrays to PyTorch tensors
X_train_t = torch.tensor(X_train, dtype=torch.float32)
Y_train_t = torch.tensor(Y_train, dtype=torch.long)
X_test_t = torch.tensor(X_test, dtype=torch.float32)
Y_test_t = torch.tensor(Y_test, dtype=torch.long)

# Wrap tensors in Dataset and DataLoader for batched training
train_dataset = TensorDataset(X_train_t, Y_train_t)
test_dataset = TensorDataset(X_test_t, Y_test_t)

train_loader = DataLoader(train_dataset, batch_size=256, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=256, shuffle=False)

What does this code do?

  • train_test_split with stratify=Y ensures each modulation type is represented equally in both the training and test sets.
  • random_state=42 makes the split reproducible so your results are consistent across runs.
  • DataLoaders feed data to the model in batches of 256 samples, which is much more efficient than processing one sample at a time on the GPU.
  • Run the cell.
  • You should see no errors. The cell completes silently, which confirms the split and DataLoaders were created successfully.
  • Verify the split by adding a quick print statement in a new cell:
print(f"Train: {X_train.shape} | Test: {X_test.shape}")
  • Run the cell. You should see Train: (176000, 2, 128) | Test: (44000, 2, 128) confirming the 80/20 split.

Seeing a NameError?

If you see NameError: name 'X' is not defined, it means the cells from the previous step haven't been run yet. Go back and run all prior cells in order.

Help me fix my train/test split error.

Define the 1D CNN model

The CNN needs two convolutional layers to detect patterns at different scales in the IQ waveform, followed by fully connected layers that combine those patterns into a modulation prediction.

  • Create a new cell and define the model class:
# === Define Model ===
class ModulationClassifier(
    nn.Module,
    PyTorchModelHubMixin,
    library_name="modulation-classifier",
    pipeline_tag="tabular-classification",
    license="cc-by-nc-sa-4.0",
):
    def __init__(self, num_classes: int = 11, input_channels: int = 2, input_length: int = 128):
        super().__init__()
        self.conv1 = nn.Conv1d(input_channels, 64, kernel_size=8, padding="same")
        self.relu1 = nn.ReLU()
        self.pool1 = nn.MaxPool1d(2)
        self.conv2 = nn.Conv1d(64, 128, kernel_size=5, padding="same")
        self.relu2 = nn.ReLU()
        self.pool2 = nn.MaxPool1d(2)
        self.flatten = nn.Flatten()
        self.fc1 = nn.Linear(128 * (input_length // 4), 256)
        self.relu3 = nn.ReLU()
        self.dropout = nn.Dropout(0.5)
        self.fc2 = nn.Linear(256, num_classes)

    def forward(self, x):
        x = self.pool1(self.relu1(self.conv1(x)))
        x = self.pool2(self.relu2(self.conv2(x)))
        x = self.flatten(x)
        x = self.dropout(self.relu3(self.fc1(x)))
        x = self.fc2(x)
        return x

What does this architecture do?

  • The class inherits from both nn.Module (standard PyTorch) and PyTorchModelHubMixin (adds push_to_hub and from_pretrained methods for Hugging Face).
  • conv1 (kernel size 8) detects broad patterns. conv2 (kernel size 5) detects finer details within those patterns.
  • Each MaxPool1d(2) halves the sequence length: 128 becomes 64, then 32. This gives the fc1 layer an input size of 128 channels times 32 positions = 4096.
  • Dropout(0.5) randomly zeros half the neurons during training, which prevents overfitting.
  • Now instantiate the model and check the parameter count by pasting this in the same cell or a new one:
# Create model instance and move to GPU
model = ModulationClassifier(num_classes=11, input_channels=2, input_length=128).to(device)
total_params = sum(p.numel() for p in model.parameters())
print(f"Model parameters: {total_params:,}")
  • Run the cell. You should see a parameter count printed (around 1 million parameters). This confirms the model is defined and loaded onto the GPU.

Seeing a RuntimeError about CUDA?

If you see a CUDA-related error, your runtime may not have a GPU attached. Go to Runtime > Change runtime type and confirm T4 GPU is selected. Then restart the runtime and re-run all cells from the top.

Help me fix my CUDA error in Colab.

Train for 30 epochs

Training means repeatedly showing the model batches of IQ samples, comparing its predictions to the correct labels, and adjusting its weights to reduce the error. Each full pass through the dataset is one epoch.

  • Create a new cell and paste the training loop:
# === Train ===
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
criterion = nn.CrossEntropyLoss()
num_epochs = 30
losses = []

for epoch in range(num_epochs):
    model.train()
    epoch_loss = 0
    for batch_X, batch_Y in train_loader:
        batch_X, batch_Y = batch_X.to(device), batch_Y.to(device)
        optimizer.zero_grad()
        outputs = model(batch_X)
        loss = criterion(outputs, batch_Y)
        loss.backward()
        optimizer.step()
        epoch_loss += loss.item()
    avg_loss = epoch_loss / len(train_loader)
    losses.append(avg_loss)
    if (epoch + 1) % 5 == 0:
        model.eval()
        correct = 0
        total = 0
        with torch.no_grad():
            for batch_X, batch_Y in test_loader:
                batch_X, batch_Y = batch_X.to(device), batch_Y.to(device)
                outputs = model(batch_X)
                _, predicted = torch.max(outputs, 1)
                total += batch_Y.size(0)
                correct += (predicted == batch_Y).sum().item()
        val_acc = correct / total
        print(f"Epoch {epoch+1}/{num_epochs} | Loss: {avg_loss:.4f} | Val Acc: {val_acc:.4f}")

What does this training loop do?

  • Adam optimizer adjusts the model weights using a learning rate of 0.001. Adam adapts its step size per parameter, which works well for signal data.
  • CrossEntropyLoss measures how far the model's predictions are from the correct labels. Lower loss means better predictions.
  • Every 5 epochs, the loop pauses training to check validation accuracy on the held-out test set. This shows whether the model is actually learning to generalize.

Before you run this cell, what do you think will happen to the loss value over 30 epochs? Will it go up, down, or stay flat?

  • Run the cell. Training takes approximately 3-5 minutes on a T4 GPU.
  • You should see output printed every 5 epochs showing the loss decreasing and validation accuracy rising. The final validation accuracy should land around 55-65%.

Why is 60% accuracy not bad here?

The dataset includes signals at extremely low SNR levels (-20dB) where even experts cannot distinguish modulation types. The overall accuracy averages across all noise levels. In the next step, you'll see that accuracy at practical SNR levels (used in real 5G systems) is much higher.

  • Now plot the training loss curve by pasting this in a new cell:
# Plot how the loss decreased over training
plt.figure(figsize=(8, 4))
plt.plot(losses)
plt.xlabel("Epoch")
plt.ylabel("Training Loss")
plt.title("Training Loss Curve")
plt.savefig("training_loss.png", dpi=100)
plt.show()
  • Run the cell. You should see a curve that drops steeply in the first 5-10 epochs and then flattens out. This confirms the model learned most of its useful patterns early on.

Loss not decreasing?

If your loss stays flat or increases, check that you ran the model definition cell and that the model is on the GPU (model.to(device)). Also verify your DataLoaders were created correctly by checking that len(train_loader) returns a number greater than 0.

Help me debug my training loop.

✔️ Awesome, I've got everything!

Great. Double check you've run all cells in order and that your training loss plot is visible.

ⓧ I'd like to double check the full code

Scroll up through your notebook and confirm these code chunks are present in order:

  • Train/Test Split cell: train_test_split call, tensor conversions, TensorDataset and DataLoader creation.
  • Model Definition cell: ModulationClassifier class with two Conv1d layers, two MaxPool1d layers, and two fully connected layers.
  • Model Instantiation cell: model = ModulationClassifier(...).to(device) and the parameter count print.
  • Training Loop cell: 30-epoch loop with Adam optimizer, CrossEntropyLoss, and validation accuracy printed every 5 epochs.
  • Loss Plot cell: plt.plot(losses) saving to training_loss.png.

Your CNN is trained and achieving around 60% overall accuracy. That number might feel underwhelming, but it hides a powerful insight. Next up, you'll break down accuracy by signal quality and discover the model is actually excellent at the SNR levels that matter in real 5G systems.

Evaluate and Diagnose Model Performance

Your model finished training with an overall validation accuracy around 55-65%. That number looks underwhelming for a classifier with 11 classes.

The headline accuracy is misleading. It averages performance across every SNR level, including signals so buried in noise that even a human expert could not classify them. In this step, you will break down performance by SNR and discover the model is actually excellent at signal strengths used in real 5G systems.

In this step, get ready to:
  • Evaluate per-SNR accuracy and plot the characteristic S-curve.
  • Generate a confusion matrix at practical SNR levels to identify which modulation pairs confuse the model.
  • Record 3 quantitative results points for your project README.
Evaluate accuracy per SNR level

Your test set contains samples from 20 different SNR levels, ranging from -20dB (extremely noisy) to +18dB (clean signal). Evaluating accuracy at each level separately reveals where the model actually works well.

Before you run this, think: do you expect accuracy to be uniform across all SNR levels, or do you expect a pattern?

  • Create a new code cell in your Colab notebook and compute per-SNR accuracy by running this code:
# === Evaluate Per-SNR ===
model.eval()
snr_accuracy = {}

for snr_val in snr_levels:
    mask = SNR_test == snr_val
    if mask.sum() == 0:
        continue
    X_snr = torch.tensor(X_test[mask], dtype=torch.float32).to(device)
    Y_snr = torch.tensor(Y_test[mask], dtype=torch.long).to(device)

    with torch.no_grad():
        outputs = model(X_snr)
        _, predicted = torch.max(outputs, 1)
        acc = (predicted == Y_snr).float().mean().item()
    snr_accuracy[snr_val] = acc

plt.figure(figsize=(10, 5))
plt.plot(list(snr_accuracy.keys()), list(snr_accuracy.values()), marker="o")
plt.xlabel("SNR (dB)")
plt.ylabel("Accuracy")
plt.title("Classification Accuracy vs SNR")
plt.grid(True)
plt.savefig("accuracy_vs_snr.png", dpi=100)
plt.show()

What does this code do?

For each of the 20 SNR levels, the loop filters the test set to only samples at that SNR, runs inference, and records the accuracy.

The result is a line chart showing how accuracy varies with signal quality. You should see a classic S-curve: near-random performance (~20%) at very low SNR, a steep rise through the middle range, and a plateau at high accuracy for SNR >= 0dB.

  • Run the cell.
  • You should see a line chart with accuracy jumping from roughly 20% at -20dB to 88%+ at SNR levels of 0dB and above.

Why does accuracy look so different across SNR?

At -20dB, the noise power is 100x stronger than the signal. The modulation pattern is completely buried. No classifier (human or machine) can reliably distinguish modulation types at those levels.

At 0dB and above, the signal is at least as strong as the noise. These are the practical operating ranges for real 5G systems. Your model performs well exactly where it matters.

Chart looks flat or all zeros?

Make sure you used SNR_test (not SNR) to filter the test set. The full SNR array includes training data and would produce a shape mismatch.

Also confirm the model is in eval mode (model.eval()) before the loop. Without it, dropout is still active and accuracy drops.

Help me debug my per-SNR evaluation loop.

Compute high-SNR accuracy and generate a confusion matrix

The S-curve showed the big picture. Now you will compute one clean number (accuracy at SNR >= 0dB) and build a confusion matrix to see exactly which modulation types trip up the model.

  • Create a new code cell and compute the high-SNR accuracy and overall accuracy by running this code:
# High-SNR accuracy
high_snr_mask = SNR_test >= 0
X_high = torch.tensor(X_test[high_snr_mask], dtype=torch.float32).to(device)
Y_high = torch.tensor(Y_test[high_snr_mask], dtype=torch.long).to(device)

with torch.no_grad():
    outputs = model(X_high)
    _, predicted = torch.max(outputs, 1)
    high_snr_acc = (predicted == Y_high).float().mean().item()

overall_mask = np.ones(len(Y_test), dtype=bool)
X_all = torch.tensor(X_test, dtype=torch.float32).to(device)
Y_all = torch.tensor(Y_test, dtype=torch.long).to(device)

with torch.no_grad():
    outputs = model(X_all)
    _, predicted_all = torch.max(outputs, 1)
    overall_acc = (predicted_all == Y_all).float().mean().item()

print(f"\nOverall test accuracy: {overall_acc:.4f}")
print(f"High-SNR (>= 0dB) accuracy: {high_snr_acc:.4f}")
print(f"Model parameters: {total_params:,}")

What does this code do?

  • high_snr_mask filters for test samples where SNR is 0dB or above, the practical operating range.
  • high_snr_acc is your key performance metric. This is the number that proves your model works at real-world signal strengths.
  • overall_acc includes every SNR level (even the impossible ones) for comparison.
  • Run the cell. You should see overall accuracy around 0.60 and high-SNR accuracy around 0.88 or higher.

Now generate the confusion matrix to see which specific modulation types the model confuses.

  • Create a new code cell and generate the confusion matrix by running this code:
# Confusion matrix at high SNR
Y_high_np = Y_high.cpu().numpy()
predicted_np = predicted.cpu().numpy()

cm = confusion_matrix(Y_high_np, predicted_np)
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=modulation_types)
fig, ax = plt.subplots(figsize=(10, 8))
disp.plot(ax=ax, cmap="Blues", values_format="d")
plt.title("Confusion Matrix (SNR >= 0dB)")
plt.xticks(rotation=45, ha="right")
plt.tight_layout()
plt.savefig("confusion_matrix.png", dpi=100)
plt.show()

What does this code do?

A confusion matrix shows, for each true modulation type, how many samples the model assigned to each predicted type. A perfect model would have all counts on the diagonal (correct predictions) and zeros everywhere else.

You should see strong diagonal dominance. The most common off-diagonal confusions are typically between QAM16 and QAM64, which share similar constellation structures and differ mainly in density.

  • Run the cell. You should see a heatmap with most counts concentrated along the diagonal.
  • Look for the brightest off-diagonal cells. These show which modulation pairs the model most often confuses.

Seeing a NameError for Y_high or predicted?

The confusion matrix cell uses variables created in the previous cell (Y_high and predicted). Make sure you ran the high-SNR accuracy cell first without errors.

Help me fix my confusion matrix code.

Record your 3 quantitative results points

You now have all the numbers you need for your project README. These three points tell the complete story of your model's performance.

  • Note down these three results from your output:
  • Dataset size and composition: 220,000 IQ signal samples across 11 modulation types and 20 SNR levels (-20dB to +18dB).
  • Accuracy metrics: your high-SNR accuracy (the high_snr_acc value, approximately 88%) and overall accuracy (the overall_acc value, approximately 60%).
  • Model size: the total_params value printed in your output.

Why these three points?

A strong technical README answers three questions: what data did you use, how well does it work, and how complex is the model. These three numbers let a reader (or a recruiter) quickly assess the scope and quality of your work.

Use your exact numbers

The approximate values (~88%, ~60%) are guidelines. Your exact numbers will differ slightly due to random initialization and GPU non-determinism. Use whatever your notebook actually printed.

✔️ Awesome, I've got everything!

Great. Make sure all three cells ran without errors and you have noted your three quantitative results.

ⓧ I'd like to double check the full code

Your notebook should now have three new cells added in this step, in this order:

  • Cell 1: The per-SNR evaluation loop that creates snr_accuracy and plots the S-curve chart (saved as accuracy_vs_snr.png).
  • Cell 2: The high-SNR and overall accuracy computation that prints three metrics and creates high_snr_acc and overall_acc.
  • Cell 3: The confusion matrix generation that saves confusion_matrix.png.

Scroll up through your notebook and confirm each cell is present and ran successfully (no red error output).

You have diagnosed your model's real-world performance and have the numbers to prove it works. Next up, you will deploy this model to Hugging Face Hub and build a live web demo so anyone can try it in their browser.

Deploy to Hugging Face and GitHub

Your model works, but it lives inside a Colab notebook that nobody else can use. A trained model only becomes valuable when other people can load it, try it, and build on it.

In this step, you will publish your model to Hugging Face Hub so anyone can load it with two lines of Python. You will also deploy a live Gradio demo on Hugging Face Spaces and create a GitHub repository that packages the whole project with your results.

In this step, get ready to:
  • Push your trained model to Hugging Face Hub and verify it reloads correctly.
  • Create a live Gradio demo on Hugging Face Spaces that classifies modulation from IQ input.
  • Create a GitHub repository with a professional README showcasing your 3 quantitative results.
Push the model to Hugging Face Hub

The PyTorchModelHubMixin you inherited earlier gives your model a push_to_hub method. This uploads the model weights and a config.json so anyone can reload it later.

  • In your Colab notebook, create a new code cell and push the model to the Hub by running this code:
# === Push to Hugging Face Hub ===
model.push_to_hub("your-hf-username/5g-modulation-classifier")
print("Model pushed to Hugging Face Hub!")

What does this code do?

The push_to_hub call uploads your model's model.safetensors (the trained weights) and a config.json (recording the constructor arguments) to a new repository on Hugging Face Hub.

Replace your-hf-username with the Hugging Face username you created in Step 1.

  • Run the cell. You should see a progress bar as the weights upload, followed by the confirmation message.

Now confirm the model can be reloaded from the Hub. Before you run the next cell, predict: will it load successfully, or will there be a missing file error?

  • Verify the upload works by reloading the model in a new cell:
# Verify reload
loaded_model = ModulationClassifier.from_pretrained("your-hf-username/5g-modulation-classifier")
print("Model loaded successfully from Hub!")
  • Run the cell. You should see Model loaded successfully from Hub! printed with no errors.

Seeing a repository not found error?

  • Make sure you replaced your-hf-username with your actual Hugging Face username in both the push and load cells.
  • Confirm your Hugging Face token has write access. Return to huggingface.co/settings/tokens and check the token's permissions.
  • Help me fix my push_to_hub or from_pretrained error
Create a Hugging Face Space with Gradio

Your model is now loadable by anyone with two lines of Python. Next, you will wrap it in a web interface so people can try it without writing any code.

  • Open huggingface.co/new-space in a new browser tab.
  • Enter 5g-modulation-classifier as the Space name.
  • Select Gradio as the SDK.
  • Set the visibility to Public.
  • Click Create Space.

You should see an empty Space repository. Now you will add two files: app.py and requirements.txt.

  • Click Files in the top navigation of your Space.
  • Click + Add file then Create a new file.
  • Name the file app.py.
  • Paste the following imports and configuration at the top of app.py:
import gradio as gr
import torch
import torch.nn as nn
import numpy as np
from huggingface_hub import PyTorchModelHubMixin


MODULATION_TYPES = ["8PSK", "AM-DSB", "AM-SSB", "BPSK", "CPFSK", "GFSK", "PAM4", "QAM16", "QAM64", "QPSK", "WBFM"]

What does this code do?

The imports bring in Gradio for the web interface, PyTorch for running the model, and PyTorchModelHubMixin to load from the Hub.

The MODULATION_TYPES list maps the model's numeric outputs back to human-readable modulation names. The order must match the sorted order from training.

  • Below the imports, paste the model class definition (identical to the one in your Colab notebook):
class ModulationClassifier(
    nn.Module,
    PyTorchModelHubMixin,
    library_name="modulation-classifier",
    pipeline_tag="tabular-classification",
    license="cc-by-nc-sa-4.0",
):
    def __init__(self, num_classes: int = 11, input_channels: int = 2, input_length: int = 128):
        super().__init__()
        self.conv1 = nn.Conv1d(input_channels, 64, kernel_size=8, padding="same")
        self.relu1 = nn.ReLU()
        self.pool1 = nn.MaxPool1d(2)
        self.conv2 = nn.Conv1d(64, 128, kernel_size=5, padding="same")
        self.relu2 = nn.ReLU()
        self.pool2 = nn.MaxPool1d(2)
        self.flatten = nn.Flatten()
        self.fc1 = nn.Linear(128 * (input_length // 4), 256)
        self.relu3 = nn.ReLU()
        self.dropout = nn.Dropout(0.5)
        self.fc2 = nn.Linear(256, num_classes)

    def forward(self, x):
        x = self.pool1(self.relu1(self.conv1(x)))
        x = self.pool2(self.relu2(self.conv2(x)))
        x = self.flatten(x)
        x = self.dropout(self.relu3(self.fc1(x)))
        x = self.fc2(x)
        return x

Why repeat the class here?

The Space needs the class definition to reconstruct the model architecture when it calls from_pretrained. Without it, PyTorch cannot load the saved weights back into the correct layers.

  • Below the class, paste the model loading and prediction logic:
model = ModulationClassifier.from_pretrained("your-hf-username/5g-modulation-classifier")
model.eval()


def predict(iq_text):
    try:
        lines = iq_text.strip().split("\n")
        i_samples = [float(x) for x in lines[0].split(",")]
        q_samples = [float(x) for x in lines[1].split(",")]

        if len(i_samples) != 128 or len(q_samples) != 128:
            return {"Error": 1.0}

        iq_data = np.array([[i_samples, q_samples]], dtype=np.float32)
        tensor_input = torch.tensor(iq_data)

        with torch.no_grad():
            output = model(tensor_input)
            probabilities = torch.nn.functional.softmax(output[0], dim=0)

        result = {MODULATION_TYPES[i]: prob.item() for i, prob in enumerate(probabilities)}
        return result

    except Exception as e:
        return {"Error": 1.0}

What does the predict function do?

It parses two lines of comma-separated text into I and Q sample arrays (128 values each). Then it converts them to a tensor, runs a forward pass through the model, and returns a dictionary of modulation type probabilities.

Replace your-hf-username with the same username you used when pushing the model.

  • Finally, paste the Gradio interface that wires the predict function to a web UI:
demo = gr.Interface(
    fn=predict,
    inputs=gr.Textbox(
        label="IQ Samples (line 1: 128 I values comma-separated, line 2: 128 Q values comma-separated)",
        lines=4,
        placeholder="0.01, -0.03, 0.05, ...\n0.02, 0.01, -0.04, ...",
    ),
    outputs=gr.Label(label="Predicted Modulation", num_top_classes=5),
    title="5G Signal Modulation Classifier",
    description="Classify radio signal modulation type from raw IQ samples. Paste 128 I-channel values on line 1 and 128 Q-channel values on line 2.",
)

if __name__ == "__main__":
    demo.launch()

What does gr.Interface do?

It creates a web form with a text input box and a label output. The num_top_classes=5 argument shows the top 5 predicted modulation types ranked by confidence score.

  • Do not commit yet. First, go back to + Add file and create a second file named requirements.txt.
  • Paste the following dependencies into requirements.txt:
torch
huggingface_hub
numpy

Why is Gradio not listed?

Gradio is pre-installed by the Hugging Face Spaces runtime when you select the Gradio SDK. You only need to list additional packages your code imports.

  • Commit both files by clicking Commit changes to main (commit app.py first, then create and commit requirements.txt).
  • Switch to the App tab at the top of your Space. You should see a build log as the Space installs dependencies and starts the app.

The build takes 2-3 minutes. Once complete, you should see the Gradio interface with the title "5G Signal Modulation Classifier" and a text input box.

Space stuck on 'Building' or showing an error?

  • Check the build log for import errors. A misspelled package in requirements.txt will cause the build to fail.
  • Verify that your-hf-username in the from_pretrained call inside app.py matches your actual Hugging Face username.
  • Make sure your model repository is set to Public. The Space cannot access a private model without extra configuration.
  • Help me debug my Hugging Face Space build failure

Now test your live demo. Before pasting sample data, predict: will the model produce confident predictions on random text, or will it error out?

  • Return to your Colab notebook and run this cell to generate test IQ data you can paste into the Space:
# Generate a test sample to paste into the Space
test_idx = np.where((Y == modulation_types.index("QPSK")) & (SNR == 18))[0][0]
test_sample = X[test_idx]
print("I samples (copy this line):")
print(", ".join(f"{v:.6f}" for v in test_sample[0]))
print("\nQ samples (copy this line):")
print(", ".join(f"{v:.6f}" for v in test_sample[1]))
  • Copy the I samples line from the output.
  • Go to your Space's Gradio interface in the browser.
  • Paste the I samples on line 1 of the text box.
  • Paste the Q samples on line 2.
  • Click Submit.

You should see QPSK as the top prediction with a high confidence score. The model correctly identified the modulation type from raw IQ samples.

Create a GitHub repository with your results

Your model is live and usable. The final piece is packaging the whole project into a GitHub repository with a professional README that highlights your 3 quantitative results.

  • In your Colab notebook, export the notebook by clicking File then Download then Download .ipynb.
  • Go to github.com/new in your browser.
  • Name the repository 5g-modulation-classifier.
  • Set it to Public.
  • Check Add a README file.
  • Click Create repository.
  • Upload your downloaded .ipynb file by clicking Add file then Upload files, and rename it to train.ipynb.
  • Also upload the app.py and requirements.txt files (you can copy them from your Space).

Now replace the default README with your project README. Click the README.md file, then click the pencil icon to edit.

  • Replace the README contents with the following header and results table:
# 5G Signal Modulation Classifier

A deep learning model that classifies radio signal modulation types from raw IQ samples, trained with PyTorch and deployed on Hugging Face.

## Results

| Metric | Value |
|--------|-------|
| Dataset | 220,000 IQ signal samples, 11 modulation types, 20 SNR levels (-20dB to +18dB) |
| Accuracy (SNR >= 0dB) | ~88% (your measured value here) |
| Overall accuracy (all SNR) | ~60% (your measured value here) |
| Model parameters | (your measured value here) |
| Training time | ~4 min on Google Colab T4 GPU |

## 3 Key Points

### 1. Problem and Data
Classified 11 radio modulation types (BPSK, QPSK, 8PSK, QAM16, QAM64, CPFSK, GFSK, PAM4, WB-FM, AM-SSB, AM-DSB) from raw IQ samples using the RadioML 2016.10a dataset. The dataset contains 220,000 synthetic signals generated with GNU Radio across 20 SNR levels from -20dB to +18dB.

### 2. Model and Results
Built a 1D CNN in PyTorch with two convolutional layers achieving **~88% accuracy at practical SNR levels (>= 0dB)** used in real 5G systems. Overall accuracy across all SNR levels is ~60%, reflecting the fundamental difficulty of classifying signals buried in noise at extreme low-SNR conditions.

### 3. Deployment
Published the trained model to [Hugging Face Hub](https://huggingface.co/your-hf-username/5g-modulation-classifier) using `PyTorchModelHubMixin`. Deployed a live [Gradio demo](https://huggingface.co/spaces/your-hf-username/5g-modulation-classifier) on Hugging Face Spaces for real-time browser-based inference.

What goes in the Results table?

Replace the placeholder values with your actual numbers from the evaluation step. Check your Colab output for high_snr_acc, overall_acc, and total_params.

Also replace your-hf-username in the links with your actual Hugging Face username.

  • Below the 3 Key Points section, add the architecture, quick start, and file listing:
## Architecture

- Input: 2 x 128 (I and Q channels, 128 time samples each)
- Conv1D(2, 64, kernel=8) + ReLU + MaxPool
- Conv1D(64, 128, kernel=5) + ReLU + MaxPool
- Fully connected (128*32 -> 256) + ReLU + Dropout(0.5)
- Output: 11 classes (softmax)

## Quick Start

```python
from model import ModulationClassifier

model = ModulationClassifier.from_pretrained("your-hf-username/5g-modulation-classifier")
model.eval()
```

## Files

- `train.ipynb` - Full training notebook (run on Google Colab)
- `app.py` - Gradio demo for Hugging Face Spaces
- `requirements.txt` - Dependencies for the Space
- `signal_examples.png` - Visualization of different modulation types
- `accuracy_vs_snr.png` - Accuracy vs SNR performance chart
- `confusion_matrix.png` - Confusion matrix at high SNR

## Dataset

[RadioML 2016.10a](https://zenodo.org/records/18397070) - CC BY-NC-SA 4.0 license.

## Links

- [Live Demo (Hugging Face Spaces)](https://huggingface.co/spaces/your-hf-username/5g-modulation-classifier)
- [Model (Hugging Face Hub)](https://huggingface.co/your-hf-username/5g-modulation-classifier)
  • Click Commit changes to save the README.

Visit your repository's main page. You should see the rendered README with a results table, 3 key points, and live links to your Hugging Face Space and model.

README not rendering correctly?

  • Check for missing blank lines before headings. Markdown requires a blank line before each ## heading.
  • Verify the table formatting has matching pipe characters on each row.
  • Help me fix my GitHub README markdown formatting

✔️ Awesome, I've got everything!

Great. Double check you have saved all files and both your Space and GitHub repo are publicly accessible.

ⓧ I'd like to double check the full code

Scroll up and confirm you have all the pieces in place:

  • The push_to_hub and from_pretrained cells in your Colab notebook ran successfully.
  • Your Hugging Face Space has app.py (with imports, model class, predict function, and Gradio interface) and requirements.txt (listing torch, huggingface_hub, numpy).
  • Your GitHub repo has train.ipynb, app.py, requirements.txt, and a README.md with the results table and 3 key points.

You have gone from a trained model in a notebook to a shareable, deployable artifact. Anyone can load your model with from_pretrained, try it in their browser via the Space, or read your results on GitHub.

Secret mission

Improve Low-SNR Classification with Noise Augmentation

Your model crushes it at high SNR, but struggles in noisy conditions. Production signal classifiers use a clever trick: they inject random noise during training so the model learns to be robust. Can you implement noise augmentation and prove it improves mid-range SNR accuracy?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely on free services (Google Colab, Hugging Face free tier, GitHub free), so there are no ongoing costs.

Resources you used:

  • Google Colab notebook (saved to your Google Drive).
  • Hugging Face model repository (your-hf-username/5g-modulation-classifier).
  • Hugging Face Space (Gradio demo).
  • GitHub repository with notebook, app.py, requirements.txt, and README.md.
  • Hugging Face account (created during this project).

Keep everything running

No action needed. Choose this if you're still actively building or want to keep your deployed model and demo accessible.

  • Your Google Colab notebook stays in Google Drive indefinitely. The GPU runtime disconnects automatically after inactivity.
  • Your Hugging Face model and Space remain live and publicly accessible on the free tier at no cost.
  • Your GitHub repository remains public and accessible.
  • Your Hugging Face account remains active for future projects.

Pause - I'll come back to this later

Shut down the active runtime to free up GPU resources, but keep your files and deployed artifacts intact.

  • In Google Colab, click Runtime in the top menu, then Disconnect and delete runtime. Your notebook remains saved in Google Drive.
  • Your Hugging Face Space can be paused. Go to your Space's page, click Settings, scroll down and click Pause Space. It will stop serving requests but can be resumed later.
  • Your model repository and GitHub repo need no action. They are static files with no running process.

Delete - I don't want to use this again

Remove all project resources permanently. This cannot be undone.

  • Delete the Google Colab notebook from Google Drive. Open Google Drive, find the notebook file, right-click it, and select Move to trash.
  • Delete your Hugging Face model repository. Go to huggingface.co/your-hf-username/5g-modulation-classifier, click Settings, scroll to the bottom, and click Delete this repository. Confirm the deletion when prompted.
  • Delete your Hugging Face Space. Go to your Space's page, click Settings, scroll to the bottom, and click Delete this Space. Confirm the deletion when prompted.
  • Delete your GitHub repository. Go to your repo's page, click Settings, scroll to the Danger Zone section at the bottom, and click Delete this repository. Type the repository name to confirm.

That's a Wrap!

That's a Wrap!

Nice work. You just built, evaluated, and deployed a deep learning model that classifies radio signal modulation types from raw IQ waveform data.

You've learned how to:

  • Train a 1D CNN in PyTorch that classifies 11 radio modulation types from raw IQ signal data across 20 SNR levels.
  • Evaluate model performance with per-SNR accuracy breakdowns and confusion matrices to identify practical accuracy at 5G-relevant signal strengths.
  • Publish a trained model to Hugging Face Hub using PyTorchModelHubMixin and deploy a live Gradio demo on Hugging Face Spaces for browser-based inference.
  • Secret Mission: Improved low-SNR robustness through noise augmentation during training, a standard production technique for signal classifiers.

Ready to quiz yourself?