Decision Tree, Classificazione

Parte (3/3)

Potrebbe interessarti:

Il progetto Python, che comprende gli script mostrati in questo post, e quelli relativi alla creazione dei grafici Matplotlib e Plotly, è disponibile assieme al corso Introduzione al Machine Learning.

Dimmi di Più

Nel post precedente abbiamo “toccato con mano” l’implementazione della classe DecisionTreeClassifier. Ora proviamo ad usarla con alcuni semplici dataset, per mostrare su schermo le regioni decisionali del modello. Questo ci sarà utile per confrontare l’algoritmo di classificazione con i risultati visuali proposti.

Il Dataset “Loves Pop Music”

Ora vediamo cosa accade quando utilizziamo il dataset “Loves Pop Music”, del quale useremo solo le due feature “Loves Soda” e “Age”, in quanto vogliamo mostrare le regioni decisionali in uno spazio 2D. Come prima cosa, definiamo il metodo plot_decision_regions:

import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap

def plot_decision_regions(X, y, classifier, x_label, y_label, test_range=None, resolution=0.02):
    # setup marker generator and color map
    markers = ('o', 's', '^', 'v', '<')
    colors = ('red', 'blue', 'lightgreen', 'gray', 'cyan')
    cmap = ListedColormap(colors[:len(np.unique(y))])

    # plot the decision surface:
    # for each sample's feature, find the boundary values
    # (minimum and maximum values for that feature, considering all examples)
    x1_min, x1_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    x2_min, x2_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    x1_range = np.arange(x1_min, x1_max, resolution)
    x2_range = np.arange(x2_min, x2_max, resolution)
    xx1, xx2 = np.meshgrid(x1_range, x2_range)
    xx = np.array([xx1.ravel(), xx2.ravel()]).T

    # compute the predictions for each point (x1, x2) of the grid
    preds = classifier.predict(xx)

    # reshape the prediction matrix and use it as colormap to draw the filled contours
    preds = preds.reshape(xx1.shape)
    plt.figure(figsize=(8, 5))
    plt.contourf(xx1, xx2, preds, alpha=0.3, cmap=cmap)
    plt.xlim(xx1.min(), xx1.max())
    plt.ylim(xx2.min(), xx2.max())

    # plot class examples
    for idx, y_class in enumerate(np.unique(y)):
        plt.scatter(x=X[y == y_class, 0], y=X[y == y_class, 1],
                    alpha=0.8, c=colors[idx], marker=markers[idx],
                    label=f'Class {y_class}',
                    edgecolor='black')

    # highlight test examples
    if test_range:
        # plot all examples with a circle marker
        X_test, y_test = X[test_range, :], y[test_range]
        plt.scatter(X_test[:, 0], X_test[:, 1],
                    c='none', edgecolor='black', alpha=1.0, linewidth=1,
                    marker='o', s=100, label='Test set')

    plt.xlabel(x_label)
    plt.ylabel(y_label)
    plt.legend(loc='upper left')
    plt.grid(color='white', alpha=0.5, linestyle='--')
    plt.tight_layout()
    plt.show()

Poi instanziamo e addestriamo il classificatore, e quindi tracciamo le sue regioni decisionali:

def get_lpm_data():
    """Get 'Loves Pop Music' Dataset with 7 examples, 3 features, and 2 classes."""
    # features are: "Loves Popcorn", "Loves Soda", "Age"
    X = np.array([[True,  True,   7],
                  [True,  False, 12],
                  [False, True,  18],
                  [False, True,  35],
                  [True,  True,  38],
                  [True,  False, 50],
                  [False, False, 83]])

    # labels
    y = np.array([0, 0, 1, 1, 1, 0, 0])
    return X, y

# Get "Loves Pop Music" Dataset, but remove the "Loves Popcorn" feature
X, y = get_lpm_data()
X = X[:,1:3]

# instance and train the Decision Tree classifier,
# then plot its decision regions in a 2D space
decision_tree = DecisionTreeClassifier(max_depth=4, criterion="gini").fit(X, y)
plot_decision_regions(X, y, decision_tree,
                      "Loves Soda (True = 1 / False = 0)", "Age")
Esempio di classificazione binaria con il Dataset &#39;Loves Pop Music&#39;
Figura 1: Esempio con il Dataset ‘Loves Pop Music’

In questo caso, come abbiamo già visto nella sezione L’Algoritmo di Classificazione, il confine che separa le due regioni decisionali è costruito dalla seguente funzione di ipotesi:

if (Loves Soda) {
    if (Age <= 15) {
        return "Class 0"  # Does Not Love Pop Music
    }
    else {
        return "Class 1"  # Loves Pop Music
    }
}
else {
    return "Class 0"      # Does Not Love Pop Music
}
Suddivisioni Allineate Agli Assi

Perché abbiamo solo linee allineate agli assi dello spazio delle feature? Questo accade perché l’albero decisionale considera le feature una alla volta, e non le combina assieme per creare suddivisioni diagonali o non lineari.

