Repository: Lightning-AI/pytorch-lightning

Issue:
Relax hparams in model saving/loading
I've managed to train a model using pl.fit(model) and have the .ckpt file. Now, I'm trying to load the .ckpt file so that I can do inference on a single image:

```
model = CoolSystem()
to_infer = torch.load('checkpoints/try_ckpt_epoch_1_v0.ckpt')
model.load_from_checkpoint(to_infer) # ------------- error is thrown at this line
```

However, upon loading the .ckpt file, the following error is thrown:
```
AttributeError: 'dict' object has no attribute 'seek'. You can only torch.load from a file that is seekable. Please pre-load the data into a buffer like io.BytesIO and try to load from it instead.
```

**Am I doing something wrong when using PyTorch Lightning for inference?**


For reference, this is my system:
```
import pytorch_lightning as pl

import os
import matplotlib.pyplot as plt
import torch
import torchvision
import torchvision.transforms as transforms
import torch.nn.functional as F

class CoolSystem(pl.LightningModule):

    def __init__(self):
        super(CoolSystem, self).__init__()

        # self.hparams = hparams
        self.data_dir = '/content/hymenoptera_data'

        self.model = torchvision.models.resnet18(pretrained=True) # final layer is of size [bs, 1000]
        num_ftrs = self.model.fc.in_features
        self.model.fc = torch.nn.Linear(num_ftrs, 2) # change final layer to be of size [bs, 2]
        
    def forward(self, x):
        x = self.model(x)
        return x
    
    def configure_optimizers(self):
        # Observe that all parameters are being optimized
        optimizer = torch.optim.SGD(self.model.parameters(), lr=0.001, momentum=0.9)

        # Decay LR by a factor of 0.1 every 7 epochs
        exp_lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=7, gamma=0.1)
 
        return [optimizer], [exp_lr_scheduler]

    def training_step(self, batch, batch_idx):
        # REQUIRED
        x, y = batch
        y_hat = self.forward(x)
        loss = F.cross_entropy(y_hat, y)
        tensorboard_logs = {'train_loss': loss}
        return {'loss': loss, 'log': tensorboard_logs}
    
    def validation_step(self, batch, batch_idx):
        # OPTIONAL
        x, y = batch
        y_hat = self.forward(x)
        return {'val_loss': F.cross_entropy(y_hat, y)}

    def validation_end(self, outputs):
        # OPTIONAL
        avg_loss = torch.stack([x['val_loss'] for x in outputs]).mean()
        tensorboard_logs = {'val_loss': avg_loss}
        return {'avg_val_loss': avg_loss, 'log': tensorboard_logs}

    @pl.data_loader
    def train_dataloader(self):
        # REQUIRED

        transform = transforms.Compose([
                                transforms.RandomResizedCrop(224),
                                transforms.RandomHorizontalFlip(),
                                transforms.ToTensor(),
                                transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
                                ])

        train_set = torchvision.datasets.ImageFolder(os.path.join(self.data_dir, 'train'), transform)
        train_loader = torch.utils.data.DataLoader(train_set, batch_size=32, shuffle=True, num_workers=4)

        return train_loader
    
    @pl.data_loader
    def val_dataloader(self):
      transform = transforms.Compose([
                                transforms.Resize(256),
                                transforms.CenterCrop(224),
                                transforms.ToTensor(),
                                transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
                                ])
                              
      val_set = torchvision.datasets.ImageFolder(os.path.join(self.data_dir, 'val'), transform)
      val_loader = torch.utils.data.DataLoader(val_set, batch_size=32, shuffle=True, num_workers=4)

      return val_loader
```

And I'm training it this way:
```
model = CoolSystem() 

import os

checkpoint_callback = pl.callbacks.ModelCheckpoint(
          filepath=os.path.join(os.getcwd(), 'checkpoints'),
          verbose=True,
          monitor='val_loss', 
          mode='min', 
          prefix='try',
          save_top_k=-1,
          period=1 # check val_loss every n periods, and saves the checkpoint if it is better than the val_loss at the previous period
      )

trainer = pl.Trainer(
      max_epochs=2,
      checkpoint_callback=checkpoint_callback)  

trainer.fit(model)
```


