LightGBM: Unexpected Behavior for Early Stopping with Custom Metric

Hello -

When passing a custom metric to feval argument in Booster.train(), I’m getting what I believe to be unexpected behavior. Or what I presume to be undesirable. The Booster is not returning the best iteration as determined by the validation set as can be seen with the Reproducible example and Output below.

Environment:

lightgbm: 2.2.3 os: Linux CPU ami: 4.14.138-114.102.amzn2.x86_64

Reproducible example

import numpy as np
import lightgbm as lgb

np.random.seed(90210)
N = 100
x = np.random.uniform(size = (N, 7))

lp = (x[:,0]*1.34 + x[:,1]*x[:,2]*0.89 + x[:,2]*0.05 + x[:,2]*x[:,3]*1.34 + x[:,3]*0.31
      + np.random.normal(loc = 2, scale = 0.5, size = 100))

y = np.exp(lp - lp.mean()) / (1 + np.exp(lp - lp.mean()))
y = (y > 0.5).astype(np.uint8)

in_trn = np.random.binomial(1, p = 0.6, size = 100).astype(np.bool)
x_trn = x[ in_trn]
x_val = x[~in_trn]
y_trn = y[ in_trn]
y_val = y[~in_trn]

l_trn = lgb.Dataset(x_trn, y_trn)
l_val = lgb.Dataset(x_val, y_val)

def feval(prd, dmat):
    act = dmat.get_label()
    cls = (prd > 0.5).astype(np.uint8)
    tp = (cls * act).sum()
    fp = ((cls == 1) & (act == 0)).sum()
    savings = (100 * tp -75 * fp) / len(act)
    return 'savings', savings, True

params = {
        'learning_rate':0.05,
        'reg_lambda': 1,
        'feature_fraction': 0.75,
        'bagging_fraction': 0.7,
        'bagging_freq': 1,
        'boosting_type': 'gbdt',
        'objective': 'binary',
        'reg_alpha': 2,
        'num_leaves':  31,
        'min_data_in_leaf': 1,
        'feature_fraction_seed': 201,
        'bagging_seed': 427,
        'metric': 'None',
        }

mod = lgb.train(params, l_trn, num_boost_round = 1000, valid_sets = [l_trn,l_val],
                        valid_names = ['trn','val'], early_stopping_rounds = 100,
                        verbose_eval = 1, feval = feval)

Output

[1] trn’s savings: 16.6667 val’s savings: 5.40541 Training until validation scores don’t improve for 100 rounds. [2] trn’s savings: 30.5556 val’s savings: 21.6216 [3] trn’s savings: 34.127 val’s savings: 14.8649 [4] trn’s savings: 36.9048 val’s savings: 14.8649 [5] trn’s savings: 36.1111 val’s savings: 16.8919 [6] trn’s savings: 34.5238 val’s savings: 18.2432 [7] trn’s savings: 35.7143 val’s savings: 18.2432 [8] trn’s savings: 34.5238 val’s savings: 20.2703 [9] trn’s savings: 32.1429 val’s savings: 18.2432 [10] trn’s savings: 36.9048 val’s savings: 20.2703 [11] trn’s savings: 40.0794 val’s savings: 20.2703 [12] trn’s savings: 44.8413 val’s savings: 22.2973 [13] trn’s savings: 44.8413 val’s savings: 19.5946 [14] trn’s savings: 43.254 val’s savings: 19.5946 [15] trn’s savings: 44.8413 val’s savings: 21.6216 [16] trn’s savings: 44.8413 val’s savings: 21.6216 [17] trn’s savings: 43.6508 val’s savings: 21.6216 [18] trn’s savings: 42.0635 val’s savings: 21.6216 [19] trn’s savings: 44.8413 val’s savings: 19.5946 [20] trn’s savings: 44.8413 val’s savings: 21.6216 [21] trn’s savings: 46.0317 val’s savings: 21.6216 [22] trn’s savings: 46.0317 val’s savings: 21.6216

… … [110] trn’s savings: 44.4444 val’s savings: 23.6486 [111] trn’s savings: 43.254 val’s savings: 23.6486 [112] trn’s savings: 43.254 val’s savings: 23.6486 [113] trn’s savings: 43.254 val’s savings: 23.6486 [114] trn’s savings: 43.254 val’s savings: 23.6486 [115] trn’s savings: 43.254 val’s savings: 23.6486 [116] trn’s savings: 43.254 val’s savings: 23.6486 [117] trn’s savings: 43.254 val’s savings: 23.6486 [118] trn’s savings: 43.254 val’s savings: 23.6486 [119] trn’s savings: 43.254 val’s savings: 23.6486 [120] trn’s savings: 43.254 val’s savings: 23.6486 [121] trn’s savings: 43.254 val’s savings: 23.6486 Early stopping, best iteration is: [21] trn’s savings: 46.0317 val’s savings: 21.6216

Other Notes When I remove the training set from valid_sets and update valid_names accordingly, I do indeed get the correct iteration returned.

mod = lgb.train(params, l_trn, num_boost_round = 1000, valid_sets = l_val,
                        valid_names = ['val'], early_stopping_rounds = 100,
                        verbose_eval = 1, feval = feval)

[1] val’s savings: 5.40541 Training until validation scores don’t improve for 100 rounds. [2] val’s savings: 21.6216 [3] val’s savings: 14.8649 [4] val’s savings: 14.8649 [5] val’s savings: 16.8919 [6] val’s savings: 18.2432 [7] val’s savings: 18.2432 [8] val’s savings: 20.2703 [9] val’s savings: 18.2432 [10] val’s savings: 20.2703 [11] val’s savings: 20.2703 … … [72] val’s savings: 21.6216 [73] val’s savings: 21.6216 [74] val’s savings: 21.6216 [75] val’s savings: 21.6216 [76] val’s savings: 21.6216 [77] val’s savings: 21.6216 [78] val’s savings: 21.6216 [79] val’s savings: 23.6486 [80] val’s savings: 23.6486 [81] val’s savings: 23.6486 [82] val’s savings: 23.6486 [83] val’s savings: 23.6486 [84] val’s savings: 23.6486 … … [172] val’s savings: 23.6486 [173] val’s savings: 23.6486 [174] val’s savings: 23.6486 [175] val’s savings: 23.6486 [176] val’s savings: 23.6486 [177] val’s savings: 23.6486 [178] val’s savings: 23.6486 [179] val’s savings: 23.6486 Early stopping, best iteration is: [79] val’s savings: 23.6486

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Comments: 18 (3 by maintainers)

Most upvoted comments

@guolinke Nice! #2209 starts the work towards that:

if (((env.evaluation_result_list[i][0] == "cv_agg" 
       and env.evaluation_result_list[i][1].split(" ")[0] == "train")
      or env.evaluation_result_list[i][0] == env.model._train_data_name)):
     continue  # train data for lgb.cv or sklearn wrapper (underlying lgb.train)

After merging it I’ll help with the rest Python code.

@StrikerRUS yeah, I think we can do it.

@guolinke What about cpp code? Is it easy to ignore training data there? For R-package I guess we need to create a separate feature request issue, as we have some delay in the R-package development.