Aprendizaje de operadores a través de DeepONet informado por física Vamos a implementarlo desde cero

Aprendizaje de operadores con DeepONet informado por física, implementación desde cero.

Una inmersión profunda en los DeepONets, redes neuronales informadas por la física y DeepONets informados por la física

Figura 1. Las EDO/PDE se utilizan ampliamente para describir los procesos del sistema. En muchos escenarios, estas EDO/PDE aceptan una función (por ejemplo, la función de forzamiento u(t)) como entrada y producen otra función (por ejemplo, s(t)) como salida. Tradicionalmente, se utilizan solucionadores numéricos para conectar la entrada y la salida. Más recientemente, se han desarrollado operadores neuronales para abordar el mismo problema pero con una eficiencia mucho mayor. (Imagen del autor)

Las ecuaciones diferenciales ordinarias y parciales (EDOs/PDEs) son la base de muchas disciplinas en ciencia e ingeniería, desde física y biología hasta economía y ciencias climáticas. Son herramientas fundamentales utilizadas para describir sistemas y procesos físicos, capturando el cambio continuo de las cantidades a lo largo del tiempo y el espacio.

Sin embargo, una característica única de muchas de estas ecuaciones es que no solo toman valores individuales como entradas, sino que toman funciones. Por ejemplo, consideremos el caso de predecir las vibraciones en un edificio debido a un terremoto. El movimiento del suelo, que varía con el tiempo, se puede representar como una función que actúa como entrada de la ecuación diferencial que describe el movimiento del edificio. De manera similar, en el caso de las ondas sonoras que se propagan en una sala de conciertos, las ondas sonoras producidas por un instrumento musical pueden ser una función de entrada con volumen y tono variables a lo largo del tiempo. Estas funciones de entrada variables influyen fundamentalmente en las funciones de salida resultantes: las vibraciones del edificio y el campo acústico en la sala, respectivamente.

Tradicionalmente, estas EDOs/PDEs se abordan utilizando solucionadores numéricos como métodos de diferencias finitas o elementos finitos. Sin embargo, estos métodos presentan un cuello de botella: para cada nueva función de entrada, el solucionador debe ejecutarse nuevamente. Este proceso puede ser intensivo en recursos computacionales y lento, especialmente para sistemas complejos o entradas de alta dimensionalidad.

Para abordar este desafío, Lu et al. introdujeron en 2019 un marco novedoso: la Red de Operadores Profundos, o DeepONet. Los DeepONets tienen como objetivo aprender el operador que mapea funciones de entrada a funciones de salida, aprendiendo así a predecir la salida de estas EDOs/PDEs para cualquier función de entrada sin tener que ejecutar un solucionador numérico cada vez.

Pero los DeepONets, aunque poderosos, heredan los problemas comunes de los métodos basados en datos: ¿Cómo podemos asegurarnos de que las predicciones de la red estén de acuerdo con las leyes conocidas de la física encapsuladas en las ecuaciones gobernantes?

Ingresa el aprendizaje informado por la física.

El aprendizaje informado por la física es una rama en rápido desarrollo del aprendizaje automático que combina principios físicos con la ciencia de datos para mejorar la modelización y comprensión de sistemas físicos complejos. Implica aprovechar el conocimiento específico del dominio y las leyes físicas para guiar el proceso de aprendizaje y mejorar la precisión, generalización e interpretabilidad de los modelos de aprendizaje automático.

Bajo este marco, en 2021, Wang et al. presentaron una nueva variante de DeepONets: el DeepONet Informado por la Física. Este enfoque innovador se basa en los cimientos de los DeepONets al incorporar nuestro entendimiento de las leyes físicas en el proceso de aprendizaje. Ya no solo le pedimos a nuestro modelo que aprenda a partir de datos; lo guiamos con principios derivados de siglos de investigación científica.

¡Esta parece ser una aproximación muy prometedora! ¿Pero cómo la implementamos en la práctica? Eso es precisamente lo que vamos a explorar hoy 🤗

En este blog, discutiremos la teoría detrás del DeepONet Informado por la Física y veremos cómo implementarlo desde cero. También pondremos en acción nuestro modelo desarrollado y demostraremos su poder a través de un estudio de caso práctico.

¡Comencemos!

Tabla de contenidos · 1. Estudio de caso · 2. DeepONet informado por la física ∘ 2.1 DeepONet: Una visión general ∘ 2.2 Redes neuronales informadas por la física (PINNs) ∘ 2.3 DeepONet informado por la física · 3. Implementación de DeepONet informado por la física ∘ 3.1 Definir la arquitectura ∘ 3.2 Definir la pérdida de EDO ∘ 3.3 Definir el paso de descenso de gradiente · 4. Generación y organización de datos ∘ 4.1 Generación de perfiles u(·) ∘ 4.2 Generación del conjunto de datos ∘ 4.3 Organización del conjunto de datos · 5. Entrenamiento de DeepONet informado por la física · 6. Discusión de resultados · 7. Conclusiones · Referencias

1. Estudio de caso

Aterricemos nuestra discusión en un ejemplo concreto. En este blog, vamos a reproducir el primer estudio de caso considerado en el artículo original de Wang et al., es decir, un problema de valor inicial descrito por la siguiente ecuación diferencial ordinaria (ODE):

con una condición inicial s(0) = 0.

En esta ecuación, u( t ) es la función de entrada que varía en el tiempo, y s( t ) es el estado del sistema en el tiempo t que nos interesa predecir. En un escenario físico, u( t ) podría representar una fuerza aplicada a un sistema, y s( t ) podría representar la respuesta del sistema, como su desplazamiento o velocidad, dependiendo del contexto. Nuestro objetivo aquí es aprender la relación entre el término de fuerza u( t ) y la solución de la EDO s( t ).

Métodos numéricos tradicionales como el método de Euler o los métodos de Runge-Kutta pueden resolver esta ecuación de manera efectiva. Sin embargo, debemos notar que el término de fuerza u( t ) puede tener varios perfiles, como se muestra en la siguiente figura:

