Pipeline Recipes
Build channel selection, dimensionality reduction, cross-validation, LOSO and domain adaptation from blocks — starting from nothing but Filtered recordings.
Before you start
Every recipe on this page starts from the same place and adds blocks. They are written for the case where you have nothing but the recordings — no finished Analysis stage to inherit — so they work identically on every plan.
The starting point
Drop a Select Files block and set:
| Setting | Set to | Why |
|---|---|---|
| modality | EEG / fNIRS / EMG | Whichever you recorded |
| data_source | Filtered | The band-passed recording — the one pre-analysis stage, available on every plan |
| session | All Sessions | Or one session, if you are deliberately scoping |
| selected_subjects | (pick in the dialog) | More than one subject is required for anything cross-subject |
That block now emits X (the signal table) and Y (labels), plus Channels for a recording-stage source. Everything below hangs off those two ports.
Why start from Filtered rather than a finished stage
MLFlow, Workflow, or both
Any of these recipes can be built entirely in the Workflow canvas — that is the simplest thing to do, and for a one-off analysis it is the right answer. Build in MLFlow instead when you want the same chain reused across several workflows or studies. The combined section below shows the split.
Channel selection
Keeps the channels that actually separate your classes and drops the rest. On high-density montages this is usually the single biggest win: fewer, better channels means fewer parameters, less overfitting, and a model that trains in seconds.
Wiring it
| From | To |
|---|---|
| Select Files · X | Channel Selection · Data |
| Select Files · Y | Channel Selection · Labels |
| Channel Selection · Selected Data | the next block in the chain |
| Setting | Set to | Why |
|---|---|---|
| method_name | t-Value Method | A solid default for two-class contrasts |
| percentile | 5 | Keeps the top 5% of channels by score. Raise it if you are dropping too much |
| rest_label | 0 | The integer label of your rest / baseline class |
| ignore_classes | (blank) | Comma-separated labels to leave out of the ranking |
See what it did
Selected Channels and Scores into a Data Graph. It is a pass-through tap, so it costs you nothing in the graph and shows exactly which channels survived and why.Choosing a method
Twenty methods are available. In practice:
| Use | When |
|---|---|
| t-Value Method | Two classes, roughly normal — the default, and hard to beat |
| ANOVA F-value | More than two classes |
| Common Spatial Pattern (CSP) | Motor imagery. The classic choice for left/right hand EEG |
| Regularized CSP / Sparse CSP | CSP on few trials or noisy data, where plain CSP overfits |
| Mutual Information Feature Selection | You suspect a non-linear relationship |
| Wilcoxon Signed-Rank Test | Non-normal distributions, paired design |
| PSD-based Selection | The discriminating signal is a band-power difference |
Dimensionality reduction
Projects many correlated channels onto a handful of components. Useful after channel selection, when what remains is still wider than your trial count.
Wiring PCA
PCA is a class block, so it has a selected_method — set it to fit_transform and its ports become X, y and one output.
| Setting | Set to | Why |
|---|---|---|
| selected_method | fit_transform | Fits and projects in one step, which is what a pipeline stage needs |
| n_components | 10–30 | Start well below your trial count. 20 is a reasonable first guess |
| whiten | false | Turn on only if the downstream model assumes unit variance |
FastICA, KernelPCA and TruncatedSVD wire identically. Use FastICA when you want statistically independent sources rather than maximum-variance ones, KernelPCA when the structure is non-linear.
Where it belongs
Fit it inside the folds, not before them
Feature selection
Where dimensionality reduction builds new combined components, feature selection keeps a subset of the original columns — which stays interpretable, because every surviving feature is still a real channel or band.
| Setting | Set to | Why |
|---|---|---|
| SelectKBest · k | 20–50 | Number of features to keep |
| SelectKBest · score_func | f_classif | ANOVA F between each feature and the label |
| SelectPercentile | percentile: 10 | Use instead of SelectKBest when feature count varies between subjects |
| VarianceThreshold | threshold: 0.0 | Cheap first pass — drops dead / constant channels |
| RFE | n_features_to_select | Recursive elimination. Strong but slow: it refits the model repeatedly |
Class blocks need a selected_method
VarianceThreshold and RFE are class blocks, like PCA — set selected_method to fit_transform and their ports become X / y → output. SelectKBest and SelectPercentile are plain function blocks and need no method chosen.The same fold-leakage warning applies: selection is fitting, so inside the folds it goes.
Cross-validation
There is one way to do it: wire a Validation Scheme into the trainer's Val/Eval input. The trainer then trains once per fold and reports per-fold and aggregate metrics with a confidence interval, instead of holding out one split. Left empty, the port reads one hold-out split. Select the trainer and the canvas suggests the scheme; one click adds it already wired.
Wiring it
| From | To |
|---|---|
| Channel Selection · Selected Data | BCILattice Trainer · Dataset |
| Select Files · Y | BCILattice Trainer · Labels |
| your model block · model_obj | BCILattice Trainer · Model |
| Validation Scheme · Validation | BCILattice Trainer · Val/Eval |
| BCILattice Trainer · Metrics | Output Block |
| BCILattice Trainer · Fold Scores | Data Graph (optional) |
After the folds the trainer refits one final model on every row (switch off with refit_final_model), so Trained Model still feeds whatever comes next. The metrics are the fold estimate. The input takes only a Validation Scheme or an Evaluation Protocol: a scikit-learn splitter such as KFold or a bare number of folds is refused, so the card and the run record always say how the run was validated.
Picking a strategy
| Setting | Set to | Why |
|---|---|---|
| stratified_k_fold | (default) | One subject, balanced classes. The everyday choice |
| k_fold | — | Regression, or where stratification is meaningless |
| group_k_fold | — | Multiple subjects, but you only need folds not to split a subject |
| stratified_group_k_fold | — | Both: grouped AND class-balanced. Classification only |
| leave_one_group_out | — | LOSO / leave-one-session-out — see below |
| time_series | — | Ordered data where a fold must not train on the future |
| repeated_stratified_k_fold | n_repeats: 10 | Small datasets, where a single 5-fold estimate is noisy |
Training several models
A trainer's Model port takes one wire, so comparing architectures used to mean one workflow — and one reload of every subject's recordings — per model. Model Collection gathers several models into one wire: each subject's data loads once, every model trains on it, and each gets its own row of results.
Wiring it
| From | To |
|---|---|
| your model block · model_obj | Model Collection · Model 1 |
| another model block · model_obj | Model Collection · Model 2 |
| Model Collection · Models | BCILattice Trainer · Model |
| Select Files · X / Y | BCILattice Trainer · Dataset / Labels |
| BCILattice Trainer · Metrics | Output Block |
Drop a second model straight on the trainer
Model port, then drag a second model onto the same port — the canvas offers to insert a Model Collection and rewire both models into it, in one undo step. Works with Nested CV too, and with a trainer that has a Validation Scheme wired.Naming them
Results rows are named "<workflow> · <model>", where <model> is the block wired into that port — its display name, or its label with the canvas's _1 counter dropped when that stays unique. Set model_names on the collection (“CNN, LSTM, GRU”) when several models end in a layer with the same label and would otherwise be indistinguishable on the card; a blank entry keeps the wired block's own name.
| Setting | Set to | Why |
|---|---|---|
| n_inputs | 2 (default) | Grows to whatever you wire — up to 12 — without a second block |
| model_names | (comma-separated, optional) | One name per port; blank keeps the wired block's name |
LOSO (cross-subject)
Leave-One-Subject-Out holds out an entire subject per fold. It is the honest way to answer “will this work on someone the model has never seen” — and the number it returns is almost always lower than a within-subject score, which is the point.
The pooling step nobody expects
A normal run sees one subject at a time
Set Select Files' subject_scope (the File grouping dropdown) to Combined subjects, combined sessions. That collapses every selected subject into a single run, which is what gives the trainer real subject groups to hold out. The trainer's own cohort_mode can say the same thing; leave it on Inherit from the data source, because the run refuses if the two disagree.
Wiring it
| Setting | Set to | Why |
|---|---|---|
| Select Files · subject_scope | Combined subjects, combined sessions | Pools every selected subject into one run — the step that makes LOSO possible |
| Validation Scheme · scheme | leave_one_group_out | One fold per group |
| Validation Scheme · group_by | subject | Groups are subjects. Use session for leave-one-session-out |
| Select Files · selected_subjects | 3 or more | Two subjects gives you two folds — technically valid, statistically meaningless |
There are no group ids to wire. The trainer takes per-sample subject and session ids from the run's own provenance, selected by the scheme's group_by.
group_by: auto is smarter than it looks
auto the scheme prefers whichever of subject or session actually has more than one distinct value — so a single-subject multi-session recording automatically becomes cross-session rather than failing.Train on one session, test on the others
Cross-validation mixes every session into every fold. When the question is whether a model trained in one session (or one condition, such as indoors) still works in another, the sessions have to stay whole: some train, the rest only test. Wire an Evaluation Protocol into the trainer's Val/Eval input — the same input a Validation Scheme uses.
Wiring it
| From | To |
|---|---|
| Select Files · X | BCILattice Trainer · Dataset |
| Select Files · Y | BCILattice Trainer · Labels |
| your model block · model_obj | BCILattice Trainer · Model |
| Evaluation Protocol · Validation | BCILattice Trainer · Val/Eval |
| BCILattice Trainer · Metrics | Output Block |
Choosing the settings
| Setting | Set to | Why |
|---|---|---|
| Select Files · Runs | Per subject, combined sessions | Each subject is one run holding all of their sessions, so there is something to test across. Per session would give each run one session, and the run stops and says so |
| across | session | Uses the session names in your dataset as written — sessions named indoor and outdoor are trained and tested on by those names |
| train_on · train_values | named · Session 1 | One model, trained on Session 1. Several names train one combined model |
| train_on | each in turn | Instead: every session trains its own model and is tested on the others — indoor → outdoor and outdoor → indoor in one run |
| direction | both ways | Or pick one pair from the list, e.g. indoor -> outdoor, to run that way only. The list is filled with your session names |
| test_on | all others | Or named, to test on chosen sessions only |
| within_check · within_size | hold-out · 0.2 | Holds back 20% of the training session and scores it with the same model (k-fold: in rounds, averaged): the same-session number the others are compared with |
The run reports every train → test pair (Session 1 -> Session 2 accuracy), the same-session check (within_accuracy) and, as the headline accuracy, the mean over the pairs to other sessions. On the Execute page, Detailed names what each number was tested on, Per Subject shows each subject's same-session and other-session score and the drop, Statistics → Same session vs another session tests whether that drop holds subject by subject, and Graphs has the train × test matrix (its diagonal is the same-session check) and a confusion matrix per pair.
Across subjects works the same way
across to subject, and name the subjects that train (sub01, sub02, …) and, if you like, the ones that test. For leave-one-subject-out over every subject, use a Validation Scheme instead — that is cross-validation.With the other trainers
With the plain trainer the protocol only evaluates: a model never sees a row of a session it is tested on. Wire the same block into a dedicated trainer and it does what that trainer is for, and the run says so in every table:
| Setting | Set to | Why |
|---|---|---|
| DANN Trainer | adapts to each test session | Sees the test session’s data WITHOUT its labels while training. The unadapted score is reported beside it (baseline_accuracy), so the gain is a number, not a guess |
| Fine-Tuning Trainer | fine-tunes on a slice | Trains on the training session, fine-tunes on target_size (20%) of each test session with labels, and is scored on the rest — beside the same model before fine-tuning |
| Class-/Domain-Incremental, Continual Learning | learns a sequence | The sessions are learned in the order named in train_values; every session is scored after every step. Sessions left out are scored at the end (transfer_accuracy) |
| Multi-Task, Distillation | evaluates | Exactly like the plain trainer; Multi-Task is scored per task |
| Contrastive Trainer | not supported | It learns without labels, so there is nothing to score a test session against |
An adapted score is a different claim
The leakage guard
strict_leakage_check is on by default, and it will stop a run rather than let it report a number you would have to retract.
It fires when:
- you use a non-grouped strategy on data spanning more than one subject or session — trials from the same person land in both train and test, and the score measures memorisation;
- group ids were present but had to be dropped, e.g. a block changed the row count and the groups no longer align with
X.
The fix is almost always to switch to group_k_fold or leave_one_group_out, not to turn the check off. Turn it off only when you have a specific reason and know what the resulting number does and does not mean.
Domain adaptation
Cross-subject and cross-session models fail mostly because each recording sits in a slightly different feature space — different impedances, cap placement, day. Domain adaptation pulls those spaces together before the model sees them.
Euclidean Alignment
Start here. It is cheap, needs no target labels, and reliably lifts cross-subject scores — the standard first thing to try for transfer EEG.
| From | To |
|---|---|
| Channel Selection · Selected Data | Euclidean Alignment · Data |
| Euclidean Alignment · Aligned Data | BCILattice Trainer · Dataset |
CORAL / MMD / TCA
These take an explicit source and target, so they suit the setup where you train on a group of subjects and adapt to one held-out person. Use Data Splitter or two Select Files blocks to produce the two streams.
| Block | Inputs → Outputs | Use when |
|---|---|---|
| CORAL Adapter | Source Data, Target Data → Aligned Source, Alignment Matrix | Fast, linear second-order alignment. The usual first choice after EA |
| MMD Aligner | Source Data, Target Data → Aligned Data, MMD Score | Distribution-level alignment; MMD Score tells you how far apart they were |
| Transfer Component Analysis | Source Data, Target Data → Source Projected, Target Projected | Learns a shared subspace. Stronger, slower, needs tuning of n_components and mu |
| Feature Alignment | Source Data, Target Data → Aligned Data | Simple z-score / moment matching. Good baseline to beat |
| Session Transfer | Source Features, Target Features, Source Labels → Transferred Features, Sample Weights | Same subject, different day |
Subject Adaptation
Different shape from the rest: it takes a trained model and a small amount of labelled data from the new person, and returns a model tuned to them. This is the calibration-session workflow — a few minutes of data from a new user instead of a full training session.
| From | To |
|---|---|
| BCILattice Trainer · Trained Model | Subject Adaptation · Base Model |
| (new subject) Select Files · X | Subject Adaptation · Adaptation Data |
| (new subject) Select Files · Y | Subject Adaptation · Adaptation Labels |
| Subject Adaptation · Adapted Model | Output Block |
| Setting | Set to | Why |
|---|---|---|
| method | fine-tune | Or a lighter scheme if you have very few shots |
| n_shots | 20 | Labelled trials from the new subject. This is your calibration budget |
| lr | 0.0001 | Low on purpose — you are nudging a trained model, not retraining it |
Reusing a model trained earlier
Subject Adaptation above reuses a model within one run — the trainer's output feeds it directly. Across runs, or across studies, there is nothing to wire: the model that produced last month's number exists only inside that run's artifacts unless you bring it back. Load Trained Model does that — architecture and weights, from a published .bltm or a registry entry — and Forward Pass is how you call it once it's on the canvas.
Wiring it
| From | To |
|---|---|
| Load Trained Model · Model | Forward Pass · Model |
| Select Files · X | Forward Pass · Data |
| Forward Pass · Output | Output Block |
Evaluate Model needs labels — Forward Pass does not
| Setting | Set to | Why |
|---|---|---|
| source | package | A .bltm you were handed. Use registry for a name + version saved in this install |
| use_training_knowledge | true (default) | Off gives you the published ARCHITECTURE, freshly initialised, to train yourself |
| collection_model | (only if the graph trained several) | Which model, when the package came from a Model Collection run. A package from one model's own results row needs nothing set |
| freeze | true (default) | Off only if you mean to keep training this model — freeze protects a pretrained encoder from an accidental fine-tune |
The combined pipeline
Everything above, in one graph: filtered recordings in, an honest cross-subject number out.
Splitting it across MLFlow + Workflow
Once that chain works, the reusable half is everything between the data and the validation. Move it into an MLFlow pipeline:
Two Input Blocks and one Output Block become two input ports and one output port on the compiled block, named from each block's port_name. Point a second Select Files at fNIRS instead and the same compiled block runs on that. See the port contract.
Sanity checklist
Before you believe a number, check these:
- Is the split grouped? Multi-subject data with a plain k-fold measures memorisation. The leakage guard catches this — do not switch it off to make the error go away.
- Did anything fit outside the folds? A reduction or feature selection fitted upstream of the trainer has seen the test rows.
- Is the score above chance? The CV report carries theoretical and empirical chance levels. Compare against the empirical one — with imbalanced classes it is higher than 1/n.
- Is the confidence interval narrow enough to mean anything? A 5-fold run on 40 trials produces a wide one. Use
repeated_stratified_k_fold. - Did you pool for LOSO? Without Select Files'
subject_scopeset to a Combined subjects mode, a cross-subject strategy has one subject to work with. - Two models to compare? Use Statistical Comparison — McNemar or a paired test — rather than eyeballing two accuracies.
Block-by-block detail: Custom Blocks and Advanced Blocks. How the canvases fit together: Using the ML Suite.