ncut-pytorch 3.0.0.dev0__tar.gz → 3.0.0.dev2__tar.gz

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.
Files changed (40) hide show
  1. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/PKG-INFO +1 -1
  2. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/color/coloring.py +0 -1
  3. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/color/mspace.py +1 -1
  4. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/ncut.py +19 -26
  5. ncut_pytorch-3.0.0.dev2/ncut_pytorch/ncuts/ncut_click.py +102 -0
  6. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/ncuts/ncut_nystrom.py +117 -106
  7. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/utils/grad.py +1 -40
  8. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/utils/math.py +18 -1
  9. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/utils/sigma.py +11 -17
  10. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch.egg-info/PKG-INFO +1 -1
  11. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/pyproject.toml +1 -1
  12. ncut_pytorch-3.0.0.dev0/ncut_pytorch/ncuts/ncut_click.py +0 -106
  13. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/LICENSE +0 -0
  14. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/README.md +0 -0
  15. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/__init__.py +0 -0
  16. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/color/__init__.py +0 -0
  17. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/color/mspace_nopl.py +0 -0
  18. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/ncuts/__init__.py +0 -0
  19. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/ncuts/ncut_kway.py +0 -0
  20. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/predictor/__init__.py +0 -0
  21. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/predictor/dino/__init__.py +0 -0
  22. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/predictor/dino/api.py +0 -0
  23. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/predictor/dino/dinov3.py +0 -0
  24. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/predictor/dino/hires_dino.py +0 -0
  25. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/predictor/dino/lowres_dino.py +0 -0
  26. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/predictor/dino/patch.py +0 -0
  27. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/predictor/dino/transform.py +0 -0
  28. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/predictor/dino_predictor.py +0 -0
  29. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/predictor/jafar_predictor.py +0 -0
  30. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/predictor/predictor.py +0 -0
  31. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/predictor/vision_predictor.py +0 -0
  32. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/utils/__init__.py +0 -0
  33. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/utils/device.py +0 -0
  34. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/utils/sample.py +0 -0
  35. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch/utils/torch_mod.py +0 -0
  36. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch.egg-info/SOURCES.txt +0 -0
  37. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch.egg-info/dependency_links.txt +0 -0
  38. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch.egg-info/requires.txt +0 -0
  39. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/ncut_pytorch.egg-info/top_level.txt +0 -0
  40. {ncut_pytorch-3.0.0.dev0 → ncut_pytorch-3.0.0.dev2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ncut_pytorch
3
- Version: 3.0.0.dev0
3
+ Version: 3.0.0.dev2
4
4
  Summary: Normalized Cut and Spectral Embedding
5
5
  Author-email: Huzheng Yang <huze.yann@gmail.com>
6
6
  License-Expression: MIT
@@ -218,7 +218,6 @@ def _nystrom_dimension_reduction(
218
218
  X[subgraph_indices],
219
219
  n_neighbors=knn,
220
220
  device=device,
221
- move_output_to_cpu=True,
222
221
  ))
223
222
  rgb = rgb_func(X_nd, q)
224
223
  return X_nd, rgb
@@ -56,7 +56,7 @@ def ncut_wrapper(features, n_eig, sigma=None):
56
56
 
57
57
  # features.requires_grad_(True)
58
58
  sigma = sigma or features.std(0).sum().item()
59
- # eigvec, eigval = ncut_fn(features, n_eig, sigma=sigma, track_grad=True)
59
+ eigvec, eigval = ncut_fn(features, n_eig, sigma=sigma)
60
60
  W = rbf_affinity(features, sigma=sigma)
61
61
  # W = cosine_affinity(features, sigma=1.0)
62
62
  A = normalize_affinity(W)
@@ -14,28 +14,30 @@ class Ncut:
14
14
  def __init__(
15
15
  self,
16
16
  n_eig: int = 100,
17
- track_grad: bool = False,
18
- d_sigma: float = None,
19
- sigma: float = None,
20
- repulsion_sigma: float = None,
21
- repulsion_weight: float = 0.2,
22
- extrapolation_factor: float = 1.0,
23
- device: str = None,
17
+ quantile_sigma: float = 0.25,
18
+ quantile_sigma_repulsion: float = 0.20,
19
+ sigma: float | None = None,
20
+ repulsion_sigma: float | None = None,
21
+ repulsion_weight: float | None = None,
24
22
  affinity_fn: Union["rbf_affinity", "cosine_affinity"] = rbf_affinity,
23
+ extrapolation_factor: float = 1.0,
24
+ exact_gradient: bool = False,
25
+ device: str | None = None,
25
26
  **kwargs,
26
27
  ):
