braintools 0.0.2__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. braintools/__init__.py +27 -0
  2. braintools/functional/__init__.py +25 -0
  3. braintools/functional/_activations.py +733 -0
  4. braintools/functional/_normalization.py +62 -0
  5. braintools/functional/_spikes.py +90 -0
  6. braintools/init/__init__.py +26 -0
  7. braintools/init/_base.py +36 -0
  8. braintools/init/_generic.py +205 -0
  9. braintools/init/_random_inits.py +489 -0
  10. braintools/init/_regular_inits.py +109 -0
  11. braintools/input/__init__.py +27 -0
  12. braintools/input/currents.py +380 -0
  13. braintools/input/currents_test.py +111 -0
  14. braintools/metric/__init__.py +46 -0
  15. braintools/metric/_classification.py +569 -0
  16. braintools/metric/_classification_test.py +625 -0
  17. braintools/metric/_correlation.py +302 -0
  18. braintools/metric/_correlation_test.py +124 -0
  19. braintools/metric/_fenchel_young.py +67 -0
  20. braintools/metric/_fenchel_young_test.py +55 -0
  21. braintools/metric/_firings.py +85 -0
  22. braintools/metric/_firings_test.py +41 -0
  23. braintools/metric/_lfp.py +139 -0
  24. braintools/metric/_ranking.py +163 -0
  25. braintools/metric/_ranking_test.py +242 -0
  26. braintools/metric/_regression.py +425 -0
  27. braintools/metric/_regression_test.py +133 -0
  28. braintools/metric/_smoothing.py +46 -0
  29. braintools/metric/_smoothing_test.py +43 -0
  30. braintools/metric/_util.py +25 -0
  31. braintools/optim/__init__.py +23 -0
  32. braintools/optim/_lr_scheduler.py +482 -0
  33. braintools/optim/_sgd_optimizer.py +1130 -0
  34. braintools-0.0.2.dist-info/LICENSE +202 -0
  35. braintools-0.0.2.dist-info/METADATA +93 -0
  36. braintools-0.0.2.dist-info/RECORD +38 -0
  37. braintools-0.0.2.dist-info/WHEEL +6 -0
  38. braintools-0.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,139 @@
