pull/8580/head
parent
c26099280f
commit
af139ffbab
@ -0,0 +1,139 @@
|
||||
from typing import Any, Dict, Tuple
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
import pandas as pd
|
||||
import torch
|
||||
|
||||
from freqtrade.freqai.base_models.BasePyTorchRegressor import BasePyTorchRegressor
|
||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||
from freqtrade.freqai.torch.PyTorchDataConvertor import (DefaultPyTorchDataConvertor,
|
||||
PyTorchDataConvertor)
|
||||
from freqtrade.freqai.torch.PyTorchModelTrainer import PyTorchTransformerTrainer
|
||||
from freqtrade.freqai.torch.PyTorchTransformerModel import PyTorchTransformerModel
|
||||
|
||||
|
||||
class PyTorchTransformerRegressor(BasePyTorchRegressor):
|
||||
"""
|
||||
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": null,
|
||||
"window_size": 10
|
||||
},
|
||||
"model_kwargs": {
|
||||
"hidden_dim": 512,
|
||||
"dropout_percent": 0.2,
|
||||
"n_layer": 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
@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
|
||||
"""
|
||||
|
||||
n_features = data_dictionary["train_features"].shape[-1]
|
||||
n_labels = data_dictionary["train_labels"].shape[-1]
|
||||
model = PyTorchTransformerModel(
|
||||
input_dim=n_features,
|
||||
output_dim=n_labels,
|
||||
time_window=self.window_size,
|
||||
**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 = PyTorchTransformerTrainer(
|
||||
model=model,
|
||||
optimizer=optimizer,
|
||||
criterion=criterion,
|
||||
device=self.device,
|
||||
init_model=init_model,
|
||||
data_convertor=self.data_convertor,
|
||||
window_size=self.window_size,
|
||||
**self.trainer_kwargs,
|
||||
)
|
||||
trainer.fit(data_dictionary, self.splits)
|
||||
return trainer
|
||||
|
||||
def predict(
|
||||
self, unfiltered_df: pd.DataFrame, dk: FreqaiDataKitchen, **kwargs
|
||||
) -> Tuple[pd.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 = self.data_convertor.convert_x(
|
||||
dk.data_dictionary["prediction_features"],
|
||||
device=self.device
|
||||
)
|
||||
# if user is asking for multiple predictions, slide the window
|
||||
# along the tensor
|
||||
x = x.unsqueeze(0)
|
||||
# create empty torch tensor
|
||||
self.model.model.eval()
|
||||
yb = torch.empty(0)
|
||||
if x.shape[1] > 1:
|
||||
ws = self.window_size
|
||||
for i in range(0, x.shape[1] - ws):
|
||||
xb = x[:, i:i + ws, :]
|
||||
y = self.model.model(xb)
|
||||
yb = torch.cat((yb, y), dim=0)
|
||||
else:
|
||||
yb = self.model.model(x)
|
||||
|
||||
yb = yb.cpu().squeeze()
|
||||
pred_df = pd.DataFrame(yb.detach().numpy(), columns=dk.label_list)
|
||||
pred_df = dk.denormalize_labels_from_metadata(pred_df)
|
||||
|
||||
if x.shape[1] > 1:
|
||||
zeros_df = pd.DataFrame(np.zeros((x.shape[1] - len(pred_df), len(pred_df.columns))),
|
||||
columns=pred_df.columns)
|
||||
pred_df = pd.concat([zeros_df, pred_df], axis=0, ignore_index=True)
|
||||
return (pred_df, dk.do_predict)
|
||||
@ -0,0 +1,91 @@
|
||||
import math
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
|
||||
"""
|
||||
The architecture is based on the paper “Attention Is All You Need”.
|
||||
Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N Gomez,
|
||||
Lukasz Kaiser, and Illia Polosukhin. 2017.
|
||||
"""
|
||||
|
||||
|
||||
class PyTorchTransformerModel(nn.Module):
|
||||
"""
|
||||
A transformer approach to time series modeling using positional encoding.
|
||||
The architecture is based on the paper “Attention Is All You Need”.
|
||||
Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N Gomez,
|
||||
Lukasz Kaiser, and Illia Polosukhin. 2017.
|
||||
"""
|
||||
|
||||
def __init__(self, input_dim: int = 7, output_dim: int = 7, hidden_dim=1024,
|
||||
n_layer=2, dropout_percent=0.1, time_window=10):
|
||||
super().__init__()
|
||||
self.time_window = time_window
|
||||
self.input_net = nn.Sequential(
|
||||
nn.Dropout(dropout_percent), nn.Linear(input_dim, hidden_dim)
|
||||
)
|
||||
|
||||
# Encode the timeseries with Positional encoding
|
||||
self.positional_encoding = PositionalEncoding(d_model=hidden_dim, max_len=hidden_dim)
|
||||
|
||||
# Define the encoder block of the Transformer
|
||||
self.encoder_layer = nn.TransformerEncoderLayer(
|
||||
d_model=hidden_dim, nhead=8, dropout=dropout_percent, batch_first=True)
|
||||
self.transformer = nn.TransformerEncoder(self.encoder_layer, num_layers=n_layer)
|
||||
|
||||
# Pseudo decoder
|
||||
self.output_net = nn.Sequential(
|
||||
nn.Linear(hidden_dim, hidden_dim),
|
||||
nn.LayerNorm(hidden_dim),
|
||||
nn.Tanh(),
|
||||
nn.Dropout(dropout_percent),
|
||||
)
|
||||
|
||||
self.output_layer = nn.Sequential(
|
||||
nn.Linear(hidden_dim * time_window, output_dim),
|
||||
nn.Tanh()
|
||||
)
|
||||
|
||||
def forward(self, x, mask=None, add_positional_encoding=True):
|
||||
"""
|
||||
Args:
|
||||
x: Input features of shape [Batch, SeqLen, input_dim]
|
||||
mask: Mask to apply on the attention outputs (optional)
|
||||
add_positional_encoding: If True, we add the positional encoding to the input.
|
||||
Might not be desired for some tasks.
|
||||
"""
|
||||
x = self.input_net(x)
|
||||
if add_positional_encoding:
|
||||
x = self.positional_encoding(x)
|
||||
x = self.transformer(x, mask=mask)
|
||||
x = self.output_net(x)
|
||||
x = x.reshape(-1, 1, self.time_window * x.shape[-1])
|
||||
x = self.output_layer(x)
|
||||
return x
|
||||
|
||||
|
||||
class PositionalEncoding(torch.nn.Module):
|
||||
def __init__(self, d_model, max_len=5000):
|
||||
"""
|
||||
Args
|
||||
d_model: Hidden dimensionality of the input.
|
||||
max_len: Maximum length of a sequence to expect.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
# Create matrix of [SeqLen, HiddenDim] representing the positional encoding
|
||||
# for max_len inputs
|
||||
pe = torch.zeros(max_len, d_model)
|
||||
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
|
||||
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
|
||||
pe[:, 0::2] = torch.sin(position * div_term)
|
||||
pe[:, 1::2] = torch.cos(position * div_term)
|
||||
pe = pe.unsqueeze(0)
|
||||
|
||||
self.register_buffer("pe", pe, persistent=False)
|
||||
|
||||
def forward(self, x):
|
||||
x = x + self.pe[:, : x.size(1)]
|
||||
return x
|
||||
@ -0,0 +1,19 @@
|
||||
import torch
|
||||
|
||||
|
||||
class WindowDataset(torch.utils.data.Dataset):
|
||||
def __init__(self, xs, ys, window_size):
|
||||
self.xs = xs
|
||||
self.ys = ys
|
||||
self.window_size = window_size
|
||||
|
||||
def __len__(self):
|
||||
return len(self.xs) - self.window_size
|
||||
|
||||
def __getitem__(self, index):
|
||||
idx_rev = len(self.xs) - self.window_size - index - 1
|
||||
window_x = self.xs[idx_rev:idx_rev + self.window_size, :]
|
||||
# Beware of indexing, these two window_x and window_y are aimed at the same row!
|
||||
# this is what happens when you use :
|
||||
window_y = self.ys[idx_rev + self.window_size - 1, :].unsqueeze(0)
|
||||
return window_x, window_y
|
||||
Loading…
Reference in new issue