Completed
Push — main ( bbe77b...232bc8 )
by Yunguan
22s queued 13s
created

TestCrossEntropy.test_smooth()   A

Complexity

Conditions 1

Size

Total Lines 34
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 22
dl 0
loc 34
rs 9.352
c 0
b 0
f 0
cc 1
nop 4
1
# coding=utf-8
2
3
"""
4
Tests for deepreg/model/loss/label.py in
5
pytest style
6
"""
7
8
from test.unit.util import is_equal_tf
9
from typing import Tuple
10
11
import numpy as np
12
import pytest
13
import tensorflow as tf
14
15
import deepreg.loss.label as label
16
from deepreg.constant import EPS
17
18
19
class TestMultiScaleLoss:
20
    def test_call(self):
21
        loss = label.MultiScaleLoss()
22
        with pytest.raises(NotImplementedError):
23
            loss.call(0, 0)
24
25
    def test_get_config(self):
26
        loss = label.MultiScaleLoss()
27
        got = loss.get_config()
28
        expected = dict(
29
            scales=None,
30
            kernel="gaussian",
31
            reduction=tf.keras.losses.Reduction.NONE,
32
            name="MultiScaleLoss",
33
        )
34
        assert got == expected
35
36
37
class TestDiceScore:
38 View Code Duplication
    @pytest.mark.parametrize(
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
39
        ("value", "smooth_nr", "smooth_dr", "expected"),
40
        [
41
            (0, 1e-5, 1e-5, 1),
42
            (0, 0, 1e-5, 0),
43
            (0, 1e-5, 0, np.inf),
44
            (0, 0, 0, np.nan),
45
            (0, 1e-7, 1e-7, 1),
46
            (1, 1e-5, 1e-5, 1),
47
            (1, 0, 1e-5, 1),
48
            (1, 1e-5, 0, 1),
49
            (1, 0, 0, 1),
50
            (1, 1e-7, 1e-7, 1),
51
        ],
52
    )
53
    def test_smooth(
54
        self,
55
        value: float,
56
        smooth_nr: float,
57
        smooth_dr: float,
58
        expected: float,
59
    ):
60
        """
61
        Test values in extreme cases where numerator/denominator are all zero.
62
63
        :param value: value for input.
64
        :param smooth_nr: constant for numerator.
65
        :param smooth_dr: constant for denominator.
66
        :param expected: target value.
67
        """
68
        shape = (1, 10)
69
        y_true = tf.ones(shape=shape) * value
70
        y_pred = tf.ones(shape=shape) * value
71
72
        got = label.DiceScore(smooth_nr=smooth_nr, smooth_dr=smooth_dr)._call(
73
            y_true,
74
            y_pred,
75
        )
76
        expected = tf.constant(expected)
77
        assert is_equal_tf(got[0], expected)
78
79 View Code Duplication
    @pytest.mark.parametrize("binary", [True, False])
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
80
    @pytest.mark.parametrize("background_weight", [0.0, 0.1, 0.5, 1.0])
81
    @pytest.mark.parametrize("shape", [(1,), (10,), (100,), (2, 3), (2, 3, 4)])
82
    def test_exact_value(self, binary: bool, background_weight: float, shape: Tuple):
83
        """
84
        Test dice score by comparing at ground truth values.
85
86
        :param binary: if project labels to binary values.
87
        :param background_weight: the weight of background class.
88
        :param shape: shape of input.
89
        """
90
        # init
91
        shape = (1,) + shape  # add batch axis
92
        foreground_weight = 1 - background_weight
93
        tf.random.set_seed(0)
94
        y_true = tf.random.uniform(shape=shape)
95
        y_pred = tf.random.uniform(shape=shape)
96
97
        # obtained value
98
        got = label.DiceScore(
99
            binary=binary,
100
            background_weight=background_weight,
101
        ).call(y_true=y_true, y_pred=y_pred)
102
103
        # expected value
104
        flatten = tf.keras.layers.Flatten()
105
        y_true = flatten(y_true)
106
        y_pred = flatten(y_pred)
107
        if binary:
108
            y_true = tf.cast(y_true >= 0.5, dtype=y_true.dtype)
