https://mooseframework.inl.gov
Loading...
Searching...
No Matches
TestLibtorchActorNeuralNet.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
12#include "gtest/gtest.h"
14#include "LibtorchRandomUtils.h"
15#include "MooseUnitUtils.h"
16
17#include <cmath>
18
19namespace
20{
21
22class TestableLibtorchArtificialNeuralNet : public Moose::LibtorchArtificialNeuralNet
23{
24public:
27};
28
29class TestableLibtorchActorNeuralNet : public Moose::LibtorchActorNeuralNet
30{
31public:
34};
35
36Real
37inverseSoftplusPlusOne(const Real target)
38{
39 return std::log(std::exp(target - 1.0) - 1.0);
40}
41
42} // namespace
43
44TEST(LibtorchActorNeuralNetTest, artificialNetAppliesAffineInputAndOutputScaling)
45{
46 TestableLibtorchArtificialNeuralNet network("test_ann",
47 2,
48 1,
49 {},
50 {"linear"},
51 torch::kCPU,
52 torch::kDouble,
53 true,
54 {1.0, 2.0},
55 {2.0, 3.0},
56 {10.0});
57
58 ASSERT_EQ(network._weights.size(), 1u);
59
60 network._weights[0]->weight.data().fill_(0.0);
61 network._weights[0]->weight.data()[0][0] = 1.0;
62 network._weights[0]->weight.data()[0][1] = -1.0;
63 network._weights[0]->bias.data().fill_(0.0);
64
65 auto input = torch::tensor({{2.0, 6.0}}, at::kDouble);
66 const Real actual = network.forward(input).item<Real>();
67
68 EXPECT_NEAR(actual, -100.0, 1e-12);
69}
70
71TEST(LibtorchActorNeuralNetTest, boundedBetaLogProbability)
72{
73 constexpr Real min_value = -2.0;
74 constexpr Real max_value = 4.0;
75 constexpr Real alpha_target = 2.3;
76 constexpr Real beta_target = 3.7;
77 constexpr Real action_value = 1.2;
78
79 TestableLibtorchActorNeuralNet network(
80 "test_beta", 1, 1, {1}, {"linear"}, {min_value}, {max_value});
81
82 ASSERT_EQ(network._weights.size(), 1u);
83
84 network._weights[0]->weight.data().fill_(0.0);
85 network._weights[0]->bias.data().fill_(1.0);
86 network.betaActionDistribution().alphaModule()->weight.data().fill_(
87 inverseSoftplusPlusOne(alpha_target));
88 network.betaActionDistribution().betaModule()->weight.data().fill_(
89 inverseSoftplusPlusOne(beta_target));
90
91 auto input = torch::zeros({1, 1}, at::kDouble);
92 network.evaluate(input, false);
93
94 const Real alpha = network.betaActionDistribution().alphaTensor().item<Real>();
95 const Real beta = network.betaActionDistribution().betaTensor().item<Real>();
96 const Real normalized = (action_value - min_value) / (max_value - min_value);
97 const Real log_norm = std::lgamma(alpha) + std::lgamma(beta) - std::lgamma(alpha + beta);
98 const Real expected = (alpha - 1.0) * std::log(normalized) +
99 (beta - 1.0) * std::log1p(-normalized) - log_norm -
100 std::log(max_value - min_value);
101
102 auto action = torch::tensor({{action_value}}, at::kDouble);
103 const Real actual = network.logProbability(action).item<Real>();
104
105 EXPECT_NEAR(actual, expected, 1e-12);
106}
107
108TEST(LibtorchActorNeuralNetTest, boundedBetaStaysFiniteForLargeLogits)
109{
110 constexpr Real min_value = -2.0;
111 constexpr Real max_value = 4.0;
112 // exp(800) overflows a double, so log(exp(x) + 1) would return inf for this logit.
113 constexpr Real large_logit = 800.0;
114 constexpr Real action_value = 3.9;
115
116 TestableLibtorchActorNeuralNet network(
117 "test_beta", 1, 1, {1}, {"linear"}, {min_value}, {max_value});
118
119 ASSERT_EQ(network._weights.size(), 1u);
120
121 network._weights[0]->weight.data().fill_(0.0);
122 network._weights[0]->bias.data().fill_(1.0);
123 auto & distribution = network.betaActionDistribution();
124 distribution.alphaModule()->weight.data().fill_(large_logit);
125 distribution.alphaModule()->bias.data().fill_(0.0);
126 distribution.betaModule()->weight.data().fill_(0.0);
127 distribution.betaModule()->bias.data().fill_(0.0);
128
129 auto input = torch::zeros({1, 1}, at::kDouble);
130 const Real deterministic_action = network.evaluate(input, false).item<Real>();
131
132 EXPECT_NEAR(distribution.alphaTensor().item<Real>(), large_logit + 1.0, 1e-12);
133 EXPECT_NEAR(distribution.betaTensor().item<Real>(), std::log(2.0) + 1.0, 1e-12);
134 EXPECT_TRUE(std::isfinite(deterministic_action));
135
136 auto log_probability = network.logProbability(torch::tensor({{action_value}}, at::kDouble));
137 EXPECT_TRUE(std::isfinite(log_probability.item<Real>()));
138
139 log_probability.backward();
140 EXPECT_TRUE(torch::isfinite(distribution.alphaModule()->weight.grad()).all().item<bool>());
141}
142
143TEST(LibtorchActorNeuralNetTest, gaussianActorUsesPhysicalActionScalingAndStateIndependentStd)
144{
145 constexpr Real input_shift = 1.0;
146 constexpr Real input_scale = 2.0;
147 constexpr Real action_scale = 5.0;
148 const Real log_std = std::log(2.0);
149 constexpr Real physical_action = 20.0;
150 constexpr Real expected_deterministic_action = 15.0;
151
152 TestableLibtorchActorNeuralNet network("test_gaussian",
153 1,
154 1,
155 {},
156 {"linear"},
157 {},
158 {},
159 torch::kCPU,
160 torch::kDouble,
161 true,
162 {input_shift},
163 {input_scale},
164 {action_scale});
165
166 network.gaussianActionDistribution().meanModule()->weight.data().fill_(1.5);
167 network.gaussianActionDistribution().meanModule()->bias.data().fill_(0.0);
168 network.gaussianActionDistribution().logStd().data().fill_(log_std);
169
170 auto input = torch::tensor({{2.0}}, at::kDouble);
171 const Real deterministic_action = network.evaluate(input, false).item<Real>();
172 EXPECT_NEAR(deterministic_action, expected_deterministic_action, 1e-12);
173 EXPECT_NEAR(
174 network.gaussianActionDistribution().stdTensor().item<Real>(), std::exp(log_std), 1e-12);
175
176 const Real unscaled_mean = expected_deterministic_action / action_scale;
177 const Real unscaled_action = physical_action / action_scale;
178 const Real expected_log_probability =
179 -std::pow(unscaled_action - unscaled_mean, 2) / (2.0 * 4.0) - log_std -
180 0.5 * std::log(2.0 * libMesh::pi) - std::log(action_scale);
181
182 auto action = torch::tensor({{physical_action}}, at::kDouble);
183 const Real actual_log_probability = network.logProbability(action).item<Real>();
184
185 EXPECT_NEAR(actual_log_probability, expected_log_probability, 1e-12);
186
187 auto second_input = torch::tensor({{4.0}}, at::kDouble);
188 network.evaluate(second_input, false);
189 EXPECT_NEAR(
190 network.gaussianActionDistribution().stdTensor().item<Real>(), std::exp(log_std), 1e-12);
191}
192
193TEST(LibtorchActorNeuralNetTest, gaussianActorCanUseStateDependentStdWhenRequested)
194{
195 TestableLibtorchActorNeuralNet network("test_state_dependent_gaussian",
196 1,
197 1,
198 {},
199 {"linear"},
200 {},
201 {},
202 torch::kCPU,
203 torch::kDouble,
204 true,
205 {1.0},
206 {2.0},
207 {1.0},
208 false);
209
210 network.gaussianActionDistribution().meanModule()->weight.data().fill_(0.0);
211 network.gaussianActionDistribution().meanModule()->bias.data().fill_(0.0);
212 network.gaussianActionDistribution().stdModule()->weight.data().fill_(0.5);
213 network.gaussianActionDistribution().stdModule()->bias.data().fill_(0.0);
214
215 auto first_input = torch::tensor({{2.0}}, at::kDouble);
216 network.evaluate(first_input, false);
217 const Real first_std = network.gaussianActionDistribution().stdTensor().item<Real>();
218
219 auto second_input = torch::tensor({{4.0}}, at::kDouble);
220 network.evaluate(second_input, false);
221 const Real second_std = network.gaussianActionDistribution().stdTensor().item<Real>();
222
223 EXPECT_NEAR(first_std, std::exp(1.0), 1e-12);
224 EXPECT_NEAR(second_std, std::exp(3.0), 1e-12);
225 EXPECT_GT(second_std, first_std);
226}
227
228TEST(LibtorchActorNeuralNetTest, explicitGeneratorKeepsGaussianSamplingStableAcrossCopies)
229{
230 TestableLibtorchActorNeuralNet original("test_gaussian",
231 1,
232 1,
233 {},
234 {"linear"},
235 {},
236 {},
237 torch::kCPU,
238 torch::kDouble,
239 true,
240 {0.0},
241 {1.0},
242 {1.0});
243
244 original.gaussianActionDistribution().meanModule()->weight.data().fill_(0.75);
245 original.gaussianActionDistribution().meanModule()->bias.data().fill_(1.25);
246 original.gaussianActionDistribution().logStd().data().fill_(std::log(0.5));
247
248 TestableLibtorchActorNeuralNet copied(original);
249
250 auto original_input = torch::tensor({{2.0}}, at::kDouble);
251 auto copied_input = torch::tensor({{2.0}}, at::kDouble);
252 auto original_generator = Moose::makeLibtorchCPUGenerator(12345);
253 auto copied_generator = Moose::makeLibtorchCPUGenerator(12345);
254
255 const auto original_action = original.evaluate(original_input, true, original_generator);
256 const auto copied_action = copied.evaluate(copied_input, true, copied_generator);
257
258 EXPECT_TRUE(torch::allclose(original_action, copied_action, /* rtol = */ 0.0, /* atol = */ 0.0));
259}
260
261TEST(LibtorchActorNeuralNetTest, explicitGeneratorMakesInitializationIndependentOfConstructionOrder)
262{
263 TestableLibtorchActorNeuralNet first("first_actor",
264 2,
265 1,
266 {3},
267 {"relu"},
268 {},
269 {},
270 torch::kCPU,
271 torch::kDouble,
272 true,
273 {0.0, 0.0},
274 {1.0, 1.0},
275 {1.0});
276 TestableLibtorchActorNeuralNet second("second_actor",
277 2,
278 1,
279 {3},
280 {"relu"},
281 {},
282 {},
283 torch::kCPU,
284 torch::kDouble,
285 true,
286 {0.0, 0.0},
287 {1.0, 1.0},
288 {1.0});
289
290 first.initializeNeuralNetwork(Moose::makeLibtorchCPUGenerator(2468));
291 second.initializeNeuralNetwork(Moose::makeLibtorchCPUGenerator(2468));
292
293 const auto first_parameters = first.named_parameters();
294 const auto second_parameters = second.named_parameters();
295 ASSERT_EQ(first_parameters.size(), second_parameters.size());
296 for (const auto i : index_range(first_parameters))
297 {
298 EXPECT_EQ(first_parameters[i].key(), second_parameters[i].key());
299 EXPECT_TRUE(torch::allclose(first_parameters[i].value(),
300 second_parameters[i].value(),
301 /* rtol = */ 0.0,
302 /* atol = */ 0.0));
303 }
304}
305
306TEST(LibtorchActorNeuralNetTest, loadActorStateAcceptsTorchSaveArchive)
307{
308 TestableLibtorchActorNeuralNet saved("saved_actor",
309 2,
310 1,
311 {2},
312 {"linear"},
313 {},
314 {},
315 torch::kCPU,
316 torch::kDouble,
317 true,
318 {1.0, -2.0},
319 {0.5, 3.0},
320 {4.0});
321
322 saved._weights[0]->weight.data() = torch::tensor({{1.0, 2.0}, {3.0, 4.0}}, at::kDouble);
323 saved._weights[0]->bias.data() = torch::tensor({5.0, 6.0}, at::kDouble);
324 saved.gaussianActionDistribution().meanModule()->weight.data() =
325 torch::tensor({{7.0, 8.0}}, at::kDouble);
326 saved.gaussianActionDistribution().meanModule()->bias.data() = torch::tensor({9.0}, at::kDouble);
327 saved.gaussianActionDistribution().logStd().data() = torch::tensor({-3.5}, at::kDouble);
328
330 torch::save(std::make_shared<Moose::LibtorchActorNeuralNet>(saved), archive.path().string());
331
332 TestableLibtorchActorNeuralNet restored("restored_actor",
333 2,
334 1,
335 {2},
336 {"linear"},
337 {},
338 {},
339 torch::kCPU,
340 torch::kDouble,
341 true,
342 {1.0, -2.0},
343 {0.5, 3.0},
344 {4.0});
345
346 Moose::loadLibtorchActorNeuralNetState(restored, archive.path().string());
347
348 const auto saved_parameters = saved.named_parameters();
349 const auto restored_parameters = restored.named_parameters();
350 ASSERT_EQ(saved_parameters.size(), restored_parameters.size());
351 for (std::size_t i = 0; i < saved_parameters.size(); ++i)
352 {
353 EXPECT_EQ(saved_parameters[i].key(), restored_parameters[i].key());
354 EXPECT_TRUE(torch::allclose(saved_parameters[i].value(),
355 restored_parameters[i].value(),
356 /*rtol=*/0.0,
357 /*atol=*/0.0));
358 }
359
360 const auto saved_buffers = saved.named_buffers();
361 const auto restored_buffers = restored.named_buffers();
362 ASSERT_EQ(saved_buffers.size(), restored_buffers.size());
363 for (std::size_t i = 0; i < saved_buffers.size(); ++i)
364 {
365 EXPECT_EQ(saved_buffers[i].key(), restored_buffers[i].key());
366 EXPECT_TRUE(torch::allclose(saved_buffers[i].value(),
367 restored_buffers[i].value(),
368 /*rtol=*/0.0,
369 /*atol=*/0.0));
370 }
371
372 auto saved_input = torch::tensor({{3.0, -1.0}}, at::kDouble);
373 auto restored_input = saved_input.clone();
374 EXPECT_TRUE(torch::allclose(saved.evaluate(saved_input, false),
375 restored.evaluate(restored_input, false),
376 /*rtol=*/0.0,
377 /*atol=*/0.0));
378}
379
380TEST(LibtorchActorNeuralNetTest, loadActorStateRejectsHiddenLayerMismatch)
381{
382 TestableLibtorchActorNeuralNet saved("saved_actor",
383 2,
384 1,
385 {2},
386 {"linear"},
387 {},
388 {},
389 torch::kCPU,
390 torch::kDouble,
391 true,
392 {1.0, -2.0},
393 {0.5, 3.0},
394 {4.0});
395
397 torch::save(std::make_shared<Moose::LibtorchActorNeuralNet>(saved), archive.path().string());
398
399 TestableLibtorchActorNeuralNet restored("restored_actor",
400 2,
401 1,
402 {3},
403 {"linear"},
404 {},
405 {},
406 torch::kCPU,
407 torch::kDouble,
408 true,
409 {1.0, -2.0},
410 {0.5, 3.0},
411 {4.0});
412
413 EXPECT_ANY_THROW(Moose::loadLibtorchActorNeuralNetState(restored, archive.path().string()));
414}
415
416TEST(LibtorchActorNeuralNetTest, loadActorStateRejectsBoundednessMismatch)
417{
418 TestableLibtorchActorNeuralNet saved("saved_actor",
419 1,
420 1,
421 {2},
422 {"linear"},
423 {},
424 {},
425 torch::kCPU,
426 torch::kDouble,
427 true,
428 {1.0},
429 {2.0},
430 {1.0});
431
433 torch::save(std::make_shared<Moose::LibtorchActorNeuralNet>(saved), archive.path().string());
434
435 TestableLibtorchActorNeuralNet restored("restored_actor",
436 1,
437 1,
438 {2},
439 {"linear"},
440 {-2.0},
441 {2.0},
442 torch::kCPU,
443 torch::kDouble,
444 true,
445 {1.0},
446 {2.0},
447 {1.0});
448
449 EXPECT_ANY_THROW(Moose::loadLibtorchActorNeuralNetState(restored, archive.path().string()));
450}
451
452#endif
TEST(LibtorchActorNeuralNetTest, artificialNetAppliesAffineInputAndOutputScaling)
Feed-forward actor network coupled to a Gaussian or Beta action distribution.
LibtorchActorNeuralNet(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 std::vector< Real > &minimum_values={}, const std::vector< Real > &maximum_values={}, 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={}, const bool state_independent_std=true)
Build an actor network with either a Gaussian or Beta action distribution.
std::vector< torch::nn::Linear > _weights
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={})
at::Generator makeLibtorchCPUGenerator()
void loadLibtorchActorNeuralNetState(Moose::LibtorchActorNeuralNet &nn, const std::string &filename)
Load an actor checkpoint written as a native libtorch state archive.
const Real pi
DIE A HORRIBLE DEATH HERE typedef LIBMESH_DEFAULT_SCALAR_TYPE Real