Métodos supervisados

En esta práctica vamos a usar el clásico dataset de las flores de iris (iris data set), tratando de clasificar distintas variedades de la flor de iris según la longitud y anchura de sus pétalos y sépalos. Trataremos de optimizar distintas métricas y veremos como los diferentes modelos clasifican los puntos y con cuales obtenemos una mayor precisión.

La práctica está estructurada de la siguiente manera (en el que se detalla la puntuación de cada parte).

  1. Carga de dados
  2. Análisis exploratorio de los datos
  3. k nearest neighbours
  4. Support vector machines
  5. Árbol de decisión
  6. Random forest
  7. Redes neuronales

Importante: Cada ejercicio puede suponer varios minutos de ejecución, por lo que la entrega debe hacerse en formato notebook y html, donde se vea el código y los resultados, junto con los comentarios de cada ejercicio.

0. Cargar del conjunto de datos

In [1]:
# Importamos librerías
import pandas as pd
import matplotlib as mpl
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import pandas as pd
import colorsys
import graphviz

from pandas.plotting import scatter_matrix
from matplotlib.colors import ListedColormap

from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import StandardScaler
from sklearn import model_selection
from sklearn.metrics import classification_report
from sklearn.metrics import confusion_matrix
from sklearn.metrics import accuracy_score
from sklearn import datasets, neighbors, tree, svm
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.svm import SVC
from sklearn.datasets import load_iris
from sklearn.model_selection import GridSearchCV
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import OneHotEncoder
from sklearn import tree
from sklearn.metrics import classification_report, accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.tree import export_graphviz

%matplotlib inline
In [2]:
#Importamos el dataset para iniciar el análisis
#También se podría hacer a partir de la clase datasets
#iris = datasets.load_iris()
iris = pd.read_csv("Iris.csv")

1. Análisis exploratorio de los datos

Exploraremos nuestro conjunto de datos. Para ello, realizaremos las siguientes inspecciones:

  • Miraremos el tamaño del dataset y veremos si existen valores nulos
  • Calcularemos los principales estadísticos del dataset (es decir, número de registros, valor medio, desviación estándar y cuartiles)
  • Veremos la distribución de las clases (i.e., si el dataset está balanceado)
  • Realizaremos algunas visualizaciones para hacernos una idea.

Os hemos puesto en forma de comentario los análisis que tendríais que hacer

In [3]:
#Visualizamos los primeros 5 datos del dataset
iris = pd.read_csv("Iris.csv")
display(iris.head())
#Eliminamos la primera columna ID
print()
print('------Eliminamos la columna ID-------------')
iris = iris.drop('Id',axis=1)
display(iris.head())
#Forma, tamaño y número de valores del dataset
print()
print('------Información del dataset------')
print(iris.info())
print("El número de líneas es: " + str(iris.shape[0]) + " y el número de columnas: "+ str(iris.shape[1]))

print("No existe ningún null")
display(iris.isnull().sum())

#Resumen estadístico
print()
print('------Descripción del dataset------')
display(iris.describe())

#Grafico Sépalo - Longitud vs Ancho
fig = iris[iris.Species == 'Iris-setosa'].plot(kind='scatter', x='SepalLengthCm', y='SepalWidthCm', color='blue', label='Setosa')
iris[iris.Species == 'Iris-versicolor'].plot(kind='scatter', x='SepalLengthCm', y='SepalWidthCm', color='green', label='Versicolor', ax=fig)
iris[iris.Species == 'Iris-virginica'].plot(kind='scatter', x='SepalLengthCm', y='SepalWidthCm', color='red', label='Virginica', ax=fig)
fig.set_xlabel('Sépalo - Longitud')
fig.set_ylabel('Sépalo - Ancho')
fig.set_title('Sépalo - Longitud vs Ancho')
plt.show()

#Grafico Pétalo - Longitud vs Ancho
fig = iris[iris.Species == 'Iris-setosa'].plot(kind='scatter', x='PetalLengthCm', y='PetalWidthCm', color='blue', label='Setosa')
iris[iris.Species == 'Iris-versicolor'].plot(kind='scatter', x='PetalLengthCm', y='PetalWidthCm', color='green', label='Versicolor', ax=fig)
iris[iris.Species == 'Iris-virginica'].plot(kind='scatter', x='PetalLengthCm', y='PetalWidthCm', color='red', label='Virginica', ax=fig)
fig.set_xlabel('Pétalo - Longitud')
fig.set_ylabel('Pétalo - Ancho')
fig.set_title('Pétalo Longitud vs Ancho')
plt.show()
IdSepalLengthCmSepalWidthCmPetalLengthCmPetalWidthCmSpecies
015.13.51.40.2Iris-setosa
124.93.01.40.2Iris-setosa
234.73.21.30.2Iris-setosa
344.63.11.50.2Iris-setosa
455.03.61.40.2Iris-setosa
------Eliminamos la columna ID-------------
SepalLengthCmSepalWidthCmPetalLengthCmPetalWidthCmSpecies
05.13.51.40.2Iris-setosa
14.93.01.40.2Iris-setosa
24.73.21.30.2Iris-setosa
34.63.11.50.2Iris-setosa
45.03.61.40.2Iris-setosa
------Información del dataset------
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 150 entries, 0 to 149
Data columns (total 5 columns):
 #   Column         Non-Null Count  Dtype
---  ------         --------------  -----
 0   SepalLengthCm  150 non-null    float64
 1   SepalWidthCm   150 non-null    float64
 2   PetalLengthCm  150 non-null    float64
 3   PetalWidthCm   150 non-null    float64
 4   Species        150 non-null    object
dtypes: float64(4), object(1)
memory usage: 6.0+ KB
None
El número de líneas es: 150 y el número de columnas: 5
No existe ningún null
SepalLengthCm    0
SepalWidthCm     0
PetalLengthCm    0
PetalWidthCm     0
Species          0
dtype: int64
------Descripción del dataset------
SepalLengthCmSepalWidthCmPetalLengthCmPetalWidthCm
count150.000000150.000000150.000000150.000000
mean5.8433333.0540003.7586671.198667
std0.8280660.4335941.7644200.763161
min4.3000002.0000001.0000000.100000
25%5.1000002.8000001.6000000.300000
50%5.8000003.0000004.3500001.300000
75%6.4000003.3000005.1000001.800000
max7.9000004.4000006.9000002.500000

El análisis univariado es la forma más simple de analizar datos. No trata con causas o relaciones (a diferencia de la regresión) y su propósito principal es describir y encontrar patrones en los datos.

Para ello vamos a realizar lo que se conoce como Distribution Plots (o histogramas). Los gráficos de distribución se utilizan para evaluar visualmente cómo se distribuyen los puntos de datos con respecto a su frecuencia. Por lo general, los puntos de datos se agrupan en contenedores y la altura de las barras indica el número de puntos de datos (frecuencua de aparición).

Implementación:Para ello dividimos nuestro dataset en tres partes, una para cada una de las clases, y representamos cada una de las clases de flores por separado.
In [4]:
import warnings
warnings.filterwarnings('ignore')

iris_setosa=iris.loc[iris["Species"]=="Iris-setosa"]
iris_virginica=iris.loc[iris["Species"]=="Iris-virginica"]
iris_versicolor=iris.loc[iris["Species"]=="Iris-versicolor"]


sns.FacetGrid(iris,hue="Species",size=3).map(sns.distplot,"PetalLengthCm").add_legend()
sns.FacetGrid(iris,hue="Species",size=3).map(sns.distplot,"PetalWidthCm").add_legend()
sns.FacetGrid(iris,hue="Species",size=3).map(sns.distplot,"SepalLengthCm").add_legend()
sns.FacetGrid(iris,hue="Species",size=3).map(sns.distplot,"SepalWidthCm").add_legend()
plt.show()
Análisis: A tenor de los resultados vistos hasta el momento, ¿qué conclusiones podrías extraer de los histogramas?.
  • si úsamos PetalLengthCm podemos separar la especie iris-setosa.
  • no podemos utilizar SepalLengthCm o SepalWidthCm porque esta todo mezclado y no podemos separar las flores.
  • PetalWidthCm tampoco esta separado correctamente.

La única conclusión es que con PetalLengthCm podemos separa la especie iris-setosa.

Un diagrama de caja (box plot) es una forma estandarizada de mostrar la distribución de datos basada en un resumen de cinco números ("mínimo", primer cuartil (Q1), mediana, tercer cuartil (Q3) y "máximo"). Los box plots nos informan sobre valores atípicos y cuáles son sus valores. También puede decirnos si los datos son simétricos, si están agrupados y si están sesgados. Para realizarlos podemos usar la función boxplot de seaborn.

