Commit f3d33582 authored by wxchan's avatar wxchan Committed by Guolin Ke
Browse files

re-define callback order (#114)

current problem:
callback order: user-defined in callback parameter -> reset_learning_rate/print_evaluation/record_evaluation/early_stop
user can't insert a callback between last 4 callbacks

solution:
set order variable:
reset_learning_rate = 10
print_evaluation = 10
record_evaluation = 20
early_stop = 30
user-defined = some int
default: according to index in callback parameter list, = index - len(callbacks) (all < 0)

current callback order:
before iter: user-defined -> reset_learning_rate -> user-defined
after iter: user-defined -> print_evaluation -> user-defined -> record_evaluation -> user-defined -> early_stop -> user-defined
parent 6a792d16
...@@ -362,3 +362,6 @@ ENV/ ...@@ -362,3 +362,6 @@ ENV/
# Rope project settings # Rope project settings
.ropeproject .ropeproject
# macOS
.DS_Store
# coding: utf-8 # coding: utf-8
# pylint: disable = invalid-name, W0105 # pylint: disable = invalid-name, W0105, C0301
from __future__ import absolute_import from __future__ import absolute_import
import collections import collections
import inspect
class EarlyStopException(Exception): class EarlyStopException(Exception):
"""Exception of early stopping. """Exception of early stopping.
...@@ -57,10 +58,11 @@ def print_evaluation(period=1, show_stdv=True): ...@@ -57,10 +58,11 @@ def print_evaluation(period=1, show_stdv=True):
"""internal function""" """internal function"""
if not env.evaluation_result_list or period <= 0: if not env.evaluation_result_list or period <= 0:
return return
if env.iteration % period == 0 or env.iteration + 1 == env.begin_iteration: if (env.iteration + 1) % period == 0:
result = '\t'.join([_format_eval_result(x, show_stdv) \ result = '\t'.join([_format_eval_result(x, show_stdv) \
for x in env.evaluation_result_list]) for x in env.evaluation_result_list])
print('[%d]\t%s' % (env.iteration, result)) print('[%d]\t%s' % (env.iteration + 1, result))
callback.order = 10
return callback return callback
...@@ -92,6 +94,7 @@ def record_evaluation(eval_result): ...@@ -92,6 +94,7 @@ def record_evaluation(eval_result):
init(env) init(env)
for data_name, eval_name, result, _ in env.evaluation_result_list: for data_name, eval_name, result, _ in env.evaluation_result_list:
eval_result[data_name][eval_name].append(result) eval_result[data_name][eval_name].append(result)
callback.order = 20
return callback return callback
...@@ -108,8 +111,8 @@ def reset_learning_rate(learning_rates): ...@@ -108,8 +111,8 @@ def reset_learning_rate(learning_rates):
current number of round and the total number of boosting round \ current number of round and the total number of boosting round \
(e.g. yields learning rate decay) (e.g. yields learning rate decay)
- list l: learning_rate = l[current_round] - list l: learning_rate = l[current_round]
- function f: learning_rate = f(current_round, total_boost_round) - function f: learning_rate = f(current_round, total_boost_round) \
or learning_rate = f(current_round)
Returns Returns
------- -------
callback : function callback : function
...@@ -117,15 +120,21 @@ def reset_learning_rate(learning_rates): ...@@ -117,15 +120,21 @@ def reset_learning_rate(learning_rates):
""" """
def callback(env): def callback(env):
"""internal function""" """internal function"""
booster = env.model
iteration = env.iteration
if isinstance(learning_rates, list): if isinstance(learning_rates, list):
if len(learning_rates) != env.end_iteration: if len(learning_rates) != env.end_iteration - env.begin_iteration:
raise ValueError("Length of list 'learning_rates' has to equal 'num_boost_round'.") raise ValueError("Length of list 'learning_rates' has to equal to 'num_boost_round'.")
booster.reset_parameter({'learning_rate':learning_rates[iteration]}) env.model.reset_parameter({'learning_rate':learning_rates[env.iteration]})
else: else:
booster.reset_parameter({'learning_rate':learning_rates(iteration, env.end_iteration)}) argc = len(inspect.getargspec(learning_rates).args)
if argc is 1:
env.model.reset_parameter({"learning_rate": learning_rates(env.iteration - env.begin_iteration)})
elif argc is 2:
env.model.reset_parameter({"learning_rate": \
learning_rates(env.iteration - env.begin_iteration, env.end_iteration - env.begin_iteration)})
else:
raise ValueError("Self-defined function 'learning_rates' should have 1 or 2 arguments")
callback.before_iteration = True callback.before_iteration = True
callback.order = 10
return callback return callback
...@@ -178,7 +187,7 @@ def early_stop(stopping_rounds, verbose=True): ...@@ -178,7 +187,7 @@ def early_stop(stopping_rounds, verbose=True):
best_score[i] = score best_score[i] = score
best_iter[i] = env.iteration best_iter[i] = env.iteration
if verbose: if verbose:
best_msg[i] = '[%d]\t%s' % (env.iteration, \ best_msg[i] = '[%d]\t%s' % (env.iteration + 1, \
'\t'.join([_format_eval_result(x) for x in env.evaluation_result_list])) '\t'.join([_format_eval_result(x) for x in env.evaluation_result_list]))
else: else:
if env.iteration - best_iter[i] >= stopping_rounds: if env.iteration - best_iter[i] >= stopping_rounds:
...@@ -188,4 +197,5 @@ def early_stop(stopping_rounds, verbose=True): ...@@ -188,4 +197,5 @@ def early_stop(stopping_rounds, verbose=True):
print('early stopping, best iteration is:') print('early stopping, best iteration is:')
print(best_msg[i]) print(best_msg[i])
raise EarlyStopException(best_iter[i]) raise EarlyStopException(best_iter[i])
callback.order = 30
return callback return callback
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
"""Training Library containing training routines of LightGBM.""" """Training Library containing training routines of LightGBM."""
from __future__ import absolute_import from __future__ import absolute_import
import collections
from operator import attrgetter
import numpy as np import numpy as np
from .basic import LightGBMError, Predictor, Dataset, Booster, is_str from .basic import LightGBMError, Predictor, Dataset, Booster, is_str
from . import callback from . import callback
...@@ -20,7 +22,7 @@ def _construct_dataset(X_y, reference=None, ...@@ -20,7 +22,7 @@ def _construct_dataset(X_y, reference=None,
init_score = None init_score = None
if other_fields is not None: if other_fields is not None:
if not isinstance(other_fields, dict): if not isinstance(other_fields, dict):
raise TypeError("other filed data should be dict type") raise TypeError("type of other filed data should be dict")
weight = other_fields.get('weight', None) weight = other_fields.get('weight', None)
group = other_fields.get('group', None) group = other_fields.get('group', None)
init_score = other_fields.get('init_score', None) init_score = other_fields.get('init_score', None)
...@@ -29,7 +31,7 @@ def _construct_dataset(X_y, reference=None, ...@@ -29,7 +31,7 @@ def _construct_dataset(X_y, reference=None,
label = None label = None
else: else:
if len(X_y) != 2: if len(X_y) != 2:
raise TypeError("should pass (data, label) pair") raise TypeError("should pass (data, label) tuple for dataset")
data = X_y[0] data = X_y[0]
label = X_y[1] label = X_y[1]
if reference is None: if reference is None:
...@@ -114,7 +116,8 @@ def train(params, train_data, num_boost_round=100, ...@@ -114,7 +116,8 @@ def train(params, train_data, num_boost_round=100,
current number of round and the total number of boosting round \ current number of round and the total number of boosting round \
(e.g. yields learning rate decay) (e.g. yields learning rate decay)
- list l: learning_rate = l[current_round] - list l: learning_rate = l[current_round]
- function f: learning_rate = f(current_round, total_boost_round) - function f: learning_rate = f(current_round, total_boost_round) \
or learning_rate = f(current_round)
callbacks : list of callback functions callbacks : list of callback functions
List of callback functions that are applied at end of each iteration. List of callback functions that are applied at end of each iteration.
...@@ -148,7 +151,7 @@ def train(params, train_data, num_boost_round=100, ...@@ -148,7 +151,7 @@ def train(params, train_data, num_boost_round=100,
train_data_name = "training" train_data_name = "training"
valid_sets = [] valid_sets = []
name_valid_sets = [] name_valid_sets = []
if valid_datas is not None: if valid_datas:
if isinstance(valid_datas, (Dataset, tuple)): if isinstance(valid_datas, (Dataset, tuple)):
valid_datas = [valid_datas] valid_datas = [valid_datas]
if isinstance(valid_names, str): if isinstance(valid_names, str):
...@@ -180,35 +183,43 @@ def train(params, train_data, num_boost_round=100, ...@@ -180,35 +183,43 @@ def train(params, train_data, num_boost_round=100,
name_valid_sets.append(valid_names[i]) name_valid_sets.append(valid_names[i])
else: else:
name_valid_sets.append('valid_'+str(i)) name_valid_sets.append('valid_'+str(i))
"""process callbacks""" """process callbacks"""
callbacks = [] if callbacks is None else callbacks if not callbacks:
callbacks = set()
else:
for i, cb in enumerate(callbacks):
cb.__dict__.setdefault('order', i - len(callbacks))
callbacks = set(callbacks)
# Most of legacy advanced options becomes callbacks # Most of legacy advanced options becomes callbacks
if isinstance(verbose_eval, bool) and verbose_eval: if verbose_eval is True:
callbacks.append(callback.print_evaluation()) callbacks.add(callback.print_evaluation())
elif isinstance(verbose_eval, int): elif isinstance(verbose_eval, int):
callbacks.append(callback.print_evaluation(verbose_eval)) callbacks.add(callback.print_evaluation(verbose_eval))
if early_stopping_rounds is not None: if early_stopping_rounds:
callbacks.append(callback.early_stop(early_stopping_rounds, callbacks.add(callback.early_stop(early_stopping_rounds,
verbose=bool(verbose_eval))) verbose=bool(verbose_eval)))
if learning_rates is not None: if learning_rates is not None:
callbacks.append(callback.reset_learning_rate(learning_rates)) callbacks.add(callback.reset_learning_rate(learning_rates))
if evals_result is not None: if evals_result is not None:
callbacks.append(callback.record_evaluation(evals_result)) callbacks.add(callback.record_evaluation(evals_result))
callbacks_before_iter = {cb for cb in callbacks if getattr(cb, 'before_iteration', False)}
callbacks_after_iter = callbacks - callbacks_before_iter
callbacks_before_iter = sorted(callbacks_before_iter, key=attrgetter('order'))
callbacks_after_iter = sorted(callbacks_after_iter, key=attrgetter('order'))
callbacks_before_iter = [
cb for cb in callbacks if cb.__dict__.get('before_iteration', False)]
callbacks_after_iter = [
cb for cb in callbacks if not cb.__dict__.get('before_iteration', False)]
"""construct booster""" """construct booster"""
booster = Booster(params=params, train_set=train_set) booster = Booster(params=params, train_set=train_set)
if is_valid_contain_train: if is_valid_contain_train:
booster.set_train_data_name(train_data_name) booster.set_train_data_name(train_data_name)
for valid_set, name_valid_set in zip(valid_sets, name_valid_sets): for valid_set, name_valid_set in zip(valid_sets, name_valid_sets):
booster.add_valid(valid_set, name_valid_set) booster.add_valid(valid_set, name_valid_set)
"""start training""" """start training"""
for i in range(init_iteration, init_iteration + num_boost_round): for i in range(init_iteration, init_iteration + num_boost_round):
for cb in callbacks_before_iter: for cb in callbacks_before_iter:
...@@ -271,7 +282,7 @@ except ImportError: ...@@ -271,7 +282,7 @@ except ImportError:
except ImportError: except ImportError:
SKLEARN_StratifiedKFold = False SKLEARN_StratifiedKFold = False
def _make_n_folds(full_data, nfold, param, seed, fpreproc=None, stratified=False): def _make_n_folds(full_data, nfold, params, seed, fpreproc=None, stratified=False):
""" """
Make an n-fold list of CVBooster from random indices. Make an n-fold list of CVBooster from random indices.
""" """
...@@ -293,9 +304,9 @@ def _make_n_folds(full_data, nfold, param, seed, fpreproc=None, stratified=False ...@@ -293,9 +304,9 @@ def _make_n_folds(full_data, nfold, param, seed, fpreproc=None, stratified=False
valid_set = full_data.subset(idset[k]) valid_set = full_data.subset(idset[k])
# run preprocessing on the data set if needed # run preprocessing on the data set if needed
if fpreproc is not None: if fpreproc is not None:
train_set, valid_set, tparam = fpreproc(train_set, valid_set, param.copy()) train_set, valid_set, tparam = fpreproc(train_set, valid_set, params.copy())
else: else:
tparam = param tparam = params
ret.append(CVBooster(train_set, valid_set, tparam)) ret.append(CVBooster(train_set, valid_set, tparam))
return ret return ret
...@@ -303,21 +314,13 @@ def _agg_cv_result(raw_results): ...@@ -303,21 +314,13 @@ def _agg_cv_result(raw_results):
""" """
Aggregate cross-validation results. Aggregate cross-validation results.
""" """
cvmap = {} cvmap = collections.defaultdict(list)
metric_type = {} metric_type = {}
for one_result in raw_results: for one_result in raw_results:
for one_line in one_result: for one_line in one_result:
key = one_line[1] metric_type[one_line[1]] = one_line[3]
metric_type[key] = one_line[3] cvmap[one_line[1]].append(one_line[2])
if key not in cvmap: return [('cv_agg', k, np.mean(v), metric_type[k], np.std(v)) for k, v in cvmap.items()]
cvmap[key] = []
cvmap[key].append(one_line[2])
results = []
for k, v in cvmap.items():
v = np.array(v)
mean, std = np.mean(v), np.std(v)
results.append(('cv_agg', k, mean, metric_type[k], std))
return results
def cv(params, train_data, num_boost_round=10, nfold=5, stratified=False, def cv(params, train_data, num_boost_round=10, nfold=5, stratified=False,
metrics=(), fobj=None, feval=None, train_fields=None, metrics=(), fobj=None, feval=None, train_fields=None,
...@@ -331,7 +334,7 @@ def cv(params, train_data, num_boost_round=10, nfold=5, stratified=False, ...@@ -331,7 +334,7 @@ def cv(params, train_data, num_boost_round=10, nfold=5, stratified=False,
---------- ----------
params : dict params : dict
Booster params. Booster params.
train_data : pair, (X, y) or filename of data train_data : tuple (X, y) or filename of data
Data to be trained. Data to be trained.
num_boost_round : int num_boost_round : int
Number of boosting iterations. Number of boosting iterations.
...@@ -391,23 +394,27 @@ def cv(params, train_data, num_boost_round=10, nfold=5, stratified=False, ...@@ -391,23 +394,27 @@ def cv(params, train_data, num_boost_round=10, nfold=5, stratified=False,
feature_name=feature_name, feature_name=feature_name,
categorical_feature=categorical_feature) categorical_feature=categorical_feature)
results = {} results = collections.defaultdict(list)
cvfolds = _make_n_folds(train_set, nfold, params, seed, fpreproc, stratified) cvfolds = _make_n_folds(train_set, nfold, params, seed, fpreproc, stratified)
# setup callbacks # setup callbacks
callbacks = [] if callbacks is None else callbacks if not callbacks:
if early_stopping_rounds is not None: callbacks = set()
callbacks.append(callback.early_stop(early_stopping_rounds, else:
verbose=False)) for i, cb in enumerate(callbacks):
if isinstance(verbose_eval, bool) and verbose_eval: cb.__dict__.setdefault('order', i - len(callbacks))
callbacks.append(callback.print_evaluation(show_stdv=show_stdv)) callbacks = set(callbacks)
if early_stopping_rounds:
callbacks.add(callback.early_stop(early_stopping_rounds, verbose=False))
if verbose_eval is True:
callbacks.add(callback.print_evaluation(show_stdv=show_stdv))
elif isinstance(verbose_eval, int): elif isinstance(verbose_eval, int):
callbacks.append(callback.print_evaluation(verbose_eval, show_stdv=show_stdv)) callbacks.add(callback.print_evaluation(verbose_eval, show_stdv=show_stdv))
callbacks_before_iter = [ callbacks_before_iter = {cb for cb in callbacks if getattr(cb, 'before_iteration', False)}
cb for cb in callbacks if cb.__dict__.get('before_iteration', False)] callbacks_after_iter = callbacks - callbacks_before_iter
callbacks_after_iter = [ callbacks_before_iter = sorted(callbacks_before_iter, key=attrgetter('order'))
cb for cb in callbacks if not cb.__dict__.get('before_iteration', False)] callbacks_after_iter = sorted(callbacks_after_iter, key=attrgetter('order'))
for i in range(num_boost_round): for i in range(num_boost_round):
for cb in callbacks_before_iter: for cb in callbacks_before_iter:
...@@ -421,12 +428,8 @@ def cv(params, train_data, num_boost_round=10, nfold=5, stratified=False, ...@@ -421,12 +428,8 @@ def cv(params, train_data, num_boost_round=10, nfold=5, stratified=False,
fold.update(fobj) fold.update(fobj)
res = _agg_cv_result([f.eval(feval) for f in cvfolds]) res = _agg_cv_result([f.eval(feval) for f in cvfolds])
for _, key, mean, _, std in res: for _, key, mean, _, std in res:
if key + '-mean' not in results:
results[key + '-mean'] = []
if key + '-std' not in results:
results[key + '-std'] = []
results[key + '-mean'].append(mean) results[key + '-mean'].append(mean)
results[key + '-std'].append(std) results[key + '-stdv'].append(std)
try: try:
for cb in callbacks_after_iter: for cb in callbacks_after_iter:
cb(callback.CallbackEnv(model=None, cb(callback.CallbackEnv(model=None,
...@@ -437,6 +440,6 @@ def cv(params, train_data, num_boost_round=10, nfold=5, stratified=False, ...@@ -437,6 +440,6 @@ def cv(params, train_data, num_boost_round=10, nfold=5, stratified=False,
evaluation_result_list=res)) evaluation_result_list=res))
except callback.EarlyStopException as e: except callback.EarlyStopException as e:
for k in results: for k in results:
results[k] = results[k][:(e.best_iteration + 1)] results[k] = results[k][:e.best_iteration + 1]
break break
return results return dict(results)
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment