Browse code

Merge branch 'test_loss_functions' into 'master'

test all the different loss functions in each test

See merge request qt/adaptive!135

Bas Nijholt authored on 26/11/2018 13:26:22
Showing 2 changed files
... ...
@@ -123,6 +123,7 @@ def triangle_loss(xs, ys):
123 123
 
124 124
 
125 125
 def curvature_loss_function(area_factor=1, euclid_factor=0.02, horizontal_factor=0.02):
126
+    # XXX: add a doc-string
126 127
     @uses_nth_neighbors(1)
127 128
     def curvature_loss(xs, ys):
128 129
         xs_middle = xs[1:3]
... ...
@@ -227,6 +228,11 @@ class Learner1D(BaseLearner):
227 228
         self.losses = {}
228 229
         self.losses_combined = {}
229 230
 
231
+        # When the scale changes by a factor 2, the losses are
232
+        # recomputed. This is tunable such that we can test
233
+        # the learners behavior in the tests.
234
+        self._recompute_losses_factor = 2
235
+
230 236
         self.data = {}
231 237
         self.pending_points = set()
232 238
 
... ...
@@ -446,7 +452,7 @@ class Learner1D(BaseLearner):
446 452
         self._update_losses(x, real=True)
447 453
 
448 454
         # If the scale has increased enough, recompute all losses.
449
-        if self._scale[1] > 2 * self._oldscale[1]:
455
+        if self._scale[1] > self._recompute_losses_factor * self._oldscale[1]:
450 456
 
451 457
             for interval in self.losses:
452 458
                 self._update_interpolated_loss_in_interval(*interval)
... ...
@@ -562,8 +568,13 @@ class Learner1D(BaseLearner):
562 568
         def finite_loss(loss, xs):
563 569
             # If the loss is infinite we return the
564 570
             # distance between the two points.
565
-            return (loss if not math.isinf(loss)
566
-                    else (xs[1] - xs[0]) / self._scale[0])
571
+            if math.isinf(loss):
572
+                loss = (xs[1] - xs[0]) / self._scale[0]
573
+
574
+            # We round the loss to 12 digits such that losses
575
+            # are equal up to numerical precision will be considered
576
+            # equal.
577
+            return round(loss, ndigits=12)
567 578
 
568 579
         quals = [(-finite_loss(loss, x), x, 1)
569 580
                  for x, loss in self.losses_combined.items()]
... ...
@@ -15,18 +15,39 @@ import numpy as np
15 15
 import pytest
16 16
 import scipy.spatial
17 17
 