El Violin Plot es un método para visualizar la distribución de datos numéricos de diferentes variables. Es similar al diagrama de caja (box plot) pero con un diagrama rotado en cada lado que brinda más información sobre la estimación de densidad en el eje y. La densidad se refleja y se voltea y la forma resultante se rellena creando una imagen que se parece a un violín. La ventaja de una trama de violín es que puede mostrar matices en la distribución que no son perceptibles en una gráfica de caja. Por otro lado, el diagrama de caja muestra más claramente los valores atípicos en los datos. Los gráficos de violín suelen contener más información que los gráficos de caja aunque son menos populares.

Ahora tracemos los gráficos de violín para nuestro conjunto de datos de iris. Para ello podemos utilizar la función violinplot de seaborn . Para su interpretación tengamos en cuenta que el rectángulo que aparece en el violin plot equivale a la información que nos da el box plot y que el círculo blanco nos indica donde está el percentil 50.

Por último realizaremos un pequeño estudio mediante un pair-plot para visualizar posibles relaciones entre nuestras variables (por pares).

En este caso emplearemos la función pairplot de la librería seaborn.

Implementación: Realiza las correspondientes visualizaciones de los box-plots, violin-plots y pair-plot.
In [5]:
import warnings
warnings.filterwarnings('ignore')
sns.boxplot(x="Species",y="PetalLengthCm",data=iris)
plt.show()
sns.boxplot(x="Species",y="PetalWidthCm",data=iris)
plt.show()
sns.boxplot(x="Species",y="SepalLengthCm",data=iris)
plt.show()
sns.boxplot(x="Species",y="SepalWidthCm",data=iris)
plt.show()
display(iris.boxplot())
<matplotlib.axes._subplots.AxesSubplot at 0x127482b38>
In [6]:
sns.violinplot(x="Species",y="PetalLengthCm",data=iris)
plt.show()
sns.violinplot(x="Species",y="PetalWidthCm",data=iris)
plt.show()
sns.violinplot(x="Species",y="SepalLengthCm",data=iris)
plt.show()
sns.violinplot(x="Species",y="SepalWidthCm",data=iris)
plt.show()
In [7]:
sns.set_style("whitegrid")
sns.pairplot(iris,hue="Species",size=3);
plt.show()
Análisis: ¿Qué conclusiones podrías sacar de la visualización de tu dataset mediante diagramas de cajas, diagramas de violín y un gráfico de dispersión?

Si nos fijamos en la gráficas de dispersión que relacionan las carácteristicas del campo sepal veremos como están distribuidos de manera casi uniforme (sobretodo los correspondientes al Iris setosa), mientras que los correspondientes a versicolor y virginica tienen cualidades algo parecidas por lo que se solapan en ocasiones.

En cambio si comparamos el pétalo es una distribución mucho más uniforme en comparación con el sépalo.

Si análizamos el diagramade violín muestra que Iris Virginica tiene un valor medio más alto en longitud de pétalo, ancho de pétalo y longitud de sépalo en comparación con Versicolor y Setosa. En otro sentido, Iris Setosa tiene el mayor valor medio de ancho de sépalo. También podemos ver una diferencia significativa entre la longitud y el ancho del sépalo de Setosa contra la longitud y el ancho de sus pétalos. Esa diferencia es menor en Versicolor y Virginica. El diagrama del violín también indica que el peso del ancho del sépalo de Virginica y el ancho del pétalo están altamente concentrados alrededor de la mediana.

Respecto al diagrama de cajas, los puntos aislados que se pueden ver son los valores atípicos en los datos. Dado que estos son muy pocos en número, no tendría ningún impacto significativo en nuestro análisis.

Aplicación de modelos

Antes de aplicar ningún modelo, tenemos que separar los datos entre los conjuntos de train y test. Siempre trabajaremos sobre el conjunto de train y evaluaremos los resultados en el conjunto de test.

Es importante tener en cuenta que nuestra variable target es categórica. El clasificador KNeighborsClassifier no acepta etiquetas de tipo string, por lo que debemos tranformar estas etiquetas a números (esto es lo que conocemos como Label encoding).

Para ello dividiremos el dataset en dos arrays: X (características) e Y (etiquetas) y aplicaremos la siguiente correspondencia:

  • Iris-setosa corresponde a 0
  • Iris-versicolor corresponde a 1
  • Iris.virginica corresponde a 2
Para poder visualizar las fronteras de decisión de los diferentes métodos, primero lo haremos con las caracterísiticas del sépalo y después con la del pétalo.
Implementación: Dividir el dataset en dos subconjuntons, train (80% aprox.) y test. Podeis usar la implementación train_test_split de sklearn.
In [4]:
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn import preprocessing
label_encoder = preprocessing.LabelEncoder()
iris = pd.read_csv("Iris.csv")
iris = iris.drop('Id',axis=1)
print('----------Aplicación de la correspondecia ----------------------')
print(iris['Species'].unique())
iris['Species']= label_encoder.fit_transform(iris['Species'])
print(iris['Species'].unique())

#X_train, X_test, y_train, y_test = train_test_split(iris[['PetalLengthCm', 'PetalWidthCm','SepalLengthCm','SepalWidthCm']], 



print('----------Sepal----------------------')
X_train, X_test, y_train, y_test = train_test_split(iris[['SepalLengthCm', 'SepalWidthCm']],
                                                    iris['Species'],
                                                    test_size=0.2,
                                                    stratify=iris['Species'])
----------Aplicación de la correspondecia ----------------------
['Iris-setosa' 'Iris-versicolor' 'Iris-virginica']
[0 1 2]
----------Sepal----------------------

A lo largo de los ejercicios aprenderemos a visualizar gráficamente las fronteras de decisión que nos devuelven los diferentes modelos. Para este fin usaremos la función definida a continuación (que nos servirá para trazar las respectivas fronteras de decisión a lo largo de toda la PEC), la cual sigue los siguientes pasos:

  • Crea una meshgrid con los valores mínimo y máximo de x e y.
  • Entrena el clasificador con los valores de la meshgrid.
  • Hace un reshape de los datos para obtener el formato correcto.

Después de este proceso, ya podemos hacer el gráfico de las fronteras de decisión y añadir los puntos reales. Así veremos las areas en las que el modelo considera que son de una clase y las que considera que son de la otra. Al poner encima los puntos reales veremos si los clasifica correctamente.

In [5]:
# Creamos la meshgrid con los valores mínimo y máximo de 'x' i 'y'.
# La variable X es nuestro dataframe con las variables a estudiar (las del pétalo o las del sépalo)
X=iris[['SepalLengthCm', 'SepalWidthCm']].to_numpy()

# Creamos la meshgrid con los valores mínimo y máximo de 'x' i 'y'.

x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1

# Definimos la función que nos graficará las fronteras de decisión

def plot_decision_boundaries(model, X, y, x_min=x_min,
                             x_max=x_max,
                             y_min=y_min,
                             y_max=y_max,  delta: float = .02) -> None:
    """Plot data points and deicision boundaries learned by the model.
    
    Arguments:
    ----------
    model: scikit-learn like model
    
    X: np.array[n_samples, n_features]
        Only first 2 features will be considered because it is a 2d plot.
        Feature 0 in the x axis, and feature 1 in the y axis.
        
    y: np.array
        Labels for each sample.
        
    delta: float
        Increment between consecutive points when computing the grid for plotting boundaries.
        Lower value for higher resolution.
    """

    xx, yy = np.meshgrid(np.arange(x_min, x_max, delta),
                         np.arange(y_min, y_max, delta))

    #Predecimos el clasificador con los valores de la meshgrid
    # En este caso model será nuestra variable que contiene el modelo a estudiar, es decir K-nn, SVM,...
    # Por ejemplo para K-nn sería model = KNeighborsClassifier()

    Z = model.predict(np.c_[xx.ravel(), yy.ravel()])

    # Creamos mapas de colores con ListedColormap para ver como separa las clases. 
    # En este caso usaremos: 
    # Iris-setosa : darkorange
    # Iris-versicolor: c
    # Iris-virginica: darkblue

    cmap_light = ListedColormap(['orange', 'cyan', 'cornflowerblue'])
    cmap_bold = ListedColormap(['darkorange', 'c', 'darkblue'])

    # Ponemos el resultado en una figura de color
    Z = Z.reshape(xx.shape)
    plt.figure()
    plt.pcolormesh(xx, yy, Z, cmap= cmap_light)

    # Dibujamos también los puntos de entrenamiento
    plt.scatter(X[:, 0], X[:, 1], c=y, cmap= cmap_bold)
    plt.xlim(xx.min(), xx.max())
    plt.ylim(yy.min(), yy.max())
    plt.show()



