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.
@@ -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