The Engine

Published on August 22, 2025

By Elijah Ghossein

You're reading part 1 of the series Data Factories

Daydreams

The Surfliner runs from Los Angeles to San Diego in about four hours. For an overthinker like me, that’s plenty of time to replay every decision I’ve made and every conversation I’ve had in the past week.

At our last Neuclear meeting, we bounced around a lot of ideas before settling on two projects: building an artifact-detection machine learning (ML) model and simulating acute ischemic stroke data. In the middle of that brainstorming, Arsam tossed us an interesting idea: what if, instead of just detecting artifacts, we trained a model to clean them?

No idea is too far-fetched to explore. But this one wasn’t even that far out, research has already been working on this. CLEnet is one example. That got me daydreaming and sketching in my notebook:


Draft of an artifact cleaning pipeline

Draft of an artifact cleaning pipeline


This idea didn’t come completely out of the blue. While working on the artifact-detection model, I found myself enjoying the process of taking raw data and shaping it into an ML-ready training set. It felt like building a production line, except it only worked on the TUH Artifact Corpus.

Around that time, we also started to notice just how inconsistent the TUH labels were. Our detection model kept hitting an accuracy ceiling at ~86%, which made it clear how unreliable it can be to trust data we didn’t collect and label ourselves.

That led to this vision: a pipeline that could automatically clean any dataset. With that, we could generate a massive paired dataset, raw and cleaned versions side by side, capable of training a model to see through artifacts. The ultimate goal would be to fine-tune such a model in real time so it could adapt to each person’s brain signature.

One problem at a time, though. Let's start with the automatic cleaning pipeline.

Automatic cleaning?


Contaminated vs. clean EEG

Contaminated vs. clean EEG


Outside the realm of ML, Independent Component Analysis (ICA) remains one of the most reliable methods for cleaning EEG. After decomposing EEG into several components, you can automatically reject bad ones using ICLabel (so yes, back to ML again), which I’ll add to the toolbox. According to Onton and Makeig, an EEG recording should contain at least 30 × (number of channels)² data samples to undergo ICA decomposition, so that becomes a constraint.

Another method I’ve been wanting to test is Wavelet-enhanced ICA (wICA). It combines ICA with wavelet thresholding to preserve as much neural signal as possible while removing artifacts.

At this point, I could already picture the pipeline: band-pass → notch filter → resample → average reference → decompose with ICA → compute IC features → ICLabel rejection → recompose → epoch. Something like that. And then I stumbled across a goldmine…

Just RELAX


The complete RELAX pipeline

The complete RELAX pipeline


Neil Wayne Bailey and his colleagues had already created an automated pipeline to clean EEG data. Not only was it more sophisticated than anything I could imagine at the time, but it was also packaged as an EEGLAB plugin for MATLAB. It’s called the RELAX pipeline.

This was great news, I wouldn’t need to code the entire pipeline from scratch. All I had to do was set it up and integrate it into my program.

Pasadena City College gives us free access to MATLAB, which was a surprise, but I won’t question it.


List of Data Engine dependencies

List of Data Engine dependencies


A million dependency installations later…

The pipes

I was finally able to—nope, source code bugs:


List of source code patches

List of source code patches


A couple patches later, one last MATLAB error popped up:

Cannot find X Y Z coordinates for channels: [fp1, f4, ...]

Nice. It was using spatial context. Channel coordinates are useful for detecting bad electrodes and for mapping relationships across the scalp, instead of treating channels as disconnected time series. So I added a .elp file, which looks like this:


Spatial coordinates for the TUH dataset based on BioSemi

Spatial coordinates for the TUH dataset based on BioSemi


After that final touch, the pipeline finally worked. I configured it to use AMICA, which is essentially ICA on steroids. Unlike standard ICA, it doesn’t assume EEG is stationary and instead runs multiple ICA models in parallel, choosing the one that best explains the data. Pretty cool, right? Even though it takes about 10 minutes to clean a single recording, at least the output is more trustworthy.

Everything.yaml

As for configurations, there are two key files that drive the entire engine:

  • config.yaml, which single-handedly adjusts the behavior of the whole program.
  • RELAX_config.yaml, which sets the preferences for the RELAX pipeline.

You’ll generally want to minimize changes to RELAX_config.yaml to maintain consistent cleaning across all datasets. Credit goes to Arsam for showing me just how powerful a simple YAML file can be.


Excerpt from RELAX_config.yaml

Excerpt from RELAX_config.yaml


With the pipeline set up and running, the next step was integration.

Interoperability

To call MATLAB code from a Python script and pass variables between the two, you need a package called matlabengine. Once installed, calling the pipeline looks something like this:


Passing to and calling MATLAB from Python

Passing to and calling MATLAB from Python


Key in the ignition

With everything integrated, you can now fire up the engine with just a few lines of code:


Cleaning EEG with the Data Engine

Cleaning EEG with the Data Engine


For testing, let’s look at some before-and-after plots to prove that our workout plan isn't bullsh*t:


EEG data before and after processing

EEG data before and after processing


Things are looking pretty good so far. The engine’s output follows this structure:

data/
├── raw/
│   └── raw_training_epochs/
│       └── subject1/
│           └── c3_epoch0_raw.pt
└── clean/
    └── clean_training_epochs/
        └── subject1/
            └── c3_epoch0_clean.pt

Single-channel epochs are generated with the goal of creating a headset-agnostic cleaning model. You can use the data_loader.py script to load the engine’s output for training.

The engine still needs some tweaks and testing before it’s squeaky clean and ready for large-scale data production. For now, I’ll be passing a test dataset to Jason so we can start training a model and see how it performs.

Views: 643

Leave a Comment

Comments

No comments yet. Be the first to comment!