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:
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:
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()
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
# 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:
# 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
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
df_train.label.unique()
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.
Continuaremos ahora siguiendo la metodología que mencionamos al inicio de la clase, de tal manera:
Tenemos que la primer columna (label
) representa la variable de respuesta y las características son el resto de las columnas, de tal manera
# 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)
# 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)
# 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:
df_train.value_counts('label')
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
df_test.value_counts('label')
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
# ya sea desde pandas directamente
df_test.label.hist()
plt.show()
# 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()
# 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:
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:
# 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')
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
# 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')
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
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.
# 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
# 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%
<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
# 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%
<keras.callbacks.History at 0x25e0dfd2fa0>
obtenemos igualmente un modelo malo. Al parecer hay algo que hace que nuestro modelo vaya mal.
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:
# 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
:
# 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:
# 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:
# 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%
<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
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
(X_test.shape[0] * 100) / (X_train.shape[0] + X_test.shape[0])
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:
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:
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:
# 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()
:
# 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%
# 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%
# 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.
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
modelo_3.history['accuracy']
[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:
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%.