dmlc / xgboost

Scalable, Portable and Distributed Gradient Boosting (GBDT, GBRT or GBM) Library, for Python, R, Java, Scala, C++ and more. Runs on single machine, Hadoop, Spark, Dask, Flink and DataFlow
https://xgboost.readthedocs.io/en/stable/
Apache License 2.0
25.77k stars 8.69k forks source link

support sequence interface like lightgbm.Sequence #10091

Open xbanke opened 3 months ago

xbanke commented 3 months ago

Is it possible to support sequence interface (an object with __getitem__ and __len__) in DMatrix without copying data.

trivialfis commented 3 months ago

is there any use case that numpy/pandas and alike is not a better alternative?

xbanke commented 3 months ago

For timeseries data like stock exchange data, to predict the next server days return. Saying that there are 100 features, and then rolling the data with 20 days. In order to fit DMatrix, we have to shift the features 20 times, and the memory usage become 20x. Actually most of the data are duplicated. If I can define custom __getitem__, it will highly reduce the memory usage.

BTW, please do me a favor, check #9625.

trivialfis commented 3 months ago

Currently, can can consume data in batch by using the callback function, I took a quick look into LGB, which implements the from_seq with a function named _push_rows. I assume that's similar to the callback function we use in terms of the underlying mechanism.

See https://github.com/dmlc/xgboost/blob/master/demo/guide-python/quantile_data_iterator.py

trivialfis commented 3 months ago

BTW, please do me a favor, check https://github.com/dmlc/xgboost/issues/9625.

sure, will look into it.

xbanke commented 3 months ago

Currently, can can consume data in batch by using the callback function, I took a quick look into LGB, which implements the from_seq with a function named _push_rows. I assume that's similar to the callback function we use in terms of the underlying mechanism.

See https://github.com/dmlc/xgboost/blob/master/demo/guide-python/quantile_data_iterator.py

I have looked into this demo code. It looks like that the QuantileDMatrix consume all the dataiter more than one time (4 in my case). As a quantile structure, this will save much memory. But for ranking problem, how to set group weight if neccessary. My original demands is that the data consuming at the training stage, not the QuantileDMatrix.__init__ stage.
One more thing, there is a little trap one may fall into if not careful.


# run in version 1.7.6

import numpy as np
import pandas as pd
import xgboost as xgb

np.random.seed(42)

n_groups = 100
group_size = 2000
n_features = 10
n_levels = 20

rows = n_groups * group_size

features = pd.DataFrame(np.random.randn(rows, n_features).astype('float32'), columns=[f'f{i:03d}' for i in range(n_features)])
qids = pd.Series(np.arange(rows, dtype='int') // group_size)
labels = pd.Series(np.random.randn(rows).astype('float32')).groupby(qids).rank(method='first').sub(1) // (group_size // n_levels)
weights = np.arange(1, 101)

# dmatrix = xgb.DMatrix(features, label=labels, qid=qids)
qmatrix = xgb.QuantileDMatrix(features, label=labels, qid=qids)

sub_rows = 10000
sub_qmatrix = xgb.QuantileDMatrix(features.tail(sub_rows))
sub_dmatrix = xgb.DMatrix(features.tail(sub_rows))

params = {
    'objective': 'rank:pairwise',
    # 'objective': 'multi:softprob',
    # 'num_class': n_levels,

    'base_score': 0.5,
    # 'lambdarank_pair_method': 'mean',
    # 'lambdarank_num_pair_per_sample': 1,
    'booster': 'gbtree',
    'tree_method': 'hist',
    'verbosity': 1,
    # 'seed': 42,
    'learning_rate': 0.1,
    'max_depth': 6,
    'gamma': 1,
    'min_child_weight': 4,
    'subsample': 0.9,
    'colsample_bytree': 0.7,
    'nthread': 20,
    'reg_lambda': 1,
    'reg_alpha': 1,
    'eval_metric': ['ndcg@100', 'ndcg@500', 'ndcg@1000'],
}

booster = xgb.train(params, qmatrix, 100, verbose_eval=10, evals=[(qmatrix, 'train')]) 

preds_d = booster.predict(sub_dmatrix)
preds_q = booster.predict(sub_qmatrix)
preds_o = booster.predict(qmatrix)[-sub_rows:]

assert np.allclose(preds_d, preds_q)  # False
assert np.allclose(preds_o, preds_q)  # False
assert np.allclose(preds_o, preds_d)  # True

The script above will raise error. So if one train booster with QuantileDMatrix and then predict with QuantileDMatrix that is not origin from the training one, wrong predition might occurs, since the hist seperation points changed I guess.

trivialfis commented 1 week ago

There's a ref parameter for the QuantileDMatrix, which is used for getting the histogram cuts from the training data.

consume all the dataiter more than one time

Yes. That's expected, we need to iterate over the batches to gather needed statistics.

But for ranking problem, how to set group weight if neccessary

Pass the weight in the next method as in the example.

My original demands is that the data consuming at the training stage, not the QuantileDMatrix.init stage.

I don't think that's possible for any gbm implementation at the moment, assuming you want to generate the batches (rolling window) on demand. XGBoost has external memory support, please find it in the document, which might help reduce memory usage.