109
            y_pred = tf.cast(y_pred >= 0.5, dtype=y_pred.dtype)
110
111
        num = foreground_weight * tf.reduce_sum(
112
            y_true * y_pred, axis=1
113
        ) + background_weight * tf.reduce_sum((1 - y_true) * (1 - y_pred), axis=1)
114
        num *= 2
115
        denom = foreground_weight * tf.reduce_sum(
116
            y_true + y_pred, axis=1
117
        ) + background_weight * tf.reduce_sum((1 - y_true) + (1 - y_pred), axis=1)
118
        expected = (num + EPS) / (denom + EPS)
119
120
        assert is_equal_tf(got, expected)
121
122
    def test_get_config(self):
123
        got = label.DiceScore().get_config()
124
        expected = dict(
125
            binary=False,
126
            background_weight=0.0,
127
            smooth_nr=1e-5,
128
            smooth_dr=1e-5,
129
            scales=None,
130
            kernel="gaussian",
131
            reduction=tf.keras.losses.Reduction.NONE,
132
            name="DiceScore",
133
        )
134
        assert got == expected
135
136
    @pytest.mark.parametrize("background_weight", [-0.1, 1.1])
137
    def test_background_weight_err(self, background_weight: float):
138
        """
139
        Test the error message when using wrong background weight.
140
141
        :param background_weight: weight for background class.
142
        """
143
        with pytest.raises(ValueError) as err_info:
144
            label.DiceScore(background_weight=background_weight)
145
        assert "The background weight for Dice Score must be within [0, 1]" in str(
146
            err_info.value
147
        )
148
149
150
class TestCrossEntropy:
151
    shape = (3, 3, 3, 3)
152
153
    @pytest.fixture()
154
    def y_true(self):
155
        return np.ones(shape=self.shape) * 0.6
156
157
    @pytest.fixture()
158
    def y_pred(self):
159
        return np.ones(shape=self.shape) * 0.3
160
161
    @pytest.mark.parametrize(
162
        ("value", "smooth", "expected"),
163
        [
164
            (0, 1e-5, 0),
165
            (0, 0, np.nan),
166
            (0, 1e-7, 0),
167
            (1, 1e-5, -np.log(1 + 1e-5)),
168
            (1, 0, 0),
169
            (1, 1e-7, -np.log(1 + 1e-7)),
170
        ],
171
    )
172
    def test_smooth(
173
        self,
174
        value: float,
175
        smooth: float,
176
        expected: float,
177
    ):
178
        """
179
        Test values in extreme cases where numerator/denominator are all zero.
180
181
        :param value: value for input.
182
        :param smooth: constant for log.
183
        :param expected: target value.
184
        """
185
        shape = (1, 10)
186
        y_true = tf.ones(shape=shape) * value
187
        y_pred = tf.ones(shape=shape) * value
188
189
        got = label.CrossEntropy(smooth=smooth)._call(
190
            y_true,
191
            y_pred,
192
        )
193
        expected = tf.constant(expected)
194
        assert is_equal_tf(got[0], expected)
195
196
    @pytest.mark.parametrize(
197
        "binary,background_weight,scales,expected",
198
        [
199
            (True, 0.0, None, -np.log(EPS)),
200
            (False, 0.0, None, -0.6 * np.log(0.3 + EPS)),
201
            (False, 0.2, None, -0.48 * np.log(0.3 + EPS) - 0.08 * np.log(0.7 + EPS)),
202
            (False, 0.2, [0, 0], -0.48 * np.log(0.3 + EPS) - 0.08 * np.log(0.7 + EPS)),
203
            (False, 0.2, [0, 1], 0.5239465),
204
        ],
205
    )
206
    def test_call(self, y_true, y_pred, binary, background_weight, scales, expected):
207
        expected = np.array([expected] * self.shape[0])  # call returns (batch, )
208
        got = label.CrossEntropy(
209
            binary=binary, background_weight=background_weight, scales=scales
210
        ).call(y_true=y_true, y_pred=y_pred)
211
        assert is_equal_tf(got, expected)
212
213
    def test_get_config(self):
214
        got = label.CrossEntropy().get_config()