27
28
  """
28
29
 
29
30
  Args:
30
31
  n_eig (int): number of eigenvectors
31
- track_grad (bool): keep track of pytorch gradients
32
- d_sigma (float): affinity sigma parameter, lower d_sigma results in a sharper eigenvectors
32
+ n_eig (int): number of eigenvectors
33
+ quantile_sigma (float): quantile of affinity sigma parameter, lower quantile_sigma results in sharper eigenvectors
34
+ quantile_sigma_repulsion (float): quantile of repulsion sigma parameter, lower quantile_sigma_repulsion results in sharper eigenvectors
33
35
  sigma (float): affinity parameter, override d_sigma if provided
34
36
  repulsion_sigma (float): (if use repulsion) repulsion sigma parameter, default None (no repulsion)
35
37
  repulsion_weight (float): (if use repulsion) repulsion weight, default 0.2
36
- extrapolation_factor (float): control how far can we extrapolate, larger extrapolation_factor means we can extrapolate further, default 1.0
37
- device (str): device, default 'auto' (auto detect GPU)
38
38
  affinity_fn (callable): affinity function, default rbf_affinity. Should accept (X1, X2=None, sigma=float) and return affinity matrix
39
+ extrapolation_factor (float): control how far can we extrapolate, larger extrapolation_factor means we can extrapolate further, default 1.0
40
+ exact_gradient (bool): use full spectrum and exact gradient, can be slower and unstable, default False device (str): device, default 'auto' (auto detect GPU)
39
41
 
40
42
  Examples:
41
43
  >>> from ncut_pytorch import Ncut
@@ -52,13 +54,14 @@ class Ncut:
52
54
  >>> print(new_eigvec.shape) # (500, 20)
53
55
  """
54
56
  self.n_eig = n_eig
55
- self.d_sigma = d_sigma
57
+ self.quantile_sigma = quantile_sigma
58
+ self.quantile_sigma_repulsion = quantile_sigma_repulsion
56
59
  self.sigma = sigma
57
60
  self.repulsion_sigma = repulsion_sigma
58
61
  self.repulsion_weight = repulsion_weight
59
62
  self.extrapolation_factor = extrapolation_factor
63
+ self.exact_gradient = exact_gradient
60
64
  self.device = device
61
- self.track_grad = track_grad
62
65
  self.affinity_fn = affinity_fn
63
66
  self.kwargs = kwargs
64
67
 
@@ -83,12 +86,13 @@ class Ncut:
83
86
  ncut_fn(
84
87
  X,
85
88
  n_eig=self.n_eig,
86
- d_sigma=self.d_sigma,
89
+ quantile_sigma=self.quantile_sigma,
90
+ quantile_sigma_repulsion=self.quantile_sigma_repulsion,
87
91
  sigma=self.sigma,
88
92
  repulsion_sigma=self.repulsion_sigma,
89
93
  repulsion_weight=self.repulsion_weight,
90
94
  device=self.device,
91
- track_grad=self.track_grad,
95
+ exact_gradient=self.exact_gradient,
92
96
  no_propagation=True,
93
97
  affinity_fn=self.affinity_fn,
94
98
  **self.kwargs
@@ -121,7 +125,6 @@ class Ncut:
121
125
  self._nystrom_x,
122
126
  extrapolation_factor=self.extrapolation_factor,
123
127
  device=self.device,
124
- track_grad=self.track_grad,
125
128
  **self.kwargs
126
129
  )
127
130
  return eigvec
