Redes neuronales¶


Clase2: Clasificador multiclase¶


Autor: Luis Fernando Apáez Álvarez

Contenido¶

  • Programación de la red neuronal
  • One hot encoder
  • Conjunto de validación
  • Accuracy y val_accuracy

En la clase pasa definimos una red neuronal para un problema de clasificación binaria, donde con base en la información del conjunto de datos predeciamos si un tumor detectado era benigno (B) o maligno (M). Recordemos que la metodología fue la siguiente:

  • División de los datos en conjunto de entrenamiento y prueba.
  • Definición de la arquitectura de la red neuronal.
  • Entrenamiento y evalución.
  • En caso de ser necesaria regresabamos al primer punto a al conjunto de datos.

Además, recordemos que fue preciso modificar la información de la columna de interés diagnosis, donde pasamos de las etiquetas M y B a los números 0 y 1, respectivamente.

Ahora trabajaremos con un conjunto de datos que contiene la información numérica de imágenes, esto es, cada fila del siguiente conjunto de datos representa un número de pixel para una imagen que representa una letra en el lenguaje de señas. Recordemos que dicho conjunto de datos ya fuer abordado en clases posteriores.

Ahora, lo que tendremos serán dos conjuntos de datos que han sido separados específicamente para el entrenamiento y las pruebas:

In [1]:
import pandas as pd
import matplotlib.pyplot as plt
plt.style.use('seaborn')

# Conjuntos de datos
df_train = pd.read_csv('sign_mnist_train.csv')
df_test = pd.read_csv('sign_mnist_test.csv')
df_train.head()
Out[1]:
label pixel1 pixel2 pixel3 pixel4 pixel5 pixel6 pixel7 pixel8 pixel9 ... pixel775 pixel776 pixel777 pixel778 pixel779 pixel780 pixel781 pixel782 pixel783 pixel784
0 3 107 118 127 134 139 143 146 150 153 ... 207 207 207 207 206 206 206 204 203 202
1 6 155 157 156 156 156 157 156 158 158 ... 69 149 128 87 94 163 175 103 135 149
2 2 187 188 188 187 187 186 187 188 187 ... 202 201 200 199 198 199 198 195 194 195
3 2 211 211 212 212 211 210 211 210 210 ... 235 234 233 231 230 226 225 222 229 163
4 13 164 167 170 172 176 179 180 184 185 ... 92 105 105 108 133 163 157 163 164 179

5 rows × 785 columns

In [2]:
# Formas de los dataframes
print(df_test.shape)
print(df_train.shape)
(7172, 785)
(27455, 785)

Tenemos entonces que las imágenes sonde 28 pixeles por 28 pixeles. Además, la columna label representa la etiqueta referente a una letra del abecedario.

Recordemos que habíamos definido una función para visualizar las imágenes:

In [3]:
# importaciones necesarias
import numpy as np
from PIL import Image

# Definimos una funcion
def to_image(df, fila):
    # dada una fila del dataframe df, convertimos
    # todos los valores de dicha fila en un array numpy
    array = np.array(df.iloc[fila][1:])
    # Cambiamos la forma del array y especificamos el tipo de dato
    # de entero de 8 bits (para asi poder convertir el array a imagen)
    # Donde colocaremos la dimension de los arrays en 28x28, puesto que
    # las imagenes que usaremos son de 28 pixeles por 28 pixeles
    array = np.array(array.reshape((28, 28)), dtype='uint8')
    # Convertimos el array a imagen
    img = Image.fromarray(array)
    # Cambiamos la dimension de la imagen
    img = img.resize((128,128))
    # mostramos la imagen
    display(img)

Veamos algunas imágenes

In [4]:
for i in range(10):
    print(df_train.iloc[i, 0])
    to_image(df_train, i)
3
6
2
2
13
16
8
22
3
3

Veamos cuántos valores únicos, número total de distintas etiquetas, hay en la columna label

In [5]:
df_train.label.unique()
Out[5]:
array([ 3,  6,  2, 13, 16,  8, 22, 18, 10, 20, 17, 19, 21, 23, 24,  1, 12,
       11, 15,  4,  0,  5,  7, 14], dtype=int64)

