The project's confession · model card
The machine that tried to read them
Every letter is downstream of this model. The full card is reproduced below, unedited, from the same document the repository ships.
Accuracy0.198
Chance0.111
One dot per guess in a hundred, over nine classes.
Model card
The classifier that wrote to 265 strangers, and everything wrong with it.
**Version** v0.3.0, frozen 2026-07-04. Never retrained, never fine-tuned.
Generated metrics: `pipeline/artifacts/model-card-generated.md`.
Bundle evidence: `pipeline/artifacts/model-bundle-manifest.json`.
Summary
A balanced multinomial logistic regression over 70 differential-entropy
features, trained on one EEG dataset and pointed at four others.
| --- | ---: |
| Overall accuracy | 0.1983 |
| Balanced accuracy | 0.1959 |
| Macro F1 | 0.1884 |
| Expected calibration error | 0.0132 |
| Chance, nine classes | 0.1111 |
**It is barely better than guessing.** That is the finding, not a defect to be
engineered away. A project about machine overconfidence needs a machine that is
confidently wrong, and this one is: it will hand you a dominant emotion for a
person who was doing mental arithmetic in 2018, and it will be wrong roughly
four times in five.
Training data
**FACED** — Finer-grained Affective Computing EEG Dataset.
`https://www.synapse.org/Synapse:syn50614194`
Access is account-gated: an authenticated Synapse account, the official
`Data.zip`, and the official `Code.zip`. 123 subjects watched emotional film
clips. Nine classes, in the order the corpus locks:
amusement inspiration joy tenderness anger fear disgust sadness neutral
Four positive, four negative, then neutral. Every downstream contract — corpus
schema, web legend, glyph assignment — depends on that order.
**Labels** are the last 30 seconds before each subject-specific `102` event
marker, mapped through the official `video2label()` function extracted from
`Code.zip`. The training entry point refuses to run if that function is not
found in the archive, so the mapping can never silently drift to a
reimplementation.
Emotion labels here are **elicitation labels** — what the stimulus was intended
to provoke. They are not measurements of anyone's inner state, not even in the
training set.
Features
70 values per 4-second epoch: 14 channels × 5 frequency bands.
For a band-filtered Gaussian signal, differential entropy reduces to a
log-variance:
DE = 0.5 * log(2 * pi * e * variance)
Bands, extracted with a 4th-order Butterworth bandpass applied zero-phase:
| Band | Hz |
|---|---|
| delta | 1 - 4 |
| theta | 4 - 8 |
| alpha | 8 - 14 |
| beta | 14 - 31 |
| gamma | 31 - 45 |
Channels are the 14 that every cleared dataset has in common:
Fp1 Fp2 F3 F4 F7 F8 C3 C4 T7 T8 P3 P4 O1 O2
Feature order is `channel:band`, channel-major. Both training and inference
assert that the computed order matches the order stored in the bundle, so a
montage edit cannot silently permute the feature space.
The estimator
StandardScaler
LogisticRegression(C=1.0, class_weight="balanced",
max_iter=1000, random_state=23, solver="lbfgs")
That is the whole model. No deep network, no convolution, no attention. Two
reasons, and the second is the real one:
1. It is a defensible baseline for 70-dimensional tabular features.
2. **It is legible.** The project's argument only holds if the model's behaviour
can be stated in one sentence and audited by someone who did not build it.
Probabilities are the native logistic softmax with no post-hoc calibration.
Validation
Leave-one-subject-out across all 123 FACED subjects. 44,664 labelled epochs,
123 folds.
Splits are **subject-level, never epoch-level**. Epoch-level splitting would put
adjacent 4-second windows from the same person on both sides of the split and
inflate accuracy badly. Every learned transform — scaler and classifier alike —
is fitted inside each fold.
A sensitivity run excluding flagged subject `sub091` moves overall accuracy by
less than 0.0001, so no single subject carries the result.
Confusion matrix and per-class figures: `pipeline/artifacts/model-card-generated.md`.
Architectures considered and rejected
Recorded at the v0.3 design stage:
- **EEGNet or another neural model.** Rejected. It might have improved eventual
accuracy, but it introduces leakage, calibration and reproducibility risk
before the baseline contract was proven. Higher accuracy was never the goal.
- **Waiting for SEED or DEAP access before modelling.** Rejected. It would have
produced a stronger cross-dataset story, but it blocks all model work on
external access that is not under repository control.
An SVM-versus-MLP comparison appears in the original v0.1 roadmap as a planned
decision point. **It was never run.** The v0.3 design supersedes it by choosing
the linear baseline and deferring neural models entirely. Earlier documentation
claimed those architectures were "explicitly discarded"; that claim was not
supported by the record and has been removed.
Projection limits
This is the section that matters most, and the one most easily skipped.
The model was validated **only on FACED**. It is then applied to four cohorts
drawn from entirely different studies, doing entirely different things:
| Cohort | Subjects | What they were actually doing |
|---|---|---|
| --- | ---: | --- |
| Counters | 36 | mental arithmetic |
| Imaginers | 109 | imagined limb movement |
| Listeners | 21 | listening to affective music |
| Lonely | 99 | auditory oddball task |
Only Listeners involves emotion at all, and even there the emotion is a property
of the stimulus rather than a measured state.
**No cross-dataset accuracy has ever been measured.** There is exactly one
validated number in this system — 0.1983, internal to FACED. Every projection
output is an artistic projection, not a measurement, and must be read as one.
Fraction of each cohort whose mean confidence falls below 0.35, from
`pipeline/artifacts/domain-diagnostics.json`:
| Cohort | Low-confidence fraction |
|---|---|
| --- | ---: |
| Counters | 93.1% |
| Imaginers | 97.4% |
| Listeners | 74.0% |
| Lonely | 90.9% |
Listeners is the least out-of-distribution cohort, which is consistent with it
being the only one where anyone was shown emotional stimuli.
Across the whole 265-subject corpus, mean confidence runs from 0.196 to 0.826
with a median of 0.255, and **245 of 265 subjects sit below 0.35**. The model
rarely commits to anything. Downstream prose must hedge every emotional
assertion, and the archive's visual language is built to show this rather than
hide it.
Per-feature drift and out-of-distribution scores are available through
`pipeline.model.domain_shift` — two-sample KS tests per feature plus a
standardised mean-shift score against the FACED reference.
Class order reconciliation
The frozen bundle stores `class_order` alphabetically, because that is what the
underlying scikit-learn estimator produces. The corpus contract fixes the
locked order shown above.
The remap happens in `pipeline/inference/predictions.py`: epoch-level softmax
probabilities are summed per emotion and re-emitted in the locked order before
the corpus distribution is computed. The bundle's alphabetical ordering is an
internal detail that never reaches the corpus or the website.
Provenance and redistribution
The trained bundle, its deterministic outputs, and the FACED preprocessing
pipeline are **local-only artifacts**. The bundle is derived from account-gated
data and is not redistributed; `pipeline/model_bundle/` is gitignored.
`pipeline/artifacts/model-bundle-manifest.json` is committed and records the
bundle's SHA-256, feature count, class order, dependency versions and metrics.
Inference verifies the local bundle against that manifest before loading it, so
a bundle that does not match the committed evidence cannot silently produce
predictions.
No waveform data is redistributed anywhere in this project.
Intended and unintended use
**Intended.** An artwork about machine overconfidence, and a demonstration of
what happens when a weak classifier is applied outside its training
distribution with full disclosure.
**Not intended, and not supported by anything here.** Diagnosis. Screening.
Assessment of any individual. Any claim about what a participant actually felt.
Any deployment where a person is affected by the output.
Participants did not consent to this and cannot answer it. Corrections and
withdrawal: `hello@aadrikas.space`.
Edge policy
How the archive decides that two strangers are kin.
**Decided** 2026-07-04, at the v0.4 → v0.5 hand-off. Unchanged since.
Implemented in `pipeline/corpus/edges.py`; rendered on `/the-model`.
What an edge is
Nothing about these people is compared directly. The only thing the archive
knows about a participant is the nine-dimensional probability distribution the
classifier produced for them, so an edge compares *those* — one guess against
another guess.
An edge therefore asserts: **the model was confused about these two people in
the same way.** It is not a claim that they are alike.
The rules
- **Threshold.** Cosine similarity ≥ 0.85 between two subjects' nine-dimensional
prediction distributions. Below that, no edge exists.
- **Ceiling.** Each subject nominates at most its five strongest
threshold-clearing candidates. The persisted edge set is the undirected union
of those nominations; the rendered kin list is capped at five.
- **No quota filling.** A subject with fewer than five candidates above the
threshold keeps the honest shorter list. No weaker edge is ever invented to
round the number up. One participant — `lonely/sub-37` — has no edges at all,
and the archive says so rather than manufacturing one.
- **Ordering.** By similarity descending, then by global corpus ID ascending, so
the output is deterministic.
- **Edge identity.** `edge_id = "{low_id}--{high_id}"` over the lexically sorted
endpoint corpus IDs. Qualified IDs are required because raw subject IDs
collide across cohorts, and sorted IDs keep the corpus JSON diffable.
- **No self-edges.** The assembler drops them with a warning.
- **Stable skip.** Subjects whose distribution is all-zero or perfectly uniform
are excluded from the candidate pool with a warning, so their low
`mean_confidence` is preserved rather than laundered into a false edge.
- **Cohort tie-break.** When two candidates tie on similarity and ordering, the
cross-cohort edge wins. This only affects exact ties; ordinary sorting already
produces far more cross-cohort edges than within-cohort ones.
What this produces
| --- | ---: |
| Edges | 910 |
| Nodes with at least one edge | 264 of 265 |
| Similarity range | 0.850 – 0.998 |
| Cross-cohort share | 43% |
| Mean degree | 6.9 |
The caveat that matters
Cosine similarity on a nine-simplex where `neutral` and `tenderness` are the
dominant prediction for 153 of 265 subjects produces a graph that is **close to
a clique in disguise**. The 0.85 threshold is the only thing holding it at 910
edges rather than tens of thousands.
Anything built on this graph — layout, clustering, community detection — must be
validated against that fact rather than assuming the structure is meaningful.
The web layer's spatial layout does exactly this: it runs a force-directed
embedding first, measures whether the result collapsed, and falls back to a
stress-majorised embedding when it has. See `web/lib/layout.ts`.
Out of scope
- A manual "kin of N" override at the UI level. Reserved for the opt-out
workflow.
- Revealing projection-cohort edges without the domain limits alongside them.
The limits are published in [`model-card.md`](model-card.md); the edges are
never shown without them.