https://mooseframework.inl.gov
SurrogateTrainer.C
Go to the documentation of this file.
1 //* This file is part of the MOOSE framework
2 //* https://mooseframework.inl.gov
3 //*
4 //* All rights reserved, see COPYRIGHT for full restrictions
5 //* https://github.com/idaholab/moose/blob/master/COPYRIGHT
6 //*
7 //* Licensed under LGPL 2.1, please see LICENSE for details
8 //* https://www.gnu.org/licenses/lgpl-2.1.html
9 
10 #include "SurrogateTrainer.h"
11 #include "SurrogateModel.h"
12 #include "Sampler.h"
13 #include "StochasticToolsApp.h"
14 #include "MooseRandom.h"
15 #include "Shuffle.h"
16 #include <algorithm>
17 
20 {
23  params.registerBase("SurrogateTrainer");
24  return params;
25 }
26 
28  : GeneralUserObject(parameters),
29  RestartableModelInterface(*this, /*read_only=*/false, _type + "_" + name())
30 {
31 }
32 
35 {
37  params.addRequiredParam<SamplerName>("sampler",
38  "Sampler used to create predictor and response data.");
39  params.addParam<ReporterName>(
40  "converged_reporter",
41  "Reporter value used to determine if a sample's multiapp solve converged.");
42  params.addParam<bool>("skip_unconverged_samples",
43  false,
44  "True to skip samples where the multiapp did not converge, "
45  "'stochastic_reporter' is required to do this.");
46 
47  // Common Training Data
48  MooseEnum data_type("real=0 vector_real=1", "real");
50  "response",
51  "Reporter value of response results, can be vpp with <vpp_name>/<vector_name> or sampler "
52  "column with 'sampler/col_<index>'.");
53  params.addParam<MooseEnum>("response_type", data_type, "Response data type.");
54  params.addParam<std::vector<ReporterName>>(
55  "predictors",
56  std::vector<ReporterName>(),
57  "Reporter values used as the independent random variables, If 'predictors' and "
58  "'predictor_cols' are both empty, all sampler columns are used.");
59  params.addParam<std::vector<unsigned int>>(
60  "predictor_cols",
61  std::vector<unsigned int>(),
62  "Sampler columns used as the independent random variables, If 'predictors' and "
63  "'predictor_cols' are both empty, all sampler columns are used.");
64  // End Common Training Data
65 
66  MooseEnum cv_type("none=0 k_fold=1", "none");
67  params.addParam<MooseEnum>(
68  "cv_type",
69  cv_type,
70  "Cross-validation method to use for dataset. Options are 'none' or 'k_fold'.");
71  params.addRangeCheckedParam<unsigned int>(
72  "cv_splits", 10, "cv_splits > 1", "Number of splits (k) to use in k-fold cross-validation.");
73  params.addParam<UserObjectName>("cv_surrogate",
74  "Name of Surrogate object used for model cross-validation.");
75  params.addParam<unsigned int>(
76  "cv_n_trials", 1, "Number of repeated trials of cross-validation to perform.");
77  params.addParam<unsigned int>("cv_seed",
78  std::numeric_limits<unsigned int>::max(),
79  "Seed used to initialize random number generator for data "
80  "splitting during cross validation.");
81 
82  return params;
83 }
84 
86  : SurrogateTrainerBase(parameters),
88  _sampler(getSampler("sampler")),
89  _rval(nullptr),
90  _rvecval(nullptr),
91  _pvals(getParam<std::vector<ReporterName>>("predictors").size()),
92  _pcols(getParam<std::vector<unsigned int>>("predictor_cols")),
93  _n_outputs(declareModelData<unsigned int>("_n_outputs", 1)),
94  _row_data(_sampler.getNumberOfCols()),
95  _skip_unconverged(getParam<bool>("skip_unconverged_samples")),
96  _converged(nullptr),
97  _cv_type(getParam<MooseEnum>("cv_type")),
98  _n_splits(getParam<unsigned int>("cv_splits")),
99  _cv_n_trials(getParam<unsigned int>("cv_n_trials")),
100  _cv_seed(getParam<unsigned int>("cv_seed")),
101  _doing_cv(_cv_type != "none"),
102  _cv_trial_scores(declareModelData<std::vector<std::vector<Real>>>("cv_scores"))
103 {
104  if (_skip_unconverged)
105  {
106  if (!isParamValid("converged_reporter"))
107  paramError("skip_unconverged_samples",
108  "'converged_reporter' needs to be specified to skip unconverged sample.");
109  _converged = &getTrainingData<bool>(getParam<ReporterName>("converged_reporter"));
110  }
111 
112  if (_doing_cv)
113  {
114  if (!isParamValid("cv_surrogate"))
115  paramError("cv_type",
116  "To perform cross-validation, the option cv_surrogate needs to be specified",
117  " to provide a Surrogate object for training and evaluation.");
118 
120  paramError("cv_splits",
121  "The specified number of splits (cv_splits = ",
122  _n_splits,
123  ")",
124  " exceeds the number of rows in Sampler '",
125  getParam<SamplerName>("sampler"),
126  "'");
127 
129  }
130 
131  // Get TrainingData for responses and predictors
132  if (getParam<MooseEnum>("response_type") == 0)
133  _rval = &getTrainingData<Real>(getParam<ReporterName>("response"));
134  else if (getParam<MooseEnum>("response_type") == 1)
135  _rvecval = &getTrainingData<std::vector<Real>>(getParam<ReporterName>("response"));
136 
137  const auto & pnames = getParam<std::vector<ReporterName>>("predictors");
138  for (unsigned int i = 0; i < pnames.size(); ++i)
139  _pvals[i] = &getTrainingData<Real>(pnames[i]);
140 
141  // If predictors and predictor_cols are empty, use all sampler columns
142  if (_pvals.empty() && _pcols.empty())
143  {
144  _pcols.resize(_sampler.getNumberOfCols());
145  std::iota(_pcols.begin(), _pcols.end(), 0);
146  }
147  _n_dims = _pvals.size() + _pcols.size();
148 
149  _predictor_data.resize(_n_dims);
150 }
151 
152 void
154 {
155  // Figure out if data is distributed
156  for (auto & pair : _training_data)
157  {
158  const ReporterName & name = pair.first;
159  TrainingDataBase & data = *pair.second;
160 
161  const auto & mode = _fe_problem.getReporterData().getReporterMode(name);
162  if (mode == REPORTER_MODE_DISTRIBUTED || (mode == REPORTER_MODE_ROOT && processor_id() != 0))
163  data.isDistributed() = true;
164  else if (mode == REPORTER_MODE_REPLICATED ||
165  (mode == REPORTER_MODE_ROOT && processor_id() == 0))
166  data.isDistributed() = false;
167  else
168  mooseError("Predictor reporter value ", name, " is not of supported mode.");
169  }
170 
171  if (_doing_cv)
172  _cv_surrogate = &getSurrogateModel("cv_surrogate");
173 }
174 
175 void
177 {
178  if (_doing_cv)
179  for (const auto & trial : make_range(_cv_n_trials))
180  {
181  std::vector<Real> trial_score = crossValidate();
182 
183  // Expand _cv_trial_scores with more columns if necessary, then insert values.
184  for (unsigned int r = _cv_trial_scores.size(); r < trial_score.size(); ++r)
185  _cv_trial_scores.push_back(std::vector<Real>(_cv_n_trials, 0.0));
186  for (auto r : make_range(trial_score.size()))
187  _cv_trial_scores[r][trial] = trial_score[r];
188  }
189 
192  executeTraining();
193 }
194 
195 void
197 {
198  // Check that the number of sampler columns hasn't changed
199  if (_row_data.size() != _sampler.getNumberOfCols())
200  mooseError("Number of sampler columns has changed.");
201 
202  // Check that training data is correctly sized
203  for (auto & pair : _training_data)
204  {
205  dof_id_type rsize = pair.second->size();
206  dof_id_type nrow =
207  pair.second->isDistributed() ? _sampler.getNumberOfLocalRows() : _sampler.getNumberOfRows();
208  if (rsize != nrow)
209  mooseError("Reporter value ",
210  pair.first,
211  " of size ",
212  rsize,
213  " does not match sampler size (",
214  nrow,
215  ").");
216  }
217 }
218 
219 void
221 {
222  checkIntegrity();
224  _local_row = 0;
225 
226  preTrain();
227 
229  {
230  // Need to do this manually in order to keep the iterators valid
231  const std::vector<Real> data = _sampler.getNextLocalRow();
232  for (unsigned int i = 0; i < _row_data.size(); ++i)
233  _row_data[i] = data[i];
234 
235  // Set training data
236  for (auto & pair : _training_data)
237  pair.second->setCurrentIndex((pair.second->isDistributed() ? _local_row : _row));
238 
240 
241  if ((!_skip_unconverged || *_converged) &&
242  std::find(_skip_indices.begin(), _skip_indices.end(), _row) == _skip_indices.end())
243  train();
244 
245  _local_row++;
246  }
247 
248  postTrain();
249 }
250 
251 std::vector<Real>
253 {
254  std::vector<Real> cv_score(1, 0.0);
255 
256  // Get skipped indices for each split
258  std::vector<std::vector<dof_id_type>> split_indices;
259  if (processor_id() == 0)
260  {
261  std::vector<dof_id_type> indices_flat(n_rows);
262  std::iota(indices_flat.begin(), indices_flat.end(), 0);
263  MooseUtils::shuffle(indices_flat, _cv_generator, 0);
264 
265  split_indices.resize(_n_splits);
266  for (const auto & k : make_range(_n_splits))
267  {
268  const dof_id_type num_ind = n_rows / _n_splits + (k < (n_rows % _n_splits) ? 1 : 0);
269  split_indices[k].insert(split_indices[k].begin(),
270  std::make_move_iterator(indices_flat.begin()),
271  std::make_move_iterator(indices_flat.begin() + num_ind));
272  std::sort(split_indices[k].begin(), split_indices[k].end());
273  indices_flat.erase(indices_flat.begin(), indices_flat.begin() + num_ind);
274  }
275  }
276 
277  std::vector<dof_id_type> split_ids_buffer;
278  for (const auto & k : make_range(_n_splits))
279  {
280  if (processor_id() == 0)
281  split_ids_buffer = split_indices[k];
282  _communicator.broadcast(split_ids_buffer, 0);
283 
284  _current_sample_size = _sampler.getNumberOfRows() - split_ids_buffer.size();
285 
286  auto first = std::lower_bound(
287  split_ids_buffer.begin(), split_ids_buffer.end(), _sampler.getLocalRowBegin());
288  auto last = std::upper_bound(
289  split_ids_buffer.begin(), split_ids_buffer.end(), _sampler.getLocalRowEnd());
290  _skip_indices.insert(_skip_indices.begin(), first, last);
291 
293 
294  // Train the model
295  executeTraining();
296 
297  // Evaluate the model
298  std::vector<Real> split_mse(1, 0.0);
299  std::vector<Real> row_mse(1, 0.0);
300 
301  auto skipped_row = _skip_indices.begin();
302 
304  {
305  const std::vector<Real> row = _sampler.getNextLocalRow();
306  if (skipped_row != _skip_indices.end() && p == *skipped_row)
307  {
308  for (unsigned int i = 0; i < _row_data.size(); ++i)
309  _row_data[i] = row[i];
310 
311  for (auto & pair : _training_data)
312  pair.second->setCurrentIndex(
313  (pair.second->isDistributed() ? p - _sampler.getLocalRowBegin() : p));
314 
316 
317  row_mse = evaluateModelError(*_cv_surrogate);
318 
319  // Expand split_mse if needed.
320  split_mse.resize(row_mse.size(), 0.0);
321 
322  // Increment errors
323  for (unsigned int r = 0; r < split_mse.size(); ++r)
324  split_mse[r] += row_mse[r];
325 
326  skipped_row++;
327  }
328  }
329  gatherSum(split_mse);
330 
331  // Expand cv_score if necessary.
332  cv_score.resize(split_mse.size(), 0.0);
333 
334  for (auto r : make_range(split_mse.size()))
335  cv_score[r] += split_mse[r] / n_rows;
336 
337  _skip_indices.clear();
338  }
339 
340  for (auto r : make_range(cv_score.size()))
341  cv_score[r] = std::sqrt(cv_score[r]);
342 
343  return cv_score;
344 }
345 
346 std::vector<Real>
348 {
349  std::vector<Real> error(1, 0.0);
350 
351  if (_rval)
352  {
353  Real model_eval = surr.evaluate(_predictor_data);
354  error[0] = MathUtils::pow(model_eval - (*_rval), 2);
355  }
356  else if (_rvecval)
357  {
358  error.resize(_rvecval->size());
359 
360  // Evaluate for vector response.
361  std::vector<Real> model_eval(error.size());
362  surr.evaluate(_predictor_data, model_eval);
363  for (auto r : make_range(_rvecval->size()))
364  error[r] = MathUtils::pow(model_eval[r] - (*_rvecval)[r], 2);
365  }
366 
367  return error;
368 }
369 
370 void
372 {
373  unsigned int d = 0;
374  for (const auto & val : _pvals)
375  _predictor_data[d++] = *val;
376  for (const auto & col : _pcols)
377  _predictor_data[d++] = _row_data[col];
378 }
SurrogateTrainerBase(const InputParameters &parameters)
virtual void initialize() final
const bool _doing_cv
Set to true if cross validation is being performed, controls behavior in execute().
const Real * _rval
Response value.
unsigned int _n_dims
Dimension of predictor data - either _sampler.getNumberOfCols() or _pvals.size() + _pcols...
static InputParameters validParams()
MPI_Datatype data_type
void addParam(const std::string &name, const std::initializer_list< typename T::value_type > &value, const std::string &doc_string)
const unsigned int & _cv_n_trials
Number of repeated trials of cross validation to perform.
const ReporterMode REPORTER_MODE_ROOT
const std::vector< Real > * _rvecval
Vector response value.
void seed(std::size_t i, unsigned int seed)
std::vector< unsigned int > _pcols
Columns from sampler for predictors.
void shuffle(std::vector< T > &data, MooseRandom &generator, const std::size_t seed_index=0)
std::vector< Real > getNextLocalRow()
std::vector< const Real * > _pvals
Predictor values from reporters.
dof_id_type getLocalRowBegin() const
const Parallel::Communicator & _communicator
T & getSurrogateModel(const std::string &name) const
Get a SurrogateModel/Trainer with a given name.
MooseRandom _cv_generator
Random number generator used for shuffling sampler rows during splitting.
std::vector< std::vector< Real > > & _cv_trial_scores
RMSE scores from each CV trial - can be grabbed by VPP or Reporter.
dof_id_type getNumberOfLocalRows() const
virtual const std::string & name() const
void addRequiredParam(const std::string &name, const std::string &doc_string)
static InputParameters validParams()
bool isParamValid(const std::string &name) const
void registerBase(const std::string &value)
void checkIntegrity() const
std::vector< Real > crossValidate()
const ReporterData & getReporterData() const
dof_id_type _row
During training loop, this is the row index of the data.
virtual void train()
void gatherSum(T &value)
dof_id_type _local_row
During training loop, this is the local row index of the data.
virtual void postTrain()
const std::string name
Definition: Setup.h:20
std::vector< Real > _predictor_data
Predictor data for current row - can be combination of Sampler and Reporter values.
static InputParameters validParams()
virtual Real evaluate(const std::vector< Real > &x) const
Evaluate surrogate model given a row of parameters.
std::vector< dof_id_type > _skip_indices
void paramError(const std::string &param, Args... args) const
const ReporterMode REPORTER_MODE_DISTRIBUTED
std::unordered_map< ReporterName, std::shared_ptr< TrainingDataBase > > _training_data
Vector of reporter names and their corresponding values (to be filled by getTrainingData) ...
void broadcast(T &data, const unsigned int root_id=0, const bool identical_sizes=false) const
virtual void preTrain()
dof_id_type getLocalRowEnd() const
virtual std::vector< Real > evaluateModelError(const SurrogateModel &surr)
dof_id_type getNumberOfRows() const
const ReporterProducerEnum & getReporterMode(const ReporterName &reporter_name) const
DIE A HORRIBLE DEATH HERE typedef LIBMESH_DEFAULT_SCALAR_TYPE Real
Interface for objects that need to use samplers.
FEProblemBase & _fe_problem
std::vector< Real > _row_data
Sampler data for the current row.
IntRange< T > make_range(T beg, T end)
unsigned int _local_sample_size
Number of samples (locally) used to train the model.
void mooseError(Args &&... args) const
unsigned int _current_sample_size
Number of samples used to train the model.
const unsigned int & _n_splits
Number of splits (k) to split sampler data into.
void addRangeCheckedParam(const std::string &name, const T &value, const std::string &parsed_function, const std::string &doc_string)
const bool * _converged
Whether or not the current sample has a converged solution.
const ReporterMode REPORTER_MODE_REPLICATED
SurrogateTrainer(const InputParameters &parameters)
const SurrogateModel * _cv_surrogate
SurrogateModel used to evaluate model error relative to test points.
static InputParameters validParams()
This is the base trainer class whose main functionality is the API for declaring model data...
T pow(T x, int e)
processor_id_type processor_id() const
static const std::string k
Definition: NS.h:130
void ErrorVector unsigned int
const bool _skip_unconverged
Whether or not we are skipping samples that have unconverged solutions.
dof_id_type getNumberOfCols() const
const unsigned int & _cv_seed
Seed used for _cv_generator.
uint8_t dof_id_type
An interface class which manages the model data save and load functionalities from moose objects (suc...
virtual void execute() final