dinn.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. import torch
  2. import os
  3. import imageio
  4. import numpy as np
  5. import matplotlib.pyplot as plt
  6. from matplotlib import rcParams
  7. from .dataset import PandemicDataset
  8. from .problem import PandemicProblem
  9. from .plotter import Plotter
  10. class DINN:
  11. class NN(torch.nn.Module):
  12. def __init__(self,
  13. output_size: int,
  14. input_size: int,
  15. hidden_size: int,
  16. hidden_layers: int,
  17. activation_layer) -> None:
  18. """Neural Network
  19. Args:
  20. output_size (int): number of outputs
  21. input_size (int): number of inputs
  22. hidden_size (int): number of hidden nodes per layer
  23. hidden_layers (int): number of hidden layers
  24. activation_layer (_type_): activation layer
  25. """
  26. super(DINN.NN, self).__init__()
  27. self.input = torch.nn.Sequential(torch.nn.Linear(input_size, hidden_size), activation_layer)
  28. self.hidden = torch.nn.Sequential(*[torch.nn.Sequential(torch.nn.Linear(hidden_size, hidden_size), activation_layer) for _ in range(hidden_layers)])
  29. self.output = torch.nn.Linear(hidden_size, output_size)
  30. def forward(self, t):
  31. x = self.input(t)
  32. x = self.hidden(x)
  33. x = self.output(x)
  34. return x
  35. def __init__(self,
  36. number_groups: int,
  37. data: PandemicDataset,
  38. parameter_list: list,
  39. problem: PandemicProblem,
  40. plotter: Plotter,
  41. parameter_regulator=torch.tanh,
  42. input_size=1,
  43. hidden_size=20,
  44. hidden_layers=7,
  45. activation_layer=torch.nn.ReLU()) -> None:
  46. """Desease Informed Neural Network. Uses the PandemicProblem, DINN.NN and PandemicDataset to solve Inverse Problems and find the
  47. parameters of a specific mathematical model.
  48. Args:
  49. number_groups (int): The number of groups, that the population is split into.
  50. data (PandemicDataset): Data collected showing the course of the pandemic
  51. parameter_list (list): List of the parameter names(strings), that are supposed to be found.
  52. problem (PandemicProblem): Problem class implementing the calculation of the residuals.
  53. parameter_regulator (optional): Function to force the parameters to be in a certain range. Defaults to torch.tanh.
  54. input_size (int, optional): Number of the input nodes of the NN. Defaults to 1.
  55. hidden_size (int, optional): Number of the hidden nodes of the NN. Defaults to 20.
  56. hidden_layers (int, optional): Number of the hidden layers for the NN. Defaults to 7.
  57. activation_layer (optional): Class of the activation function. Defaults to torch.nn.ReLU().
  58. """
  59. self.device = torch.device(data.device_name)
  60. self.device_name = data.device_name
  61. self.plotter = plotter
  62. self.model = DINN.NN(number_groups, input_size, hidden_size, hidden_layers, activation_layer)
  63. self.model = self.model.to(self.device)
  64. self.data = data
  65. self.parameter_regulator = parameter_regulator
  66. self.problem = problem
  67. self.parameters_tilda = {}
  68. for parameter in parameter_list:
  69. self.parameters_tilda.update({parameter : torch.nn.Parameter(torch.rand(1, requires_grad=True, device=self.device_name))})
  70. self.epochs = None
  71. self.losses = np.zeros(1)
  72. self.parameters = [np.zeros(1) for _ in range(len(parameter_list))]
  73. self.frames = []
  74. def get_regulated_param(self, parameter_name: str):
  75. """Function to get the searched parameters, forced into a certain range.
  76. Args:
  77. parameter_name (str): Name of the parameter to be returned.
  78. Returns:
  79. torch.Parameter: Regulated parameter object of the search parameter.
  80. """
  81. return self.parameter_regulator(self.parameters_tilda[parameter_name])
  82. def get_parameters_tilda(self):
  83. """Function to get the original value (not forced into any range).
  84. Returns:
  85. torch.Parameter: Parameter object of the search parameter.
  86. """
  87. return list(self.parameters_tilda.values())
  88. def get_regulated_param_list(self):
  89. """Get the list of regulated parameters (forced into a specific range).
  90. Returns:
  91. list: list of regulated parameters
  92. """
  93. return [self.parameter_regulator(parameter) for parameter in self.get_parameters_tilda()]
  94. def train(self,
  95. epochs: int,
  96. lr: float,
  97. optimizer_class=torch.optim.Adam,
  98. create_animation=False,
  99. animation_sample_rate=500):
  100. """Training routine for the DINN
  101. Args:
  102. epochs (int): Number of epochs the NN is supposed to be trained for.
  103. lr (float): Learning rate for the optimizer.
  104. optimizer_class (optional): Class of the optimizer. Defaults to torch.optim.Adam.
  105. create_animation (boolean, optional): Decides on wether a prediction animation is supposed to be created during training. Defaults to False.
  106. animation_sample_rate (int, optional): Sample rate of the prediction animation. Only used, when create_animation=True. Defaults to 500.
  107. """
  108. # define optimizer and scheduler
  109. optimizer = optimizer_class(list(self.model.parameters()) + list(self.parameters_tilda.values()), lr=lr)
  110. scheduler = torch.optim.lr_scheduler.CyclicLR(optimizer, base_lr=1e-5, max_lr=1e-3, step_size_up=1000, mode="exp_range", gamma=0.85, cycle_momentum=False)
  111. self.epochs = epochs
  112. # arrays to hold values for plotting
  113. self.losses = np.zeros(epochs)
  114. self.parameters = [np.zeros(epochs) for _ in self.parameters]
  115. for epoch in range(epochs):
  116. # get the prediction and the fitting residuals
  117. prediction = self.model(self.data.t_batch)
  118. residuals = self.problem.residual(prediction, *self.get_regulated_param_list())
  119. optimizer.zero_grad()
  120. # calculate loss from the differential system
  121. loss_physics = 0
  122. for residual in residuals:
  123. loss_physics += torch.mean(torch.square(residual))
  124. # calculate loss from the dataset
  125. loss_obs = 0
  126. for i, group in enumerate(self.data.get_group_names()):
  127. loss_obs += torch.mean(torch.square(self.data.get_norm(group) - prediction[:, i]))
  128. loss = loss_physics + loss_obs
  129. loss.backward()
  130. optimizer.step()
  131. scheduler.step()
  132. # append values for plotting
  133. self.losses[epoch] = loss.item()
  134. for i, parameter in enumerate(self.parameters_tilda.items()):
  135. self.parameters[i][epoch] = self.get_regulated_param(parameter[0]).item()
  136. # do snapshot for prediction animation
  137. if epoch % animation_sample_rate == 0 and create_animation:
  138. # prediction
  139. prediction = self.model(self.data.t_batch)
  140. t = torch.arange(0, self.data.t_raw[-1].item(), (self.data.t_raw[-1] / self.data.t_raw.shape[0]).item())
  141. groups = self.problem.denormalization(prediction)
  142. self.plotter.plot(t,
  143. groups + tuple(self.data.get_data()),
  144. [name + '_pred' for name in self.data.get_group_names()] + [name + '_true' for name in self.data.get_group_names()],
  145. 'frame',
  146. f'epoch {epoch}',
  147. figure_shape=(12, 6),
  148. is_frame=True,
  149. is_background=[0, 0, 0, 1, 1, 1],
  150. lw=3,
  151. legend_loc='upper right',
  152. ylim=(0, self.data.N),
  153. xlabel='time / days',
  154. ylabel='amount of people')
  155. # print training advancements
  156. if epoch % 1000 == 0:
  157. print('\nEpoch ', epoch)
  158. print(f'physics loss:\t\t{loss_physics.item()}')
  159. print(f'observation loss:\t{loss_obs.item()}')
  160. print(f'loss:\t\t\t{loss.item()}')
  161. print('---------------------------------')
  162. for parameter in self.parameters_tilda.items():
  163. print(f'{parameter[0]}:\t\t\t{self.parameter_regulator(parameter[1]).item()}')
  164. print('#################################')
  165. # create prediction animation
  166. if create_animation:
  167. self.plotter.animate(self.data.name + '_animation')
  168. def plot_training_graphs(self, ground_truth=[]):
  169. """Plot the loss graph and the graphs of the advancements of the parameters.
  170. Args:
  171. ground_truth (list): List of the ground truth parameters
  172. """
  173. assert self.epochs != None
  174. epochs = np.arange(0, self.epochs, 1)
  175. # plot loss
  176. self.plotter.plot(epochs, [self.losses], ['loss'], self.data.name + '_loss', 'Loss', (6, 6), y_log_scale=True, plot_legend=False, xlabel='epochs')
  177. # plot parameters
  178. for i, parameter in enumerate(self.parameters):
  179. if len(ground_truth) > i:
  180. self.plotter.plot(epochs, [parameter, np.ones_like(epochs) * ground_truth[i]], ['prediction', 'ground truth'], self.data.name + '_' + list(self.parameters_tilda.items())[i][0], list(self.parameters_tilda.items())[i][0], (6,6), is_background=[0, 1], xlabel='epochs')
  181. else:
  182. self.plotter.plot(epochs, [parameter], ['prediction'], self.data.name + '_' + list(self.parameters_tilda.items())[i][0], list(self.parameters_tilda.items())[i][0], (6,6), xlabel='epochs', plot_legend=False)