def plot_decision_boundaries_bonus(x, y, labels, model,
                             x_min=x_min,
                             x_max=x_max,
                             y_min=y_min,
                             y_max=y_max,
                             grid_step=0.02):
    xx, yy = np.meshgrid(np.arange(x_min, x_max, grid_step),
                         np.arange(y_min, y_max, grid_step))

    # Predecimos el classifier con los valores de la meshgrid.
    Z = model.predict_proba(np.c_[xx.ravel(), yy.ravel()])[:,1]

    # Hacemos reshape para tener el formato correcto.
    Z = Z.reshape(xx.shape)

    # Seleccionamos una paleta de color.
    arr = plt.cm.coolwarm(np.arange(plt.cm.coolwarm.N))
    arr_hsv = mpl.colors.rgb_to_hsv(arr[:,0:3])
    arr_hsv[:,2] = arr_hsv[:,2] * 1.5
    arr_hsv[:,1] = arr_hsv[:,1] * .5
    arr_hsv = np.clip(arr_hsv, 0, 1)
    arr[:,0:3] = mpl.colors.hsv_to_rgb(arr_hsv)
    my_cmap = ListedColormap(arr)

    # Hacemos el gráfico de las fronteras de decisión.
    fig, ax = plt.subplots(figsize=(7,7))
    plt.pcolormesh(xx, yy, Z, cmap=my_cmap)

    # Añadimos los punts.
    ax.scatter(x, y, c=labels, cmap='coolwarm')
    ax.set_xlim(xx.min(), xx.max())
    ax.set_ylim(yy.min(), yy.max())
    ax.grid(False)

1. k nearest neighbours

El primer algoritmo que usaremos para clasificar los puntos es el k-nn. En este ejercicio ajustaremos dos hiperparámetros para tratar de obtener una mayor precisión:

  • k: el número de vecinos que se consideran para clasificar un nuevo ejemplo. Probaremos todos los valores entre 1 y 10.
  • pesos: importancia que se le da a cada vecino. En este caso probaremos dos opciones: pesos uniformes, donde todos los vecinos son considerados igual; y pesos según la distancia, donde los vecinos más próximos tienen más peso que los vecinos más lejanos.

Para decidir los hiperparámetros óptimos usaremos la técnica de grid search, que consiste en entrenar un modelo para cada combinación posible de hiperparámetros y la evaluaremos usando cross validation con 4 particiones estratificadas. Posteriormente, escojeremos la combinación de hiperparametros que haya obtenido mejores resultados.

Implementación: Calcula el valor óptimo de los hiperparámetros k y pesos. A continuación, haz un heatmap para visualizar las precisiones según los dos hiperparámetros.

Para resolver la primer parte podéis usar los módulos GridSearchCV y KNeighborsClassifier de sklearn. Para la visualización del heatmap podéis usar la función pivot que permite la librería Pandas.

In [6]:
from sklearn.model_selection import GridSearchCV
from sklearn.neighbors import KNeighborsClassifier

clf = KNeighborsClassifier()

param_grid = {"n_neighbors": range(1, 11), "weights": ["uniform", "distance"]}

grid_search = GridSearchCV(clf, param_grid=param_grid, cv=4)

grid_search.fit(X_train, y_train)

means = grid_search.cv_results_["mean_test_score"]
stds = grid_search.cv_results_["std_test_score"]
params = grid_search.cv_results_['params']

for mean, std, pms in zip(means, stds, params):
    print("Precisión media:  {:.2f} +/- {:.2f} con parametros {}".format(mean*100, std*100, pms))
              
Precisión media:  74.17 +/- 4.93 con parametros {'n_neighbors': 1, 'weights': 'uniform'}
Precisión media:  74.17 +/- 4.93 con parametros {'n_neighbors': 1, 'weights': 'distance'}
Precisión media:  75.83 +/- 5.95 con parametros {'n_neighbors': 2, 'weights': 'uniform'}
Precisión media:  75.83 +/- 4.33 con parametros {'n_neighbors': 2, 'weights': 'distance'}
Precisión media:  75.00 +/- 2.89 con parametros {'n_neighbors': 3, 'weights': 'uniform'}
Precisión media:  77.50 +/- 4.33 con parametros {'n_neighbors': 3, 'weights': 'distance'}
Precisión media:  75.83 +/- 2.76 con parametros {'n_neighbors': 4, 'weights': 'uniform'}
Precisión media:  76.67 +/- 5.27 con parametros {'n_neighbors': 4, 'weights': 'distance'}
Precisión media:  80.00 +/- 2.36 con parametros {'n_neighbors': 5, 'weights': 'uniform'}
Precisión media:  78.33 +/- 3.73 con parametros {'n_neighbors': 5, 'weights': 'distance'}
Precisión media:  79.17 +/- 1.44 con parametros {'n_neighbors': 6, 'weights': 'uniform'}
Precisión media:  79.17 +/- 3.63 con parametros {'n_neighbors': 6, 'weights': 'distance'}
Precisión media:  79.17 +/- 3.63 con parametros {'n_neighbors': 7, 'weights': 'uniform'}
Precisión media:  79.17 +/- 3.63 con parametros {'n_neighbors': 7, 'weights': 'distance'}
Precisión media:  78.33 +/- 3.73 con parametros {'n_neighbors': 8, 'weights': 'uniform'}
Precisión media:  79.17 +/- 3.63 con parametros {'n_neighbors': 8, 'weights': 'distance'}
Precisión media:  81.67 +/- 3.73 con parametros {'n_neighbors': 9, 'weights': 'uniform'}
Precisión media:  79.17 +/- 3.63 con parametros {'n_neighbors': 9, 'weights': 'distance'}
Precisión media:  78.33 +/- 2.89 con parametros {'n_neighbors': 10, 'weights': 'uniform'}
Precisión media:  79.17 +/- 3.63 con parametros {'n_neighbors': 10, 'weights': 'distance'}
In [11]:
import seaborn as sns

param1 = [x['n_neighbors'] for x in params]
param2 = [x['weights'] for x in params]

precisions = pd.DataFrame(zip(param1, param2, means), columns=['n_neighbors', 'weights', 'means'])
precisions = precisions.pivot('n_neighbors', 'weights', 'means')
sns.heatmap(precisions)
Out[11]:
<matplotlib.axes._subplots.AxesSubplot at 0x127543400>
Análisis: ¿Qué parámetros han dado mejores resultados? ¿Qué variación hay entre las diferentes combinaciones de parámetros? ¿Es significativa la variación entre las diferentes combinaciones? ¿Hay algún parámetro con más influencia que otro? ¿Era previsible?
  • La mejor solución se ha dado con un valor k = 9 i los pesos calculados con uniform. Estos resultados pueden variar si se modifica el conjunto de entrenamiento y de test, es decir, estos resultados varian por ejecución de la celda que genera el división.
  • El valor mínimo es 74.17 y el valor máximo es 81.67, es decir, la diferencia es de 7 puntos percentuales, con desviaciones estandard del orden de 0.5 puntos porcentuales podemos afirmar que hay opciones claramente mejor que otras.
  • He observado que excepto par k=1 que no importan los pesos, para el resto si que es significativa. En k=1 se clasifican los nuevos ejemplos con la clase del vecino más próximo.
  • Parece que la precisión depende más de k que del tipo de peso. Aunque observamos que los pesos con 'uniform' tienen menor desviación estandard.
Implementación: Representa gráficamente la frontera de decisión. Opcional ("BONUS TRACK") : Mejora la función meshgrid proporcionada de forma que a la hora de realizar gráficamente las fronteras de decisión (durante todos los apartados de la PEC) los colores se degraden en función de la probabilidad de forma que se vea las zonas de incertidumbre de los diferentes algoritmos.
In [7]:
clf = KNeighborsClassifier(n_neighbors=9, weights='uniform')
clf.fit(X_train, y_train)
plot_decision_boundaries(X=X_test[['SepalLengthCm', 'SepalWidthCm']].to_numpy(), y=y_test, model=clf)

plot_decision_boundaries_bonus(x=X_test['SepalLengthCm'], y=X_test['SepalWidthCm'], labels=y_test, model=clf)
In [8]:
from sklearn.metrics import confusion_matrix

preds = clf.predict(X_test)

accuracy = np.true_divide(np.sum(preds == y_test), preds.shape[0])*100
cnf_matrix = confusion_matrix(y_test, preds)

print(accuracy)
print(cnf_matrix)
83.33333333333334
[[10  0  0]
 [ 0  7  3]
 [ 0  2  8]]
Análisis: Analiza los resultados y en especial la frontera de decisión.

Lo resultados no han sido muy buenos. La frontera de desición no parece acurada, hay bastantes zonas que no se han clasificado adecuadamente.

Implementación: Realizar el mismo proceso, pero en este caso para la altura-anchura de los pétalos.
In [9]:
print('----------Petal----------------------')
print('----------División en entrenamiento y test ----------------------')
X_train, X_test, y_train, y_test = train_test_split(iris[['PetalLengthCm', 'PetalWidthCm']],
                                                    iris['Species'],
                                                    test_size=0.2,
                                                    stratify=iris['Species'])

print('----------Creamos la meshgrid con los valores mínimo y máximo de x y y ----------------------')
X=iris[['PetalLengthCm', 'PetalWidthCm']].to_numpy()
# Creamos la meshgrid con los valores mínimo y máximo de 'x' i 'y'.
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1

