Repository: mindsdb/lightwood

Issue:
Better defaults for Neural model in time series tasks
Current default values for early stopping seem too aggressive, as the neural model is not able to correctly fit training data from e.g. arrivals. 


Discussion:

Resolving patch:
diff --git a/lightwood/encoder/numeric/ts_numeric.py b/lightwood/encoder/numeric/ts_numeric.py
--- a/lightwood/encoder/numeric/ts_numeric.py
+++ b/lightwood/encoder/numeric/ts_numeric.py
@@ -45,9 +45,10 @@ def encode(self, data, dependency_data={}):
                         mean = self.normalizers['__default'].abs_mean
                 else:
                     mean = self._abs_mean
-                if real is not None and mean > 0:
+
+                if real is not None:
                     vector[0] = 1 if real < 0 and not self.positive_domain else 0
-                    vector[1] = real / mean
+                    vector[1] = real / mean if mean != 0 else real
                 else:
                     raise Exception(f'Can\'t encode target value: {real}')
 
@@ -100,7 +101,7 @@ def decode(self, encoded_values, decode_log=None, dependency_data=None):
                         else:
                             mean = self._abs_mean
 
-                        real_value = vector[1] * mean
+                        real_value = vector[1] * mean if mean != 0 else vector[1]
 
                     if self.positive_domain:
                         real_value = abs(real_value)
diff --git a/lightwood/helpers/ts.py b/lightwood/helpers/ts.py
--- a/lightwood/helpers/ts.py
+++ b/lightwood/helpers/ts.py
@@ -18,6 +18,10 @@ def get_inferred_timestamps(df: pd.DataFrame, col: str, deltas: dict, tss: Times
         else:
             series_delta = deltas['__default'][col]
         timestamps = [last + t * series_delta for t in range(nr_predictions)]
+
+        if tss.nr_predictions == 1:
+            timestamps = timestamps[0]  # preserves original input format if nr_predictions == 1
+
         df[f'order_{col}'].iloc[idx] = timestamps
     return df[f'order_{col}']
 
diff --git a/lightwood/model/neural.py b/lightwood/model/neural.py
--- a/lightwood/model/neural.py
+++ b/lightwood/model/neural.py
@@ -184,7 +184,9 @@ def _max_fit(self, train_dl, dev_dl, criterion, optimizer, scaler, stop_after, r
 
             train_error = np.mean(running_losses)
 
-            running_errors.append(self._error(dev_dl, criterion))
+            epoch_error = self._error(dev_dl, criterion)
+            running_errors.append(epoch_error)
+            log.debug(f'Loss @ epoch {epoch}: {epoch_error}')
 
             if np.isnan(train_error) or np.isnan(
                     running_errors[-1]) or np.isinf(train_error) or np.isinf(
@@ -197,8 +199,8 @@ def _max_fit(self, train_dl, dev_dl, criterion, optimizer, scaler, stop_after, r
                 epochs_to_best = epoch
 
             if len(running_errors) >= 5:
-                delta_mean = np.mean([running_errors[-i - 1] - running_errors[-i]
-                                     for i in range(1, len(running_errors[-5:]))])
+                delta_mean = np.average([running_errors[-i - 1] - running_errors[-i] for i in range(1, 5)],
+                                        weights=[(1 / 2)**i for i in range(1, 5)])
                 if delta_mean <= 0:
                     break
             elif (time.time() - started) > stop_after:
@@ -261,18 +263,12 @@ def fit(self, ds_arr: List[EncodedDs]) -> None:
         criterion = self._select_criterion()
         scaler = GradScaler()
 
-        for subset_itt in (0, 1):
-            for subset_idx in range(len(dev_ds_arr)):
-                train_dl = DataLoader(
-                    ConcatedEncodedDs(train_ds_arr[subset_idx * 9: (subset_idx + 1) * 9]),
-                    batch_size=200, shuffle=True)
+        train_dl = DataLoader(ConcatedEncodedDs(train_ds_arr), batch_size=200, shuffle=True)
 
-                stop_after = self.stop_after / 4
+        self.model, epoch_to_best_model, err = self._max_fit(
+            train_dl, dev_dl, criterion, optimizer, scaler, self.stop_after, return_model_after=20000)
 
-                self.model, epoch_to_best_model, err = self._max_fit(
-                    train_dl, dev_dl, criterion, optimizer, scaler, stop_after / 2, 20000 if subset_itt > 0 else 1)
-
-                self.epochs_to_best += epoch_to_best_model
+        self.epochs_to_best += epoch_to_best_model
 
         if len(con_test_ds) > 0:
             if self.fit_on_dev:


Test patch: