Enefit - Predict Energy Behavior of Prosumers, part 4

technical
Author

Sameer Sant

Published

July 11, 2025

1 Intro

Show code
from numpy import random
random.seed(42)

def nn_mae(preds, y): return torch.round((preds - y).abs().mean(), decimals=6)
def mae(preds, y): return round((preds-y).abs().mean(), 6)
def m_mae(m, xs, y): return mae(m.predict(xs), y)

class CrossValidate:
    
    is_partial = True
    # percent of currently known ids to drop (3 -> 4%, 4 -> 6%, 5 -> 7%, 6->9%, 7->10%)
    max_n_pids_to_drop = 7 
    n_pids_drop = []
    pids_to_drop = []
    
    # current for each round
    production_models = None
    production_ensemble_predictions = None
    consumption_ensemble_predictions = None
    
    valid_errors = []
    cross_val_score = None

    train_dates = {'part': [pd.to_datetime('2022-11-01 00:00:00'), pd.to_datetime('2023-01-29 23:00:00')],
                   'full': [pd.to_datetime('2022-01-01 00:00:00'), pd.to_datetime('2023-01-29 23:00:00')]
                  }
    
    eval_dates = {'feb23': [pd.to_datetime('2023-02-01 00:00:00'), pd.to_datetime('2023-02-28 23:00:00')],
                  'mar23': [pd.to_datetime('2023-03-01 00:00:00'), pd.to_datetime('2023-03-31 23:00:00')],
                  'apr23': [pd.to_datetime('2023-04-01 00:00:00'), pd.to_datetime('2023-04-30 23:00:00')],
                  'may23': [pd.to_datetime('2023-05-01 00:00:00'), pd.to_datetime('2023-05-31 23:00:00')]
                 }
    
    def __init__(self, is_partial=True, max_n_pids_to_drop=7):
        self.is_partial = is_partial
        self.max_n_pids_to_drop = max_n_pids_to_drop
        return
    
    def _get_endpoints(self, features_df, eval_month:str):
        
        if self.is_partial:
            train_dates_conds = [features_df['datetime'] == self.train_dates['part'][0], 
                                 features_df['datetime'] == self.train_dates['part'][1]
                                ]
        else:
            train_dates_conds = [features_df['datetime'] == self.train_dates['full'][0], 
                                 features_df['datetime'] == self.train_dates['full'][1]
                                ]
        
        train_endpoints = [features_df['datetime'][train_dates_conds[0]].index[0], 
                           features_df['datetime'][train_dates_conds[1]].index[-1]
                          ]
                
        valid_dates_cond = [features_df['datetime'] == self.eval_dates[eval_month][0], 
                            features_df['datetime'] == self.eval_dates[eval_month][1]
                           ]
        
        valid_endpoints = [features_df['datetime'][valid_dates_cond[0]].index[0], 
                           features_df['datetime'][valid_dates_cond[1]].index[-1]
                          ]
        
        return train_endpoints, valid_endpoints
        
    def _drop_pids_from_train(self, features_df, splits, pid_lookup_df):
        training_pids = set(features_df.iloc[splits[0]].merge(pid_lookup_df, how='left', on=['row_id'])['prediction_unit_id'].unique().tolist())
        valid_pids = set(features_df.iloc[splits[1]].merge(pid_lookup_df, how='left', on=['row_id'])['prediction_unit_id'].unique().tolist())
        intersection_pids = list(training_pids.intersection(valid_pids))
        
        # pids drop list may not be continuous or sorted.
        intersection_pids.sort() 
        
        # drop from those not already missing in the valid fold, THIS ROUND.
        # to get the number of pids to drop THIS ROUND, pick a random int b/w 1 and the max allowed to drop.
        n_pids_drop = random.randint(3, self.max_n_pids_to_drop) 
        
        # to get the pids to drop, index intersection_pids w/ a random int b/w 0 and its last index. repeat for the n_pids_drop chosen for THIS ROUND. 
        pids_to_drop = [random.randint(0, len(intersection_pids)-1) for i in range(n_pids_drop)]
        
        self.n_pids_drop.append(n_pids_drop)
        self.pids_to_drop.append(pids_to_drop)
    
    def _get_splits(self, features_df, val_round, pid_lookup_df):
        # val on feb23
        if val_round == 1:
            eval_month = 'feb23'
            train_endpoints, valid_endpoints = self._get_endpoints(features_df, eval_month)
            train = features_df.iloc[train_endpoints[0]:train_endpoints[1]+1].index.tolist()
            valid = features_df.iloc[valid_endpoints[0]:valid_endpoints[1]+1].index.tolist()
            splits = (train, valid)

        # val on mar23
        elif val_round == 2: 
            eval_month = 'mar23'
            train_endpoints, valid_endpoints = self._get_endpoints(features_df, eval_month)
            
            # update train endpoints to include end of feb23
            cond = features_df['datetime'] == self.eval_dates['feb23'][1] # last datetime of feb23
            train_endpoints[1] = features_df['datetime'][cond].index[-1] # last index of last datetime of feb23
            
            train = features_df.iloc[train_endpoints[0]:train_endpoints[1]+1].index.tolist()
            valid = features_df.iloc[valid_endpoints[0]:valid_endpoints[1]+1].index.tolist()
            splits = (train, valid)

        # val on apr23
        elif val_round == 3: 
            eval_month = 'apr23'
            train_endpoints, valid_endpoints = self._get_endpoints(features_df, eval_month)
            
            # update train endpoints to include end of mar23
            cond = features_df['datetime'] == self.eval_dates['mar23'][1] # last datetime of mar23
            train_endpoints[1] = features_df['datetime'][cond].index[-1] # last index of last datetime of mar23
            
            train = features_df.iloc[train_endpoints[0]:train_endpoints[1]+1].index.tolist()
            valid = features_df.iloc[valid_endpoints[0]:valid_endpoints[1]+1].index.tolist()
            splits = (train, valid)
            
        # val on may23 (test)
        else:
            eval_month = 'may23'
            train_endpoints, valid_endpoints = self._get_endpoints(features_df, eval_month)
            
            # update train endpoints to include end of apr23
            cond = features_df['datetime'] == self.eval_dates['apr23'][1] # last datetime of apr23
            train_endpoints[1] = features_df['datetime'][cond].index[-1] # last index of last datetime of apr23
            
            train = features_df.iloc[train_endpoints[0]:train_endpoints[1]+1].index.tolist()
            valid = features_df.iloc[valid_endpoints[0]:valid_endpoints[1]+1].index.tolist()
            splits = (train, valid)
            
        return splits
    
    def _setup_tabular_pandas(self, features_df, procs, max_card, splits, dep_var='target'):  
        # variables
        cont, cat = cont_cat_split(features_df, max_card, dep_var=dep_var)

        # setup object
        return TabularPandas(features_df, procs, cat, cont, y_names=dep_var, splits=splits)        

    def _rf(self, to, n_estimators=40, max_features=0.5, min_samples_leaf=5, **kwargs):
        return RandomForestRegressor(n_jobs=-1, 
                                     n_estimators=n_estimators,
                                     max_samples=int(0.5*len(to.train.xs)), 
                                     max_features=max_features,
                                     min_samples_leaf=min_samples_leaf, 
                                     oob_score=True).fit(to.train.xs, to.train.y)
    
    def _nn(self, to_nn, bs=1024, n_epochs=16, lr=1e-3):
        """
        extrapolates up to np.ceil(y.max() * 2)
        """
        dls = to_nn.dataloaders(bs)
        learn = tabular_learner(dls, y_range=(0, np.ceil(to_nn.train.y.max()*2)), 
                                layers=[500,250], n_out=1, loss_func=F.l1_loss)
        learn.fit_one_cycle(n_epochs, lr)
        return learn        
    
    def cross_validate_merged(self, features_df, final_features, pid_lookup_df, valid_only):
        """
        final_features = [production_features, consumption_features]
        """
           
        if valid_only:
            n_rounds = 3
        else: 
            n_rounds = 4
            
        valid_errors = []
        for val_round in range(1, n_rounds+1):
            
            if val_round < 4: 
                print(f" ######## ROUND {val_round} ########\n")
            else:
                print(f" ######## ROUND {val_round} (TEST) ########\n")
            for i in range(2): 
                is_consumption_mask = features_df['is_consumption'] == i
                is_cons_df = features_df[is_consumption_mask]
                is_cons_df = is_cons_df.reset_index(drop=True)

                # generate pid mask
                tmp_splits = self._get_splits(is_cons_df, val_round, pid_lookup_df)
                self._drop_pids_from_train(is_cons_df, tmp_splits, pid_lookup_df) # appends local pids_to_drop to self.pids_to_drop (a list)
                pids_to_drop = self.pids_to_drop[val_round-1] # ensures the same pids dropped from production and consumption
                pid_mask = is_cons_df.iloc[tmp_splits[0]].merge(pid_lookup_df, how='left', on=['row_id'])['prediction_unit_id'].isin(pids_to_drop)
                valid_buffer = pd.Series(np.zeros(len(tmp_splits[1]), dtype='bool'))
                pid_mask = pd.concat([pid_mask, valid_buffer])
                pid_mask.index = tmp_splits[0] + tmp_splits[1]

                # apply pid mask
                tmp_df = is_cons_df[final_features[i] + ['datetime', 'row_id', 'target']].iloc[tmp_splits[0]+tmp_splits[1]][~pid_mask]
                is_cons_df_final = tmp_df.reset_index(drop=True)[final_features[i] + ['datetime', 'target']] # need datetime for splits
                final_splits = self._get_splits(is_cons_df_final, val_round, pid_lookup_df) # re-compute the splits after applying pid_mask.
                
                del tmp_splits, tmp_df
                gc.collect()        

                # RF
                procs = [Categorify, FillMissing]
                max_card = 20
                to = self._setup_tabular_pandas(is_cons_df_final[final_features[i] + ['target']], procs, max_card, final_splits)

                # NN
                procs_nn = [Categorify, FillMissing, Normalize]
                max_card_nn = 9000
                to_nn = self._setup_tabular_pandas(is_cons_df_final[final_features[i] + ['target']], procs_nn, max_card_nn, final_splits)
                
                # fit
                if i == 0:
                    print(f"Fitting PRODUCTION models...\n")
                elif i == 1: 
                    print(f"Fitting CONSUMPTION models...\n")
                print(f"Random Forest...\n")
                m_rf = self._rf(to)
                print(f"Neural Network...\n")
                m_nn = self._nn(to_nn) # default 16 epochs 
            
                # score
                print("")
                print(f"-------- Scoring Random Forest --------")
                print("train mae: ", m_mae(m_rf, to.train.xs, to.train.y))
                print("valid mae: ", m_mae(m_rf, to.valid.xs, to.valid.y))
                print("oob mae: ", mae(m_rf.oob_prediction_, to.train.y))
                print("\n")

                print(f"-------- Scoring Neural Network --------")
                nn_preds, targs = m_nn.get_preds()
                print("nn valid mae: ", nn_mae(nn_preds, targs))
                print("\n")
            
                print(f"-------- Scoring Random Forest + Neural Network ensemble --------")
                rf_preds = m_rf.predict(to.valid.xs)
                ens_preds = (to_np(nn_preds.squeeze()) + rf_preds)/2
                ens_mae = mae(ens_preds, to.valid.y)  
                print("ens valid mae: ", ens_mae)
                print("\n")
                
                if i == 0:
                    self.production_models = [m_rf, m_nn]
                    self.production_ensemble_predictions = ens_preds
                if i == 1:
                    self.consumption_models = [m_rf, m_nn]
                    self.consumption_ensemble_predictions = ens_preds
            
            splits = self._get_splits(features_df, val_round, pid_lookup_df)
            valid_xs = features_df.iloc[splits[1]].drop(columns=['target'])
            valid_y = features_df.iloc[splits[1]]['target']
            valid_xs['target'] = np.zeros(valid_xs.shape[0])
            valid_xs.loc[valid_xs['is_consumption'] == 0, 'target'] = self.production_ensemble_predictions
            valid_xs.loc[valid_xs['is_consumption'] == 1, 'target'] = self.consumption_ensemble_predictions
            
            merged_ens_mae = mae(valid_xs['target'], valid_y)
            
            if val_round < 4:
                self.valid_errors.append(merged_ens_mae)
                print(f"ROUND {val_round} Merged Ensemble MAE: {merged_ens_mae}\n")
            else:
                self.cross_validation_score = np.mean(self.valid_errors)
        print("Merged Ensemble Valid errors: ", self.valid_errors)
        print(f"Cross validation score: {self.cross_validation_score}")
        print(f"ROUND {val_round} (TEST) Merged Ensemble MAE: {merged_ens_mae}\n")
        print(f"n_pids dropped: {list(zip([i for i in range(1, n_rounds+1)], self.n_pids_drop))}")
        print(f"pids dropped: {list(zip([i for i in range(1, n_rounds+1)], self.pids_to_drop))}")
        return valid_xs, valid_y

In part 2 and part 3, we delved deeper into prosumers’ consumption and production behavior and carefully selected the most predictive features. In part 4, we’ll evaluate our models on varying partitions of the data to approximate how well they’ll hold up on new data.

To do this, I implemented a CrossValidate class (see code above) that performs 3-fold cross validation as described in part 1.

Hide code
%%time
d = Storage()
feature_generator = FeatureGenerator(d)
CPU times: user 18.8 s, sys: 3.85 s, total: 22.7 s
Wall time: 22.9 s
Hide code
%%time 
features_df = feature_generator.generate_features()
CPU times: user 2min 48s, sys: 12min 31s, total: 15min 20s
Wall time: 16min 23s

We will model production and consumption separately using random forest and neural network ensembles. Here are the final feature sets for production and consumption:

Hide code
pkl_path = d.path/'pkl_files'
pid_lookup_df = load_pickle(pkl_path/'pid_lookup.pkl')

final_features_prod = ['target_48h', 
                       'target_168h', 
                       'target_336h', 
                       'target_144h', 
                       'log_direct_solar_radiation_mean_0h_fd', 
                       'log_surface_solar_radiation_downwards_mean_0h_fd', 
                       'target_120h', 
                       'log_installed_capacity_48h', 
                       'target_72h', 
                       'target_96h', 
                       'log_eic_count_48h', 
                       'log_direct_solar_radiation_std_0h_fd', 
                       'cloudcover_low_mean_0h_fd', 
                       'target_product_type_std_48h', 
                       'target_product_type_mean_48h', 
                       'log_surface_solar_radiation_downwards_std_0h_fd', 
                       'cloudcover_total_mean_0h_fd', 
                       'log_direct_solar_radiation_mean_0h_fl', 
                       'log_total_precipitation_mean_0h_fd', 
                       'log_total_precipitation_std_0h_fd', 
                       'log_surface_solar_radiation_downwards_mean_0h_fl', 
                       '10_metre_u_wind_component_mean_0h_fd', 
                       'cloudcover_mid_mean_0h_fd', 
                       '10_metre_v_wind_component_mean_0h_fd', 
                       'dewpoint_std_0h_fd', 
                       'target_county_std_48h', 
                       'cloudcover_mid_std_0h_fd', 
                       '10_metre_u_wind_component_std_0h_fd', 
                       'cloudcover_low_std_0h_fd', 
                       'target_county_std_72h', 
                       '10_metre_u_wind_component_std_168h_fd', 
                       'cloudcover_total_std_0h_fd', 
                       'log_total_precipitation_mean_168h_fd', 
                       'dewpoint_mean_0h_fd', 
                       'target_county_mean_168h', 
                       '10_metre_v_wind_component_std_0h_fd', 
                       '10_metre_u_wind_component_mean_168h_fd', 
                       'cloudcover_low_mean_168h_fd', 
                       'datetimeDayofweek', 
                       'county']
final_features_cons = ['target_168h', 
                       'target_336h', 
                       'target_144h', 
                       'target_48h', 
                       'target_120h', 
                       'target_county_std_168h', 
                       'target_county_std_336h', 
                       'target_72h', 
                       'target_county_mean_168h', 
                       'target_county_mean_336h', 
                       'target_96h', 
                       'log_surface_solar_radiation_downwards_mean_0h_fd', 
                       'log_installed_capacity_48h', 
                       'log_direct_solar_radiation_mean_0h_fd', 
                       'log_eic_count_48h', 
                       'target_is_business_std_168h', 
                       'target_county_std_144h', 
                       'target_is_cons_std_168h', 
                       'sin(datetimeDayofyear)', 
                       'target_product_type_std_168h', 
                       'target_county_std_48h', 
                       'log_direct_solar_radiation_std_0h_fd', 
                       'log_surface_solar_radiation_downwards_mean_168h_fd', 
                       'target_is_business_mean_336h', 
                       'cloudcover_low_mean_0h_fd', 
                       'temperature_mean_0h_fd', 
                       'county', 
                       'datetimeWeek', 
                       'is_holiday', 
                       'product_type', 
                       'datetimeDayofweek', 
                       'is_business']

final_features = [final_features_prod, final_features_cons]
is_partial = False # full training set
valid_only = False # train through apr23 for eval on may23 (test set)
cv = CrossValidate(is_partial)

Each cross validation round, we fit production and consumption models and score them on their corresponding data, i.e. production models on production data, consumption models on consumption data. Finally, the two sets of ensembles are evaluated against the test set.

The cross validation results are summarized in the tables below. For details, see the appendix.

image-3.png image-4.png

2 Analysis

The validation errors increased each round as we approached the end of the available data. There was ultimately a ~23% difference between the cross validation score and the test score. Overfitting in both production and consumption models was markedly greater from round 2 onwards. However, the random forests’ OOB errors were not that different from the training errors. And unless we’ve missed something, we’ve handled data leakage too. Since the individual trees are not overfitting their training subsets that badly, something other than normal overfitting is driving the validation errors. All this in spite of the training data spanning more than 1 year and presumably capturing some trends, seasonality, and cycles.

These findings point to domain shift: the patterns underlying prosumers’ behavior have evolved over time. One known factor is rapidly increasing solar panel adoption by Enefit’s clients. Other external factors may include changes in weather patterns (perhaps over time horizons longer than 1 year) or government policy.

During the evaluation period, the models are re-trained online and scored as more new data is collected, so the competition’s test set spans data beyond May 2023, i.e. different from our test set. That said, our 63.5 score places in the top 10% (bronze medal range), which is an encouraging point to improve from.

Kaggle leaderboard snapshot

