Skip to content

About

MIME (Mouse Imitation via Masked Events): a generative model of human mouse movement. Near chance against adversarial detectors, essentially indistinguishable from human movement to the detectors tested (0.504 tuning / 0.513 out-of-sample). Full methodology and 200+ logged experiments.

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

154 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

verify License: Apache-2.0 Python 3.10+

MIME-mouse

MIME (Mouse Imitation via Masked Events) is a generative model of human mouse movement. The name says what it does twice over: like a mime, it imitates human movement, and it does so by filling in masked events, the technique at the model's core.

Bots and scripts move a mouse cursor in ways that give them away. Real human movement has a signature: it accelerates and decelerates unevenly, curves slightly off the direct line, and even pauses mid-motion for a few milliseconds before continuing. Synthetic movement, the straight lines and smooth curves a script generates, looks nothing like that up close, which is why detection systems can catch it.

Recorded human, this project's model, and a scripted bot, drawn dot by dot

The picture above shows the difference. Each dot is one recorded sample of cursor position, so the spacing between dots shows speed. The top row is a real person (from the public Balabit dataset), the middle row is this project's model, and the bottom row is what a naive script produces.

This project asks a narrower question: starting from nothing but a start point and an end point, can a generative model learn to produce a cursor path that a detector built to catch synthetic movement cannot tell apart from a real human's? The detector here is a Random Forest trained on 18 measurements of a movement's shape (its speed, acceleration, jerk, curvature, and so on). Random guessing on a two-way choice scores 0.50 on this test (AUC); the detector started this project at 0.998, meaning it could spot the synthetic paths almost every time. Over about two months and 200+ experiments, that number came down to roughly 0.51, which is close to random guessing.

Detector AUC over the course of the project

The rest of this page explains how to run it yourself, what the result actually is, and where it still falls short.

Quick start

Each step below does one thing. Run them in order from a fresh clone.

git clone https://github.com/4LAU/MIME-mouse.git
cd MIME-mouse
pip install -e .
  1. Install the project. The three commands above clone the repository, move into it, and install its Python dependencies.

  2. Download the data. python setup_data.py pulls the trained model checkpoints, the evaluation data, and a bundle of cached results, all from the GitHub release, into ./data, ./training and a couple of expected root locations. Nothing here requires a GPU.

  3. Generate a trajectory. Give the model a start point and an end point in screen pixels; it returns a human-like path between them.

    python generate.py 200 600 1500 300

    That prints a list of {"x", "y", "delay"} points, where delay is the wait in milliseconds before each move, ready to feed straight into an input-replay layer. The path starts exactly on the start point and ends exactly on the end point. Add --n 5 for several variations, --seed 7 to make a run repeatable, --format csv for spreadsheet-friendly output, or --plot out.png to save a picture of the paths. From Python, from generate import generate gives you the same thing as arrays.

    Each path is one draw from the model behind the current single-trajectory headline (0.5622, see below): the model is asked once and whatever it draws is what you get, with no second candidates and no picking. The raw path usually misses the target by a few pixels (about two percent of the distance); the only edit after the draw spends that miss as single-pixel changes on the path's longest steps so the last point lands on the requested pixel and every other step is exactly what the model drew. --no-land shows the uncorrected path.

    It runs on a laptop CPU. The first call in a process loads the model (a second or two); after that one trajectory takes about a third of a second, and asking for many at once is much cheaper per path (n=50 costs about 70 ms each on a desktop CPU). A GPU is not needed and makes a single trajectory no faster; it only helps when generating hundreds at a time.

  4. Check that what you installed matches the record. python verify_serve.py regenerates 2000 trajectories for one recorded seed through the same code generate.py uses, scores them with the project's detector against the human reference, and compares the result to the value logged when the headline was set. It passes when the two agree within draw noise. A few minutes on a CPU.

  5. Check the older selection headline. python verify_headline.py replays the cached, already-selected trajectories for three seeds through the same evaluator used throughout the project and confirms each one matches the published 0.504. It runs on CPU in about two minutes and never touches a model.

Everything above runs on CPU, including on an Apple laptop. If you have an NVIDIA GPU, a CUDA build of PyTorch (see pytorch.org) speeds up bulk generation and the older experiments under Reproduce below.

Results

