Open In Colab

Exercise: RNN to predict SST

Download a monthly sea surface temperature (SST) time series for the Mediterranean Sea from surftemp.net in CSV format. Choose a geographic area of your choice.

Once the data is loaded:

  • Visualise the series and identify the trend and seasonality.

  • Prepare the data using a sliding window and sequential train/test split.

  • Train an RNN with PyTorch to predict the SST of the following month.

  • Evaluate the model with MAE, RMSE, and MAPE and visualise the predictions.

Download Sample data

[ ]:
import torch
import torch.nn as nn
import pandas as pd
import numpy as np


from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_absolute_error

import matplotlib.pyplot as plt

En primer lloc carregarem les dades. Com sempre que llegim un CSV: read_csv

[ ]:
df = pd.read_csv("PATH/TO/FILE")  # Segurament necessitem un poc d'ajuda del paràmetre header
print(df.columns)
print(df.shape)

A continuació agafarem les característiques que ens interessen per inferir la temperatura. Descartarem la característica mean temperature kelvin (seria fer trampes) i fraction of sea-ice-covered ocean ja que el conjunt de dades és afagat del mar mediterrani.

La variable objectiu serà mean temperature deg C.

[ ]:
features = #TODO seleccionar features
target = #TODO selecionar variable objectiu

Escalarem les variables al rang [0,1], ja que això ajuda a la xarxa.

Tindrem dos Scalers independents, un per les característiques i un per la variable objectiu, això ens permetrà simplificar el procés de donar resultat (truco de programador vell).

[ ]:
#Scalers independents
scaler_features = MinMaxScaler()
scaler_target   = MinMaxScaler()

features_scaled = # TODO
target_scaled   = # TODO

Ara ja podem dividir les dades entre entrenament i test. Hem d’aconseguir 4 variables:

  • features_train i features_test

  • target_train i features_train

NOTA: Estem emprant sèries temporals, això implica que la partició ha de seguir el principi de seqüèncialitat:

[ ]:
n_train = int(df.shape[0]*0.8) #nombre de mostres al conjunt d'entrenament.

features_train = #TODO
features_test = #TODO

target_train = #TODO
target_test = #TODO

# Comprovem que el que hem fet és correcte
print(n_train)
print(features_train.shape, target_train.shape)
print(features_test.shape, target_test.shape)

El següent codi és una mica més complex; Ens permet generar petites seqüències de mida WINDOW_SIZE que permetran entrenar la nostra RNN. Si comparam la funció amb la de la part teòrica ha canviat una mica ja que ara la variable objectiu és diferent de les característiques que empram per entrenar.

L’objectiu és que entenguem el que fa el codi:

[ ]:
def create_sequences(features, target, window_size):
    X, y = [], []
    for i in range(window_size, len(features)):
        X.append(features[i-window_size:i])
        y.append(target[i])
    return np.array(X), np.array(y)

WINDOW_SIZE = 12

X_train, y_train = create_sequences(features_train, target_train,  WINDOW_SIZE)

X_test, y_test  = create_sequences(np.concatenate([features_train[-WINDOW_SIZE:], features_test]),
                                   np.concatenate([target_train[-WINDOW_SIZE:], target_test]),
                                   WINDOW_SIZE)

print(X_train.shape)
print(y_train.shape)

La darrera passa és transformar-ho en un tensor, ja que és l’estructura de dades que empra Pytorch

[ ]:

Creació del model

Ara toca crear el model, en problemes «senzills» no cal complicar-se massa en pensar l’arquitectura, podem reutilitzar la mateixa del bloc de teoria:

[ ]:
class RNNModel(nn.Module):
    def __init__(self, input_size=¿?, hidden_size=32, num_layers=1):
        super(RNNModel, self).__init__()
        self.rnn = nn.RNN(
            input_size=input_size,
            hidden_size=hidden_size,
            num_layers=num_layers,
            batch_first=True
        )
        self.fc = nn.Linear(hidden_size, 1)

    def forward(self, x):
        out, _ = self.rnn(x)
        out = self.fc(out[:, -1, :])  # agafem l'últim pas temporal i el processam per una capa tipus MLP
        return out.squeeze()

El bloc d’entrenament també és bastant genèric. Hi ha una millora que es pot fer que consisteix a afegir una passa d’avaluació al final de cada iteració, així mentre s’entrena es pot tenir una estimació de com de bé la xarxa treballa amb dades noves.

EXTRA: Afegir una avaluació després de cada cicle d’entrenament.

[ ]:
model = RNNModel() # És molt important crear el model en la mateixa cel·la que entrenem. En saps el motiu?

criterion = nn.MSELoss() # En aquest cas la funció de pèrdua és de regressió
optimizer = torch.optim.Adam(model.parameters(), lr=¿?)

epochs = ¿?
for epoch in range(epochs):
    model.train()
    y_pred = model(X_train)
    loss = criterion(y_pred, y_train)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    if (epoch + 1) % 50 == 0:
        print(f"Epoch {epoch+1}/{epochs} - Loss: {loss.item():.6f}")

Finallment ens queda avaluar els resultats. Això implica realitzar les següents passes_:

  • Fer una predicció del conjunt de test.

  • Desfer la passa de normalització tant del conjunt de test com de la predicció de la xarxa emprant el mètode inverse_transform de la classe MinMaxScaler.

  • Calcular les mètriques MAE, RMSE i MAPE.

[ ]:
# Avaluació del conjunt de test



[ ]:
# Desnormalització


[ ]:
# Càlcul de mètriques


A més de mostrar les mètriques, sempre està bé fer un gràfic que mostra com s’ajusten les prediccions al conjunt de test:

[ ]:
# Construcció de l'eix temporal a partir de les columnes year, month, day
dates = pd.to_datetime(df[['year', 'month', 'day']])

plt.figure(figsize=(12, 4))
plt.plot(dates[n_train:], ¿?, label='Actual SST', color='steelblue')
plt.plot(dates[n_train:], ¿?, label='Predicted SST', color='tomato', linestyle='--')
plt.xlabel('Data')
plt.ylabel('Temperatura (°C)')
plt.title('Prediccions vs Valors Reals')
plt.legend()
plt.tight_layout()
plt.show()