nn_models.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. from random import sample
  2. from sklearn.base import BaseEstimator
  3. from sklearn.metrics import SCORERS
  4. from sklearn.model_selection import train_test_split
  5. from sklearn.utils import check_X_y, check_array
  6. import torch
  7. from torch import nn, optim
  8. from torch.utils.data import DataLoader
  9. from scipy.sparse import issparse
  10. import numpy as np
  11. class torchMLP (nn.Module):
  12. """
  13. Neural network model for
  14. """
  15. def __init__(self, n_features, n_labels):
  16. super().__init__()
  17. self.network = nn.Sequential(*[
  18. nn.Linear(n_features, 200),
  19. nn.ReLU(),
  20. nn.Linear(200, 50),
  21. nn.ReLU(),
  22. nn.Linear(50, n_labels),
  23. nn.Sigmoid()
  24. ])
  25. def forward(self, x):
  26. y_hat = self.network(x)
  27. return y_hat
  28. class torchMLPClassifier_sklearn (BaseEstimator):
  29. """
  30. Pytorch neural network with a sklearn-like API
  31. """
  32. def __init__ (self, model, n_epochs=50, early_stop=True, early_stop_metric="accuracy", early_stop_validations_size=0.1, batch_size=1024, learning_rate=1e-3, class_weight=None, device_train="cpu", device_predict="cpu", verbose=False):
  33. """
  34. Parameters:
  35. -----------
  36. model: non instanciated pytorch neural network model with a n_features and n_labels parameter
  37. n_epochs: int, number of epochs
  38. early_stop: boolean, if true an evaluation dataset is created and used to stop the training
  39. early_stop_metric: str, metric score to evaluate the model, according to sklearn.metrics.SCORERS.keys()
  40. early_stop_validations_size: int or float, if float percentage of the train dataset used for validation, otherwise number of sample to use
  41. batch_size: int, size of the training batch
  42. learning_rate: float, Adam optimizer learning rate
  43. class_weight: dict or str, same as the sklearn API
  44. device_train: str, device on which to train
  45. device_predict: str, device on which to predict
  46. verbose: boolean, if true the loss and score are printed
  47. """
  48. self.model = model
  49. self.n_epochs = n_epochs
  50. if early_stop and (early_stop_metric is not None) and (early_stop_metric in SCORERS.keys()) and (isinstance(early_stop_validations_size, int) or isinstance(early_stop_validations_size, float)):
  51. self.early_stop = early_stop
  52. self.early_stop_metric_name = early_stop_metric
  53. self.early_stop_metric = SCORERS[early_stop_metric]
  54. self.early_stop_validations_size = early_stop_validations_size
  55. else:
  56. self.early_stop = False
  57. self.early_stop_metric = None
  58. self.early_stop_validations_size = None
  59. self.class_weight = class_weight
  60. self.learning_rate = learning_rate
  61. self.device_train = device_train
  62. self.device_predict = device_predict
  63. self.batch_size = batch_size
  64. self.verbose = verbose
  65. def fit(self, X, y):
  66. """
  67. Training the model
  68. Parameters:
  69. -----------
  70. X_test: pandas dataframe of the features
  71. y_test: pandas dataframe of the labels
  72. """
  73. X, y = check_X_y(X, y, accept_sparse=True, multi_output=True)
  74. if y.ndim == 1:
  75. y = np.expand_dims(y, 1)
  76. # Validation split if early stopping
  77. if self.early_stop:
  78. X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=self.early_stop_validations_size)
  79. if issparse(X_val): # To deal with the sparse matrix situations
  80. X_val = X_val.toarray()
  81. else:
  82. X_train, y_train = X, y
  83. n_samples = y_train.shape[0]
  84. n_labels_values = len(np.unique(y_train))
  85. n_labels = y_train.shape[1]
  86. n_features = X.shape[1]
  87. # Raising the model
  88. self.network = self.model(n_features=n_features, n_labels=n_labels)
  89. self.optimizer = optim.Adam(self.network.parameters(), lr=self.learning_rate)
  90. # Creating dataloader for X_train, y_train
  91. data_loader = DataLoader(range(X_train.shape[0]), shuffle=True, batch_size=self.batch_size)
  92. # Initializing loss function
  93. ## Getting weights
  94. if self.class_weight is not None:
  95. if self.class_weight == "balanced":
  96. weights = n_samples/(n_labels_values*np.bincount(y_train[:,0]))
  97. weights_dict = dict(zip(range(len(weights)), weights))
  98. else:
  99. weights_dict = self.class_weight
  100. else:
  101. weights_dict = None
  102. criterion = nn.BCELoss()
  103. # Running train
  104. last_score = 0
  105. for i in range(self.n_epochs):
  106. self.network = self.network.to(self.device_train)
  107. # Starting an epoch
  108. for indices in data_loader:
  109. self.optimizer.zero_grad()
  110. X_train_sample, y_train_sample = X_train[indices, :], y_train[indices, :]
  111. if issparse(X_train_sample): # To deal with the sparse matrix situations
  112. X_train_sample = X_train_sample.toarray()
  113. X_train_sample_tensor, y_train_sample_tensor = [torch.tensor(x, dtype=torch.float32).to(self.device_train) for x in [X_train_sample, y_train_sample]]
  114. # Weighting the loss
  115. if self.class_weight is not None:
  116. sample_weights = y_train_sample.copy()
  117. for x, y in weights_dict.items():
  118. sample_weights[sample_weights == x] = y
  119. criterion.weigths = sample_weights
  120. # Get prediction
  121. X_train_sample_tensor, y_train_sample_tensor = X_train_sample_tensor.to(self.device_train), y_train_sample_tensor.to(self.device_train)
  122. y_train_sample_hat = self.network(X_train_sample_tensor)
  123. loss = criterion(y_train_sample_hat, y_train_sample_tensor)
  124. loss.backward()
  125. self.optimizer.step()
  126. # End of the Epoch : evaluating the score
  127. if self.early_stop:
  128. score = self.early_stop_metric(self, X_val, y_val)
  129. if score < last_score:
  130. return self
  131. else:
  132. last_score = score
  133. if self.verbose:
  134. if self.early_stop:
  135. print(f"Epoch {i} : Loss {loss.item():.3f} - {self.early_stop_metric_name} {score:.3f}")
  136. else:
  137. print(f"Epoch {i} : Loss {loss.item():.3f}")
  138. return self
  139. def predict(self, X):
  140. """
  141. Getting the prediction
  142. Parameters:
  143. -----------
  144. X_test: pandas dataframe of the features
  145. """
  146. y_hat_proba = self.predict_raw_proba(X)
  147. y_hat = ((y_hat_proba >= 0.5)*1).flatten()
  148. return y_hat
  149. def predict_raw_proba(self, X):
  150. """
  151. Getting the prediction score in tensor format
  152. Parameters:
  153. -----------
  154. X_test: pandas dataframe of the features
  155. """
  156. X = check_array(X, accept_sparse=True)
  157. if issparse(X): # To deal with the sparse matrix situations
  158. X = X.toarray()
  159. with torch.no_grad():
  160. model_predict = self.network.to(self.device_predict)
  161. model_predict.eval()
  162. # Create a tensor from X
  163. X_tensor = torch.tensor(X, dtype=torch.float32).to(self.device_predict)
  164. y_hat_proba_torch = model_predict(X_tensor)
  165. y_hat_proba_torch = y_hat_proba_torch.detach().cpu().numpy()
  166. return y_hat_proba_torch
  167. def predict_proba(self, X):
  168. """
  169. Getting the prediction score in sklearn format
  170. Parameters:
  171. -----------
  172. X_test: pandas dataframe of the features
  173. """
  174. y_hat_proba_torch = self.predict_raw_proba(X)
  175. y_hat_proba_torch = np.concatenate([
  176. 1-y_hat_proba_torch,
  177. y_hat_proba_torch
  178. ], axis=1)
  179. y_hat_proba = y_hat_proba_torch
  180. return y_hat_proba