|
|
@@ -0,0 +1,326 @@
|
|
|
+from Model.profil import profile
|
|
|
+import Model.Portefeuille
|
|
|
+import Model.Parametre
|
|
|
+import Model.Donnee
|
|
|
+
|
|
|
+import random
|
|
|
+import numpy as np
|
|
|
+import os
|
|
|
+import copy
|
|
|
+
|
|
|
+import torch
|
|
|
+import torch.nn as nn
|
|
|
+import torch.optim as optim
|
|
|
+from torch.utils.data import DataLoader, TensorDataset
|
|
|
+
|
|
|
+
|
|
|
+class CL_Model(nn.Module):
|
|
|
+ def __init__(self, Parametre, hidden=64, d_model=64, n_heads=4):
|
|
|
+ super().__init__()
|
|
|
+
|
|
|
+ self.temporal_encoder = nn.Sequential(
|
|
|
+ nn.Linear(Parametre.NbAnalysis, hidden),
|
|
|
+ nn.ReLU(),
|
|
|
+ nn.Linear(hidden, d_model)
|
|
|
+ )
|
|
|
+
|
|
|
+ self.temporal_mixer = nn.Sequential(
|
|
|
+ nn.Conv1d(d_model, d_model, kernel_size=3, padding=1),
|
|
|
+ nn.ReLU(),
|
|
|
+ nn.Conv1d(d_model, d_model, kernel_size=3, padding=1)
|
|
|
+ )
|
|
|
+
|
|
|
+ # inter-assets attention
|
|
|
+ self.asset_attention = nn.MultiheadAttention(
|
|
|
+ embed_dim=d_model,
|
|
|
+ num_heads=n_heads,
|
|
|
+ batch_first=True
|
|
|
+ )
|
|
|
+
|
|
|
+ self.norm = nn.LayerNorm(d_model)
|
|
|
+
|
|
|
+ self.head = nn.Sequential(
|
|
|
+ nn.Linear(d_model, d_model),
|
|
|
+ nn.ReLU(),
|
|
|
+ nn.Linear(d_model, 1),
|
|
|
+ )
|
|
|
+
|
|
|
+ # ===== TRAINING =====
|
|
|
+ """"""
|
|
|
+ # self.optimizer = optim.Adam(self.parameters(), lr=Parametre.LearningRate)
|
|
|
+ self.optimizer = torch.optim.AdamW(self.parameters(), lr=1e-3, weight_decay=1e-4)
|
|
|
+ # self.scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, factor=0.1, patience=5)
|
|
|
+ self.scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(self.optimizer, T_max=50)
|
|
|
+ """
|
|
|
+ self.optimizer = torch.optim.Adam(model.parameters(), lr=1e-2)
|
|
|
+ self.scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)
|
|
|
+ """
|
|
|
+
|
|
|
+ self.loss_fn = nn.HuberLoss(delta=1.0) # nn.MSELoss()#nn.SmoothL1Loss() nn.HuberLoss()#
|
|
|
+
|
|
|
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
|
+ print(f"le réseau s'éxécute sur {self.device}")
|
|
|
+ self.to(self.device)
|
|
|
+
|
|
|
+ # =========================
|
|
|
+ # FORWARD
|
|
|
+ # =========================
|
|
|
+ def forward(self, x):
|
|
|
+ # (B, A, T, F)
|
|
|
+ B, A, T, F = x.shape
|
|
|
+
|
|
|
+ # =========================
|
|
|
+ # 1. Temporal encoding par asset
|
|
|
+ # =========================
|
|
|
+ x = self.temporal_encoder(x) # (B, A, T, D)
|
|
|
+
|
|
|
+ # =========================
|
|
|
+ # 2. Temporal convolution (mixing local patterns)
|
|
|
+ # =========================
|
|
|
+ x = x.view(B * A, T, -1)
|
|
|
+ x = x.permute(0, 2, 1) # (B*A, D, T)
|
|
|
+ x = self.temporal_mixer(x)
|
|
|
+ x = x.mean(dim=2) # (B*A, D)
|
|
|
+
|
|
|
+ x = x.view(B, A, -1) # (B, A, D)
|
|
|
+
|
|
|
+ # =========================
|
|
|
+ # 3. Inter-asset modeling
|
|
|
+ # =========================
|
|
|
+ residual = x
|
|
|
+ x, _ = self.asset_attention(x, x, x)
|
|
|
+ x = self.norm(x + residual)
|
|
|
+
|
|
|
+ # =========================
|
|
|
+ # 4. Head regression
|
|
|
+ # =========================
|
|
|
+ x = self.head(x).squeeze(-1) # (B, A)
|
|
|
+
|
|
|
+ return x
|
|
|
+
|
|
|
+ # =========================
|
|
|
+ # FIT
|
|
|
+ # =========================
|
|
|
+ @profile
|
|
|
+ def ancienfit(self, X, y, Parametre, EntreeValidation, TargetValidation):
|
|
|
+
|
|
|
+ self.train()
|
|
|
+
|
|
|
+ X = torch.tensor(X, dtype=torch.float32)
|
|
|
+ y = torch.tensor(y, dtype=torch.float32)
|
|
|
+
|
|
|
+ dataset = TensorDataset(X, y)
|
|
|
+ loader = DataLoader(dataset, batch_size=Parametre.batch_size, shuffle=False)
|
|
|
+ best_val = float("inf")
|
|
|
+ nbStagne = 0
|
|
|
+ for epoch in range(Parametre.epochs):
|
|
|
+ total_loss = 0
|
|
|
+ self.train()
|
|
|
+
|
|
|
+ self.optimizer.step()
|
|
|
+ self.scheduler.step()
|
|
|
+
|
|
|
+ for X_batch, y_batch in loader:
|
|
|
+ X_batch = X_batch.to(self.device)
|
|
|
+ y_batch = y_batch.to(self.device)
|
|
|
+ self.optimizer.zero_grad()
|
|
|
+
|
|
|
+ pred = self(X_batch)
|
|
|
+
|
|
|
+ loss = self.loss_fn(pred, y_batch)
|
|
|
+ direction_loss = torch.mean(torch.relu(-pred * y_batch))
|
|
|
+ loss = loss + 2 * direction_penalty
|
|
|
+
|
|
|
+ loss.backward()
|
|
|
+
|
|
|
+ self.optimizer.step()
|
|
|
+
|
|
|
+ total_loss += loss.item()
|
|
|
+
|
|
|
+ avg_loss = total_loss / len(loader)
|
|
|
+ current_lr = self.optimizer.param_groups[0]['lr']
|
|
|
+
|
|
|
+ validLoss = self.ValidLoss(EntreeValidation, TargetValidation, Parametre)
|
|
|
+
|
|
|
+ if validLoss < best_val:
|
|
|
+ best_val = validLoss
|
|
|
+ nbStagne = 0
|
|
|
+ best_weights = copy.deepcopy(self.state_dict())
|
|
|
+ print(
|
|
|
+ f"epoch {epoch} | loss: {avg_loss:.6f} | ValidLoss : {validLoss} | Stagne : {nbStagne}| LR : {current_lr} ")
|
|
|
+ else:
|
|
|
+ nbStagne += 1
|
|
|
+ if nbStagne == 20:
|
|
|
+ self.load_state_dict(best_weights)
|
|
|
+ if nbStagne == 1000:
|
|
|
+ self.load_state_dict(best_weights)
|
|
|
+ return epoch
|
|
|
+
|
|
|
+ @profile
|
|
|
+ def fit(self, X, y, Parametre, EntreeValidation, TargetValidation):
|
|
|
+
|
|
|
+ self.train()
|
|
|
+
|
|
|
+ X = torch.tensor(X, dtype=torch.float32)
|
|
|
+ y = torch.tensor(y, dtype=torch.float32)
|
|
|
+
|
|
|
+ dataset = TensorDataset(X, y)
|
|
|
+
|
|
|
+ loader = DataLoader(dataset, batch_size=Parametre.batch_size, shuffle=False)
|
|
|
+
|
|
|
+ best_val = float("inf")
|
|
|
+ nbStagne = 0
|
|
|
+ best_weights = None
|
|
|
+
|
|
|
+ for epoch in range(Parametre.epochs):
|
|
|
+
|
|
|
+ total_loss = 0
|
|
|
+ self.train()
|
|
|
+
|
|
|
+ for X_batch, y_batch in loader:
|
|
|
+ X_batch = X_batch.to(self.device)
|
|
|
+ y_batch = y_batch.to(self.device)
|
|
|
+
|
|
|
+ self.optimizer.zero_grad()
|
|
|
+
|
|
|
+ pred = self(X_batch)
|
|
|
+
|
|
|
+ loss = self.CalculLoss(pred,y_batch)
|
|
|
+
|
|
|
+ # stabilité gradients
|
|
|
+ norm = torch.nn.utils.clip_grad_norm_(self.parameters(), 5.0)
|
|
|
+
|
|
|
+ loss.backward()
|
|
|
+ self.optimizer.step()
|
|
|
+
|
|
|
+ total_loss += loss.item()
|
|
|
+
|
|
|
+ avg_loss = total_loss / len(loader)
|
|
|
+
|
|
|
+ # validation
|
|
|
+ validLoss = self.ValidLoss(EntreeValidation, TargetValidation, Parametre)
|
|
|
+
|
|
|
+ current_lr = self.optimizer.param_groups[0]["lr"]
|
|
|
+
|
|
|
+ self.scheduler.step()
|
|
|
+
|
|
|
+ # early stopping
|
|
|
+ if validLoss < best_val:
|
|
|
+ best_val = validLoss
|
|
|
+
|
|
|
+ best_weights = copy.deepcopy(self.state_dict())
|
|
|
+ print(
|
|
|
+ f"epoch {epoch} | " f"loss: {avg_loss:.6f} | " f"valid: {validLoss:.6f} | "f"stagnation: {nbStagne} | " f"lr: {current_lr}")
|
|
|
+ nbStagne = 0
|
|
|
+ else:
|
|
|
+ nbStagne += 1
|
|
|
+ if nbStagne >= 1000:
|
|
|
+ self.load_state_dict(best_weights)
|
|
|
+ return epoch
|
|
|
+
|
|
|
+ @profile
|
|
|
+ def CalculLoss(self, X, Y):
|
|
|
+ loss = self.loss_fn(X,Y)
|
|
|
+
|
|
|
+ direction_loss = torch.mean(torch.relu(-X * Y))
|
|
|
+ # print(direction_loss)
|
|
|
+
|
|
|
+ loss = loss + direction_loss
|
|
|
+ return direction_loss
|
|
|
+
|
|
|
+ # =========================
|
|
|
+ # PREDICT
|
|
|
+ # =========================
|
|
|
+ @profile
|
|
|
+ def predict(self, X):
|
|
|
+ self.eval()
|
|
|
+ with torch.no_grad():
|
|
|
+ X = torch.tensor(X, dtype=torch.float32).to(self.device)
|
|
|
+ pred = self(X)
|
|
|
+
|
|
|
+ return pred.cpu().numpy()
|
|
|
+
|
|
|
+ @profile
|
|
|
+ def EnregistreModel(self, Parametre, NomDossier, valeur):
|
|
|
+ if not os.path.exists(NomDossier):
|
|
|
+ os.makedirs(NomDossier)
|
|
|
+
|
|
|
+ nom = valeur / Parametre.PortefeuilleDemarage
|
|
|
+ nom = str(nom).replace('.', '_')
|
|
|
+
|
|
|
+ nom_model = f"{NomDossier}/{nom}.pt"
|
|
|
+
|
|
|
+ torch.save({
|
|
|
+ "model_state_dict": self.state_dict(),
|
|
|
+ "optimizer_state_dict": self.optimizer.state_dict(),
|
|
|
+ "best_score": valeur
|
|
|
+ }, nom_model)
|
|
|
+
|
|
|
+ nom_Parametre = f"{NomDossier}/{nom}"
|
|
|
+ Parametre.StockParam(nom_Parametre)
|
|
|
+
|
|
|
+ return nom
|
|
|
+
|
|
|
+ @profile
|
|
|
+ def RestaurationModel(self, NomDuModel):
|
|
|
+ NomDuModel = NomDuModel + ".pt"
|
|
|
+ checkpoint = torch.load(NomDuModel, map_location=self.device)
|
|
|
+
|
|
|
+ self.load_state_dict(checkpoint["model_state_dict"])
|
|
|
+ self.optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
|
|
|
+
|
|
|
+ self.to(self.device)
|
|
|
+ self.eval()
|
|
|
+
|
|
|
+ return self
|
|
|
+
|
|
|
+ @profile
|
|
|
+ def Evaluation(self, Date, Portefeuille, Donnee):
|
|
|
+ return Portefeuille.getValueTotal(Date, Donnee)
|
|
|
+
|
|
|
+
|
|
|
+ def ValidLoss(self, Entree, Target, Parametre):
|
|
|
+
|
|
|
+ self.eval()
|
|
|
+
|
|
|
+ X = torch.tensor(Entree, dtype=torch.float32)
|
|
|
+ y = torch.tensor(Target, dtype=torch.float32)
|
|
|
+
|
|
|
+ dataset = TensorDataset(X, y)
|
|
|
+
|
|
|
+ loader = DataLoader(
|
|
|
+ dataset,
|
|
|
+ batch_size=Parametre.batch_size,
|
|
|
+ shuffle=False
|
|
|
+ )
|
|
|
+
|
|
|
+ total_loss = 0
|
|
|
+
|
|
|
+ with torch.no_grad():
|
|
|
+ for X_batch, y_batch in loader:
|
|
|
+ X_batch = X_batch.to(self.device)
|
|
|
+ y_batch = y_batch.to(self.device)
|
|
|
+
|
|
|
+ pred = self(X_batch)
|
|
|
+
|
|
|
+ loss = self.CalculLoss(pred, y_batch)
|
|
|
+
|
|
|
+ total_loss += loss.item()
|
|
|
+
|
|
|
+ avg_loss = total_loss / len(loader)
|
|
|
+
|
|
|
+ return avg_loss
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|