Bases: BasePredictor, Module
Implementation for the Kappa GCN.
| Attributes: |
-
pm
–
ProductManifold object for the Kappa GCN.
-
output_dim
–
Number of output features.
-
num_hidden
–
-
nonlinearity
–
Function for nonlinear activation.
-
task
–
Task type, one of ["classification", "regression", "link_prediction"]
-
random_state
–
Random seed for reproducibility.
-
device
–
Device to run the model on (default: None, uses current device).
-
is_fitted_
(bool)
–
Whether the model has been fitted.
-
loss_history_
(dict[str, list[float]])
–
History of loss values during training.
|
| Parameters: |
-
pm
(ProductManifold)
–
ProductManifold object for the Kappa GCN
-
output_dim
(int)
–
Number of output features
-
num_hidden
(int, default:
2
)
–
-
nonlinearity
(Callable, default:
relu
)
–
Function for nonlinear activation.
-
task
(Literal['classification', 'regression', 'link_prediction'], default:
'classification'
)
–
Task type, one of ["classification", "regression", "link_prediction"].
-
random_state
(int | None, default:
None
)
–
Random seed for reproducibility.
-
device
(str | None, default:
None
)
–
Device to run the model on (default: None, uses current device).
|
| Raises: |
-
ValueError
–
If the ProductManifold is not stereographic.
|
Source code in manify/predictors/kappa_gcn.py
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127 | def __init__(
self,
pm: ProductManifold,
output_dim: int,
num_hidden: int = 2,
nonlinearity: Callable = torch.relu,
task: Literal["classification", "regression", "link_prediction"] = "classification",
random_state: int | None = None,
device: str | None = None,
):
BasePredictor.__init__(self, pm=pm, task=task, random_state=random_state, device=device)
torch.nn.Module.__init__(self)
self.pm = pm
self.task = task
self.output_dim = output_dim
self.num_hidden = num_hidden
self.nonlinearity = nonlinearity
# Ensure pm is stereographic
if not pm.is_stereographic:
raise ValueError(
"ProductManifold must be stereographic for KappaGCN to work.Please use pm.stereographic() to convert."
)
# Build layer dimensions
dims = [pm.dim] + [pm.dim] * num_hidden
# Build the main GCN layers using Sequential
gcn_layers = []
for i in range(len(dims) - 1):
gcn_layers.append(KappaGCNLayer(dims[i], dims[i + 1], pm, nonlinearity))
self.gcn_layers = KappaSequential(*gcn_layers)
# Task-specific output layers - much cleaner now!
if task == "link_prediction":
self.output_layer = FermiDiracDecoder(pm, learnable_params=True)
else:
# This is the same for classification/regression since we apply softmax in the loss function, not here
self.output_layer = StereographicLogits(output_dim, pm, apply_softmax=False)
|
forward(X, A_hat=None, aggregate_logits=True, softmax=False)
Forward pass through the GCN layers and output head.
Source code in manify/predictors/kappa_gcn.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154 | def forward(
self,
X: Float[torch.Tensor, "n_nodes dim"],
A_hat: Float[torch.Tensor, "n_nodes n_nodes"] | None = None,
aggregate_logits: bool = True,
softmax: bool = False,
) -> (
Float[torch.Tensor, "n_nodes n_classes"]
| Float[torch.Tensor, "n_nodes"]
| Float[torch.Tensor, "n_nodes n_nodes"]
):
"""Forward pass through the GCN layers and output head."""
# Pass through main GCN layers
H = self.gcn_layers(X, A_hat)
# Task-specific output using the specialized layers
if self.task == "link_prediction":
return self.output_layer(H) # Flattened for link prediction
else:
# For classification/regression, use stereographic logits
logits = self.output_layer(H, A_hat, aggregate_logits=aggregate_logits)
if softmax:
logits = torch.softmax(logits, dim=-1)
return logits.squeeze()
|
fit(X, y, A=None, epochs=2000, lr=0.01, use_tqdm=True, tqdm_prefix=None)
Fit the Kappa GCN model.
| Parameters: |
-
X
(Tensor)
–
-
y
(Tensor)
–
Labels for training nodes.
-
A
(Tensor, default:
None
)
–
Adjacency or distance matrix.
-
epochs
(int, default:
2000
)
–
Number of training epochs (default=200).
-
lr
(float, default:
0.01
)
–
Learning rate (default=1e-2).
-
use_tqdm
(bool, default:
True
)
–
Whether to use tqdm for progress bar.
-
tqdm_prefix
(str | None, default:
None
)
–
Prefix for tqdm progress bar.
|
Source code in manify/predictors/kappa_gcn.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245 | def fit(
self,
X: Float[torch.Tensor, "n_nodes dim"],
y: Real[torch.Tensor, "n_nodes"],
A: Float[torch.Tensor, "n_nodes n_nodes"] | None = None,
epochs: int = 2_000,
lr: float = 1e-2,
use_tqdm: bool = True,
tqdm_prefix: str | None = None,
) -> KappaGCN:
"""Fit the Kappa GCN model.
Args:
X (torch.Tensor): Feature matrix.
y (torch.Tensor): Labels for training nodes.
A (torch.Tensor): Adjacency or distance matrix.
epochs: Number of training epochs (default=200).
lr: Learning rate (default=1e-2).
use_tqdm: Whether to use tqdm for progress bar.
tqdm_prefix: Prefix for tqdm progress bar.
"""
# Copy everything
X = X.clone()
y = y.clone()
A = A.clone() if A is not None else None
# Convert A to A_hat
A_hat = get_A_hat(A, make_symmetric=True, add_self_loops=True) if A is not None else None
# Collect all paramters
euclidean_params = []
riemannian_params = []
for layer in self.gcn_layers.layers:
euclidean_params.append(layer.W)
if self.task == "link_prediction":
euclidean_params += [self.output_layer.temperature, self.output_layer.bias]
else:
euclidean_params += [self.output_layer.W]
riemannian_params += [self.output_layer.p_ks]
# Optimizers
opt = torch.optim.Adam(euclidean_params, lr=lr)
ropt = geoopt.optim.RiemannianAdam(riemannian_params, lr=lr) if riemannian_params else None
if self.task == "classification":
loss_fn = torch.nn.CrossEntropyLoss()
y = y.long()
elif self.task == "regression":
loss_fn = torch.nn.MSELoss()
y = y.float()
elif self.task == "link_prediction":
loss_fn = torch.nn.BCEWithLogitsLoss()
# y = y.flatten().float()
y = y.float()
else:
raise ValueError("Invalid task!")
self.train()
if use_tqdm:
my_tqdm = tqdm(total=epochs, desc=tqdm_prefix)
losses = []
for i in range(epochs):
opt.zero_grad()
if riemannian_params:
ropt.zero_grad() # type: ignore
y_pred = self(X, A_hat)
loss = loss_fn(y_pred, y)
loss.backward()
opt.step()
if riemannian_params:
ropt.step() # type: ignore
# Progress bar
if use_tqdm:
my_tqdm.update(1)
my_tqdm.set_description(f"Epoch {i + 1}/{epochs}, Loss: {loss.item():.4f}")
# Early termination for nan loss
if torch.isnan(loss):
print("Loss is NaN, stopping training.")
break
losses.append(loss.item())
if use_tqdm:
my_tqdm.close()
self.is_fitted_ = True
self.loss_history_["train"] = losses
return self
|
predict_proba(X, A=None)
Predict class probabilities using the trained Kappa GCN.
| Parameters: |
-
X
(Tensor)
–
-
A
(Tensor, default:
None
)
–
Adjacency or distance matrix (NxN).
|
| Returns: |
-
Real[Tensor, 'n_nodes n_classes'] | Real[Tensor, 'n_nodes']
–
torch.Tensor: Predicted class probabilities / regression targets.
|
Source code in manify/predictors/kappa_gcn.py
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267 | def predict_proba(
self, X: Float[torch.Tensor, "n_nodes dim"], A: Float[torch.Tensor, "n_nodes n_nodes"] | None = None
) -> Real[torch.Tensor, "n_nodes n_classes"] | Real[torch.Tensor, "n_nodes"]:
"""Predict class probabilities using the trained Kappa GCN.
Args:
X (torch.Tensor): Feature matrix (NxD).
A (torch.Tensor): Adjacency or distance matrix (NxN).
Returns:
torch.Tensor: Predicted class probabilities / regression targets.
"""
# Copy everything
X = X.clone()
A = A.clone() if A is not None else None
A_hat = get_A_hat(A, make_symmetric=True, add_self_loops=True) if A is not None else None
# Get edges for test set
self.eval()
y_pred = self(X, A_hat)
return y_pred
|