Initial Commit

This commit is contained in:
Victor Mylle
2023-11-07 18:00:20 +00:00
commit 56c763a6f4
41 changed files with 358954 additions and 0 deletions

2
src/data/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
from .dataset import NrvDataset
from .preprocessing import DataProcessor

15
src/data/dataset.py Normal file
View File

@@ -0,0 +1,15 @@
import torch
from torch.utils.data import Dataset, DataLoader
import pandas as pd
class NrvDataset(Dataset):
def __init__(self, dataframe, sequence_length=96, predict_sequence_length=96):
self.data = torch.tensor(dataframe['nrv'].to_numpy(), dtype=torch.float32)
self.sequence_length = sequence_length
self.predict_sequence_length = predict_sequence_length
def __len__(self):
return len(self.data) - self.sequence_length - self.predict_sequence_length
def __getitem__(self, idx):
return self.data[idx:idx+self.sequence_length], self.data[idx+self.sequence_length:idx+self.sequence_length+self.predict_sequence_length]

116
src/data/preprocessing.py Normal file
View File

@@ -0,0 +1,116 @@
import pandas as pd
import numpy as np
from sklearn.preprocessing import MinMaxScaler
import torch
from data.dataset import NrvDataset
from datetime import datetime
import pytz
history_data_path = "../../data/history-quarter-hour-data.csv"
class DataProcessor:
def __init__(self):
self.batch_size = 2048
self.train_range = (-np.inf, datetime(year=2022, month=11, day=30, tzinfo=pytz.UTC))
self.test_range = (datetime(year=2023, month=1, day=1, tzinfo=pytz.UTC), np.inf)
self.update_range_str()
self.features = ['nrv']
self.nrv_df = self.get_nrv_history()
self.nrv_scaler = MinMaxScaler(feature_range=(-1, 1))
def set_train_range(self, train_range: tuple):
self.train_range = train_range
self.update_range_str()
def set_test_range(self, test_range: tuple):
self.test_range = test_range
self.update_range_str()
def update_range_str(self):
self.train_range_start = str(self.train_range[0]) if self.train_range[0] != -np.inf else "-inf"
self.train_range_end = str(self.train_range[1]) if self.train_range[1] != np.inf else "inf"
self.test_range_start = str(self.test_range[0]) if self.test_range[0] != -np.inf else "-inf"
self.test_range_end = str(self.test_range[1]) if self.test_range[1] != np.inf else "inf"
def get_nrv_history(self):
df = pd.read_csv(history_data_path, delimiter=';')
df = df[['datetime', 'netregulationvolume']]
df = df.rename(columns={'netregulationvolume': 'nrv'})
df['datetime'] = pd.to_datetime(df['datetime'])
counts = df['datetime'].dt.date.value_counts().sort_index()
df = df[df['datetime'].dt.date.isin(counts[counts == 96].index)]
df.sort_values(by="datetime", inplace=True)
return df
def set_batch_size(self, batch_size: int):
self.batch_size = batch_size
def get_dataloader(self, dataset, shuffle: bool = True):
return torch.utils.data.DataLoader(dataset, batch_size=self.batch_size, shuffle=shuffle, num_workers=4)
def get_train_dataloader(self, transform: bool = True, predict_sequence_length: int = 96):
train_df = self.nrv_df.copy()
if self.train_range[0] != -np.inf:
train_df = train_df[(train_df['datetime'] >= self.train_range[0])]
if self.train_range[1] != np.inf:
train_df = train_df[(train_df['datetime'] <= self.train_range[1])]
if transform:
train_df['nrv'] = self.nrv_scaler.fit_transform(train_df['nrv'].values.reshape(-1, 1)).reshape(-1)
train_dataset = NrvDataset(train_df, predict_sequence_length=predict_sequence_length)
return self.get_dataloader(train_dataset)
def get_test_dataloader(self, transform: bool = True, predict_sequence_length: int = 96):
test_df = self.nrv_df.copy()
if self.test_range[0] != -np.inf:
test_df = test_df[(test_df['datetime'] >= self.test_range[0])]
if self.test_range[1] != np.inf:
test_df = test_df[(test_df['datetime'] <= self.test_range[1])]
if transform:
test_df['nrv'] = self.nrv_scaler.transform(test_df['nrv'].values.reshape(-1, 1)).reshape(-1)
test_dataset = NrvDataset(test_df, predict_sequence_length=predict_sequence_length)
return self.get_dataloader(test_dataset, shuffle=False)
def get_dataloaders(self, transform: bool = True, predict_sequence_length: int = 96):
return self.get_train_dataloader(transform=transform, predict_sequence_length=predict_sequence_length), self.get_test_dataloader(transform=transform, predict_sequence_length=predict_sequence_length)
def get_random_day(self, train: bool = True, transform: bool = True):
df = self.nrv_df.copy()
range = self.train_range if train else self.test_range
if range[0] != -np.inf:
df = df[(df['datetime'] >= range[0])]
if range[1] != np.inf:
df = df[(df['datetime'] <= range[1])]
if transform:
df['nrv'] = self.nrv_scaler.transform(df['nrv'].values.reshape(-1, 1)).reshape(-1)
data_tensor = torch.tensor(df[self.features].values, dtype=torch.float32)
random_start_idx = np.random.randint(0, len(df) - 191)
current_day_features = data_tensor[random_start_idx:random_start_idx+96]
next_day_features = data_tensor[random_start_idx+96:random_start_idx+192]
return (current_day_features, next_day_features)
def inverse_transform(self, tensor: torch.Tensor):
return self.nrv_scaler.inverse_transform(tensor.cpu().numpy()).reshape(-1)