@@ -137,15 +140,5 @@ class Ncut:
137
140
  """
138
141
  return self.fit(X).transform(X)
139
142
 
140
- def __new__(cls, X: torch.Tensor = None, n_eig: int = 100, track_grad: bool = False, d_sigma: float = None,
141
- device: str = None, affinity_fn: Callable[[torch.Tensor, torch.Tensor, float], torch.Tensor] = rbf_affinity,
142
- **kwargs) -> Union["Ncut", torch.Tensor]:
143
- if X is not None:
144
- # function-like behavior
145
- eigvec, eigval = ncut_fn(X, n_eig=n_eig, track_grad=track_grad, d_sigma=d_sigma, device=device, affinity_fn=affinity_fn, **kwargs)
146
- return eigvec
147
- # normal class instantiation
148
- return super().__new__(cls)
149
-
150
143
  def __call__(self, X: torch.Tensor) -> torch.Tensor:
151
144
  return self.fit_transform(X)
@@ -0,0 +1,102 @@
1
+ __all__ = ['ncut_click_prompt']
2
+
3
+ from typing import Callable, Union
4
+
5
+ import numpy as np
6
+ import torch
7
+
8
+ from ncut_pytorch.utils.sigma import find_sigma_by_degree
9
+ from ncut_pytorch.utils.math import rbf_affinity, cosine_affinity, normalize_affinity
10
+ from ncut_pytorch.utils.sample import farthest_point_sampling
11
+ from ncut_pytorch.utils.device import auto_device
12
+ from .ncut_nystrom import NystromConfig
13
+ from .ncut_nystrom import nystrom_propagate
14
+ from .ncut_nystrom import _plain_ncut
15
+
16
+
17
+ #TODO: automatically optimize click_weight based on the iou of fg and bg
18
+ def ncut_click_prompt(
19
+ X: torch.Tensor,
20
+ fg_indices: np.ndarray,
21
+ bg_indices: np.ndarray = None,
22
+ click_weight: float = 0.5,
23
+ bg_weight: float = 0.1,
24
+ n_eig: int = 2,
25
+ quantile_sigma: float = 0.25,
26
+ device: str = None,
27
+ sigma: float = None,
28
+ affinity_fn: Callable[[torch.Tensor, torch.Tensor, float], torch.Tensor] = rbf_affinity,
29
+ exact_gradient: bool = False,
30
+ no_propagation: bool = False,
31
+ return_indices_and_sigma: bool = False,
32
+ **kwargs,
33
+ ) -> Union[tuple[torch.Tensor, torch.Tensor], tuple[torch.Tensor, torch.Tensor, torch.Tensor, float]]:
34
+
35
+ config = NystromConfig()
36
+ config.update(kwargs)
37
+
38
+ # use GPU if available
39
+ device = auto_device(X.device, device)
40
+
41
+ if bg_indices is None:
42
+ bg_indices = np.array([], dtype=np.int64)
43
+
44
+ # subsample for nystrom approximation
45
+ nystrom_indices = farthest_point_sampling(X, n_sample=config.n_sample, device=device)
46
+ nystrom_indices = torch.tensor(nystrom_indices, dtype=torch.long)
47
+ # remove fg and bg from fps_idx
48
+ nystrom_indices = nystrom_indices[~np.isin(nystrom_indices, np.concatenate([fg_indices, bg_indices]))]
49
+ # add fg and bg to fps_idx
50
+ nystrom_indices = np.concatenate([fg_indices, bg_indices, nystrom_indices])
51
+ fg_indices = np.arange(len(fg_indices))
52
+ bg_indices = np.arange(len(bg_indices)) + len(fg_indices)
53
+ n_fgbg = len(fg_indices) + len(bg_indices)
54
+
55
+ nystrom_X = X[nystrom_indices].to(device)
56
+
57
+ # find optimal sigma for affinity matrix
58
+ if sigma is None and affinity_fn == rbf_affinity:
59
+ sigma = find_sigma_by_degree(nystrom_X, quantile_sigma, affinity_fn)
60
+ # TODO: change to std()
61
+ elif sigma is None and affinity_fn == cosine_affinity:
62
+ sigma = 0.5
63
+
64
+ # compute Ncut on the nystrom sampled subgraph
65
+ A = affinity_fn(nystrom_X, sigma=sigma)
66
+ A = normalize_affinity(A)
67
+
68
+ # modify the affinity from the clicks
69
+ X_click = 1 * A[fg_indices].mean(0)
70
+ if len(bg_indices) > 0:
71
+ X_click = X_click - bg_weight * A[bg_indices].mean(0)
72
+
73
+ X_click = X_click * A.shape[0]
74
+
75
+ A_click = affinity_fn(X_click.unsqueeze(1), sigma=0.5)
76
+ A_click = normalize_affinity(A_click)
77
+
78
+ _A = click_weight * A_click + (1 - click_weight) * A
79
+
80
+ nystrom_eigvec, eigval = _plain_ncut(_A, n_eig, exact_gradient=exact_gradient)
81
+
82
+ if no_propagation:
83
+ return nystrom_eigvec, eigval, nystrom_indices, sigma
84
+
85
+ # propagate eigenvectors from subgraph to full graph
86
+ eigvec, nystrom_indices2 = nystrom_propagate(
87
+ nystrom_eigvec,
88
+ X,
89
+ nystrom_X,
90
+ n_neighbors=config.n_neighbors,
91
+ n_sample=config.n_sample2,
92
+ matmul_chunk_size=config.matmul_chunk_size,
93
+ device=device,
94
+ return_indices=True,
95
+ )
96
+
97
+
98
+ if return_indices_and_sigma:
99
+ indices = nystrom_indices[nystrom_indices2]
100
+ return eigvec, eigval, indices, sigma
101
+
102
+ return eigvec, eigval
@@ -6,10 +6,9 @@ import torch
6
6
  import numpy as np
7
7
  from ncut_pytorch.utils.sigma import find_sigma_by_degree
8
8
  from ncut_pytorch.utils.math import rbf_affinity, cosine_affinity
9
- from ncut_pytorch.utils.math import gram_schmidt, normalize_affinity, grad_safe_eig_solve, correct_rotation, keep_topk_per_row
9
+ from ncut_pytorch.utils.math import gram_schmidt, normalize_affinity, grad_safe_eig_solve, correct_rotation, keep_topk_per_row, svd_lowrank
10
10
  from ncut_pytorch.utils.sample import farthest_point_sampling
11
11
  from ncut_pytorch.utils.device import auto_device
12
- from ncut_pytorch.utils.grad import grad_manager
13
12
 
14
13
 
15
14
  class NystromConfig:
@@ -23,7 +22,6 @@ class NystromConfig:
23
22
  n_neighbors = 32 # number of neighbors for eigenvector propagation, 10 is large enough for most cases
24
23
  n_neighbors_max_ratio = 1/32 # max ratio of n_neighbors to n_sample2, to avoid over smoothing
25
24
  matmul_chunk_size = 65536 # chunk size for matrix multiplication, larger chunk size is faster but requires more memory
26
- move_output_to_cpu = True # if True, will move output to cpu, saves VRAM
27
25
 
28
26
  def update(self, kwargs: dict):
29
27
  for key, value in kwargs.items():
@@ -36,15 +34,16 @@ class NystromConfig:
36
34
  def ncut_fn(
37
35
  X: torch.Tensor,
38
36
  n_eig: int = 100,
39
- track_grad: bool = False,
40
- d_sigma: float = None,
41
- device: str = None,
42
- sigma: float = None,
43
- repulsion_sigma: float = None,
44
- repulsion_weight: float = 0.2,
37
+ quantile_sigma: float = 0.25,
38
+ quantile_sigma_repulsion: float = 0.20,
39
+ sigma: float | None = None,
40
+ repulsion_sigma: float | None = None,
41
+ repulsion_weight: float | None = None,
42
+ affinity_fn: Union["rbf_affinity", "cosine_affinity"] = rbf_affinity,
45
43
  extrapolation_factor: float = 1.0,
44
+ exact_gradient: bool = False,
45
+ device: str | None = None,
46
46
  make_orthogonal: bool = False,
47
- affinity_fn: Union["rbf_affinity", "cosine_affinity"] = rbf_affinity,
48
47
  no_propagation: bool = False,
49
48
  **kwargs,
50
49
  ) -> Union[tuple[torch.Tensor, torch.Tensor], tuple[torch.Tensor, torch.Tensor, torch.Tensor, float]]:
@@ -53,15 +52,15 @@ def ncut_fn(
53
52
  Args:
54
53
  X (torch.Tensor): input features, shape (N, D)
55
54
  n_eig (int): number of eigenvectors
56
- track_grad (bool): keep track of pytorch gradients
57
- d_sigma (float): affinity sigma parameter, lower d_sigma results in sharper eigenvectors
58
- device (str): device, default 'auto' (auto detect GPU)
55
+ quantile_sigma (float): quantile of affinity sigma parameter, lower quantile_sigma results in sharper eigenvectors
56
+ quantile_sigma_repulsion (float): quantile of repulsion sigma parameter, lower quantile_sigma_repulsion results in sharper eigenvectors
59
57
  sigma (float): affinity parameter, override d_sigma if provided
60
58
  repulsion_sigma (float): (if use repulsion) repulsion sigma parameter, default None (no repulsion)
61
59
  repulsion_weight (float): (if use repulsion) repulsion weight, default 0.2
60
+ affinity_fn (callable): affinity function, default rbf_affinity. Should accept (X1, X2=None, sigma=float) and return affinity matrix
62
61
  extrapolation_factor (float): control how far can we extrapolate, larger extrapolation_factor means we can extrapolate further, default 1.0
62
+ exact_gradient (bool): use full spectrum and exact gradient, can be slower and unstable, default False
63
63
  make_orthogonal (bool): make eigenvectors orthogonal
64
- affinity_fn (callable): affinity function, default rbf_affinity. Should accept (X1, X2=None, sigma=float) and return affinity matrix
65
64
 
66
65
  Returns:
67
66
  eigenvectors (torch.Tensor): shape (N, n_eig)
@@ -76,60 +75,67 @@ def ncut_fn(
76
75
  """
