Conforme se ha referido previamente, el desarrollo del modelo predictivo se realizó utilizando el framework DeepXDE (versión 1.10.1) con backend de TensorFlow 1.x (configurado mediante tensorflow.compat.v1). Para asegurar reproducibilidad, se fijó la semilla aleatoria en 123 a nivel de DeepXDE, TensorFlow y NumPy. La red neuronal se implementó como un DeepONetCartesianProd con la siguiente estructura especializada:
Rama (branch)
Capa de entrada: (num_sensors + 1)² = 49 neuronas.
3 capas ocultas de 20 neuronas cada una.
Tronco (trunk)
Capa de entrada: 3 neuronas (coordenadas espaciotemporales x, y, t).
Misma configuración de capas ocultas que la rama.
Hiperparámetros clave
Función de activación: ELU (Exponential Linear Unit).
Inicialización de pesos: Glorot normal.
Optimizador: ADAM con tasa de aprendizaje inicial de 2×10⁻³ y decaimiento exponencial (decay_rate=0.05 cada 500 pasos).
Código
import deepxde as ddeimport numpy as npimport tensorflow as tf# -------------------------------------------------------------------------# Constantes y Parametros# -------------------------------------------------------------------------# Backend y semilladde.backend.set_default_backend("tensorflow.compat.v1")dde.config.set_random_seed(123)# Parametros fisicosp =1050c =3639keff =5final_time =1800L0 =0.05cb =3825Q =0TM =45Ta =37alpha = p * c / keff# Coeficientes adimencionalesa1 = final_time / (alpha * L0**2)a2 = final_time * cb / (p * c)a3 = (final_time * Q) / (p * c * (TM - Ta))# Dominio de las fronterasx_initial, x_boundary =0.0, 1.0y_initial, y_boundary =0.0, 1.0t_initial, t_final =0.0, 1.0# Configuracion del numero de datospts_dom =45pts_bc =30pts_ic =20num_test =25# Malla de sensores y espacio de funcionesnum_sensors =6num_function =25size_cov_matrix =50# Arquitectura de la redwidth_net =20len_net =3AF ="elu"k_initializer ="Glorot normal"# Parámetros de entrenamientonum_iterations =20000learning_rate =2e-3decay_rate =0.05decay_steps =500# -------------------------------------------------------------------------# Dominio espacial y temporal# -------------------------------------------------------------------------spatial_domain = dde.geometry.Rectangle([x_initial, y_initial], [x_boundary, y_boundary])time_domain = dde.geometry.TimeDomain(t_initial, t_final)geomtime = dde.geometry.GeometryXTime(spatial_domain, time_domain)# -------------------------------------------------------------------------# EDP, CI y CF # -------------------------------------------------------------------------def initial_condition(X): X = np.asarray(X)if X.ndim ==1:return0.0return np.zeros((X.shape[0], 1))def heat_equation(x, u, coords): u_t = dde.grad.jacobian(u, x, i=0, j=2) u_xx = dde.grad.hessian(u, x, i=0, j=0) u_yy = dde.grad.hessian(u, x, i=1, j=1)return u_t - a1*(u_xx + u_yy) + a2*udef zero_value(X):return0def time_value(X):return X[:, 2]def is_on_vertex(x): vertices = np.array([[x_initial, y_initial], [x_boundary, y_initial], [x_initial, y_boundary], [x_boundary, y_boundary]])returnany(np.allclose(x, v) for v in vertices)def is_initial(X, on_initial):return on_initial and np.isclose(X[2], t_initial)def left_boundary(X, on_boundary): spatial = X[0:2] t = X[2]return ( on_boundary and np.isclose(spatial[0], x_initial) andnot np.isclose(t, t_initial) andnot is_on_vertex(spatial) )def right_boundary(X, on_boundary): spatial = X[0:2] t = X[2]return ( on_boundary and np.isclose(spatial[0], x_boundary) andnot np.isclose(t, t_initial) andnot is_on_vertex(spatial) )def up_low_boundary(X, on_boundary): spatial = X[0:2] t = X[2]return (on_boundary and (np.isclose(spatial[1], y_initial) or np.isclose(spatial[1], y_boundary)) andnot np.isclose(t, t_initial) andnot is_on_vertex(spatial) )# Condiciones iniciales y de fronteraic = dde.icbc.IC(geomtime, initial_condition, is_initial)left_bc = dde.icbc.DirichletBC(geomtime, zero_value, left_boundary)right_bc = dde.icbc.NeumannBC(geomtime, time_value, right_boundary)up_low_bc = dde.icbc.NeumannBC(geomtime, zero_value, up_low_boundary)# -------------------------------------------------------------------------# Construccion de los datos# -------------------------------------------------------------------------pde_data = dde.data.TimePDE( geomtime, heat_equation, [ic, left_bc, right_bc, up_low_bc], num_domain=pts_dom, num_boundary=pts_bc, num_initial=pts_ic )# -------------------------------------------------------------------------# Sensores y espacio de funciones# -------------------------------------------------------------------------side = np.linspace(x_initial, x_boundary, num_sensors +1)x, y = np.meshgrid(side, side, indexing='xy')sensor_pts = np.stack([x.ravel(), y.ravel()], axis=1)fs = dde.data.function_spaces.GRF2D(N=size_cov_matrix, interp="linear")data = dde.data.PDEOperatorCartesianProd( pde_data, fs, sensor_pts, num_function=num_function, function_variables=[0, 1], num_test=num_test)# -------------------------------------------------------------------------# Definicion de la red# -------------------------------------------------------------------------branch_layers = [(num_sensors +1)**2] + len_net * [width_net]trunk_layers = [3] + len_net * [width_net]net = dde.nn.DeepONetCartesianProd( branch_layers, trunk_layers, activation=AF, kernel_initializer=k_initializer)# -------------------------------------------------------------------------# Compilacion y entrenamiento del modelo# -------------------------------------------------------------------------model = dde.Model(data, net)model.compile("adam", lr=learning_rate, decay=("inverse time", decay_steps, decay_rate))losshistory, train_state = model.train(iterations=num_iterations,display_every=decay_steps)# Refinamiento con el optimizador L-BFGSmodel.compile("L-BFGS")losshistory, train_state = model.train()
13.1 Gráficas de pérdida del modelo
El proceso de entrenamiento del modelo se monitoreó mediante el seguimiento detallado de cinco componentes de pérdida, cada una asociada a restricciones físicas y matemáticas específicas del problema:
Pérdida residual de la EDP
Función: Mide el cumplimiento de la ecuación de Bio-Calor en el dominio interior.
Importancia: Garantiza que la solución aprendida satisfaga la física subyacente.
Comportamiento esperado: Debe converger a valores cercanos a cero (típicamente < 1e-3).
Pérdida de condición inicial
Función: Controla la precisión en t=0.
Importancia: Asegura coherencia con el estado inicial del sistema.
Patrón típico: Suele ser la primera en converger por su carácter puntual.
Pérdida de frontera izquierda (Dirichlet)
Función: Evalúa el cumplimiento de condiciones de valor prescrito.
Relevancia: Mantiene valores fijos en bordes específicos.
Convergencia: Normalmente rápida por ser restrictiva.
Pérdida de frontera derecha (Neumann)
Función: Verifica gradientes normales en esta frontera
Dificultad característica: Puede mostrar oscilaciones iniciales
Pérdida de fronteras superior/inferior (Neumann)
Función: Controla condiciones de flujo en estos bordes
Complejidad: En problemas 2D/3D suele ser la última en estabilizarse
13.1.1 Perdida para el conjunto de entrenamiento
La tendencia decreciente de la pérdida durante el entrenamiento evidencia que el modelo neuronal está aprendiendo progresivamente a satisfacer las restricciones físicas impuestas por la ecuación de Bio-Calor y sus condiciones asociadas. Este comportamiento indica una correcta adaptación de los parámetros de la red, permitiendo reducir consistentemente el error en las distintas componentes de la función objetivo y aproximando con mayor precisión la dinámica térmica en el dominio.
Código
import plotly.graph_objects as go# Nombres de las componentes del lossloss_labels = ["Pérdida residual PDE","Pérdida de condición inicial","Pérdida de frontera izquierda (Dirichlet)","Pérdida de frontera derecha (Neumann)","Pérdida de fronteras superior/inferior (Neumann)"]# Extraer pasos y pérdida de entrenamientosteps = losshistory.stepstrain_loss = np.array(losshistory.loss_train)# Crear figurafig_train = go.Figure()for i inrange(train_loss.shape[1]): fig_train.add_trace(go.Scatter( x=steps, y=train_loss[:, i], mode='lines', name=loss_labels[i] ))fig_train.update_layout( title="Historial de pérdida en el entrenamiento", xaxis=dict(title="Iteración", tickformat=".1e"), yaxis=dict(title="Pérdida (log)", type="log", tickformat=".1e"), template="plotly_white", legend=dict(x=0.99, y=0.99), font=dict(size=14))
Figura 13.1: Historial de pérdida en el entrenamiento de la red neuronal.
13.1.2 Pérdida para el conjunto de prueba
La disminución del error en el conjunto de prueba confirma que el modelo no solo memoriza los datos de entrenamiento, sino que logra generalizar a situaciones no vistas. Esto constituye un resultado favorable, pues asegura que la red neuronal mantiene su capacidad predictiva fuera de los escenarios empleados para el ajuste, garantizando robustez y confiabilidad en aplicaciones biomédicas donde la precisión en la estimación térmica es fundamental.
Código
import plotly.graph_objects as go# Nombres de las componentes del lossloss_labels = ["Pérdida residual PDE","Pérdida de condición inicial","Pérdida de frontera izquierda (Dirichlet)","Pérdida de frontera derecha (Neumann)","Pérdida de fronteras superior/inferior (Neumann)"]# Extraer pasos y pérdida de entrenamientosteps = losshistory.stepstest_loss = np.array(losshistory.loss_test)# Crear figurafig_test = go.Figure()for i inrange(test_loss.shape[1]): fig_test.add_trace(go.Scatter( x=steps, y=test_loss[:, i], mode='lines', name=loss_labels[i] ))fig_test.update_layout( title="Historial de pérdida en el conjunto de prueba", xaxis=dict(title="Iteración", tickformat=".1e"), yaxis=dict(title="Pérdida (log)", type="log", tickformat=".1e"), template="plotly_white", legend=dict(x=0.99, y=0.99), font=dict(size=14))
Figura 13.2: Historial de pérdida en el conjunto de prueba de la red neuronal.
13.2 Guardado de datos
Para permitir la comparación cuantitativa con el método de Crank-Nicolson y facilitar la generación de visualizaciones consistentes, se exportaron las predicciones del modelo neuronal en formato CSV. El proceso consistió en:
Discretización: 26 segmentos equiespaciados en cada eje (x, y).
Puntos totales: 676 (26 × 26).
Tiempos evaluados: t = [0.0, 0.25, 0.50, 0.75, 1.0].
Estructura del archivo:
Coordenadas espacio-temporales (t, x, y) para cada punto de la grilla 26×26.
Valores de la solución en los tiempos de interés.
Listado 13.1: Guardado de los datos de la red neuronal.
Código
import pandas as pd# Lista de tiempostimes = [0.0, 0.25, 0.5, 0.75, 1.0]# Crear la malla (x, y)num_points =26x = np.linspace(0, 1, num_points)y = np.linspace(0, 1, num_points)X, Y = np.meshgrid(x, y)# Lista para almacenar resultadosresults = []for t_val in times:# Crear entrada trunk: (num_points^2, 3) points = np.vstack((X.flatten(), Y.flatten(), t_val * np.ones_like(X.flatten()))).T# Crear entrada branch: condición inicial constante cero branch_input = np.zeros((1, sensor_pts.shape[0]))# Predecir predicted = model.predict((branch_input, points)).flatten()# Agregar los datos al resultadofor xi, yi, thetai inzip(points[:, 0], points[:, 1], predicted): results.append([t_val, xi, yi, thetai])# Crear el DataFramedf = pd.DataFrame(results, columns=["time", "X", "Y", "Theta"])# Obtener la ruta del script actual y guardar el archivo CSVruta =r"data/model_DoN.csv"df.to_csv(ruta, index=False)
13.2.1 Visualización interactiva
Para facilitar el análisis exploratorio de las predicciones generadas por el modelo DeepONet, se implementó una tabla dinámica interactiva mediante la librería itables (versión 1.5.2), que extiende las funcionalidades de Pandas para su visualización (Solo en versión en línea).
Código
from itables import show, optionsoptions.maxBytes =0# Mostrar la tabla interactivashow(df, paging=True, ordering=True, searching =False, scrollY="350px", buttons=["pageLength", "csv", "excel"], lengthMenu=[10, 25, 50, 100], classes="display nowrap cell-border" )
Tabla 13.1: Predicciones de la red neuronal.
time
X
Y
Theta
Loading ITables v2.3.0 from the internet...
(need help?)