donde vemos que faltan algunas letras, lo cual es debido a que para poder representarlas en lenguaje de señas se requiere movimiento.

Progamación de la red neuronal ¶

Continuaremos ahora siguiendo la metodología que mencionamos al inicio de la clase, de tal manera:

División de los datos¶

Tenemos que la primer columna (label) representa la variable de respuesta y las características son el resto de las columnas, de tal manera

In [6]:
# Conjunto de entrenamiento
X_train = df_train.drop('label', axis=1).values
# Recordemos que es necesario cambiar la forma de la variable y
y_train = df_train['label'].values.reshape(-1,1)
In [7]:
# Conjunto de prueba
X_test = df_test.drop('label', axis=1).values
# Recordemos que es necesario cambiar la forma de la variable y
y_test = df_test['label'].values.reshape(-1,1)
In [8]:
# Veamos las formas
print(X_train.shape, y_train.shape)
print(X_test.shape, y_test.shape)
(27455, 784) (27455, 1)
(7172, 784) (7172, 1)

Notemos además el número de elementos por categorías:

In [9]:
df_train.value_counts('label')
Out[9]:
label
17    1294
16    1279
11    1241
22    1225
5     1204
18    1199
3     1196
14    1196
19    1186
23    1164
8     1162
20    1161
13    1151
2     1144
0     1126
24    1118
10    1114
6     1090
15    1088
21    1082
12    1055
7     1013
1     1010
4      957
dtype: int64

Por lo cual vemos que el número de elementos por clase está equilibrado

In [10]:
df_test.value_counts('label')
Out[10]:
label
4     498
7     436
1     432
12    394
6     348
15    347
21    346
24    332
10    331
0     331
2     310
13    291
8     288
23    267
20    266
19    248
5     247
14    246
18    246
3     245
11    209
22    206
16    164
17    144
dtype: int64

Alternativamente podemos ver histogramas

In [11]:
# ya sea desde pandas directamente
df_test.label.hist()
plt.show()
In [12]:
# o tambien utilizando matplotlib:

plt.figure(figsize=(14,8))
plt.subplot(2,2,1)
plt.hist(df_test.label)
plt.title('Número de elementos por categoría: test')

plt.subplot(2,2,2)
plt.hist(df_train.label)
plt.title('Número de elementos por categoría: train')
plt.show()

Definiendo la arquitectura de la red neuronal¶

In [13]:
# importaciones necesarias:
from keras.models import Sequential
from keras.layers import Dense

Dado que tenemos 28x28=784 caraterísticas, entonces consideraremos en la primer capa oculta una 1500 neuronas, en la siguiente capa unas 1000. Para realizar la definición de la red neuronal, así como el entrenamiento y la evaluación del modelo, definiremos la siguiente función:

In [14]:
def model_result_v1(num_epocas, num_lote, xtr, ytr, xts, yts, n1, n2):
    """num_epocas: número de épocas
       num_lote: tamañio de los lotes
       xtr: X_train
       ytr: y_train
       xts: X_test
       yts: y_test
       n1: número de neuronas en la capa 1
       n2: número de neuronas en la capa 2"""
    # Instanciamos el modelo
    model = Sequential()
    # Capa de entrada de 784 neuronas y capa oculta de n1 neuronas.
    # Para la capa oculta utilizamos la funcion de activacion relu
    model.add(Dense(n1, activation='relu', input_shape=(784,)))
    # Capa oculta 2:
    # n2 neuronas y funcion de activacion relu
    model.add(Dense(n2, activation='relu'))
    # Capa de salida. Dado que es un problema de clasificacion multiclase
    # utilizamos la funcion de activacion softmax
    model.add(Dense(1, activation='softmax'))
    # Compilacion. Utilizaremos ahora categorical en vez de binary
    model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
    # Entrenamiento
    modelo = model.fit(xtr, ytr, epochs=num_epocas, 
                       batch_size=num_lote, verbose=False)
    # Evaluacion sobre el conjunto de pruebas
    scores = model.evaluate(xts, yts)
    print('%s: %.4f%%' % (model.metrics_names[1], scores[1] * 100))
    # Retornamos el modelo obtenido
    return modelo