3 Next steps

This post concludes my series on this competition.

To go further, I would continue to experiment with:

  • new features: ratios between the lags, features that better capture complex trends/seasonality/cycles
  • new models: gradient-boosted decision trees (like LightGBM)
  • hyperparameter optimization

Thanks for reading!

4 Appendix

Hide code

%%time
valid_xs, valid_y = cv.cross_validate_merged(features_df=features_df, final_features=final_features, 
                         pid_lookup_df=pid_lookup_df, valid_only=valid_only)
 ######## ROUND 1 ########

Fitting PRODUCTION models...

Random Forest...

Neural Network...


-------- Scoring Random Forest --------
train mae:  13.96642
valid mae:  12.999196
oob mae:  18.762053


-------- Scoring Neural Network --------
nn valid mae:  tensor(11.5633)


-------- Scoring Random Forest + Neural Network ensemble --------
ens valid mae:  11.635252


Fitting CONSUMPTION models...

Random Forest...

Neural Network...


-------- Scoring Random Forest --------
train mae:  29.415285
valid mae:  54.327958
oob mae:  38.985235


-------- Scoring Neural Network --------
nn valid mae:  tensor(60.0059)


-------- Scoring Random Forest + Neural Network ensemble --------
ens valid mae:  52.030718


ROUND 1 Merged Ensemble MAE: 31.832985

 ######## ROUND 2 ########

Fitting PRODUCTION models...

Random Forest...

Neural Network...


-------- Scoring Random Forest --------
train mae:  14.185293
valid mae:  39.943662
oob mae:  19.027633


-------- Scoring Neural Network --------
nn valid mae:  tensor(42.7942)


-------- Scoring Random Forest + Neural Network ensemble --------
ens valid mae:  39.813432


Fitting CONSUMPTION models...

Random Forest...

Neural Network...


-------- Scoring Random Forest --------
train mae:  30.964493
valid mae:  82.149156
oob mae:  41.096259


-------- Scoring Neural Network --------
nn valid mae:  tensor(74.4403)


-------- Scoring Random Forest + Neural Network ensemble --------
ens valid mae:  74.145632


ROUND 2 Merged Ensemble MAE: 56.979532

 ######## ROUND 3 ########

Fitting PRODUCTION models...

Random Forest...

Neural Network...


-------- Scoring Random Forest --------
train mae:  14.989263
valid mae:  77.545777
oob mae:  20.090887


-------- Scoring Neural Network --------
nn valid mae:  tensor(68.8054)


-------- Scoring Random Forest + Neural Network ensemble --------
ens valid mae:  68.321733


Fitting CONSUMPTION models...

Random Forest...

Neural Network...


-------- Scoring Random Forest --------
train mae:  31.640666
valid mae:  68.262013
oob mae:  42.035292


-------- Scoring Neural Network --------
nn valid mae:  tensor(66.3969)


-------- Scoring Random Forest + Neural Network ensemble --------
ens valid mae:  62.975937


ROUND 3 Merged Ensemble MAE: 65.648835

 ######## ROUND 4 (TEST) ########

Fitting PRODUCTION models...

Random Forest...

Neural Network...


-------- Scoring Random Forest --------
train mae:  16.116092
valid mae:  80.082593
oob mae:  21.638424


-------- Scoring Neural Network --------
nn valid mae:  tensor(83.4254)


-------- Scoring Random Forest + Neural Network ensemble --------
ens valid mae:  75.02018


Fitting CONSUMPTION models...

Random Forest...

Neural Network...


-------- Scoring Random Forest --------
train mae:  32.219198
valid mae:  54.529984
oob mae:  42.820492


-------- Scoring Neural Network --------
nn valid mae:  tensor(55.3141)


-------- Scoring Random Forest + Neural Network ensemble --------
ens valid mae:  51.992111


Merged Ensemble Valid errors:  [31.832985, 56.979532, 65.648835]
Cross validation score: 51.48711733333334
ROUND 4 (TEST) Merged Ensemble MAE: 63.506146

