Introducción al aprendizaje automático con scikit-learn

En los últimos tiempos habrás oído hablar de machine learning, deep learning, reinforcement learning, muchas más cosas que contienen la palabra learning y, por supuesto, Big Data. Con los avances en capacidad de cálculo de los últimos años y la popularización de lenguajes de alto nivel, hemos entrado de lleno en la fiebre de hacer que las máquinas aprendan. En esta clase veremos cómo utilizar el paquete scikit-learn de Python para poder crear modelos predictivos a partir de nuestros datos de una manera rápida y sencilla.


In [1]:
%matplotlib inline
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

np.random.seed(42)

En primer lugar vamos a probar con un ejemplo muy sencillo: ajustar una recta a unos datos. Esto difícilmente se puede llamar machine learning, pero nos servirá para ver cómo es la forma de trabajar con scikit-learn, cómo se entrenan los modelos y cómo se calculan las predicciones.

En primer lugar fabricamos unos datos distribuidos a lo largo de una recta con un poco de ruido:


In [2]:
x = np.random.randn(50)
y = 2.0 * x + 0.8 * np.random.randn(50)

plt.scatter(x, y)


Out[2]:
<matplotlib.collections.PathCollection at 0x7f54a0b9e828>

El proceso para usar scikit-learn es el siguiente:

  1. Separar los datos en matriz de características features y variable a predecir y
  2. Seleccionar el modelo
  3. Elegir los hiperparámetros
  4. Ajustar o entrenar el modelo (model.fit)
  5. Predecir con datos nuevos (model.predict)

In [3]:
from sklearn.linear_model import LinearRegression

In [4]:
model = LinearRegression(fit_intercept=True)
Tenemos que hacer este `reshape` para transformar nuestro vector en una matriz de columnas. Rara vez tendremos que repetir este paso, puesto que en la práctica siempre tendremos varias variables.

In [5]:
features = x.reshape(-1, 1)

In [6]:
model.fit(features, y)


Out[6]:
LinearRegression(copy_X=True, fit_intercept=True, n_jobs=1, normalize=False)

In [7]:
y_hat = model.predict(features)

Para calcular el error, en el módulo sklearn.metrics tenemos varias funciones útiles:


In [8]:
from sklearn import metrics

In [9]:
abs_error = metrics.mean_absolute_error(y, y_hat)
abs_error


Out[9]:
0.52948707531800132

Y ahora predecimos con datos nuevos:


In [10]:
x_new = np.linspace(x.min(), x.max(), 10)

In [11]:
y_pred = model.predict(x_new.reshape(-1, 1))

In [12]:
plt.scatter(x, y)

plt.plot(x_new, y_pred, 'k--')
plt.scatter(x_new, y_pred, marker='x', lw=3, zorder=10)

plt.fill_between(x_new, y_pred + abs_error, y_pred - abs_error, color="C0", alpha=0.3)


Out[12]:
<matplotlib.collections.PolyCollection at 0x7f5495d0a320>

¡Y ya está! Lo básico de scikit-learn está aquí. Lo próximo será usar diferentes tipos de modelos y examinar con rigor su rendimiento para poder seleccionar el que mejor funcione para nuestros datos.

Introducción rápida al aprendizaje automático

En aprendizaje automático tenemos dos tipos de problemas:

  • Aprendizaje supervisado, cuando tengo datos etiquetados, es decir: conozco la variable a predecir de un cierto número de observaciones. Pasándole esta información al algoritmo, este será capaz de predecir dicha variable cuando reciba observaciones nuevas. Dependiendo de la naturaleza de la variable a predecir, tendremos a su vez:
    • Regresión, si es continua (como el caso anterior), o
    • Clasificación, si es discreta o categórica (sí/no, color de ojos, etc)
  • Aprendizaje no supervisado, cuando no tenemos datos etiquetados y por tanto no tengo ninguna información a priori. En este caso usaremos los algoritmos para descubrir patrones en los datos y agruparlos, pero tendremos que manualmente inspeccionar el resultado después y ver qué sentido podemos darle a esos grupos.

En función de la naturaleza de nuestro problema, scikit-learn proporciona una gran variedad de algoritmos que podemos elegir.

Clasificación

En scikit-learn tenemos disponibles muchos datasets clásicos de ejemplo que podemos utilizar para practicar. Uno de ellos es el dataset MNIST, que consiste en imágenes escaneadas de números escritos a mano por funcionarios de los EEUU. Para cargarlo, importamos la función correspondiente de sklearn.datasets:


In [13]:
from sklearn.datasets import load_digits

In [14]:
digits = load_digits()
print(digits["DESCR"])


Optical Recognition of Handwritten Digits Data Set
===================================================