Figura 2. Perfiles de ejemplo de u(t). (Imagen del autor)

Consecuentemente, cada vez que u( t ) cambia, necesitaríamos volver a ejecutar toda la simulación para obtener el correspondiente s( t ) (como se muestra en la Figura 3), lo cual puede ser computacionalmente intensivo e ineficiente.

Figura 3. Perfiles correspondientes de s(t). Se calculan utilizando el algoritmo RK45 para resolver la EDO. (Imagen del autor)

Entonces, ¿cómo podemos abordar este tipo de problema de manera más eficiente?

2. DeepONet informado por la física

Como se mencionó en la introducción, DeepONet informado por la física constituye una solución prometedora para nuestro problema objetivo. En esta sección, desglosaremos sus conceptos fundamentales para que sean más comprensibles.

Primero discutiremos los principios que sustentan el DeepONet original. A continuación, exploraremos el concepto de redes neuronales informadas por la física y cómo aporta una dimensión adicional a la resolución de problemas. Por último, demostraremos cómo podemos integrar de manera fluida estas dos ideas para construir los DeepONets informados por la física.

2.1 DeepONet: Una visión general

DeepONet, abreviatura de Red de Operadores Profundos, representa una nueva frontera en el aprendizaje profundo. A diferencia de los métodos tradicionales de aprendizaje automático que mapean un conjunto de valores de entrada a valores de salida, DeepONet está diseñado para mapear funciones completas a otras funciones. Esto hace que DeepONet sea particularmente poderoso cuando se trata de problemas que naturalmente involucran entradas y salidas funcionales. ¿Entonces, cómo logra exactamente ese objetivo?

Para formular lo que queremos lograr de forma simbólica:

Figura 4. Nuestro objetivo es entrenar una red neuronal para aproximar el operador que mapea el término de fuerza u(·) a la salida objetivo s(·), ambos son una función de t. (Imagen del autor)

En el lado izquierdo, tenemos el operador G que mapea desde una función de entrada u(·) a una función de salida s(·). En el lado derecho, nos gustaría utilizar una red neuronal para aproximar el operador G. Una vez que esto se pueda lograr, podríamos usar la red neuronal entrenada para realizar un cálculo rápido de s(·) dado cualquier u(·).

Para el estudio de caso actual, tanto la función de entrada u(·) como la función de salida s(·) toman el tiempo t como único argumento. Por lo tanto, la “entrada” y “salida” de la red neuronal que buscamos construir deberían lucir así:

Figura 5. La entrada y salida para el modelo de red neuronal que buscamos entrenar. (Imagen del autor)

Esencialmente, nuestra red neuronal debería aceptar el perfil completo de u( t ) como la primera entrada, así como una instancia de tiempo específica t como la segunda entrada. Posteriormente, debería generar la función de salida objetivo s(·) evaluada en la instancia de tiempo t, es decir, s( t ).

Para comprender mejor esta configuración, reconocemos que el valor de s( t ) depende en primer lugar del perfil de s(·), que a su vez depende de u(·), y en segundo lugar depende de en qué instancia de tiempo se evalúa s(·). Es por eso que el tiempo t es necesario que esté entre las entradas.

Hay dos preguntas que necesitamos aclarar en este momento: en primer lugar, ¿cómo debemos ingresar un perfil continuo de u(·) a la red? Y en segundo lugar, ¿cómo debemos concatenar las dos entradas, es decir, t y u(·)?

1️⃣ ¿Cómo debemos ingresar un perfil continuo de u(·)?

Bueno, en realidad no lo hacemos. Una solución sencilla es representar la función u(·) de manera discreta. Específicamente, simplemente evaluamos los valores de u(·) en suficientes pero finitos lugares y posteriormente alimentamos esos valores discretos de u(·) a la red neuronal:

Figura 6. El perfil de u(·) se discretiza antes de ser alimentado a la red neuronal. (Imagen del autor)

Esos lugares se denominan sensores en el artículo original de DeepONet.

2️⃣ ¿Cómo debemos concatenar la entrada t y u(·)?

A primera vista, podríamos querer concatenarlos directamente en la capa de entrada. Sin embargo, resulta que este enfoque ingenuo no solo pondrá una restricción sobre qué tipos de redes neuronales podemos usar, sino que también conducirá a una precisión de predicción subóptima en la práctica. Sin embargo, hay una mejor manera. Es hora de presentar a DeepONet.

En pocas palabras, DeepONet propuso una nueva arquitectura de red para realizar el aprendizaje de operadores: consta de dos componentes principales: una red de ramificación y una red de tronco. La red de ramificación toma los valores discretos de la función como entradas y los transforma en un vector de características. Mientras tanto, la red de tronco toma la(s) coordenada(s) (en nuestro estudio de caso actual, la coordenada es solo t. Para las EDP, incluirá tanto coordenadas temporales como espaciales) y también las convierte en un vector de características con las mismas dimensiones. Estos dos vectores de características se fusionan mediante un producto punto, y el resultado final se utiliza como la predicción de s(·) evaluada en la coordenada de entrada.

Figura 7. Un DeepONet consta de una red de ramificación para manejar la función de entrada u(·) y una red de tronco para manejar las coordenadas temporales/espaciales. Las salidas de las dos redes tienen las mismas dimensiones y se fusionan mediante un producto punto. Opcionalmente, se puede agregar un término de sesgo después del producto punto para mejorar aún más la expresividad del modelo. (Imagen del autor)

En el artículo original de DeepONet, los autores afirmaron que esta estrategia de “dividir y conquistar”, ejemplificada en redes separadas de “rama” y “tronco”, está inspirada en el teorema de aproximación universal para operadores, y sirve para introducir un fuerte sesgo inductivo específicamente para el aprendizaje de operadores. Este es también el punto clave que hace de DeepONet una solución efectiva, como afirman los autores.

Si tienes curiosidad por aprender más sobre las bases teóricas de DeepONet, consulta el Apéndice A en el artículo original.

