From 751b2056181b8f7df8d492803b62210131f35bb5 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 5 Mar 2023 16:59:24 +0200 Subject: [PATCH 001/115] initial commit --- .../freqai/base_models/BasePytorchModel.py | 69 +++++++++++++ .../freqai/base_models/PytorchModelTrainer.py | 51 ++++++++++ freqtrade/freqai/data_drawer.py | 7 +- .../PytorchClassifierMultiTarget.py | 97 +++++++++++++++++++ .../prediction_models/PytorchMLPModel.py | 31 ++++++ 5 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 freqtrade/freqai/base_models/BasePytorchModel.py create mode 100644 freqtrade/freqai/base_models/PytorchModelTrainer.py create mode 100644 freqtrade/freqai/prediction_models/PytorchClassifierMultiTarget.py create mode 100644 freqtrade/freqai/prediction_models/PytorchMLPModel.py diff --git a/freqtrade/freqai/base_models/BasePytorchModel.py b/freqtrade/freqai/base_models/BasePytorchModel.py new file mode 100644 index 000000000..da0590a36 --- /dev/null +++ b/freqtrade/freqai/base_models/BasePytorchModel.py @@ -0,0 +1,69 @@ +import logging +from time import time +from typing import Any, Dict + +import torch +from pandas import DataFrame + +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.freqai_interface import IFreqaiModel + +logger = logging.getLogger(__name__) + + +class BasePytorchModel(IFreqaiModel): + """ + Base class for TensorFlow type models. + User *must* inherit from this class and set fit() and predict(). + """ + + def __init__(self, **kwargs): + super().__init__(config=kwargs['config']) + self.dd.model_type = 'pytorch' + self.device = 'cuda' if torch.cuda.is_available() else 'cpu' + + def train( + self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs + ) -> Any: + """ + Filter the training data and train a model to it. Train makes heavy use of the datakitchen + for storing, saving, loading, and analyzing the data. + :param unfiltered_df: Full dataframe for the current training period + :param metadata: pair metadata from strategy. + :return: + :model: Trained model which can be used to inference (self.predict) + """ + + logger.info(f"-------------------- Starting training {pair} --------------------") + + start_time = time() + + features_filtered, labels_filtered = dk.filter_features( + unfiltered_df, + dk.training_features_list, + dk.label_list, + training_filter=True, + ) + + # split data into train/test data. + data_dictionary = dk.make_train_test_datasets(features_filtered, labels_filtered) + if not self.freqai_info.get("fit_live_predictions", 0) or not self.live: + dk.fit_labels() + # normalize all data based on train_dataset only + data_dictionary = dk.normalize_data(data_dictionary) + + # optional additional data cleaning/analysis + self.data_cleaning_train(dk) + + logger.info( + f"Training model on {len(dk.data_dictionary['train_features'].columns)} features" + ) + logger.info(f"Training model on {len(data_dictionary['train_features'])} data points") + + model = self.fit(data_dictionary, dk) + end_time = time() + + logger.info(f"-------------------- Done training {pair} " + f"({end_time - start_time:.2f} secs) --------------------") + + return model diff --git a/freqtrade/freqai/base_models/PytorchModelTrainer.py b/freqtrade/freqai/base_models/PytorchModelTrainer.py new file mode 100644 index 000000000..43a37baf2 --- /dev/null +++ b/freqtrade/freqai/base_models/PytorchModelTrainer.py @@ -0,0 +1,51 @@ +import logging +from pathlib import Path +from typing import Dict + +import torch +import torch.nn as nn + +logger = logging.getLogger(__name__) + + +class PytorchModelTrainer: + def __init__(self, model: nn.Module, optimizer, init_model: Dict): + self.model = model + self.optimizer = optimizer + if init_model: + self.load_from_checkpoint(init_model) + + def fit(self, tensor_dictionary, max_iters, batch_size): + for iter in range(max_iters): + + # todo add validation evaluation here + + xb, yb = self.get_batch(tensor_dictionary, 'train', batch_size) + logits, loss = self.model(xb, yb) + + self.optimizer.zero_grad(set_to_none=True) + loss.backward() + self.optimizer.step() + + def save(self, path): + torch.save({ + 'model_state_dict': self.model.state_dict(), + 'optimizer_state_dict': self.optimizer.state_dict(), + }, path) + + def load_from_file(self, path: Path): + checkpoint = torch.load(path) + return self.load_from_checkpoint(checkpoint) + + def load_from_checkpoint(self, checkpoint: Dict): + self.model.load_state_dict(checkpoint['model_state_dict']) + self.optimizer.load_state_dict(checkpoint['optimizer_state_dict']) + return self + + @staticmethod + def get_batch(tensor_dictionary: Dict, split: str, batch_size: int): + ix = torch.randint(len(tensor_dictionary[f'{split}_labels']), (batch_size,)) + x = tensor_dictionary[f'{split}_features'][ix] + y = tensor_dictionary[f'{split}_labels'][ix] + return x, y + diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index 14986d854..d167a39eb 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -446,7 +446,9 @@ class FreqaiDataDrawer: dump(model, save_path / f"{dk.model_filename}_model.joblib") elif self.model_type == 'keras': model.save(save_path / f"{dk.model_filename}_model.h5") - elif 'stable_baselines' in self.model_type or 'sb3_contrib' == self.model_type: + elif 'stable_baselines' in self.model_type or\ + 'sb3_contrib' == self.model_type or\ + 'pytorch' == self.model_type: model.save(save_path / f"{dk.model_filename}_model.zip") if dk.svm_model is not None: @@ -537,6 +539,9 @@ class FreqaiDataDrawer: self.model_type, self.freqai_info['rl_config']['model_type']) MODELCLASS = getattr(mod, self.freqai_info['rl_config']['model_type']) model = MODELCLASS.load(dk.data_path / f"{dk.model_filename}_model") + elif self.model_type == 'pytorch': + import torch + model = torch.load(dk.data_path / f"{dk.model_filename}_model.zip") if Path(dk.data_path / f"{dk.model_filename}_svm_model.joblib").is_file(): dk.svm_model = load(dk.data_path / f"{dk.model_filename}_svm_model.joblib") diff --git a/freqtrade/freqai/prediction_models/PytorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PytorchClassifierMultiTarget.py new file mode 100644 index 000000000..e4a090bb4 --- /dev/null +++ b/freqtrade/freqai/prediction_models/PytorchClassifierMultiTarget.py @@ -0,0 +1,97 @@ +import logging + +from typing import Dict +from typing import Any, Dict, Tuple +import numpy.typing as npt + +import numpy as np +import pandas as pd +import torch +from pandas import DataFrame + +from torch.nn import functional as F + +from freqtrade.freqai.base_models.BasePytorchModel import BasePytorchModel +from freqtrade.freqai.base_models.PytorchModelTrainer import PytorchModelTrainer +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.prediction_models.PytorchMLPModel import MLP + +logger = logging.getLogger(__name__) + + +class PytorchClassifierMultiTarget(BasePytorchModel): + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + # todo move to config + self.n_hidden = 1024 + self.labels = ['0.0', '1.0', '2.0'] + self.max_iters = 100 + self.batch_size = 64 + self.learning_rate = 3e-4 + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + """ + User sets up the training and test data to fit their desired model here + :param tensor_dictionary: the dictionary constructed by DataHandler to hold + all the training and test data/labels. + """ + n_features = data_dictionary['train_features'].shape[-1] + tensor_dictionary = self.convert_data_to_tensors(data_dictionary) + model = MLP( + input_dim=n_features, + hidden_dim=self.n_hidden, + output_dim=len(self.labels) + ) + model.to(self.device) + optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) + init_model = self.get_init_model(dk.pair) + trainer = PytorchModelTrainer(model, optimizer, init_model=init_model) + trainer.fit(tensor_dictionary, self.max_iters, self.batch_size) + return trainer + + def predict( + self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs + ) -> Tuple[DataFrame, npt.NDArray[np.int_]]: + """ + Filter the prediction features data and predict with it. + :param unfiltered_df: Full dataframe for the current backtest period. + :return: + :pred_df: dataframe containing the predictions + :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove + data (NaNs) or felt uncertain about data (PCA and DI index) + """ + + dk.find_features(unfiltered_df) + filtered_df, _ = dk.filter_features( + unfiltered_df, dk.training_features_list, training_filter=False + ) + filtered_df = dk.normalize_data_from_metadata(filtered_df) + dk.data_dictionary["prediction_features"] = filtered_df + + self.data_cleaning_predict(dk) + dk.data_dictionary["prediction_features"] = torch.tensor( + dk.data_dictionary["prediction_features"].values + ).to(self.device) + + logits, _ = self.model.model(dk.data_dictionary["prediction_features"]) + probs = F.softmax(logits, dim=-1) + label_ints = torch.argmax(probs, dim=-1) + + pred_df_prob = DataFrame(probs.detach().numpy(), columns=self.labels) + pred_df = DataFrame(label_ints, columns=dk.label_list).astype(float).astype(str) + pred_df = pd.concat([pred_df, pred_df_prob], axis=1) + return (pred_df, dk.do_predict) + + def convert_data_to_tensors(self, data_dictionary: Dict) -> Dict: + tensor_dictionary = {} + for split in ['train', 'test']: + tensor_dictionary[f'{split}_features'] = torch.tensor( + data_dictionary[f'{split}_features'].values + ).to(self.device) + tensor_dictionary[f'{split}_labels'] = torch.tensor( + data_dictionary[f'{split}_labels'].astype(float).values + ).long().to(self.device) + + return tensor_dictionary diff --git a/freqtrade/freqai/prediction_models/PytorchMLPModel.py b/freqtrade/freqai/prediction_models/PytorchMLPModel.py new file mode 100644 index 000000000..c70a21395 --- /dev/null +++ b/freqtrade/freqai/prediction_models/PytorchMLPModel.py @@ -0,0 +1,31 @@ +import logging + + +import torch +import torch.nn as nn +from torch.nn import functional as F + +logger = logging.getLogger(__name__) + + +class MLP(nn.Module): + def __init__(self, input_dim, hidden_dim, output_dim): + super(MLP, self).__init__() + self.input_layer = nn.Linear(input_dim, hidden_dim) + self.hidden_layer = nn.Linear(hidden_dim, hidden_dim) + self.output_layer = nn.Linear(hidden_dim, output_dim) + self.relu = nn.ReLU() + self.dropout = nn.Dropout(p=0.2) + + def forward(self, x, targets=None): + x = self.relu(self.input_layer(x)) + x = self.dropout(x) + x = self.relu(self.hidden_layer(x)) + x = self.dropout(x) + logits = self.output_layer(x) + + if targets is None: + return logits, None + + loss = F.cross_entropy(logits, targets.squeeze()) + return logits, loss From b1ac2bf515637e565c038c910c82657ed069482c Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 6 Mar 2023 16:16:45 +0200 Subject: [PATCH 002/115] use data loader, add evaluation on epoch --- ...asePytorchModel.py => BasePyTorchModel.py} | 5 +- .../freqai/base_models/PyTorchModelTrainer.py | 136 ++++++++++++++++++ .../freqai/base_models/PytorchModelTrainer.py | 51 ------- ...get.py => PyTorchClassifierMultiTarget.py} | 50 ++++--- ...{PytorchMLPModel.py => PyTorchMLPModel.py} | 16 +-- 5 files changed, 167 insertions(+), 91 deletions(-) rename freqtrade/freqai/base_models/{BasePytorchModel.py => BasePyTorchModel.py} (94%) create mode 100644 freqtrade/freqai/base_models/PyTorchModelTrainer.py delete mode 100644 freqtrade/freqai/base_models/PytorchModelTrainer.py rename freqtrade/freqai/prediction_models/{PytorchClassifierMultiTarget.py => PyTorchClassifierMultiTarget.py} (70%) rename freqtrade/freqai/prediction_models/{PytorchMLPModel.py => PyTorchMLPModel.py} (60%) diff --git a/freqtrade/freqai/base_models/BasePytorchModel.py b/freqtrade/freqai/base_models/BasePyTorchModel.py similarity index 94% rename from freqtrade/freqai/base_models/BasePytorchModel.py rename to freqtrade/freqai/base_models/BasePyTorchModel.py index da0590a36..1074ddeea 100644 --- a/freqtrade/freqai/base_models/BasePytorchModel.py +++ b/freqtrade/freqai/base_models/BasePyTorchModel.py @@ -1,6 +1,6 @@ import logging from time import time -from typing import Any, Dict +from typing import Any import torch from pandas import DataFrame @@ -11,7 +11,7 @@ from freqtrade.freqai.freqai_interface import IFreqaiModel logger = logging.getLogger(__name__) -class BasePytorchModel(IFreqaiModel): +class BasePyTorchModel(IFreqaiModel): """ Base class for TensorFlow type models. User *must* inherit from this class and set fit() and predict(). @@ -29,7 +29,6 @@ class BasePytorchModel(IFreqaiModel): Filter the training data and train a model to it. Train makes heavy use of the datakitchen for storing, saving, loading, and analyzing the data. :param unfiltered_df: Full dataframe for the current training period - :param metadata: pair metadata from strategy. :return: :model: Trained model which can be used to inference (self.predict) """ diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py new file mode 100644 index 000000000..13c5ffe74 --- /dev/null +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -0,0 +1,136 @@ +import logging +from pathlib import Path +from typing import Dict + +import torch +import torch.nn as nn +from torch.utils.data import DataLoader +from torch.utils.data import TensorDataset +import pandas as pd + +logger = logging.getLogger(__name__) + + +class PyTorchModelTrainer: + def __init__( + self, + model: nn.Module, + optimizer: nn.Module, + criterion: nn.Module, + device: str, + batch_size: int, + max_iters: int, + eval_iters: int, + init_model: Dict + ): + self.model = model + self.optimizer = optimizer + self.criterion = criterion + self.device = device + self.max_iters = max_iters + self.batch_size = batch_size + self.eval_iters = eval_iters + + if init_model: + self.load_from_checkpoint(init_model) + + def fit(self, data_dictionary: Dict[str, pd.DataFrame]): + data_loaders_dictionary = self.create_data_loaders_dictionary(data_dictionary) + epochs = self.calc_n_epochs( + n_obs=len(data_dictionary['train_features']), + batch_size=self.batch_size, + n_iters=self.max_iters + ) + for epoch in range(epochs): + # evaluation + losses = self.estimate_loss(data_loaders_dictionary, data_dictionary) + logger.info( + f"epoch ({epoch}/{epochs}):" + f" train loss {losses['train']:.4f} ; test loss {losses['test']:.4f}" + ) + # training + for batch_data in data_loaders_dictionary['train']: + xb, yb = batch_data + xb = xb.to(self.device) # type: ignore + yb = yb.to(self.device) + yb_pred = self.model(xb) + loss = self.criterion(yb_pred, yb) + + self.optimizer.zero_grad(set_to_none=True) + loss.backward() + self.optimizer.step() + + @torch.no_grad() + def estimate_loss( + self, + data_loader_dictionary: Dict[str, DataLoader], + data_dictionary: Dict[str, pd.DataFrame] + ) -> Dict[str, float]: + + self.model.eval() + epochs = self.calc_n_epochs( + n_obs=len(data_dictionary[f'test_features']), + batch_size=self.batch_size, + n_iters=self.eval_iters + ) + loss_dictionary = {} + for split in ['train', 'test']: + losses = torch.zeros(epochs) + for i, batch in enumerate(data_loader_dictionary[split]): + xb, yb = batch + xb = xb.to(self.device) + yb = yb.to(self.device) + yb_pred = self.model(xb) + loss = self.criterion(yb_pred, yb) + losses[i] = loss.item() + + loss_dictionary[split] = losses.mean() + + self.model.train() + return loss_dictionary + + def create_data_loaders_dictionary( + self, + data_dictionary: Dict[str, pd.DataFrame] + ) -> Dict[str, DataLoader]: + data_loader_dictionary = {} + for split in ['train', 'test']: + labels_shape = data_dictionary[f'{split}_labels'].shape + labels_view = labels_shape[0] if labels_shape[1] == 1 else labels_shape + dataset = TensorDataset( + torch.from_numpy(data_dictionary[f'{split}_features'].values).float(), + torch.from_numpy(data_dictionary[f'{split}_labels'].astype(float).values) + .long() + .view(labels_view) + ) + data_loader = DataLoader( + dataset, + batch_size=self.batch_size, + shuffle=True, + drop_last=True, + num_workers=0, + ) + data_loader_dictionary[split] = data_loader + + return data_loader_dictionary + + @staticmethod + def calc_n_epochs(n_obs: int, batch_size: int, n_iters: int) -> int: + n_batches = n_obs // batch_size + epochs = n_iters // n_batches + return epochs + + def save(self, path: Path): + torch.save({ + 'model_state_dict': self.model.state_dict(), + 'optimizer_state_dict': self.optimizer.state_dict(), + }, path) + + def load_from_file(self, path: Path): + checkpoint = torch.load(path) + return self.load_from_checkpoint(checkpoint) + + def load_from_checkpoint(self, checkpoint: Dict): + self.model.load_state_dict(checkpoint['model_state_dict']) + self.optimizer.load_state_dict(checkpoint['optimizer_state_dict']) + return self diff --git a/freqtrade/freqai/base_models/PytorchModelTrainer.py b/freqtrade/freqai/base_models/PytorchModelTrainer.py deleted file mode 100644 index 43a37baf2..000000000 --- a/freqtrade/freqai/base_models/PytorchModelTrainer.py +++ /dev/null @@ -1,51 +0,0 @@ -import logging -from pathlib import Path -from typing import Dict - -import torch -import torch.nn as nn - -logger = logging.getLogger(__name__) - - -class PytorchModelTrainer: - def __init__(self, model: nn.Module, optimizer, init_model: Dict): - self.model = model - self.optimizer = optimizer - if init_model: - self.load_from_checkpoint(init_model) - - def fit(self, tensor_dictionary, max_iters, batch_size): - for iter in range(max_iters): - - # todo add validation evaluation here - - xb, yb = self.get_batch(tensor_dictionary, 'train', batch_size) - logits, loss = self.model(xb, yb) - - self.optimizer.zero_grad(set_to_none=True) - loss.backward() - self.optimizer.step() - - def save(self, path): - torch.save({ - 'model_state_dict': self.model.state_dict(), - 'optimizer_state_dict': self.optimizer.state_dict(), - }, path) - - def load_from_file(self, path: Path): - checkpoint = torch.load(path) - return self.load_from_checkpoint(checkpoint) - - def load_from_checkpoint(self, checkpoint: Dict): - self.model.load_state_dict(checkpoint['model_state_dict']) - self.optimizer.load_state_dict(checkpoint['optimizer_state_dict']) - return self - - @staticmethod - def get_batch(tensor_dictionary: Dict, split: str, batch_size: int): - ix = torch.randint(len(tensor_dictionary[f'{split}_labels']), (batch_size,)) - x = tensor_dictionary[f'{split}_features'][ix] - y = tensor_dictionary[f'{split}_labels'][ix] - return x, y - diff --git a/freqtrade/freqai/prediction_models/PytorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py similarity index 70% rename from freqtrade/freqai/prediction_models/PytorchClassifierMultiTarget.py rename to freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index e4a090bb4..9504fffb8 100644 --- a/freqtrade/freqai/prediction_models/PytorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -1,6 +1,5 @@ import logging -from typing import Dict from typing import Any, Dict, Tuple import numpy.typing as npt @@ -8,28 +7,29 @@ import numpy as np import pandas as pd import torch from pandas import DataFrame - from torch.nn import functional as F -from freqtrade.freqai.base_models.BasePytorchModel import BasePytorchModel -from freqtrade.freqai.base_models.PytorchModelTrainer import PytorchModelTrainer from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.PytorchMLPModel import MLP + +from freqtrade.freqai.base_models.BasePyTorchModel import BasePyTorchModel +from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer +from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel + logger = logging.getLogger(__name__) -class PytorchClassifierMultiTarget(BasePytorchModel): +class PyTorchClassifierMultiTarget(BasePyTorchModel): def __init__(self, **kwargs): super().__init__(**kwargs) - # todo move to config - self.n_hidden = 1024 self.labels = ['0.0', '1.0', '2.0'] + self.n_hidden = 1024 self.max_iters = 100 self.batch_size = 64 self.learning_rate = 3e-4 + self.eval_iters = 10 def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ @@ -38,17 +38,27 @@ class PytorchClassifierMultiTarget(BasePytorchModel): all the training and test data/labels. """ n_features = data_dictionary['train_features'].shape[-1] - tensor_dictionary = self.convert_data_to_tensors(data_dictionary) - model = MLP( + + model = PyTorchMLPModel( input_dim=n_features, hidden_dim=self.n_hidden, output_dim=len(self.labels) ) model.to(self.device) optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) + criterion = torch.nn.CrossEntropyLoss() init_model = self.get_init_model(dk.pair) - trainer = PytorchModelTrainer(model, optimizer, init_model=init_model) - trainer.fit(tensor_dictionary, self.max_iters, self.batch_size) + trainer = PyTorchModelTrainer( + model=model, + optimizer=optimizer, + criterion=criterion, + device=self.device, + batch_size=self.batch_size, + max_iters=self.max_iters, + eval_iters=self.eval_iters, + init_model=init_model + ) + trainer.fit(data_dictionary) return trainer def predict( @@ -73,9 +83,9 @@ class PytorchClassifierMultiTarget(BasePytorchModel): self.data_cleaning_predict(dk) dk.data_dictionary["prediction_features"] = torch.tensor( dk.data_dictionary["prediction_features"].values - ).to(self.device) + ).float().to(self.device) - logits, _ = self.model.model(dk.data_dictionary["prediction_features"]) + logits = self.model.model(dk.data_dictionary["prediction_features"]) probs = F.softmax(logits, dim=-1) label_ints = torch.argmax(probs, dim=-1) @@ -83,15 +93,3 @@ class PytorchClassifierMultiTarget(BasePytorchModel): pred_df = DataFrame(label_ints, columns=dk.label_list).astype(float).astype(str) pred_df = pd.concat([pred_df, pred_df_prob], axis=1) return (pred_df, dk.do_predict) - - def convert_data_to_tensors(self, data_dictionary: Dict) -> Dict: - tensor_dictionary = {} - for split in ['train', 'test']: - tensor_dictionary[f'{split}_features'] = torch.tensor( - data_dictionary[f'{split}_features'].values - ).to(self.device) - tensor_dictionary[f'{split}_labels'] = torch.tensor( - data_dictionary[f'{split}_labels'].astype(float).values - ).long().to(self.device) - - return tensor_dictionary diff --git a/freqtrade/freqai/prediction_models/PytorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py similarity index 60% rename from freqtrade/freqai/prediction_models/PytorchMLPModel.py rename to freqtrade/freqai/prediction_models/PyTorchMLPModel.py index c70a21395..4e1cc32ba 100644 --- a/freqtrade/freqai/prediction_models/PytorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -3,29 +3,23 @@ import logging import torch import torch.nn as nn -from torch.nn import functional as F logger = logging.getLogger(__name__) -class MLP(nn.Module): - def __init__(self, input_dim, hidden_dim, output_dim): - super(MLP, self).__init__() +class PyTorchMLPModel(nn.Module): + def __init__(self, input_dim: int, hidden_dim: int, output_dim: int): + super(PyTorchMLPModel, self).__init__() self.input_layer = nn.Linear(input_dim, hidden_dim) self.hidden_layer = nn.Linear(hidden_dim, hidden_dim) self.output_layer = nn.Linear(hidden_dim, output_dim) self.relu = nn.ReLU() self.dropout = nn.Dropout(p=0.2) - def forward(self, x, targets=None): + def forward(self, x: torch.tensor) -> torch.tensor: x = self.relu(self.input_layer(x)) x = self.dropout(x) x = self.relu(self.hidden_layer(x)) x = self.dropout(x) logits = self.output_layer(x) - - if targets is None: - return logits, None - - loss = F.cross_entropy(logits, targets.squeeze()) - return logits, loss + return logits From 348a08f1c41b47601bb5592280a567e7c0225b8b Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 6 Mar 2023 16:41:47 +0200 Subject: [PATCH 003/115] add todo - currently assuming class labels are strings ['0.0', '1.0' .. n_classes]. need to resolve it per ClassifierModel --- freqtrade/freqai/base_models/PyTorchModelTrainer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 13c5ffe74..03d264371 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -101,7 +101,7 @@ class PyTorchModelTrainer: torch.from_numpy(data_dictionary[f'{split}_features'].values).float(), torch.from_numpy(data_dictionary[f'{split}_labels'].astype(float).values) .long() - .view(labels_view) + .view(labels_view) # todo currently assuming class labels are strings ['0.0', '1.0' .. n_classes]. need to resolve it per ClassifierModel ) data_loader = DataLoader( dataset, From e6e747bcd819b28336dbf4232c6d23226102e6bf Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 6 Mar 2023 17:50:02 +0200 Subject: [PATCH 004/115] reformat code --- .../freqai/base_models/PyTorchModelTrainer.py | 7 ++++-- freqtrade/freqai/data_drawer.py | 23 +++++++++---------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 03d264371..992ad37ef 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -69,7 +69,7 @@ class PyTorchModelTrainer: self.model.eval() epochs = self.calc_n_epochs( - n_obs=len(data_dictionary[f'test_features']), + n_obs=len(data_dictionary['test_features']), batch_size=self.batch_size, n_iters=self.eval_iters ) @@ -101,8 +101,11 @@ class PyTorchModelTrainer: torch.from_numpy(data_dictionary[f'{split}_features'].values).float(), torch.from_numpy(data_dictionary[f'{split}_labels'].astype(float).values) .long() - .view(labels_view) # todo currently assuming class labels are strings ['0.0', '1.0' .. n_classes]. need to resolve it per ClassifierModel + .view(labels_view) ) + # todo currently assuming class labels are strings ['0.0', '1.0' .. n_classes]. + # need to resolve it per ClassifierModel + data_loader = DataLoader( dataset, batch_size=self.batch_size, diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index d167a39eb..aecab0640 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -24,7 +24,6 @@ from freqtrade.exceptions import OperationalException from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.strategy.interface import IStrategy - logger = logging.getLogger(__name__) @@ -90,8 +89,8 @@ class FreqaiDataDrawer: self.metric_tracker_lock = threading.Lock() self.old_DBSCAN_eps: Dict[str, float] = {} self.empty_pair_dict: pair_info = { - "model_filename": "", "trained_timestamp": 0, - "data_path": "", "extras": {}} + "model_filename": "", "trained_timestamp": 0, + "data_path": "", "extras": {}} self.model_type = self.freqai_info.get('model_save_type', 'joblib') def update_metric_tracker(self, metric: str, value: float, pair: str) -> None: @@ -446,9 +445,9 @@ class FreqaiDataDrawer: dump(model, save_path / f"{dk.model_filename}_model.joblib") elif self.model_type == 'keras': model.save(save_path / f"{dk.model_filename}_model.h5") - elif 'stable_baselines' in self.model_type or\ - 'sb3_contrib' == self.model_type or\ - 'pytorch' == self.model_type: + elif ('stable_baselines' in self.model_type or + 'sb3_contrib' == self.model_type or + 'pytorch' == self.model_type): model.save(save_path / f"{dk.model_filename}_model.zip") if dk.svm_model is not None: @@ -581,16 +580,16 @@ class FreqaiDataDrawer: if len(df_dp.index) == 0: continue if str(hist_df.iloc[-1]["date"]) == str( - df_dp.iloc[-1:]["date"].iloc[-1] + df_dp.iloc[-1:]["date"].iloc[-1] ): continue try: index = ( - df_dp.loc[ - df_dp["date"] == hist_df.iloc[-1]["date"] - ].index[0] - + 1 + df_dp.loc[ + df_dp["date"] == hist_df.iloc[-1]["date"] + ].index[0] + + 1 ) except IndexError: if hist_df.iloc[-1]['date'] < df_dp['date'].iloc[0]: @@ -643,7 +642,7 @@ class FreqaiDataDrawer: ) def get_base_and_corr_dataframes( - self, timerange: TimeRange, pair: str, dk: FreqaiDataKitchen + self, timerange: TimeRange, pair: str, dk: FreqaiDataKitchen ) -> Tuple[Dict[Any, Any], Dict[Any, Any]]: """ Searches through our historic_data in memory and returns the dataframes relevant From 7eedcb9c1475146b0df4b02040c92cadd0575542 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 6 Mar 2023 17:56:07 +0200 Subject: [PATCH 005/115] reformat code --- freqtrade/freqai/data_drawer.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index aecab0640..8d31586fe 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -24,6 +24,7 @@ from freqtrade.exceptions import OperationalException from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.strategy.interface import IStrategy + logger = logging.getLogger(__name__) @@ -89,8 +90,8 @@ class FreqaiDataDrawer: self.metric_tracker_lock = threading.Lock() self.old_DBSCAN_eps: Dict[str, float] = {} self.empty_pair_dict: pair_info = { - "model_filename": "", "trained_timestamp": 0, - "data_path": "", "extras": {}} + "model_filename": "", "trained_timestamp": 0, + "data_path": "", "extras": {}} self.model_type = self.freqai_info.get('model_save_type', 'joblib') def update_metric_tracker(self, metric: str, value: float, pair: str) -> None: @@ -580,16 +581,16 @@ class FreqaiDataDrawer: if len(df_dp.index) == 0: continue if str(hist_df.iloc[-1]["date"]) == str( - df_dp.iloc[-1:]["date"].iloc[-1] + df_dp.iloc[-1:]["date"].iloc[-1] ): continue try: index = ( - df_dp.loc[ - df_dp["date"] == hist_df.iloc[-1]["date"] - ].index[0] - + 1 + df_dp.loc[ + df_dp["date"] == hist_df.iloc[-1]["date"] + ].index[0] + + 1 ) except IndexError: if hist_df.iloc[-1]['date'] < df_dp['date'].iloc[0]: @@ -642,7 +643,7 @@ class FreqaiDataDrawer: ) def get_base_and_corr_dataframes( - self, timerange: TimeRange, pair: str, dk: FreqaiDataKitchen + self, timerange: TimeRange, pair: str, dk: FreqaiDataKitchen ) -> Tuple[Dict[Any, Any], Dict[Any, Any]]: """ Searches through our historic_data in memory and returns the dataframes relevant From 125085fbaf38713e11a4657a20b2cc7833553ed9 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 6 Mar 2023 18:10:49 +0200 Subject: [PATCH 006/115] add freqai.model_exists pytorch file type support --- freqtrade/freqai/freqai_interface.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 884849446..79bd7d672 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -563,8 +563,11 @@ class IFreqaiModel(ABC): file_type = ".joblib" elif self.dd.model_type == 'keras': file_type = ".h5" - elif 'stable_baselines' in self.dd.model_type or 'sb3_contrib' == self.dd.model_type: + elif ('stable_baselines' in self.dd.model_type or + 'sb3_contrib' == self.dd.model_type or + 'pytorch' == self.dd.model_type): file_type = ".zip" + path_to_modelfile = Path(dk.data_path / f"{dk.model_filename}_model{file_type}") file_exists = path_to_modelfile.is_file() if file_exists: From 8acdd0b47c8cb7239933653b393460a267f39501 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 6 Mar 2023 19:14:54 +0200 Subject: [PATCH 007/115] type hints fixes --- freqtrade/freqai/base_models/PyTorchModelTrainer.py | 2 +- freqtrade/freqai/prediction_models/PyTorchMLPModel.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 992ad37ef..52fb0ceb5 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -51,7 +51,7 @@ class PyTorchModelTrainer: # training for batch_data in data_loaders_dictionary['train']: xb, yb = batch_data - xb = xb.to(self.device) # type: ignore + xb = xb.to(self.device) yb = yb.to(self.device) yb_pred = self.model(xb) loss = self.criterion(yb_pred, yb) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index 4e1cc32ba..9bbf95019 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -3,6 +3,7 @@ import logging import torch import torch.nn as nn +from torch import Tensor logger = logging.getLogger(__name__) @@ -16,7 +17,7 @@ class PyTorchMLPModel(nn.Module): self.relu = nn.ReLU() self.dropout = nn.Dropout(p=0.2) - def forward(self, x: torch.tensor) -> torch.tensor: + def forward(self, x: Tensor) -> Tensor: x = self.relu(self.input_layer(x)) x = self.dropout(x) x = self.relu(self.hidden_layer(x)) From 5dd60eda3693df999f80305cb60f2c6fc3b22a3d Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 6 Mar 2023 19:37:08 +0200 Subject: [PATCH 008/115] type hints fixes --- freqtrade/freqai/base_models/PyTorchModelTrainer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 52fb0ceb5..02ff35085 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -84,7 +84,7 @@ class PyTorchModelTrainer: loss = self.criterion(yb_pred, yb) losses[i] = loss.item() - loss_dictionary[split] = losses.mean() + loss_dictionary[split] = losses.mean().item() self.model.train() return loss_dictionary From 4241bff32aee728f4d2b57f52c667e15ece3e33c Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 6 Mar 2023 20:15:36 +0200 Subject: [PATCH 009/115] type hints fixes --- freqtrade/freqai/base_models/PyTorchModelTrainer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 02ff35085..fc0a7600e 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -1,6 +1,7 @@ import logging from pathlib import Path from typing import Dict +from torch.optim import Optimizer import torch import torch.nn as nn @@ -15,7 +16,7 @@ class PyTorchModelTrainer: def __init__( self, model: nn.Module, - optimizer: nn.Module, + optimizer: Optimizer, criterion: nn.Module, device: str, batch_size: int, From 76fbec0c175714148cead01a6c85f36d91de3244 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Wed, 8 Mar 2023 14:29:38 +0200 Subject: [PATCH 010/115] ad multiclass target names encoder to ints --- config_examples/config_freqai.example.json | 3 +- .../PyTorchClassifierMultiTarget.py | 63 +++++++++++++++---- 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/config_examples/config_freqai.example.json b/config_examples/config_freqai.example.json index 65a93379e..479e94aa3 100644 --- a/config_examples/config_freqai.example.json +++ b/config_examples/config_freqai.example.json @@ -79,7 +79,8 @@ "test_size": 0.33, "random_state": 1 }, - "model_training_parameters": {} + "model_training_parameters": {}, + "multiclass_target_names": ["down", "neither", "up"] }, "bot_name": "", "force_entry_enable": true, diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index 9504fffb8..aead0e46c 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -1,6 +1,6 @@ import logging -from typing import Any, Dict, Tuple +from typing import Any, Dict, Tuple, List import numpy.typing as npt import numpy as np @@ -9,6 +9,7 @@ import torch from pandas import DataFrame from torch.nn import functional as F +from freqtrade.exceptions import OperationalException from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.base_models.BasePyTorchModel import BasePyTorchModel @@ -23,13 +24,23 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): def __init__(self, **kwargs): super().__init__(**kwargs) - # todo move to config - self.labels = ['0.0', '1.0', '2.0'] - self.n_hidden = 1024 - self.max_iters = 100 - self.batch_size = 64 - self.learning_rate = 3e-4 - self.eval_iters = 10 + self.multiclass_names = self.freqai_info["multiclass_target_names"] + if not self.multiclass_names: + raise OperationalException( + "Missing 'multiclass_names' in freqai_info," + " multi class pytorch model requires predefined list of" + " class names matching the strategy being used" + ) + + self.class_name_to_index = {s: i for i, s in enumerate(self.multiclass_names)} + self.index_to_class_name = {i: s for i, s in enumerate(self.multiclass_names)} + + model_training_parameters = self.freqai_info["model_training_parameters"] + self.n_hidden = model_training_parameters.get("n_hidden", 1024) + self.max_iters = model_training_parameters.get("max_iters", 100) + self.batch_size = model_training_parameters.get("batch_size", 64) + self.learning_rate = model_training_parameters.get("learning_rate", 3e-4) + self.eval_iters = model_training_parameters.get("eval_iters", 10) def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ @@ -37,12 +48,13 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): :param tensor_dictionary: the dictionary constructed by DataHandler to hold all the training and test data/labels. """ - n_features = data_dictionary['train_features'].shape[-1] + self.encode_classes_name(data_dictionary, dk) + n_features = data_dictionary['train_features'].shape[-1] model = PyTorchMLPModel( input_dim=n_features, hidden_dim=self.n_hidden, - output_dim=len(self.labels) + output_dim=len(self.multiclass_names) ) model.to(self.device) optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) @@ -87,9 +99,34 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): logits = self.model.model(dk.data_dictionary["prediction_features"]) probs = F.softmax(logits, dim=-1) - label_ints = torch.argmax(probs, dim=-1) + predicted_classes = torch.argmax(probs, dim=-1) + predicted_classes_str = self.decode_classes_name(predicted_classes) - pred_df_prob = DataFrame(probs.detach().numpy(), columns=self.labels) - pred_df = DataFrame(label_ints, columns=dk.label_list).astype(float).astype(str) + pred_df_prob = DataFrame(probs.detach().numpy(), columns=self.multiclass_names) + pred_df = DataFrame(predicted_classes_str, columns=[dk.label_list[0]]) pred_df = pd.concat([pred_df, pred_df_prob], axis=1) return (pred_df, dk.do_predict) + + def encode_classes_name(self, data_dictionary: Dict[str, pd.DataFrame], dk: FreqaiDataKitchen): + """ + encode class name str -> int + assuming first column of *_labels data frame to contain class names + """ + target_column_name = dk.label_list[0] + for split in ["train", "test"]: + label_df = data_dictionary[f"{split}_labels"] + self.assert_valid_class_names(label_df[target_column_name]) + label_df[target_column_name] = list( + map(lambda x: self.class_name_to_index[x], label_df[target_column_name]) + ) + + def assert_valid_class_names(self, labels: pd.Series): + non_defined_labels = set(labels) - set(self.multiclass_names) + if len(non_defined_labels) != 0: + raise OperationalException( + f"Found non defined labels {non_defined_labels} ", + f"expecting labels {self.multiclass_names}" + ) + + def decode_classes_name(self, classes: List[int]) -> List[str]: + return list(map(lambda x: self.index_to_class_name[x], classes)) \ No newline at end of file From 1805db2b077157e65c78543a91ce1e1f8eb09fdc Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Wed, 8 Mar 2023 15:38:22 +0200 Subject: [PATCH 011/115] change documentation and small bugfix --- .../freqai/base_models/PyTorchModelTrainer.py | 2 -- .../PyTorchClassifierMultiTarget.py | 22 +++++++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index fc0a7600e..d02f1d896 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -104,8 +104,6 @@ class PyTorchModelTrainer: .long() .view(labels_view) ) - # todo currently assuming class labels are strings ['0.0', '1.0' .. n_classes]. - # need to resolve it per ClassifierModel data_loader = DataLoader( dataset, diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index aead0e46c..e58fa9cff 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -24,16 +24,18 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): def __init__(self, **kwargs): super().__init__(**kwargs) - self.multiclass_names = self.freqai_info["multiclass_target_names"] + self.multiclass_names = self.freqai_info.get("multiclass_target_names", None) + logger.info(f"setting multiclass_names: {self.multiclass_names}") if not self.multiclass_names: raise OperationalException( - "Missing 'multiclass_names' in freqai_info," - " multi class pytorch model requires predefined list of" - " class names matching the strategy being used" + "Missing 'multiclass_names' in freqai_info, " + "multi class pytorch classifier model requires predefined list of " + "class names matching the strategy being used." ) self.class_name_to_index = {s: i for i, s in enumerate(self.multiclass_names)} self.index_to_class_name = {i: s for i, s in enumerate(self.multiclass_names)} + logger.info(f"class_name_to_index: {self.class_name_to_index}") model_training_parameters = self.freqai_info["model_training_parameters"] self.n_hidden = model_training_parameters.get("n_hidden", 1024) @@ -48,7 +50,6 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): :param tensor_dictionary: the dictionary constructed by DataHandler to hold all the training and test data/labels. """ - self.encode_classes_name(data_dictionary, dk) n_features = data_dictionary['train_features'].shape[-1] model = PyTorchMLPModel( @@ -124,9 +125,12 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): non_defined_labels = set(labels) - set(self.multiclass_names) if len(non_defined_labels) != 0: raise OperationalException( - f"Found non defined labels {non_defined_labels} ", - f"expecting labels {self.multiclass_names}" + f"Found non defined labels: {non_defined_labels}, ", + f"expecting labels: {self.multiclass_names}" ) - def decode_classes_name(self, classes: List[int]) -> List[str]: - return list(map(lambda x: self.index_to_class_name[x], classes)) \ No newline at end of file + def decode_classes_name(self, classes: torch.Tensor[int]) -> List[str]: + """ + decode class name int -> str + """ + return list(map(lambda x: self.index_to_class_name[x.item()], classes)) From dfbb2e2b35e79f023bdfb98ca8a8e840ea4fae8d Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Wed, 8 Mar 2023 16:03:36 +0200 Subject: [PATCH 012/115] sort imports --- freqtrade/freqai/base_models/BasePyTorchModel.py | 1 + freqtrade/freqai/base_models/PyTorchModelTrainer.py | 8 ++++---- .../prediction_models/PyTorchClassifierMultiTarget.py | 9 +++------ freqtrade/freqai/prediction_models/PyTorchMLPModel.py | 5 ++--- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/freqtrade/freqai/base_models/BasePyTorchModel.py b/freqtrade/freqai/base_models/BasePyTorchModel.py index 1074ddeea..efc36fdec 100644 --- a/freqtrade/freqai/base_models/BasePyTorchModel.py +++ b/freqtrade/freqai/base_models/BasePyTorchModel.py @@ -8,6 +8,7 @@ from pandas import DataFrame from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.freqai_interface import IFreqaiModel + logger = logging.getLogger(__name__) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index d02f1d896..464c5dc43 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -1,13 +1,13 @@ import logging from pathlib import Path from typing import Dict -from torch.optim import Optimizer +import pandas as pd import torch import torch.nn as nn -from torch.utils.data import DataLoader -from torch.utils.data import TensorDataset -import pandas as pd +from torch.optim import Optimizer +from torch.utils.data import DataLoader, TensorDataset + logger = logging.getLogger(__name__) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index e58fa9cff..3623728db 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -1,19 +1,16 @@ import logging - -from typing import Any, Dict, Tuple, List -import numpy.typing as npt +from typing import Any, Dict, Tuple import numpy as np +import numpy.typing as npt import pandas as pd import torch from pandas import DataFrame from torch.nn import functional as F -from freqtrade.exceptions import OperationalException -from freqtrade.freqai.data_kitchen import FreqaiDataKitchen - from freqtrade.freqai.base_models.BasePyTorchModel import BasePyTorchModel from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index 9bbf95019..88599acb6 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -1,9 +1,8 @@ import logging - -import torch -import torch.nn as nn from torch import Tensor +import torch.nn as nn + logger = logging.getLogger(__name__) From b65ade51bed5c2bf5c4113b9f1c90c20d55a330a Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Wed, 8 Mar 2023 16:05:02 +0200 Subject: [PATCH 013/115] revert config_freqai_example changes --- config_examples/config_freqai.example.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config_examples/config_freqai.example.json b/config_examples/config_freqai.example.json index 479e94aa3..65a93379e 100644 --- a/config_examples/config_freqai.example.json +++ b/config_examples/config_freqai.example.json @@ -79,8 +79,7 @@ "test_size": 0.33, "random_state": 1 }, - "model_training_parameters": {}, - "multiclass_target_names": ["down", "neither", "up"] + "model_training_parameters": {} }, "bot_name": "", "force_entry_enable": true, From 1921a07b8912a447f0fa8eed87f6f22caafdafa1 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Wed, 8 Mar 2023 16:08:04 +0200 Subject: [PATCH 014/115] sort imports --- freqtrade/freqai/prediction_models/PyTorchMLPModel.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index 88599acb6..de9c25293 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -1,8 +1,7 @@ import logging -from torch import Tensor import torch.nn as nn - +from torch import Tensor logger = logging.getLogger(__name__) From 6161b858c44a3f278a81c648661e0d722be03a6d Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Wed, 8 Mar 2023 16:10:25 +0200 Subject: [PATCH 015/115] sort imports --- freqtrade/freqai/prediction_models/PyTorchMLPModel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index de9c25293..1f13ca069 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -3,6 +3,7 @@ import logging import torch.nn as nn from torch import Tensor + logger = logging.getLogger(__name__) From 04564dc134bae650ba7fed16bdf01c5309ddb2b3 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Wed, 8 Mar 2023 16:11:51 +0200 Subject: [PATCH 016/115] add missing import --- .../freqai/prediction_models/PyTorchClassifierMultiTarget.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index 3623728db..ae8728490 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -8,6 +8,7 @@ import torch from pandas import DataFrame from torch.nn import functional as F +from freqtrade.exceptions import OperationalException from freqtrade.freqai.base_models.BasePyTorchModel import BasePyTorchModel from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer from freqtrade.freqai.data_kitchen import FreqaiDataKitchen From 8d60327d60e08a78168342e8fd2ee18ecb01c547 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Wed, 8 Mar 2023 16:12:47 +0200 Subject: [PATCH 017/115] add missing import --- .../freqai/prediction_models/PyTorchClassifierMultiTarget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index ae8728490..cfcf57364 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Dict, Tuple +from typing import Any, Dict, Tuple, List import numpy as np import numpy.typing as npt From c8296ccb2d289b68171924bb61af96954b26effc Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Wed, 8 Mar 2023 16:13:35 +0200 Subject: [PATCH 018/115] sort imports --- .../freqai/prediction_models/PyTorchClassifierMultiTarget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index cfcf57364..62bec0fd9 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Dict, Tuple, List +from typing import Any, Dict, List, Tuple import numpy as np import numpy.typing as npt From 7d26df01b814bcd0b9612740a976cfc74e284618 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Wed, 8 Mar 2023 16:16:49 +0200 Subject: [PATCH 019/115] fix tensor type hint --- .../freqai/prediction_models/PyTorchClassifierMultiTarget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index 62bec0fd9..f33248e7d 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -127,7 +127,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): f"expecting labels: {self.multiclass_names}" ) - def decode_classes_name(self, classes: torch.Tensor[int]) -> List[str]: + def decode_classes_name(self, classes: torch.Tensor) -> List[str]: """ decode class name int -> str """ From 1597c3aa89f2425f7ec076520a837b7582844a54 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Wed, 8 Mar 2023 18:36:44 +0200 Subject: [PATCH 020/115] set class names in IStrategy.set_freqai_targets method, also save class name with model meta data --- .../freqai/base_models/PyTorchModelTrainer.py | 8 +++- .../PyTorchClassifierMultiTarget.py | 45 +++++++++++-------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 464c5dc43..5ebecef34 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import Dict +from typing import Any, Dict import pandas as pd import torch @@ -22,11 +22,13 @@ class PyTorchModelTrainer: batch_size: int, max_iters: int, eval_iters: int, - init_model: Dict + init_model: Dict, + model_meta_data: Dict[str, Any] = {}, ): self.model = model self.optimizer = optimizer self.criterion = criterion + self.model_meta_data = model_meta_data self.device = device self.max_iters = max_iters self.batch_size = batch_size @@ -126,6 +128,7 @@ class PyTorchModelTrainer: torch.save({ 'model_state_dict': self.model.state_dict(), 'optimizer_state_dict': self.optimizer.state_dict(), + 'model_meta_data': self.model_meta_data, }, path) def load_from_file(self, path: Path): @@ -135,4 +138,5 @@ class PyTorchModelTrainer: def load_from_checkpoint(self, checkpoint: Dict): self.model.load_state_dict(checkpoint['model_state_dict']) self.optimizer.load_state_dict(checkpoint['optimizer_state_dict']) + self.model_meta_data = checkpoint["model_meta_data"] return self diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index f33248e7d..13ec2d0bb 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -22,25 +22,14 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): def __init__(self, **kwargs): super().__init__(**kwargs) - self.multiclass_names = self.freqai_info.get("multiclass_target_names", None) - logger.info(f"setting multiclass_names: {self.multiclass_names}") - if not self.multiclass_names: - raise OperationalException( - "Missing 'multiclass_names' in freqai_info, " - "multi class pytorch classifier model requires predefined list of " - "class names matching the strategy being used." - ) - - self.class_name_to_index = {s: i for i, s in enumerate(self.multiclass_names)} - self.index_to_class_name = {i: s for i, s in enumerate(self.multiclass_names)} - logger.info(f"class_name_to_index: {self.class_name_to_index}") - model_training_parameters = self.freqai_info["model_training_parameters"] self.n_hidden = model_training_parameters.get("n_hidden", 1024) self.max_iters = model_training_parameters.get("max_iters", 100) self.batch_size = model_training_parameters.get("batch_size", 64) self.learning_rate = model_training_parameters.get("learning_rate", 3e-4) self.eval_iters = model_training_parameters.get("eval_iters", 10) + self.class_name_to_index = None + self.index_to_class_name = None def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ @@ -48,12 +37,20 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): :param tensor_dictionary: the dictionary constructed by DataHandler to hold all the training and test data/labels. """ + if not hasattr(self, "class_names"): + raise ValueError( + "Missing attribute: self.class_names " + "set self.freqai.class_names = [\"class a\", \"class b\", \"class c\"] " + "inside IStrategy.set_freqai_targets method." + ) + + self.init_class_names_to_index_mapping(self.class_names) self.encode_classes_name(data_dictionary, dk) n_features = data_dictionary['train_features'].shape[-1] model = PyTorchMLPModel( input_dim=n_features, hidden_dim=self.n_hidden, - output_dim=len(self.multiclass_names) + output_dim=len(self.class_names) ) model.to(self.device) optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) @@ -63,6 +60,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): model=model, optimizer=optimizer, criterion=criterion, + model_meta_data={"class_names": self.class_names}, device=self.device, batch_size=self.batch_size, max_iters=self.max_iters, @@ -83,6 +81,13 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove data (NaNs) or felt uncertain about data (PCA and DI index) """ + class_names = self.model.model_meta_data.get("class_names", None) + if not class_names: + raise ValueError( + "Missing class names. " + "self.model.model_meta_data[\"class_names\"] is None." + ) + self.init_class_names_to_index_mapping(class_names) dk.find_features(unfiltered_df) filtered_df, _ = dk.filter_features( @@ -100,8 +105,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): probs = F.softmax(logits, dim=-1) predicted_classes = torch.argmax(probs, dim=-1) predicted_classes_str = self.decode_classes_name(predicted_classes) - - pred_df_prob = DataFrame(probs.detach().numpy(), columns=self.multiclass_names) + pred_df_prob = DataFrame(probs.detach().numpy(), columns=class_names) pred_df = DataFrame(predicted_classes_str, columns=[dk.label_list[0]]) pred_df = pd.concat([pred_df, pred_df_prob], axis=1) return (pred_df, dk.do_predict) @@ -120,11 +124,11 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): ) def assert_valid_class_names(self, labels: pd.Series): - non_defined_labels = set(labels) - set(self.multiclass_names) + non_defined_labels = set(labels) - set(self.class_names) if len(non_defined_labels) != 0: raise OperationalException( f"Found non defined labels: {non_defined_labels}, ", - f"expecting labels: {self.multiclass_names}" + f"expecting labels: {self.class_names}" ) def decode_classes_name(self, classes: torch.Tensor) -> List[str]: @@ -132,3 +136,8 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): decode class name int -> str """ return list(map(lambda x: self.index_to_class_name[x.item()], classes)) + + def init_class_names_to_index_mapping(self, class_names): + self.class_name_to_index = {s: i for i, s in enumerate(class_names)} + self.index_to_class_name = {i: s for i, s in enumerate(class_names)} + logger.info(f"class_name_to_index: {self.class_name_to_index}") From 3081b9402b78406c4edee6e6ef6cdc4937b64e50 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 9 Mar 2023 11:14:54 +0200 Subject: [PATCH 021/115] add documentation --- .../freqai/base_models/BasePyTorchModel.py | 2 +- .../freqai/base_models/PyTorchModelTrainer.py | 21 +++++++++++++++++++ .../PyTorchClassifierMultiTarget.py | 17 +++++++++++++-- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqai/base_models/BasePyTorchModel.py b/freqtrade/freqai/base_models/BasePyTorchModel.py index efc36fdec..8e608ee1a 100644 --- a/freqtrade/freqai/base_models/BasePyTorchModel.py +++ b/freqtrade/freqai/base_models/BasePyTorchModel.py @@ -14,7 +14,7 @@ logger = logging.getLogger(__name__) class BasePyTorchModel(IFreqaiModel): """ - Base class for TensorFlow type models. + Base class for PyTorch type models. User *must* inherit from this class and set fit() and predict(). """ diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 5ebecef34..26149e2fa 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -25,6 +25,21 @@ class PyTorchModelTrainer: init_model: Dict, model_meta_data: Dict[str, Any] = {}, ): + """ + A class for training PyTorch models. + + :param model: The PyTorch model to be trained. + :param optimizer: The optimizer to use for training. + :param criterion: The loss function to use for training. + :param device: The device to use for training (e.g. 'cpu', 'cuda'). + :param batch_size: The size of the batches to use during training. + :param max_iters: The number of training iterations to run. + iteration here refers to the number of times we call + self.optimizer.step() . used to calculate n_epochs. + :param eval_iters: The number of iterations used to estimate the loss. + :param init_model: A dictionary containing the initial model parameters. + :param model_meta_data: Additional metadata about the model (optional). + """ self.model = model self.optimizer = optimizer self.criterion = criterion @@ -38,6 +53,12 @@ class PyTorchModelTrainer: self.load_from_checkpoint(init_model) def fit(self, data_dictionary: Dict[str, pd.DataFrame]): + """ + general training loop: + - converting data to tensors + - calculating n_epochs + - + """ data_loaders_dictionary = self.create_data_loaders_dictionary(data_dictionary) epochs = self.calc_n_epochs( n_obs=len(data_dictionary['train_features']), diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index 13ec2d0bb..e8326ffe9 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -19,8 +19,19 @@ logger = logging.getLogger(__name__) class PyTorchClassifierMultiTarget(BasePyTorchModel): - + """ + A PyTorch implementation of a multi-target classifier. + """ def __init__(self, **kwargs): + """ + int: The number of nodes in the hidden layer of the neural network. + int: The maximum number of iterations to run during training. + int: The batch size to use during training. + float: The learning rate to use during training. + int: The number of training iterations between each evaluation. + dict: A dictionary mapping class names to their corresponding indices. + dict: A dictionary mapping indices to their corresponding class names. + """ super().__init__(**kwargs) model_training_parameters = self.freqai_info["model_training_parameters"] self.n_hidden = model_training_parameters.get("n_hidden", 1024) @@ -34,8 +45,10 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param tensor_dictionary: the dictionary constructed by DataHandler to hold + :param data_dictionary: the dictionary constructed by DataHandler to hold all the training and test data/labels. + :raises ValueError: If self.class_names is not defined in the parent class. + """ if not hasattr(self, "class_names"): raise ValueError( From ba5de0cd00423570cc484a44ffa281718bda47a9 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 9 Mar 2023 11:21:10 +0200 Subject: [PATCH 022/115] add documentation --- freqtrade/freqai/base_models/PyTorchModelTrainer.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 26149e2fa..41d26e31a 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -54,10 +54,7 @@ class PyTorchModelTrainer: def fit(self, data_dictionary: Dict[str, pd.DataFrame]): """ - general training loop: - - converting data to tensors - - calculating n_epochs - - + General training loop. """ data_loaders_dictionary = self.create_data_loaders_dictionary(data_dictionary) epochs = self.calc_n_epochs( @@ -117,6 +114,9 @@ class PyTorchModelTrainer: self, data_dictionary: Dict[str, pd.DataFrame] ) -> Dict[str, DataLoader]: + """ + Converts the input data to PyTorch tensors using a data loader. + """ data_loader_dictionary = {} for split in ['train', 'test']: labels_shape = data_dictionary[f'{split}_labels'].shape @@ -141,6 +141,10 @@ class PyTorchModelTrainer: @staticmethod def calc_n_epochs(n_obs: int, batch_size: int, n_iters: int) -> int: + """ + Calculates the number of epochs required to reach the maximum number + of iterations specified in the model training parameters. + """ n_batches = n_obs // batch_size epochs = n_iters // n_batches return epochs From 6f962362f2dc4f9e3959ede2df045fbc144b8412 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 9 Mar 2023 12:45:46 +0200 Subject: [PATCH 023/115] expand pytorch trainer documentation --- .../freqai/base_models/PyTorchModelTrainer.py | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 41d26e31a..525d543af 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -27,6 +27,27 @@ class PyTorchModelTrainer: ): """ A class for training PyTorch models. + Implements the training loop logic, load/save methods. + + fit method - training loop logic: + - Calculates the predicted output for the batch using the PyTorch model. + - Calculates the loss between the predicted and actual output using a loss function. + - Computes the gradients of the loss with respect to the model's parameters using + backpropagation. + - Updates the model's parameters using an optimizer. + + save method: + called by DataDrawer + - Saving any nn.Module state_dict + - Saving model_meta_data, this dict should contain any additional data that the + user needs to store. e.g class_names for classification models. + + load method: + currently DataDrawer is responsible for the actual loading. + when using continual_learning the DataDrawer will load the dict + (saved by self.save(path)). and this class will populate the necessary + state_dict of the self.model & self.optimizer and self.model_meta_data. + :param model: The PyTorch model to be trained. :param optimizer: The optimizer to use for training. @@ -34,10 +55,11 @@ class PyTorchModelTrainer: :param device: The device to use for training (e.g. 'cpu', 'cuda'). :param batch_size: The size of the batches to use during training. :param max_iters: The number of training iterations to run. - iteration here refers to the number of times we call - self.optimizer.step() . used to calculate n_epochs. + iteration here refers to the number of times we call self.optimizer.step(). + used to calculate n_epochs. :param eval_iters: The number of iterations used to estimate the loss. - :param init_model: A dictionary containing the initial model parameters. + :param init_model: A dictionary containing the initial model/optimizer + state_dict and model_meta_data saved by self.save() method. :param model_meta_data: Additional metadata about the model (optional). """ self.model = model From c9eee2944b2b498557d4e5a44028812b8a350920 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 9 Mar 2023 13:01:04 +0200 Subject: [PATCH 024/115] reformat documentation --- freqtrade/freqai/base_models/PyTorchModelTrainer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 525d543af..4a091f52c 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -55,8 +55,8 @@ class PyTorchModelTrainer: :param device: The device to use for training (e.g. 'cpu', 'cuda'). :param batch_size: The size of the batches to use during training. :param max_iters: The number of training iterations to run. - iteration here refers to the number of times we call self.optimizer.step(). - used to calculate n_epochs. + iteration here refers to the number of times we call + self.optimizer.step(). used to calculate n_epochs. :param eval_iters: The number of iterations used to estimate the loss. :param init_model: A dictionary containing the initial model/optimizer state_dict and model_meta_data saved by self.save() method. From 2ef11faba7cd7fc89805a10edfe735e81bc8aca2 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 9 Mar 2023 13:25:20 +0200 Subject: [PATCH 025/115] reformat documentation --- .../freqai/base_models/PyTorchModelTrainer.py | 46 ++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 4a091f52c..a934814ef 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -26,29 +26,6 @@ class PyTorchModelTrainer: model_meta_data: Dict[str, Any] = {}, ): """ - A class for training PyTorch models. - Implements the training loop logic, load/save methods. - - fit method - training loop logic: - - Calculates the predicted output for the batch using the PyTorch model. - - Calculates the loss between the predicted and actual output using a loss function. - - Computes the gradients of the loss with respect to the model's parameters using - backpropagation. - - Updates the model's parameters using an optimizer. - - save method: - called by DataDrawer - - Saving any nn.Module state_dict - - Saving model_meta_data, this dict should contain any additional data that the - user needs to store. e.g class_names for classification models. - - load method: - currently DataDrawer is responsible for the actual loading. - when using continual_learning the DataDrawer will load the dict - (saved by self.save(path)). and this class will populate the necessary - state_dict of the self.model & self.optimizer and self.model_meta_data. - - :param model: The PyTorch model to be trained. :param optimizer: The optimizer to use for training. :param criterion: The loss function to use for training. @@ -76,7 +53,11 @@ class PyTorchModelTrainer: def fit(self, data_dictionary: Dict[str, pd.DataFrame]): """ - General training loop. + - Calculates the predicted output for the batch using the PyTorch model. + - Calculates the loss between the predicted and actual output using a loss function. + - Computes the gradients of the loss with respect to the model's parameters using + backpropagation. + - Updates the model's parameters using an optimizer. """ data_loaders_dictionary = self.create_data_loaders_dictionary(data_dictionary) epochs = self.calc_n_epochs( @@ -172,6 +153,12 @@ class PyTorchModelTrainer: return epochs def save(self, path: Path): + """ + - Saving any nn.Module state_dict + - Saving model_meta_data, this dict should contain any additional data that the + user needs to store. e.g class_names for classification models. + """ + torch.save({ 'model_state_dict': self.model.state_dict(), 'optimizer_state_dict': self.optimizer.state_dict(), @@ -183,7 +170,14 @@ class PyTorchModelTrainer: return self.load_from_checkpoint(checkpoint) def load_from_checkpoint(self, checkpoint: Dict): - self.model.load_state_dict(checkpoint['model_state_dict']) - self.optimizer.load_state_dict(checkpoint['optimizer_state_dict']) + """ + when using continual_learning, DataDrawer will load the dictionary + (containing state dicts and model_meta_data) by calling torch.load(path). + you can access this dict from any class that inherits IFreqaiModel by calling + get_init_model method. + """ + + self.model.load_state_dict(checkpoint["model_state_dict"]) + self.optimizer.load_state_dict(checkpoint["optimizer_state_dict"]) self.model_meta_data = checkpoint["model_meta_data"] return self From e88a0d52482ed8f7d955eaae7055a90d76543c3a Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 9 Mar 2023 13:29:11 +0200 Subject: [PATCH 026/115] convert single quotes to double quotes --- .../freqai/base_models/BasePyTorchModel.py | 6 ++--- .../freqai/base_models/PyTorchModelTrainer.py | 23 ++++++++++--------- .../PyTorchClassifierMultiTarget.py | 2 +- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/freqtrade/freqai/base_models/BasePyTorchModel.py b/freqtrade/freqai/base_models/BasePyTorchModel.py index 8e608ee1a..d6372fa36 100644 --- a/freqtrade/freqai/base_models/BasePyTorchModel.py +++ b/freqtrade/freqai/base_models/BasePyTorchModel.py @@ -19,9 +19,9 @@ class BasePyTorchModel(IFreqaiModel): """ def __init__(self, **kwargs): - super().__init__(config=kwargs['config']) - self.dd.model_type = 'pytorch' - self.device = 'cuda' if torch.cuda.is_available() else 'cpu' + super().__init__(config=kwargs["config"]) + self.dd.model_type = "pytorch" + self.device = "cuda" if torch.cuda.is_available() else "cpu" def train( self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index a934814ef..0ca28d2e9 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -61,7 +61,7 @@ class PyTorchModelTrainer: """ data_loaders_dictionary = self.create_data_loaders_dictionary(data_dictionary) epochs = self.calc_n_epochs( - n_obs=len(data_dictionary['train_features']), + n_obs=len(data_dictionary["train_features"]), batch_size=self.batch_size, n_iters=self.max_iters ) @@ -73,7 +73,7 @@ class PyTorchModelTrainer: f" train loss {losses['train']:.4f} ; test loss {losses['test']:.4f}" ) # training - for batch_data in data_loaders_dictionary['train']: + for batch_data in data_loaders_dictionary["train"]: xb, yb = batch_data xb = xb.to(self.device) yb = yb.to(self.device) @@ -93,12 +93,12 @@ class PyTorchModelTrainer: self.model.eval() epochs = self.calc_n_epochs( - n_obs=len(data_dictionary['test_features']), + n_obs=len(data_dictionary["test_features"]), batch_size=self.batch_size, n_iters=self.eval_iters ) loss_dictionary = {} - for split in ['train', 'test']: + for split in ["train", "test"]: losses = torch.zeros(epochs) for i, batch in enumerate(data_loader_dictionary[split]): xb, yb = batch @@ -121,12 +121,12 @@ class PyTorchModelTrainer: Converts the input data to PyTorch tensors using a data loader. """ data_loader_dictionary = {} - for split in ['train', 'test']: - labels_shape = data_dictionary[f'{split}_labels'].shape + for split in ["train", "test"]: + labels_shape = data_dictionary[f"{split}_labels"].shape labels_view = labels_shape[0] if labels_shape[1] == 1 else labels_shape dataset = TensorDataset( - torch.from_numpy(data_dictionary[f'{split}_features'].values).float(), - torch.from_numpy(data_dictionary[f'{split}_labels'].astype(float).values) + torch.from_numpy(data_dictionary[f"{split}_features"].values).float(), + torch.from_numpy(data_dictionary[f"{split}_labels"].astype(float).values) .long() .view(labels_view) ) @@ -148,6 +148,7 @@ class PyTorchModelTrainer: Calculates the number of epochs required to reach the maximum number of iterations specified in the model training parameters. """ + n_batches = n_obs // batch_size epochs = n_iters // n_batches return epochs @@ -160,9 +161,9 @@ class PyTorchModelTrainer: """ torch.save({ - 'model_state_dict': self.model.state_dict(), - 'optimizer_state_dict': self.optimizer.state_dict(), - 'model_meta_data': self.model_meta_data, + "model_state_dict": self.model.state_dict(), + "optimizer_state_dict": self.optimizer.state_dict(), + "model_meta_data": self.model_meta_data, }, path) def load_from_file(self, path: Path): diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index e8326ffe9..a98643b3f 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -59,7 +59,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): self.init_class_names_to_index_mapping(self.class_names) self.encode_classes_name(data_dictionary, dk) - n_features = data_dictionary['train_features'].shape[-1] + n_features = data_dictionary["train_features"].shape[-1] model = PyTorchMLPModel( input_dim=n_features, hidden_dim=self.n_hidden, From 8a9f2aedbbfa9e4019f132751c23d48f670651b0 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 9 Mar 2023 14:55:52 +0200 Subject: [PATCH 027/115] improve documentation --- .../prediction_models/PyTorchClassifierMultiTarget.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index a98643b3f..a5b8b1591 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -32,6 +32,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): dict: A dictionary mapping class names to their corresponding indices. dict: A dictionary mapping indices to their corresponding class names. """ + super().__init__(**kwargs) model_training_parameters = self.freqai_info["model_training_parameters"] self.n_hidden = model_training_parameters.get("n_hidden", 1024) @@ -50,6 +51,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): :raises ValueError: If self.class_names is not defined in the parent class. """ + if not hasattr(self, "class_names"): raise ValueError( "Missing attribute: self.class_names " @@ -93,7 +95,9 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): :pred_df: dataframe containing the predictions :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove data (NaNs) or felt uncertain about data (PCA and DI index) + :raises ValueError: if 'class_name' doesn't exist in model meta_data. """ + class_names = self.model.model_meta_data.get("class_names", None) if not class_names: raise ValueError( @@ -128,6 +132,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): encode class name str -> int assuming first column of *_labels data frame to contain class names """ + target_column_name = dk.label_list[0] for split in ["train", "test"]: label_df = data_dictionary[f"{split}_labels"] @@ -148,6 +153,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): """ decode class name int -> str """ + return list(map(lambda x: self.index_to_class_name[x.item()], classes)) def init_class_names_to_index_mapping(self, class_names): From 1cf0e7be2495bd879025b44e19afddeb2c7d3508 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 12 Mar 2023 12:48:15 +0200 Subject: [PATCH 028/115] use one iteration on all test and train data for evaluation --- .../freqai/base_models/PyTorchModelTrainer.py | 28 +++++++++---------- .../PyTorchClassifierMultiTarget.py | 4 +-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 0ca28d2e9..99ee44e3b 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import Any, Dict +from typing import Any, Dict, Optional import pandas as pd import torch @@ -21,7 +21,7 @@ class PyTorchModelTrainer: device: str, batch_size: int, max_iters: int, - eval_iters: int, + max_n_eval_batches: int, init_model: Dict, model_meta_data: Dict[str, Any] = {}, ): @@ -34,7 +34,7 @@ class PyTorchModelTrainer: :param max_iters: The number of training iterations to run. iteration here refers to the number of times we call self.optimizer.step(). used to calculate n_epochs. - :param eval_iters: The number of iterations used to estimate the loss. + :param max_n_eval_batches: The maximum number batches to use for evaluation. :param init_model: A dictionary containing the initial model/optimizer state_dict and model_meta_data saved by self.save() method. :param model_meta_data: Additional metadata about the model (optional). @@ -46,7 +46,7 @@ class PyTorchModelTrainer: self.device = device self.max_iters = max_iters self.batch_size = batch_size - self.eval_iters = eval_iters + self.max_n_eval_batches = max_n_eval_batches if init_model: self.load_from_checkpoint(init_model) @@ -67,7 +67,7 @@ class PyTorchModelTrainer: ) for epoch in range(epochs): # evaluation - losses = self.estimate_loss(data_loaders_dictionary, data_dictionary) + losses = self.estimate_loss(data_loaders_dictionary, self.max_n_eval_batches) logger.info( f"epoch ({epoch}/{epochs}):" f" train loss {losses['train']:.4f} ; test loss {losses['test']:.4f}" @@ -88,27 +88,27 @@ class PyTorchModelTrainer: def estimate_loss( self, data_loader_dictionary: Dict[str, DataLoader], - data_dictionary: Dict[str, pd.DataFrame] + max_n_eval_batches: Optional[int] ) -> Dict[str, float]: self.model.eval() - epochs = self.calc_n_epochs( - n_obs=len(data_dictionary["test_features"]), - batch_size=self.batch_size, - n_iters=self.eval_iters - ) loss_dictionary = {} + n_batches = 0 for split in ["train", "test"]: - losses = torch.zeros(epochs) + losses = [] for i, batch in enumerate(data_loader_dictionary[split]): + if max_n_eval_batches and i > max_n_eval_batches: + n_batches += 1 + break + xb, yb = batch xb = xb.to(self.device) yb = yb.to(self.device) yb_pred = self.model(xb) loss = self.criterion(yb_pred, yb) - losses[i] = loss.item() + losses.append(loss.item()) - loss_dictionary[split] = losses.mean().item() + loss_dictionary[split] = sum(losses) / len(losses) self.model.train() return loss_dictionary diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index a5b8b1591..f951778bf 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -39,7 +39,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): self.max_iters = model_training_parameters.get("max_iters", 100) self.batch_size = model_training_parameters.get("batch_size", 64) self.learning_rate = model_training_parameters.get("learning_rate", 3e-4) - self.eval_iters = model_training_parameters.get("eval_iters", 10) + self.max_n_eval_batches = model_training_parameters.get("max_n_eval_batches", None) self.class_name_to_index = None self.index_to_class_name = None @@ -79,7 +79,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): device=self.device, batch_size=self.batch_size, max_iters=self.max_iters, - eval_iters=self.eval_iters, + max_n_eval_batches=self.max_n_eval_batches, init_model=init_model ) trainer.fit(data_dictionary) From f9fdf1c31b7d437f269aed33626d05c6bae6bf10 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 12 Mar 2023 14:31:08 +0200 Subject: [PATCH 029/115] generalize mlp model --- .../PyTorchClassifierMultiTarget.py | 19 +++++----- .../prediction_models/PyTorchMLPModel.py | 38 ++++++++++++++++--- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index f951778bf..be42fd8e6 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Dict, List, Tuple +from typing import Any, Dict, List, Tuple, Optional import numpy as np import numpy.typing as npt @@ -34,12 +34,13 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): """ super().__init__(**kwargs) - model_training_parameters = self.freqai_info["model_training_parameters"] - self.n_hidden = model_training_parameters.get("n_hidden", 1024) - self.max_iters = model_training_parameters.get("max_iters", 100) - self.batch_size = model_training_parameters.get("batch_size", 64) - self.learning_rate = model_training_parameters.get("learning_rate", 3e-4) - self.max_n_eval_batches = model_training_parameters.get("max_n_eval_batches", None) + trainer_kwargs = self.freqai_info.get("trainer_kwargs", {}) + self.n_hidden: int = trainer_kwargs.get("n_hidden", 1024) + self.max_iters: int = trainer_kwargs.get("max_iters", 100) + self.batch_size: int = trainer_kwargs.get("batch_size", 64) + self.learning_rate: float = trainer_kwargs.get("learning_rate", 3e-4) + self.max_n_eval_batches: Optional[int] = trainer_kwargs.get("max_n_eval_batches", None) + self.model_kwargs: Dict = trainer_kwargs.get("model_kwargs", {}) self.class_name_to_index = None self.index_to_class_name = None @@ -64,8 +65,8 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): n_features = data_dictionary["train_features"].shape[-1] model = PyTorchMLPModel( input_dim=n_features, - hidden_dim=self.n_hidden, - output_dim=len(self.class_names) + output_dim=len(self.class_names), + **self.model_kwargs ) model.to(self.device) optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index 1f13ca069..91e496c5d 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -8,18 +8,46 @@ logger = logging.getLogger(__name__) class PyTorchMLPModel(nn.Module): - def __init__(self, input_dim: int, hidden_dim: int, output_dim: int): + def __init__(self, input_dim: int, output_dim: int, **kwargs): super(PyTorchMLPModel, self).__init__() + hidden_dim: int = kwargs.get("hidden_dim", 1024) + dropout_percent: int = kwargs.get("dropout_percent", 0.2) + n_layer: int = kwargs.get("n_layer", 1) self.input_layer = nn.Linear(input_dim, hidden_dim) - self.hidden_layer = nn.Linear(hidden_dim, hidden_dim) + self.blocks = nn.Sequential(*[Block(hidden_dim, dropout_percent) for _ in range(n_layer)]) self.output_layer = nn.Linear(hidden_dim, output_dim) self.relu = nn.ReLU() - self.dropout = nn.Dropout(p=0.2) + self.dropout = nn.Dropout(p=dropout_percent) def forward(self, x: Tensor) -> Tensor: x = self.relu(self.input_layer(x)) x = self.dropout(x) - x = self.relu(self.hidden_layer(x)) - x = self.dropout(x) + x = self.relu(self.blocks(x)) logits = self.output_layer(x) return logits + + +class Block(nn.Module): + def __init__(self, hidden_dim: int, dropout_percent: int): + super(Block, self).__init__() + self.ff = FeedForward(hidden_dim) + self.dropout = nn.Dropout(p=dropout_percent) + self.ln = nn.LayerNorm(hidden_dim) + + def forward(self, x): + x = self.dropout(self.ff(x)) + x = self.ln(x) + return x + + +class FeedForward(nn.Module): + def __init__(self, hidden_dim: int): + super(FeedForward, self).__init__() + self.net = nn.Sequential( + nn.Linear(hidden_dim, hidden_dim), + nn.ReLU(), + nn.Linear(hidden_dim, hidden_dim), + ) + + def forward(self, x): + return self.net(x) From cb17b36981ba8513c5d0a370864b0cfe312c0c52 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 12 Mar 2023 14:50:08 +0200 Subject: [PATCH 030/115] simplify file_type check comparisons --- freqtrade/freqai/data_drawer.py | 3 +-- freqtrade/freqai/freqai_interface.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index 8d31586fe..fd839ad2f 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -447,8 +447,7 @@ class FreqaiDataDrawer: elif self.model_type == 'keras': model.save(save_path / f"{dk.model_filename}_model.h5") elif ('stable_baselines' in self.model_type or - 'sb3_contrib' == self.model_type or - 'pytorch' == self.model_type): + self.model_type in ['sb3_contrib', 'pytorch']): model.save(save_path / f"{dk.model_filename}_model.zip") if dk.svm_model is not None: diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 79bd7d672..3d06745f9 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -564,8 +564,7 @@ class IFreqaiModel(ABC): elif self.dd.model_type == 'keras': file_type = ".h5" elif ('stable_baselines' in self.dd.model_type or - 'sb3_contrib' == self.dd.model_type or - 'pytorch' == self.dd.model_type): + self.dd.model_type in ['sb3_contrib', 'pytorch']): file_type = ".zip" path_to_modelfile = Path(dk.data_path / f"{dk.model_filename}_model{file_type}") From 0012fe36ca6e6a556936039aad77d054c0cdef25 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 12 Mar 2023 16:16:04 +0200 Subject: [PATCH 031/115] sort imports --- .../freqai/prediction_models/PyTorchClassifierMultiTarget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index be42fd8e6..2855dda33 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Dict, List, Tuple, Optional +from typing import Any, Dict, List, Optional, Tuple import numpy as np import numpy.typing as npt From 523a58d3d67ee058df60de0c059053bfe307f199 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 13 Mar 2023 00:16:44 +0200 Subject: [PATCH 032/115] simplify statement for pytorch file_type extension --- freqtrade/freqai/data_drawer.py | 3 +-- freqtrade/freqai/freqai_interface.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index fd839ad2f..aaf9a869c 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -446,8 +446,7 @@ class FreqaiDataDrawer: dump(model, save_path / f"{dk.model_filename}_model.joblib") elif self.model_type == 'keras': model.save(save_path / f"{dk.model_filename}_model.h5") - elif ('stable_baselines' in self.model_type or - self.model_type in ['sb3_contrib', 'pytorch']): + elif self.model_type in ["stable_baselines", "sb3_contrib", "pytorch"]: model.save(save_path / f"{dk.model_filename}_model.zip") if dk.svm_model is not None: diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 3d06745f9..7c45d4642 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -563,8 +563,7 @@ class IFreqaiModel(ABC): file_type = ".joblib" elif self.dd.model_type == 'keras': file_type = ".h5" - elif ('stable_baselines' in self.dd.model_type or - self.dd.model_type in ['sb3_contrib', 'pytorch']): + elif self.dd.model_type in ["stable_baselines", "sb3_contrib", "pytorch"]: file_type = ".zip" path_to_modelfile = Path(dk.data_path / f"{dk.model_filename}_model{file_type}") From b927c9dc01277368f3a944779f81ece329d24080 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 13 Mar 2023 00:17:34 +0200 Subject: [PATCH 033/115] remove train loss calculation from estimate_loss --- .../freqai/base_models/PyTorchModelTrainer.py | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 99ee44e3b..1b328f4fe 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -66,14 +66,9 @@ class PyTorchModelTrainer: n_iters=self.max_iters ) for epoch in range(epochs): - # evaluation - losses = self.estimate_loss(data_loaders_dictionary, self.max_n_eval_batches) - logger.info( - f"epoch ({epoch}/{epochs}):" - f" train loss {losses['train']:.4f} ; test loss {losses['test']:.4f}" - ) # training - for batch_data in data_loaders_dictionary["train"]: + losses = [] + for i, batch_data in enumerate(data_loaders_dictionary["train"]): xb, yb = batch_data xb = xb.to(self.device) yb = yb.to(self.device) @@ -83,35 +78,40 @@ class PyTorchModelTrainer: self.optimizer.zero_grad(set_to_none=True) loss.backward() self.optimizer.step() + losses.append(loss.item()) + train_loss = sum(losses) / len(losses) + + # evaluation + test_loss = self.estimate_loss(data_loaders_dictionary, self.max_n_eval_batches, "test") + logger.info( + f"epoch ({epoch}/{epochs}):" + f" train loss {train_loss:.4f} ; test loss {test_loss:.4f}" + ) @torch.no_grad() def estimate_loss( self, data_loader_dictionary: Dict[str, DataLoader], - max_n_eval_batches: Optional[int] - ) -> Dict[str, float]: - + max_n_eval_batches: Optional[int], + split: str, + ) -> float: self.model.eval() - loss_dictionary = {} n_batches = 0 - for split in ["train", "test"]: - losses = [] - for i, batch in enumerate(data_loader_dictionary[split]): - if max_n_eval_batches and i > max_n_eval_batches: - n_batches += 1 - break + losses = [] + for i, batch in enumerate(data_loader_dictionary[split]): + if max_n_eval_batches and i > max_n_eval_batches: + n_batches += 1 + break - xb, yb = batch - xb = xb.to(self.device) - yb = yb.to(self.device) - yb_pred = self.model(xb) - loss = self.criterion(yb_pred, yb) - losses.append(loss.item()) - - loss_dictionary[split] = sum(losses) / len(losses) + xb, yb = batch + xb = xb.to(self.device) + yb = yb.to(self.device) + yb_pred = self.model(xb) + loss = self.criterion(yb_pred, yb) + losses.append(loss.item()) self.model.train() - return loss_dictionary + return sum(losses) / len(losses) def create_data_loaders_dictionary( self, From b6096efadd575112842526dfe67d66b6fdcf9a5a Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 13 Mar 2023 00:35:14 +0200 Subject: [PATCH 034/115] logging change --- freqtrade/freqai/base_models/PyTorchModelTrainer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 1b328f4fe..90fb472e5 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -84,7 +84,7 @@ class PyTorchModelTrainer: # evaluation test_loss = self.estimate_loss(data_loaders_dictionary, self.max_n_eval_batches, "test") logger.info( - f"epoch ({epoch}/{epochs}):" + f"epoch {epoch}/{epochs}:" f" train loss {train_loss:.4f} ; test loss {test_loss:.4f}" ) From d7ea75082384c160217a7382798b42e48c6e70fb Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 13 Mar 2023 00:35:51 +0200 Subject: [PATCH 035/115] revert to using model_training_parameters --- .../PyTorchClassifierMultiTarget.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index 2855dda33..3abc56fb1 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -34,13 +34,15 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): """ super().__init__(**kwargs) - trainer_kwargs = self.freqai_info.get("trainer_kwargs", {}) - self.n_hidden: int = trainer_kwargs.get("n_hidden", 1024) - self.max_iters: int = trainer_kwargs.get("max_iters", 100) - self.batch_size: int = trainer_kwargs.get("batch_size", 64) - self.learning_rate: float = trainer_kwargs.get("learning_rate", 3e-4) - self.max_n_eval_batches: Optional[int] = trainer_kwargs.get("max_n_eval_batches", None) - self.model_kwargs: Dict = trainer_kwargs.get("model_kwargs", {}) + model_training_params = self.freqai_info.get("model_training_parameters", {}) + self.n_hidden: int = model_training_params.get("n_hidden", 1024) + self.max_iters: int = model_training_params.get("max_iters", 100) + self.batch_size: int = model_training_params.get("batch_size", 64) + self.learning_rate: float = model_training_params.get("learning_rate", 3e-4) + self.max_n_eval_batches: Optional[int] = model_training_params.get( + "max_n_eval_batches", None + ) + self.model_kwargs: Dict = model_training_params.get("model_kwargs", {}) self.class_name_to_index = None self.index_to_class_name = None From 9c8c30b0e8b804f2b9b67de8d752c532313e59d7 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 13 Mar 2023 17:17:00 +0200 Subject: [PATCH 036/115] add test --- tests/freqai/test_freqai_interface.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index f8bee3659..da3c28de8 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -52,7 +52,8 @@ def can_run_model(model: str) -> None: ('ReinforcementLearner_multiproc', False, False, False, True, False, 0), ('ReinforcementLearner_test_3ac', False, False, False, False, False, 0), ('ReinforcementLearner_test_3ac', False, False, False, True, False, 0), - ('ReinforcementLearner_test_4ac', False, False, False, True, False, 0) + ('ReinforcementLearner_test_4ac', False, False, False, True, False, 0), + ('PyTorchClassifierMultiTarget', False, False, False, True, False, 0) ]) def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, dbscan, float32, can_short, shuffle, buffer): @@ -85,6 +86,21 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, if 'test_3ac' in model or 'test_4ac' in model: freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models") + if 'PyTorchClassifierMultiTarget' in model: + model_save_ext = 'zip' + freqai_conf['freqai']['model_training_parameters'].update({ + "n_hidden": 1024, + "max_iters": 100, + "batch_size": 64, + "learning_rate": 3e-4, + "max_n_eval_batches": None, + "model_kwargs": { + "hidden_dim": 1024, + "dropout_percent": 0.2, + "n_layer": 1, + } + }) + strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) strategy.dp = DataProvider(freqai_conf, exchange) From 918889a2bdbe53d4c502da8d4ce89f8380bda357 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 13 Mar 2023 20:09:12 +0200 Subject: [PATCH 037/115] reduce mlp number of parameters for testing --- .../freqai/prediction_models/PyTorchClassifierMultiTarget.py | 1 - tests/freqai/test_freqai_interface.py | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index 3abc56fb1..edafb3b7a 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -35,7 +35,6 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): super().__init__(**kwargs) model_training_params = self.freqai_info.get("model_training_parameters", {}) - self.n_hidden: int = model_training_params.get("n_hidden", 1024) self.max_iters: int = model_training_params.get("max_iters", 100) self.batch_size: int = model_training_params.get("batch_size", 64) self.learning_rate: float = model_training_params.get("learning_rate", 3e-4) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index da3c28de8..3b31012b2 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -89,13 +89,12 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, if 'PyTorchClassifierMultiTarget' in model: model_save_ext = 'zip' freqai_conf['freqai']['model_training_parameters'].update({ - "n_hidden": 1024, - "max_iters": 100, + "max_iters": 1, "batch_size": 64, "learning_rate": 3e-4, "max_n_eval_batches": None, "model_kwargs": { - "hidden_dim": 1024, + "hidden_dim": 32, "dropout_percent": 0.2, "n_layer": 1, } From 366740885a85aa9111aed8cfa8a92d9c5b862e07 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 13 Mar 2023 20:18:26 +0200 Subject: [PATCH 038/115] reduce mlp number of parameters for testing --- tests/freqai/test_freqai_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 3b31012b2..f34659398 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -92,7 +92,7 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, "max_iters": 1, "batch_size": 64, "learning_rate": 3e-4, - "max_n_eval_batches": None, + "max_n_eval_batches": 1, "model_kwargs": { "hidden_dim": 32, "dropout_percent": 0.2, From 4550447409a7895de8627718d03bd6bfd14577df Mon Sep 17 00:00:00 2001 From: robcaulk Date: Tue, 14 Mar 2023 21:13:30 +0100 Subject: [PATCH 039/115] cheat flake8 for now until we can refactor save into the model class --- freqtrade/freqai/data_drawer.py | 4 ++-- freqtrade/freqai/freqai_interface.py | 2 +- .../freqai/prediction_models/PyTorchClassifierMultiTarget.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index aaf9a869c..c8dadb171 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -446,7 +446,7 @@ class FreqaiDataDrawer: dump(model, save_path / f"{dk.model_filename}_model.joblib") elif self.model_type == 'keras': model.save(save_path / f"{dk.model_filename}_model.h5") - elif self.model_type in ["stable_baselines", "sb3_contrib", "pytorch"]: + elif self.model_type in ["stable_baselines3", "sb3_contrib", "pytorch"]: model.save(save_path / f"{dk.model_filename}_model.zip") if dk.svm_model is not None: @@ -496,7 +496,7 @@ class FreqaiDataDrawer: dk.training_features_list = dk.data["training_features_list"] dk.label_list = dk.data["label_list"] - def load_data(self, coin: str, dk: FreqaiDataKitchen) -> Any: + def load_data(self, coin: str, dk: FreqaiDataKitchen) -> Any: # noqa: C901 """ loads all data required to make a prediction on a sub-train time range :returns: diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 7c45d4642..8a1ac436b 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -563,7 +563,7 @@ class IFreqaiModel(ABC): file_type = ".joblib" elif self.dd.model_type == 'keras': file_type = ".h5" - elif self.dd.model_type in ["stable_baselines", "sb3_contrib", "pytorch"]: + elif self.dd.model_type in ["stable_baselines3", "sb3_contrib", "pytorch"]: file_type = ".zip" path_to_modelfile = Path(dk.data_path / f"{dk.model_filename}_model{file_type}") diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index edafb3b7a..967199c12 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -41,7 +41,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): self.max_n_eval_batches: Optional[int] = model_training_params.get( "max_n_eval_batches", None ) - self.model_kwargs: Dict = model_training_params.get("model_kwargs", {}) + self.model_kwargs: Dict[str, any] = model_training_params.get("model_kwargs", {}) self.class_name_to_index = None self.index_to_class_name = None From 244662b1a45a3d5c394936400d805215bf7508e3 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sat, 18 Mar 2023 14:12:31 +0200 Subject: [PATCH 040/115] set class names attribute in the general classifier testing strategy --- tests/strategy/strats/freqai_test_classifier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/strategy/strats/freqai_test_classifier.py b/tests/strategy/strats/freqai_test_classifier.py index 61b9f0c37..a68a87b2a 100644 --- a/tests/strategy/strats/freqai_test_classifier.py +++ b/tests/strategy/strats/freqai_test_classifier.py @@ -82,7 +82,7 @@ class freqai_test_classifier(IStrategy): return dataframe def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs): - + self.freqai.class_names = ["down", "up"] dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-100) > dataframe["close"], 'up', 'down') From fab9ff129461141dfaa4a3b69d4dccb4e922e955 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sat, 18 Mar 2023 15:27:38 +0200 Subject: [PATCH 041/115] fix tests --- tests/freqai/test_freqai_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index f34659398..c35d1afb4 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -52,8 +52,7 @@ def can_run_model(model: str) -> None: ('ReinforcementLearner_multiproc', False, False, False, True, False, 0), ('ReinforcementLearner_test_3ac', False, False, False, False, False, 0), ('ReinforcementLearner_test_3ac', False, False, False, True, False, 0), - ('ReinforcementLearner_test_4ac', False, False, False, True, False, 0), - ('PyTorchClassifierMultiTarget', False, False, False, True, False, 0) + ('ReinforcementLearner_test_4ac', False, False, False, True, False, 0) ]) def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, dbscan, float32, can_short, shuffle, buffer): @@ -183,6 +182,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s 'CatboostClassifier', 'XGBoostClassifier', 'XGBoostRFClassifier', + 'PyTorchClassifierMultiTarget', ]) def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): if (is_arm() or is_py11()) and model == 'CatboostClassifier': From a49f62eecbb9838e211300964c61ba889214cb80 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sat, 18 Mar 2023 20:51:30 +0200 Subject: [PATCH 042/115] classifier test - set model file extension --- tests/freqai/test_freqai_interface.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index c35d1afb4..a5fe9b90b 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -210,8 +210,16 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): freqai.extract_data_and_train_model(new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) + if freqai.dd.model_type == 'joblib': + model_file_extension = ".joblib" + elif freqai.dd.model_type == "pytorch": + model_file_extension = ".zip" + else: + raise Exception(f"Unsupported model type: {freqai.dd.model_type}," + f" can't assign model_file_extension") - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").exists() + assert Path(freqai.dk.data_path / + f"{freqai.dk.model_filename}_model{model_file_extension}").exists() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").exists() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_trained_df.pkl").exists() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_svm_model.joblib").exists() From 366c148c10769e745e7748f02503f0ccecca994f Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 14:38:49 +0200 Subject: [PATCH 043/115] create children class to PyTorchClassifier to implement the fit method where we initialize the trainer and model objects --- .../freqai/base_models/PyTorchModelTrainer.py | 19 ++- .../prediction_models/MLPPyTorchClassifier.py | 81 ++++++++++++ ...rget.py => PyTorchClassifierClassifier.py} | 125 ++++++++---------- tests/freqai/test_freqai_interface.py | 8 +- 4 files changed, 146 insertions(+), 87 deletions(-) create mode 100644 freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py rename freqtrade/freqai/prediction_models/{PyTorchClassifierMultiTarget.py => PyTorchClassifierClassifier.py} (53%) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 90fb472e5..f91b44924 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -19,35 +19,32 @@ class PyTorchModelTrainer: optimizer: Optimizer, criterion: nn.Module, device: str, - batch_size: int, - max_iters: int, - max_n_eval_batches: int, init_model: Dict, model_meta_data: Dict[str, Any] = {}, + **kwargs ): """ :param model: The PyTorch model to be trained. :param optimizer: The optimizer to use for training. :param criterion: The loss function to use for training. :param device: The device to use for training (e.g. 'cpu', 'cuda'). - :param batch_size: The size of the batches to use during training. + :param init_model: A dictionary containing the initial model/optimizer + state_dict and model_meta_data saved by self.save() method. + :param model_meta_data: Additional metadata about the model (optional). :param max_iters: The number of training iterations to run. iteration here refers to the number of times we call self.optimizer.step(). used to calculate n_epochs. + :param batch_size: The size of the batches to use during training. :param max_n_eval_batches: The maximum number batches to use for evaluation. - :param init_model: A dictionary containing the initial model/optimizer - state_dict and model_meta_data saved by self.save() method. - :param model_meta_data: Additional metadata about the model (optional). """ self.model = model self.optimizer = optimizer self.criterion = criterion self.model_meta_data = model_meta_data self.device = device - self.max_iters = max_iters - self.batch_size = batch_size - self.max_n_eval_batches = max_n_eval_batches - + self.max_iters: int = kwargs.get("max_iters", 100) + self.batch_size: int = kwargs.get("batch_size", 64) + self.max_n_eval_batches: Optional[int] = kwargs.get("max_n_eval_batches", None) if init_model: self.load_from_checkpoint(init_model) diff --git a/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py b/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py new file mode 100644 index 000000000..d6be8c1df --- /dev/null +++ b/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py @@ -0,0 +1,81 @@ +from typing import Any, Dict + +from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.prediction_models.PyTorchClassifierClassifier import PyTorchClassifier +from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel + +import torch + + +class MLPPyTorchClassifier(PyTorchClassifier): + """ + This class implements the fit method of IFreqaiModel. + int the fit method we initialize the model and trainer objects. + the only requirement from the model is to be aligned to PyTorchClassifier + predict method that expects the model to predict tensor of type long. + the trainer defines the training loop. + + parameters are passed via `model_training_parameters` under the freqai + section in the config file. e.g: + { + ... + "freqai": { + ... + "model_training_parameters" : { + "learning_rate": 3e-4, + "trainer_kwargs": { + "max_iters": 5000, + "batch_size": 64, + "max_n_eval_batches": None, + }, + "model_kwargs": { + "hidden_dim": 512, + "dropout_percent": 0.2, + "n_layer": 1, + }, + } + } + } + + + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + model_training_params = self.freqai_info.get("model_training_parameters", {}) + self.learning_rate: float = model_training_params.get("learning_rate", 3e-4) + self.model_kwargs: Dict[str, any] = model_training_params.get("model_kwargs", {}) + self.trainer_kwargs: Dict[str, any] = model_training_params.get("trainer_kwargs", {}) + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + """ + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary constructed by DataHandler to hold + all the training and test data/labels. + :raises ValueError: If self.class_names is not defined in the parent class. + """ + + class_names = self.get_class_names() + self.convert_label_column_to_int(data_dictionary, dk, class_names) + n_features = data_dictionary["train_features"].shape[-1] + model = PyTorchMLPModel( + input_dim=n_features, + output_dim=len(class_names), + **self.model_kwargs + ) + model.to(self.device) + optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) + criterion = torch.nn.CrossEntropyLoss() + init_model = self.get_init_model(dk.pair) + trainer = PyTorchModelTrainer( + model=model, + optimizer=optimizer, + criterion=criterion, + model_meta_data={"class_names": class_names}, + device=self.device, + init_model=init_model, + **self.trainer_kwargs, + ) + trainer.fit(data_dictionary) + return trainer diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierClassifier.py similarity index 53% rename from freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py rename to freqtrade/freqai/prediction_models/PyTorchClassifierClassifier.py index 967199c12..0be10b31e 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierClassifier.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Dict, List, Optional, Tuple +from typing import Dict, List, Tuple import numpy as np import numpy.typing as npt @@ -10,17 +10,16 @@ from torch.nn import functional as F from freqtrade.exceptions import OperationalException from freqtrade.freqai.base_models.BasePyTorchModel import BasePyTorchModel -from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel logger = logging.getLogger(__name__) -class PyTorchClassifierMultiTarget(BasePyTorchModel): +class PyTorchClassifier(BasePyTorchModel): """ - A PyTorch implementation of a multi-target classifier. + A PyTorch implementation of a classifier. + User must implement fit method """ def __init__(self, **kwargs): """ @@ -34,59 +33,9 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): """ super().__init__(**kwargs) - model_training_params = self.freqai_info.get("model_training_parameters", {}) - self.max_iters: int = model_training_params.get("max_iters", 100) - self.batch_size: int = model_training_params.get("batch_size", 64) - self.learning_rate: float = model_training_params.get("learning_rate", 3e-4) - self.max_n_eval_batches: Optional[int] = model_training_params.get( - "max_n_eval_batches", None - ) - self.model_kwargs: Dict[str, any] = model_training_params.get("model_kwargs", {}) self.class_name_to_index = None self.index_to_class_name = None - def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: - """ - User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. - :raises ValueError: If self.class_names is not defined in the parent class. - - """ - - if not hasattr(self, "class_names"): - raise ValueError( - "Missing attribute: self.class_names " - "set self.freqai.class_names = [\"class a\", \"class b\", \"class c\"] " - "inside IStrategy.set_freqai_targets method." - ) - - self.init_class_names_to_index_mapping(self.class_names) - self.encode_classes_name(data_dictionary, dk) - n_features = data_dictionary["train_features"].shape[-1] - model = PyTorchMLPModel( - input_dim=n_features, - output_dim=len(self.class_names), - **self.model_kwargs - ) - model.to(self.device) - optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) - criterion = torch.nn.CrossEntropyLoss() - init_model = self.get_init_model(dk.pair) - trainer = PyTorchModelTrainer( - model=model, - optimizer=optimizer, - criterion=criterion, - model_meta_data={"class_names": self.class_names}, - device=self.device, - batch_size=self.batch_size, - max_iters=self.max_iters, - max_n_eval_batches=self.max_n_eval_batches, - init_model=init_model - ) - trainer.fit(data_dictionary) - return trainer - def predict( self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs ) -> Tuple[DataFrame, npt.NDArray[np.int_]]: @@ -97,7 +46,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): :pred_df: dataframe containing the predictions :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove data (NaNs) or felt uncertain about data (PCA and DI index) - :raises ValueError: if 'class_name' doesn't exist in model meta_data. + :raises ValueError: if 'class_names' doesn't exist in model meta_data. """ class_names = self.model.model_meta_data.get("class_names", None) @@ -106,7 +55,9 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): "Missing class names. " "self.model.model_meta_data[\"class_names\"] is None." ) - self.init_class_names_to_index_mapping(class_names) + + if not self.class_name_to_index: + self.init_class_names_to_index_mapping(class_names) dk.find_features(unfiltered_df) filtered_df, _ = dk.filter_features( @@ -116,49 +67,77 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): dk.data_dictionary["prediction_features"] = filtered_df self.data_cleaning_predict(dk) - dk.data_dictionary["prediction_features"] = torch.tensor( - dk.data_dictionary["prediction_features"].values - ).float().to(self.device) + x = torch.from_numpy(dk.data_dictionary["prediction_features"].values)\ + .float()\ + .to(self.device) - logits = self.model.model(dk.data_dictionary["prediction_features"]) + logits = self.model.model(x) probs = F.softmax(logits, dim=-1) predicted_classes = torch.argmax(probs, dim=-1) - predicted_classes_str = self.decode_classes_name(predicted_classes) + predicted_classes_str = self.decode_class_names(predicted_classes) pred_df_prob = DataFrame(probs.detach().numpy(), columns=class_names) pred_df = DataFrame(predicted_classes_str, columns=[dk.label_list[0]]) pred_df = pd.concat([pred_df, pred_df_prob], axis=1) return (pred_df, dk.do_predict) - def encode_classes_name(self, data_dictionary: Dict[str, pd.DataFrame], dk: FreqaiDataKitchen): + def encode_class_names( + self, + data_dictionary: Dict[str, pd.DataFrame], + dk: FreqaiDataKitchen, + class_names: List[str], + ): """ - encode class name str -> int - assuming first column of *_labels data frame to contain class names + encode class name, str -> int + assuming first column of *_labels data frame to be the target column + containing the class names """ target_column_name = dk.label_list[0] for split in ["train", "test"]: label_df = data_dictionary[f"{split}_labels"] - self.assert_valid_class_names(label_df[target_column_name]) + self.assert_valid_class_names(label_df[target_column_name], class_names) label_df[target_column_name] = list( map(lambda x: self.class_name_to_index[x], label_df[target_column_name]) ) - def assert_valid_class_names(self, labels: pd.Series): - non_defined_labels = set(labels) - set(self.class_names) + @staticmethod + def assert_valid_class_names( + target_column: pd.Series, + class_names: List[str] + ): + non_defined_labels = set(target_column) - set(class_names) if len(non_defined_labels) != 0: raise OperationalException( f"Found non defined labels: {non_defined_labels}, ", - f"expecting labels: {self.class_names}" + f"expecting labels: {class_names}" ) - def decode_classes_name(self, classes: torch.Tensor) -> List[str]: + def decode_class_names(self, class_ints: torch.Tensor) -> List[str]: """ - decode class name int -> str + decode class name, int -> str """ - return list(map(lambda x: self.index_to_class_name[x.item()], classes)) + return list(map(lambda x: self.index_to_class_name[x.item()], class_ints)) def init_class_names_to_index_mapping(self, class_names): self.class_name_to_index = {s: i for i, s in enumerate(class_names)} self.index_to_class_name = {i: s for i, s in enumerate(class_names)} - logger.info(f"class_name_to_index: {self.class_name_to_index}") + logger.info(f"encoded class name to index: {self.class_name_to_index}") + + def convert_label_column_to_int( + self, + data_dictionary: Dict[str, pd.DataFrame], + dk: FreqaiDataKitchen, + class_names: List[str] + ): + self.init_class_names_to_index_mapping(class_names) + self.encode_class_names(data_dictionary, dk, class_names) + + def get_class_names(self) -> List[str]: + if not hasattr(self, "class_names"): + raise ValueError( + "Missing attribute: self.class_names " + "set self.freqai.class_names = [\"class a\", \"class b\", \"class c\"] " + "inside IStrategy.set_freqai_targets method." + ) + return self.class_names diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index a5fe9b90b..181c0539d 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -88,10 +88,12 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, if 'PyTorchClassifierMultiTarget' in model: model_save_ext = 'zip' freqai_conf['freqai']['model_training_parameters'].update({ - "max_iters": 1, - "batch_size": 64, "learning_rate": 3e-4, - "max_n_eval_batches": 1, + "trainer_kwargs": { + "max_iters": 1, + "batch_size": 64, + "max_n_eval_batches": 1, + }, "model_kwargs": { "hidden_dim": 32, "dropout_percent": 0.2, From 61ac36c576e2dfafeba560ce5fe96a1fc5735d34 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 14:49:12 +0200 Subject: [PATCH 044/115] fix test --- tests/freqai/test_freqai_interface.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 181c0539d..33c18677b 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -85,22 +85,6 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, if 'test_3ac' in model or 'test_4ac' in model: freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models") - if 'PyTorchClassifierMultiTarget' in model: - model_save_ext = 'zip' - freqai_conf['freqai']['model_training_parameters'].update({ - "learning_rate": 3e-4, - "trainer_kwargs": { - "max_iters": 1, - "batch_size": 64, - "max_n_eval_batches": 1, - }, - "model_kwargs": { - "hidden_dim": 32, - "dropout_percent": 0.2, - "n_layer": 1, - } - }) - strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) strategy.dp = DataProvider(freqai_conf, exchange) @@ -184,7 +168,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s 'CatboostClassifier', 'XGBoostClassifier', 'XGBoostRFClassifier', - 'PyTorchClassifierMultiTarget', + 'PyTorchClassifier', ]) def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): if (is_arm() or is_py11()) and model == 'CatboostClassifier': From 9f477aa3c9c7de59d44db45088ef30e1027b93e0 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 15:09:50 +0200 Subject: [PATCH 045/115] sort imports --- freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py b/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py index d6be8c1df..2f6705311 100644 --- a/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py +++ b/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py @@ -1,12 +1,12 @@ from typing import Any, Dict +import torch + from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.prediction_models.PyTorchClassifierClassifier import PyTorchClassifier from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel -import torch - class MLPPyTorchClassifier(PyTorchClassifier): """ From 719faab4b8301410f86956cf09d698529a2ee93b Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 15:21:34 +0200 Subject: [PATCH 046/115] fix test --- tests/freqai/test_freqai_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 33c18677b..d183501ea 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -168,7 +168,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s 'CatboostClassifier', 'XGBoostClassifier', 'XGBoostRFClassifier', - 'PyTorchClassifier', + 'MLPPyTorchClassifier', ]) def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): if (is_arm() or is_py11()) and model == 'CatboostClassifier': From 8bee499328e9d242361fb11289b63bca6bf17d9d Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 17:03:36 +0200 Subject: [PATCH 047/115] modify feedforward net, move layer norm to start of thr block --- freqtrade/freqai/prediction_models/PyTorchMLPModel.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index 91e496c5d..482b3f889 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -22,7 +22,7 @@ class PyTorchMLPModel(nn.Module): def forward(self, x: Tensor) -> Tensor: x = self.relu(self.input_layer(x)) x = self.dropout(x) - x = self.relu(self.blocks(x)) + x = self.blocks(x) logits = self.output_layer(x) return logits @@ -35,8 +35,8 @@ class Block(nn.Module): self.ln = nn.LayerNorm(hidden_dim) def forward(self, x): - x = self.dropout(self.ff(x)) - x = self.ln(x) + x = self.ff(self.ln(x)) + x = self.dropout(x) return x @@ -46,7 +46,6 @@ class FeedForward(nn.Module): self.net = nn.Sequential( nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), - nn.Linear(hidden_dim, hidden_dim), ) def forward(self, x): From 6f9a8a089cd5bd54ca91d0d5c57231d75519af2d Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 17:45:30 +0200 Subject: [PATCH 048/115] add mlp documentation --- .../prediction_models/PyTorchMLPModel.py | 54 ++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index 482b3f889..07056e930 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -1,16 +1,42 @@ import logging import torch.nn as nn -from torch import Tensor - +import torch logger = logging.getLogger(__name__) class PyTorchMLPModel(nn.Module): + """ + A multi-layer perceptron (MLP) model implemented using PyTorch. + + :param input_dim: The number of input features. + :param output_dim: The number of output classes. + :param hidden_dim: The number of hidden units in each layer. Default: 256 + :param dropout_percent: The dropout rate for regularization. Default: 0.2 + :param n_layer: The number of layers in the MLP. Default: 1 + + :returns: The output of the MLP, with shape (batch_size, output_dim) + + + A neural network typically consists of input, output, and hidden layers, where the + information flows from the input layer through the hidden layers to the output layer. + In a feedforward neural network, also known as a multilayer perceptron (MLP), the + information flows in one direction only. Each hidden layer contains multiple units + or nodes that take input from the previous layer and produce output that goes to the + next layer. + + The hidden_dim parameter in the FeedForward class refers to the number of units + (or nodes) in the hidden layer. This parameter controls the complexity of the neural + network and determines how many nonlinear relationships the network can represent. + A higher value of hidden_dim allows the network to represent more complex functions + but may also make the network more prone to overfitting, where the model memorizes + the training data instead of learning general patterns. + """ + def __init__(self, input_dim: int, output_dim: int, **kwargs): super(PyTorchMLPModel, self).__init__() - hidden_dim: int = kwargs.get("hidden_dim", 1024) + hidden_dim: int = kwargs.get("hidden_dim", 256) dropout_percent: int = kwargs.get("dropout_percent", 0.2) n_layer: int = kwargs.get("n_layer", 1) self.input_layer = nn.Linear(input_dim, hidden_dim) @@ -19,7 +45,7 @@ class PyTorchMLPModel(nn.Module): self.relu = nn.ReLU() self.dropout = nn.Dropout(p=dropout_percent) - def forward(self, x: Tensor) -> Tensor: + def forward(self, x: torch.Tensor) -> torch.Tensor: x = self.relu(self.input_layer(x)) x = self.dropout(x) x = self.blocks(x) @@ -28,19 +54,35 @@ class PyTorchMLPModel(nn.Module): class Block(nn.Module): + """ + A building block for a multi-layer perceptron (MLP) implemented using PyTorch. + + :param hidden_dim: The number of hidden units in the feedforward network. + :param dropout_percent: The dropout rate for regularization. + + :returns: torch.Tensor. with shape (batch_size, hidden_dim) + """ + def __init__(self, hidden_dim: int, dropout_percent: int): super(Block, self).__init__() self.ff = FeedForward(hidden_dim) self.dropout = nn.Dropout(p=dropout_percent) self.ln = nn.LayerNorm(hidden_dim) - def forward(self, x): + def forward(self, x: torch.Tensor) -> torch.Tensor: x = self.ff(self.ln(x)) x = self.dropout(x) return x class FeedForward(nn.Module): + """ + A fully-connected feedforward neural network block. + + :param hidden_dim: The number of hidden units in the block. + :return: torch.Tensor. with shape (batch_size, hidden_dim) + """ + def __init__(self, hidden_dim: int): super(FeedForward, self).__init__() self.net = nn.Sequential( @@ -48,5 +90,5 @@ class FeedForward(nn.Module): nn.ReLU(), ) - def forward(self, x): + def forward(self, x: torch.Tensor) -> torch.Tensor: return self.net(x) From 903a1dc3e52507457e46242348beb2e6ea54ad45 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 18:04:01 +0200 Subject: [PATCH 049/115] improve mlp documentation --- .../prediction_models/PyTorchMLPModel.py | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index 07056e930..c2b6c1b93 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -10,28 +10,25 @@ class PyTorchMLPModel(nn.Module): """ A multi-layer perceptron (MLP) model implemented using PyTorch. - :param input_dim: The number of input features. - :param output_dim: The number of output classes. - :param hidden_dim: The number of hidden units in each layer. Default: 256 - :param dropout_percent: The dropout rate for regularization. Default: 0.2 - :param n_layer: The number of layers in the MLP. Default: 1 + :param input_dim: The number of input features. This parameter specifies the number + of features in the input data that the MLP will use to make predictions. + :param output_dim: The number of output classes. This parameter specifies the number + of classes that the MLP will predict. + :param hidden_dim: The number of hidden units in each layer. This parameter controls + the complexity of the MLP and determines how many nonlinear relationships the MLP + can represent. Increasing the number of hidden units can increase the capacity of + the MLP to model complex patterns, but it also increases the risk of overfitting + the training data. Default: 256 + :param dropout_percent: The dropout rate for regularization. This parameter specifies + the probability of dropping out a neuron during training to prevent overfitting. + The dropout rate should be tuned carefully to balance between underfitting and + overfitting. Default: 0.2 + :param n_layer: The number of layers in the MLP. This parameter specifies the number + of layers in the MLP architecture. Adding more layers to the MLP can increase its + capacity to model complex patterns, but it also increases the risk of overfitting + the training data. Default: 1 :returns: The output of the MLP, with shape (batch_size, output_dim) - - - A neural network typically consists of input, output, and hidden layers, where the - information flows from the input layer through the hidden layers to the output layer. - In a feedforward neural network, also known as a multilayer perceptron (MLP), the - information flows in one direction only. Each hidden layer contains multiple units - or nodes that take input from the previous layer and produce output that goes to the - next layer. - - The hidden_dim parameter in the FeedForward class refers to the number of units - (or nodes) in the hidden layer. This parameter controls the complexity of the neural - network and determines how many nonlinear relationships the network can represent. - A higher value of hidden_dim allows the network to represent more complex functions - but may also make the network more prone to overfitting, where the model memorizes - the training data instead of learning general patterns. """ def __init__(self, input_dim: int, output_dim: int, **kwargs): @@ -55,7 +52,7 @@ class PyTorchMLPModel(nn.Module): class Block(nn.Module): """ - A building block for a multi-layer perceptron (MLP) implemented using PyTorch. + A building block for a multi-layer perceptron (MLP). :param hidden_dim: The number of hidden units in the feedforward network. :param dropout_percent: The dropout rate for regularization. @@ -77,7 +74,7 @@ class Block(nn.Module): class FeedForward(nn.Module): """ - A fully-connected feedforward neural network block. + A simple fully-connected feedforward neural network block. :param hidden_dim: The number of hidden units in the block. :return: torch.Tensor. with shape (batch_size, hidden_dim) From 1c11a5f0485d2bdca5295712f4607abc0fe6d6a9 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 18:10:57 +0200 Subject: [PATCH 050/115] improve mlp documentation --- freqtrade/freqai/prediction_models/PyTorchMLPModel.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index c2b6c1b93..0e6b3c7bb 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -10,6 +10,9 @@ class PyTorchMLPModel(nn.Module): """ A multi-layer perceptron (MLP) model implemented using PyTorch. + This class mainly serves as a simple example for the integration of PyTorch model's + to freqai. It is not optimized at all and should not be used for production purposes. + :param input_dim: The number of input features. This parameter specifies the number of features in the input data that the MLP will use to make predictions. :param output_dim: The number of output classes. This parameter specifies the number From 2f386913ac0406371790b38d92f3b76c234552a8 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 11:54:17 +0200 Subject: [PATCH 051/115] refactor classifiers class names --- .../{PyTorchClassifierClassifier.py => PyTorchClassifier.py} | 0 .../{MLPPyTorchClassifier.py => PyTorchMLPClassifier.py} | 4 ++-- freqtrade/freqai/prediction_models/PyTorchMLPModel.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename freqtrade/freqai/prediction_models/{PyTorchClassifierClassifier.py => PyTorchClassifier.py} (100%) rename freqtrade/freqai/prediction_models/{MLPPyTorchClassifier.py => PyTorchMLPClassifier.py} (95%) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierClassifier.py b/freqtrade/freqai/prediction_models/PyTorchClassifier.py similarity index 100% rename from freqtrade/freqai/prediction_models/PyTorchClassifierClassifier.py rename to freqtrade/freqai/prediction_models/PyTorchClassifier.py diff --git a/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py similarity index 95% rename from freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py rename to freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index 2f6705311..453995ce8 100644 --- a/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -4,11 +4,11 @@ import torch from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.PyTorchClassifierClassifier import PyTorchClassifier +from freqtrade.freqai.prediction_models.PyTorchClassifier import PyTorchClassifier from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel -class MLPPyTorchClassifier(PyTorchClassifier): +class PyTorchMLPClassifier(PyTorchClassifier): """ This class implements the fit method of IFreqaiModel. int the fit method we initialize the model and trainer objects. diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index 0e6b3c7bb..f711a53a7 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -49,8 +49,8 @@ class PyTorchMLPModel(nn.Module): x = self.relu(self.input_layer(x)) x = self.dropout(x) x = self.blocks(x) - logits = self.output_layer(x) - return logits + x = self.output_layer(x) + return x class Block(nn.Module): From fab505be1bde88aa772c5ff482a8e9ec6c4e90c0 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Tue, 14 Mar 2023 21:13:30 +0100 Subject: [PATCH 052/115] cheat flake8 for now until we can refactor save into the model class --- freqtrade/freqai/data_drawer.py | 4 ++-- freqtrade/freqai/freqai_interface.py | 2 +- .../freqai/prediction_models/PyTorchClassifierMultiTarget.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index aaf9a869c..c8dadb171 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -446,7 +446,7 @@ class FreqaiDataDrawer: dump(model, save_path / f"{dk.model_filename}_model.joblib") elif self.model_type == 'keras': model.save(save_path / f"{dk.model_filename}_model.h5") - elif self.model_type in ["stable_baselines", "sb3_contrib", "pytorch"]: + elif self.model_type in ["stable_baselines3", "sb3_contrib", "pytorch"]: model.save(save_path / f"{dk.model_filename}_model.zip") if dk.svm_model is not None: @@ -496,7 +496,7 @@ class FreqaiDataDrawer: dk.training_features_list = dk.data["training_features_list"] dk.label_list = dk.data["label_list"] - def load_data(self, coin: str, dk: FreqaiDataKitchen) -> Any: + def load_data(self, coin: str, dk: FreqaiDataKitchen) -> Any: # noqa: C901 """ loads all data required to make a prediction on a sub-train time range :returns: diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 7c45d4642..8a1ac436b 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -563,7 +563,7 @@ class IFreqaiModel(ABC): file_type = ".joblib" elif self.dd.model_type == 'keras': file_type = ".h5" - elif self.dd.model_type in ["stable_baselines", "sb3_contrib", "pytorch"]: + elif self.dd.model_type in ["stable_baselines3", "sb3_contrib", "pytorch"]: file_type = ".zip" path_to_modelfile = Path(dk.data_path / f"{dk.model_filename}_model{file_type}") diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py index edafb3b7a..967199c12 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py @@ -41,7 +41,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): self.max_n_eval_batches: Optional[int] = model_training_params.get( "max_n_eval_batches", None ) - self.model_kwargs: Dict = model_training_params.get("model_kwargs", {}) + self.model_kwargs: Dict[str, any] = model_training_params.get("model_kwargs", {}) self.class_name_to_index = None self.index_to_class_name = None From d0a33d2ee7aa05ac08900cf5e44be79e195633f6 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sat, 18 Mar 2023 15:27:38 +0200 Subject: [PATCH 053/115] fix tests --- tests/freqai/test_freqai_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index f34659398..c35d1afb4 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -52,8 +52,7 @@ def can_run_model(model: str) -> None: ('ReinforcementLearner_multiproc', False, False, False, True, False, 0), ('ReinforcementLearner_test_3ac', False, False, False, False, False, 0), ('ReinforcementLearner_test_3ac', False, False, False, True, False, 0), - ('ReinforcementLearner_test_4ac', False, False, False, True, False, 0), - ('PyTorchClassifierMultiTarget', False, False, False, True, False, 0) + ('ReinforcementLearner_test_4ac', False, False, False, True, False, 0) ]) def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, dbscan, float32, can_short, shuffle, buffer): @@ -183,6 +182,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s 'CatboostClassifier', 'XGBoostClassifier', 'XGBoostRFClassifier', + 'PyTorchClassifierMultiTarget', ]) def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): if (is_arm() or is_py11()) and model == 'CatboostClassifier': From 566346dd87cbefba1ec1f5210828661314c35164 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sat, 18 Mar 2023 20:51:30 +0200 Subject: [PATCH 054/115] classifier test - set model file extension --- tests/freqai/test_freqai_interface.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index c35d1afb4..a5fe9b90b 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -210,8 +210,16 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): freqai.extract_data_and_train_model(new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) + if freqai.dd.model_type == 'joblib': + model_file_extension = ".joblib" + elif freqai.dd.model_type == "pytorch": + model_file_extension = ".zip" + else: + raise Exception(f"Unsupported model type: {freqai.dd.model_type}," + f" can't assign model_file_extension") - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").exists() + assert Path(freqai.dk.data_path / + f"{freqai.dk.model_filename}_model{model_file_extension}").exists() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").exists() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_trained_df.pkl").exists() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_svm_model.joblib").exists() From 833aaf8e101027e9af5314007035ba5b6cd858fe Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 14:38:49 +0200 Subject: [PATCH 055/115] create children class to PyTorchClassifier to implement the fit method where we initialize the trainer and model objects --- .../freqai/base_models/PyTorchModelTrainer.py | 19 ++- .../prediction_models/MLPPyTorchClassifier.py | 81 ++++++++++++ ...rget.py => PyTorchClassifierClassifier.py} | 125 ++++++++---------- tests/freqai/test_freqai_interface.py | 8 +- 4 files changed, 146 insertions(+), 87 deletions(-) create mode 100644 freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py rename freqtrade/freqai/prediction_models/{PyTorchClassifierMultiTarget.py => PyTorchClassifierClassifier.py} (53%) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 90fb472e5..f91b44924 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -19,35 +19,32 @@ class PyTorchModelTrainer: optimizer: Optimizer, criterion: nn.Module, device: str, - batch_size: int, - max_iters: int, - max_n_eval_batches: int, init_model: Dict, model_meta_data: Dict[str, Any] = {}, + **kwargs ): """ :param model: The PyTorch model to be trained. :param optimizer: The optimizer to use for training. :param criterion: The loss function to use for training. :param device: The device to use for training (e.g. 'cpu', 'cuda'). - :param batch_size: The size of the batches to use during training. + :param init_model: A dictionary containing the initial model/optimizer + state_dict and model_meta_data saved by self.save() method. + :param model_meta_data: Additional metadata about the model (optional). :param max_iters: The number of training iterations to run. iteration here refers to the number of times we call self.optimizer.step(). used to calculate n_epochs. + :param batch_size: The size of the batches to use during training. :param max_n_eval_batches: The maximum number batches to use for evaluation. - :param init_model: A dictionary containing the initial model/optimizer - state_dict and model_meta_data saved by self.save() method. - :param model_meta_data: Additional metadata about the model (optional). """ self.model = model self.optimizer = optimizer self.criterion = criterion self.model_meta_data = model_meta_data self.device = device - self.max_iters = max_iters - self.batch_size = batch_size - self.max_n_eval_batches = max_n_eval_batches - + self.max_iters: int = kwargs.get("max_iters", 100) + self.batch_size: int = kwargs.get("batch_size", 64) + self.max_n_eval_batches: Optional[int] = kwargs.get("max_n_eval_batches", None) if init_model: self.load_from_checkpoint(init_model) diff --git a/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py b/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py new file mode 100644 index 000000000..d6be8c1df --- /dev/null +++ b/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py @@ -0,0 +1,81 @@ +from typing import Any, Dict + +from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.prediction_models.PyTorchClassifierClassifier import PyTorchClassifier +from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel + +import torch + + +class MLPPyTorchClassifier(PyTorchClassifier): + """ + This class implements the fit method of IFreqaiModel. + int the fit method we initialize the model and trainer objects. + the only requirement from the model is to be aligned to PyTorchClassifier + predict method that expects the model to predict tensor of type long. + the trainer defines the training loop. + + parameters are passed via `model_training_parameters` under the freqai + section in the config file. e.g: + { + ... + "freqai": { + ... + "model_training_parameters" : { + "learning_rate": 3e-4, + "trainer_kwargs": { + "max_iters": 5000, + "batch_size": 64, + "max_n_eval_batches": None, + }, + "model_kwargs": { + "hidden_dim": 512, + "dropout_percent": 0.2, + "n_layer": 1, + }, + } + } + } + + + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + model_training_params = self.freqai_info.get("model_training_parameters", {}) + self.learning_rate: float = model_training_params.get("learning_rate", 3e-4) + self.model_kwargs: Dict[str, any] = model_training_params.get("model_kwargs", {}) + self.trainer_kwargs: Dict[str, any] = model_training_params.get("trainer_kwargs", {}) + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + """ + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary constructed by DataHandler to hold + all the training and test data/labels. + :raises ValueError: If self.class_names is not defined in the parent class. + """ + + class_names = self.get_class_names() + self.convert_label_column_to_int(data_dictionary, dk, class_names) + n_features = data_dictionary["train_features"].shape[-1] + model = PyTorchMLPModel( + input_dim=n_features, + output_dim=len(class_names), + **self.model_kwargs + ) + model.to(self.device) + optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) + criterion = torch.nn.CrossEntropyLoss() + init_model = self.get_init_model(dk.pair) + trainer = PyTorchModelTrainer( + model=model, + optimizer=optimizer, + criterion=criterion, + model_meta_data={"class_names": class_names}, + device=self.device, + init_model=init_model, + **self.trainer_kwargs, + ) + trainer.fit(data_dictionary) + return trainer diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/PyTorchClassifierClassifier.py similarity index 53% rename from freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py rename to freqtrade/freqai/prediction_models/PyTorchClassifierClassifier.py index 967199c12..0be10b31e 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifierClassifier.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Dict, List, Optional, Tuple +from typing import Dict, List, Tuple import numpy as np import numpy.typing as npt @@ -10,17 +10,16 @@ from torch.nn import functional as F from freqtrade.exceptions import OperationalException from freqtrade.freqai.base_models.BasePyTorchModel import BasePyTorchModel -from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel logger = logging.getLogger(__name__) -class PyTorchClassifierMultiTarget(BasePyTorchModel): +class PyTorchClassifier(BasePyTorchModel): """ - A PyTorch implementation of a multi-target classifier. + A PyTorch implementation of a classifier. + User must implement fit method """ def __init__(self, **kwargs): """ @@ -34,59 +33,9 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): """ super().__init__(**kwargs) - model_training_params = self.freqai_info.get("model_training_parameters", {}) - self.max_iters: int = model_training_params.get("max_iters", 100) - self.batch_size: int = model_training_params.get("batch_size", 64) - self.learning_rate: float = model_training_params.get("learning_rate", 3e-4) - self.max_n_eval_batches: Optional[int] = model_training_params.get( - "max_n_eval_batches", None - ) - self.model_kwargs: Dict[str, any] = model_training_params.get("model_kwargs", {}) self.class_name_to_index = None self.index_to_class_name = None - def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: - """ - User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. - :raises ValueError: If self.class_names is not defined in the parent class. - - """ - - if not hasattr(self, "class_names"): - raise ValueError( - "Missing attribute: self.class_names " - "set self.freqai.class_names = [\"class a\", \"class b\", \"class c\"] " - "inside IStrategy.set_freqai_targets method." - ) - - self.init_class_names_to_index_mapping(self.class_names) - self.encode_classes_name(data_dictionary, dk) - n_features = data_dictionary["train_features"].shape[-1] - model = PyTorchMLPModel( - input_dim=n_features, - output_dim=len(self.class_names), - **self.model_kwargs - ) - model.to(self.device) - optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) - criterion = torch.nn.CrossEntropyLoss() - init_model = self.get_init_model(dk.pair) - trainer = PyTorchModelTrainer( - model=model, - optimizer=optimizer, - criterion=criterion, - model_meta_data={"class_names": self.class_names}, - device=self.device, - batch_size=self.batch_size, - max_iters=self.max_iters, - max_n_eval_batches=self.max_n_eval_batches, - init_model=init_model - ) - trainer.fit(data_dictionary) - return trainer - def predict( self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs ) -> Tuple[DataFrame, npt.NDArray[np.int_]]: @@ -97,7 +46,7 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): :pred_df: dataframe containing the predictions :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove data (NaNs) or felt uncertain about data (PCA and DI index) - :raises ValueError: if 'class_name' doesn't exist in model meta_data. + :raises ValueError: if 'class_names' doesn't exist in model meta_data. """ class_names = self.model.model_meta_data.get("class_names", None) @@ -106,7 +55,9 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): "Missing class names. " "self.model.model_meta_data[\"class_names\"] is None." ) - self.init_class_names_to_index_mapping(class_names) + + if not self.class_name_to_index: + self.init_class_names_to_index_mapping(class_names) dk.find_features(unfiltered_df) filtered_df, _ = dk.filter_features( @@ -116,49 +67,77 @@ class PyTorchClassifierMultiTarget(BasePyTorchModel): dk.data_dictionary["prediction_features"] = filtered_df self.data_cleaning_predict(dk) - dk.data_dictionary["prediction_features"] = torch.tensor( - dk.data_dictionary["prediction_features"].values - ).float().to(self.device) + x = torch.from_numpy(dk.data_dictionary["prediction_features"].values)\ + .float()\ + .to(self.device) - logits = self.model.model(dk.data_dictionary["prediction_features"]) + logits = self.model.model(x) probs = F.softmax(logits, dim=-1) predicted_classes = torch.argmax(probs, dim=-1) - predicted_classes_str = self.decode_classes_name(predicted_classes) + predicted_classes_str = self.decode_class_names(predicted_classes) pred_df_prob = DataFrame(probs.detach().numpy(), columns=class_names) pred_df = DataFrame(predicted_classes_str, columns=[dk.label_list[0]]) pred_df = pd.concat([pred_df, pred_df_prob], axis=1) return (pred_df, dk.do_predict) - def encode_classes_name(self, data_dictionary: Dict[str, pd.DataFrame], dk: FreqaiDataKitchen): + def encode_class_names( + self, + data_dictionary: Dict[str, pd.DataFrame], + dk: FreqaiDataKitchen, + class_names: List[str], + ): """ - encode class name str -> int - assuming first column of *_labels data frame to contain class names + encode class name, str -> int + assuming first column of *_labels data frame to be the target column + containing the class names """ target_column_name = dk.label_list[0] for split in ["train", "test"]: label_df = data_dictionary[f"{split}_labels"] - self.assert_valid_class_names(label_df[target_column_name]) + self.assert_valid_class_names(label_df[target_column_name], class_names) label_df[target_column_name] = list( map(lambda x: self.class_name_to_index[x], label_df[target_column_name]) ) - def assert_valid_class_names(self, labels: pd.Series): - non_defined_labels = set(labels) - set(self.class_names) + @staticmethod + def assert_valid_class_names( + target_column: pd.Series, + class_names: List[str] + ): + non_defined_labels = set(target_column) - set(class_names) if len(non_defined_labels) != 0: raise OperationalException( f"Found non defined labels: {non_defined_labels}, ", - f"expecting labels: {self.class_names}" + f"expecting labels: {class_names}" ) - def decode_classes_name(self, classes: torch.Tensor) -> List[str]: + def decode_class_names(self, class_ints: torch.Tensor) -> List[str]: """ - decode class name int -> str + decode class name, int -> str """ - return list(map(lambda x: self.index_to_class_name[x.item()], classes)) + return list(map(lambda x: self.index_to_class_name[x.item()], class_ints)) def init_class_names_to_index_mapping(self, class_names): self.class_name_to_index = {s: i for i, s in enumerate(class_names)} self.index_to_class_name = {i: s for i, s in enumerate(class_names)} - logger.info(f"class_name_to_index: {self.class_name_to_index}") + logger.info(f"encoded class name to index: {self.class_name_to_index}") + + def convert_label_column_to_int( + self, + data_dictionary: Dict[str, pd.DataFrame], + dk: FreqaiDataKitchen, + class_names: List[str] + ): + self.init_class_names_to_index_mapping(class_names) + self.encode_class_names(data_dictionary, dk, class_names) + + def get_class_names(self) -> List[str]: + if not hasattr(self, "class_names"): + raise ValueError( + "Missing attribute: self.class_names " + "set self.freqai.class_names = [\"class a\", \"class b\", \"class c\"] " + "inside IStrategy.set_freqai_targets method." + ) + return self.class_names diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index a5fe9b90b..181c0539d 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -88,10 +88,12 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, if 'PyTorchClassifierMultiTarget' in model: model_save_ext = 'zip' freqai_conf['freqai']['model_training_parameters'].update({ - "max_iters": 1, - "batch_size": 64, "learning_rate": 3e-4, - "max_n_eval_batches": 1, + "trainer_kwargs": { + "max_iters": 1, + "batch_size": 64, + "max_n_eval_batches": 1, + }, "model_kwargs": { "hidden_dim": 32, "dropout_percent": 0.2, From 2a1a8c0e644b39b72b5a88eb78945162274d8bde Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 14:49:12 +0200 Subject: [PATCH 056/115] fix test --- tests/freqai/test_freqai_interface.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 181c0539d..33c18677b 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -85,22 +85,6 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, if 'test_3ac' in model or 'test_4ac' in model: freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models") - if 'PyTorchClassifierMultiTarget' in model: - model_save_ext = 'zip' - freqai_conf['freqai']['model_training_parameters'].update({ - "learning_rate": 3e-4, - "trainer_kwargs": { - "max_iters": 1, - "batch_size": 64, - "max_n_eval_batches": 1, - }, - "model_kwargs": { - "hidden_dim": 32, - "dropout_percent": 0.2, - "n_layer": 1, - } - }) - strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) strategy.dp = DataProvider(freqai_conf, exchange) @@ -184,7 +168,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s 'CatboostClassifier', 'XGBoostClassifier', 'XGBoostRFClassifier', - 'PyTorchClassifierMultiTarget', + 'PyTorchClassifier', ]) def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): if (is_arm() or is_py11()) and model == 'CatboostClassifier': From fbf7049ac5715e1d047fec1b4f99cb75f6efa1d7 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 15:09:50 +0200 Subject: [PATCH 057/115] sort imports --- freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py b/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py index d6be8c1df..2f6705311 100644 --- a/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py +++ b/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py @@ -1,12 +1,12 @@ from typing import Any, Dict +import torch + from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.prediction_models.PyTorchClassifierClassifier import PyTorchClassifier from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel -import torch - class MLPPyTorchClassifier(PyTorchClassifier): """ From e08d8190ae80a1acf8aa1e8dd75891c78d134063 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 15:21:34 +0200 Subject: [PATCH 058/115] fix test --- tests/freqai/test_freqai_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 33c18677b..d183501ea 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -168,7 +168,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s 'CatboostClassifier', 'XGBoostClassifier', 'XGBoostRFClassifier', - 'PyTorchClassifier', + 'MLPPyTorchClassifier', ]) def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): if (is_arm() or is_py11()) and model == 'CatboostClassifier': From ddd1b5c0fffd233c77c976ac1396309fa189cfa8 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 17:03:36 +0200 Subject: [PATCH 059/115] modify feedforward net, move layer norm to start of thr block --- freqtrade/freqai/prediction_models/PyTorchMLPModel.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index 91e496c5d..482b3f889 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -22,7 +22,7 @@ class PyTorchMLPModel(nn.Module): def forward(self, x: Tensor) -> Tensor: x = self.relu(self.input_layer(x)) x = self.dropout(x) - x = self.relu(self.blocks(x)) + x = self.blocks(x) logits = self.output_layer(x) return logits @@ -35,8 +35,8 @@ class Block(nn.Module): self.ln = nn.LayerNorm(hidden_dim) def forward(self, x): - x = self.dropout(self.ff(x)) - x = self.ln(x) + x = self.ff(self.ln(x)) + x = self.dropout(x) return x @@ -46,7 +46,6 @@ class FeedForward(nn.Module): self.net = nn.Sequential( nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), - nn.Linear(hidden_dim, hidden_dim), ) def forward(self, x): From ea08931ab3455fb3e083fc2ae65cacbce455fef4 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 17:45:30 +0200 Subject: [PATCH 060/115] add mlp documentation --- .../prediction_models/PyTorchMLPModel.py | 54 ++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index 482b3f889..07056e930 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -1,16 +1,42 @@ import logging import torch.nn as nn -from torch import Tensor - +import torch logger = logging.getLogger(__name__) class PyTorchMLPModel(nn.Module): + """ + A multi-layer perceptron (MLP) model implemented using PyTorch. + + :param input_dim: The number of input features. + :param output_dim: The number of output classes. + :param hidden_dim: The number of hidden units in each layer. Default: 256 + :param dropout_percent: The dropout rate for regularization. Default: 0.2 + :param n_layer: The number of layers in the MLP. Default: 1 + + :returns: The output of the MLP, with shape (batch_size, output_dim) + + + A neural network typically consists of input, output, and hidden layers, where the + information flows from the input layer through the hidden layers to the output layer. + In a feedforward neural network, also known as a multilayer perceptron (MLP), the + information flows in one direction only. Each hidden layer contains multiple units + or nodes that take input from the previous layer and produce output that goes to the + next layer. + + The hidden_dim parameter in the FeedForward class refers to the number of units + (or nodes) in the hidden layer. This parameter controls the complexity of the neural + network and determines how many nonlinear relationships the network can represent. + A higher value of hidden_dim allows the network to represent more complex functions + but may also make the network more prone to overfitting, where the model memorizes + the training data instead of learning general patterns. + """ + def __init__(self, input_dim: int, output_dim: int, **kwargs): super(PyTorchMLPModel, self).__init__() - hidden_dim: int = kwargs.get("hidden_dim", 1024) + hidden_dim: int = kwargs.get("hidden_dim", 256) dropout_percent: int = kwargs.get("dropout_percent", 0.2) n_layer: int = kwargs.get("n_layer", 1) self.input_layer = nn.Linear(input_dim, hidden_dim) @@ -19,7 +45,7 @@ class PyTorchMLPModel(nn.Module): self.relu = nn.ReLU() self.dropout = nn.Dropout(p=dropout_percent) - def forward(self, x: Tensor) -> Tensor: + def forward(self, x: torch.Tensor) -> torch.Tensor: x = self.relu(self.input_layer(x)) x = self.dropout(x) x = self.blocks(x) @@ -28,19 +54,35 @@ class PyTorchMLPModel(nn.Module): class Block(nn.Module): + """ + A building block for a multi-layer perceptron (MLP) implemented using PyTorch. + + :param hidden_dim: The number of hidden units in the feedforward network. + :param dropout_percent: The dropout rate for regularization. + + :returns: torch.Tensor. with shape (batch_size, hidden_dim) + """ + def __init__(self, hidden_dim: int, dropout_percent: int): super(Block, self).__init__() self.ff = FeedForward(hidden_dim) self.dropout = nn.Dropout(p=dropout_percent) self.ln = nn.LayerNorm(hidden_dim) - def forward(self, x): + def forward(self, x: torch.Tensor) -> torch.Tensor: x = self.ff(self.ln(x)) x = self.dropout(x) return x class FeedForward(nn.Module): + """ + A fully-connected feedforward neural network block. + + :param hidden_dim: The number of hidden units in the block. + :return: torch.Tensor. with shape (batch_size, hidden_dim) + """ + def __init__(self, hidden_dim: int): super(FeedForward, self).__init__() self.net = nn.Sequential( @@ -48,5 +90,5 @@ class FeedForward(nn.Module): nn.ReLU(), ) - def forward(self, x): + def forward(self, x: torch.Tensor) -> torch.Tensor: return self.net(x) From d04146d1b15cc35b5cac3223e23b05297b391986 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 18:04:01 +0200 Subject: [PATCH 061/115] improve mlp documentation --- .../prediction_models/PyTorchMLPModel.py | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index 07056e930..c2b6c1b93 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -10,28 +10,25 @@ class PyTorchMLPModel(nn.Module): """ A multi-layer perceptron (MLP) model implemented using PyTorch. - :param input_dim: The number of input features. - :param output_dim: The number of output classes. - :param hidden_dim: The number of hidden units in each layer. Default: 256 - :param dropout_percent: The dropout rate for regularization. Default: 0.2 - :param n_layer: The number of layers in the MLP. Default: 1 + :param input_dim: The number of input features. This parameter specifies the number + of features in the input data that the MLP will use to make predictions. + :param output_dim: The number of output classes. This parameter specifies the number + of classes that the MLP will predict. + :param hidden_dim: The number of hidden units in each layer. This parameter controls + the complexity of the MLP and determines how many nonlinear relationships the MLP + can represent. Increasing the number of hidden units can increase the capacity of + the MLP to model complex patterns, but it also increases the risk of overfitting + the training data. Default: 256 + :param dropout_percent: The dropout rate for regularization. This parameter specifies + the probability of dropping out a neuron during training to prevent overfitting. + The dropout rate should be tuned carefully to balance between underfitting and + overfitting. Default: 0.2 + :param n_layer: The number of layers in the MLP. This parameter specifies the number + of layers in the MLP architecture. Adding more layers to the MLP can increase its + capacity to model complex patterns, but it also increases the risk of overfitting + the training data. Default: 1 :returns: The output of the MLP, with shape (batch_size, output_dim) - - - A neural network typically consists of input, output, and hidden layers, where the - information flows from the input layer through the hidden layers to the output layer. - In a feedforward neural network, also known as a multilayer perceptron (MLP), the - information flows in one direction only. Each hidden layer contains multiple units - or nodes that take input from the previous layer and produce output that goes to the - next layer. - - The hidden_dim parameter in the FeedForward class refers to the number of units - (or nodes) in the hidden layer. This parameter controls the complexity of the neural - network and determines how many nonlinear relationships the network can represent. - A higher value of hidden_dim allows the network to represent more complex functions - but may also make the network more prone to overfitting, where the model memorizes - the training data instead of learning general patterns. """ def __init__(self, input_dim: int, output_dim: int, **kwargs): @@ -55,7 +52,7 @@ class PyTorchMLPModel(nn.Module): class Block(nn.Module): """ - A building block for a multi-layer perceptron (MLP) implemented using PyTorch. + A building block for a multi-layer perceptron (MLP). :param hidden_dim: The number of hidden units in the feedforward network. :param dropout_percent: The dropout rate for regularization. @@ -77,7 +74,7 @@ class Block(nn.Module): class FeedForward(nn.Module): """ - A fully-connected feedforward neural network block. + A simple fully-connected feedforward neural network block. :param hidden_dim: The number of hidden units in the block. :return: torch.Tensor. with shape (batch_size, hidden_dim) From 501e746c52dfcd2eb7a54e1d0b1f45a0bdba2b0f Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Sun, 19 Mar 2023 18:10:57 +0200 Subject: [PATCH 062/115] improve mlp documentation --- freqtrade/freqai/prediction_models/PyTorchMLPModel.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index c2b6c1b93..0e6b3c7bb 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -10,6 +10,9 @@ class PyTorchMLPModel(nn.Module): """ A multi-layer perceptron (MLP) model implemented using PyTorch. + This class mainly serves as a simple example for the integration of PyTorch model's + to freqai. It is not optimized at all and should not be used for production purposes. + :param input_dim: The number of input features. This parameter specifies the number of features in the input data that the MLP will use to make predictions. :param output_dim: The number of output classes. This parameter specifies the number From 601c37f862eea19c703ca73e2e619737c8754864 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 11:54:17 +0200 Subject: [PATCH 063/115] refactor classifiers class names --- .../{PyTorchClassifierClassifier.py => PyTorchClassifier.py} | 0 .../{MLPPyTorchClassifier.py => PyTorchMLPClassifier.py} | 4 ++-- freqtrade/freqai/prediction_models/PyTorchMLPModel.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename freqtrade/freqai/prediction_models/{PyTorchClassifierClassifier.py => PyTorchClassifier.py} (100%) rename freqtrade/freqai/prediction_models/{MLPPyTorchClassifier.py => PyTorchMLPClassifier.py} (95%) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifierClassifier.py b/freqtrade/freqai/prediction_models/PyTorchClassifier.py similarity index 100% rename from freqtrade/freqai/prediction_models/PyTorchClassifierClassifier.py rename to freqtrade/freqai/prediction_models/PyTorchClassifier.py diff --git a/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py similarity index 95% rename from freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py rename to freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index 2f6705311..453995ce8 100644 --- a/freqtrade/freqai/prediction_models/MLPPyTorchClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -4,11 +4,11 @@ import torch from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.PyTorchClassifierClassifier import PyTorchClassifier +from freqtrade.freqai.prediction_models.PyTorchClassifier import PyTorchClassifier from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel -class MLPPyTorchClassifier(PyTorchClassifier): +class PyTorchMLPClassifier(PyTorchClassifier): """ This class implements the fit method of IFreqaiModel. int the fit method we initialize the model and trainer objects. diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index 0e6b3c7bb..f711a53a7 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -49,8 +49,8 @@ class PyTorchMLPModel(nn.Module): x = self.relu(self.input_layer(x)) x = self.dropout(x) x = self.blocks(x) - logits = self.output_layer(x) - return logits + x = self.output_layer(x) + return x class Block(nn.Module): From 54db239175643c9628f2c2278642b03b28cea6ee Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 17:06:33 +0200 Subject: [PATCH 064/115] add pytorch regressor example --- .../freqai/base_models/PyTorchModelTrainer.py | 10 ++- .../prediction_models/PyTorchClassifier.py | 10 --- .../prediction_models/PyTorchMLPClassifier.py | 3 +- .../prediction_models/PyTorchMLPRegressor.py | 78 +++++++++++++++++++ .../prediction_models/PyTorchRegressor.py | 50 ++++++++++++ 5 files changed, 137 insertions(+), 14 deletions(-) create mode 100644 freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py create mode 100644 freqtrade/freqai/prediction_models/PyTorchRegressor.py diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index f91b44924..63bf63c40 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, Type import pandas as pd import torch @@ -20,6 +20,7 @@ class PyTorchModelTrainer: criterion: nn.Module, device: str, init_model: Dict, + target_tensor_type: torch.dtype, model_meta_data: Dict[str, Any] = {}, **kwargs ): @@ -30,6 +31,8 @@ class PyTorchModelTrainer: :param device: The device to use for training (e.g. 'cpu', 'cuda'). :param init_model: A dictionary containing the initial model/optimizer state_dict and model_meta_data saved by self.save() method. + :param target_tensor_type: type of target tensor, for classification usually + torch.long, for regressor usually torch.float. :param model_meta_data: Additional metadata about the model (optional). :param max_iters: The number of training iterations to run. iteration here refers to the number of times we call @@ -42,6 +45,7 @@ class PyTorchModelTrainer: self.criterion = criterion self.model_meta_data = model_meta_data self.device = device + self.target_tensor_type = target_tensor_type self.max_iters: int = kwargs.get("max_iters", 100) self.batch_size: int = kwargs.get("batch_size", 64) self.max_n_eval_batches: Optional[int] = kwargs.get("max_n_eval_batches", None) @@ -123,8 +127,8 @@ class PyTorchModelTrainer: labels_view = labels_shape[0] if labels_shape[1] == 1 else labels_shape dataset = TensorDataset( torch.from_numpy(data_dictionary[f"{split}_features"].values).float(), - torch.from_numpy(data_dictionary[f"{split}_labels"].astype(float).values) - .long() + torch.from_numpy(data_dictionary[f"{split}_labels"].values) + .to(self.target_tensor_type) .view(labels_view) ) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifier.py b/freqtrade/freqai/prediction_models/PyTorchClassifier.py index 0be10b31e..01432e0fe 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifier.py @@ -22,16 +22,6 @@ class PyTorchClassifier(BasePyTorchModel): User must implement fit method """ def __init__(self, **kwargs): - """ - int: The number of nodes in the hidden layer of the neural network. - int: The maximum number of iterations to run during training. - int: The batch size to use during training. - float: The learning rate to use during training. - int: The number of training iterations between each evaluation. - dict: A dictionary mapping class names to their corresponding indices. - dict: A dictionary mapping indices to their corresponding class names. - """ - super().__init__(**kwargs) self.class_name_to_index = None self.index_to_class_name = None diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index 453995ce8..ce8fbd336 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -11,7 +11,7 @@ from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel class PyTorchMLPClassifier(PyTorchClassifier): """ This class implements the fit method of IFreqaiModel. - int the fit method we initialize the model and trainer objects. + in the fit method we initialize the model and trainer objects. the only requirement from the model is to be aligned to PyTorchClassifier predict method that expects the model to predict tensor of type long. the trainer defines the training loop. @@ -75,6 +75,7 @@ class PyTorchMLPClassifier(PyTorchClassifier): model_meta_data={"class_names": class_names}, device=self.device, init_model=init_model, + target_tensor_type=torch.long, **self.trainer_kwargs, ) trainer.fit(data_dictionary) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py new file mode 100644 index 000000000..4685c332a --- /dev/null +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -0,0 +1,78 @@ +from typing import Any, Dict + +import torch + +from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel +from freqtrade.freqai.prediction_models.PyTorchRegressor import PyTorchRegressor + + +class PyTorchMLPRegressor(PyTorchRegressor): + """ + This class implements the fit method of IFreqaiModel. + in the fit method we initialize the model and trainer objects. + the only requirement from the model is to be aligned to PyTorchRegressor + predict method that expects the model to predict tensor of type float. + the trainer defines the training loop. + + parameters are passed via `model_training_parameters` under the freqai + section in the config file. e.g: + { + ... + "freqai": { + ... + "model_training_parameters" : { + "learning_rate": 3e-4, + "trainer_kwargs": { + "max_iters": 5000, + "batch_size": 64, + "max_n_eval_batches": None, + }, + "model_kwargs": { + "hidden_dim": 512, + "dropout_percent": 0.2, + "n_layer": 1, + }, + } + } + } + + + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + model_training_params = self.freqai_info.get("model_training_parameters", {}) + self.learning_rate: float = model_training_params.get("learning_rate", 3e-4) + self.model_kwargs: Dict[str, any] = model_training_params.get("model_kwargs", {}) + self.trainer_kwargs: Dict[str, any] = model_training_params.get("trainer_kwargs", {}) + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + """ + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary constructed by DataHandler to hold + all the training and test data/labels. + """ + + n_features = data_dictionary["train_features"].shape[-1] + model = PyTorchMLPModel( + input_dim=n_features, + output_dim=1, + **self.model_kwargs + ) + model.to(self.device) + optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) + criterion = torch.nn.MSELoss() + init_model = self.get_init_model(dk.pair) + trainer = PyTorchModelTrainer( + model=model, + optimizer=optimizer, + criterion=criterion, + device=self.device, + init_model=init_model, + target_tensor_type=torch.float, + **self.trainer_kwargs, + ) + trainer.fit(data_dictionary) + return trainer diff --git a/freqtrade/freqai/prediction_models/PyTorchRegressor.py b/freqtrade/freqai/prediction_models/PyTorchRegressor.py new file mode 100644 index 000000000..837fbd836 --- /dev/null +++ b/freqtrade/freqai/prediction_models/PyTorchRegressor.py @@ -0,0 +1,50 @@ +import logging +from typing import Tuple + +import numpy as np +import numpy.typing as npt +import torch +from pandas import DataFrame + +from freqtrade.freqai.base_models.BasePyTorchModel import BasePyTorchModel +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen + + +logger = logging.getLogger(__name__) + + +class PyTorchRegressor(BasePyTorchModel): + """ + A PyTorch implementation of a regressor. + User must implement fit method + """ + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def predict( + self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs + ) -> Tuple[DataFrame, npt.NDArray[np.int_]]: + """ + Filter the prediction features data and predict with it. + :param unfiltered_df: Full dataframe for the current backtest period. + :return: + :pred_df: dataframe containing the predictions + :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove + data (NaNs) or felt uncertain about data (PCA and DI index) + """ + + dk.find_features(unfiltered_df) + filtered_df, _ = dk.filter_features( + unfiltered_df, dk.training_features_list, training_filter=False + ) + filtered_df = dk.normalize_data_from_metadata(filtered_df) + dk.data_dictionary["prediction_features"] = filtered_df + + self.data_cleaning_predict(dk) + x = torch.from_numpy(dk.data_dictionary["prediction_features"].values)\ + .float()\ + .to(self.device) + + y = self.model.model(x) + pred_df = DataFrame(y.detach().numpy(), columns=[dk.label_list[0]]) + return (pred_df, dk.do_predict) \ No newline at end of file From f659f8e309dbeec31f313d2ef0e0d54c266100c7 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 17:52:52 +0200 Subject: [PATCH 065/115] remove unused imports --- freqtrade/freqai/base_models/PyTorchModelTrainer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 63bf63c40..8097b8b85 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import Any, Dict, Optional, Type +from typing import Any, Dict, Optional import pandas as pd import torch From d98890f32e2a866c9a9934858998ebd163c88aed Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 17:55:05 +0200 Subject: [PATCH 066/115] sort imports --- freqtrade/freqai/prediction_models/PyTorchMLPModel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index f711a53a7..c43b06685 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -1,7 +1,7 @@ import logging -import torch.nn as nn import torch +import torch.nn as nn logger = logging.getLogger(__name__) From 9aec1ddb17f03b62a9dc5781eb76d182dfa7c1f4 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 17:56:51 +0200 Subject: [PATCH 067/115] sort imports --- freqtrade/freqai/prediction_models/PyTorchMLPModel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index c43b06685..a9f609e8e 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -3,6 +3,7 @@ import logging import torch import torch.nn as nn + logger = logging.getLogger(__name__) From c00ffcee59e60f3218b5e09258ad15d57a439ead Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 18:04:02 +0200 Subject: [PATCH 068/115] fix pytorch classifier test --- tests/freqai/test_freqai_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index d183501ea..01aa0d1db 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -168,7 +168,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s 'CatboostClassifier', 'XGBoostClassifier', 'XGBoostRFClassifier', - 'MLPPyTorchClassifier', + 'PyTorchMLPClassifier', ]) def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): if (is_arm() or is_py11()) and model == 'CatboostClassifier': From 68728409aa5c7b668fbb1228cfed117901a7f1a9 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 18:04:14 +0200 Subject: [PATCH 069/115] add pytorch regressor test --- tests/freqai/test_freqai_interface.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 01aa0d1db..3407a5a95 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -48,11 +48,12 @@ def can_run_model(model: str) -> None: ('XGBoostRegressor', False, True, False, True, False, 10), ('XGBoostRFRegressor', False, False, False, True, False, 0), ('CatboostRegressor', False, False, False, True, True, 0), + ('MLPPyTorchRegressor', False, False, False, True, False, 0), ('ReinforcementLearner', False, True, False, True, False, 0), ('ReinforcementLearner_multiproc', False, False, False, True, False, 0), ('ReinforcementLearner_test_3ac', False, False, False, False, False, 0), ('ReinforcementLearner_test_3ac', False, False, False, True, False, 0), - ('ReinforcementLearner_test_4ac', False, False, False, True, False, 0) + ('ReinforcementLearner_test_4ac', False, False, False, True, False, 0), ]) def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, dbscan, float32, can_short, shuffle, buffer): @@ -85,6 +86,9 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, if 'test_3ac' in model or 'test_4ac' in model: freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models") + if 'MLPPyTorchRegressor' in model: + model_save_ext = 'zip' + strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) strategy.dp = DataProvider(freqai_conf, exchange) From 0510cf44910d4980eb66ecf2a5f6947607a7a8f9 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 18:08:38 +0200 Subject: [PATCH 070/115] add config params to tests --- tests/freqai/test_freqai_interface.py | 30 +++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 3407a5a95..d35b00013 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -88,6 +88,19 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, if 'MLPPyTorchRegressor' in model: model_save_ext = 'zip' + freqai_conf['freqai']['model_training_parameters'].update({ + "learning_rate": 3e-4, + "trainer_kwargs": { + "max_iters": 1, + "batch_size": 64, + "max_n_eval_batches": 1, + }, + "model_kwargs": { + "hidden_dim": 32, + "dropout_percent": 0.2, + "n_layer": 1, + } + }) strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) @@ -200,6 +213,23 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): freqai.extract_data_and_train_model(new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) + + if 'MLPPyTorchClassifier': + freqai_conf['freqai']['model_training_parameters'].update({ + "learning_rate": 3e-4, + "trainer_kwargs": { + "max_iters": 1, + "batch_size": 64, + "max_n_eval_batches": 1, + }, + "model_kwargs": { + "hidden_dim": 32, + "dropout_percent": 0.2, + "n_layer": 1, + } + }) + + if freqai.dd.model_type == 'joblib': model_file_extension = ".joblib" elif freqai.dd.model_type == "pytorch": From 81a2cbb4eb65c7b2a7df3b3da93a0b5d7c87800a Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 18:10:17 +0200 Subject: [PATCH 071/115] fix tests --- tests/freqai/test_freqai_interface.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index d35b00013..7931dc7a4 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -48,7 +48,7 @@ def can_run_model(model: str) -> None: ('XGBoostRegressor', False, True, False, True, False, 10), ('XGBoostRFRegressor', False, False, False, True, False, 0), ('CatboostRegressor', False, False, False, True, True, 0), - ('MLPPyTorchRegressor', False, False, False, True, False, 0), + ('PyTorchMLPRegressor', False, False, False, True, False, 0), ('ReinforcementLearner', False, True, False, True, False, 0), ('ReinforcementLearner_multiproc', False, False, False, True, False, 0), ('ReinforcementLearner_test_3ac', False, False, False, False, False, 0), @@ -86,7 +86,7 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, if 'test_3ac' in model or 'test_4ac' in model: freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models") - if 'MLPPyTorchRegressor' in model: + if 'PyTorchMLPRegressor' in model: model_save_ext = 'zip' freqai_conf['freqai']['model_training_parameters'].update({ "learning_rate": 3e-4, @@ -214,7 +214,7 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): freqai.extract_data_and_train_model(new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) - if 'MLPPyTorchClassifier': + if 'PyTorchMLPClassifier': freqai_conf['freqai']['model_training_parameters'].update({ "learning_rate": 3e-4, "trainer_kwargs": { From 500c401b759b7cac45d2c54ff5d74ed4aab3221c Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 18:39:50 +0200 Subject: [PATCH 072/115] improve pytorch classifier documentation --- .../freqai/prediction_models/PyTorchClassifier.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifier.py b/freqtrade/freqai/prediction_models/PyTorchClassifier.py index 01432e0fe..b14a89b38 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifier.py @@ -20,6 +20,18 @@ class PyTorchClassifier(BasePyTorchModel): """ A PyTorch implementation of a classifier. User must implement fit method + + Important! + User must declare the target class names in the strategy, under + IStrategy.set_freqai_targets method. + ``` + def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs): + self.freqai.class_names = ["down", "up"] + dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-100) > + dataframe["close"], 'up', 'down') + + return dataframe + ``` """ def __init__(self, **kwargs): super().__init__(**kwargs) @@ -127,7 +139,7 @@ class PyTorchClassifier(BasePyTorchModel): if not hasattr(self, "class_names"): raise ValueError( "Missing attribute: self.class_names " - "set self.freqai.class_names = [\"class a\", \"class b\", \"class c\"] " + "set self.freqai.class_names = ['class a', 'class b', 'class c'] " "inside IStrategy.set_freqai_targets method." ) return self.class_names From 6b4d9f97c13472267c5b6d7ef920eafd8001acb3 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 19:28:30 +0200 Subject: [PATCH 073/115] clean code --- freqtrade/freqai/prediction_models/PyTorchMLPModel.py | 6 +++--- freqtrade/freqai/prediction_models/PyTorchRegressor.py | 2 +- tests/freqai/test_freqai_interface.py | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py index a9f609e8e..22fb9c3f0 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPModel.py @@ -36,7 +36,7 @@ class PyTorchMLPModel(nn.Module): """ def __init__(self, input_dim: int, output_dim: int, **kwargs): - super(PyTorchMLPModel, self).__init__() + super().__init__() hidden_dim: int = kwargs.get("hidden_dim", 256) dropout_percent: int = kwargs.get("dropout_percent", 0.2) n_layer: int = kwargs.get("n_layer", 1) @@ -65,7 +65,7 @@ class Block(nn.Module): """ def __init__(self, hidden_dim: int, dropout_percent: int): - super(Block, self).__init__() + super().__init__() self.ff = FeedForward(hidden_dim) self.dropout = nn.Dropout(p=dropout_percent) self.ln = nn.LayerNorm(hidden_dim) @@ -85,7 +85,7 @@ class FeedForward(nn.Module): """ def __init__(self, hidden_dim: int): - super(FeedForward, self).__init__() + super().__init__() self.net = nn.Sequential( nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), diff --git a/freqtrade/freqai/prediction_models/PyTorchRegressor.py b/freqtrade/freqai/prediction_models/PyTorchRegressor.py index 837fbd836..440db96b9 100644 --- a/freqtrade/freqai/prediction_models/PyTorchRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchRegressor.py @@ -47,4 +47,4 @@ class PyTorchRegressor(BasePyTorchModel): y = self.model.model(x) pred_df = DataFrame(y.detach().numpy(), columns=[dk.label_list[0]]) - return (pred_df, dk.do_predict) \ No newline at end of file + return (pred_df, dk.do_predict) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 7931dc7a4..c1d9998d6 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -229,7 +229,6 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): } }) - if freqai.dd.model_type == 'joblib': model_file_extension = ".joblib" elif freqai.dd.model_type == "pytorch": From 0a55753faf54fb3e7e741041617de6a44c9e37aa Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 19:40:36 +0200 Subject: [PATCH 074/115] move default attributes of pytorch classifier to initializer, to prevent mypy from complaining --- .../prediction_models/PyTorchMLPClassifier.py | 16 +++++++++++----- .../prediction_models/PyTorchMLPRegressor.py | 16 +++++++++++----- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index ce8fbd336..edba75c2a 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -41,12 +41,18 @@ class PyTorchMLPClassifier(PyTorchClassifier): """ - def __init__(self, **kwargs): + def __init__( + self, + learning_rate: float = 3e-4, + model_kwargs: Dict[str, Any] = {}, + trainer_kwargs: Dict[str, Any] = {}, + **kwargs + ): super().__init__(**kwargs) - model_training_params = self.freqai_info.get("model_training_parameters", {}) - self.learning_rate: float = model_training_params.get("learning_rate", 3e-4) - self.model_kwargs: Dict[str, any] = model_training_params.get("model_kwargs", {}) - self.trainer_kwargs: Dict[str, any] = model_training_params.get("trainer_kwargs", {}) + config = self.freqai_info.get("model_training_parameters", {}) + self.learning_rate: float = config.get("learning_rate", learning_rate) + self.model_kwargs: Dict[str, any] = config.get("model_kwargs", model_kwargs) + self.trainer_kwargs: Dict[str, any] = config.get("trainer_kwargs", trainer_kwargs) def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py index 4685c332a..2118c27e1 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -41,12 +41,18 @@ class PyTorchMLPRegressor(PyTorchRegressor): """ - def __init__(self, **kwargs): + def __init__( + self, + learning_rate: float = 3e-4, + model_kwargs: Dict[str, Any] = {}, + trainer_kwargs: Dict[str, Any] = {}, + **kwargs + ): super().__init__(**kwargs) - model_training_params = self.freqai_info.get("model_training_parameters", {}) - self.learning_rate: float = model_training_params.get("learning_rate", 3e-4) - self.model_kwargs: Dict[str, any] = model_training_params.get("model_kwargs", {}) - self.trainer_kwargs: Dict[str, any] = model_training_params.get("trainer_kwargs", {}) + config = self.freqai_info.get("model_training_parameters", {}) + self.learning_rate: float = config.get("learning_rate", learning_rate) + self.model_kwargs: Dict[str, any] = config.get("model_kwargs", model_kwargs) + self.trainer_kwargs: Dict[str, any] = config.get("trainer_kwargs", trainer_kwargs) def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ From c06cd38951e5fbdb08c805cf573ff2bb4877a5cd Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 19:55:39 +0200 Subject: [PATCH 075/115] clean code --- freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py index 2118c27e1..06092c5a0 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -37,8 +37,6 @@ class PyTorchMLPRegressor(PyTorchRegressor): } } } - - """ def __init__( From a4b617e4824f633a03a65c4d6541f2e7587a7883 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 20:22:28 +0200 Subject: [PATCH 076/115] type hints fixes --- .../freqai/prediction_models/PyTorchMLPClassifier.py | 11 ++++------- .../freqai/prediction_models/PyTorchMLPRegressor.py | 4 ++-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index edba75c2a..6b7d9c034 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -13,8 +13,7 @@ class PyTorchMLPClassifier(PyTorchClassifier): This class implements the fit method of IFreqaiModel. in the fit method we initialize the model and trainer objects. the only requirement from the model is to be aligned to PyTorchClassifier - predict method that expects the model to predict tensor of type long. - the trainer defines the training loop. + predict method that expects the model to predict a tensor of type long. parameters are passed via `model_training_parameters` under the freqai section in the config file. e.g: @@ -37,8 +36,6 @@ class PyTorchMLPClassifier(PyTorchClassifier): } } } - - """ def __init__( @@ -51,15 +48,15 @@ class PyTorchMLPClassifier(PyTorchClassifier): super().__init__(**kwargs) config = self.freqai_info.get("model_training_parameters", {}) self.learning_rate: float = config.get("learning_rate", learning_rate) - self.model_kwargs: Dict[str, any] = config.get("model_kwargs", model_kwargs) - self.trainer_kwargs: Dict[str, any] = config.get("trainer_kwargs", trainer_kwargs) + self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", model_kwargs) + self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", trainer_kwargs) def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here :param data_dictionary: the dictionary constructed by DataHandler to hold all the training and test data/labels. - :raises ValueError: If self.class_names is not defined in the parent class. + :raises ValueError: If self.class_names is empty. """ class_names = self.get_class_names() diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py index 06092c5a0..16e7c0e79 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -49,8 +49,8 @@ class PyTorchMLPRegressor(PyTorchRegressor): super().__init__(**kwargs) config = self.freqai_info.get("model_training_parameters", {}) self.learning_rate: float = config.get("learning_rate", learning_rate) - self.model_kwargs: Dict[str, any] = config.get("model_kwargs", model_kwargs) - self.trainer_kwargs: Dict[str, any] = config.get("trainer_kwargs", trainer_kwargs) + self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", model_kwargs) + self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", trainer_kwargs) def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ From e8f040bfbd37108b50dab712716a5abc1ccfc2ec Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 20 Mar 2023 20:38:43 +0200 Subject: [PATCH 077/115] add class_name attribute to freqai interface --- freqtrade/freqai/freqai_interface.py | 1 + .../freqai/prediction_models/PyTorchClassifier.py | 15 +++++++++------ .../prediction_models/PyTorchMLPClassifier.py | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 8a1ac436b..470ae1911 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -83,6 +83,7 @@ class IFreqaiModel(ABC): self.CONV_WIDTH = self.freqai_info.get('conv_width', 1) if self.ft_params.get("inlier_metric_window", 0): self.CONV_WIDTH = self.ft_params.get("inlier_metric_window", 0) * 2 + self.class_names: List[str] = [] # used in classification children classes self.pair_it = 0 self.pair_it_train = 0 self.total_pairs = len(self.config.get("exchange", {}).get("pair_whitelist")) diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifier.py b/freqtrade/freqai/prediction_models/PyTorchClassifier.py index b14a89b38..e47021a55 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchClassifier.py @@ -22,8 +22,11 @@ class PyTorchClassifier(BasePyTorchModel): User must implement fit method Important! - User must declare the target class names in the strategy, under - IStrategy.set_freqai_targets method. + + - User must declare the target class names in the strategy, + under IStrategy.set_freqai_targets method. + + for example, in your strategy: ``` def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs): self.freqai.class_names = ["down", "up"] @@ -31,7 +34,6 @@ class PyTorchClassifier(BasePyTorchModel): dataframe["close"], 'up', 'down') return dataframe - ``` """ def __init__(self, **kwargs): super().__init__(**kwargs) @@ -55,7 +57,7 @@ class PyTorchClassifier(BasePyTorchModel): if not class_names: raise ValueError( "Missing class names. " - "self.model.model_meta_data[\"class_names\"] is None." + "self.model.model_meta_data['class_names'] is None." ) if not self.class_name_to_index: @@ -136,10 +138,11 @@ class PyTorchClassifier(BasePyTorchModel): self.encode_class_names(data_dictionary, dk, class_names) def get_class_names(self) -> List[str]: - if not hasattr(self, "class_names"): + if not self.class_names: raise ValueError( - "Missing attribute: self.class_names " + "self.class_names is empty, " "set self.freqai.class_names = ['class a', 'class b', 'class c'] " "inside IStrategy.set_freqai_targets method." ) + return self.class_names diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index 6b7d9c034..373b81a82 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -56,7 +56,7 @@ class PyTorchMLPClassifier(PyTorchClassifier): User sets up the training and test data to fit their desired model here :param data_dictionary: the dictionary constructed by DataHandler to hold all the training and test data/labels. - :raises ValueError: If self.class_names is empty. + :raises ValueError: If self.class_names is not defined in the parent class. """ class_names = self.get_class_names() From 9906e7d646f980ebbed60e4f9501042ac802750e Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 21 Mar 2023 11:23:45 +0200 Subject: [PATCH 078/115] clean code --- .../prediction_models/PyTorchMLPClassifier.py | 14 ++++---------- .../prediction_models/PyTorchMLPRegressor.py | 14 ++++---------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index 373b81a82..e26b8b52c 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -38,18 +38,12 @@ class PyTorchMLPClassifier(PyTorchClassifier): } """ - def __init__( - self, - learning_rate: float = 3e-4, - model_kwargs: Dict[str, Any] = {}, - trainer_kwargs: Dict[str, Any] = {}, - **kwargs - ): + def __init__(self, **kwargs): super().__init__(**kwargs) config = self.freqai_info.get("model_training_parameters", {}) - self.learning_rate: float = config.get("learning_rate", learning_rate) - self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", model_kwargs) - self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", trainer_kwargs) + self.learning_rate: float = config.get("learning_rate", 3e-4) + self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {}) + self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {}) def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py index 16e7c0e79..94c0dfe46 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -39,18 +39,12 @@ class PyTorchMLPRegressor(PyTorchRegressor): } """ - def __init__( - self, - learning_rate: float = 3e-4, - model_kwargs: Dict[str, Any] = {}, - trainer_kwargs: Dict[str, Any] = {}, - **kwargs - ): + def __init__(self, **kwargs): super().__init__(**kwargs) config = self.freqai_info.get("model_training_parameters", {}) - self.learning_rate: float = config.get("learning_rate", learning_rate) - self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", model_kwargs) - self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", trainer_kwargs) + self.learning_rate: float = config.get("learning_rate", 3e-4) + self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {}) + self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {}) def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ From 443263803ce332bc6aed38a1502594a31af9c49a Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 21 Mar 2023 11:42:05 +0200 Subject: [PATCH 079/115] unsqueeze target tensor when 1 dimensional --- freqtrade/freqai/base_models/PyTorchModelTrainer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 8097b8b85..52e6d5138 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -124,7 +124,7 @@ class PyTorchModelTrainer: data_loader_dictionary = {} for split in ["train", "test"]: labels_shape = data_dictionary[f"{split}_labels"].shape - labels_view = labels_shape[0] if labels_shape[1] == 1 else labels_shape + labels_view = (labels_shape[0], 1) if labels_shape[1] == 1 else labels_shape dataset = TensorDataset( torch.from_numpy(data_dictionary[f"{split}_features"].values).float(), torch.from_numpy(data_dictionary[f"{split}_labels"].values) From 97339e14cf63b116ef86da123ad52b1bb735ef54 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 21 Mar 2023 12:29:05 +0200 Subject: [PATCH 080/115] round up divisions in calc_n_epochs --- freqtrade/freqai/base_models/PyTorchModelTrainer.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 52e6d5138..6a4b128e3 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -1,4 +1,5 @@ import logging +import math from pathlib import Path from typing import Any, Dict, Optional @@ -148,10 +149,13 @@ class PyTorchModelTrainer: """ Calculates the number of epochs required to reach the maximum number of iterations specified in the model training parameters. + + the motivation here is that `max_iters` is easier to optimize and keep stable, + across different n_obs - the number of data points. """ - n_batches = n_obs // batch_size - epochs = n_iters // n_batches + n_batches = math.ceil(n_obs // batch_size) + epochs = math.ceil(n_iters // n_batches) return epochs def save(self, path: Path): From a80afc8f1b930334486c13b6836bf81fea978708 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 21 Mar 2023 13:20:54 +0200 Subject: [PATCH 081/115] add optional target tensor squeezing to pytorch trainer --- .../freqai/base_models/PyTorchModelTrainer.py | 18 +++++++++++------- .../prediction_models/PyTorchMLPClassifier.py | 1 + 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/base_models/PyTorchModelTrainer.py index 6a4b128e3..2ef4b57c9 100644 --- a/freqtrade/freqai/base_models/PyTorchModelTrainer.py +++ b/freqtrade/freqai/base_models/PyTorchModelTrainer.py @@ -22,6 +22,7 @@ class PyTorchModelTrainer: device: str, init_model: Dict, target_tensor_type: torch.dtype, + squeeze_target_tensor: bool = False, model_meta_data: Dict[str, Any] = {}, **kwargs ): @@ -35,11 +36,14 @@ class PyTorchModelTrainer: :param target_tensor_type: type of target tensor, for classification usually torch.long, for regressor usually torch.float. :param model_meta_data: Additional metadata about the model (optional). + :param squeeze_target_tensor: controls the target shape, used for loss functions + that requires 0D or 1D. :param max_iters: The number of training iterations to run. iteration here refers to the number of times we call self.optimizer.step(). used to calculate n_epochs. :param batch_size: The size of the batches to use during training. :param max_n_eval_batches: The maximum number batches to use for evaluation. + """ self.model = model self.optimizer = optimizer @@ -50,6 +54,7 @@ class PyTorchModelTrainer: self.max_iters: int = kwargs.get("max_iters", 100) self.batch_size: int = kwargs.get("batch_size", 64) self.max_n_eval_batches: Optional[int] = kwargs.get("max_n_eval_batches", None) + self.squeeze_target_tensor = squeeze_target_tensor if init_model: self.load_from_checkpoint(init_model) @@ -124,15 +129,14 @@ class PyTorchModelTrainer: """ data_loader_dictionary = {} for split in ["train", "test"]: - labels_shape = data_dictionary[f"{split}_labels"].shape - labels_view = (labels_shape[0], 1) if labels_shape[1] == 1 else labels_shape - dataset = TensorDataset( - torch.from_numpy(data_dictionary[f"{split}_features"].values).float(), - torch.from_numpy(data_dictionary[f"{split}_labels"].values) + x = torch.from_numpy(data_dictionary[f"{split}_features"].values).float() + y = torch.from_numpy(data_dictionary[f"{split}_labels"].values)\ .to(self.target_tensor_type) - .view(labels_view) - ) + if self.squeeze_target_tensor: + y = y.squeeze() + + dataset = TensorDataset(x, y) data_loader = DataLoader( dataset, batch_size=self.batch_size, diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index e26b8b52c..b8f2df28b 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -73,6 +73,7 @@ class PyTorchMLPClassifier(PyTorchClassifier): device=self.device, init_model=init_model, target_tensor_type=torch.long, + squeeze_target_tensor=True, **self.trainer_kwargs, ) trainer.fit(data_dictionary) From 3fa23860c01747a572f81f37cc6bc6dbae4640ac Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 21 Mar 2023 14:34:27 +0200 Subject: [PATCH 082/115] skip pytorch tests on python 3.11 and intel based mac os --- tests/freqai/test_freqai_interface.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index c1d9998d6..718d80f44 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -34,13 +34,14 @@ def is_mac() -> bool: def can_run_model(model: str) -> None: if (is_arm() or is_py11()) and "Catboost" in model: - pytest.skip("CatBoost is not supported on ARM") + pytest.skip("CatBoost is not supported on ARM.") - if is_mac() and not is_arm() and 'Reinforcement' in model: - pytest.skip("Reinforcement learning module not available on intel based Mac OS") + is_pytorch_model = 'Reinforcement' in model or 'PyTorch' in model + if is_pytorch_model and is_mac() and not is_arm(): + pytest.skip("Reinforcement learning / PyTorch module not available on intel based Mac OS.") - if is_py11() and 'Reinforcement' in model: - pytest.skip("Reinforcement learning currently not available on python 3.11.") + if is_pytorch_model and is_py11(): + pytest.skip("Reinforcement learning / PyTorch currently not available on python 3.11.") @pytest.mark.parametrize('model, pca, dbscan, float32, can_short, shuffle, buffer', [ From eba82360fac3776bcaed85e44d66e58292fa02df Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 21 Mar 2023 15:18:05 +0200 Subject: [PATCH 083/115] skip pytorch tests on python 3.11 and intel based mac os --- tests/freqai/test_freqai_interface.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 718d80f44..b4d808af2 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -146,8 +146,7 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, ('CatboostClassifierMultiTarget', "freqai_test_multimodel_classifier_strat") ]) def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, strat): - if (is_arm() or is_py11()) and 'Catboost' in model: - pytest.skip("CatBoost is not supported on ARM") + can_run_model(model) freqai_conf.update({"timerange": "20180110-20180130"}) freqai_conf.update({"strategy": strat}) @@ -189,8 +188,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s 'PyTorchMLPClassifier', ]) def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): - if (is_arm() or is_py11()) and model == 'CatboostClassifier': - pytest.skip("CatBoost is not supported on ARM") + can_run_model(model) freqai_conf.update({"freqaimodel": model}) freqai_conf.update({"strategy": "freqai_test_classifier"}) From 83a7d888bc4ec4f13a92a6f80c20a8c4f8a8b603 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 21 Mar 2023 15:19:34 +0200 Subject: [PATCH 084/115] type hint init in pytorch mlp classes --- freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py | 2 +- freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index b8f2df28b..f9214d410 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -38,7 +38,7 @@ class PyTorchMLPClassifier(PyTorchClassifier): } """ - def __init__(self, **kwargs): + def __init__(self, **kwargs) -> None: super().__init__(**kwargs) config = self.freqai_info.get("model_training_parameters", {}) self.learning_rate: float = config.get("learning_rate", 3e-4) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py index 94c0dfe46..20417736c 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -39,7 +39,7 @@ class PyTorchMLPRegressor(PyTorchRegressor): } """ - def __init__(self, **kwargs): + def __init__(self, **kwargs) -> None: super().__init__(**kwargs) config = self.freqai_info.get("model_training_parameters", {}) self.learning_rate: float = config.get("learning_rate", 3e-4) From 1ba01746a0993d1f826ddceaa99ce4bb2370f205 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Tue, 21 Mar 2023 15:09:54 +0100 Subject: [PATCH 085/115] organize pytorch files --- .../BaseTorchClassifier.py} | 2 +- .../BaseTorchRegressor.py} | 2 +- .../freqai/prediction_models/PyTorchMLPClassifier.py | 8 ++++---- freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py | 8 ++++---- .../{prediction_models => torch}/PyTorchMLPModel.py | 0 .../freqai/{base_models => torch}/PyTorchModelTrainer.py | 0 freqtrade/freqai/torch/__init__.py | 0 7 files changed, 10 insertions(+), 10 deletions(-) rename freqtrade/freqai/{prediction_models/PyTorchClassifier.py => base_models/BaseTorchClassifier.py} (99%) rename freqtrade/freqai/{prediction_models/PyTorchRegressor.py => base_models/BaseTorchRegressor.py} (97%) rename freqtrade/freqai/{prediction_models => torch}/PyTorchMLPModel.py (100%) rename freqtrade/freqai/{base_models => torch}/PyTorchModelTrainer.py (100%) create mode 100644 freqtrade/freqai/torch/__init__.py diff --git a/freqtrade/freqai/prediction_models/PyTorchClassifier.py b/freqtrade/freqai/base_models/BaseTorchClassifier.py similarity index 99% rename from freqtrade/freqai/prediction_models/PyTorchClassifier.py rename to freqtrade/freqai/base_models/BaseTorchClassifier.py index e47021a55..1cfd742db 100644 --- a/freqtrade/freqai/prediction_models/PyTorchClassifier.py +++ b/freqtrade/freqai/base_models/BaseTorchClassifier.py @@ -16,7 +16,7 @@ from freqtrade.freqai.data_kitchen import FreqaiDataKitchen logger = logging.getLogger(__name__) -class PyTorchClassifier(BasePyTorchModel): +class BaseTorchClassifier(BasePyTorchModel): """ A PyTorch implementation of a classifier. User must implement fit method diff --git a/freqtrade/freqai/prediction_models/PyTorchRegressor.py b/freqtrade/freqai/base_models/BaseTorchRegressor.py similarity index 97% rename from freqtrade/freqai/prediction_models/PyTorchRegressor.py rename to freqtrade/freqai/base_models/BaseTorchRegressor.py index 440db96b9..baaf097ee 100644 --- a/freqtrade/freqai/prediction_models/PyTorchRegressor.py +++ b/freqtrade/freqai/base_models/BaseTorchRegressor.py @@ -13,7 +13,7 @@ from freqtrade.freqai.data_kitchen import FreqaiDataKitchen logger = logging.getLogger(__name__) -class PyTorchRegressor(BasePyTorchModel): +class BaseTorchRegressor(BasePyTorchModel): """ A PyTorch implementation of a regressor. User must implement fit method diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index f9214d410..16866859b 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -2,13 +2,13 @@ from typing import Any, Dict import torch -from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer +from freqtrade.freqai.base_models.BaseTorchClassifier import BaseTorchClassifier from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.PyTorchClassifier import PyTorchClassifier -from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel +from freqtrade.freqai.torch.PyTorchMLPModel import PyTorchMLPModel +from freqtrade.freqai.torch.PyTorchModelTrainer import PyTorchModelTrainer -class PyTorchMLPClassifier(PyTorchClassifier): +class PyTorchMLPClassifier(BaseTorchClassifier): """ This class implements the fit method of IFreqaiModel. in the fit method we initialize the model and trainer objects. diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py index 20417736c..861d90a21 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -2,13 +2,13 @@ from typing import Any, Dict import torch -from freqtrade.freqai.base_models.PyTorchModelTrainer import PyTorchModelTrainer +from freqtrade.freqai.base_models.BaseTorchRegressor import BaseTorchRegressor from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.PyTorchMLPModel import PyTorchMLPModel -from freqtrade.freqai.prediction_models.PyTorchRegressor import PyTorchRegressor +from freqtrade.freqai.torch.PyTorchMLPModel import PyTorchMLPModel +from freqtrade.freqai.torch.PyTorchModelTrainer import PyTorchModelTrainer -class PyTorchMLPRegressor(PyTorchRegressor): +class PyTorchMLPRegressor(BaseTorchRegressor): """ This class implements the fit method of IFreqaiModel. in the fit method we initialize the model and trainer objects. diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPModel.py b/freqtrade/freqai/torch/PyTorchMLPModel.py similarity index 100% rename from freqtrade/freqai/prediction_models/PyTorchMLPModel.py rename to freqtrade/freqai/torch/PyTorchMLPModel.py diff --git a/freqtrade/freqai/base_models/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py similarity index 100% rename from freqtrade/freqai/base_models/PyTorchModelTrainer.py rename to freqtrade/freqai/torch/PyTorchModelTrainer.py diff --git a/freqtrade/freqai/torch/__init__.py b/freqtrade/freqai/torch/__init__.py new file mode 100644 index 000000000..e69de29bb From 02bccd0097aa442b46a4b4eb818bfba138057401 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 21 Mar 2023 16:20:35 +0200 Subject: [PATCH 086/115] add pytorch mlp models to test_start_backtesting --- tests/freqai/conftest.py | 16 +++++++++++ tests/freqai/test_freqai_interface.py | 38 ++++++++------------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/tests/freqai/conftest.py b/tests/freqai/conftest.py index 68e7ea49a..02cfdd882 100644 --- a/tests/freqai/conftest.py +++ b/tests/freqai/conftest.py @@ -83,6 +83,22 @@ def make_rl_config(conf): return conf +def mock_pytorch_mlp_model_training_parameters(conf): + return { + "learning_rate": 3e-4, + "trainer_kwargs": { + "max_iters": 1, + "batch_size": 64, + "max_n_eval_batches": 1, + }, + "model_kwargs": { + "hidden_dim": 32, + "dropout_percent": 0.2, + "n_layer": 1, + } + } + + def get_patched_data_kitchen(mocker, freqaiconf): dk = FreqaiDataKitchen(freqaiconf) return dk diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index b4d808af2..5b460cda1 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -89,19 +89,8 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, if 'PyTorchMLPRegressor' in model: model_save_ext = 'zip' - freqai_conf['freqai']['model_training_parameters'].update({ - "learning_rate": 3e-4, - "trainer_kwargs": { - "max_iters": 1, - "batch_size": 64, - "max_n_eval_batches": 1, - }, - "model_kwargs": { - "hidden_dim": 32, - "dropout_percent": 0.2, - "n_layer": 1, - } - }) + pytorch_mlp_mtp = mock_pytorch_mlp_model_training_parameters() + freqai_conf['freqai']['model_training_parameters'].update(pytorch_mlp_mtp) strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) @@ -214,19 +203,8 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): strategy, freqai.dk, data_load_timerange) if 'PyTorchMLPClassifier': - freqai_conf['freqai']['model_training_parameters'].update({ - "learning_rate": 3e-4, - "trainer_kwargs": { - "max_iters": 1, - "batch_size": 64, - "max_n_eval_batches": 1, - }, - "model_kwargs": { - "hidden_dim": 32, - "dropout_percent": 0.2, - "n_layer": 1, - } - }) + pytorch_mlp_mtp = mock_pytorch_mlp_model_training_parameters() + freqai_conf['freqai']['model_training_parameters'].update(pytorch_mlp_mtp) if freqai.dd.model_type == 'joblib': model_file_extension = ".joblib" @@ -251,10 +229,12 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): ("LightGBMRegressor", 2, "freqai_test_strat"), ("XGBoostRegressor", 2, "freqai_test_strat"), ("CatboostRegressor", 2, "freqai_test_strat"), + ("PyTorchMLPRegressor", 2, "freqai_test_strat"), ("ReinforcementLearner", 3, "freqai_rl_test_strat"), ("XGBoostClassifier", 2, "freqai_test_classifier"), ("LightGBMClassifier", 2, "freqai_test_classifier"), - ("CatboostClassifier", 2, "freqai_test_classifier") + ("CatboostClassifier", 2, "freqai_test_classifier"), + ("PyTorchMLPClassifier", 2, "freqai_test_classifier") ], ) def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog): @@ -275,6 +255,10 @@ def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog) if 'test_4ac' in model: freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models") + if 'PyTorchMLP' in model: + pytorch_mlp_mtp = mock_pytorch_mlp_model_training_parameters() + freqai_conf['freqai']['model_training_parameters'].update(pytorch_mlp_mtp) + freqai_conf.get("freqai", {}).get("feature_parameters", {}).update( {"indicator_periods_candles": [2]}) From b9c7d338b39e7a28bf3536bf219b1798c9b5d4e8 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 21 Mar 2023 16:38:05 +0200 Subject: [PATCH 087/115] fix test_start_backtesting --- tests/freqai/conftest.py | 3 ++- tests/freqai/test_freqai_interface.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/freqai/conftest.py b/tests/freqai/conftest.py index 02cfdd882..0f29301d0 100644 --- a/tests/freqai/conftest.py +++ b/tests/freqai/conftest.py @@ -1,5 +1,6 @@ from copy import deepcopy from pathlib import Path +from typing import Any, Dict from unittest.mock import MagicMock import pytest @@ -83,7 +84,7 @@ def make_rl_config(conf): return conf -def mock_pytorch_mlp_model_training_parameters(conf): +def mock_pytorch_mlp_model_training_parameters() -> Dict[str, Any]: return { "learning_rate": 3e-4, "trainer_kwargs": { diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 5b460cda1..8b126fe55 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -15,7 +15,8 @@ from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence import Trade from freqtrade.plugins.pairlistmanager import PairListManager from tests.conftest import EXMS, create_mock_trades, get_patched_exchange, log_has_re -from tests.freqai.conftest import get_patched_freqai_strategy, make_rl_config +from tests.freqai.conftest import get_patched_freqai_strategy, make_rl_config, \ + mock_pytorch_mlp_model_training_parameters def is_py11() -> bool: From f81e3d86677ff0f1549983f3665b2d8269014ca1 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 21 Mar 2023 16:42:13 +0200 Subject: [PATCH 088/115] sort imports --- tests/freqai/test_freqai_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 8b126fe55..ffac1e248 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -15,8 +15,8 @@ from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence import Trade from freqtrade.plugins.pairlistmanager import PairListManager from tests.conftest import EXMS, create_mock_trades, get_patched_exchange, log_has_re -from tests.freqai.conftest import get_patched_freqai_strategy, make_rl_config, \ - mock_pytorch_mlp_model_training_parameters +from tests.freqai.conftest import (get_patched_freqai_strategy, make_rl_config, + mock_pytorch_mlp_model_training_parameters) def is_py11() -> bool: From 479aafc331410d2533e8ae7e3cd1e8554890ac38 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Wed, 22 Mar 2023 17:50:00 +0200 Subject: [PATCH 089/115] rename Torch to PyTorch --- .../{BaseTorchClassifier.py => BasePyTorchClassifier.py} | 2 +- .../{BaseTorchRegressor.py => BasePyTorchRegressor.py} | 2 +- freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py | 4 ++-- freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) rename freqtrade/freqai/base_models/{BaseTorchClassifier.py => BasePyTorchClassifier.py} (99%) rename freqtrade/freqai/base_models/{BaseTorchRegressor.py => BasePyTorchRegressor.py} (97%) diff --git a/freqtrade/freqai/base_models/BaseTorchClassifier.py b/freqtrade/freqai/base_models/BasePyTorchClassifier.py similarity index 99% rename from freqtrade/freqai/base_models/BaseTorchClassifier.py rename to freqtrade/freqai/base_models/BasePyTorchClassifier.py index 1cfd742db..c08142876 100644 --- a/freqtrade/freqai/base_models/BaseTorchClassifier.py +++ b/freqtrade/freqai/base_models/BasePyTorchClassifier.py @@ -16,7 +16,7 @@ from freqtrade.freqai.data_kitchen import FreqaiDataKitchen logger = logging.getLogger(__name__) -class BaseTorchClassifier(BasePyTorchModel): +class BasePyTorchClassifier(BasePyTorchModel): """ A PyTorch implementation of a classifier. User must implement fit method diff --git a/freqtrade/freqai/base_models/BaseTorchRegressor.py b/freqtrade/freqai/base_models/BasePyTorchRegressor.py similarity index 97% rename from freqtrade/freqai/base_models/BaseTorchRegressor.py rename to freqtrade/freqai/base_models/BasePyTorchRegressor.py index baaf097ee..756853496 100644 --- a/freqtrade/freqai/base_models/BaseTorchRegressor.py +++ b/freqtrade/freqai/base_models/BasePyTorchRegressor.py @@ -13,7 +13,7 @@ from freqtrade.freqai.data_kitchen import FreqaiDataKitchen logger = logging.getLogger(__name__) -class BaseTorchRegressor(BasePyTorchModel): +class BasePyTorchRegressor(BasePyTorchModel): """ A PyTorch implementation of a regressor. User must implement fit method diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index 16866859b..20c0b0c65 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -2,13 +2,13 @@ from typing import Any, Dict import torch -from freqtrade.freqai.base_models.BaseTorchClassifier import BaseTorchClassifier +from freqtrade.freqai.base_models.BasePyTorchClassifier import BasePyTorchClassifier from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.torch.PyTorchMLPModel import PyTorchMLPModel from freqtrade.freqai.torch.PyTorchModelTrainer import PyTorchModelTrainer -class PyTorchMLPClassifier(BaseTorchClassifier): +class PyTorchMLPClassifier(BasePyTorchClassifier): """ This class implements the fit method of IFreqaiModel. in the fit method we initialize the model and trainer objects. diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py index 861d90a21..df149ffbf 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -2,13 +2,13 @@ from typing import Any, Dict import torch -from freqtrade.freqai.base_models.BaseTorchRegressor import BaseTorchRegressor +from freqtrade.freqai.base_models.BasePyTorchRegressor import BasePyTorchRegressor from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.torch.PyTorchMLPModel import PyTorchMLPModel from freqtrade.freqai.torch.PyTorchModelTrainer import PyTorchModelTrainer -class PyTorchMLPRegressor(BaseTorchRegressor): +class PyTorchMLPRegressor(BasePyTorchRegressor): """ This class implements the fit method of IFreqaiModel. in the fit method we initialize the model and trainer objects. From 36a005754a955747cb1bf4a5f2819a3c3dfa7abe Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Wed, 22 Mar 2023 18:15:57 +0200 Subject: [PATCH 090/115] add pytorch documentation --- docs/freqai-configuration.md | 88 ++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/docs/freqai-configuration.md b/docs/freqai-configuration.md index 886dc2338..f1cf37923 100644 --- a/docs/freqai-configuration.md +++ b/docs/freqai-configuration.md @@ -236,3 +236,91 @@ If you want to predict multiple targets you must specify all labels in the same df['&s-up_or_down'] = np.where( df["close"].shift(-100) > df["close"], 'up', 'down') df['&s-up_or_down'] = np.where( df["close"].shift(-100) == df["close"], 'same', df['&s-up_or_down']) ``` + +## PyTorch Models + +### Quick start + +The easiest way to quickly run a pytorch model is with the following command (for regression task): + +```bash +freqtrade trade --config config_examples/config_freqai.example.json --strategy FreqaiExampleStrategy --freqaimodel PyTorchMLPRegressor --strategy-path freqtrade/templates +``` + +### Structure + +#### Model +You can use any pytorch model. Here is an example of logistic regression model implementation using pytorch (should be used with nn.BCELoss criterion) for classification tasks. + +```python +import torch.nn as nn +import torch + +class LogisticRegression(nn.Module): + def __init__(self, input_size: int): + super().__init__() + # Define your layers + self.linear = nn.Linear(input_size, 1) + self.activation = nn.Sigmoid() + + def forward(self, x: torch.Tensor) -> torch.Tensor: + # Define the forward pass + out = self.linear(x) + out = self.activation(out) + return out +``` + + +#### Trainer +The `PyTorchModelTrainer` performs the idiomatic pytorch train loop: +Define our model, loss function, and optimizer, and then move them to the appropriate device (GPU or CPU). Inside the loop, we iterate through the batches in the dataloader, move the data to the device, compute the prediction and loss, backpropagate, and update the model parameters using the optimizer. + +In addition, the trainer is responsible for the following: + - saving and loading the model + - converting the data from `pandas.DataFrame` to `torch.Tensor`. + +#### Integration with Freqai module +Like all freqai models, PyTorch models inherit `IFreqaiModel`. `IFreqaiModel` declares three abstract methods: `train`, `fit`, and `predict`. we implement these methods in three levels of hierarchy. +From top to bottom: +1. `BasePyTorchModel` - all `BasePyTorch*` inherit it. Implements the `train` method responsible for general data preparation (e.g., data normalization) and calling the `fit` method. Sets `device _type` attribute used by children classes. Sets `model_type` attribute used by the parent class. +2. `BasePyTorch*` - Here, the `*` represents a group of algorithms, such as classifiers or regressors. the `predict` method is responsible for data preprocessing, predicting, and postprocessing if needed. + +3. PyTorch*Classifier / PyTorch*Regressor - implements the `fit` method, responsible for the main train flaw, where we initialize the trainer and model objects. + +#### Full example +Building a PyTorch regressor using MLP (multilayer perceptron) model, MSELoss criterion, and AdamW optimizer. + +```python +class PyTorchMLPRegressor(BasePyTorchRegressor): + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + config = self.freqai_info.get("model_training_parameters", {}) + self.learning_rate: float = config.get("learning_rate", 3e-4) + self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {}) + self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {}) + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + n_features = data_dictionary["train_features"].shape[-1] + model = PyTorchMLPModel( + input_dim=n_features, + output_dim=1, + **self.model_kwargs + ) + model.to(self.device) + optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) + criterion = torch.nn.MSELoss() + init_model = self.get_init_model(dk.pair) + trainer = PyTorchModelTrainer( + model=model, + optimizer=optimizer, + criterion=criterion, + device=self.device, + init_model=init_model, + target_tensor_type=torch.float, + **self.trainer_kwargs, + ) + trainer.fit(data_dictionary) + return trainer +``` + +Here we create `PyTorchMLPRegressor` that implements the `fit` method. The `fit` method specifies the training building blocks: model, optimizer, criterion, and trainer. We inherit both `BasePyTorchRegressor` and `BasePyTorchModel` (a parent of `BasePyTorchRegressor`). The former implements the `predict` method that suits our regression task. The latter implements the `train` method. \ No newline at end of file From fc8625c5c5fdde64f7b5737ac1d521bf3a8db7f8 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 23 Mar 2023 12:13:27 +0200 Subject: [PATCH 091/115] add pytorch classes uml diagram --- docs/assets/freqai_pytorch-diagram.png | Bin 0 -> 18611 bytes docs/freqai-configuration.md | 9 ++++++--- 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 docs/assets/freqai_pytorch-diagram.png diff --git a/docs/assets/freqai_pytorch-diagram.png b/docs/assets/freqai_pytorch-diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..f48ebae2570b76ad7cea67c4084b350db3491894 GIT binary patch literal 18611 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 EA8mju#{d8T literal 0 HcmV?d00001 diff --git a/docs/freqai-configuration.md b/docs/freqai-configuration.md index f1cf37923..ba2976bec 100644 --- a/docs/freqai-configuration.md +++ b/docs/freqai-configuration.md @@ -282,10 +282,13 @@ In addition, the trainer is responsible for the following: #### Integration with Freqai module Like all freqai models, PyTorch models inherit `IFreqaiModel`. `IFreqaiModel` declares three abstract methods: `train`, `fit`, and `predict`. we implement these methods in three levels of hierarchy. From top to bottom: -1. `BasePyTorchModel` - all `BasePyTorch*` inherit it. Implements the `train` method responsible for general data preparation (e.g., data normalization) and calling the `fit` method. Sets `device _type` attribute used by children classes. Sets `model_type` attribute used by the parent class. -2. `BasePyTorch*` - Here, the `*` represents a group of algorithms, such as classifiers or regressors. the `predict` method is responsible for data preprocessing, predicting, and postprocessing if needed. -3. PyTorch*Classifier / PyTorch*Regressor - implements the `fit` method, responsible for the main train flaw, where we initialize the trainer and model objects. +![image](assets/freqai_pytorch-diagram.png) + +1. `BasePyTorchModel` - Implements the `train` method. all `BasePyTorch*` inherit it. responsible for general data preparation (e.g., data normalization) and calling the `fit` method. Sets `device _type` attribute used by children classes. Sets `model_type` attribute used by the parent class. +2. `BasePyTorch*` - Implements the `predict` method. Here, the `*` represents a group of algorithms, such as classifiers or regressors. responsible for data preprocessing, predicting, and postprocessing if needed. + +3. `PyTorch*Classifier` / `PyTorch*Regressor` - implements the `fit` method. responsible for the main train flaw, where we initialize the trainer and model objects. #### Full example Building a PyTorch regressor using MLP (multilayer perceptron) model, MSELoss criterion, and AdamW optimizer. From c44b5b1b3aa165bc247348748102d587262df351 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 23 Mar 2023 12:41:20 +0200 Subject: [PATCH 092/115] add pytorch parameters to parameter table docs --- docs/freqai-parameter-table.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/freqai-parameter-table.md b/docs/freqai-parameter-table.md index 275062a33..122d87459 100644 --- a/docs/freqai-parameter-table.md +++ b/docs/freqai-parameter-table.md @@ -85,6 +85,26 @@ Mandatory parameters are marked as **Required** and have to be set in one of the | `net_arch` | Network architecture which is well described in [`stable_baselines3` doc](https://stable-baselines3.readthedocs.io/en/master/guide/custom_policy.html#examples). In summary: `[, dict(vf=[], pi=[])]`. By default this is set to `[128, 128]`, which defines 2 shared hidden layers with 128 units each. | `randomize_starting_position` | Randomize the starting point of each episode to avoid overfitting.
**Datatype:** bool.
Default: `False`. +### PyTorch parameters + +#### general + +| Parameter | Description | +|------------|-------------| +| | **Model training parameters within the freqai.model_training_parameters sub dictionary** +| `learning_rate` | learning rate to be passed to the optimizer.
**Datatype:** float.
Default: `3e-4`. +| `model_kwargs` | paramters to be passed to the model class.
**Datatype:** dict.
Default: `{}`. +| `trainer_kwargs` | paramters to be passed to the trainer class.
**Datatype:** dict.
Default: `{}`. + +#### trainer_kwargs + +| Parameter | Description | +|------------|-------------| +| `max_iters` | The number of training iterations to run. iteration here refers to the number of times we call self.optimizer.step(). used to calculate n_epochs.
**Datatype:** int.
Default: `100`. +| `batch_size` | The size of the batches to use during training..
**Datatype:** int.
Default: `64`. +| `max_n_eval_batches` | The maximum number batches to use for evaluation..
**Datatype:** int, optional.
Default: `None`. + + ### Additional parameters | Parameter | Description | From 952e6412137e2e7dfbc79a8624bc7d781cecf722 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 23 Mar 2023 12:43:37 +0200 Subject: [PATCH 093/115] small docs change --- docs/freqai-parameter-table.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/freqai-parameter-table.md b/docs/freqai-parameter-table.md index 122d87459..2d875fe89 100644 --- a/docs/freqai-parameter-table.md +++ b/docs/freqai-parameter-table.md @@ -87,7 +87,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the ### PyTorch parameters -#### general +#### general: | Parameter | Description | |------------|-------------| @@ -96,7 +96,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the | `model_kwargs` | paramters to be passed to the model class.
**Datatype:** dict.
Default: `{}`. | `trainer_kwargs` | paramters to be passed to the trainer class.
**Datatype:** dict.
Default: `{}`. -#### trainer_kwargs +#### trainer_kwargs: | Parameter | Description | |------------|-------------| From 45c6ae446f19c7c2216671778f58391472c6dd2e Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 23 Mar 2023 15:04:29 +0200 Subject: [PATCH 094/115] small docs change --- docs/freqai-configuration.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/freqai-configuration.md b/docs/freqai-configuration.md index ba2976bec..63d7c571c 100644 --- a/docs/freqai-configuration.md +++ b/docs/freqai-configuration.md @@ -283,13 +283,12 @@ In addition, the trainer is responsible for the following: Like all freqai models, PyTorch models inherit `IFreqaiModel`. `IFreqaiModel` declares three abstract methods: `train`, `fit`, and `predict`. we implement these methods in three levels of hierarchy. From top to bottom: -![image](assets/freqai_pytorch-diagram.png) - 1. `BasePyTorchModel` - Implements the `train` method. all `BasePyTorch*` inherit it. responsible for general data preparation (e.g., data normalization) and calling the `fit` method. Sets `device _type` attribute used by children classes. Sets `model_type` attribute used by the parent class. 2. `BasePyTorch*` - Implements the `predict` method. Here, the `*` represents a group of algorithms, such as classifiers or regressors. responsible for data preprocessing, predicting, and postprocessing if needed. - 3. `PyTorch*Classifier` / `PyTorch*Regressor` - implements the `fit` method. responsible for the main train flaw, where we initialize the trainer and model objects. +![image](assets/freqai_pytorch-diagram.png) + #### Full example Building a PyTorch regressor using MLP (multilayer perceptron) model, MSELoss criterion, and AdamW optimizer. From eabd321281a3dd112ed0dbcd325a1b28b9b5625a Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Thu, 23 Mar 2023 15:59:57 +0200 Subject: [PATCH 095/115] small docs change --- docs/freqai-configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/freqai-configuration.md b/docs/freqai-configuration.md index 63d7c571c..92cff06ff 100644 --- a/docs/freqai-configuration.md +++ b/docs/freqai-configuration.md @@ -283,7 +283,7 @@ In addition, the trainer is responsible for the following: Like all freqai models, PyTorch models inherit `IFreqaiModel`. `IFreqaiModel` declares three abstract methods: `train`, `fit`, and `predict`. we implement these methods in three levels of hierarchy. From top to bottom: -1. `BasePyTorchModel` - Implements the `train` method. all `BasePyTorch*` inherit it. responsible for general data preparation (e.g., data normalization) and calling the `fit` method. Sets `device _type` attribute used by children classes. Sets `model_type` attribute used by the parent class. +1. `BasePyTorchModel` - Implements the `train` method. all `BasePyTorch*` inherit it. responsible for general data preparation (e.g., data normalization) and calling the `fit` method. Sets `device` attribute used by children classes. Sets `model_type` attribute used by the parent class. 2. `BasePyTorch*` - Implements the `predict` method. Here, the `*` represents a group of algorithms, such as classifiers or regressors. responsible for data preprocessing, predicting, and postprocessing if needed. 3. `PyTorch*Classifier` / `PyTorch*Regressor` - implements the `fit` method. responsible for the main train flaw, where we initialize the trainer and model objects. From 8903ba5d89eb434ab506b55ff703346a5b003d43 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Fri, 24 Mar 2023 20:35:55 +0300 Subject: [PATCH 096/115] fix enf of file --- docs/freqai-configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/freqai-configuration.md b/docs/freqai-configuration.md index 92cff06ff..7b08dd67c 100644 --- a/docs/freqai-configuration.md +++ b/docs/freqai-configuration.md @@ -325,4 +325,4 @@ class PyTorchMLPRegressor(BasePyTorchRegressor): return trainer ``` -Here we create `PyTorchMLPRegressor` that implements the `fit` method. The `fit` method specifies the training building blocks: model, optimizer, criterion, and trainer. We inherit both `BasePyTorchRegressor` and `BasePyTorchModel` (a parent of `BasePyTorchRegressor`). The former implements the `predict` method that suits our regression task. The latter implements the `train` method. \ No newline at end of file +Here we create `PyTorchMLPRegressor` that implements the `fit` method. The `fit` method specifies the training building blocks: model, optimizer, criterion, and trainer. We inherit both `BasePyTorchRegressor` and `BasePyTorchModel` (a parent of `BasePyTorchRegressor`). The former implements the `predict` method that suits our regression task. The latter implements the `train` method. From 026b6a39a9e91ecc8f2827b73db3e8429b55292e Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 28 Mar 2023 14:40:23 +0300 Subject: [PATCH 097/115] bugfix skip test split when empty --- .../base_models/BasePyTorchClassifier.py | 2 +- .../freqai/base_models/BasePyTorchModel.py | 2 ++ .../prediction_models/PyTorchMLPClassifier.py | 2 +- .../prediction_models/PyTorchMLPRegressor.py | 2 +- freqtrade/freqai/torch/PyTorchModelTrainer.py | 35 ++++++++++++------- 5 files changed, 28 insertions(+), 15 deletions(-) diff --git a/freqtrade/freqai/base_models/BasePyTorchClassifier.py b/freqtrade/freqai/base_models/BasePyTorchClassifier.py index c08142876..7795b37ce 100644 --- a/freqtrade/freqai/base_models/BasePyTorchClassifier.py +++ b/freqtrade/freqai/base_models/BasePyTorchClassifier.py @@ -97,7 +97,7 @@ class BasePyTorchClassifier(BasePyTorchModel): """ target_column_name = dk.label_list[0] - for split in ["train", "test"]: + for split in self.splits: label_df = data_dictionary[f"{split}_labels"] self.assert_valid_class_names(label_df[target_column_name], class_names) label_df[target_column_name] = list( diff --git a/freqtrade/freqai/base_models/BasePyTorchModel.py b/freqtrade/freqai/base_models/BasePyTorchModel.py index d6372fa36..189f7d906 100644 --- a/freqtrade/freqai/base_models/BasePyTorchModel.py +++ b/freqtrade/freqai/base_models/BasePyTorchModel.py @@ -22,6 +22,8 @@ class BasePyTorchModel(IFreqaiModel): super().__init__(config=kwargs["config"]) self.dd.model_type = "pytorch" self.device = "cuda" if torch.cuda.is_available() else "cpu" + test_size = self.freqai_info.get('data_split_parameters', {}).get('test_size') + self.splits = ["train", "test"] if test_size != 0 else ["train"] def train( self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index 20c0b0c65..389aa6155 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -76,5 +76,5 @@ class PyTorchMLPClassifier(BasePyTorchClassifier): squeeze_target_tensor=True, **self.trainer_kwargs, ) - trainer.fit(data_dictionary) + trainer.fit(data_dictionary, self.splits) return trainer diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py index df149ffbf..ca6a13f6e 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -72,5 +72,5 @@ class PyTorchMLPRegressor(BasePyTorchRegressor): target_tensor_type=torch.float, **self.trainer_kwargs, ) - trainer.fit(data_dictionary) + trainer.fit(data_dictionary, self.splits) return trainer diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index 2ef4b57c9..609e19eda 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -1,7 +1,7 @@ import logging import math from pathlib import Path -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional import pandas as pd import torch @@ -43,7 +43,6 @@ class PyTorchModelTrainer: self.optimizer.step(). used to calculate n_epochs. :param batch_size: The size of the batches to use during training. :param max_n_eval_batches: The maximum number batches to use for evaluation. - """ self.model = model self.optimizer = optimizer @@ -58,21 +57,27 @@ class PyTorchModelTrainer: if init_model: self.load_from_checkpoint(init_model) - def fit(self, data_dictionary: Dict[str, pd.DataFrame]): + def fit(self, data_dictionary: Dict[str, pd.DataFrame], splits: List[str]): """ + :param data_dictionary: the dictionary constructed by DataHandler to hold + all the training and test data/labels. + :param splits: splits to use in training, splits must contain "train", + optional "test" could be added by setting freqai.data_split_parameters.test_size > 0 + in the config file. + - Calculates the predicted output for the batch using the PyTorch model. - Calculates the loss between the predicted and actual output using a loss function. - Computes the gradients of the loss with respect to the model's parameters using backpropagation. - Updates the model's parameters using an optimizer. """ - data_loaders_dictionary = self.create_data_loaders_dictionary(data_dictionary) + data_loaders_dictionary = self.create_data_loaders_dictionary(data_dictionary, splits) epochs = self.calc_n_epochs( n_obs=len(data_dictionary["train_features"]), batch_size=self.batch_size, n_iters=self.max_iters ) - for epoch in range(epochs): + for epoch in range(1, epochs+1): # training losses = [] for i, batch_data in enumerate(data_loaders_dictionary["train"]): @@ -87,13 +92,18 @@ class PyTorchModelTrainer: self.optimizer.step() losses.append(loss.item()) train_loss = sum(losses) / len(losses) + log_message = f"epoch {epoch}/{epochs}: train loss {train_loss:.4f}" # evaluation - test_loss = self.estimate_loss(data_loaders_dictionary, self.max_n_eval_batches, "test") - logger.info( - f"epoch {epoch}/{epochs}:" - f" train loss {train_loss:.4f} ; test loss {test_loss:.4f}" - ) + if "test" in splits: + test_loss = self.estimate_loss( + data_loaders_dictionary, + self.max_n_eval_batches, + "test" + ) + log_message += f" ; test loss {test_loss:.4f}" + + logger.info(log_message) @torch.no_grad() def estimate_loss( @@ -122,13 +132,14 @@ class PyTorchModelTrainer: def create_data_loaders_dictionary( self, - data_dictionary: Dict[str, pd.DataFrame] + data_dictionary: Dict[str, pd.DataFrame], + splits: List[str] ) -> Dict[str, DataLoader]: """ Converts the input data to PyTorch tensors using a data loader. """ data_loader_dictionary = {} - for split in ["train", "test"]: + for split in splits: x = torch.from_numpy(data_dictionary[f"{split}_features"].values).float() y = torch.from_numpy(data_dictionary[f"{split}_labels"].values)\ .to(self.target_tensor_type) From b795a70102bbc6b0c26a57b0d7d87586500ac747 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 28 Mar 2023 14:41:25 +0300 Subject: [PATCH 098/115] fix config example in pytorch mlp documentation --- freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py | 4 ++-- freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index 389aa6155..a44214367 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -26,7 +26,7 @@ class PyTorchMLPClassifier(BasePyTorchClassifier): "trainer_kwargs": { "max_iters": 5000, "batch_size": 64, - "max_n_eval_batches": None, + "max_n_eval_batches": null, }, "model_kwargs": { "hidden_dim": 512, @@ -49,7 +49,7 @@ class PyTorchMLPClassifier(BasePyTorchClassifier): """ User sets up the training and test data to fit their desired model here :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + all the training and test data/labels. :raises ValueError: If self.class_names is not defined in the parent class. """ diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py index ca6a13f6e..6fc2be1a5 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -27,7 +27,7 @@ class PyTorchMLPRegressor(BasePyTorchRegressor): "trainer_kwargs": { "max_iters": 5000, "batch_size": 64, - "max_n_eval_batches": None, + "max_n_eval_batches": null, }, "model_kwargs": { "hidden_dim": 512, @@ -50,7 +50,7 @@ class PyTorchMLPRegressor(BasePyTorchRegressor): """ User sets up the training and test data to fit their desired model here :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + all the training and test data/labels. """ n_features = data_dictionary["train_features"].shape[-1] From dfbebdea9bf328f4946295de00ad0830fa643efc Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 28 Mar 2023 14:42:52 +0300 Subject: [PATCH 099/115] improve comment on class_names in freqai interface --- freqtrade/freqai/freqai_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 470ae1911..9218d130c 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -83,7 +83,7 @@ class IFreqaiModel(ABC): self.CONV_WIDTH = self.freqai_info.get('conv_width', 1) if self.ft_params.get("inlier_metric_window", 0): self.CONV_WIDTH = self.ft_params.get("inlier_metric_window", 0) * 2 - self.class_names: List[str] = [] # used in classification children classes + self.class_names: List[str] = [] # used in classification subclasses self.pair_it = 0 self.pair_it_train = 0 self.total_pairs = len(self.config.get("exchange", {}).get("pair_whitelist")) From 8ac3a9435865b482977cefcfe3ea438a3b9b4e27 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 28 Mar 2023 14:45:54 +0300 Subject: [PATCH 100/115] add note to pytorch docs - setting class names for classifiers --- docs/freqai-configuration.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/freqai-configuration.md b/docs/freqai-configuration.md index 7b08dd67c..442705b53 100644 --- a/docs/freqai-configuration.md +++ b/docs/freqai-configuration.md @@ -325,4 +325,18 @@ class PyTorchMLPRegressor(BasePyTorchRegressor): return trainer ``` -Here we create `PyTorchMLPRegressor` that implements the `fit` method. The `fit` method specifies the training building blocks: model, optimizer, criterion, and trainer. We inherit both `BasePyTorchRegressor` and `BasePyTorchModel` (a parent of `BasePyTorchRegressor`). The former implements the `predict` method that suits our regression task. The latter implements the `train` method. +Here we create a `PyTorchMLPRegressor` class that implements the `fit` method. The `fit` method specifies the training building blocks: model, optimizer, criterion, and trainer. We inherit both `BasePyTorchRegressor` and `BasePyTorchModel`, where the former implements the `predict` method that is suitable for our regression task, and the latter implements the train method. + +??? Note "Setting Class Names for Classifiers" + When using classifiers, the user must declare the class names (or targets) by overriding the `IFreqaiModel.class_names` attribute. This is achieved by setting `self.freqai.class_names` in the FreqAI strategy inside the `set_freqai_targets` method. + + For example, if you are using a binary classifier to predict price movements as up or down, you can set the class names as follows: + ```python + def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs): + self.freqai.class_names = ["down", "up"] + dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-100) > + dataframe["close"], 'up', 'down') + + return dataframe + ``` + To see a full example, you can refer to the [classifier test strategy class](https://github.com/freqtrade/freqtrade/blob/develop/tests/strategy/strats/freqai_test_classifier.py). From 077a9479728adbc7e341a019b24708e1b552758e Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 28 Mar 2023 15:18:10 +0300 Subject: [PATCH 101/115] clean code --- freqtrade/freqai/torch/PyTorchModelTrainer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index 609e19eda..eda880d02 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -77,7 +77,7 @@ class PyTorchModelTrainer: batch_size=self.batch_size, n_iters=self.max_iters ) - for epoch in range(1, epochs+1): + for epoch in range(1, epochs + 1): # training losses = [] for i, batch_data in enumerate(data_loaders_dictionary["train"]): From 5a7ca35c6b4c7c2db68a440b65d7ec4c611674e6 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 28 Mar 2023 16:24:31 +0300 Subject: [PATCH 102/115] declare class names in FreqaiExampleHybridStrategy --- freqtrade/templates/FreqaiExampleHybridStrategy.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/templates/FreqaiExampleHybridStrategy.py b/freqtrade/templates/FreqaiExampleHybridStrategy.py index 0e7113f8c..3f27ee4a1 100644 --- a/freqtrade/templates/FreqaiExampleHybridStrategy.py +++ b/freqtrade/templates/FreqaiExampleHybridStrategy.py @@ -223,6 +223,7 @@ class FreqaiExampleHybridStrategy(IStrategy): :param metadata: metadata of current pair usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"] """ + self.freqai.class_names = ["down", "up"] dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-50) > dataframe["close"], 'up', 'down') From bd3b70293f3227facb476e6a0eb1f090009a26cc Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 3 Apr 2023 15:19:10 +0300 Subject: [PATCH 103/115] add pytorch data convertor --- .../base_models/BasePyTorchClassifier.py | 9 ++- .../freqai/base_models/BasePyTorchModel.py | 12 +++- .../base_models/BasePyTorchRegressor.py | 10 ++-- .../prediction_models/PyTorchMLPClassifier.py | 9 ++- .../prediction_models/PyTorchMLPRegressor.py | 8 ++- .../freqai/torch/PyTorchDataConvertor.py | 56 +++++++++++++++++++ freqtrade/freqai/torch/PyTorchMLPModel.py | 4 +- freqtrade/freqai/torch/PyTorchModelTrainer.py | 46 +++++++-------- .../freqai/torch/PyTorchTrainerInterface.py | 54 ++++++++++++++++++ 9 files changed, 168 insertions(+), 40 deletions(-) create mode 100644 freqtrade/freqai/torch/PyTorchDataConvertor.py create mode 100644 freqtrade/freqai/torch/PyTorchTrainerInterface.py diff --git a/freqtrade/freqai/base_models/BasePyTorchClassifier.py b/freqtrade/freqai/base_models/BasePyTorchClassifier.py index 7795b37ce..977152cc5 100644 --- a/freqtrade/freqai/base_models/BasePyTorchClassifier.py +++ b/freqtrade/freqai/base_models/BasePyTorchClassifier.py @@ -69,12 +69,11 @@ class BasePyTorchClassifier(BasePyTorchModel): ) filtered_df = dk.normalize_data_from_metadata(filtered_df) dk.data_dictionary["prediction_features"] = filtered_df - self.data_cleaning_predict(dk) - x = torch.from_numpy(dk.data_dictionary["prediction_features"].values)\ - .float()\ - .to(self.device) - + x = self.data_convertor.convert_x( + dk.data_dictionary["prediction_features"], + device=self.device + ) logits = self.model.model(x) probs = F.softmax(logits, dim=-1) predicted_classes = torch.argmax(probs, dim=-1) diff --git a/freqtrade/freqai/base_models/BasePyTorchModel.py b/freqtrade/freqai/base_models/BasePyTorchModel.py index 189f7d906..7b968c762 100644 --- a/freqtrade/freqai/base_models/BasePyTorchModel.py +++ b/freqtrade/freqai/base_models/BasePyTorchModel.py @@ -1,4 +1,5 @@ import logging +from abc import ABC, abstractmethod from time import time from typing import Any @@ -7,15 +8,17 @@ from pandas import DataFrame from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.freqai_interface import IFreqaiModel +from freqtrade.freqai.torch import PyTorchDataConvertor logger = logging.getLogger(__name__) -class BasePyTorchModel(IFreqaiModel): +class BasePyTorchModel(IFreqaiModel, ABC): """ Base class for PyTorch type models. - User *must* inherit from this class and set fit() and predict(). + User *must* inherit from this class and set fit() and predict() and + data_convertor property. """ def __init__(self, **kwargs): @@ -69,3 +72,8 @@ class BasePyTorchModel(IFreqaiModel): f"({end_time - start_time:.2f} secs) --------------------") return model + + @property + @abstractmethod + def data_convertor(self) -> PyTorchDataConvertor: + raise NotImplementedError("Abstract property") diff --git a/freqtrade/freqai/base_models/BasePyTorchRegressor.py b/freqtrade/freqai/base_models/BasePyTorchRegressor.py index 756853496..bf6f86041 100644 --- a/freqtrade/freqai/base_models/BasePyTorchRegressor.py +++ b/freqtrade/freqai/base_models/BasePyTorchRegressor.py @@ -3,7 +3,6 @@ from typing import Tuple import numpy as np import numpy.typing as npt -import torch from pandas import DataFrame from freqtrade.freqai.base_models.BasePyTorchModel import BasePyTorchModel @@ -41,9 +40,12 @@ class BasePyTorchRegressor(BasePyTorchModel): dk.data_dictionary["prediction_features"] = filtered_df self.data_cleaning_predict(dk) - x = torch.from_numpy(dk.data_dictionary["prediction_features"].values)\ - .float()\ - .to(self.device) + x = self.data_convertor.convert_x( + dk.data_dictionary["prediction_features"], + device=self.device + ) + logger.info(self.model.model) + logger.info(self.model.model) y = self.model.model(x) pred_df = DataFrame(y.detach().numpy(), columns=[dk.label_list[0]]) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index a44214367..5b7ea462e 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -4,6 +4,8 @@ import torch from freqtrade.freqai.base_models.BasePyTorchClassifier import BasePyTorchClassifier from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.torch import PyTorchDataConvertor +from freqtrade.freqai.torch.PyTorchDataConvertor import DefaultPyTorchDataConvertor from freqtrade.freqai.torch.PyTorchMLPModel import PyTorchMLPModel from freqtrade.freqai.torch.PyTorchModelTrainer import PyTorchModelTrainer @@ -38,6 +40,10 @@ class PyTorchMLPClassifier(BasePyTorchClassifier): } """ + @property + def data_convertor(self) -> PyTorchDataConvertor: + return DefaultPyTorchDataConvertor(target_tensor_type=torch.long, squeeze_target_tensor=True) + def __init__(self, **kwargs) -> None: super().__init__(**kwargs) config = self.freqai_info.get("model_training_parameters", {}) @@ -72,8 +78,7 @@ class PyTorchMLPClassifier(BasePyTorchClassifier): model_meta_data={"class_names": class_names}, device=self.device, init_model=init_model, - target_tensor_type=torch.long, - squeeze_target_tensor=True, + data_convertor=self.data_convertor, **self.trainer_kwargs, ) trainer.fit(data_dictionary, self.splits) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py index 6fc2be1a5..326f14994 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -4,6 +4,8 @@ import torch from freqtrade.freqai.base_models.BasePyTorchRegressor import BasePyTorchRegressor from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.torch import PyTorchDataConvertor +from freqtrade.freqai.torch.PyTorchDataConvertor import DefaultPyTorchDataConvertor from freqtrade.freqai.torch.PyTorchMLPModel import PyTorchMLPModel from freqtrade.freqai.torch.PyTorchModelTrainer import PyTorchModelTrainer @@ -39,6 +41,10 @@ class PyTorchMLPRegressor(BasePyTorchRegressor): } """ + @property + def data_convertor(self) -> PyTorchDataConvertor: + return DefaultPyTorchDataConvertor(target_tensor_type=torch.float) + def __init__(self, **kwargs) -> None: super().__init__(**kwargs) config = self.freqai_info.get("model_training_parameters", {}) @@ -69,7 +75,7 @@ class PyTorchMLPRegressor(BasePyTorchRegressor): criterion=criterion, device=self.device, init_model=init_model, - target_tensor_type=torch.float, + data_convertor=self.data_convertor, **self.trainer_kwargs, ) trainer.fit(data_dictionary, self.splits) diff --git a/freqtrade/freqai/torch/PyTorchDataConvertor.py b/freqtrade/freqai/torch/PyTorchDataConvertor.py new file mode 100644 index 000000000..1c948c72e --- /dev/null +++ b/freqtrade/freqai/torch/PyTorchDataConvertor.py @@ -0,0 +1,56 @@ +from abc import ABC, abstractmethod +from typing import Optional, Tuple + +import pandas as pd +import torch + + +class PyTorchDataConvertor(ABC): + + @abstractmethod + def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> Tuple[torch.Tensor, ...]: + """ + :param df: "*_features" dataframe. + :param device: cpu/gpu. + :returns: tuple of tensors. + """ + + @abstractmethod + def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> Tuple[torch.Tensor, ...]: + """ + :param df: "*_labels" dataframe. + :param device: cpu/gpu. + :returns: tuple of tensors. + """ + + +class DefaultPyTorchDataConvertor(PyTorchDataConvertor): + + def __init__( + self, + target_tensor_type: Optional[torch.dtype] = None, + squeeze_target_tensor: bool = False + ): + self._target_tensor_type = target_tensor_type + self._squeeze_target_tensor = squeeze_target_tensor + + def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> Tuple[torch.Tensor, ...]: + x = torch.from_numpy(df.values).float() + if device: + x = x.to(device) + + return x, + + def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> Tuple[torch.Tensor, ...]: + y = torch.from_numpy(df.values) + + if self._target_tensor_type: + y = y.to(self._target_tensor_type) + + if self._squeeze_target_tensor: + y = y.squeeze() + + if device: + y = y.to(device) + + return y, diff --git a/freqtrade/freqai/torch/PyTorchMLPModel.py b/freqtrade/freqai/torch/PyTorchMLPModel.py index 22fb9c3f0..2deffd708 100644 --- a/freqtrade/freqai/torch/PyTorchMLPModel.py +++ b/freqtrade/freqai/torch/PyTorchMLPModel.py @@ -1,4 +1,5 @@ import logging +from typing import Tuple, List import torch import torch.nn as nn @@ -46,7 +47,8 @@ class PyTorchMLPModel(nn.Module): self.relu = nn.ReLU() self.dropout = nn.Dropout(p=dropout_percent) - def forward(self, x: torch.Tensor) -> torch.Tensor: + def forward(self, x: List[torch.Tensor]) -> torch.Tensor: + x, = x x = self.relu(self.input_layer(x)) x = self.dropout(x) x = self.blocks(x) diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index eda880d02..ef5c64a8a 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -9,11 +9,13 @@ import torch.nn as nn from torch.optim import Optimizer from torch.utils.data import DataLoader, TensorDataset +from freqtrade.freqai.torch.PyTorchDataConvertor import PyTorchDataConvertor +from freqtrade.freqai.torch.PyTorchTrainerInterface import PyTorchTrainerInterface logger = logging.getLogger(__name__) -class PyTorchModelTrainer: +class PyTorchModelTrainer(PyTorchTrainerInterface): def __init__( self, model: nn.Module, @@ -21,8 +23,7 @@ class PyTorchModelTrainer: criterion: nn.Module, device: str, init_model: Dict, - target_tensor_type: torch.dtype, - squeeze_target_tensor: bool = False, + data_convertor: PyTorchDataConvertor, model_meta_data: Dict[str, Any] = {}, **kwargs ): @@ -33,11 +34,7 @@ class PyTorchModelTrainer: :param device: The device to use for training (e.g. 'cpu', 'cuda'). :param init_model: A dictionary containing the initial model/optimizer state_dict and model_meta_data saved by self.save() method. - :param target_tensor_type: type of target tensor, for classification usually - torch.long, for regressor usually torch.float. :param model_meta_data: Additional metadata about the model (optional). - :param squeeze_target_tensor: controls the target shape, used for loss functions - that requires 0D or 1D. :param max_iters: The number of training iterations to run. iteration here refers to the number of times we call self.optimizer.step(). used to calculate n_epochs. @@ -49,11 +46,10 @@ class PyTorchModelTrainer: self.criterion = criterion self.model_meta_data = model_meta_data self.device = device - self.target_tensor_type = target_tensor_type self.max_iters: int = kwargs.get("max_iters", 100) self.batch_size: int = kwargs.get("batch_size", 64) self.max_n_eval_batches: Optional[int] = kwargs.get("max_n_eval_batches", None) - self.squeeze_target_tensor = squeeze_target_tensor + self.data_convertor = data_convertor if init_model: self.load_from_checkpoint(init_model) @@ -81,9 +77,12 @@ class PyTorchModelTrainer: # training losses = [] for i, batch_data in enumerate(data_loaders_dictionary["train"]): - xb, yb = batch_data - xb = xb.to(self.device) - yb = yb.to(self.device) + + for tensor in batch_data: + tensor.to(self.device) + + xb = batch_data[:-1] + yb = batch_data[-1] yb_pred = self.model(xb) loss = self.criterion(yb_pred, yb) @@ -115,14 +114,16 @@ class PyTorchModelTrainer: self.model.eval() n_batches = 0 losses = [] - for i, batch in enumerate(data_loader_dictionary[split]): + for i, batch_data in enumerate(data_loader_dictionary[split]): if max_n_eval_batches and i > max_n_eval_batches: n_batches += 1 break - xb, yb = batch - xb = xb.to(self.device) - yb = yb.to(self.device) + for tensor in batch_data: + tensor.to(self.device) + + xb = batch_data[:-1] + yb = batch_data[-1] yb_pred = self.model(xb) loss = self.criterion(yb_pred, yb) losses.append(loss.item()) @@ -140,14 +141,9 @@ class PyTorchModelTrainer: """ data_loader_dictionary = {} for split in splits: - x = torch.from_numpy(data_dictionary[f"{split}_features"].values).float() - y = torch.from_numpy(data_dictionary[f"{split}_labels"].values)\ - .to(self.target_tensor_type) - - if self.squeeze_target_tensor: - y = y.squeeze() - - dataset = TensorDataset(x, y) + x = self.data_convertor.convert_x(data_dictionary[f"{split}_features"]) + y = self.data_convertor.convert_y(data_dictionary[f"{split}_labels"]) + dataset = TensorDataset(*x, *y) data_loader = DataLoader( dataset, batch_size=self.batch_size, @@ -186,7 +182,7 @@ class PyTorchModelTrainer: "model_meta_data": self.model_meta_data, }, path) - def load_from_file(self, path: Path): + def load(self, path: Path): checkpoint = torch.load(path) return self.load_from_checkpoint(checkpoint) diff --git a/freqtrade/freqai/torch/PyTorchTrainerInterface.py b/freqtrade/freqai/torch/PyTorchTrainerInterface.py new file mode 100644 index 000000000..2924f2ef9 --- /dev/null +++ b/freqtrade/freqai/torch/PyTorchTrainerInterface.py @@ -0,0 +1,54 @@ +from abc import ABC, abstractmethod +from typing import Any, Dict, List, Optional, Tuple + +import pandas as pd +import torch +import torch.nn as nn + +from pathlib import Path + + +class PyTorchTrainerInterface(ABC): + + @abstractmethod + def fit(self, data_dictionary: Dict[str, pd.DataFrame], splits: List[str]) -> None: + """ + :param data_dictionary: the dictionary constructed by DataHandler to hold + all the training and test data/labels. + :param splits: splits to use in training, splits must contain "train", + optional "test" could be added by setting freqai.data_split_parameters.test_size > 0 + in the config file. + + - Calculates the predicted output for the batch using the PyTorch model. + - Calculates the loss between the predicted and actual output using a loss function. + - Computes the gradients of the loss with respect to the model's parameters using + backpropagation. + - Updates the model's parameters using an optimizer. + """ + + @abstractmethod + def save(self, path: Path) -> None: + """ + - Saving any nn.Module state_dict + - Saving model_meta_data, this dict should contain any additional data that the + user needs to store. e.g class_names for classification models. + """ + + def load(self, path: Path) -> nn.Module: + """ + :param path: path to zip file. + :returns: pytorch model. + """ + checkpoint = torch.load(path) + return self.load_from_checkpoint(checkpoint) + + @abstractmethod + def load_from_checkpoint(self, checkpoint: Dict) -> nn.Module: + """ + when using continual_learning, DataDrawer will load the dictionary + (containing state dicts and model_meta_data) by calling torch.load(path). + you can access this dict from any class that inherits IFreqaiModel by calling + get_init_model method. + :checkpoint checkpoint: dict containing the model & optimizer state dicts, + model_meta_data, etc.. + """ \ No newline at end of file From c137666230875cdc7fa6af7962382d681f8a4a19 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 3 Apr 2023 16:03:15 +0300 Subject: [PATCH 104/115] fix imports --- freqtrade/freqai/base_models/BasePyTorchModel.py | 2 +- .../freqai/prediction_models/PyTorchMLPClassifier.py | 9 ++++++--- .../freqai/prediction_models/PyTorchMLPRegressor.py | 4 ++-- freqtrade/freqai/torch/PyTorchMLPModel.py | 2 +- freqtrade/freqai/torch/PyTorchModelTrainer.py | 1 + freqtrade/freqai/torch/PyTorchTrainerInterface.py | 7 +++---- 6 files changed, 14 insertions(+), 11 deletions(-) diff --git a/freqtrade/freqai/base_models/BasePyTorchModel.py b/freqtrade/freqai/base_models/BasePyTorchModel.py index 7b968c762..d017f1fec 100644 --- a/freqtrade/freqai/base_models/BasePyTorchModel.py +++ b/freqtrade/freqai/base_models/BasePyTorchModel.py @@ -8,7 +8,7 @@ from pandas import DataFrame from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.freqai_interface import IFreqaiModel -from freqtrade.freqai.torch import PyTorchDataConvertor +from freqtrade.freqai.torch.PyTorchDataConvertor import PyTorchDataConvertor logger = logging.getLogger(__name__) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index 5b7ea462e..8694453be 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -4,8 +4,8 @@ import torch from freqtrade.freqai.base_models.BasePyTorchClassifier import BasePyTorchClassifier from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.torch import PyTorchDataConvertor -from freqtrade.freqai.torch.PyTorchDataConvertor import DefaultPyTorchDataConvertor +from freqtrade.freqai.torch.PyTorchDataConvertor import (DefaultPyTorchDataConvertor, + PyTorchDataConvertor) from freqtrade.freqai.torch.PyTorchMLPModel import PyTorchMLPModel from freqtrade.freqai.torch.PyTorchModelTrainer import PyTorchModelTrainer @@ -42,7 +42,10 @@ class PyTorchMLPClassifier(BasePyTorchClassifier): @property def data_convertor(self) -> PyTorchDataConvertor: - return DefaultPyTorchDataConvertor(target_tensor_type=torch.long, squeeze_target_tensor=True) + return DefaultPyTorchDataConvertor( + target_tensor_type=torch.long, + squeeze_target_tensor=True + ) def __init__(self, **kwargs) -> None: super().__init__(**kwargs) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py index 326f14994..5ca3486e1 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -4,8 +4,8 @@ import torch from freqtrade.freqai.base_models.BasePyTorchRegressor import BasePyTorchRegressor from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.torch import PyTorchDataConvertor -from freqtrade.freqai.torch.PyTorchDataConvertor import DefaultPyTorchDataConvertor +from freqtrade.freqai.torch.PyTorchDataConvertor import (DefaultPyTorchDataConvertor, + PyTorchDataConvertor) from freqtrade.freqai.torch.PyTorchMLPModel import PyTorchMLPModel from freqtrade.freqai.torch.PyTorchModelTrainer import PyTorchModelTrainer diff --git a/freqtrade/freqai/torch/PyTorchMLPModel.py b/freqtrade/freqai/torch/PyTorchMLPModel.py index 2deffd708..01192e115 100644 --- a/freqtrade/freqai/torch/PyTorchMLPModel.py +++ b/freqtrade/freqai/torch/PyTorchMLPModel.py @@ -1,5 +1,5 @@ import logging -from typing import Tuple, List +from typing import List import torch import torch.nn as nn diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index ef5c64a8a..09de6f940 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -12,6 +12,7 @@ from torch.utils.data import DataLoader, TensorDataset from freqtrade.freqai.torch.PyTorchDataConvertor import PyTorchDataConvertor from freqtrade.freqai.torch.PyTorchTrainerInterface import PyTorchTrainerInterface + logger = logging.getLogger(__name__) diff --git a/freqtrade/freqai/torch/PyTorchTrainerInterface.py b/freqtrade/freqai/torch/PyTorchTrainerInterface.py index 2924f2ef9..6686555f9 100644 --- a/freqtrade/freqai/torch/PyTorchTrainerInterface.py +++ b/freqtrade/freqai/torch/PyTorchTrainerInterface.py @@ -1,12 +1,11 @@ from abc import ABC, abstractmethod -from typing import Any, Dict, List, Optional, Tuple +from pathlib import Path +from typing import Dict, List import pandas as pd import torch import torch.nn as nn -from pathlib import Path - class PyTorchTrainerInterface(ABC): @@ -51,4 +50,4 @@ class PyTorchTrainerInterface(ABC): get_init_model method. :checkpoint checkpoint: dict containing the model & optimizer state dicts, model_meta_data, etc.. - """ \ No newline at end of file + """ From 36a0a14a2328db6a06acc42a523ef5cd7007890f Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 3 Apr 2023 16:26:42 +0300 Subject: [PATCH 105/115] clean code --- freqtrade/freqai/base_models/BasePyTorchRegressor.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/freqtrade/freqai/base_models/BasePyTorchRegressor.py b/freqtrade/freqai/base_models/BasePyTorchRegressor.py index bf6f86041..b9c5fa685 100644 --- a/freqtrade/freqai/base_models/BasePyTorchRegressor.py +++ b/freqtrade/freqai/base_models/BasePyTorchRegressor.py @@ -44,9 +44,6 @@ class BasePyTorchRegressor(BasePyTorchModel): dk.data_dictionary["prediction_features"], device=self.device ) - logger.info(self.model.model) - logger.info(self.model.model) - y = self.model.model(x) pred_df = DataFrame(y.detach().numpy(), columns=[dk.label_list[0]]) return (pred_df, dk.do_predict) From bc9454e0f9a2a67e1b9ffdb6ade4fe500aee7682 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 3 Apr 2023 16:36:38 +0300 Subject: [PATCH 106/115] add device to data convertor class doc --- freqtrade/freqai/torch/PyTorchDataConvertor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/torch/PyTorchDataConvertor.py b/freqtrade/freqai/torch/PyTorchDataConvertor.py index 1c948c72e..1070b0fb5 100644 --- a/freqtrade/freqai/torch/PyTorchDataConvertor.py +++ b/freqtrade/freqai/torch/PyTorchDataConvertor.py @@ -11,7 +11,7 @@ class PyTorchDataConvertor(ABC): def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> Tuple[torch.Tensor, ...]: """ :param df: "*_features" dataframe. - :param device: cpu/gpu. + :param device: The device to use for training (e.g. 'cpu', 'cuda'). :returns: tuple of tensors. """ @@ -19,7 +19,7 @@ class PyTorchDataConvertor(ABC): def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> Tuple[torch.Tensor, ...]: """ :param df: "*_labels" dataframe. - :param device: cpu/gpu. + :param device: The device to use for training (e.g. 'cpu', 'cuda'). :returns: tuple of tensors. """ From 7b494c8333f5d0bdbec0d93376339b79d1f07909 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 3 Apr 2023 16:39:49 +0300 Subject: [PATCH 107/115] add documentation to pytorch data convertor --- freqtrade/freqai/torch/PyTorchDataConvertor.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/freqtrade/freqai/torch/PyTorchDataConvertor.py b/freqtrade/freqai/torch/PyTorchDataConvertor.py index 1070b0fb5..5982a1b48 100644 --- a/freqtrade/freqai/torch/PyTorchDataConvertor.py +++ b/freqtrade/freqai/torch/PyTorchDataConvertor.py @@ -31,6 +31,12 @@ class DefaultPyTorchDataConvertor(PyTorchDataConvertor): target_tensor_type: Optional[torch.dtype] = None, squeeze_target_tensor: bool = False ): + """ + :param target_tensor_type: type of target tensor, for classification use + torch.long, for regressor use torch.float or torch.double. + :param squeeze_target_tensor: controls the target shape, used for loss functions + that requires 0D or 1D. + """ self._target_tensor_type = target_tensor_type self._squeeze_target_tensor = squeeze_target_tensor From d9d99931792d0cf21c1e83f2b75da330f3ea8bdf Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 3 Apr 2023 17:06:39 +0300 Subject: [PATCH 108/115] add documentation --- freqtrade/freqai/base_models/BasePyTorchModel.py | 4 ++++ freqtrade/freqai/torch/PyTorchDataConvertor.py | 7 +++++++ freqtrade/freqai/torch/PyTorchModelTrainer.py | 1 + 3 files changed, 12 insertions(+) diff --git a/freqtrade/freqai/base_models/BasePyTorchModel.py b/freqtrade/freqai/base_models/BasePyTorchModel.py index d017f1fec..8177b8eb8 100644 --- a/freqtrade/freqai/base_models/BasePyTorchModel.py +++ b/freqtrade/freqai/base_models/BasePyTorchModel.py @@ -76,4 +76,8 @@ class BasePyTorchModel(IFreqaiModel, ABC): @property @abstractmethod def data_convertor(self) -> PyTorchDataConvertor: + """ + a class responsible for converting `*_features` & `*_labels` pandas dataframes + to pytorch tensors. + """ raise NotImplementedError("Abstract property") diff --git a/freqtrade/freqai/torch/PyTorchDataConvertor.py b/freqtrade/freqai/torch/PyTorchDataConvertor.py index 5982a1b48..e7d5c3ffe 100644 --- a/freqtrade/freqai/torch/PyTorchDataConvertor.py +++ b/freqtrade/freqai/torch/PyTorchDataConvertor.py @@ -6,6 +6,10 @@ import torch class PyTorchDataConvertor(ABC): + """ + This class is responsible for converting `*_features` & `*_labels` pandas dataframes + to pytorch tensors. + """ @abstractmethod def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> Tuple[torch.Tensor, ...]: @@ -25,6 +29,9 @@ class PyTorchDataConvertor(ABC): class DefaultPyTorchDataConvertor(PyTorchDataConvertor): + """ + A default conversion that keeps features dataframe shapes. + """ def __init__( self, diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index 09de6f940..6449d98b5 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -36,6 +36,7 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): :param init_model: A dictionary containing the initial model/optimizer state_dict and model_meta_data saved by self.save() method. :param model_meta_data: Additional metadata about the model (optional). + :param data_convertor: convertor from pd.DataFrame to torch.tensor. :param max_iters: The number of training iterations to run. iteration here refers to the number of times we call self.optimizer.step(). used to calculate n_epochs. From 0c4574b3b7e2dac0ec2d441e2bc8bccf4c3ff9f5 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 3 Apr 2023 18:10:47 +0300 Subject: [PATCH 109/115] prevent mypy error, explicitly unpack input list of pytorch mlp model, --- freqtrade/freqai/torch/PyTorchMLPModel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/torch/PyTorchMLPModel.py b/freqtrade/freqai/torch/PyTorchMLPModel.py index 01192e115..192d2ad89 100644 --- a/freqtrade/freqai/torch/PyTorchMLPModel.py +++ b/freqtrade/freqai/torch/PyTorchMLPModel.py @@ -48,7 +48,7 @@ class PyTorchMLPModel(nn.Module): self.dropout = nn.Dropout(p=dropout_percent) def forward(self, x: List[torch.Tensor]) -> torch.Tensor: - x, = x + x = x[0] x = self.relu(self.input_layer(x)) x = self.dropout(x) x = self.blocks(x) From 6b204c97ed44c4e1c8258a3cebfda8ea2694f44d Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Mon, 3 Apr 2023 19:02:07 +0300 Subject: [PATCH 110/115] fix pytorch data convertor type hints --- freqtrade/freqai/torch/PyTorchDataConvertor.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/freqtrade/freqai/torch/PyTorchDataConvertor.py b/freqtrade/freqai/torch/PyTorchDataConvertor.py index e7d5c3ffe..a31ccdc79 100644 --- a/freqtrade/freqai/torch/PyTorchDataConvertor.py +++ b/freqtrade/freqai/torch/PyTorchDataConvertor.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Optional, Tuple +from typing import List, Optional import pandas as pd import torch @@ -12,19 +12,17 @@ class PyTorchDataConvertor(ABC): """ @abstractmethod - def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> Tuple[torch.Tensor, ...]: + def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> List[torch.Tensor]: """ :param df: "*_features" dataframe. :param device: The device to use for training (e.g. 'cpu', 'cuda'). - :returns: tuple of tensors. """ @abstractmethod - def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> Tuple[torch.Tensor, ...]: + def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> List[torch.Tensor]: """ :param df: "*_labels" dataframe. :param device: The device to use for training (e.g. 'cpu', 'cuda'). - :returns: tuple of tensors. """ @@ -47,14 +45,14 @@ class DefaultPyTorchDataConvertor(PyTorchDataConvertor): self._target_tensor_type = target_tensor_type self._squeeze_target_tensor = squeeze_target_tensor - def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> Tuple[torch.Tensor, ...]: + def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> List[torch.Tensor]: x = torch.from_numpy(df.values).float() if device: x = x.to(device) - return x, + return [x] - def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> Tuple[torch.Tensor, ...]: + def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> List[torch.Tensor]: y = torch.from_numpy(df.values) if self._target_tensor_type: @@ -66,4 +64,4 @@ class DefaultPyTorchDataConvertor(PyTorchDataConvertor): if device: y = y.to(device) - return y, + return [y] From 26738370c75ceaeafcc9c4c353f02a30d7dedd19 Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 4 Apr 2023 12:12:02 +0300 Subject: [PATCH 111/115] pytorch mlp add explicit annotation to fix mypy error --- freqtrade/freqai/torch/PyTorchMLPModel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/torch/PyTorchMLPModel.py b/freqtrade/freqai/torch/PyTorchMLPModel.py index 192d2ad89..94cfa8e64 100644 --- a/freqtrade/freqai/torch/PyTorchMLPModel.py +++ b/freqtrade/freqai/torch/PyTorchMLPModel.py @@ -48,7 +48,7 @@ class PyTorchMLPModel(nn.Module): self.dropout = nn.Dropout(p=dropout_percent) def forward(self, x: List[torch.Tensor]) -> torch.Tensor: - x = x[0] + x: torch.Tensor = x[0] x = self.relu(self.input_layer(x)) x = self.dropout(x) x = self.blocks(x) From a6555242218ddbf7269353baad69e0472531b61d Mon Sep 17 00:00:00 2001 From: Yinon Polak Date: Tue, 4 Apr 2023 12:24:29 +0300 Subject: [PATCH 112/115] pytorch mlp rename input to fix mypy error --- freqtrade/freqai/torch/PyTorchMLPModel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/torch/PyTorchMLPModel.py b/freqtrade/freqai/torch/PyTorchMLPModel.py index 94cfa8e64..e50ccd5ef 100644 --- a/freqtrade/freqai/torch/PyTorchMLPModel.py +++ b/freqtrade/freqai/torch/PyTorchMLPModel.py @@ -47,8 +47,8 @@ class PyTorchMLPModel(nn.Module): self.relu = nn.ReLU() self.dropout = nn.Dropout(p=dropout_percent) - def forward(self, x: List[torch.Tensor]) -> torch.Tensor: - x: torch.Tensor = x[0] + def forward(self, tensors: List[torch.Tensor]) -> torch.Tensor: + x: torch.Tensor = tensors[0] x = self.relu(self.input_layer(x)) x = self.dropout(x) x = self.blocks(x) From 48d3c8e62e8988376c472dd985797c06ecfed944 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sat, 8 Apr 2023 12:09:53 +0200 Subject: [PATCH 113/115] fix model loading from disk bug, improve doc, clarify installation/docker instructions, add a torch tag to the freqairl docker image. Fix seriously outdated prediction_model docstrings --- build_helpers/publish_docker_multi.sh | 2 + docs/freqai-configuration.md | 62 +++++++++++++++++-- freqtrade/freqai/data_drawer.py | 4 +- .../prediction_models/CatboostClassifier.py | 14 +++-- .../CatboostClassifierMultiTarget.py | 14 +++-- .../prediction_models/CatboostRegressor.py | 14 +++-- .../CatboostRegressorMultiTarget.py | 14 +++-- .../prediction_models/LightGBMClassifier.py | 14 +++-- .../LightGBMClassifierMultiTarget.py | 14 +++-- .../prediction_models/LightGBMRegressor.py | 18 +++--- .../LightGBMRegressorMultiTarget.py | 14 +++-- .../prediction_models/PyTorchMLPClassifier.py | 5 +- .../prediction_models/PyTorchMLPRegressor.py | 5 +- .../prediction_models/XGBoostClassifier.py | 14 +++-- .../prediction_models/XGBoostRFClassifier.py | 14 +++-- .../prediction_models/XGBoostRFRegressor.py | 14 +++-- .../prediction_models/XGBoostRegressor.py | 14 +++-- .../XGBoostRegressorMultiTarget.py | 14 +++-- freqtrade/freqai/torch/PyTorchModelTrainer.py | 10 ++- .../freqai/torch/PyTorchTrainerInterface.py | 2 +- setup.sh | 2 +- 21 files changed, 195 insertions(+), 83 deletions(-) diff --git a/build_helpers/publish_docker_multi.sh b/build_helpers/publish_docker_multi.sh index 3e5e61564..6c5d11d94 100755 --- a/build_helpers/publish_docker_multi.sh +++ b/build_helpers/publish_docker_multi.sh @@ -7,6 +7,7 @@ TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g") TAG_PLOT=${TAG}_plot TAG_FREQAI=${TAG}_freqai TAG_FREQAI_RL=${TAG_FREQAI}rl +TAG_FREQAI_RL=${TAG_FREQAI}torch TAG_PI="${TAG}_pi" PI_PLATFORM="linux/arm/v7" @@ -64,6 +65,7 @@ docker build --cache-from freqtrade:${TAG_FREQAI} --build-arg sourceimage=${CACH docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT docker tag freqtrade:$TAG_FREQAI ${CACHE_IMAGE}:$TAG_FREQAI docker tag freqtrade:$TAG_FREQAI_RL ${CACHE_IMAGE}:$TAG_FREQAI_RL +docker tag freqtrade:$TAG_FREQAI_RL ${CACHE_IMAGE}:$TAG_FREQAI_TORCH # Run backtest docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3 diff --git a/docs/freqai-configuration.md b/docs/freqai-configuration.md index 442705b53..8f1aa5079 100644 --- a/docs/freqai-configuration.md +++ b/docs/freqai-configuration.md @@ -237,7 +237,7 @@ df['&s-up_or_down'] = np.where( df["close"].shift(-100) > df["close"], 'up', 'do df['&s-up_or_down'] = np.where( df["close"].shift(-100) == df["close"], 'same', df['&s-up_or_down']) ``` -## PyTorch Models +## PyTorch Module ### Quick start @@ -247,14 +247,16 @@ The easiest way to quickly run a pytorch model is with the following command (fo freqtrade trade --config config_examples/config_freqai.example.json --strategy FreqaiExampleStrategy --freqaimodel PyTorchMLPRegressor --strategy-path freqtrade/templates ``` +!!! note "Installation/docker" + The PyTorch module requires large packages such as `torch`, which should be explicitly requested during `./setup.sh -i` by answering "y" to the question "Do you also want dependencies for freqai-rl or PyTorch (~700mb additional space required) [y/N]?". + Users who prefer docker should ensure they use the docker image appended with `_freqaitorch`. + ### Structure #### Model -You can use any pytorch model. Here is an example of logistic regression model implementation using pytorch (should be used with nn.BCELoss criterion) for classification tasks. +You can construct your own Neural Network architecture in PyTorch by simply defining your `nn.Module` class inside your custom [`IFreqaiModel` file](#using-different-prediction-models) and then using that class in your `def train()` function. Here is an example of logistic regression model implementation using PyTorch (should be used with nn.BCELoss criterion) for classification tasks. ```python -import torch.nn as nn -import torch class LogisticRegression(nn.Module): def __init__(self, input_size: int): @@ -268,11 +270,59 @@ class LogisticRegression(nn.Module): out = self.linear(x) out = self.activation(out) return out + +class MyCoolPyTorchClassifier(BasePyTorchClassifier): + """ + This is a custom IFreqaiModel showing how a user might setup their own + custom Neural Network architecture for their training. + """ + + @property + def data_convertor(self) -> PyTorchDataConvertor: + return DefaultPyTorchDataConvertor(target_tensor_type=torch.float) + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + config = self.freqai_info.get("model_training_parameters", {}) + self.learning_rate: float = config.get("learning_rate", 3e-4) + self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {}) + self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {}) + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + """ + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model + """ + + class_names = self.get_class_names() + self.convert_label_column_to_int(data_dictionary, dk, class_names) + n_features = data_dictionary["train_features"].shape[-1] + model = LogisticRegression( + input_dim=n_features + ) + model.to(self.device) + optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) + criterion = torch.nn.CrossEntropyLoss() + init_model = self.get_init_model(dk.pair) + trainer = PyTorchModelTrainer( + model=model, + optimizer=optimizer, + criterion=criterion, + model_meta_data={"class_names": class_names}, + device=self.device, + init_model=init_model, + data_convertor=self.data_convertor, + **self.trainer_kwargs, + ) + trainer.fit(data_dictionary, self.splits) + return trainer + ``` - #### Trainer -The `PyTorchModelTrainer` performs the idiomatic pytorch train loop: +The `PyTorchModelTrainer` performs the idiomatic PyTorch train loop: Define our model, loss function, and optimizer, and then move them to the appropriate device (GPU or CPU). Inside the loop, we iterate through the batches in the dataloader, move the data to the device, compute the prediction and loss, backpropagate, and update the model parameters using the optimizer. In addition, the trainer is responsible for the following: diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index c8dadb171..b68a9dcad 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -539,7 +539,9 @@ class FreqaiDataDrawer: model = MODELCLASS.load(dk.data_path / f"{dk.model_filename}_model") elif self.model_type == 'pytorch': import torch - model = torch.load(dk.data_path / f"{dk.model_filename}_model.zip") + zip = torch.load(dk.data_path / f"{dk.model_filename}_model.zip") + model = zip["pytrainer"] + model = model.load_from_checkpoint(zip) if Path(dk.data_path / f"{dk.model_filename}_svm_model.joblib").is_file(): dk.svm_model = load(dk.data_path / f"{dk.model_filename}_svm_model.joblib") diff --git a/freqtrade/freqai/prediction_models/CatboostClassifier.py b/freqtrade/freqai/prediction_models/CatboostClassifier.py index ca1d8ece0..b9904e40d 100644 --- a/freqtrade/freqai/prediction_models/CatboostClassifier.py +++ b/freqtrade/freqai/prediction_models/CatboostClassifier.py @@ -14,16 +14,20 @@ logger = logging.getLogger(__name__) class CatboostClassifier(BaseClassifierModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ train_data = Pool( diff --git a/freqtrade/freqai/prediction_models/CatboostClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/CatboostClassifierMultiTarget.py index c6f900fad..58c47566a 100644 --- a/freqtrade/freqai/prediction_models/CatboostClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/CatboostClassifierMultiTarget.py @@ -15,16 +15,20 @@ logger = logging.getLogger(__name__) class CatboostClassifierMultiTarget(BaseClassifierModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ cbc = CatBoostClassifier( diff --git a/freqtrade/freqai/prediction_models/CatboostRegressor.py b/freqtrade/freqai/prediction_models/CatboostRegressor.py index 4b17a703b..28b1b11cc 100644 --- a/freqtrade/freqai/prediction_models/CatboostRegressor.py +++ b/freqtrade/freqai/prediction_models/CatboostRegressor.py @@ -14,16 +14,20 @@ logger = logging.getLogger(__name__) class CatboostRegressor(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ train_data = Pool( diff --git a/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py index 976d0b29b..1562c2024 100644 --- a/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py @@ -15,16 +15,20 @@ logger = logging.getLogger(__name__) class CatboostRegressorMultiTarget(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ cbr = CatBoostRegressor( diff --git a/freqtrade/freqai/prediction_models/LightGBMClassifier.py b/freqtrade/freqai/prediction_models/LightGBMClassifier.py index e467ad3c1..45f3a31d0 100644 --- a/freqtrade/freqai/prediction_models/LightGBMClassifier.py +++ b/freqtrade/freqai/prediction_models/LightGBMClassifier.py @@ -12,16 +12,20 @@ logger = logging.getLogger(__name__) class LightGBMClassifier(BaseClassifierModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) == 0: diff --git a/freqtrade/freqai/prediction_models/LightGBMClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/LightGBMClassifierMultiTarget.py index d1eb6daa2..72a8ee259 100644 --- a/freqtrade/freqai/prediction_models/LightGBMClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/LightGBMClassifierMultiTarget.py @@ -13,16 +13,20 @@ logger = logging.getLogger(__name__) class LightGBMClassifierMultiTarget(BaseClassifierModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ lgb = LGBMClassifier(**self.model_training_parameters) diff --git a/freqtrade/freqai/prediction_models/LightGBMRegressor.py b/freqtrade/freqai/prediction_models/LightGBMRegressor.py index 85c9b691c..3d1c30ed3 100644 --- a/freqtrade/freqai/prediction_models/LightGBMRegressor.py +++ b/freqtrade/freqai/prediction_models/LightGBMRegressor.py @@ -12,18 +12,20 @@ logger = logging.getLogger(__name__) class LightGBMRegressor(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ - Most regressors use the same function names and arguments e.g. user - can drop in LGBMRegressor in place of CatBoostRegressor and all data - management will be properly handled by Freqai. - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) == 0: diff --git a/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py index 37c6bb186..663a611f0 100644 --- a/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py @@ -13,16 +13,20 @@ logger = logging.getLogger(__name__) class LightGBMRegressorMultiTarget(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ lgb = LGBMRegressor(**self.model_training_parameters) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index 8694453be..ea7981405 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -57,8 +57,9 @@ class PyTorchMLPClassifier(BasePyTorchClassifier): def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model :raises ValueError: If self.class_names is not defined in the parent class. """ diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py index 5ca3486e1..64f0f4b03 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -55,8 +55,9 @@ class PyTorchMLPRegressor(BasePyTorchRegressor): def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ n_features = data_dictionary["train_features"].shape[-1] diff --git a/freqtrade/freqai/prediction_models/XGBoostClassifier.py b/freqtrade/freqai/prediction_models/XGBoostClassifier.py index 67c7c7783..b6f04b497 100644 --- a/freqtrade/freqai/prediction_models/XGBoostClassifier.py +++ b/freqtrade/freqai/prediction_models/XGBoostClassifier.py @@ -18,16 +18,20 @@ logger = logging.getLogger(__name__) class XGBoostClassifier(BaseClassifierModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ X = data_dictionary["train_features"].to_numpy() diff --git a/freqtrade/freqai/prediction_models/XGBoostRFClassifier.py b/freqtrade/freqai/prediction_models/XGBoostRFClassifier.py index 470c283ea..20156e9fd 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRFClassifier.py +++ b/freqtrade/freqai/prediction_models/XGBoostRFClassifier.py @@ -18,16 +18,20 @@ logger = logging.getLogger(__name__) class XGBoostRFClassifier(BaseClassifierModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ X = data_dictionary["train_features"].to_numpy() diff --git a/freqtrade/freqai/prediction_models/XGBoostRFRegressor.py b/freqtrade/freqai/prediction_models/XGBoostRFRegressor.py index e7cc27f2e..1aefbf19a 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRFRegressor.py +++ b/freqtrade/freqai/prediction_models/XGBoostRFRegressor.py @@ -12,16 +12,20 @@ logger = logging.getLogger(__name__) class XGBoostRFRegressor(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ X = data_dictionary["train_features"] diff --git a/freqtrade/freqai/prediction_models/XGBoostRegressor.py b/freqtrade/freqai/prediction_models/XGBoostRegressor.py index 9a280286b..93dfb319e 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRegressor.py +++ b/freqtrade/freqai/prediction_models/XGBoostRegressor.py @@ -12,16 +12,20 @@ logger = logging.getLogger(__name__) class XGBoostRegressor(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ X = data_dictionary["train_features"] diff --git a/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py index 920745ec9..a0330485e 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py @@ -13,16 +13,20 @@ logger = logging.getLogger(__name__) class XGBoostRegressorMultiTarget(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ xgb = XGBRegressor(**self.model_training_parameters) diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index 6449d98b5..9c1a1cb6e 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -5,7 +5,7 @@ from typing import Any, Dict, List, Optional import pandas as pd import torch -import torch.nn as nn +from torch import nn from torch.optim import Optimizer from torch.utils.data import DataLoader, TensorDataset @@ -169,6 +169,12 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): n_batches = math.ceil(n_obs // batch_size) epochs = math.ceil(n_iters // n_batches) + if epochs <= 10: + logger.warning("User set `max_iters` in such a way that the trainer will only perform " + f" {epochs} epochs. Please consider increasing this value accordingly") + if epochs <= 1: + logger.warning("Epochs set to 1. Please review your `max_iters` value") + epochs = 1 return epochs def save(self, path: Path): @@ -182,6 +188,7 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): "model_state_dict": self.model.state_dict(), "optimizer_state_dict": self.optimizer.state_dict(), "model_meta_data": self.model_meta_data, + "pytrainer": self }, path) def load(self, path: Path): @@ -195,7 +202,6 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): you can access this dict from any class that inherits IFreqaiModel by calling get_init_model method. """ - self.model.load_state_dict(checkpoint["model_state_dict"]) self.optimizer.load_state_dict(checkpoint["optimizer_state_dict"]) self.model_meta_data = checkpoint["model_meta_data"] diff --git a/freqtrade/freqai/torch/PyTorchTrainerInterface.py b/freqtrade/freqai/torch/PyTorchTrainerInterface.py index 6686555f9..840c145f7 100644 --- a/freqtrade/freqai/torch/PyTorchTrainerInterface.py +++ b/freqtrade/freqai/torch/PyTorchTrainerInterface.py @@ -4,7 +4,7 @@ from typing import Dict, List import pandas as pd import torch -import torch.nn as nn +from torch import nn class PyTorchTrainerInterface(ABC): diff --git a/setup.sh b/setup.sh index a9ff36536..77c77000d 100755 --- a/setup.sh +++ b/setup.sh @@ -85,7 +85,7 @@ function updateenv() { if [[ $REPLY =~ ^[Yy]$ ]] then REQUIREMENTS_FREQAI="-r requirements-freqai.txt --use-pep517" - read -p "Do you also want dependencies for freqai-rl (~700mb additional space required) [y/N]? " + read -p "Do you also want dependencies for freqai-rl or PyTorch (~700mb additional space required) [y/N]? " if [[ $REPLY =~ ^[Yy]$ ]] then REQUIREMENTS_FREQAI="-r requirements-freqai-rl.txt" From bed51fa7902a99f7a3221a509b34a82ec120ad82 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Apr 2023 16:59:17 +0200 Subject: [PATCH 114/115] Properly build specific Torch image --- build_helpers/publish_docker_arm64.sh | 6 ++++++ build_helpers/publish_docker_multi.sh | 2 -- docs/freqai-configuration.md | 4 ++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/build_helpers/publish_docker_arm64.sh b/build_helpers/publish_docker_arm64.sh index 229325efb..8f0de2cc9 100755 --- a/build_helpers/publish_docker_arm64.sh +++ b/build_helpers/publish_docker_arm64.sh @@ -12,6 +12,7 @@ TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g") TAG_PLOT=${TAG}_plot TAG_FREQAI=${TAG}_freqai TAG_FREQAI_RL=${TAG_FREQAI}rl +TAG_FREQAI_TORCH=${TAG_FREQAI}torch TAG_PI="${TAG}_pi" TAG_ARM=${TAG}_arm @@ -84,6 +85,10 @@ docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI} docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM} docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_RL} +# Create special Torch tag - which is identical to the RL tag. +docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_TORCH} ${CACHE_IMAGE}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM} +docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_TORCH} + # copy images to ghcr.io alias crane="docker run --rm -i -v $(pwd)/.crane:/home/nonroot/.docker/ gcr.io/go-containerregistry/crane" @@ -93,6 +98,7 @@ chmod a+rwx .crane echo "${GHCR_TOKEN}" | crane auth login ghcr.io -u "${GHCR_USERNAME}" --password-stdin crane copy ${IMAGE_NAME}:${TAG_FREQAI_RL} ${GHCR_IMAGE_NAME}:${TAG_FREQAI_RL} +crane copy ${IMAGE_NAME}:${TAG_FREQAI_RL} ${GHCR_IMAGE_NAME}:${TAG_FREQAI_TORCH} crane copy ${IMAGE_NAME}:${TAG_FREQAI} ${GHCR_IMAGE_NAME}:${TAG_FREQAI} crane copy ${IMAGE_NAME}:${TAG_PLOT} ${GHCR_IMAGE_NAME}:${TAG_PLOT} crane copy ${IMAGE_NAME}:${TAG} ${GHCR_IMAGE_NAME}:${TAG} diff --git a/build_helpers/publish_docker_multi.sh b/build_helpers/publish_docker_multi.sh index 3cbe9609b..72b20ac5d 100755 --- a/build_helpers/publish_docker_multi.sh +++ b/build_helpers/publish_docker_multi.sh @@ -9,7 +9,6 @@ TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g") TAG_PLOT=${TAG}_plot TAG_FREQAI=${TAG}_freqai TAG_FREQAI_RL=${TAG_FREQAI}rl -TAG_FREQAI_RL=${TAG_FREQAI}torch TAG_PI="${TAG}_pi" PI_PLATFORM="linux/arm/v7" @@ -66,7 +65,6 @@ docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_FREQA docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT docker tag freqtrade:$TAG_FREQAI ${CACHE_IMAGE}:$TAG_FREQAI docker tag freqtrade:$TAG_FREQAI_RL ${CACHE_IMAGE}:$TAG_FREQAI_RL -docker tag freqtrade:$TAG_FREQAI_RL ${CACHE_IMAGE}:$TAG_FREQAI_TORCH # Run backtest docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3 diff --git a/docs/freqai-configuration.md b/docs/freqai-configuration.md index 8f1aa5079..233edf2c5 100644 --- a/docs/freqai-configuration.md +++ b/docs/freqai-configuration.md @@ -254,6 +254,7 @@ freqtrade trade --config config_examples/config_freqai.example.json --strategy F ### Structure #### Model + You can construct your own Neural Network architecture in PyTorch by simply defining your `nn.Module` class inside your custom [`IFreqaiModel` file](#using-different-prediction-models) and then using that class in your `def train()` function. Here is an example of logistic regression model implementation using PyTorch (should be used with nn.BCELoss criterion) for classification tasks. ```python @@ -322,6 +323,7 @@ class MyCoolPyTorchClassifier(BasePyTorchClassifier): ``` #### Trainer + The `PyTorchModelTrainer` performs the idiomatic PyTorch train loop: Define our model, loss function, and optimizer, and then move them to the appropriate device (GPU or CPU). Inside the loop, we iterate through the batches in the dataloader, move the data to the device, compute the prediction and loss, backpropagate, and update the model parameters using the optimizer. @@ -330,6 +332,7 @@ In addition, the trainer is responsible for the following: - converting the data from `pandas.DataFrame` to `torch.Tensor`. #### Integration with Freqai module + Like all freqai models, PyTorch models inherit `IFreqaiModel`. `IFreqaiModel` declares three abstract methods: `train`, `fit`, and `predict`. we implement these methods in three levels of hierarchy. From top to bottom: @@ -340,6 +343,7 @@ From top to bottom: ![image](assets/freqai_pytorch-diagram.png) #### Full example + Building a PyTorch regressor using MLP (multilayer perceptron) model, MSELoss criterion, and AdamW optimizer. ```python From 5404905d2828d814def95353b258cef389448655 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Apr 2023 17:13:51 +0200 Subject: [PATCH 115/115] Fix typos in docs --- docs/freqai-parameter-table.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/freqai-parameter-table.md b/docs/freqai-parameter-table.md index 6c95892d4..9ed3d6dce 100644 --- a/docs/freqai-parameter-table.md +++ b/docs/freqai-parameter-table.md @@ -88,19 +88,20 @@ Mandatory parameters are marked as **Required** and have to be set in one of the ### PyTorch parameters -#### general: +#### general | Parameter | Description | |------------|-------------| -| | **Model training parameters within the freqai.model_training_parameters sub dictionary** -| `learning_rate` | learning rate to be passed to the optimizer.
**Datatype:** float.
Default: `3e-4`. -| `model_kwargs` | paramters to be passed to the model class.
**Datatype:** dict.
Default: `{}`. -| `trainer_kwargs` | paramters to be passed to the trainer class.
**Datatype:** dict.
Default: `{}`. +| | **Model training parameters within the `freqai.model_training_parameters` sub dictionary** +| `learning_rate` | Learning rate to be passed to the optimizer.
**Datatype:** float.
Default: `3e-4`. +| `model_kwargs` | Parameters to be passed to the model class.
**Datatype:** dict.
Default: `{}`. +| `trainer_kwargs` | Parameters to be passed to the trainer class.
**Datatype:** dict.
Default: `{}`. -#### trainer_kwargs: +#### trainer_kwargs | Parameter | Description | |------------|-------------| +| | **Model training parameters within the `freqai.model_training_parameters.model_kwargs` sub dictionary** | `max_iters` | The number of training iterations to run. iteration here refers to the number of times we call self.optimizer.step(). used to calculate n_epochs.
**Datatype:** int.
Default: `100`. | `batch_size` | The size of the batches to use during training..
**Datatype:** int.
Default: `64`. | `max_n_eval_batches` | The maximum number batches to use for evaluation..
**Datatype:** int, optional.
Default: `None`.