77
76
  config = NystromConfig()
78
77
  config.update(kwargs)
79
-
80
- # use GPU if available
81
78
  device = auto_device(X.device, device)
82
79
 
83
- # check if enough data for nystrom approximation
80
+ # subsample for nystrom approximation
84
81
  is_enough_data = X.shape[0] > config.n_sample
82
+ n_sample = min(config.n_sample, int(X.shape[0]*config.n_sample_max_ratio))
83
+ nystrom_indices = farthest_point_sampling(X, n_sample=n_sample, device=device) if is_enough_data else np.arange(X.shape[0])
84
+ nystrom_X = X[nystrom_indices].to(device)
85
+
86
+ sigma, repulsion_sigma = find_optimal_sigma(nystrom_X, quantile_sigma, quantile_sigma_repulsion, sigma, repulsion_sigma, affinity_fn)
87
+
88
+ if repulsion_sigma and repulsion_weight:
89
+ nystrom_eigvec, eigval = ncut_with_repulsion(nystrom_X, n_eig, sigma,
90
+ repulsion_sigma, repulsion_weight, affinity_fn, exact_gradient)
91
+ else:
92
+ A = affinity_fn(nystrom_X, sigma=sigma)
93
+ nystrom_eigvec, eigval = _plain_ncut(A, n_eig, exact_gradient)
94
+
95
+ if no_propagation:
96
+ return nystrom_eigvec, eigval, nystrom_indices, sigma
97
+
98
+ if not is_enough_data:
99
+ # skip nystrom approximation if not enough data, use exact ncut
100
+ return nystrom_eigvec, eigval
101
+
102
+ # propagate eigenvectors from subgraph to full graph
103
+ eigvec = nystrom_propagate(
104
+ nystrom_eigvec,
105
+ X,
106
+ nystrom_X,
107
+ extrapolation_factor=extrapolation_factor,
108
+ n_neighbors=config.n_neighbors,
109
+ n_sample=config.n_sample2,
110
+ matmul_chunk_size=config.matmul_chunk_size,
111
+ device=device,
112
+ )
113
+
114
+ # post-hoc orthogonalization
115
+ if make_orthogonal:
116
+ eigvec = gram_schmidt(eigvec)
85
117
 
