KSpiliop / Fraud_Detection

Tuning XGBoost hyper-parameters with Simulated Annealing

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Tuning XGBoost hyper-parameters with heuristic search

An example in Credit Card Fraud Detection

The purpose of this experiment is to show how heuristics such as Simulated Annealing can be used to find efficiently good combinations of hyper-parameters in machine learning algorithms. This approach is better than blind random generation of parameters. It is also preferable to fine-tuning each hyper-parameter separately because typically there are interactions between them.

The XGBoost algorithm is a good show case because it has many hyper-parameters. Exhaustive grid search can be computationally prohibitive.

For a very good discussion of the theoretical details of XGBoost, see this Slideshare presentation of the algorithm with title "Kaggle Winning Solution Xgboost algorithm -- Let us learn from its author" by Tianqi Chen.

Loading the data

This is a Kaggle dataset taken from here which contains credit card transactions data and a fraud flag. It appeared originally in Dal Pozzolo, Andrea, et al. "Calibrating Probability with Undersampling for Unbalanced Classification." Computational Intelligence, 2015 IEEE Symposium Series on. IEEE, 2015. There is a Time variable (seconds from the first transaction in the dataset), an Amount variable, the Class variable (1=fraud, 0= no fraud) and the rest (V1-V28) are factor variables obtained through Principal Components Analysis from the original variables.

This is not a very difficult case for XGBoost as it will be seen. The main objective in this experiment is to show that the heuristic search finds a suitable set of hyper-parameters out of a quite large set of potential combinations.

We can verify below that this is a highly imbalanced dataset, typical of fraud detection data. We will take this into account when setting the weights of observations in XGBoost parameters.

Since we have plenty of data we are going to calibrate the hyper-parameters on a validation dataset and evaluate performance on an unseen testing dataset.

import numpy as np 
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline

import random
random.seed(1234)

plt.style.use('ggplot')

dat = pd.read_csv('creditcard.csv')

print(dat.head())
print('\nThe distribution of the target variable:\n')
dat['Class'].value_counts()
   Time        V1        V2        V3        V4        V5        V6        V7  \
0   0.0 -1.359807 -0.072781  2.536347  1.378155 -0.338321  0.462388  0.239599   
1   0.0  1.191857  0.266151  0.166480  0.448154  0.060018 -0.082361 -0.078803   
2   1.0 -1.358354 -1.340163  1.773209  0.379780 -0.503198  1.800499  0.791461   
3   1.0 -0.966272 -0.185226  1.792993 -0.863291 -0.010309  1.247203  0.237609   
4   2.0 -1.158233  0.877737  1.548718  0.403034 -0.407193  0.095921  0.592941   

         V8        V9  ...         V21       V22       V23       V24  \
0  0.098698  0.363787  ...   -0.018307  0.277838 -0.110474  0.066928   
1  0.085102 -0.255425  ...   -0.225775 -0.638672  0.101288 -0.339846   
2  0.247676 -1.514654  ...    0.247998  0.771679  0.909412 -0.689281   
3  0.377436 -1.387024  ...   -0.108300  0.005274 -0.190321 -1.175575   
4 -0.270533  0.817739  ...   -0.009431  0.798278 -0.137458  0.141267   

        V25       V26       V27       V28  Amount  Class  
0  0.128539 -0.189115  0.133558 -0.021053  149.62      0  
1  0.167170  0.125895 -0.008983  0.014724    2.69      0  
2 -0.327642 -0.139097 -0.055353 -0.059752  378.66      0  
3  0.647376 -0.221929  0.062723  0.061458  123.50      0  
4 -0.206010  0.502292  0.219422  0.215153   69.99      0  

[5 rows x 31 columns]

The distribution of the target variable:

 0    284315
1       492
Name: Class, dtype: int64

Data exploration.

Although the context of most of the variables is not known (recall that V1-V28 are factors summarizing the transactional data), we know that V1-V28 are by construction standardized, with a mean of 0 and and a standard deviation of 1. We standardize the Time and Amount variables too.

The data exploration and in particular Welch’s t-tests reveal that almost all the factors are significantly associated with the Class variable. The mean of these variables is almost zero in Class 0 and clearly non-zero in Class 1. The Time and Amount variables are also significant. There does not seem to by any reason for variable selection yet. We could drop the Time variable which is probably useless.

