Previsão de Demanda em Mobilidade Urbana com RNNs
Vamos prever quantas bicicletas serão alugadas na próxima hora usando três redes com "memória": SimpleRNN, LSTM e GRU. Entenda por que séries temporais precisam de arquiteturas especiais.
Por que RNN para séries temporais?
Imagina que você quer prever o movimento na ciclovia às 17h. Você precisaria saber: é segunda ou sexta? Está chovendo? Mas também: como esteve o movimento nas últimas 24 horas?
Uma rede MLP analisa cada ponto de forma isolada. Uma RNN tem memória — ela leva em conta o contexto das horas anteriores. É como se dissesse: "as últimas 24h tiveram esse padrão, então a próxima hora provavelmente será assim."
Vamos comparar 3 arquiteturas recorrentes para o mesmo problema e medir qual performa melhor.
O que você vai precisar
- Python 3.10+
- TensorFlow / Keras — para construir as RNNs.
- Pandas / NumPy — para manipular a série temporal.
- Scikit-learn — para normalização (MinMaxScaler) e métricas.
- Matplotlib — para o gráfico real vs. previsto.
Dataset: Bike Sharing Dataset (UCI) — hour.csv com 17.379 registros horários do Capital Bikeshare, Washington D.C. (2011–2012).
Configurar e carregar os dados
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
np.random.seed(42)
tf.random.set_seed(42)
# Hiperparâmetros — centralizados para fácil ajuste
WINDOW_SIZE = 24 # horas do passado que o modelo vai "ver"
FEATURES = ['hr', 'temp', 'hum', 'windspeed',
'season', 'weekday', 'workingday', 'weathersit', 'cnt']
TARGET_COL = 'cnt'
TARGET_IDX = FEATURES.index(TARGET_COL) # posição de cnt na lista
N_FEATURES = len(FEATURES) # 9 variáveis
UNITS = 64 # neurônios na camada recorrente
EPOCHS = 30
BATCH_SIZE = 64
PATIENCE = 5 # épocas sem melhoria antes do early stopping
df = pd.read_csv('dataset/hour.csv', parse_dates=['dteday'])
print(f'Shape: {df.shape[0]} linhas x {df.shape[1]} colunas')
print(f'Periodo: {df["dteday"].min().date()} -> {df["dteday"].max().date()}')
# Shape: 17379 linhas x 17 colunas
# Periodo: 2011-01-01 -> 2012-12-31
O que descobrimos? A série cobre 2 anos completos (17.379 horas). WINDOW_SIZE=24 significa que para prever as 17h, o modelo vai olhar de 17h do dia anterior até as 16h de hoje.
Normalizar e criar janelas deslizantes
Redes neurais funcionam melhor com valores entre 0 e 1. E precisamos transformar a série em pares (janela de 24h → próximo valor).
dados_brutos = df[FEATURES].values.astype(np.float32)
# Dividir cronologicamente — não misturar futuro com passado!
n_janelas = len(dados_brutos) - WINDOW_SIZE
n_treino = int(n_janelas * 0.80)
indice_corte = WINDOW_SIZE + n_treino
# Ajusta o scaler APENAS no treino (evita data leakage — ver o futuro)
scaler = MinMaxScaler(feature_range=(0, 1))
scaler.fit(dados_brutos[:indice_corte])
dados_norm = scaler.transform(dados_brutos)
def criar_janelas(dados, tamanho_janela, indice_alvo):
"""
Janela deslizante:
X = últimas `tamanho_janela` horas (shape: n, 24, 9)
y = próximo valor de cnt (shape: n,)
"""
X_list, y_list = [], []
for i in range(len(dados) - tamanho_janela):
X_list.append(dados[i : i + tamanho_janela])
y_list.append(dados[i + tamanho_janela, indice_alvo])
return np.array(X_list), np.array(y_list)
X_all, y_all = criar_janelas(dados_norm, WINDOW_SIZE, TARGET_IDX)
X_train, X_test = X_all[:n_treino], X_all[n_treino:]
y_train, y_test = y_all[:n_treino], y_all[n_treino:]
print(f'X_train: {X_train.shape}') # (n, 24, 9)
O que descobrimos? Cada amostra de X tem shape (24, 9) — 24 horas, 9 variáveis. A divisão cronológica é fundamental: não podemos usar dados do futuro para treinar.
Construir os três modelos recorrentes
def construir_modelo(camada_rnn, unidades, n_features):
return keras.Sequential([
keras.Input(shape=(WINDOW_SIZE, n_features)),
camada_rnn(unidades, return_sequences=False),
layers.Dense(32, activation='relu'),
layers.Dense(1) # saída: 1 valor (cnt da próxima hora)
])
# SimpleRNN: recorrência básica
# Problema: gradientes somem em sequências longas (vanishing gradient)
modelo_srnn = construir_modelo(layers.SimpleRNN, UNITS, N_FEATURES)
# LSTM: 3 portas — entrada, saída e esquecimento
# Permite lembrar padrões de longo prazo (ex: padrão semanal)
modelo_lstm = construir_modelo(layers.LSTM, UNITS, N_FEATURES)
# GRU: 2 portas — mais simples que LSTM, mas igualmente eficaz na prática
modelo_gru = construir_modelo(layers.GRU, UNITS, N_FEATURES)
early_stop = keras.callbacks.EarlyStopping(
monitor='val_loss', patience=PATIENCE, restore_best_weights=True
)
def treinar(modelo, nome):
modelo.compile(optimizer='adam', loss='mse', metrics=['mae'])
hist = modelo.fit(
X_train, y_train,
epochs=EPOCHS,
batch_size=BATCH_SIZE,
validation_split=0.15,
callbacks=[early_stop],
verbose=0
)
print(f'{nome}: {len(hist.history["loss"])} épocas')
return hist
hist_srnn = treinar(modelo_srnn, 'SimpleRNN')
hist_lstm = treinar(modelo_lstm, 'LSTM')
hist_gru = treinar(modelo_gru, 'GRU')
O que descobrimos? LSTM e GRU convergiram mais rápido. As "portas" de memória evitam o problema de gradiente que some no SimpleRNN — o modelo consegue lembrar padrões de dias atrás.
Avaliar e comparar
def avaliar(modelo, nome, X_test, y_test):
y_pred_norm = modelo.predict(X_test, verbose=0).flatten()
# Desnormalizamos: voltamos para escala real (número de bicicletas)
dummy = np.zeros((len(y_pred_norm), N_FEATURES))
dummy[:, TARGET_IDX] = y_pred_norm
y_pred_real = scaler.inverse_transform(dummy)[:, TARGET_IDX]
dummy[:, TARGET_IDX] = y_test
y_true_real = scaler.inverse_transform(dummy)[:, TARGET_IDX]
mae = mean_absolute_error(y_true_real, y_pred_real)
rmse = np.sqrt(mean_squared_error(y_true_real, y_pred_real))
r2 = r2_score(y_true_real, y_pred_real)
print(f'{nome:10} → MAE={mae:.1f} RMSE={rmse:.1f} R²={r2:.3f}')
return y_true_real, y_pred_real
y_true, y_srnn = avaliar(modelo_srnn, 'SimpleRNN', X_test, y_test)
_, y_lstm = avaliar(modelo_lstm, 'LSTM', X_test, y_test)
_, y_gru = avaliar(modelo_gru, 'GRU', X_test, y_test)
# Gráfico: Real vs. Previsto para os 3 modelos
n_plot = 200 # primeiras 200 horas do teste
fig, axes = plt.subplots(3, 1, figsize=(14, 10))
for ax, y_pred, nome in zip(axes,
[y_srnn, y_lstm, y_gru],
['SimpleRNN', 'LSTM', 'GRU']):
ax.plot(y_true[:n_plot], label='Real', color='steelblue')
ax.plot(y_pred[:n_plot], label=nome, color='tomato', alpha=0.8)
ax.legend()
ax.set_title(f'Real vs. Previsto — {nome}')
plt.tight_layout()
plt.savefig('real_vs_previsto_todos.png', dpi=150)
plt.show()
O que descobrimos? LSTM e GRU tiveram MAE e RMSE menores que o SimpleRNN. O GRU chegou a desempenho similar ao LSTM com menos parâmetros e treino mais rápido — um trade-off relevante para produção.
Código completo e referências
github.com/Jair-pc/puc-minas-arquiteturas_de_deep_learning
Dataset: Bike Sharing Dataset (UCI Machine Learning Repository) — Capital Bikeshare, Washington D.C.