1
src/losses/__init__.py Normal file
View File

@@ -0,0 +1 @@
from .pinball_loss import PinballLoss

View File

@@ -0,0 +1,33 @@
import torch
from torch import nn
class PinballLoss(nn.Module):
"""
Calculates the quantile loss function.
Attributes
----------
self.pred : torch.tensor
Predictions.
self.target : torch.tensor
Target to predict.
self.quantiles : torch.tensor
"""
def __init__(self, quantiles):
super(PinballLoss, self).__init__()
self.quantiles_tensor = quantiles
self.quantiles = quantiles.tolist()
def forward(self, pred, target):
"""
Computes the loss for the given prediction.
"""
error = target - pred
upper = self.quantiles_tensor * error
lower = (self.quantiles_tensor - 1) * error
losses = torch.max(lower, upper)
loss = torch.mean(torch.sum(losses, dim=1))
return loss

3
src/models/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from .linear_regression import LinearRegression
from .complex_model import TimeSeriesModel
from .non_linear_regression import NonLinearRegression

View File

@@ -0,0 +1,29 @@
import torch
import torch.nn as nn
class TimeSeriesModel(nn.Module):
def __init__(self, input_size, output_size, num_layers=5, hidden_size=128, dropout_rate=0.3):
super(TimeSeriesModel, self).__init__()
self.output_size = output_size
layers = []
for i in range(num_layers):
if i == 0:
layers.append(nn.Linear(input_size, hidden_size))
else:
layers.append(nn.Linear(hidden_size, hidden_size))
layers.append(nn.BatchNorm1d(hidden_size))
layers.append(nn.Dropout(dropout_rate))
layers.append(nn.ReLU())
self.layers = nn.ModuleList(layers)
self.output = nn.Linear(hidden_size, output_size)
def forward(self, x):
x = torch.squeeze(x, -1)
for layer in self.layers:
x = layer(x)
x = self.output(x)
return x

View File

@@ -0,0 +1,14 @@
import torch
class LinearRegression(torch.nn.Module):
def __init__(self, inputSize, output_size):
super(LinearRegression, self).__init__()
self.inputSize = inputSize
self.output_size = output_size
self.linear = torch.nn.Linear(inputSize, output_size)
def forward(self, x):
x = torch.squeeze(x, -1)
out = self.linear(x)
return out

View File

@@ -0,0 +1,31 @@
import torch
class NonLinearRegression(torch.nn.Module):
def __init__(self, inputSize, output_size, hiddenSize=128, numLayers=2):
super(NonLinearRegression, self).__init__()
self.inputSize = inputSize
self.output_size = output_size
self.hiddenSize = hiddenSize
self.numLayers = numLayers
# add linear layers with relu
self.layers = torch.nn.ModuleList()
self.layers.append(torch.nn.Linear(inputSize, hiddenSize))
for _ in range(numLayers - 2):
self.layers.append(torch.nn.Linear(hiddenSize, hiddenSize))
self.layers.append(torch.nn.Linear(hiddenSize, output_size))
self.relu = torch.nn.ReLU()
def forward(self, x):
x = torch.squeeze(x, -1)
for layer in self.layers[:-1]:
x = self.relu(layer(x))
out = self.layers[-1](x)
return out

