chore: initialize git repo, add matplotlib dep, extend config
- Add .gitignore for Python/data/models - Add matplotlib>=3.8.0 for eval plots - Add PretrainConfig, FinetuneConfig, BalabitAdapterConfig, EvalConfig dataclasses
This commit is contained in:
121
tests/test_trainer.py
Normal file
121
tests/test_trainer.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""Tests for Flow Matching training pipeline."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from ai_mouse.trainer import load_and_prepare_data, train, _augment
|
||||
|
||||
|
||||
def _make_synthetic_trace(start, end, n_moves=30):
|
||||
"""Create a synthetic trace dict mimicking real JSONL format."""
|
||||
sx, sy = start
|
||||
ex, ey = end
|
||||
dist = math.hypot(ex - sx, ey - sy)
|
||||
angle = math.degrees(math.atan2(ey - sy, ex - sx))
|
||||
|
||||
events = []
|
||||
for i in range(n_moves):
|
||||
t_frac = i / (n_moves - 1)
|
||||
x = int(sx + (ex - sx) * t_frac + np.random.normal(0, 3))
|
||||
y = int(sy + (ey - sy) * t_frac + np.random.normal(0, 3))
|
||||
t = int(t_frac * 500 + np.random.normal(0, 5))
|
||||
events.append({"type": "move", "x": x, "y": y, "t": max(0, t)})
|
||||
|
||||
events.sort(key=lambda e: e["t"])
|
||||
events[0]["t"] = 0
|
||||
|
||||
last_t = events[-1]["t"]
|
||||
events.append({"type": "down", "x": ex, "y": ey, "t": last_t + 50})
|
||||
events.append({"type": "up", "x": ex, "y": ey, "t": last_t + 130})
|
||||
|
||||
return {
|
||||
"meta": {"start": [sx, sy], "end": [ex, ey], "dist": int(dist), "angle": round(angle, 1)},
|
||||
"events": events,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def synthetic_traces_file(tmp_path):
|
||||
"""Create a temp JSONL file with 25 synthetic traces."""
|
||||
traces_path = tmp_path / "traces.jsonl"
|
||||
rng = np.random.default_rng(42)
|
||||
lines = []
|
||||
for _ in range(25):
|
||||
sx, sy = int(rng.integers(50, 750)), int(rng.integers(50, 750))
|
||||
angle = rng.uniform(0, 2 * math.pi)
|
||||
dist = int(rng.integers(100, 500))
|
||||
ex = int(sx + dist * math.cos(angle))
|
||||
ey = int(sy + dist * math.sin(angle))
|
||||
ex = max(0, min(800, ex))
|
||||
ey = max(0, min(600, ey))
|
||||
trace = _make_synthetic_trace((sx, sy), (ex, ey))
|
||||
lines.append(json.dumps(trace))
|
||||
traces_path.write_text("\n".join(lines), encoding="utf-8")
|
||||
return traces_path
|
||||
|
||||
|
||||
class TestLoadAndPrepare:
|
||||
def test_returns_correct_shapes(self, synthetic_traces_file):
|
||||
seq, cond, click_durs = load_and_prepare_data(synthetic_traces_file, seq_len=64)
|
||||
assert seq.shape[1] == 64
|
||||
assert seq.shape[2] == 3
|
||||
assert cond.shape[1] == 3
|
||||
assert len(seq) > 0
|
||||
|
||||
def test_forward_starts_near_zero(self, synthetic_traces_file):
|
||||
seq, cond, _ = load_and_prepare_data(synthetic_traces_file, seq_len=64)
|
||||
assert abs(seq[:, 0, 0].mean()) < 0.15
|
||||
|
||||
def test_forward_ends_near_one(self, synthetic_traces_file):
|
||||
seq, cond, _ = load_and_prepare_data(synthetic_traces_file, seq_len=64)
|
||||
assert abs(seq[:, -1, 0].mean() - 1.0) < 0.15
|
||||
|
||||
|
||||
class TestAugment:
|
||||
def test_augmentation_multiplies_data(self, synthetic_traces_file):
|
||||
seq, cond, _ = load_and_prepare_data(synthetic_traces_file, seq_len=64)
|
||||
n_orig = len(seq)
|
||||
seq_aug, cond_aug = _augment(seq, cond)
|
||||
assert len(seq_aug) == n_orig * 6
|
||||
assert len(cond_aug) == n_orig * 6
|
||||
|
||||
|
||||
class TestTrain:
|
||||
def test_train_produces_model_files(self, synthetic_traces_file, tmp_path):
|
||||
output_dir = tmp_path / "models"
|
||||
train(
|
||||
data_path=synthetic_traces_file,
|
||||
output_dir=output_dir,
|
||||
epochs=3,
|
||||
batch_size=8,
|
||||
seq_len=64,
|
||||
)
|
||||
assert (output_dir / "flow_model.pt").exists()
|
||||
assert (output_dir / "click_dist.json").exists()
|
||||
assert (output_dir / "duration_dist.json").exists()
|
||||
assert (output_dir / "train_config.json").exists()
|
||||
|
||||
def test_train_loss_decreases(self, synthetic_traces_file, tmp_path):
|
||||
output_dir = tmp_path / "models"
|
||||
losses = []
|
||||
|
||||
def cb(msg):
|
||||
if "loss" in msg:
|
||||
losses.append(msg["loss"])
|
||||
|
||||
train(
|
||||
data_path=synthetic_traces_file,
|
||||
output_dir=output_dir,
|
||||
epochs=20,
|
||||
batch_size=8,
|
||||
seq_len=64,
|
||||
progress_callback=cb,
|
||||
)
|
||||
first_half = np.mean(losses[:10])
|
||||
second_half = np.mean(losses[10:])
|
||||
assert second_half < first_half
|
||||
Reference in New Issue
Block a user