-
Notifications
You must be signed in to change notification settings - Fork 0
/
model.py
336 lines (277 loc) · 12.1 KB
/
model.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# -*- coding: utf-8 -*-
"""
Created on Mon Jun 12 17:30:08 2023
@author: Tommaso Giacometti
"""
import torch
from torch import nn, Tensor
from torch.autograd import grad
from numpy.typing import ArrayLike
#Neural Network structure for the PINN for the direct problem
class PINN(nn.Module):
def __init__(self):
super().__init__()
self.seq = nn.Sequential(
nn.Linear(1, 64), nn.Tanh(), # In PINN, dropout, can NOT be use
nn.Linear(64, 32), nn.Tanh(), # and the activation function must have a smooth derivative
nn.Linear(32, 16), nn.Tanh(), # so ReLU doesn't work
nn.Linear(16, 4)
)
def forward(self, inp):
return self.seq(inp)
def add_pde_parameters(self, params : ArrayLike) -> None:
'''
Add the parameters of the pde as buffer to the neural network structure
Parameters
----------
params : ArrayLike
numpy array containing the parameters
Returns
-------
None
'''
assert len(params) == 4
params = torch.from_numpy(params).float()
self.register_buffer('params', params)
pass
def add_initial_condition(self, init : ArrayLike, t_ic_zero = None) -> None:
'''
Add the initial condition as buffers to the network structure.
If t_in_zero is none the initial time will be automatically set to zero.
Parameters
----------
init : ArrayLike
numpy array of the initial condition for the four states
t_ic_zero : optional
The default is None.
Returns
-------
None
'''
assert len(init) == 4
if t_ic_zero is None:
self.register_buffer('t_ic', torch.zeros(1, dtype=torch.float, requires_grad=True))
else:
self.register_buffer('t_ic', torch.tensor(t_ic_zero, dtype=torch.float, requires_grad=True))
init = torch.from_numpy(init).float()
self.register_buffer('init', init)
pass
def train_step(self, t : Tensor, optimizer, loss_fn = None, lbfgs : bool = False) -> float:
'''
Perform a step for the training.
Parameters
----------
t : torch.Tensor
Time tensor for the domain. It must have the shape : Nx1 where N is the number of training points.
optimizer : torch.optim
Optimizer to use for the training.
loss_fn : torch.nn
Loss function to use for the training, by default (None) MSELoss will be used.
lbfgs : bool, optional
Parameter to use this function as closure() for the LBFGS optimizer training. DEFAULT False,
set to True ONLY for the LBFGS otherwise the training will fail.
Returns
-------
Tensor
The loss of the training step as Tensor.
'''
if loss_fn is None:
loss_fn = nn.MSELoss()
self.train()
optimizer.zero_grad()
# Forward pass for the initial conditions
out_ic = self(self.t_ic)
loss_ic = loss_fn(out_ic, self.init)
# Forwward pass in time domain
out = self(t)
x1 = out[:, 0].view(-1, 1)
x2 = out[:, 1].view(-1, 1)
y1 = out[:, 2].view(-1, 1)
z = out[:, 3].view(-1, 1)
grad_out = torch.ones_like(x1) # Define the starting gradient for backprop. using autograd.grad
# Compute gradients
dx1 = grad(x1, t, grad_outputs=grad_out, create_graph=True)[0]
dx2 = grad(x2, t, grad_outputs=grad_out, create_graph=True)[0]
dy1 = grad(y1, t, grad_outputs=grad_out, create_graph=True)[0]
dz = grad(z, t, grad_outputs=grad_out, create_graph=True)[0]
# Compute partial differential equation (PDE) and their losses
pde_x1 = 0. - dx1
pde_x2 = self.params[0] * x1 + (self.params[0] - self.params[1]) * x2 - dx2
pde_y1 = self.params[1] * x2 - self.params[2] * y1 - dy1
pde_z = 2 * self.params[2] * y1 - self.params[3] * z - dz
loss_pdex1 = loss_fn(pde_x1, torch.zeros_like(dx1))
loss_pdex2 = loss_fn(pde_x2, torch.zeros_like(pde_x2))
loss_pdey1 = loss_fn(pde_y1, torch.zeros_like(pde_y1))
loss_pdez = loss_fn(pde_z, torch.zeros_like(pde_z))
# Total loss and optim step
loss = loss_pdex1 + loss_pdex2 + loss_pdey1 + loss_pdez + loss_ic
loss.backward()
if not lbfgs: # In the LBFGS, the optimizer step is permormed in the LBFGS class
optimizer.step()
return loss # Return the computed loss as tensor
def set_up_lbfgs(self, t : Tensor):
self.register_buffer('time', t)
optimizer = torch.optim.LBFGS(self.parameters())
self.optimizer = optimizer
self.loss_fn = nn.MSELoss()
def closure():
return self.train_step(self.time, self.optimizer, self.loss_fn, lbfgs = True)
return optimizer, closure
#Neural Network structure for the PINN for the inverse problem
#In this case we want to infer 2 parameters: lambda and gamma.
class PINN_inverse(nn.Module):
def __init__(self):
super().__init__()
self.seq = nn.Sequential(
nn.Linear(1, 64), nn.Tanh(), # In PINN, dropout, can NOT be use
nn.Linear(64, 32), nn.Tanh(), # and the activation function must have a smooth derivative
nn.Linear(32, 16), nn.Tanh(), # so ReLU doesn't work
nn.Linear(16, 4)
)
def forward(self, inp):
return self.seq(inp)
def add_pde_parameters_known(self, params : ArrayLike) -> None:
'''
Add the parameters of the pde as buffer to the neural network structure.
In this case the parameters must be nu and delta
Parameters
----------
params : ArrayLike
numpy array containing the parameters nu and delta
Returns
-------
None
'''
assert len(params) == 2
params = torch.from_numpy(params).float()
self.register_buffer('params_known', params)
pass
def add_pde_parameters_to_infer(self, params : ArrayLike) -> None:
'''
Add the parameters of the pde to infer as torch.Parameters to the neural network structure.
In this case the parameters must be lambda (lam) and gamma.
Parameters
----------
params : ArrayLike
numpy array containing the parameters lambda and gamma
Returns
-------
None
'''
assert len(params) == 2
params = torch.from_numpy(params).float()
params = nn.Parameter(params)
self.register_parameter('params_to_infer', params)
pass
def add_initial_condition(self, init : ArrayLike, t_ic_zero = None) -> None:
'''
Add the initial condition as buffers to the network structure.
If t_in_zero is none the initial time will be automatically set to zero.
Parameters
----------
init : ArrayLike
numpy array of the initial condition for the four states
t_ic_zero : optional
The default is None.
Returns
-------
None
'''
assert len(init) == 4
if t_ic_zero is None:
self.register_buffer('t_ic', torch.zeros(1, dtype=torch.float, requires_grad=True))
else:
self.register_buffer('t_ic', torch.tensor(t_ic_zero, dtype=torch.float, requires_grad=True))
init = torch.from_numpy(init).float()
self.register_buffer('init', init)
pass
def train_step(self, t : Tensor, data : Tensor, optimizer, loss_fn = None, lbfgs : bool = False) -> float:
'''
Perform a step for the training.
Parameters
----------
t : torch.Tensor
Time tensor for the domain. It must have the shape : Nx1 where N is the number of training points.
data : torch.Tensor
data generated for the fit of the parameters. In the first column must be the time
while in the last four the generated solution.
optimizer : torch.optim
Optimizer to use for the training.
loss_fn : torch.nn
Loss function to use for the training, by default (None) MSELoss will be used.
lbfgs : bool, optional
Parameter to use this function as closure() for the LBFGS optimizer training. DEFAULT False,
set to True ONLY for the LBFGS otherwise the training will fail.
Returns
-------
Tensot
The loss of the training step as tensor.
'''
if loss_fn is None:
loss_fn = nn.MSELoss()
self.train()
optimizer.zero_grad()
# Forward pass for the initial conditions
out_ic = self(self.t_ic)
loss_ic = loss_fn(out_ic, self.init)
# Forwward pass in time domain
out = self(t)
x1 = out[:, 0].view(-1, 1)
x2 = out[:, 1].view(-1, 1)
y1 = out[:, 2].view(-1, 1)
z = out[:, 3].view(-1, 1)
grad_out = torch.ones_like(x1) # Define the starting gradient for backprop. using autograd.grad
# Compute gradients
dx1 = grad(x1, t, grad_outputs=grad_out, create_graph=True)[0]
dx2 = grad(x2, t, grad_outputs=grad_out, create_graph=True)[0]
dy1 = grad(y1, t, grad_outputs=grad_out, create_graph=True)[0]
dz = grad(z, t, grad_outputs=grad_out, create_graph=True)[0]
# Compute partial differential equation (PDE) and their losses
pde_x1 = 0. - dx1
pde_x2 = self.params_to_infer[0] * x1 + (self.params_to_infer[0] - self.params_known[0]) * x2 - dx2
pde_y1 = self.params_known[0] * x2 - self.params_to_infer[1] * y1 - dy1
pde_z = 2 * self.params_to_infer[1] * y1 - self.params_known[0] * z - dz
loss_pdex1 = loss_fn(pde_x1, torch.zeros_like(dx1))
loss_pdex2 = loss_fn(pde_x2, torch.zeros_like(pde_x2))
loss_pdey1 = loss_fn(pde_y1, torch.zeros_like(pde_y1))
loss_pdez = loss_fn(pde_z, torch.zeros_like(pde_z))
# Data loss
data_t = data[:,0:1] #selection of the time column
data_points = data[:,1:] #selection of the generated solution (all the four variables)
out_data = self(data_t)
loss_data = loss_fn(out_data[:,2:4], data_points[:,2:4]) #using only the generated solution of y1 and z
# Total loss and optim step
loss = loss_pdex1 + loss_pdex2 + loss_pdey1 + loss_pdez + loss_ic + 0.5*loss_data # The 0.5 factor is to get smoother solution and avoid overfitting with datapoints
loss.backward()
if not lbfgs: # In the LBFGS, the optimizer step is permormed in the LBFGS class
optimizer.step()
return loss # Return the computed loss as tensor
def set_up_lbfgs(self, t : Tensor, data : Tensor):
'''
Preparing the network for the lbfgs training.\n
The lbfgs optimizer need a closure function, in this case is recycled the train_step() function
adapted for the lgsbf train (optioin lbfgs=True).
Parameters
----------
t : Tensor
time Tensor where to evaluate the pde loss.
data : Tensor
data with in the first column the time points and in the last 4 the generated data.
Returns
-------
optimizer
LBFGS optimizer ready for the use.
TYPE
closure function needed by the LBFGS.
'''
self.register_buffer('time', t)
self.register_buffer('data', data)
#With noisy data lbfgs is likely to return nan result due to convergence problem
#for this reason I reduced the history size, but it can't always solve the problem.
#If the problem persist the lbfgs training can be simpli avoided
optimizer = torch.optim.LBFGS(self.parameters(), history_size=10)
self.optimizer = optimizer
self.loss_fn = nn.MSELoss()
def closure():
return self.train_step(self.time, self.data, self.optimizer, self.loss_fn, lbfgs = True)
return optimizer, closure