Notes
-----
Data Set Characteristics:
    :Number of Instances: 5620
    :Number of Attributes: 64
    :Attribute Information: 8x8 image of integer pixels in the range 0..16.
    :Missing Attribute Values: None
    :Creator: E. Alpaydin (alpaydin '@' boun.edu.tr)
    :Date: July; 1998

This is a copy of the test set of the UCI ML hand-written digits datasets
http://archive.ics.uci.edu/ml/datasets/Optical+Recognition+of+Handwritten+Digits

The data set contains images of hand-written digits: 10 classes where
each class refers to a digit.

Preprocessing programs made available by NIST were used to extract
normalized bitmaps of handwritten digits from a preprinted form. From a
total of 43 people, 30 contributed to the training set and different 13
to the test set. 32x32 bitmaps are divided into nonoverlapping blocks of
4x4 and the number of on pixels are counted in each block. This generates
an input matrix of 8x8 where each element is an integer in the range
0..16. This reduces dimensionality and gives invariance to small
distortions.

For info on NIST preprocessing routines, see M. D. Garris, J. L. Blue, G.
T. Candela, D. L. Dimmick, J. Geist, P. J. Grother, S. A. Janet, and C.
L. Wilson, NIST Form-Based Handprint Recognition System, NISTIR 5469,
1994.

References
----------
  - C. Kaynak (1995) Methods of Combining Multiple Classifiers and Their
    Applications to Handwritten Digit Recognition, MSc Thesis, Institute of
    Graduate Studies in Science and Engineering, Bogazici University.
  - E. Alpaydin, C. Kaynak (1998) Cascading Classifiers, Kybernetika.
  - Ken Tang and Ponnuthurai N. Suganthan and Xi Yao and A. Kai Qin.
    Linear dimensionalityreduction using relevance weighted LDA. School of
    Electrical and Electronic Engineering Nanyang Technological University.
    2005.
  - Claudio Gentile. A New Approximate Maximal Margin Classification
    Algorithm. NIPS. 2000.

Ya tenemos los datos separados en matriz de características y vector de predicción. En este caso, tendré 64 = 8x8 características (un valor numérico por cada pixel de la imagen) y mi variable a predecir será el número en sí.

Siempre que se hace aprendizaje supervisado, se ha de dividir el dataset en una parte para entrenamiento y otra para test (incluso a veces hay una partición más para validación)


In [25]:
from sklearn.model_selection import train_test_split

In [26]:
# X_train, X_test, Y_train, Y_test =
X_train, X_test, Y_train, Y_test = train_test_split(digits.data, digits.target, train_size=0.75)

In [27]:
# preserve
X_train.shape, Y_train.shape


Out[27]:
((1347, 64), (1347,))

In [28]:
# preserve
X_test.shape, Y_test.shape


Out[28]:
((450, 64), (450,))

Para visualizar estas imágenes tendremos que hacer un .reshape:


In [29]:
num_ = X_test[42]
label_ = Y_test[42]
num_


Out[29]:
array([  0.,   2.,  15.,  16.,   6.,   0.,   0.,   0.,   0.,   5.,  16.,
        15.,  14.,   0.,   0.,   0.,   0.,   5.,  13.,  10.,  14.,   0.,
         0.,   0.,   0.,   0.,   0.,  12.,  12.,   0.,   0.,   0.,   0.,
         0.,   1.,  16.,   7.,   0.,   0.,   0.,   0.,   0.,  10.,  15.,
         2.,   0.,   0.,   0.,   0.,   3.,  16.,  10.,   8.,   6.,   1.,
         0.,   0.,   2.,  15.,  16.,  16.,  16.,   7.,   0.])

In [30]:
num_.reshape(8, 8).astype(int)


Out[30]:
array([[ 0,  2, 15, 16,  6,  0,  0,  0],
       [ 0,  5, 16, 15, 14,  0,  0,  0],
       [ 0,  5, 13, 10, 14,  0,  0,  0],
       [ 0,  0,  0, 12, 12,  0,  0,  0],
       [ 0,  0,  1, 16,  7,  0,  0,  0],
       [ 0,  0, 10, 15,  2,  0,  0,  0],
       [ 0,  3, 16, 10,  8,  6,  1,  0],
       [ 0,  2, 15, 16, 16, 16,  7,  0]])

In [31]:
plt.figure(figsize=(2, 2))
plt.imshow(num_.reshape(8, 8), cmap=plt.cm.gray_r)


Out[31]:
<matplotlib.image.AxesImage at 0x7f5494d40278>

In [32]:
label_


Out[32]:
2

Ten en cuenta que nosotros sabemos qué número es cada imagen porque somos humanos y podemos leerlas. El ordenador lo sabe porque están etiquetadas, pero ¿qué pasa si viene una imagen nueva? Para eso tendremos que construir un modelo de clasificación. En este caso aplicaremos la regresión logística


In [34]:
from sklearn.linear_model import LogisticRegression