Una de las principales fortalezas de DeepONet es su eficiencia. Una vez entrenado, un DeepONet puede inferir la función de salida para una nueva función de entrada en tiempo real, sin necesidad de entrenamiento adicional, siempre y cuando la nueva función de entrada esté dentro del rango de funciones de entrada en las que fue entrenado. Esto hace de DeepONet una herramienta poderosa en aplicaciones que requieren inferencia en tiempo real.

Otra notable fortaleza de DeepONet radica en su flexibilidad y versatilidad. Si bien la elección más común para las redes de tronco y rama pueden ser capas completamente conectadas, el marco de DeepONet permite un alto nivel de personalización de la arquitectura. Dependiendo de las características de la función de entrada u(·) y las coordenadas, también se pueden emplear una variedad de arquitecturas de redes neuronales como CNN, RNN, etc. Esta adaptabilidad hace de DeepONet una herramienta altamente versátil.

Sin embargo, a pesar de estas fortalezas, las limitaciones de DeepONet también son prominentes: como método puramente basado en datos, DeepONet no puede garantizar que sus predicciones sigan el conocimiento previo o las ecuaciones que describen el sistema físico en consideración. En consecuencia, DeepONet puede no generalizar bien, especialmente cuando se enfrenta a funciones de entrada que se encuentran fuera de la distribución de los datos de entrenamiento, denominadas entradas fuera de distribución (OOD). Un remedio común para esto es simplemente preparar una gran cantidad de datos para el entrenamiento, lo cual puede no ser siempre factible en la práctica, especialmente en campos científicos e ingenieriles donde la recolección de datos puede ser costosa o llevar mucho tiempo.

Entonces, ¿cómo debemos abordar estas limitaciones? Es hora de hablar de aprendizaje informado por la física, y más específicamente, de redes neuronales informadas por la física (PINNs).

2.2 Redes Neuronales Informadas por la Física (PINNs)

En los modelos tradicionales de aprendizaje automático, nos basamos principalmente en datos para aprender los patrones subyacentes. Sin embargo, en muchos campos científicos e ingenieriles, las ecuaciones (ODE/PDEs) que capturan nuestro conocimiento previo sobre el sistema dinámico están disponibles, y presentan otra fuente de información que podemos aprovechar además de los datos observados. Esta fuente adicional de conocimiento, si se incorpora correctamente, podría mejorar potencialmente el rendimiento y la capacidad de generalización del modelo, especialmente cuando se trata de datos limitados o ruidosos. Aquí es donde entra en juego el aprendizaje informado por la física.

Cuando fusionamos el concepto de aprendizaje informado por la física y las redes neuronales, llegamos a las redes neuronales informadas por la física (PINNs).

Las PINNs son un tipo de red neuronal donde la red se entrena no solo para ajustarse a los datos, sino también para respetar las leyes físicas conocidas descritas por ecuaciones diferenciales. Esto se logra introduciendo una pérdida de ODE/PDE, que mide el grado de violación de las ecuaciones diferenciales que rigen. De esta manera, inyectamos las leyes físicas en el proceso de entrenamiento de la red y la hacemos informada físicamente.

Figura 8. La función de pérdida de una red neuronal informada por la física incluye un término de contribución de pérdida de PDE, que mide efectivamente si la solución predicha satisface la ecuación diferencial que gobierna. Tenga en cuenta que la derivada de la salida respecto a las entradas se puede calcular fácilmente gracias a la diferenciación automática. (Imagen del autor)

Aunque las PINNs han demostrado ser efectivas en muchas aplicaciones, no están exentas de limitaciones. Las PINNs suelen entrenarse para parámetros de entrada específicos (por ejemplo, condiciones de frontera e iniciales, fuerza externa, etc.). En consecuencia, cada vez que los parámetros de entrada cambien, necesitaríamos volver a entrenar la PINN. Por lo tanto, no son particularmente efectivas para inferencia en tiempo real bajo diferentes condiciones de funcionamiento.

¿Recuerdas qué método está específicamente diseñado para manejar parámetros de entrada variables? Así es, ¡es el DeepONet! Es hora de combinar la idea de aprendizaje informado por la física con DeepONet.

2.3 DeepONet Informado por la Física

La idea principal detrás del DeepONet Informado por la Física es combinar las fortalezas tanto de los DeepONets como de los PINNs. Al igual que un DeepONet, un DeepONet Informado por la Física es capaz de tomar una función como entrada y producir una función como salida. Esto lo hace altamente eficiente para la inferencia en tiempo real de nuevas funciones de entrada, sin necesidad de volver a entrenar.

Por otro lado, al igual que un PINN, un DeepONet Informado por la Física incorpora leyes físicas conocidas en su proceso de aprendizaje. Estas leyes se introducen como restricciones adicionales en la función de pérdida durante el entrenamiento. Este enfoque permite que el modelo haga predicciones físicamente consistentes, incluso cuando se trata de datos limitados o ruidosos.

¿Cómo logramos esta integración? Similar a los PINNs, agregamos una contribución adicional de pérdida para medir qué tan bien se adhieren las predicciones del modelo a la ecuación diferencial conocida. Al optimizar esta función de pérdida, el modelo aprende a hacer predicciones que son tanto consistentes con los datos (si se proporcionan datos de medición durante el entrenamiento) como consistentes con la física.

Figura 10. DeepONet Informado por la Física utiliza DeepONet como la arquitectura principal mientras aprovecha el concepto de aprendizaje informado por la física para entrenar el modelo. De esta manera, el DeepONet Informado por la Física entrenado es consistente tanto con los datos como con la física. (Imagen del autor)

En resumen, el DeepONet Informado por la Física es una herramienta poderosa que combina lo mejor de ambos mundos: la eficiencia del DeepONet y la precisión del aprendizaje informado por la física. Representa un enfoque prometedor para resolver problemas complejos en campos donde tanto la inferencia en tiempo real como la consistencia física son cruciales.

En la siguiente sección, comenzaremos a trabajar en nuestro estudio de caso y convertiremos la teoría en código real.

