Config reference#
One YAML file per project. Relative paths resolve against the file’s own
folder. base: other.yaml deep-merges this file over another. Every stage
is a method on eto.spot.Project; overrides use the same dotted spelling
the YAML has, passed to Project(...):
import ethograph as eto
project = eto.spot.Project("project.yaml", "clip.context_s=4", "train.epochs=12")
Important
An unknown key is an error, in the file and in an override alike — a typo
must not silently become a default. So is a section this pipeline retired:
graph:, fuse: and teacher.features are refused by name with the
replacement, and a columns: key under features: (the segmentation
pipeline’s section shape) with the difference.
Important
Every temporal setting is a duration. Seconds and milliseconds in the file; the frame counts the vendored trainer wants are derived from each video’s own rate at run time, so a config moved between a 200 fps rig and a 60 fps one keeps meaning the same thing. See Every temporal setting is a duration.
Top level#
Key |
Default |
Meaning |
|---|---|---|
|
the config’s folder |
Project directory: |
|
required |
List of sessions — see below. |
|
|
Where decoded frames go, or a folder another project decoded. One folder whatever the crop: each trial’s |
|
|
Stamped into every predicted row’s |
|
|
The pose side, optional — see below. |
sessions#
Key |
Default |
Meaning |
|---|---|---|
|
required |
The session file ( |
|
the file’s stem |
The session id in every output ( |
|
|
The curated labels TSV — where the point events to learn come from. |
|
the alignment’s |
The folder holding this session’s videos. |
The sessions layer is the segmentation pipeline’s, imported unchanged: a
session has no role; train.split gives every trial one.
trials#
Key |
Default |
Meaning |
|---|---|---|
|
|
Metadata column → allowed values. The one trial filter, applied in every stage. |
|
|
Keep only the first N trials per session — a smoke run. |
labels#
Key |
Default |
Meaning |
|---|---|---|
|
required |
The point-event label ids to spot. Their order is the order the events happen in (first contact before last contact): |
|
the alignment’s default |
Which camera’s video, one per project. |
|
|
|
|
|
Height the (cropped) frame is resized to; width follows the aspect ratio. E2E-Spot’s own [Hong et al., 2022]. |
clip#
The three durations everything else is derived from
(ClipConfig.resolve(fps)):
stride = round(resolution_ms / 1000 * fps)
clip_len = round(context_s * fps / stride)
dilate_len = round(positive_window_ms / 1000 * fps / stride)
Key |
Default |
Meaning |
|---|---|---|
|
|
Seconds of video the model sees at once. Below about 2 s the model misses events outright. |
|
|
Milliseconds one model frame spans — the grid a label can land on. Unset, the stride is the smallest that fits |
|
|
± this around the labelled event counts as positive during training. A duration, so dilation is not confounded with resolution when the latter changes. |
The frame budget — frames per loader batch the card holds — scales
with the card present: 200 was measured on a 10 GB card (MAX_FRAMES_PER_BATCH,
the one measured point), so a 24 GB card gets ~480 and an 8 GB one ~160
(frame_budget()); with no CUDA device the measured card is assumed, so a
config resolves the same on every machine without one. A spelled
resolution_ms whose clip would exceed the budget is refused, naming the
durations to change rather than the frame counts you never wrote; a clip
shorter than MIN_CLIP_LEN (8 model frames — below that the GRU has nothing
to integrate over) likewise. A trained run records its stride in
config.json and is read back from there, so a session predicted on another
card uses the run’s stride. A strided prediction is read back at the
centre of its bin, bin * k + (k - 1) / 2.
model#
Key |
Default |
Meaning |
|---|---|---|
|
|
Upstream’s |
|
|
Upstream’s |
|
|
MSAGSM only: how far each gated-shift branch reaches, in milliseconds, resolved against the strided clock. The paper’s |
|
|
MSAGSM only: channel groups of its spatial attention (the paper’s 2). |
train#
Key |
Default |
Meaning |
|---|---|---|
|
|
The run’s folder under |
|
|
Every run trains its full budget; the epoch used afterwards is the one the sweep ranks first on the run’s own validation predictions (fewest misses, then most within 4 frames, 20 ms at 200 fps), never the last and never |
|
|
Frames per epoch. What an epoch costs, whatever the trial count (~6.5 min per 250 k at 3.2 it/s on one RTX 3080). |
|
|
Linear warm-up then cosine, upstream’s schedule. |
|
|
Epochs of warm-up; must be fewer than |
|
|
First epoch that writes validation predictions ( |
|
|
Clips per optimiser step and gradient-accumulation steps: |
|
|
Resume-and-retry a training that crashed (a GPU hiccup), from its last checkpoint. |
|
|
|
|
|
With |
|
|
Share of training clips whose feature block is zeroed (modality dropout), so the pixels are trained to carry the event on their own too — which is what keeps |
train.split#
The segmentation pipeline’s: three ratios drawn by whole trial over every
trial of every session, and holdout_sessions for a cross-validation fold
(written per fold by project.cross_validate(), not by hand).
Key |
Default |
Meaning |
|---|---|---|
|
|
Fraction of trials the model learns from. |
|
|
Trials that choose the epoch. |
|
|
Trials |
|
|
Change it to re-draw the split. |
|
|
Sessions held out whole as |
The three fractions must sum to 1. Adding a session reshuffles the existing trials, same seed or not; a fold pins the split instead.
features#
The pose side, optional, and the whole of it: session variables in the
segmentation pipeline’s column spelling — feature → dim → values, every
dim pinned except the individual’s — on the pose’s own rate.
features:
velocity: {space: [x, y], keypoint: [stickTip, pellet]}
pellet_stickClosest_dist: {}
Every entry is a variable in the session file you built and can plot; there
is no graph, no adjacency, no learned geometry. Listed, the columns are
written once per trial to features/{video_id}.npz at materialise() and
serve two models: fed to the pixel model beside the frames
(train.features_as_input, z-scored on the training split under
features/block/), and read by the pose teacher (train_teacher()). Absent,
the model is E2E-Spot on pixels alone. See Pixels + Pose (extra features/ model distillation).
teacher#
The pose-only teacher (pose_model.PoseSpotter): the listed features → a
linear embedding → depth blocks of a parameter-free multi-scale temporal
shift → a bi-GRU → a K + 1 softmax. Trained by train_teacher(), minutes on
a GPU; distilled into the pixel model by distil().
Key |
Default |
Meaning |
|---|---|---|
|
|
Temporal shift scales of the blocks, in ms, resolved against the features’ own rate (UMEG-Net’s |
|
|
Width of every block. |
|
|
Stacked blocks. |
|
|
Channels shifted forward and backward, as a fraction of |
|
|
Bi-GRU width. |
|
|
Training budget; the epoch is chosen by the same sweep as a pixel run’s. |
|
|
AdamW. |
|
|
Clips per step. |
|
|
Foreground class weight in the per-frame cross-entropy — E2E-Spot’s own. |
|
|
The features and teacher sections are fingerprinted into the teacher’s
folder (teacher/{clip}_{fingerprint}) and the distilled student’s, so an
edited list lands beside the earlier result, never on top of it.
distil#
The two distillation steps, inside the vendored trainer: (2) the baseline’s
trunk + GRU learn to reproduce the teacher’s per-frame embedding on every
clip that has pose — no labels; (3) the CNN is frozen and the head learns the
labels. Both are ordinary runs under runs/{baseline}_distil_{fingerprint}/.
Key |
Default |
Meaning |
|---|---|---|
|
the one whose embeddings are under |
The teacher run under |
|
the newest trained run (refused if it is a distilled student) |
The baseline the student starts from; must agree about |
|
|
The embedding-matching step. |
|
|
The head step. |
|
|
As |
infer#
Key |
Default |
Meaning |
|---|---|---|
|
|
± this around the tallest peak counts as the same event when reading |
|
|
A trial whose predicted events are not in |
|
|
Currently unused: every predicted row’s |
|
|
Events below this confidence are logged as flagged, never dropped. |
|
|
Inference decodes the video straight into the model; each frame passes through JPEG in memory first, so the model sees what training saw (the export writes JPEGs). Off = an ablation. |
|
|
Which reading of a prediction’s curve is written as its |
|
|
α of the |
Inference never exports frames: inference() decodes each trial front to
back into the model (spot/stream.py), with a rolling one-window buffer.
Frames on disk are training’s alone.
What a run writes#
dataset/
train.json / val.json / test.json E2E-Spot's own index, one entry per trial with its events
class.txt, index.tsv class names; every materialised trial with its rate and frame count
{frames}/{video_id}/%06d.jpg decoded frames, plus export.json (size + crop)
features/ with features: listed
{video_id}.npz, features.json the listed columns per trial; their names in order
block/ the same, z-scored on the training split (stats.npz, block.json)
embeddings/ the teacher's per-clip embeddings (teacher.json names the teacher)
teacher/{clip}_{fingerprint}/ a teacher run: checkpoints, pred-val.*.recall.json.gz, loss.json, stats.npz
runs/{run}/
config.yaml, config.json the resolved config; upstream's own record (stride, clip_len, …)
checkpoint_{epoch}.pt, loss.json weights per epoch; train/val loss and val_mAP per epoch
pred-val.{epoch}.recall.json.gz validation predictions per epoch — what the epoch choice reads
pred-test.{epoch}.recall.json.gz test predictions of the chosen epoch, written by evaluate()
test_metrics.yaml per class: misses, spurious, error in ms, hit rate per tolerance
test_metrics_nofeatures.yaml the same with the feature block zeroed (features runs)
train.log, evaluate.log everything logged
runs/{baseline}_distil_{fingerprint}/stage2/, stage3/ the distilled student, two ordinary runs
runs/compare.tsv written by project.compare(): every scored run side by side
cross_validation/{session}/ one project per fold, its own dataset/ and runs/fold_{session}
Predictions land beside each session, never under root:
{session}/labels/predictions_spot_{run}_{timestamp}/ holds the labels TSV
({stem}_predictions.tsv) the GUI imports and onset_curves.npz, the curves frame-by-frame review
draws.