Skip to content

Commit 93a03f8

Browse files
alanjvanotymorrow
andcommitted
Tweak and refactor neural nets and losses.
Detailed changes are as follows: - add custom final activation to MLPClassifier - update license information related to using sparsemax code from TFA - refactor sparsemax functions - refactor some loss functions and the folder structure - update dependencies - update README Co-authored-by: Tyler Morrow <[email protected]>
1 parent cab15de commit 93a03f8

File tree

6 files changed

+343
-176
lines changed

6 files changed

+343
-176
lines changed

NOTICE.md

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
This source code is part of the PyRIID project and is licensed under the BSD-style licence.
2+
This project also contains code covered under the Apache-2.0 license based on Tensorflow-Addons functions which can be found in `riid/models/losses/sparsemax.py`.
3+
4+
The following is a list of the relevent copyright and license information.
5+
6+
---
7+
8+
Copyright 2021 National Technology & Engineering Solutions of Sandia, LLC (NTESS).
9+
Under the terms of Contract DE-NA0003525 with NTESS, the U.S. Government retains certain rights in this software.
10+
This source code is licensed under the BSD-style license found [here](https://github.com/sandialabs/PyRIID/blob/main/LICENSE.md).
11+
12+
---
13+
14+
Copyright 2016 The TensorFlow Authors. All Rights Reserved.
15+
16+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
17+
You may obtain a copy of the License at
18+
19+
http://www.apache.org/licenses/LICENSE-2.0
20+
21+
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22+
See the License for the specific language governing permissions and limitations under the License.

README.md

+23-9
Original file line numberDiff line numberDiff line change
@@ -5,39 +5,56 @@
55
[![Python](https://img.shields.io/pypi/pyversions/riid)](https://badge.fury.io/py/riid)
66
[![PyPI](https://badge.fury.io/py/riid.svg)](https://badge.fury.io/py/riid)
77

8-
This repository contains the PyRIID package (as well as tests and examples) which is intended to provide utilities that support machine learning-based research and solutions to radioisotope identification.
8+
This repository contains the PyRIID package (as well as tests and examples) which provides utilities that support machine learning-based research and solutions to radioisotope identification.
99

1010
## Installation
1111

12-
These instructions assume you have an up-to-date and stable Python installation; a virtual environment is recommended.
12+
These instructions assume you meet the following requirements:
1313

14-
To use the latest version on PyPI (note: changes are slower to appear here), run:
14+
- Python version: 3.7+
15+
- Operating systems: Windows, Mac, or Ubuntu
16+
17+
A virtual environment is recommended.
18+
19+
Tests and examples are ran via Actions on many combinations of Python version and operating system.
20+
You can verify support for your platform by checking the workflow files.
21+
22+
### For Use
23+
24+
To use the latest version on PyPI (note: changes are currently slower to appear here), run:
1525

1626
```
1727
pip install riid
1828
```
1929

20-
For the latest features, run:
30+
**For the latest features, run:**
2131

2232
```
2333
pip install git+https://github.com/sandialabs/pyriid.git@main
2434
```
2535

36+
### For Development
2637

2738
If you are developing PyRIID, clone this repository and run:
2839

2940
```
3041
pip install -e ".[dev]"
3142
```
3243

33-
If you have trouble with Pylance resolving imports for an editable install, try this:
44+
**If you have trouble with Pylance resolving imports for an editable install, try this:**
3445

3546
```
3647
pip install -e ".[dev]" --config-settings editable_mode=compat
3748
```
3849

50+
## Examples
51+
52+
Examples for how to use this package can be found [here](https://github.com/sandialabs/PyRIID/blob/main/examples).
53+
3954
## Tests
4055

56+
Unit tests for this package can be found [here](https://github.com/sandialabs/PyRIID/blob/main/tests).
57+
4158
Run all unit tests with the following command:
4259

4360
```sh
@@ -59,10 +76,7 @@ Maintainers and authors can be found [here](https://github.com/sandialabs/PyRIID
5976

6077
## Copyright
6178

62-
Copyright 2021 National Technology & Engineering Solutions of Sandia, LLC (NTESS).
63-
Under the terms of Contract DE-NA0003525 with NTESS, the U.S. Government retains certain rights in this software.
64-
65-
This source code is licensed under the BSD-style license found [here](https://github.com/sandialabs/PyRIID/blob/main/LICENSE.md).
79+
Full copyright details are outlined [here](https://github.com/sandialabs/PyRIID/blob/main/NOTICE.md)
6680

6781
## Acknowlegements
6882

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ dependencies = [
6161
"tensorflow-io ~=0.27",
6262
"tensorflow-model-optimization ~=0.7",
6363
"tensorflow-probability ==0.18.*", # this package is currently limiting the Python version to 3.9
64+
"typeguard >=2.7,<3.0.0",
6465
"scikit-learn >=1.1; python_version >= '3.10'",
6566
"scikit-learn ~=1.0; python_version < '3.10'",
6667
"seaborn ~=0.12",

riid/models/losses.py renamed to riid/models/losses/__init__.py

+3-154
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
# the U.S. Government retains certain rights in this software.
44
"""This module contains custom loss functions."""
55
import numpy as np
6-
from tensorflow.keras import backend as K
76
import tensorflow as tf
8-
from math import pi
7+
from tensorflow.keras import backend as K
98

109

1110
def negative_log_f1(y_true: np.ndarray, y_pred: np.ndarray):
@@ -127,7 +126,7 @@ def normal_nll_diff(spectra, reconstructed_spectra, eps=1e-8):
127126

128127
var = tf.clip_by_value(spectra, clip_value_min=1, clip_value_max=np.inf)
129128

130-
sigma_term = tf.math.log(2 * pi * var)
129+
sigma_term = tf.math.log(2 * np.pi * var)
131130
mu_term = tf.math.divide(tf.math.square(scaled_reconstructed_spectra - spectra), var)
132131
diff = sigma_term + mu_term
133132
diff = 0.5 * tf.reduce_sum(diff, axis=-1)
@@ -152,7 +151,7 @@ def weighted_sse_diff(spectra, reconstructed_spectra):
152151

153152
sample_variance = tf.sqrt(tf.math.reduce_variance(spectra, axis=1))
154153

155-
sigma_term = tf.math.log(2 * pi * sample_variance)
154+
sigma_term = tf.math.log(2 * np.pi * sample_variance)
156155

157156
mu_term = tf.math.divide(
158157
tf.math.square(scaled_reconstructed_spectra - spectra),
@@ -167,153 +166,3 @@ def reconstruction_error(spectra, lpes, dictionary, diff_func):
167166
reconstructed_spectra = tf.matmul(lpes, dictionary)
168167
reconstruction_errors = diff_func(spectra, reconstructed_spectra)
169168
return reconstruction_errors
170-
171-
172-
# based off code from Tensorflow-Addons (https://www.tensorflow.org/addons)
173-
def sparsemax(logits, axis: int = -1) -> tf.Tensor:
174-
"""Sparsemax activation function.
175-
176-
Args:
177-
logits: tensor of logits (should not be activated)
178-
axis: axis along which activation is applied
179-
"""
180-
181-
logits = tf.convert_to_tensor(logits, name="logits")
182-
183-
shape = logits.get_shape()
184-
rank = shape.rank
185-
is_last_axis = (axis == -1) or (axis == rank - 1)
186-
187-
if not is_last_axis:
188-
raise ValueError("Currently only last axis is supported.")
189-
190-
output = _compute_2d_sparsemax(logits)
191-
output.set_shape(shape)
192-
return output
193-
194-
195-
# based off code from Tensorflow-Addons (https://www.tensorflow.org/addons)
196-
@tf.function
197-
def sparsemax_loss_from_logits(y_true, logits_pred) -> tf.Tensor:
198-
logits = tf.convert_to_tensor(logits_pred, name="logits")
199-
sparsemax_values = tf.convert_to_tensor(sparsemax(logits_pred), name="sparsemax")
200-
labels = tf.convert_to_tensor(y_true, name="labels")
201-
202-
z = logits
203-
sum_s = tf.where(
204-
tf.math.logical_or(sparsemax_values > 0, tf.math.is_nan(sparsemax_values)),
205-
sparsemax_values * (z - 0.5 * sparsemax_values),
206-
tf.zeros_like(sparsemax_values),
207-
)
208-
q_part = labels * (0.5 * labels - z)
209-
210-
q_part_safe = tf.where(
211-
tf.math.logical_and(tf.math.equal(labels, 0), tf.math.is_inf(z)),
212-
tf.zeros_like(z),
213-
q_part,
214-
)
215-
216-
loss = tf.math.reduce_sum(sum_s + q_part_safe, axis=1)
217-
218-
return loss
219-
220-
221-
# taken from Tensorflow-Addons (https://www.tensorflow.org/addons)
222-
def _compute_2d_sparsemax(logits):
223-
"""Performs the sparsemax operation when axis=-1."""
224-
shape_op = tf.shape(logits)
225-
obs = tf.math.reduce_prod(shape_op[:-1])
226-
dims = shape_op[-1]
227-
228-
# In the paper, they call the logits z.
229-
# The mean(logits) can be substracted from logits to make the algorithm
230-
# more numerically stable. the instability in this algorithm comes mostly
231-
# from the z_cumsum. Substacting the mean will cause z_cumsum to be close
232-
# to zero. However, in practise the numerical instability issues are very
233-
# minor and substacting the mean causes extra issues with inf and nan
234-
# input.
235-
# Reshape to [obs, dims] as it is almost free and means the remanining
236-
# code doesn't need to worry about the rank.
237-
z = tf.reshape(logits, [obs, dims])
238-
239-
# sort z
240-
z_sorted, _ = tf.nn.top_k(z, k=dims)
241-
242-
# calculate k(z)
243-
z_cumsum = tf.math.cumsum(z_sorted, axis=-1)
244-
k = tf.range(1, tf.cast(dims, logits.dtype) + 1, dtype=logits.dtype)
245-
z_check = 1 + k * z_sorted > z_cumsum
246-
# because the z_check vector is always [1,1,...1,0,0,...0] finding the
247-
# (index + 1) of the last `1` is the same as just summing the number of 1.
248-
k_z = tf.math.reduce_sum(tf.cast(z_check, tf.int32), axis=-1)
249-
250-
# calculate tau(z)
251-
# If there are inf values or all values are -inf, the k_z will be zero,
252-
# this is mathematically invalid and will also cause the gather_nd to fail.
253-
# Prevent this issue for now by setting k_z = 1 if k_z = 0, this is then
254-
# fixed later (see p_safe) by returning p = nan. This results in the same
255-
# behavior as softmax.
256-
k_z_safe = tf.math.maximum(k_z, 1)
257-
indices = tf.stack([tf.range(0, obs), tf.reshape(k_z_safe, [-1]) - 1], axis=1)
258-
tau_sum = tf.gather_nd(z_cumsum, indices)
259-
tau_z = (tau_sum - 1) / tf.cast(k_z, logits.dtype)
260-
261-
# calculate p
262-
p = tf.math.maximum(tf.cast(0, logits.dtype), z - tf.expand_dims(tau_z, -1))
263-
# If k_z = 0 or if z = nan, then the input is invalid
264-
p_safe = tf.where(
265-
tf.expand_dims(
266-
tf.math.logical_or(tf.math.equal(k_z, 0), tf.math.is_nan(z_cumsum[:, -1])),
267-
axis=-1,
268-
),
269-
tf.fill([obs, dims], tf.cast(float("nan"), logits.dtype)),
270-
p,
271-
)
272-
273-
# Reshape back to original size
274-
p_safe = tf.reshape(p_safe, shape_op)
275-
return p_safe
276-
277-
278-
# taken from Tensorflow-Addons (https://www.tensorflow.org/addons)
279-
class SparsemaxLoss(tf.keras.losses.Loss):
280-
"""Sparsemax loss function.
281-
282-
Computes the generalized multi-label classification loss for the sparsemax
283-
function.
284-
285-
Because the sparsemax loss function needs both the probability output and
286-
the logits to compute the loss value, `from_logits` must be `True`.
287-
288-
Because it computes the generalized multi-label loss, the shape of both
289-
`y_pred` and `y_true` must be `[batch_size, num_classes]`.
290-
291-
Args:
292-
from_logits: Whether `y_pred` is expected to be a logits tensor. Default
293-
is `True`, meaning `y_pred` is the logits.
294-
reduction: (Optional) Type of `tf.keras.losses.Reduction` to apply to
295-
loss. Default value is `SUM_OVER_BATCH_SIZE`.
296-
name: Optional name for the op.
297-
"""
298-
299-
def __init__(
300-
self,
301-
from_logits: bool = True,
302-
reduction: str = tf.keras.losses.Reduction.SUM_OVER_BATCH_SIZE,
303-
name: str = "sparsemax_loss",
304-
):
305-
if from_logits is not True:
306-
raise ValueError("from_logits must be True")
307-
308-
super().__init__(name=name, reduction=reduction)
309-
self.from_logits = from_logits
310-
311-
def call(self, y_true, y_pred):
312-
return sparsemax_loss_from_logits(y_true, y_pred)
313-
314-
def get_config(self):
315-
config = {
316-
"from_logits": self.from_logits,
317-
}
318-
base_config = super().get_config()
319-
return {**base_config, **config}

0 commit comments

Comments
 (0)