nystrom-ncut 0.2.2__py3-none-any.whl → 0.3.1__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.
- nystrom_ncut/common.py +18 -5
- nystrom_ncut/distance_utils.py +54 -32
- nystrom_ncut/nystrom/__init__.py +0 -3
- nystrom_ncut/nystrom/distance_realization.py +8 -9
- nystrom_ncut/nystrom/normalized_cut.py +51 -47
- nystrom_ncut/nystrom/nystrom_utils.py +78 -70
- nystrom_ncut/sampling_utils.py +64 -51
- nystrom_ncut/transformer/axis_align.py +58 -47
- nystrom_ncut/transformer/transformer_mixin.py +0 -2
- nystrom_ncut/visualize_utils.py +31 -43
- {nystrom_ncut-0.2.2.dist-info → nystrom_ncut-0.3.1.dist-info}/METADATA +1 -1
- nystrom_ncut-0.3.1.dist-info/RECORD +18 -0
- {nystrom_ncut-0.2.2.dist-info → nystrom_ncut-0.3.1.dist-info}/WHEEL +1 -1
- nystrom_ncut-0.2.2.dist-info/RECORD +0 -18
- {nystrom_ncut-0.2.2.dist-info → nystrom_ncut-0.3.1.dist-info}/LICENSE +0 -0
- {nystrom_ncut-0.2.2.dist-info → nystrom_ncut-0.3.1.dist-info}/top_level.txt +0 -0
nystrom_ncut/common.py
CHANGED
@@ -12,8 +12,8 @@ def ceildiv(a: int, b: int) -> int:
|
|
12
12
|
def lazy_normalize(x: torch.Tensor, n: int = 1000, **normalize_kwargs: Any) -> torch.Tensor:
|
13
13
|
numel = np.prod(x.shape[:-1])
|
14
14
|
n = min(n, numel)
|
15
|
-
random_indices = torch.randperm(numel)[:n]
|
16
|
-
_x = x.
|
15
|
+
random_indices = torch.randperm(numel, device=x.device)[:n]
|
16
|
+
_x = x.view((-1, x.shape[-1]))[random_indices]
|
17
17
|
if torch.allclose(torch.norm(_x, **normalize_kwargs), torch.ones(n, device=x.device)):
|
18
18
|
return x
|
19
19
|
else:
|
@@ -21,13 +21,14 @@ def lazy_normalize(x: torch.Tensor, n: int = 1000, **normalize_kwargs: Any) -> t
|
|
21
21
|
|
22
22
|
|
23
23
|
def quantile_min_max(x: torch.Tensor, q1: float, q2: float, n_sample: int = 10000):
|
24
|
-
|
24
|
+
x = x.flatten()
|
25
|
+
if len(x) > n_sample:
|
25
26
|
np.random.seed(0)
|
26
|
-
random_idx = np.random.choice(x
|
27
|
+
random_idx = np.random.choice(len(x), n_sample, replace=False)
|
27
28
|
vmin, vmax = x[random_idx].quantile(q1), x[random_idx].quantile(q2)
|
28
29
|
else:
|
29
30
|
vmin, vmax = x.quantile(q1), x.quantile(q2)
|
30
|
-
return vmin, vmax
|
31
|
+
return vmin.item(), vmax.item()
|
31
32
|
|
32
33
|
|
33
34
|
def quantile_normalize(x: torch.Tensor, q: float = 0.95):
|
@@ -57,5 +58,17 @@ def quantile_normalize(x: torch.Tensor, q: float = 0.95):
|
|
57
58
|
return x
|
58
59
|
|
59
60
|
|
61
|
+
class default_device:
|
62
|
+
def __init__(self, device: torch.device):
|
63
|
+
self._device = device
|
64
|
+
|
65
|
+
def __enter__(self):
|
66
|
+
self._original_device = torch.get_default_device()
|
67
|
+
torch.set_default_device(self._device)
|
68
|
+
|
69
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
70
|
+
torch.set_default_device(self._original_device)
|
71
|
+
|
72
|
+
|
60
73
|
def profile(name: str, t: torch.Tensor) -> None:
|
61
74
|
print(f"{name} --- nan: {t.isnan().any()}, inf: {t.isinf().any()}, max: {t.abs().max()}, min: {t.abs().min()}")
|
nystrom_ncut/distance_utils.py
CHANGED
@@ -1,61 +1,71 @@
|
|
1
|
-
|
1
|
+
import collections
|
2
|
+
from typing import List, Literal, OrderedDict
|
2
3
|
|
3
4
|
import torch
|
4
5
|
|
5
6
|
from .common import lazy_normalize
|
6
7
|
|
7
8
|
|
8
|
-
DistanceOptions = Literal["cosine", "euclidean"
|
9
|
+
DistanceOptions = Literal["cosine", "euclidean"]
|
10
|
+
AffinityOptions = Literal["cosine", "rbf", "laplacian"]
|
9
11
|
|
12
|
+
# noinspection PyTypeChecker
|
13
|
+
DISTANCE_TO_AFFINITY: OrderedDict[DistanceOptions, List[AffinityOptions]] = collections.OrderedDict([
|
14
|
+
("cosine", ["cosine"]),
|
15
|
+
("euclidean", ["rbf", "laplacian"]),
|
16
|
+
])
|
17
|
+
# noinspection PyTypeChecker
|
18
|
+
AFFINITY_TO_DISTANCE: OrderedDict[AffinityOptions, DistanceOptions] = collections.OrderedDict(sum([
|
19
|
+
[(affinity_type, distance_type) for affinity_type in affinity_types]
|
20
|
+
for distance_type, affinity_types in DISTANCE_TO_AFFINITY.items()
|
21
|
+
], start=[]))
|
10
22
|
|
11
|
-
|
12
|
-
|
23
|
+
|
24
|
+
|
25
|
+
def to_euclidean(x: torch.Tensor, distance_type: DistanceOptions) -> torch.Tensor:
|
26
|
+
if distance_type == "cosine":
|
13
27
|
return lazy_normalize(x, p=2, dim=-1)
|
14
|
-
elif
|
28
|
+
elif distance_type == "euclidean":
|
15
29
|
return x
|
16
30
|
else:
|
17
|
-
raise ValueError(f"to_euclidean not implemented for
|
31
|
+
raise ValueError(f"to_euclidean not implemented for distance_type {distance_type}.")
|
18
32
|
|
19
33
|
|
20
34
|
def distance_from_features(
|
21
35
|
features: torch.Tensor,
|
22
36
|
features_B: torch.Tensor,
|
23
|
-
|
37
|
+
distance_type: DistanceOptions,
|
24
38
|
):
|
25
|
-
"""Compute
|
39
|
+
"""Compute distance matrix from input features.
|
26
40
|
Args:
|
27
41
|
features (torch.Tensor): input features, shape (n_samples, n_features)
|
28
42
|
features_B (torch.Tensor, optional): optional, if not None, compute affinity between two features
|
29
|
-
|
43
|
+
distance_type (str): distance metric, 'cosine' (default) or 'euclidean', 'rbf'.
|
30
44
|
Returns:
|
31
45
|
(torch.Tensor): affinity matrix, shape (n_samples, n_samples)
|
32
46
|
"""
|
33
47
|
# compute distance matrix from input features
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
D = D / (torch.linalg.norm(stds) ** 2)
|
49
|
-
else:
|
50
|
-
raise ValueError("distance should be 'cosine' or 'euclidean', 'rbf'")
|
51
|
-
return D
|
48
|
+
shape: torch.Size = features.shape[:-2]
|
49
|
+
features = features.view((-1, *features.shape[-2:]))
|
50
|
+
features_B = features_B.view((-1, *features_B.shape[-2:]))
|
51
|
+
|
52
|
+
match distance_type:
|
53
|
+
case "cosine":
|
54
|
+
features = lazy_normalize(features, dim=-1)
|
55
|
+
features_B = lazy_normalize(features_B, dim=-1)
|
56
|
+
D = 1 - features @ features_B.mT
|
57
|
+
case "euclidean":
|
58
|
+
D = torch.cdist(features, features_B, p=2)
|
59
|
+
case _:
|
60
|
+
raise ValueError("Distance should be 'cosine' or 'euclidean'")
|
61
|
+
return D.view((*shape, *D.shape[-2:]))
|
52
62
|
|
53
63
|
|
54
64
|
def affinity_from_features(
|
55
65
|
features: torch.Tensor,
|
56
66
|
features_B: torch.Tensor = None,
|
57
67
|
affinity_focal_gamma: float = 1.0,
|
58
|
-
|
68
|
+
affinity_type: AffinityOptions = "cosine",
|
59
69
|
):
|
60
70
|
"""Compute affinity matrix from input features.
|
61
71
|
|
@@ -64,7 +74,7 @@ def affinity_from_features(
|
|
64
74
|
features_B (torch.Tensor, optional): optional, if not None, compute affinity between two features
|
65
75
|
affinity_focal_gamma (float): affinity matrix parameter, lower t reduce the edge weights
|
66
76
|
on weak connections, default 1.0
|
67
|
-
|
77
|
+
affinity_type (str): distance metric, 'cosine' (default) or 'euclidean'.
|
68
78
|
Returns:
|
69
79
|
(torch.Tensor): affinity matrix, shape (n_samples, n_samples)
|
70
80
|
"""
|
@@ -75,9 +85,21 @@ def affinity_from_features(
|
|
75
85
|
features_B = features if features_B is None else features_B
|
76
86
|
|
77
87
|
# compute distance matrix from input features
|
78
|
-
D = distance_from_features(features, features_B,
|
88
|
+
D = distance_from_features(features, features_B, AFFINITY_TO_DISTANCE[affinity_type])
|
79
89
|
|
80
|
-
# torch.exp make affinity matrix positive definite,
|
81
90
|
# lower affinity_focal_gamma reduce the weak edge weights
|
82
|
-
|
91
|
+
match affinity_type:
|
92
|
+
case "cosine" | "laplacian":
|
93
|
+
A = torch.exp(-D / affinity_focal_gamma) # [... x n x n]
|
94
|
+
case "rbf":
|
95
|
+
# Outlier-robust scale invariance using quantiles to estimate standard deviation
|
96
|
+
c = 2.0
|
97
|
+
p = torch.erf(torch.tensor((-c, c), device=features.device) * (2 ** -0.5))
|
98
|
+
stds = torch.nanquantile(features, q=(p + 1) / 2, dim=-2) # [2 x ... x d]
|
99
|
+
stds = (stds[1] - stds[0]) / (2 * c) # [... x d]
|
100
|
+
D = 0.5 * (D / torch.norm(stds, dim=-1)[..., None, None]) ** 2
|
101
|
+
A = torch.exp(-D / affinity_focal_gamma)
|
102
|
+
case _:
|
103
|
+
raise ValueError("Affinity should be 'cosine', 'rbf', or 'laplacian'")
|
104
|
+
|
83
105
|
return A
|
nystrom_ncut/nystrom/__init__.py
CHANGED
@@ -18,10 +18,10 @@ from ..sampling_utils import (
|
|
18
18
|
class GramKernel(OnlineKernel):
|
19
19
|
def __init__(
|
20
20
|
self,
|
21
|
-
|
21
|
+
distance_type: DistanceOptions,
|
22
22
|
eig_solver: EigSolverOptions,
|
23
23
|
):
|
24
|
-
self.
|
24
|
+
self.distance_type: DistanceOptions = distance_type
|
25
25
|
self.eig_solver: EigSolverOptions = eig_solver
|
26
26
|
|
27
27
|
# Anchor matrices
|
@@ -40,7 +40,7 @@ class GramKernel(OnlineKernel):
|
|
40
40
|
self.A = -0.5 * distance_from_features(
|
41
41
|
self.anchor_features, # [n x d]
|
42
42
|
self.anchor_features,
|
43
|
-
|
43
|
+
distance_type=self.distance_type,
|
44
44
|
) # [n x n]
|
45
45
|
d = features.shape[-1]
|
46
46
|
U, L = solve_eig(
|
@@ -58,7 +58,7 @@ class GramKernel(OnlineKernel):
|
|
58
58
|
B = -0.5 * distance_from_features(
|
59
59
|
self.anchor_features, # [n x d]
|
60
60
|
features, # [m x d]
|
61
|
-
|
61
|
+
distance_type=self.distance_type,
|
62
62
|
) # [n x m]
|
63
63
|
b_r = torch.sum(B, dim=-1) # [n]
|
64
64
|
b_c = torch.sum(B, dim=-2) # [m]
|
@@ -84,7 +84,7 @@ class GramKernel(OnlineKernel):
|
|
84
84
|
B = -0.5 * distance_from_features(
|
85
85
|
self.anchor_features,
|
86
86
|
features,
|
87
|
-
|
87
|
+
distance_type=self.distance_type,
|
88
88
|
)
|
89
89
|
b_c = torch.sum(B, dim=-2) # [m]
|
90
90
|
col_sum = b_c + B.mT @ self.Ainv @ self.b_r # [m]
|
@@ -98,7 +98,7 @@ class DistanceRealization(OnlineNystromSubsampleFit):
|
|
98
98
|
def __init__(
|
99
99
|
self,
|
100
100
|
n_components: int = 100,
|
101
|
-
|
101
|
+
distance_type: DistanceOptions = "cosine",
|
102
102
|
sample_config: SampleConfig = SampleConfig(),
|
103
103
|
eig_solver: EigSolverOptions = "svd_lowrank",
|
104
104
|
chunk_size: int = 8192,
|
@@ -115,13 +115,12 @@ class DistanceRealization(OnlineNystromSubsampleFit):
|
|
115
115
|
OnlineNystromSubsampleFit.__init__(
|
116
116
|
self,
|
117
117
|
n_components=n_components,
|
118
|
-
kernel=GramKernel(
|
119
|
-
|
118
|
+
kernel=GramKernel(distance_type, eig_solver),
|
119
|
+
distance_type=distance_type,
|
120
120
|
sample_config=sample_config,
|
121
121
|
eig_solver=eig_solver,
|
122
122
|
chunk_size=chunk_size,
|
123
123
|
)
|
124
|
-
self.distance: DistanceOptions = distance
|
125
124
|
|
126
125
|
def fit_transform(
|
127
126
|
self,
|
@@ -8,7 +8,8 @@ from .nystrom_utils import (
|
|
8
8
|
solve_eig,
|
9
9
|
)
|
10
10
|
from ..distance_utils import (
|
11
|
-
|
11
|
+
AffinityOptions,
|
12
|
+
AFFINITY_TO_DISTANCE,
|
12
13
|
affinity_from_features,
|
13
14
|
)
|
14
15
|
from ..sampling_utils import (
|
@@ -20,80 +21,83 @@ class LaplacianKernel(OnlineKernel):
|
|
20
21
|
def __init__(
|
21
22
|
self,
|
22
23
|
affinity_focal_gamma: float,
|
23
|
-
|
24
|
+
affinity_type: AffinityOptions,
|
24
25
|
adaptive_scaling: bool,
|
25
26
|
eig_solver: EigSolverOptions,
|
26
27
|
):
|
27
28
|
self.affinity_focal_gamma = affinity_focal_gamma
|
28
|
-
self.
|
29
|
+
self.affinity_type: AffinityOptions = affinity_type
|
29
30
|
self.adaptive_scaling: bool = adaptive_scaling
|
30
31
|
self.eig_solver: EigSolverOptions = eig_solver
|
31
32
|
|
32
33
|
# Anchor matrices
|
33
|
-
self.anchor_features: torch.Tensor = None
|
34
|
-
self.
|
35
|
-
self.
|
34
|
+
self.anchor_features: torch.Tensor = None # [... x n x d]
|
35
|
+
self.anchor_mask: torch.Tensor = None
|
36
|
+
self.A: torch.Tensor = None # [... x n x n]
|
37
|
+
self.Ainv: torch.Tensor = None # [... x n x n]
|
36
38
|
|
37
39
|
# Updated matrices
|
38
|
-
self.a_r: torch.Tensor = None
|
39
|
-
self.b_r: torch.Tensor = None
|
40
|
+
self.a_r: torch.Tensor = None # [... x n]
|
41
|
+
self.b_r: torch.Tensor = None # [... x n]
|
40
42
|
|
41
43
|
def fit(self, features: torch.Tensor) -> None:
|
42
|
-
self.anchor_features = features
|
43
|
-
self.
|
44
|
-
|
44
|
+
self.anchor_features = features # [... x n x d]
|
45
|
+
self.anchor_mask = torch.all(torch.isnan(self.anchor_features), dim=-1) # [... x n]
|
46
|
+
|
47
|
+
self.A = torch.nan_to_num(affinity_from_features(
|
48
|
+
self.anchor_features, # [... x n x d]
|
45
49
|
affinity_focal_gamma=self.affinity_focal_gamma,
|
46
|
-
|
47
|
-
)
|
50
|
+
affinity_type=self.affinity_type,
|
51
|
+
), nan=0.0) # [... x n x n]
|
48
52
|
d = features.shape[-1]
|
49
53
|
U, L = solve_eig(
|
50
54
|
self.A,
|
51
55
|
num_eig=d + 1, # d * (d + 3) // 2 + 1,
|
52
56
|
eig_solver=self.eig_solver,
|
53
|
-
)
|
54
|
-
self.Ainv = U @ torch.
|
55
|
-
self.a_r = torch.sum(self.A, dim=-1)
|
56
|
-
self.b_r = torch.zeros_like(self.a_r)
|
57
|
+
) # [... x n x (d + 1)], [... x (d + 1)]
|
58
|
+
self.Ainv = U @ torch.diag_embed(1 / L) @ U.mT # [... x n x n]
|
59
|
+
self.a_r = torch.where(self.anchor_mask, torch.inf, torch.sum(self.A, dim=-1)) # [... x n]
|
60
|
+
self.b_r = torch.zeros_like(self.a_r) # [... x n]
|
57
61
|
|
58
62
|
def _affinity(self, features: torch.Tensor) -> torch.Tensor:
|
59
|
-
B = affinity_from_features(
|
60
|
-
self.anchor_features,
|
61
|
-
features,
|
63
|
+
B = torch.where(self.anchor_mask[..., None], 0.0, affinity_from_features(
|
64
|
+
self.anchor_features, # [... x n x d]
|
65
|
+
features, # [... x m x d]
|
62
66
|
affinity_focal_gamma=self.affinity_focal_gamma,
|
63
|
-
|
64
|
-
)
|
67
|
+
affinity_type=self.affinity_type,
|
68
|
+
)) # [... x n x m]
|
65
69
|
if self.adaptive_scaling:
|
66
70
|
diagonal = (
|
67
|
-
einops.rearrange(B, "n m -> m 1 n")
|
68
|
-
@ self.Ainv
|
69
|
-
@ einops.rearrange(B, "n m -> m n 1")
|
70
|
-
).squeeze(
|
71
|
-
adaptive_scale = diagonal ** -0.5
|
72
|
-
B = B * adaptive_scale
|
73
|
-
return B
|
71
|
+
einops.rearrange(B, "... n m -> ... m 1 n") # [... x m x 1 x n]
|
72
|
+
@ self.Ainv # [... x n x n]
|
73
|
+
@ einops.rearrange(B, "... n m -> ... m n 1") # [... x m x n x 1]
|
74
|
+
).squeeze(-2, -1) # [... x m]
|
75
|
+
adaptive_scale = diagonal ** -0.5 # [... x m]
|
76
|
+
B = B * adaptive_scale[..., None, :]
|
77
|
+
return B # [... x n x m]
|
74
78
|
|
75
79
|
def update(self, features: torch.Tensor) -> torch.Tensor:
|
76
|
-
B = self._affinity(features)
|
77
|
-
b_r = torch.sum(B, dim=-1)
|
78
|
-
b_c = torch.sum(B, dim=-2)
|
79
|
-
self.b_r = self.b_r + b_r
|
80
|
+
B = self._affinity(features) # [... x n x m]
|
81
|
+
b_r = torch.sum(torch.nan_to_num(B, nan=0.0), dim=-1) # [... x n]
|
82
|
+
b_c = torch.sum(B, dim=-2) # [... x m]
|
83
|
+
self.b_r = self.b_r + b_r # [... x n]
|
80
84
|
|
81
|
-
row_sum = self.a_r + self.b_r
|
82
|
-
col_sum = b_c + B.mT @ self.Ainv @ self.b_r
|
83
|
-
scale = (row_sum[:, None] * col_sum) ** -0.5
|
84
|
-
return (B * scale).mT
|
85
|
+
row_sum = self.a_r + self.b_r # [... x n]
|
86
|
+
col_sum = b_c + (B.mT @ (self.Ainv @ self.b_r[..., None]))[..., 0] # [... x m]
|
87
|
+
scale = (row_sum[..., :, None] * col_sum[..., None, :]) ** -0.5 # [... x n x m]
|
88
|
+
return (B * scale).mT # [... x m x n]
|
85
89
|
|
86
90
|
def transform(self, features: torch.Tensor = None) -> torch.Tensor:
|
87
|
-
row_sum = self.a_r + self.b_r
|
91
|
+
row_sum = self.a_r + self.b_r # [... x n]
|
88
92
|
if features is None:
|
89
|
-
B = self.A
|
90
|
-
col_sum = row_sum
|
93
|
+
B = self.A # [... x n x n]
|
94
|
+
col_sum = row_sum # [... x n]
|
91
95
|
else:
|
92
96
|
B = self._affinity(features)
|
93
|
-
b_c = torch.sum(B, dim=-2)
|
94
|
-
col_sum = b_c + B.mT @ self.Ainv @ self.b_r
|
95
|
-
scale = (row_sum[:, None] * col_sum) ** -0.5
|
96
|
-
return (B * scale).mT
|
97
|
+
b_c = torch.sum(B, dim=-2) # [... x m]
|
98
|
+
col_sum = b_c + (B.mT @ (self.Ainv @ self.b_r[..., None]))[..., 0] # [... x m]
|
99
|
+
scale = (row_sum[..., :, None] * col_sum[..., None, :]) ** -0.5 # [... x n x m]
|
100
|
+
return (B * scale).mT # [... x m x n]
|
97
101
|
|
98
102
|
|
99
103
|
class NCut(OnlineNystromSubsampleFit):
|
@@ -103,7 +107,7 @@ class NCut(OnlineNystromSubsampleFit):
|
|
103
107
|
self,
|
104
108
|
n_components: int = 100,
|
105
109
|
affinity_focal_gamma: float = 1.0,
|
106
|
-
|
110
|
+
affinity_type: AffinityOptions = "cosine",
|
107
111
|
adaptive_scaling: bool = False,
|
108
112
|
sample_config: SampleConfig = SampleConfig(),
|
109
113
|
eig_solver: EigSolverOptions = "svd_lowrank",
|
@@ -124,8 +128,8 @@ class NCut(OnlineNystromSubsampleFit):
|
|
124
128
|
OnlineNystromSubsampleFit.__init__(
|
125
129
|
self,
|
126
130
|
n_components=n_components,
|
127
|
-
kernel=LaplacianKernel(affinity_focal_gamma,
|
128
|
-
|
131
|
+
kernel=LaplacianKernel(affinity_focal_gamma, affinity_type, adaptive_scaling, eig_solver),
|
132
|
+
distance_type=AFFINITY_TO_DISTANCE[affinity_type],
|
129
133
|
sample_config=sample_config,
|
130
134
|
eig_solver=eig_solver,
|
131
135
|
chunk_size=chunk_size,
|