Lightning-AI / Lightning-AI/pytorch-lightning

Logging a TorchMetric resets it, produces very subtle and unexpected behaviour

Open
#11,262 4 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

feature logging
Dominant language
Python
Stars
31.4k
Forks
3.8k
Avg merge
6d 7h
Merged PRs (30d)
6

Description

🐛 Bug

The way I understand it, it is recommended to log a TorchMetric instance, instead of its .compute() output (only if you do this, DDP synchronization is possible, should you want it):

def __init__(self):
    self.val_acc = Accuracy()
    ...

def validation_step_end(self, outputs):
    self.log("val/acc", self.val_acc)

However I was suprised to learn that this also quietly seems to reset the metric, even if the metric should not be reset:

def __init__(self):
    self.val_acc = Accuracy()
    self.val_acc_best = MaxMetric()
    ...

def validation_epoch_end(self, outputs):
    self.val_acc_best.update(self.val_acc.compute())
    self.log("val/acc_best", self.val_acc_best)
    # after this line, self.val_acc_best.value = -Inf

Imagine my surprise that my "best accuracy" was decreasing every few epochs.

To Reproduce
import os

import torch
from torch.utils.data import DataLoader, Dataset
from torchmetrics import Accuracy, MaxMetric

from pytorch_lightning import LightningModule, Trainer


class RandomDataset(Dataset):
    def __init__(self, size, length):
        self.len = length
        self.data = torch.randn(length, size)

    def __getitem__(self, index):
        return self.data[index]

    def __len__(self):
        return self.len


class BoringModel(LightningModule):
    def __init__(self):
        super().__init__()
        self.layer = torch.nn.Linear(32, 2)
        self.val_acc = Accuracy()
        self.val_acc_best = MaxMetric()

    def forward(self, x):
        return self.layer(x)

    def training_step(self, batch, batch_idx):
        loss = self(batch).sum()
        self.log("train_loss", loss)
        return {"loss": loss}

    def validation_step(self, batch, batch_idx):
        val_loss = self(batch).sum()
        self.log("valid_loss", val_loss)
        return {"loss": val_loss}

    def validation_step_end(self, outputs):
        print(self.val_acc_best.value)
        self.val_acc_best.update(outputs['loss']) # get val accuracy from current epoch
        print(self.val_acc_best.value)
        self.log("valid_loss_best", self.val_acc_best)
        print(self.val_acc_best.value)

    def test_step(self, batch, batch_idx):
        loss = self(batch).sum()
        self.log("test_loss", loss)

    def configure_optimizers(self):
        return torch.optim.SGD(self.layer.parameters(), lr=0.1)


def run():
    train_data = DataLoader(RandomDataset(32, 64), batch_size=2)
    val_data = DataLoader(RandomDataset(32, 64), batch_size=2)
    test_data = DataLoader(RandomDataset(32, 64), batch_size=2)

    model = BoringModel()
    trainer = Trainer(
        default_root_dir=os.getcwd(),
        limit_train_batches=1,
        limit_val_batches=1,
        limit_test_batches=1,
        num_sanity_val_steps=0,
        max_epochs=2,
        enable_model_summary=False,
    )
    trainer.fit(model, train_dataloaders=train_data, val_dataloaders=val_data)
    trainer.test(model, dataloaders=test_data)


if __name__ == "__main__":
    run()

As you will see in the output, self.val_acc_best.value will be -Inf in the second epoch, and therefore fails to track the best loss.

Expected behavior
Environment
* CUDA:
        - GPU:
                - NVIDIA GeForce GTX 1060 6GB
        - available:         True
        - version:           10.2
* Packages:
        - numpy:             1.20.3
        - pyTorch_debug:     False
        - pyTorch_version:   1.10.0+cu102
        - pytorch-lightning: 1.6.0dev
        - tqdm:              4.62.3
* System:
        - OS:                Linux
        - architecture:
                - 64bit
                - ELF
        - processor:         
        - python:            3.8.12
        - version:           #1 ZEN SMP PREEMPT Wed, 22 Dec 2021 09:23:53 +0000

cc @borda @carmocca @edward-io @ananthsub @rohitgr7

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by running the provided minimal reproduction and tracing the self.log call in validation_step_end when passed a TorchMetric or MaxMetric instance. Check how metric state is handled during logging and add a regression test showing that val_acc_best.value remains available across epochs instead of resetting to -Inf.

Written by the indexing model from the issue text.

Assessment

Tech stack
python, pytorch
Domain
machine-learning, observability
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.