print('----------Pivoteamos ----------------------')
clf = KNeighborsClassifier()

param_grid = {"n_neighbors": range(1, 11), "weights": ["uniform", "distance"]}

grid_search = GridSearchCV(clf, param_grid=param_grid, cv=4)

grid_search.fit(X_train, y_train)

means = grid_search.cv_results_["mean_test_score"]
stds = grid_search.cv_results_["std_test_score"]
params = grid_search.cv_results_['params']

for mean, std, pms in zip(means, stds, params):
    print("Precisión media: {:.2f} +/- {:.2f} con parametros {}".format(mean*100, std*100, pms))


              param1 = [x['n_neighbors'] for x in params]
              param2 = [x['weights'] for x in params]


              precisions = pd.DataFrame(zip(param1, param2, means), columns=['n_neighbors', 'weights', 'means'])
              precisions = precisions.pivot('n_neighbors', 'weights', 'means')
              sns.heatmap(precisions)
              
----------Petal----------------------
                  ----------División en entrenamiento y test ----------------------
                ----------Creamos la meshgrid con los valores mínimo y máximo de x y y ----------------------
                ----------Pivoteamos ----------------------
                Precisión media: 95.83 +/- 3.63 con parametros {'n_neighbors': 1, 'weights': 'uniform'}
Precisión media: 95.83 +/- 3.63 con parametros {'n_neighbors': 1, 'weights': 'distance'}
Precisión media: 95.83 +/- 5.46 con parametros {'n_neighbors': 2, 'weights': 'uniform'}
Precisión media: 95.83 +/- 3.63 con parametros {'n_neighbors': 2, 'weights': 'distance'}
Precisión media: 96.67 +/- 4.08 con parametros {'n_neighbors': 3, 'weights': 'uniform'}
Precisión media: 96.67 +/- 4.08 con parametros {'n_neighbors': 3, 'weights': 'distance'}
Precisión media: 95.83 +/- 3.63 con parametros {'n_neighbors': 4, 'weights': 'uniform'}
Precisión media: 96.67 +/- 4.08 con parametros {'n_neighbors': 4, 'weights': 'distance'}
Precisión media: 96.67 +/- 4.08 con parametros {'n_neighbors': 5, 'weights': 'uniform'}
Precisión media: 96.67 +/- 4.08 con parametros {'n_neighbors': 5, 'weights': 'distance'}
Precisión media: 96.67 +/- 4.08 con parametros {'n_neighbors': 6, 'weights': 'uniform'}
Precisión media: 96.67 +/- 4.08 con parametros {'n_neighbors': 6, 'weights': 'distance'}
Precisión media: 96.67 +/- 4.08 con parametros {'n_neighbors': 7, 'weights': 'uniform'}
Precisión media: 96.67 +/- 4.08 con parametros {'n_neighbors': 7, 'weights': 'distance'}
Precisión media: 96.67 +/- 4.08 con parametros {'n_neighbors': 8, 'weights': 'uniform'}
Precisión media: 96.67 +/- 4.08 con parametros {'n_neighbors': 8, 'weights': 'distance'}
Precisión media: 96.67 +/- 4.08 con parametros {'n_neighbors': 9, 'weights': 'uniform'}
Precisión media: 96.67 +/- 4.08 con parametros {'n_neighbors': 9, 'weights': 'distance'}
Precisión media: 96.67 +/- 4.08 con parametros {'n_neighbors': 10, 'weights': 'uniform'}
Precisión media: 96.67 +/- 4.08 con parametros {'n_neighbors': 10, 'weights': 'distance'}
Out[9]:
<matplotlib.axes._subplots.AxesSubplot at 0x102c27ba8>
Implementación: En este punto habrás comprobado que las características relacionadas con el pétalo discriminan mejor las especies de la flor de iris que las caracterísiticas relacionadas con el sépalo. Realiza las predicciones con las KNN usando las características del pétalo y calcula su precisión y matriz de confusión.
In [10]:
clf = KNeighborsClassifier(n_neighbors=3, weights='uniform')
clf.fit(X_train, y_train)
plot_decision_boundaries(X=X_test[['PetalLengthCm', 'PetalWidthCm']].to_numpy(),x_min=x_min, x_max=x_max ,y_min=y_min, y_max=y_max, y=y_test, model=clf)

plot_decision_boundaries_bonus(x=X_test['PetalLengthCm'], y=X_test['PetalWidthCm'],x_min=x_min, x_max=x_max ,y_min=y_min, y_max=y_max, labels=y_test, model=clf)
In [11]:
preds = clf.predict(X_test)

accuracy = np.true_divide(np.sum(preds == y_test), preds.shape[0])*100
cnf_matrix = confusion_matrix(y_test, preds)

print(accuracy)
print(cnf_matrix)
93.33333333333333
[[10  0  0]
 [ 0  9  1]
 [ 0  1  9]]
Análisis: A tenor de los resultados, ¿con cuáles características obtenemos una mejor frontera de decisión y por lo tanto discriminamos mucho mejor las clases de flores de iris? (no hace falta hacerlo de forma numérica, visualmente sería suficiente). Analiza las posibles ventajas y limitaciones del algoritmo.

La frontera de decisión resultante cuando hemos utilizando la información basada en los petalos parece más acurada y exacta que la información proporcionada con sépal. Cuando hemos utilizado sépal aparecen "islas" y no son claras las fronteras entre clases.

Las ventajas que tiene este algoritmo son las siguientes:

  • No paramétrico. No hace suposiciones explícitas sobre la forma funcional de los datos, evitando los peligros de la distribución subyacente de los datos.

  • Algoritmo simple. Para explicar, comprender e interpretar.

  • Alta precisión (relativa). Es bastante alta pero no competitiva en comparación con modelos de aprendizaje mejor supervisados.

  • Insensible a los valores atípicos. La precisión puede verse afectada por el ruido o las características irrelevantes.

Las desventajas de este algoritmo son:

  • Basado en instancia. El algoritmo no aprende explícitamente un modelo, en su lugar, elige memorizar las instancias de capacitación que se utilizan posteriormente como conocimiento para la fase de predicción. Concretamente, esto significa que solo cuando se realiza una consulta a nuestra base de datos, es decir cuando le pedimos que prediga una etiqueta dada una entrada, el algoritmo usará las instancias de entrenamiento para escupir una respuesta.

  • Computacionalmente costoso. Porque el algoritmo almacena todos los datos de entrenamiento.

  • Requisito de memoria alta. Almacena todos (o casi todos) los datos de entrenamiento.

2. Support Vector Machine

En este segundo ejercicio clasificaremos los puntos usando el algoritmo SVM con diferentes tipos de kernel. En este caso utilizaremos un kernel radial, un kernel lineal y un kernel polinomial de grado 3. Volveremos a usar una búsqueda en grid (grid search) para la optimización de los hiperparámetros.

En este caso los hiperparámetros a optimitzar son:

  • C: es la regularización, es decir, el valor de penalización de los errores de clasificación. Probaremos los valores: 0.01, 0.1, 1, 10, 50, 100 y 200.
  • gamma: coeficiente que multiplica la distancia entre dos puntos en el kernel. A grosso modo, cuanto más pequeño sea gamma, más influencia tienen dos puntos proximos. Probaremos los valores: 0.001, 0.01, 0.1, 1 y 10.

Como en el caso anterior, para validar el rendimiento del algoritmo usaremos la validación cruzada (cross-validation) con 4 particiones estratificadas. En este caso solo lo haremos para las características altura-anchura del pétalo.

Material adicional que os puede servir de ayuda:

  • Introduction to Statistical Learning. Gareth James, Daniela Witten, Trevor Hastie and Robert Tibshirani

  • Support Vector Machines Succinctly. Alexandre Kowalczyk

  • A Practical Guide to Support Vector Classification. Chih-Wei Hsu, Chih-Chung Chang, and Chih-Jen Lin

  • Tutorial sobre Máquinas de Vector Soporte (SVM). Enrique J. Carmona Suárez

  • A Gentle Introduction to Support Vector Machines in Biomedicine. Alexander Statnikov, Douglas Hardin, Isabelle Guyon, Constantin F. Aliferis

Implementación: Calcula el valor óptimo de los hiperparámetros C y gamma. Hacer un heatmap para visualizar el valor de precisión según los dos hiperparámetros.

Podéis usar los módulos GridSearchCV y svm de sklearn. Analizar que influencia tienen los hiperparámetros C y gamma una vez calculados los mejores hiperparámetros. Para cada tipo de kernel, hacer predicciones para cada uno de ellos, calcular su matriz de confusión y finalmente dibujar sus fronteras de decisión.

In [12]:
from sklearn import svm

clf = svm.SVC()

param_grid = {"C": [0.01, 0.1, 1, 10, 50, 100, 200], "gamma": [0.001, 0.01, 0.1, 1, 10]}

