Completed
Push — main ( de3728...ca54a2 )
by Yunguan
19s queued 13s
created

TestCrossEntropy.testcall()   A

Complexity

Conditions 1

Size

Total Lines 15
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 13
dl 0
loc 15
rs 9.75
c 0
b 0
f 0
cc 1
nop 6
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 TestSumSquaredDistance:
20
    @pytest.mark.parametrize(
21
        "y_true,y_pred,shape,expected",
22
        [
23
            (0.6, 0.3, (3,), 0.09),
24
            (0.6, 0.3, (3, 3), 0.09),
25
            (0.6, 0.3, (3, 3, 3), 0.09),
26
            (0.6, 0.3, (3, 3, 3), 0.09),
27
            (0.5, 0.5, (3, 3), 0.0),
28
            (0.3, 0.6, (3, 3), 0.09),
29
        ],
30
    )
31
    def test_output(self, y_true, y_pred, shape, expected):
32
        y_true = y_true * np.ones(shape=shape)
33
        y_pred = y_pred * np.ones(shape=shape)
34
        expected = expected * np.ones(shape=(shape[0],))
35
        got = label.SumSquaredDifference().call(
36
            y_true,
37
            y_pred,
38
        )
39
        assert is_equal_tf(got, expected)
40
41
42
class TestDiceScore:
43 View Code Duplication
    @pytest.mark.parametrize(
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
44
        ("value", "smooth_nr", "smooth_dr", "expected"),
45
        [
46
            (0, 1e-5, 1e-5, 1),
47
            (0, 0, 1e-5, 0),
48
            (0, 1e-5, 0, np.inf),
49
            (0, 0, 0, np.nan),
50
            (0, 1e-7, 1e-7, 1),
51
            (1, 1e-5, 1e-5, 1),
52
            (1, 0, 1e-5, 1),
53
            (1, 1e-5, 0, 1),
54
            (1, 0, 0, 1),
55
            (1, 1e-7, 1e-7, 1),
56
        ],
57
    )
58
    def test_smooth(
59
        self,
60
        value: float,
61
        smooth_nr: float,
62
        smooth_dr: float,
63
        expected: float,
64
    ):
65
        """
66
        Test values in extreme cases where numerator/denominator are all zero.
67
68
        :param value: value for input.
69
        :param smooth_nr: constant for numerator.
70
        :param smooth_dr: constant for denominator.
71
        :param expected: target value.
72
        """
73
        shape = (1, 10)
74
        y_true = tf.ones(shape=shape) * value
75
        y_pred = tf.ones(shape=shape) * value
76
77
        got = label.DiceScore(smooth_nr=smooth_nr, smooth_dr=smooth_dr).call(
78
            y_true,
79
            y_pred,
80
        )
81
        expected = tf.constant(expected)
82
        assert is_equal_tf(got[0], expected)
83
84 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...
85
    @pytest.mark.parametrize("background_weight", [0.0, 0.1, 0.5, 1.0])
86
    @pytest.mark.parametrize("shape", [(1,), (10,), (100,), (2, 3), (2, 3, 4)])
87
    def test_exact_value(self, binary: bool, background_weight: float, shape: Tuple):
88
        """
89
        Test dice score by comparing at ground truth values.
90
91
        :param binary: if project labels to binary values.
92
        :param background_weight: the weight of background class.
93
        :param shape: shape of input.
94
        """
95
        # init
96
        shape = (1,) + shape  # add batch axis
97
        foreground_weight = 1 - background_weight
98
        tf.random.set_seed(0)
99
        y_true = tf.random.uniform(shape=shape)
100
        y_pred = tf.random.uniform(shape=shape)
101
102
        # obtained value
103
        got = label.DiceScore(
104
            binary=binary,
105
            background_weight=background_weight,
106
        ).call(y_true=y_true, y_pred=y_pred)
107
108
        # expected value
109
        flatten = tf.keras.layers.Flatten()