18
-from ..learner import (AverageLearner, BalancingLearner, DataSaver,
18
+import adaptive
19
+from adaptive.learner import (AverageLearner, BalancingLearner, DataSaver,
19 20
     IntegratorLearner, Learner1D, Learner2D, LearnerND)
20
-from ..runner import simple
21
+from adaptive.runner import simple
21 22
 
22 23
 
23 24
 try:
24 25
     import skopt
25
-    from ..learner import SKOptLearner
26
+    from adaptive.learner import SKOptLearner
26 27
 except ModuleNotFoundError:
27 28
     SKOptLearner = None
28 29
 
29 30
 
31
+LOSS_FUNCTIONS = {
32
+    Learner1D: ('loss_per_interval', (
33
+        adaptive.learner.learner1D.default_loss,
34
+        adaptive.learner.learner1D.uniform_loss,
35
+        adaptive.learner.learner1D.curvature_loss_function(),
36
+    )),
37
+    Learner2D: ('loss_per_triangle', (
38
+        adaptive.learner.learner2D.default_loss,
39
+        adaptive.learner.learner2D.uniform_loss,
40
+        adaptive.learner.learner2D.minimize_triangle_surface_loss,
41
+        adaptive.learner.learner2D.resolution_loss_function(),
42
+    )),
43
+    LearnerND: ('loss_per_simplex', (
44
+        adaptive.learner.learnerND.default_loss,
45
+        adaptive.learner.learnerND.std_loss,
46
+        adaptive.learner.learnerND.uniform_loss,
47
+    )),
48
+}
49
+
50
+
30 51
 def generate_random_parametrization(f):
31 52
     """Return a realization of 'f' with parameters bound to random values.
32 53
 
... ...
@@ -74,7 +95,6 @@ def maybe_skip(learner):
74 95
 # All parameters except the first must be annotated with a callable that
75 96
 # returns a random value for that parameter.
76 97
 
77
-
78 98
 @learn_with(Learner1D, bounds=(-1, 1))
79 99
 def quadratic(x, m: uniform(0, 10), b: uniform(0, 1)):
80 100
     return m * x**2 + b
... ...
@@ -108,20 +128,33 @@ def gaussian(n):
108 128
 
109 129
 # Decorators for tests.
110 130
 
111
-def run_with(*learner_types):
131
+
132
+# Create a sequence of learner parameters by adding all
133
+# possible loss functions to an existing parameter set.
134
+def add_loss_to_params(learner_type, existing_params):
135
+    if learner_type not in LOSS_FUNCTIONS:
136
+        return [existing_params]
137
+    loss_param, loss_functions = LOSS_FUNCTIONS[learner_type]
138
+    loss_params = [{loss_param: f} for f in loss_functions]
139
+    return [dict(**existing_params, **lp) for lp in loss_params]
140
+
141
+
142
+def run_with(*learner_types, with_all_loss_functions=True):
112 143
     pars = []
113 144
     for l in learner_types:
114 145
         has_marker = isinstance(l, tuple)
115 146
         if has_marker:
116 147
             marker, l = l
117 148
         for f, k in learner_function_combos[l]:
118
-            # Check if learner was marked with our `xfail` decorator
119
-            # XXX: doesn't work when feeding kwargs to xfail.
120
-            if has_marker:
121
-                pars.append(pytest.param(l, f, dict(k),
122
-                                         marks=[marker]))
123
-            else:
124
-                pars.append((l, f, dict(k)))
149
+            ks = add_loss_to_params(l, k) if with_all_loss_functions else [k]
150
+            for k in ks:
151
+                # Check if learner was marked with our `xfail` decorator
152
+                # XXX: doesn't work when feeding kwargs to xfail.
153
+                if has_marker:
154
+                    pars.append(pytest.param(l, f, dict(k),
155
+                                             marks=[marker]))
156
+                else:
157
+                    pars.append((l, f, dict(k)))
125 158
     return pytest.mark.parametrize('learner_type, f, learner_kwargs', pars)
126 159
 
127 160
 
... ...
@@ -196,22 +229,19 @@ def test_learner_accepts_lists(learner_type, bounds):
196 229
     simple(learner, goal=lambda l: l.npoints > 10)
197 230
 
198 231
 
199
-@run_with(xfail(Learner1D), Learner2D, LearnerND)
232
+@run_with(Learner1D, Learner2D, LearnerND)
200 233
 def test_adding_existing_data_is_idempotent(learner_type, f, learner_kwargs):
201 234
     """Adding already existing data is an idempotent operation.
202 235
 
203 236
     Either it is idempotent, or it is an error.
204 237
     This is the only sane behaviour.
205
-
206
-    This test will fail for the Learner1D because the losses are normalized by
207
-    _scale which is updated after every point. After one iteration of adding
208
-    points, the _scale could be different from what it was when calculating
209
-    the losses of the intervals. Readding the points a second time means
210
-    that the losses are now all normalized by the correct _scale.
211 238
     """
212 239
     f = generate_random_parametrization(f)
213 240
     learner = learner_type(f, **learner_kwargs)
214 241
     control = learner_type(f, **learner_kwargs)
242
+    if learner_type is Learner1D:
243
+        learner._recompute_losses_factor = 1
244
+        control._recompute_losses_factor = 1
215 245
 
216 246
     N = random.randint(10, 30)
217 247
     control.ask(N)
... ...
@@ -265,14 +295,11 @@ def test_adding_non_chosen_data(learner_type, f, learner_kwargs):
265 295
     assert set(pls) == set(cpls)
266 296
 
267 297
 
268
-@run_with(xfail(Learner1D), xfail(Learner2D), xfail(LearnerND), AverageLearner)
298
+@run_with(Learner1D, xfail(Learner2D), xfail(LearnerND), AverageLearner)
269 299
 def test_point_adding_order_is_irrelevant(learner_type, f, learner_kwargs):
270 300
     """The order of calls to 'tell' between calls to 'ask'
271 301
     is arbitrary.
272 302
 
273
-    This test will fail for the Learner1D for the same reason as described in
274
-    the doc-string in `test_adding_existing_data_is_idempotent`.
275
-
276 303
     This test will fail for the Learner2D because
277 304
     `interpolate.interpnd.estimate_gradients_2d_global` will give different
278 305
     outputs based on the order of the triangles and values in
... ...
@@ -282,6 +309,10 @@ def test_point_adding_order_is_irrelevant(learner_type, f, learner_kwargs):
282 309
     learner = learner_type(f, **learner_kwargs)
283 310
     control = learner_type(f, **learner_kwargs)
284 311
 
312
+    if learner_type is Learner1D:
313
+        learner._recompute_losses_factor = 1
314
+        control._recompute_losses_factor = 1
315
+
285 316
     N = random.randint(10, 30)
286 317
     control.ask(N)
287 318
     xs, _ = learner.ask(N)
... ...
@@ -353,7 +384,7 @@ def test_learner_performance_is_invariant_under_scaling(learner_type, f, learner
353 384
     learner = learner_type(lambda x: yscale * f(np.array(x) / xscale),
354 385
                            **l_kwargs)
355 386
 
356
-    npoints = random.randrange(1000, 2000)
387
+    npoints = random.randrange(300, 500)
357 388
 
358 389
     for n in range(npoints):
359 390
         cxs, _ = control.ask(1)
... ...
@@ -366,10 +397,11 @@ def test_learner_performance_is_invariant_under_scaling(learner_type, f, learner
366 397
         assert np.allclose(xs_unscaled, cxs)
367 398
 
368 399
     # Check if the losses are close
369
-    assert abs(learner.loss() - control.loss()) / learner.loss() < 1e-11
400
+    assert math.isclose(learner.loss(), control.loss(), rel_tol=1e-10)
370 401
 
371 402
 
372
-@run_with(Learner1D, Learner2D, LearnerND, AverageLearner)
403
+@run_with(Learner1D, Learner2D, LearnerND, AverageLearner,
404
+    with_all_loss_functions=False)
373 405
 def test_balancing_learner(learner_type, f, learner_kwargs):
374 406
     """Test if the BalancingLearner works with the different types of learners."""
375 407
     learners = [learner_type(generate_random_parametrization(f), **learner_kwargs)
... ...
@@ -403,19 +435,22 @@ def test_balancing_learner(learner_type, f, learner_kwargs):
403 435
 
404 436
 
405 437
 @run_with(Learner1D, Learner2D, LearnerND, AverageLearner,
406
-    maybe_skip(SKOptLearner), IntegratorLearner)
438
+    maybe_skip(SKOptLearner), IntegratorLearner,
439
+    with_all_loss_functions=False)
407 440
 def test_saving(learner_type, f, learner_kwargs):
408 441
     f = generate_random_parametrization(f)
409 442
     learner = learner_type(f, **learner_kwargs)
410 443
     control = learner_type(f, **learner_kwargs)
444
+    if learner_type is Learner1D:
445
+        learner._recompute_losses_factor = 1
446
+        control._recompute_losses_factor = 1
411 447
     simple(learner, lambda l: l.npoints > 100)
412 448
     fd, path = tempfile.mkstemp()
413 449
     try:
414 450
         learner.save(path)
415 451
         control.load(path)
416
-        if learner_type is not Learner1D:
417
-            # Because different scales result in differnt losses
418
-            np.testing.assert_almost_equal(learner.loss(), control.loss())
452
+
453
+        np.testing.assert_almost_equal(learner.loss(), control.loss())
419 454
 
420 455
         # Try if the control is runnable
421 456
         simple(control, lambda l: l.npoints > 200)
... ...
@@ -424,12 +459,18 @@ def test_saving(learner_type, f, learner_kwargs):
424 459
 
425 460
 
426 461
 @run_with(Learner1D, Learner2D, LearnerND, AverageLearner,
427
-    maybe_skip(SKOptLearner), IntegratorLearner)
462
+    maybe_skip(SKOptLearner), IntegratorLearner,
463
+    with_all_loss_functions=False)
428 464
 def test_saving_of_balancing_learner(learner_type, f, learner_kwargs):
429 465
     f = generate_random_parametrization(f)
430 466
     learner = BalancingLearner([learner_type(f, **learner_kwargs)])
431 467
     control = BalancingLearner([learner_type(f, **learner_kwargs)])
432 468
 
469
+    if learner_type is Learner1D:
470
+        for l, c in zip(learner.learners, control.learners):
471
+            l._recompute_losses_factor = 1
472
+            c._recompute_losses_factor = 1
473
+
433 474
     simple(learner, lambda l: l.learners[0].npoints > 100)
434 475
     folder = tempfile.mkdtemp()
435 476
 
... ...
@@ -437,11 +478,10 @@ def test_saving_of_balancing_learner(learner_type, f, learner_kwargs):
437 478
         return folder + 'test'
438 479
 
439 480
     try:
440
-        learner.save(fname)
441
-        control.load(fname)
442
-        if learner_type is not Learner1D:
443
-            # Because different scales result in differnt losses
444
-            np.testing.assert_almost_equal(learner.loss(), control.loss())
481
+        learner.save(fname=fname)
482
+        control.load(fname=fname)
483
+
484
+        np.testing.assert_almost_equal(learner.loss(), control.loss())
445 485
 
446 486
         # Try if the control is runnable
447 487
         simple(control, lambda l: l.learners[0].npoints > 200)
... ...
@@ -450,21 +490,27 @@ def test_saving_of_balancing_learner(learner_type, f, learner_kwargs):
450 490
 
451 491
 
452 492
 @run_with(Learner1D, Learner2D, LearnerND, AverageLearner,
453
-    maybe_skip(SKOptLearner), IntegratorLearner)
493
+    maybe_skip(SKOptLearner), IntegratorLearner,
494
+    with_all_loss_functions=False)
454 495
 def test_saving_with_datasaver(learner_type, f, learner_kwargs):
455 496
     f = generate_random_parametrization(f)
456 497
     g = lambda x: {'y': f(x), 't': random.random()}
457 498
     arg_picker = operator.itemgetter('y')
458 499
     learner = DataSaver(learner_type(g, **learner_kwargs), arg_picker)
459 500
     control = DataSaver(learner_type(g, **learner_kwargs), arg_picker)
501
+
502
+    if learner_type is Learner1D:
503
+        learner.learner._recompute_losses_factor = 1
504
+        control.learner._recompute_losses_factor = 1
505
+
460 506
     simple(learner, lambda l: l.npoints > 100)
461 507
     fd, path = tempfile.mkstemp()
462 508
     try:
463 509
         learner.save(path)
464 510
         control.load(path)
465
-        if learner_type is not Learner1D:
466
-            # Because different scales result in differnt losses
467
-            np.testing.assert_almost_equal(learner.loss(), control.loss())
511
+
512
+        np.testing.assert_almost_equal(learner.loss(), control.loss())
513
+
468 514
         assert learner.extra_data == control.extra_data
469 515
 
470 516
         # Try if the control is runnable