215
        expected = dict(
216
            binary=False,
217
            background_weight=0.0,
218
            smooth=1e-5,
219
            scales=None,
220
            kernel="gaussian",
221
            reduction=tf.keras.losses.Reduction.NONE,
222
            name="CrossEntropy",
223
        )
224
        assert got == expected
225
226
    @pytest.mark.parametrize("background_weight", [-0.1, 1.1])
227
    def test_background_weight_err(self, background_weight: float):
228
        """
229
        Test the error message when using wrong background weight.
230
231
        :param background_weight: weight for background class.
232
        """
233
        with pytest.raises(ValueError) as err_info:
234
            label.CrossEntropy(background_weight=background_weight)
235
        assert "The background weight for Cross Entropy must be within [0, 1]" in str(
236
            err_info.value
237
        )
238
239
240
class TestJaccardIndex:
241 View Code Duplication
    @pytest.mark.parametrize(
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
242
        ("value", "smooth_nr", "smooth_dr", "expected"),
243
        [
244
            (0, 1e-5, 1e-5, 1),
245
            (0, 0, 1e-5, 0),
246
            (0, 1e-5, 0, np.inf),
247
            (0, 0, 0, np.nan),
248
            (0, 1e-7, 1e-7, 1),
249
            (1, 1e-5, 1e-5, 1),
250
            (1, 0, 1e-5, 1),
251
            (1, 1e-5, 0, 1),
252
            (1, 0, 0, 1),
253
            (1, 1e-7, 1e-7, 1),
254
        ],
255
    )
256
    def test_smooth(
257
        self,
258
        value: float,
259
        smooth_nr: float,
260
        smooth_dr: float,
261
        expected: float,
262
    ):
263
        """
264
        Test values in extreme cases where numerator/denominator are all zero.
265
266
        :param value: value for input.
267
        :param smooth_nr: constant for numerator.
268
        :param smooth_dr: constant for denominator.
269
        :param expected: target value.
270
        """
271
        shape = (1, 10)
272
        y_true = tf.ones(shape=shape) * value
273
        y_pred = tf.ones(shape=shape) * value
274
275
        got = label.JaccardIndex(smooth_nr=smooth_nr, smooth_dr=smooth_dr)._call(
276
            y_true,
277
            y_pred,
278
        )
279
        expected = tf.constant(expected)
280
        assert is_equal_tf(got[0], expected)
281
282 View Code Duplication
    @pytest.mark.parametrize("binary", [True, False])
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
283
    @pytest.mark.parametrize("background_weight", [0.0, 0.1, 0.5, 1.0])
284
    @pytest.mark.parametrize("shape", [(1,), (10,), (100,), (2, 3), (2, 3, 4)])
285
    def test_exact_value(self, binary: bool, background_weight: float, shape: Tuple):
286
        """
287
        Test Jaccard index by comparing at ground truth values.
288
289
        :param binary: if project labels to binary values.
290
        :param background_weight: the weight of background class.
291
        :param shape: shape of input.
292
        """
293
        # init
294
        shape = (1,) + shape  # add batch axis
295
        foreground_weight = 1 - background_weight
296
        tf.random.set_seed(0)
297
        y_true = tf.random.uniform(shape=shape)
298
        y_pred = tf.random.uniform(shape=shape)
299
300
        # obtained value
301
        got = label.JaccardIndex(
302
            binary=binary,
303
            background_weight=background_weight,
304
        ).call(y_true=y_true, y_pred=y_pred)
305
306
        # expected value
307
        flatten = tf.keras.layers.Flatten()
308
        y_true = flatten(y_true)
309
        y_pred = flatten(y_pred)
310
        if binary:
311
            y_true = tf.cast(y_true >= 0.5, dtype=y_true.dtype)
312
            y_pred = tf.cast(y_pred >= 0.5, dtype=y_pred.dtype)
313
314
        num = foreground_weight * tf.reduce_sum(
315
            y_true * y_pred, axis=1
316
        ) + background_weight * tf.reduce_sum((1 - y_true) * (1 - y_pred), axis=1)