1
+ # Copyright 2024- BrainPy Ecosystem Limited. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ # -*- coding: utf-8 -*-
17
+
18
+ import braincore as bc
19
+ import jax
20
+ from jax import numpy as jnp
21
+
22
+ __all__ = [
23
+ 'unitary_LFP',
24
+ ]
25
+
26
+
27
+ def unitary_LFP(
28
+ times: bc.typing.ArrayLike,
29
+ spikes: bc.typing.ArrayLike,
30
+ spike_type: str,
31
+ xmax: bc.typing.ArrayLike = 0.2,
32
+ ymax: bc.typing.ArrayLike = 0.2,
33
+ va: bc.typing.ArrayLike = 200.,
34
+ lambda_: bc.typing.ArrayLike = 0.2,
35
+ sig_i: bc.typing.ArrayLike = 2.1,
36
+ sig_e: bc.typing.ArrayLike = 2.1 * 1.5,
37
+ location: str = 'soma layer',
38
+ seed: bc.typing.SeedOrKey = None
39
+ ) -> jax.Array:
40
+ """
41
+ A kernel-based method to calculate unitary local field potentials (uLFP)
42
+ from a network of spiking neurons [1]_.
43
+
44
+ .. note::
45
+ This method calculates LFP only from the neuronal spikes. It does not consider
46
+ the subthreshold synaptic events, or the dendritic voltage-dependent ion channels.
47
+
48
+ Examples
49
+ --------
50
+
51
+ If you have spike data of excitatory and inhibtiory neurons, you can get the LFP
52
+ by the following methods:
53
+
54
+ >>> import braincore as bc
55
+ >>> import jax
56
+ >>> import braintools as bt
57
+ >>> n_time = 1000
58
+ >>> n_exc = 100
59
+ >>> n_inh = 25
60
+ >>> times = jax.numpy.arange(n_time) * 0.1
61
+ >>> exc_sps = bc.random.random((n_time, n_exc)) < 0.3
62
+ >>> inh_sps = bc.random.random((n_time, n_inh)) < 0.4
63
+ >>> lfp = bt.metric.unitary_LFP(times, exc_sps, 'exc')
64
+ >>> lfp += bt.metric.unitary_LFP(times, inh_sps, 'inh')
65
+
66
+ Parameters
67
+ ----------
68
+ times: ndarray
69
+ The times of the recording points.
70
+ spikes: ndarray
71
+ The spikes of excitatory neurons recorded by brainpy monitors.
72
+ spike_type: str
73
+ The neuron type of the spike trains. It can be "exc" or "inh".
74
+ location: str
75
+ The location of the spikes recorded. It can be "soma layer", "deep layer",
76
+ "superficial layer" and "surface".
77
+ xmax: float
78
+ Size of the array (in mm).
79
+ ymax: float
80
+ Size of the array (in mm).
81
+ va: int, float
82
+ The axon velocity (mm/sec).
83
+ lambda_: float
84
+ The space constant (mm).
85
+ sig_i: float
86
+ The std-dev of inhibition (in ms)
87
+ sig_e: float
88
+ The std-dev for excitation (in ms).
89
+ seed: int
90
+ The random seed.
91
+
92
+ References
93
+ ----------
94
+ .. [1] Telenczuk, Bartosz, Maria Telenczuk, and Alain Destexhe. "A kernel-based
95
+ method to calculate local field potentials from networks of spiking
96
+ neurons." Journal of Neuroscience Methods 344 (2020): 108871.
97
+
98
+ """
99
+ if spike_type not in ['exc', 'inh']:
100
+ raise ValueError('"spike_type" should be "exc or ""inh". ')
101
+ if spikes.ndim != 2:
102
+ raise ValueError('"E_spikes" should be a matrix with shape of (num_time, num_neuron). '
103
+ f'But we got {spikes.shape}')
104
+ if times.shape[0] != spikes.shape[0]:
105
+ raise ValueError('times and spikes should be consistent at the firs axis. '
106
+ f'Bug we got {times.shape[0]} != {spikes.shape}.')
107
+
108
+ # Distributing cells in a 2D grid
109
+ rng = bc.random.RandomState(seed)
110
+ num_neuron = spikes.shape[1]
111
+ pos_xs, pos_ys = rng.rand(2, num_neuron).value * jnp.array([[xmax], [ymax]])
112
+ pos_xs, pos_ys = jnp.asarray(pos_xs), jnp.asarray(pos_ys)
113
+
114
+ # distance/coordinates
115
+ xe, ye = xmax / 2, ymax / 2 # coordinates of electrode
116
+ dist = jnp.sqrt((pos_xs - xe) ** 2 + (pos_ys - ye) ** 2) # distance to electrode in mm
117
+
118
+ # amplitude
119
+ if location == 'soma layer':
120
+ amp_e, amp_i = 0.48, 3. # exc/inh uLFP amplitude (soma layer)
121
+ elif location == 'deep layer':
122
+ amp_e, amp_i = -0.16, -0.2 # exc/inh uLFP amplitude (deep layer)
123
+ elif location == 'superficial layer':
124
+ amp_e, amp_i = 0.24, -1.2 # exc/inh uLFP amplitude (superficial layer)
125
+ elif location == 'surface layer':
126
+ amp_e, amp_i = -0.08, 0.3 # exc/inh uLFP amplitude (surface)
127
+ else:
128
+ raise NotImplementedError
129
+ A = jnp.exp(-dist / lambda_) * (amp_e if spike_type == 'exc' else amp_i)
130
+
131
+ # delay
132
+ delay = 10.4 + dist / va # delay to peak (in ms)
133
+
134
+ # LFP Calculation
135
+ iis, ids = jnp.where(spikes)
136
+ tts = times[iis] + delay[ids]
137
+ exc_amp = A[ids]
138
+ tau = (2 * sig_e * sig_e) if spike_type == 'exc' else (2 * sig_i * sig_i)
139
+ return bc.transform.for_loop(lambda t: jnp.sum(exc_amp * jnp.exp(-(t - tts) ** 2 / tau)), times)
@@ -0,0 +1,163 @@
1
+ # Copyright 2024 BrainPy Ecosystem Limited. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """
17
+ Ranking losses.
18
+
19
+ A ranking loss is a differentiable function that expresses the cost of a ranking
20
+ induced by item scores compared to a ranking induced from relevance labels.
21
+
22
+ Ranking losses are designed to operate on the last dimension of its inputs. The
23
+ leading dimensions are considered batch dimensions.
24
+
25
+ Standalone usage:
26
+
27
+ >>> import braintools as bt
28
+ >>> scores = jnp.array([2., 1., 3.])
29
+ >>> labels = jnp.array([1., 0., 0.])
30
+ >>> loss = bt.metric.ranking_softmax_loss(scores, labels)
31
+ >>> print(f"{loss:.3f}")
32
+ 1.408
33
+
34
+ Usage with a batch of data and a mask to indicate valid items.
35
+
36
+ >>> scores = jnp.array([[2., 1., 0.], [1., 0.5, 1.5]])
37
+ >>> labels = jnp.array([[1., 0., 0.], [0., 0., 1.]])
38
+ >>> where = jnp.array([[True, True, False], [True, True, True]])
39
+ >>> loss = bt.metric.ranking_softmax_loss(scores, labels, where=where)
40
+ >>> print(f"{loss:.3f}")
41
+ 0.497
42
+ """
43
+
44
+ from typing import Callable, Optional
45
+
46
+ import jax
47
+ import jax.numpy as jnp
48
+ import braincore as bc
49
+
50
+ __all__ = [
51
+ 'ranking_softmax_loss',
52
+ ]
53
+
54
+
55
+ def _safe_reduce(
56
+ a: bc.typing.ArrayLike,
57
+ where: Optional[bc.typing.ArrayLike] = None,
58
+ reduce_fn: Optional[Callable[..., bc.typing.ArrayLike]] = None,
59
+ ) -> bc.typing.ArrayLike:
60
+ """Reduces the values of given array while preventing NaN in the output.
61
+
62
+ For :func:`jax.numpy.mean` reduction, this additionally prevents ``NaN`` in
63
+ the output if all elements are masked. This can happen with pairwise losses
64
+ where there are no valid pairs because all labels are the same. In this
65
+ situation, 0 is returned instead.
66
+
67
+ When there is no ``reduce_fn``, this will set elements of ``a`` to 0 according
68
+ to the ``where`` mask.
69
+
70
+ Args:
71
+ a: The :class:`jax.Array` to reduce.
72
+ where: An optional :class:`jax.Array` indicating which elements to include
73
+ in the reduction.
74
+ reduce_fn: The function used to reduce. If None, no reduction is performed.
75
+
76
+ Returns:
77
+ The result of reducing the values of ``a`` using given ``reduce_fn``.
78
+ """
79
+ # Reduce values if there is a reduce_fn, otherwise keep the values as-is.
80
+ output = reduce_fn(a, where=where) if reduce_fn is not None else a
81
+
82
+ if reduce_fn is jnp.mean:
83
+ # For mean reduction, we have to check whether the input contains any NaN
84
+ # values, to ensure that masked mean reduction does not hide them (see
85
+ # below).
86
+ is_input_valid = jnp.logical_not(jnp.any(jnp.isnan(a)))
87
+
88
+ # The standard jnp.mean implementation returns NaN if `where` is False
89
+ # everywhere. This can happen in our case, e.g. pairwise losses with no
90
+ # valid pairs. Instead, we prefer that the loss returns 0 in these cases.
91
+ # Note that this only hides those NaN values if the input did not contain
92
+ # any NaN values. Otherwise it just returns the output as-is.
93
+ output = jnp.where(jnp.isnan(output) & is_input_valid, 0.0, output)
94
+
95
+ if reduce_fn is None and where is not None:
96
+ # When there is no reduce_fn (i.e. we are returning an unreduced
97
+ # loss/metric), set the values of `a` to 0 for invalid (masked) items.
98
+ # This makes sure that manual sum reduction on an unreduced loss works as
99
+ # expected:
100
+ # `jnp.sum(loss_fn(reduce_fn=None)) == loss_fn(reduce_fn=jnp.sum)`
101
+ output = jnp.where(where, output, 0.0)
102
+
103
+ return output
104
+
105
+
106
+ def ranking_softmax_loss(
107
+ logits: bc.typing.ArrayLike,
108
+ labels: bc.typing.ArrayLike,
109
+ *,
110
+ where: Optional[bc.typing.ArrayLike] = None,
111
+ weights: Optional[bc.typing.ArrayLike] = None,
112
+ reduce_fn: Optional[Callable[..., bc.typing.ArrayLike]] = jnp.mean
113
+ ) -> bc.typing.ArrayLike:
114
+ r"""Ranking softmax loss.
115
+
116
+ Definition:
117
+
118
+ .. math::
119
+ \ell(s, y) = -\sum_i y_i \log \frac{\exp(s_i)}{\sum_j \exp(s_j)}
120
+
121
+ Args:
122
+ logits: A ``[..., list_size]``-:class:`~jax.Array`, indicating the score of
123
+ each item.
124
+ labels: A ``[..., list_size]``-:class:`~jax.Array`, indicating the relevance
125
+ label for each item.
126
+ where: An optional ``[..., list_size]``-:class:`~jax.Array`, indicating
127
+ which items are valid for computing the loss. Items for which this is
128
+ False will be ignored when computing the loss.
129
+ weights: An optional ``[..., list_size]``-:class:`~jax.Array`, indicating
130
+ the weight for each item.
131
+ reduce_fn: An optional function that reduces the loss values. Can be
132
+ :func:`jax.numpy.sum` or :func:`jax.numpy.mean`. If ``None``, no reduction
133
+ is performed.
134
+
135
+ Returns:
136
+ The ranking softmax loss.
137
+ """
138
+ assert bc.math.is_float(logits), "logits must be a float type."
139
+ labels = labels.astype(logits.dtype)
140
+
141
+ # Applies mask so that masked elements do not count towards the loss.
142
+ if where is not None:
143
+ labels = jnp.where(where, labels, jnp.zeros_like(labels))
144
+ logits = jnp.where(where, logits, -jnp.ones_like(logits) * jnp.inf)
145
+
146
+ # Apply weights to labels.
147
+ if weights is not None:
148
+ labels *= weights
149
+
150
+ # Scales labels and logits to match the cross entropy loss.
151
+ logits_log_softmax = jax.nn.log_softmax(logits, axis=-1)
152
+
153
+ # Computes per-element cross entropy.
154
+ softmax_cross_entropy = labels * logits_log_softmax
155
+
156
+ # Reduces softmax cross-entropy loss.
157
+ loss = -jnp.sum(softmax_cross_entropy, axis=-1, where=where)
158
+
159
+ # Setup mask to ignore lists with only invalid items in reduce_fn.
160
+ if where is not None:
161
+ where = jnp.any(where, axis=-1)
162
+
163
+ return _safe_reduce(loss, where=where, reduce_fn=reduce_fn)
@@ -0,0 +1,242 @@
1
+ # Copyright 2024 BrainPy Ecosystem Limited. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+
17
+ import functools
18
+ import math
19
+ from absl.testing import parameterized
20
+ import jax
21
+ import jax.numpy as jnp
22
+ import numpy as np
23
+
24
+ from braintools.metric import _ranking
25
+
26
+ # Export symbols from math for conciser test value definitions.
27
+ exp = math.exp
28
+ log = math.log
29
+ logloss = lambda x: log(1.0 + exp(-x))
30
+ sigmoid = lambda x: 1.0 / (1.0 + exp(-x))
31
+
32
+
33
+ class RankingLossesTest(parameterized.TestCase):
34
+
35
+ @parameterized.parameters([
36
+ {
37
+ "loss_fn": _ranking.ranking_softmax_loss,
38
+ "expected_value": -(
39
+ log(exp(2.0) / (exp(0.0) + exp(3.0) + exp(1.0) + exp(2.0)))
40
+ + log(exp(1.0) / (exp(0.0) + exp(3.0) + exp(1.0) + exp(2.0)))
41
+ ),
42
+ },
43
+ ])
44
+ def test_computes_loss_value(self, loss_fn, expected_value):
45
+ scores = jnp.asarray([0.0, 3.0, 1.0, 2.0])
46
+ labels = jnp.asarray([0.0, 0.0, 1.0, 1.0])
47
+
48
+ loss = loss_fn(scores, labels, reduce_fn=jnp.sum)
49
+
50
+ np.testing.assert_allclose(jnp.asarray(expected_value), loss, rtol=1e-3)
51
+
52
+ @parameterized.parameters([
53
+ {
54
+ "loss_fn": _ranking.ranking_softmax_loss,
55
+ "expected_value": -(
56
+ (-2.1e26 - (0.0 + -2.1e26 + 3.4e37 + 42.0))
57
+ + (3.4e37 - (0.0 + -2.1e26 + 3.4e37 + 42.0))
58
+ ),
59
+ },
60
+ ])
61
+ def test_computes_loss_with_extreme_inputs(self, loss_fn, expected_value):
62
+ scores = jnp.asarray([0.0, -2.1e26, 3.4e37, 42.0])
63
+ labels = jnp.asarray([0.0, 1.0, 1.0, 0.0])
64
+
65
+ loss = loss_fn(scores, labels, reduce_fn=jnp.sum)
66
+
67
+ np.testing.assert_allclose(jnp.asarray(expected_value), loss, rtol=1e-3)
68
+
69
+ @parameterized.parameters([
70
+ {"loss_fn": _ranking.ranking_softmax_loss, "expected_value": 0.0},
71
+ ])
72
+ def test_computes_loss_for_zero_labels(self, loss_fn, expected_value):
73
+ scores = jnp.asarray([0.0, 3.0, 1.0, 2.0])
74
+ labels = jnp.asarray([0.0, 0.0, 0.0, 0.0])
75
+
76
+ loss = loss_fn(scores, labels, reduce_fn=jnp.sum)
77
+
78
+ np.testing.assert_allclose(jnp.asarray(expected_value), loss, rtol=1e-3)
79
+
80
+ @parameterized.parameters([
81
+ {
82
+ "loss_fn": _ranking.ranking_softmax_loss,
83
+ "expected_value": -(
84
+ 2.0 * log(exp(2.0) / (exp(0.0) + exp(3.0) + exp(1.0) + exp(2.0)))
85
+ + log(exp(1.0) / (exp(0.0) + exp(3.0) + exp(1.0) + exp(2.0)))
86
+ ),
87
+ },
88
+ ])
89
+ def test_computes_weighted_loss_value(self, loss_fn, expected_value):
90
+ scores = jnp.asarray([0.0, 3.0, 2.0, 1.0])
91
+ labels = jnp.asarray([0.0, 0.0, 1.0, 1.0])
92
+ weights = jnp.asarray([1.0, 1.0, 2.0, 1.0])
93
+
94
+ loss = loss_fn(scores, labels, weights=weights, reduce_fn=jnp.sum)
95
+
96
+ np.testing.assert_allclose(jnp.asarray(expected_value), loss, rtol=1e-3)
97
+
98
+ @parameterized.parameters([
99
+ {
100
+ "loss_fn": _ranking.ranking_softmax_loss,
101
+ "expected_value": [
102
+ -(
103
+ log(exp(2.0) / (exp(0.0) + exp(3.0) + exp(1.0) + exp(2.0)))
104
+ + log(exp(1.0) / (exp(0.0) + exp(3.0) + exp(1.0) + exp(2.0)))
105
+ ),
106
+ -(
107
+ 2.0
108
+ * log(exp(3.0) / (exp(3.0) + exp(1.0) + exp(4.0) + exp(2.0)))
109
+ + log(exp(4.0) / (exp(3.0) + exp(1.0) + exp(4.0) + exp(2.0)))
110
+ ),
111
+ ],
112
+ },
113
+ ])
114
+ def test_computes_loss_value_with_vmap(self, loss_fn, expected_value):
115
+ scores = jnp.asarray([[0.0, 3.0, 1.0, 2.0], [3.0, 1.0, 4.0, 2.0]])
116
+ labels = jnp.asarray([[0.0, 0.0, 1.0, 1.0], [2.0, 0.0, 1.0, 0.0]])
117
+
118
+ loss_fn = functools.partial(loss_fn, reduce_fn=jnp.sum)
119
+ vmap_loss_fn = jax.vmap(loss_fn, in_axes=(0, 0), out_axes=0)
120
+ loss = vmap_loss_fn(scores, labels)
121
+
122
+ np.testing.assert_allclose(jnp.asarray(expected_value), loss, rtol=1e-3)
123
+
124
+ @parameterized.parameters([
125
+ {
126
+ "loss_fn": _ranking.ranking_softmax_loss,
127
+ "expected_value": [
128
+ -log(exp(2.0) / (exp(2.0) + exp(1.0) + exp(3.0))),
129
+ -log(exp(1.5) / (exp(1.0) + exp(0.5) + exp(1.5))),
130
+ ],
131
+ "normalizer": 2.0,
132
+ },
133
+ ])
134
+ def test_computes_reduced_loss(self, loss_fn, expected_value, normalizer):
135
+ scores = jnp.array([[2.0, 1.0, 3.0], [1.0, 0.5, 1.5]])
136
+ labels = jnp.array([[1.0, 0.0, 0.0], [0.0, 0.0, 1.0]])
137
+ expected_value = jnp.asarray(expected_value)
138
+
139
+ mean_loss = loss_fn(scores, labels, reduce_fn=jnp.mean)
140
+ sum_loss = loss_fn(scores, labels, reduce_fn=jnp.sum)
141
+
142
+ np.testing.assert_allclose(
143
+ mean_loss, jnp.sum(expected_value) / normalizer, rtol=1e-3
144
+ )
145
+ np.testing.assert_allclose(sum_loss, jnp.sum(expected_value), rtol=1e-3)
146
+
147
+ @parameterized.parameters([
148
+ {"loss_fn": _ranking.ranking_softmax_loss, "expected_shape": (2,)},
149
+ ])
150
+ def test_computes_unreduced_loss(self, loss_fn, expected_shape):
151
+ scores = jnp.array([[2.0, 1.0, 3.0], [1.0, 0.5, 1.5]])
152
+ labels = jnp.array([[1.0, 0.0, 0.0], [0.0, 0.0, 1.0]])
153
+
154
+ none_loss = loss_fn(scores, labels, reduce_fn=None)
155
+ sum_loss = loss_fn(scores, labels, reduce_fn=jnp.sum)
156
+
157
+ self.assertEqual(none_loss.shape, expected_shape)
158
+ self.assertEqual(jnp.sum(none_loss), sum_loss)
159
+
160
+ @parameterized.parameters([
161
+ _ranking.ranking_softmax_loss,
162
+ ])
163
+ def test_computes_loss_value_with_where(self, loss_fn):
164
+ scores = jnp.asarray([0.0, 3.0, 1.0, 2.0])
165
+ labels = jnp.asarray([0.0, 0.0, 2.0, 1.0])
166
+ where = jnp.asarray([True, True, True, False])
167
+ expected_scores = jnp.asarray([0.0, 3.0, 1.0])
168
+ expected_labels = jnp.asarray([0.0, 0.0, 2.0])
169
+
170
+ loss = loss_fn(scores, labels, where=where)
171
+ expected_loss = loss_fn(expected_scores, expected_labels)
172
+
173
+ np.testing.assert_allclose(expected_loss, loss, rtol=1e-3)
174
+
175
+ @parameterized.parameters([
176
+ _ranking.ranking_softmax_loss,
177
+ ])
178
+ def test_computes_loss_value_with_all_masked(self, loss_fn):
179
+ scores = jnp.asarray([0.0, 3.0, 1.0, 2.0])
180
+ labels = jnp.asarray([0.0, 0.0, 1.0, 1.0])
181
+ where = jnp.asarray([False, False, False, False])
182
+
183
+ loss = loss_fn(scores, labels, where=where)
184
+
185
+ np.testing.assert_allclose(jnp.asarray(0.0), loss, rtol=1e-3)
186
+
187
+ @parameterized.parameters([
188
+ _ranking.ranking_softmax_loss,
189
+ ])
190
+ def test_computes_loss_with_arbitrary_batch_dimensions(self, loss_fn):
191
+ scores = jnp.asarray([2.0, 3.0, 1.0])
192
+ labels = jnp.asarray([0.0, 0.0, 1.0])
193
+ where = jnp.asarray([False, True, True])
194
+ original_loss = loss_fn(scores, labels, where=where)
195
+
196
+ scores = jnp.asarray([[[[2.0, 3.0, 1.0]]]])
197
+ labels = jnp.asarray([[[[0.0, 0.0, 1.0]]]])
198
+ where = jnp.asarray([[[[False, True, True]]]])
199
+ batched_loss = loss_fn(scores, labels, where=where)
200
+
201
+ np.testing.assert_allclose(original_loss, batched_loss, rtol=1e-3)
202
+
203
+ @parameterized.parameters([
204
+ _ranking.ranking_softmax_loss,
205
+ ])
206
+ def test_grad_does_not_return_nan_for_zero_labels(self, loss_fn):
207
+ scores = jnp.asarray([0.0, 3.0, 1.0, 2.0])
208
+ labels = jnp.asarray([0.0, 0.0, 0.0, 0.0])
209
+
210
+ grads = jax.grad(loss_fn)(scores, labels, reduce_fn=jnp.mean)
211
+
212
+ np.testing.assert_array_equal(
213
+ jnp.isnan(grads), jnp.zeros_like(jnp.isnan(grads))
214
+ )
215
+
216
+ @parameterized.parameters([
217
+ _ranking.ranking_softmax_loss,
218
+ ])
219
+ def test_grad_does_not_return_nan_with_all_masked(self, loss_fn):
220
+ scores = jnp.asarray([0.0, 3.0, 1.0, 2.0])
221
+ labels = jnp.asarray([0.0, 0.0, 1.0, 1.0])
222
+ where = jnp.asarray([False, False, False, False])
223
+
224
+ grads = jax.grad(loss_fn)(scores, labels, where=where, reduce_fn=jnp.mean)
225
+
226
+ np.testing.assert_array_equal(
227
+ jnp.isnan(grads), jnp.zeros_like(jnp.isnan(grads))
228
+ )
229
+
230
+ @parameterized.parameters([
231
+ _ranking.ranking_softmax_loss,
232
+ ])
233
+ def test_ignores_lists_containing_only_invalid_items(self, loss_fn):
234
+ scores = jnp.asarray([[0.0, 3.0, 1.0, 2.0], [3.0, 1.0, 4.0, 2.0]])
235
+ labels = jnp.asarray([[0.0, 0.0, 1.0, 1.0], [2.0, 0.0, 1.0, 0.0]])
236
+ mask = jnp.asarray([[1, 1, 1, 1], [0, 0, 0, 0]], dtype=jnp.bool_)
237
+
238
+ output = loss_fn(scores, labels, where=mask)
239
+ expected = loss_fn(scores[0, :], labels[0, :])
240
+
241
+ np.testing.assert_allclose(output, expected, rtol=1e-3)
242
+