Some of the factors (and the Amount variable) are quite skewed and have very thin distributions. If we were to apply some other method (say, logistic regression) we could apply some transformations (and probably binning) but XGBoost is insensitive to such deviations from normality.

from scipy import stats
from sklearn import preprocessing

dat['Time'] = preprocessing.scale(dat['Time'])
dat['Amount'] = preprocessing.scale(dat['Amount'])

print('\nMeans of variables in the two Class categories:\n')
pt = pd.pivot_table(dat, values=dat.columns, columns = 'Class', aggfunc='mean')
print(pt.loc[dat.columns])

print('\nP-values of Welch’s t-tests and shape statistics:\n')
for i in range(30):
    col_name = dat.columns[i]
    t, p_val = stats.ttest_ind(dat.loc[ dat['Class']==0, col_name], dat.loc[ dat['Class']==1, col_name],equal_var=False)  
    skewness = dat.loc[:,col_name].skew()
    kurtosis = stats.kurtosis(dat.loc[:,col_name])
    print('Variable: {:7s}'.format(col_name),end='')    
    print('p-value: {:6.3f}  skewness: {:6.3f}  kurtosis: {:6.3f}'.format(p_val, skewness, kurtosis))

    
fig, axes = plt.subplots(nrows=6, ncols=5,figsize=(10,10))
axes = axes.flatten()

columns = dat.columns
for i in range(30):
  axes[i].hist(dat[columns[i]], bins=50,facecolor='b',alpha=0.5)
  axes[i].set_title(columns[i])
  axes[i].set_xlim([-4., +4.])
  plt.setp(axes[i].get_xticklabels(), visible=False) 
  plt.setp(axes[i].get_yticklabels(), visible=False) 
Means of variables in the two Class categories:

Class          0         1
Time    0.000513 -0.296223
V1      0.008258 -4.771948
V2     -0.006271  3.623778
V3      0.012171 -7.033281
V4     -0.007860  4.542029
V5      0.005453 -3.151225
V6      0.002419 -1.397737
V7      0.009637 -5.568731
V8     -0.000987  0.570636
V9      0.004467 -2.581123
V10     0.009824 -5.676883
V11    -0.006576  3.800173
V12     0.010832 -6.259393
V13     0.000189 -0.109334
V14     0.012064 -6.971723
V15     0.000161 -0.092929
V16     0.007164 -4.139946
V17     0.011535 -6.665836
V18     0.003887 -2.246308
V19    -0.001178  0.680659
V20    -0.000644  0.372319
V21    -0.001235  0.713588
V22    -0.000024  0.014049
V23     0.000070 -0.040308
V24     0.000182 -0.105130
V25    -0.000072  0.041449
V26    -0.000089  0.051648
V27    -0.000295  0.170575
V28    -0.000131  0.075667
Amount -0.000234  0.135382
Class        NaN       NaN

P-values of Welch’s t-tests and shape statistics:

