-
Notifications
You must be signed in to change notification settings - Fork 332
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add lightgbm.booster support #270
base: master
Are you sure you want to change the base?
Changes from all commits
d369977
cc5b7a4
9c2bb8e
9c31a99
a042650
72a50a2
bb19a4f
4bf2f6d
ef991cf
45379f5
a35e5c8
0bbf3e2
fa1eccd
f053399
87438d4
3c213e9
866015a
802e543
5155979
6f1ee40
0a488e3
19e9bf3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -5,13 +5,12 @@ LightGBM | |||||
|
||||||
LightGBM_ is a fast Gradient Boosting framework; it provides a Python | ||||||
interface. eli5 supports :func:`eli5.explain_weights` | ||||||
and :func:`eli5.explain_prediction` for ``lightgbm.LGBMClassifer`` | ||||||
and ``lightgbm.LGBMRegressor`` estimators. | ||||||
and :func:`eli5.explain_prediction` for ``lightgbm.LGBMClassifer``, ``lightgbm.LGBMRegressor`` and ``lightgbm.Booster`` estimators. | ||||||
|
||||||
.. _LightGBM: https://github.com/Microsoft/LightGBM | ||||||
|
||||||
:func:`eli5.explain_weights` uses feature importances. Additional | ||||||
arguments for LGBMClassifier and LGBMClassifier: | ||||||
arguments for LGBMClassifier , LGBMClassifier and lightgbm.Booster: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
* ``importance_type`` is a way to get feature importance. Possible values are: | ||||||
|
||||||
|
@@ -22,7 +21,7 @@ arguments for LGBMClassifier and LGBMClassifier: | |||||
- 'weight' - the same as 'split', for better compatibility with | ||||||
:ref:`library-xgboost`. | ||||||
|
||||||
``target_names`` and ``target`` arguments are ignored. | ||||||
``target_names`` arguement is ignored for ``lightgbm.LGBMClassifer`` / ``lightgbm.LGBMRegressor``, but used for ``lightgbm.Booster``. ``target`` argument is ignored. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
.. note:: | ||||||
Top-level :func:`eli5.explain_weights` calls are dispatched | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think lightgbm.Booster should be mentioned here as well. |
||||||
|
@@ -37,7 +36,7 @@ contribution of a feature on the decision path is how much the score changes | |||||
from parent to child. | ||||||
|
||||||
Additional :func:`eli5.explain_prediction` keyword arguments supported | ||||||
for ``lightgbm.LGBMClassifer`` and ``lightgbm.LGBMRegressor``: | ||||||
for ``lightgbm.LGBMClassifer``, ``lightgbm.LGBMRegressor`` and ``lightgbm.Booster``: | ||||||
|
||||||
* ``vec`` is a vectorizer instance used to transform | ||||||
raw features to the input of the estimator ``lgb`` | ||||||
|
@@ -50,6 +49,14 @@ for ``lightgbm.LGBMClassifer`` and ``lightgbm.LGBMRegressor``: | |||||
estimator. Set it to True if you're passing ``vec``, | ||||||
but ``doc`` is already vectorized. | ||||||
|
||||||
``lightgbm.Booster`` estimator accepts one more optional argument: | ||||||
|
||||||
* ``is_regression`` - True if solving a regression problem | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this parameter supported? It is not an argument of explain_prediction_lightgbm. |
||||||
("objective" starts with "reg") | ||||||
and False for a classification problem. | ||||||
If not set, regression is assumed for a single target estimator | ||||||
and proba will not be shown unless the ``target_names`` is defined as a list with length of two. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems to contradict a note above (line 25, "target_names is ignored") - does the note about target_names apply only to classifier/regressor, but not for Booster? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for a single target estimater, booster cannot recognize wheather it is a regression problem or not. We assume it is regression in default, unless users set it as a classification problem by assigning 'target names' input [0,1] etc. Only in this case 'target names' is used. Should I remove " target names is ignored" to prevent confusion? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. target names is still ignored for classifier/regressor, right? I think it makes sense to clarify it - just say that it is ignored for LGBMClassifier / LGBMRegressor, but used for lightgbm.Booster. "is defined as a list with length of two" - sohuld it be 2 elements, or 2+ is also supported? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, for single target booster, only 1+ elements target_name point booster to classification. Otherwise, this booster is regression . there is risk user assign wrong number of elements like 3, but not sure how eli5 will raise error. 2+ should not support here. |
||||||
|
||||||
.. note:: | ||||||
Top-level :func:`eli5.explain_prediction` calls are dispatched | ||||||
to :func:`eli5.xgboost.explain_prediction_lightgbm` for | ||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -31,7 +31,7 @@ following machine learning frameworks and packages: | |||||
of XGBClassifier, XGBRegressor and xgboost.Booster. | ||||||
|
||||||
* :ref:`library-lightgbm` - show feature importances and explain predictions | ||||||
of LGBMClassifier and LGBMRegressor. | ||||||
of LGBMClassifier , LGBMRegressor and lightgbm.Booster. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
* :ref:`library-lightning` - explain weights and predictions of lightning | ||||||
classifiers and regressors. | ||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,7 +1,7 @@ | ||||||
# -*- coding: utf-8 -*- | ||||||
from __future__ import absolute_import, division | ||||||
from collections import defaultdict | ||||||
from typing import DefaultDict | ||||||
from typing import DefaultDict, Any, Tuple | ||||||
|
||||||
import numpy as np # type: ignore | ||||||
import lightgbm # type: ignore | ||||||
|
@@ -17,7 +17,7 @@ | |||||
all values sum to 1. | ||||||
""" | ||||||
|
||||||
|
||||||
@explain_weights.register(lightgbm.Booster) | ||||||
@explain_weights.register(lightgbm.LGBMClassifier) | ||||||
@explain_weights.register(lightgbm.LGBMRegressor) | ||||||
def explain_weights_lightgbm(lgb, | ||||||
|
@@ -32,13 +32,15 @@ def explain_weights_lightgbm(lgb, | |||||
): | ||||||
""" | ||||||
Return an explanation of an LightGBM estimator (via scikit-learn wrapper | ||||||
LGBMClassifier or LGBMRegressor) as feature importances. | ||||||
LGBMClassifier or LGBMRegressor, or via lightgbm.Booster) as feature importances. | ||||||
|
||||||
See :func:`eli5.explain_weights` for description of | ||||||
``top``, ``feature_names``, | ||||||
``feature_re`` and ``feature_filter`` parameters. | ||||||
|
||||||
``target_names`` and ``targets`` parameters are ignored. | ||||||
``target_names`` arguement is ignored for ``lightgbm.LGBMClassifer`` / ``lightgbm.LGBMRegressor``, | ||||||
but used for ``lightgbm.Booster``. | ||||||
``target`` argument is ignored. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
Parameters | ||||||
---------- | ||||||
|
@@ -51,8 +53,9 @@ def explain_weights_lightgbm(lgb, | |||||
across all trees | ||||||
- 'weight' - the same as 'split', for compatibility with xgboost | ||||||
""" | ||||||
coef = _get_lgb_feature_importances(lgb, importance_type) | ||||||
lgb_feature_names = lgb.booster_.feature_name() | ||||||
booster, is_regression = _check_booster_args(lgb) | ||||||
coef = _get_lgb_feature_importances(booster, importance_type) | ||||||
lgb_feature_names = booster.feature_name() | ||||||
return get_feature_importance_explanation(lgb, vec, coef, | ||||||
feature_names=feature_names, | ||||||
estimator_feature_names=lgb_feature_names, | ||||||
|
@@ -64,7 +67,7 @@ def explain_weights_lightgbm(lgb, | |||||
is_regression=isinstance(lgb, lightgbm.LGBMRegressor), | ||||||
) | ||||||
|
||||||
|
||||||
@explain_prediction.register(lightgbm.Booster) | ||||||
@explain_prediction.register(lightgbm.LGBMClassifier) | ||||||
@explain_prediction.register(lightgbm.LGBMRegressor) | ||||||
def explain_prediction_lightgbm( | ||||||
|
@@ -80,7 +83,7 @@ def explain_prediction_lightgbm( | |||||
vectorized=False, | ||||||
): | ||||||
""" Return an explanation of LightGBM prediction (via scikit-learn wrapper | ||||||
LGBMClassifier or LGBMRegressor) as feature weights. | ||||||
LGBMClassifier or LGBMRegressor, or via lightgbm.Booster) as feature weights. | ||||||
|
||||||
See :func:`eli5.explain_prediction` for description of | ||||||
``top``, ``top_targets``, ``target_names``, ``targets``, | ||||||
|
@@ -108,20 +111,49 @@ def explain_prediction_lightgbm( | |||||
Weights of all features sum to the output score of the estimator. | ||||||
""" | ||||||
|
||||||
vec, feature_names = handle_vec(lgb, doc, vec, vectorized, feature_names) | ||||||
booster, is_regression = _check_booster_args(lgb) | ||||||
lgb_feature_names = booster.feature_name() | ||||||
vec, feature_names = handle_vec(lgb, doc, vec, vectorized, feature_names, | ||||||
num_features=len(lgb_feature_names)) | ||||||
if feature_names.bias_name is None: | ||||||
# LightGBM estimators do not have an intercept, but here we interpret | ||||||
# them as having an intercept | ||||||
feature_names.bias_name = '<BIAS>' | ||||||
X = get_X(doc, vec, vectorized=vectorized) | ||||||
|
||||||
if isinstance(lgb, lightgbm.Booster): | ||||||
prediction = lgb.predict(X) | ||||||
n_targets = prediction.shape[-1] | ||||||
if is_regression is None and target_names is None: | ||||||
# When n_targets is 1, this can be classification too. | ||||||
# It's safer to assume regression in this case, | ||||||
# unless users set it as a classification problem by assigning 'target_names' input [0,1] etc. | ||||||
# If n_targets > 1, it must be classification. | ||||||
is_regression = n_targets == 1 | ||||||
elif is_regression is None: | ||||||
is_regression = len(target_names) == 1 and n_targets == 1 | ||||||
|
||||||
if is_regression: | ||||||
proba = None | ||||||
else: | ||||||
if n_targets == 1: | ||||||
p, = prediction | ||||||
proba = np.array([1 - p, p]) | ||||||
else: | ||||||
proba, = prediction | ||||||
else: | ||||||
proba = predict_proba(lgb, X) | ||||||
n_targets = _lgb_n_targets(lgb) | ||||||
|
||||||
proba = predict_proba(lgb, X) | ||||||
weight_dicts = _get_prediction_feature_weights(lgb, X, _lgb_n_targets(lgb)) | ||||||
x = get_X0(add_intercept(X)) | ||||||
if is_regression: | ||||||
names = ['y'] | ||||||
elif isinstance(lgb, lightgbm.Booster): | ||||||
names = np.arange(max(2, n_targets)) | ||||||
else: | ||||||
names = lgb.classes_ | ||||||
|
||||||
is_regression = isinstance(lgb, lightgbm.LGBMRegressor) | ||||||
is_multiclass = _lgb_n_targets(lgb) > 2 | ||||||
names = lgb.classes_ if not is_regression else ['y'] | ||||||
weight_dicts = _get_prediction_feature_weights(booster, X, n_targets) | ||||||
x = get_X0(add_intercept(X)) | ||||||
|
||||||
def get_score_weights(_label_id): | ||||||
_weights = _target_feature_weights( | ||||||
|
@@ -145,22 +177,38 @@ def get_score_weights(_label_id): | |||||
targets=targets, | ||||||
top_targets=top_targets, | ||||||
is_regression=is_regression, | ||||||
is_multiclass=is_multiclass, | ||||||
is_multiclass=n_targets > 1, | ||||||
proba=proba, | ||||||
get_score_weights=get_score_weights, | ||||||
) | ||||||
|
||||||
|
||||||
def _check_booster_args(lgb, is_regression=None): | ||||||
# type: (Any, bool) -> Tuple[lightgbm.Booster, bool] | ||||||
if isinstance(lgb, lightgbm.Booster): | ||||||
booster = lgb | ||||||
else: | ||||||
booster = lgb.booster_ | ||||||
_is_regression = isinstance(lgb, lightgbm.LGBMRegressor) | ||||||
if is_regression is not None and is_regression != _is_regression: | ||||||
raise ValueError( | ||||||
'Inconsistent is_regression={} passed. ' | ||||||
'You don\'t have to pass it when using scikit-learn API' | ||||||
.format(is_regression)) | ||||||
is_regression = _is_regression | ||||||
return booster, is_regression | ||||||
|
||||||
def _lgb_n_targets(lgb): | ||||||
if isinstance(lgb, lightgbm.LGBMClassifier): | ||||||
return lgb.n_classes_ | ||||||
else: | ||||||
return 1 if lgb.n_classes_ == 2 else lgb.n_classes_ | ||||||
elif isinstance(lgb, lightgbm.LGBMRegressor): | ||||||
return 1 | ||||||
else: | ||||||
raise TypeError | ||||||
|
||||||
|
||||||
def _get_lgb_feature_importances(lgb, importance_type): | ||||||
def _get_lgb_feature_importances(booster, importance_type): | ||||||
aliases = {'weight': 'split'} | ||||||
coef = lgb.booster_.feature_importance( | ||||||
coef = booster.feature_importance( | ||||||
importance_type=aliases.get(importance_type, importance_type) | ||||||
) | ||||||
norm = coef.sum() | ||||||
|
@@ -237,17 +285,15 @@ def walk(tree, parent_id=-1): | |||||
return leaf_index, split_index | ||||||
|
||||||
|
||||||
def _get_prediction_feature_weights(lgb, X, n_targets): | ||||||
def _get_prediction_feature_weights(booster, X, n_targets): | ||||||
""" | ||||||
Return a list of {feat_id: value} dicts with feature weights, | ||||||
following ideas from http://blog.datadive.net/interpreting-random-forests/ | ||||||
""" | ||||||
if n_targets == 2: | ||||||
n_targets = 1 | ||||||
dump = lgb.booster_.dump_model() | ||||||
dump = booster.dump_model() | ||||||
tree_info = dump['tree_info'] | ||||||
_compute_node_values(tree_info) | ||||||
pred_leafs = lgb.booster_.predict(X, pred_leaf=True).reshape(-1, n_targets) | ||||||
pred_leafs = booster.predict(X, pred_leaf=True).reshape(-1, n_targets) | ||||||
tree_info = np.array(tree_info).reshape(-1, n_targets) | ||||||
assert pred_leafs.shape == tree_info.shape | ||||||
|
||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a nitpick: extra whitespace before comma