Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions tests/test_scale.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Tests for scaling methods."""

import numpy as np
import pytest

from tiatoolbox.tools.scale import PlattScaling


def test_platt_scaler():
"""Test for Platt scaler."""
np.random.seed(5)
sample_size = 1000
logit = np.random.rand(sample_size)
# binary class
label = np.concatenate(
[np.full(int(0.9 * sample_size), -1), np.full(int(0.1 * sample_size), 1)]
)
scaler = PlattScaling(num_iters=1)
scaler._fixer_a = 0.0
scaler._fixer_b = 0.0
_ = scaler.fit_transform(logit * 0.01, label)

scaler = PlattScaling(num_iters=1)
scaler._fixer_a = 0.0
scaler._fixer_b = 1.0
_ = scaler.fit_transform(logit * 0.01, label)

scaler = PlattScaling(num_iters=10)
_ = scaler.fit_transform(logit * 100, label)

label = np.concatenate([np.full(int(sample_size), -1)])
scaler = PlattScaling(num_iters=1)
_ = scaler.fit_transform(logit * 0.01, label)

with pytest.raises(ValueError, match=r".*same shape.*"):
scaler.fit_transform(logit, label[:2])
print(scaler)
16 changes: 16 additions & 0 deletions tests/test_visualization.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from tiatoolbox.utils.visualization import (
overlay_prediction_contours,
overlay_prediction_mask,
plot_graph,
)
from tiatoolbox.wsicore.wsireader import get_wsireader

Expand Down Expand Up @@ -130,3 +131,18 @@ def test_overlay_instance_prediction():
# test crash
with pytest.raises(ValueError, match=r"`.*inst_colours`.*tuple.*"):
overlay_prediction_contours(canvas, inst_dict, inst_colours=inst_colours)


def test_plot_graph():
"""Test plotting graph."""
canvas = np.zeros([10, 10])
nodes = np.array([[1, 1], [2, 2], [2, 5]])
edges = np.array([[0, 1], [1, 2], [2, 0]])
node_colors = np.array([[0, 0, 0]] * 3)
edge_colors = np.array([[1, 1, 1]] * 3)
plot_graph(
canvas,
nodes,
edges,
)
plot_graph(canvas, nodes, edges, node_colors=node_colors, edge_colors=edge_colors)
1 change: 1 addition & 0 deletions tiatoolbox/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
WSIPatchDataset,
)
from tiatoolbox.models.engine.semantic_segmentor import (
FeatureExtractor,
IOSegmentorConfig,
SemanticSegmentor,
WSIStreamDataset,
Expand Down
1 change: 1 addition & 0 deletions tiatoolbox/models/architecture/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import torch

from tiatoolbox import rcParam
from tiatoolbox.models.architecture.vanilla import CNNExtractor, CNNModel
from tiatoolbox.models.dataset.classification import predefined_preproc_func
from tiatoolbox.utils.misc import download_data

Expand Down
2 changes: 1 addition & 1 deletion tiatoolbox/tools/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# The Original Code is Copyright (C) 2021, TIALab, University of Warwick
# The Original Code is Copyright (C) 2021, TIA Centre, University of Warwick
# All rights reserved.
# ***** END GPL LICENSE BLOCK *****

Expand Down
2 changes: 1 addition & 1 deletion tiatoolbox/tools/pyramid.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# The Original Code is Copyright (C) 2021, TIALab, University of Warwick
# The Original Code is Copyright (C) 2021, TIA Centre, University of Warwick
# All rights reserved.
# ***** END GPL LICENSE BLOCK *****

Expand Down
184 changes: 184 additions & 0 deletions tiatoolbox/tools/scale.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# ***** BEGIN GPL LICENSE BLOCK *****
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# The Original Code is Copyright (C) 2021, TIA Centre, University of Warwick
# All rights reserved.
# ***** END GPL LICENSE BLOCK *****

import numpy as np


# Fit model output to the label range
class PlattScaling:
"""Platt scaling.

Fitting a logistic regression model to a classifier scores such that
the model outputs are transformed into a probability distribution over classes.

Args:
num_iters (int): Number of iterations for training.

