Skip to content

Commit 33ce690

Browse files
committed
feat: device-agnostic confidence
1 parent ed31bd6 commit 33ce690

1 file changed

Lines changed: 66 additions & 67 deletions

File tree

Lines changed: 66 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,30 @@
1+
from typing import Tuple
2+
13
import torch
2-
from torch import nn
4+
from torch import Tensor, nn
5+
6+
from .utils import get_device
37

48
# We use standard deviation to measure uncertainity since entropy is not
59
# defined for continuous variables and differential entropy is not ideal.
610
# In case all predictions are identical, std is 0. If 50% are 0 and 50% are
711
# one, it is maximal, i.e. 0.5.
812
MAX_STD = 0.5
9-
MIN_STD = 0.
13+
MIN_STD = 0.0
14+
15+
DEVICE = get_device()
16+
17+
18+
def map_to_device(inputs: Tuple[Tensor, ...]) -> Tuple[Tensor, ...]:
19+
return tuple(x.to(DEVICE) for x in inputs)
1020

1121

1222
def monte_carlo_dropout(
13-
model, regime='loader', loader=None, tensors=None, repetitions=20
23+
model, regime="loader", loader=None, tensors=None, repetitions=20
1424
):
1525
"""
1626
Attempts to approximate epistemic uncertainity through MC dropout.
17-
Performs Monte Carlo dropout for a given model and returns a list of
27+
Performs Monte Carlo dropout for a given model and returns a list of
1828
sample-wise confidence estimates.
1929
This method can be used in two regimes, either by passing a dataloader
2030
or by passing a tensor with the raw input to the model.
@@ -25,7 +35,7 @@ def monte_carlo_dropout(
2535
2636
2737
Arguments:
28-
model (torch.nn.Module): The torch network to be investigated.
38+
model (torch.nn.Module): The torch network to be investigated.
2939
NOTE: Model is assumed to return either a single tensor of
3040
predictions or a n-tupel with the first part being a tensor
3141
of predictions. They need to be [0, 1] where 0 and 1 represent
@@ -50,7 +60,7 @@ def monte_carlo_dropout(
5060
Contains the averaged predictions across all MC dropout estimates.
5161
"""
5262

53-
if regime != 'loader' and regime != 'tensors':
63+
if regime != "loader" and regime != "tensors":
5464
raise ValueError("Choose regime from {'loader', 'tensors'}")
5565

5666
# Activate dropout layers while keeping other rest in eval mode.
@@ -61,53 +71,48 @@ def enable_dropout(m):
6171
model.eval()
6272
model.apply(enable_dropout)
6373

64-
if regime == 'loader':
74+
if regime == "loader":
6575

6676
# Error handling
67-
if not isinstance(
68-
loader.sampler, torch.utils.data.sampler.SequentialSampler
69-
):
77+
if not isinstance(loader.sampler, torch.utils.data.sampler.SequentialSampler):
7078
raise AttributeError(
71-
'Data loader does not use sequential sampling. Consider set'
72-
'ting shuffle=False when instantiating the data loader.'
79+
"Data loader does not use sequential sampling. Consider set"
80+
"ting shuffle=False when instantiating the data loader."
7381
)
7482

7583
# Run over all batches in the loader
7684

7785
def call_fn():
7886
preds = []
79-
for ind, inputs in enumerate(loader):
87+
for inputs in loader:
8088
# inputs is a tuple with the last element being the labels
8189
# outs can be a n-tuple returned by the model
82-
outs = model(*inputs[:-1])
83-
preds.append(outs[0] if isinstance(outs, tuple) else outs)
90+
outs = model(*map_to_device(inputs[:-1]))
91+
preds.append(
92+
outs[0].detach().cpu()
93+
if isinstance(outs, tuple)
94+
else outs.detach().cpu()
95+
)
8496

8597
return torch.cat(preds)
8698

87-
elif regime == 'tensors':
99+
elif regime == "tensors":
88100

89-
if (
90-
not isinstance(tensors, tuple)
91-
and not isinstance(tensors, torch.Tensor)
92-
):
93-
raise ValueError('Tensor needs to either tuple or torch.Tensor')
101+
if not isinstance(tensors, tuple) and not isinstance(tensors, torch.Tensor):
102+
raise ValueError("Tensor needs to either tuple or torch.Tensor")
94103

95-
inputs = tensors if isinstance(tensors, tuple) else (tensors, )
104+
inputs = tensors if isinstance(tensors, tuple) else (tensors,)
96105

97106
def call_fn():
98-
outs = model(*inputs)
107+
outs = model(*map_to_device(inputs))
99108
return outs[0] if isinstance(outs, tuple) else outs
100109

101110
with torch.no_grad():
102-
predictions = [
103-
torch.unsqueeze(call_fn(), -1) for _ in range(repetitions)
104-
]
111+
predictions = [torch.unsqueeze(call_fn(), -1) for _ in range(repetitions)]
105112
predictions = torch.cat(predictions, dim=-1)
106113

107114
# Scale confidences to [0, 1]
108-
confidences = -1 * (
109-
(predictions.std(dim=-1) - MIN_STD) / (MAX_STD - MIN_STD)
110-
) + 1
115+
confidences = -1 * ((predictions.std(dim=-1) - MIN_STD) / (MAX_STD - MIN_STD)) + 1
111116

112117
model.eval()
113118

@@ -116,12 +121,12 @@ def call_fn():
116121