3. Implementación de DeepONet Informado por la Física

En esta sección, veremos cómo definir un modelo DeepONet Informado por la Física para abordar nuestro estudio de caso objetivo. Lo implementaremos en TensorFlow. Comencemos importando las bibliotecas necesarias:

import numpy as npimport matplotlib.pyplot as pltimport tensorflow as tffrom tensorflow import kerastf.random.set_seed(42)

3.1 Definir la Arquitectura

Como se discutió anteriormente, el DeepONet Informado por la Física comparte la misma arquitectura que el DeepONet original. La siguiente función define la arquitectura para el DeepONet:

def create_model(mean, var, verbose=False):    """Definición de un DeepONet con capas de ramas y tronco totalmente conectadas.        Args:    ----    mean: diccionario, valores promedio de las entradas    var: diccionario, valores de varianza de las entradas    verbose: booleano, indica si mostrar el resumen del modelo        Outputs:    --------    model: el modelo DeepONet    """        # Red de ramas    branch_input = tf.keras.Input(shape=(len(mean['forcing'])), name="forcing")    branch = tf.keras.layers.Normalization(mean=mean['forcing'], variance=var['forcing'])(branch_input)    for i in range(3):        branch = tf.keras.layers.Dense(50, activation="tanh")(branch)        # Red de tronco    trunk_input = tf.keras.Input(shape=(len(mean['time'])), name="time")    trunk = tf.keras.layers.Normalization(mean=mean['time'], variance=var['time'])(trunk_input)       for i in range(3):        trunk = tf.keras.layers.Dense(50, activation="tanh")(trunk)        # Calcular el producto punto entre la red de ramas y la red de tronco    dot_product = tf.reduce_sum(tf.multiply(branch, trunk), axis=1, keepdims=True)        # Agregar el sesgo    output = BiasLayer()(dot_product)        # Crear el modelo    model = tf.keras.models.Model(inputs=[branch_input, trunk_input], outputs=output)        if verbose:        model.summary()            return model   

En el código anterior:

  1. Suponemos que tanto las redes de tronco como de ramas son redes totalmente conectadas, con 3 capas ocultas, cada una con 50 neuronas y con la función de activación tangente hiperbólica. Esta arquitectura se elige en función de pruebas preliminares y debería servir como un buen punto de partida para este problema. Sin embargo, es fácil reemplazarlo con otras arquitecturas (por ejemplo, CNN, RNN, etc.) y otros hiperparámetros de capa.
  2. Las salidas de las redes de tronco y ramas se fusionan mediante un producto punto. Como se sugiere en el artículo original de DeepONet, agregamos un término de sesgo para mejorar la precisión de la predicción. La clase BiasLayer() está definida personalizada para lograr ese objetivo:
class BiasLayer(tf.keras.layers.Layer):    
    def build(self, input_shape):        
        self.bias = self.add_weight(shape=(1,),                                    
                                    initializer=tf.keras.initializers.Zeros(),                                    
                                    trainable=True)    
    def call(self, inputs):        
        return inputs + self.bias

3.2 Definir pérdida de EDO

A continuación, definimos una función para calcular la pérdida de la EDO. Recordemos que nuestra EDO objetivo es:

Por lo tanto, podemos definir la función de la siguiente manera:

@tf.function
def Calculadora_residual_EDO(t, u, u_t, modelo):
    """Cálculo del residual de la EDO.
        Argumentos:
        - t: coordenada temporal
        - u: función de entrada evaluada en coordenadas temporales discretas
        - u_t: función de entrada evaluada en t
        - modelo: modelo DeepONet

        Salidas:
        - Residual_EDO: residual de la EDO gobernante
    """
    with tf.GradientTape() as cinta:
        cinta.watch(t)
        s = modelo({"forzamiento": u, "tiempo": t})
        # Calcular gradientes
        ds_dt = cinta.gradient(s, t)
        # Residual de la EDO
        Residual_EDO = ds_dt - u_t
        return Residual_EDO

En el código anterior:

  1. Utilizamos `tf.GradientTape()` para calcular el gradiente de s(·) con respecto a t . Tenga en cuenta que en TensorFlow, `tf.GradientTape()` se utiliza como un gestor de contexto, y todas las operaciones ejecutadas dentro del contexto serán registradas por la cinta. Aquí, observamos explícitamente la variable t . Como resultado, TensorFlow rastreará automáticamente todas las operaciones que involucren a t , que en este caso, es una ejecución hacia adelante del modelo DeepONet. Luego, utilizamos el método `gradient()` de la cinta para calcular el gradiente de s(·) con respecto a t .
  2. Incluimos un argumento de entrada adicional `u_t` , que denota el valor de la función de entrada u(·) evaluada en t . Esto constituye el término del lado derecho de nuestra EDO objetivo, y es necesario para calcular la pérdida del residual de la EDO.
  3. Utilizamos el decorador `@tf.function` para convertir la función Python regular que acabamos de definir en un grafo de TensorFlow. Es útil hacer esto ya que el cálculo del gradiente puede ser bastante costoso y ejecutarlo en el modo de gráfico puede acelerar significativamente los cálculos.

3.3 Definir paso del descenso del gradiente

A continuación, definimos la función para compilar la función de pérdida total y calcular los gradientes de la pérdida total con respecto a los parámetros del modelo de la red:

@tf.function
def Paso_descenso_gradiente(X, X_init, peso_IC, peso_EDO, modelo):
    """Calcular gradientes de la pérdida total con respecto a los parámetros del modelo de red.
        Argumentos:
        - X: conjunto de datos de entrenamiento para evaluar los residuos de la EDO
        - X_init: conjunto de datos de entrenamiento para evaluar las condiciones iniciales
        - peso_IC: peso para la pérdida de la condición inicial
        - peso_EDO: peso para la pérdida de la EDO
        - modelo: modelo DeepONet

        Salidas:
        - perdida_EDO: pérdida de la EDO calculada
        - perdida_IC: pérdida de la condición inicial calculada
        - perdida_total: suma ponderada de la pérdida de la EDO y la pérdida de la condición inicial
        - gradientes: gradientes de la pérdida total con respecto a los parámetros del modelo de red.
    """
    with tf.GradientTape() as cinta:
        cinta.watch(modelo.trainable_weights)
        # Predicción de la condición inicial
        y_pred_IC = modelo({"forzamiento": X_init[:, 1:-1], "tiempo": X_init[:, :1]})
        # Residual de la ecuación
        Residual_EDO = Calculadora_residual_EDO(t=X[:, :1], u=X[:, 1:-1], u_t=X[:, -1:], modelo=modelo)
        # Calcular pérdida
        perdida_IC = tf.reduce_mean(keras.losses.mean_squared_error(0, y_pred_IC))
        perdida_EDO = tf.reduce_mean(tf.square(Residual_EDO))
        # Pérdida total
        perdida_total = perdida_IC*peso_IC + perdida_EDO*peso_EDO
    gradientes = cinta.gradient(perdida_total, modelo.trainable_variables)
    return perdida_EDO, perdida_IC, perdida_total, gradientes

En el código anterior:

  1. Solo consideramos dos términos de pérdida: la pérdida asociada con la condición inicial `perdida_IC` y la pérdida del residual de la EDO `perdida_EDO` . La `perdida_IC` se calcula comparando s( t =0) predicho por el modelo con el valor inicial conocido de 0, y la `perdida_EDO` se calcula llamando a nuestra función `Calculadora_residual_EDO` previamente definida. La pérdida de datos también se puede calcular y agregar a la pérdida total si están disponibles los valores medidos de s( t ) (no implementado en el código anterior).
  2. En general, la pérdida total es una suma ponderada de `perdida_IC` y `perdida_EDO` , donde los pesos controlan cuánta importancia o prioridad se le da a esos términos de pérdida individuales durante el proceso de entrenamiento. En nuestro estudio de caso, es suficiente establecer tanto `peso_IC` como `peso_EDO` en 1.
  3. De manera similar a cómo calculamos la `perdida_EDO` , también utilizamos `tf.GradientTape()` como el gestor de contexto para calcular los gradientes. Sin embargo, aquí calculamos los gradientes de la pérdida total con respecto a los parámetros del modelo de la red, que son necesarios para realizar el descenso del gradiente.

Antes de continuar, resumamos rápidamente lo que hemos desarrollado hasta ahora:

1️⃣ Podemos inicializar un modelo DeepONet con la función create_model().

2️⃣ Podemos calcular los residuos de las EDO para evaluar qué tan bien las predicciones del modelo se ajustan a las EDO gobernantes. Esto se logra con la función ODE_residual_calculator.

3️⃣ Podemos calcular la pérdida total, así como sus gradientes con respecto a los parámetros del modelo de red, con train_step.

Ahora la preparación está medio hecha 🚀 En la próxima sección, discutiremos la generación de datos y los problemas de organización de datos (esperamos que el extraño X[:, :1] en el código anterior se aclare entonces). Después de eso, finalmente podemos entrenar el modelo y ver cómo se desempeña.

4. Generación de Datos y Organización

En esta sección, discutiremos la generación de datos sintéticos y cómo organizarlos para entrenar el modelo DeepONet informado por la física.

4.1 Generación de perfiles u(·)

Los datos utilizados para el entrenamiento, validación y prueba se generarán de forma sintética. La razón detrás de este enfoque es doble: no solo es conveniente, sino que también permite un control total sobre las características de los datos.

En el contexto de nuestro estudio de caso, generaremos la función de entrada u(·) utilizando un Proceso Gaussiano con media cero, con una función de base radial (RBF) como kernel.

Un Proceso Gaussiano es un marco matemático poderoso comúnmente utilizado en el aprendizaje automático para modelar funciones. El kernel RBF es una elección popular para capturar la similitud entre los puntos de entrada. Al usar el kernel RBF dentro del Proceso Gaussiano, aseguramos que los datos sintéticos generados exhiban un patrón suave y continuo, lo cual es a menudo deseable en diversas aplicaciones. Para obtener más información sobre el Proceso Gaussiano, no dudes en consultar mi blog anterior.

En scikit-learn, esto se puede lograr en solo unas pocas líneas de código:

from sklearn.gaussian_process import GaussianProcessRegressorfrom sklearn.gaussian_process.kernels import RBFdef create_samples(length_scale, sample_num):    """Crear datos sintéticos para u(·)        Args:    ----    length_scale: float, escala de longitud para el kernel RBF    sample_num: número de perfiles u(·) a generar        Outputs:    --------    u_sample: perfiles u(·) generados    """    # Definir kernel con la escala de longitud dada    kernel = RBF(length_scale)    # Crear un regresor de proceso gaussiano    gp = GaussianProcessRegressor(kernel=kernel)    # Ubicaciones de los puntos de colocación    X_sample = np.linspace(0, 1, 100).reshape(-1, 1)         # Crear muestras    u_sample = np.zeros((sample_num, 100))    for i in range(sample_num):        # muestrear directamente de la distribución previa        n = np.random.randint(0, 10000)        u_sample[i, :] = gp.sample_y(X_sample, random_state=n).flatten()              return u_sample

En el código anterior:

  1. Usamos length_scale para controlar la forma de la función generada. Para un kernel RBF, la Figura 11 muestra el perfil u(·) dado diferentes escalas de longitud del kernel.
  2. Recordemos que necesitamos discretizar u(·) antes de alimentarlo al DeepONet. Esto se hace especificando una variable X_sample, que asigna 100 puntos distribuidos uniformemente dentro de nuestro dominio temporal de interés.
  3. En scikit-learn, el objeto GaussianProcessRegressor expone un método sample_y para permitir la generación de muestras aleatorias a partir del proceso gaussiano con el kernel especificado por la escala de longitud. Observa que no llamamos a .fit() antes de usar el objeto GaussianProcessRegressor, a diferencia de lo que normalmente hacemos con otros regresores de scikit-learn. Esto es intencional, ya que queremos que GaussianProcessRegressor use la exacta length_scale que proporcionamos. Si llamas a .fit(), la length_scale se optimizará a otro valor para ajustarse mejor a los datos proporcionados.
  4. La salida u_sample es una matriz con una dimensión de sample_num * 100. Cada fila de u_sample representa un perfil de u(·), que consta de 100 valores discretos.
Figura 11. Perfiles sintéticos de u(·) bajo diferentes escalas de longitud de kernel. (Imagen del autor)

4.2 Generación del conjunto de datos

Ahora que hemos generado los perfiles de u(·), vamos a centrarnos en cómo organizar el conjunto de datos de manera que pueda ser alimentado al modelo DeepONet.

Recuerde que el modelo DeepONet que desarrollamos en la última sección requiere 3 entradas:

  1. la coordenada de tiempo t , que es un escalar entre 0 y 1 (no consideraremos el tamaño del lote por el momento);
  2. el perfil de u(·), que es un vector que consiste en los valores de u(·) evaluados en coordenadas de tiempo predefinidas y fijas entre 0 y 1;
  3. el valor de u( t ), que nuevamente es un escalar. Este valor de u( t ) se utiliza para calcular la pérdida de la EDO en la coordenada de tiempo t .

Por lo tanto, podemos formular una sola muestra de la siguiente manera:

(Imagen del autor)

Por supuesto, para cada perfil de u(·) (marcado en verde en la ilustración anterior), debemos considerar múltiples valores de t (y los correspondientes u( t )) para evaluar la pérdida de la EDO y así imponer mejor las restricciones físicas. En teoría, t puede tomar cualquier valor dentro del dominio temporal considerado (es decir, entre 0 y 1 para nuestro estudio de caso). Sin embargo, para simplificar las cosas, solo consideraremos t en las mismas ubicaciones temporales donde se discretiza el perfil de u(·). Como resultado, nuestro conjunto de datos actualizado se verá así:

(Imagen del autor)

Tenga en cuenta que la discusión anterior solo considera un solo perfil de u(·). Si tenemos en cuenta todos los perfiles de u(·), nuestro conjunto de datos final se vería así:

(Imagen del autor)

donde N representa el número de perfiles de u(·). Ahora, teniendo eso en cuenta, veamos algo de código:

from tqdm import tqdmfrom scipy.integrate import solve_ivpdef generate_dataset(N, length_scale, ODE_solve=False):    """Generar conjunto de datos para el entrenamiento de DeepONet con información física.        Args:    ----    N: int, número de perfiles de u(·)    length_scale: float, escala de longitud para el kernel RNF    ODE_solve: booleano, indica si calcular la correspondiente s(·)        Outputs:    --------    X: el conjunto de datos para t, perfiles de u(·), y u(t)    y: el conjunto de datos para la solución de la EDO correspondiente s(·)    """        # Crear campos aleatorios    random_field = create_samples(length_scale, N)        # Compilar conjunto de datos    X = np.zeros((N*100, 100+2))    y = np.zeros((N*100, 1))    for i in tqdm(range(N)):        u = np.tile(random_field[i, :], (100, 1))        t = np.linspace(0, 1, 100).reshape(-1, 1)        # u(·) evaluado en t        u_t = np.diag(u).reshape(-1, 1)        # Actualizar matriz general        X[i*100:(i+1)*100, :] = np.concatenate((t, u, u_t), axis=1)        # Resolver la EDO        if ODE_solve:            sol = solve_ivp(lambda var_t, var_s: np.interp(var_t, t.flatten(), random_field[i, :]),                             t_span=[0, 1], y0=[0], t_eval=t.flatten(), method='RK45')            y[i*100:(i+1)*100, :] = sol.y[0].reshape(-1, 1)            return X, y

En el código anterior, agregamos una opción para calcular el correspondiente s(·) para un perfil u(·) dado. Aunque no utilizaremos los valores de s(·) en el entrenamiento, aún los necesitaremos para evaluar el rendimiento del modelo. El cálculo de s(·) se logra utilizando scipy.integrate.solve_ivp, que es un solucionador de EDO de SciPy diseñado específicamente para resolver problemas de valor inicial.

Ahora podemos generar los conjuntos de datos de entrenamiento, validación y prueba. Tenga en cuenta que, para este caso de estudio, utilizaremos una escala de longitud de 0.4 para generar los perfiles u(·) y entrenar el DeepONet informado por la física.

# Crear conjunto de datos de entrenamientoN_train = 2000length_scale_train = 0.4X_train, y_train = generate_dataset(N_train, length_scale_train)# Crear conjunto de datos de validaciónN_val = 100length_scale_test = 0.4X_val, y_val = generate_dataset(N_val, length_scale_test)# Crear conjunto de datos de pruebaN_test = 100length_scale_test = 0.4X_test, y_test = generate_dataset(N_test, length_scale_test, ODE_solve=True)

4.3 Organización del conjunto de datos

Finalmente, convertimos la matriz NumPy en objetos de conjunto de datos de TensorFlow para facilitar la ingestión de datos.

# Determinar el tamaño del loteini_batch_size = int(2000/100)col_batch_size = 2000# Crear objeto de conjunto de datos (condiciones iniciales)X_train_ini = tf.convert_to_tensor(X_train[X_train[:, 0]==0], dtype=tf.float32)ini_ds = tf.data.Dataset.from_tensor_slices((X_train_ini))ini_ds = ini_ds.shuffle(5000).batch(ini_batch_size)# Crear objeto de conjunto de datos (puntos de colocación)X_train = tf.convert_to_tensor(X_train, dtype=tf.float32)train_ds = tf.data.Dataset.from_tensor_slices((X_train))train_ds = train_ds.shuffle(100000).batch(col_batch_size)# Escalado mean = {    'forcing': np.mean(X_train[:, 1:-1], axis=0),    'time': np.mean(X_train[:, :1], axis=0)}var = {    'forcing': np.var(X_train[:, 1:-1], axis=0),    'time': np.var(X_train[:, :1], axis=0)}