grid_search = GridSearchCV(clf, param_grid=param_grid, cv=4)
grid_search.fit(X_train, y_train)

means = grid_search.cv_results_["mean_test_score"]
stds = grid_search.cv_results_["std_test_score"]
params = grid_search.cv_results_['params']

for mean, std, pms in zip(means, stds, params):
    print("Precisión media: {:.2f} +/- {:.2f} con parámetros {}".format(mean*100, std*100, pms))
              
Precisión media: 95.00 +/- 5.00 con parámetros {'C': 0.01, 'gamma': 0.001}
Precisión media: 95.00 +/- 5.00 con parámetros {'C': 0.01, 'gamma': 0.01}
Precisión media: 96.67 +/- 5.77 con parámetros {'C': 0.01, 'gamma': 0.1}
Precisión media: 96.67 +/- 4.08 con parámetros {'C': 0.01, 'gamma': 1}
Precisión media: 93.33 +/- 4.08 con parámetros {'C': 0.01, 'gamma': 10}
Precisión media: 95.00 +/- 5.00 con parámetros {'C': 0.1, 'gamma': 0.001}
Precisión media: 95.00 +/- 5.00 con parámetros {'C': 0.1, 'gamma': 0.01}
Precisión media: 96.67 +/- 5.77 con parámetros {'C': 0.1, 'gamma': 0.1}
Precisión media: 96.67 +/- 4.08 con parámetros {'C': 0.1, 'gamma': 1}
Precisión media: 93.33 +/- 4.08 con parámetros {'C': 0.1, 'gamma': 10}
Precisión media: 95.00 +/- 5.00 con parámetros {'C': 1, 'gamma': 0.001}
Precisión media: 96.67 +/- 4.08 con parámetros {'C': 1, 'gamma': 0.01}
Precisión media: 96.67 +/- 4.08 con parámetros {'C': 1, 'gamma': 0.1}
Precisión media: 96.67 +/- 4.08 con parámetros {'C': 1, 'gamma': 1}
Precisión media: 96.67 +/- 4.08 con parámetros {'C': 1, 'gamma': 10}
Precisión media: 95.83 +/- 5.46 con parámetros {'C': 10, 'gamma': 0.001}
Precisión media: 96.67 +/- 4.08 con parámetros {'C': 10, 'gamma': 0.01}
Precisión media: 96.67 +/- 4.08 con parámetros {'C': 10, 'gamma': 0.1}
Precisión media: 96.67 +/- 4.08 con parámetros {'C': 10, 'gamma': 1}
Precisión media: 95.83 +/- 3.63 con parámetros {'C': 10, 'gamma': 10}
Precisión media: 97.50 +/- 4.33 con parámetros {'C': 50, 'gamma': 0.001}
Precisión media: 96.67 +/- 4.08 con parámetros {'C': 50, 'gamma': 0.01}
Precisión media: 96.67 +/- 4.08 con parámetros {'C': 50, 'gamma': 0.1}
Precisión media: 95.83 +/- 5.46 con parámetros {'C': 50, 'gamma': 1}
Precisión media: 95.00 +/- 2.89 con parámetros {'C': 50, 'gamma': 10}
Precisión media: 96.67 +/- 4.08 con parámetros {'C': 100, 'gamma': 0.001}
Precisión media: 96.67 +/- 4.08 con parámetros {'C': 100, 'gamma': 0.01}
Precisión media: 95.83 +/- 3.63 con parámetros {'C': 100, 'gamma': 0.1}
Precisión media: 95.00 +/- 5.00 con parámetros {'C': 100, 'gamma': 1}
Precisión media: 95.00 +/- 2.89 con parámetros {'C': 100, 'gamma': 10}
Precisión media: 95.83 +/- 5.46 con parámetros {'C': 200, 'gamma': 0.001}
Precisión media: 96.67 +/- 4.08 con parámetros {'C': 200, 'gamma': 0.01}
Precisión media: 95.00 +/- 5.00 con parámetros {'C': 200, 'gamma': 0.1}
Precisión media: 94.17 +/- 4.33 con parámetros {'C': 200, 'gamma': 1}
Precisión media: 95.00 +/- 2.89 con parámetros {'C': 200, 'gamma': 10}
In [48]:
param1 = [x['C'] for x in params]
param2 = [x['gamma'] for x in params]

precisions = pd.DataFrame(zip(param1, param2, means), columns=['C', 'gamma', 'means'])
precisions = precisions.pivot('C', 'gamma', 'means')
sns.heatmap(precisions)
Out[48]:
<matplotlib.axes._subplots.AxesSubplot at 0x1159d5d30>
Implementación: Dibuja las fronteras de decisión para cada tipo de kernel (con la configuración de parámetros óptima en los diferentes modelos) y calcula la precisión en training y test del mejor modelo.
In [13]:
# Entrenem el classificador amb els paràmetres amb els que hem obtingut major precisió.
clf = svm.SVC(C=100, gamma=0.01, probability=True)
clf.fit(X_train, y_train)

plot_decision_boundaries(X=X_test[['PetalLengthCm', 'PetalWidthCm']].to_numpy(),x_min=x_min, x_max=x_max ,y_min=y_min, y_max=y_max, y=y_test, model=clf)

plot_decision_boundaries_bonus(x=X_test['PetalLengthCm'], y=X_test['PetalWidthCm'],x_min=x_min, x_max=x_max ,y_min=y_min, y_max=y_max, labels=y_test, model=clf)
In [14]:
preds = clf.predict(X_test)

accuracy = np.true_divide(np.sum(preds == y_test), preds.shape[0])*100
cnf_matrix = confusion_matrix(y_test, preds)

print(accuracy)
print(cnf_matrix)
93.33333333333333
[[10  0  0]
 [ 0  9  1]
 [ 0  1  9]]
Análisis: ¿Qué parámetros han dado mejores resultados? ¿Qué variación hay entre las diferentes combinaciones de parámetros? ¿Es significativa la variación entre las diferentes combinaciones? ¿Qué kernel ha ido mejor? ¿Hay algún parámetro con más influencia que otro? ¿Era previsible? Compara visualmente las fronteras de decisión del SVM para los distintos kernels con la frontera de decisión del KNN y analiza las ventajas y desventajas del método.
  • Los parámetros han obtenido unos mejores resultados han sido C = 100 y gamma = 0.01 para la iteración ejecutada. Cada iteración puede variar porque los conjuntos de entrenamiento y test se modifican.
  • Nos encontramos con una diferencia maxima de 5 puntos percentuales, es decir, existe una diferencia considerable y hay variaciones destacables con las desviaciones estandard. Esto deja claro que algunas combinaciones son mejores que otras.
  • Las mejores soluciones se dan con gammas entre 0.001 y 1. Se puede observar que en las mejores soluciones C es bastante variable, por lo que podemos concluir que el parámetro gamma tiene más peso. También se ha detectado que la solución gamma = 10 proporciona peores resultados.

Las fronteras de decisión son muy fluidas y definidas. Se observan las tres clases bien diferenciadas con dos únicos errores.

3. Árboles de decisión

En este tercer ejercicio trazaremos las fronteras de decisión de los dos tipos de atributos (sépalos y pétalos). Veremos que precisión obtenemos con los árboles de decisión. Mapearemos el árbol y lo analizaremos.

Para dibujar el árbol necesitaremos instalar la librería graphviz. Para ello desde terminal escribiremos el siguiente comando:

sudo apt-get install graphviz

Si alguno utiliza el entorno Conda también puede instalarse desde este entorno.

Los árboles de decisión son un método usado en distintas disciplinas como modelo de predicción. Estos son similares a diagramas de flujo, en los que llegamos a puntos en los que se toman decisiones de acuerdo a una regla.

En el campo del aprendizaje automático hay distintas maneras de obtener árboles de decisión, la que usaremos en esta ocasión es conocida como CART: Classification And Regression Trees. Esta es una técnica de aprendizaje supervisado. Tenemos una variable objetivo (dependiente) y nuestra meta es obtener una función que nos permita predecir, a partir de variables predictoras (independientes), el valor de la variable objetivo para casos desconocidos.

Como el nombre indica, CART es una técnica con la que se pueden obtener árboles de clasificación y de regresión. Usamos clasificación cuando nuestra variable objetivo es discreta, mientras que usamos regresión cuando es continua. Nosotros tendremos una variable discreta, así que haremos clasificación.

De manera general, lo que hace este algoritmo es encontrar la variable independiente que mejor separa nuestros datos en grupos, las cuales corresponden con las categorías de la variable objetivo. Esta mejor separación es expresada con una regla. A cada regla le corresponde un nodo.

Implementación: Dibuja el árbol de decisión.

Para ello debemos asegurarnos que tenemos instalada en nuestro entorno la librería graphviz.

In [20]:
from sklearn.tree import DecisionTreeClassifier

clf = DecisionTreeClassifier()