Variable: Time   p-value:  0.000  skewness: -0.036  kurtosis: -1.294
Variable: V1     p-value:  0.000  skewness: -3.281  kurtosis: 32.486
Variable: V2     p-value:  0.000  skewness: -4.625  kurtosis: 95.771
Variable: V3     p-value:  0.000  skewness: -2.240  kurtosis: 26.619
Variable: V4     p-value:  0.000  skewness:  0.676  kurtosis:  2.635
Variable: V5     p-value:  0.000  skewness: -2.426  kurtosis: 206.901
Variable: V6     p-value:  0.000  skewness:  1.827  kurtosis: 42.642
Variable: V7     p-value:  0.000  skewness:  2.554  kurtosis: 405.600
Variable: V8     p-value:  0.063  skewness: -8.522  kurtosis: 220.583
Variable: V9     p-value:  0.000  skewness:  0.555  kurtosis:  3.731
Variable: V10    p-value:  0.000  skewness:  1.187  kurtosis: 31.988
Variable: V11    p-value:  0.000  skewness:  0.357  kurtosis:  1.634
Variable: V12    p-value:  0.000  skewness: -2.278  kurtosis: 20.241
Variable: V13    p-value:  0.028  skewness:  0.065  kurtosis:  0.195
Variable: V14    p-value:  0.000  skewness: -1.995  kurtosis: 23.879
Variable: V15    p-value:  0.050  skewness: -0.308  kurtosis:  0.285
Variable: V16    p-value:  0.000  skewness: -1.101  kurtosis: 10.419
Variable: V17    p-value:  0.000  skewness: -3.845  kurtosis: 94.798
Variable: V18    p-value:  0.000  skewness: -0.260  kurtosis:  2.578
Variable: V19    p-value:  0.000  skewness:  0.109  kurtosis:  1.725
Variable: V20    p-value:  0.000  skewness: -2.037  kurtosis: 271.011
Variable: V21    p-value:  0.000  skewness:  3.593  kurtosis: 207.283
Variable: V22    p-value:  0.835  skewness: -0.213  kurtosis:  2.833
Variable: V23    p-value:  0.571  skewness: -5.875  kurtosis: 440.081
Variable: V24    p-value:  0.000  skewness: -0.552  kurtosis:  0.619
Variable: V25    p-value:  0.249  skewness: -0.416  kurtosis:  4.290
Variable: V26    p-value:  0.015  skewness:  0.577  kurtosis:  0.919
Variable: V27    p-value:  0.006  skewness: -1.170  kurtosis: 244.985
Variable: V28    p-value:  0.002  skewness: 11.192  kurtosis: 933.381
Variable: Amount p-value:  0.004  skewness: 16.978  kurtosis: 845.078

png

Data partitioning.

In this step, we partition the dataset into 40% training, 30% validation and 30% testing. Note the use of the random.shuffle() function from numpy. We also make the corresponding matrices train, valid and test containing predictors only with labels trainY, validY and testY, respectively.

Class = dat['Class'].values

allIndices = np.arange(len(Class))
np.random.shuffle(allIndices) ## shuffle the indices of the observations

numTrain = int(round(0.40*len(Class)))
numValid = int(round(0.30*len(Class)))
numTest = len(Class)-numTrain-numValid

inTrain = allIndices[:numTrain]
inValid = allIndices[numTrain:(numTrain+numValid)]
inTest =  allIndices[(numTrain+numValid):]

train = dat.iloc[inTrain,:30]
valid= dat.iloc[inValid,:30]
test =  dat.iloc[inTest,:30]

trainY = Class[inTrain]
validY = Class[inValid]
testY = Class[inTest]

Preparing the Booster: Fixed parameters.

First we create the matrices in the format required by XGBoost with the xgb.DMatrix() function, passing for each dataset the predictors data and the labels. Then we set some fixed parameters. The number of boosting iterations (num_rounds) is set to 20. Normally we would use a larger number, but we want to keep the processing time low for the purposes of this experiment.

We initialize the param dictionary with silent=1 (no messages). Parameter min_child_weight is set at the default value of 1 because the data is highly unbalanced. This is the minimum weighted number of observations in a child node for further partitioning. The objective is binary classification and the evaluation metric is the Area Under Curve (AUC), the default for binary classification. In a more advanced implementation we could make a customized evaluation function, as described in XGBoost API. The internal random numbers seed is set to a constant for reproducible results (this is not guaranteed though, among other reasons because XGBoost runs in threads).

We are going to expand the param dictionary with the parameters in the heuristic search.

import xgboost as xgb

dtrain = xgb.DMatrix(train, label=trainY)
dvalid = xgb.DMatrix(valid, label=validY)
dtest = xgb.DMatrix(test, label=testY)

## fixed parameters
num_rounds=20 # number of boosting iterations

param = {'silent':1,
         'min_child_weight':1,
         'objective':'binary:logistic',
         'eval_metric':'auc',
         'seed' : 1234}  

Preparing the Booster: Variable parameters

In what follows we combine the suggestions from several sources, notably:

  1. The official XGBoost documentation and in particular the Notes on Parameter Tuning

  2. The article "Complete Guide to Parameter Tuning in XGBoost" from Analytics Vidhya

  3. Another Slideshare presentation with title "Open Source Tools & Data Science Competitions"

