research(phase3b): benchmark 1D CNN on RAW pulses — data-starved, benched
Trains a PyTorch 1D CNN on encoded RAW pulse arrays as the Phase 3B leg of the designed ensemble, scored on the SAME group-disjoint held-out test set as the heuristic and statistical models (GroupShuffleSplit by device sub-folder, no near-duplicate leakage). RESULT — CNN is data-starved and loses decisively: CNN balanced 0.338, top-1 0.795 (garage-inflated) statistical balanced 0.625, top-1 0.868 heuristic top-1 0.300 Only 489 train samples with severe class imbalance (Garage 565, Security 2, Weather 12). A blend sweep confirmed every non-zero CNN weight degrades the statistical model (0.625 -> 0.613 at 15% CNN, worse beyond), so the CNN is NOT wired into decode(). Production ensemble stays heuristic + statistical, both already live. Committing the trainer + shared pulse_encoder (trainer/inference parity) + metrics.json to document the reproducible negative result. The benched .pt/.onnx binaries are intentionally NOT committed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Shared RAW-pulse encoder for the 1D-CNN (Phase 3B).
|
||||
|
||||
A single source of truth for turning a Flipper RAW pulse array into the fixed
|
||||
normalized tensor the CNN consumes — imported by BOTH the trainer
|
||||
(``scripts/train_cnn_classifier.py``) and the inference wrapper
|
||||
(``src/matcher/cnn_classifier.py``) so the two can never drift.
|
||||
|
||||
Encoding:
|
||||
* take the first ``length`` signed durations (µs; +high / -low),
|
||||
* scale by the sequence's *median absolute* duration — a robust, TE-like
|
||||
unit so a short pulse is ~±1 and a long pulse ~±2-3 (the ratios that carry
|
||||
the protocol identity), while absolute gain / capture level is normalised
|
||||
out. Using the median instead of the max is deliberate: a single huge
|
||||
inter-packet gap (~10 000 µs) would otherwise squash every informative
|
||||
short pulse toward zero and blind the CNN.
|
||||
* clip to ±``CLIP`` and rescale to [-1, 1] so outsized gaps saturate the
|
||||
ceiling instead of dominating the dynamic range,
|
||||
* right-pad with zeros to ``length``.
|
||||
|
||||
Returns a float32 array of shape ``(length,)`` or ``None`` when the pulse train
|
||||
is empty or degenerate (all-zero).
|
||||
"""
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
PULSE_LEN = 512
|
||||
CLIP = 8.0 # in median-pulse units; gaps beyond this saturate to ±1
|
||||
|
||||
|
||||
def encode_pulses(pulses: List[int], length: int = PULSE_LEN) -> Optional[np.ndarray]:
|
||||
if pulses is None or len(pulses) == 0:
|
||||
return None
|
||||
arr = np.asarray(pulses[:length], dtype=np.float32)
|
||||
nz = np.abs(arr[arr != 0])
|
||||
if nz.size == 0:
|
||||
return None
|
||||
scale = float(np.median(nz))
|
||||
if scale <= 0.0:
|
||||
scale = float(np.max(np.abs(arr)))
|
||||
if scale <= 0.0:
|
||||
return None
|
||||
arr = np.clip(arr / scale, -CLIP, CLIP) / CLIP
|
||||
if arr.size < length:
|
||||
arr = np.pad(arr, (0, length - arr.size))
|
||||
return arr.astype(np.float32)
|
||||
Reference in New Issue
Block a user