param_grid = {"max_depth": range(4, 10), "min_samples_split": [2, 10, 20, 50, 100]}

grid_search = GridSearchCV(clf, param_grid=param_grid, cv=4)
grid_search.fit(X_train, y_train)

means = grid_search.cv_results_["mean_test_score"]
stds = grid_search.cv_results_["std_test_score"]
params = grid_search.cv_results_['params']

for mean, std, pms in zip(means, stds, params):
    print("Precisión media  {:.2f} +/- {:.2f} con parametros {}".format(mean*100, std*100, pms))


              
Precisión media  95.83 +/- 3.63 con parametros {'max_depth': 4, 'min_samples_split': 2}
Precisión media  95.83 +/- 3.63 con parametros {'max_depth': 4, 'min_samples_split': 10}
Precisión media  95.83 +/- 3.63 con parametros {'max_depth': 4, 'min_samples_split': 20}
Precisión media  95.00 +/- 5.00 con parametros {'max_depth': 4, 'min_samples_split': 50}
Precisión media  33.33 +/- 0.00 con parametros {'max_depth': 4, 'min_samples_split': 100}
Precisión media  95.83 +/- 3.63 con parametros {'max_depth': 5, 'min_samples_split': 2}
Precisión media  95.83 +/- 3.63 con parametros {'max_depth': 5, 'min_samples_split': 10}
Precisión media  95.83 +/- 3.63 con parametros {'max_depth': 5, 'min_samples_split': 20}
Precisión media  95.00 +/- 5.00 con parametros {'max_depth': 5, 'min_samples_split': 50}
Precisión media  33.33 +/- 0.00 con parametros {'max_depth': 5, 'min_samples_split': 100}
Precisión media  95.83 +/- 3.63 con parametros {'max_depth': 6, 'min_samples_split': 2}
Precisión media  95.83 +/- 3.63 con parametros {'max_depth': 6, 'min_samples_split': 10}
Precisión media  95.83 +/- 3.63 con parametros {'max_depth': 6, 'min_samples_split': 20}
Precisión media  95.00 +/- 5.00 con parametros {'max_depth': 6, 'min_samples_split': 50}
Precisión media  33.33 +/- 0.00 con parametros {'max_depth': 6, 'min_samples_split': 100}
Precisión media  95.83 +/- 3.63 con parametros {'max_depth': 7, 'min_samples_split': 2}
Precisión media  95.83 +/- 3.63 con parametros {'max_depth': 7, 'min_samples_split': 10}
Precisión media  95.83 +/- 3.63 con parametros {'max_depth': 7, 'min_samples_split': 20}
Precisión media  95.00 +/- 5.00 con parametros {'max_depth': 7, 'min_samples_split': 50}
Precisión media  33.33 +/- 0.00 con parametros {'max_depth': 7, 'min_samples_split': 100}
Precisión media  95.83 +/- 3.63 con parametros {'max_depth': 8, 'min_samples_split': 2}
Precisión media  95.83 +/- 3.63 con parametros {'max_depth': 8, 'min_samples_split': 10}
Precisión media  95.83 +/- 3.63 con parametros {'max_depth': 8, 'min_samples_split': 20}
Precisión media  95.00 +/- 5.00 con parametros {'max_depth': 8, 'min_samples_split': 50}
Precisión media  33.33 +/- 0.00 con parametros {'max_depth': 8, 'min_samples_split': 100}
Precisión media  95.83 +/- 3.63 con parametros {'max_depth': 9, 'min_samples_split': 2}
Precisión media  95.83 +/- 3.63 con parametros {'max_depth': 9, 'min_samples_split': 10}
Precisión media  95.83 +/- 3.63 con parametros {'max_depth': 9, 'min_samples_split': 20}
Precisión media  95.00 +/- 5.00 con parametros {'max_depth': 9, 'min_samples_split': 50}
Precisión media  33.33 +/- 0.00 con parametros {'max_depth': 9, 'min_samples_split': 100}
In [16]:
clf = DecisionTreeClassifier(max_depth = 4, min_samples_split = 10)
clf.fit(X_train, y_train)

from sklearn.tree import export_graphviz
from pydotplus import graph_from_dot_data

dot_data = export_graphviz(clf)
from IPython.display import Image as PImage

graph = graph_from_dot_data(dot_data)
graph.write_png('tree.png')
PImage("tree.png")
Out[16]:
Análisis: Como se puede observar el árbol de decisión nos muestra diferente información. Analiza dicha información y explícala. Un parámetro muy importante en este método es el índice Gini. Explica en qué consiste y qué influencia tiene en la construcción del árbol de decisión. Por último, comenta las principales ventajas y desventajas de los árboles de decisión.

La interpretación del árbol de este árbol de decisión sería: si la anchura del pétalo es menos de 0.8 centímetros, entonces la flor iris pertenece a la variedad iris-setosa. Si por el contrario, la longitud del pétalo es mayor que 0.8 centímetros y mayor a 1.75 pertenece a Iris.virginica . Si fuera mayor que 0.8 centímetros y menor a 1.75 miraremos la longitud del pétalo. Si es mayor la longitud a 5.45 directamente es Iris.virginica. Si la longitud es menor de 5.45 cm y la anchura es menor de 1.65 entonces es Iris-versicolor y si fuera mayor de 1.65 cm Iris.virginica

  • Indice Gini: Se utiliza para atributos con valores continuos (precio de una casa). Esta función de coste mide el «grado de impureza» de los nodos, es decir, cuán desordenados o mezclados quedan los nodos una vez divididos. Gini es una medida de impureza. Cuando Gini vale 0, significa que ese nodo es totalmente puro, es decir, este debe ser nuestro objetivo.

Entre otros métodos de minería de datos, los árboles de decisión tienen varias ventajas:

  • Fácil de entender e interpretar. Las personas son capaces de comprender los modelos de árboles de decisión después de una breve explicación.
  • Requiere poca preparación de los datos. Otras técnicas a menudo requieren la normalización de datos, utilización de variables ficticias necesitan ser creados y valores en blanco deben ser eliminados.
  • Capaz de manejar tanto datos numéricos y categorizados. Otras técnicas son generalmente especializadas en el análisis de conjuntos de datos que tienen sólo un tipo de variable. (Por ejemplo, las normas de relación sólo se pueden utilizar con variables nominales, mientras que las redes neuronales pueden ser utilizados sólo con variables numéricas.)
  • Utiliza un modelo de caja blanca. Si una situación dada es observable en un modelo entonces la condición se explica fácilmente por la lógica booleana. (Un ejemplo de un modelo de caja negro es una red neural artificial ya que la explicación de los resultados es difícil de entender.)
  • Es posible validar un modelo utilizando pruebas estadísticas. Eso hace que sea posible tener en cuenta la fiabilidad del modelo.
  • Robusto. Se desempeña bien incluso si sus suposiciones son violadas por el verdadero modelo a partir del cual se generaron los datos.
  • Funciona bien con grandes conjuntos de datos. Grandes cantidades de datos pueden ser analizados utilizando recursos informáticos estándar en un plazo razonable.

Desventajas

  • Sobreajuste
  • Pérdida de información al categorizar variables continuas
  • Inestabilidad: un pequeño cambio en los datos puede modificar ampliamente la estructura del árbol. Por lo tanto la interpretación no es tan directa como parece.
Implementación: Calcula la precisión de clasificación que ha obtenido el árbol de decisión que has desarrollado anteriormente.
In [17]:
preds = clf.predict(X_test)

accuracy = np.true_divide(np.sum(preds == y_test), preds.shape[0])*100
cnf_matrix = confusion_matrix(y_test, preds)

print(accuracy)
print(cnf_matrix)
93.33333333333333
[[10  0  0]
 [ 0  9  1]
 [ 0  1  9]]
Implementación: Calcula las fronteras de decisión (por pares) de las características del dataset para training y para test.
In [18]:
plot_decision_boundaries(X=X_test[['PetalLengthCm', 'PetalWidthCm']].to_numpy(),x_min=x_min, x_max=x_max ,y_min=y_min, y_max=y_max, y=y_test, model=clf)

plot_decision_boundaries_bonus(x=X_test['PetalLengthCm'], y=X_test['PetalWidthCm'],x_min=x_min, x_max=x_max ,y_min=y_min, y_max=y_max, labels=y_test, model=clf)
Análisis: Analizar los resultados de las fronteras de decisión.

Las fronteras de decisión son fluidas y definidas. Se observan las tres clases bien diferenciadas con dos únicos errores.

4. Random forest

En este cuarto apartado clasificaremos los puntos usando un Random forest. Utilizaremos, igual que en los casos anteriores, una búsqueda en grid (grid search) parar ajustar los hiperparámetros.

En este caso, los hiperparámetros que debemos ajustar son:

  • max_depth: la profundidad máxima del árbol. Exploraremos los valores entre 6 y 12.
  • n_estimators: nombre de árboles. Exploraremos los valores: 10, 50, 100 y 200.

