Semi-Supervised Calibration for EEG BCI: Extending Your Decoder with Unlabeled Streaming Data
Calibration is the bottleneck that no BCI engineer wants to talk about. You collect 100–200 labeled trials, train a Bayesian decoder, and ship it. Then, five minutes into a live session, the signal shifts — electrode impedance drifts, mental fatigue sets in, the user relaxes their scalp — and your decoder quietly degrades.
The usual answer is more labeled data: longer calibration sessions, adaptive recalibration triggers, active learning. These all work, but they all require the user to stop and provide ground-truth labels. In many clinical and consumer contexts, that cost is prohibitive.
Semi-supervised learning offers a different path. The decoder that the user is already running produces posterior probabilities on every epoch. When that posterior is sharp — when the model is confident — those predictions are likely correct, and can be treated as pseudo-labels to update the model's parameters. No new labeled data required. (If you want the broader deploy-time view of confidence gating and streaming inference, see From Training to Streaming: Deploying a Live EEG Decoder with the Nimbus Python SDK.)
This post explains how that loop works, when it is safe, and how to implement it using the Nimbus Python SDK and Nimbus Studio.
The Core Idea: Confidence-Filtered Pseudo-Labels
A Bayesian classifier like NimbusLDA or NimbusSoftmax does not just return a class label — it returns a full posterior distribution over classes. From that distribution you can compute prediction entropy:
H = -∑ p(c|x) · log p(c|x)
Low entropy means the model is confident. High entropy means the evidence is ambiguous. The key insight behind semi-supervised adaptation is:
If the model is confident about an unlabeled epoch, that epoch probably is what the model thinks it is. Feed it back as a training example.
This is not a new idea in machine learning — it goes by several names: pseudo-labeling, self-training, expectation-propagation. What makes it well-suited to BCI specifically is that Bayesian classifiers provide calibrated uncertainty, so the entropy threshold is actually meaningful. With a properly calibrated model (which NimbusLDA provides out of the box via its Student's t predictive), an epoch below a given entropy threshold genuinely has a high probability of being labeled correctly. If you're tuning thresholds in practice, Confidence-Gated BCI: How Entropy and Rejection Policies Keep Decoders Reliable in the Real World goes deeper on the accuracy–coverage tradeoff.
Why Bayesian Classifiers Make This Safe
A softmax output from a neural network is not calibrated by default — it will often produce near-certain predictions on out-of-distribution inputs. Plugging those pseudo-labels back into training creates a feedback loop that can rapidly collapse to a degenerate solution.
The Nimbus Python SDK's Bayesian classifiers behave differently. NimbusLDA uses a Student's t predictive distribution that naturally spreads probability mass when the input is far from the training clusters. NimbusSoftmax uses Polya-Gamma variational inference, which regularizes uncertainty through the prior. This means:
- Entropy is a reliable signal. An epoch with H < 0.2 nats genuinely has high posterior certainty under a calibrated model.
- Retraining with wrong pseudo-labels is less catastrophic. The Bayesian prior acts as a regularizer, dampening the impact of a small fraction of mislabeled pseudo-examples.
- You can monitor calibration drift. The SDK's
evaluate_rejection_policyand ECE/MCE metrics let you detect when pseudo-label quality is degrading before it corrupts the model.
None of this makes semi-supervised adaptation risk-free — but it makes the failure modes detectable and recoverable. (For the deeper motivation behind treating confidence as a first-class output, see Uncertainty Quantification in BCI: Why Confidence Scores Matter as Much as Accuracy.)
Implementing the Loop with NimbusSDK's partial_fit
The Nimbus Python SDK's classifiers expose partial_fit(X, y) — the same online update interface used by Conductor and Adaptive Deploy. For semi-supervised adaptation, the loop is:
from nimbus_bci import NimbusLDA
from nimbus_bci.metrics import entropy
ENTROPY_THRESHOLD = 0.35 # tune per paradigm
MIN_CONFIDENCE_BATCH = 10 # accumulate before updating
# After initial calibration:
clf = NimbusLDA().fit(X_cal, y_cal)
buffer_X, buffer_y = [], []
for epoch in live_stream(): # streaming EEG epochs
proba = clf.predict_proba(epoch.reshape(1, -1))
H = entropy(proba)
predicted_label = proba.argmax(axis=1)[0]
if H < ENTROPY_THRESHOLD:
buffer_X.append(epoch)
buffer_y.append(predicted_label)
if len(buffer_X) >= MIN_CONFIDENCE_BATCH:
clf.partial_fit(np.array(buffer_X), np.array(buffer_y))
buffer_X, buffer_y = [], []
yield predicted_label, H # downstream BCI output
A few design decisions embedded in this snippet are worth unpacking:
- Batched updates, not single-epoch updates. Calling
partial_fitafter every epoch is noisy. Accumulating 10–20 high-confidence epochs before updating smooths the parameter trajectory. - Entropy, not raw confidence. For multi-class paradigms, max probability can be misleading. Entropy aggregates uncertainty across all classes.
- No ground truth required. The loop runs entirely on unlabeled streaming data after calibration.

Wiring It in Nimbus Studio
Nimbus Studio's deploy path does not yet expose a native semi-supervised adaptation node — the partial_fit loop above is most naturally implemented as a Python script wrapping the SDK. The recommended integration pattern for a Studio pipeline is:
- Build phase: Run a standard calibration pipeline —
custom_data→ preprocess →epoching→csp→NimbusLDAviarxlda_sdk. Export the trained model checkpoint. - Deploy phase: Use
hardware_device→ preprocess →epochingto produce live epochs. Wire these to a custom Python step that runs the confidence-gating loop above. - decision_policy node: Feed the final predictions from your custom step into Studio's
decision_policyfor streaming stability.
For teams using the Personalizer node (rxpersonalizer_sdk with nimbus-bci 0.6.0), the integration is more direct. The Personalizer exposes partial_fit on the Bayesian head while keeping the frozen trunk (REVE, EEGNet) intact. High-confidence streaming epochs update only the head parameters — a much smaller parameter space — reducing the risk of catastrophic forgetting. (The Personalizer Node: Bayesian Personalization for Frozen EEG Trunks in Nimbus Studio walks through the wiring and decision-policy gating.)
Entropy Thresholds and When to Distrust Them

The entropy threshold is the most important hyperparameter in this loop. Too low, and you discard most streaming epochs — the adaptation is too slow to be useful. Too high, and you accept mislabeled pseudo-examples — the model drifts toward a wrong solution.
Practical guidance by paradigm:
- Motor imagery (binary): Start at H < 0.3 nats. The two-class structure means confident predictions are usually reliable.
- P300 speller: More conservative — H < 0.2. The class imbalance (rare P300 vs frequent non-P300) means the model can be confident on the majority class without learning much.
- SSVEP (4+ classes): H < 0.5 is reasonable given the wider entropy range with more classes.
Importantly, entropy thresholds can become unreliable when the signal itself is drifting rapidly. A sign that something is wrong: the model's ECE (expected calibration error) rises while entropy-filtered pseudo-label accuracy falls. Monitor both during a session using evaluate_rejection_policy on any held-out labeled trials you retain from calibration.
When Semi-Supervised Adaptation Helps — and When It Doesn't
This approach shines in specific scenarios:
- Long sessions with stable paradigms. Motor imagery decoding over 30+ minutes, where slow session drift is the dominant degradation mode.
- Low-calibration contexts. Wearable devices or repeated daily sessions where users cannot afford a fresh 200-trial calibration each time.
- Personalizer-based pipelines. Frozen trunk encoders are expensive to re-train; adapting only the Bayesian head with partial_fit is cheap and safe.
It is not a substitute for labeled calibration when:
- The paradigm changes (e.g., switching from left/right MI to a new motor imagery pair).
- Electrode impedance or montage changes dramatically between sessions — in these cases, the classifier's prior is wrong, not just outdated, and unlabeled adaptation can accelerate the wrong direction (more on why this happens in Neural Drift and Why It Breaks Your BCI Classifier (And How Adaptive Bayesian Models Fix It)).
- You are operating in a safety-critical context where prediction errors have consequences. In those settings, keep Conductor's
for_deploymentsafety runtime in the loop.
Conclusion
Semi-supervised calibration does not replace labeled data — it stretches it. A properly calibrated Bayesian decoder has everything it needs to assess its own confidence, and every high-confidence prediction is a free training signal. Feeding those signals back through partial_fit with a sensible entropy gate is one of the lowest-cost ways to extend decoder lifetime across long sessions.
The Nimbus Python SDK's calibrated uncertainty, online partial_fit, and evaluate_rejection_policy diagnostics make this pattern practical today. For teams building on Nimbus Studio's Personalizer node, the Bayesian head architecture makes it even more natural — adaptation is cheap precisely because the trunk is frozen.
The next time a long session degrades your decoder, the labeled trials you need may already be in your streaming buffer.
Ready to run real-time Bayesian BCI?
Design and simulate pipelines visually in Nimbus Studio, or install the sklearn-compatible Python SDK for sub-20ms local inference.
Related Articles
Zero-Calibration BCI with Nimbus Studio's Indicator Nodes: When You Don't Need a Trained Decoder
Most BCI pipelines assume a trained decoder — but Nimbus Studio's indicator nodes let you stream brain-state signals into live apps with zero per-user calibration. This post explains when to reach for bandpower_indicator and blink_detector instead of a classifier, and how INDICATOR and EVENT streams wire into interactive applications.
Minimal Montage, Maximum Signal: EEG Channel Selection for Deployable BCI Pipelines
Most BCI pipelines are designed with lab-grade EEG headsets in mind — 64 or 128 channels, clean recordings, patient subjects. But the moment you move toward a wearable device or a clinical product, channel count becomes a hard constraint. This post explains how to select a minimal, informative electrode subset without sacrificing decoder performance.