The short version: a 6-million-parameter model, paired with a step that selects which of its own outputs to keep, gets a dedicated detector down from spotting synthetic movement almost every time to nearly a coin flip, and that holds up on data the tuning process never saw.

Here is what changed over the project. The first row is a reference point (copying real trajectories), not a generative result:

Approach AUC What it means
Corpus replay (real trajectories, translated to match endpoints) 0.51 Retrieval floor, not a generative result
Best continuous model (diffusion, flow matching, and similar) 0.86 Ceiling that every non-discrete architecture hit
Event-stream model alone, no selection 0.652 A single generative model, no access to recorded trajectories at inference time
+ per-item selection 0.568 Choosing one candidate per movement from several generated options
+ set-level selection (tuning headline) 0.504 Choosing a whole population of candidates at once against three tuning seeds
+ set-level selection, six fresh seeds 0.513 Same recipe, no retuning, on seeds never seen during development

The number to actually trust is the second-to-last row paired with the last one. Three seeds (42, 43, 44) were used while building and tuning the selection recipe, and on those three it averages 0.504. Six more seeds (45 through 50) then ran the identical, frozen recipe with no further tuning, and those average 0.513, with a 95 percent confidence interval of [0.506, 0.520]. That interval sits just above chance rather than on it, so the summary this project uses is: fresh out-of-sample tests average 0.513, near chance, across three detector families (Random Forest, gradient-boosted trees, and a raw-signal neural network), with a worst case around 0.53.

That worst case matters for a reason worth spelling out. The selection process was tuned against the Random Forest detector, so a skeptic is right to ask whether it just learned to fool that one model rather than to move closer to human. The strongest check against that is the raw-signal neural network, which never took part in tuning at all: it reads 0.508 on the same six seeds, in line with the Random Forest number rather than exposing daylight underneath it. A gradient-boosted-tree detector, also uninvolved in tuning, reads a bit higher at 0.523 (worst single seed 0.536), which is the number a skeptic would reach for first, and it is disclosed here rather than left for someone else to find.

None of this should be read as "undetectable." It means near chance against the detectors this project actually built and tested, on the data distribution the model was trained on. Two external datasets (AdSERP and M4D, neither used in training) separate from the synthetics at 0.92 to 0.95, which sounds damning until you notice that those same external sets separate from this project's own internal humans by almost exactly the same margin. The two datasets were captured with different hardware and software than the training data, and that instrumentation gap shows up as separability whether the trajectories are real or synthetic. Read plainly, the synthetics sit exactly where the internal humans sit relative to any outside dataset tried so far. It is a tie, not a clean pass, and the project has no data yet that separates "different population" from "not human enough" for certain.

AUC by architecture family

Feature distribution comparison

The harder question: one trajectory, no selection

Everything above allows the pipeline to generate several candidate paths and keep one. Work since July 2026 asks a stricter question: what can the model do when it returns exactly one path per request, chosen before it is scored, and that path is required to start and end on the requested whole pixel? That is the product shape anything real would need, and selection is not available in it.

Two things make this harder than the 0.652 row above. There is no second chance at a bad draw. And forcing the path to land on the target pixel is itself expensive, because moving the points to hit the target damages the movement.

Step AUC
Model alone, no correction 0.654 Lands on the requested pixel 0.4% of the time, so it is not a usable product
+ the endpoint correction in service since July 20 0.721 Lands 100% of the time. Enforcing arrival costs 0.067
+ a corrected endpoint correction, July 26 0.699 Lands 100% of the time

The July 26 change is worth explaining because the mechanism was a surprise. The old correction spread the landing error thinly across every point in the path and rounded each one to the nearest pixel. Rounding a smooth ramp produces a staircase, and the steps of that staircase land in the middle of straight runs, so the correction was inserting sharp turns into paths the model had drawn straight. Measured per speed band, the model's own turning matches the human closely and the correction is what pushes it high. The replacement spends the landing error as whole-pixel nudges on the longest steps of the path, where one pixel bends the least angle, and leaves every other step exactly as the model drew it.

These numbers come from a different scorer and a different human reference set than the table above, so they are not directly comparable to the 0.652 row. They are internally consistent with each other.

