DEV Community

Cover image for Aprendiendo Recurrent Neural Networks: prediciendo fallas usando el dataset C-MAPSS de la NASA, Python y C++
galp76
galp76

Posted on • Edited on

Aprendiendo Recurrent Neural Networks: prediciendo fallas usando el dataset C-MAPSS de la NASA, Python y C++

Bienvenidos a esta serie de posts. Lo que están por leer comenzó como un simple ejercicio de fin de semana para aprender a usar el ecosistema de Deep Learning en Python. Mi objetivo era trabajar en redes neuronales aplicadas al mantenimiento predictivo. Pero las cosas se fueron complicando y el proyecto ya incluye un microcontrolador Arduino Uno y C++. Para no hacer de esto un texto interminable, he decidido dividir la historia. En esta Parte 1, arrancaremos desde el principio: el análisis, limpieza y preparación de los datos de la NASA usando Python.

El contexto

Soy Ingeniero Industrial desde el año 2002 y, desde el 2020, también soy un apasionado autodidacta de la programación. Hace algunas semanas le pregunté a Google Gemini si podía ayudarme a encontrar un proyecto donde pudiera aplicar ambas áreas. ¿Su respuesta? Una genialidad: usar el famoso Turbofan Engine Degradation Simulation Dataset de la NASA para entrenar redes neuronales y hacer mantenimiento predictivo. Así nació MAJN (el nombre lo escogió mi hijo), y así comenzó el proyecto que quiero documentar a través de esta serie.

Buckle your seatbelt, Dorothy, 'cause Kansas is going bye-bye.

Hasta ahora había jugado con redes neuronales simples y un clasificador que alcanzaba fácilmente el 99,9 % de precisión.

Pero este dataset de la NASA es otro nivel.

Aquí ya no basta con un perceptrón multicapa tradicional. Vamos a tener que trabajar con series temporales, memoria a largo plazo y redes LSTM de verdad.

Primer intento

Hace algunos meses usé el código del excelente libro Neural Networks and Deep Learning de Michael Nielsen para entrenar un clasificador sencillo de tan solo 3 clases, el cual obtuvo 99,9% de precisión en los tests, así que decidí hacer pruebas con este nuevo Dataset.
Comenzamos a trabajar con el archivo train0001.txt del dataset. La primera prueba dió apenas un 50% de precisión en los tests, y después de nuevas iteraciones (más capas, más neuronas por capa, mas épocas) solo pude obtener un 59% en los tests, así que tocó hacerle caso a Gemini y aprovechar el dataset preparado para el Perceptrón en el entrenamiento de una Red Neuronal Recurrente, una red LSTM para ser más exactos. Estas están diseñadas específicamente para trabajar con series temporales, donde es vital tomar en consideración cómo evolucionan las variables independientes (en nuestro caso, los datos de los sensores) en función del tiempo.

Preparando los datos

Aquí está el código que usamos para preparar el dataset. Como ya se sabe, los datos de train_FD001.txt fueron simulados usando una sola condición de vuelo, así que lo primero que tuvimos que hacer fue buscar sensores que no cambiaran sus valores a lo largo de la vida de los motores, ya que los mismos representan ruido en el dataset a la hora de entrenar la red neuronal. Para ello, convertimos train_FD001.txt en un Dataframe de Pandas y usamos la función describe() para ubicar desviaciones estándar iguales o cercanas a cero.

import pandas as pd

# Definimos los nombres de las 26 columnas
nombres_columnas =[
    'id_motor', 'ciclo', 
    'config_1', 'config_2', 'config_3', 
    'sensor_1', 'sensor_2', 'sensor_3', 'sensor_4', 'sensor_5', 
    'sensor_6', 'sensor_7', 'sensor_8', 'sensor_9', 'sensor_10', 
    'sensor_11', 'sensor_12', 'sensor_13', 'sensor_14', 'sensor_15', 
    'sensor_16', 'sensor_17', 'sensor_18', 'sensor_19', 'sensor_20', 
    'sensor_21'
]

# TIP: Usamos sep=r'\s+' porque los datos están separados por uno o más espacios, no por comas.
df_train = pd.read_csv('train_FD001.txt', sep=r'\s+', header=None, names=nombres_columnas)

