feat(Project): use SKLearn models directly, removed custom ensembling, use 5 minute data, batch inference, numba cusum filter (#192)

* feat(Project): use 5 minute data, running training in parallel, sped up cusum filter by 10x with numba

* fix(WalkForward): inference mini-batch parallelization

* fix(WalkForward): don't use the parallel version of any of the functions

* feat(CI): download the data required

* fix(Project): 5min_crypto folder added

* fix(Evaluate): make sure we have numerical stability in returns

* feat(Models): use SKLearn models directly to enable composability

* feat(Inference): batched inference now working, added forecasting_horizon

* fix(Inference): works again

* fix(Inference)

* chore(Models): remove unused Ensemble model

* fix(Labeller): don't just forward shift returns, also take the sum of the data happened until then

* Update test.yml
This commit is contained in:
Mark Aron Szulyovszky
2022-02-17 16:36:35 +01:00
committed by GitHub
parent 5c94af8b01
commit 9d47ee942d
52 changed files with 470 additions and 628 deletions
+2 -11
View File
@@ -6,10 +6,8 @@ import numpy as np
class Model(ABC):
name: str = ""
method: Literal["regression", "classification"]
data_transformation: Literal["transformed", "original"]
only_column: Optional[str]
model_type: Literal['ml', 'static']
predict_window_size: Literal['single_timestamp', 'window_size']
@abstractmethod
@@ -17,17 +15,10 @@ class Model(ABC):
raise NotImplementedError
@abstractmethod
def predict(self, X: np.ndarray) -> tuple[float, np.ndarray]:
def predict(self, X: np.ndarray) -> np.ndarray:
raise NotImplementedError
@abstractmethod
def clone(self) -> Model:
def predict_proba(self, X: np.ndarray) -> np.ndarray:
raise NotImplementedError
@abstractmethod
def initialize_network(self, input_dim:int, output_dim:int):
pass
+16 -60
View File
@@ -3,7 +3,7 @@ from sklearnex.ensemble import RandomForestClassifier
from sklearnex.ensemble import RandomForestRegressor
from .base import Model
default_feature_selector_classification = SKLearnModel(RandomForestClassifier(n_jobs=-1, max_depth=20, random_state=1), 'classification')
default_feature_selector_classification = SKLearnModel(RandomForestClassifier(n_jobs=-1, max_depth=20, random_state=1))
def get_model(model_name: str) -> Model:
@@ -11,85 +11,41 @@ def get_model(model_name: str) -> Model:
model.name = model_name
return model
if model_name == 'LinearRegression':
from sklearn.linear_model import LinearRegression
return set_name(SKLearnModel(LinearRegression(n_jobs=-1), 'regression'))
elif model_name == 'Lasso':
from sklearn.linear_model import Lasso
return set_name(SKLearnModel(Lasso(alpha=100, random_state=1), 'regression'))
elif model_name == 'Ridge':
from sklearn.linear_model import Ridge
return set_name(SKLearnModel(Ridge(alpha=0.1), 'regression'))
elif model_name == 'BayesianRidge':
from sklearn.linear_model import BayesianRidge
return set_name(SKLearnModel(BayesianRidge(), 'regression'))
elif model_name == 'KNN':
from sklearnex.neighbors import KNeighborsRegressor
return set_name(SKLearnModel(KNeighborsRegressor(n_neighbors=25), 'regression'))
elif model_name == 'AB':
from sklearn.ensemble import AdaBoostRegressor
return set_name(SKLearnModel(AdaBoostRegressor(random_state=1), 'regression'))
elif model_name == 'MLP':
from sklearn.neural_network import MLPRegressor
return set_name(SKLearnModel(MLPRegressor(hidden_layer_sizes=(100,20), max_iter=1000), 'regression'))
elif model_name == 'RFR':
return set_name(SKLearnModel(RandomForestRegressor(n_jobs=-1, max_depth=20, random_state=1), 'regression'))
elif model_name == 'SVR':
from sklearnex.svm import SVR
return set_name(SKLearnModel(SVR(kernel='rbf', C=1e3, gamma=0.1), 'regression'))
elif model_name == 'StaticNaive':
from models.naive import StaticNaiveModel
return set_name(StaticNaiveModel())
elif model_name == 'DNN':
from models.neural import LightningNeuralNetModel
from models.pytorch.neural_nets import MultiLayerPerceptron
import torch.nn.functional as F
return set_name(LightningNeuralNetModel(
MultiLayerPerceptron(
hidden_layers_ratio = [1.0],
probabilities = False,
loss_function = F.mse_loss),
max_epochs=15
))
elif model_name == 'LogisticRegression_two_class':
if model_name == 'LogisticRegression_two_class':
from sklearn.linear_model import LogisticRegression
return set_name(SKLearnModel(LogisticRegression(C=10, random_state=1, solver='liblinear', max_iter=1000), 'classification'))
return set_name(SKLearnModel(LogisticRegression(C=10, random_state=1, solver='liblinear', max_iter=1000)))
elif model_name == 'LogisticRegression_three_class':
from sklearnex.linear_model import LogisticRegression as LogisticRegression_EX
return set_name(SKLearnModel(LogisticRegression_EX(C=10, random_state=1, max_iter=1000, n_jobs=-1), 'classification'))
return set_name(SKLearnModel(LogisticRegression_EX(C=10, random_state=1, max_iter=1000, n_jobs=-1)))
elif model_name == 'LDA':
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
return set_name(SKLearnModel(LinearDiscriminantAnalysis(), 'classification'))
return set_name(SKLearnModel(LinearDiscriminantAnalysis()))
elif model_name == 'KNN':
from sklearn.neighbors import KNeighborsClassifier
return set_name(SKLearnModel(KNeighborsClassifier(), 'classification'))
return set_name(SKLearnModel(KNeighborsClassifier()))
elif model_name == 'CART':
from sklearn.tree import DecisionTreeClassifier
return set_name(SKLearnModel(DecisionTreeClassifier(max_depth=15, random_state=1), 'classification'))
return set_name(SKLearnModel(DecisionTreeClassifier(max_depth=15, random_state=1)))
elif model_name == 'NB':
from sklearn.naive_bayes import GaussianNB
return set_name(SKLearnModel(GaussianNB(), 'classification'))
return set_name(SKLearnModel(GaussianNB()))
elif model_name == 'AB':
from sklearn.ensemble import AdaBoostClassifier
return set_name(SKLearnModel(AdaBoostClassifier(n_estimators=15), 'classification'))
return set_name(SKLearnModel(AdaBoostClassifier(n_estimators=15)))
elif model_name == 'RFC':
return set_name(SKLearnModel(RandomForestClassifier(n_jobs=-1, max_depth=20, random_state=1), 'classification'))
return set_name(SKLearnModel(RandomForestClassifier(n_jobs=-1, max_depth=20, random_state=1)))
elif model_name == 'SVC':
from sklearn.svm import SVC
return set_name(SKLearnModel(SVC(kernel='rbf', C=1e3, probability=True, random_state=1), 'classification'))
elif model_name == 'XGB_two_class':
from xgboost import XGBClassifier
from models.xgboost import XGBoostModel
return set_name(XGBoostModel(XGBClassifier(n_jobs=-1, max_depth = 20, random_state=1, objective='binary:logistic', use_label_encoder= False, eval_metric='mlogloss')))
return set_name(SKLearnModel(SVC(kernel='rbf', C=1e3, probability=True, random_state=1)))
# elif model_name == 'XGB_two_class':
# from xgboost import XGBClassifier
# from models.xgboost import XGBoostModel
# return set_name(XGBoostModel(XGBClassifier(n_jobs=-1, max_depth = 20, random_state=1, objective='binary:logistic', use_label_encoder= False, eval_metric='mlogloss')))
elif model_name == 'LGBM':
from lightgbm import LGBMClassifier
return set_name(SKLearnModel(LGBMClassifier(n_jobs=-1, max_depth=20, random_state=1), 'classification'))
return set_name(SKLearnModel(LGBMClassifier(n_jobs=-1, max_depth=20, random_state=1)))
elif model_name == 'StaticMom':
from models.momentum import StaticMomentumModel
return set_name(StaticMomentumModel(allow_short=True))
elif model_name == 'Average':
from models.average import StaticAverageModel
return set_name(StaticAverageModel())
else:
raise Exception(f'Model {model_name} not found')
+8 -12
View File
@@ -1,16 +1,15 @@
from __future__ import annotations
from models.base import Model
import numpy as np
from .base import Model
from sklearn.base import BaseEstimator, ClassifierMixin
class StaticMomentumModel(Model):
class StaticMomentumModel(BaseEstimator, ClassifierMixin, Model):
'''
Model that uses only one feature: momentum. It's positive if momentum is greater than 0, otherwise it's negative.
'''
method = 'classification'
data_transformation = 'original'
only_column = 'mom'
model_type = 'static'
predict_window_size = 'single_timestamp'
def __init__(self, allow_short: bool) -> None:
@@ -21,13 +20,10 @@ class StaticMomentumModel(Model):
# This is a static model, it can' learn anything
pass
def predict(self, X) -> tuple[float, np.ndarray]:
def predict(self, X) -> np.ndarray:
negative_class = -1.0 if self.allow_short == True else 0.0
prediction = 1.0 if X[-1][0] > 0 else negative_class
return (prediction, np.array([]))
def clone(self) -> StaticMomentumModel:
return self
def initialize_network(self, input_dim:int, output_dim:int):
pass
return np.array(prediction)
def predict_proba(self, X) -> np.ndarray:
return np.array([])
-27
View File
@@ -1,27 +0,0 @@
from __future__ import annotations
from models.base import Model
import numpy as np
class StaticNaiveModel(Model):
'''
Model that carries the last observation (from returns) to the next one, naively.
'''
method = 'regression'
data_transformation = 'original'
only_column = None
model_type = 'static'
predict_window_size = 'single_timestamp'
def fit(self, X: np.ndarray, y: np.ndarray) -> None:
# This is a static model, it can' learn anything
pass
def predict(self, X) -> tuple[float, np.ndarray]:
return (X[-1][0], np.array([]))
def clone(self) -> StaticNaiveModel:
return self
def initialize_network(self, input_dim:int, output_dim:int):
pass
-38
View File
@@ -1,38 +0,0 @@
from __future__ import annotations
from models.base import Model
import numpy as np
from models.pytorch.pytorch_dataset import get_dataloader
import copy
import pytorch_lightning as pl
class LightningNeuralNetModel(Model):
method = 'regression'
data_transformation = 'transformed'
only_column = None
model_type = 'ml'
''' Standard lightning methods '''
def __init__(self, model, max_epochs=5):
self.model = model
self.trainer = pl.Trainer(max_epochs=max_epochs)
def fit(self, X: np.ndarray, y: np.ndarray) -> None:
train_dataloader = self.__prepare_data(X.astype(float), y.astype(float))
self.trainer.fit(self.model, train_dataloader)
def predict(self, X: np.ndarray) -> tuple[float, np.ndarray]:
return self.model(X)
def clone(self):
model_copy = copy.deepcopy(self.model)
return LightningNeuralNetModel(model_copy)
''' Non-standard lightning methods '''
def __prepare_data(self, X:np.ndarray, y:np.ndarray):
dataloader = get_dataloader(X, y)
return dataloader
def initialize_network(self, input_dim:int, output_dim:int):
self.model.initialize_network(input_dim, output_dim)
-61
View File
@@ -1,61 +0,0 @@
import torch
from torch import nn
import torch.nn.functional as F
from torch.utils.data import DataLoader, random_split
import pytorch_lightning as pl
import math
import numpy as np
class MultiLayerPerceptron(pl.LightningModule):
def __init__(self, hidden_layers_ratio: list[float] = [2.0, 2.0], probabilities: bool = False, loss_function=F.mse_loss):
super().__init__()
self.hidden_layers_ratio = hidden_layers_ratio
self.probabilities = probabilities
self.loss_function = loss_function
self.float()
def initialize_network(self, input_dim: int, output_dim: int) -> None:
self.layers = nn.ModuleList()
current_dim = input_dim
for hdim in self.hidden_layers_ratio:
hidden_layer_size = int(math.floor(current_dim * hdim))
self.layers.append(nn.Linear(current_dim, hidden_layer_size))
self.layers.append(nn.ReLU())
current_dim = hidden_layer_size
self.layers.append(nn.Linear(current_dim, output_dim))
def forward(self, x: torch.Tensor):
# in lightning, forward defines the prediction/inference actions
x = torch.from_numpy(x).float()
for layer in self.layers:
x = layer(x)
if self.probabilities:
x = F.softmax(x, dim=1)
return (x.item(), np.array([]))
def training_step(self, batch: torch.Tensor, batch_idx):
# training_step defined the train loop.
# It is independent of forward
x, y = batch
x = x.view(x.size(0), -1)
loss = 0
for layer in self.layers:
x = layer(x.float())
if self.probabilities:
p = F.softmax(x, dim=1)
loss = F.nll_loss(torch.log(p), y.float())
loss = self.loss_function(x, y.float())
return loss
def configure_optimizers(self):
optimizer = torch.optim.Adam(self.parameters(), lr=1e-3)
return optimizer
-26
View File
@@ -1,26 +0,0 @@
import os
import pandas as pd
import torch
from torch.utils.data import Dataset
from torch.utils.data import DataLoader
import numpy as np
class TimeSeriesDataset(Dataset):
def __init__(self, X: np.ndarray, y: np.ndarray):
self.X = X
self.y = y
def __len__(self):
return len(self.X)
def __getitem__(self, idx):
return self.X[idx].astype(float), self.y[idx].astype(float)
def get_dataloader(X: np.ndarray, y: np.ndarray, batch_size: int = 32, shuffle: bool = True):
training_data = TimeSeriesDataset(X, y)
train_dataloader = DataLoader(training_data, batch_size=batch_size, shuffle=shuffle)
return train_dataloader
+13 -23
View File
@@ -1,33 +1,23 @@
from __future__ import annotations
from typing import Literal
from models.base import Model
from .base import Model
import numpy as np
from sklearn.base import clone
class SKLearnModel(Model):
method: Literal["regression", "classification"]
data_transformation = 'transformed'
only_column = None
model_type = 'ml'
predict_window_size = 'single_timestamp'
def SKLearnModel(instance) -> Model:
def __init__(self, model, method: Literal['regression', 'classification']):
self.model = model
self.method = method
instance.data_transformation = 'transformed'
instance.only_column = None
instance.predict_window_size = 'single_timestamp'
instance.name = instance.__class__.__name__
return instance
def fit(self, X: np.ndarray, y: np.ndarray) -> None:
self.model.fit(X, y)
def predict(self, X) -> tuple[float, np.ndarray]:
pred = self.model.predict(X).item()
probability = self.model.predict_proba(X).squeeze()
return (pred, probability)
def clone(self) -> SKLearnModel:
return SKLearnModel(clone(self.model), self.method)
def initialize_network(self, input_dim:int, output_dim:int):
pass
# def predict(self, X) -> tuple[float, np.ndarray]:
# pred = self.model.predict(X).item()
# probability = self.model.predict_proba(X).squeeze()
# return (pred, probability)
-31
View File
@@ -1,31 +0,0 @@
from __future__ import annotations
from statsmodels.tsa.base.tsa_model import TimeSeriesModel
from models.base import Model
import numpy as np
from copy import deepcopy
class StatsModel(Model):
# This is work in progress
data_transformation = 'transformed'
only_column = None
model_type = 'ml'
predict_window_size = 'single_timestamp'
def __init__(self, model: TimeSeriesModel):
self.model = model
def fit(self, X: np.ndarray, y: np.ndarray) -> None:
self.model.fit(X, y)
def predict(self, X) -> tuple[float, np.ndarray]:
pred = self.model.predict(X).item()
return (pred, np.array([0]))
def clone(self) -> StatsModel:
return StatsModel(deepcopy(self.model))
def initialize_network(self, input_dim:int, output_dim:int):
pass
+18 -29
View File
@@ -1,33 +1,22 @@
from __future__ import annotations
from models.base import Model
import numpy as np
from xgboost import XGBClassifier
from sklearn.base import clone
# from __future__ import annotations
# from models.base import Model
# import numpy as np
# from xgboost import XGBClassifier
class XGBoostModel(Model):
# class XGBoostModel(XGBClassifier):
method = 'classification'
data_transformation = 'transformed'
only_column = None
model_type = 'ml'
predict_window_size = 'single_timestamp'
def __init__(self, model: XGBClassifier):
self.model = model
# method = 'classification'
# data_transformation = 'transformed'
# only_column = None
# predict_window_size = 'single_timestamp'
def fit(self, X: np.ndarray, y: np.ndarray) -> None:
def map_to_xgb(y): return np.array([1 if i == 1 else 0 for i in y])
self.model.fit(X, map_to_xgb(y))
# def fit(self, X: np.ndarray, y: np.ndarray) -> None:
# def map_to_xgb(y): return np.array([1 if i == 1 else 0 for i in y])
# self.fit(X, map_to_xgb(y))
# def predict(self, X) -> tuple[float, np.ndarray]:
# pred = self.predict(X).item()
# probability = self.predict_proba(X).squeeze()
# def map_from_xgb(y): return 1 if y == 1 else -1
# return (map_from_xgb(pred), probability)
def predict(self, X) -> tuple[float, np.ndarray]:
pred = self.model.predict(X).item()
probability = self.model.predict_proba(X).squeeze()
def map_from_xgb(y): return 1 if y == 1 else -1
return (map_from_xgb(pred), probability)
def clone(self) -> XGBoostModel:
return XGBoostModel(clone(self.model))
def initialize_network(self, input_dim:int, output_dim:int):
pass