In [37]:
# Inicializamos el modelo
model = LogisticRegression()

In [44]:
# Lo entrenamos
model.fit(X_train, Y_train)


Out[44]:
LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
          intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1,
          penalty='l2', random_state=None, solver='liblinear', tol=0.0001,
          verbose=0, warm_start=False)

Y una vez que hemos ajustado el modelo, comprobemos cuáles son sus predicciones usando los mismos datos de entrenamiento:


In [45]:
# Vemos los resultados para los datos de test
predictions = model.predict(X_test)

De nuevo usamos sklearn.metrics para medir la eficacia del algoritmo:


In [46]:
metrics.accuracy_score(predictions, Y_test)


Out[46]:
0.96444444444444444

¡Parece que hemos acertado prácticamente todas! Más tarde volveremos sobre este porcentaje de éxito, que bien podría ser engañoso. De momento, representemos otra medida de éxito que es la matriz de confusión:


In [48]:
metrics.confusion_matrix(predictions, Y_test)


Out[48]:
array([[47,  0,  0,  0,  0,  0,  0,  0,  0,  0],
       [ 0, 36,  1,  0,  1,  0,  0,  1,  3,  0],
       [ 0,  0, 43,  0,  0,  0,  0,  0,  0,  0],
       [ 0,  1,  0, 42,  0,  0,  0,  0,  0,  0],
       [ 0,  0,  0,  0, 46,  0,  0,  0,  0,  0],
       [ 0,  1,  0,  0,  0, 43,  1,  0,  0,  0],
       [ 0,  0,  0,  0,  1,  0, 43,  0,  0,  0],
       [ 0,  0,  0,  0,  1,  0,  0, 43,  0,  0],
       [ 0,  1,  0,  1,  1,  0,  0,  0, 43,  1],
       [ 0,  0,  0,  0,  0,  1,  0,  0,  0, 48]])

In [59]:
plt.imshow(metrics.confusion_matrix(predictions, Y_test), cmap=plt.cm.Blues_r)


Out[59]:
<matplotlib.image.AxesImage at 0x7f54941ea518>

Clustering y reducción de dimensionalidad

Una vez que hemos visto los dos tipos de problemas supervisados, vamos a ver cómo se trabajan los problemas no supervisados. En primer lugar vamos a fabricar dos nubes de puntos usando la función make_blobs:


In [61]:
# preserve
# https://github.com/amueller/scipy-2016-sklearn/blob/master/notebooks/05%20Supervised%20Learning%20-%20Classification.ipynb
from sklearn.datasets import make_blobs

In [62]:
# preserve
features, labels = make_blobs(centers=[[6, 0], [2, -1]], random_state=0)
features.shape


Out[62]:
(100, 2)

In [63]:
# preserve
plt.scatter(features[:, 0], features[:, 1], c=labels)


Out[63]:
<matplotlib.collections.PathCollection at 0x7f54943a2198>

Hemos creado dos grupos y algunos puntos se solapan, pero ¿qué pasaría si no tuviésemos esta información visual? Vamos a emplear un modelo de clustering para agrupar los datos: en este caso KMeans


In [64]:
from sklearn.cluster import KMeans

In [65]:
model = KMeans()
model


Out[65]:
KMeans(algorithm='auto', copy_x=True, init='k-means++', max_iter=300,
    n_clusters=8, n_init=10, n_jobs=1, precompute_distances='auto',
    random_state=None, tol=0.0001, verbose=0)

Observa que por defecto tenemos 8 clusters. Veamos qué ocurre:


In [66]:
model.fit(features)


Out[66]:
KMeans(algorithm='auto', copy_x=True, init='k-means++', max_iter=300,
    n_clusters=8, n_init=10, n_jobs=1, precompute_distances='auto',
    random_state=None, tol=0.0001, verbose=0)

Ahora no pasamos la información de las etiquetas al algoritmo a la hora de entrenar. En la práctica por supuesto no la tendremos.


In [67]:
centroids = model.cluster_centers_
centroids


Out[67]:
array([[ 1.22637705,  0.15951107],
       [ 5.77543498,  0.15908811],
       [ 1.32217579, -1.72755035],
       [ 2.71409315, -1.16992711],
       [ 4.30407524,  0.56861558],
       [ 7.09225761, -0.56687762],
       [ 4.92457211, -1.73109765],
       [ 6.89743394,  1.6747775 ]])

In [68]:
labels_pred = model.predict(features)

Y ahora preparamos el código para representar todas las regiones:


In [77]:
# preserve
xmin, xmax = features[:, 0].min(), features[:, 0].max()
ymin, ymax = features[:, 1].min(), features[:, 1].max()

xx, yy = np.meshgrid(
    np.linspace(xmin, xmax),
    np.linspace(ymin, ymax)
)

mesh = np.c_[xx.ravel(), yy.ravel()]
mesh


