The Engine
Published on August 22, 2025
By Elijah Ghossein
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
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
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
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
A million dependency installations later…
The pipes
I was finally able to—nope, source code bugs:
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
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
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
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
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
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.
Leave a Comment
Comments
No comments yet. Be the first to comment!