text: # Copyright (c) Microsoft Corporation.
  # Licensed under the MIT License.
  
  
  from __future__ import division
  from __future__ import print_function
  
  import os
  import numpy as np
  import pandas as pd
  import copy
  from sklearn.metrics import roc_auc_score, mean_squared_error
  import logging
  from ...utils import (
      unpack_archive_with_buffer,
      save_multiple_parts_file,
      create_save_path,
      drop_nan_by_y_index,
  )
  from ...log import get_module_logger, TimeInspector
  
  import torch
  import torch.nn as nn
  import torch.optim as optim
  from torch.utils.data import DataLoader
  
  from ...model.base import Model
  from ...data.dataset import DatasetH, TSDatasetH
  from ...data.dataset.handler import DataHandlerLP
  
  
  class ALSTM(Model):
      """ALSTM Model
  
      Parameters
      ----------
      d_feat : int
          input dimension for each time step
      metric: str
          the evaluate metric used in early stop
      optimizer : str
          optimizer name
      GPU : str
          the GPU ID(s) used for training
      """
  
      def __init__(
          self,
          d_feat=6,
          hidden_size=64,
          num_layers=2,
          dropout=0.0,
          n_epochs=200,
          lr=0.001,
          metric="",
          batch_size=2000,
          early_stop=20,
          loss="mse",
          optimizer="adam",
          n_jobs=10,
          GPU=0,
          seed=None,
          **kwargs
      ):
          # Set logger.
          self.logger = get_module_logger("ALSTM")
          self.logger.info("ALSTM pytorch version...")
  
          # set hyper-parameters.
          self.d_feat = d_feat
          self.hidden_size = hidden_size
          self.num_layers = num_layers
          self.dropout = dropout
          self.n_epochs = n_epochs
          self.lr = lr
          self.metric = metric
          self.batch_size = batch_size
          self.early_stop = early_stop
          self.optimizer = optimizer.lower()
          self.loss = loss
          self.device = torch.device("cuda:%d" % (GPU) if torch.cuda.is_available() else "cpu")
          self.n_jobs = n_jobs
          self.use_gpu = torch.cuda.is_available()
          self.seed = seed
  
          self.logger.info(
              "ALSTM parameters setting:"
              "\nd_feat : {}"
              "\nhidden_size : {}"
              "\nnum_layers : {}"
              "\ndropout : {}"
              "\nn_epochs : {}"
              "\nlr : {}"
              "\nmetric : {}"
              "\nbatch_size : {}"
              "\nearly_stop : {}"
              "\noptimizer : {}"
              "\nloss_type : {}"
              "\nvisible_GPU : {}"
              "\nn_jobs : {}"
              "\nuse_GPU : {}"
              "\nseed : {}".format(
                  d_feat,
                  hidden_size,
                  num_layers,
                  dropout,
                  n_epochs,
                  lr,
                  metric,
                  batch_size,
                  early_stop,
                  optimizer.lower(),
                  loss,
                  GPU,
                  n_jobs,
                  self.use_gpu,
                  seed,
              )
          )
  
          if self.seed is not None:
              np.random.seed(self.seed)
              torch.manual_seed(self.seed)
  
          self.ALSTM_model = ALSTMModel(
              d_feat=self.d_feat,
              hidden_size=self.hidden_size,
              num_layers=self.num_layers,
              dropout=self.dropout,
          ).to(self.device)
          if optimizer.lower() == "adam":
              self.train_optimizer = optim.Adam(self.ALSTM_model.parameters(), lr=self.lr)
          elif optimizer.lower() == "gd":
              self.train_optimizer = optim.SGD(self.ALSTM_model.parameters(), lr=self.lr)
          else:
              raise NotImplementedError("optimizer {} is not supported!".format(optimizer))
  
          self._fitted = False
          self.ALSTM_model.to(self.device)
  
      def mse(self, pred, label):
          loss = (pred - label) ** 2
          return torch.mean(loss)
  
      def loss_fn(self, pred, label):
          mask = ~torch.isnan(label)
  
          if self.loss == "mse":
              return self.mse(pred[mask], label[mask])
  
          raise ValueError("unknown loss `%s`" % self.loss)
  
      def metric_fn(self, pred, label):
  
          mask = torch.isfinite(label)
  
          if self.metric == "" or self.metric == "loss":
              return -self.loss_fn(pred[mask], label[mask])
  
          raise ValueError("unknown metric `%s`" % self.metric)
  
      def train_epoch(self, data_loader):
  
          self.ALSTM_model.train()
  
          for data in data_loader:
              feature = data[:, :, 0:-1].to(self.device)
              label = data[:, -1, -1].to(self.device)
  
              pred = self.ALSTM_model(feature.float())
              loss = self.loss_fn(pred, label)
  
              self.train_optimizer.zero_grad()
              loss.backward()
              torch.nn.utils.clip_grad_value_(self.ALSTM_model.parameters(), 3.0)
              self.train_optimizer.step()
  
      def test_epoch(self, data_loader):
  
          self.ALSTM_model.eval()
  
          scores = []
          losses = []
  
          for data in data_loader:
  
              feature = data[:, :, 0:-1].to(self.device)
              # feature[torch.isnan(feature)] = 0
              label = data[:, -1, -1].to(self.device)
  
              pred = self.ALSTM_model(feature.float())
              loss = self.loss_fn(pred, label)
              losses.append(loss.item())
  
              score = self.metric_fn(pred, label)
              scores.append(score.item())
  
          return np.mean(losses), np.mean(scores)
  
      def fit(
          self,
          dataset,
          evals_result=dict(),
          verbose=True,
          save_path=None,
      ):
          dl_train = dataset.prepare("train", col_set=["feature", "label"], data_key=DataHandlerLP.DK_L)
          dl_valid = dataset.prepare("valid", col_set=["feature", "label"], data_key=DataHandlerLP.DK_L)
  
          dl_train.config(fillna_type="ffill+bfill")  # process nan brought by dataloader
          dl_valid.config(fillna_type="ffill+bfill")  # process nan brought by dataloader
  
          train_loader = DataLoader(dl_train, batch_size=self.batch_size, shuffle=True, num_workers=self.n_jobs)
          valid_loader = DataLoader(dl_valid, batch_size=self.batch_size, shuffle=False, num_workers=self.n_jobs)
  
          if save_path == None:
              save_path = create_save_path(save_path)
  
          stop_steps = 0
          train_loss = 0
          best_score = -np.inf
          best_epoch = 0
          evals_result["train"] = []
          evals_result["valid"] = []
  
          # train
          self.logger.info("training...")
          self._fitted = True
  
          for step in range(self.n_epochs):
              self.logger.info("Epoch%d:", step)
              self.logger.info("training...")
              self.train_epoch(train_loader)
              self.logger.info("evaluating...")
              train_loss, train_score = self.test_epoch(train_loader)
              val_loss, val_score = self.test_epoch(valid_loader)
              self.logger.info("train %.6f, valid %.6f" % (train_score, val_score))
              evals_result["train"].append(train_score)
              evals_result["valid"].append(val_score)
  
              if val_score > best_score:
                  best_score = val_score
                  stop_steps = 0
                  best_epoch = step
                  best_param = copy.deepcopy(self.ALSTM_model.state_dict())
              else:
                  stop_steps += 1
                  if stop_steps >= self.early_stop:
                      self.logger.info("early stop")
                      break
  
          self.logger.info("best score: %.6lf @ %d" % (best_score, best_epoch))
          self.ALSTM_model.load_state_dict(best_param)
          torch.save(best_param, save_path)
  
          if self.use_gpu:
              torch.cuda.empty_cache()
  
      def predict(self, dataset):
          if not self._fitted:
              raise ValueError("model is not fitted yet!")
  
          dl_test = dataset.prepare("test", col_set=["feature", "label"], data_key=DataHandlerLP.DK_I)
          dl_test.config(fillna_type="ffill+bfill")
          test_loader = DataLoader(dl_test, batch_size=self.batch_size, num_workers=self.n_jobs)
          self.ALSTM_model.eval()
          preds = []
  
          for data in test_loader:
  
              feature = data[:, :, 0:-1].to(self.device)
  
              with torch.no_grad():
                  if self.use_gpu:
                      pred = self.ALSTM_model(feature.float()).detach().cpu().numpy()
                  else:
                      pred = self.ALSTM_model(feature.float()).detach().numpy()
  
              preds.append(pred)
  
          return pd.Series(np.concatenate(preds), index=dl_test.get_index())
  
  
  class ALSTMModel(nn.Module):
      def __init__(self, d_feat=6, hidden_size=64, num_layers=2, dropout=0.0, rnn_type="GRU"):
          super().__init__()
          self.hid_size = hidden_size
          self.input_size = d_feat
          self.dropout = dropout
          self.rnn_type = rnn_type
          self.rnn_layer = num_layers
          self._build_model()
  
      def _build_model(self):
          try:
              klass = getattr(nn, self.rnn_type.upper())
          except:
              raise ValueError("unknown rnn_type `%s`" % self.rnn_type)
          self.net = nn.Sequential()
          self.net.add_module("fc_in", nn.Linear(in_features=self.input_size, out_features=self.hid_size))
          self.net.add_module("act", nn.Tanh())
          self.rnn = klass(
              input_size=self.hid_size,
              hidden_size=self.hid_size,
              num_layers=self.rnn_layer,
              batch_first=True,
              dropout=self.dropout,
          )
          self.fc_out = nn.Linear(in_features=self.hid_size * 2, out_features=1)
          self.att_net = nn.Sequential()
          self.att_net.add_module(
              "att_fc_in",
              nn.Linear(in_features=self.hid_size, out_features=int(self.hid_size / 2)),
          )
          self.att_net.add_module("att_dropout", torch.nn.Dropout(self.dropout))
          self.att_net.add_module("att_act", nn.Tanh())
          self.att_net.add_module(
              "att_fc_out",
              nn.Linear(in_features=int(self.hid_size / 2), out_features=1, bias=False),
          )
          self.att_net.add_module("att_softmax", nn.Softmax(dim=1))
  
      def forward(self, inputs):
          rnn_out, _ = self.rnn(self.net(inputs))  # [batch, seq_len, num_directions * hidden_size]
          attention_score = self.att_net(rnn_out)  # [batch, seq_len, 1]
          out_att = torch.mul(rnn_out, attention_score)
          out_att = torch.sum(out_att, dim=1)
          out = self.fc_out(
              torch.cat((rnn_out[:, -1, :], out_att), dim=1)
          )  # [batch, seq_len, num_directions * hidden_size] -> [batch, 1]
          return out[..., 0]
  