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.
- braintools/__init__.py +27 -0
- braintools/functional/__init__.py +25 -0
- braintools/functional/_activations.py +733 -0
- braintools/functional/_normalization.py +62 -0
- braintools/functional/_spikes.py +90 -0
- braintools/init/__init__.py +26 -0
- braintools/init/_base.py +36 -0
- braintools/init/_generic.py +205 -0
- braintools/init/_random_inits.py +489 -0
- braintools/init/_regular_inits.py +109 -0
- braintools/input/__init__.py +27 -0
- braintools/input/currents.py +380 -0
- braintools/input/currents_test.py +111 -0
- braintools/metric/__init__.py +46 -0
- braintools/metric/_classification.py +569 -0
- braintools/metric/_classification_test.py +625 -0
- braintools/metric/_correlation.py +302 -0
- braintools/metric/_correlation_test.py +124 -0
- braintools/metric/_fenchel_young.py +67 -0
- braintools/metric/_fenchel_young_test.py +55 -0
- braintools/metric/_firings.py +85 -0
- braintools/metric/_firings_test.py +41 -0
- braintools/metric/_lfp.py +139 -0
- braintools/metric/_ranking.py +163 -0
- braintools/metric/_ranking_test.py +242 -0
- braintools/metric/_regression.py +425 -0
- braintools/metric/_regression_test.py +133 -0
- braintools/metric/_smoothing.py +46 -0
- braintools/metric/_smoothing_test.py +43 -0
- braintools/metric/_util.py +25 -0
- braintools/optim/__init__.py +23 -0
- braintools/optim/_lr_scheduler.py +482 -0
- braintools/optim/_sgd_optimizer.py +1130 -0
- braintools-0.0.2.dist-info/LICENSE +202 -0
- braintools-0.0.2.dist-info/METADATA +93 -0
- braintools-0.0.2.dist-info/RECORD +38 -0
- braintools-0.0.2.dist-info/WHEEL +6 -0
- 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
|
+
|