Igual que en el caso anterior, usaremos validación cruzada (cross-validation) con 4 particiones estratificadas para validar el rendimento del algoritmo con cada combinación de hiperparámetros.

Implementación: Calcula el valor óptimo de los hiperparámetros max_depth y n_estimators. Haz un heatmap para visualizar las precisiones según los dos hiperparámetros.

Podéis usar los módulos GridSearchCV y RandomForestClassifier de sklearn.

In [23]:
from sklearn.ensemble import RandomForestClassifier

clf = RandomForestClassifier()

param_grid = {"max_depth": range(6, 13), "n_estimators": [10, 50, 100, 200]}

grid_search = GridSearchCV(clf, param_grid=param_grid, cv=4)
grid_search.fit(X_train, y_train)

means = grid_search.cv_results_["mean_test_score"]
stds = grid_search.cv_results_["std_test_score"]
params = grid_search.cv_results_['params']

for mean, std, pms in zip(means, stds, params):
    print("Precisión Media  {:.2f} +/- {:.2f} con parametros {}".format(mean*100, std*100, pms))

              param1 = [x['max_depth'] for x in params]
              param2 = [x['n_estimators'] for x in params]

              precisions = pd.DataFrame(zip(param1, param2, means), columns=['max_depth', 'n_estimators', 'means'])
              precisions = precisions.pivot('max_depth', 'n_estimators', 'means')
              sns.heatmap(precisions)
              