86
- with grad_manager(track_grad):
87
- # subsample for nystrom approximation
88
- n_sample = min(config.n_sample, int(X.shape[0]*config.n_sample_max_ratio))
89
- nystrom_indices = farthest_point_sampling(X, n_sample=n_sample, device=device) if is_enough_data else np.arange(X.shape[0])
90
- nystrom_X = X[nystrom_indices].to(device)
91
-
92
- # find optimal sigma for affinity matrix
93
- if sigma is None:
94
- if affinity_fn == rbf_affinity:
95
- sigma = find_sigma_by_degree(nystrom_X, d_sigma, affinity_fn)
96
- elif affinity_fn == cosine_affinity:
97
- sigma = 0.5
98
- else:
99
- raise ValueError(f"`sigma` needs to be provided for affinity function {affinity_fn}, (sigma=0.5)")
100
-
101
- if repulsion_sigma is not None:
102
- nystrom_eigvec, eigval = ncut_with_repulsion(nystrom_X, n_eig, sigma_attraction=sigma, sigma_repulsion=repulsion_sigma, repulsion_weight=repulsion_weight, affinity_fn=affinity_fn)
103
- else:
104
- A = affinity_fn(nystrom_X, sigma=sigma)
105
- nystrom_eigvec, eigval = _plain_ncut(A, n_eig)
106
-
107
- if no_propagation:
108
- return nystrom_eigvec, eigval, nystrom_indices, sigma
109
-
110
- if not is_enough_data:
111
- return nystrom_eigvec, eigval
112
-
113
- # propagate eigenvectors from subgraph to full graph
114
- eigvec = nystrom_propagate(
115
- nystrom_eigvec,
116
- X,
117
- nystrom_X,
118
- extrapolation_factor=extrapolation_factor,
119
- n_neighbors=config.n_neighbors,
120
- n_sample=config.n_sample2,
121
- matmul_chunk_size=config.matmul_chunk_size,
122
- device=device,
123
- move_output_to_cpu=config.move_output_to_cpu,
124
- track_grad=track_grad,
125
- )
126
-
127
- # post-hoc orthogonalization
128
- if make_orthogonal:
129
- eigvec = gram_schmidt(eigvec)
130
-
131
- return eigvec, eigval
118
+ return eigvec, eigval
132
119
 
120
+ def find_optimal_sigma(
121
+ X: torch.Tensor,
122
+ quantile_sigma: float = 0.25,
123
+ quantile_sigma_repulsion: float = 0.20,
124
+ sigma: float | None = None,
125
+ repulsion_sigma: float | None = None,
126
+ affinity_fn: Union["rbf_affinity", "cosine_affinity"] = rbf_affinity,
127
+ ):
128
+ """Find optimal sigma for affinity matrix and repulsion matrix."""
129
+ if affinity_fn == rbf_affinity:
130
+ sigma = sigma or find_sigma_by_degree(X, quantile_sigma, affinity_fn)
131
+ repulsion_sigma = repulsion_sigma or find_sigma_by_degree(X, quantile_sigma_repulsion, affinity_fn, init_sigma=sigma)
132
+ elif affinity_fn == cosine_affinity:
133
+ sigma = sigma or 0.5
134
+ repulsion_sigma = repulsion_sigma or 0.3
135
+ else:
136
+ if sigma is None:
137
+ raise ValueError(f"`sigma` need to be provided for affinity function {affinity_fn}, (sigma=0.5, repulsion_sigma=0.3)")
138
+ return sigma, repulsion_sigma
133
139
 