We select several important parameters for the heuristic search:

  • max_depth: the maximum depth of a tree, in [1,∞], with default value 6. This is highly data-dependent. [2] quotes as typical values: 3-10 and [3] advises to start from 6. We choose to explore also larger values and select 5-25 levels in steps of 5.
  • subsample, in (0,1] with default value 1. This is the proportion of the training instances used in trees and smaller values can prevent over-fitting. In [2] values in 0.5-1 are suggested. [3] suggests to leave this at 1. We decide to test values in 0.5-1.0 in steps of 0.1.
  • colsample_bytree, in (0,1] with default value 1. This is the subsample ratio of columns (features) used to construct a tree. In [2] values in 0.5-1 are suggested. The advice in [3] is 0.3-0.5. We will try similar values as with subsample.
  • eta (or learning_rate), in [0,1], with default value 0.3. This is the shrinking rate of the feature weights and larger values (but not too high!) can be used to prevent overfitting. A suggestion in [2] is to use values in 0.01-0.2. We can select some values in [0.01,0.4].
  • gamma, in [0, ∞], with default value 0. This is the minimum loss function reduction required for a split. [3] suggests to leave this at 0. We can experiment with values in 0-2 in steps of 0.05.
  • scale_pos_weight which controls the balance of positive and negative weights with default value 1. The advice in [1] is to use the ratio of negative to positive cases which is 595 here, i.e. to put a weight that large to the positive cases. [2] similarly suggests a large value in case of high class imbalance as is the case here. We can try some small values and some larger ones.

The total number of possible combinations is 43200 and we are only going to test a small fraction of 100 of them, i.e. as many as the number of the heuristic search iterations.

We also initialize a dataframe which will hold the results, for later examination.

from collections import OrderedDict

ratio_neg_to_pos = sum(trainY==0)/sum(trainY==1)  ## = 608
print('Ratio of negative to positive instances: {:6.1f}'.format(ratio_neg_to_pos))

## parameters to be tuned
tune_dic = OrderedDict()

tune_dic['max_depth']= [5,10,15,20,25] ## maximum tree depth
tune_dic['subsample']=[0.5,0.6,0.7,0.8,0.9,1.0] ## proportion of training instances used in trees
tune_dic['colsample_bytree']= [0.5,0.6,0.7,0.8,0.9,1.0] ## subsample ratio of columns
tune_dic['eta']= [0.01,0.05,0.10,0.20,0.30,0.40]  ## learning rate
tune_dic['gamma']= [0.00,0.05,0.10,0.15,0.20]  ## minimum loss function reduction required for a split
tune_dic['scale_pos_weight']=[30,40,50,300,400,500,600,700] ## relative weight of positive/negative instances

lengths = [len(lst) for lst in tune_dic.values()]

combs=1
for i in range(len(lengths)):
    combs *= lengths[i]
print('Total number of combinations: {:16d}'.format(combs))  

maxiter=100

columns=[*tune_dic.keys()]+['F-Score','Best F-Score']
results = pd.DataFrame(index=range(maxiter), columns=columns) ## dataframe to hold training results
Ratio of negative to positive instances:  595.5
Total number of combinations:            43200

Functions for training and performance reporting.

Next we define two functions:

Function perf_measures() accepts some predictions and labels, optionally prints the confusion matrix, and returns the F-Score This is a measure of performance combining precision and recall and will guide the heuristic search.

Function do_train() accepts as parameters:

  • the current choice of variable parameters in a dictionary (cur_choice),
  • the full dictionary of parameters to be passed to the main XGBoost training routine (param),
  • a train dataset in XGBoost format (train),
  • a string identifier (train_s),
  • its labels (trainY),
  • the corresponding arguments for a validation dataset (valid, valid_s, validY),
  • and the option to print the confusion matrix (print_conf_matrix).

It then trains the model and returns the F-score of the predictions on the validation dataset together with the model. The call to the main train routine xgb.train() has the following arguments:

  • the full dictionary of the parameters (param),
  • the train dataset in XGBoost format (train),
  • the number of boosting iterations (num_boost),
  • [ a watchlist with datasets information to show progress (evals) ] --> this is commented-out below.
def perf_measures(preds, labels, print_conf_matrix=False):
    
    act_pos=sum(labels==1) ## actual positive
    act_neg=len(labels) - act_pos ## actual negative
    
    pred_pos=sum(1 for i in range(len(preds)) if (preds[i]>=0.5)) ## predicted positive
    true_pos=sum(1 for i in range(len(preds)) if (preds[i]>=0.5) & (labels[i]==1)) ## predicted negative
    
    false_pos=pred_pos - true_pos ## false positive
    false_neg=act_pos-true_pos ## false negative
    true_neg=act_neg-false_pos ## true negative
      
    precision = true_pos/pred_pos ## tp/(tp+fp) percentage of correctly classified predicted positives
    recall = true_pos/act_pos ## tp/(tp+fn) percentage of positives correctly classified
    
    f_score = 2*precision*recall/(precision+recall) 
    
    if print_conf_matrix:
        print('\nconfusion matrix')
        print('----------------')
        print( 'tn:{:6d} fp:{:6d}'.format(true_neg,false_pos))
        print( 'fn:{:6d} tp:{:6d}'.format(false_neg,true_pos))
    
    return(f_score)


def do_train(cur_choice, param, train,train_s,trainY,valid,valid_s,validY,print_conf_matrix=False):
    ## train with given fixed and variable parameters
    ## and report the F-score on the validation dataset
    
    print('Parameters:')
    for (key,value) in cur_choice.items():
        print(key,': ',value,' ',end='')
        param[key]=value
    print('\n')    
    
##    the commented-out segment below uses a watchlist to monitor the progress of the boosting iterations 
##    evallist  = [(train,train_s), (valid,valid_s)]
##    model = xgb.train( param, train, num_boost_round=num_rounds,
##                      evals=evallist,verbose_eval=False)  
    
    model = xgb.train( param, train, num_boost_round=num_rounds)  
    
    preds = model.predict(valid)
    labels = valid.get_label()
      
    f_score = perf_measures(preds, labels,print_conf_matrix)
    
    return(f_score, model)    

Producing neighboring combinations.

Next we define a function next_choice() which either produces a random combination of the variable parameters (if no current parameters are passed with cur_params) or generates a neighboring combination of the parameters passed in cur_params.

In the second case we first select at random a parameter to be changed. Then:

  • If this parameter currently has the smallest value, we select the next (larger) one.
  • If this parameter currently has the largest value, we select the previous (smaller) one.
  • Otherwise, we select the left (smaller) or right (larger) value randomly.

Repetitions are avoided in the function which carries out the heuristic search.

def next_choice(cur_params=None):
    ## returns a random combination of the variable parameters (if cur_params=None)
    ## or a random neighboring combination from cur_params
    if cur_params:
        ## chose parameter to change
        ## parameter name and current value
        choose_param_name, cur_value = random.choice(list(cur_choice.items())) ## parameter name 
       
        all_values =  list(tune_dic[choose_param_name]) ## all values of selected parameter
        cur_index = all_values.index(cur_value) ## current index of selected parameter
        
        if cur_index==0: ## if it is the first in the range select the second one
            next_index=1
        elif cur_index==len(all_values)-1: ## if it is the last in the range select the previous one
            next_index=len(all_values)-2
        else: ## otherwise select the left or right value randomly
            direction=np.random.choice([-1,1])
            next_index=cur_index + direction

        next_params = dict((k,v) for k,v in cur_params.items())
        next_params[choose_param_name] = all_values[next_index] ## change the value of the selected parameter
        print('selected move: {:10s}: from {:6.2f} to {:6.2f}'.
              format(choose_param_name, cur_value, all_values[next_index] ))
    else: ## generate a random combination of parameters
        next_params=dict()
        for i in range(len(tune_dic)):
            key = [*tune_dic.keys()][i] 
            values = [*tune_dic.values()][i]
            next_params[key] = np.random.choice(values)
    return(next_params)  

Application of the Simulated Annealing algorithm.

At each iteration of the Simulated Annealing algorith, one combination of hyper-parameters is selected. The XGBoost algorithm is trained with these parameters and the F-score on the validation set is produced.

  • If this F-score is better (larger) than the one at the previous iteration, i.e. there is a "local" improvement, the combination is accepted as the current combination and a neighbouring combination is selected for the next iteration through a call to the next_choice() function.
  • Otherwise, i.e. if this F-score is worse (smaller) than the one at the previous iteration and the decline is Δf < 0, the combination is accepted as the current one with probability exp(-beta Δf/T) where beta is a constant and T is the current "temperature". The idea is that we start with a high temperature and "bad" solutions are easily accepted at first, in the hope of exploring wide areas of the search space. But as the temperature drops, bad solutions are less likely to be accepted and the search becomes more focused.

