Passed
Pull Request — main (#736)
by Yunguan
01:30
created

test.unit.test_loss_label.TestDiceScore.y_pred()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nop 1
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.SUM,
32
            name="MultiScaleLoss",
33
        )
34
        assert got == expected
35
36
37 View Code Duplication
class TestDiceScore:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
38
    @pytest.mark.parametrize(
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
    @pytest.mark.parametrize("binary", [True, False])
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.SUM,
132
            name="DiceScore",
133
        )
134
        assert got == expected
135
136
137
class TestCrossEntropy:
138
    shape = (3, 3, 3, 3)
139
140
    @pytest.fixture()
141
    def y_true(self):
142
        return np.ones(shape=self.shape) * 0.6
143
144
    @pytest.fixture()
145
    def y_pred(self):
146
        return np.ones(shape=self.shape) * 0.3
147
148
    @pytest.mark.parametrize(
149
        ("value", "smooth", "expected"),
150
        [
151
            (0, 1e-5, 0),
152
            (0, 0, np.nan),
153
            (0, 1e-7, 0),
154
            (1, 1e-5, -np.log(1 + 1e-5)),
155
            (1, 0, 0),
156
            (1, 1e-7, -np.log(1 + 1e-7)),
157
        ],
158
    )
159
    def test_smooth(
160
        self,
161
        value: float,
162
        smooth: float,
163
        expected: float,
164
    ):
165
        """
166
        Test values in extreme cases where numerator/denominator are all zero.
167
168
        :param value: value for input.
169
        :param smooth: constant for log.
170
        :param expected: target value.
171
        """
172
        shape = (1, 10)
173
        y_true = tf.ones(shape=shape) * value
174
        y_pred = tf.ones(shape=shape) * value
175
176
        got = label.CrossEntropy(smooth=smooth)._call(
177
            y_true,
178
            y_pred,
179
        )
180
        expected = tf.constant(expected)
181
        assert is_equal_tf(got[0], expected)
182
183
    @pytest.mark.parametrize(
184
        "binary,background_weight,scales,expected",
185
        [
186
            (True, 0.0, None, -np.log(EPS)),
187
            (False, 0.0, None, -0.6 * np.log(0.3 + EPS)),
188
            (False, 0.2, None, -0.48 * np.log(0.3 + EPS) - 0.08 * np.log(0.7 + EPS)),
189
            (False, 0.2, [0, 0], -0.48 * np.log(0.3 + EPS) - 0.08 * np.log(0.7 + EPS)),
190
            (False, 0.2, [0, 1], 0.5239465),
191
        ],
192
    )
193
    def test_call(self, y_true, y_pred, binary, background_weight, scales, expected):
194
        expected = np.array([expected] * self.shape[0])  # call returns (batch, )
195
        got = label.CrossEntropy(
196
            binary=binary, background_weight=background_weight, scales=scales
197
        ).call(y_true=y_true, y_pred=y_pred)
198
        assert is_equal_tf(got, expected)
199
200
    def test_get_config(self):
201
        got = label.CrossEntropy().get_config()
202
        expected = dict(
203
            binary=False,
204
            background_weight=0.0,
205
            smooth=1e-5,
206
            scales=None,
207
            kernel="gaussian",
208
            reduction=tf.keras.losses.Reduction.SUM,
209
            name="CrossEntropy",
210
        )
211
        assert got == expected
212
213
214 View Code Duplication
class TestJaccardIndex:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
215
    @pytest.mark.parametrize(
216
        ("value", "smooth_nr", "smooth_dr", "expected"),
217
        [
218
            (0, 1e-5, 1e-5, 1),
219
            (0, 0, 1e-5, 0),
220
            (0, 1e-5, 0, np.inf),
221
            (0, 0, 0, np.nan),
222
            (0, 1e-7, 1e-7, 1),
223
            (1, 1e-5, 1e-5, 1),
224
            (1, 0, 1e-5, 1),
225
            (1, 1e-5, 0, 1),
226
            (1, 0, 0, 1),
227
            (1, 1e-7, 1e-7, 1),
228
        ],
229
    )
230
    def test_smooth(
231
        self,
232
        value: float,
233
        smooth_nr: float,
234
        smooth_dr: float,
235
        expected: float,
236
    ):
237
        """
238
        Test values in extreme cases where numerator/denominator are all zero.
239
240
        :param value: value for input.
241
        :param smooth_nr: constant for numerator.
242
        :param smooth_dr: constant for denominator.
243
        :param expected: target value.
244
        """
245
        shape = (1, 10)
246
        y_true = tf.ones(shape=shape) * value
247
        y_pred = tf.ones(shape=shape) * value
248
249
        got = label.JaccardIndex(smooth_nr=smooth_nr, smooth_dr=smooth_dr)._call(
250
            y_true,
251
            y_pred,
252
        )
253
        expected = tf.constant(expected)
254
        assert is_equal_tf(got[0], expected)
255
256
    @pytest.mark.parametrize("binary", [True, False])
257
    @pytest.mark.parametrize("background_weight", [0.0, 0.1, 0.5, 1.0])
258
    @pytest.mark.parametrize("shape", [(1,), (10,), (100,), (2, 3), (2, 3, 4)])
259
    def test_exact_value(self, binary: bool, background_weight: float, shape: Tuple):