Discussion:
Hey, thanks for your contribution! Great first issue!
Have not tested it, but I think it should be 
`model.load_from_checkpoint('checkpoints/try_ckpt_epoch_1_v0.ckpt')`
(the method takes a string). 
See docs: 
https://pytorch-lightning.readthedocs.io/en/0.6.0/pytorch_lightning.core.html#pytorch_lightning.core.LightningModule.load_from_checkpoint

After trying `model.load_from_checkpoint('checkpoints/try_ckpt_epoch_1_v0.ckpt')`, the following error is now thrown:

```
OSError: Checkpoint does not contain hyperparameters. Are your model hyperparameters storedin self.hparams?
```

I built `CoolSystem()` without self.hparams, as per the example Colab notebook (https://colab.research.google.com/drive/1F_RNcHzTfFuQf-LeKvSlud6x7jXYkG31#scrollTo=HOk9c4_35FKg)

Any advice on this?
I guess it should be added to that example. The GAN example has it.
Add the hparams to the `__init__`,  train it, and then try to load again.
Looks like it is always needed, even if you don't pass any hparams in.
Got it! Will take note to always add hparams to `__init__` then
@awaelchli find submitting a PR to fix?

i think the point was for hparams to be optional? or should we make it more flexible? @neggert 
I can look at it. 
To make it optional, I guess we could simply change the loading behaviour depending on whether the user has defined hparams or not.
I will hold back until #849 is finalized because it affects ModelCheckpoint callback.

Resolving patch:
diff --git a/pytorch_lightning/core/lightning.py b/pytorch_lightning/core/lightning.py
--- a/pytorch_lightning/core/lightning.py
+++ b/pytorch_lightning/core/lightning.py
@@ -1,4 +1,5 @@
 import collections
+import inspect
 import logging as log
 import csv
 import os
@@ -15,6 +16,7 @@
 from pytorch_lightning.core.saving import ModelIO
 from pytorch_lightning.core.memory import ModelSummary
 from pytorch_lightning.overrides.data_parallel import LightningDistributedDataParallel
+from pytorch_lightning.utilities.debugging import MisconfigurationException
 
 try:
     import torch_xla.core.xla_model as xm
@@ -1111,13 +1113,10 @@ def load_from_metrics(cls, weights_path, tags_csv, map_location=None):
         else:
             checkpoint = torch.load(weights_path, map_location=lambda storage, loc: storage)
 
-        # load the state_dict on the model automatically
-        model = cls(hparams)
-        model.load_state_dict(checkpoint['state_dict'])
-
-        # give model a chance to load something
-        model.on_load_checkpoint(checkpoint)
+        # add the hparams from csv file to checkpoint
+        checkpoint['hparams'] = vars(hparams)
 
+        model = cls._load_model_state(checkpoint)
         return model
 
     @classmethod
@@ -1182,17 +1181,36 @@ def __init__(self, hparams):
         else:
             checkpoint = torch.load(checkpoint_path, map_location=lambda storage, loc: storage)
 
-        try:
-            ckpt_hparams = checkpoint['hparams']
-        except KeyError:
-            raise IOError(
-                "Checkpoint does not contain hyperparameters. Are your model hyperparameters stored"
-                "in self.hparams?"
-            )
-        hparams = Namespace(**ckpt_hparams)
+        model = cls._load_model_state(checkpoint)
+        return model
+
+    @classmethod
+    def _load_model_state(cls, checkpoint):
+        cls_takes_hparams = 'hparams' in inspect.signature(cls.__init__).parameters
+        ckpt_hparams = checkpoint.get('hparams')
+
+        if cls_takes_hparams:
+            if ckpt_hparams is not None:
+                hparams = Namespace(**ckpt_hparams)
+            else:
+                warnings.warn(
+                    f"Checkpoint does not contain hyperparameters but {cls.__name__}'s __init__ contains"
+                    " argument 'hparams'. Will pass in an empty Namespace instead."
+                    " Did you forget to store your model hyperparameters in self.hparams?"
+                )
+                hparams = Namespace()
+        else:  # The user's LightningModule does not define a hparams argument
+            if ckpt_hparams is None:
+                hparams = None
+            else:
+                raise MisconfigurationException(
+                    f"Checkpoint contains hyperparameters but {cls.__name__}'s __init__ is missing the"
+                    " argument 'hparams'. Are you loading the correct checkpoint?"
+                )
 
         # load the state_dict on the model automatically
-        model = cls(hparams)
+        model_args = [hparams] if hparams else []
+        model = cls(*model_args)
         model.load_state_dict(checkpoint['state_dict'])
 
         # give model a chance to load something


Test patch:
diff --git a/tests/models/__init__.py b/tests/models/__init__.py
--- a/tests/models/__init__.py
+++ b/tests/models/__init__.py
@@ -26,3 +26,21 @@ class LightningTestModel(LightningValidationMixin, LightningTestMixin, Lightning
 
     def on_training_metrics(self, logs):
         logs['some_tensor_to_test'] = torch.rand(1)
+
+
+class LightningTestModelWithoutHyperparametersArg(LightningTestModel):
+    """ without hparams argument in constructor """
+
+    def __init__(self):
+        import tests.models.utils as tutils
+
+        # the user loads the hparams in some other way
+        hparams = tutils.get_hparams()
+        super().__init__(hparams)
+
+
+class LightningTestModelWithUnusedHyperparametersArg(LightningTestModelWithoutHyperparametersArg):
+    """ has hparams argument in constructor but is not used """
+
+    def __init__(self, hparams):
+        super().__init__()
diff --git a/tests/test_restore_models.py b/tests/test_restore_models.py
--- a/tests/test_restore_models.py
+++ b/tests/test_restore_models.py
@@ -1,12 +1,18 @@
 import logging as log
 import os
 
+import pytest
 import torch
 
 import tests.models.utils as tutils
 from pytorch_lightning import Trainer
 from pytorch_lightning.callbacks import ModelCheckpoint
-from tests.models import LightningTestModel
+from pytorch_lightning.utilities.debugging import MisconfigurationException
+from tests.models import (
+    LightningTestModel,
+    LightningTestModelWithoutHyperparametersArg,
+    LightningTestModelWithUnusedHyperparametersArg
+)
 
 
 def test_running_test_pretrained_model_ddp(tmpdir):
@@ -380,5 +386,39 @@ def test_model_saving_loading(tmpdir):
     new_pred = model_2(x)
     assert torch.all(torch.eq(pred_before_saving, new_pred)).item() == 1
 
+
+def test_load_model_with_missing_hparams(tmpdir):
+    trainer_options = dict(
+        show_progress_bar=False,
+        max_epochs=1,
+        checkpoint_callback=ModelCheckpoint(tmpdir, save_top_k=-1),
+        logger=False,
+        default_save_path=tmpdir,
+    )
+
+    # fit model
+    trainer = Trainer(**trainer_options)
+
+    model = LightningTestModelWithoutHyperparametersArg()
+    trainer.fit(model)
+    last_checkpoint = os.path.join(trainer.checkpoint_callback.filepath, "_ckpt_epoch_0.ckpt")
+
+    # try to load a checkpoint that has hparams but model is missing hparams arg
+    with pytest.raises(MisconfigurationException, match=r".*__init__ is missing the argument 'hparams'.*"):
+        LightningTestModelWithoutHyperparametersArg.load_from_checkpoint(last_checkpoint)
+
+    # create a checkpoint without hyperparameters
+    # if the model does not take a hparams argument, it should not throw an error
+    ckpt = torch.load(last_checkpoint)
+    del(ckpt['hparams'])
+    torch.save(ckpt, last_checkpoint)
+    LightningTestModelWithoutHyperparametersArg.load_from_checkpoint(last_checkpoint)
+
+    # load checkpoint without hparams again
+    # warn if user's model has hparams argument
+    with pytest.warns(UserWarning, match=r".*Will pass in an empty Namespace instead."):
+        LightningTestModelWithUnusedHyperparametersArg.load_from_checkpoint(last_checkpoint)
+
+
 # if __name__ == '__main__':
 #     pytest.main([__file__])