Metrics on a stream: windows, predict then learn, model choice¶
6. Windows: only the recent past¶
A robot changes over time; the accuracy of last week says little about now. Rolling keeps a
metric over the last window samples, or the last window_s seconds.
from dense_armor.utility.metrics import Accuracy, Rolling
m = Rolling(Accuracy(), window_s=0.05)
for i in range(20):
m.update(1 if i < 17 else 0, 1, t=i * 0.01)
print(m.count, m.get())
At 100 Hz a window of 0.05 s holds 5 samples: the window keeps the samples with
$t_\text{now} - t < 0.05$. The last three predictions are wrong, so the accuracy over the
window is 2/5. The window uses the timestamps, not the sample count, so a dropped sample does
not stretch it. Any metric with revert can be rolled.
The ROC-AUC is the probability that a random positive gets a higher score than a random
negative. RollingAUC computes it exactly on the last window pairs.
import numpy as np
from dense_armor.utility.metrics import RollingAUC
rng = np.random.default_rng(0)
m = RollingAUC(window=200)
for _ in range(1000):
y = int(rng.random() < 0.3)
m.update(y, y + rng.normal(0, 1.0))
print(round(m.get(), 3))
$$\mathrm{AUC} = \frac{1}{n_+ n_-}\sum_{i \in +}\sum_{j \in -} \begin{cases} 1 & s_i > s_j \ \tfrac12 & s_i = s_j \ 0 & \text{otherwise} \end{cases}$$
Here positives score one unit higher than negatives with unit noise, so the long-run value is $\Phi(1/\sqrt2) = 0.760$; a window of 200 pairs gives 0.712.
7. Predict, then learn¶
progressive_val_score runs the protocol for you: for each sample the model predicts, the
metric scores, then the model learns. Labels often arrive late (a technician confirms a fault at
the end of the shift); with delay the model learns each label only when it arrives.
import numpy as np
from dense_armor.utility.evaluate import progressive_val_score
from dense_armor.utility.learn.online_classifiers import OnlineGaussianNB
from dense_armor.utility.metrics import Accuracy, Rolling
rng = np.random.default_rng(0)
qd = rng.normal(0, 1, 2000)
y = np.abs(qd) > np.where(np.arange(2000) < 1000, 0.5, 1.5)
s = [({"qd": a}, int(b)) for a, b in zip(qd, y)]
for d in (None, 300):
m, tr = progressive_val_score(s, OnlineGaussianNB(), Rolling(Accuracy(), window=200), delay=d, every=200)
print(d, [round(r, 2) for r in tr])
None [0.91, 0.94, 0.95, 0.95, 0.92, 0.54, 0.66, 0.76, 0.84, 0.84]
300 [0.0, 0.0, 0.0, 0.89, 0.95, 0.92, 0.71, 0.38, 0.59, 0.7]
The task is "is the joint moving fast?", and at sample 1000 the meaning of "fast" changes from
$|\dot q| > 0.5$ to $|\dot q| > 1.5$. With labels on time the rolling accuracy drops to 0.54 and
recovers to 0.84. With labels 300 samples late the drop comes later and goes deeper (0.38),
because the model keeps answering with the old rule until the new labels reach it. For delay=300 the first
three values are 0.0 because no label has arrived yet and nothing has been scored. every=200
returns the trace of the metric.
For metrics that need a probability, progressive_val_proba_score uses predict_proba_one.
import numpy as np
from dense_armor.utility.evaluate import progressive_val_proba_score
from dense_armor.utility.learn.online_classifiers import OnlineGaussianNB
from dense_armor.utility.metrics import LogLoss
rng = np.random.default_rng(0)
s = [({"qd": v}, int(abs(v) > 0.5)) for v in rng.normal(0, 1, 2000)]
m, tr = progressive_val_proba_score(s, OnlineGaussianNB(), LogLoss(), every=500)
print([round(v, 3) for v in tr])
The log-loss falls as the model sees more samples.
8. Choosing a model on the stream¶
best_of runs several models on the same stream with the same protocol and tells which one is
ahead.
import numpy as np
from dense_armor.utility.evaluate import best_of
from dense_armor.utility.learn.online_classifiers import OnlineGaussianNB, OnlineSoftmaxRegression
from dense_armor.utility.metrics import Accuracy
rng = np.random.default_rng(0)
s = [({"qd": v}, int(abs(v) > 0.5)) for v in rng.normal(0, 1, 2000)]
sel = best_of([OnlineGaussianNB(), OnlineSoftmaxRegression()], Accuracy, stream=s)
print([round(v, 3) for v in sel.scores()], sel.current_best())
"Fast in either direction" is not a straight cut on $\dot q$, so a linear softmax model cannot
learn it (0.621), while the Gaussian model, which learns a different spread per class, can
(0.941). current_best() returns the index 0 and best_model() the model itself. The second
argument is the metric class: each model gets its own fresh metric.