https://mooseframework.inl.gov
Loading...
Searching...
No Matches
LibtorchArtificialNeuralNet.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#ifdef MOOSE_LIBTORCH_ENABLED
11
13#include "LibtorchRandomUtils.h"
14#include "MooseError.h"
15#include "LibtorchUtils.h"
16
17#include <cmath>
18
19namespace
20{
21
29bool
30readArchiveTensor(torch::serialize::InputArchive & archive,
31 const std::string & key,
32 torch::Tensor & tensor)
33{
34 try
35 {
36 archive.read(key, tensor);
37 return true;
38 }
39 catch (const c10::Error &)
40 {
41 return false;
42 }
43}
44
50void
51copyTensor(torch::Tensor & destination, const torch::Tensor & source)
52{
53 destination.data().copy_(source.to(destination.options()));
54}
55
57bool
58isOptionalArtificialNeuralNetBuffer(const std::string & key)
59{
60 return key == "input_shift" || key == "input_scale" || key == "output_scale";
61}
62
70template <typename NamedTensorList>
71bool
72hasNamedTensor(const NamedTensorList & tensors, const std::string & key, torch::Tensor & tensor)
73{
74 for (const auto & entry : tensors)
75 if (entry.name == key)
76 {
77 tensor = entry.value;
78 return true;
79 }
80
81 return false;
82}
83
91bool
92loadArtificialNeuralNetStateFromArchive(Moose::LibtorchArtificialNeuralNet & nn,
93 const std::string & filename,
94 std::string & error)
95{
96 try
97 {
98 torch::serialize::InputArchive archive;
99 archive.load_from(filename);
100
101 for (auto & parameter : nn.named_parameters())
102 {
103 torch::Tensor stored_tensor;
104 if (!readArchiveTensor(archive, parameter.key(), stored_tensor))
105 {
106 error = "Missing serialized parameter: " + parameter.key();
107 return false;
108 }
109
110 copyTensor(parameter.value(), stored_tensor);
111 }
112
113 for (auto & buffer : nn.named_buffers())
114 {
115 torch::Tensor stored_tensor;
116 if (!readArchiveTensor(archive, buffer.key(), stored_tensor))
117 {
118 if (isOptionalArtificialNeuralNetBuffer(buffer.key()))
119 continue;
120
121 error = "Missing serialized buffer: " + buffer.key();
122 return false;
123 }
124
125 copyTensor(buffer.value(), stored_tensor);
126 }
127
129 return true;
130 }
131 catch (const c10::Error & e)
132 {
133 error = e.msg();
134 return false;
135 }
136}
137
145bool
146loadArtificialNeuralNetStateFromTorchScript(Moose::LibtorchArtificialNeuralNet & nn,
147 const std::string & filename,
148 std::string & error)
149{
150 try
151 {
152 const auto scripted = torch::jit::load(filename);
153 const auto scripted_parameters = scripted.named_parameters();
154 const auto scripted_buffers = scripted.named_buffers();
155
156 for (auto & parameter : nn.named_parameters())
157 {
158 torch::Tensor stored_tensor;
159 if (!hasNamedTensor(scripted_parameters, parameter.key(), stored_tensor))
160 {
161 error = "Missing scripted parameter: " + parameter.key();
162 return false;
163 }
164
165 copyTensor(parameter.value(), stored_tensor);
166 }
167
168 for (auto & buffer : nn.named_buffers())
169 {
170 torch::Tensor stored_tensor;
171 if (!hasNamedTensor(scripted_buffers, buffer.key(), stored_tensor))
172 {
173 if (isOptionalArtificialNeuralNetBuffer(buffer.key()))
174 continue;
175
176 error = "Missing scripted buffer: " + buffer.key();
177 return false;
178 }
179
180 copyTensor(buffer.value(), stored_tensor);
181 }
182
184 return true;
185 }
186 catch (const c10::Error & e)
187 {
188 error = e.msg();
189 return false;
190 }
191}
192
193} // namespace
194
195namespace Moose
196{
197
199 const std::string name,
200 const unsigned int num_inputs,
201 const unsigned int num_outputs,
202 const std::vector<unsigned int> & num_neurons_per_layer,
203 const std::vector<std::string> & activation_function,
204 const torch::DeviceType device_type,
205 const torch::ScalarType data_type,
206 const bool build_on_construct,
207 const std::vector<Real> & input_shift_factors,
208 const std::vector<Real> & input_scaling_factors,
209 const std::vector<Real> & output_scaling_factors)
210 : _name(name),
211 _num_inputs(num_inputs),
212 _num_outputs(num_outputs),
213 _num_neurons_per_layer(num_neurons_per_layer),
214 _activation_function(MultiMooseEnum("relu sigmoid elu gelu linear tanh", "relu")),
215 _device_type(device_type),
216 _data_type(data_type),
217 _input_shift_factors(
218 setAffineFactors(input_shift_factors, num_inputs, 0.0, "input_shift_factors")),
219 _input_scaling_factors(
220 setAffineFactors(input_scaling_factors, num_inputs, 1.0, "input_scaling_factors")),
221 _output_scaling_factors(
222 setAffineFactors(output_scaling_factors, num_outputs, 1.0, "output_scaling_factors"))
223{
224 _activation_function = activation_function;
226
227 // Check if the number of activation functions matches the number of hidden layers
228 if ((_activation_function.size() != 1) &&
230 mooseError("The number of activation functions should be either one or the same as the number "
231 "of hidden layers");
232
233 if (build_on_construct)
235}
236
238 const Moose::LibtorchArtificialNeuralNet & nn, const bool build_on_construct)
239 : torch::nn::Module(),
240 _name(nn.name()),
241 _num_inputs(nn.numInputs()),
242 _num_outputs(nn.numOutputs()),
243 _num_neurons_per_layer(nn.numNeuronsPerLayer()),
244 _activation_function(nn.activationFunctions()),
245 _device_type(nn.deviceType()),
246 _data_type(nn.dataType()),
247 _input_shift_factors(nn.inputShiftFactors()),
248 _input_scaling_factors(nn.inputScalingFactors()),
249 _output_scaling_factors(nn.outputScalingFactors())
250{
252
253 // We construct the NN architecture
254 if (build_on_construct)
255 {
257 // We fill it up with the current parameter values
258 const auto & from_params = nn.named_parameters();
259 auto to_params = this->named_parameters();
260 for (unsigned int param_i : make_range(from_params.size()))
261 to_params[param_i].value().data() = from_params[param_i].value().data().clone();
262
263 const auto & from_buffers = nn.named_buffers();
264 auto to_buffers = this->named_buffers();
265 for (unsigned int buffer_i : make_range(from_buffers.size()))
266 to_buffers[buffer_i].value().data() = from_buffers[buffer_i].value().data().clone();
267 }
268}
269
270Real
271LibtorchArtificialNeuralNet::determineGain(const std::string & activation)
272{
273 if (activation == "relu")
274 return std::sqrt(2);
275 if (activation == "tanh")
276 return 5.0 / 3.0;
277
278 return 1.0;
279}
280
281void
282LibtorchArtificialNeuralNet::initializeNeuralNetwork(const c10::optional<at::Generator> generator)
283{
284 for (const auto i : make_range(numHiddenLayers()))
285 {
286 const auto & activation =
288 const Real gain = determineGain(activation);
289 Moose::orthogonalInitializeTensor(_weights[i]->weight, gain, generator);
290 torch::nn::init::zeros_(_weights[i]->bias);
291 }
292
293 Moose::orthogonalInitializeTensor(_weights.back()->weight, 1.0, generator);
294 torch::nn::init::zeros_(_weights.back()->bias);
295}
296
297std::vector<Real>
298LibtorchArtificialNeuralNet::setAffineFactors(const std::vector<Real> & factors,
299 const unsigned int expected_size,
300 const Real default_value,
301 const std::string & factor_name)
302{
303 const auto resolved_factors =
304 factors.empty() ? std::vector<Real>(expected_size, default_value) : factors;
305
306 if (resolved_factors.size() != expected_size)
307 mooseError("The number of ", factor_name, " entries must match ", expected_size, ".");
308
309 return resolved_factors;
310}
311
312void
314{
315 auto input_shift = _input_shift_factors;
317 _input_shift_tensor = register_buffer(
318 "input_shift", _input_shift_tensor.transpose(0, 1).to(_data_type).to(_device_type));
319
320 auto input_scale = _input_scaling_factors;
322 _input_scale_tensor = register_buffer(
323 "input_scale", _input_scale_tensor.transpose(0, 1).to(_data_type).to(_device_type));
324
325 auto output_scale = _output_scaling_factors;
327 _output_scale_tensor = register_buffer(
328 "output_scale", _output_scale_tensor.transpose(0, 1).to(_data_type).to(_device_type));
329}
330
331void
333{
334 auto input_shift = _input_shift_tensor.detach().reshape({-1}).to(torch::kCPU).to(torch::kDouble);
336
337 auto input_scale = _input_scale_tensor.detach().reshape({-1}).to(torch::kCPU).to(torch::kDouble);
339
340 auto output_scale =
341 _output_scale_tensor.detach().reshape({-1}).to(torch::kCPU).to(torch::kDouble);
343}
344
345void
347{
348 // Adding hidden layers
349 unsigned int inp_neurons = _num_inputs;
350 for (unsigned int i = 0; i < numHiddenLayers(); ++i)
351 {
352 std::unordered_map<std::string, unsigned int> parameters = {
353 {"inp_neurons", inp_neurons}, {"out_neurons", _num_neurons_per_layer[i]}};
354 addLayer("hidden_layer_" + std::to_string(i + 1), parameters);
355
356 // Necessary to retain double precision (and error-free runs)
358 inp_neurons = _num_neurons_per_layer[i];
359 }
360 // Adding output layer
361 std::unordered_map<std::string, unsigned int> parameters = {{"inp_neurons", inp_neurons},
362 {"out_neurons", _num_outputs}};
363 addLayer("output_layer_", parameters);
364 _weights.back()->to(_device_type, _data_type);
365}
366
367torch::Tensor
369{
370 torch::Tensor input(x);
371 if (_data_type != input.scalar_type())
372 input = input.to(_data_type);
373 if (_device_type != input.device().type())
374 input = input.to(_device_type);
375
376 return (input - _input_shift_tensor) * _input_scale_tensor;
377}
378
379torch::Tensor
380LibtorchArtificialNeuralNet::scaleOutput(const torch::Tensor & y) const
381{
382 return y * _output_scale_tensor;
383}
384
385torch::Tensor
387{
388 torch::Tensor output = preprocessInput(x);
389
390 for (unsigned int i = 0; i < _weights.size() - 1; ++i)
391 {
392 std::string activation =
394 if (activation == "relu")
395 output = torch::relu(_weights[i]->forward(output));
396 else if (activation == "sigmoid")
397 output = torch::sigmoid(_weights[i]->forward(output));
398 else if (activation == "tanh")
399 output = torch::tanh(_weights[i]->forward(output));
400 else if (activation == "elu")
401 output = torch::elu(_weights[i]->forward(output));
402 else if (activation == "gelu")
403 output = torch::gelu(_weights[i]->forward(output));
404 else if (activation == "linear")
405 output = _weights[i]->forward(output);
406 }
407
408 return scaleOutput(_weights[_weights.size() - 1]->forward(output));
409}
410
411void
413 const std::string & layer_name,
414 const std::unordered_map<std::string, unsigned int> & parameters)
415{
416 auto it = parameters.find("inp_neurons");
417 if (it == parameters.end())
418 ::mooseError("Number of input neurons not found during the construction of "
419 "LibtorchArtificialNeuralNet!");
420 unsigned int inp_neurons = it->second;
421
422 it = parameters.find("out_neurons");
423 if (it == parameters.end())
424 ::mooseError("Number of output neurons not found during the construction of "
425 "LibtorchArtificialNeuralNet!");
426 unsigned int out_neurons = it->second;
427
428 _weights.push_back(register_module(layer_name, torch::nn::Linear(inp_neurons, out_neurons)));
429}
430
431void
432LibtorchArtificialNeuralNet::store(nlohmann::json & json) const
433{
434 const auto & named_params = this->named_parameters();
435 for (const auto & param_i : make_range(named_params.size()))
436 {
437 // We cast the parameters into a 1D vector
438 json[named_params[param_i].key()] = std::vector<Real>(
439 named_params[param_i].value().data_ptr<Real>(),
440 named_params[param_i].value().data_ptr<Real>() + named_params[param_i].value().numel());
441 }
442
443 json["input_shift_factors"] = _input_shift_factors;
444 json["input_scaling_factors"] = _input_scaling_factors;
445 json["output_scaling_factors"] = _output_scaling_factors;
446}
447
448void
449to_json(nlohmann::json & json, const Moose::LibtorchArtificialNeuralNet * const & network)
450{
451 if (network)
452 network->store(json);
453}
454
455void
457 const std::string & filename)
458{
459 std::string archive_error;
460 if (loadArtificialNeuralNetStateFromArchive(nn, filename, archive_error))
461 return;
462
463 std::string scripted_error;
464 if (loadArtificialNeuralNetStateFromTorchScript(nn, filename, scripted_error))
465 return;
466
467 mooseError("The requested pytorch parameter file could not be loaded. This can either be the "
468 "result of the file not existing or a misalignment in the generated container and "
469 "the data in the file. Make sure the dimensions of the generated neural net are the "
470 "same as the dimensions of the parameters in the input file!\nArchive load error: ",
471 archive_error,
472 "\nTorchScript load error: ",
473 scripted_error);
474}
475}
476
477template <>
478void
480 std::ostream & stream, std::shared_ptr<Moose::LibtorchArtificialNeuralNet> & nn, void * context)
481{
482 std::string n(nn->name());
483 dataStore(stream, n, context);
484
485 unsigned int ni(nn->numInputs());
486 dataStore(stream, ni, context);
487
488 unsigned int no(nn->numOutputs());
489 dataStore(stream, no, context);
490
491 unsigned int nhl(nn->numHiddenLayers());
492 dataStore(stream, nhl, context);
493
494 std::vector<unsigned int> nnpl(nn->numNeuronsPerLayer());
495 dataStore(stream, nnpl, context);
496
497 unsigned int afs(nn->activationFunctions().size());
498 dataStore(stream, afs, context);
499
500 std::vector<std::string> items(afs);
501 for (unsigned int i = 0; i < afs; ++i)
502 items[i] = nn->activationFunctions()[i];
503
504 dataStore(stream, items, context);
505
506 auto device_type = static_cast<std::underlying_type<torch::DeviceType>::type>(nn->deviceType());
507 dataStore(stream, device_type, context);
508
509 auto data_type = static_cast<std::underlying_type<torch::ScalarType>::type>(nn->dataType());
510 dataStore(stream, data_type, context);
511
512 torch::save(nn, nn->name());
513}
514
515template <>
516void
518 std::istream & stream, std::shared_ptr<Moose::LibtorchArtificialNeuralNet> & nn, void * context)
519{
520 std::string name;
521 dataLoad(stream, name, context);
522
523 unsigned int num_inputs;
524 dataLoad(stream, num_inputs, context);
525
526 unsigned int num_outputs;
527 dataLoad(stream, num_outputs, context);
528
529 unsigned int num_hidden_layers;
530 dataLoad(stream, num_hidden_layers, context);
531
532 std::vector<unsigned int> num_neurons_per_layer;
533 num_neurons_per_layer.resize(num_hidden_layers);
534 dataLoad(stream, num_neurons_per_layer, context);
535
536 unsigned int num_activation_items;
537 dataLoad(stream, num_activation_items, context);
538
539 std::vector<std::string> activation_functions;
540 activation_functions.resize(num_activation_items);
541 dataLoad(stream, activation_functions, context);
542
543 std::underlying_type<torch::DeviceType>::type device_type;
544 dataLoad(stream, device_type, context);
545 const torch::DeviceType divt(static_cast<torch::DeviceType>(device_type));
546
547 std::underlying_type<torch::ScalarType>::type data_type;
548 dataLoad(stream, data_type, context);
549 const torch::ScalarType datt(static_cast<torch::ScalarType>(data_type));
550
551 nn = std::make_shared<Moose::LibtorchArtificialNeuralNet>(
552 name, num_inputs, num_outputs, num_neurons_per_layer, activation_functions, divt, datt);
553
555}
556
557template <>
558void
560 std::ostream & /*stream*/,
562 void * /*context*/)
563{
564}
565
566template <>
567void
569 std::istream & /*stream*/,
571 void * /*context*/)
572{
573}
574
575#endif
void dataStore< Moose::LibtorchArtificialNeuralNet >(std::ostream &stream, std::shared_ptr< Moose::LibtorchArtificialNeuralNet > &nn, void *context)
Serialize the artificial-neural-network metadata needed for restart.
void dataLoad< Moose::LibtorchArtificialNeuralNet >(std::istream &stream, std::shared_ptr< Moose::LibtorchArtificialNeuralNet > &nn, void *context)
Deserialize the artificial-neural-network metadata needed for restart.
void dataLoad< Moose::LibtorchArtificialNeuralNet const >(std::istream &, Moose::LibtorchArtificialNeuralNet const *&, void *)
Placeholder deserializer for reporter-only artificial-neural-network pointers.
void dataStore< Moose::LibtorchArtificialNeuralNet const >(std::ostream &, Moose::LibtorchArtificialNeuralNet const *&, void *)
Placeholder serializer for reporter-only artificial-neural-network pointers.
void dataLoad(std::istream &stream, LineSegment &l, void *context)
void dataStore(std::ostream &stream, LineSegment &l, void *context)
void mooseError(Args &&... args)
Emit an error message with the given stringified, concatenated args and terminate the application.
Definition MooseError.h:311
if(!dmm->_nl) SETERRQ(PETSC_COMM_WORLD
Simple feed-forward neural net with optional affine input and output scaling.
const unsigned int _num_inputs
Number of neurons on the input layer.
Real determineGain(const std::string &activation)
Map an activation name to the orthogonal-initialization gain we want to use.
virtual torch::Tensor forward(const torch::Tensor &x) override
Run a forward pass through the network.
torch::Tensor _input_shift_tensor
Registered libtorch buffer holding the affine input shifts.
const torch::ScalarType _data_type
The data type used in this neural network.
void store(nlohmann::json &json) const
Store the network architecture in a JSON object for debugging or visualization.
static std::vector< Real > setAffineFactors(const std::vector< Real > &factors, unsigned int expected_size, Real default_value, const std::string &factor_name)
Set affine metadata by either accepting the user values or filling defaults.
std::vector< Real > _input_shift_factors
Affine preprocessing applied to the flattened input.
void synchronizeAffineFactorsFromBuffers()
Update cached affine metadata vectors from the registered libtorch buffers.
MultiMooseEnum _activation_function
Activation functions (either one for all hidden layers or one for every layer separately)
std::vector< Real > _input_scaling_factors
Multiplicative affine preprocessing applied after shifting the input.
std::vector< torch::nn::Linear > _weights
Submodules that hold linear operations and the corresponding weights and biases (y = W * x + b)
void initializeAffineBuffers()
Initialize the registered affine metadata buffers used by serialization.
virtual void addLayer(const std::string &layer_name, const std::unordered_map< std::string, unsigned int > &parameters)
Add one linear layer to the network.
torch::Tensor _input_scale_tensor
Registered libtorch buffer holding the affine input scaling factors.
const unsigned int _num_outputs
Number of neurons on the output layer.
torch::Tensor _output_scale_tensor
Registered libtorch buffer holding the output scaling factors.
const torch::DeviceType _device_type
The device type used for this neural network.
virtual torch::Tensor preprocessInput(const torch::Tensor &x) const
Apply affine preprocessing to the raw input tensor.
unsigned int numHiddenLayers() const
Return the number of hidden layers.
LibtorchArtificialNeuralNet(const std::string name, const unsigned int num_inputs, const unsigned int num_outputs, const std::vector< unsigned int > &num_neurons_per_layer, const std::vector< std::string > &activation_function={"relu"}, const torch::DeviceType device_type=torch::kCPU, const torch::ScalarType scalar_type=torch::kDouble, const bool build_on_construct=true, const std::vector< Real > &input_shift_factors={}, const std::vector< Real > &input_scaling_factors={}, const std::vector< Real > &output_scaling_factors={})
Build a plain feed-forward neural network.
const std::vector< unsigned int > _num_neurons_per_layer
Hidden layer architecture.
virtual void initializeNeuralNetwork(c10::optional< at::Generator > generator=c10::nullopt)
Initialize the trainable weights and biases.
std::vector< Real > _output_scaling_factors
Multiplicative scaling applied after the network output is formed.
virtual torch::Tensor scaleOutput(const torch::Tensor &y) const
Apply the configured output scaling to a network output tensor.
virtual void constructNeuralNetwork()
Construct the neural network.
This is a "smart" enum class intended to replace many of the shortcomings in the C++ enum type.
unsigned int size() const
Return the number of active items in the MultiMooseEnum.
void vectorToTensor(const std::vector< DataType > &vector, torch::Tensor &tensor, const bool detach=false)
Utility function that converts a standard vector to a torch::Tensor.
void tensorToVector(torch::Tensor &tensor, std::vector< DataType > &vector)
Utility function that converts a torch::Tensor to a standard vector.
MOOSE now contains C++17 code, so give a reasonable error message stating what the user can do to add...
void to_json(nlohmann::json &json, const Moose::LibtorchArtificialNeuralNet *const &network)
Dump an artificial neural network into JSON for reporter output and debugging.
void orthogonalInitializeTensor(torch::Tensor &tensor, Real gain=1.0, c10::optional< at::Generator > generator=c10::nullopt)
Fill a tensor with a (semi) orthogonal matrix using the provided generator.
void loadLibtorchArtificialNeuralNetState(Moose::LibtorchArtificialNeuralNet &nn, const std::string &filename)
Load an artificial-neural-network checkpoint written as a native libtorch state archive.
std::string name(const ElemQuality q)