Since August 2026 this line runs on a different model, an autoregressive event model (models/event_ar.py) that emits one token at a time rather than filling in a masked stream, and that needs no endpoint correction. Its first version read 0.6215. The number to read that against is not 0.50, and this turned out to matter more than expected. Feeding real recorded human trajectories, taken from the training corpus rather than from the evaluation set, through the identical encoding and decoding scores 0.533 against the same reference. In other words a perfect model of this training corpus would still read about 0.53, because the people in the training data and the people in the reference set are not the same people and were not recorded on the same hardware. So the quantity actually left to close is roughly 0.09, not 0.12.

Two findings from that work are worth stating here because they are general rather than specific to this project.

The representation is not the bottleneck. Round-tripping real human movement through the discrete event tokens improves its score, from 0.5681 to 0.5330, rather than costing anything. Whatever the model is getting wrong, it is not a limitation of what the tokens can express.

Every individual decision the model makes is correct; the error is made entirely by running it forward on its own output. Fed real recorded human history and asked what it would do next, all three of the model's prediction heads reproduce the true answer to three or four decimal places, in every situation tested. Fed its own output, the same model produces sub-millisecond pauses 1.68 times too often, and that propagates into a small, consistent, mutually reinforcing distortion of all eighteen measurements the detector reads. This is compounding error, or exposure bias, and it is a known failure mode of models that generate a step at a time. It means no amount of further training on recorded data fixes it, because on recorded data there is nothing to fix.

Training the model on its own output helps, and it is the remedy that diagnosis points to. If the error is only made when the model runs forward on itself, then the training signal has to be built from what the model itself generates. Since August 2026 that line works: the model is trained against the distance between the whole distribution of the eighteen measurements it generates and the human one, with the gradient pushed back through the sampling process. On repeat samples it moves the detector from 0.6301 to 0.5990, about five times the measurement noise. Six of the eighteen measurements are deliberately kept out of the training signal as a check, and they improve slightly faster than the twelve that are in it, so the model is not simply learning to flatter the score it is shown. For scale, real recorded trajectories from the training corpus read 0.5455 through the same measurement, so roughly a third of the reachable distance is now closed and 0.054 remains.

