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
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
__pycache__
.devcontainer

5
Dockerfile Normal file
View File

@@ -0,0 +1,5 @@
FROM pytorch/pytorch:2.0.1-cuda11.7-cudnn8-runtime
COPY requirements.txt /tmp/requirements.txt
RUN pip install -r /tmp/requirements.txt

Binary file not shown.

BIN
Reports/November/ea-en.pdf Normal file

Binary file not shown.

BIN
Reports/November/ea-nl.pdf Normal file

Binary file not shown.

BIN
Reports/November/eb-en.pdf Normal file

Binary file not shown.

View File

@@ -0,0 +1,185 @@
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% Ghent University document class
% Created by DF Benoit, December 15, 2022
%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\NeedsTeXFormat{LaTeX2e}
\ProvidesClass{ugent-doc}
% Required packages
\RequirePackage{kvoptions}
\RequirePackage{geometry}
\RequirePackage{calc}
\RequirePackage{graphicx}
\RequirePackage{xcolor}
% ugent-doc specific options (kvoptions)
\SetupKeyvalOptions{family=ugd,prefix=ugd@} %UGentArticle
% Declare the class specific options
\DeclareStringOption[eb]{faculty}[eb]
\DeclareStringOption[en]{language}[en]
\DeclareStringOption[article]{doctype}[article]
\DeclareBoolOption[true]{sftitles} % Default: true
\ProcessKeyvalOptions*
% Pass options not specified above to the parent class
% \@unusedoptionlist is a macro in kvoptions
\LoadClass[\@unusedoptionlist]{\ugd@doctype}
% All sections, subsections and subsubsections in sans serif
\ifugd@sftitles
\RequirePackage[sf]{titlesec}
\fi
% Define UGent colors
%= = = = = = = = = = =
% Base colors
% UGent blue
\definecolor{ugentblue}{RGB}{30,100,200}
% UGent yellow
\definecolor{ugentyellow}{RGB}{255,210,0}
% UGent white
\definecolor{ugentwhite}{RGB}{255,255,255}
% UGent black
\definecolor{ugentblack}{RGB}{0,0,0}
% Faculty specific colors
% Faculty of Literature & Philosophy
\definecolor{ugent-lw}{RGB}{241,164,43}
% Faculty of Law
\definecolor{ugent-re}{RGB}{220,78,40}
% Faculty of Science
\definecolor{ugent-we}{RGB}{45,140,168}
% Faculty of Medicine and Health Sciences
\definecolor{ugent-ge}{RGB}{232,94,113}
% Faculty of Engineering and Architecture
\definecolor{ugent-ea}{RGB}{139,190,232}
% Faculty of Economics and Business Administration
\definecolor{ugent-eb}{RGB}{174,176,80}
% Faculty of Veterinary Medicine
\definecolor{ugent-di}{RGB}{130,84,145}
% Faculty of Psychology and Educational Sciences
\definecolor{ugent-pp}{RGB}{251,126,58}
% Faculty of Bioscience Engineering
\definecolor{ugent-bw}{RGB}{39,171,173}
% Faculty of Pharmaceutical Sciences
\definecolor{ugent-fw}{RGB}{190,81,144}
% Faculty of Political and Social Sciences
\definecolor{ugent-ps}{RGB}{113,168,96}
% Define new commands
\def\thetitle#1{\def\@thetitle{#1}}
\def\thesubtitle#1{\def\@thesubtitle{#1}}
\def\infoboxa#1{\def\@infoboxa{#1}}
\def\infoboxb#1{\def\@infoboxb{#1}}
\def\infoboxc#1{\def\@infoboxc{#1}}
\def\infoboxd#1{\def\@infoboxd{#1}}
% Initialize new commands as 'empty'
\def\@thetitle{}
\def\@thesubtitle{}
\def\@infoboxa{}
\def\@infoboxb{}
\def\@infoboxc{}
\def\@infoboxd{}
% Define lengths based on UGent document grid
% See: https://styleguide.ugent.be/basic-principles/grid-and-layout.html
\newlength{\longedge}
\setlength{\longedge}{\maxof{\paperheight}{\paperwidth}}
\newlength{\gridunit}
\setlength{\gridunit}{\longedge/28} %Divide long edge by 7 and next by 4
\newlength{\subpaperheight}
\setlength{\subpaperheight}{\paperheight-7\gridunit} %Type area: 3 units for faculty logo, 4 units for UGent logo
\newlength{\subpaperwidth}
\setlength{\subpaperwidth}{\paperwidth-\gridunit} %Left margin of 1 gridunit
% Define strut based on \gridunit
\newcommand{\mystrut}[1][-.5]{\rule[#1\gridunit]{0pt}{0pt}}
% Set default page layout
% Can be overwritten in preamble of document
\renewcommand{\baselinestretch}{1.15} % line spacing
\geometry{bottom=2.5cm,top=2.5cm,left=3cm,right=2cm} % margins
% Redefine the titlepage in accordance with UGent styleguide
\renewcommand\maketitle{\begin{titlepage}%
\thispagestyle{empty} % by default, the pagestyle of title page is plain
\newgeometry{top=0cm, bottom=0cm, left=0cm, right=0cm} % set special margins
\setlength{\parindent}{0cm} % necessary to put minipages/boxes at extreme left of page
\setlength{\parsep}{0cm} % necessary to stack minipages/boxes without space
\setlength{\fboxsep}{0cm} % no border around minipages/boxes
\setlength{\parskip}{0cm}
\setlength{\lineskip}{0cm}
\ifugd@sftitles
\sffamily % Titlepage in sans serif font
\fi
\includegraphics[height=3\gridunit]{\ugd@faculty-\ugd@language.pdf}%
\makebox[\gridunit]{}% Left margin of 1 gridunit
\colorbox{ugent-\ugd@faculty!30}{%
%\colorbox{ugentwhite}{%
\begin{minipage}[c][\subpaperheight][t]{\subpaperwidth}%
\vskip 5\gridunit % top margin within minipage
\hskip \gridunit % left margin of 1 within the colorbox
%\fbox{%
\begin{minipage}{\subpaperwidth-2\gridunit} % tile minipage, right margin of 1
\raggedright\bfseries\huge
\textcolor{ugentblue}{\mystrut\@thetitle}\newline
\Large\textcolor{ugentblue}{\@thesubtitle}
\mystrut[1]
\end{minipage}%}
\vskip\fill % Push down to bottom of minipage
\ifx\@infoboxa\empty\else % ony put box if not empty
\hskip\gridunit % left margin of infobox
%\fbox{%
\begin{minipage}[b]{\subpaperwidth-3\gridunit} % right margin of 1
\@infoboxa
\end{minipage}%}
\baselineskip0pt\mystrut
\fi
\ifx\@infoboxb\empty\else % ony put box if not empty
\hskip\gridunit % left margin of infobox
%\fbox{%
\begin{minipage}[b]{\subpaperwidth-3\gridunit} % right margin of 1
\@infoboxb
\end{minipage}%}
\baselineskip0pt\mystrut
\fi
\ifx\@infoboxc\empty\else % ony put box if not empty
\hskip\gridunit % left margin of infobox
%\fbox{%
\begin{minipage}[b]{\subpaperwidth-3\gridunit} % right margin of 1
\@infoboxc
\end{minipage}%}
\baselineskip0pt\mystrut
\fi
\ifx\@infoboxd\empty\else % ony put box if not empty
\hskip\gridunit % left margin of infobox
%\fbox{%
\begin{minipage}[b]{\subpaperwidth-3\gridunit} % right margin of 1
\@infoboxd
\end{minipage}%}
\fi
\baselineskip0pt\mystrut[-1]
\end{minipage}
}%
\includegraphics[height=4\gridunit]{ugent-\ugd@language.pdf}%
\end{titlepage}
\restoregeometry
}

Binary file not shown.

View File

@@ -0,0 +1,31 @@
\relax
\providecommand\babel@aux[2]{}
\@nameuse{bbl@beforestart}
\abx@aux@refcontext{nyt/global//global/global}
\providecommand\hyper@newdestlabel[2]{}
\providecommand\HyperFirstAtBeginDocument{\AtBeginDocument}
\HyperFirstAtBeginDocument{\ifx\hyper@anchor\@undefined
\global\let\oldnewlabel\newlabel
\gdef\newlabel#1#2{\newlabelxx{#1}#2}
\gdef\newlabelxx#1#2#3#4#5#6{\oldnewlabel{#1}{{#2}{#3}}}
\AtEndDocument{\ifx\hyper@anchor\@undefined
\let\newlabel\oldnewlabel
\fi}
\fi}
\global\let\hyper@last\relax
\gdef\HyperFirstAtBeginDocument#1{#1}
\providecommand\HyField@AuxAddToFields[1]{}
\providecommand\HyField@AuxAddToCoFields[2]{}
\babel@aux{english}{}
\@writefile{toc}{\contentsline {section}{\numberline {1}Intermediate Results}{1}{section.1}\protected@file@percent }
\@writefile{toc}{\contentsline {subsection}{\numberline {1.1}Previous day as forecast}{1}{subsection.1.1}\protected@file@percent }
\@writefile{toc}{\contentsline {subsection}{\numberline {1.2}All Zeros}{1}{subsection.1.2}\protected@file@percent }
\@writefile{toc}{\contentsline {subsection}{\numberline {1.3}Linear Model}{1}{subsection.1.3}\protected@file@percent }
\@writefile{lot}{\contentsline {table}{\numberline {1}{\ignorespaces Results of the linear model with different ranges of training data}}{2}{table.1}\protected@file@percent }
\newlabel{tab:linear_model}{{1}{2}{Results of the linear model with different ranges of training data}{table.1}{}}
\@writefile{toc}{\contentsline {section}{\numberline {2}Schedule next months}{3}{section.2}\protected@file@percent }
\@writefile{toc}{\contentsline {subsection}{\numberline {2.1}Other input features}{3}{subsection.2.1}\protected@file@percent }
\@writefile{toc}{\contentsline {subsection}{\numberline {2.2}More complex models}{3}{subsection.2.2}\protected@file@percent }
\@writefile{toc}{\contentsline {subsection}{\numberline {2.3}Reinforcement learning}{3}{subsection.2.3}\protected@file@percent }
\abx@aux@read@bbl@mdfivesum{nobblfile}
\gdef \@abspage@last{4}

2872
Reports/November/verslag.bcf Normal file

File diff suppressed because it is too large Load Diff

1036
Reports/November/verslag.log Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
\BOOKMARK [1][-]{section.1}{\376\377\000I\000n\000t\000e\000r\000m\000e\000d\000i\000a\000t\000e\000\040\000R\000e\000s\000u\000l\000t\000s}{}% 1
\BOOKMARK [2][-]{subsection.1.1}{\376\377\000P\000r\000e\000v\000i\000o\000u\000s\000\040\000d\000a\000y\000\040\000a\000s\000\040\000f\000o\000r\000e\000c\000a\000s\000t}{section.1}% 2
\BOOKMARK [2][-]{subsection.1.2}{\376\377\000A\000l\000l\000\040\000Z\000e\000r\000o\000s}{section.1}% 3
\BOOKMARK [2][-]{subsection.1.3}{\376\377\000L\000i\000n\000e\000a\000r\000\040\000M\000o\000d\000e\000l}{section.1}% 4
\BOOKMARK [1][-]{section.2}{\376\377\000S\000c\000h\000e\000d\000u\000l\000e\000\040\000n\000e\000x\000t\000\040\000m\000o\000n\000t\000h\000s}{}% 5
\BOOKMARK [2][-]{subsection.2.1}{\376\377\000O\000t\000h\000e\000r\000\040\000i\000n\000p\000u\000t\000\040\000f\000e\000a\000t\000u\000r\000e\000s}{section.2}% 6
\BOOKMARK [2][-]{subsection.2.2}{\376\377\000M\000o\000r\000e\000\040\000c\000o\000m\000p\000l\000e\000x\000\040\000m\000o\000d\000e\000l\000s}{section.2}% 7
\BOOKMARK [2][-]{subsection.2.3}{\376\377\000R\000e\000i\000n\000f\000o\000r\000c\000e\000m\000e\000n\000t\000\040\000l\000e\000a\000r\000n\000i\000n\000g}{section.2}% 8

Binary file not shown.

View File

@@ -0,0 +1,89 @@
<?xml version="1.0" standalone="yes"?>
<!-- logreq request file -->
<!-- logreq version 1.0 / dtd version 1.0 -->
<!-- Do not edit this file! -->
<!DOCTYPE requests [
<!ELEMENT requests (internal | external)*>
<!ELEMENT internal (generic, (provides | requires)*)>
<!ELEMENT external (generic, cmdline?, input?, output?, (provides | requires)*)>
<!ELEMENT cmdline (binary, (option | infile | outfile)*)>
<!ELEMENT input (file)+>
<!ELEMENT output (file)+>
<!ELEMENT provides (file)+>
<!ELEMENT requires (file)+>
<!ELEMENT generic (#PCDATA)>
<!ELEMENT binary (#PCDATA)>
<!ELEMENT option (#PCDATA)>
<!ELEMENT infile (#PCDATA)>
<!ELEMENT outfile (#PCDATA)>
<!ELEMENT file (#PCDATA)>
<!ATTLIST requests
version CDATA #REQUIRED
>
<!ATTLIST internal
package CDATA #REQUIRED
priority (9) #REQUIRED
active (0 | 1) #REQUIRED
>
<!ATTLIST external
package CDATA #REQUIRED
priority (1 | 2 | 3 | 4 | 5 | 6 | 7 | 8) #REQUIRED
active (0 | 1) #REQUIRED
>
<!ATTLIST provides
type (static | dynamic | editable) #REQUIRED
>
<!ATTLIST requires
type (static | dynamic | editable) #REQUIRED
>
<!ATTLIST file
type CDATA #IMPLIED
>
]>
<requests version="1.0">
<internal package="biblatex" priority="9" active="0">
<generic>latex</generic>
<provides type="dynamic">
<file>verslag.bcf</file>
</provides>
<requires type="dynamic">
<file>verslag.bbl</file>
</requires>
<requires type="static">
<file>blx-dm.def</file>
<file>apa.dbx</file>
<file>blx-compat.def</file>
<file>biblatex.def</file>
<file>standard.bbx</file>
<file>apa.bbx</file>
<file>apa.cbx</file>
<file>biblatex.cfg</file>
<file>english.lbx</file>
<file>american.lbx</file>
<file>american-apa.lbx</file>
<file>english-apa.lbx</file>
</requires>
</internal>
<external package="biblatex" priority="5" active="0">
<generic>biber</generic>
<cmdline>
<binary>biber</binary>
<infile>verslag</infile>
</cmdline>
<input>
<file>verslag.bcf</file>
</input>
<output>
<file>verslag.bbl</file>
</output>
<provides type="dynamic">
<file>verslag.bbl</file>
</provides>
<requires type="dynamic">
<file>verslag.bcf</file>
</requires>
<requires type="editable">
<file>./references.bib</file>
</requires>
</external>
</requests>

Binary file not shown.

View File

@@ -0,0 +1,192 @@
\documentclass[12pt,a4paper,faculty=ea,language=en,doctype=article]{ugent-doc}
% Optional: margins and spacing
%-------------------------------
% Uncomment and adjust to change the default values set by the template
% Note: the defaults are suggested values by Ghent University
%\geometry{bottom=2.5cm,top=2.5cm,left=3cm,right=2cm}
%\renewcommand{\baselinestretch}{1.15} % line spacing
% Font
%------
\usepackage[T1]{fontenc}
\usepackage[utf8]{inputenc} % allows non-ascii input characters
% Comment or remove the two lines below to use the default Computer Modern font
\usepackage{libertine}
\usepackage{libertinust1math}
% NOTE: because the UGent font Panno is proprietary, it is not possible to use it
% in Overleaf. But UGent does not suggest to use Panno for documents (or maybe only for
% the titlepage). For the body, the UGent suggestion is to use a good serif font (for
% LaTeX this could be libertine or Computer Modern).
% Proper word splitting
%-----------------------
\usepackage[english]{babel}
% Mathematics
%-------------
\usepackage{amsmath}
% Figures
%---------
\usepackage{graphicx} % optional: the package is already loaded by the template
\graphicspath{{./figures/}}
% Bibliography settings
%-----------------------
\usepackage[backend=biber, style=apa, sorting=nyt, hyperref=true]{biblatex}
\addbibresource{./references.bib}
\usepackage{csquotes} % Suggested when using babel+biblatex
% Hyperreferences
%-----------------
\usepackage[colorlinks=true, allcolors=ugentblue]{hyperref}
% Whitespace between paragraphs and no indentation
%--------------------------------------------------
\usepackage[parfill]{parskip}
% Input for title page
%----------------------
% The title
\thetitle{Forecasting and generative modeling of the Belgian electricity market}
\thesubtitle{November Intermediate Report}
%% Note: a stricter UGent style could be achieved with, e.g.:
\usepackage{ulem} % for colored underline
\renewcommand{\ULthickness}{2pt} % adjust thickness of underline
\thetitle{Forecasting and generative modeling of the Belgian electricity market}
% Note: do not forget to reset the \ULthickness to 1pt after invoking \maketitle
% (otherwise all underlines in the rest of your document will be too thick):
%\renewcommand{\ULthickness}{1pt}
% The first (top) infobox at bottom of titlepage
\infoboxa{\bfseries\large Master Thesis}
% The second infobox at bottom of titlepage
\infoboxb{Name:
\begin{tabular}[t]{l}
Victor Mylle
\end{tabular}
}
% The third infobox at bottom of titlepage
\infoboxc{
Promotors:
\begin{tabular}[t]{l}
prof. dr. ir. Chris Develder \\
prof. Bert Claessens
\end{tabular}
\\\\
Supervisor:
\begin{tabular}[t]{l}
Jonas Van Gompel
\end{tabular}
}
% The last (bottom) infobox at bottom of titlepage
\infoboxd{Academic year: 2023--2024} % note dash, not hyphen
\begin{document}
% =====================================================================
% Cover
% =====================================================================
% ------------ TITLE PAGE ---------
\maketitle
\renewcommand{\ULthickness}{1pt}
% =====================================================================
% Front matter
% =====================================================================
% ------------ TABLE OF CONTENTS ---------
% {\hypersetup{hidelinks}\tableofcontents} % hide link color in toc
% \newpage
% \begin{titlepage}
% \centering % Centers everything on the page
% % Logo or Image (Optional)
% % \includegraphics[width=0.5\textwidth]{path_to_logo.jpg}
% \vspace*{2cm} % Add vertical space
% {\large Title: Forecasting and generative modeling of the Belgian electricity market\par}
% \vspace{2cm}
% {\Large Victor Mylle\par}
% \vspace{1cm}
% {\large Period of Internship: 3 July 2023 - 31 August 2023\par}
% \vspace{1cm}
% {\large Mentor: dr. ir. Femke De Backere\par}
% {\large TechWolf supervisor: ir. Jens-Joris Decorte}
% \end{titlepage}
\newpage
\section{Intermediate Results}
The electricity market is a complicated system with many different factors. During this thesis, we will try to model the day-ahead system imbalance. Using this imbalance, a model can be trained using reinforcement learning to trade on the electricity market to generate profit. The first step is to model the imbalance. The imbalance is the difference between the amount of electricity that is bought and sold on the day-ahead market and the amount of electricity that is actually consumed. Elia (Transmission System Operator) is responsible for keeping the grid stable and takes the steps necessary to do so. They provide electricity when there is a shortage and take electricity when there is a surplus. The amount of energy that is provided or consumed is called the Net Regulation Volume. Based on the Net Regulation Volume and the bid ladder, the electricity price can be calculated. \\\\
Elia publishes a lot of data on their website. This data can then be used as training data. First, simple baselines can be implemented to forecast the NRV of the next day.
\\\\
The data available ranges from 01-01-2015 until the current date. The data is available in minute or quarter intervals. For our use case, the quarter interval will do. The data is split into training data and test data. The data from 2023 is used as the test set.
\subsection{Previous day as forecast}
One baseline can be to use the previous day NRV values as the forecast for the next day. This gives the following results on the test set: \\\\
MAE: 145.97317296006946 \\
MSE: 39632.622958020256
\subsection{All Zeros}
Using all zeros as forecast gives the following results on the test set: \\\\
MAE: 106.1727146629051 \\
MSE: 21977.654647179577 \\
\\
The first small conclusion that can be made is that just using all zeros as the forecast gives better results than using the previous day NRV values.
\subsection{Linear Model}
A simple linear model can also be trained on the data. This doesn't generatively model the NRV but forecasts the next value based on the given NRV values. This model can then be used autoregressively to forecast the NRV of the next day.
% Table with results with different ranges of training data
\begin{table}[h]
\centering
\begin{tabular}{|l|l|l|}
\hline
Training data range & MAE & MSE \\ \hline
2015-2022 & 78.04712677001953& 10891.9501953125 \\ \hline
2016-2022 & 77.98072814941406 & 10872.8173828125 \\ \hline
2017-2022 & 77.94755554199219 & 10859.1943359375 \\ \hline
2018-2022 & 77.90494537353516 & 10840.017578125 \\ \hline
2019-2022 & 77.88092041015625 & 10830.2880859375 \\ \hline
2020-2022 & 77.84571075439453 & 10823.6826171875 \\ \hline
2021-2022 & 77.86540985107422 & 10831.35546875 \\ \hline
2022-2022 & 77.95752716064453 & 10871.7685546875 \\ \hline
\end{tabular}
\caption{Results of the linear model with different ranges of training data}
\label{tab:linear_model}
\end{table}
The experiments performed use a linear model. The input size is 96 (the quarter-hour values of the NRV) and the output is one value that represents the predicted next NRV value. The experiments use Adam as an optimizer with a learning rate of 0.0003. All input values were rescaled using the MinMaxScaler.
\newpage
\section{Schedule next months}
An overview of the planning for the next months is given below. The planning is subject to change depending on the results of the experiments.
\subsection{Other input features}
For the moment, only the NRV is used as input. More inputs can be used to model the NRV. For example, Elia provides a load forecast for every quarter hour. This can also be used as input for the model. Weather and other dependencies should be further explored.
\subsection{More complex models}
For now, the models were kept simple. More complex models can however be used to generatively model the NRV. For example, diffusion models can be explored.
\subsection{Reinforcement learning}
Once a model is trained to generatively model the NRV, a reinforcement learning model can be trained to make better decisions on the electricity market. This step however, requires a good generative model of the NRV.
\end{document}

View File

@@ -0,0 +1,87 @@
# Result Report November
## 1. TODOs
- [x] Compare autoregressive vs non-autoregressive
- [ ] Add more input parameters (load forecast)
- [x] Quantile Regression sampling fix
- [x] Quantile Regression exploration
## 2. Autoregressive vs Non-Autoregressive
Training data: 2015 - 2022 \
Batch_size: 1024 \
Learning_rate: 0.0003 \
Early_stopping: 10
### 2.1 Linear Model
Comparison: [Link](https://clearml.victormylle.be/projects/2e46d4af6f1e4c399cf9f5aa30bc8795/compare-experiments;ids=3b512226b24c46199b584d7abc23bf96,097b3832eb5e4c5a8fa2e04887975c29/scalars/values?scalars=values)
<!-- table with 3 columns and rows: experiment, Train-MAE, Train-MSE, Test-MAE, Test-MSE -->
| | Autoregressive | Non-Autoregressive |
| --- | --- | --- |
| Experiment (ClearML) | [Link](https://clearml.victormylle.be/projects/2e46d4af6f1e4c399cf9f5aa30bc8795/experiments/3b512226b24c46199b584d7abc23bf96/execution?columns=selected&columns=type&columns=name&columns=tags&columns=status&columns=project.name&columns=users&columns=started&columns=last_update&columns=last_iteration&columns=parent.name&order=-last_update&filter=) | [Link](https://clearml.victormylle.be/projects/2e46d4af6f1e4c399cf9f5aa30bc8795/experiments/097b3832eb5e4c5a8fa2e04887975c29/execution?columns=selected&columns=type&columns=name&columns=tags&columns=status&columns=project.name&columns=users&columns=started&columns=last_update&columns=last_iteration&columns=parent.name&order=-last_update&filter=) |
| Train-MAE | 68.0202865600586 | 94.83179473876953 |
| Train-MSE | 7861.2197265625 | 15977.8759765625 |
| Test-MAE | 78.05316925048828 | 104.11575317382812 |
| Test-MSE | 10882.755859375 | 21145.583984375 |
### 2.2 Non Linear Model
Hidden layers: 1 \
Hidden units: 512
Comparison: [Link](hhttps://clearml.victormylle.be/projects/135c055e64e54486a54055f33ca9a5af/compare-experiments;ids=57f4a09c6ce54296b6e034f2e0236420,b5f6862f900948c18a834a1a970c8710/scalars/values?scalars=values)
| | Autoregressive | Non-Autoregressive |
| --- | --- | --- |
| Experiment (ClearML) | [Link](https://clearml.victormylle.be/projects/135c055e64e54486a54055f33ca9a5af/experiments/57f4a09c6ce54296b6e034f2e0236420/output/execution) | [Link](https://clearml.victormylle.be/projects/2e46d4af6f1e4c399cf9f5aa30bc8795/experiments/b5f6862f900948c18a834a1a970c8710/output/execution) |
| Train-MAE | 66.78179931640625 | 94.52633666992188 |
| Train-MSE | 7507.53955078125 | 15835.671875 |
| Test-MAE | 77.63729095458984 | 104.07229614257812 |
| Test-MSE | 10789.544921875 | 21090.9765625 |
Also tried with [3 hidden layers for Non-Autoregressive model](https://clearml.victormylle.be/projects/2e46d4af6f1e4c399cf9f5aa30bc8795/experiments/583d0bc1dfb7494d81cf356f6d003dbb/output/execution), test results didn't improve.
# 3. Quantile Regression
## 3.1 Sampling Fix
The model outputs values for the quantiles it is trained on. For example, the quantiles [0.025, 0.05, 0.1, 0.15, 0.85, 0.9, 0.95, 0.975] can be used. The values outputted by the model are like: [-0.23013, -0.19831, -0.15217, -0.13654, 0.011687, 0.015129, 0.043187, 0.047704]. Plotting these as CDF:
![Plotted output values](images/quantile_regression_sampling.png)\
*Plotted output values with cubic interpolation*
Samling from a uniform distribution, we can convert it to our distribution by using the inverse CDF. In python we can interpolate immediately by switching the x and y axis. This gives us the following code:
```
interp1d(quantiles, output_values, kind='quadratic', bounds_error=False, fill_value="extrapolate")
```
The mean of x amount of samples can be calculated.
## 3.2 Exploration
### 3.2.1 Linear Model
Learning Rate: 0.0003 \
Batch Size: 1024 \
Early Stopping: 10 \
Trining Data: 2015 - 2022
| Quantiles | Train-MAE | Train-MSE | Test-MAE | Test-MSE |
| --- | --- | --- | --- | --- |
| 0.025, 0.1, 0.2, 0.3, 0.5, 0.6, 0.8, 0.85, 0.9, 0.975 | 68.07254628777868 | 7872.668472187121 | 78.11135584669907 | 10903.793883789216 |
| 0.025, 0.1, 0.15, 0.2, 0.5, 0.8, 0.85, 0.9, 0.975 | 68.0732244289865 | 7873.212834241974 | 78.1143230666738 | 10907.350919114313 |
| 0.025, 0.05, 0.1, 0.15, 0.85, 0.9, 0.95, 0.975 | 68.2798014428824 | 7936.7644114273935 | 78.50109206637464 | 11005.706457116454 |
| 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9 | 68.06139224378171 | 7871.467571921973 | 78.10843456751378 | 10904.55519059502 |
| 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9 | 68.0350635204691 | 7868.661882749334 | 78.1139478264609 | 10905.562798801011 |
| 0.1, 0.2, 0.3, 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.8, 0.9 | 68.03191172170285 | 7868.483061240721 | 78.13204232055722 | 10908.837301340453 |
These are all very close
### 3.2.2 Non Linear Model
Learning Rate: 0.0003 \
Batch Size: 1024 \
Early Stopping: 10 \
Trining Data: 2015 - 2022 \
Hidden Layers: 3 \
Hidden Units: 1024
| Quantiles | Train-MAE | Train-MSE | Test-MAE | Test-MSE |
| --- | --- | --- | --- | --- |
| 0.1, 0.2, 0.3, 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.8, 0.9 | 65.6542529596313 | 7392.5142575554955 | 77.55779692831604 | 10769.161724849037 |
| 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9 | 65.68495924348356 | 7326.2239225611975 | 77.62433888969542 | 10789.003223366473 |

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because it is too large Load Diff

10
requirements.txt Normal file
View File

@@ -0,0 +1,10 @@
pandas
matplotlib
plotly
nbformat
skforecast
seaborn
statsmodels
lightgbm
prettytable
clearml

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