The temperature starts at a fixed value T0 and is reduced by a factor of alpha < 1 every n number of iterations. Here T0 = 0.40, n=5 and alpha = 0.85. The beta constant is 1.3.

The selection of the parameters of this "cooling schedule" can be done easily in MS Excel. In this example we select the average acceptance probabilities for F-Score deterioration of 0.150, 0.075, 0.038, 0.019, 0.009, i.e. starting from 0.150 and dividing by 2. We set these average probabilities to be around 50% during the first, second,...,fifth 20 iterations respectively and we use Excel Solver to find suitable parameters. The Excel file can be found here.

Repetitions are avoided with a simple hashing scheme.

A warning: if the number of iterations is not suficiently smaller than the total number of combinations, there may be too many repetitions and delays. The simple approach for producing combinations implemented here does not address such cases.

import time

t0 = time.clock()

T=0.40
best_params = dict() ## initialize dictionary to hold the best parameters

best_f_score = -1. ## initialize best f-score
prev_f_score = -1. ## initialize previous f-score
prev_choice = None ## initialize previous selection of parameters
weights = list(map(lambda x: 10**x, [0,1,2,3,4])) ## weights for the hash function
hash_values=set()

for iter in range(maxiter):
    print('\nIteration = {:5d}  T = {:12.6f}'.format(iter,T))

    ## find next selection of parameters not visited before
    while True:
        cur_choice=next_choice(prev_choice) ## first selection or selection-neighbor of prev_choice
         
        ## indices of the selections in alphabetical order of the parameters    
        indices=[tune_dic[name].index(cur_choice[name]) for name in sorted([*tune_dic.keys()])]
        
        ## check if selection has already been visited
        hash_val = sum([i*j for (i, j) in zip(weights, indices)])
        if hash_val in hash_values:
            print('\nCombination revisited - searching again')

#        tmp=abs(results.loc[:,[*cur_choice.keys()]] - list(cur_choice.values()))
#        tmp=tmp.sum(axis=1)
#        if any(tmp==0): ## selection has already been visited
#            print('\nCombination revisited - searching again')
        else:
            hash_values.add(hash_val)
            break ## break out of the while-loop
    
    
    ## train the model and obtain f-score on the validation dataset
    f_score,model=do_train(cur_choice, param, dtrain,'train',trainY,dvalid,'valid',validY)
    
    ## store the parameters
    results.loc[iter,[*cur_choice.keys()]]=list(cur_choice.values())
    
    print('    F-Score: {:6.2f}  previous: {:6.2f}  best so far: {:6.2f}'.format(f_score, prev_f_score, best_f_score))
 
    if f_score > prev_f_score:
        print('    Local improvement')
        
        ## accept this combination as the new starting point
        prev_f_score = f_score
        prev_choice = cur_choice
        
        ## update best parameters if the f-score is globally better
        if f_score > best_f_score:
            
            best_f_score = f_score
            print('    Global improvement - best f-score updated')
            for (key,value) in prev_choice.items():
                best_params[key]=value

    else: ## f-score is smaller than the previous one
        
        ## accept this combination as the new starting point with probability exp(-(1.6 x f-score decline)/temperature) 
        rnd = random.random()
        diff = f_score-prev_f_score
        thres=np.exp(1.3*diff/T)
        if rnd <= thres:
            print('    Worse result. F-Score change: {:8.4f}  threshold: {:6.4f}  random number: {:6.4f} -> accepted'.
                  format(diff, thres, rnd))
            prev_f_score = f_score
            prev_choice = cur_choice
 
        else:
            ## do not update previous f-score and previous choice
            print('    Worse result. F-Score change: {:8.4f}  threshold: {:6.4f}  random number: {:6.4f} -> rejected'.
                 format(diff, thres, rnd))
    ## store results
    results.loc[iter,'F-Score']=f_score
    results.loc[iter,'Best F-Score']=best_f_score
    if iter % 5 == 0: T=0.85*T  ## reduce temperature every 5 iterations and continue 
        
