tweet-response

Sequence modelling · NLP · on-device inference

Six emotions,
176,000 parameters,
zero servers.

A bidirectional LSTM trained on tweets from the dair-ai/emotion corpus, quantised to int8 and shipped as a  KB binary. Everything below — the prediction, the token attributions, the nearest-neighbour search — is computed on your machine, in plain JavaScript, with no framework and no API call.

test accuracy
macro F1
parameters
inference, in-browser
01

The model, live

Type anything. The network re-runs on every keystroke — a forward pass, one leave-one-out pass per token, and one pass per prefix — inside a web worker.

loading weights…

Posterior
Token attribution Δ log-odds() when the token is removed

Warm tokens push the winning class up; cool tokens hold it down. Exact leave-one-out ablation — one full forward pass per token, no gradient approximation. Reported in log-odds because a saturated softmax flattens every raw Δp to zero; hover a token for both numbers.

Belief trajectory posterior after reading the first k tokens

Each step re-encodes the truncated prefix from scratch, so the curve shows what the model would have answered had the sentence stopped there.

02

Architecture

Four layers, faithful to the original Keras notebook — re-implemented in PyTorch so the initialisers, gate order and unmasked padding all match.

Parameter budget

Why the second BiLSTM sees 40 channels

The first bidirectional layer returns sequences, concatenating a 20-unit forward pass with a 20-unit backward pass at every timestep. The second consumes that 40-channel sequence and emits only its endpoints: forward state at t=49, backward state at t=0.

Padding is not masked

The original model had no mask_zero, so all 50 timesteps — including padding — flow through the recurrence. The port keeps that behaviour rather than quietly "fixing" it, because fixing it would no longer be the same model.

03

The data, honestly

20,000 self-reported emotion tweets, heavily imbalanced. Everything the model can and cannot learn starts here.

Split composition

Tweet length words per tweet, train

Vocabulary

Rank-frequency truncation at 10,000 types is the single largest information bottleneck in the pipeline — everything rarer collapses to one <UNK> vector.

Most discriminative tokens per class weighted log-odds, informative Dirichlet prior (Monroe et al. 2008)
04

Training run

Adam, batch 32, early stopping on validation accuracy with patience 2. One run, no cherry-picking — the curve below is the run that produced the weights you just used.

Accuracy train validation
Cross-entropy loss train validation
Reproduction ledger
05

Evaluation

2,000 held-out tweets the model never saw. Accuracy alone would hide the interesting parts, so: per-class scores, a confusion matrix you can click, a reliability diagram and a risk–coverage curve.

Confusion matrix row-normalised · click a cell
Per-class performance
Calibration ECE

Risk–coverage abstain below a confidence threshold
06

Where it breaks

The failures are not random. Two structural confusions account for most of the lost accuracy, and both are visible in the label distribution.

Dominant confusion pairs
Confidently wrong highest-confidence mistakes on the test set

Click any example to load it into the live model above.

07

What the embedding layer learned

16 dimensions, trained from scratch on 16k tweets — no GloVe, no pretraining. Projected to two principal components and coloured by the emotion the model predicts when the token is shown alone.

Embedding space · PC1 × PC2
Nearest neighbours

Cosine similarity over the raw 16-d embedding matrix, computed in the browser across all 10,000 rows.

08

Getting it into a browser

Training a model is half the job. The other half is making it small enough, fast enough and verifiable enough to hand to a stranger on a phone.

Quantisation

Symmetric per-tensor int8 for weight matrices, float32 for biases. The fidelity numbers are measured against the float32 checkpoint on all 2,000 test tweets — not estimated.

Payload

No TensorFlow.js (≈900 KB), no ONNX runtime, no WASM blob. The forward pass is 90 lines of typed-array arithmetic.

Measured on this device

Wall time of a batch of single forward passes at sequence length 50, divided by the batch size and timed right now in your browser.

Numerical parity

The Python reference implementation in ml/export.py emits golden probability vectors; the JavaScript engine reproduces them to the shown tolerance. Parity is asserted in CI, not assumed.

Reproduce it
# 1 · fetch the official 16k/2k/2k split
python ml/fetch_data.py

# 2 · train (CPU, ~6 min) — writes ml/artifacts/
python ml/train.py --config baseline

# 3 · evaluate + quantise + emit docs/model/
python ml/export.py --config baseline

# 4 · assert the JS engine matches the Python reference
node ml/parity.mjs
09

Model card

What it is for, and what it is not for.

Intended use
  • Demonstrating end-to-end sequence classification: data → training → evaluation → quantised deployment.
  • Coarse emotional tone on short, first-person English text.
Out of scope
  • Clinical, safety, hiring or moderation decisions about real people.
  • Sarcasm, negation-heavy text, code-switching, or any language but English.
  • Text unlike the training distribution — the corpus is self-reported "i feel …" statements, and the model leans hard on that framing.
Known limitations
    Provenance