Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancement/87 add geometric loss #88

Merged
merged 8 commits into from
Jan 11, 2025
30 changes: 30 additions & 0 deletions .github/workflows/tutorials.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,33 @@ jobs:
- name: Run tutorial notebooks
run: build_tools/run_tutorials.sh
shell: bash

run-notebook-skorch-tutorials:
runs-on: ubuntu-22.04
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install notebook
pip install jupyterlab
pip install .
pip install skorch

- name: Grant execute permissions to run_skorch_tutorials.sh
run: chmod +x build_tools/run_skorch_tutorials.sh

- name: Run tutorial notebooks
run: build_tools/run_skorch_tutorials.sh
shell: bash
30 changes: 30 additions & 0 deletions build_tools/run_skorch_tutorials.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/bin/bash

# Script to run all example notebooks.
set -euo pipefail

CMD="jupyter nbconvert --to notebook --inplace --execute --ExecutePreprocessor.timeout=600"

included=(
"tutorials/dlordinal_with_skorch_tutorial.ipynb"
"tutorials/losses_with_skorch_tutorial.ipynb"
)

shopt -s lastpipe
notebooks=()
runtimes=()

for notebook in "${included[@]}"; do
echo "Running: $notebook"

start=$(date +%s)
$CMD "$notebook"
end=$(date +%s)

notebooks+=("$notebook")
runtimes+=($((end-start)))
done

# print runtimes and notebooks
echo "Runtimes:"
paste <(printf "%s\n" "${runtimes[@]}") <(printf "%s\n" "${notebooks[@]}")
39 changes: 17 additions & 22 deletions build_tools/run_tutorials.sh
Original file line number Diff line number Diff line change
@@ -1,37 +1,32 @@
#!/bin/bash

# Script to run all example notebooks.
set -euxo pipefail
set -euo pipefail

CMD="jupyter nbconvert --to notebook --inplace --execute --ExecutePreprocessor.timeout=600"

excluded=(
"tutorials/adience_tutorial.ipynb"
"tutorials/dlordinal_with_skorch_tutorial.ipynb"
included=(
"tutorials/ecoc_tutorial.ipynb"
"tutorials/fgnet_tutorial.ipynb"
"tutorials/hybrid_dropout_tutorial.ipynb"
"tutorials/losses_tutorial.ipynb"
"tutorials/stick_breaking_tutorial.ipynb"
)

shopt -s lastpipe
notebooks=()
runtimes=()

# Loop over all notebooks in the tutorials directory.
find "tutorials/" -name "*.ipynb" -print0 |
while IFS= read -r -d "" notebook; do
# Skip notebooks in the excluded list.
if printf "%s\0" "${excluded[@]}" | grep -Fxqz -- "$notebook"; then
echo "Skipping: $notebook"
# Run the notebook.
else
echo "Running: $notebook"

start=$(date +%s)
$CMD "$notebook"
end=$(date +%s)

notebooks+=("$notebook")
runtimes+=($((end-start)))
fi
done
for notebook in "${included[@]}"; do
echo "Running: $notebook"

start=$(date +%s)
$CMD "$notebook"
end=$(date +%s)

notebooks+=("$notebook")
runtimes+=($((end-start)))
done

# print runtimes and notebooks
echo "Runtimes:"
Expand Down
2 changes: 2 additions & 0 deletions dlordinal/losses/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .custom_targets_loss import CustomTargetsCrossEntropyLoss
from .exponential_loss import ExponentialRegularisedCrossEntropyLoss
from .general_triangular_loss import GeneralTriangularCrossEntropyLoss
from .geometric_loss import GeometricCrossEntropyLoss
from .mceloss import MCELoss
from .mcewkloss import MCEAndWKLoss
from .ordinal_ecoc_distance_loss import OrdinalECOCDistanceLoss
Expand All @@ -22,4 +23,5 @@
"OrdinalECOCDistanceLoss",
"TriangularCrossEntropyLoss",
"GeneralTriangularCrossEntropyLoss",
"GeometricCrossEntropyLoss",
]
92 changes: 92 additions & 0 deletions dlordinal/losses/geometric_loss.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
from typing import Optional, Union

import torch
from torch import Tensor

from ..soft_labelling import get_geometric_soft_labels
from .custom_targets_loss import CustomTargetsCrossEntropyLoss