Out[77]:
array([[ 0.50874241, -3.22340315],
       [ 0.66713041, -3.22340315],
       [ 0.82551842, -3.22340315],
       ..., 
       [ 7.95297862,  2.2408932 ],
       [ 8.11136662,  2.2408932 ],
       [ 8.26975462,  2.2408932 ]])

In [78]:
Z = model.predict(mesh)

In [80]:
# http://pybonacci.org/2015/01/14/introduccion-a-machine-learning-con-python-parte-1/
plt.pcolormesh(xx, yy, Z.reshape(xx.shape))

plt.scatter(features[:, 0], features[:, 1], marker='x', color='k') 
plt.scatter(centroids[:, 0], centroids[:, 1], marker='+', color='r', lw=2)


Out[80]:
<matplotlib.collections.PathCollection at 0x7f54947376d8>

Si lo metemos todo en una función interactiva:


In [81]:
from ipywidgets import interact

In [82]:
# preseve
@interact(n=(2, 6))
def cluster(n=3):
    model = KMeans(n_clusters=n)
    model.fit(features)
    labels_pred = model.predict(features)
    centroids = model.cluster_centers_

    Z = model.predict(mesh)

    plt.pcolormesh(xx, yy, Z.reshape(xx.shape))
    plt.scatter(features[:, 0], features[:, 1], marker='x', color='k')
    plt.scatter(centroids[:, 0], centroids[:, 1], marker='+', color='r', lw=2)

    plt.show()


Reducción de dimensionalidad

Vamos a rescatar nuestro dataset de los dígitos y tratar de visualizarlo en dos dimensiones, lo que se conoce como reducción de dimensionalidad.


In [43]:
from sklearn.manifold import Isomap

In [44]:
model = Isomap(n_components=2)

In [45]:
model.fit(digits.data)


Out[45]:
Isomap(eigen_solver='auto', max_iter=None, n_components=2, n_jobs=1,
    n_neighbors=5, neighbors_algorithm='auto', path_method='auto', tol=0)

Y ahora proyectamos los datos usando .transform:


In [46]:
digits_proj = model.transform(digits.data)

In [47]:
plt.scatter(digits_proj[:, 0], digits_proj[:, 1],
            c=digits.target, cmap=plt.cm.Vega10, alpha=0.5)
plt.colorbar()


Out[47]:
<matplotlib.colorbar.Colorbar at 0xe127b70>

Ejercicio

  1. Visualiza el dataset de las flores (load_iris) utilizando las funciones que tienes más abajo. ¿Hay alguna forma clara de separar las tres especies de flores?
  2. Separa el dataset en matriz de características features y vector de etiquetas labels. Conviértelos a arrays de NumPy usando .as_matrix().
  3. Reduce la dimensionalidad del dataset a 2 usando sklearn.manifold.Isomap o sklearn.decomposition.PCA y usa un algoritmo de clustering con 3 clusters. ¿Se parecen los clusters que aparecen a los grupos originales?
  4. Predice el tipo de flor usando un algoritmo de clasificación. Visualiza la matriz de confusión. ¿Cuál es el porcentaje de aciertos del algoritmo? ¿Es más certero en algún tipo de flor en concreto? ¿Concuerda esto con lo que pensaste en el apartado 1?

In [84]:
# preserve
import pandas as pd

def load_iris_df():
    from sklearn.datasets import load_iris

    iris = load_iris()
    features, labels = iris.data, iris.target

    df = pd.DataFrame(features, columns=iris.feature_names)
    df["species"] = pd.Categorical.from_codes(iris.target, categories=iris.target_names)

    #df = df.replace({'species': {0: iris.target_names[0], 1: iris.target_names[1], 2: iris.target_names[2]}})

    return df

iris_df = load_iris_df()

In [85]:
# preserve
iris_df.head()


Out[85]:
sepal length (cm) sepal width (cm) petal length (cm) petal width (cm) species
0 5.1 3.5 1.4 0.2 setosa
1 4.9 3.0 1.4 0.2 setosa
2 4.7 3.2 1.3 0.2 setosa
3 4.6 3.1 1.5 0.2 setosa
4 5.0 3.6 1.4 0.2 setosa

In [88]:
# preserve
_ = pd.tools.plotting.scatter_matrix(iris_df, c=iris_df["species"].cat.codes, figsize=(10, 10))




¡Síguenos en Twitter!



Este notebook ha sido realizado por: Juan Luis Cano



<span xmlns:dct="http://purl.org/dc/terms/" property="dct:title">Curso AeroPython</span> por <span xmlns:cc="http://creativecommons.org/ns#" property="cc:attributionName">Juan Luis Cano Rodriguez y Alejandro Sáez Mollejo</span> se distribuye bajo una Licencia Creative Commons Atribución 4.0 Internacional.