Precisión Media  95.83 +/- 3.63 con parametros {'max_depth': 6, 'n_estimators': 10}
Precisión Media  96.67 +/- 2.36 con parametros {'max_depth': 6, 'n_estimators': 50}
Precisión Media  95.83 +/- 3.63 con parametros {'max_depth': 6, 'n_estimators': 100}
Precisión Media  95.83 +/- 3.63 con parametros {'max_depth': 6, 'n_estimators': 200}
Precisión Media  95.83 +/- 3.63 con parametros {'max_depth': 7, 'n_estimators': 10}
Precisión Media  95.83 +/- 3.63 con parametros {'max_depth': 7, 'n_estimators': 50}
Precisión Media  95.83 +/- 3.63 con parametros {'max_depth': 7, 'n_estimators': 100}
Precisión Media  96.67 +/- 2.36 con parametros {'max_depth': 7, 'n_estimators': 200}
Precisión Media  96.67 +/- 2.36 con parametros {'max_depth': 8, 'n_estimators': 10}
Precisión Media  95.83 +/- 3.63 con parametros {'max_depth': 8, 'n_estimators': 50}
Precisión Media  95.83 +/- 3.63 con parametros {'max_depth': 8, 'n_estimators': 100}
Precisión Media  95.83 +/- 3.63 con parametros {#39;max_depth': 8, 'n_estimators': 200}
Precisión Media  96.67 +/- 2.36 con parametros {#39;max_depth': 9, 'n_estimators': 10}
Precisión Media  95.83 +/- 3.63 con parametros {#39;max_depth': 9, 'n_estimators': 50}
Precisión Media  95.83 +/- 3.63 con parametros {#39;max_depth': 9, 'n_estimators': 100}
Precisión Media  96.67 +/- 4.08 con parametros {#39;max_depth': 9, 'n_estimators': 200}
Precisión Media  96.67 +/- 4.08 con parametros {#39;max_depth': 10, 'n_estimators': 10}
Precisión Media  96.67 +/- 4.08 con parametros {#39;max_depth': 10, 'n_estimators': 50}
Precisión Media  95.83 +/- 3.63 con parametros {#39;max_depth': 10, 'n_estimators': 100}
Precisión Media  95.83 +/- 3.63 con parametros {#39;max_depth': 10, 'n_estimators': 200}
Precisión Media  96.67 +/- 4.08 con parametros {#39;max_depth': 11, 'n_estimators': 10}
Precisión Media  95.83 +/- 3.63 con parametros {#39;max_depth': 11, 'n_estimators': 50}
Precisión Media  95.83 +/- 3.63 con parametros {#39;max_depth': 11, 'n_estimators': 100}
Precisión Media  95.83 +/- 3.63 con parametros {#39;max_depth': 11, 'n_estimators': 200}
Precisión Media  97.50 +/- 2.76 con parametros {#39;max_depth': 12, 'n_estimators': 10}
Precisión Media  95.83 +/- 3.63 con parametros {#39;max_depth': 12, 'n_estimators': 50}
Precisión Media  96.67 +/- 4.08 con parametros {#39;max_depth': 12, 'n_estimators': 100}
Precisión Media  96.67 +/- 2.36 con parametros {#39;max_depth': 12, 'n_estimators': 200}
Out[23]:
<matplotlib.axes._subplots.AxesSubplot at 0x127f9e9e8>
Análisis: ¿Qué parámetros han dado mejores resultados? ¿Qué variación hay entre las diferentes combinaciones de parámetros? ¿Es significativa la variación entre las diferentes combinaciones? ¿Hay algún parámetro con más influencia que otro? ¿Era previsible?
  • El mejor resultado lo hemos obtenido con max_depth = 12 y n_estimators = 10 (puede variar dependiendo de la distribución de conjunto de entrenamiento y test )
  • Las diferencias son de casi 2 puntos percentuales como máximo, es decir, es bastante pequeña.
  • No he observado en esta iteración ninguna relación que me haga ver una mayor influencia. Esperava que cuando mayor seria n_estimators mayor precisión pero no ha sido así.

En la práctica anterior estuvimos estudiando la influencia de algunos de los parámetros del Random forest, entre ellos el parámetro max_depth y cómo una profundidad del árbol demasiado grande podía provocar lo que conocemos como sobreentrenamiento (overfitting).

En este apartado vamos a usar las capacidades interactivas que nos ofrece la librería plotly para visualizar efectos del overfitting y como cambia la frontera de decisión debido a ello. Para ello, crearemos gráficos de dos clasificadores Random Forest, el primero con una profundidad de árbol razonable (max_depth=4) y el segundo presentando un claro overfitting (por ejemplo, max_depth=300).

Invocar el código de Plotly es muy similar al de Matplotlib para generar la frontera de decisión. Necesitaremos una malla de Numpy para formar la base de nuestras gráficas de superficie, así como el método predict del modelo de aprendizaje para poblar nuestra frontera con datos.

Implementación: Genera dichas visualizaciones interactivas con Plotly.

Recordar que tendréis que instalaros la libería Plotly. Esto se puede hacer mediante:

pip install plotly

Nota: No hace falta que lo hagáis para las cuatro características (altura-anchura del pétalo y altura-anchura del sépalo), con hacerlo para dos sería suficiente.

In [24]:
clf = RandomForestClassifier(n_estimators = 10, max_depth = 4)
clf.fit(X_train, y_train)

plot_decision_boundaries(X=X_test[['PetalLengthCm', 'PetalWidthCm']].to_numpy(),x_min=x_min, x_max=x_max ,y_min=y_min, y_max=y_max, y=y_test, model=clf)


clf = RandomForestClassifier(n_estimators = 10, max_depth = 300)
clf.fit(X_train, y_train)

plot_decision_boundaries(X=X_test[['PetalLengthCm', 'PetalWidthCm']].to_numpy(),x_min=x_min, x_max=x_max ,y_min=y_min, y_max=y_max, y=y_test, model=clf)
Análisis: Razona las ventajas y desventajas del algoritmo de Random Forest.

Ventajas de Random Forest

  • Existen muy pocas suposiciones y por lo tanto la preparación de los datos es mínima.
  • Puede manejar hasta miles de variables de entrada e identificar las más significativas. Método de reducción de dimensionalidad.
  • Una de las salidas del modelo es la importancia de variables.
  • Incorpora métodos efectivos para estimar valores faltantes.
  • Es posible usarlo como método no supervisado (clustering) y detección de outliers.

Desventajas de Random Forest

  • Pérdida de interpretación
  • Bueno para clasificación, no tanto para regresión. Las predicciones no son de naturaleza continua.
  • En regresión, no puede predecir más allá del rango de valores del conjunto de entrenamiento.
  • Poco control en lo que hace el modelo (modelo caja negra para modeladores estadísticos)

5. Redes neuronales

En esta última parte de la PEC vamos a usar la librería Keras. Para ello compararemos las redes con la capa densa regular (regular Dense Layer) con un número diferente de nodos, empleando como función de activación Softmax y como optimizador Adam.

Para ello tendremos que asegurarnos de tener las librerias Tensorflow y Keras instaladas.

Para ello desde terminal escribiremos el siguiente comando:

pip install tensorflow

pip install keras

Si alguno utiliza el entorno Conda también puede instalarse desde este entorno.

Por otro lado, este es el apartado más complicado de toda la práctica y con el que menos estáis familiarizados. Por ello a lo largo del apartado os iremos dando una sería de enlaces a conceptos y ejemplos que os ayudarán a entender mejor lo que estamos haciendo. Es altamente recomendable leer con detenimiento los links (marcados en azul) y referencias indicadas y entender las explicaciones teóricas y los ejemplos de código proporcionados.

In [27]:
# Importamos la librerías necesarias
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import tensorflow as tf

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import OneHotEncoder, StandardScaler


from tensorflow.python.keras.models import Sequential
from tensorflow.python.keras.layers import Dense
from tensorflow.python.keras.callbacks import TensorBoard
from tensorflow.python.keras.wrappers.scikit_learn import KerasClassifier

from sklearn.metrics import roc_curve, auc
from sklearn.model_selection import cross_val_score

# Para ignorar Warnings futuros
import warnings
warnings.simplefilter(action='ignore', category=FutureWarning)

plt.style.use('ggplot')
%matplotlib inline

Para la preparación de los datos, simplemente usaremos el OneHotEncoder para codificar las características enteras en un vector One-hot y utilizaremos un StandardScaler para eliminar la media y escalar las características a la varianza unitaria. Finalmente, usaremos train_test_split para comparar nuestros resultados más adelante.

Implementación: Prepara los datos de entrada en la red. Te ofrecemos de ayuda los pasos que has de realizar. Pasos: - Cargamos los datos - One hot encoding - Estandarizamos los datos (esto es importante para la convergencia de la red neuronal) - Dividimos los datos en train/test
In [97]:
iris = load_iris()
X = iris['data'][:,[2,3]]
y = iris['target']
names = iris['target_names']
feature_names = iris['feature_names']

# One hot encoding
enc = OneHotEncoder()
Y = enc.fit_transform(y[:, np.newaxis]).toarray()

# Scale data to have mean 0 and variance 1 
# which is importance for convergence of the neural network
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# Split the data set into training and testing
X_train, X_test, Y_train, Y_test = train_test_split(
    X_scaled, Y, test_size=0.3, random_state=2)

n_features = X.shape[1]
n_classes = Y.shape[1]

Configuramos nuestros modelos de red neuronal

Para ello nos definimos una función que será la encargada de realizar nuestros modelos (en este caso concreto vamos a crear tres modelos que llamaremos Model1, Model2 y Model3). Utilizaremos como función de activación la función ReLu y como función de pérdidas la función categorical_crossentropy

Para mayor profundidad sobre el estudio de las funciones de activación:

Para mayor profundidad en el estudio de las funciones de pérdidas:

In [98]:
def create_custom_model(input_dim, output_dim, nodes, n=1, name='model'):
    def create_model():

        # Creamos el modelo
        model = Sequential(name=name)
        for i in range(n):
            model.add(Dense(nodes, input_dim=input_dim, activation='relu'))
        model.add(Dense(output_dim, activation='softmax'))

        # Compilamos el modelo
        model.compile(loss='categorical_crossentropy',
                      optimizer='adam',
                      metrics=['accuracy'])
        return model
    return create_model

models = [create_custom_model(n_features, n_classes, 8, i, 'model_{}'.format(i))
          for i in range(1, 4)]

for create_model in models:
    create_model().summary()
Model: "model_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
dense_339 (Dense)            (None, 8)                 24
_________________________________________________________________
dense_340 (Dense)            (None, 3)                 27
=================================================================
Total params: 51
Trainable params: 51
Non-trainable params: 0
_________________________________________________________________
Model: "model_2"
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
dense_341 (Dense)            (None, 8)                 24
_________________________________________________________________
dense_342 (Dense)            (None, 8)                 72
_________________________________________________________________
dense_343 (Dense)            (None, 3)                 27
=================================================================
Total params: 123
Trainable params: 123
Non-trainable params: 0
_________________________________________________________________
Model: "model_3"
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
dense_344 (Dense)            (None, 8)                 24
_________________________________________________________________
dense_345 (Dense)            (None, 8)                 72
_________________________________________________________________
dense_346 (Dense)            (None, 8)                 72
_________________________________________________________________
dense_347 (Dense)            (None, 3)                 27
=================================================================
Total params: 195
Trainable params: 195
Non-trainable params: 0
_________________________________________________________________

Entrenando los modelos

Pasamos ahora al entrenamiento. Para ello utilizaremos TensorBoard Callback1, 2, 3, 4 para poder explorar el modelo y las salidas en detalle.

Implementación: Utilizando TensorBoard Callback crea una función de entrenamiento para nuestros modelos con los siguientes parámetros: epochs = 50 y batch_size = 5. Posteriormente calcula la precisión y la pérdida en test de nuestros modelos y guardate los modelos para después poder visualizarlos.
In [99]:
from keras.callbacks import TensorBoard

history_dict = {}

# TensorBoard Callback
cb = TensorBoard()

for create_model in models:
    model = create_model()
    print('Model name:', model.name)
    history_callback = model.fit(X_train, Y_train,
                                 batch_size=5,
                                 epochs=50,
                                 verbose=0,
                                 validation_data=(X_test, Y_test),
                                 callbacks=[cb])
    score = model.evaluate(X_test, Y_test, verbose=0)
    print('Test loss:', score[0])
    print('Test accuracy:', score[1])

    history_dict[model.name] = [history_callback, model]
Model name: model_1
WARNING:tensorflow:Method (on_train_batch_end) is slow compared to the batch update (0.153528). Check your callbacks.
Test loss: 0.1975460648536682
Test accuracy: 0.9777777791023254
Model name: model_2
Test loss: 0.10104452073574066
Test accuracy: 0.9777777791023254
Model name: model_3
Test loss: 0.07522713392972946
Test accuracy: 0.9777777791023254

Visualización de resultados

Implementación: Visualiza para los tres modelos la precisión y pérdida en validación. Calcula la curva ROC y la precisión media del modelo, para ello utiliza una k-fold validation = 10. Más información sobre la interpretación de las curvas ROC: - https://developers.google.com/machine-learning/crash-course/classification/roc-and-auc?hl=es-419 - https://towardsdatascience.com/understanding-auc-roc-curve-68b2303cc9c5 - http://mlwiki.org/index.php/ROC_Analysis - https://machinelearningmastery.com/roc-curves-and-precision-recall-curves-for-classification-in-python/
In [102]:
fig, (ax1, ax2) = plt.subplots(2, figsize=(8, 6))

for model_name in history_dict:
    val_acc = history_dict[model_name][0].history['val_accuracy']
    val_loss = history_dict[model_name][0].history['val_loss']
    ax1.plot(val_acc, label=model_name)
    ax2.plot(val_loss, label=model_name)

ax1.set_ylabel('validation accuracy')
ax2.set_ylabel('validation loss')
ax2.set_xlabel('epochs')
ax1.legend()
ax2.legend()
plt.show()


from sklearn.metrics import roc_curve, auc

plt.figure(figsize=(10, 10))
plt.plot([0, 1], [0, 1], 'k--')

for model_name in history_dict:
    model = history_dict[model_name][1]

    Y_pred = model.predict(X_test)
    fpr, tpr, threshold = roc_curve(Y_test.ravel(), Y_pred.ravel())

    plt.plot(fpr, tpr, label='{}, AUC = {:.3f}'.format(model_name, auc(fpr, tpr)))
plt.xlabel('False positive rate')
plt.ylabel('True positive rate')
plt.title('ROC curve')
plt.legend();


from keras.wrappers.scikit_learn import KerasClassifier
from sklearn.model_selection import cross_val_score

create_model = create_custom_model(n_features, n_classes, 8, 3)

estimator = KerasClassifier(build_fn=create_model, epochs=50, batch_size=5, verbose=0)
scores = cross_val_score(estimator, X_scaled, Y, cv=10)
print("Accuracy : {:0.2f} (+/- {:0.2f})".format(scores.mean(), scores.std()))
Accuracy : 0.95 (+/- 0.06)
Análisis: Analiza los resultados obtenidos

Sabiendo que la gráfica ROC compara la tasa de falsos positivos con la tasa de verdaderos positimos, he decidido utilizar este valor para ver que módelo ha obtenido mejores resultados. Los resultados obtenidos han sido muy buenos, en todos los modelos hemos obtenidos valores cercanos a 1. Creo que el modelo 3 es el mejor de todos.

Además, calculamos para cada modelo el Área bajo la curva (AUC), donde auc = 1 es una clasificación perfecta y auc = 0.5 es una suposición aleatoria y, en este caso, obtenemos resultados muy cercanos a 1.

Implementación: Representa las fronteras de decisión que se obtienen de la red.
In [105]:
model.fit(X_train, Y_train,batch_size=5,
                                 epochs=50,
                                 verbose=0,
                                 validation_data=(X_test, Y_test),
                                 callbacks=[cb])

plot_decision_boundaries_bonus(X_test[:,[0]],X_test[:,[1]], Y_test, model)
WARNING:tensorflow:Method (on_train_batch_end) is slow compared to the batch update (0.270889). Check your callbacks.