En el código anterior, creamos dos conjuntos de datos distintos: uno para evaluar la pérdida de la EDO (train_ds), y otro para evaluar la pérdida de la condición inicial (ini_ds). También hemos precalculado los valores de media y varianza para t y u(·). Esos valores se utilizarán para estandarizar las entradas.

Esto es todo para la generación y organización de datos. A continuación, iniciaremos el entrenamiento del modelo y veremos cómo se desempeña.

5. Entrenamiento del DeepONet informado por la física

Como primer paso, creemos una clase personalizada para rastrear la evolución de la pérdida:

from collections import defaultdictclass LossTracking:    def __init__(self):        self.mean_total_loss = keras.metrics.Mean()        self.mean_IC_loss = keras.metrics.Mean()        self.mean_ODE_loss = keras.metrics.Mean()        self.loss_history = defaultdict(list)    def update(self, total_loss, IC_loss, ODE_loss):        self.mean_total_loss(total_loss)        self.mean_IC_loss(IC_loss)        self.mean_ODE_loss(ODE_loss)    def reset(self):        self.mean_total_loss.reset_states()        self.mean_IC_loss.reset_states()        self.mean_ODE_loss.reset_states()    def print(self):        print(f"IC={self.mean_IC_loss.result().numpy():.4e}, \              ODE={self.mean_ODE_loss.result().numpy():.4e}, \              total_loss={self.mean_total_loss.result().numpy():.4e}")            def history(self):        self.loss_history['total_loss'].append(self.mean_total_loss.result().numpy())        self.loss_history['IC_loss'].append(self.mean_IC_loss.result().numpy())        self.loss_history['ODE_loss'].append(self.mean_ODE_loss.result().numpy())

Luego, definimos la lógica principal de entrenamiento/validación:

# Configuraciones de entrenamienton_epochs = 300IC_weight= tf.constant(1.0, dtype=tf.float32)   ODE_weight= tf.constant(1.0, dtype=tf.float32)loss_tracker = LossTracking()val_loss_hist = []# Configurar optimizadoroptimizer = keras.optimizers.Adam(learning_rate=1e-3)# Instanciar el modelo PINNPI_DeepONet= create_model(mean, var)PI_DeepONet.compile(optimizer=optimizer)  # Configurar los callbacks_callbacks = [keras.callbacks.ReduceLROnPlateau(factor=0.5, patience=30),             tf.keras.callbacks.ModelCheckpoint('NN_model.h5', monitor='val_loss', save_best_only=True)]callbacks = tf.keras.callbacks.CallbackList(                _callbacks, add_history=False, model=PI_DeepONet)# Iniciar el proceso de entrenamientofor epoch in range(1, n_epochs + 1):      print(f"Epoch {epoch}:")    for X_init, X in zip(ini_ds, train_ds):                # Calcular los gradientes        ODE_loss, IC_loss, total_loss, gradients = train_step(X, X_init,                                                             IC_weight, ODE_weight,                                                            PI_DeepONet)        # Descenso de gradiente        PI_DeepONet.optimizer.apply_gradients(zip(gradients, PI_DeepONet.trainable_variables))                # Rastreo de pérdida        loss_tracker.update(total_loss, IC_loss, ODE_loss)    # Resumen de pérdida    loss_tracker.history()    loss_tracker.print()    loss_tracker.reset()    ####### Validación    val_res = ODE_residual_calculator(X_val[:, :1], X_val[:, 1:-1], X_val[:, -1:], PI_DeepONet)    val_ODE = tf.cast(tf.reduce_mean(tf.square(val_res)), tf.float32)        X_val_ini = X_val[X_val[:, 0]==0]    pred_ini_valid = PI_DeepONet.predict({"forcing": X_val_ini[:, 1:-1], "time": X_val_ini[:, :1]}, batch_size=12800)    val_IC = tf.reduce_mean(keras.losses.mean_squared_error(0, pred_ini_valid))    print(f"val_IC: {val_IC.numpy():.4e}, val_ODE: {val_ODE.numpy():.4e}, lr: {PI_DeepONet.optimizer.lr.numpy():.2e}")        # Callback al final de la época    callbacks.on_epoch_end(epoch, logs={'val_loss': val_IC+val_ODE})    val_loss_hist.append(val_IC+val_ODE)        # Reorganizar el conjunto de datos    ini_ds = tf.data.Dataset.from_tensor_slices((X_train_ini))    ini_ds = ini_ds.shuffle(5000).batch(ini_batch_size)    train_ds = tf.data.Dataset.from_tensor_slices((X_train))    train_ds = train_ds.shuffle(100000).batch(col_batch_size)

Es un fragmento de código bastante largo, pero debería ser autoexplicativo ya que ya hemos cubierto todas las piezas importantes.

Para visualizar el rendimiento del entrenamiento, podemos trazar las curvas de convergencia de pérdida:

# Historiafig, ax = plt.subplots(1, 3, figsize=(12, 4))ax[0].plot(range(n_epochs), loss_tracker.loss_history['IC_loss'])ax[1].plot(range(n_epochs), loss_tracker.loss_history['ODE_loss'])ax[2].plot(range(n_epochs), val_loss_hist)ax[0].set_title('Pérdida IC')ax[1].set_title('Pérdida ODE')ax[2].set_title('Pérdida Val')for axs in ax:    axs.set_yscale('log')

Los resultados del entrenamiento se ven así:

Figura 12. Gráfico de convergencia de pérdida. (Imagen del autor)

Además, también podemos ver cómo evoluciona la precisión de predicción para un objetivo específico s(·) durante el entrenamiento:

Al comienzo del entrenamiento, podemos ver una discrepancia visible entre la predicción del modelo y la verdad absoluta. Sin embargo, hacia el final del entrenamiento, la s(·) predicha converge a la verdad absoluta. Esto indica que nuestro DeepONet informado por la física aprende adecuadamente.

6. Discusión de los resultados

Una vez completado el entrenamiento, podemos cargar los pesos guardados y evaluar el rendimiento.

