To answer your question let's look what does multi-class classification really does in xgboost using multi:softmax
objective and, say, 6 classes.
Say, you want to train a classifier specifying num_boost_round=5
. How many trees would you expect xgboost to train for you? Correct answer is 30 trees. The reason is because softmax expecting for each training row to have num_classes=6
different scores, so that xgboost can compute gradients/hessian w.r.t. each of these 6 scores and use them to build a new tree for each of the scores (effectively updating 6 parallel models in order to output 6 updated scores per sample).
In order to ask xgboost classifier output the final 6 values for each sample e.g. from test set you will need to call bst.predict(xg_test, output_margin=True)
(where bst
is your classifier and xg_test
is e.g. test set). The output of regular bst.predict(xg_test)
is effectively same as picking the class with the highest value of 6 in bst.predict(xg_test, output_margin=True)
.
You can look at all the trees using bst.trees_to_dataframe()
function if you are interested (where bst
is your trained classifier).
Now to the question what does base_score
do in multi:softmax
case. Answer is - it is added as a starting score for each of 6 classes' scores before any trees were added. So if you, e.g. apply base_score=42.
you will be able to observe that all values in bst.predict(xg_test, output_margin=True)
will also increase by 42
. In the same time for softmax
increasing scores for all classes by equal amount doesn't change anything, so because of that in the case of multi:softmax
applying base_score
different from 0 doesn't have any visible effect.
Compare this behavior to binary classification. While almost same as multi:softmax
with 2 classes, the big difference is that xgboost is only trying to produce 1 score for class 1, leaving score for class 0 equal to 0.0
. Because of that when you use base_score
in binary classification it is only added to the score of class 1 thus increasing starting prediction probability for class 1. In theory with multiple classes it would be meaningful to e.g. pass multiple base scores (one per class), which you can't do using base_score
. Instead of that you can use set_base_margin
functionality applied to the training set, but it is not working very conveniently with default predict
, so after that you'll need to always use it with output_margin=True
and adding same values as ones you used in set_base_margin
for your training data (if you want to use set_base_margin
in multi-class case you'll need to flatten the margin values as suggested here).
Example of how it all works:
import numpy as np
import xgboost as xgb
TRAIN = 1000
TEST = 2
F = 10
def gen_data(M):
np_train_features = np.random.rand(M, F)
np_train_labels = np.random.binomial(2, np_train_features[:,0])
return xgb.DMatrix(np_train_features, label=np_train_labels)
def regenerate_data():
np.random.seed(1)
return gen_data(TRAIN), gen_data(TEST)
param = {}
param['objective'] = 'multi:softmax'
param['eta'] = 0.001
param['max_depth'] = 1
param['nthread'] = 4
param['num_class'] = 3
def sbm(xg_data, original_scores):
xg_data.set_base_margin(np.array(original_scores * xg_data.num_row()).reshape(-1, 1))
num_round = 3
print("#1. No base_score, no set_base_margin")
xg_train, xg_test = regenerate_data()
bst = xgb.train(param, xg_train, num_round)
print(bst.predict(xg_test, output_margin=True))
print(bst.predict(xg_test))
print("Easy to see that in this case all scores/margins have 0.5 added to them initially, which is default value for base_score here for some bizzare reason, but it doesn't really affect anything, so no one cares.")
print()
bst1 = bst
print("#2. Use base_score")
xg_train, xg_test = regenerate_data()
param['base_score'] = 5.8
bst = xgb.train(param, xg_train, num_round)
print(bst.predict(xg_test, output_margin=True))
print(bst.predict(xg_test))
print("In this case all scores/margins have 5.8 added to them initially. And it doesn't really change anything compared to previous case.")
print()
bst2 = bst
print("#3. Use very large base_score and screw up numeric precision")
xg_train, xg_test = regenerate_data()
param['base_score'] = 5.8e10
bst = xgb.train(param, xg_train, num_round)
print(bst.predict(xg_test, output_margin=True))
print(bst.predict(xg_test))
print("In this case all scores/margins have too big number added to them and xgboost thinks all probabilities are equal so picks class 0 as prediction.")
print("But the training actually was fine - only predict is being affect here. If you set normal base margins for test set you can see (also can look at bst.trees_to_dataframe()).")
xg_train, xg_test = regenerate_data() # if we don't regenerate the dataframe here xgboost seems to be either caching it or somehow else remembering that it didn't have base_margins and result will be different.
sbm(xg_test, [0.1, 0.1, 0.1])
print(bst.predict(xg_test, output_margin=True))
print(bst.predict(xg_test))
print()
bst3 = bst
print("#4. Use set_base_margin for training")
xg_train, xg_test = regenerate_data()
# only used in train/test whenever set_base_margin is not applied.
# Peculiar that trained model will remember this value even if it was trained with
# dataset which had set_base_margin. In that case this base_score will be used if
# and only if test set passed to `bst.predict` didn't have `set_base_margin` applied to it.
param['base_score'] = 4.2
sbm(xg_train, [-0.4, 0., 0.8])
bst = xgb.train(param, xg_train, num_round)
sbm(xg_test, [-0.4, 0., 0.8])
print(bst.predict(xg_test, output_margin=True))
print(bst.predict(xg_test))
print("Working - the base margin values added to the classes skewing predictions due to low eta and small number of boosting rounds.")
print("If we don't set base margins for `predict` input it will use base_score to start all scores with. Bizzare, right? But then again, not much difference on what to add here if we are adding same value to all classes' scores.")
xg_train, xg_test = regenerate_data() # regenerate test and don't set the base margin values
print(bst.predict(xg_test, output_margin=True))
print(bst.predict(xg_test))
print()
bst4 = bst
print("Trees bst1, bst2, bst3 are almost identical, because there is no difference in how they were trained. bst4 is different though.")
print(bst1.trees_to_dataframe().iloc[1,])
print()
print(bst2.trees_to_dataframe().iloc[1,])
print()
print(bst3.trees_to_dataframe().iloc[1,])
print()
print(bst4.trees_to_dataframe().iloc[1,])
The output for this is the following:
#1. No base_score, no set_base_margin
[[0.50240415 0.5003637 0.49870378]
[0.49863306 0.5003637 0.49870378]]
[0. 1.]
Easy to see that in this case all scores/margins have 0.5 added to them initially, which is default value for base_score here for some bizzare reason, but it doesn't really affect anything, so no one cares.
#2. Use base_score
[[5.8024044 5.800364 5.798704 ]
[5.798633 5.800364 5.798704 ]]
[0. 1.]
In this case all scores/margins have 5.8 added to them initially. And it doesn't really change anything compared to previous case.
#3. Use very large base_score and screw up numeric precision
[[5.8e+10 5.8e+10 5.8e+10]
[5.8e+10 5.8e+10 5.8e+10]]
[0. 0.]
In this case all scores/margins have too big number added to them and xgboost thinks all probabilities are equal so picks class 0 as prediction.
But the training actually was fine - only predict is being affect here. If you set normal base margins for test set you can see (also can look at bst.trees_to_dataframe()).
[[0.10240632 0.10036398 0.09870315]
[0.09863247 0.10036398 0.09870315]]
[0. 1.]
#4. Use set_base_margin for training
[[-0.39458954 0.00102317 0.7973728 ]
[-0.40044016 0.00102317 0.7973728 ]]
[2. 2.]
Working - the base margin values added to the classes skewing predictions due to low eta and small number of boosting rounds.
If we don't set base margins for `predict` input it will use base_score to start all scores with. Bizzare, right? But then again, not much difference on what to add here if we are adding same value to all classes' scores.
[[4.2054105 4.201023 4.1973724]
[4.1995597 4.201023 4.1973724]]
[0. 1.]
Trees bst1, bst2, bst3 are almost identical, because there is no difference in how they were trained. bst4 is different though.
Tree 0
Node 1
ID 0-1
Feature Leaf
Split NaN
Yes NaN
No NaN
Missing NaN
Gain 0.000802105
Cover 157.333
Name: 1, dtype: object
Tree 0
Node 1
ID 0-1
Feature Leaf
Split NaN
Yes NaN
No NaN
Missing NaN
Gain 0.000802105
Cover 157.333
Name: 1, dtype: object
Tree 0
Node 1
ID 0-1
Feature Leaf
Split NaN
Yes NaN
No NaN
Missing NaN
Gain 0.000802105
Cover 157.333
Name: 1, dtype: object
Tree 0
Node 1
ID 0-1
Feature Leaf
Split NaN
Yes NaN
No NaN
Missing NaN
Gain 0.00180733
Cover 100.858
Name: 1, dtype: object