BIN
src/notebooks/checkpoint.pt Normal file

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,230 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"sys.path.append('..')\n",
"from data import DataProcessor\n",
"from trainers.quantile_trainer import QuantileTrainer\n",
"from trainers.autoregressive_trainer import AutoRegressiveTrainer\n",
"from trainers.trainer import Trainer\n",
"from utils.clearml import ClearMLHelper\n",
"from models import *\n",
"from losses import *\n",
"import torch\n",
"import numpy as np\n",
"from torch.nn import MSELoss, L1Loss\n",
"from datetime import datetime\n",
"import pytz\n",
"import torch.nn as nn\n",
"\n",
"# auto reload\n",
"%load_ext autoreload\n",
"%autoreload 2"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Non-AutoRegressive"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"InsecureRequestWarning: Certificate verification is disabled! Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"ClearML Task: created new task id=909da25a8d214f75ab3858506ae615e8\n",
"2023-11-07 16:29:35,665 - clearml.Task - INFO - Storing jupyter notebook directly as code\n",
"ClearML results page: http://192.168.1.182:8080/projects/2e46d4af6f1e4c399cf9f5aa30bc8795/experiments/909da25a8d214f75ab3858506ae615e8/output/log\n",
"2023-11-07 16:30:08,121 - clearml.model - WARNING - 500 model found when searching for `file:///workspaces/Thesis/src/notebooks/checkpoint.pt`\n",
"2023-11-07 16:30:08,123 - clearml.model - WARNING - Selected model `Quantile Regression - Linear` (id=bc0cb0d7fc614e2e8b0edf5b85348646)\n",
"2023-11-07 16:30:08,130 - clearml.frameworks - INFO - Found existing registered model id=bc0cb0d7fc614e2e8b0edf5b85348646 [/workspaces/Thesis/src/notebooks/checkpoint.pt] reusing it.\n",
"2023-11-07 16:30:08,677 - clearml.Task - INFO - Completed model upload to http://192.168.1.182:8081/Thesis/NrvForecast/Non-AutoRegressive%20-%20Non%20Linear%20%283%20hidden%20layers%20-%201024%20units%29.909da25a8d214f75ab3858506ae615e8/models/checkpoint.pt\n",
"2023-11-07 16:30:10,302 - clearml.Task - INFO - Completed model upload to http://192.168.1.182:8081/Thesis/NrvForecast/Non-AutoRegressive%20-%20Non%20Linear%20%283%20hidden%20layers%20-%201024%20units%29.909da25a8d214f75ab3858506ae615e8/models/checkpoint.pt\n",
"Early stopping triggered\n"
]
}
],
"source": [
"#### Hyperparameters ####\n",
"inputDim = 96\n",
"learningRate = 0.0003\n",
"epochs = 50\n",
"\n",
"# model = LinearRegression(inputDim, 96)\n",
"model = NonLinearRegression(inputDim, 96, hiddenSize=1024, numLayers=5)\n",
"optimizer = torch.optim.Adam(model.parameters(), lr=learningRate)\n",
"\n",
"#### Data Processor ####\n",
"data_processor = DataProcessor()\n",
"data_processor.set_batch_size(1024)\n",
"\n",
"\n",
"data_processor.set_train_range((datetime(year=2015, month=1, day=1, tzinfo=pytz.UTC), datetime(year=2022, month=11, day=30, tzinfo=pytz.UTC)))\n",
"data_processor.set_test_range((datetime(year=2023, month=1, day=1, tzinfo=pytz.UTC), np.inf))\n",
"\n",
"#### ClearML ####\n",
"clearml_helper = ClearMLHelper(project_name=\"Thesis/NrvForecast\")\n",
"\n",
"#### Trainer ####\n",
"trainer = Trainer(model, optimizer, nn.MSELoss(), data_processor, \"cuda\", debug=False, clearml_helper=clearml_helper)\n",
"trainer.add_metrics_to_track([MSELoss(), L1Loss()])\n",
"trainer.plot_every(10)\n",
"trainer.early_stopping(patience=10)\n",
"trainer.train(epochs=epochs)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# AutoRegressive Simple Linear"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"ClearML Task: created new task id=6467cef37fdc408d95b89f0dca0e26dd\n",
"ClearML results page: http://192.168.1.182:8080/projects/2e46d4af6f1e4c399cf9f5aa30bc8795/experiments/6467cef37fdc408d95b89f0dca0e26dd/output/log\n",
"Early stopping triggered\n"
]
}
],
"source": [
"#### Hyperparameters ####\n",
"inputDim = 96\n",
"learningRate = 0.0003\n",
"epochs = 50\n",
"\n",
"# model = LinearRegression(inputDim, 1)\n",
"model = NonLinearRegression(inputDim, 1, hiddenSize=1024, numLayers=5)\n",
"optimizer = torch.optim.Adam(model.parameters(), lr=learningRate)\n",
"\n",
"#### Data Processor ####\n",
"data_processor = DataProcessor()\n",
"data_processor.set_batch_size(1024)\n",
"\n",
"\n",
"data_processor.set_train_range((datetime(year=2015, month=1, day=1, tzinfo=pytz.UTC), datetime(year=2022, month=11, day=30, tzinfo=pytz.UTC)))\n",
"data_processor.set_test_range((datetime(year=2023, month=1, day=1, tzinfo=pytz.UTC), np.inf))\n",
"\n",
"#### ClearML ####\n",
"clearml_helper = ClearMLHelper(project_name=\"Thesis/NrvForecast\")\n",
"\n",
"#### Trainer ####\n",
"trainer = AutoRegressiveTrainer(model, optimizer, nn.MSELoss(), data_processor, \"cuda\", debug=False, clearml_helper=clearml_helper)\n",
"trainer.add_metrics_to_track([MSELoss(), L1Loss()])\n",
"trainer.plot_every(10)\n",
"trainer.early_stopping(patience=10)\n",
"trainer.train(epochs=epochs)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Quantile Regression"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"/workspaces/Thesis/src/notebooks/../trainers/quantile_trainer.py:16: UserWarning:\n",
"\n",
"To copy construct from a tensor, it is recommended to use sourceTensor.clone().detach() or sourceTensor.clone().detach().requires_grad_(True), rather than torch.tensor(sourceTensor).\n",
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"ClearML Task: created new task id=07a2dc72793446d8a8101eafce0d80db\n",
"ClearML results page: http://192.168.1.182:8080/projects/2e46d4af6f1e4c399cf9f5aa30bc8795/experiments/07a2dc72793446d8a8101eafce0d80db/output/log\n",
"Early stopping triggered\n"
]
}
],
"source": [
"#### Hyperparameters ####\n",
"inputDim = 96\n",
"learningRate = 0.0003\n",
"epochs = 50\n",
"\n",
"quantiles = torch.tensor([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]).to(\"cuda\")\n",
"\n",
"# model = LinearRegression(inputDim, len(quantiles))\n",
"model = NonLinearRegression(inputDim, len(quantiles), hiddenSize=1024, numLayers=5)\n",
"model.output_size = 1\n",
"optimizer = torch.optim.Adam(model.parameters(), lr=learningRate)\n",
"\n",
"#### Data Processor ####\n",
"data_processor = DataProcessor()\n",
"data_processor.set_batch_size(1024)\n",
"\n",
"data_processor.set_train_range((-np.inf, datetime(year=2022, month=11, day=30, tzinfo=pytz.UTC)))\n",
"data_processor.set_test_range((datetime(year=2023, month=1, day=1, tzinfo=pytz.UTC), np.inf))\n",
"\n",
"#### ClearML ####\n",
"clearml_helper = ClearMLHelper(project_name=\"Thesis/NrvForecast\")\n",
"\n",
"#### Trainer ####\n",
"trainer = QuantileTrainer(model, optimizer, data_processor, quantiles, \"cuda\", debug=True, clearml_helper=clearml_helper)\n",
"trainer.add_metrics_to_track([PinballLoss(quantiles), MSELoss(), L1Loss()])\n",
"trainer.early_stopping(patience=10)\n",
"trainer.plot_every(10)\n",
"trainer.train(epochs=epochs)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "base",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.11"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@@ -0,0 +1,59 @@
from clearml import OutputModel
import torch
from data.preprocessing import DataProcessor
from utils.clearml import ClearMLHelper
from utils.autoregressive import predict_auto_regressive
import plotly.graph_objects as go
import numpy as np
import plotly.subplots as sp
from plotly.subplots import make_subplots
from trainers.trainer import Trainer
class AutoRegressiveTrainer(Trainer):
def debug_plots(self, task, train: bool, samples, epoch):
X, y = samples
X = X.to(self.device)
num_samples = len(X)
rows = num_samples # One row per sample since we only want one column
cols = 1
fig = make_subplots(rows=rows, cols=cols, subplot_titles=[f'Sample {i+1}' for i in range(num_samples)])
for i, (current_day, next_day) in enumerate(zip(X, y)):
predictions = self.predict_auto_regressive(current_day)
sub_fig = self.get_plot(current_day, next_day, predictions, show_legend=(i == 0))
row = i + 1
col = 1
for trace in sub_fig.data:
fig.add_trace(trace, row=row, col=col)
loss = self.criterion(predictions.to(self.device), next_day.to(self.device)).item()
fig['layout']['annotations'][i].update(text=f"{loss.__class__.__name__}: {loss:.6f}")
# y axis same for all plots
fig.update_yaxes(range=[-1, 1], col=1)
fig.update_layout(height=300 * rows)
task.get_logger().report_plotly(
title=f"{'Training' if train else 'Test'} Samples",
series="full_day",
iteration=epoch,
figure=fig
)
def predict_auto_regressive(self, initial_sequence: torch.Tensor, sequence_length: int = 96):
initial_sequence = initial_sequence.to(self.device)
return predict_auto_regressive(self.model, initial_sequence, sequence_length)
def random_day_prediction(self):
current_day_features, next_day_features = self.data_processor.get_random_test_day()
predictions = self.predict_auto_regressive(current_day_features)
return current_day_features, next_day_features, predictions

View File

@@ -0,0 +1,102 @@
import torch
from utils.autoregressive import predict_auto_regressive_quantile
from scipy.interpolate import interp1d
from trainers.trainer import Trainer
from trainers.autoregressive_trainer import AutoRegressiveTrainer
from data.preprocessing import DataProcessor
from utils.clearml import ClearMLHelper
from losses import PinballLoss
from plotly.subplots import make_subplots
import plotly.graph_objects as go
import numpy as np
class QuantileTrainer(AutoRegressiveTrainer):
def __init__(self, model: torch.nn.Module, optimizer: torch.optim.Optimizer, data_processor: DataProcessor, quantiles: list, device: torch.device, clearml_helper: ClearMLHelper = None, debug: bool = True):
quantiles_tensor = torch.tensor(quantiles)
quantiles_tensor = quantiles_tensor.to(device)
self.quantiles = quantiles
criterion = PinballLoss(quantiles=quantiles_tensor)
super().__init__(model=model, optimizer=optimizer, criterion=criterion, data_processor=data_processor, device=device, clearml_helper=clearml_helper, debug=debug)
def predict_auto_regressive(self, initial_sequence: torch.Tensor, sequence_length: int = 96):
initial_sequence = initial_sequence.to(self.device)
return predict_auto_regressive_quantile(self.model, self.sample_from_dist, initial_sequence, self.quantiles, sequence_length)
def log_final_metrics(self, task, dataloader, train: bool = True):
metrics = { metric.__class__.__name__: 0.0 for metric in self.metrics_to_track }
transformed_metrics = { metric.__class__.__name__: 0.0 for metric in self.metrics_to_track }
with torch.no_grad():
for inputs, targets in dataloader:
inputs, targets = inputs.to(self.device), targets
outputs = self.model(inputs)
samples = []
for output in outputs:
samples.append(self.sample_from_dist(self.quantiles.cpu().numpy(), output.cpu().numpy()))
samples = torch.tensor(samples).to(self.device).reshape(-1, 1)
inversed_samples = torch.tensor(self.data_processor.inverse_transform(samples))
inversed_targets = torch.tensor(self.data_processor.inverse_transform(targets.reshape(-1, 1)))
for metric in self.metrics_to_track:
if metric.__class__ != PinballLoss:
transformed_metrics[metric.__class__.__name__] += metric(samples, targets.view(-1, 1).to(self.device))
metrics[metric.__class__.__name__] += metric(inversed_samples, inversed_targets)
else:
transformed_metrics[metric.__class__.__name__] += metric(outputs, targets.view(-1, 1).to(self.device))
for metric in self.metrics_to_track:
metrics[metric.__class__.__name__] /= len(dataloader)
transformed_metrics[metric.__class__.__name__] /= len(dataloader)
for metric_name, metric_value in metrics.items():
if PinballLoss.__name__ in metric_name:
continue
name = f'train_{metric_name}' if train else f'test_{metric_name}'
task.get_logger().report_single_value(name=name, value=metric_value)
for metric_name, metric_value in transformed_metrics.items():
name = f'train_transformed_{metric_name}' if train else f'test_transformed_{metric_name}'
task.get_logger().report_single_value(name=name, value=metric_value)
def get_plot(self, current_day, next_day, predictions, show_legend: bool = True):
fig = go.Figure()
# Convert to numpy for plotting
current_day_np = current_day.view(-1).cpu().numpy()
next_day_np = next_day.view(-1).cpu().numpy()
predictions_np = predictions.cpu().numpy()
# Add traces for current and next day
fig.add_trace(go.Scatter(x=np.arange(96), y=current_day_np, name="Current Day"))
fig.add_trace(go.Scatter(x=96 + np.arange(96), y=next_day_np, name="Next Day"))
for i, q in enumerate(self.quantiles):
fig.add_trace(go.Scatter(x=96 + np.arange(96), y=predictions_np[:, i],
name=f"Prediction (Q={q})", line=dict(dash='dash')))
# Update the layout
fig.update_layout(title="Predictions and Quantiles of the Linear Model", showlegend=show_legend)
return fig
@staticmethod
def sample_from_dist(quantiles, output_values):
# Interpolate the inverse CDF
inverse_cdf = interp1d(quantiles, output_values, kind='quadratic', bounds_error=False, fill_value="extrapolate")
# generate one random uniform number
uniform_random_numbers = np.random.uniform(0, 1, 1000)
# Apply the inverse CDF to the uniform random numbers
samples = inverse_cdf(uniform_random_numbers)
# Return the mean of the samples
return np.mean(samples)

301
src/trainers/trainer.py Normal file
View File

@@ -0,0 +1,301 @@
from clearml import OutputModel
import torch
from data.preprocessing import DataProcessor
from utils.clearml import ClearMLHelper
import plotly.graph_objects as go
import numpy as np
import plotly.subplots as sp
from plotly.subplots import make_subplots
class Trainer:
def __init__(self, model: torch.nn.Module, optimizer: torch.optim.Optimizer, criterion: torch.nn.Module, data_processor: DataProcessor, device: torch.device, clearml_helper: ClearMLHelper = None, debug: bool = True):
self.model = model
self.optimizer = optimizer
self.criterion = criterion
self.device = device
self.clearml_helper = clearml_helper
self.debug = debug
self.metrics_to_track = []
self.data_processor = data_processor
self.patience = None
self.delta = None
self.plot_every_n_epochs = 1
self.model.to(self.device)
def plot_every(self, n: int):
self.plot_every_n_epochs = n
def early_stopping(self, patience: int = 5, delta: float = 0.0):
self.patience = patience
self.delta = delta
def add_metrics_to_track(self, loss: torch.nn.Module | list[torch.nn.Module]):
if isinstance(loss, list):
self.metrics_to_track.extend(loss)
else:
self.metrics_to_track.append(loss)
def init_clearml_task(self):
if not self.clearml_helper:
return None
task_name = input("Enter a task name: ")
if task_name == "":
task_name = "Untitled Task"
task = self.clearml_helper.get_task(task_name=task_name)
if self.debug:
task.add_tags('Debug')
change_description = input("Enter a change description: ")
if change_description:
task.set_comment(change_description)
task.add_tags(self.model.__class__.__name__)
task.add_tags(self.criterion.__class__.__name__)
task.add_tags(self.optimizer.__class__.__name__)
task.add_tags(self.__class__.__name__)
self.optimizer.name = self.optimizer.__class__.__name__
self.criterion.name = self.criterion.__class__.__name__
task.connect(self.optimizer, name="optimizer")
task.connect(self.criterion, name="criterion")
task.connect(self.data_processor, name="data_processor")
return task
def random_samples(self, train: bool = True, num_samples: int = 10):
random_X = []
random_Y = []
for _ in range(num_samples):
X, y = self.data_processor.get_random_day(train=train)
random_X.append(X)
random_Y.append(y)
random_X = torch.stack(random_X)
random_Y = torch.stack(random_Y)
return random_X, random_Y
def train(self, epochs: int):
train_loader, test_loader = self.data_processor.get_dataloaders(predict_sequence_length=self.model.output_size)
train_random_X, train_random_y = self.random_samples(train=True)
test_random_X, test_random_y = self.random_samples(train=False)
task = self.init_clearml_task()
self.best_score = None
counter = 0
for epoch in range(1, epochs + 1):
self.model.train()
running_loss = 0.0
for inputs, targets in train_loader:
inputs, targets = inputs.to(self.device), targets.to(self.device)
self.optimizer.zero_grad()
output = self.model(inputs)
loss = self.criterion(output, targets)
loss.backward()
self.optimizer.step()
running_loss += loss.item()
running_loss /= len(train_loader.dataset)
test_loss = self.test(test_loader)
if self.patience is not None:
if self.best_score is None or test_loss < self.best_score + self.delta:
self.save_checkpoint(test_loss, task, epoch)
counter = 0
else:
counter += 1
if counter >= self.patience:
print('Early stopping triggered')
break
if task:
task.get_logger().report_scalar(title=self.criterion.__class__.__name__, series="train", value=running_loss, iteration=epoch)
task.get_logger().report_scalar(title=self.criterion.__class__.__name__, series="test", value=test_loss, iteration=epoch)
if epoch % self.plot_every_n_epochs == 0:
self.debug_plots(task, True, (train_random_X, train_random_y), epoch)
self.debug_plots(task, False, (test_random_X, test_random_y), epoch)
if task:
self.finish_training(task=task)
task.close()
def log_final_metrics(self, task, dataloader, train: bool = True):
metrics = { metric.__class__.__name__: 0.0 for metric in self.metrics_to_track }
transformed_metrics = { metric.__class__.__name__: 0.0 for metric in self.metrics_to_track }
with torch.no_grad():
for inputs, targets in dataloader:
inputs, targets = inputs.to(self.device), targets
outputs = self.model(inputs)
inversed_outputs = torch.tensor(self.data_processor.inverse_transform(outputs))
inversed_inputs = torch.tensor(self.data_processor.inverse_transform(targets))
for metric in self.metrics_to_track:
transformed_metrics[metric.__class__.__name__] += metric(outputs, targets.to(self.device))
metrics[metric.__class__.__name__] += metric(inversed_outputs, inversed_inputs)
for metric in self.metrics_to_track:
metrics[metric.__class__.__name__] /= len(dataloader)
transformed_metrics[metric.__class__.__name__] /= len(dataloader)
for metric_name, metric_value in metrics.items():
if train:
metric_name = f'train_{metric_name}'
else:
metric_name = f'test_{metric_name}'
task.get_logger().report_single_value(name=metric_name, value=metric_value)
for metric_name, metric_value in transformed_metrics.items():
if train:
metric_name = f'train_transformed_{metric_name}'
else:
metric_name = f'test_transformed_{metric_name}'
task.get_logger().report_single_value(name=metric_name, value=metric_value)
def finish_training(self, task):
if self.best_score is not None:
self.model.load_state_dict(torch.load('checkpoint.pt'))
self.model.eval()
transformed_train_loader, transformed_test_loader = self.data_processor.get_dataloaders(predict_sequence_length=self.model.output_size)
self.log_final_metrics(task, transformed_train_loader, train=True)
self.log_final_metrics(task, transformed_test_loader, train=False)
def test(self, test_loader: torch.utils.data.DataLoader):
self.model.eval()
test_loss = 0
with torch.no_grad():
for data, target in test_loader:
data, target = data.to(self.device), target.to(self.device)
output = self.model(data)
test_loss += self.criterion(output, target).item()
test_loss /= len(test_loader.dataset)
return test_loss
def save_checkpoint(self, val_loss, task, iteration: int):
torch.save(self.model.state_dict(), 'checkpoint.pt')
task.update_output_model(model_path='checkpoint.pt', iteration=iteration, auto_delete_file=False)
self.best_score = val_loss
def get_plot(self, current_day, next_day, predictions, show_legend: bool = True):
fig = go.Figure()
fig.add_trace(go.Scatter(x=np.arange(96), y=current_day.view(-1).cpu().numpy(), name="Current Day"))
fig.add_trace(go.Scatter(x=96 + np.arange(96), y=next_day.view(-1).cpu().numpy(), name="Next Day"))
fig.add_trace(go.Scatter(x=96 + np.arange(96), y=predictions.reshape(-1), name="Predictions"))
fig.update_layout(title="Predictions of the Linear Model")
return fig
def debug_plots(self, task, train: bool, samples, epoch):
X, y = samples
X = X.to(self.device)
num_samples = len(X)
rows = num_samples # One row per sample since we only want one column
cols = 1
fig = make_subplots(rows=rows, cols=cols, subplot_titles=[f'Sample {i+1}' for i in range(num_samples)])
for i, (current_day, next_day) in enumerate(zip(X, y)):
self.model.eval()
with torch.no_grad():
predictions = self.model(current_day).cpu()
sub_fig = self.get_plot(current_day, next_day, predictions, show_legend=(i == 0))
row = i + 1
col = 1
for trace in sub_fig.data:
fig.add_trace(trace, row=row, col=col)
loss = self.criterion(predictions.to(self.device), next_day.squeeze(-1).to(self.device)).item()
fig['layout']['annotations'][i].update(text=f"{loss.__class__.__name__}: {loss:.6f}")
# y axis same for all plots
fig.update_yaxes(range=[-1, 1], col=1)
fig.update_layout(height=300 * rows)
task.get_logger().report_plotly(
title=f"{'Training' if train else 'Test'} Samples",
series="full_day",
iteration=epoch,
figure=fig
)
def debug_scatter_plot(self, task, train: bool, samples, epoch):
X, y = samples
X = X.to(self.device)
y = y.to(self.device)
y = y[:, 0]
self.model.eval()
predictions = self.model(X)
num_samples = len(X)
rows = -(-num_samples // 2) # Ceiling division to handle odd number of samples
cols = 2
fig = make_subplots(rows=rows, cols=cols, subplot_titles=[f'Sample {i+1}' for i in range(num_samples)])
for i, (current_day, next_value, pred) in enumerate(zip(X, y, predictions)):
sub_fig = self.scatter_plot(current_day, pred, next_value)
row = (i // cols) + 1
col = (i % cols) + 1
for trace in sub_fig.data:
fig.add_trace(trace, row=row, col=col)
fig.update_layout(height=300 * rows)
task.get_logger().report_plotly(
title=f"{'Training' if train else 'Test'} Samples",
series="scatter",
iteration=epoch,
figure=fig
)
def scatter_plot(self, x, y, real_y):
fig = go.Figure()
# 96 values of x
fig.add_trace(go.Scatter(x=np.arange(96), y=x.view(-1).cpu().numpy(), name="Current Day"))
# add one value of y
fig.add_trace(go.Scatter(x=[96], y=[y.item()], name="Next Day"))
# add one value of real_y
fig.add_trace(go.Scatter(x=[96], y=[real_y.item()], name="Real Next Day"))
fig.update_layout(title="Predictions of the Linear Model")
return fig

0
src/utils/__init__.py Normal file
View File

View File

@@ -0,0 +1,60 @@
import torch
import numpy as np
def predict_auto_regressive(model: torch.nn.Module, features: torch.Tensor, sequence_length: int = 96):
"""
Predicts the next value in a sequence using an autoregressive approach.
Args:
- model: The trained PyTorch model.
- features: Initial sequence of data with length equal to sequence_length.
- sequence_length: The length of the sequence.
Returns:
- A tensor containing the predicted values.
"""
model.eval()
predictions = []
with torch.no_grad():
for _ in range(sequence_length):
output = model(features.unsqueeze(0))
predictions.append(output.item())
features = torch.cat((features[1:], output), dim=0)
return torch.tensor(predictions).unsqueeze(-1)
def predict_auto_regressive_quantile(model: torch.nn.Module, sample_from_dist, features: torch.Tensor, quantiles: torch.Tensor, sequence_length: int = 96):
"""
Predicts the next value in a sequence using an autoregressive approach.
Args:
- model: The trained PyTorch model.
- features: Initial sequence of data with length equal to sequence_length.
- sequence_length: The length of the sequence.
Returns:
- A tensor containing the predicted values.
"""
model.eval()
predictions = []
with torch.no_grad():
for _ in range(sequence_length):
output = model(features.unsqueeze(0))
predictions.append(output.squeeze(0))
sample = sample_from_dist(quantiles.cpu().numpy(), output.squeeze(0).cpu().numpy())
features = torch.cat((features[1:], torch.tensor(sample,).to("cuda").unsqueeze(0).unsqueeze(0)), dim=0)
features = features.float()
return torch.stack(predictions)

8
src/utils/cdf_pdf.py Normal file
View File

@@ -0,0 +1,8 @@
import numpy as np
import matplotlib.pyplot as plt
# Given lists of quantiles and their corresponding probabilities
quantiles = [-0.23013, -0.19831, -0.15217, -0.13654, -0.05726,
0.011687, 0.015129, 0.043187, 0.047704]
probs = [0.025, 0.05, 0.1, 0.15, 0.5, 0.85, 0.9, 0.95, 0.975]

10
src/utils/clearml.py Normal file
View File

@@ -0,0 +1,10 @@
from clearml import Task
class ClearMLHelper:
def __init__(self, project_name: str):
self.project_name = project_name
def get_task(self, task_name: str = "Model Training"):
task = Task.init(project_name=self.project_name, task_name=task_name, continue_last_task=False)
task.set_base_docker(f"pytorch/pytorch:2.0.1-cuda11.7-cudnn8-runtime")
return task