134
140
  def ncut_with_repulsion(
135
141
  X: torch.Tensor,
@@ -138,6 +144,7 @@ def ncut_with_repulsion(
138
144
  sigma_repulsion: float = None,
139
145
  repulsion_weight: float = 0.2,
140
146
  affinity_fn: Union["rbf_affinity", "cosine_affinity"] = cosine_affinity,
147
+ exact_gradient: bool = False,
141
148
  eps: float = 1e-8,
142
149
  ):
143
150
  A = affinity_fn(X, sigma=sigma_attraction)
@@ -148,7 +155,10 @@ def ncut_with_repulsion(
148
155
  D = D_A + D_R
149
156
  W = A - R + torch.diag(D_R)
150
157
  W = W / D[:, None]
151
- eigvec, eigval, _ = grad_safe_eig_solve(W, n_eig)
158
+ if exact_gradient:
159
+ eigvec, eigval, _ = grad_safe_eig_solve(W, n_eig)
160
+ else:
161
+ eigvec, eigval, _ = svd_lowrank(W, n_eig)
152
162
  eigvec = correct_rotation(eigvec)
153
163
  return eigvec, eigval
154
164
 
@@ -156,9 +166,13 @@ def ncut_with_repulsion(
156
166
  def _plain_ncut(
157
167
  A: torch.Tensor,
158
168
  n_eig: int = 100,
169
+ exact_gradient: bool = False,
159
170
  ):
160
171
  A = normalize_affinity(A)
161
- eigvec, eigval, _ = grad_safe_eig_solve(A, n_eig)
172
+ if exact_gradient:
173
+ eigvec, eigval, _ = grad_safe_eig_solve(A, n_eig)
174
+ else:
175
+ eigvec, eigval, _ = svd_lowrank(A, n_eig)
162
176
  eigvec = eigvec[:, :n_eig]
163
177
  eigval = eigval[:n_eig]
164
178
  eigvec = correct_rotation(eigvec)
@@ -170,7 +184,6 @@ def nystrom_propagate(
170
184
  X: torch.Tensor,
171
185
  nystrom_X: torch.Tensor,
172
186
  extrapolation_factor: float = 1.0,
173
- track_grad: bool = False,
174
187
  device: str = None,
175
188
  return_indices: bool = False,
176
189
  **kwargs,
@@ -183,7 +196,6 @@ def nystrom_propagate(
183
196
  X (torch.Tensor): input features for all nodes, shape (N, D)
184
197
  nystrom_X (torch.Tensor): input features from nystrom sampled nodes, shape (m, D)
185
198
  extrapolation_factor (float): control how far can we extrapolate, larger extrapolation_factor means we can extrapolate further, default 1.0
186
- track_grad (bool): keep track of pytorch gradients, default False
187
199
  device (str): device to use for computation, if 'auto', will detect GPU automatically
188
200
  affinity_fn (callable): affinity function, default rbf_affinity. Should accept (X1, X2=None, sigma=float) and return affinity matrix
189
201
 
@@ -194,45 +206,44 @@ def nystrom_propagate(
194
206
  config = NystromConfig()
195
207
  config.update(kwargs)
196
208
 
197
- with grad_manager(track_grad):
198
- device = auto_device(nystrom_out.device, device)
199
- indices = farthest_point_sampling(nystrom_out, config.n_sample2, device=device)
200
- nystrom_out = nystrom_out[indices].to(device)
201
- nystrom_X = nystrom_X[indices].to(device)
202
-
203
- sigma = find_sigma_by_degree(nystrom_X, affinity_fn=rbf_affinity)
204
- sigma = sigma * extrapolation_factor
205
-
206
- D = rbf_affinity(nystrom_X, sigma=sigma).mean(1)
207
-
208
- all_outs = []
209
- n_chunk = config.matmul_chunk_size
210
- n_neighbors = int(min(config.n_neighbors, len(indices)*config.n_neighbors_max_ratio))
211
- n_neighbors = max(n_neighbors, 4)
212
- for i in range(0, X.shape[0], n_chunk):
213
- end = min(i + n_chunk, X.shape[0])
214
-
215
- _Ai = rbf_affinity(X[i:end].to(device), nystrom_X, sigma=sigma)
216
- _Ai, _indices = keep_topk_per_row(_Ai, n_neighbors) # (n, n_neighbors)
217
- _Di = D[_indices].sum(1)
218
- _Ai = _Ai / _Di[:, None]
219
-
220
- weights = _Ai[..., None] # (n, n_neighbors, 1)
221
- neighbors = nystrom_out[_indices.flatten()]
222
- neighbors = neighbors.reshape(-1, n_neighbors, nystrom_out.shape[-1]) # (n, n_neighbors, d)
223
- out = weights * neighbors # (n, n_neighbors, d)
224
- out = out.sum(dim=1) # (n, d)
225
-
226
- if config.move_output_to_cpu and not track_grad:
227
- out = out.to("cpu")
228
- all_outs.append(out)
229
-
230
- all_outs = torch.cat(all_outs, dim=0)
231
-
232
- if return_indices:
233
- return all_outs, indices
234
-
235
- return all_outs
209
+ device = auto_device(nystrom_out.device, device)
210
+ output_device = X.device
211
+ indices = farthest_point_sampling(nystrom_out, config.n_sample2, device=device)
212
+ nystrom_out = nystrom_out[indices].to(device)
213
+ nystrom_X = nystrom_X[indices].to(device)
214
+
215
+ sigma = find_sigma_by_degree(nystrom_X, affinity_fn=rbf_affinity, quantile_sigma=0.25)
216
+ sigma = sigma * extrapolation_factor
217
+
218
+ D = rbf_affinity(nystrom_X, sigma=sigma).mean(1)
219
+
220
+ all_outs = []
221
+ n_chunk = config.matmul_chunk_size
222
+ n_neighbors = int(min(config.n_neighbors, len(indices)*config.n_neighbors_max_ratio))
223
+ n_neighbors = max(n_neighbors, 4)
224
+ for i in range(0, X.shape[0], n_chunk):
225
+ end = min(i + n_chunk, X.shape[0])
226
+
227
+ _Ai = rbf_affinity(X[i:end].to(device), nystrom_X, sigma=sigma)
228
+ _Ai, _indices = keep_topk_per_row(_Ai, n_neighbors) # (n, n_neighbors)
229
+ _Di = D[_indices].sum(1)
230
+ _Ai = _Ai / _Di[:, None]
231
+
232
+ weights = _Ai[..., None] # (n, n_neighbors, 1)
233
+ neighbors = nystrom_out[_indices.flatten()]
234
+ neighbors = neighbors.reshape(-1, n_neighbors, nystrom_out.shape[-1]) # (n, n_neighbors, d)
235
+ out = weights * neighbors # (n, n_neighbors, d)
236
+ out = out.sum(dim=1) # (n, d)
237
+
238
+ out = out.to(output_device)
239
+ all_outs.append(out)
240
+
241
+ all_outs = torch.cat(all_outs, dim=0)
242
+
243
+ if return_indices:
244
+ return all_outs, indices
245
+
246
+ return all_outs
236
247
 
237
248
 
238
249
 
@@ -1,8 +1,6 @@
1
- __all__ = ["rbf_eigvec_manual_grad", "grad_manager"]
1
+ __all__ = ["rbf_eigvec_manual_grad"]
2
2
 
3
3
  import torch
4
- from contextlib import contextmanager
5
-
6
4
 
7
5
  @torch.no_grad()
8
6
  def rbf_eigvec_manual_grad(
@@ -115,40 +113,3 @@ def rbf_eigvec_manual_grad(
115
113
 
116
114
  return grad_u
117
115
 
118
-
119
-
120
- @contextmanager
121
- def grad_manager(enabled: bool):
122
- """Context manager to temporarily set gradient computation mode.
123
-
124
- This context manager allows you to control gradient computation for a block
125
- of code, and automatically restores the previous gradient state when exiting
126
- the context.
127
-
128
- Args:
129
- enabled (bool): If True, enables gradient tracking within the context.
130
- If False, disables gradient tracking within the context.
131
-
132
- Yields:
133
- None
134
-
135
- Examples:
136
- >>> import torch
137
- >>> from ncut_pytorch.utils.grad import set_grad_enabled
138
- >>>
139
- >>> # Disable gradients for inference
140
- >>> with set_grad_enabled(False):
141
- ... result = model(input_tensor)
142
- >>>
143
- >>> # Enable gradients for training
144
- >>> with set_grad_enabled(True):
145
- ... loss = criterion(model(input_tensor), target)
146
- ... loss.backward()
147
- """
148
- prev_grad_state = torch.is_grad_enabled()
149
- torch.set_grad_enabled(enabled)
150
- try:
151
- yield
152
- finally:
153
- torch.set_grad_enabled(prev_grad_state)
154
-
@@ -18,7 +18,7 @@ import logging
18
18
  import numpy as np
19
19
  import torch
20
20
 
21
- from .torch_mod import svd_lowrank
21
+ from .torch_mod import svd_lowrank as my_svd_lowrank
22
22
 
23
23
 
24
24
  def check_gamma_deprecated(gamma: float | None) -> float:
@@ -122,6 +122,23 @@ def pca_lowrank(
122
122
  return u @ torch.diag(s)
123
123
 
124
124
 
125
+ def svd_lowrank(mat: torch.Tensor, q: int) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
126
+ """SVD lowrank implementation for float16 and bfloat16."""
127
+ dtype = mat.dtype
128
+ try:
129
+ with torch.autocast(device_type=mat.device.type, enabled=False):
130
+ if dtype == torch.float16 or dtype == torch.bfloat16:
131
+ mat = mat.float() # svd_lowrank does not support float16
132
+ u, s, v = my_svd_lowrank(mat, q=q + 10)
133
+ except RuntimeError:
134
+ if dtype == torch.float16 or dtype == torch.bfloat16:
135
+ mat = mat.float()
136
+ u, s, v = my_svd_lowrank(mat, q=q + 10)
137
+
138
+ u, s, v = u[:, :q], s[:q], v[:, :q]
139
+ return u.to(dtype), s.to(dtype), v.to(dtype)
140
+
141
+
125
142
  def quantile_min_max(
126
143
  x: torch.Tensor,
127
144
  q1: float = 0.01,
@@ -9,7 +9,7 @@ from .sample import farthest_point_sampling
9
9
  @torch.no_grad()
10
10
  def _find_sigma_by_degree(
11
11
  X: torch.Tensor, # [n_samples, n_features]
12
- d_sigma: float | str | None = 'auto',
12
+ quantile_sigma: float = 0.25,
13
13
  affinity_fn: callable = rbf_affinity,
14
14
  X2: torch.Tensor | None = None,
15
15
  init_sigma: float = 0.5,
@@ -17,27 +17,21 @@ def _find_sigma_by_degree(
17
17
  max_iter: int = 100,
18
18
  ) -> float:
19
19
  """Binary search for optimal sigma to achieve target mean edge weight."""
20
- if isinstance(d_sigma, float):
21
- assert d_sigma > 0, "d_sigma must be positive"
20
+ if quantile_sigma <= 0 or quantile_sigma >= 1:
21
+ raise ValueError(f"quantile_sigma must be between 0 and 1, got {quantile_sigma}")
22
22
  sigma = init_sigma
23
23
 
24
- # Find d_sigma if 'auto'
25
- if d_sigma in ('auto', None):
26
- scale_inv_sigma = sigma * X.std(0).sum()
27
- current_degrees = affinity_fn(X, X2=X2, sigma=scale_inv_sigma).mean(1)
28
- for _ in range(2):
29
- current_degree = current_degrees.mean().item()
30
- mask = current_degrees < current_degree
31
- current_degrees = current_degrees[mask]
32
- d_sigma = current_degrees.mean().item()
24
+ scale_inv_sigma = X.std(0).sum()
25
+ current_degrees = affinity_fn(X, X2=X2, sigma=scale_inv_sigma).mean(1)
26
+ target_degree = current_degrees.float().quantile(quantile_sigma).item()
33
27
 
34
28
  # Binary search for sigma
35
29
  current_degree = affinity_fn(X, X2=X2, sigma=sigma).mean().item()
36
30
  low, high = 0, float('inf')
37
- tol = r_tol * d_sigma
31
+ tol = r_tol * target_degree
38
32
  i_iter = 0
39
- while abs(current_degree - d_sigma) > tol and i_iter < max_iter:
40
- if current_degree > d_sigma:
33
+ while abs(current_degree - target_degree) > tol and i_iter < max_iter:
34
+ if current_degree > target_degree:
41
35
  high = sigma
42
36
  sigma = (low + sigma) / 2
43
37
  else:
@@ -52,7 +46,7 @@ def _find_sigma_by_degree(
52
46
  @torch.no_grad()
53
47
  def find_sigma_by_degree(
54
48
  X: torch.Tensor, # [n_samples, n_features]
55
- d_sigma: float | str | None = 'auto',
49
+ quantile_sigma: float = 0.25,
56
50
  affinity_fn: callable = rbf_affinity,
57
51
  X2: torch.Tensor | None = None,
58
52
  init_sigma: float = 0.5,
@@ -62,4 +56,4 @@ def find_sigma_by_degree(
62
56
  ) -> float:
63
57
  """Find sigma after FPS-based downsampling for efficiency."""
64
58
  indices = farthest_point_sampling(X, n_sample)
65
- return _find_sigma_by_degree(X[indices], d_sigma, affinity_fn, X2=X2, init_sigma=init_sigma, r_tol=r_tol, max_iter=max_iter)
59
+ return _find_sigma_by_degree(X[indices], quantile_sigma, affinity_fn, X2=X2, init_sigma=init_sigma, r_tol=r_tol, max_iter=max_iter)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ncut_pytorch
3
- Version: 3.0.0.dev0
3
+ Version: 3.0.0.dev2
4
4
  Summary: Normalized Cut and Spectral Embedding
5
5
  Author-email: Huzheng Yang <huze.yann@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ncut_pytorch"
7
- version = "3.0.0dev0"
7
+ version = "3.0.0dev2"
8
8
  authors = [
9
9
  { name = "Huzheng Yang", email = "huze.yann@gmail.com" },
10
10
  ]
@@ -1,106 +0,0 @@
1
- __all__ = ['ncut_click_prompt']
2
-
3
- from typing import Callable, Union
4
-
5
- import numpy as np
6
- import torch
7
-
8
- from ncut_pytorch.utils.sigma import find_sigma_by_degree
9
- from ncut_pytorch.utils.math import rbf_affinity, cosine_affinity, normalize_affinity
10
- from ncut_pytorch.utils.sample import farthest_point_sampling
11
- from ncut_pytorch.utils.device import auto_device
12
- from ncut_pytorch.utils.grad import grad_manager
13
- from .ncut_nystrom import NystromConfig
14
- from .ncut_nystrom import nystrom_propagate
15
- from .ncut_nystrom import _plain_ncut
16
-
17
-
18
- #TODO: automatically optimize click_weight based on the iou of fg and bg
19
- def ncut_click_prompt(
20
- X: torch.Tensor,
21
- fg_indices: np.ndarray,
22
- bg_indices: np.ndarray = None,
23
- click_weight: float = 0.5,
24
- bg_weight: float = 0.1,
25
- n_eig: int = 2,
26
- track_grad: bool = False,
27
- d_sigma: float = None,
28
- device: str = None,
29
- sigma: float = None,
30
- affinity_fn: Callable[[torch.Tensor, torch.Tensor, float], torch.Tensor] = rbf_affinity,
31
- no_propagation: bool = False,
32
- return_indices_and_sigma: bool = False,
33
- **kwargs,
34
- ) -> Union[tuple[torch.Tensor, torch.Tensor], tuple[torch.Tensor, torch.Tensor, torch.Tensor, float]]:
35
-
36
- config = NystromConfig()
37
- config.update(kwargs)
38
-
39
- # use GPU if available
40
- device = auto_device(X.device, device)
41
-
42
- with grad_manager(track_grad):
43
- if bg_indices is None:
44
- bg_indices = np.array([], dtype=np.int64)
45
-
46
- # subsample for nystrom approximation
47
- nystrom_indices = farthest_point_sampling(X, n_sample=config.n_sample, device=device)
48
- nystrom_indices = torch.tensor(nystrom_indices, dtype=torch.long)
49
- # remove fg and bg from fps_idx
50
- nystrom_indices = nystrom_indices[~np.isin(nystrom_indices, np.concatenate([fg_indices, bg_indices]))]
51
- # add fg and bg to fps_idx
52
- nystrom_indices = np.concatenate([fg_indices, bg_indices, nystrom_indices])
53
- fg_indices = np.arange(len(fg_indices))
54
- bg_indices = np.arange(len(bg_indices)) + len(fg_indices)
55
- n_fgbg = len(fg_indices) + len(bg_indices)
56
-
57
- nystrom_X = X[nystrom_indices].to(device)
58
-
59
- # find optimal sigma for affinity matrix
60
- if sigma is None and affinity_fn == rbf_affinity:
61
- sigma = find_sigma_by_degree(nystrom_X, d_sigma, affinity_fn)
62
- # TODO: change to std()
63
- elif sigma is None and affinity_fn == cosine_affinity:
64
- sigma = 0.5
65
-
66
- # compute Ncut on the nystrom sampled subgraph
67
- A = affinity_fn(nystrom_X, sigma=sigma)
68
- A = normalize_affinity(A)
69
-
70
- # modify the affinity from the clicks
71
- X_click = 1 * A[fg_indices].mean(0)
72
- if len(bg_indices) > 0:
73
- X_click = X_click - bg_weight * A[bg_indices].mean(0)
74
-
75
- X_click = X_click * A.shape[0]
76
-
77
- A_click = affinity_fn(X_click.unsqueeze(1), sigma=0.5)
78
- A_click = normalize_affinity(A_click)
79
-
80
- _A = click_weight * A_click + (1 - click_weight) * A
81
-
82
- nystrom_eigvec, eigval = _plain_ncut(_A, n_eig)
83
-
84
- if no_propagation:
85
- return nystrom_eigvec, eigval, nystrom_indices, sigma
86
-
87
- # propagate eigenvectors from subgraph to full graph
88
- eigvec, nystrom_indices2 = nystrom_propagate(
89
- nystrom_eigvec,
90
- X,
91
- nystrom_X,
92
- n_neighbors=config.n_neighbors,
93
- n_sample=config.n_sample2,
94
- matmul_chunk_size=config.matmul_chunk_size,
95
- device=device,
96
- move_output_to_cpu=config.move_output_to_cpu,
97
- track_grad=track_grad,
98
- return_indices=True,
99
- )
100
-
101
-
102
- if return_indices_and_sigma:
103
- indices = nystrom_indices[nystrom_indices2]
104
- return eigvec, eigval, indices, sigma
105
-
106
- return eigvec, eigval