|
| 1 | +import torch |
| 2 | +from torch import nn |
| 3 | +import torch.nn.functional as F |
| 4 | + |
| 5 | +from torch_geometric.nn.conv import MessagePassing |
| 6 | +from torch_geometric.utils import add_self_loops |
| 7 | +from torch_geometric.utils import get_laplacian |
| 8 | +from scipy.special import comb |
| 9 | + |
| 10 | + |
| 11 | +class BernProp(MessagePassing): |
| 12 | + """ |
| 13 | + K-order Bernstein polynomial approximation. |
| 14 | +
|
| 15 | + Parameters |
| 16 | + ---------- |
| 17 | + K : int |
| 18 | + Order of the polynomial filter. Determines the complexity of the spectral filter. |
| 19 | + is_source_domain : bool, optional |
| 20 | + Whether this layer is used for source domain. If True, temperature parameters |
| 21 | + are learnable. If False, they are fixed to a linear interpolation from 1 to 0. |
| 22 | + Default: ``True``. |
| 23 | + bias : bool, optional |
| 24 | + Whether to add bias. Currently not used but kept for compatibility. |
| 25 | + Default: ``True``. |
| 26 | + **kwargs : optional |
| 27 | + Additional keyword arguments passed to the MessagePassing parent class. |
| 28 | +
|
| 29 | + Notes |
| 30 | + ----- |
| 31 | + This class implements a graph neural network layer that performs spectral domain |
| 32 | + adaptation. It uses a polynomial filter approach with learnable temperature parameters |
| 33 | + to adapt the spectral characteristics of the graph. |
| 34 | +
|
| 35 | + The layer computes a polynomial filter of order K and applies it to the graph |
| 36 | + signal through message passing operations. The filter coefficients are determined |
| 37 | + by learnable temperature parameters that differ between source and target domains. |
| 38 | +
|
| 39 | + Attributes |
| 40 | + ---------- |
| 41 | + K : int |
| 42 | + Order of the polynomial filter. |
| 43 | + is_source_domain : bool |
| 44 | + Whether this layer is for source domain. |
| 45 | + cached_terms : torch.Tensor, optional |
| 46 | + Cached polynomial terms for filter computation. |
| 47 | + cached_coefs : torch.Tensor, optional |
| 48 | + Cached polynomial coefficients. |
| 49 | + temp : nn.Parameter |
| 50 | + Learnable temperature parameters for the filter. |
| 51 | + """ |
| 52 | + |
| 53 | + def __init__(self, K, is_source_domain=True, bias=True, **kwargs): |
| 54 | + super(BernProp, self).__init__(aggr='add', **kwargs) |
| 55 | + |
| 56 | + self.K = K |
| 57 | + self.is_source_domain = is_source_domain |
| 58 | + self.cached_terms = None |
| 59 | + self.cached_coefs = None |
| 60 | + self.temp = nn.Parameter(torch.Tensor(self.K + 1), requires_grad=is_source_domain) |
| 61 | + self.reset_parameters() |
| 62 | + |
| 63 | + def reset_parameters(self): |
| 64 | + """ |
| 65 | + Reset the learnable parameters of the layer. |
| 66 | +
|
| 67 | + Notes |
| 68 | + ----- |
| 69 | + - For source domain layers, temperature parameters are initialized to 1. |
| 70 | +
|
| 71 | + - For target domain layers, temperature parameters are set to a linear |
| 72 | + interpolation from 1 to 0 over K+1 values. |
| 73 | + """ |
| 74 | + if self.is_source_domain: |
| 75 | + self.temp.data.fill_(1) |
| 76 | + else: |
| 77 | + self.temp.data = torch.linspace(1, 0, self.K + 1) |
| 78 | + |
| 79 | + def get_filter(self): |
| 80 | + """ |
| 81 | + Compute the spectral filter using cached terms and coefficients. |
| 82 | +
|
| 83 | + Returns |
| 84 | + ------- |
| 85 | + torch.Tensor |
| 86 | + The computed spectral filter H. |
| 87 | +
|
| 88 | + Notes |
| 89 | + ----- |
| 90 | + This method requires cached_terms and cached_coefs to be set before calling. |
| 91 | + The filter is computed as a weighted sum of polynomial terms. |
| 92 | + """ |
| 93 | + TEMP = F.relu(self.temp) |
| 94 | + H = 0 |
| 95 | + |
| 96 | + for k in range(self.K + 1): |
| 97 | + H = H + TEMP[k] * self.cached_coefs[k] * self.cached_terms[k] |
| 98 | + |
| 99 | + return H |
| 100 | + |
| 101 | + def forward(self, x, edge_index, edge_weight=None): |
| 102 | + """ |
| 103 | + Forward pass of the DGSD layer. |
| 104 | +
|
| 105 | + Parameters |
| 106 | + ---------- |
| 107 | + x : torch.Tensor |
| 108 | + Node feature matrix of shape [num_nodes, num_features]. |
| 109 | + edge_index : torch.LongTensor |
| 110 | + Graph connectivity in COO format with shape [2, num_edges]. |
| 111 | + edge_weight : torch.Tensor, optional |
| 112 | + Edge weights of shape [num_edges]. If None, all edges are assumed |
| 113 | + to have weight 1. Default: ``None``. |
| 114 | +
|
| 115 | + Returns |
| 116 | + ------- |
| 117 | + torch.Tensor |
| 118 | + Updated node features after spectral domain adaptation. |
| 119 | +
|
| 120 | + Notes |
| 121 | + ----- |
| 122 | + This method implements the spectral domain adaptation by: |
| 123 | +
|
| 124 | + - Computing the symmetric normalized Laplacian |
| 125 | +
|
| 126 | + - Adding self-loops with appropriate weights |
| 127 | +
|
| 128 | + - Propagating messages through the graph using polynomial filters |
| 129 | +
|
| 130 | + - Combining the results using binomial coefficients and temperature parameters |
| 131 | + """ |
| 132 | + TEMP = F.relu(self.temp) |
| 133 | + |
| 134 | + edge_index1, norm1 = get_laplacian(edge_index, edge_weight, normalization='sym', dtype=x.dtype, |
| 135 | + num_nodes=x.size(self.node_dim)) |
| 136 | + edge_index2, norm2 = add_self_loops(edge_index1, -norm1, fill_value=2., num_nodes=x.size(self.node_dim)) |
| 137 | + |
| 138 | + tmp = [] |
| 139 | + tmp.append(x) |
| 140 | + for i in range(self.K): |
| 141 | + x = self.propagate(edge_index2, x=x, norm=norm2, size=None) |
| 142 | + tmp.append(x) |
| 143 | + |
| 144 | + out = (comb(self.K, 0) / (2 ** self.K)) * TEMP[0] * tmp[self.K] |
| 145 | + |
| 146 | + for i in range(self.K): |
| 147 | + x = tmp[self.K - i - 1] |
| 148 | + x = self.propagate(edge_index1, x=x, norm=norm1, size=None) |
| 149 | + for j in range(i): |
| 150 | + x = self.propagate(edge_index1, x=x, norm=norm1, size=None) |
| 151 | + |
| 152 | + out = out + (comb(self.K, i + 1) / (2 ** self.K)) * TEMP[i + 1] * x |
| 153 | + return out |
| 154 | + |
| 155 | + def message(self, x_j, norm): |
| 156 | + """ |
| 157 | + Message function for message passing. |
| 158 | +
|
| 159 | + Parameters |
| 160 | + ---------- |
| 161 | + x_j : torch.Tensor |
| 162 | + Source node features of shape [num_edges, num_features]. |
| 163 | + norm : torch.Tensor |
| 164 | + Normalized edge weights of shape [num_edges]. |
| 165 | +
|
| 166 | + Returns |
| 167 | + ------- |
| 168 | + torch.Tensor |
| 169 | + Messages of shape [num_edges, num_features]. |
| 170 | + """ |
| 171 | + return norm.view(-1, 1) * x_j |
| 172 | + |
| 173 | + def __repr__(self): |
| 174 | + """ |
| 175 | + String representation of the layer. |
| 176 | +
|
| 177 | + Returns |
| 178 | + ------- |
| 179 | + str |
| 180 | + String representation showing the class name, filter order K, and |
| 181 | + temperature parameters. |
| 182 | + """ |
| 183 | + return '{}(K={}, temp={})'.format(self.__class__.__name__, self.K, self.temp) |
| 184 | + |
| 185 | + |
| 186 | +class DGSDABase(nn.Module): |
| 187 | + """ |
| 188 | + Base class for DGSDA. |
| 189 | +
|
| 190 | + Parameters |
| 191 | + ---------- |
| 192 | + features : int |
| 193 | + Input feature dimension. |
| 194 | + hidden : int |
| 195 | + Hidden layer dimension. |
| 196 | + classes : int |
| 197 | + Number of output classes. |
| 198 | + dprate : float, optional |
| 199 | + Dropout rate for propagation layers. Default: ``0.0``. |
| 200 | + K : int, optional |
| 201 | + Order of the polynomial filter for propagation layers. Default: ``15``. |
| 202 | +
|
| 203 | + Notes |
| 204 | + ----- |
| 205 | + This class implements a domain generalization model using spectral domain |
| 206 | + adaptation. It uses separate propagation layers for source and target domains |
| 207 | + to adapt the spectral characteristics of the graph data. |
| 208 | +
|
| 209 | + The model consists of: |
| 210 | +
|
| 211 | + - Linear transformation layers |
| 212 | +
|
| 213 | + - Bernoulli propagation layers with polynomial filters |
| 214 | +
|
| 215 | + - Domain-specific propagation paths |
| 216 | + """ |
| 217 | + |
| 218 | + def __init__(self, features, hidden, classes, dprate=0.0, K=15): |
| 219 | + super(DGSDABase, self).__init__() |
| 220 | + self.lin1 = nn.Linear(features, hidden) |
| 221 | + self.lin2 = nn.Linear(hidden, classes) |
| 222 | + self.prop1 = BernProp(K) |
| 223 | + self.prop2 = BernProp(K) |
| 224 | + self.prop3 = BernProp(K) |
| 225 | + |
| 226 | + self.dprate = dprate |
| 227 | + |
| 228 | + def reset_parameters(self): |
| 229 | + """ |
| 230 | + Reset the learnable parameters of the model. |
| 231 | +
|
| 232 | + Notes |
| 233 | + ----- |
| 234 | + Currently only resets the first propagation layer (prop1). |
| 235 | + """ |
| 236 | + self.prop1.reset_parameters() |
| 237 | + |
| 238 | + def forward(self, data, is_source_domain=True): |
| 239 | + """ |
| 240 | + Forward pass of the DGSDA model. |
| 241 | +
|
| 242 | + Parameters |
| 243 | + ---------- |
| 244 | + data : torch_geometric.data.Data |
| 245 | + Input graph data containing node features and edge indices. |
| 246 | + is_source_domain : bool, optional |
| 247 | + Whether the input is from source domain. Determines which |
| 248 | + propagation layer to use. Default: ``True``. |
| 249 | +
|
| 250 | + Returns |
| 251 | + ------- |
| 252 | + torch.Tensor |
| 253 | + Model predictions for node classification. |
| 254 | +
|
| 255 | + Notes |
| 256 | + ----- |
| 257 | + The forward pass consists of: |
| 258 | +
|
| 259 | + - Domain-specific feature propagation |
| 260 | +
|
| 261 | + - Dropout regularization |
| 262 | +
|
| 263 | + - Final linear classification |
| 264 | +
|
| 265 | + - Final propagation step |
| 266 | + """ |
| 267 | + x, edge_index = data.x, data.edge_index |
| 268 | + |
| 269 | + x = self.get_props(x, edge_index, is_source_domain) |
| 270 | + |
| 271 | + x = F.dropout(x, p=self.dprate, training=self.training) |
| 272 | + x = self.lin2(x) |
| 273 | + |
| 274 | + x = F.dropout(x, p=self.dprate, training=self.training) |
| 275 | + x = self.prop3(x, edge_index) |
| 276 | + return x |
| 277 | + |
| 278 | + def get_props(self, x, edge_index, is_source_domain=True): |
| 279 | + """ |
| 280 | + Apply domain-specific feature propagation. |
| 281 | +
|
| 282 | + Parameters |
| 283 | + ---------- |
| 284 | + x : torch.Tensor |
| 285 | + Node features of shape [num_nodes, num_features]. |
| 286 | + edge_index : torch.LongTensor |
| 287 | + Graph connectivity in COO format with shape [2, num_edges]. |
| 288 | + is_source_domain : bool, optional |
| 289 | + Whether to use source domain propagation layer. If True, uses prop1; |
| 290 | + if False, uses prop2. Default: ``True``. |
| 291 | +
|
| 292 | + Returns |
| 293 | + ------- |
| 294 | + torch.Tensor |
| 295 | + Propagated node features. |
| 296 | +
|
| 297 | + Notes |
| 298 | + ----- |
| 299 | + This method implements the domain-specific feature processing: |
| 300 | +
|
| 301 | + - Initial dropout and ReLU activation |
| 302 | +
|
| 303 | + - Domain-specific propagation using Bernoulli polynomial filters |
| 304 | +
|
| 305 | + - Separate propagation paths for source and target domains |
| 306 | + """ |
| 307 | + x = F.dropout(x, p=self.dprate, training=self.training) |
| 308 | + x = F.relu(self.lin1(x)) |
| 309 | + |
| 310 | + x = F.dropout(x, p=self.dprate, training=self.training) |
| 311 | + if is_source_domain: |
| 312 | + x = self.prop1(x, edge_index) |
| 313 | + else: |
| 314 | + x = self.prop2(x, edge_index) |
| 315 | + return x |
0 commit comments