n_pids dropped: [(1, 5), (2, 3), (3, 6), (4, 3)]
pids dropped: [(1, [51, 14, 60, 20, 23]), (2, [50, 6, 20]), (3, [33, 61, 13, 47, 14, 61]), (4, [27, 6, 11])]
CPU times: user 2h 7min 8s, sys: 16min 19s, total: 2h 23min 28s
Wall time: 1h 8min 1s
epoch train_loss valid_loss time
0 1897.237427 596.624756 00:14
1 131.962891 25.660812 00:11
2 44.994175 13.523369 00:14
3 32.978306 12.858157 00:12
4 30.727938 12.241396 00:12
5 25.955965 12.525143 00:12
6 25.092102 12.716256 00:11
7 23.074623 12.318460 00:11
8 22.043545 11.943836 00:11
9 20.941635 11.832774 00:11
10 19.678612 11.596600 00:10
11 18.917330 11.705679 00:11
12 17.988184 11.665542 00:11
13 17.137285 11.546488 00:11
14 17.078348 11.548635 00:11
15 16.543385 11.563257 00:11
epoch train_loss valid_loss time
0 3775.096436 6579.505371 00:12
1 916.656982 840.267761 00:12
2 97.175354 93.464661 00:12
3 78.853874 80.872475 00:13
4 66.517845 116.705086 00:10
5 61.436050 83.521637 00:11
6 59.296417 67.924034 00:11
7 54.841911 69.048393 00:12
8 51.824608 67.985527 00:12
9 51.090561 67.804237 00:12
10 47.829353 64.920143 00:12
11 46.321831 65.150032 00:12
12 43.187836 60.894318 00:13
13 42.405373 60.907848 00:12
14 41.502060 59.960979 00:12
15 41.460133 60.005856 00:12
epoch train_loss valid_loss time
0 1918.483398 3197.017578 00:12
1 106.539482 94.487633 00:12
2 33.473465 51.522110 00:13
3 27.788330 44.613903 00:12
4 26.346468 43.765388 00:12
5 24.332930 40.168346 00:13
6 22.658882 43.689201 00:12
7 21.428637 43.339996 00:13
8 20.970291 40.491524 00:14
9 19.334044 42.483467 00:13
10 19.594343 43.086784 00:13
11 17.809017 43.580494 00:13
12 16.891586 44.859009 00:12
13 16.655016 42.335251 00:12
14 16.358154 42.752422 00:14
15 16.382271 42.794182 00:13
epoch train_loss valid_loss time
0 3088.579346 2704.017822 00:14
1 371.510559 395.678558 00:15
2 89.183846 94.844002 00:15
3 78.834976 97.243515 00:14
4 74.536369 120.522392 00:15
5 63.672607 82.953667 00:15
6 59.640804 85.300270 00:15
7 57.323330 77.391953 00:15
8 54.513256 74.703590 00:14
9 51.521042 86.552254 00:15
10 48.603882 82.986351 00:15
11 46.393955 75.914795 00:14
12 45.324986 79.105553 00:15
13 44.271095 79.203072 00:14
14 44.234680 76.418030 00:14
15 43.255112 74.440346 00:15
epoch train_loss valid_loss time
0 1522.577148 2891.114990 00:14
1 125.203377 274.183868 00:15
2 36.977863 100.894608 00:11
3 31.134560 87.945694 00:14
4 28.394548 73.625877 00:15
5 26.845119 75.816299 00:14
6 23.084717 73.610870 00:14
7 23.182734 67.326370 00:14
8 20.494146 70.074257 00:14
9 20.534546 74.101440 00:15
10 18.696215 69.963242 00:14
11 18.074734 70.161385 00:15
12 16.931160 68.944092 00:13
13 16.421127 69.088921 00:14
14 16.183321 69.261986 00:14
15 16.043547 68.805428 00:14
epoch train_loss valid_loss time
0 4166.456055 7456.684082 00:17
1 148.912262 131.713806 00:14
2 84.211311 89.738197 00:14
3 88.657539 106.802567 00:15
4 73.954124 112.972267 00:15
5 67.365410 73.485825 00:15
6 59.275486 73.756104 00:15
7 56.476234 67.226044 00:15
8 53.836773 70.530540 00:13
9 51.957397 67.051041 00:15
10 49.541191 67.395409 00:14
11 45.644688 67.668068 00:15
12 45.104977 69.366867 00:16
13 44.234295 66.431992 00:15
14 44.004173 68.562370 00:15
15 42.839439 66.396904 00:15
epoch train_loss valid_loss time
0 1744.674683 3726.719971 00:15
1 121.919487 451.102600 00:15
2 50.071571 192.743378 00:15
3 30.977331 121.338982 00:16
4 29.078804 107.513428 00:16
5 27.684288 90.804955 00:14
6 25.089111 91.261833 00:16
7 24.139738 92.008888 00:13
8 22.452591 91.237976 00:15
9 21.153240 86.473862 00:15
10 20.343084 85.188683 00:16
11 18.962919 85.287933 00:15
12 18.682301 83.847382 00:15
13 18.138475 82.454552 00:16
14 17.603161 83.941856 00:15
15 17.368151 83.425362 00:16
epoch train_loss valid_loss time
0 2438.275146 1545.411377 00:17
1 265.113831 302.674377 00:17
2 89.039391 104.442749 00:17
3 79.027908 66.114830 00:16
4 73.917580 71.415833 00:17
5 62.791897 59.743301 00:18
6 61.536259 60.322983 00:16
7 56.388382 58.034599 00:16
8 53.285770 57.329102 00:16
9 52.855766 56.106373 00:15
10 50.596954 55.296417 00:17
11 48.833817 54.825047 00:17
12 46.613522 56.708889 00:17
13 44.668262 56.256340 00:17
14 44.222977 55.261826 00:16
15 44.248558 55.314072 00:17