-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathdynamics_model.py
More file actions
553 lines (467 loc) · 22.6 KB
/
Copy pathdynamics_model.py
File metadata and controls
553 lines (467 loc) · 22.6 KB
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
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
import sys
import warnings
import os
import torch
import numpy as np
from torch.autograd import Variable
import torch.nn as nn
import torch.nn.functional as F
import torch.backends.cudnn as cudnn
from collections import OrderedDict
import hydra
import math
import GPy
from omegaconf import OmegaConf
class GP(object):
def __init__(self, n_in, n_out, cfg, loss_fn, env="Reacher", tf=nn.ReLU()):
self.name = 'GP' # Default value
self.probabilistic = True # Default value
# self.verbosity = parameters.get('verbosity', 3)
# self.indent = parameters.get('indent', 0)
self.n_inputs = n_in
self.n_outputs = n_out
self.kernel = 'Matern52'
self.ARD = True
self.fixNoise = None
self.normalizeOutput = False
self.t_output = None # Store the output transformation
self._kernel = []
self._model = []
self._logs = []
self._startTime = None
def optimize(self, dataset, cfg):
# logging.info('Training GP')
# self._startTime = timer()
# if self.normalizeOutput is True:
# self.t_output = train_set.normalize_output()
# self.n_inputs = train_set.get_dim_input()
# self.n_outputs = train_set.get_dim_output()
# logging.info('Dataset %d -> %d with %d data' % (self.n_inputs, self.n_outputs, train_set.get_n_data()))
if 0 < cfg.model.optimizer.max_size < len(dataset[0]):
use = np.random.randint(0, len(dataset[0]), cfg.model.optimizer.max_size)
d = []
d.append(dataset[0][use])
d.append(dataset[1][use])
for i in range(self.n_outputs):
# logging.info('Training covariate %d (of %d)' % (i+1, self.n_outputs))
print('Training covariate %d (of %d)' % (i + 1, self.n_outputs))
if self.kernel == 'Matern52':
self._kernel.append(GPy.kern.Matern52(input_dim=self.n_inputs, ARD=self.ARD))
if self.kernel == 'Linear':
self._kernel.append(GPy.kern.Linear(input_dim=self.n_inputs, ARD=self.ARD))
# TODO check this line
self._model.append(GPy.models.GPRegression(d[0], d[1][:, 1].reshape(-1, 1), kernel=self._kernel[i]))
if self.fixNoise is not None:
self._model[i].likelihood.variance.fix(self.fixNoise)
self._model[i].optimize_restarts(num_restarts=10, verbose=False) # , parallel=True, num_processes=5
return 0, 0
#
# end = timer()
# logging.info('Training completed in %f[s]' % (end - self._startTime))
def forward(self, x):
n_data = np.shape(x)[0]
mean = np.zeros((n_data, self.n_outputs))
var = np.zeros((n_data, self.n_outputs))
for i in range(self.n_outputs):
t_mean, t_var = self._model[i].predict(np.array(x))
mean[:, i] = t_mean.T
var[:, i] = t_var.T
if np.any(var < 0):
# logging.warning('Variance was negative! Now it is 0, but you should be careful!')
var[var < 0] = 0 # Make sure that variance is always positive
return np.concatenate((mean, var))
def get_hyperparameters(self):
return self._model._param_array_
class Net(nn.Module):
"""
General Neural Network
"""
def __init__(self, n_in, n_out, cfg, loss_fn, env="Reacher", tf=nn.ReLU()):
"""
:param structure: layer sizes
:param tf: nonlinearity function
"""
super(Net, self).__init__()
self.activation = tf
self._onGPU = False
self.loss_fn = loss_fn
self.n_in = n_in
self.n_out = n_out
self.hidden_w = cfg.model.training.hid_width
self.cfg = cfg
self.trained = False
if cfg.model.lstm is not None:
if cfg.model.lstm:
self.is_lstm = True
else:
self.is_lstm = False
else:
self.is_lstm = False
if env == "Reacher":
self.state_indices = cfg.model.training.state_indices
elif env == "Lorenz":
self.state_indices = cfg.model.training.state_indices_lorenz
else:
self.state_indices = np.arange(n_in)
# create object nicely
if cfg.model.lstm is not None: # self.is_lstm:
# The LSTM takes word embeddings as inputs, and outputs hidden states
# with dimensionality hidden_dim.
# https://ieeexplore-ieee-org.libproxy.berkeley.edu/stamp/stamp.jsp?tp=&arnumber=8461076
if self.is_lstm:
if cfg.model.str == "rnn":
self.lstm = nn.RNN(n_in, self.hidden_w)
else:
self.lstm = nn.LSTM(n_in, self.hidden_w)
# The linear layer that maps from hidden state space to tag space
self.hidden2tag = nn.Linear(self.hidden_w, n_out)
for name, param in self.named_parameters():
if 'bias' in name:
nn.init.uniform_(param, -.08, 0.08)
elif 'weight' in name:
nn.init.uniform_(param, -.08, 0.08)
else:
layers = []
layers.append(('dynm_input_lin', nn.Linear(self.n_in, self.hidden_w)))
layers.append(('dynm_input_act', self.activation))
for d in range(cfg.model.training.hid_depth):
layers.append(('dynm_lin_' + str(d), nn.Linear(self.hidden_w, self.hidden_w)))
layers.append(('dynm_act_' + str(d), self.activation))
layers.append(('dynm_out_lin', nn.Linear(self.hidden_w, self.n_out)))
self.features = nn.Sequential(OrderedDict([*layers]))
else:
layers = []
layers.append(('dynm_input_lin', nn.Linear(self.n_in, self.hidden_w)))
layers.append(('dynm_input_act', self.activation))
for d in range(cfg.model.training.hid_depth):
layers.append(('dynm_lin_' + str(d), nn.Linear(self.hidden_w, self.hidden_w)))
layers.append(('dynm_act_' + str(d), self.activation))
layers.append(('dynm_out_lin', nn.Linear(self.hidden_w, self.n_out)))
self.features = nn.Sequential(OrderedDict([*layers]))
def forward(self, x, num_traj=1):
"""
Runs a forward pass of x through this network
"""
if type(x) == np.ndarray:
x = torch.from_numpy(x).float()
if x.dtype == torch.double:
x = x.float()
OmegaConf.set_struct(self.cfg.model, False)
if self.cfg.model.lstm is not None:
if self.cfg.model.lstm:
# if self.trained:
lstm_out, _ = self.lstm(x.view(len(x), num_traj, -1).float())
# else:
x = self.hidden2tag(lstm_out.view(len(x), num_traj, -1))
else:
x = self.features(x.float())
else:
x = self.features(x.float())
return x
def testPreprocess(self, input, cfg):
if (cfg.model.traj):
inputStates = input[:, :len(self.state_indices)]
inputIndex = input[:, len(self.state_indices)]
inputParams = input[:, len(self.state_indices) + 1:]
inputIndex = inputIndex.reshape(-1, 1)
normStates = self.stateScaler.transform(inputStates)
normIndex = self.indexScaler.transform(inputIndex)
if inputParams.shape[-1] == 0:
return np.hstack((normStates, normIndex))
else:
normParams = self.paramScaler.transform(inputParams)
return np.hstack((normStates, normIndex, normParams))
else:
inputStates = input[:, :len(self.state_indices)]
normStates = self.stateScaler.transform(inputStates)
if cfg.env.action_size > 0:
inputActions = input[:, len(self.state_indices):]
normActions = self.actionScaler.transform(inputActions)
normInput = np.hstack((normStates, normActions))
return normInput
else:
normInput = normStates
return normInput
def testPostprocess(self, output):
return torch.from_numpy(self.outputScaler.inverse_transform(output.detach().numpy()))
def preprocess(self, dataset, cfg):
# Select scaling, minmax vs standard (fits to a gaussian with unit variance and 0 mean)
# TODO: Selection should be in config
# StandardScaler, MinMaxScaler
# TODO: Instead of hardcoding, include in config, trajectory vs. one-step, length of different inputs, etc.
# 26 -> one-step, 37 -> trajectory
input = dataset[0]
output = dataset[1]
if cfg.model.traj:
# no control params (state + time index)
if np.shape(dataset[0])[1] == len(self.state_indices) + 1:
self.stateScaler = hydra.utils.instantiate(cfg.model.preprocess.state)
self.indexScaler = hydra.utils.instantiate(cfg.model.preprocess.index)
self.outputScaler = hydra.utils.instantiate(cfg.model.preprocess.output)
inputStates = input[:, :len(self.state_indices)]
inputIndex = input[:, len(self.state_indices)]
# reshape index for one feature length
inputIndex = inputIndex.reshape(-1, 1)
self.stateScaler.fit(inputStates)
self.indexScaler.fit(inputIndex)
self.outputScaler.fit(output)
normStates = self.stateScaler.transform(inputStates)
normIndex = self.indexScaler.transform(inputIndex)
normOutput = self.outputScaler.transform(output)
normInput = np.hstack((normStates, normIndex))
# control params
else:
self.stateScaler = hydra.utils.instantiate(cfg.model.preprocess.state)
self.indexScaler = hydra.utils.instantiate(cfg.model.preprocess.index)
self.paramScaler = hydra.utils.instantiate(cfg.model.preprocess.param)
self.outputScaler = hydra.utils.instantiate(cfg.model.preprocess.output)
inputStates = input[:, :len(self.state_indices)]
inputIndex = input[:, len(self.state_indices)]
inputParams = input[:, len(self.state_indices) + 1:]
# reshape index for one feature length
inputIndex = inputIndex.reshape(-1, 1)
self.stateScaler.fit(inputStates)
self.indexScaler.fit(inputIndex)
self.paramScaler.fit(inputParams)
self.outputScaler.fit(output)
normStates = self.stateScaler.transform(inputStates)
normIndex = self.indexScaler.transform(inputIndex)
normParams = self.paramScaler.transform(inputParams)
normOutput = self.outputScaler.transform(output)
normInput = np.hstack((normStates, normIndex, normParams))
return list(zip(normInput, normOutput))
else:
self.stateScaler = hydra.utils.instantiate(cfg.model.preprocess.state)
self.actionScaler = hydra.utils.instantiate(cfg.model.preprocess.action)
self.outputScaler = hydra.utils.instantiate(cfg.model.preprocess.output)
inputStates = input[:, :len(self.state_indices)]
inputActions = input[:, len(self.state_indices):]
self.stateScaler.fit(inputStates)
self.outputScaler.fit(output)
normStates = self.stateScaler.transform(inputStates)
normOutput = self.outputScaler.transform(output)
if np.shape(inputActions)[1] > 0:
self.actionScaler.fit(inputActions)
normActions = self.actionScaler.transform(inputActions)
normInput = np.hstack((normStates, normActions))
else:
normInput = normStates
return list(zip(normInput, normOutput))
def optimize(self, dataset, cfg):
"""
Uses dataset to train this net according to the parameters in cfg
Returns:
train_errors: a list of average errors for each epoch on the training data
test_errors: a list of average errors for each epoch on the test data
"""
from torch.utils.data import DataLoader
# Extract parameters from cfg
lr = cfg.model.optimizer.lr
bs = cfg.model.optimizer.batch
split = cfg.model.optimizer.split
epochs = cfg.model.optimizer.epochs
t_range = cfg.model.training.t_range
# Set up the optimizer and scheduler
# TODO: the scheduler is currently unused. Should it be doing something it isn't or removed?
if self.is_lstm:
optimizer = torch.optim.Adam(self.parameters(), lr=lr, weight_decay=cfg.model.optimizer.regularization)
else:
optimizer = torch.optim.Adam(self.features.parameters(), lr=lr,
weight_decay=cfg.model.optimizer.regularization)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=6, gamma=0.7)
# data preprocessing for normalization
dataset = self.preprocess(dataset, cfg)
if 0 < cfg.model.optimizer.max_size < len(dataset):
import random
if not self.is_lstm:
dataset = random.sample(dataset, cfg.model.optimizer.max_size)
else:
# lstm must be batched by sequence length
divisible_max_size = int((len(dataset) - cfg.model.optimizer.max_size) / bs) * bs
dataset = dataset[:divisible_max_size]
# Puts it in PyTorch dataset form and then converts to DataLoader
if self.is_lstm:
num_sequences = int(len(dataset) / bs)
sequence_split = int(split * num_sequences)
trainLoader = DataLoader(dataset[:int(sequence_split * bs)], batch_size=bs, shuffle=False)
testLoader = DataLoader(dataset[int(sequence_split * bs):], batch_size=bs, shuffle=False)
else:
trainLoader = DataLoader(dataset[:int(split * len(dataset))], batch_size=bs, shuffle=True)
testLoader = DataLoader(dataset[int(split * len(dataset)):], batch_size=bs, shuffle=True)
# Optimization loop
train_errors = []
test_errors = []
for epoch in range(epochs):
train_error = 0
test_error = 0
# Iterate through dataset and take gradient descent steps
for i, (inputs, targets) in enumerate(trainLoader):
optimizer.zero_grad()
outputs = self.forward(inputs)
loss = self.loss_fn(outputs.float(), targets.float())
train_error += loss.item() / (len(trainLoader))
loss.backward()
optimizer.step() # Does the update
# Iterate through dataset to calculate test set accuracy
# test_error = torch.zeros(1)
for i, (inputs, targets) in enumerate(testLoader):
outputs = self.forward(inputs)
loss = self.loss_fn(outputs.float(), targets.float())
test_error += loss.item() / (len(testLoader))
print(f" Epoch {epoch + 1}, Train err: {train_error}, Test err: {test_error}")
train_errors.append(train_error)
test_errors.append(test_error)
self.trained = True
return train_errors, test_errors
class DynamicsModel(object):
"""
Wrapper class for a general dynamics model.
The model is an ensemble of neural nets. For cases where the model should not be an ensemble it is just
an ensemble of 1 net.
"""
def __init__(self, cfg, env="Reacher"):
self.str = cfg.model.str
self.ens = cfg.model.ensemble
self.traj = cfg.model.traj
self.prob = cfg.model.prob
self.delta = cfg.model.delta
self.train_target = cfg.model.training.train_target
self.control_params = cfg.model.training.control_params
if env == "Reacher":
self.state_indices = cfg.model.training.state_indices
elif env == "Lorenz" or env == "SS":
self.state_indices = cfg.model.training.state_indices_lorenz
self.cfg = cfg
# Setup for data structure
if self.ens:
self.E = cfg.model.training.E
else:
self.E = 1
self.n_in = len(self.state_indices)
if self.traj:
self.n_in += 1
if self.control_params:
self.n_in += cfg.env.param_size - cfg.env.target_size
if self.train_target:
self.n_in += cfg.env.target_size
else:
self.n_in += cfg.env.action_size
self.n_out = len(self.state_indices)
if self.prob:
# ordering matters here, because size is the number of predicted output states
self.loss_fn = ProbLoss(self.n_out)
self.n_out = self.n_out * 2
else:
self.loss_fn = nn.MSELoss()
if env == "Reacher":
if cfg.model.gp:
self.nets = [GP(self.n_in, self.n_out, cfg, self.loss_fn) for i in range(self.E)]
else:
self.nets = [Net(self.n_in, self.n_out, cfg, self.loss_fn) for i in range(self.E)]
elif env == "Lorenz" or env == "SS":
self.nets = [Net(self.n_in, self.n_out, cfg, self.loss_fn, env="Lorenz") for i in range(self.E)]
def predict_lstm(self, x, num_traj=1):
# LSTM takes in a variable length object and predicts the next in the future.
if type(x) == np.ndarray:
x = torch.from_numpy(np.float64(x))
prediction = torch.zeros((x.shape[0], num_traj, len(self.state_indices)))
assert np.ndim(x) == 3, "Not correct num of dim for LSTM (seq_len, batch, input_size)"
for n in self.nets:
x = np.stack([n.testPreprocess(sub, self.cfg) for sub in x])
scaledInput = x
out = n.forward(scaledInput, num_traj=num_traj)
out = np.stack([n.testPostprocess(o) for o in out])
prediction += out / len(self.nets)
return prediction[:, :, :]
def predict(self, x):
"""
Use the model to predict values with x as input
TODO: Fix hardcoding in this method
TODO: particle sampling approach for probabilistic model
"""
if type(x) == np.ndarray:
x = torch.from_numpy(np.float64(x))
prediction = torch.zeros((x.shape[0], len(self.state_indices)))
for n in self.nets:
scaledInput = n.testPreprocess(x, self.cfg)
if self.prob:
prediction += n.testPostprocess(n.forward(scaledInput)[:, :len(self.state_indices)]) / len(self.nets)
# prediction += n.forward(scaledInput)[:, :len(self.state_indices)] / len(self.nets)
else:
prediction += n.testPostprocess(n.forward(scaledInput)) / len(self.nets)
# prediction += n.forward(scaledInput) / len(self.nets)
if not self.delta:
return prediction[:, :len(self.state_indices)]
else:
# This hardcode is the state size changing. X also includes the action / index
return x[:, :len(self.state_indices)] + prediction
def train(self, dataset, cfg):
acctest_l = []
acctrain_l = []
# The purpose of this line is to reform the dataset to use only the state indices requested
if not self.train_target and not self.control_params:
dataset = (np.hstack((dataset[0][:, self.state_indices],
# dataset[0][:, (self.cfg.env.state_size - len(self.state_indices)):])),
dataset[0][:, [self.cfg.env.state_size]])),
dataset[1][:, self.state_indices])
else:
dataset = (np.hstack((dataset[0][:, self.state_indices],
# dataset[0][:, (self.cfg.env.state_size - len(self.state_indices)):])),
dataset[0][:, self.cfg.env.state_size:])),
dataset[1][:, self.state_indices])
if self.ens:
from sklearn.model_selection import KFold # for dataset
# setup cross validation-ish datasets for training ensemble
kf = KFold(n_splits=self.E)
kf.get_n_splits(dataset)
# iterate through the validation sets
for (i, n), (train_idx, test_idx) in zip(enumerate(self.nets), kf.split(dataset[0])):
print(" Model %d" % (i + 1))
# only train on training data to ensure diversity
# sub_data = (dataset[0][train_idx], dataset[1][train_idx])
# num = len(dataset[0])
# k = int(num * .8)
# train_idx = np.random.choice(num, k)
sub_data = (dataset[0][train_idx], dataset[1][train_idx])
print('test ' + str(len(sub_data[0])))
train_e, test_e = n.optimize(sub_data, cfg)
acctrain_l.append(train_e)
acctest_l.append(test_e)
else:
train_e, test_e = self.nets[0].optimize(dataset, cfg)
acctrain_l.append(train_e)
acctest_l.append(test_e)
self.acctrain, self.acctest = acctrain_l, acctest_l
return acctrain_l, acctest_l
class ProbLoss(nn.Module):
"""
Class for probabilistic loss function
"""
def __init__(self, size):
super(ProbLoss, self).__init__()
self.size = size
self.max_logvar = torch.nn.Parameter(
torch.tensor(1 * np.ones([1, size]), dtype=torch.float, requires_grad=True))
self.min_logvar = torch.nn.Parameter(
torch.tensor(-1 * np.ones([1, size]), dtype=torch.float, requires_grad=True))
def softplus_raw(self, input):
# Performs the elementwise softplus on the input
# softplus(x) = 1/B * log(1+exp(B*x))
B = torch.tensor(1, dtype=torch.float)
return (torch.log(1 + torch.exp(input.mul_(B)))).div_(B)
def forward(self, inputs, targets):
# size = targets.size()[1]
mean = inputs[:, :self.size]
logvar = inputs[:, self.size:]
# Caps max and min log to avoid NaNs
# logvar = self.max_logvar - self.softplus_raw(self.max_logvar - logvar)
# logvar = self.min_logvar + self.softplus_raw(logvar - self.min_logvar)
logvar = torch.min(logvar, self.max_logvar)
logvar = torch.max(logvar, self.min_logvar)
var = torch.exp(logvar)
diff = mean - targets
mid = torch.div(diff, var)
lg = torch.sum(torch.log(var))
out = torch.trace(torch.mm(diff, mid.t())) + lg
# same as torch.sum(((mean - targets) ** 2) / var) + lg
return out