donde dado que queremos predecir un valor referente a la etiqueta, entonces definimos que en la capa de salida sólo haya una neurona. Ahora bien, para buscar el mejor modelo tomaremos una muestra más pequeña de los conjuntos de entrenamiento y prueba, lo cual se hace porque como los conjuntos de datos son grandes, entonces el entrenamiento del modelo es mucho más tardado. Así, tomamos una muestra más pequeña de los datos:

In [15]:
# definimos una lista con todos los numeros de las categorias
categorias = df_train['label'].unique().tolist()

# definimos una lista de dataframes donde cada dataframe 
# representa una muestra de 900 elementos para cada categoria
l_train = [df_train[df_train.label == cat].sample(385) for cat in categorias]

# apilamos los dataframes de l_train en un solo dataframe
df_train_muestra = pd.concat(l_train)
# veamos el numero de elementos por clase
df_train_muestra.value_counts('label')
Out[15]:
label
0     385
1     385
23    385
22    385
21    385
20    385
19    385
18    385
17    385
16    385
15    385
14    385
13    385
12    385
11    385
10    385
8     385
7     385
6     385
5     385
4     385
3     385
2     385
24    385
dtype: int64
In [16]:
# Hacemos lo mismo para el conjunto de pruebas
# definimos una lista de dataframes donde cada dataframe 
# representa una muestra de 140 elementos para cada categoria
l_test = [df_test[df_test.label == cat].sample(100) for cat in categorias]

# apilamos los dataframes de l_test en un solo dataframe
df_test_muestra = pd.concat(l_test)
# veamos el numero de elementos por clase
df_test_muestra.value_counts('label')
Out[16]:
label
0     100
1     100
23    100
22    100
21    100
20    100
19    100
18    100
17    100
16    100
15    100
14    100
13    100
12    100
11    100
10    100
8     100
7     100
6     100
5     100
4     100
3     100
2     100
24    100
dtype: int64
In [17]:
print(df_train_muestra.shape, df_test_muestra.shape)
(9240, 785) (2400, 785)

Donde se tomaron 100 elementos por categoría para el conjunto de pruebas pues el valor mínimo de elementos por categoría es de 144, de modo que se decidió tomar 100 por categoría. Luego, dado que en total hay 2400 elementos en el conjunto de pruebas, entonces sería conveniente tener alrededor de 9240 elementos en el conjunto de entrenamiento para así mantener la proporción original de la división de los datos.

In [18]:
# Definimos las variables para el entrenamiento y la prueba

X_train_2 = df_train_muestra.drop('label', axis=1).values
y_train_2 = df_train_muestra['label'].values.reshape(-1,1)
X_test_2 = df_test_muestra.drop('label', axis=1).values
y_test_2 = df_test_muestra['label'].values.reshape(-1,1)

Definimos entonces un primer modelo

In [19]:
# 10 epocas
# batch_size de 32
# 1500 neuronas en la primer capa
# 1000 neuronas en la segunda capa
model_result_v1(10, 32, X_train_2, y_train_2, 
                X_test_2, y_test_2, 
                1500, 1000)
75/75 [==============================] - 1s 6ms/step - loss: 0.0000e+00 - accuracy: 0.0417
accuracy: 4.1667%
Out[19]:
<keras.callbacks.History at 0x25e0ca1ccd0>

Vemos que, a pesar de que tomamos una muestra de los datos, el resultado obtenido es muy malo. Si probramos otros parámetros

In [20]:
# 10 epocas
# batch_size de 60
# 900 neuronas en la primer capa
# 600 neuronas en la segunda capa
model_result_v1(10, 60, X_train_2, y_train_2, 
                X_test_2, y_test_2, 
                900, 600)
75/75 [==============================] - 0s 4ms/step - loss: 0.0000e+00 - accuracy: 0.0417
accuracy: 4.1667%
Out[20]:
<keras.callbacks.History at 0x25e0dfd2fa0>