260
        """
261
        Test Jaccard index by comparing at ground truth values.
262
263
        :param binary: if project labels to binary values.
264
        :param background_weight: the weight of background class.
265
        :param shape: shape of input.
266
        """
267
        # init
268
        shape = (1,) + shape  # add batch axis
269
        foreground_weight = 1 - background_weight
270
        tf.random.set_seed(0)
271
        y_true = tf.random.uniform(shape=shape)
272
        y_pred = tf.random.uniform(shape=shape)
273
274
        # obtained value
275
        got = label.JaccardIndex(
276
            binary=binary,
277
            background_weight=background_weight,
278
        ).call(y_true=y_true, y_pred=y_pred)
279
280
        # expected value
281
        flatten = tf.keras.layers.Flatten()
282
        y_true = flatten(y_true)
283
        y_pred = flatten(y_pred)
284
        if binary:
285
            y_true = tf.cast(y_true >= 0.5, dtype=y_true.dtype)
286
            y_pred = tf.cast(y_pred >= 0.5, dtype=y_pred.dtype)
287
288
        num = foreground_weight * tf.reduce_sum(
289
            y_true * y_pred, axis=1
290
        ) + background_weight * tf.reduce_sum((1 - y_true) * (1 - y_pred), axis=1)
291
        denom = foreground_weight * tf.reduce_sum(
292
            y_true + y_pred, axis=1
293
        ) + background_weight * tf.reduce_sum((1 - y_true) + (1 - y_pred), axis=1)
294
        denom = denom - num
295
        expected = (num + EPS) / (denom + EPS)
296
297
        assert is_equal_tf(got, expected)
298
299
    def test_get_config(self):
300
        got = label.JaccardIndex().get_config()
301
        expected = dict(
302
            binary=False,
303
            background_weight=0.0,
304
            smooth_nr=1e-5,
305
            smooth_dr=1e-5,
306
            scales=None,
307
            kernel="gaussian",
308
            reduction=tf.keras.losses.Reduction.SUM,
309
            name="JaccardIndex",
310
        )
311
        assert got == expected
312
313
314
def test_foreground_prop_binary():
315
    """
316
    Test foreground function with a
317
    tensor of zeros with some ones, asserting
318
    equal to known precomputed tensor.
319
    Testing with binary case.
320
    """
321
    array_eye = np.identity(3, dtype=np.float32)
322
    tensor_eye = np.zeros((3, 3, 3, 3), dtype=np.float32)
323
    tensor_eye[:, :, 0:3, 0:3] = array_eye
324
    expect = tf.convert_to_tensor([1.0 / 3, 1.0 / 3, 1.0 / 3], dtype=tf.float32)
325
    get = label.foreground_proportion(tensor_eye)
326
    assert is_equal_tf(get, expect)
327
328
329
def test_foreground_prop_simple():
330
    """
331
    Test foreground functions with a tensor
332
    of zeros with some ones and some values below
333
    one to assert the thresholding works.
334
    """
335
    array_eye = np.identity(3, dtype=np.float32)
336
    tensor_eye = np.zeros((3, 3, 3, 3), dtype=np.float32)
337
    tensor_eye[:, 0, :, :] = 0.4 * array_eye  # 0
338
    tensor_eye[:, 1, :, :] = array_eye
339
    tensor_eye[:, 2, :, :] = array_eye
340
    tensor_eye = tf.convert_to_tensor(tensor_eye, dtype=tf.float32)
341
    expect = [54 / (27 * 9), 54 / (27 * 9), 54 / (27 * 9)]
342
    get = label.foreground_proportion(tensor_eye)
343
    assert is_equal_tf(get, expect)
344
345
346
def test_compute_centroid():
347
    """
348
    Testing compute centroid function
349
    and comparing to expected values.
350
    """
351
    tensor_mask = np.zeros((3, 2, 2, 2))
352
    tensor_mask[0, :, :, :] = np.ones((2, 2, 2))
353
    tensor_mask = tf.constant(tensor_mask, dtype=tf.float32)
354
355
    tensor_grid = np.ones((1, 2, 2, 2, 3))
356
    tensor_grid[:, :, :, :, 1] *= 2
357
    tensor_grid[:, :, :, :, 2] *= 3
358
    tensor_grid = tf.constant(tensor_grid, dtype=tf.float32)
359
360
    expected = np.ones((3, 3))  # use 1 because 0/0 ~= (0+eps)/(0+eps) = 1
361
    expected[0, :] = [1, 2, 3]
362
    got = label.compute_centroid(tensor_mask, tensor_grid)
363
    assert is_equal_tf(got, expected)
364
365
366
def test_compute_centroid_d():
367
    """
368
    Testing compute centroid distance between equal
369
    tensors returns 0s.
370
    """
371
    array_ones = np.ones((2, 2))
372
    tensor_mask = np.zeros((3, 2, 2, 2))
373
    tensor_mask[0, :, :, :] = array_ones
374
    tensor_mask = tf.convert_to_tensor(tensor_mask, dtype=tf.float32)
375
376
    tensor_grid = np.zeros((1, 2, 2, 2, 3))
377
    tensor_grid[:, :, :, :, 0] = array_ones
378
    tensor_grid = tf.convert_to_tensor(tensor_grid, dtype=tf.float32)
379
380
    get = label.compute_centroid_distance(tensor_mask, tensor_mask, tensor_grid)
381
    expect = np.zeros((3))
382
    assert is_equal_tf(get, expect)
383