Repository: deepset-ai/haystack

Issue:
EarlyStopping wrong save_dir type.
**Describe the bug**
The EarlyStopping class has save_dir argument as:
https://github.com/deepset-ai/haystack/blob/3ea57801ae31fd2962af3713acef158ebf9890ca/haystack/utils/early_stopping.py#L25
However passing a path as a `str` to it will lead to an error, as the `save` method (at least of the `biadaptive_model`) expects a `Path` type:
https://github.com/deepset-ai/haystack/blob/eb57e1fc096691b27f4c4d7e667eb56e9abec3db/haystack/modeling/model/biadaptive_model.py#L95
**Error message**
```
Traceback (most recent call last):                    
  File "retriever/train_retriever.py", line 29, in <module>                                                  
    retriever.train(                                  
  File "/home/huguetcabot/miniconda3/envs/alby-re/lib/python3.8/site-packages/haystack/nodes/retriever/dense.py", line 711, in train                                                                                      
    trainer.train()                                   
  File "/home/huguetcabot/miniconda3/envs/alby-re/lib/python3.8/site-packages/haystack/modeling/training/base.py", line 240, in train                                                                                     
    self.model.save(self.early_stopping.save_dir)     
  File "/home/huguetcabot/miniconda3/envs/alby-re/lib/python3.8/site-packages/haystack/modeling/model/biadaptive_model.py", line 102, in save                                                                             
    if not os.path.exists(Path.joinpath(save_dir, Path(lm1_name))):                                          
  File "/home/huguetcabot/miniconda3/envs/alby-re/lib/python3.8/pathlib.py", line 931, in joinpath           
    return self._make_child(args)                     
AttributeError: 'str' object has no attribute '_make_child'
```

**Expected behavior**
Either specify a Path needs to be inputed, or the EarlyStopping method should store it as Path

**Additional context**
I don't think it is needed
**To Reproduce**
Try to train DensePassageRetriever using its train method with EarlyStopping.

