nystrom-ncut 0.3.2__py3-none-any.whl → 0.3.4__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/__init__.py CHANGED
@@ -1,5 +1,8 @@
1
+ from .kernel import (
2
+ KernelNCut,
3
+ )
1
4
  from .nystrom import (
2
- NCut,
5
+ NystromNCut,
3
6
  )
4
7
  from .transformer import (
5
8
  AxisAlign,
@@ -6,13 +6,25 @@ import torch
6
6
  from .common import lazy_normalize
7
7
 
8
8
 
9
- DistanceOptions = Literal["cosine", "euclidean"]
10
- AffinityOptions = Literal["cosine", "rbf", "laplacian"]
9
+ DistanceOptions = Literal[
10
+ "cosine",
11
+ "euclidean",
12
+ ]
13
+ AffinityOptions = Literal[
14
+ "cosine",
15
+ "rbf",
16
+ # "laplacian",
17
+ ]
11
18
 
12
19
  # noinspection PyTypeChecker
13
20
  DISTANCE_TO_AFFINITY: OrderedDict[DistanceOptions, List[AffinityOptions]] = collections.OrderedDict([
14
- ("cosine", ["cosine"]),
15
- ("euclidean", ["rbf", "laplacian"]),
21
+ ("cosine", [
22
+ "cosine",
23
+ ]),
24
+ ("euclidean", [
25
+ "rbf",
26
+ # "laplacian",
27
+ ]),
16
28
  ])
17
29
  # noinspection PyTypeChecker
18
30
  AFFINITY_TO_DISTANCE: OrderedDict[AffinityOptions, DistanceOptions] = collections.OrderedDict(sum([
@@ -32,45 +44,51 @@ def to_euclidean(x: torch.Tensor, distance_type: DistanceOptions) -> torch.Tenso
32
44
 
33
45
 
34
46
  def distance_from_features(
35
- features: torch.Tensor,
47
+ features_A: torch.Tensor,
36
48
  features_B: torch.Tensor,
37
49
  distance_type: DistanceOptions,
38
50
  ):
39
51
  """Compute distance matrix from input features.
40
52
  Args:
41
- features (torch.Tensor): input features, shape (n_samples, n_features)
53
+ features_A (torch.Tensor): input features, shape (n_samples, n_features)
42
54
  features_B (torch.Tensor, optional): optional, if not None, compute affinity between two features
43
55
  distance_type (str): distance metric, 'cosine' (default) or 'euclidean', 'rbf'.
44
56
  Returns:
45
57
  (torch.Tensor): affinity matrix, shape (n_samples, n_samples)
46
58
  """
47
59
  # compute distance matrix from input features
48
- shape: torch.Size = features.shape[:-2]
49
- features = features.view((-1, *features.shape[-2:]))
60
+ shape: torch.Size = features_A.shape[:-2]
61
+ features_A = features_A.view((-1, *features_A.shape[-2:]))
50
62
  features_B = features_B.view((-1, *features_B.shape[-2:]))
51
63
 
52
64
  match distance_type:
53
65
  case "cosine":
54
- features = lazy_normalize(features, dim=-1)
66
+ features_A = lazy_normalize(features_A, dim=-1)
55
67
  features_B = lazy_normalize(features_B, dim=-1)
56
- D = 1 - features @ features_B.mT
68
+ D = 1 - features_A @ features_B.mT
57
69
  case "euclidean":
58
- D = torch.cdist(features, features_B, p=2)
70
+ D = torch.cdist(features_A, features_B, p=2)
59
71
  case _:
60
72
  raise ValueError("Distance should be 'cosine' or 'euclidean'")
61
73
  return D.view((*shape, *D.shape[-2:]))
62
74
 
63
75
 
76
+ def get_normalization_factor(features: torch.Tensor, c: float = 2.0) -> torch.Tensor:
77
+ p = torch.erf(torch.tensor((-c, c), device=features.device) * (2 ** -0.5))
78
+ lo, hi = torch.nanquantile(features, q=(p + 1) / 2, dim=-2) # [... x d], [... x d]
79
+ return torch.norm(hi - lo, dim=-1) / (2 * c) # [...]
80
+
81
+
64
82
  def affinity_from_features(
65
- features: torch.Tensor,
66
- features_B: torch.Tensor = None,
67
- affinity_focal_gamma: float = 1.0,
68
- affinity_type: AffinityOptions = "cosine",
83
+ features_A: torch.Tensor,
84
+ features_B: torch.Tensor,
85
+ affinity_type: AffinityOptions,
86
+ affinity_focal_gamma: float,
69
87
  ):
70
88
  """Compute affinity matrix from input features.
71
89
 
72
90
  Args:
73
- features (torch.Tensor): input features, shape (n_samples, n_features)
91
+ features_A (torch.Tensor): input features, shape (n_samples, n_features)
74
92
  features_B (torch.Tensor, optional): optional, if not None, compute affinity between two features
75
93
  affinity_focal_gamma (float): affinity matrix parameter, lower t reduce the edge weights
76
94
  on weak connections, default 1.0
@@ -82,24 +100,17 @@ def affinity_from_features(
82
100
 
83
101
  # if feature_B is not provided, compute affinity matrix on features x features
84
102
  # if feature_B is provided, compute affinity matrix on features x feature_B
85
- features_B = features if features_B is None else features_B
86
-
87
- # compute distance matrix from input features
88
- D = distance_from_features(features, features_B, AFFINITY_TO_DISTANCE[affinity_type])
103
+ D = distance_from_features(features_A, features_B, AFFINITY_TO_DISTANCE[affinity_type])
89
104
 
90
105
  # lower affinity_focal_gamma reduce the weak edge weights
91
106
  match affinity_type:
92
- case "cosine" | "laplacian":
93
- A = torch.exp(-D / affinity_focal_gamma) # [... x n x n]
107
+ case "cosine":
108
+ pass
109
+ # case "laplacian":
110
+ # D = D / get_normalization_factor(features_A)[..., None, None]
94
111
  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)
112
+ D = 0.5 * (D / get_normalization_factor(features_A)[..., None, None]) ** 2
102
113
  case _:
103
114
  raise ValueError("Affinity should be 'cosine', 'rbf', or 'laplacian'")
104
-
115
+ A = torch.exp(-D / affinity_focal_gamma) # [... x n x n]
105
116
  return A
@@ -0,0 +1,2 @@
1
+
2
+ CHUNK_SIZE: int = 8192
@@ -0,0 +1,3 @@
1
+ from .kernel_ncut import (
2
+ KernelNCut,
3
+ )
@@ -0,0 +1,113 @@
1
+ import torch
2
+
3
+ from ..distance_utils import (
4
+ AffinityOptions,
5
+ AFFINITY_TO_DISTANCE,
6
+ get_normalization_factor,
7
+ )
8
+ from ..sampling_utils import (
9
+ SampleConfig,
10
+ OnlineTransformerSubsampleFit,
11
+ )
12
+ from ..transformer import (
13
+ OnlineTorchTransformerMixin,
14
+ )
15
+
16
+
17
+ class KernelNCutBaseTransformer(OnlineTorchTransformerMixin):
18
+ def __init__(
19
+ self,
20
+ n_components: int,
21
+ kernel_dim: int,
22
+ affinity_type: AffinityOptions,
23
+ affinity_focal_gamma: float,
24
+ ):
25
+ self.n_components: int = n_components
26
+ self.kernel_dim: int = kernel_dim
27
+ self.affinity_type: AffinityOptions = affinity_type
28
+ self.affinity_focal_gamma = affinity_focal_gamma
29
+
30
+ # Anchor matrices
31
+ self.W: torch.Tensor = None # [... x d x kernel_dim]
32
+ self.kernelized_anchor: torch.Tensor = None # [... x n x (2 * kernel_dim)]
33
+
34
+ # Updated matrices
35
+ self.r: torch.Tensor = None # [... x (2 * kernel_dim)]
36
+ self.transform_matrix: torch.Tensor = None # [... x (2 * kernel_dim) x n_components]
37
+ self.eigenvalues_: torch.Tensor = None # [... x n_components]
38
+
39
+ def _update(self) -> None:
40
+ row_sum = self.kernelized_anchor @ self.r[..., None] # [... x n x 1]
41
+ normalized_kernelized_anchor = self.kernelized_anchor / (row_sum ** 0.5) # [... x n x (2 * kernel_dim)]
42
+ _, S, V = torch.svd_lowrank(torch.nan_to_num(
43
+ normalized_kernelized_anchor, nan=0.0,
44
+ ), q=self.n_components) # [... x n_components], [... x (2 * kernel_dim) x n_components]
45
+ self.transform_matrix = V * torch.nan_to_num(1 / S, posinf=0.0, neginf=0.0)[..., None, :] # [... x (2 * kernel_dim) x n_components]
46
+ self.eigenvalues_ = S ** 2
47
+
48
+ def fit(self, features: torch.Tensor) -> "KernelNCutBaseTransformer":
49
+ d = features.shape[-1]
50
+ scale = get_normalization_factor(features) * (self.affinity_focal_gamma ** 0.5) # [...]
51
+ self.W = torch.randn((*features.shape[:-2], d, self.kernel_dim)) / scale[..., None, None] # [... x d x kernel_dim]
52
+
53
+ W_anchor = features @ self.W # [... x n x kernel_dim]
54
+ self.kernelized_anchor = torch.cat((
55
+ torch.cos(W_anchor),
56
+ torch.sin(W_anchor),
57
+ ), dim=-1) / (self.kernel_dim ** 0.5) # [... x n * (2 * kernel_dim)]
58
+ self.r = torch.sum(torch.nan_to_num(self.kernelized_anchor, nan=0.0), dim=-2) # [... x (2 * kernel_dim)]
59
+ self._update()
60
+ return self
61
+
62
+ def update(self, features: torch.Tensor) -> torch.Tensor:
63
+ W_features = features @ self.W # [... x m x kernel_dim]
64
+ kernelized_features = torch.cat((
65
+ torch.cos(W_features),
66
+ torch.sin(W_features),
67
+ ), dim=-1) / (self.kernel_dim ** 0.5) # [... x m x (2 * kernel_dim)]
68
+ b_r = torch.sum(torch.nan_to_num(kernelized_features, nan=0.0), dim=-2) # [... x (2 * kernel_dim)]
69
+ self.r = self.r + b_r
70
+ self._update()
71
+
72
+ row_sum = kernelized_features @ self.r[..., None] # [... x m x 1]
73
+ normalized_kernelized_features = kernelized_features / (row_sum ** 0.5) # [... x m x (2 * kernel_dim)]
74
+ return normalized_kernelized_features @ self.transform_matrix # [... x m x n_components]
75
+
76
+ def transform(self, features: torch.Tensor = None) -> torch.Tensor:
77
+ if features is None:
78
+ kernelized_features = self.kernelized_anchor # [... x n x (2 * kernel_dim)]
79
+ else:
80
+ W_features = features @ self.W
81
+ kernelized_features = torch.cat((
82
+ torch.cos(W_features),
83
+ torch.sin(W_features),
84
+ ), dim=-1) / (self.kernel_dim ** 0.5) # [... x m x (2 * kernel_dim)]
85
+ row_sum = kernelized_features @ self.r[..., None] # [... x m x 1]
86
+ normalized_kernelized_features = kernelized_features / (row_sum ** 0.5) # [... x m x (2 * kernel_dim)]
87
+ return normalized_kernelized_features @ self.transform_matrix # [... x m x n_components]
88
+
89
+
90
+ class KernelNCut(OnlineTransformerSubsampleFit):
91
+ """Kernelized Normalized Cut for large scale graph."""
92
+
93
+ def __init__(
94
+ self,
95
+ n_components: int,
96
+ kernel_dim: int = 1024,
97
+ affinity_type: AffinityOptions = "cosine",
98
+ affinity_focal_gamma: float = 1.0,
99
+ sample_config: SampleConfig = SampleConfig(),
100
+ ):
101
+ OnlineTransformerSubsampleFit.__init__(
102
+ self,
103
+ base_transformer=KernelNCutBaseTransformer(
104
+ n_components=n_components,
105
+ kernel_dim=kernel_dim,
106
+ affinity_type=affinity_type,
107
+ affinity_focal_gamma=affinity_focal_gamma,
108
+ ),
109
+ distance_type=AFFINITY_TO_DISTANCE[affinity_type],
110
+ sample_config=sample_config,
111
+ )
112
+
113
+
@@ -1,3 +1,3 @@
1
1
  from .normalized_cut import (
2
- NCut,
2
+ NystromNCut,
3
3
  )
@@ -101,7 +101,6 @@ class DistanceRealization(OnlineNystromSubsampleFit):
101
101
  distance_type: DistanceOptions = "cosine",
102
102
  sample_config: SampleConfig = SampleConfig(),
103
103
  eig_solver: EigSolverOptions = "svd_lowrank",
104
- chunk_size: int = 8192,
105
104
  ):
106
105
  """
107
106
  Args:
@@ -110,7 +109,6 @@ class DistanceRealization(OnlineNystromSubsampleFit):
110
109
  farthest point sampling is recommended for better Nystrom-approximation accuracy
111
110
  distance (str): distance metric for affinity matrix, ['cosine', 'euclidean', 'rbf'].
112
111
  eig_solver (str): eigen decompose solver, ['svd_lowrank', 'lobpcg', 'svd', 'eigh'].
113
- chunk_size (int): chunk size for large-scale matrix multiplication
114
112
  """
115
113
  OnlineNystromSubsampleFit.__init__(
116
114
  self,
@@ -119,7 +117,6 @@ class DistanceRealization(OnlineNystromSubsampleFit):
119
117
  distance_type=distance_type,
120
118
  sample_config=sample_config,
121
119
  eig_solver=eig_solver,
122
- chunk_size=chunk_size,
123
120
  )
124
121
 
125
122
  def fit_transform(
@@ -4,7 +4,7 @@ import torch
4
4
  from .nystrom_utils import (
5
5
  EigSolverOptions,
6
6
  OnlineKernel,
7
- OnlineNystromSubsampleFit,
7
+ OnlineNystrom,
8
8
  solve_eig,
9
9
  )
10
10
  from ..distance_utils import (
@@ -14,19 +14,20 @@ from ..distance_utils import (
14
14
  )
15
15
  from ..sampling_utils import (
16
16
  SampleConfig,
17
+ OnlineTransformerSubsampleFit,
17
18
  )
18
19
 
19
20
 
20
21
  class LaplacianKernel(OnlineKernel):
21
22
  def __init__(
22
23
  self,
23
- affinity_focal_gamma: float,
24
24
  affinity_type: AffinityOptions,
25
+ affinity_focal_gamma: float,
25
26
  adaptive_scaling: bool,
26
27
  eig_solver: EigSolverOptions,
27
28
  ):
28
- self.affinity_focal_gamma = affinity_focal_gamma
29
29
  self.affinity_type: AffinityOptions = affinity_type
30
+ self.affinity_focal_gamma = affinity_focal_gamma
30
31
  self.adaptive_scaling: bool = adaptive_scaling
31
32
  self.eig_solver: EigSolverOptions = eig_solver
32
33
 
@@ -44,27 +45,29 @@ class LaplacianKernel(OnlineKernel):
44
45
  self.anchor_features = features # [... x n x d]
45
46
  self.anchor_mask = torch.all(torch.isnan(self.anchor_features), dim=-1) # [... x n]
46
47
 
47
- self.A = torch.nan_to_num(affinity_from_features(
48
- self.anchor_features, # [... x n x d]
49
- affinity_focal_gamma=self.affinity_focal_gamma,
48
+
49
+ self.A = torch.where(self.anchor_mask[..., None], 0.0, affinity_from_features(
50
+ features_A=self.anchor_features, # [... x n x d]
51
+ features_B=self.anchor_features, # [... x n x d]
50
52
  affinity_type=self.affinity_type,
51
- ), nan=0.0) # [... x n x n]
53
+ affinity_focal_gamma=self.affinity_focal_gamma,
54
+ )) # [... x n x n]
52
55
  d = features.shape[-1]
53
56
  U, L = solve_eig(
54
- self.A,
57
+ torch.nan_to_num(self.A, nan=0.0),
55
58
  num_eig=d + 1, # d * (d + 3) // 2 + 1,
56
59
  eig_solver=self.eig_solver,
57
60
  ) # [... x n x (d + 1)], [... x (d + 1)]
58
61
  self.Ainv = U @ torch.nan_to_num(torch.diag_embed(1 / L), posinf=0.0, neginf=0.0) @ 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]
62
+ self.a_r = torch.where(self.anchor_mask, torch.inf, torch.sum(self.A.mT, dim=-1)) # [... x n]
60
63
  self.b_r = torch.zeros_like(self.a_r) # [... x n]
61
64
 
62
65
  def _affinity(self, features: torch.Tensor) -> torch.Tensor:
63
66
  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]
66
- affinity_focal_gamma=self.affinity_focal_gamma,
67
+ features_A=self.anchor_features, # [... x n x d]
68
+ features_B=features, # [... x m x d]
67
69
  affinity_type=self.affinity_type,
70
+ affinity_focal_gamma=self.affinity_focal_gamma,
68
71
  )) # [... x n x m]
69
72
  if self.adaptive_scaling:
70
73
  diagonal = (
@@ -93,44 +96,43 @@ class LaplacianKernel(OnlineKernel):
93
96
  B = self.A # [... x n x n]
94
97
  col_sum = row_sum # [... x n]
95
98
  else:
96
- B = self._affinity(features)
99
+ B = self._affinity(features) # [... x n x m]
97
100
  b_c = torch.sum(B, dim=-2) # [... x m]
98
101
  col_sum = b_c + (B.mT @ (self.Ainv @ self.b_r[..., None]))[..., 0] # [... x m]
99
102
  scale = (row_sum[..., :, None] * col_sum[..., None, :]) ** -0.5 # [... x n x m]
100
103
  return (B * scale).mT # [... x m x n]
101
104
 
102
105
 
103
- class NCut(OnlineNystromSubsampleFit):
106
+ class NystromNCut(OnlineTransformerSubsampleFit):
104
107
  """Nystrom Normalized Cut for large scale graph."""
105
108
 
106
109
  def __init__(
107
110
  self,
108
- n_components: int = 100,
109
- affinity_focal_gamma: float = 1.0,
111
+ n_components: int,
110
112
  affinity_type: AffinityOptions = "cosine",
113
+ affinity_focal_gamma: float = 1.0,
111
114
  adaptive_scaling: bool = False,
112
115
  sample_config: SampleConfig = SampleConfig(),
113
116
  eig_solver: EigSolverOptions = "svd_lowrank",
114
- chunk_size: int = 8192,
115
117
  ):
116
118
  """
117
119
  Args:
118
120
  n_components (int): number of top eigenvectors to return
121
+ affinity_type (str): distance metric for affinity matrix, ['cosine', 'euclidean', 'rbf'].
119
122
  affinity_focal_gamma (float): affinity matrix temperature, lower t reduce the not-so-connected edge weights,
120
123
  smaller t result in more sharp eigenvectors.
121
- distance (str): distance metric for affinity matrix, ['cosine', 'euclidean', 'rbf'].
122
124
  adaptive_scaling (bool): whether to scale off-diagonal affinity vectors so extended diagonal equals 1
123
125
  sample_config (str): subgraph sampling, ['farthest', 'random'].
124
126
  farthest point sampling is recommended for better Nystrom-approximation accuracy
125
127
  eig_solver (str): eigen decompose solver, ['svd_lowrank', 'lobpcg', 'svd', 'eigh'].
126
- chunk_size (int): chunk size for large-scale matrix multiplication
127
128
  """
128
- OnlineNystromSubsampleFit.__init__(
129
+ OnlineTransformerSubsampleFit.__init__(
129
130
  self,
130
- n_components=n_components,
131
- kernel=LaplacianKernel(affinity_focal_gamma, affinity_type, adaptive_scaling, eig_solver),
131
+ base_transformer=OnlineNystrom(
132
+ n_components=n_components,
133
+ kernel=LaplacianKernel(affinity_type, affinity_focal_gamma, adaptive_scaling, eig_solver),
134
+ eig_solver=eig_solver,
135
+ ),
132
136
  distance_type=AFFINITY_TO_DISTANCE[affinity_type],
133
137
  sample_config=sample_config,
134
- eig_solver=eig_solver,
135
- chunk_size=chunk_size,
136
138
  )
@@ -1,5 +1,3 @@
1
- import copy
2
- import logging
3
1
  from abc import abstractmethod
4
2
  from typing import Literal, Tuple
5
3
 
@@ -8,15 +6,11 @@ import torch
8
6
  from ..common import (
9
7
  ceildiv,
10
8
  )
11
- from ..distance_utils import (
12
- DistanceOptions,
13
- )
14
- from ..sampling_utils import (
15
- SampleConfig,
16
- subsample_features,
9
+ from ..global_settings import (
10
+ CHUNK_SIZE,
17
11
  )
18
12
  from ..transformer import (
19
- TorchTransformerMixin,
13
+ OnlineTorchTransformerMixin,
20
14
  )
21
15
 
22
16
 
@@ -37,13 +31,12 @@ class OnlineKernel:
37
31
  """"""
38
32
 
39
33
 
40
- class OnlineNystrom(TorchTransformerMixin):
34
+ class OnlineNystrom(OnlineTorchTransformerMixin):
41
35
  def __init__(
42
36
  self,
43
37
  n_components: int,
44
38
  kernel: OnlineKernel,
45
39
  eig_solver: EigSolverOptions,
46
- chunk_size: int = 8192,
47
40
  ):
48
41
  """
49
42
  Args:
@@ -56,8 +49,6 @@ class OnlineNystrom(TorchTransformerMixin):
56
49
  self.eig_solver: EigSolverOptions = eig_solver
57
50
  self.shape: torch.Size = None # ...
58
51
 
59
- self.chunk_size = chunk_size
60
-
61
52
  # Anchor matrices
62
53
  self.anchor_features: torch.Tensor = None # [... x n x d]
63
54
  self.A: torch.Tensor = None # [... x n x n]
@@ -68,12 +59,13 @@ class OnlineNystrom(TorchTransformerMixin):
68
59
  # Updated matrices
69
60
  self.S: torch.Tensor = None # [... x n x n]
70
61
  self.transform_matrix: torch.Tensor = None # [... x n x n_components]
71
- self.eigenvalues_: torch.Tensor = None # [... x n]
62
+ self.eigenvalues_: torch.Tensor = None # [... x n_components]
72
63
 
73
64
  def _update_to_kernel(self, d: int) -> Tuple[torch.Tensor, torch.Tensor]:
74
- self.A = self.S = self.kernel.transform()
65
+ self.A = self.kernel.transform()
66
+ self.S = torch.nan_to_num(self.A, nan=0.0)
75
67
  U, L = solve_eig(
76
- self.A,
68
+ self.S,
77
69
  num_eig=d + 1, # d * (d + 3) // 2 + 1,
78
70
  eig_solver=self.eig_solver,
79
71
  ) # [... x n x (? + 1)], [... x (? + 1)]
@@ -83,10 +75,6 @@ class OnlineNystrom(TorchTransformerMixin):
83
75
  return U, L
84
76
 
85
77
  def fit(self, features: torch.Tensor) -> "OnlineNystrom":
86
- OnlineNystrom.fit_transform(self, features)
87
- return self
88
-
89
- def fit_transform(self, features: torch.Tensor) -> torch.Tensor:
90
78
  self.anchor_features = features
91
79
 
92
80
  self.kernel.fit(self.anchor_features)
@@ -94,11 +82,11 @@ class OnlineNystrom(TorchTransformerMixin):
94
82
 
95
83
  self.transform_matrix = (U / L[..., None, :])[..., :, :self.n_components] # [... x n x n_components]
96
84
  self.eigenvalues_ = L[..., :self.n_components] # [... x n_components]
97
- return U[..., :, :self.n_components] # [... x n x n_components]
85
+ return self
98
86
 
99
87
  def update(self, features: torch.Tensor) -> torch.Tensor:
100
88
  d = features.shape[-1]
101
- n_chunks = ceildiv(features.shape[-2], self.chunk_size)
89
+ n_chunks = ceildiv(features.shape[-2], CHUNK_SIZE)
102
90
  if n_chunks > 1:
103
91
  """ Chunked version """
104
92
  chunks = torch.chunk(features, n_chunks, dim=-2)
@@ -134,119 +122,24 @@ class OnlineNystrom(TorchTransformerMixin):
134
122
 
135
123
  return B.mT @ self.transform_matrix # [... x m x n_components]
136
124
 
137
- def transform(self, features: torch.Tensor) -> torch.Tensor:
138
- n_chunks = ceildiv(features.shape[-2], self.chunk_size)
139
- if n_chunks > 1:
140
- """ Chunked version """
141
- chunks = torch.chunk(features, n_chunks, dim=-2)
142
- VS = []
143
- for chunk in chunks:
144
- VS.append(self.kernel.transform(chunk) @ self.transform_matrix) # [... x _m x n_components]
145
- VS = torch.cat(VS, dim=-2)
125
+ def transform(self, features: torch.Tensor = None) -> torch.Tensor:
126
+ if features is None:
127
+ VS = self.A @ self.transform_matrix # [... x n x n_components]
146
128
  else:
147
- """ Unchunked version """
148
- VS = self.kernel.transform(features) @ self.transform_matrix # [... x m x n_components]
129
+ n_chunks = ceildiv(features.shape[-2], CHUNK_SIZE)
130
+ if n_chunks > 1:
131
+ """ Chunked version """
132
+ chunks = torch.chunk(features, n_chunks, dim=-2)
133
+ VS = []
134
+ for chunk in chunks:
135
+ VS.append(self.kernel.transform(chunk) @ self.transform_matrix) # [... x _m x n_components]
136
+ VS = torch.cat(VS, dim=-2)
137
+ else:
138
+ """ Unchunked version """
139
+ VS = self.kernel.transform(features) @ self.transform_matrix # [... x m x n_components]
149
140
  return VS # [... x m x n_components]
150
141
 
151
142
 
152
- class OnlineNystromSubsampleFit(OnlineNystrom):
153
- def __init__(
154
- self,
155
- n_components: int,
156
- kernel: OnlineKernel,
157
- distance_type: DistanceOptions,
158
- sample_config: SampleConfig,
159
- eig_solver: EigSolverOptions = "svd_lowrank",
160
- chunk_size: int = 8192,
161
- ):
162
- OnlineNystrom.__init__(
163
- self,
164
- n_components=n_components,
165
- kernel=kernel,
166
- eig_solver=eig_solver,
167
- chunk_size=chunk_size,
168
- )
169
- self.distance_type: DistanceOptions = distance_type
170
- self.sample_config: SampleConfig = sample_config
171
- self.sample_config._ncut_obj = copy.deepcopy(self)
172
- self.anchor_indices: torch.Tensor = None
173
-
174
- def _fit_helper(
175
- self,
176
- features: torch.Tensor,
177
- precomputed_sampled_indices: torch.Tensor,
178
- ) -> Tuple[torch.Tensor, torch.Tensor]:
179
- _n = features.shape[-2]
180
- if self.sample_config.num_sample >= _n:
181
- logging.info(
182
- f"NCUT nystrom num_sample is larger than number of input samples, nyström approximation is not needed, setting num_sample={_n}"
183
- )
184
- self.num_sample = _n
185
-
186
- if precomputed_sampled_indices is not None:
187
- self.anchor_indices = precomputed_sampled_indices
188
- else:
189
- self.anchor_indices = subsample_features(
190
- features=features,
191
- distance_type=self.distance_type,
192
- config=self.sample_config,
193
- )
194
- sampled_features = torch.gather(features, -2, self.anchor_indices[..., None].expand([-1] * self.anchor_indices.ndim + [features.shape[-1]]))
195
- OnlineNystrom.fit(self, sampled_features)
196
-
197
- _n_not_sampled = _n - self.anchor_indices.shape[-1]
198
- if _n_not_sampled > 0:
199
- unsampled_mask = torch.full(features.shape[:-1], True, device=features.device).scatter_(-1, self.anchor_indices, False)
200
- unsampled_indices = torch.where(unsampled_mask)[-1].view((*features.shape[:-2], -1))
201
- unsampled_features = torch.gather(features, -2, unsampled_indices[..., None].expand([-1] * unsampled_indices.ndim + [features.shape[-1]]))
202
- V_unsampled = OnlineNystrom.update(self, unsampled_features)
203
- else:
204
- unsampled_indices = V_unsampled = None
205
- return unsampled_indices, V_unsampled
206
-
207
- def fit(
208
- self,
209
- features: torch.Tensor,
210
- precomputed_sampled_indices: torch.Tensor = None,
211
- ) -> "OnlineNystromSubsampleFit":
212
- """Fit Nystrom Normalized Cut on the input features.
213
- Args:
214
- features (torch.Tensor): input features, shape (n_samples, n_features)
215
- precomputed_sampled_indices (torch.Tensor): precomputed sampled indices, shape (num_sample,)
216
- override the sample_method, if not None
217
- Returns:
218
- (NCut): self
219
- """
220
- OnlineNystromSubsampleFit._fit_helper(self, features, precomputed_sampled_indices)
221
- return self
222
-
223
- def fit_transform(
224
- self,
225
- features: torch.Tensor,
226
- precomputed_sampled_indices: torch.Tensor = None,
227
- ) -> torch.Tensor:
228
- """
229
- Args:
230
- features (torch.Tensor): input features, shape (n_samples, n_features)
231
- precomputed_sampled_indices (torch.Tensor): precomputed sampled indices, shape (num_sample,)
232
- override the sample_method, if not None
233
-
234
- Returns:
235
- (torch.Tensor): eigen_vectors, shape (n_samples, num_eig)
236
- (torch.Tensor): eigen_values, sorted in descending order, shape (num_eig,)
237
- """
238
- unsampled_indices, V_unsampled = OnlineNystromSubsampleFit._fit_helper(self, features, precomputed_sampled_indices)
239
- V_sampled = OnlineNystrom.transform(self, self.anchor_features)
240
-
241
- if unsampled_indices is not None:
242
- V = torch.zeros((*features.shape[:-1], self.n_components), device=features.device)
243
- for (indices, _V) in [(self.anchor_indices, V_sampled), (unsampled_indices, V_unsampled)]:
244
- V.scatter_(-2, indices[..., None].expand([-1] * indices.ndim + [self.n_components]), _V)
245
- else:
246
- V = V_sampled
247
- return V
248
-
249
-
250
143
  def solve_eig(
251
144
  A: torch.Tensor,
252
145
  num_eig: int,
@@ -269,7 +162,7 @@ def solve_eig(
269
162
  bsz: int = A.shape[0]
270
163
 
271
164
  A = A + eig_value_buffer * torch.eye(A.shape[-1], device=A.device)
272
-
165
+ num_eig = min(A.shape[-1], num_eig)
273
166
  # compute eigenvectors
274
167
  if eig_solver == "svd_lowrank": # default
275
168
  # only top q eigenvectors, fastest
@@ -1,5 +1,6 @@
1
+ import copy
1
2
  from dataclasses import dataclass
2
- from typing import Literal
3
+ from typing import Any, Literal, Tuple
3
4
 
4
5
  import torch
5
6
  from pytorch3d.ops import sample_farthest_points
@@ -13,6 +14,7 @@ from .distance_utils import (
13
14
  )
14
15
  from .transformer import (
15
16
  TorchTransformerMixin,
17
+ OnlineTorchTransformerMixin,
16
18
  )
17
19
 
18
20
 
@@ -21,11 +23,11 @@ SampleOptions = Literal["full", "random", "fps", "fps_recursive"]
21
23
 
22
24
  @dataclass
23
25
  class SampleConfig:
24
- method: SampleOptions = "fps"
26
+ method: SampleOptions = "full"
25
27
  num_sample: int = 10000
26
28
  fps_dim: int = 12
27
29
  n_iter: int = None
28
- _ncut_obj: TorchTransformerMixin = None
30
+ _recursive_obj: TorchTransformerMixin = None
29
31
 
30
32
 
31
33
  @torch.no_grad()
@@ -56,7 +58,7 @@ def subsample_features(
56
58
  distance_type=distance_type,
57
59
  config=SampleConfig(method="fps", num_sample=config.num_sample, fps_dim=config.fps_dim)
58
60
  ) # int: [... x num_sample]
59
- nc = config._ncut_obj
61
+ nc = config._recursive_obj
60
62
  for _ in range(config.n_iter):
61
63
  fps_features, eigenvalues = nc.fit_transform(features, precomputed_sampled_indices=sampled_indices)
62
64
 
@@ -99,3 +101,95 @@ def fpsample(
99
101
  sample_indices = torch.gather(order, 1, sample_indices) # int: [(...) x num_sample]
100
102
 
101
103
  return sample_indices.view((*shape, *sample_indices.shape[-1:])) # int: [... x num_sample]
104
+
105
+
106
+ class OnlineTransformerSubsampleFit(TorchTransformerMixin, OnlineTorchTransformerMixin):
107
+ def __init__(
108
+ self,
109
+ base_transformer: OnlineTorchTransformerMixin,
110
+ distance_type: DistanceOptions,
111
+ sample_config: SampleConfig,
112
+ ):
113
+ OnlineTorchTransformerMixin.__init__(self)
114
+ self.base_transformer: OnlineTorchTransformerMixin = base_transformer
115
+ self.distance_type: DistanceOptions = distance_type
116
+ self.sample_config: SampleConfig = sample_config
117
+ self.sample_config._recursive_obj = copy.deepcopy(self)
118
+ self.anchor_indices: torch.Tensor = None
119
+
120
+ def _fit_helper(
121
+ self,
122
+ features: torch.Tensor,
123
+ precomputed_sampled_indices: torch.Tensor,
124
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
125
+ _n = features.shape[-2]
126
+ self.sample_config.num_sample = min(self.sample_config.num_sample, _n)
127
+
128
+ if precomputed_sampled_indices is not None:
129
+ self.anchor_indices = precomputed_sampled_indices
130
+ else:
131
+ self.anchor_indices = subsample_features(
132
+ features=features,
133
+ distance_type=self.distance_type,
134
+ config=self.sample_config,
135
+ )
136
+ sampled_features = torch.gather(features, -2, self.anchor_indices[..., None].expand([-1] * self.anchor_indices.ndim + [features.shape[-1]]))
137
+ self.base_transformer.fit(sampled_features)
138
+
139
+ _n_not_sampled = _n - self.anchor_indices.shape[-1]
140
+ if _n_not_sampled > 0:
141
+ unsampled_mask = torch.full(features.shape[:-1], True, device=features.device).scatter_(-1, self.anchor_indices, False)
142
+ unsampled_indices = torch.where(unsampled_mask)[-1].view((*features.shape[:-2], -1))
143
+ unsampled_features = torch.gather(features, -2, unsampled_indices[..., None].expand([-1] * unsampled_indices.ndim + [features.shape[-1]]))
144
+ V_unsampled = self.base_transformer.update(unsampled_features)
145
+ else:
146
+ unsampled_indices = V_unsampled = None
147
+ return unsampled_indices, V_unsampled
148
+
149
+ def fit(
150
+ self,
151
+ features: torch.Tensor,
152
+ precomputed_sampled_indices: torch.Tensor = None,
153
+ ) -> "OnlineTransformerSubsampleFit":
154
+ """Fit Nystrom Normalized Cut on the input features.
155
+ Args:
156
+ features (torch.Tensor): input features, shape (n_samples, n_features)
157
+ precomputed_sampled_indices (torch.Tensor): precomputed sampled indices, shape (num_sample,)
158
+ override the sample_method, if not None
159
+ Returns:
160
+ (NCut): self
161
+ """
162
+ self._fit_helper(features, precomputed_sampled_indices)
163
+ return self
164
+
165
+ def fit_transform(
166
+ self,
167
+ features: torch.Tensor,
168
+ precomputed_sampled_indices: torch.Tensor = None,
169
+ ) -> torch.Tensor:
170
+ """
171
+ Args:
172
+ features (torch.Tensor): input features, shape (n_samples, n_features)
173
+ precomputed_sampled_indices (torch.Tensor): precomputed sampled indices, shape (num_sample,)
174
+ override the sample_method, if not None
175
+
176
+ Returns:
177
+ (torch.Tensor): eigen_vectors, shape (n_samples, num_eig)
178
+ (torch.Tensor): eigen_values, sorted in descending order, shape (num_eig,)
179
+ """
180
+ unsampled_indices, V_unsampled = self._fit_helper(features, precomputed_sampled_indices)
181
+ V_sampled = self.base_transformer.transform()
182
+
183
+ if unsampled_indices is not None:
184
+ V = torch.zeros((*features.shape[:-1], V_sampled.shape[-1]), device=features.device)
185
+ for (indices, _V) in [(self.anchor_indices, V_sampled), (unsampled_indices, V_unsampled)]:
186
+ V.scatter_(-2, indices[..., None].expand([-1] * indices.ndim + [V_sampled.shape[-1]]), _V)
187
+ else:
188
+ V = V_sampled
189
+ return V
190
+
191
+ def update(self, features: torch.Tensor) -> torch.Tensor:
192
+ return self.base_transformer.update(features)
193
+
194
+ def transform(self, features: torch.Tensor = None, **transform_kwargs) -> torch.Tensor:
195
+ return self.base_transformer.transform(features)
@@ -1,5 +1,6 @@
1
1
  from .transformer_mixin import (
2
2
  TorchTransformerMixin,
3
+ OnlineTorchTransformerMixin,
3
4
  )
4
5
  from .axis_align import (
5
6
  AxisAlign,
@@ -36,15 +36,28 @@ class TorchTransformerMixin:
36
36
  >>> transformer.fit_transform(X)
37
37
  array([1, 1, 1])
38
38
  """
39
+ @abstractmethod
40
+ def fit(self, X: torch.Tensor, **fit_kwargs: Any) -> "OnlineTorchTransformerMixin":
41
+ """"""
42
+
43
+ @abstractmethod
44
+ def transform(self, X: torch.Tensor = None, **transform_kwargs: Any) -> torch.Tensor:
45
+ """"""
46
+
47
+ @abstractmethod
48
+ def fit_transform(self, X: torch.Tensor, **fit_transform_kwargs: Any) -> torch.Tensor:
49
+ """"""
50
+
39
51
 
52
+ class OnlineTorchTransformerMixin:
40
53
  @abstractmethod
41
- def fit(self, X: torch.Tensor, **fit_kwargs: Any) -> "TorchTransformerMixin":
54
+ def fit(self, X: torch.Tensor) -> "OnlineTorchTransformerMixin":
42
55
  """"""
43
56
 
44
57
  @abstractmethod
45
- def transform(self, X: torch.Tensor, **transform_kwargs: Any) -> torch.Tensor:
58
+ def transform(self, X: torch.Tensor = None) -> torch.Tensor:
46
59
  """"""
47
60
 
48
61
  @abstractmethod
49
- def fit_transform(self, X: torch.Tensor, **kwargs: Any) -> torch.Tensor:
62
+ def update(self, X: torch.Tensor) -> torch.Tensor:
50
63
  """"""
@@ -17,6 +17,9 @@ from .distance_utils import (
17
17
  to_euclidean,
18
18
  affinity_from_features,
19
19
  )
20
+ from .global_settings import (
21
+ CHUNK_SIZE,
22
+ )
20
23
  from .sampling_utils import (
21
24
  SampleConfig,
22
25
  subsample_features,
@@ -30,7 +33,6 @@ def extrapolate_knn(
30
33
  affinity_type: AffinityOptions,
31
34
  knn: int = 10, # k
32
35
  affinity_focal_gamma: float = 1.0,
33
- chunk_size: int = 8192,
34
36
  device: str = None,
35
37
  move_output_to_cpu: bool = False,
36
38
  ) -> torch.Tensor: # [m x d']
@@ -42,7 +44,6 @@ def extrapolate_knn(
42
44
  extrapolation_features (torch.Tensor): features from existing nodes, shape (new_num_samples, n_features)
43
45
  knn (int): number of KNN to propagate eige nvectors
44
46
  affinity_type (str): distance metric, 'cosine' (default) or 'euclidean', 'rbf'
45
- chunk_size (int): chunk size for matrix multiplication
46
47
  device (str): device to use for computation, if None, will not change device
47
48
  Returns:
48
49
  torch.Tensor: propagated eigenvectors, shape (new_num_samples, D)
@@ -61,12 +62,17 @@ def extrapolate_knn(
61
62
  # propagate eigen_vector from subgraph to full graph
62
63
  anchor_output = anchor_output.to(device)
63
64
 
64
- n_chunks = ceildiv(extrapolation_features.shape[0], chunk_size)
65
+ n_chunks = ceildiv(extrapolation_features.shape[0], CHUNK_SIZE)
65
66
  V_list = []
66
67
  for _v in torch.chunk(extrapolation_features, n_chunks, dim=0):
67
68
  _v = _v.to(device) # [_m x d]
68
69
 
69
- _A = affinity_from_features(anchor_features, _v, affinity_focal_gamma, affinity_type).mT # [_m x n]
70
+ _A = affinity_from_features(
71
+ features_A=anchor_features,
72
+ features_B=_v,
73
+ affinity_type=affinity_type,
74
+ affinity_focal_gamma=affinity_focal_gamma,
75
+ ).mT # [_m x n]
70
76
  if knn is not None:
71
77
  _A, indices = _A.topk(k=knn, dim=-1, largest=True) # [_m x k], [_m x k]
72
78
  _anchor_output = anchor_output[indices] # [_m x k x d]
@@ -93,7 +99,6 @@ def extrapolate_knn_with_subsampling(
93
99
  affinity_type: AffinityOptions,
94
100
  knn: int = 10, # k
95
101
  affinity_focal_gamma: float = 1.0,
96
- chunk_size: int = 8192,
97
102
  device: str = None,
98
103
  move_output_to_cpu: bool = False,
99
104
  ) -> torch.Tensor: # [m x d']
@@ -104,7 +109,6 @@ def extrapolate_knn_with_subsampling(
104
109
  extrapolation_features (torch.Tensor): features from new nodes, shape (n_new_samples, n_features)
105
110
  knn (int): number of KNN to propagate eigenvectors, default 3
106
111
  sample_config (str): sample method, 'farthest' (default) or 'random'
107
- chunk_size (int): chunk size for matrix multiplication, default 8192
108
112
  device (str): device to use for computation, if None, will not change device
109
113
  Returns:
110
114
  torch.Tensor: propagated eigenvectors, shape (n_new_samples, num_eig)
@@ -138,7 +142,6 @@ def extrapolate_knn_with_subsampling(
138
142
  affinity_type,
139
143
  knn=knn,
140
144
  affinity_focal_gamma=affinity_focal_gamma,
141
- chunk_size=chunk_size,
142
145
  device=device,
143
146
  move_output_to_cpu=move_output_to_cpu,
144
147
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: nystrom_ncut
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Normalized Cut and Nyström Approximation
5
5
  Author-email: Huzheng Yang <huze.yann@gmail.com>, Wentinn Liao <wentinn.liao@gmail.com>
6
6
  Project-URL: Documentation, https://github.com/JophiArcana/Nystrom-NCUT/
@@ -0,0 +1,21 @@
1
+ __init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ nystrom_ncut/__init__.py,sha256=4qNyWD5s1Uvd9OpfiMV4mF-3yFCi_K2QVRJIcAOXh70,587
3
+ nystrom_ncut/common.py,sha256=eie19AHTMk6AGTxNnYq1UcFkHJVimeywAUYryXwaiHk,2428
4
+ nystrom_ncut/distance_utils.py,sha256=zMI651RlIbd6ygvIxRp6jY5Ilfu7j9WQ5FD4I1wmmeo,4198
5
+ nystrom_ncut/global_settings.py,sha256=TckHuF8geWM2ofd99jBupHD3TQdeEB583_3pdIVRU34,24
6
+ nystrom_ncut/sampling_utils.py,sha256=lPWWNcDMBIvK9MmtNkVUeokeCOH9P1Fm8TZZ4sjlCpM,9037
7
+ nystrom_ncut/visualize_utils.py,sha256=A1qmL8eNZjtvOOlyl9KIeObnpPVUGZVsCB9QBRV7n9I,22762
8
+ nystrom_ncut/kernel/__init__.py,sha256=pvJ3tFukmlNZqw8VUB_iKPY9gbchUAlNkmnRCZcp0uU,44
9
+ nystrom_ncut/kernel/kernel_ncut.py,sha256=u2oIu-SmNGn9tg-SXBJY4G9WGJ0SeGSnyZcvX8DGAE0,5276
10
+ nystrom_ncut/nystrom/__init__.py,sha256=NHse-dW4nTo9wJUEJ6G4_Gw8uAi2vP6Hxm9aeBWxmqc,49
11
+ nystrom_ncut/nystrom/distance_realization.py,sha256=kvPS-jGUn85MRJx-Dh2IZJ3IwvavRDCbXq6wh_aEBxc,5684
12
+ nystrom_ncut/nystrom/normalized_cut.py,sha256=BD1F9Wz1BXbTGC-AVwT4IGmsPp334z-7jE9CuwhpNjY,7415
13
+ nystrom_ncut/nystrom/nystrom_utils.py,sha256=5cMoF8UFgi_N-nEzbSQqVGhuep_eOn-FzErCFMqT7VM,9784
14
+ nystrom_ncut/transformer/__init__.py,sha256=2FJEG9CXavfDDdDk1i9OOGkd8uSOHMkP8LBH49nnPnM,138
15
+ nystrom_ncut/transformer/axis_align.py,sha256=j3LlAPrp8O_jQAlwZz-gu3D7n_wICEJranye-YK5wvA,4880
16
+ nystrom_ncut/transformer/transformer_mixin.py,sha256=9wYdWknnJP7jijz70lRAyDn_kpC9aWwYN7Pbt5Mf6gQ,2012
17
+ nystrom_ncut-0.3.4.dist-info/LICENSE,sha256=2bm9uFabQZ3Ykb_SaSU_uUbAj2-htc6WJQmS_65qD00,1073
18
+ nystrom_ncut-0.3.4.dist-info/METADATA,sha256=I3V55oiDNJl4hw9v7TdEb1ibmJWtU5Ife9KwJ03rJPQ,6058
19
+ nystrom_ncut-0.3.4.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
20
+ nystrom_ncut-0.3.4.dist-info/top_level.txt,sha256=gM8IWWHYysIRTCvCTcdS4RShOyl9pxpylgSwPUZR2XM,22
21
+ nystrom_ncut-0.3.4.dist-info/RECORD,,
@@ -1,18 +0,0 @@
1
- __init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- nystrom_ncut/__init__.py,sha256=tKq9-2QRNFetckHY77qAaKEMjMCYTYcorS2f74aNtvk,540
3
- nystrom_ncut/common.py,sha256=eie19AHTMk6AGTxNnYq1UcFkHJVimeywAUYryXwaiHk,2428
4
- nystrom_ncut/distance_utils.py,sha256=pJA8NcIKyS7-YDpRGOkc7mwBQQEsYVemdkHiTjyU4n8,4300
5
- nystrom_ncut/sampling_utils.py,sha256=6lP8F6gftl4mgkavPsD7Vuk4erj4RtgILPhcj3YqLXk,4840
6
- nystrom_ncut/visualize_utils.py,sha256=Sfi_kKpvFFzBFoJnbo-pQpH2jhs-A6tH64SV_WGoq58,22740
7
- nystrom_ncut/nystrom/__init__.py,sha256=1aUXK87g4cXRXqNt6XkZsfyauw1-yv3sv0NmdmkWo-8,42
8
- nystrom_ncut/nystrom/distance_realization.py,sha256=RTI1_Q8fCUGAPSbXaVuNA-2B-11CEAfy2CwKWPJj6xQ,5830
9
- nystrom_ncut/nystrom/normalized_cut.py,sha256=cjkG8JeDmTPDK8KwfkAIqF9f1dI-D9s1muJ9WWZlUoc,7237
10
- nystrom_ncut/nystrom/nystrom_utils.py,sha256=hksDO8uuAb9xKoA1ZafGwXDlQN_gZJn_qHscaSoO8JE,14120
11
- nystrom_ncut/transformer/__init__.py,sha256=jjXjcNp3LrxeF6mqG9VY5k3asrqaY6bXzJz6wTpH78Q,105
12
- nystrom_ncut/transformer/axis_align.py,sha256=j3LlAPrp8O_jQAlwZz-gu3D7n_wICEJranye-YK5wvA,4880
13
- nystrom_ncut/transformer/transformer_mixin.py,sha256=YAjrDWTL5Hjnk9J2OsoxvtwT2N0u8IdgMSx0rRFmZzE,1653
14
- nystrom_ncut-0.3.2.dist-info/LICENSE,sha256=2bm9uFabQZ3Ykb_SaSU_uUbAj2-htc6WJQmS_65qD00,1073
15
- nystrom_ncut-0.3.2.dist-info/METADATA,sha256=4E42fHnXLNvbErWrwxE5K_oeOdpi5Bfabed9VF-YkV0,6058
16
- nystrom_ncut-0.3.2.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
17
- nystrom_ncut-0.3.2.dist-info/top_level.txt,sha256=gM8IWWHYysIRTCvCTcdS4RShOyl9pxpylgSwPUZR2XM,22
18
- nystrom_ncut-0.3.2.dist-info/RECORD,,