Este breve tutorial explica algunos de los conceptos relacionados con la librería scikit-learn
de python.
scikit-learn
¶python
.SciPy
(Scientific Python), que debe ser instalada antes de utilizarse, incluyendo:
scikit-learn
¶scikit-learn
son:Las principales ventajas de scikit-learn
son las siguientes:
Cell
(celda) pulsando [shift] + [Enter]
o presionando el botón Play
en la barra de herramientas.[shift] + [tab]
después de los paréntesis de apertura function(
function?
Una parte muy importante del aprendizaje automático es la visualización de datos. La herramienta más habitual para esto en Python es matplotlib
. Es un paquete extremadamente flexible y ahora veremos algunos elementos básicos.
Ya que estamos usando los libros (notebooks) Jupyter, vamos a usar una de las funciones mágicas que vienen incluidas en IPython, el modo "matoplotlib inline", que dibujará los plots directamente en el libro.
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
# Dibujar una línea
x = np.linspace(0, 10, 100)
plt.plot(x, np.sin(x));
# Dibujar un scatter
x = np.random.normal(size=500)
y = np.random.normal(size=500)
plt.scatter(x, y);
# Mostrar imágenes usando imshow
# - Tener en cuenta que el origen por defecto está arriba a la izquierda
x = np.linspace(start=1, stop=12, num=100)
y = x[:, np.newaxis]
im = y * np.sin(x) * np.cos(y)
print(im.shape)
plt.imshow(im);
# Hacer un diagrama de curvas de nivel (contour plot)
# - El origen aquí está abajo a la izquierda
plt.contour(im);
# El modo mágico "notebook" en lugar de inline permite que los plots sean interactivos
%matplotlib notebook
# Plot en 3D
from mpl_toolkits.mplot3d import Axes3D
ax = plt.axes(projection='3d')
xgrid, ygrid = np.meshgrid(x, y.ravel())
ax.plot_surface(xgrid, ygrid, im, cmap=plt.cm.viridis, cstride=2, rstride=2, linewidth=0);
Hay muchísimos tipos de gráficos disponibles. Una forma útila de explorarlos es mirar la galería de matplotlib.
Puedes probar estos ejemplos fácilmente en el libro de notas: simplemente copia el enlace Source Code
de cada página y pégalo en el libro usando el comando mágico %load
.
Por ejemplo:
# %load https://matplotlib.org/mpl_examples/mplot3d/scatter3d_demo.py
iris
¶Vamos a utilizar un ejemplo típico en machine learning que es la base de datos iris
. En esta base de datos hay tres clases a predecir, que son tres especies distintas de la flor iris, de manera que, para cada flor, se extraen cuatro medidas o variables de entrada (longitud y ancho de los pétalos y de los sépalos, en cm). Las tres especies a distinguir son iris setosa, iris virginica e iris versicolor.
Como ya hemos comentado, para la lectura de datos haremos uso de Pandas. Esta librería tiene un método read_csv
que nos va a permitir leer los datos desde un fichero de texto csv
.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn import neighbors
from sklearn import preprocessing
Con estas líneas, importamos la funcionalidad necesaria para el ejemplo. pandas
nos permitirá leer los datos, numpy
nos va a permitir trabajar con ellos de forma matricial, matplotlib
nos permite hacer representaciones gráficas y, de la librería scikit-learn
, en este caso, utilizaremos un método de clasificación basado en los vecinos más cercanos y algunas funciones de preprocesamiento.
El método read_csv
de pandas
permite dos modos de trabajo: que el propio fichero csv tenga una fila con los nombres de las variables o que nosotros especifiquemos los nombres de las variables en la llamada. En este caso, vamos a utilizar la segunda aproximación. De esta forma, creamos un array con los nombres de las variables:
nombre_variables = ['longitud_sepalo', 'ancho_sepalo', 'longitud_petalo', 'ancho_petalo', 'clase']
y leemos el dataset con:
iris = pd.read_csv('data/iris.csv', names = nombre_variables)
iris
es un objeto de la clase DataFrame
de pandas
. También podríamos haber obviado el nombre de las columnas estableciendo header=None
, de forma que read_csv
le hubiera asignado un nombre por defecto, pero como lo conocemos, queda más claro si le ponemos el nombre apropiado a cada columna.
Antes de nada, es conveniente realizar una pequeña inspección de los datos. Si simplemente queremos ver la cabecera del dataset, podemos utilizar el método head(n)
, que devuelve un DataFrame incluyendo los primeros n
patrones:
print(iris.head(9))
Estos datos tienen cuatro dimensiones, pero podemos visualizar una o dos de las dimensiones usando un histograma o un scatter. Primero, activamos el matplotlib inline mode:
%matplotlib inline
import matplotlib.pyplot as plt
variable_x = 3
colors = ['blue', 'red', 'green']
iris_target_names = np.unique(iris['clase'])
for indice, color in zip(range(len(iris_target_names)), colors): #¿qué hace zip?
#Separamos el conjunto en las distintas clases
patrones = (iris['clase']==iris_target_names[indice]) #esta comparación la explicaremos más adelante
plt.hist(iris.values[patrones, variable_x].reshape(sum(patrones),1), label=iris_target_names[indice], color=color)
plt.xlabel(nombre_variables[variable_x])
plt.legend(loc='upper right')
plt.show()
Recuerda que las variables de entrada eran ['longitud_sepalo', 'ancho_sepalo', 'longitud_petalo', 'ancho_petalo', 'clase'], sabiendo esto, ¿qué debemos modificar en el código superior para mostrar la longitud del sépalo?
A continuación vamos a representar en un gráfico la relación entre dos variables de entrada, así podremos ver si los patrones tienen algunas características que nos ayuden a crear un modelo lineal. Prueba distintas combinaciones de variables que se representan en los ejes, para ello modifica los valores de vairable_x y variable_y.
variable_x = 0
variable_y = 2
colors = ['blue', 'red', 'green']
for indice, color in zip(range(len(iris_target_names)), colors): #¿qué hace zip?
patrones = (iris['clase']==iris_target_names[indice])
plt.scatter(iris.values[patrones, variable_x],
iris.values[patrones, variable_y],
label=iris_target_names[indice],
c=color)
plt.xlabel(nombre_variables[variable_x])
plt.ylabel(nombre_variables[variable_y])
plt.legend(loc='upper left')
plt.show()
¿Has encontrado alguna combinación que haga que los datos sean linealmente separables? Es un poco tedioso probar todas las posibles combinaciones, ¡y eso que en este ejemplo tenemos pocas variables!
En lugar de realizar los plots por separado, una herramienta común que utilizan los analistas son las matrices scatterplot.
Estas matrices muestran los scatter plots entre todas las características del dataset, así como los histogramas para ver la distribución de cada característica.
import pandas as pd
from sklearn import preprocessing
le = preprocessing.LabelEncoder()
le.fit(iris['clase'])
clases_numeros = le.transform(iris['clase'])
iris_df = pd.DataFrame(iris[nombre_variables], columns=nombre_variables)
#Para versiones de pandas<0.16 se debe llamar al método como pd.plotting.scatter_matrix
if pd.__version__<0.16:
pd.plotting.scatter_matrix(iris_df, c=clases_numeros, figsize=(8, 8));
else:
pd.tools.plotting.scatter_matrix(iris_df, c=clases_numeros, figsize=(8, 8));
DataFrame
y matrices numpy (ndarray
)¶Los DataFrame
son objetos que representan a los datasets con los que vamos a operar. Permiten realizar muchas operaciones de forma automática, ayudando a transformar las variables de forma muy cómoda. Internamente, el dataset se guarda en un array bidimensional de numpy
(clase ndarray
). El acceso a los elementos de un DataFrame
es algo más simple si utilizamos su versión ndarray
, para lo cual simplemente tenemos que utilizar el atributo values
:
print(iris['longitud_sepalo'])
print(iris[nombre_variables[0]])
iris_array = iris.values
print(iris_array[:,0])
La sintaxis de indexación en un ndarray
es la siguiente:
array[i,j]
: accede al valor de la fila i
columna j
.array[i:j,k]
: devuelve otro ndarray
con la submatriz correspondiente a las filas desde la i
hasta la j-1
y a la columna k
.array[i:j,k:l]
: devuelve otro ndarray
con la submatriz correspondiente a las filas desde la i
hasta la j-1
y a las columnas desde la k
hasta la l
.array[i:j,:]
: devuelve otro ndarray
con la submatriz correspondiente a las filas desde la i
hasta la j-1
y todas las columnas.array[:,i:j]
: devuelve otro ndarray
con la submatriz correspondiente a todas las filas y a las columnas desde la k
hasta la l
.
De esta forma:# Mostrando un Array de Numpy
iris_array[0:2,2:4]
# Mostrando un DataFrame de Pandas
iris[0:2][nombre_variables[2:4]]
# Mostrando un Array de Numpy
iris_array[1:6,:]
# Mostrando un DataFrame de Pandas
iris[1:6][nombre_variables[:]]
Vemos que el acceso a través del ndarray
es, por lo general, más cómodo, ya que no requerimos del nombre de las variables.
Ahora vamos a manejar una matriz de valores aleatorios, para ver algunas características adicionales
import numpy as np
# Semilla de números aleatorios (para reproducibilidad)
rnd = np.random.RandomState(seed=123)
# Generar una matriz aleatoria
X = rnd.uniform(low=0.0, high=1.0, size=(3, 5)) # dimensiones 3x5
print(X)
(tened en cuenta que los arrays en numpy se indexan desde el 0, al igual que la mayoría de estructuras en Python)
# Acceder a los elementos
# Obtener un único elemento
# (primera fila, primera columna)
print "Único elemento: {0}\n".format(X[0, 0])
# Obtener una fila
# (segunda fila)
print "Segunda fila: {0}\n".format(X[1]) #O también: print(X[1,:])
# Obtener una columna
# (segunda columna)
print "Segunda columna: {0}\n".format(X[:, 1])
# Obtener la última fila
print "Última fila: {0}\n".format(X[-1])
# Obtener la última columna
print "Última columna: {0}\n".format(X[:, -1])
# Obtener la traspuesta
print(X.T)
# Crear un vector fila de números con la misma separación
# sobre un intervalo prefijado
y = np.linspace(start=0, stop=12, num=5)
print(y)
# Transformar el vector fila en un vector columna
print(y[:, np.newaxis])
# Obtener la forma de un array y cambiarla
# Generar un array aleatorio
rnd = np.random.RandomState(seed=123)
X = rnd.uniform(low=0.0, high=1.0, size=(3, 5)) # a 3 x 5 array
print(X)
print(X.shape)
print(X.reshape((5, 3)))
# Indexar según un conjunto de números enteros
indices = np.array([3, 1, 0])
print(indices)
X[:, indices]
En scikit-learn
, al igual que en otros lenguajes de programación como R o Matlab, debemos intentar, siempre que sea posible, vectorizar las operaciones. Esto es utilizar operaciones matriciales en lugar de bucles que recorran los arrays. La razón es que este tipo de operaciones están muchos más optimizadas y que el proceso de referenciación de arrays puede consumir mucho tiempo.
Imaginemos que queremos imprimir el área de sépalo de todas las flores. Compara la diferencia entre hacerlo mediante un bucle for
y mediante operaciones matriciales:
# Generar un array con el área del sépalo (longitud*anchura), utilizando un for
# Crear un array vacío
areaSepaloArray = np.empty(iris_array.shape[0])
# Se recorre con un bucle for la matriz de datos sacando la longitud (indice 0) y el ancho (indice 1) del sepalo
for i in range(0,iris_array.shape[0]):
areaSepaloArray[i] = iris_array[i,0] * iris_array[i,1]
print(areaSepaloArray)
# Generar un array con el área del sépalo (longitud*anchura), utilizando operaciones matriciales
print(iris_array[:,0] * iris_array[:,1])
Es más, los ndarray
permiten aplicar operaciones lógicas, que devuelven otro ndarray
con el resultado de realizar esas operaciones lógicas: "Obtener aquellas flores cuya longitud de pétalo es mayor que 5"
# El índice de la longitud de pétalo es el 2
iris_array[:,2] > 5
A su vez, este ndarray
se puede usar para indexar el ndarray
original: "Obtener la clase de aquellas flores cuya longitud de pétalo es mayor que 5"
# El índice de la longitud de pétalo es el 2 y el índice de la clase es el 4
iris_array[iris_array[:,2] > 5, 4]
Imagina que ahora queremos imprimir la longitud de sépalo de aquellas flores cuya longitud de sépalo es mayor que 2. Compara la versión con for
y la versión "vectorizada":
# Imprimir las longitudes de sépalo mayores que 2, utilizando un for
# El índice de la longitud de sépalo es el 0
iris_array = iris.values
for i in range(0,iris_array.shape[0]):
valorSepalo = iris_array[i,0]
if valorSepalo > 2:
print(valorSepalo)
# Imprimir las longitudes de sépalo mayores que 2, utilizando operaciones matriciales
# El índice de la longitud de sépalo es el 0
print(iris_array[ iris_array[:,0] > 2, 0])
Podemos usar algunas funciones adicionales sobre objetos de tipo ndarray
. Por ejemplo, las funciones numpy.mean
y numpy.std
nos sirven para calcular la media y la desviación típica, respectivamente, de los valores contenidos en el ndarray
que se pasa como argumento.
Por último, podemos realizar operaciones matriciales con los ndarray
de forma muy simple y optimizada. La función numpy.dot
multiplica dos ndarray
, siempre que sus dimensiones sean compatibles. La función numpy.transpose
nos devuelve la traspuesta de la matriz.
a = [[1, 0], [0, 1]]
b = [[4, 1], [2, 2]]
np.dot(a, b)
x = np.arange(4).reshape((2,2))
x
np.transpose(x)
x.T
Ejercicio: Prueba a imprimir la media y la desviación típica del área del sépalo de aquellas flores que son de tipo virginica.
# Escribe a continuación el código que resuelve el ejercicio propuesto:
Aunque a veces nos proporcionan los datos ya divididos en los conjuntos de entrenamiento y test, conviene saber como podríamos realizar esta división. Debemos tener precaución al hacer divisiones en los conjuntos de datos, pues siempre tenemos que tener en cuenta el número de patrones de cada clase, para ellos vamos usar la función Counter de la librería collections:
from collections import Counter
# Mostramos cuántos patrones hay por cada clase
Counter(iris_array[:,-1])
Una vez confirmado que el conjunto está balanceado (hay el mismo número de patrones de todas las clases) podremos hacer divisiones del mismo sin problema. El siguiente código muestra una función que divide los datos de forma aleatoria, utilizando operaciones vectorizadas:
def dividir_ent_test(dataframe, porcentaje=0.6):
"""
Función que divide un dataframe aleatoriamente en entrenamiento y en test.
Recibe los siguientes argumentos:
- dataframe: DataFrame que vamos a utilizar para extraer los datos
- porcentaje: porcentaje de patrones en el conjunto de entrenamiento
Devuelve:
- train: DataFrame con los datos del conjunto de entrenamiento
- test: DataFrame con los datos del conjunto de test
"""
# Creamos un vector de índices según el tamaño del dataframe
indices = np.arange(len(dataframe))
# Mezclamos el vector variando el orden
np.random.shuffle(indices)
# Creamos un vector del mismo tamaño que el vector de indices pero booleano con todos los valores True
mascara = np.ones((len(dataframe),1), dtype=bool)
# Los primeros elementos de esta máscara se ponen a False
# (tantos como indique el porcentaje por el número de elementos del dataframe)
mascara[indices[int(porcentaje*len(dataframe)):]]=False
train = dataframe[mascara]
test = dataframe[~mascara]
return train, test
iris_train, iris_test = dividir_ent_test(iris)
Ahora, podemos quedarnos con las columnas correspondientes a las variables de entrada, los atributos, (todas las columnas salvo la última) y la correspondiente a la variable de salida, la clase, (en este caso, la última columna):
# Conjunto de entrenamiento
train_inputs_iris = iris_train.values[:, 0:-1]
train_outputs_iris = iris_train.values[:, -1]
# Conjunto de test
test_inputs_iris = iris_test.values[:, 0:-1]
test_outputs_iris = iris_test.values[:, -1]
print(train_inputs_iris.shape)
Si nos proporcionan la base de datos completa para que hagamos nosotros las particiones, todas las clases y funciones del módulo sklearn.cross_validation
de scikit-learn
nos pueden facilitar mucho la labor.
La librería scikit-learn
no acepta cadenas de texto como parámetros de las funciones, todo deben de ser números. Por tanto debemos transformar la variable de salida, la clase, en números. Para ello, nos podemos valer del objeto sklearn.preprocessing.LabelEncoder
, que nos transforma automáticamente las cadenas a números. La forma en que se utiliza es la siguiente:
#Creación del método
label_e = preprocessing.LabelEncoder()
#Entrenamiento del método
label_e.fit(train_outputs_iris)
#Obtención de salidas
train_outputs_iris_encoded = label_e.transform(train_outputs_iris)
test_outputs_iris_encoded = label_e.transform(test_outputs_iris)
Como podéis observar, primero se crea el LabelEncoder
y luego se "entrena" mediante el método fit
. Para un LabelEncoder
, "entrenar" el modelo es decidir el de cadenas de texto a números, en este caso:
Iris-setosa
-> 0Iris-versicolor
-> 1Iris-virginica
-> 2Esta estructura (método fit
más método transform
o predict
) se repite en muchos de los objetos de scikit-learn
.
Una vez entrenado, utilizando el método transform
del LabelEncoder
, podremos transformar cualquier ndarray
que queramos. Cabe destacar que nos habríamos encontrado con un error si alguna de las etiquetas del conjunto de test no hubiese estado en el conjunto de entrenamiento.
Hay muchas más tareas de preprocesamiento que se pueden hacer en scikit-learn
. Consulta el paquete sklearn.preprocessing
.
A continuación, vamos a crear un modelo de clasificación y a obtener su matriz de confusión. Vamos a utilizar el clasificador KNeighborsClassifier, que clasifica cada patrón asignándole la clase mayoritaria según los k
vecinos más cercanos al patrón a clasificar. Consulta siempre la documentación de cada objeto para ver los parámetros del algoritmo (en este caso, el parámetro decisivo es n_neighbors
). Veamos como se realizaría el entrenamiento:
knn = neighbors.KNeighborsClassifier()
knn.fit(train_inputs_iris, train_outputs_iris_encoded)
print(knn)
Ya tenemos el modelo entrenado. Este modelo es de tipo lazy, en el sentido de que no existen parámetros a ajustar durante el entrenamiento. Lo único que hacemos es acomodar las entradas en una serie de estructuras de datos que faciliten el cálculo de distancias a la hora de predecir la etiqueta de datos nuevos. Si ahora queremos predecir las etiquetas de test, podemos hacer uso del método predict
, que aplica el modelo ya entrenado a datos nuevos:
prediccion_test = knn.predict(test_inputs_iris)
print(prediccion_test)
Si queremos saber cómo de buena ha sido la clasificación, todo modelo de clasificación o regresión en scikit-learn
tiene un método score
que nos devuelve la bondad del modelo con respecto a los valores esperados, a partir de las entradas suministradas. La medida por defecto utilizada en KNeighborsClassifier es el porcentaje de patrones bien clasificados (CCR o accuracy). La función se utiliza de la siguiente forma (internamente, esta función llama a predict
):
precision = knn.score(test_inputs_iris, test_outputs_iris_encoded)
precision
np.mean(prediccion_test == test_outputs_iris_encoded)
Para imprimir la matriz de confusión de unas predicciones, podemos utilizar la función sklearn.metrics.confusion_matrix
, que nos va devolver la matriz ya formada:
from sklearn.metrics import confusion_matrix
cm = confusion_matrix(test_outputs_iris_encoded, prediccion_test)
print(cm)
Imagina que quieres configurar el número de vecinos más cercanos (n_neighbors
), de forma que la precisión en entrenamiento. Lo podríamos hacer de la siguiente forma:
for nn in range(1,15):
knn = neighbors.KNeighborsClassifier(n_neighbors=nn)
knn.fit(train_inputs_iris, train_outputs_iris_encoded)
precisionTrain = knn.score(train_inputs_iris, train_outputs_iris_encoded)
precisionTest = knn.score(test_inputs_iris, test_outputs_iris_encoded)
print "{0} vecinos:\tCCR entrenamiento = {1:.2f}% \tCCR test = {2:.2f}%".format(nn, precisionTrain*100, precisionTest*100)
#print("%d vecinos:\tCCR train = %.2f%%,\tCCR test = %.2f%%" % (nn, precisionTrain*100, precisionTest*100))
Debes utilizar la base de datos digits
para entrenar dos modelos supervisados de clasificación:
La base de datos está disponible en la UCI, bajo el nombre Optical Recognition of Handwritten Digits Data Set. Bájala y preprocésala para realizar el entrenamiento. Utiliza las particiones de entrenamiento y test incluidas en el sitio web de la UCI. Tienes que normalizar todas las variables de entrada para que queden en el intervalo [0,1]
(consulta información sobre MinMaxScaler). Intenta ajustar lo mejor posibles los parámetros de los clasificadores.
Este tutorial se ha basado en gran parte en el siguiente material:
Se recomiendan los siguientes tutoriales adicionales para aprender más sobre el manejo de la librería:
scikit-learn
. http://scikit-learn.org/stable/tutorial/basic/tutorial.html.scikit-learn
. http://scikit-learn.org/stable/tutorial/statistical_inference/index.html.Por último, para aprender la sintaxis básica de Python en menos de 13 horas, se recomienda el siguiente curso de CodeAcademy: