dinn.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. import torch
  2. import numpy as np
  3. from .dataset import PandemicDataset
  4. from .problem import PandemicProblem
  5. from .plotter import Plotter
  6. class DINN:
  7. class NN(torch.nn.Module):
  8. def __init__(self,
  9. output_size: int,
  10. input_size: int,
  11. hidden_size: int,
  12. hidden_layers: int,
  13. activation_layer,
  14. t_init,
  15. t_final) -> None:
  16. """Neural Network
  17. Args:
  18. output_size (int): number of outputs
  19. input_size (int): number of inputs
  20. hidden_size (int): number of hidden nodes per layer
  21. hidden_layers (int): number of hidden layers
  22. activation_layer (_type_): activation layer
  23. """
  24. super(DINN.NN, self).__init__()
  25. self.input = torch.nn.Sequential(torch.nn.Linear(input_size, hidden_size), activation_layer)
  26. self.hidden = torch.nn.Sequential(*[torch.nn.Sequential(torch.nn.Linear(hidden_size, hidden_size), activation_layer) for _ in range(hidden_layers)])
  27. self.output = torch.nn.Linear(hidden_size, output_size)
  28. self.__t_init = t_init
  29. self.__t_final = t_final
  30. def forward(self, t):
  31. # normalize input
  32. t_scaled = (t - self.__t_init) / (self.__t_final - self.__t_init)
  33. x = self.input(t_scaled)
  34. x = self.hidden(x)
  35. x = self.output(x)
  36. return x
  37. def __init__(self,
  38. output_size: int,
  39. data: PandemicDataset,
  40. parameter_list: list,
  41. problem: PandemicProblem,
  42. plotter: Plotter,
  43. state_variables=[],
  44. parameter_regulator=torch.tanh,
  45. input_size=1,
  46. hidden_size=20,
  47. hidden_layers=7,
  48. activation_layer=torch.nn.ReLU()) -> None:
  49. """Desease Informed Neural Network. Uses the PandemicProblem, DINN.NN and PandemicDataset to solve Inverse Problems and find the
  50. parameters of a specific mathematical model.
  51. Args:
  52. output_size (int): Number of the output nodes of the NN.
  53. data (PandemicDataset): Data collected showing the course of the pandemic
  54. parameter_list (list): List of the parameter names(strings), that are supposed to be found.
  55. problem (PandemicProblem): Problem class implementing the calculation of the residuals.
  56. plotter (Plotter): Plotter object to plot dataset curves.
  57. state_variables (list, optional): List of the names of state variables. Defaults to [].
  58. parameter_regulator (optional): Function to force the parameters to be in a certain range. Defaults to torch.tanh.
  59. input_size (int, optional): Number of the input nodes of the NN. Defaults to 1.
  60. hidden_size (int, optional): Number of the hidden nodes of the NN. Defaults to 20.
  61. hidden_layers (int, optional): Number of the hidden layers for the NN. Defaults to 7.
  62. activation_layer (optional): Class of the activation function. Defaults to torch.nn.ReLU().
  63. """
  64. assert len(state_variables) + data.number_groups == output_size, f'The number of groups plus the number of state variable must result in the output size\nGroups:\t{data.number_groups}\nState variables:\t{len(state_variables)}\noutput_size: {output_size}\n'
  65. self.device = torch.device(data.device_name)
  66. self.device_name = data.device_name
  67. self.plotter = plotter
  68. self.model = DINN.NN(output_size,
  69. input_size,
  70. hidden_size,
  71. hidden_layers,
  72. activation_layer,
  73. data.t_init,
  74. data.t_final)
  75. self.model = self.model.to(self.device)
  76. self.data = data
  77. self.parameter_regulator = parameter_regulator
  78. self.problem = problem
  79. self.problem.def_grad_matrix(output_size)
  80. self.parameters_tilda = {}
  81. for parameter in parameter_list:
  82. self.parameters_tilda.update({parameter : torch.nn.Parameter(torch.rand(1, requires_grad=True, device=self.device_name))})
  83. # new model has to be configured and then trained
  84. self.__is_configured = False
  85. self.__has_trained = False
  86. self.__state_variables = state_variables
  87. self.parameters = [np.zeros(1) for _ in range(len(parameter_list))]
  88. self.frames = []
  89. @property
  90. def number_state_variables(self):
  91. return len(self.__state_variables)
  92. def get_regulated_param(self, parameter_name: str):
  93. """Function to get the searched parameters, forced into a certain range.
  94. Args:
  95. parameter_name (str): Name of the parameter to be returned.
  96. Returns:
  97. torch.Parameter: Regulated parameter object of the search parameter.
  98. """
  99. return self.parameter_regulator(self.parameters_tilda[parameter_name])
  100. def get_parameters_tilda(self):
  101. """Function to get the original value (not forced into any range).
  102. Returns:
  103. torch.Parameter: Parameter object of the search parameter.
  104. """
  105. return list(self.parameters_tilda.values())
  106. def get_regulated_param_list(self):
  107. """Get the list of regulated parameters (forced into a specific range).
  108. Returns:
  109. list: list of regulated parameters
  110. """
  111. return [self.parameter_regulator(parameter) for parameter in self.get_parameters_tilda()]
  112. def configure_training(self, lr:float, epochs:int, optimizer_name='Adam', scheduler_name='CyclicLR', scheduler_factor = 1, verbose=False):
  113. """This method sets the optimizer, scheduler, learning rate and number of epochs for the following training process.
  114. Args:
  115. lr (float): Learning rate for the optimizer.
  116. epochs (int): Number of epochs the NN is supposed to be trained for.
  117. optimizer_name (str, optional): Name of the optimizer class that is supposed to be used. Defaults to 'Adam'.
  118. scheduler_name (str, optional): Name of the scheduler class that is supposed to be used. Defaults to 'CyclicLR'.
  119. verbose (bool, optional): Controles if the configuration process, is to be verbosed. Defaults to False.
  120. """
  121. parameter_list = list(self.model.parameters()) + list(self.parameters_tilda.values())
  122. self.epochs = epochs
  123. match optimizer_name:
  124. case 'Adam':
  125. self.optimizer = torch.optim.Adam(parameter_list, lr=lr)
  126. case _:
  127. self.optimizer = torch.optim.Adam(parameter_list, lr=lr)
  128. if verbose:
  129. print('---------------------------------')
  130. print(f' Entered unknown optimizer name: {optimizer_name}\n Defaulted to Adam.')
  131. print('---------------------------------')
  132. optimizer_name = 'Adam'
  133. match scheduler_name:
  134. case 'CyclicLR':
  135. self.scheduler = torch.optim.lr_scheduler.CyclicLR(self.optimizer, base_lr=lr * 10, max_lr=lr * 1e3, step_size_up=1000, mode="exp_range", gamma=0.85, cycle_momentum=False)
  136. case 'LinearLR':
  137. self.scheduler = torch.optim.lr_scheduler.LinearLR(self.optimizer, start_factor=lr, total_iters=epochs/scheduler_factor)
  138. case 'PolynomialLR':
  139. self.scheduler = torch.optim.lr_scheduler.PolynomialLR(self.optimizer, total_iters=epochs/scheduler_factor, power=1.0)
  140. case _:
  141. self.scheduler = torch.optim.lr_scheduler.CyclicLR(self.optimizer, base_lr=lr * 10, max_lr=lr * 1e3, step_size_up=1000, mode="exp_range", gamma=0.85, cycle_momentum=False)
  142. if verbose:
  143. print('---------------------------------')
  144. print(f' Entered unknown scheduler name: {scheduler_name}\n Defaulted to CyclicLR.')
  145. print('---------------------------------')
  146. scheduler_name = 'CyclicLR'
  147. if verbose:
  148. print(f'\nLearning Rate:\t{lr}\nOptimizer:\t{optimizer_name}\nScheduler:\t{scheduler_name}\n')
  149. self.__is_configured = True
  150. def train(self,
  151. create_animation=False,
  152. animation_sample_rate=500,
  153. verbose=False):
  154. """Training routine for the DINN.
  155. Args:
  156. create_animation (boolean, optional): Decides on wether a prediction animation is supposed to be created during training. Defaults to False.
  157. animation_sample_rate (int, optional): Sample rate of the prediction animation. Only used, when create_animation=True. Defaults to 500.
  158. verbose (bool, optional): Controles if the training process, is to be verbosed. Defaults to False.
  159. """
  160. assert self.__is_configured, 'The model has to be configured before training through the use of self.configure training.'
  161. if verbose:
  162. print(f'torch seed: {torch.seed()}')
  163. # arrays to hold values for plotting
  164. self.losses = np.zeros(self.epochs)
  165. self.obs_losses = np.zeros(self.epochs)
  166. self.physics_losses = np.zeros(self.epochs)
  167. self.parameters = [np.zeros(self.epochs) for _ in self.parameters]
  168. for epoch in range(self.epochs):
  169. # get the prediction and the fitting residuals
  170. prediction = self.model(self.data.t_batch)
  171. residuals = self.problem.residual(prediction, *self.get_regulated_param_list())
  172. self.optimizer.zero_grad()
  173. # calculate loss from the differential system
  174. loss_physics = 0
  175. for residual in residuals:
  176. loss_physics += torch.mean(torch.square(residual))
  177. # calculate loss from the dataset
  178. loss_obs = 0
  179. for i, group in enumerate(self.data.group_names):
  180. loss_obs += torch.mean(torch.square(self.data.get_norm(group) - prediction[:, i]))
  181. loss = loss_obs + loss_physics
  182. loss.backward()
  183. self.optimizer.step()
  184. self.scheduler.step()
  185. # append values for plotting
  186. self.losses[epoch] = loss.item()
  187. self.obs_losses[epoch] = loss_obs.item()
  188. self.physics_losses[epoch] = loss_physics.item()
  189. for i, parameter in enumerate(self.parameters_tilda.items()):
  190. self.parameters[i][epoch] = self.get_regulated_param(parameter[0]).item()
  191. # do snapshot for prediction animation
  192. if epoch % animation_sample_rate == 0 and create_animation:
  193. # prediction
  194. prediction = self.model(self.data.t_batch)
  195. t = torch.arange(0, self.data.t_raw[-1].item(), (self.data.t_raw[-1] / self.data.t_raw.shape[0]).item())
  196. groups = self.data.get_denormalized_data([prediction[:, i] for i in range(self.data.number_groups)])
  197. plot_labels = [name + '_pred' for name in self.data.group_names] + [name + '_true' for name in self.data.group_names]
  198. background_list = [0 for _ in self.data.group_names] + [1 for _ in self.data.group_names]
  199. self.plotter.plot(t,
  200. list(groups) + list(self.data.data),
  201. plot_labels,
  202. 'frame',
  203. f'epoch {epoch}',
  204. figure_shape=(12, 6),
  205. is_frame=True,
  206. is_background=background_list,
  207. lw=3,
  208. legend_loc='upper right',
  209. ylim=(0, self.data.N),
  210. xlabel='time / days',
  211. ylabel='amount of people')
  212. # print training advancements
  213. if epoch % 1000 == 0 and verbose:
  214. print(f'\nEpoch {epoch} | LR {self.scheduler.get_last_lr()[0]}')
  215. print(f'physics loss:\t\t{loss_physics.item()}')
  216. print(f'observation loss:\t{loss_obs.item()}')
  217. print(f'loss:\t\t\t{loss.item()}')
  218. print('---------------------------------')
  219. if len(self.parameters_tilda.items()) != 0:
  220. for parameter in self.parameters_tilda.items():
  221. print(f'{parameter[0]}:\t\t\t{self.parameter_regulator(parameter[1]).item()}')
  222. print('#################################')
  223. # create prediction animation
  224. if create_animation:
  225. self.plotter.animate(self.data.name + '_animation')
  226. self.plotter.reset_animation()
  227. self.__has_trained = True
  228. def plot_training_graphs(self, ground_truth=[]):
  229. """Plot the loss graph and the graphs of the advancements of the parameters.
  230. Args:
  231. ground_truth (list): List of the ground truth parameters
  232. """
  233. assert self.__has_trained, 'Model has to be trained, before plotting the training graphs'
  234. epochs = np.arange(0, self.epochs, 1)
  235. # plot loss
  236. self.plotter.plot(epochs, [self.losses, self.obs_losses, self.physics_losses], ['loss', 'observation loss', 'physics loss'], self.data.name + '_loss', 'Loss', (6, 6), y_log_scale=True, plot_legend=True, xlabel='epochs')
  237. # plot parameters
  238. for i, parameter in enumerate(self.parameters):
  239. if len(ground_truth) > i:
  240. self.plotter.plot(epochs,
  241. [parameter,
  242. np.ones_like(epochs) * ground_truth[i]],
  243. ['prediction', 'ground truth'],
  244. self.data.name + '_' + list(self.parameters_tilda.items())[i][0],
  245. list(self.parameters_tilda.items())[i][0], (6,6),
  246. is_background=[0, 1],
  247. xlabel='epochs')
  248. else:
  249. self.plotter.plot(epochs,
  250. [parameter],
  251. ['prediction'],
  252. self.data.name + '_' + list(self.parameters_tilda.items())[i][0],
  253. list(self.parameters_tilda.items())[i][0], (6,6),
  254. xlabel='epochs',
  255. plot_legend=False)
  256. def plot_state_variables(self):
  257. for i in range(self.data.number_groups, self.data.number_groups+self.number_state_variables):
  258. prediction = self.model(self.data.t_batch)
  259. groups = [prediction[:, i] for i in range(self.data.number_groups)]
  260. t = torch.arange(0, self.data.t_raw[-1].item(), (self.data.t_raw[-1] / self.data.t_raw.shape[0]).item())
  261. self.plotter.plot(t,
  262. [prediction[:, i]] + groups,
  263. [self.__state_variables[i-self.data.number_groups]] + self.data.group_names,
  264. f'{self.data.name}_{self.__state_variables[i-self.data.number_groups]}',
  265. self.__state_variables[i-self.data.number_groups],
  266. is_background=[0, 1, 1],
  267. figure_shape=(12, 6),
  268. plot_legend=True,
  269. xlabel='time / days')