class GeometricCrossEntropyLoss(CustomTargetsCrossEntropyLoss):
"""Unimodal label smoothing based on the discrete geometric distribution according to :footcite:t:`haas2023geometric`.

Parameters
----------
num_classes : int
Number of classes.
alphas : float or list, default=0.1
The smoothing factor(s) for geometric distribution-based unimodal smoothing.

- **Single alpha value**:
When a single alpha value in the range `[0, 1]`, e.g., `0.1`, is provided,
all classes will be smoothed equally and symmetrically.
This is done by deducting alpha from the actual class, :math:`1 - \\alpha`,
and allocating :math:`\\alpha` to the rest of the classes, decreasing monotonically
from the actual class in the form of the geometric distribution.

- **List of alpha values**:
Alternatively, a list of size :attr:`num_classes` can be provided to specify class-wise symmetric
smoothing factors. An example for five classes is: `[0.2, 0.05, 0.1, 0.15, 0.1]`.

- **List of smoothing relations**:
To control the fraction of the left-over probability mass :math:`\\alpha` allocated to the left
(:math:`F_l \\in [0,1]`) and right (:math:`F_r \\in [0,1]`) sides of the true class, with
:math:`F_l + F_r = 1`, a list of smoothing relations of the form :math:`(\\alpha, F_l, F_r)`
can be specified. This enables asymmetric unimodal smoothing. An example for five classes is:
`[(0.2, 0.0, 1.0), (0.05, 0.8, 0.2), (0.1, 0.5, 0.5), (0.15, 0.6, 0.4), (0.1, 1.0, 0.0)]`.

eta : float, default=1.0
Parameter that controls the influence of the regularisation.
weight : Optional[Tensor], default=None
A manual rescaling weight given to each class. If given, has to be a Tensor
of size `C`. Otherwise, it is treated as if having all ones.
size_average : Optional[bool], default=None
Deprecated (see :attr:`reduction`). By default, the losses are averaged over
each loss element in the batch. Note that for some losses, there are
multiple elements per sample. If the field :attr:`size_average` is set to
``False``, the losses are instead summed for each minibatch. Ignored when
reduce is ``False``. Default: ``True``
ignore_index : int, default=-100
Specifies a target value that is ignored and does not contribute to the
input gradient. When :attr:`size_average` is ``True``, the loss is averaged
over non-ignored targets.
reduce : Optional[bool], default=None
Deprecated (see :attr:`reduction`). By default, the losses are averaged or
summed over observations for each minibatch depending on :attr:`size_average`.
When :attr:`reduce` is ``False``, returns a loss per batch element instead
and ignores :attr:`size_average`. Default: ``True``
reduction : str, default='mean'
Specifies the reduction to apply to the output: ``'none'`` | ``'mean'`` |
``'sum'``. ``'none'``: no reduction will be applied, ``'mean'``: the sum of
the output will be divided by the number of elements in the output,
``'sum'``: the output will be summed. Note: :attr:`size_average` and
:attr:`reduce` are in the process of being deprecated, and in the meantime,
specifying either of those two args will override :attr:`reduction`.
Default: ``'mean'``
"""

def __init__(
self,
num_classes: int,
alphas: Union[float, list] = 0.1,
eta: float = 1.0,
weight: Optional[Tensor] = None,
size_average=None,
ignore_index: int = -100,
reduce=None,
reduction: str = "mean",
):
# Precompute class probabilities for each label
r = get_geometric_soft_labels(num_classes, alphas)
cls_probs = torch.tensor(r)

super().__init__(
cls_probs=cls_probs,
eta=eta,
weight=weight,
size_average=size_average,
ignore_index=ignore_index,
reduce=reduce,
reduction=reduction,
label_smoothing=0.0,
)
64 changes: 64 additions & 0 deletions dlordinal/losses/tests/test_geometric_loss.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import torch

from dlordinal.losses import GeometricCrossEntropyLoss


def test_geometric_loss_creation():
loss = GeometricCrossEntropyLoss(num_classes=5)
assert isinstance(loss, GeometricCrossEntropyLoss)


def test_geometric_loss_basic():
loss = GeometricCrossEntropyLoss(num_classes=6)

input_data = torch.tensor(
[
[-1.6488, -2.5838, -2.8312, -1.9495, -2.4759, -3.4682],
[-1.7872, -3.9560, -6.2586, -8.3967, -7.9779, -8.0079],
[-2.4078, -2.5133, -2.5584, -1.7485, -2.3675, -2.6099],
]
)
target = torch.tensor([4, 0, 5])

# Compute the loss
output = loss(input_data, target)

# Verifies that the output is a tensor
assert isinstance(output, torch.Tensor)

# Verifies that the loss is greater than zero
assert output.item() > 0


def test_geometric_loss_relative():
loss = GeometricCrossEntropyLoss(num_classes=6)

input_data = torch.tensor(
[
[100.0, 0.0, 0.0, 0.0, 0.0, 0.0],
]
)

input_data2 = torch.tensor(
[
[0.0, 0.0, 100.0, 0.0, 0.0, 0.0],
]
)

input_data3 = torch.tensor(
[
[0.0, 0.0, 0.0, 0.0, 100.0, 0.0],
]
)

target = torch.tensor([0])

# Compute the loss
output = loss(input_data, target)
output2 = loss(input_data2, target)
output3 = loss(input_data3, target)

# Verifies that the output is a tensor
assert isinstance(output, torch.Tensor)

assert output3.item() > output2.item() > output.item()
2 changes: 2 additions & 0 deletions dlordinal/soft_labelling/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
get_general_triangular_params,
get_general_triangular_soft_labels,
)
from .geometric_distribution import get_geometric_soft_labels
from .poisson_distribution import get_poisson_soft_labels
from .triangular_distribution import get_triangular_soft_labels

Expand All @@ -16,4 +17,5 @@
"get_triangular_soft_labels",
"get_general_triangular_params",
"get_general_triangular_soft_labels",
"get_geometric_soft_labels",
]
Loading