obtenemos igualmente un modelo malo. Al parecer hay algo que hace que nuestro modelo vaya mal.

One hot encoder ¶

Recordemos que en la clase anterior hicimos una modificación de la columna diagnosis, donde pasamos de las etiquetas M y B a los números 0 y 1, lo cual fue necesario para la posterior implementación del entrenamiento de la red. El problema que estamos teniendo con la precisión de los dos modelos anteriores tiene que ver justamente con la columna label, la cual contiene números del 1 al 24 (con algunas ausencias como el 9). Lo que debemos hacer es algo similar a lo hecho en la columna diagnosis, pero en vez de intentar alguna implementación manual utilizaremos la librería sklearn, en particular utilizaremos OneHotEncoder.

Consideremos de nuevo la columna diagnosis, por ejemplo

diagnosis

M B B M

Lo que hace la implementación de one hot encoder es codificar características categóricas como una matriz numérica. Considerando el ejemplo anterior, sabemos que sólo hay dos categorías: M y B. Luego, con el one hot encoder consideraremos una matriz (la cual por fines ilustrativos la agregaremos junto a la columna diagnosis)

diagnosis M B
M 1 0
B 0 1
B 0 1
M 1 0

Donde, como la primer fila tiene la etiqueta M, entonces se ha colocado un uno en la columna M y un cero en la columna B; la segunda fila tiene la etiqueta B, por lo que se ha colocado el cero en la columna M y un uno en la columna B; y así sucesivamente. Si suponemos que tenemos una etiqueta adicional I (indeterminado) en la columna diagnosis, entonces:

diagnosis M B I
M 1 0 0
B 0 1 0
I 0 0 1
M 1 0 0

Básicamente es ese el funcionamiento del one hot encoder y nos sirve para transformar las variables y's (ya sea y_test o y_train) en problemas de clasificación multiclase. Para ello requerimos escribir:

In [21]:
# importacion necesaria
from sklearn.preprocessing import OneHotEncoder

# Instanciamos y configuramos one hot encoder
enc = OneHotEncoder(handle_unknown='ignore')

Después realizaremos las transformaciones para las y's:

In [22]:
# ahora si realizamos las transformaciones mediante fit_transform()
y_train_2 = enc.fit_transform(y_train_2).toarray()
# es necesario convertir el resultado de la transformacion de
# nuevo en un array
y_test_2 = enc.fit_transform(y_test_2).toarray()
print(y_train_2.shape, y_test_2.shape)
(9240, 24) (2400, 24)

como tenemos 24 categorías, entonces los arrays resultantes tienen 24 "columnas".

Probemos ahora el rendimiento del modelo que definimos pero con las y's transformadas, pero antes es necesario cambiar el número de neuronas en la capa de salida, pues, como tenemos 24 clases o 24 posibles resultados de predicción, requerimos entonces 24 neuronas en esa capa:

In [23]:
# cambiamos el numero de neuronas en la capa de salida
def model_result_v2(num_epocas, num_lote, xtr, ytr, xts, yts, n1, n2):
    """num_epocas: número de épocas
       num_lote: tamañio de los lotes
       xtr: X_train
       ytr: y_train
       xts: X_test
       yts: y_test
       n1: número de neuronas en la capa 1
       n2: número de neuronas en la capa 2"""
    # Instanciamos el modelo
    model = Sequential()
    # Capa de entrada de 784 neuronas y capa oculta de n1 neuronas.
    # Para la capa oculta utilizamos la funcion de activacion relu
    model.add(Dense(n1, activation='relu', input_shape=(784,)))
    # Capa oculta 2:
    # n2 neuronas y funcion de activacion relu
    model.add(Dense(n2, activation='relu'))
    # Capa de salida. Dado que es un problema de clasificacion multiclase
    # utilizamos la funcion de activacion softmax
    model.add(Dense(24, activation='softmax'))
    # Compilacion. Utilizaremos ahora categorical en vez de binary
    model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
    # Entrenamiento
    modelo = model.fit(xtr, ytr, epochs=num_epocas, batch_size=num_lote, verbose=False)
    # Evaluacion sobre el conjunto de pruebas
    scores = model.evaluate(xts, yts)
    print('%s: %.4f%%' % (model.metrics_names[1], scores[1] * 100))
    # Retornamos el modelo obtenido
    return modelo

Probamos ahora el modelo:

In [24]:
# 13 epocas
# batch_size de 60
# 900 neuronas en la primer capa
# 600 neuronas en la segunda capa
model_result_v2(13, 60, X_train_2, y_train_2, 
                X_test_2, y_test_2, 
                900, 600)
75/75 [==============================] - 0s 4ms/step - loss: 1.9140 - accuracy: 0.5979
accuracy: 59.7917%
Out[24]:
<keras.callbacks.History at 0x25e0dfbedf0>

Vemos que el rendimiento ha mejorado bastante después de haber realizado la transformación de las y's y de haber colocado 24 neuronas en la capa de salida. Recordemos que el entrenamiento del modelo ha sido con la muestra de los datos y no con los datos completos

Implementación final del modelo¶

Recordemos que la definición de las variable X_train, y_train, X_test, y_test se hizo de manera directa a partir de los dataframes iniciales. Así, dado que ya hemos elegido un modelo, en principio, de la red neuronal, ahora sí podremos utilizar los datos completos.

Antes, notemos que

In [38]:
(X_test.shape[0] * 100) / (X_train.shape[0] + X_test.shape[0])
Out[38]:
20.712161030409796

es el porcentaje que representa el conjunto de prueba respecto al total de datos.

Podemos observar que hay bastantes datos para el entrenamiento comparados con el número de datos para la prueba, lo cual nos dice que tenemos un poco menos del 80% de los datos para entrenamiento y 20% para datos de prueba. A pesar que los porcentajes usuales son 75-25%, no deberíamos tener grandes problemas en tener el porcentaje que tenemos, no obstante, aprovechando que tenemos muchos datos para el entrenamiento abordaremos el siguiente subtema:

Conjunto de validación

Este conjunto de datos es utilizado dentro del entrenamiento de nuestra red neuronal y sólo existirá temporalmente durante ese entrenamiento. Este conjunto de datos sirve para verificar si las "decisiones" que toma la red durante el entrenamiento ajustan mejor o peor el modelo, esto es, sirve para afinar los parámetros de la red. La validación dentro del entrenamiento nos arrojará una métrica denominada val_accuracy referente a la precisión en la validación, lo cual nos indica el porcentaje de elementos bien predichos respecto al conjunto de validación (este conjunto se extrae del conjunto de entrenamiento).

Para la implementación escribiremos validation_split=n dentro de model.fit() y donde n es el porcentaje de datos que queremos destinar para la validación.

Con base en lo anterior podemos definir:

In [30]:
def model_result_v3(num_epocas, num_lote, xtr, ytr, xts, yts, n1, n2):
    """num_epocas: número de épocas
       num_lote: tamañio de los lotes
       xtr: X_train
       ytr: y_train
       xts: X_test
       yts: y_test
       n1: número de neuronas en la capa 1
       n2: número de neuronas en la capa 2"""
    # Instanciamos el modelo
    model = Sequential()
    # Capa de entrada de 784 neuronas y capa oculta de n1 neuronas.
    # Para la capa oculta utilizamos la funcion de activacion relu
    model.add(Dense(n1, activation='relu', input_shape=(784,)))
    # Capa oculta 2:
    # n2 neuronas y funcion de activacion relu
    model.add(Dense(n2, activation='relu'))
    # Capa de salida. Dado que es un problema de clasificacion multiclase
    # utilizamos la funcion de activacion softmax
    model.add(Dense(24, activation='softmax'))
    # Compilacion. Utilizaremos ahora categorical en vez de binary
    model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
    
    # Entrenamiento:
    # configuramos que se destine un 20% del conjunto de entrenamiento para 
    # el conjunto de validacion:
    
    modelo = model.fit(xtr, ytr, epochs=num_epocas, batch_size=num_lote, verbose=False,
                       validation_split=0.2)
    # Evaluacion sobre el conjunto de pruebas
    scores = model.evaluate(xts, yts)
    print('%s: %.4f%%' % (model.metrics_names[1], scores[1] * 100))
    # Retornamos el modelo obtenido
    return modelo

Así, podemos implementar la función anterior para los primeros conjunto de entrenamiento y prueba que definimos:

In [39]:
# 13 epocas
# batch_size de 60
# 900 neuronas en la primer capa
# 600 neuronas en la segunda capa
modelo_1 = model_result_v3(13, 60, X_train, enc.fit_transform(y_train).toarray(), 
                X_test, enc.fit_transform(y_test).toarray(), 
                900, 600)
225/225 [==============================] - 1s 4ms/step - loss: 3.3764 - accuracy: 0.5714
accuracy: 57.1389%

Observemos que hemos colocado enc.fit_transform(y_train).toarray() pues es necesario realizar la transformación del one hot encoder. Además, el resultado obtenido fue malo, por lo cual modificamos los parámetros de model_result_v3():

In [40]:
# 15 epocas
# batch_size de 60
# 900 neuronas en la primer capa
# 600 neuronas en la segunda capa
modelo_2 = model_result_v3(15, 60, X_train, enc.fit_transform(y_train).toarray(), 
                X_test, enc.fit_transform(y_test).toarray(), 
                900, 600)
225/225 [==============================] - 1s 4ms/step - loss: 1.6309 - accuracy: 0.7447
accuracy: 74.4702%
In [34]:
# 15 epocas
# batch_size de 60
# 900 neuronas en la primer capa
# 700 neuronas en la segunda capa
modelo_3  = model_result_v3(15, 60, X_train, enc.fit_transform(y_train).toarray(), 
                X_test, enc.fit_transform(y_test).toarray(), 
                900, 700)
225/225 [==============================] - 1s 5ms/step - loss: 1.4163 - accuracy: 0.7916
accuracy: 79.1550%
In [45]:
# 20 epocas
# batch_size de 60
# 900 neuronas en la primer capa
# 700 neuronas en la segunda capa
modelo_4  = model_result_v3(20, 60, X_train, enc.fit_transform(y_train).toarray(), 
                X_test, enc.fit_transform(y_test).toarray(), 
                900, 700)
225/225 [==============================] - 1s 5ms/step - loss: 1.8796 - accuracy: 0.7850
accuracy: 78.4997%

Nos quedaremos con el modelo_3 pues ha obtenido un buen desempeño en menor número de épocas, en comparativa con el modelo 4.

Accuracy y val_accuracy ¶

De acuerdo al modelo que tengamos, podemos visualizar un gráfico en el cual podemos observar el comportamiento de los valores de accuracy y de val_accuracy conforme pasan el número de épocas. Para ello deberemos de escribir modelo_3.history['accuracy'] lo cual nos da los valores obtenidos en el entrenamiento referentes a la precisión

In [49]:
modelo_3.history['accuracy']
Out[49]:
[0.3533053994178772,
 0.6831633448600769,
 0.7719905376434326,
 0.8141049146652222,
 0.8229830861091614,
 0.9224640130996704,
 0.8704243302345276,
 0.8938717842102051,
 0.8651429414749146,
 0.9523766040802002,
 0.8844017386436462,
 0.9239664673805237,
 0.9162721037864685,
 0.9147241115570068,
 0.9984064698219299]

y también podemos escribir modelo_3.history['val_accuracy']. Así, para realizar el gráfico escribimos entonces:

In [48]:
plt.plot(modelo_3.history['accuracy'])
plt.plot(modelo_3.history['val_accuracy'])
plt.title('Precisión del modelo')
plt.xlabel('Epoch')
plt.ylabel('Precisión')
plt.legend(['Train', 'Validation'])
plt.show()

Podemos notar que tanto accuracy como val_accuracy llegaron a tomar un valor muy muy cercano a uno, y a pesar de ello el accuracy sobre el conjunto de pruebas fue del 79%.