# Build your own classifier
ARCADE ranks discriminators under one shared pipeline. Adding a method means
writing a small Python class that implements the classifier contract,
registering it under a short name, and listing that name in YAML. Splits,
optional duration sweep, analytical hardware estimation, and the summary
report stay the same as for built-in methods.
This guide covers three patterns:
1. A minimal classical toy on filter features.
2. How to leverage custom classes from YAML and from Python imports.
3. A transformer-style custom class that consumes time-resolved sequences.
A checked-in toy lives under ``examples/custom_classifier/``.
```{mermaid}
flowchart TB
subgraph Your code
CLS["@register_classifier name
BaseClassifier subclass"]
end
subgraph ARCADE pipeline
DATA[Data stage] --> FEAT["Feature build
FEATURE_KIND"]
FEAT --> FIT[fit / predict]
FIT --> REP[Report row]
end
CLS --> FEAT
CLS --> FIT
```
## Contract you must satisfy
| Piece | Role |
|-------|------|
| ``@register_classifier("name")`` | Puts the class in the registry for YAML ``classifiers.run`` |
| ``FEATURE_KIND`` | Tells the classify stage which matrix to build |
| ``fit`` / ``predict`` | Train and infer |
| ``from_config`` | Optional; maps a YAML option block into constructor arguments |
| ``CATEGORY`` / ``DESCRIPTION`` | Shown by ``arcade list`` and ``arcade describe`` |
Supported feature kinds you will use most often are ``filter_features``,
``iq_trace``, and ``raw_trace_seq``. Wrong kinds usually surface as shape errors
rather than clear messages, so set ``FEATURE_KIND`` deliberately.
## Pattern A: nearest-mean on filter features
```python
# my_pkg/nearest_mean.py
from __future__ import annotations
import numpy as np
from arcade.classifiers.base import BaseClassifier, register_classifier
@register_classifier("nearest_mean")
class NearestMeanClassifier(BaseClassifier):
DESCRIPTION = "Nearest class-mean on filter features"
CATEGORY = "classical"
FEATURE_KIND = "filter_features"
def __init__(self) -> None:
self._means: dict[int, np.ndarray] = {}
def fit(self, train_features, train_labels, **kwargs):
X = np.asarray(train_features)
y = np.asarray(train_labels).astype(int)
self._means = {int(c): X[y == c].mean(axis=0) for c in np.unique(y)}
return self
def predict(self, features):
X = np.asarray(features)
labels = sorted(self._means)
means = np.stack([self._means[c] for c in labels], axis=0)
d = ((X[:, None, :] - means[None, :, :]) ** 2).sum(axis=-1)
return np.asarray(labels, dtype=int)[d.argmin(axis=1)]
@classmethod
def from_config(cls, config: dict) -> "NearestMeanClassifier":
return cls()
```
Ensure the module is importable before ``arcade run``. Install your package into
the same environment, or set ``PYTHONPATH`` so Python can find ``my_pkg``.
### Leverage the class from YAML
Registered name after the import side effect:
```yaml
classifiers:
run:
- threshold
- lda
- nearest_mean
filter_types: [MF, RMF]
```
Dotted path when you do not want to rely on prior registration. The loader
imports the class from the path string:
```yaml
classifiers:
run:
- threshold
- my_pkg.nearest_mean.NearestMeanClassifier
```
### Leverage the class from Python
```python
from arcade.classifiers.base import get_classifier
import my_pkg.nearest_mean # registration side effect
clf = get_classifier("nearest_mean").from_config({})
# or pass the class into a notebook cell that builds features yourself
```
You can also keep the class unregistered and pass the dotted path only in YAML.
That is useful when you are prototyping inside a private package and do not want
a global short name yet.
## Pattern B: transformer-style custom class
The built-in ``transformer`` method already consumes ``raw_trace_seq`` features
and trains a ``TraceTransformer`` through ``arcade.nn.builder.build_nn``. Use it
when the stock path is enough:
```yaml
classifiers:
run: [threshold, transformer]
transformer:
input_mode: raw
nn_config:
model:
type: transformer
d_model: 64
nhead: 4
num_layers: 2
dim_feedforward: 128
dropout: 0.1
training:
epochs: 50
batch_size: 512
lr: 1.0e-3
device: auto
```
When you need a custom variant, for example a different default input mode, an
extra preprocessing step, or a renamed experimental method, subclass the same
contract and keep ``FEATURE_KIND = "raw_trace_seq"`` so the pipeline builds
time-resolved sequences.
```python
# my_pkg/tiny_transformer.py
from __future__ import annotations
from typing import Any
import numpy as np
from arcade.classifiers.base import BaseClassifier, register_classifier
@register_classifier("tiny_transformer")
class TinyTransformerClassifier(BaseClassifier):
"""Minimal attention classifier on raw IQ sequences."""
DESCRIPTION = "Custom tiny transformer on raw_trace_seq features"
CATEGORY = "neural"
FEATURE_KIND = "raw_trace_seq"
REQUIRES_NN_CONFIG = True
def __init__(self, *, nn_config: dict | None = None) -> None:
self.nn_config = nn_config or {}
self._model = None
self._seq_len = 0
self._feature_dim = 0
def fit(
self,
train_features: np.ndarray,
train_labels: np.ndarray,
val_features: np.ndarray | None = None,
val_labels: np.ndarray | None = None,
**kwargs: Any,
) -> "TinyTransformerClassifier":
from arcade.nn.builder import build_nn
from arcade.training.transformer_training import train_trace_transformer
train_seq = np.asarray(train_features, dtype=np.float32)
if train_seq.ndim != 3:
raise ValueError(f"expected (n,T,F), got {train_seq.shape}")
self._seq_len = int(train_seq.shape[1])
self._feature_dim = int(train_seq.shape[2])
num_classes = int(np.asarray(train_labels).max()) + 1
cfg = dict(self.nn_config)
model_cfg = dict(cfg.get("model", cfg))
train_cfg = dict(cfg.get("training", {}))
model_cfg.setdefault("type", "transformer")
self._model = build_nn(
{"model": model_cfg},
seq_len=self._seq_len,
feature_dim=self._feature_dim,
output_dim=num_classes,
)
result = train_trace_transformer(
self._model,
train_seq,
np.asarray(train_labels).ravel(),
np.asarray(val_features, dtype=np.float32) if val_features is not None else None,
np.asarray(val_labels).ravel() if val_labels is not None else None,
epochs=int(train_cfg.get("epochs", 30)),
batch_size=int(train_cfg.get("batch_size", 256)),
lr=float(train_cfg.get("lr", 1e-3)),
device=str(train_cfg.get("device", "auto")),
)
self._model = result["model"]
return self
def predict(self, features: np.ndarray) -> np.ndarray:
import torch
if self._model is None:
raise RuntimeError("Call fit() before predict()")
seq = np.asarray(features, dtype=np.float32)
self._model.eval()
dev = next(self._model.parameters()).device
x = torch.tensor(seq, dtype=torch.float32, device=dev)
with torch.no_grad():
return self._model(x).argmax(dim=-1).cpu().numpy().astype(np.int64)
@classmethod
def from_config(cls, config: dict[str, Any]) -> "TinyTransformerClassifier":
nn_cfg = config.get("nn_config")
if nn_cfg is None and config.get("config_file"):
import yaml
with open(config["config_file"]) as f:
nn_cfg = yaml.safe_load(f)
return cls(nn_config=nn_cfg)
```
```{mermaid}
flowchart LR
RAW[Prepared traces] --> SEQ["raw_trace_seq
(n shots × T × F)"]
SEQ --> TT[tiny_transformer fit]
TT --> MET[Metrics + report row]
TT --> HW[Analytical hardware]
```
### YAML that runs the custom transformer beside a baseline
```yaml
classifiers:
run:
- threshold
- tiny_transformer
tiny_transformer:
nn_config:
model:
type: transformer
d_model: 64
nhead: 4
num_layers: 2
dim_feedforward: 128
training:
epochs: 30
batch_size: 256
lr: 1.0e-3
device: auto
visualization:
output_dir: output/custom_transformer_bakeoff
```
Install ``torch`` first:
```bash
pip install -e ".[torch]"
export PYTHONPATH=path/to/my_pkg_parent:$PYTHONPATH
arcade run path/to/config.yaml
```
The custom method appears in the same summary report as ``threshold``. That is
the point of the registry: your experiment gets the shared recipe for free.
## What you get for free
The same train, validation, and test cuts as every other method in the run.
Feature matrices selected by ``FEATURE_KIND``. Optional duration sweep when
enabled. Analytical FPGA estimates when hardware estimation is enabled. A row
in the summary report next to zoo methods.
## Pitfalls
FEATURE_KIND to what fit expects. Sequence models need
raw_trace_seq; filter models need filter_features.torch extra and usually an nn_config or
config_file.arcade run, or use a dotted class path in YAML.