**FAQ Check**
- [x ] Have you had a look at [our new FAQ page](https://docs.haystack.deepset.ai/docs/faq)?

**System:** 
Unrelated.



Discussion:
Hey, I'm a research engineer currently working on a mix of ml research and software engineering with a focus on language modelling. I'd like to get involved and take this issue if it's still free?
Hey, @jebbbbb thanks a lot! We would really appreciate it if you could open a PR for this issue. If you haven't already, I'd also recommend checking out our [Contribution](https://github.com/deepset-ai/haystack/blob/main/CONTRIBUTING.md) guidelines for setting up your dev environment and opening a PR to Haystack. 
Hey @sjrl, I've changed my username to this one now. Yes sure, I'll look into opening a PR today.

Resolving patch:
diff --git a/haystack/modeling/model/biadaptive_model.py b/haystack/modeling/model/biadaptive_model.py
--- a/haystack/modeling/model/biadaptive_model.py
+++ b/haystack/modeling/model/biadaptive_model.py
@@ -92,19 +92,18 @@ def __init__(
             loss_aggregation_fn = loss_per_head_sum
         self.loss_aggregation_fn = loss_aggregation_fn
 
-    def save(self, save_dir: Path, lm1_name: str = "lm1", lm2_name: str = "lm2"):
+    def save(self, save_dir: Union[str, Path], lm1_name: str = "lm1", lm2_name: str = "lm2"):
         """
         Saves the 2 language model weights and respective config_files in directories lm1 and lm2 within save_dir.
 
-        :param save_dir: Path to save the BiAdaptiveModel to.
+        :param save_dir: Path | str to save the BiAdaptiveModel to.
         """
         os.makedirs(save_dir, exist_ok=True)
-        if not os.path.exists(Path.joinpath(save_dir, Path(lm1_name))):
-            os.makedirs(Path.joinpath(save_dir, Path(lm1_name)))
-        if not os.path.exists(Path.joinpath(save_dir, Path(lm2_name))):
-            os.makedirs(Path.joinpath(save_dir, Path(lm2_name)))
-        self.language_model1.save(Path.joinpath(save_dir, Path(lm1_name)))
-        self.language_model2.save(Path.joinpath(save_dir, Path(lm2_name)))
+        for name, model in zip([lm1_name, lm2_name], [self.language_model1, self.language_model2]):
+            model_save_dir = Path.joinpath(Path(save_dir), Path(name))
+            os.makedirs(model_save_dir, exist_ok=True)
+            model.save(model_save_dir)
+
         for i, ph in enumerate(self.prediction_heads):
             logger.info("prediction_head saving")
             ph.save(save_dir, i)
diff --git a/haystack/modeling/model/triadaptive_model.py b/haystack/modeling/model/triadaptive_model.py
--- a/haystack/modeling/model/triadaptive_model.py
+++ b/haystack/modeling/model/triadaptive_model.py
@@ -105,22 +105,20 @@ def __init__(
             loss_aggregation_fn = loss_per_head_sum
         self.loss_aggregation_fn = loss_aggregation_fn
 
-    def save(self, save_dir: Path, lm1_name: str = "lm1", lm2_name: str = "lm2", lm3_name: str = "lm3"):
+    def save(self, save_dir: Union[str, Path], lm1_name: str = "lm1", lm2_name: str = "lm2", lm3_name: str = "lm3"):
         """
         Saves the 3 language model weights and respective config_files in directories lm1 and lm2 within save_dir.
 
-        :param save_dir: Path to save the TriAdaptiveModel to.
+        :param save_dir: Path | str to save the TriAdaptiveModel to.
         """
         os.makedirs(save_dir, exist_ok=True)
-        if not os.path.exists(Path.joinpath(save_dir, Path(lm1_name))):
-            os.makedirs(Path.joinpath(save_dir, Path(lm1_name)))
-        if not os.path.exists(Path.joinpath(save_dir, Path(lm2_name))):
-            os.makedirs(Path.joinpath(save_dir, Path(lm2_name)))
-        if not os.path.exists(Path.joinpath(save_dir, Path(lm3_name))):
-            os.makedirs(Path.joinpath(save_dir, Path(lm3_name)))
-        self.language_model1.save(Path.joinpath(save_dir, Path(lm1_name)))
-        self.language_model2.save(Path.joinpath(save_dir, Path(lm2_name)))
-        self.language_model3.save(Path.joinpath(save_dir, Path(lm3_name)))
+        for name, model in zip(
+            [lm1_name, lm2_name, lm3_name], [self.language_model1, self.language_model2, self.language_model3]
+        ):
+            model_save_dir = Path.joinpath(Path(save_dir), Path(name))
+            os.makedirs(model_save_dir, exist_ok=True)
+            model.save(model_save_dir)
+
         for i, ph in enumerate(self.prediction_heads):
             logger.info("prediction_head saving")
             ph.save(save_dir, i)
diff --git a/haystack/modeling/training/base.py b/haystack/modeling/training/base.py
--- a/haystack/modeling/training/base.py
+++ b/haystack/modeling/training/base.py
@@ -281,7 +281,7 @@ def train(self):
         # With early stopping we want to restore the best model
         if self.early_stopping and self.early_stopping.save_dir:
             logger.info("Restoring best model so far from {}".format(self.early_stopping.save_dir))
-            self.model = AdaptiveModel.load(self.early_stopping.save_dir, self.device)
+            self.model = self.model.load(self.early_stopping.save_dir, self.device)
             self.model.connect_heads_with_processor(self.data_silo.processor.tasks, require_labels=True)
 
         # Eval on test set


Test patch:
diff --git a/test/modeling/test_dpr.py b/test/modeling/test_dpr.py
--- a/test/modeling/test_dpr.py
+++ b/test/modeling/test_dpr.py
@@ -20,6 +20,7 @@
 from haystack.nodes.retriever.dense import DensePassageRetriever
 
 from haystack.modeling.utils import set_all_seeds, initialize_device_settings
+from haystack.utils.early_stopping import EarlyStopping
 
 from ..conftest import SAMPLES_PATH
 
@@ -1003,6 +1004,34 @@ def test_dpr_training(document_store, tmp_path):
     )
 
 
+@pytest.mark.parametrize("document_store", ["memory"], indirect=True)
+def test_dpr_training_with_earlystopping(document_store, tmp_path):
+    retriever = DensePassageRetriever(
+        document_store=document_store,
+        query_embedding_model="facebook/dpr-question_encoder-single-nq-base",
+        passage_embedding_model="facebook/dpr-ctx_encoder-single-nq-base",
+        max_seq_len_query=8,
+        max_seq_len_passage=8,
+    )
+
+    save_dir = f"{tmp_path}/test_dpr_training"
+    retriever.train(
+        data_dir=str(SAMPLES_PATH / "dpr"),
+        train_filename="sample.json",
+        dev_filename="sample.json",
+        test_filename="sample.json",
+        n_epochs=1,
+        batch_size=1,
+        grad_acc_steps=1,
+        save_dir=save_dir,
+        evaluate_every=1,
+        embed_title=True,
+        num_positives=1,
+        num_hard_negatives=1,
+        early_stopping=EarlyStopping(save_dir=save_dir),
+    )
+
+
 # TODO fix CI errors (test pass locally or on AWS, next steps: isolate PyTorch versions once FARM dependency is removed)
 # def test_dpr_training():
 #     batch_size = 1