En la Parte 1 de esta serie, comenzamos nuestro viaje para predecir la falla de turbinas aeroespaciales de la NASA. Logramos limpiar nuestros datos, eliminar los sensores "muertos", normalizar las lecturas y, lo más importante, aplicar el Piecewise Linear RUL topando la vida útil a 125 ciclos.
Teníamos una tabla de datos impecable, pero nos enfrentábamos a un problema grave: las redes neuronales clásicas no tienen memoria.
Si a una red normal le mostramos una foto de un manómetro marcando 50 PSI, no sabe si la presión está subiendo, bajando o estable. Pero si le muestro un "video" de los últimos 30 segundos donde la aguja pasa de 20 a 50 PSI, cualquier una red diseñada correctamente detectará una tendencia.
En esta Parte 2, vamos a convertir nuestros datos estáticos en "videos", construiremos nuestro cerebro con una red LSTM y veremos los asombrosos resultados.
El concepto de las "Sliding Windows" (Ventanas Deslizantes)
Para darle contexto histórico a nuestra IA, necesitamos transformar nuestra tabla plana (2D) en un cubo de datos (3D). Para esto usamos una técnica llamada Ventanas Deslizantes.
Definimos una ventana de 30 ciclos.
La IA mirará los vuelos del 1 al 30 de un motor, y tratará de predecir cuánto le queda en el ciclo 30.
Luego, la ventana se "desliza": mirará del 2 al 31, e intentará predecir el ciclo 31. Y así sucesivamente.
Aquí está la función en Python (usando NumPy) que hace esta magia:
import numpy as np
def crear_secuencias(df, longitud_secuencia, columnas_sensores):
X, Y = [],[]
# Iteramos motor por motor para no mezclar vuelos de turbinas distintas
for id_motor in df['id_motor'].unique():
df_motor = df[df['id_motor'] == id_motor]
datos_sensores = df_motor[columnas_sensores].values
datos_rul = df_motor['RUL'].values
for i in range(len(df_motor) - longitud_secuencia + 1):
ventana_X = datos_sensores[i : i + longitud_secuencia]
X.append(ventana_X)
# El objetivo a predecir es el RUL del ÚLTIMO ciclo de la ventana
etiqueta_Y = datos_rul[i + longitud_secuencia - 1]
Y.append(etiqueta_Y)
return np.array(X), np.array(Y)
# Aplicamos la función pidiendo 30 ciclos de memoria hacia atrás
longitud_ventana = 30
X_train, Y_train = crear_secuencias(df_train, longitud_ventana, columnas_sensores)
print(f"Forma de X_train: {X_train.shape}")
Al ejecutar esto, la consola nos devolvió (17731, 30, 14).
¿Qué significa esto? Que hemos creado 17,731 "mini-videos", donde cada uno dura 30 ciclos temporales y monitorea 14 sensores simultáneamente.
Construyendo el Cerebro: La arquitectura LSTM
Con nuestros datos listos, era hora de usar TensorFlow/Keras para diseñar nuestra red neuronal.
Elegí una LSTM (Long Short-Term Memory). Estas redes tienen "compuertas" lógicas (olvido, entrada y salida) que deciden matemáticamente qué tendencias históricas son importantes para el desgaste y cuáles son solo ruido pasajero.
La arquitectura fue sencilla:
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Dropout
model = Sequential()
# La Capa Mágica: 50 "neuronas" evaluando nuestras ventanas 3D
model.add(LSTM(units=50, return_sequences=False, input_shape=(30, 14)))
# Dropout del 20%: Apagamos aleatoriamente neuronas para evitar que
# la red se "memorice" los datos (Overfitting) y la obligamos a generalizar.
model.add(Dropout(0.2))
# Capa de análisis matemático
model.add(Dense(units=32, activation='relu'))
# Salida: 1 sola neurona (Regresión). Nos dará un solo número: El RUL pronosticado.
model.add(Dense(units=1, activation='linear'))
# Optimizador Adam y Error Cuadrático Medio
model.compile(optimizer='adam', loss='mse', metrics=['mae'])
Total de parámetros entrenables: 14,665. (Este número será un dolor de cabeza en la Parte 3).
El Entrenamiento: Encendiendo los motores
Le pedí a la red que estudiara el dataset dando 40 pasadas completas (epochs=40), procesando en lotes de 64 "videos" a la vez, y escondiéndole un 10% de los datos para que se examinara a sí misma sin hacer trampa (validation_split=0.1).
print("¡Iniciando el entrenamiento de la Red Neuronal LSTM...")
historial = model.fit(
X_train,
Y_train,
epochs=40, # Dará 40 pasadas completas a los datos
batch_size=64, # Procesará los videos en bloques de 64
validation_split=0.1, # Guarda un 10% de datos para examinarse a sí misma
verbose=1 # Muestra una barra de progreso bonita
)
print("¡Entrenamiento finalizado!")
Al graficar el Error Absoluto Medio (MAE), vi lo siguiente: a partir de la época 10, la línea de error dejaba de caer bruscamente y empezaba a zigzaguear en el fondo. La red neuronal ya había entendido la termodinámica y el desgaste de los motores de la NASA casi a la perfección.
import matplotlib.pyplot as plt
# Configuramos el tamaño del gráfico
plt.figure(figsize=(10, 5))
# Graficamos el MAE de entrenamiento y el de validación
plt.plot(historial.history['mae'], label='Error de Entrenamiento (MAE)', color='blue')
plt.plot(historial.history['val_mae'], label='Error de Validación (val_MAE)', color='red', linestyle='--')
# Le ponemos títulos y etiquetas para que se vea profesional
plt.title('Curva de Aprendizaje del Modelo LSTM (Predicción de RUL)')
plt.xlabel('Épocas (Iteraciones de aprendizaje)')
plt.ylabel('Margen de Error Promedio (Ciclos)')
plt.legend()
plt.grid(True)
# Mostramos el gráfico
plt.show()
El "Jefe Final": El Test Set
En Machine Learning, sacar buenas notas en el entrenamiento no sirve de nada si el modelo falla en el mundo real.
Tomamos el archivo test_FD001.txt. A diferencia de los datos de entrenamiento, a estos motores se les cortó el registro antes de fallar. Tuvimos que pasarle a nuestra red los últimos 30 ciclos de estos motores "ciegos" y preguntarle: "¿Cuántos vuelos le quedan?". Luego, comparamos sus respuestas con el archivo secreto de respuestas reales.
import pandas as pd
import numpy as np
from sklearn.metrics import mean_absolute_error
print("1. Cargando datos de prueba...")
# Cargamos el test set
df_test = pd.read_csv('test_FD001.txt', sep=r'\s+')
# Eliminamos las mismas columnas inútiles
df_test.drop(columns=columnas_a_eliminar, inplace=True)
# NORMALIZACIÓN (¡ATENCIÓN AQUÍ!)
# Usamos transform() y NO fit_transform(). Queremos usar la misma escala
# que aprendimos con los datos de entrenamiento para no confundir a la red.
df_test[columnas_sensores] = scaler.transform(df_test[columnas_sensores])
print("2. Extrayendo la última ventana (30 ciclos) de cada motor...")
X_test =[]
# Agrupamos por motor
for engine in df_test['engine'].unique():
df_motor = df_test[df_test['engine'] == engine]
# Solo nos interesan los ÚLTIMOS 30 ciclos de este motor para predecir su futuro
# Si el motor tiene menos de 30 ciclos, esto fallaría, pero en FD001 todos tienen más de 30.
datos_sensores = df_motor[columnas_sensores].values[-longitud_ventana:]
X_test.append(datos_sensores)
X_test = np.array(X_test)
print(f"Forma de X_test lista para la red: {X_test.shape}")
print("3. Cargando las respuestas correctas (True RUL)...")
# Cargamos el archivo con los RUL reales
Y_test = pd.read_csv('RUL_FD001.txt', sep=r'\s+', header=None, names=['RUL_real'])
# Aplicamos el mismo tope de 125 (Piecewise RUL) a las respuestas correctas
Y_test['RUL_real'] = Y_test['RUL_real'].clip(upper=125)
Y_test_real = Y_test['RUL_real'].values
print("4. ¡Haciendo las predicciones con la Red Neuronal!")
# Le pedimos a la red que prediga
predicciones = model.predict(X_test)
# Calculamos el error final
error_final_mae = mean_absolute_error(Y_test_real, predicciones)
print("\n==================================================")
print(f"ERROR ABSOLUTO MEDIO EN EL TEST SET (FINAL BOSS): {error_final_mae:.2f} ciclos")
print("==================================================")
El resultado arrojado en la consola fue:
ERROR ABSOLUTO MEDIO EN EL TEST SET: 10.05 ciclos.
¡Un resultado fenomenal! Pasamos de un intento exploratorio inicial con un perceptrón multicapa y un modesto 60% de precisión, a un modelo recurrente capaz de predecir la falla inminente de un motor con un margen de error promedio de solo 10 vuelos. En la industria, tener esta ventana de certeza es oro puro para pedir repuestos, evitar correctivos catastróficos y programar paradas de planta.
¿Qué sigue ahora?
Teníamos un modelo exitoso, entrenado en la nube con Python y punto flotante de 32 bits. El proyecto podría haber terminado aquí.
Pero quise llevarlo al mundo físico: ¿Y si metemos este "cerebro" dentro de un microcontrolador?
Me compré un Arduino Uno, pensando ingenuamente que "todas las placas son iguales", lo cual complicó (para bien) las siguientes partes del experimento.
El problema: El modelo pesa casi 60 KB, pero un Arduino Uno tiene apenas 2 KB de memoria RAM.
En la Parte 3 de esta serie (más técnica que las anteriores), veremos como (con mucha ayuda de Google Gemini) agarramos esta red neuronal, la comprimimos a 8 bits aplicando una técnica llamada quantización y escribimos un motor de álgebra lineal en C++ para hacerla correr dentro del Arduino.
¡Nos vemos en la siguiente entrega!

Top comments (0)