print('\n{:6.1f} minutes process time\n'.format((time.clock() - t0)/60))    

print('Best variable parameters found:\n')
print(best_params)
Iteration =     0  T =     0.400000
Parameters:
eta :  0.05  colsample_bytree :  0.9  scale_pos_weight :  300  gamma :  0.2  max_depth :  20  subsample :  0.5  

    F-Score:   0.80  previous:  -1.00  best so far:  -1.00
    Local improvement
    Global improvement - best f-score updated

Iteration =     1  T =     0.340000
selected move: gamma     : from   0.20 to   0.15
Parameters:
eta :  0.05  colsample_bytree :  0.9  scale_pos_weight :  300  gamma :  0.15  max_depth :  20  subsample :  0.5  

    F-Score:   0.80  previous:   0.80  best so far:   0.80
    Worse result. F-Score change:   0.0000  threshold: 1.0000  random number: 0.1169 -> accepted

Iteration =     2  T =     0.340000
selected move: eta       : from   0.05 to   0.10
Parameters:
eta :  0.1  colsample_bytree :  0.9  scale_pos_weight :  300  gamma :  0.15  max_depth :  20  subsample :  0.5  

    F-Score:   0.80  previous:   0.80  best so far:   0.80
    Worse result. F-Score change:  -0.0087  threshold: 0.9672  random number: 0.9110 -> accepted

Iteration =     3  T =     0.340000
selected move: max_depth : from  20.00 to  15.00
Parameters:
eta :  0.1  colsample_bytree :  0.9  scale_pos_weight :  300  gamma :  0.15  max_depth :  15  subsample :  0.5  

    F-Score:   0.80  previous:   0.80  best so far:   0.80
    Local improvement

Iteration =     4  T =     0.340000
selected move: eta       : from   0.10 to   0.05
Parameters:
eta :  0.05  colsample_bytree :  0.9  scale_pos_weight :  300  gamma :  0.15  max_depth :  15  subsample :  0.5  

    F-Score:   0.80  previous:   0.80  best so far:   0.80
    Worse result. F-Score change:  -0.0001  threshold: 0.9998  random number: 0.6716 -> accepted

Iteration =     5  T =     0.340000
selected move: eta       : from   0.05 to   0.01
Parameters:
eta :  0.01  colsample_bytree :  0.9  scale_pos_weight :  300  gamma :  0.15  max_depth :  15  subsample :  0.5  

    F-Score:   0.81  previous:   0.80  best so far:   0.80
    Local improvement
    Global improvement - best f-score updated

Iteration =     6  T =     0.289000
selected move: eta       : from   0.01 to   0.05

Combination revisited - searching again
selected move: scale_pos_weight: from 300.00 to 400.00
Parameters:
eta :  0.01  colsample_bytree :  0.9  scale_pos_weight :  400  gamma :  0.15  max_depth :  15  subsample :  0.5  

    F-Score:   0.81  previous:   0.81  best so far:   0.81
    Local improvement
    Global improvement - best f-score updated

Iteration =     7  T =     0.289000
selected move: colsample_bytree: from   0.90 to   1.00
Parameters:
eta :  0.01  colsample_bytree :  1.0  scale_pos_weight :  400  gamma :  0.15  max_depth :  15  subsample :  0.5  

    F-Score:   0.81  previous:   0.81  best so far:   0.81
    Worse result. F-Score change:  -0.0043  threshold: 0.9809  random number: 0.0174 -> accepted

Iteration =     8  T =     0.289000
selected move: eta       : from   0.01 to   0.05
Parameters:
eta :  0.05  colsample_bytree :  1.0  scale_pos_weight :  400  gamma :  0.15  max_depth :  15  subsample :  0.5  

    F-Score:   0.81  previous:   0.81  best so far:   0.81
    Local improvement
    Global improvement - best f-score updated

Iteration =     9  T =     0.289000
selected move: scale_pos_weight: from 400.00 to 500.00
Parameters:
eta :  0.05  colsample_bytree :  1.0  scale_pos_weight :  500  gamma :  0.15  max_depth :  15  subsample :  0.5  

    F-Score:   0.82  previous:   0.81  best so far:   0.81
    Local improvement
    Global improvement - best f-score updated

...