Examples:
>>> import numpy as np
>>> logit = np.random.rand(10)
>>> # binary class
>>> label = np.random.randint(0, 2, 10)
>>> scaler = PlattScaling()
>>> probabilities = scaler.fit_transform(label, logit)

"""

def __init__(self, num_iters=100):
self.a = None
self.b = None
self.num_iters = num_iters + 1
self._fixer_a = 1.0
self._fixer_b = 1.0

def fit(self, logits, labels):
"""Fit function like sklearn.

Fit the sigmoid to the classifier scores logits and labels
using the Platt Method.

Args:
logits (array-like): Classifier output scores.
labels (array like): Classifier labels, must be `+1` vs `-1` or `1` vs `0`.
Returns:
Model with fitted coefficients a and b for the sigmoid function.

"""

def mylog(v):
"""Log with epilon."""
return np.log(v + 1.0e-200)

out = np.array(logits)
labels = np.array(labels)

if len(logits) != len(labels):
raise ValueError(
(
f"`logits` and `labels` must have same shape: "
f"{len(logits)} vs {len(labels)}"
)
)

target = labels == 1
prior1 = float(np.sum(target))
prior0 = len(target) - prior1
a_ = 0
b_ = np.log((prior0 + 1) / (prior1 + 1))
self.a, self.b = a_, b_

hi_target = (prior1 + 1) / (prior1 + 2)
lo_target = 1 / (prior0 + 2)
labda = 1e-3
olderr = 1e300
pp = np.ones(out.shape) * (prior1 + 1) / (prior0 + prior1 + 2)
idx_t = np.zeros(target.shape)
for _ in range(1, self.num_iters):
a = 0
b = 0
c = 0
d = 0
e = 0
for i, _ in enumerate(out):
if target[i]:
t = hi_target
idx_t[i] = t
else:
t = lo_target
idx_t[i] = t
d1 = pp[i] - t
d2 = pp[i] * (1 - pp[i])
a += out[i] * out[i] * d2
b += d2
c += out[i] * d2
d += out[i] * d1
e += d1

flag = abs(d) < 1.0e-9 and abs(e) < 1.0e-9
if flag:
break

old_a_ = a_
old_b_ = b_
count = 0
while 1:
det = (a + labda) * (b + labda) - c * c
if self._fixer_a * det == 0:
labda *= 10
continue
a_ = old_a_ + ((b + labda) * d - c * e) / det
b_ = old_b_ + ((a + labda) * e - c * d) / det

self.a, self.b = a_, b_
err = 0
for i, _ in enumerate(out):
p = self.transform(out[i])
pp[i] = p
t = idx_t[i]
err -= t * mylog(p) + (1 - t) * mylog(1 - p)

if err < self._fixer_a * olderr * (1 + 1e-7):
labda *= 0.1
break
labda *= 10

if self._fixer_b * labda > 1e6:
break
diff = err - olderr
scale = 0.5 * (err + olderr + 1)

flag = -1e-3 * scale < diff < 1e-7 * scale
if flag:
count += 1
else:
count = 0
olderr = err

if count == 3:
break
self.a, self.b = a_, b_
return self

def transform(self, logits):
"""Tranform input to probabilities basing on trained parameters.

Args:
labels (array like): Classifier labels, must be `+1` vs `-1` or `1` vs `0`.
Returns:
Array of probabilities.

"""
return 1 / (1 + np.exp(logits * self.a + self.b))

def fit_transform(self, logits, labels):
"""Fit and tranform input to probabilities.

Args:
logits (array-like): Classifier output scores.
labels (array like): Classifier labels, must be `+1` vs `-1` or `1` vs `0`.
Returns:
Array of probabilities.

"""
return self.fit(logits, labels).transform(logits)

def __repr__(self):
a, b = self.a, self.b
return "Platt Scaling: " + f"a: {a}, b: {b}"
2 changes: 1 addition & 1 deletion tiatoolbox/tools/stainaugment.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# This file contains code inspired by StainTools
# [https://github.com/Peter554/StainTools] written by Peter Byfield.
#
# The Original Code is Copyright (C) 2021, TIACentre, University of Warwick
# The Original Code is Copyright (C) 2021, TIA Centre, University of Warwick
# All rights reserved.
# ***** END GPL LICENSE BLOCK *****

Expand Down
Loading