# 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 torch.utils.data import Sampler from ...model.base import Model from ...data.dataset import DatasetH from ...data.dataset.handler import DataHandlerLP from ...contrib.model.pytorch_lstm import LSTMModel from ...contrib.model.pytorch_gru import GRUModel class DailyBatchSampler(Sampler): def __init__(self, data_source): self.data_source = data_source self.data = self.data_source.data.loc[self.data_source.get_index()] self.daily_count = self.data.groupby(level=0).size().values # calculate number of samples in each batch self.daily_index = np.roll(np.cumsum(self.daily_count), 1) # calculate begin index of each batch self.daily_index[0] = 0 def __iter__(self): for idx, count in zip(self.daily_index, self.daily_count): yield np.arange(idx, idx + count) def __len__(self): return len(self.data_source) class GATs(Model): """GATs Model Parameters ---------- lr : float learning rate d_feat : int input dimensions 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=20, hidden_size=64, num_layers=2, dropout=0.0, n_epochs=200, lr=0.001, metric="", early_stop=20, loss="mse", base_model="GRU", with_pretrain=True, model_path=None, optimizer="adam", GPU="0", n_jobs=10, seed=None, **kwargs ): # Set logger. self.logger = get_module_logger("GATs") self.logger.info("GATs 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.early_stop = early_stop self.optimizer = optimizer.lower() self.loss = loss self.base_model = base_model self.with_pretrain = with_pretrain self.model_path = model_path 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( "GATs parameters setting:" "\nd_feat : {}" "\nhidden_size : {}" "\nnum_layers : {}" "\ndropout : {}" "\nn_epochs : {}" "\nlr : {}" "\nmetric : {}" "\nearly_stop : {}" "\noptimizer : {}" "\nloss_type : {}" "\nbase_model : {}" "\nwith_pretrain : {}" "\nmodel_path : {}" "\nvisible_GPU : {}" "\nuse_GPU : {}" "\nseed : {}".format( d_feat, hidden_size, num_layers, dropout, n_epochs, lr, metric, early_stop, optimizer.lower(), loss, base_model, with_pretrain, model_path, GPU, self.use_gpu, seed, ) ) if self.seed is not None: np.random.seed(self.seed) torch.manual_seed(self.seed) self.GAT_model = GATModel( d_feat=self.d_feat, hidden_size=self.hidden_size, num_layers=self.num_layers, dropout=self.dropout, base_model=self.base_model, ) if optimizer.lower() == "adam": self.train_optimizer = optim.Adam(self.GAT_model.parameters(), lr=self.lr) elif optimizer.lower() == "gd": self.train_optimizer = optim.SGD(self.GAT_model.parameters(), lr=self.lr) else: raise NotImplementedError("optimizer {} is not supported!".format(optimizer)) self._fitted = False self.GAT_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 get_daily_inter(self, df, shuffle=False): # organize the train data into daily batches daily_count = df.groupby(level=0).size().values daily_index = np.roll(np.cumsum(daily_count), 1) daily_index[0] = 0 if shuffle: # shuffle data daily_shuffle = list(zip(daily_index, daily_count)) np.random.shuffle(daily_shuffle) daily_index, daily_count = zip(*daily_shuffle) return daily_index, daily_count def train_epoch(self, data_loader): self.GAT_model.train() for data in data_loader: data = data.squeeze() feature = data[:, :, 0:-1].to(self.device) label = data[:, -1, -1].to(self.device) pred = self.GAT_model(feature.float()) loss = self.loss_fn(pred, label) self.train_optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_value_(self.GAT_model.parameters(), 3.0) self.train_optimizer.step() def test_epoch(self, data_loader): self.GAT_model.eval() scores = [] losses = [] for data in data_loader: data = data.squeeze() feature = data[:, :, 0:-1].to(self.device) # feature[torch.isnan(feature)] = 0 label = data[:, -1, -1].to(self.device) pred = self.GAT_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 sampler_train = DailyBatchSampler(dl_train) sampler_valid = DailyBatchSampler(dl_valid) train_loader = DataLoader(dl_train, sampler=sampler_train, num_workers=self.n_jobs) valid_loader = DataLoader(dl_valid, sampler=sampler_valid, 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"] = [] # load pretrained base_model if self.with_pretrain: if self.model_path == None: raise ValueError("the path of the pretrained model should be given first!") self.logger.info("Loading pretrained model...") if self.base_model == "LSTM": pretrained_model = LSTMModel( d_feat=self.d_feat, hidden_size=self.hidden_size, num_layers=self.num_layers ) pretrained_model.load_state_dict(torch.load(self.model_path)) elif self.base_model == "GRU": pretrained_model = GRUModel( d_feat=self.d_feat, hidden_size=self.hidden_size, num_layers=self.num_layers ) pretrained_model.load_state_dict(torch.load(self.model_path)) else: raise ValueError("unknown base model name `%s`" % self.base_model) model_dict = self.GAT_model.state_dict() pretrained_dict = {k: v for k, v in pretrained_model.state_dict().items() if k in model_dict} model_dict.update(pretrained_dict) self.GAT_model.load_state_dict(model_dict) self.logger.info("Loading pretrained model Done...") # 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.GAT_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.GAT_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") sampler_test = DailyBatchSampler(dl_test) test_loader = DataLoader(dl_test, sampler=sampler_test, num_workers=self.n_jobs) self.GAT_model.eval() preds = [] for data in test_loader: data = data.squeeze() feature = data[:, :, 0:-1].to(self.device) with torch.no_grad(): if self.use_gpu: pred = self.GAT_model(feature.float()).detach().cpu().numpy() else: pred = self.GAT_model(feature.float()).detach().numpy() preds.append(pred) return pd.Series(np.concatenate(preds), index=dl_test.get_index()) class GATModel(nn.Module): def __init__(self, d_feat=6, hidden_size=64, num_layers=2, dropout=0.0, base_model="GRU"): super().__init__() if base_model == "GRU": self.rnn = nn.GRU( input_size=d_feat, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout, ) elif base_model == "LSTM": self.rnn = nn.LSTM( input_size=d_feat, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout, ) else: raise ValueError("unknown base model name `%s`" % base_model) self.hidden_size = hidden_size self.d_feat = d_feat self.transformation = nn.Linear(self.hidden_size, self.hidden_size) self.a = nn.Parameter(torch.randn(self.hidden_size * 2, 1)) self.a.requires_grad = True self.fc = nn.Linear(self.hidden_size, self.hidden_size) self.fc_out = nn.Linear(hidden_size, 1) self.leaky_relu = nn.LeakyReLU() self.softmax = nn.Softmax(dim=1) def cal_attention(self, x, y): x = self.transformation(x) y = self.transformation(y) sample_num = x.shape[0] dim = x.shape[1] e_x = x.expand(sample_num, sample_num, dim) e_y = torch.transpose(e_x, 0, 1) attention_in = torch.cat((e_x, e_y), 2).view(-1, dim * 2) self.a_t = torch.t(self.a) attention_out = self.a_t.mm(torch.t(attention_in)).view(sample_num, sample_num) attention_out = self.leaky_relu(attention_out) att_weight = self.softmax(attention_out) return att_weight def forward(self, x): out, _ = self.rnn(x) hidden = out[:, -1, :] att_weight = self.cal_attention(hidden, hidden) hidden = att_weight.mm(hidden) + hidden hidden = self.fc(hidden) hidden = self.leaky_relu(hidden) return self.fc_out(hidden).squeeze()