acteval 0.1.0__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.
- acteval/__init__.py +10 -0
- acteval/config.toml +24 -0
- acteval/creativity/__init__.py +2 -0
- acteval/creativity/features/__init__.py +0 -0
- acteval/creativity/features/creativity.py +88 -0
- acteval/density/__init__.py +2 -0
- acteval/density/describe/__init__.py +0 -0
- acteval/density/describe/frequency.py +101 -0
- acteval/density/describe/times.py +269 -0
- acteval/density/describe/transitions.py +93 -0
- acteval/density/features/__init__.py +0 -0
- acteval/density/features/_discretise.py +97 -0
- acteval/density/features/frequency.py +51 -0
- acteval/density/features/participation.py +192 -0
- acteval/density/features/pid_features.py +73 -0
- acteval/density/features/times.py +171 -0
- acteval/density/features/transitions.py +241 -0
- acteval/density/features/utils.py +197 -0
- acteval/distance/__init__.py +16 -0
- acteval/distance/scalar.py +68 -0
- acteval/distance/wasserstein.py +200 -0
- acteval/evaluate.py +811 -0
- acteval/filters.py +16 -0
- acteval/jobs.py +215 -0
- acteval/ops.py +49 -0
- acteval/population.py +165 -0
- acteval/structural/__init__.py +2 -0
- acteval/structural/features/__init__.py +0 -0
- acteval/structural/features/structural.py +210 -0
- acteval-0.1.0.dist-info/METADATA +12 -0
- acteval-0.1.0.dist-info/RECORD +32 -0
- acteval-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from ot import dist, emd2, emd2_1d
|
|
3
|
+
from scipy.stats import wasserstein_distance
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
import torch
|
|
7
|
+
|
|
8
|
+
_TORCH_AVAILABLE = True
|
|
9
|
+
except ImportError:
|
|
10
|
+
_TORCH_AVAILABLE = False
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def emd(a: tuple[np.array, np.array], b: tuple[np.array, np.array]) -> float:
|
|
14
|
+
if a[0].ndim == 1:
|
|
15
|
+
return emd1d(a, b)
|
|
16
|
+
elif a[0].ndim == 2:
|
|
17
|
+
return emd2d(a, b)
|
|
18
|
+
else:
|
|
19
|
+
raise ValueError("Only 1d and 2d features are supported")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def emd2d(a: tuple[np.array, np.array], b: tuple[np.array, np.array]) -> float:
|
|
23
|
+
ak, aw = a
|
|
24
|
+
bk, bw = b
|
|
25
|
+
aw = aw / aw.sum()
|
|
26
|
+
bw = bw / bw.sum()
|
|
27
|
+
d = dist(ak, bk, metric="cityblock")
|
|
28
|
+
return emd2(aw, bw, d, check_marginals=False)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def emd1d(a: tuple[np.array, np.array], b: tuple[np.array, np.array]) -> float:
|
|
32
|
+
ak, aw = a
|
|
33
|
+
bk, bw = b
|
|
34
|
+
if (
|
|
35
|
+
aw.sum() == 0
|
|
36
|
+
): # avoid division by zero but also has to assume distribution of just [0]
|
|
37
|
+
ak = np.array([0.0])
|
|
38
|
+
aw = np.array([1.0])
|
|
39
|
+
if bw.sum() == 0:
|
|
40
|
+
bk = np.array([0.0])
|
|
41
|
+
bw = np.array([1.0])
|
|
42
|
+
aw = aw / aw.sum()
|
|
43
|
+
bw = bw / bw.sum()
|
|
44
|
+
return emd2_1d(ak, bk, aw, bw, metric="cityblock")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def wasserstein(x: list[list], y: list[list]):
|
|
48
|
+
return wasserstein_distance(x, y)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def sliced_wasserstein(x: list[list], y: list[list], num_proj=100):
|
|
52
|
+
"""
|
|
53
|
+
https://stats.stackexchange.com/questions/404775/calculate-earth-movers-distance-for-two-grayscale-images
|
|
54
|
+
"""
|
|
55
|
+
x = np.array(x)
|
|
56
|
+
y = np.array(y)
|
|
57
|
+
dim = x.shape[1]
|
|
58
|
+
ests = []
|
|
59
|
+
for _ in range(num_proj):
|
|
60
|
+
# sample uniformly from the unit sphere
|
|
61
|
+
dir = np.random.randn(dim)
|
|
62
|
+
dir /= np.linalg.norm(dir)
|
|
63
|
+
|
|
64
|
+
# project the data
|
|
65
|
+
x_proj = x @ dir
|
|
66
|
+
y_proj = y @ dir
|
|
67
|
+
|
|
68
|
+
# compute 1d wasserstein
|
|
69
|
+
ests.append(wasserstein_distance(x_proj, y_proj))
|
|
70
|
+
return np.mean(ests)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def sinkhorn(x: list[list], y: list[list], eps=0.01, max_iter=10, reduction=None):
|
|
74
|
+
if not _TORCH_AVAILABLE:
|
|
75
|
+
raise ImportError("torch is required for sinkhorn")
|
|
76
|
+
x = torch.tensor(x, dtype=torch.float)
|
|
77
|
+
y = torch.tensor(y, dtype=torch.float)
|
|
78
|
+
model = SinkhornDistance(eps=eps, max_iter=max_iter, reduction=reduction)
|
|
79
|
+
return model(x, y)[0].item()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# Adapted from https://github.com/gpeyre/SinkhornAutoDiff
|
|
83
|
+
class SinkhornDistance:
|
|
84
|
+
r"""
|
|
85
|
+
https://dfdazac.github.io/sinkhorn.html
|
|
86
|
+
Given two empirical measures each with :math:`P_1` locations
|
|
87
|
+
:math:`x\in\mathbb{R}^{D_1}` and :math:`P_2` locations :math:`y\in\mathbb{R}^{D_2}`,
|
|
88
|
+
outputs an approximation of the regularized OT cost for point clouds.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
eps (float): regularization coefficient
|
|
92
|
+
max_iter (int): maximum number of Sinkhorn iterations
|
|
93
|
+
reduction (string, optional): Specifies the reduction to apply to the output:
|
|
94
|
+
'none' | 'mean' | 'sum'. 'none': no reduction will be applied,
|
|
95
|
+
'mean': the sum of the output will be divided by the number of
|
|
96
|
+
elements in the output, 'sum': the output will be summed. Default: 'none'
|
|
97
|
+
|
|
98
|
+
Shape:
|
|
99
|
+
- Input: :math:`(N, P_1, D_1)`, :math:`(N, P_2, D_2)`
|
|
100
|
+
- Output: :math:`(N)` or :math:`()`, depending on `reduction`
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(self, eps, max_iter, reduction="none"):
|
|
104
|
+
if not _TORCH_AVAILABLE:
|
|
105
|
+
raise ImportError("torch is required for SinkhornDistance")
|
|
106
|
+
super(SinkhornDistance, self).__init__()
|
|
107
|
+
self.eps = eps
|
|
108
|
+
self.max_iter = max_iter
|
|
109
|
+
self.reduction = reduction
|
|
110
|
+
|
|
111
|
+
def forward(self, x, y):
|
|
112
|
+
# The Sinkhorn algorithm takes as input three variables :
|
|
113
|
+
print("calc cost matrix")
|
|
114
|
+
C = self._cost_matrix(x, y) # Wasserstein cost function
|
|
115
|
+
x_points = x.shape[-2]
|
|
116
|
+
y_points = y.shape[-2]
|
|
117
|
+
if x.dim() == 2:
|
|
118
|
+
batch_size = 1
|
|
119
|
+
else:
|
|
120
|
+
batch_size = x.shape[0]
|
|
121
|
+
|
|
122
|
+
# both marginals are fixed with equal weights
|
|
123
|
+
print("mu")
|
|
124
|
+
mu = (
|
|
125
|
+
torch.empty(batch_size, x_points, dtype=torch.float, requires_grad=False)
|
|
126
|
+
.fill_(1.0 / x_points)
|
|
127
|
+
.squeeze()
|
|
128
|
+
)
|
|
129
|
+
print("nu")
|
|
130
|
+
nu = (
|
|
131
|
+
torch.empty(batch_size, y_points, dtype=torch.float, requires_grad=False)
|
|
132
|
+
.fill_(1.0 / y_points)
|
|
133
|
+
.squeeze()
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
u = torch.zeros_like(mu)
|
|
137
|
+
v = torch.zeros_like(nu)
|
|
138
|
+
# To check if algorithm terminates because of threshold
|
|
139
|
+
# or max iterations reached
|
|
140
|
+
actual_nits = 0
|
|
141
|
+
# Stopping criterion
|
|
142
|
+
thresh = 1e-1
|
|
143
|
+
|
|
144
|
+
# Sinkhorn iterations
|
|
145
|
+
for i in range(self.max_iter):
|
|
146
|
+
print(i, "start")
|
|
147
|
+
u1 = u # useful to check the update
|
|
148
|
+
u = (
|
|
149
|
+
self.eps
|
|
150
|
+
* (torch.log(mu + 1e-8) - torch.logsumexp(self.M(C, u, v), dim=-1))
|
|
151
|
+
+ u
|
|
152
|
+
)
|
|
153
|
+
v = (
|
|
154
|
+
self.eps
|
|
155
|
+
* (
|
|
156
|
+
torch.log(nu + 1e-8)
|
|
157
|
+
- torch.logsumexp(self.M(C, u, v).transpose(-2, -1), dim=-1)
|
|
158
|
+
)
|
|
159
|
+
+ v
|
|
160
|
+
)
|
|
161
|
+
err = (u - u1).abs().sum(-1).mean()
|
|
162
|
+
print(i, err.item())
|
|
163
|
+
|
|
164
|
+
actual_nits += 1
|
|
165
|
+
if err.item() < thresh:
|
|
166
|
+
print(f"Sinkhorn converged at iteration {i}")
|
|
167
|
+
break
|
|
168
|
+
|
|
169
|
+
U, V = u, v
|
|
170
|
+
# Transport plan pi = diag(a)*K*diag(b)
|
|
171
|
+
pi = torch.exp(self.M(C, U, V))
|
|
172
|
+
# Sinkhorn distance
|
|
173
|
+
cost = torch.sum(pi * C, dim=(-2, -1))
|
|
174
|
+
|
|
175
|
+
if self.reduction == "mean":
|
|
176
|
+
cost = cost.mean()
|
|
177
|
+
elif self.reduction == "sum":
|
|
178
|
+
cost = cost.sum()
|
|
179
|
+
|
|
180
|
+
return cost, pi, C
|
|
181
|
+
|
|
182
|
+
def __call__(self, x, y):
|
|
183
|
+
return self.forward(x, y)
|
|
184
|
+
|
|
185
|
+
def M(self, C, u, v):
|
|
186
|
+
"Modified cost for logarithmic updates"
|
|
187
|
+
return (-C + u.unsqueeze(-1) + v.unsqueeze(-2)) / self.eps
|
|
188
|
+
|
|
189
|
+
@staticmethod
|
|
190
|
+
def _cost_matrix(x, y, p=1):
|
|
191
|
+
"Returns the matrix of $|x_i-y_j|^p$."
|
|
192
|
+
x_col = x.unsqueeze(-2)
|
|
193
|
+
y_lin = y.unsqueeze(-3)
|
|
194
|
+
C = torch.sum((torch.abs(x_col - y_lin)) ** p, -1)
|
|
195
|
+
return C
|
|
196
|
+
|
|
197
|
+
@staticmethod
|
|
198
|
+
def ave(u, u1, tau):
|
|
199
|
+
"Barycenter subroutine, used by kinetic acceleration through extrapolation."
|
|
200
|
+
return tau * u + (1 - tau) * u1
|