317
        denom = foreground_weight * tf.reduce_sum(
318
            y_true + y_pred, axis=1
319
        ) + background_weight * tf.reduce_sum((1 - y_true) + (1 - y_pred), axis=1)
320
        denom = denom - num
321
        expected = (num + EPS) / (denom + EPS)
322
323
        assert is_equal_tf(got, expected)
324
325
    def test_get_config(self):
326
        got = label.JaccardIndex().get_config()
327
        expected = dict(
328
            binary=False,
329
            background_weight=0.0,
330
            smooth_nr=1e-5,
331
            smooth_dr=1e-5,
332
            scales=None,
333
            kernel="gaussian",
334
            reduction=tf.keras.losses.Reduction.NONE,
335
            name="JaccardIndex",
336
        )
337
        assert got == expected
338
339
340
def test_foreground_prop_binary():
341
    """
342
    Test foreground function with a
343
    tensor of zeros with some ones, asserting
344
    equal to known precomputed tensor.
345
    Testing with binary case.
346
    """
347
    array_eye = np.identity(3, dtype=np.float32)
348
    tensor_eye = np.zeros((3, 3, 3, 3), dtype=np.float32)
349
    tensor_eye[:, :, 0:3, 0:3] = array_eye
350
    expect = tf.convert_to_tensor([1.0 / 3, 1.0 / 3, 1.0 / 3], dtype=tf.float32)
351
    get = label.foreground_proportion(tensor_eye)
352
    assert is_equal_tf(get, expect)
353
354
355
def test_foreground_prop_simple():
356
    """
357
    Test foreground functions with a tensor
358
    of zeros with some ones and some values below
359
    one to assert the thresholding works.
360
    """
361
    array_eye = np.identity(3, dtype=np.float32)
362
    tensor_eye = np.zeros((3, 3, 3, 3), dtype=np.float32)
363
    tensor_eye[:, 0, :, :] = 0.4 * array_eye  # 0
364
    tensor_eye[:, 1, :, :] = array_eye
365
    tensor_eye[:, 2, :, :] = array_eye
366
    tensor_eye = tf.convert_to_tensor(tensor_eye, dtype=tf.float32)
367
    expect = [54 / (27 * 9), 54 / (27 * 9), 54 / (27 * 9)]
368
    get = label.foreground_proportion(tensor_eye)
369
    assert is_equal_tf(get, expect)
370
371
372
def test_compute_centroid():
373
    """
374
    Testing compute centroid function
375
    and comparing to expected values.
376
    """
377
    tensor_mask = np.zeros((3, 2, 2, 2))
378
    tensor_mask[0, :, :, :] = np.ones((2, 2, 2))
379
    tensor_mask = tf.constant(tensor_mask, dtype=tf.float32)
380
381
    tensor_grid = np.ones((1, 2, 2, 2, 3))
382
    tensor_grid[:, :, :, :, 1] *= 2
383
    tensor_grid[:, :, :, :, 2] *= 3
384
    tensor_grid = tf.constant(tensor_grid, dtype=tf.float32)
385
386
    expected = np.ones((3, 3))  # use 1 because 0/0 ~= (0+eps)/(0+eps) = 1
387
    expected[0, :] = [1, 2, 3]
388
    got = label.compute_centroid(tensor_mask, tensor_grid)
389
    assert is_equal_tf(got, expected)
390
391
392
def test_compute_centroid_d():
393
    """
394
    Testing compute centroid distance between equal
395
    tensors returns 0s.
396
    """
397
    array_ones = np.ones((2, 2))
398
    tensor_mask = np.zeros((3, 2, 2, 2))
399
    tensor_mask[0, :, :, :] = array_ones
400
    tensor_mask = tf.convert_to_tensor(tensor_mask, dtype=tf.float32)
401
402
    tensor_grid = np.zeros((1, 2, 2, 2, 3))
403
    tensor_grid[:, :, :, :, 0] = array_ones
404
    tensor_grid = tf.convert_to_tensor(tensor_grid, dtype=tf.float32)
405
406
    get = label.compute_centroid_distance(tensor_mask, tensor_mask, tensor_grid)
407
    expect = np.zeros((3))
408
    assert is_equal_tf(get, expect)
409