# Ver las primeras 5 filas para comprobar que cargó bien
print(df_train.head())
# Buscamos valores de std iguales o cercanos a cero
df_train.describe()
Enter fullscreen mode Exit fullscreen mode

Despues del análisis se decidió eliminar los sensores 1, 5, 6, 10, 16, 18 y 19, junto con las 3 configuraciones operativas, ya que no aportan ninguna información útil. Estas eliminaciones son la regla estándar para el estudio del dataset FD001.

# Lista de columnas inútiles para FD001
columnas_a_eliminar =['config_1', 'config_2', 'config_3', 
                       'sensor_1', 'sensor_5', 'sensor_6', 'sensor_10', 
                       'sensor_16', 'sensor_18', 'sensor_19']

# Eliminamos esas columnas de nuestro DataFrame de entrenamiento
df_train.drop(columns=columnas_a_eliminar, inplace=True)
Enter fullscreen mode Exit fullscreen mode

El siguiente paso es crear la columna de Vida Útil Restante (RUL), que será la meta (Y) que nuestra red neuronal aprenderá a predecir.

# Creamos una columna temporal con la vida máxima de CADA motor
df_train['ciclo_max'] = df_train.groupby('id_motor')['ciclo'].transform('max')

# Calculamos el RUL restando el ciclo máximo menos el ciclo actual
df_train['RUL'] = df_train['ciclo_max'] - df_train['ciclo']

# Ya no necesitamos la columna temporal, la borramos
df_train.drop(columns=['ciclo_max'], inplace=True)
Enter fullscreen mode Exit fullscreen mode

El siguiente paso es aplicar el tope al RUL: añadimos esta línea al código para topar el RUL máximo a 125 (el estándar en la NASA para este problema):

# Todo RUL mayor a 125, se convierte en 125.
df_train['RUL'] = df_train['RUL'].clip(upper=125)
Enter fullscreen mode Exit fullscreen mode

Normalización de datos de sensores

¿Qué hace la Normalización?
Toma todas las columnas, sin importar si son RPM, grados Celsius o PSI, y las exprime para meterlas en una misma escala estándar (usualmente entre 0 y 1 o entre -1 y 1).
Así, el valor máximo de RPM (15,000) se convierte en "1.0", y el valor máximo de vibración (0.05) también se convierte en "1.0". Ahora, la red neuronal los trata con el mismo nivel de respeto y puede descubrir cuál importa realmente para predecir la falla basándose en su comportamiento, no en su tamaño. OJO AQUÍ: No podemos normalizar a lo loco. El id_motor, el ciclo y nuestra meta RUL NO se normalizan. Esos son nuestros identificadores y nuestra respuesta en ciclos reales. Solo vamos a normalizar las columnas de los sensores (nuestra "X").

from sklearn.preprocessing import MinMaxScaler

# 1. Identificamos cuáles son las columnas de los sensores que nos quedaron
# (Es decir, todas menos id_motor, ciclo y RUL)
columnas_sensores = df_train.columns.drop(['id_motor', 'ciclo', 'RUL'])

# 2. Inicializamos el escalador
scaler = MinMaxScaler()

# 3. Entrenamos el escalador y transformamos los datos (solo los sensores)
df_train[columnas_sensores] = scaler.fit_transform(df_train[columnas_sensores])
Enter fullscreen mode Exit fullscreen mode



En este primer post nos enfocamos en entender el dataset y preparar los datos. En la próxima entrega entraremos de lleno en ventanas deslizantes (sliding windows), secuencias temporales y el corazón del modelo: las redes LSTM.


🚀 ¡ACTUALIZACIÓN! La historia continúa...
Ya está publicada la segunda entrega de esta serie. En ella damos el salto al Deep Learning: transformamos nuestros datos estáticos en secuencias temporales (sliding windows), diseñamos el "cerebro" usando una red LSTM y ponemos a prueba el modelo logrando un margen de error de apenas 10 vuelos.

👉 Lee la Parte 2: Dándole "memoria" a nuestra red y logrando un MAE de 10.05 haciendo clic aquí

Top comments (0)