117122
def test_time_augmentation(
118123
model,
119-
regime='loader',
124+
regime="loader",
120125
loader=None,
121126
tensors=None,
122127
repetitions=20,
123128
augmenter=None,
124-
tensors_to_augment=None
129+
tensors_to_augment=None,
125130
):
126131
"""
127132
Attempts to measure aleatoric uncertainity through augmentation during test
@@ -135,7 +140,7 @@ def test_time_augmentation(
135140
classification like MNIST.
136141
137142
Arguments:
138-
model (torch.nn.Module): The torch network to be investigated.
143+
model (torch.nn.Module): The torch network to be investigated.
139144
NOTE: Model is assumed to return either a single tensor of
140145
predictions or a n-tupel with the first part being a tensor
141146
of predictions. They need to be [0, 1] where 0 and 1 represent
@@ -173,86 +178,80 @@ def test_time_augmentation(
173178
Contains the averaged predictions across estimates.
174179
"""
175180

176-
if regime != 'loader' and regime != 'tensors':
181+
if regime != "loader" and regime != "tensors":
177182
raise ValueError("Choose regime from {'loader', 'tensors'}")
178183

179184
model.eval()
180185

181-
if regime == 'loader':
186+
if regime == "loader":
182187

183188
# Error handling
184-
if not isinstance(
185-
loader.sampler, torch.utils.data.sampler.SequentialSampler
186-
):
189+
if not isinstance(loader.sampler, torch.utils.data.sampler.SequentialSampler):
187190
raise AttributeError(
188-
'Data loader does not use sequential sampling. Consider set'
189-
'ting shuffle=False when instantiating the data loader.'
191+
"Data loader does not use sequential sampling. Consider set"
192+
"ting shuffle=False when instantiating the data loader."
190193
)
191194

192195
# Run over all batches in the loader
193196

194197
def call_fn():
195198
preds = []
196-
for ind, inputs in enumerate(loader):
199+
for inputs in loader:
197200
# inputs is a tuple with the last element being the labels
198201
# outs can be a n-tuple returned by the model
199-
outs = model(*inputs[:-1])
202+
outs = model(*map_to_device(inputs[:-1]))
200203
preds.append(outs[0] if isinstance(outs, tuple) else outs)
201204

202205
return torch.cat(preds)
203206

204-
elif regime == 'tensors':
207+
elif regime == "tensors":
205208

206-
if (
207-
not isinstance(tensors, tuple)
208-
and not isinstance(tensors, torch.Tensor)
209-
):
210-
raise ValueError('Tensor needs to either tuple or torch.Tensor')
211-
if (
212-
not isinstance(tensors_to_augment, list)
213-
and not isinstance(tensors_to_augment, int)
209+
if not isinstance(tensors, tuple) and not isinstance(tensors, torch.Tensor):
210+
raise ValueError("Tensor needs to either tuple or torch.Tensor")
211+
if not isinstance(tensors_to_augment, list) and not isinstance(
212+
tensors_to_augment, int
214213
):
215-
raise ValueError('tensors_to_augment needs to be list or int')
214+
raise ValueError("tensors_to_augment needs to be list or int")
216215

217216
# Convert input to common formats (tuples and lists)
218217
tensors_to_augment = (
219218
[tensors_to_augment]
220-
if isinstance(tensors_to_augment, int) else tensors_to_augment
219+
if isinstance(tensors_to_augment, int)
220+
else tensors_to_augment
221221
)
222-
inputs = tensors if isinstance(tensors, tuple) else (tensors, )
223-
aug_fns = augmenter if isinstance(augmenter, tuple) else (augmenter, )
222+
inputs = tensors if isinstance(tensors, tuple) else (tensors,)
223+
aug_fns = augmenter if isinstance(augmenter, tuple) else (augmenter,)
224224

225225
# Error handling
226226
if not len(aug_fns) == len(tensors_to_augment):
227227
raise ValueError(
228-
'Provide one augmenter for each tensor you want to augment.'
228+
"Provide one augmenter for each tensor you want to augment."
229229
)
230230
if max(tensors_to_augment) > len(inputs):
231231
raise ValueError(
232-
'tensors_to_augment should be indexes to the tensors used for '
233-
f'augmentation. {max(tensors_to_augment)} is larger than '
234-
f'length of inputs ({len(inputs)}).'
232+
"tensors_to_augment should be indexes to the tensors used for "
233+
f"augmentation. {max(tensors_to_augment)} is larger than "
234+
f"length of inputs ({len(inputs)})."
235235
)
236236

237237
def call_fn():
238238
# Perform augmentation on all designated functions
239239
augmented_inputs = [
240-
aug_fns[tensors_to_augment[tensors_to_augment == ind]](tensor)
241-
if ind in tensors_to_augment else tensor
240+
(
241+
aug_fns[tensors_to_augment[tensors_to_augment == ind]](tensor)
242+
if ind in tensors_to_augment
243+
else tensor
244+
)
242245
for ind, tensor in enumerate(tensors)
243246
]
244-
outs = model(*augmented_inputs)
247+
outs = model(*map_to_device(augmented_inputs))
245248
return outs[0] if isinstance(outs, tuple) else outs
246249

247250
with torch.no_grad():
248-
predictions = [
249-
torch.unsqueeze(call_fn(), -1) for _ in range(repetitions)
250-
]
251+
predictions = [torch.unsqueeze(call_fn(), -1) for _ in range(repetitions)]
251252
predictions = torch.cat(predictions, dim=-1)
252253

253254
# Scale confidences to [0, 1]
254-
confidences = -1 * (
255-
(predictions.std(dim=-1) - MIN_STD) / (MAX_STD - MIN_STD)
256-
) + 1
255+
confidences = -1 * ((predictions.std(dim=-1) - MIN_STD) / (MAX_STD - MIN_STD)) + 1
257256

258257
return torch.clamp(confidences, min=0), torch.mean(predictions, -1)

0 commit comments

Comments
 (0)