110
        y_true = flatten(y_true)
111
        y_pred = flatten(y_pred)
112
        if binary:
113
            y_true = tf.cast(y_true >= 0.5, dtype=y_true.dtype)
114
            y_pred = tf.cast(y_pred >= 0.5, dtype=y_pred.dtype)
115
116
        num = foreground_weight * tf.reduce_sum(
117
            y_true * y_pred, axis=1
118
        ) + background_weight * tf.reduce_sum((1 - y_true) * (1 - y_pred), axis=1)
119
        num *= 2
120
        denom = foreground_weight * tf.reduce_sum(
121
            y_true + y_pred, axis=1
122
        ) + background_weight * tf.reduce_sum((1 - y_true) + (1 - y_pred), axis=1)
123
        expected = (num + EPS) / (denom + EPS)
124
125
        assert is_equal_tf(got, expected)
126
127
    def test_get_config(self):
128
        got = label.DiceScore().get_config()
129
        expected = dict(
130
            binary=False,
131
            background_weight=0.0,
132
            smooth_nr=1e-5,
133
            smooth_dr=1e-5,
134
            reduction=tf.keras.losses.Reduction.AUTO,
135
            name="DiceScore",
136
        )
137
        assert got == expected
138
139
    @pytest.mark.parametrize("background_weight", [-0.1, 1.1])
140
    def test_background_weight_err(self, background_weight: float):
141
        """
142
        Test the error message when using wrong background weight.
143
144
        :param background_weight: weight for background class.
145
        """
146
        with pytest.raises(ValueError) as err_info:
147
            label.DiceScore(background_weight=background_weight)
148
        assert "The background weight for Dice Score must be within [0, 1]" in str(
149
            err_info.value
150
        )
151
152
153
class TestCrossEntropy:
154
    shape = (3, 3, 3, 3)
155
156
    @pytest.fixture()
157
    def y_true(self):
158
        return np.ones(shape=self.shape) * 0.6
159
160
    @pytest.fixture()
161
    def y_pred(self):
162
        return np.ones(shape=self.shape) * 0.3
163
164
    @pytest.mark.parametrize(
165
        ("value", "smooth", "expected"),
166
        [
167
            (0, 1e-5, 0),
168
            (0, 0, np.nan),
169
            (0, 1e-7, 0),
170
            (1, 1e-5, -np.log(1 + 1e-5)),
171
            (1, 0, 0),
172
            (1, 1e-7, -np.log(1 + 1e-7)),
173
        ],
174
    )
175
    def test_smooth(
176
        self,
177
        value: float,
178
        smooth: float,
179
        expected: float,
180
    ):
181
        """
182
        Test values in extreme cases where numerator/denominator are all zero.
183
184
        :param value: value for input.
185
        :param smooth: constant for log.
186
        :param expected: target value.
187
        """
188
        shape = (1, 10)
189
        y_true = tf.ones(shape=shape) * value
190
        y_pred = tf.ones(shape=shape) * value
191
192
        got = label.CrossEntropy(smooth=smooth).call(
193
            y_true,
194
            y_pred,
195
        )
196
        expected = tf.constant(expected)
197
        assert is_equal_tf(got[0], expected)
198
199
    @pytest.mark.parametrize(
200
        "binary,background_weight,expected",
201
        [
202
            (True, 0.0, -np.log(EPS)),
203
            (False, 0.0, -0.6 * np.log(0.3 + EPS)),
204
            (False, 0.2, -0.48 * np.log(0.3 + EPS) - 0.08 * np.log(0.7 + EPS)),
205
        ],
206
    )
207
    def testcall(self, y_true, y_pred, binary, background_weight, expected):
208
        expected = np.array([expected] * self.shape[0])  # call returns (batch, )
209
        got = label.CrossEntropy(
210
            binary=binary,
211
            background_weight=background_weight,
212
        ).call(y_true=y_true, y_pred=y_pred)
213
        assert is_equal_tf(got, expected)
214
215
    def test_get_config(self):
216
        got = label.CrossEntropy().get_config()
217
        expected = dict(
218
            binary=False,
219
            background_weight=0.0,
220
            smooth=1e-5,
221
            reduction=tf.keras.losses.Reduction.AUTO,
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
            reduction=tf.keras.losses.Reduction.AUTO,
333
            name="JaccardIndex",
334
        )
335
        assert got == expected
336
337
338
def test_foreground_prop_binary():
339
    """
340
    Test foreground function with a
341
    tensor of zeros with some ones, asserting
342
    equal to known precomputed tensor.
343
    Testing with binary case.
344
    """
345
    array_eye = np.identity(3, dtype=np.float32)
346
    tensor_eye = np.zeros((3, 3, 3, 3), dtype=np.float32)
347
    tensor_eye[:, :, 0:3, 0:3] = array_eye
348
    expect = tf.convert_to_tensor([1.0 / 3, 1.0 / 3, 1.0 / 3], dtype=tf.float32)
349
    get = label.foreground_proportion(tensor_eye)
350
    assert is_equal_tf(get, expect)
351
352
353
def test_foreground_prop_simple():
354
    """
355
    Test foreground functions with a tensor
356
    of zeros with some ones and some values below
357
    one to assert the thresholding works.
358
    """
359
    array_eye = np.identity(3, dtype=np.float32)
360
    tensor_eye = np.zeros((3, 3, 3, 3), dtype=np.float32)
361
    tensor_eye[:, 0, :, :] = 0.4 * array_eye  # 0
362
    tensor_eye[:, 1, :, :] = array_eye
363
    tensor_eye[:, 2, :, :] = array_eye
364
    tensor_eye = tf.convert_to_tensor(tensor_eye, dtype=tf.float32)
365
    expect = [54 / (27 * 9), 54 / (27 * 9), 54 / (27 * 9)]
366
    get = label.foreground_proportion(tensor_eye)
367
    assert is_equal_tf(get, expect)
368
369
370
def test_compute_centroid():
371
    """
372
    Testing compute centroid function
373
    and comparing to expected values.
374
    """
375
    tensor_mask = np.zeros((3, 2, 2, 2))
376
    tensor_mask[0, :, :, :] = np.ones((2, 2, 2))
377
    tensor_mask = tf.constant(tensor_mask, dtype=tf.float32)
378
379
    tensor_grid = np.ones((1, 2, 2, 2, 3))
380
    tensor_grid[:, :, :, :, 1] *= 2
381
    tensor_grid[:, :, :, :, 2] *= 3
382
    tensor_grid = tf.constant(tensor_grid, dtype=tf.float32)
383
384
    expected = np.ones((3, 3))  # use 1 because 0/0 ~= (0+eps)/(0+eps) = 1
385
    expected[0, :] = [1, 2, 3]
386
    got = label.compute_centroid(tensor_mask, tensor_grid)
387
    assert is_equal_tf(got, expected)
388
389
390
def test_compute_centroid_d():
391
    """
392
    Testing compute centroid distance between equal
393
    tensors returns 0s.
394
    """
395
    array_ones = np.ones((2, 2))
396
    tensor_mask = np.zeros((3, 2, 2, 2))
397
    tensor_mask[0, :, :, :] = array_ones
398
    tensor_mask = tf.convert_to_tensor(tensor_mask, dtype=tf.float32)
399
400
    tensor_grid = np.zeros((1, 2, 2, 2, 3))
401
    tensor_grid[:, :, :, :, 0] = array_ones
402
    tensor_grid = tf.convert_to_tensor(tensor_grid, dtype=tf.float32)
403
404
    get = label.compute_centroid_distance(tensor_mask, tensor_mask, tensor_grid)
405
    expect = np.zeros((3))
406
    assert is_equal_tf(get, expect)
407