PolyTorch 0.1.3__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.
- polytorch/__init__.py +6 -0
- polytorch/data.py +160 -0
- polytorch/embedding.py +116 -0
- polytorch/enums.py +22 -0
- polytorch/loss.py +35 -0
- polytorch/metrics.py +203 -0
- polytorch/modules.py +41 -0
- polytorch/plots.py +137 -0
- polytorch/util.py +73 -0
- polytorch-0.1.3.dist-info/LICENSE +192 -0
- polytorch-0.1.3.dist-info/METADATA +240 -0
- polytorch-0.1.3.dist-info/RECORD +13 -0
- polytorch-0.1.3.dist-info/WHEEL +4 -0
polytorch/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from .data import BinaryData, CategoricalData, ContinuousData, OrdinalData, HierarchicalData, PolyData
|
|
2
|
+
from .embedding import PolyEmbedding
|
|
3
|
+
from .loss import PolyLoss
|
|
4
|
+
from .enums import ContinuousLossType, CategoricalLossType, BinaryLossType
|
|
5
|
+
from .modules import PolyLinear, PolyLazyLinear
|
|
6
|
+
from .util import split_tensor, total_size
|
polytorch/data.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from torch import nn
|
|
2
|
+
import abc
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
from attrs import define, Factory, field, validators
|
|
5
|
+
import torch.nn.functional as F
|
|
6
|
+
from hierarchicalsoftmax import HierarchicalSoftmaxLoss, SoftmaxNode
|
|
7
|
+
|
|
8
|
+
from .util import permute_feature_axis, squeeze_prediction
|
|
9
|
+
from .enums import ContinuousLossType, BinaryLossType, CategoricalLossType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@define(kw_only=True)
|
|
13
|
+
class PolyData(abc.ABC):
|
|
14
|
+
name: str = Factory(lambda self: self.__class__.__name__, takes_self=True)
|
|
15
|
+
loss_weighting: float = 1.0
|
|
16
|
+
|
|
17
|
+
@abc.abstractmethod
|
|
18
|
+
def embedding_module(self, embedding_size:int) -> nn.Module:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
@abc.abstractmethod
|
|
22
|
+
def size(self) -> int:
|
|
23
|
+
""" The prediction size of this data type. """
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
@abc.abstractmethod
|
|
27
|
+
def calculate_loss(self, prediction, target, feature_axis:int=-1):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def binary_default_factory():
|
|
32
|
+
return ["False", "True"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@define
|
|
36
|
+
class BinaryData(PolyData):
|
|
37
|
+
loss_type:BinaryLossType = BinaryLossType.CROSS_ENTROPY
|
|
38
|
+
labels:List[str] = field(factory=binary_default_factory)
|
|
39
|
+
colors:Optional[List[str]] = None
|
|
40
|
+
|
|
41
|
+
def embedding_module(self, embedding_size:int) -> nn.Module:
|
|
42
|
+
return nn.Embedding(2, embedding_size)
|
|
43
|
+
|
|
44
|
+
def size(self) -> int:
|
|
45
|
+
return 1
|
|
46
|
+
|
|
47
|
+
def calculate_loss(self, prediction, target, feature_axis:int=-1):
|
|
48
|
+
prediction = squeeze_prediction(prediction, target, feature_axis)
|
|
49
|
+
|
|
50
|
+
if self.loss_type == BinaryLossType.CROSS_ENTROPY:
|
|
51
|
+
return F.binary_cross_entropy_with_logits(
|
|
52
|
+
prediction,
|
|
53
|
+
target.float(),
|
|
54
|
+
)
|
|
55
|
+
elif self.loss_type == BinaryLossType.IOU:
|
|
56
|
+
from .metrics import calc_iou
|
|
57
|
+
return 1 - calc_iou(
|
|
58
|
+
prediction.sigmoid(),
|
|
59
|
+
target,
|
|
60
|
+
)
|
|
61
|
+
elif self.loss_type == BinaryLossType.DICE:
|
|
62
|
+
from .metrics import calc_dice_score
|
|
63
|
+
return 1 - calc_dice_score(
|
|
64
|
+
prediction.sigmoid(),
|
|
65
|
+
target,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
raise NotImplementedError(f"Unknown loss type: {self.loss_type} for {self.__class__.__name__}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@define
|
|
72
|
+
class CategoricalData(PolyData):
|
|
73
|
+
category_count:int
|
|
74
|
+
loss_type:CategoricalLossType = CategoricalLossType.CROSS_ENTROPY
|
|
75
|
+
labels:Optional[List[str]] = None
|
|
76
|
+
colors:Optional[List[str]] = None
|
|
77
|
+
label_smoothing:float = field(default=0.0, validator=[validators.ge(0.0), validators.le(1.0)])
|
|
78
|
+
|
|
79
|
+
def embedding_module(self, embedding_size:int) -> nn.Module:
|
|
80
|
+
return nn.Embedding(self.category_count, embedding_size)
|
|
81
|
+
|
|
82
|
+
def size(self) -> int:
|
|
83
|
+
return self.category_count
|
|
84
|
+
|
|
85
|
+
def calculate_loss(self, prediction, target, feature_axis:int=-1):
|
|
86
|
+
if self.loss_type == CategoricalLossType.CROSS_ENTROPY:
|
|
87
|
+
prediction = permute_feature_axis(prediction, old_axis=feature_axis, new_axis=1)
|
|
88
|
+
return F.cross_entropy(
|
|
89
|
+
prediction,
|
|
90
|
+
target.long(),
|
|
91
|
+
reduction="none",
|
|
92
|
+
label_smoothing=self.label_smoothing,
|
|
93
|
+
)
|
|
94
|
+
elif self.loss_type == CategoricalLossType.DICE:
|
|
95
|
+
from .metrics import calc_generalized_dice_score
|
|
96
|
+
return 1. - calc_generalized_dice_score(
|
|
97
|
+
prediction.softmax(dim=feature_axis),
|
|
98
|
+
target,
|
|
99
|
+
feature_axis=feature_axis,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
raise NotImplementedError(f"Unknown loss type: {self.loss_type} for {self.__class__.__name__}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@define
|
|
106
|
+
class OrdinalData(CategoricalData):
|
|
107
|
+
color:str = ""
|
|
108
|
+
# add in option to estimate distances or to set them?
|
|
109
|
+
|
|
110
|
+
def embedding_module(self, embedding_size:int) -> nn.Module:
|
|
111
|
+
from .embedding import OrdinalEmbedding
|
|
112
|
+
return OrdinalEmbedding(self.category_count, embedding_size)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@define
|
|
116
|
+
class ContinuousData(PolyData):
|
|
117
|
+
loss_type:ContinuousLossType = ContinuousLossType.SMOOTH_L1_LOSS
|
|
118
|
+
color:str = ""
|
|
119
|
+
mean:Optional[float] = None
|
|
120
|
+
stdev:Optional[float] = None
|
|
121
|
+
|
|
122
|
+
def embedding_module(self, embedding_size:int) -> nn.Module:
|
|
123
|
+
from .embedding import ContinuousEmbedding
|
|
124
|
+
return ContinuousEmbedding(embedding_size, mean=self.mean, stdev=self.stdev)
|
|
125
|
+
|
|
126
|
+
def size(self) -> int:
|
|
127
|
+
return 1
|
|
128
|
+
|
|
129
|
+
def calculate_loss(self, prediction, target, feature_axis:int=-1):
|
|
130
|
+
prediction = squeeze_prediction(prediction, target, feature_axis)
|
|
131
|
+
if self.mean is not None:
|
|
132
|
+
target = target - self.mean
|
|
133
|
+
if self.stdev is not None:
|
|
134
|
+
target = target / self.stdev
|
|
135
|
+
return self.loss_func(prediction, target, reduction="none")
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def loss_func(self):
|
|
139
|
+
return getattr(F, self.loss_type.name.lower())
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@define
|
|
143
|
+
class HierarchicalData(PolyData):
|
|
144
|
+
root:SoftmaxNode
|
|
145
|
+
|
|
146
|
+
def embedding_module(self, embedding_size:int) -> nn.Module:
|
|
147
|
+
raise NotImplementedError("Hierarchical data types are not yet supported for embedding.")
|
|
148
|
+
|
|
149
|
+
def size(self) -> int:
|
|
150
|
+
self.root.set_indexes_if_unset()
|
|
151
|
+
return self.root.layer_size
|
|
152
|
+
|
|
153
|
+
def calculate_loss(self, prediction, target, feature_axis:int=-1):
|
|
154
|
+
prediction = permute_feature_axis(prediction, old_axis=feature_axis, new_axis=1)
|
|
155
|
+
return self.loss_module(prediction, target)
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def loss_module(self):
|
|
159
|
+
return HierarchicalSoftmaxLoss(root=self.root)
|
|
160
|
+
|
polytorch/embedding.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
import torch
|
|
4
|
+
from torch import nn
|
|
5
|
+
from torch.nn.parameter import Parameter
|
|
6
|
+
import torch.nn.functional as F
|
|
7
|
+
import plotly.graph_objects as go
|
|
8
|
+
|
|
9
|
+
from .data import PolyData
|
|
10
|
+
from .util import permute_feature_axis
|
|
11
|
+
|
|
12
|
+
class ContinuousEmbedding(nn.Module):
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
embedding_size:int,
|
|
16
|
+
bias:bool=True,
|
|
17
|
+
mean:float|None=None,
|
|
18
|
+
stdev:float|None=None,
|
|
19
|
+
device=None,
|
|
20
|
+
dtype=None,
|
|
21
|
+
**kwargs,
|
|
22
|
+
):
|
|
23
|
+
super().__init__(**kwargs)
|
|
24
|
+
|
|
25
|
+
self.embedding_size = embedding_size
|
|
26
|
+
self.mean = mean
|
|
27
|
+
self.stdev = stdev
|
|
28
|
+
|
|
29
|
+
factory_kwargs = {'device': device, 'dtype': dtype}
|
|
30
|
+
self.weight = Parameter(torch.empty((embedding_size,), **factory_kwargs), requires_grad=True)
|
|
31
|
+
if bias:
|
|
32
|
+
self.bias = Parameter(torch.empty((embedding_size,), **factory_kwargs), requires_grad=True)
|
|
33
|
+
else:
|
|
34
|
+
self.bias = Parameter(torch.zeros((embedding_size,), **factory_kwargs), requires_grad=False)
|
|
35
|
+
|
|
36
|
+
self.reset_parameters()
|
|
37
|
+
|
|
38
|
+
def forward(self, input):
|
|
39
|
+
x = input.flatten().unsqueeze(1)
|
|
40
|
+
if self.mean is not None:
|
|
41
|
+
x = x - self.mean
|
|
42
|
+
if self.stdev is not None:
|
|
43
|
+
x = x/self.stdev
|
|
44
|
+
embedded = self.bias + x * self.weight.unsqueeze(0)
|
|
45
|
+
embedded = embedded.reshape(input.shape + (-1,))
|
|
46
|
+
|
|
47
|
+
return embedded
|
|
48
|
+
|
|
49
|
+
def reset_parameters(self) -> None:
|
|
50
|
+
torch.nn.init.normal_(self.weight)
|
|
51
|
+
torch.nn.init.constant_(self.bias, 0.0)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class OrdinalEmbedding(ContinuousEmbedding):
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
category_count,
|
|
58
|
+
embedding_size,
|
|
59
|
+
bias:bool=True,
|
|
60
|
+
device=None,
|
|
61
|
+
dtype=None,
|
|
62
|
+
**kwargs,
|
|
63
|
+
):
|
|
64
|
+
super().__init__(
|
|
65
|
+
embedding_size,
|
|
66
|
+
bias=bias,
|
|
67
|
+
device=device,
|
|
68
|
+
dtype=dtype,
|
|
69
|
+
**kwargs,
|
|
70
|
+
)
|
|
71
|
+
factory_kwargs = {'device': device, 'dtype': dtype}
|
|
72
|
+
self.distance_scores = Parameter(torch.ones((category_count-1,), **factory_kwargs), requires_grad=True)
|
|
73
|
+
|
|
74
|
+
def forward(self, x):
|
|
75
|
+
distances = torch.cumsum(F.softmax(self.distance_scores, dim=0), dim=0)
|
|
76
|
+
|
|
77
|
+
# prepend zero
|
|
78
|
+
distances = torch.cat([torch.zeros((1,), device=distances.device, dtype=distances.dtype), distances])
|
|
79
|
+
distance = torch.gather(distances, 0, x.flatten())
|
|
80
|
+
embedded = self.bias + distance.unsqueeze(1) * self.weight.unsqueeze(0)
|
|
81
|
+
embedded = embedded.reshape(x.shape + (-1,))
|
|
82
|
+
|
|
83
|
+
return embedded
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class PolyEmbedding(nn.Module):
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
input_types:List[PolyData],
|
|
90
|
+
embedding_size:int,
|
|
91
|
+
feature_axis:int=-1,
|
|
92
|
+
**kwargs,
|
|
93
|
+
):
|
|
94
|
+
super().__init__(**kwargs)
|
|
95
|
+
self.input_types = input_types
|
|
96
|
+
self.embedding_size = embedding_size
|
|
97
|
+
self.embedding_modules = nn.ModuleList([
|
|
98
|
+
input.embedding_module(embedding_size) for input in input_types
|
|
99
|
+
])
|
|
100
|
+
self.feature_axis = feature_axis
|
|
101
|
+
|
|
102
|
+
def forward(self, *inputs):
|
|
103
|
+
shape = inputs[0].shape + (self.embedding_size,)
|
|
104
|
+
embedded = torch.zeros( shape, device=inputs[0].device )
|
|
105
|
+
|
|
106
|
+
for input, module in zip(inputs, self.embedding_modules):
|
|
107
|
+
if input.dtype == torch.bool:
|
|
108
|
+
input = input.int()
|
|
109
|
+
embedded += module(input)
|
|
110
|
+
|
|
111
|
+
return permute_feature_axis(embedded, old_axis=-1, new_axis=self.feature_axis)
|
|
112
|
+
|
|
113
|
+
def plot(self, **kwargs) -> go.Figure:
|
|
114
|
+
from .plots import plot_embedding
|
|
115
|
+
return plot_embedding(self, **kwargs)
|
|
116
|
+
|
polytorch/enums.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
import torch.nn.functional as F
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ContinuousLossType(Enum):
|
|
6
|
+
L1_LOSS = 0
|
|
7
|
+
MSE_LOSS = 1
|
|
8
|
+
SMOOTH_L1_LOSS = 2
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BinaryLossType(Enum):
|
|
12
|
+
CROSS_ENTROPY = 0
|
|
13
|
+
IOU = 1
|
|
14
|
+
DICE = 2
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CategoricalLossType(Enum):
|
|
18
|
+
CROSS_ENTROPY = 0
|
|
19
|
+
DICE = 1
|
|
20
|
+
# TODO: Focal Loss
|
|
21
|
+
# TODO Wasserstein distance (Earth mover loss)
|
|
22
|
+
|
polytorch/loss.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from torch import nn
|
|
3
|
+
|
|
4
|
+
from .data import PolyData
|
|
5
|
+
from .util import split_tensor
|
|
6
|
+
|
|
7
|
+
class PolyLoss(nn.Module):
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
data_types:List[PolyData],
|
|
11
|
+
feature_axis:int=-1,
|
|
12
|
+
**kwargs,
|
|
13
|
+
):
|
|
14
|
+
super().__init__(**kwargs)
|
|
15
|
+
self.data_types = data_types
|
|
16
|
+
self.feature_axis = feature_axis
|
|
17
|
+
|
|
18
|
+
def forward(self, predictions, *targets):
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if not isinstance(predictions, (tuple, list)):
|
|
22
|
+
predictions = split_tensor(predictions, self.data_types, feature_axis=self.feature_axis)
|
|
23
|
+
|
|
24
|
+
assert len(predictions) == len(targets) == len(self.data_types)
|
|
25
|
+
|
|
26
|
+
loss = 0.0
|
|
27
|
+
for prediction, target, data_type in zip(predictions, targets, self.data_types):
|
|
28
|
+
feature_axis = self.feature_axis % len(prediction.shape)
|
|
29
|
+
|
|
30
|
+
if not hasattr(data_type, "calculate_loss"):
|
|
31
|
+
raise ValueError(f"Data type {data_type} does not have a calculate_loss method")
|
|
32
|
+
|
|
33
|
+
loss += data_type.loss_weighting * data_type.calculate_loss(prediction, target, feature_axis=feature_axis).mean()
|
|
34
|
+
|
|
35
|
+
return loss
|
polytorch/metrics.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
from attrs import define, Factory
|
|
2
|
+
import torch
|
|
3
|
+
import torch.nn.functional as F
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from hierarchicalsoftmax import SoftmaxNode, greedy_accuracy
|
|
6
|
+
|
|
7
|
+
from .util import squeeze_prediction
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_predictions_target_for_index(predictions, *targets, data_index=None, feature_axis=-1):
|
|
11
|
+
if not isinstance(predictions, (tuple, list)):
|
|
12
|
+
predictions = (predictions,)
|
|
13
|
+
|
|
14
|
+
assert len(predictions) == len(targets)
|
|
15
|
+
assert data_index is not None, "data_index must be specified"
|
|
16
|
+
assert data_index >= 0, "data_index must be non-negative"
|
|
17
|
+
|
|
18
|
+
my_predictions = predictions[data_index]
|
|
19
|
+
my_targets = targets[data_index]
|
|
20
|
+
|
|
21
|
+
return my_predictions, my_targets
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def function_metric(predictions, *targets, data_index=None, feature_axis=-1, function=None) -> torch.Tensor:
|
|
25
|
+
my_predictions, my_targets = get_predictions_target_for_index(predictions, *targets, data_index=data_index, feature_axis=feature_axis)
|
|
26
|
+
my_predictions = squeeze_prediction(my_predictions, my_targets, feature_axis)
|
|
27
|
+
return function(my_predictions, my_targets)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def categorical_accuracy(predictions, *targets, data_index=None, feature_axis=-1) -> torch.Tensor:
|
|
31
|
+
my_predictions, my_targets = get_predictions_target_for_index(predictions, *targets, data_index=data_index, feature_axis=feature_axis)
|
|
32
|
+
my_predictions = torch.argmax(my_predictions, dim=feature_axis)
|
|
33
|
+
|
|
34
|
+
accuracy = (my_predictions == my_targets).float().mean()
|
|
35
|
+
return accuracy
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def binary_accuracy(predictions, *targets, data_index=None, feature_axis=-1) -> torch.Tensor:
|
|
39
|
+
my_predictions, my_targets = get_predictions_target_for_index(predictions, *targets, data_index=data_index, feature_axis=feature_axis)
|
|
40
|
+
my_predictions = my_predictions >= 0.0
|
|
41
|
+
my_predictions = squeeze_prediction(my_predictions, my_targets, feature_axis)
|
|
42
|
+
|
|
43
|
+
accuracy = (my_predictions == my_targets).float().mean()
|
|
44
|
+
return accuracy
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def mse(predictions, *targets, data_index=None, feature_axis=-1) -> torch.Tensor:
|
|
48
|
+
return function_metric(predictions, *targets, data_index=data_index, feature_axis=feature_axis, function=F.mse_loss)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def l1(predictions, *targets, data_index=None, feature_axis=-1) -> torch.Tensor:
|
|
52
|
+
return function_metric(predictions, *targets, data_index=data_index, feature_axis=feature_axis, function=F.l1_loss)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@define(kw_only=True)
|
|
56
|
+
class PolyMetric():
|
|
57
|
+
data_index:int
|
|
58
|
+
feature_axis:int = -1
|
|
59
|
+
function:Optional[callable] = None
|
|
60
|
+
name:str = Factory(lambda self:self.default_name(), takes_self=True)
|
|
61
|
+
|
|
62
|
+
def default_name(self):
|
|
63
|
+
if self.function:
|
|
64
|
+
return self.function.__name__
|
|
65
|
+
return self.__class__.__name__
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def __name__(self):
|
|
69
|
+
""" For using as a FastAI metric. """
|
|
70
|
+
return self.name
|
|
71
|
+
|
|
72
|
+
def __call__(self, predictions, *targets):
|
|
73
|
+
my_predictions, my_targets = get_predictions_target_for_index(predictions, *targets, data_index=self.data_index, feature_axis=self.feature_axis)
|
|
74
|
+
my_predictions = squeeze_prediction(my_predictions, my_targets, self.feature_axis)
|
|
75
|
+
|
|
76
|
+
return self.calc(my_predictions, my_targets)
|
|
77
|
+
|
|
78
|
+
def calc(self, predictions, targets):
|
|
79
|
+
if self.function:
|
|
80
|
+
return self.function(predictions, targets)
|
|
81
|
+
|
|
82
|
+
raise NotImplementedError(f"No function given. Either implement calc or provide a function to {self.name}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@define
|
|
86
|
+
class HierarchicalGreedyAccuracy(PolyMetric):
|
|
87
|
+
root:SoftmaxNode
|
|
88
|
+
max_depth:Optional[int]=None
|
|
89
|
+
|
|
90
|
+
def calc(self, predictions, targets):
|
|
91
|
+
return greedy_accuracy(predictions, targets, self.root, max_depth=self.max_depth)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class CategoricalAccuracy(PolyMetric):
|
|
95
|
+
def calc(self, predictions, targets):
|
|
96
|
+
predictions = torch.max(predictions, dim=self.feature_axis).indices # should be argmax but there is an issue using MPS
|
|
97
|
+
return (predictions == targets).float().mean()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def smooth_l1(predictions, *targets, data_index=None, feature_axis=-1) -> torch.Tensor:
|
|
101
|
+
return function_metric(predictions, *targets, data_index=data_index, feature_axis=feature_axis, function=F.smooth_l1_loss)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def calc_dice_score(predictions, target, smooth:float=1.) -> torch.Tensor:
|
|
105
|
+
predictions = predictions.view(-1)
|
|
106
|
+
target = target.view(-1)
|
|
107
|
+
intersection = (predictions * target).sum()
|
|
108
|
+
|
|
109
|
+
return ((2. * intersection + smooth) /
|
|
110
|
+
(predictions.sum() + target.sum() + smooth)
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def calc_generalized_dice_score(predictions, target, power:float=2.0, smooth:float=1.0, feature_axis:int=-1) -> torch.Tensor:
|
|
115
|
+
"""
|
|
116
|
+
A generalized Dice score for multi-class segmentation.
|
|
117
|
+
|
|
118
|
+
If power=0.0, this is equivalent to normal Dice score (i.e. volume "implicit" weighting)
|
|
119
|
+
If power=1.0, this is equivalent to 'equal' weighting.
|
|
120
|
+
If power=2.0, this is equivalent to 'inverse volume' weighting.
|
|
121
|
+
|
|
122
|
+
See:
|
|
123
|
+
- https://www.sciencedirect.com/science/article/pii/S2590005619300049#bib73
|
|
124
|
+
- https://arxiv.org/pdf/1707.03237.pdf
|
|
125
|
+
- https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=1717643
|
|
126
|
+
"""
|
|
127
|
+
n_classes = predictions.shape[feature_axis]
|
|
128
|
+
numerator = 0.0
|
|
129
|
+
denominator = 0.0
|
|
130
|
+
slice_indices = [slice(0, None)] * len(predictions.shape)
|
|
131
|
+
for i in range(n_classes):
|
|
132
|
+
my_target = (target == i)
|
|
133
|
+
my_target_sum = my_target.sum()
|
|
134
|
+
weight = 1/(my_target_sum**power + smooth)
|
|
135
|
+
|
|
136
|
+
slice_indices[feature_axis] = i
|
|
137
|
+
my_predictions = predictions[slice_indices]
|
|
138
|
+
|
|
139
|
+
numerator += weight * (my_predictions*my_target).sum()
|
|
140
|
+
denominator += weight * (my_predictions.sum() + my_target_sum)
|
|
141
|
+
|
|
142
|
+
return 2. * numerator / denominator
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def calc_iou(predictions, target, smooth:float=1.):
|
|
146
|
+
predictions = predictions.view(-1)
|
|
147
|
+
target = target.view(-1)
|
|
148
|
+
intersection = (predictions * target).sum()
|
|
149
|
+
union = predictions.sum() + target.sum() - intersection
|
|
150
|
+
|
|
151
|
+
return (intersection + smooth) / (union + smooth)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def binary_dice(predictions, *targets, data_index=None, feature_axis=-1):
|
|
155
|
+
my_predictions, my_targets = get_predictions_target_for_index(predictions, *targets, data_index=data_index, feature_axis=feature_axis)
|
|
156
|
+
my_predictions = my_predictions >= 0.0
|
|
157
|
+
my_predictions = squeeze_prediction(my_predictions, my_targets, feature_axis)
|
|
158
|
+
|
|
159
|
+
return calc_dice_score(my_predictions, my_targets)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def binary_iou(predictions, *targets, data_index=None, feature_axis=-1):
|
|
163
|
+
my_predictions, my_targets = get_predictions_target_for_index(predictions, *targets, data_index=data_index, feature_axis=feature_axis)
|
|
164
|
+
my_predictions = my_predictions >= 0.0
|
|
165
|
+
my_predictions = squeeze_prediction(my_predictions, my_targets, feature_axis)
|
|
166
|
+
|
|
167
|
+
return calc_iou(my_predictions, my_targets)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def generalized_dice(predictions, *targets, data_index=None, feature_axis=-1, perform_softmax:bool=True, power:float=2.0) -> torch.Tensor:
|
|
171
|
+
"""
|
|
172
|
+
Calculate the generalized dice score for a single data index. Used for for multi-class segmentation.
|
|
173
|
+
|
|
174
|
+
See:
|
|
175
|
+
- https://www.sciencedirect.com/science/article/pii/S2590005619300049#bib73
|
|
176
|
+
- https://arxiv.org/pdf/1707.03237.pdf
|
|
177
|
+
- https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=1717643
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
predictions (_type_): Logits or probabilities. If probabilities, perform_softmax must be False.
|
|
181
|
+
data_index (_type_, optional): The index of the target to . Defaults to None.
|
|
182
|
+
feature_axis (int, optional): _description_. Defaults to -1.
|
|
183
|
+
perform_softmax (bool, optional): Whether or not to normalize the predictions using the softmax function. Defaults to True.
|
|
184
|
+
power (float, optional): The power to use for the generalized dice score. Defaults to 2.0 (i.e. inverse volume weighting)
|
|
185
|
+
If power=0.0, this is equivalent to normal Dice score (i.e. volume "implicit" weighting)
|
|
186
|
+
If power=1.0, this is equivalent to 'equal' weighting.
|
|
187
|
+
If power=2.0, this is equivalent to 'inverse volume' weighting.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
torch.Tensor: The generalized dice score for the given data index.
|
|
191
|
+
"""
|
|
192
|
+
my_predictions, my_targets = get_predictions_target_for_index(predictions, *targets, data_index=data_index, feature_axis=feature_axis)
|
|
193
|
+
if perform_softmax:
|
|
194
|
+
my_predictions = my_predictions.softmax(dim=feature_axis)
|
|
195
|
+
|
|
196
|
+
score = calc_generalized_dice_score(
|
|
197
|
+
my_predictions,
|
|
198
|
+
my_targets,
|
|
199
|
+
feature_axis=feature_axis,
|
|
200
|
+
power=power,
|
|
201
|
+
)
|
|
202
|
+
return score
|
|
203
|
+
|
polytorch/modules.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from torch import nn
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
from .data import PolyData
|
|
5
|
+
from .util import total_size, split_tensor
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PolyLayerError(RuntimeError):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PolyLayerMixin():
|
|
13
|
+
def __init__(self, output_types:List[PolyData], out_features=None, **kwargs):
|
|
14
|
+
self.output_types = output_types
|
|
15
|
+
|
|
16
|
+
if out_features is not None:
|
|
17
|
+
raise PolyLayerError(
|
|
18
|
+
"Trying to create a PolyLinear Layer by explicitly setting `out_features`. "
|
|
19
|
+
"This value should be determined from the list of output types and not the `out_features` argument."
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
super().__init__(out_features=total_size(self.output_types), **kwargs)
|
|
23
|
+
|
|
24
|
+
def forward(self, *inputs):
|
|
25
|
+
outputs = super().forward(*inputs)
|
|
26
|
+
return split_tensor(outputs, self.output_types, feature_axis=-1)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PolyLinear(PolyLayerMixin, nn.Linear):
|
|
30
|
+
"""
|
|
31
|
+
Creates a linear layer designed to be the final layer in a neural network model that produces unnormalized scores given to PolyLoss.
|
|
32
|
+
|
|
33
|
+
The `out_features` value is set internally from root.layer_size and cannot be given as an argument.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
class PolyLazyLinear(PolyLayerMixin, nn.LazyLinear):
|
|
37
|
+
"""
|
|
38
|
+
Creates a lazy linear layer designed to be the final layer in a neural network model that produces unnormalized scores given to PolyLoss.
|
|
39
|
+
|
|
40
|
+
The `out_features` value is set internally from root.layer_size and cannot be given as an argument.
|
|
41
|
+
"""
|
polytorch/plots.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import plotly.graph_objects as go
|
|
3
|
+
from sklearn.decomposition import PCA
|
|
4
|
+
from itertools import cycle
|
|
5
|
+
import torch
|
|
6
|
+
|
|
7
|
+
from .embedding import PolyEmbedding
|
|
8
|
+
from .data import CategoricalData, OrdinalData
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def format_fig(fig) -> go.Figure:
|
|
12
|
+
"""Formats a plotly figure in a nicer way."""
|
|
13
|
+
fig.update_layout(
|
|
14
|
+
width=1000,
|
|
15
|
+
height=800,
|
|
16
|
+
plot_bgcolor="white",
|
|
17
|
+
title_font_color="black",
|
|
18
|
+
font=dict(
|
|
19
|
+
family="Linux Libertine Display O",
|
|
20
|
+
size=18,
|
|
21
|
+
color="black",
|
|
22
|
+
),
|
|
23
|
+
)
|
|
24
|
+
gridcolor = "#dddddd"
|
|
25
|
+
fig.update_xaxes(gridcolor=gridcolor)
|
|
26
|
+
fig.update_yaxes(gridcolor=gridcolor)
|
|
27
|
+
|
|
28
|
+
fig.update_xaxes(showline=True, linewidth=1, linecolor='black', mirror=True, ticks='outside', zeroline=True, zerolinewidth=1, zerolinecolor='black')
|
|
29
|
+
fig.update_yaxes(showline=True, linewidth=1, linecolor='black', mirror=True, ticks='outside', zeroline=True, zerolinewidth=1, zerolinecolor='black')
|
|
30
|
+
|
|
31
|
+
return fig
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def plot_embedding(embedding:PolyEmbedding, n_components:int=2, show:bool=False, output_path:str|Path|None=None) -> go.Figure:
|
|
35
|
+
"""
|
|
36
|
+
Plots the embedding in 2D or 3D.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
embedding (PolyEmbedding): The embedding to plot.
|
|
40
|
+
n_components (int, optional): The number of principal components to plot. Can be 2 or 3. Defaults to 2.
|
|
41
|
+
show (bool, optional): Whether to show the plot. Defaults to False.
|
|
42
|
+
output_path (str|Path|None, optional): The path to save the plot to.
|
|
43
|
+
Can be in HTML, PNG, JPEG, SVG or PDF. Defaults to None.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
go.Figure: The plotly figure
|
|
47
|
+
"""
|
|
48
|
+
if n_components not in [2, 3]:
|
|
49
|
+
raise ValueError(f"n_components must be 2 or 3, not {n_components}")
|
|
50
|
+
|
|
51
|
+
# get embedding weights
|
|
52
|
+
weights = []
|
|
53
|
+
labels = []
|
|
54
|
+
colors = []
|
|
55
|
+
|
|
56
|
+
# The default colors are same as px.colors.qualitative.Plotly
|
|
57
|
+
# I'm not using that directly because that requires plotly express
|
|
58
|
+
# which requires pandas to be installed
|
|
59
|
+
cmap = cycle(['#636EFA', '#EF553B', '#00CC96', '#AB63FA', '#FFA15A', '#19D3F3', '#FF6692', '#B6E880', '#FF97FF', '#FECB52'])
|
|
60
|
+
|
|
61
|
+
for input_type, module in zip(embedding.input_types, embedding.embedding_modules):
|
|
62
|
+
weight = module.weight
|
|
63
|
+
if len(weight.shape) == 1:
|
|
64
|
+
weight = weight.unsqueeze(0)
|
|
65
|
+
|
|
66
|
+
weights.append(weight)
|
|
67
|
+
|
|
68
|
+
if isinstance(input_type, CategoricalData) and not isinstance(input_type, OrdinalData):
|
|
69
|
+
my_labels = (
|
|
70
|
+
input_type.labels if input_type.labels is not None
|
|
71
|
+
else [f"{input_type.name}_{i}" for i in range(input_type.category_count)]
|
|
72
|
+
)
|
|
73
|
+
labels.extend(my_labels)
|
|
74
|
+
|
|
75
|
+
my_colors = (
|
|
76
|
+
input_type.colors if input_type.colors is not None
|
|
77
|
+
else [next(cmap) for _ in range(input_type.category_count)]
|
|
78
|
+
)
|
|
79
|
+
colors.extend(my_colors)
|
|
80
|
+
else:
|
|
81
|
+
labels.append(input_type.name)
|
|
82
|
+
colors.append(getattr(input_type, "color", '') or next(cmap))
|
|
83
|
+
|
|
84
|
+
weights = torch.cat(weights, dim=0).detach()
|
|
85
|
+
|
|
86
|
+
# Perform a principal component analysis
|
|
87
|
+
pca = PCA(n_components=n_components)
|
|
88
|
+
weights_reduced = pca.fit_transform(weights)
|
|
89
|
+
|
|
90
|
+
# plot
|
|
91
|
+
fig = go.Figure()
|
|
92
|
+
|
|
93
|
+
# This is done as a loop so that the legend has all the different categorical labels separate
|
|
94
|
+
# This will be a large list in the legend potentially
|
|
95
|
+
# This could be an option in the future
|
|
96
|
+
for vector, label, color in zip(weights_reduced, labels, colors):
|
|
97
|
+
if n_components == 2:
|
|
98
|
+
fig.add_trace(go.Scatter(
|
|
99
|
+
x=[vector[0]],
|
|
100
|
+
y=[vector[1]],
|
|
101
|
+
mode='markers',
|
|
102
|
+
name=label,
|
|
103
|
+
marker_color=color,
|
|
104
|
+
))
|
|
105
|
+
fig.update_xaxes(title_text="Component 1")
|
|
106
|
+
fig.update_yaxes(title_text="Component 2")
|
|
107
|
+
elif n_components == 3:
|
|
108
|
+
fig.add_trace(go.Scatter3d(
|
|
109
|
+
x=[vector[0]],
|
|
110
|
+
y=[vector[1]],
|
|
111
|
+
z=[vector[2]],
|
|
112
|
+
mode='markers',
|
|
113
|
+
name=label,
|
|
114
|
+
marker_color=color,
|
|
115
|
+
))
|
|
116
|
+
fig.update_layout(scene = dict(
|
|
117
|
+
xaxis_title='Component 1',
|
|
118
|
+
yaxis_title='Component 2',
|
|
119
|
+
zaxis_title='Component 3',
|
|
120
|
+
))
|
|
121
|
+
|
|
122
|
+
fig.update_layout(showlegend=True)
|
|
123
|
+
format_fig(fig)
|
|
124
|
+
|
|
125
|
+
# output image as file
|
|
126
|
+
if output_path:
|
|
127
|
+
output_path = Path(output_path)
|
|
128
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
129
|
+
if output_path.suffix == '.html':
|
|
130
|
+
fig.write_html(str(output_path))
|
|
131
|
+
else:
|
|
132
|
+
fig.write_image(str(output_path))
|
|
133
|
+
|
|
134
|
+
if show:
|
|
135
|
+
fig.show()
|
|
136
|
+
|
|
137
|
+
return fig
|
polytorch/util.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from typing import List, Tuple
|
|
2
|
+
from torch import Tensor
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def total_size(data_types) -> int:
|
|
6
|
+
"""
|
|
7
|
+
Calculates the total number of features required to predict a list of output types.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
data_types (List[PolyData]): The data types to predict.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
int: The number of features required to predict the given data types.
|
|
14
|
+
"""
|
|
15
|
+
return sum(data_type.size() for data_type in data_types)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def split_tensor(tensor:Tensor, data_types, feature_axis:int=-1) -> Tuple[Tensor, ...]:
|
|
19
|
+
"""
|
|
20
|
+
Splits a tensor into a tuple of tensors, one for each data type.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
tensor (Tensor): The predictions tensor.
|
|
24
|
+
data_types (List[PolyData]): The data types to predict.
|
|
25
|
+
feature_axis (int, optional): The axis which has the features to predict. Defaults to last axis.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Tuple[Tensor, ...]: A tuple of tensors, one for each data type.
|
|
29
|
+
"""
|
|
30
|
+
current_index = 0
|
|
31
|
+
split_tensors = []
|
|
32
|
+
slice_indices = [slice(0, None)] * len(tensor.shape)
|
|
33
|
+
for data_type in data_types:
|
|
34
|
+
size = data_type.size()
|
|
35
|
+
slice_indices[feature_axis] = slice(current_index,current_index+size)
|
|
36
|
+
split_tensors.append(tensor[slice_indices])
|
|
37
|
+
current_index += size
|
|
38
|
+
|
|
39
|
+
assert current_index == tensor.shape[feature_axis]
|
|
40
|
+
|
|
41
|
+
return tuple(split_tensors)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def permute_feature_axis(tensor:Tensor, old_axis:int, new_axis:int) -> Tensor:
|
|
45
|
+
"""
|
|
46
|
+
Changes the shape of a tensor so that the feature axis is in a new axis.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
tensor (torch.Tensor): The tensor to permute.
|
|
50
|
+
new_axis (int): The desired index of the feature axis.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
torch.Tensor: The predictions tensor with the feature axis at the specified index.
|
|
54
|
+
"""
|
|
55
|
+
axes_count = len(tensor.shape)
|
|
56
|
+
if old_axis % axes_count != new_axis % axes_count:
|
|
57
|
+
axes = list(range(axes_count))
|
|
58
|
+
axes.insert(new_axis, axes.pop(old_axis))
|
|
59
|
+
return tensor.permute(*axes)
|
|
60
|
+
return tensor
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def squeeze_prediction(prediction:Tensor, target:Tensor, feature_axis:int):
|
|
64
|
+
"""
|
|
65
|
+
Squeeze feature axis if necessary
|
|
66
|
+
"""
|
|
67
|
+
feature_axis = feature_axis % len(prediction.shape)
|
|
68
|
+
if (
|
|
69
|
+
len(prediction.shape) == len(target.shape) + 1 and
|
|
70
|
+
prediction.shape[:feature_axis] + prediction.shape[feature_axis+1:] == target.shape
|
|
71
|
+
):
|
|
72
|
+
prediction = prediction.squeeze(feature_axis)
|
|
73
|
+
return prediction
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
Apache License
|
|
4
|
+
Version 2.0, January 2004
|
|
5
|
+
http://www.apache.org/licenses/
|
|
6
|
+
|
|
7
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
8
|
+
|
|
9
|
+
1. Definitions.
|
|
10
|
+
|
|
11
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
12
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
13
|
+
|
|
14
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
15
|
+
the copyright owner that is granting the License.
|
|
16
|
+
|
|
17
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
18
|
+
other entities that control, are controlled by, or are under common
|
|
19
|
+
control with that entity. For the purposes of this definition,
|
|
20
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
21
|
+
direction or management of such entity, whether by contract or
|
|
22
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
23
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
24
|
+
|
|
25
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
26
|
+
exercising permissions granted by this License.
|
|
27
|
+
|
|
28
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
29
|
+
including but not limited to software source code, documentation
|
|
30
|
+
source, and configuration files.
|
|
31
|
+
|
|
32
|
+
"Object" form shall mean any form resulting from mechanical
|
|
33
|
+
transformation or translation of a Source form, including but
|
|
34
|
+
not limited to compiled object code, generated documentation,
|
|
35
|
+
and conversions to other media types.
|
|
36
|
+
|
|
37
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
38
|
+
Object form, made available under the License, as indicated by a
|
|
39
|
+
copyright notice that is included in or attached to the work
|
|
40
|
+
(an example is provided in the Appendix below).
|
|
41
|
+
|
|
42
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
43
|
+
form, that is based on (or derived from) the Work and for which the
|
|
44
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
45
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
46
|
+
of this License, Derivative Works shall not include works that remain
|
|
47
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
48
|
+
the Work and Derivative Works thereof.
|
|
49
|
+
|
|
50
|
+
"Contribution" shall mean any work of authorship, including
|
|
51
|
+
the original version of the Work and any modifications or additions
|
|
52
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
53
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
54
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
55
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
56
|
+
means any form of electronic, verbal, or written communication sent
|
|
57
|
+
to the Licensor or its representatives, including but not limited to
|
|
58
|
+
communication on electronic mailing lists, source code control systems,
|
|
59
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
60
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
61
|
+
excluding communication that is conspicuously marked or otherwise
|
|
62
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
63
|
+
|
|
64
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
65
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
66
|
+
subsequently incorporated within the Work.
|
|
67
|
+
|
|
68
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
69
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
70
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
71
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
72
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
73
|
+
Work and such Derivative Works in Source or Object form.
|
|
74
|
+
|
|
75
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
76
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
77
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
78
|
+
(except as stated in this section) patent license to make, have made,
|
|
79
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
80
|
+
where such license applies only to those patent claims licensable
|
|
81
|
+
by such Contributor that are necessarily infringed by their
|
|
82
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
83
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
84
|
+
institute patent litigation against any entity (including a
|
|
85
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
86
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
87
|
+
or contributory patent infringement, then any patent licenses
|
|
88
|
+
granted to You under this License for that Work shall terminate
|
|
89
|
+
as of the date such litigation is filed.
|
|
90
|
+
|
|
91
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
92
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
93
|
+
modifications, and in Source or Object form, provided that You
|
|
94
|
+
meet the following conditions:
|
|
95
|
+
|
|
96
|
+
(a) You must give any other recipients of the Work or
|
|
97
|
+
Derivative Works a copy of this License; and
|
|
98
|
+
|
|
99
|
+
(b) You must cause any modified files to carry prominent notices
|
|
100
|
+
stating that You changed the files; and
|
|
101
|
+
|
|
102
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
103
|
+
that You distribute, all copyright, patent, trademark, and
|
|
104
|
+
attribution notices from the Source form of the Work,
|
|
105
|
+
excluding those notices that do not pertain to any part of
|
|
106
|
+
the Derivative Works; and
|
|
107
|
+
|
|
108
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
109
|
+
distribution, then any Derivative Works that You distribute must
|
|
110
|
+
include a readable copy of the attribution notices contained
|
|
111
|
+
within such NOTICE file, excluding those notices that do not
|
|
112
|
+
pertain to any part of the Derivative Works, in at least one
|
|
113
|
+
of the following places: within a NOTICE text file distributed
|
|
114
|
+
as part of the Derivative Works; within the Source form or
|
|
115
|
+
documentation, if provided along with the Derivative Works; or,
|
|
116
|
+
within a display generated by the Derivative Works, if and
|
|
117
|
+
wherever such third-party notices normally appear. The contents
|
|
118
|
+
of the NOTICE file are for informational purposes only and
|
|
119
|
+
do not modify the License. You may add Your own attribution
|
|
120
|
+
notices within Derivative Works that You distribute, alongside
|
|
121
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
122
|
+
that such additional attribution notices cannot be construed
|
|
123
|
+
as modifying the License.
|
|
124
|
+
|
|
125
|
+
You may add Your own copyright statement to Your modifications and
|
|
126
|
+
may provide additional or different license terms and conditions
|
|
127
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
128
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
129
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
130
|
+
the conditions stated in this License.
|
|
131
|
+
|
|
132
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
133
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
134
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
135
|
+
this License, without any additional terms or conditions.
|
|
136
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
137
|
+
the terms of any separate license agreement you may have executed
|
|
138
|
+
with Licensor regarding such Contributions.
|
|
139
|
+
|
|
140
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
141
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
142
|
+
except as required for reasonable and customary use in describing the
|
|
143
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
144
|
+
|
|
145
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
146
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
147
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
148
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
149
|
+
implied, including, without limitation, any warranties or conditions
|
|
150
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
151
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
152
|
+
appropriateness of using or redistributing the Work and assume any
|
|
153
|
+
risks associated with Your exercise of permissions under this License.
|
|
154
|
+
|
|
155
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
156
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
157
|
+
unless required by applicable law (such as deliberate and grossly
|
|
158
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
159
|
+
liable to You for damages, including any direct, indirect, special,
|
|
160
|
+
incidental, or consequential damages of any character arising as a
|
|
161
|
+
result of this License or out of the use or inability to use the
|
|
162
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
163
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
164
|
+
other commercial damages or losses), even if such Contributor
|
|
165
|
+
has been advised of the possibility of such damages.
|
|
166
|
+
|
|
167
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
168
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
169
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
170
|
+
or other liability obligations and/or rights consistent with this
|
|
171
|
+
License. However, in accepting such obligations, You may act only
|
|
172
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
173
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
174
|
+
defend, and hold each Contributor harmless for any liability
|
|
175
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
176
|
+
of your accepting any such warranty or additional liability.
|
|
177
|
+
|
|
178
|
+
END OF TERMS AND CONDITIONS
|
|
179
|
+
|
|
180
|
+
Copyright 2023 Robert Turnbull
|
|
181
|
+
|
|
182
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
183
|
+
you may not use this file except in compliance with the License.
|
|
184
|
+
You may obtain a copy of the License at
|
|
185
|
+
|
|
186
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
187
|
+
|
|
188
|
+
Unless required by applicable law or agreed to in writing, software
|
|
189
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
190
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
191
|
+
See the License for the specific language governing permissions and
|
|
192
|
+
limitations under the License.
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: PolyTorch
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Embeddings and loss functions for different data types.
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Keywords: pytorch,deep learning,command-line interface
|
|
7
|
+
Author: Robert Turnbull
|
|
8
|
+
Author-email: robert.turnbull@unimelb.edu.au
|
|
9
|
+
Requires-Python: >=3.10,<4.0
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Dist: attrs (>=19)
|
|
20
|
+
Requires-Dist: hierarchicalsoftmax (>=1.4.1)
|
|
21
|
+
Requires-Dist: kaleido (==0.2.1)
|
|
22
|
+
Requires-Dist: plotly (>=5.15.0)
|
|
23
|
+
Requires-Dist: scikit-learn (>=1.0.2)
|
|
24
|
+
Requires-Dist: torch (>=1.10.2)
|
|
25
|
+
Project-URL: Documentation, https://rbturnbull.github.io/polytorch
|
|
26
|
+
Project-URL: Homepage, https://github.com/rbturnbull/polytorch/
|
|
27
|
+
Project-URL: Repository, https://github.com/rbturnbull/polytorch/
|
|
28
|
+
Description-Content-Type: text/x-rst
|
|
29
|
+
|
|
30
|
+
================================================================
|
|
31
|
+
polytorch
|
|
32
|
+
================================================================
|
|
33
|
+
|
|
34
|
+
.. start-badges
|
|
35
|
+
|
|
36
|
+
|pypi badge| |testing badge| |coverage badge| |docs badge| |black badge|
|
|
37
|
+
|
|
38
|
+
.. |pypi badge| image:: https://img.shields.io/pypi/v/polytorch?color=blue
|
|
39
|
+
:alt: PyPI - Version
|
|
40
|
+
:target: https://pypi.org/project/polytorch/
|
|
41
|
+
|
|
42
|
+
.. |testing badge| image:: https://github.com/rbturnbull/polytorch/actions/workflows/testing.yml/badge.svg
|
|
43
|
+
:target: https://github.com/rbturnbull/polytorch/actions
|
|
44
|
+
|
|
45
|
+
.. |docs badge| image:: https://github.com/rbturnbull/polytorch/actions/workflows/docs.yml/badge.svg
|
|
46
|
+
:target: https://rbturnbull.github.io/polytorch
|
|
47
|
+
|
|
48
|
+
.. |black badge| image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
|
49
|
+
:target: https://github.com/psf/black
|
|
50
|
+
|
|
51
|
+
.. |coverage badge| image:: https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/rbturnbull/f84ac74436887cd42d77fbe2246d1f57/raw/coverage-badge.json
|
|
52
|
+
:target: https://rbturnbull.github.io/polytorch/coverage/
|
|
53
|
+
|
|
54
|
+
.. end-badges
|
|
55
|
+
|
|
56
|
+
.. start-quickstart
|
|
57
|
+
|
|
58
|
+
Embeddings and loss functions for different data types.
|
|
59
|
+
|
|
60
|
+
Installation
|
|
61
|
+
==================================
|
|
62
|
+
|
|
63
|
+
Install using pip:
|
|
64
|
+
|
|
65
|
+
.. code-block:: bash
|
|
66
|
+
|
|
67
|
+
pip install polytorch
|
|
68
|
+
|
|
69
|
+
Or install the latest version from GitHub:
|
|
70
|
+
|
|
71
|
+
.. code-block:: bash
|
|
72
|
+
|
|
73
|
+
pip install git+https://github.com/rbturnbull/polytorch.git
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
Data Types
|
|
77
|
+
==================================
|
|
78
|
+
|
|
79
|
+
This package allow you to input and output different data types in PyTorch models.
|
|
80
|
+
|
|
81
|
+
Binary Data
|
|
82
|
+
----------------------------------------
|
|
83
|
+
|
|
84
|
+
.. code-block:: python
|
|
85
|
+
|
|
86
|
+
from polytorch import BinaryData
|
|
87
|
+
|
|
88
|
+
binary_data = BinaryData()
|
|
89
|
+
|
|
90
|
+
# Or with labels and colors
|
|
91
|
+
binary_data = BinaryData(labels=["no_feature", "with_feature", colors=["red", "blue"])
|
|
92
|
+
|
|
93
|
+
Categorical Data
|
|
94
|
+
----------------------------------------
|
|
95
|
+
|
|
96
|
+
.. code-block:: python
|
|
97
|
+
|
|
98
|
+
from polytorch import CategoricalData
|
|
99
|
+
|
|
100
|
+
category_count = 5 # Number of categories
|
|
101
|
+
categorical_data = CategoricalData(category_count)
|
|
102
|
+
|
|
103
|
+
# Or with labels, colors and label smoothing
|
|
104
|
+
categorical_data = CategoricalData(
|
|
105
|
+
category_count=category_count,
|
|
106
|
+
labels=["cat", "dog", "fish", "bird", "reptile"],
|
|
107
|
+
colors=["red", "blue", "green", "yellow", "purple"],
|
|
108
|
+
label_smoothing=0.1,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
Ordinal Data
|
|
112
|
+
----------------------------------------
|
|
113
|
+
|
|
114
|
+
.. code-block:: python
|
|
115
|
+
|
|
116
|
+
from polytorch import OrdinalData
|
|
117
|
+
|
|
118
|
+
ordinal_data = OrdinalData()
|
|
119
|
+
|
|
120
|
+
# Or with color
|
|
121
|
+
ordinal_data = OrdinalData(color="pink")
|
|
122
|
+
|
|
123
|
+
Continuous Data
|
|
124
|
+
----------------------------------------
|
|
125
|
+
|
|
126
|
+
.. code-block:: python
|
|
127
|
+
|
|
128
|
+
from polytorch import ContinuousData
|
|
129
|
+
|
|
130
|
+
continuous_data = ContinuousData()
|
|
131
|
+
|
|
132
|
+
# Or with color
|
|
133
|
+
continuous_data = ContinuousData(color="orange")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
Hierarchical Data
|
|
137
|
+
----------------------------------------
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
.. code-block:: python
|
|
141
|
+
|
|
142
|
+
from polytorch import HierarchicalData
|
|
143
|
+
from hiearchicalsoftmax import SoftmaxNode
|
|
144
|
+
|
|
145
|
+
root = SoftmaxNode("root")
|
|
146
|
+
child1 = SoftmaxNode("child1", parent=root)
|
|
147
|
+
child2 = SoftmaxNode("child2", parent=root)
|
|
148
|
+
tip1 = SoftmaxNode("tip1", parent=child1)
|
|
149
|
+
tip2 = SoftmaxNode("tip2", parent=child1)
|
|
150
|
+
tip3 = SoftmaxNode("tip3", parent=child2)
|
|
151
|
+
tip4 = SoftmaxNode("tip4", parent=child2)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
hierarchical_data = HierarchicalData(root)
|
|
155
|
+
|
|
156
|
+
Embedding your data
|
|
157
|
+
==================================
|
|
158
|
+
|
|
159
|
+
.. code-block:: python
|
|
160
|
+
|
|
161
|
+
from torch import nn
|
|
162
|
+
from polytorch import Embedding
|
|
163
|
+
|
|
164
|
+
class MyModule(nn.Module):
|
|
165
|
+
def __init__(self, embedding_size:int=128):
|
|
166
|
+
super(MyModule, self).__init__()
|
|
167
|
+
|
|
168
|
+
input_types = [binary_data, categorical_data] # for example. Could be other data types as well.
|
|
169
|
+
self.embedding = PolyEmbedding( input_types=input_types, embedding_size=embedding_size)
|
|
170
|
+
|
|
171
|
+
# Other modules
|
|
172
|
+
...
|
|
173
|
+
|
|
174
|
+
def forward(self, x_binary, x_categorical):
|
|
175
|
+
|
|
176
|
+
embedded = self.embedding( x_binary, x_categorical )
|
|
177
|
+
|
|
178
|
+
# Use the embedded features in your model
|
|
179
|
+
...
|
|
180
|
+
|
|
181
|
+
.. warning::
|
|
182
|
+
|
|
183
|
+
The ``HierarchicalData`` type is not yet supported by the ``PolyEmbedding`` class.
|
|
184
|
+
|
|
185
|
+
Outputting your data
|
|
186
|
+
==================================
|
|
187
|
+
|
|
188
|
+
You can also get your model to output to different data types.
|
|
189
|
+
|
|
190
|
+
.. code-block:: python
|
|
191
|
+
|
|
192
|
+
from torch import nn
|
|
193
|
+
from polytorch import PolyLazyLinear
|
|
194
|
+
|
|
195
|
+
output_types = [
|
|
196
|
+
CategoricalData(category_count=5, loss_weighting=0.5), # For example, a categorical output with 5 categories
|
|
197
|
+
BinaryData(loss_weighting=1.0), # A binary output
|
|
198
|
+
ContinuousData(loss_weighting=0.1) # A continuous output
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
class MyModule(nn.Module):
|
|
202
|
+
def __init__(self, output_types):
|
|
203
|
+
super(MyModule, self).__init__()
|
|
204
|
+
|
|
205
|
+
self.output = PolyLazyLinear(output_types=output_types)
|
|
206
|
+
|
|
207
|
+
def forward(self, x):
|
|
208
|
+
# Your model logic
|
|
209
|
+
...
|
|
210
|
+
|
|
211
|
+
# Output to different data types
|
|
212
|
+
return self.output(x)
|
|
213
|
+
|
|
214
|
+
Then add set this as the loss:
|
|
215
|
+
|
|
216
|
+
.. code-block:: python
|
|
217
|
+
|
|
218
|
+
from polytorch import PolyLoss
|
|
219
|
+
|
|
220
|
+
loss_module = PolyLoss(output_types=output_types)
|
|
221
|
+
|
|
222
|
+
# In your training loop
|
|
223
|
+
loss = loss_module(predictions, categorical_target, binary_target, continuous_target)
|
|
224
|
+
|
|
225
|
+
.. end-quickstart
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
Credits
|
|
229
|
+
==================================
|
|
230
|
+
|
|
231
|
+
.. start-credits
|
|
232
|
+
|
|
233
|
+
Robert Turnbull
|
|
234
|
+
For more information contact: <robert.turnbull@unimelb.edu.au>
|
|
235
|
+
|
|
236
|
+
Created using torchapp (https://github.com/rbturnbull/torchapp).
|
|
237
|
+
|
|
238
|
+
.. end-credits
|
|
239
|
+
|
|
240
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
polytorch/__init__.py,sha256=QMfk8tmvhhjAS6YCWpAFgBqIPImiMiBR7ohMAXo_76g,332
|
|
2
|
+
polytorch/data.py,sha256=Lfb08Ln6FRo9Zn9MtJK9bKQ1w-Ihx9cZBJv2GtnkJik,5351
|
|
3
|
+
polytorch/embedding.py,sha256=T5o3WFeGjVfjOWQA14ex43Mg6nnwJXWd6UptVBCmSRA,3614
|
|
4
|
+
polytorch/enums.py,sha256=Xq0DbL6ek0SHhd_rjWGf_ZI_fJ28iJXf5HPa8yqGH98,370
|
|
5
|
+
polytorch/loss.py,sha256=0qrByugj4kkD9a_2X4bOL8VKwnXBTpzC3okh7JCauj0,1148
|
|
6
|
+
polytorch/metrics.py,sha256=qXVnU0PR_fjL8LexOpaaq7q_vY01wHRIMfi59zjJx5Q,8431
|
|
7
|
+
polytorch/modules.py,sha256=MKmB9R4Qy9ZJ8pOT3SLrxLHaOE8nUO6Srf2MjI2zYRI,1459
|
|
8
|
+
polytorch/plots.py,sha256=AQY8QJrf76UVdzDft44aMZvMSFXbov8YYdTwgJI32XU,4790
|
|
9
|
+
polytorch/util.py,sha256=AYElA3agqtuxTODIQoVhz32jpGMryzt-pXTBhGaBYjU,2408
|
|
10
|
+
polytorch-0.1.3.dist-info/LICENSE,sha256=bGHVFIy-mmbSifFidZ-YRE1Rq1xVlKv2KSsoQzJjcEM,10761
|
|
11
|
+
polytorch-0.1.3.dist-info/METADATA,sha256=pmQMso0g1cMfQhnvtP363ALnU1R-CXAeswf61CWNugM,6828
|
|
12
|
+
polytorch-0.1.3.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
|
13
|
+
polytorch-0.1.3.dist-info/RECORD,,
|