Aquí seleccionamos al azar tres perfiles u(·) del conjunto de datos de prueba y comparamos el s(·) correspondiente predicho por nuestro DeepONet informado por la física, así como el calculado por el solucionador numérico de ODE. Podemos ver que las predicciones y la verdad absoluta son casi indistinguibles.

Figura 13. Se seleccionan al azar tres perfiles u(·) del conjunto de datos de prueba, que se muestran en la fila superior. La fila inferior muestra los perfiles s(·) correspondientes. Podemos ver que los resultados predichos por el DeepONet informado por la física son indistinguibles de la verdad absoluta, que se calcula mediante solucionadores numéricos de ODE. (Imagen del autor)

Estos resultados son bastante increíbles, considerando el hecho de que ni siquiera utilizamos ningún dato observacional de s(·) (excepto la condición inicial) para entrenar el DeepONet. Esto muestra que la EDO gobernante en sí misma ha proporcionado suficiente señal de “supervisión” para que el modelo haga predicciones precisas.

Otra cosa interesante de evaluar es la llamada capacidad de predicción “fuera de distribución”. Dado que impusimos la ecuación gobernante al entrenar el DeepONet, podemos esperar que el DeepONet informado por la física entrenado todavía pueda hacer predicciones decentes cuando los perfiles u(·) se encuentren fuera de la distribución de los u(·) de entrenamiento.

Para probar eso, podemos generar perfiles u(·) utilizando una escala de longitud diferente. Los siguientes resultados mostraron tres perfiles u(·) generados con una escala de longitud de 0.6, así como los s(·) predichos. Estos resultados son bastante buenos, considerando que el DeepONet informado por la física se entrena con una escala de longitud de 0.4.

Figura 14. El DeepONet informado por la física entrenado muestra cierto nivel de capacidad de predicción fuera de distribución. (Imagen del autor)

Sin embargo, si seguimos reduciendo la escala de longitud a 0.2, notaremos que comienzan a aparecer discrepancias visibles. Esto indica que hay un límite en la capacidad de generalización del DeepONet informado por la física entrenado.

Figura 15. Existe un límite en cuánto puede generalizar DeepONet informado por la física. (Imagen del autor)

En general, las escalas de longitud más pequeñas conducen a perfiles de u(·) más complejos, los cuales serían muy diferentes de los perfiles de u(·) utilizados para el entrenamiento. Esto podría explicar por qué el modelo entrenado tuvo desafíos para hacer predicciones precisas en regiones de menor escala de longitud.

Figura 16. Es desafiante para nuestro modelo entrenado generalizar a regiones de menor escala de longitud, ya que los perfiles de u(·) son más complejos y distintos de los datos de entrenamiento. (Imagen del autor)

En general, podríamos decir que el DeepONet informado por la física desarrollado puede aprender adecuadamente la dinámica del sistema y mapear desde la función de entrada hasta la función de salida solo con las restricciones de EDO. Además, el DeepONet informado por la física muestra cierto nivel de capacidad para manejar predicciones “fuera de la distribución”, lo que indica que entrenar el modelo para que se alinee con la EDO gobernante mejora la capacidad de generalización del modelo.

7. Conclusiones

Hemos recorrido un largo camino en nuestra exploración de DeepONet informado por la física. Desde comprender los conceptos fundamentales de DeepONet y el aprendizaje informado por la física, hasta verlos en acción a través de la implementación de código, hemos cubierto mucho sobre este poderoso método para resolver ecuaciones diferenciales.

Aquí hay algunas conclusiones clave:

1️⃣ DeepONet es un marco poderoso para realizar aprendizaje de operadores, gracias a su novedosa arquitectura de redes de ramas y tronco.

2️⃣ Aprendizaje Informado por la Física incorpora explícitamente las ecuaciones diferenciales gobernantes del sistema dinámico en el proceso de aprendizaje, lo que tiene el potencial de mejorar la interpretabilidad y la capacidad de generalización del modelo.

3️⃣ DeepONet Informado por la Física combina las fortalezas de DeepONet y el aprendizaje informado por la física, y se presenta como una herramienta prometedora para aprender mapeos funcionales mientras se adhiere a las ecuaciones gobernantes asociadas.

Espero que hayas disfrutado esta inmersión profunda en DeepONet informado por la física. A continuación, nos enfocaremos en resolver problemas inversos con DeepONet informado por la física. Mantente atento🤗

Puedes encontrar el cuaderno complementario con el código completo aquí 💻

Referencia

[1] Lu et al., DeepONet: Learning nonlinear operators for identifying differential equations based on the universal approximation theorem of operators. arXiv, 2019.

[2] Wang et al., Learning the solution operator of parametric partial differential equations with physics-informed DeepOnets. arXiv, 2021.

We will continue to update Zepes; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more

Inteligencia Artificial

6 Comandos Mágicos para Jupyter Notebooks en Ciencia de Datos con Python

En el campo de los proyectos de Ciencia de Datos basados en Python, la utilización de los Cuadernos de Jupyter es omn...

Inteligencia Artificial

Ex CEO de Google empoderará al ejército de Estados Unidos con IA y el Metaverso

El futuro del ejército de los Estados Unidos está a punto de experimentar una transformación revolucionaria. El cambi...

Inteligencia Artificial

Explorando NLP - Comenzando con NLP (Paso #1)

Este semestre, tengo NLP como parte de mi plan de estudios. ¡YAY! Como parte de una próxima evaluación para la materi...

Inteligencia Artificial

Cómo Reveal's Logikcull utilizó Amazon Comprehend para detectar y redactar información de identificación personal (PII) de documentos legales a gran escala.

Hoy en día, la información personal identificable (PII) está en todas partes. La PII se encuentra en correos electrón...

Inteligencia Artificial

Los investigadores de Microsoft revelan 'EmotionPrompt' mejorando la inteligencia emocional de la IA en múltiples modelos de lenguaje

La inteligencia emocional es una piedra angular históricamente ubicada dentro del vasto mosaico de cualidades humanas...