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
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 |
|
|
Whose events these are, 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, at most |
|
the alignment’s default |
Which camera’s video the model reads, one per project. Only matters with several cameras: give the camera’s name as the alignment spells it ( |
|
|
|
|
|
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. |
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. |
|
|
|
|
|
Share of training clips whose feature block is zeroed (modality dropout). Off by default; set it (e.g. |
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
fed to the pixel model beside the frames (z-scored on the training split
under features/block/). Absent,
the model is E2E-Spot on pixels alone. See Pixels + Pose (extra features).
label_inputs#
Optional. The labels of other branches as input columns, appended to
features: so they ride into the GRU beside the pose — the segmentation
pipeline’s features.label_inputs, same
keys, same renderings (a state’s indicator, a point’s Laplacian bump per
width in point_sigmas_s). Two differences:
Key |
Default |
Meaning |
|---|---|---|
|
|
This config names no mapping of its own, so say where the branches are read from when it is not the default one. |
|
the first of |
With no pose listed there is no feature to take the clock from — name the session variable whose time coordinate the columns should be rendered on. |
labels:
classes: [7] # first contact, branch 1 of the mapping
features:
pellet_stickClosest_dist: {}
label_inputs:
branches: [0] # the earlier question's labels, branch 0
A branch holding any of labels.classes is refused: the model would
learn to copy its input. The columns are rendered for individual (the one
event stream this pipeline predicts) and a trial with none of these labels
renders zeros. With label_inputs set the run is a _features run, train
z-scores the block on the training split as for the pose, and
evaluate(zero_features=True) measures what the block — pose and old labels
together — contributes.
infer#
Key |
Default |
Meaning |
|---|---|---|
|
|
± this around the tallest peak counts as the same event when reading |
|
|
A class whose curve never rises above this in a trial is absent from it: no label is written. Deliberately low — a false positive is one keypress in review, a missed event is invisible, and a curve this low scores near 0 anyway. Raise it for a class that is often absent; |
|
|
How many events of one class a trial may hold. |
|
|
With several events per trial, two peaks closer than this are one event, the taller. Read only when |
|
|
A trial whose predicted events are not in |
|
|
Currently unused: every predicted row’s |
|
|
Events below this confidence are logged as flagged, never dropped: doubt is reviewed. Absence is |
|
|
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)
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/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.