Una regola decisionale è un’espressione booleana del tipo $\text{feature}_1 \le v_1$ (oppure $\text{feature}_1 \gt v_1$), ed essa suddivide naturalmente lo spazio con confini perpendicolari all’asse corrente ($\text{feature}_1$, in questo caso). Ogni divisione divide lo spazio in due regioni più piccole, creando partizioni rettangolari, e le successive suddivisioni raffinano ulteriormente queste partizioni, mantenendo i confini allineati agli assi.

I Decision Tree prendono decisioni suddividendo ricorsivamente lo spazio delle feature in regioni basate su valori di soglia applicati alle singole feature. A ogni divisione (o nodo), l’albero decisionale valuta una singola feature, e determina una soglia per dividere il dataset corrente in due subset, massimizzando la separazione delle classi in base a funzioni di costo come Entropy o Gini Impurity. Graficamente, ogni divisione corrisponde a una linea verticale o orizzontale, a seconda della feature “da dividere”. Nel grafico di questo esempio, utilizzando un dataset con due sole feature:

  • definiamo una linea verticale quando accade una divisione sulla prima feature, “Loves Soda”, associata all’asse $x$.
  • definiamo una linea orizzontale quando accade una divisione sulla seconda feature, “Age”, associata all’asse $y$.

Ciò significa che se accade una divisione sulla prima feature “Loves Soda” usando il valore soglia $1$, si crea una linea verticale in corrispondenza di “Loves Soda” = $1$. Se la divisione successiva avviene sulla seconda feature “Age”, con una soglia di $15$, questo crea una linea orizzontale in corrispondenza di “Age” = $15$.

Possiamo inoltre notare come il Decision Tree sia riuscito a classificare correttamente un dataset non linearmente separabile. Un albero decisionale può suddividere lo spazio delle feature in diverse regioni, effettuando suddivisioni ricorsive allineate agli assi. Mentre una singola suddivisione è sempre lineare, ed allineata ad un asse, confini decisionali non lineari possono essere approssimati creando molte piccole suddivisioni (sempre allineate agli assi). Queste dividono lo spazio delle feature in regioni che approssimano la forma non lineare del confine decisionale. Dobbiamo notare però che l’approssimazione potrebbe non essere uniforme. Se se si concede all’albero una profondità sufficientemente elevata, il confine decisionale risultante potrebbe apparire frastagliato o eccessivamente complesso.

Un Altro Semplice Dataset

Di seguito, vediamo cosa accade usando un’altro semplice dataset di 10 esempi, con 2 feature e 3 classi target:

def get_other_data():
    """Get another, simple dataset with 10 examples, 2 features, and 3 classes"""
    # Features are: "x1", "x2"
    X = np.array([
        [1, 1], [2, 1], [1, 2],          # Class 0: (x1 <= 3.5) && (x2 <= 3.5)
        [5, 5], [6, 5], [5, 6], [6, 6],  # Class 1: (x1 > 3.5)
        [1, 5], [2, 6], [1, 6]           # Class 2: (x1 <= 3.5) && (x2 > 3.5)
    ])

    # labels
    y = np.array([
        0, 0, 0,                         # Class 0
        1, 1, 1, 1,                      # Class 1
        2, 2, 2                          # Class 2
    ])

    return X, y

# do the same stuff, but with another simple dataset
X, y = get_other_data()
decision_tree = DecisionTreeClassifier(max_depth=4, criterion="gini").fit(X, y)
plot_decision_regions(X, y, decision_tree, "x1", "x2")
Esempio di classificazione binaria con un altro Dataset
Figura 2: Esempio con un altro Dataset

In questo secondo caso, il confine che separa le tre regioni decisionali è costruito dalla seguente funzione di ipotesi:

if (x1 <= 3.5) {
    if (x2 <= 3.5) {
        return "Class 0"
    }
    else {
        return "Class 2"
    }
else {
    return "Class 1"
}

Il Dataset “Iris”

Ora ritorniamo ad usare il dataset Iris, considerando solo le feature “Petal length” e “Petal Width”, e vediamo come addestrare il DecisionTree sul training set, e verificare le metriche di performance sul test set:

from sklearn import datasets
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split

def get_iris_data():
    iris = datasets.load_iris()
    X = iris.data[:, [2, 3]]  # petal length, petal width
    y = iris.target
    return X, y

X, y = get_iris_data()
print('Class labels:', np.unique(y))

# split data into 70% training and 30% test data
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=1, stratify=y)

# instance and train the Decision Tree classifier
decision_tree = DecisionTreeClassifier(max_depth=4, criterion="gini").fit(X_train, y_train)

# make predictions and compute the accuracy score
y_test_pred = decision_tree.predict(X_test)
print("accuracy: %.2f" % accuracy_score(y_test, y_test_pred))

# combine X_train (105 examples, going from index #0 to #104)
# with X_test (45 examples, going from index #105 to #149)
X_combined = np.vstack((X_train, X_test))
y_combined = np.hstack((y_train, y_test))

# plot the decision regions, marking the examples belonging to the X_test dataset
plot_decision_regions(X_combined, y_combined,
                      decision_tree,
                      "Petal length [cm]",
                      "Petal width [cm]",
                      test_range=range(105, 150))  # stop to 150, excluded
Esempio di classificazione binaria con il Dataset Iris
Figura 3: Esempio con il Dataset Iris