Skip to content

Commit ed6c22f

Browse files
authored
Merge pull request #27 from wli51/dev-auto-loss-log
Add auto loss logging
2 parents c81c4e2 + 13fc3f7 commit ed6c22f

14 files changed

Lines changed: 833 additions & 218 deletions

‎CHANGELOG.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@ All notable chagnes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
9+
---
10+
11+
## [0.4.5] - 2026-04-30
12+
13+
### Added
14+
15+
#### MLflow auto logging enhancements (`virtual_stain_flow/vsf_logging/`):
16+
17+
- The logger now records model architecture tags by capturing each model config's `class_path` and setting `model.<idx>.class_path` at train start.
18+
- The loss-group auto logging routine `_log_loss_groups_config_and_tags` logs loss item names and weights as MLflow tags and persists the full loss group configuration as a JSON config artifact.
19+
820
---
921

1022
## [0.4.4] - 2026-04-23

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
66

77
[project]
88
name = "virtual_stain_flow"
9-
version = "0.4.0"
9+
version = "0.4.5"
1010
description = "For developing virtual staining models"
1111
requires-python = ">=3.9"
1212
dependencies = [

‎src/virtual_stain_flow/engine/loss_group.py‎

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,14 @@
2222
"""
2323

2424
from dataclasses import dataclass
25-
from typing import Optional, Union, Tuple, Dict, Sequence, List
25+
from typing import Optional, Union, Tuple, Dict, Sequence, List, Any
2626

2727
import torch
2828

2929
from .loss_utils import BaseLoss, _get_loss_name, _scalar_from_ctx
3030
from .context import Context, ContextValue
3131
from .names import PREDS, TARGETS
32+
from .progress import Progress
3233

3334
Scalar = Union[int, float, bool]
3435

@@ -79,6 +80,7 @@ def __post_init__(self):
7980
def __call__(
8081
self,
8182
train: bool,
83+
progress: Optional[Progress] = None,
8284
context: Optional[Context] = None,
8385
**inputs: torch.Tensor
8486
) -> Tuple[torch.Tensor, torch.Tensor]:
@@ -88,6 +90,8 @@ def __call__(
8890
skipped during validation.
8991
9092
:param train: Whether the model is in training mode.
93+
:param progress: Optional Progress object containing scheduling state (epoch, step, etc.)
94+
for dynamic weight scheduling.
9195
:param context: Optional Context object containing tensors.
9296
:param inputs: Keyword arguments containing all necessary inputs for the
9397
loss computation.
@@ -117,6 +121,21 @@ def __call__(
117121

118122
return raw, raw * _scalar_from_ctx(self.weight, inputs)
119123

124+
def get_config(self) -> Dict[str, Any]:
125+
"""
126+
Get the configuration of the LossItem for logging or checkpointing.
127+
"""
128+
return {
129+
'module': self.module.__class__.__name__,
130+
'args': self.args,
131+
'key': self.key,
132+
'weight': self.weight,
133+
'enabled': self.enabled,
134+
'compute_at_val': self.compute_at_val,
135+
'device': str(self.device)
136+
}
137+
138+
120139
@dataclass
121140
class LossGroup:
122141
"""
@@ -137,13 +156,16 @@ def item_names(self) -> List[Optional[str]]:
137156
def __call__(
138157
self,
139158
train: bool,
159+
progress: Optional[Progress] = None,
140160
context: Optional[Context] = None,
141161
**inputs: torch.Tensor
142162
) -> Tuple[torch.Tensor, Dict[str, Scalar]]:
143163
"""
144164
Compute the total loss and individual loss values.
145165
146166
:param train: Whether the model is in training mode.
167+
:param progress: Optional Progress object containing scheduling state (epoch, step, etc.)
168+
for dynamic weight scheduling.
147169
:param context: Optional Context object containing tensors.
148170
:input inputs: Keyword arguments containing all necessary inputs for the
149171
loss computations.
@@ -156,8 +178,20 @@ def __call__(
156178
logs: Dict[str, float] = {}
157179

158180
for item in self.items:
159-
raw, weighted = item(train, context=context, **inputs)
181+
raw, weighted = item(
182+
train,
183+
progress=progress,
184+
context=context,
185+
**inputs
186+
)
160187
logs[item.key] = raw.item() # type: ignore
161188
total += weighted
162189

163190
return total, logs
191+
192+
def get_config(self) -> List[Dict[str, Any]]:
193+
"""
194+
Get the configuration of the LossGroup for logging or checkpointing.
195+
"""
196+
197+
return [item.get_config() for item in self.items]
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""
2+
progress.py
3+
4+
Progress tracking for loss weight scheduling.
5+
6+
Provides a centralized abstraction for training progress state that can be used
7+
by loss schedulers operating at different granularities (epoch, step, etc.).
8+
Designed to be minimal and extensible without overcomplicating the current API.
9+
"""
10+
11+
from dataclasses import dataclass
12+
13+
14+
@dataclass
15+
class Progress:
16+
"""
17+
Tracks training progress for loss weight scheduling.
18+
19+
Provides centralized access to scheduling state including epoch and step,
20+
with room for future custom scheduling granularities.
21+
"""
22+
epoch: int = 0
23+
step: int = 0
24+
25+
def set_epoch(self, epoch: int) -> None:
26+
"""
27+
Update the current epoch number.
28+
"""
29+
self.epoch = epoch
30+
31+
def set_step(self, step: int) -> None:
32+
"""
33+
Update the current step number.
34+
Intended to be accumulated across epoch for a global step count
35+
that can be used for step-based scheduling.
36+
"""
37+
self.step = step

‎src/virtual_stain_flow/trainers/AbstractTrainer.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from .trainer_protocol import TrainerProtocol
1616
from ..metrics.AbstractMetrics import AbstractMetrics
17+
from ..engine.progress import Progress
1718
from ..datasets.data_split import default_random_split
1819

1920

@@ -113,6 +114,9 @@ def _init_state(
113114

114115
# Epoch state
115116
self._epoch = 0
117+
118+
# Progress tracking for loss weight scheduling
119+
self._progress = Progress(epoch=0, step=0)
116120

117121
# Loss and metrics state
118122
self._train_losses = defaultdict(list)
@@ -232,6 +236,8 @@ def train_epoch(self):
232236
phase="Train"
233237
)
234238

239+
self._progress.set_step(self._progress.step + 1)
240+
235241
batch_loss = self.train_step(inputs, targets)
236242
for key, value in batch_loss.items():
237243
losses[key].append(value)
@@ -512,6 +518,11 @@ def metrics(self):
512518
def epoch(self):
513519
return self._epoch
514520

521+
@property
522+
def progress(self) -> Progress:
523+
"""Returns the Progress object tracking training state (epoch, step, etc.)"""
524+
return self._progress
525+
515526
@property
516527
def train_losses(self):
517528
return self._train_losses
@@ -548,6 +559,7 @@ def early_stop_counter(self, value: int):
548559
@epoch.setter
549560
def epoch(self, value: int):
550561
self._epoch = value
562+
self._progress.set_epoch(value)
551563

552564
"""
553565
Update loss and metrics

‎src/virtual_stain_flow/trainers/logging_gan_trainer.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ def train_step(
110110
)
111111
disc_weighted_total, disc_logs = self._discriminator_loss_group(
112112
train=True,
113+
progress=self.progress,
113114
context=disc_ctx
114115
)
115116
disc_weighted_total.backward()
@@ -133,6 +134,7 @@ def train_step(
133134
)
134135
gen_weighted_total, gen_logs = self._generator_loss_group(
135136
train=True,
137+
progress=self.progress,
136138
context=gen_ctx
137139
)
138140
gen_weighted_total.backward()
@@ -142,12 +144,14 @@ def train_step(
142144
gen_logs = {}
143145

144146
self._global_step += 1
147+
self._progress.set_step(self._global_step)
145148

146149
# if generator logs are not computed this step (due to skipped update),
147150
# compute from discriminator context
148151
if not gen_logs:
149152
_, gen_logs = self._generator_loss_group(
150153
train=True,
154+
progress=self.progress,
151155
context=ctx
152156
)
153157

@@ -176,17 +180,26 @@ def evaluate_step(
176180
)
177181
_, gen_logs = self._generator_loss_group(
178182
train=False,
183+
progress=self.progress,
179184
context=ctx
180185
)
181186
_, disc_logs = self._discriminator_loss_group(
182187
train=False,
188+
progress=self.progress,
183189
context=ctx
184190
)
185191

186192
for _, metric in self.metrics.items():
187193
metric.update(*ctx.as_metric_args(), validation=True)
188194

189195
return gen_logs | disc_logs
196+
197+
@property
198+
def loss_groups(self) -> Dict[str, LossGroup]:
199+
return {
200+
'generator': self._generator_loss_group,
201+
'discriminator': self._discriminator_loss_group
202+
}
190203

191204
def save_model(
192205
self,

‎src/virtual_stain_flow/trainers/logging_trainer.py‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,11 @@ def train_step(
105105
targets=targets
106106
)
107107

108-
weighted_total, logs = self._loss_group(train=True, context=ctx)
108+
weighted_total, logs = self._loss_group(
109+
train=True,
110+
progress=self.progress,
111+
context=ctx
112+
)
109113
weighted_total.backward()
110114
self._forward_group.step()
111115

@@ -133,12 +137,20 @@ def evaluate_step(
133137
targets=targets
134138
)
135139

136-
_, logs = self._loss_group(train=False, context=ctx)
140+
_, logs = self._loss_group(
141+
train=False,
142+
progress=self.progress,
143+
context=ctx
144+
)
137145

138146
for _, metric in self.metrics.items():
139147
metric.update(*ctx.as_metric_args(), validation=True)
140148

141149
return logs
150+
151+
@property
152+
def loss_groups(self) -> Dict[str, LossGroup]:
153+
return {'main': self._loss_group}
142154

143155
def save_model(
144156
self,

0 commit comments

Comments
 (0)