ML-Fundamentals - Decision Trees
Table of Contents
- Teaching Content
- Decision Trees
- Information Gain as Splitting Criterion
- Loading the Data
- Training and Visualization
- Prediction / Validation
- Splitting Criterion Entropy / Information Gain
- Summary and Outlook
In this exercise you will train and evaluate a decision tree. Opposed to the previous exercises with the topics univariate linear regression, multivariate linear regression, logistic regression and bias variance tradeoff, you will not implement the algorithms from scratch using numpy. Instead you will use two python packages written on top of numpy, namely the pandas and the scikit-learn package, which are both widely used by the machine learning community. The steps can be broken down into:
- Load the data from a csv file into a pandas DataFrame object
- Preprocess the data
- Train a decision tree and visualize it.
- Do hyperparameter optimization by dividing the dataset into a fixed training set and a fixed validation set.
- Do hyperparameter optimization by crossvalidation
- Do hyperparameter optimization by gridsearch
Afterwards, show that you understand, what computations are done by the scikit-learn package you have used by manually computing:
- The entropy for one node
- The information gain for one node
As this exercise heavily focuses on the usage of the high-level APIs pandas and scikit-learn, reading their documentations (just the parts corresponding to the current task) is strongly recommended:
You should have a basic knowledge of:
- Decision Trees
- Information gain
By deep.TEACHING convention, all python modules needed to run the notebook are loaded centrally at the beginning.
import numpy as np import pandas as pd from sklearn.preprocessing import Imputer # in newer versions: from sklearn.impute import SimpleImputer as Imputer from sklearn import tree from sklearn.externals.six import StringIO from sklearn.model_selection import cross_val_score from sklearn.model_selection import GridSearchCV from graphviz import Source
Decision Trees for Classification
In a decision tree the data is split at each node according to a decision rule. This corresponds to nested if-then-else-rules. In the if-part of such a rule are decison is made based on a feature of the data record.
We will use the scikit learn implementation. For this implementation the features must be binary or have (at least) ordinal characteristic. If a feature is e.g. nominal with many values, it must be converted to a set of binary (one-hot-coded) features.
The splitting rules in the scikit learn implementation are binary and are based on a threshold, e.g.
- if then left subbranch, else right subbranch.
- binary features must be coded as 0/1, so the rule becomes: if then left subbranch, else right subbranch.
In the leafs of the tree the (class) predictions are made. There are two possibilities for such an inference:
- hard assignment: Predict for the data records which end up on a leaf by the majority class of the training data that end up on that leaf.
- soft assignment: Assign probabilities according to the distribution of the classes in respect to the training data which end up on that leaf.
As an example of a decision tree we will learn the following tree from the titanic data set:
A full explanation of the tree will be given later. Here just look at the desion rules (first line of the inner nodes) and at the last line of the leafs. In each leaf you see an array (values) with counts of the different targets for the train data: [number_died, number_survivors] .
Finding a tree that splits the traing data optimal is np-hard. Therefore often a greedy-strategy is used:
To build up a decision tree the algorithm starts at the root of the tree. The feature and the threshold that splits the training data best (with respect to the classes) are choosen. In an iterative way the whole tree is build up by such splitting rules.
There are different criteria for measuring the "separation (split) quality". The most important ones are:
- Gini Impurity
- Information Gain
In this tutorial we concentrate on the information gain.
Information Gain as Splitting Criterion
The entropy with respect to the target class variable of a training data set is defined as:
with the domain of the target values .
The probabilities are estimated by
with the number of training data and the number of training data with target label :
On a node a (binary) split on a feature is made by the split rule . As result there are two data sets and for the left resp. the right branch.
The feature and the split value are choosen that they maximize the 'reduction of the entropy' measured by the information gain :
Note that is the estimated probability that a random data record of has feature value which can be estimated by (analog for ).
can also be estimated by the fraction of the counts . So the information gain can be computed just with counts:
Deep decision trees generalize often poorly. The following remedies reduce overfitting:
- Limitation of the maximal depth of the tree.
- Pruning with an validation set either during training (pre-pruning) or after training (post-pruning).
- Dimensionality reduction (reducing the number of features before training)
Also often combining decision trees to an ensemble (decision forests) is used against overfitting.
Loading the Data
First we read in the titanic data set with pandas
### Exercise: Load the csv file as pandas DataFrame url = 'https://gitlab.com/deep.TEACHING/educational-materials/raw/master/datasets/titanic-train.csv' train_df = None
DataFrame class implements (and overwrites) a lot of (standard) methods. Some of the are:
### Not an exercise, just execute the cell print(train_df.ndim) print(train_df.shape)
### Not an exercise, just execute the cell ### To view the dataset as pretty table (when using jupyter) train_df
### Not an exercise, just execute the cell ### To return the data as numpy array: train_df.values
### Not an exercise, just execute the cell ### To return just certain features (columns) train_df[['PassengerId', 'Survived', 'Sex']].values
Scikit's learn decision trees can handle only numeric data. So we must convert the nominal
Convert the nominal feature into a binary feature.
### Exercise: Convert to binary feature train_df["Sex"]
Survived is the target, that we want to predict from the values of the other columns.
But not all of the other columns are helpful for classification. So we choose a feature set by hand and convert the features into a numpy array for scikit learn.
- Query the values of the feature we have chosen as target feature (class) and save them as numpy array
- Query the values of the features we want to use for training ("Fare", "Pclass", "Sex", "Age", "SibSp") and save them as numpy array.
y = None # Exercise: Extract target feature "Survived" as 1D array x = None # Exercise: Extract the features we want to use for training as 2D array
There are missing values (
NaN) for some examples in the "Age" column. Use the scikit learn
Imputer class to replace them by the mean of the columns.
# TODO as Exercise
Training and Visualization
Now we are ready to define and learn a decision tree by the criterion 'entropy' and we restrict the depth of the tree to 3. We use the scikit learn decison tree module.
Define and train the model.
clf = None # Exercise: Define and train the classifier
clf is an instance of a trained decision tree classifier.
The decision tree can be visualized (after training). For this we must write an graphviz dot-File
graph = Source(tree.export_graphviz(clf, out_file=None , feature_names=columns, class_names=['survived', 'survived'] , filled = True)) graph
According to the decision tree the main criterion (root node) for survival is the sex of the passenger (if your implementation is correct so far). In the left subtree are the male passengers (sex = 0), in the right subtree the female (sex=1).
In the leafs the class information is given by a
value array. Here the second value is the number of survivers in the leafs.
For example the leftmost leaf represents passengers that are male (sex=0) with fare<=26.2687 and age<=13.5. 13 of such boys survived and 2 of them died.
The entropy is displayed also at each node (splitting criterion).
Prediction / Validation
To make predictions with scikit learn, we must convert the data in the same way as we did it with the training data. Then we could use the method:
The depth of the decision tree is a hyperparameter. So the depth should be determined with the help of a validation set or by cross validation.
- Manually split your training data in 75% for training and 25% for validation
- Repeat the training process several times for different tree depths (define a new model every time)
clf.predict(validation_data)to predcit your validatipon data
- Manually calculate the accuracy for the validation set each time
- Accuracy is defined as
- From your results: Which seems to be the best tree depths?
### Exercise: Your code below to divide x and y
### Exercise: Your code below to train and predict trees with different depths for depth in range(1,20): # Your code here pass
Now with a dataset with less than 900 data examples (we have 891), chances to unluckily divide training and validation split seem pretty high. Also "wasting" 25% for validation on such a small data set is also not the ideal solution.
Now do not manually divide your data and do not manually compute the accuracy. Instead use the function
sklearn.model_selection.cross_val_score to search the best tree depth. Use 10-fold crossvalidation.
### Exercise: Your code below for crossvalidation for depth in range(1,20): # Your code here pass
This already works pretty well to determine one hyperparameter. But imagine you had several hyperparameters. Since the computation (even with 10 fold crossvalidation) is very fast for decision trees, it is no problem at all to try out every possible combination of hyperparameters. For each hyperparameter you would have to implement another for-loop. Should be no problem. But for convenience, you should know sciki-learn also has a built-in function for that.
sklearn.model_selection.GridSearchCV to do a grid search for the best hyperparameters. In addition to
max_depth parameter between [1, 20], add the
criterion as another hyper parameter with the possible values "entropy" and "gini".
Further useful functions after the grid search:
### Exercise: Your code below for grid search
Splitting Criterion Entropy / Information Gain
As you might have noticed, packages like scikit learn can be very comfortable, since they do most of the work for you as long as you provide the data in the right format. Though a good advice is, that you should know what happens inside of such a black box.
Recompute the root node entropy. On Pen & paper or with just some lines of code here using basic mathematical operations.
# Code below
Compute the information gain of the first split node (root node). Use the shown entropy values and number of data records (samples). Again on Pen & paper or with just some lines of code here using basic mathematical operations.
# Code below
Compute the information gain of the following split table:
The numbers are the corresponding data records, e.g. there are 13 data records with target class 1 and feature v.
Write a python function that computes the information gain.The data is given by a python array:
Like always, you are free to write as many helper functions as you want.
data = np.array([[2.,13.],[359., 41.]])
def information_gain(data): raise NotImplementedError()
np.testing.assert_almost_equal(information_gain(data), 0.07765787804281093) print("Information Gain:", information_gain(data))
Summary and Outlook
Notebook License (CC-BY-SA 4.0)
The following license applies to the complete notebook, including code cells. It does however not apply to any referenced external media (e.g., images).
Exercise: Logistic Regression and Regularization
by Christian Herta, Klaus Strohmenger
is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
Based on a work at https://gitlab.com/deep.TEACHING.
Code License (MIT)
The following license only applies to code cells of the notebook.
Copyright 2018 Christian Herta, Klaus Strohmenger
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.