The current headline, and what generate.py serves, is 0.5622 (standard error 0.0035 over ten seeds of 2000 requests each, one trajectory per request, no candidates and no selection, scored on the raw path as generate.py decodes it). The model is the autoregressive one above with one architectural change, a direction head that also reads the speed the model has just chosen (training/event_ar_hm_mlp.pt, the only change to the network that has ever moved this detector, 0.6340 to 0.6145 over five seeds). It is sampled at temperatures 0.95 for speed, 0.90 for direction and 1.00 for timing, the best point of a sweep, with four additions to the serving recipe, each tested against a pre-registered threshold before being adopted. The commanded duration of each request is drawn from real human movements of about the same length instead of from a fitted curve. The first event of each path comes from a small separate model trained only on how human movements begin, and that model's pause and direction heads carry a small two-layer network instead of a single linear layer, so the opening pause can depend jointly on the opening step, its direction and the geometry (training/firsthead_q2.pt). Those three were worth 0.010 and 0.011 on paired tests, and the served first event now reads at the level of a real human first event looked up by nearest neighbour. The fourth addition is in the decoder, not the model: every motion step is rounded to whole pixels before it is added to the path, the way a mouse reports movement. The research decoder behind the earlier records rounded only steps shorter than 2.5 px and let longer steps accumulate fractional positions that were rounded once at the end. Rounding every step moved all twenty two seeds tested, 0.0071 closer to human on average (standard error 0.0006) on the same token streams, and it is the reason the headline is quoted on the served decoder rather than the research one (the same token streams read 0.5686 through the old decoder, standard error 0.0015; research/w4_snapdecode.json holds both). The gain is specific to the model's token streams: a human's own recorded tokens read the same through either decoder, so what the rounding removes is something in how the model's steps accumulate, and the rest of the gap is in the token sequences themselves. Under the same detector, real recorded human movement from the training corpus reads about 0.5455 when built one way and 0.02 to 0.04 higher when built another (the detector's direction-change count is sensitive to how the path is assembled), so that floor is a rough guide and the paired comparisons above are the numbers to trust. Every number in this paragraph was measured on raw model output, which misses the target by a median of about 3 pixels. Making a served path end on its target pixel, by whole-pixel changes on its longest steps, costs 0.006 on average over the ten seeds (0.5622 raw against 0.5658 landed, standard error 0.0030); rotating and scaling the path instead, which is what a first version of this tool did, put every point off the pixel grid and cost 0.12. verify_serve.py prints the landing cost next to the raw score so it is never hidden inside the headline.

Two cautions on that number, since both cost real time to find. Matching each measurement's average and spread separately, rather than the joint distribution, is worth almost nothing: a full run of it returned 0.008, inside the noise. And the first version of the result above read three times larger, because each evaluation was a single sample and the two that framed the headline happened to fall at opposite extremes. Anything measured by generating fresh trajectories needs at least two independent draws on each side before it means anything.

The full account is in EXPERIMENTS.md; the current state and the full measurement chain are in HANDOFF.md.

How it works

Real mouse movement is not purely continuous. At 125 Hz sampling, 6.14 percent of all recorded samples are exact zero-displacement stalls: the cursor sits perfectly still for a frame or more before moving again, usually right at a direction change or a deceleration. Those stalls carry essentially all of the curvature signal a detector can key on.

Every continuous generative approach this project tried, diffusion, flow matching, recurrent networks, can get arbitrarily close to zero displacement but cannot produce an exact zero, and each one plateaued somewhere between 0.86 and 1.0 AUC as a result. The fix was to stop treating position as continuous. Each trajectory is instead encoded as a stream of discrete events (a speed bin, a heading-change bin, and a gap in time), with the stall represented directly as its own zero-speed token rather than approximated by a small nonzero one. The MIME model, a masked-token model in the MaskGIT and SoundStorm style (the same family used for parallel audio and image generation), is trained on 4.16 million of these event streams and gets most of the way there on its own. This masking is where the name comes from: during training, pieces of a real movement's event stream are hidden and the model learns to fill them back in; at generation time it starts from a fully masked stream and fills in everything.

Two other things mattered almost as much as the architecture. Positions have to snap to the integer pixel grid the recording hardware actually writes to; leaving slow movement off that grid alone cost about 0.05 AUC. And once the model's own kinematics were good enough, the remaining gap turned out to be a selection problem rather than a generation problem: given several candidate paths for the same start and end point, an adversarial judge that scores the whole chosen set at once, rather than one candidate in isolation, closes most of what selecting one-at-a-time leaves behind.

Generated trajectories

A reinforcement-learning pilot, run July 10 and 11, tried folding that judge's signal directly into the model's weights using GRPO instead of a separate selection step. It moved the base model's own score from about 0.66 to a plateau around 0.638 and stopped improving there. More decisively, pools generated by the RL model made the full selection pipeline worse, not better, on all three tuning seeds (0.518 against the 0.504 baseline): RL training narrows how varied the model's candidates are, and the whole value of set-level selection comes from having a diverse pool to choose from. The direction is parked, not ruled out. Details and the full go/no-go analysis are in RL_PILOT.md and the July 10-11 entries of EXPERIMENTS.md.

For the complete account, including why every continuous architecture family failed and the full derivation of the selection method, see METHODOLOGY.md.

Reproduce the current results

Verify the served model, no GPU. python verify_serve.py --seed 20 regenerates the recorded seed 20 run of the current headline (2000 requests) through the code generate.py uses and scores it with research/autoloop/scoring.py, a random forest out-of-bag AUC against the human reference in data/human_val_features_grpo.npy. The record for that seed is 0.5653 raw and 0.5697 landed (research/w4_snapdecode.json; seed 21 is 0.5522 and 0.5506). It prints three numbers from one set of sampled token streams: the served paths as generate.py decodes them, the same rows after landing, and the same tokens decoded the way the earlier record was made (steps under 2.5 px rounded, the record's timestamp arithmetic), checked against that record in research/w4_q2serve_s20.json. On the machine the records were made on, all three return their logged values to four decimals, so the token streams are the record's. On other hardware the random draws differ, so the check passes within 0.03 rather than to the fourth decimal (a CPU only run of seed 20 on the recording machine read 0.024 under its record, seed 21 read 0.007 over). The registered research run itself is research/w4_q2serve.py; it also regenerates the earlier 0.5795 recipe on the same requests and matched its record (research/w4_mserve_s*.json) to four decimals on all ten seeds before the new number was read.

Verify the selection headline in minutes, no GPU. setup_data.py downloads the cached candidate pools and the winning picks for all three tuning seeds. Replaying them through the evaluator reproduces the confirmed numbers (0.5095 / 0.5030 / 0.4993, mean 0.504; exact on the original platform, within 0.001 across OS and BLAS differences) without loading the model:

EVENT_POOL_LOAD=pool_s42_k16.npz \
EVENT_POOL_PICKS=pool_s42_k16_picks_trust33_f20d85_r30_rf.npy \
python evaluate.py --experiment experiments.event_stream_polar --seed 42 --no-raw-nn

Repeat with s43/--seed 43 and s44/--seed 44 for the other two seeds, or run python verify_headline.py to do all three and check them against the published values in one command. The same check runs in CI on every push (the verify badge at the top of this page). The human class in this replay is the held-out evaluation sample no part of selection ever saw; the pool files contain only model-generated trajectories, so nothing here can leak the answer. Drop --no-raw-nn to also run the raw-sequence neural detector (slower; needs the training data split).

Rebuild the selection results from scratch. All of the selection-era numbers come from one checkpoint, event_polar_4m_fc_v2.pt (downloaded to training/ by setup_data.py), run through experiments/event_stream_polar.py with different environment variables controlling the sampler and the selection layer. The exact locked recipe, and every knob that was tried and rejected along the way, is logged in EXPERIMENTS.md; the commands below are the short version.

Pure model, no selection (AUC ~0.652):

python evaluate.py --experiment experiments.event_stream_polar

with environment variables EVENT_CKPT=event_polar_4m_fc_v2.pt EVENT_ORDER=gumbel EVENT_CHOICE_TEMP=10 EVENT_SNAP=2.5 EVENT_DUR_STD=1.0 DUR_EMPIRICAL=1.

+ SIR selection, the multi-seed-confirmed best with per-item selection (AUC ~0.568): add EVENT_SIR=16 EVENT_SIR_TEMP=0.7 EVENT_SIR_DUR_DIVERSE=1 to the same command. This draws 16 candidate trajectories per movement and keeps one via a tempered lottery on a GBM judge's log-odds, the judge fit against a human reference set disjoint from the evaluation sample.

+ set-level reselection, the headline result (AUC ~0.504, three-seed confirmed): this one is a two-step, mostly-offline process rather than a single command.

  1. Cache every candidate from the SIR pool instead of committing to one (scripts/run_poolgen.sh does this for a list of seeds, using EVENT_POOL_SAVE on top of the SIR recipe above).
  2. Run trust33.py --pool pool_s<seed>_k16.npz to walk the set-level reselection offline against the cached pool. It fits a 33-feature RF judge (the 18 kinematic features plus 15 raw-signal summaries) between a human reference half and the currently selected set, moves only the top fraction of picks toward the judge's preference each round with a decaying step size, and repeats for about 30 rounds. The script reports a proxy AUC on a held-out reference half; the final, reported number replays the winning selection through evaluate.py itself (via EVENT_POOL_LOAD / EVENT_POOL_PICKS), where the human class is the untouched evaluation sample no part of selection has seen. scripts/verify_b.sh runs this for seeds 42/43/44 with the full detector suite.

Hardware

Developed on an RTX 4070 (12GB VRAM). A single consumer GPU is sufficient for all experiments. Generating trajectories, verify_serve.py, corpus replay, corpus rotate and verify_headline.py all run CPU-only, including on Apple silicon.

Repository structure

MIME-mouse/
├── generate.py                       # Start point + end point in, human-like trajectory out (the main entry point)
├── evaluate.py                       # Adversarial evaluator (RF OOB AUC, GBM CV, raw-NN)
├── features.py                       # 18 kinematic feature extractors
├── event_codec.py                    # Event-stream encode/decode
├── external_detectors.py             # GBM, extra-trees, MLP, logistic, histogram-GBM detectors
├── detector_raw.py                   # Raw-signal (non-summary-feature) detectors
├── selection_lab.py                  # Offline set-level selection over a cached candidate pool
├── trust33.py                        # 33-feature judge, set-level reselection loop (the 0.504/0.513 recipe)
├── tune_trust.py                     # Selection-recipe search
├── regenerate_human_features.py      # Rebuild the human reference feature cache
├── setup_data.py                     # Downloads checkpoints, eval data, and the reproduce bundle
├── verify_serve.py                   # One-command check that generate.py reproduces the 0.5622 record
├── verify_headline.py                # One-command check of the 0.504 headline against published values
├── generate_figures.py               # Regenerate the figures embedded in this page
├── test_*.py                         # Unit tests
├── scripts/                          # Shell runners: pool generation, overnight sweeps, verify_b.sh, run_oos.sh
├── experiments/
│   ├── corpus_replay.py              # kNN corpus replay (AUC 0.51)
│   ├── corpus_rotate.py              # Rotation + scale replay (AUC 0.686)
│   ├── event_stream_polar.py         # Event-stream polar model + sampler + SIR selection (AUC 0.652 / 0.568)
│   ├── event_replay_polar.py         # Event representation round-trip gate (AUC 0.507)
│   ├── zimt_magcorr.py               # ZIMT + magnitude correction, historical best continuous model (AUC 0.864)
│   ├── ddpm_arclen.py                # DDPM diffusion (AUC 0.862)
│   ├── vqvae_ar_transformer.py       # VQ-VAE + Transformer (AUC 0.890)
│   ├── cfm_unet.py                   # Conditional Flow Matching (AUC 0.919)
│   └── ...                           # 20+ additional experiment variants
├── models/
│   ├── event_stream_polar.py         # Masked-token event-stream architecture
│   ├── event_ar.py                   # Autoregressive event model, the current single-trajectory line (served by generate.py)
│   ├── firsthead.py                  # First-event model that seeds each served trajectory
│   ├── zimt.py                       # ZIMT architecture (historical)
│   ├── temporal_unet.py              # 1D U-Net for diffusion / flow matching (historical)
│   ├── vqvae.py                      # Vector-quantized variational autoencoder (historical)
│   └── trajectory_transformer.py
├── training/                         # Training scripts, including the RL trainer (train_events_polar_grpo.py)
├── notebooks/                        # Exploratory notebooks
├── figures/                          # README figures and the script that regenerates them
├── data/                             # Downloaded checkpoints and evaluation data (gitignored)
├── external_validation/              # AdSERP and M4D cross-checks, out-of-sample seed statistics
├── research/                         # Archived one-off scripts: autoresearch waves, diagnostics, sweeps (see research/README.md)
│   └── autoloop/                     # The scoring contract used by the single-trajectory line, plus the run ledger and leaderboard
├── METHODOLOGY.md                    # Evaluation framework and research findings
├── EXPERIMENTS.md                    # Full log of 200+ experiments
├── RL_PILOT.md                       # Design doc for the parked GRPO reinforcement-learning pilot
├── CITATION.cff                      # How to cite this work
└── LICENSE                           # Apache-2.0

Datasets

All trajectory data comes from public mouse dynamics datasets. Raw data is not redistributed; it is downloaded from original sources during training data preparation.

Dataset Source License Use
Balabit Mouse Dynamics Challenge github.com/balabit/Mouse-Dynamics-Challenge None declared; citation requested Primary corpus
SapiMouse ms.sapientia.ro/~manyi/sapimouse None declared; citation requested Additional trajectory data
DFL ms.sapientia.ro/~manyi/DFL None declared; citation requested Additional trajectory data
Chaoshen figshare CC BY 4.0 Additional trajectory data
Bogazici Mendeley Data CC BY 4.0 Additional trajectory data

Model weights are trained on these publicly available datasets (4.16M total trajectories). Raw trajectory data is not included in this repository.

Two further datasets, AdSERP and M4D, were located later in the project and used only as an external validation check (never for training); see Results above and external_validation/ for the full comparison.

Dataset credits, as their authors request them:

  • Fülöp, Á., Kovács, L., Kurics, T., Windhager-Pokol, E. (2016). Balabit Mouse Dynamics Challenge data set.
  • Antal, M., Fejér, N., Buza, K. (2021). SapiMouse: Mouse dynamics-based user authentication using deep feature learning. IEEE SACI 2021.
  • Antal, M., Dénes-Fazakas, L. (2019). User verification based on mouse dynamics: a comparison of public data sets. IEEE SACI 2019.
  • Shen, C. (2017). Mouse behavior data for continuous authentication. figshare, CC BY 4.0.
  • Yıldırım, M., Kılıç, A. A., Anarım, E. (2021). Boğaziçi University mouse dynamics dataset. Mendeley Data, v2, CC BY 4.0.

Open directions

The result lives entirely at inference time, in the selection step, not in the model's weights. Every attempt to fold the selection judgment into the model directly failed: plain imitation, anchored imitation, three adversarial fine-tunes, and a GRPO reinforcement-learning pilot (parked July 11, see How it works above and RL_PILOT.md) all left the frozen model plus a separate selection step as the only approach that actually works. Four directions remain open:

  • Reinforcement learning, retried differently. The GRPO pilot plateaued around 0.638 and lost to the base model once its pools went through the full selection pipeline, because RL narrowed the model's candidate diversity and selection depends on that diversity. A reward built from a panel of several detector families at once, or using an RL-trained model purely as a stronger starting point for selection rather than a replacement for it, are both untried.

  • Training against the model's own output. This has now partly worked and is the most concrete of the four. Because every per-step decision is already correct on recorded data and only free-running composition is wrong (see the single-trajectory section above), the objective has to see trajectories the model actually generated rather than trajectories a human generated. Matching the whole generated distribution of the eighteen measurements against the human one, with the gradient pushed back through sampling, moves the detector from 0.6301 to 0.5990. Two earlier attempts in this family failed and are worth knowing about: the GRPO pilot collapsed the model's variety, and a learned adversarial critic ran away from the generator faster than the generator could follow. A fixed distributional statistic avoids both. What has not worked is the cheaper version of the same idea, matching each measurement's average and spread one at a time, which returns nothing measurable. Repairing all eighteen marginals perfectly is worth about 0.030 and the trained model has already reached that point, so the room that remains is in the relationships between measurements rather than in any measurement on its own.

  • A different backbone. The masked-token design fixed the exact-stall problem but may be exactly what resists absorbing the judge's signal. Swapping it for a plain autoregressive event model was tried and helped, which is the current single-trajectory line, but it inherited the compounding-error problem that any step-at-a-time generator has. An architecture with exact zeros and a continuous movement latent, or one that commits to a whole trajectory at once rather than a step at a time, might generate closer to human before any selection is applied.

  • Cleaner external data. AdSERP and M4D gave the first outside check this project has had, but the instrumentation gap between datasets is as large as the gap between synthetic and human, so it doesn't yet resolve the question on its own. What is missing is external data collected on matching hardware and software, or a way to separate that instrumentation shift from actual generator error. See METHODOLOGY.md section 7.11 for the full write-up.

Related work

  • Plamondon (1995): Kinematic theory of rapid human movements. The sigma-lognormal model decomposes movements into neuromuscular primitives.
  • Fitts (1954): Movement time as a function of distance and target width. The foundational speed-accuracy tradeoff.
  • VALL-E (Wang et al., 2023): Discrete neural codec tokens for speech synthesis. Demonstrates that discretization captures fine-grained temporal structure.
  • SoundStorm (Borsos et al., 2023): Parallel, masked-token generation of audio tokens. The direct architectural ancestor of the event-stream model's sampler.
  • T2M-GPT (Zhang et al., 2023): Discrete tokens for human body motion generation. An early architectural analogue explored in this project's VQ-VAE experiments.
  • CANDI (2025): Hybrid discrete-continuous diffusion. Explored as an intermediate step before the fully discrete event-stream approach.

See METHODOLOGY.md for detailed discussion and EXPERIMENTS.md for the full log of 200+ experiments.

Citing this work

If you use the code, the trained model, or build on the methodology or findings, please cite this repository. GitHub's "Cite this repository" button (from CITATION.cff) gives BibTeX and APA forms.

License

Apache-2.0. See LICENSE and NOTICE. Reuse of the code, trained weights, or documentation requires keeping the attribution notice.

About

MIME (Mouse Imitation via Masked Events): a generative model of human mouse movement. Near chance against adversarial detectors, essentially indistinguishable from human movement to the detectors tested (0.504 tuning / 0.513 out-of-sample). Full methodology and 200+ logged experiments.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages