#@title Importar librerías
#importar librerías necesarias
import random
import numpy as np
import pandas as pd
from tqdm.notebook import tqdm
from matplotlib import pyplot as plt
from sklearn.datasets import make_moons
from sklearn.metrics import ConfusionMatrixDisplay
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, confusion_matrix
Implementando un Perceptrón Multi-capa (MLP) desde CERO usando python
#@title Funciones complementarias
def plot_dataset(X_train, y_train, X_test, y_test):
# Tamaño de paso en la grilla de valores
# (para la visualización del espacio de características)
= 0.02
h
# Definir los límites del gráfico en el eje x e y basados
# en los datos de entrenamiento
= X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
x_min, x_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5
y_min, y_max
# Crear una malla de puntos para cubrir el espacio de características
= np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
xx, yy
# Creación del lienzo para visualizar los datos
= plt.subplots(1,1, figsize=(8, 5))
fig, ax
# Agregar titulo a la grafica
"Dataset linealmente no separable")
ax.set_title(
# Agregar nombres a cada eje de caracteristica
"Característica x_1")
ax.set_xlabel("Característica x_2")
ax.set_ylabel(
# Puntos de entrenamiento
== 0, 0], X_train[y_train == 0, 1],
ax.scatter(X_train[y_train ="#FF0000", edgecolors="k", label='Clase de entrenamiento 1')
c== 1, 0], X_train[y_train == 1, 1],
ax.scatter(X_train[y_train ="#0000FF", edgecolors="k", label='Clase de entrenamiento 2')
c
# Puntos de prueba
== 0, 0], X_test[y_test == 0, 1],
ax.scatter(X_test[y_test ="#FF0000", edgecolors="k", alpha=0.6, label='Clase de prueba 1')
c== 1, 0], X_test[y_test == 1, 1],
ax.scatter(X_test[y_test ="#0000FF", edgecolors="k", alpha=0.6, label='Clase de prueba 2')
c
# Establecer los límites del gráfico para asegurar que todos los puntos sean visibles
min(), xx.max())
ax.set_xlim(xx.min(), yy.max())
ax.set_ylim(yy.
# Eliminar las marcas en los ejes x e y para un gráfico más limpio
ax.set_xticks(())
ax.set_yticks(())
# Añadir una leyenda para identificar las clases de los
# puntos de entrenamiento y prueba
ax.legend()
# mostrar el grafico
plt.show()
def plot_decision_boundary(mlp, X, y, h=0.02):
# Crear una malla de puntos para el espacio de características
= X[:, 0].min() - 1, X[:, 0].max() + 1
x_min, x_max = X[:, 1].min() - 1, X[:, 1].max() + 1
y_min, y_max = np.meshgrid(np.arange(x_min, x_max, h),
xx, yy
np.arange(y_min, y_max, h))
# por cada punto de la grilla, hacer una predicción del MLP
= np.array([mlp.prediccion([np.array([xx.ravel()[i], yy.ravel()[i]])])
Z for i in range(len(xx.ravel()))])
# redimensionar para que tenga el mismo shape de la grilla
= Z.reshape(xx.shape)
Z
# crear una figura de dos subplots
= plt.subplots(1, 2, figsize=(12, 5))
fig, ax
# Graficar los puntos originales
0].set_title('Puntos originales')
ax[0].scatter(X[:, 0], X[:, 1], c=y, cmap=plt.cm.RdBu,
ax[='k', alpha=0.6)
edgecolors
# Graficar los puntos de entrenamiento
1].set_title('Frontera de decisión generada por el MLP')
ax[1].scatter(X[:, 0], X[:, 1], c=y, edgecolors='k', cmap=plt.cm.RdBu)
ax[# Graficar la frontera de decisión con un contorno
1].contourf(xx, yy, Z, alpha=0.6, cmap=plt.cm.RdBu)
ax[
# mejorar la visualización
for i in range(2):
min(), xx.max())
ax[i].set_xlim(xx.min(), yy.max())
ax[i].set_ylim(yy.
ax[i].set_xticks(())
ax[i].set_yticks(())"Característica x_1")
ax[i].set_xlabel("Característica x_2")
ax[i].set_ylabel(
plt.show()
def plot_cost_history(costo_historia):
# Grafica el cambio del costo en el entrenamiento
=(8, 4))
plt.figure(figsize='Costo')
plt.plot(costo_historia, label'Historia del Costo')
plt.title('Épocas')
plt.xlabel('Costo')
plt.ylabel(
plt.legend()True)
plt.grid( plt.show()
Creación dataset sintético
# creación de dataset sintético linealmente no separable
# usando sklearn
= make_moons(n_samples=150, noise=0.5, random_state=0) X, y
# dividimos la información para entrenamiento y prueba
= train_test_split(
X_train, X_test, y_train, y_test =0.3, random_state=42
X, y, test_size )
# Visualización del conjunto de prueba creado
plot_dataset(X_train, y_train, X_test, y_test)
Creación del MLP
Nuestra implementación de un MLP en python tendrá la siguiente estructura:
class PerceptronMulticapa():
def __init__(self, params=None):
pass
def inicializacion(self):
return None
def forward_pass(self):
return None
def backward_pass(self):
return None
def calcular_perdida_entropia_cruzada(self):
return None
def actualizar_pesos(self):
return None
def prediccion(self):
return None
def entrenar(self):
return None
def evaluar(self):
return None
= {}
params 'capa_entrada'] = 2
params['capa_oculta'] = 50
params['capa_salida'] = 1
params['epochs'] = 100
params['lr'] = 0.1 params[
1. Inicialización
Asignación de atributos e inicialización de pesos y biases
class PerceptronMulticapa():
def __init__(self, params=None):
# Asignación de hiperparámetros
self.capa_entrada = params['capa_entrada']
self.capa_oculta = params['capa_oculta']
self.capa_salida = params['capa_salida']
self.epochs = params['epochs']
self.lr = params['lr']
self.relu = (lambda x: x*(x > 0))
self.derivada_relu = (lambda x: 1 * (x>0))
self.sigmoide = (lambda x: 1/(1 + np.exp(-x)))
self.derivada_sigmoide = (lambda x: x*(1-x))
# inicialización de pesos y bias
self.inicializacion()
def inicializacion(self):
# inicialización de pesos y bias aleatoria
42) # fijar una semilla para reproducir resultados
np.random.seed(
# Capa Oculta
self.pesos_capa_oculta = np.random.rand(self.capa_oculta, self.capa_entrada)
self.bias_capa_oculta = np.ones((self.capa_oculta, 1))
# Capa de salida
self.pesos_capa_salida = np.random.rand(self.capa_salida, self.capa_oculta)
self.bias_capa_salida = np.ones((self.capa_salida, 1))
# Instanciamos nuestro perceptrón multicapa
= PerceptronMulticapa(params) mlp
print('Dimensión pesos capa oculta: {}'.format(mlp.pesos_capa_oculta.shape))
print('Dimensión biases capa oculta: {}'.format(mlp.bias_capa_oculta.shape))
print('Dimensión pesos capa salida: {}'.format(mlp.pesos_capa_salida.shape))
print('Dimensión bias capa salida: {}'.format(mlp.bias_capa_salida.shape))
Dimensión pesos capa oculta: (50, 2)
Dimensión biases capa oculta: (50, 1)
Dimensión pesos capa salida: (1, 50)
Dimensión bias capa salida: (1, 1)
# ejemplo pesos capa salida
mlp.pesos_capa_salida
array([[0.03142919, 0.63641041, 0.31435598, 0.50857069, 0.90756647,
0.24929223, 0.41038292, 0.75555114, 0.22879817, 0.07697991,
0.28975145, 0.16122129, 0.92969765, 0.80812038, 0.63340376,
0.87146059, 0.80367208, 0.18657006, 0.892559 , 0.53934224,
0.80744016, 0.8960913 , 0.31800347, 0.11005192, 0.22793516,
0.42710779, 0.81801477, 0.86073058, 0.00695213, 0.5107473 ,
0.417411 , 0.22210781, 0.11986537, 0.33761517, 0.9429097 ,
0.32320293, 0.51879062, 0.70301896, 0.3636296 , 0.97178208,
0.96244729, 0.2517823 , 0.49724851, 0.30087831, 0.28484049,
0.03688695, 0.60956433, 0.50267902, 0.05147875, 0.27864646]])
2. Propagación hacia adelante (forward)
Método de propagación hacia adelante
def forward_pass(self, x):
# Realizar la operacion Wx + b de la capa oculta, x = x_0
= np.matmul(self.pesos_capa_oculta, x) + self.bias_capa_oculta
z # Aplicar función de activación
= self.relu(z) # z = x_1, h = x_2
h
# Aplicar la operación Wh + b para generar la salida, y = x_3
= np.matmul(self.pesos_capa_salida, h) + self.bias_capa_salida
y # Aplicar función de activación softmax para la clasificación
= self.sigmoide(y) # y = x_4
y_pred
return z, h, y_pred
# Añadimos nuestro nuevo método
setattr(PerceptronMulticapa, 'forward_pass', forward_pass)
# seleccionamos una muestra del dataset
# por ser solo uno se redimensiona para que tenga la estructura de entrada propia
= X_train[0,:].reshape((-1, 1))
x_i = mlp.forward_pass(x_i) z, h, y_pred
print('Dimensión biases capa oculta: {}'.format(z.shape))
print('Dimensión de la capa oculta: {}'.format(h.shape))
print('Predicción: {}'.format(y_pred))
print('Capa oculta: {}'.format(h))
Dimensión biases capa oculta: (50, 1)
Dimensión de la capa oculta: (50, 1)
Predicción: [[1.]]
Capa oculta: [[0.72633077]
[1.01761811]
[0.99129875]
[0.64226347]
[0.91951467]
[0.58240149]
[1.22554416]
[0.98915428]
[0.88627666]
[1.03760999]
[1.17304912]
[0.95112959]
[0.83016191]
[0.85085938]
[1.20642355]
[1.1577875 ]
[0.60864823]
[1.01505623]
[1.07377182]
[1.06886654]
[0.82949748]
[0.61426555]
[0.80843687]
[0.89119274]
[1.12821114]
[1.03116189]
[0.96713639]
[0.82449353]
[0.94790496]
[0.87459926]
[1.02976633]
[1.1607742 ]
[0.86948377]
[0.70204454]
[0.59561443]
[1.20847428]
[0.64438828]
[0.95081357]
[1.26279179]
[1.08640592]
[1.05700326]
[1.09879949]
[0.97640557]
[0.99963981]
[1.13251031]
[0.73290002]
[1.0450389 ]
[1.0785398 ]
[1.0125701 ]
[0.96240177]]
3. Función para calcular el error
Método que permite conocer el error de una predicción con respecto a la etiqueta real.
def calcular_perdida_entropia_cruzada(self, y_real, y_pred):
= 1e-12
epsilon # asegura que los valores de las predicciones esten en un rango
# seguro para evitar logaritmos de 0 y 1
= np.clip(y_pred, epsilon, 1 - epsilon)
y_pred # calculo de la perdida
= -(((1 - y_real) * np.log(1 - y_pred + epsilon)) + (y_real * np.log(y_pred + epsilon)))
perdida
return perdida
# Añadimos nuestro nuevo método
setattr(PerceptronMulticapa, 'calcular_perdida_entropia_cruzada', calcular_perdida_entropia_cruzada)
# Probamos nuestra función de error
= y_train[0]
y_real = mlp.calcular_perdida_entropia_cruzada(y_real, y_pred)
error print(f'Error: {error}')
Error: [[6.67165212e-11]]
4. Propagación hacía atrás (backward)
Método para propagar los errores hacía atrás.
def backward_pass(self, x, z, y_real, h, y_pred):
# Propagación de error en la capa de salida
# Calculo de error en la capa de salida g_out
#error_salida = (y_pred - y_real) * self.derivada_sigmoide(y_pred)
= y_pred - y_real
error_salida
# gradiente de los pesos respecto a la capa de salida
# X_in * g_out = error_salida * h.T
# X_in = h es la entrada a la capa de salida
self.gradiente_pesos_capa_salida = np.matmul(error_salida, h.T)
# gradiente de los bias respecto a la capa de salida
self.gradiente_bias_capa_salida = error_salida
# Propagación de error en la capa oculta
# gradiente respecto a la capa oculta
# (g_out * W) * relu'(X_in)
# X_in en esta capa es la salida de aplicar la primera transformación
= np.matmul(self.pesos_capa_salida.T, error_salida) * self.derivada_relu(z)
error_oculta # gradientes con respecto a la capa oculta, de nuevo g_out * X_in
self.gradiente_pesos_capa_oculta = np.matmul(error_oculta, x.T)
self.gradiente_bias_capa_oculta = error_oculta
# Añadimos nuestro nuevo método
setattr(PerceptronMulticapa, 'backward_pass', backward_pass)
# calcular propagación de errores
mlp.backward_pass(x_i, z, y_real, h, y_pred)
print('Dimensión gradientes capa oculta: {}'.format(mlp.gradiente_pesos_capa_oculta.shape))
print('Gradientes capa oculta: {}'.format(mlp.gradiente_pesos_capa_oculta))
Dimensión gradientes capa oculta: (50, 2)
Gradientes capa oculta: [[-8.14789923e-13 9.33629314e-13]
[-1.64987027e-11 1.89050846e-11]
[-8.14956162e-12 9.33819800e-12]
[-1.31845056e-11 1.51075026e-11]
[-2.35283225e-11 2.69599942e-11]
[-6.46280811e-12 7.40542677e-12]
[-1.06390243e-11 1.21907558e-11]
[-1.95873816e-11 2.24442561e-11]
[-5.93150715e-12 6.79663407e-12]
[-1.99567547e-12 2.28675032e-12]
[-7.51169840e-12 8.60730064e-12]
[-4.17960177e-12 4.78920839e-12]
[-2.41020651e-11 2.76174187e-11]
[-2.09502196e-11 2.40058678e-11]
[-1.64207563e-11 1.88157695e-11]
[-2.25922909e-11 2.58874398e-11]
[-2.08348990e-11 2.38737273e-11]
[-4.83675923e-12 5.54221410e-12]
[-2.31392593e-11 2.65141851e-11]
[-1.39822466e-11 1.60215964e-11]
[-2.09325850e-11 2.39856612e-11]
[-2.32308329e-11 2.66191149e-11]
[-8.24412154e-12 9.44654975e-12]
[-2.85305512e-12 3.26918119e-12]
[-5.90913412e-12 6.77099787e-12]
[-1.10726102e-11 1.26875814e-11]
[-2.12067279e-11 2.42997885e-11]
[-2.23141195e-11 2.55686963e-11]
[-1.80231392e-13 2.06518646e-13]
[-1.32409334e-11 1.51721606e-11]
[-1.08212247e-11 1.23995306e-11]
[-5.75806219e-12 6.59789168e-12]
[-3.10746496e-12 3.56069743e-12]
[-8.75254746e-12 1.00291310e-11]
[-2.44445826e-11 2.80098934e-11]
[-8.37891553e-12 9.60100382e-12]
[-1.34494535e-11 1.54110939e-11]
[-1.82255044e-11 2.08837453e-11]
[-9.42696189e-12 1.08019107e-11]
[-2.51930882e-11 2.88675707e-11]
[-2.49510873e-11 2.85902733e-11]
[-6.52736216e-12 7.47939620e-12]
[-1.28909822e-11 1.47711680e-11]
[-7.80015802e-12 8.93783290e-12]
[-7.38438363e-12 8.46141666e-12]
[-9.56280359e-13 1.09575653e-12]
[-1.58027281e-11 1.81076003e-11]
[-1.30317663e-11 1.49324859e-11]
[-1.33456744e-12 1.52921783e-12]
[-7.22380571e-12 8.27741800e-12]]
5. Entrenamiento
Método para iterar sobre todo el conjunto de datos y entrenar la red. Antes se deberá generar otro método que haga la respectiva actualización de pesos, una vez ya los gradientes son calculados.
def actualizar_pesos(self):
# actualizar pesos aplicando gradiente descendiente
self.pesos_capa_salida -= self.lr * self.gradiente_pesos_capa_salida
self.bias_capa_salida -= self.lr * self.gradiente_bias_capa_salida
self.pesos_capa_oculta -= self.lr * self.gradiente_pesos_capa_oculta
self.bias_capa_oculta -= self.lr * self.gradiente_bias_capa_oculta
def entrenar(self, X, y):
# almacenar el costo de cada iteración
self.costo_historia = []
# iterar sobre el número de épocas
for iteracion in tqdm(range(self.epochs), desc='Iteraciones'):
# iterar sobre los datos de entrenamiento
= 0 # error para la iteración i
error_total # iterar sobre todo el conjunto de datos
for i, (x_i, y_i) in tqdm(enumerate(zip(X,y)), desc='Datos', leave=False):
# asegurar de que la entrada y la salida solo tenga una columna
= x_i.reshape(-1, 1)
x_i = y_i.reshape(-1, 1)
y_i # aplicar propagación hacia adelante
= self.forward_pass(x_i)
z, h, y_pred # calcular la perdida de entropia cruzada
= self.calcular_perdida_entropia_cruzada(y_i, y_pred)
perdida += perdida
error_total # aplicar propagación hacia atras
self.backward_pass(x_i, z, y_i, h, y_pred)
# actualizar pesos y bias usando gradiente descendiente
self.actualizar_pesos()
# almacenar los costos de cada iteración
self.costo = error_total / len(X)
self.costo_historia.append(self.costo)
#print(f'costo: {self.costo[0][0]} en la iteración: {iteracion}')
# Añadimos nuestros nuevos métodos
setattr(PerceptronMulticapa, 'actualizar_pesos', actualizar_pesos)
setattr(PerceptronMulticapa, 'entrenar', entrenar)
# realizamos el respectivo entrenamiento
mlp.entrenar(X_train, y_train)
print(f'Costo final: {mlp.costo}')
Costo final: [[0.44965122]]
# Visualizar el cambio del costo durante el entrenamiento
plot_cost_history(np.array(mlp.costo_historia).ravel())
Evaluación
Para evaluar nuestro modelo con la información de test, creamos primero la función de predicción y seguidamente evaluamos el rendimiento en test.
def prediccion(self, X):
= []
predicciones for x_i in X:
= x_i.reshape(-1, 1) # Asegurar que x_i sea una columna
x_i = self.forward_pass(x_i)
z, _, y_pred
predicciones.append(y_pred)return np.array(predicciones).flatten()
def evaluar(self, X, y, umbral):
# generar predicciones
= self.prediccion(X)
y_pred # convertir a 0 y 1 bajo un umbral
= np.where(y_pred >= umbral, 1, 0)
y_pred # generar reporte de clasificación y matriz de confusión
print(classification_report(y, y_pred))
= confusion_matrix(y, y_pred)
cm
# visualizacion de la matriz de confusion
ConfusionMatrixDisplay(cm).plot()
plt.show()
return y_pred, cm
# Añadimos nuestros nuevos métodos
setattr(PerceptronMulticapa, 'prediccion', prediccion)
setattr(PerceptronMulticapa, 'evaluar', evaluar)
# evaluar y visualizar la matriz de confusion usando sklearn
= mlp.evaluar(X_test, y_test, umbral=0.5) y_pred, cm
precision recall f1-score support
0 0.81 0.81 0.81 26
1 0.74 0.74 0.74 19
accuracy 0.78 45
macro avg 0.77 0.77 0.77 45
weighted avg 0.78 0.78 0.78 45
7. Interpretabilidad
Gráficamos algunos aspectos de interpretabilidad como la frontera de decisión generada.
=0.02) plot_decision_boundary(mlp, X, y, h
Clase python PerceptronMulticapa Completo
class PerceptronMulticapa():
def __init__(self, params=None):
# Asignación de hiperparámetros
self.capa_entrada = params['capa_entrada']
self.capa_oculta = params['capa_oculta']
self.capa_salida = params['capa_salida']
self.epochs = params['epochs']
self.lr = params['lr']
self.relu = (lambda x: x*(x > 0))
self.derivada_relu = (lambda x: 1 * (x>0))
self.sigmoide = (lambda x: 1/(1 + np.exp(-x)))
self.derivada_sigmoide = (lambda x: x*(1-x))
# inicialización de pesos y bias
self.inicializacion()
def inicializacion(self):
# inicialización de pesos y bias aleatoria
42) # fijar una semilla para reproducir resultados
np.random.seed(
# Capa Oculta
self.pesos_capa_oculta = np.random.rand(self.capa_oculta, self.capa_entrada)
self.bias_capa_oculta = np.ones((self.capa_oculta, 1))
# Capa de salida
self.pesos_capa_salida = np.random.rand(self.capa_salida, self.capa_oculta)
self.bias_capa_salida = np.ones((self.capa_salida, 1))
def forward_pass(self, x):
# Realizar la operacion Wx + b de la capa oculta, x = x_0
= np.matmul(self.pesos_capa_oculta, x) + self.bias_capa_oculta
z # Aplicar función de activación
= self.relu(z) # z = x_1, h = x_2
h
# Aplicar la operación Wh + b para generar la salida, y = x_3
= np.matmul(self.pesos_capa_salida, h) + self.bias_capa_salida
y # Aplicar función de activación softmax para la clasificación
= self.sigmoide(y) # y = x_4
y_pred
return z, h, y_pred
def actualizar_pesos(self):
# actualizar pesos aplicando gradiente descendiente
self.pesos_capa_salida -= self.lr * self.gradiente_pesos_capa_salida
self.bias_capa_salida -= self.lr * self.gradiente_bias_capa_salida
self.pesos_capa_oculta -= self.lr * self.gradiente_pesos_capa_oculta
self.bias_capa_oculta -= self.lr * self.gradiente_bias_capa_oculta
def backward_pass(self, x, z, y_real, h, y_pred):
# Propagación de error en la capa de salida
# Calculo de error en la capa de salida g_out
#error_salida = (y_pred - y_real) * self.derivada_sigmoide(y_pred)
= y_pred - y_real
error_salida
# gradiente de los pesos respecto a la capa de salida
# X_in * g_out = error_salida * h.T
# X_in = h es la entrada a la capa de salida
self.gradiente_pesos_capa_salida = np.matmul(error_salida, h.T)
# gradiente de los bias respecto a la capa de salida
self.gradiente_bias_capa_salida = error_salida
# Propagación de error en la capa oculta
# gradiente respecto a la capa oculta
# (g_out * W) * relu'(X_in)
# X_in en esta capa es la salida de aplicar la primera transformación
= np.matmul(self.pesos_capa_salida.T, error_salida) * self.derivada_relu(z)
error_oculta # gradientes con respecto a la capa oculta, de nuevo g_out * X_in
self.gradiente_pesos_capa_oculta = np.matmul(error_oculta, x.T)
self.gradiente_bias_capa_oculta = error_oculta
def entrenar(self, X, y):
# almacenar el costo de cada iteración
self.costo_historia = []
# iterar sobre el número de épocas
for iteracion in tqdm(range(self.epochs), desc='Iteraciones'):
# iterar sobre los datos de entrenamiento
= 0 # error para la iteración i
error_total # iterar sobre todo el conjunto de datos
for i, (x_i, y_i) in tqdm(enumerate(zip(X,y)), desc='Datos', leave=False):
# asegurar de que la entrada y la salida solo tenga una columna
= x_i.reshape(-1, 1)
x_i = y_i.reshape(-1, 1)
y_i # aplicar propagación hacia adelante
= self.forward_pass(x_i)
z, h, y_pred # calcular la perdida de entropia cruzada
= self.calcular_perdida_entropia_cruzada(y_i, y_pred)
perdida += perdida
error_total # aplicar propagación hacia atras
self.backward_pass(x_i, z, y_i, h, y_pred)
# actualizar pesos y bias usando gradiente descendiente
self.actualizar_pesos()
# almacenar los costos de cada iteración
self.costo = error_total / len(X)
self.costo_historia.append(self.costo)
#print(f'costo: {self.costo[0][0]} en la iteración: {iteracion}')
def calcular_perdida_entropia_cruzada(self, y_real, y_pred):
= 1e-12
epsilon # asegura que los valores de las predicciones esten en un rango
# seguro para evitar logaritmos de 0 y 1
= np.clip(y_pred, epsilon, 1 - epsilon)
y_pred # calculo de la perdida
= -(((1 - y_real) * np.log(1 - y_pred + epsilon)) + (y_real * np.log(y_pred + epsilon)))
perdida
return perdida
def prediccion(self, X):
= []
predicciones for x_i in X:
= x_i.reshape(-1, 1) # Asegurar que x_i sea una columna
x_i = self.forward_pass(x_i)
z, _, y_pred
predicciones.append(y_pred)return np.array(predicciones).flatten()
def evaluar(self, X, y, umbral):
# generar predicciones
= self.prediccion(X)
y_pred # convertir a 0 y 1 bajo un umbral
= np.where(y_pred >= umbral, 1, 0)
y_pred # generar reporte de clasificación y matriz de confusión
print(classification_report(y, y_pred))
= confusion_matrix(y, y_pred)
cm
# visualizacion de la matriz de confusion
ConfusionMatrixDisplay(cm).plot()
plt.show()
return y_pred, cm