@@ -344,20 +344,20 @@ across events:
344344
345345### 7.1 Quick side effects with ` tap(...) `
346346
347- FitStream includes a small helper for side effects: ` tap(fn) ` calls ` fn(event) ` and yields the event unchanged.
348- It’s perfect for lightweight logging or writing metrics to an external system.
347+ FitStream includes a small helper for side effects: ` tap(fn, every=... ) ` calls ` fn(event) ` every N events and yields
348+ the event unchanged. It’s perfect for lightweight logging or writing metrics to an external system.
349349
350350``` python
351- from fitstream import augment, epoch_stream, pipe, take, tap, validation_loss
351+ from fitstream import augment, epoch_stream, pipe, print_keys, take, tap, validation_loss
352352
353353events = pipe(
354354 epoch_stream((x_train, y_train), model, optimizer, loss_fn, batch_size = 512 , shuffle = True ),
355355 augment(validation_loss((x_val, y_val), loss_fn)),
356- tap(lambda event : print ( f " epoch= { event[ ' step ' ] :03d } val_loss= { event[ ' val_loss ' ] :.4f } " ) ),
356+ tap(print_keys( " train_loss " , " val_loss" ), every = 5 ),
357357)
358358
359359# Consume a few events to actually run it (streams are lazy).
360- list (take(5 )(events))
360+ list (take(15 )(events))
361361```
362362
363363### 7.2 Learning rate scheduling with ` tick(...) `
@@ -390,48 +390,49 @@ If your scheduler needs a metric (e.g. `ReduceLROnPlateau`), use `tap(...)` inst
390390
391391### 7.3 Example: exponential moving average (EMA)
392392
393- This stage adds a new key like ` val_loss_ema ` to each event.
393+ FitStream includes an ` ema(...) ` stage that adds a new key like ` val_loss_ema ` to each event.
394394
395395``` python
396- from collections.abc import Iterable
397- from typing import Any
398-
399- def ema (key : str , * , alpha : float = 0.2 , out_key : str | None = None ):
400- if not (0.0 < alpha <= 1.0 ):
401- raise ValueError (" alpha must be in (0, 1]." )
402- out_key = out_key or f " { key} _ema "
403-
404- def stage (events : Iterable[dict[str , Any]]):
405- value_ema: float | None = None
406- for event in events:
407- value = float (event[key])
408- value_ema = value if value_ema is None else (1.0 - alpha) * value_ema + alpha * value
409- yield event | {out_key: value_ema}
410-
411- return stage
396+ from fitstream import augment, ema, epoch_stream, pipe
397+
398+ # Coefficient form: m = decay * m + (1 - decay) * x
399+ events = pipe(
400+ epoch_stream(... ),
401+ augment(... ),
402+ ema(" val_loss" , decay = 0.9 ),
403+ )
404+
405+ # Half-life form (more intuitive tuning in "events until ~50% influence")
406+ events = pipe(
407+ epoch_stream(... ),
408+ augment(... ),
409+ ema(" val_loss" , half_life = 10 ),
410+ )
412411```
413412
414- ### 7.4 Example: print progress every N epochs
413+ ` ema(..., bias_correction=True) ` is the default (Adam-style correction). You can disable it:
415414
416415``` python
417- from collections.abc import Iterable
418- from typing import Any
419-
420- def print_every (n : int , * , keys : tuple[str , ... ] = (" train_loss" , " val_loss" )):
421- if n <= 0 :
422- raise ValueError (" n must be >= 1." )
423-
424- def stage (events : Iterable[dict[str , Any]]):
425- for event in events:
426- if int (event[" step" ]) % n == 0 :
427- parts = [f " epoch= { event[' step' ]:04d } " ]
428- for k in keys:
429- if k in event:
430- parts.append(f " { k} = { float (event[k]):.4f } " )
431- print (" " .join(parts))
432- yield event
433-
434- return stage
416+ from fitstream import augment, ema, epoch_stream, pipe
417+
418+ events = pipe(
419+ epoch_stream(... ),
420+ augment(... ),
421+ ema(" val_loss" , half_life = 10 , bias_correction = False ),
422+ )
423+ ```
424+
425+ ### 7.4 Combine smoothing + periodic logging
426+
427+ ``` python
428+ from fitstream import augment, ema, epoch_stream, pipe, print_keys, tap
429+
430+ events = pipe(
431+ epoch_stream(... ),
432+ augment(... ),
433+ ema(" val_loss" , half_life = 10 ),
434+ tap(print_keys(" train_loss" , " val_loss" , " val_loss_ema" ), every = 10 ),
435+ )
435436```
436437
437438## 8) The “zero → hero” pipeline (put it all together)
@@ -453,7 +454,18 @@ from pathlib import Path
453454import torch
454455from torch import nn
455456
456- from fitstream import augment, collect_jsonl, early_stop, epoch_stream, pipe, take, validation_loss
457+ from fitstream import (
458+ augment,
459+ collect_jsonl,
460+ early_stop,
461+ ema,
462+ epoch_stream,
463+ pipe,
464+ print_keys,
465+ take,
466+ tap,
467+ validation_loss,
468+ )
457469
458470RUNS_DIR = Path(" runs" )
459471RUNS_DIR .mkdir(exist_ok = True )
@@ -472,8 +484,8 @@ events = pipe(
472484 epoch_stream((x_train, y_train), model, optimizer, loss_fn, batch_size = 512 , shuffle = True ),
473485 augment(validation_loss((x_val, y_val), loss_fn)),
474486 augment(model_param_norm),
475- ema(" val_loss" , alpha = 0.2 ),
476- print_every( 10 , keys = (" train_loss" , " val_loss" , " val_loss_ema" , " param_l2" )),
487+ ema(" val_loss" , half_life = 10 ),
488+ tap(print_keys (" train_loss" , " val_loss" , " val_loss_ema" , " param_l2" ), every = 10 ),
477489 take(500 ),
478490 early_stop(key = " val_loss" , patience = 20 , mode = " min" , min_delta = 1e-4 ),
479491)
0 commit comments