Iteration =    96  T =     0.015504
selected move: scale_pos_weight: from 500.00 to 600.00
Parameters:
eta :  0.1  colsample_bytree :  1.0  scale_pos_weight :  600  gamma :  0.1  max_depth :  15  subsample :  0.5  

    F-Score:   0.81  previous:   0.82  best so far:   0.86
    Worse result. F-Score change:  -0.0029  threshold: 0.7819  random number: 0.4103 -> accepted

Iteration =    97  T =     0.015504
selected move: max_depth : from  15.00 to  20.00
Parameters:
eta :  0.1  colsample_bytree :  1.0  scale_pos_weight :  600  gamma :  0.1  max_depth :  20  subsample :  0.5  

    F-Score:   0.82  previous:   0.81  best so far:   0.86
    Local improvement

Iteration =    98  T =     0.015504
selected move: scale_pos_weight: from 600.00 to 500.00
Parameters:
eta :  0.1  colsample_bytree :  1.0  scale_pos_weight :  500  gamma :  0.1  max_depth :  20  subsample :  0.5  

    F-Score:   0.82  previous:   0.82  best so far:   0.86
    Worse result. F-Score change:  -0.0055  threshold: 0.6282  random number: 0.5258 -> accepted

Iteration =    99  T =     0.015504
selected move: subsample : from   0.50 to   0.60

Combination revisited - searching again
selected move: gamma     : from   0.10 to   0.15
Parameters:
eta :  0.1  colsample_bytree :  1.0  scale_pos_weight :  500  gamma :  0.15  max_depth :  20  subsample :  0.5  

    F-Score:   0.82  previous:   0.82  best so far:   0.86
    Worse result. F-Score change:   0.0000  threshold: 1.0000  random number: 0.2742 -> accepted

  19.2 minutes process time

Best variable parameters found:

{'eta': 0.4, 'colsample_bytree': 1.0, 'scale_pos_weight': 50, 'gamma': 0.1, 'max_depth': 15, 'subsample': 0.5}
Evaluation on the test dataset.

The evaluation on the test dataset results to an F-Score of 0.86 which is considered good given the high imbalance in the classes. The run time was 19 minutes on a 64-bit Windows system with 6GB RAM and an Intel Core i3 processor 2.30GHz.

The best hyper-parameters found are in the ranges expected to be good according to all sources. One can then proceed this way:

  • Narrowing the ranges of these hyper-parameters,
  • Possibly adding others which are not used here (for example, regularization parameters),
  • Possibly doing some variable selection on the basis of the variable importance information,
  • Importantly, combining different models, ensemble-like.
print('\nBest parameters found:\n')  
print(best_params)

print('\nEvaluation on the test dataset\n')  

best_f_score,best_model=do_train(best_params, param, dtrain,'train',trainY,dtest,'test',testY,print_conf_matrix=True)


print('\nF-score on the test dataset: {:6.2f}'.format(best_f_score))


f, (ax1,ax2) = plt.subplots(nrows=1, ncols=2, sharey=False, figsize=(8,5))
ax1.plot(results['F-Score'])
ax2.plot(results['Best F-Score'])
ax1.set_xlabel('Iterations',fontsize=11)
ax2.set_xlabel('Iterations',fontsize=11)
ax1.set_ylabel('F-Score',fontsize=11)
ax2.set_ylabel('Best F-Score',fontsize=11)
ax1.set_ylim([0.7,0.9])
ax2.set_ylim([0.7,0.9])
plt.tight_layout()
plt.show()


print('\nVariables importance:\n')  

p = xgb.plot_importance(best_model) 
plt.show()
Best parameters found:

{'eta': 0.4, 'colsample_bytree': 1.0, 'scale_pos_weight': 50, 'gamma': 0.1, 'max_depth': 15, 'subsample': 0.5}

Evaluation on the test dataset

Parameters:
eta :  0.4  colsample_bytree :  1.0  scale_pos_weight :  50  gamma :  0.1  max_depth :  15  subsample :  0.5  


confusion matrix
----------------
tn: 85265 fp:    18
fn:    25 tp:   134

F-score on the test dataset:   0.86

png

Variables importance:

png

About

Tuning XGBoost hyper-parameters with Simulated Annealing


Languages

Language:Jupyter Notebook 100.0%