|
| 1 | +# Licensed to the Apache Software Foundation (ASF) under one |
| 2 | +# or more contributor license agreements. See the NOTICE file |
| 3 | +# distributed with this work for additional information |
| 4 | +# regarding copyright ownership. The ASF licenses this file |
| 5 | +# to you under the Apache License, Version 2.0 (the |
| 6 | +# "License"); you may not use this file except in compliance |
| 7 | +# with the License. You may obtain a copy of the License at |
| 8 | +# |
| 9 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | +# |
| 11 | +# Unless required by applicable law or agreed to in writing, |
| 12 | +# software distributed under the License is distributed on an |
| 13 | +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| 14 | +# KIND, either express or implied. See the License for the |
| 15 | +# specific language governing permissions and limitations |
| 16 | +# under the License. |
| 17 | + |
| 18 | +import abc |
| 19 | +import os |
| 20 | +import torch |
| 21 | +import torch.nn as nn |
| 22 | +import torch.nn.functional as F |
| 23 | + |
| 24 | +torch.set_num_threads(1) |
| 25 | + |
| 26 | + |
| 27 | +class TransFormFunction(abc.ABC): |
| 28 | + |
| 29 | + def __init__(self, input_size): |
| 30 | + self.input_size = input_size |
| 31 | + |
| 32 | + @abc.abstractmethod |
| 33 | + def load_model(self, *args): |
| 34 | + pass |
| 35 | + |
| 36 | + @abc.abstractmethod |
| 37 | + def transform_pre(self, *args): |
| 38 | + pass |
| 39 | + |
| 40 | + @abc.abstractmethod |
| 41 | + def transform_post(self, *args): |
| 42 | + pass |
| 43 | + |
| 44 | + |
| 45 | +class EmptyGCNModel(nn.Module): |
| 46 | + |
| 47 | + def __init__(self, input_dim, hidden_dim, embedding_dim, num_classes): |
| 48 | + super(EmptyGCNModel, self).__init__() |
| 49 | + self.input_proj = nn.Linear(input_dim, hidden_dim) |
| 50 | + self.embedding_proj = nn.Linear(hidden_dim, embedding_dim) |
| 51 | + self.classifier = nn.Linear(embedding_dim, num_classes) |
| 52 | + |
| 53 | + def forward(self, node_features, edge_index): |
| 54 | + hidden = self._aggregate(node_features, edge_index) |
| 55 | + hidden = F.relu(self.input_proj(hidden)) |
| 56 | + embedding = self.embedding_proj(self._aggregate(hidden, edge_index)) |
| 57 | + logits = self.classifier(embedding) |
| 58 | + return embedding, logits |
| 59 | + |
| 60 | + def _aggregate(self, features, edge_index): |
| 61 | + if edge_index is None or edge_index.numel() == 0: |
| 62 | + return features |
| 63 | + num_nodes = features.size(0) |
| 64 | + aggregated = torch.zeros_like(features) |
| 65 | + degree = torch.zeros((num_nodes, 1), dtype=features.dtype, device=features.device) |
| 66 | + src_index = edge_index[0] |
| 67 | + dst_index = edge_index[1] |
| 68 | + for idx in range(src_index.size(0)): |
| 69 | + src = int(src_index[idx].item()) |
| 70 | + dst = int(dst_index[idx].item()) |
| 71 | + if src < 0 or src >= num_nodes or dst < 0 or dst >= num_nodes: |
| 72 | + continue |
| 73 | + aggregated[dst] = aggregated[dst] + features[src] |
| 74 | + degree[dst] = degree[dst] + 1.0 |
| 75 | + degree = torch.clamp(degree, min=1.0) |
| 76 | + return aggregated / degree |
| 77 | + |
| 78 | + |
| 79 | +class GCNTransFormFunction(TransFormFunction): |
| 80 | + |
| 81 | + def __init__(self): |
| 82 | + super(GCNTransFormFunction, self).__init__(input_size=1) |
| 83 | + if torch.cuda.is_available(): |
| 84 | + self.device = torch.device("cuda") |
| 85 | + else: |
| 86 | + self.device = torch.device("cpu") |
| 87 | + self.hidden_dim = 32 |
| 88 | + self.embedding_dim = 16 |
| 89 | + self.num_classes = 2 |
| 90 | + self.model = None |
| 91 | + self.model_path = os.path.join(os.getcwd(), "model.pt") |
| 92 | + self.model_loaded = False |
| 93 | + self.load_model(self.model_path) |
| 94 | + |
| 95 | + def load_model(self, model_path=None): |
| 96 | + if model_path is None: |
| 97 | + model_path = self.model_path |
| 98 | + if not os.path.exists(model_path): |
| 99 | + self.model = None |
| 100 | + self.model_loaded = False |
| 101 | + return |
| 102 | + checkpoint = torch.load(model_path, map_location=self.device) |
| 103 | + if isinstance(checkpoint, nn.Module): |
| 104 | + self.model = checkpoint.to(self.device) |
| 105 | + self.model.eval() |
| 106 | + self.model_loaded = True |
| 107 | + return |
| 108 | + if not isinstance(checkpoint, dict): |
| 109 | + raise ValueError("Unsupported model checkpoint type: {}".format(type(checkpoint))) |
| 110 | + state_dict = checkpoint.get("state_dict", checkpoint) |
| 111 | + input_dim = int(checkpoint.get("input_dim", state_dict["input_proj.weight"].shape[1])) |
| 112 | + hidden_dim = int(checkpoint.get("hidden_dim", state_dict["input_proj.weight"].shape[0])) |
| 113 | + embedding_dim = int(checkpoint.get("embedding_dim", state_dict["embedding_proj.weight"].shape[0])) |
| 114 | + num_classes = int(checkpoint.get("num_classes", state_dict["classifier.weight"].shape[0])) |
| 115 | + self.model = EmptyGCNModel(input_dim, hidden_dim, embedding_dim, num_classes).to(self.device) |
| 116 | + self.model.load_state_dict(state_dict) |
| 117 | + self.model.eval() |
| 118 | + self.model_loaded = True |
| 119 | + |
| 120 | + def transform_pre(self, *args): |
| 121 | + payload = args[0] |
| 122 | + center_node_id = self._read_value(payload, ["center_node_id", "centerNodeId"], |
| 123 | + ["getCenter_node_id", "getCenterNodeId"]) |
| 124 | + sampled_nodes = self._read_value(payload, ["sampled_nodes", "sampledNodes"], |
| 125 | + ["getSampled_nodes", "getSampledNodes"]) |
| 126 | + node_features = self._read_value(payload, ["node_features", "nodeFeatures"], |
| 127 | + ["getNode_features", "getNodeFeatures"]) |
| 128 | + edge_index = self._read_value(payload, ["edge_index", "edgeIndex"], |
| 129 | + ["getEdge_index", "getEdgeIndex"]) |
| 130 | + |
| 131 | + feature_tensor = self._build_feature_tensor(node_features) |
| 132 | + edge_tensor = self._build_edge_tensor(edge_index, feature_tensor.size(0)) |
| 133 | + self._ensure_model(feature_tensor.size(1)) |
| 134 | + |
| 135 | + with torch.no_grad(): |
| 136 | + embedding_matrix, logits = self.model(feature_tensor, edge_tensor) |
| 137 | + |
| 138 | + center_index = self._find_center_index(sampled_nodes, center_node_id) |
| 139 | + center_embedding = embedding_matrix[center_index] |
| 140 | + center_logits = logits[center_index] |
| 141 | + probs = F.softmax(center_logits, dim=0) |
| 142 | + predicted_class = int(torch.argmax(probs).item()) |
| 143 | + confidence = float(torch.max(probs).item()) |
| 144 | + |
| 145 | + result = { |
| 146 | + "node_id": center_node_id, |
| 147 | + "embedding": center_embedding.cpu().tolist(), |
| 148 | + "predicted_class": predicted_class, |
| 149 | + "confidence": confidence |
| 150 | + } |
| 151 | + return result, center_node_id |
| 152 | + |
| 153 | + def transform_post(self, *args): |
| 154 | + if len(args) == 0: |
| 155 | + return None |
| 156 | + return args[0] |
| 157 | + |
| 158 | + def _ensure_model(self, input_dim): |
| 159 | + if self.model is not None and self.model.input_proj.in_features == input_dim: |
| 160 | + return |
| 161 | + torch.manual_seed(20260322) |
| 162 | + self.model = EmptyGCNModel(input_dim, self.hidden_dim, self.embedding_dim, |
| 163 | + self.num_classes).to(self.device) |
| 164 | + self.model.eval() |
| 165 | + |
| 166 | + def _build_feature_tensor(self, node_features): |
| 167 | + if node_features is None or len(node_features) == 0: |
| 168 | + return torch.zeros((1, 1), dtype=torch.float32, device=self.device) |
| 169 | + rows = [] |
| 170 | + for feature_row in node_features: |
| 171 | + if feature_row is None: |
| 172 | + rows.append([0.0]) |
| 173 | + continue |
| 174 | + row = [] |
| 175 | + for value in feature_row: |
| 176 | + row.append(float(0.0 if value is None else value)) |
| 177 | + if len(row) == 0: |
| 178 | + row.append(0.0) |
| 179 | + rows.append(row) |
| 180 | + max_dim = 1 |
| 181 | + for row in rows: |
| 182 | + if len(row) > max_dim: |
| 183 | + max_dim = len(row) |
| 184 | + normalized_rows = [] |
| 185 | + for row in rows: |
| 186 | + if len(row) < max_dim: |
| 187 | + row = row + [0.0] * (max_dim - len(row)) |
| 188 | + normalized_rows.append(row) |
| 189 | + return torch.tensor(normalized_rows, dtype=torch.float32, device=self.device) |
| 190 | + |
| 191 | + def _build_edge_tensor(self, edge_index, num_nodes): |
| 192 | + if edge_index is None: |
| 193 | + return torch.zeros((2, 0), dtype=torch.long, device=self.device) |
| 194 | + if len(edge_index) < 2: |
| 195 | + return torch.zeros((2, 0), dtype=torch.long, device=self.device) |
| 196 | + src_list = list(edge_index[0]) |
| 197 | + dst_list = list(edge_index[1]) |
| 198 | + edges = [] |
| 199 | + for idx in range(min(len(src_list), len(dst_list))): |
| 200 | + src = int(src_list[idx]) |
| 201 | + dst = int(dst_list[idx]) |
| 202 | + if src < 0 or dst < 0: |
| 203 | + continue |
| 204 | + if src >= num_nodes or dst >= num_nodes: |
| 205 | + continue |
| 206 | + edges.append([src, dst]) |
| 207 | + if len(edges) == 0: |
| 208 | + return torch.zeros((2, 0), dtype=torch.long, device=self.device) |
| 209 | + edge_tensor = torch.tensor(edges, dtype=torch.long, device=self.device) |
| 210 | + return edge_tensor.t().contiguous() |
| 211 | + |
| 212 | + def _find_center_index(self, sampled_nodes, center_node_id): |
| 213 | + if sampled_nodes is None or len(sampled_nodes) == 0: |
| 214 | + return 0 |
| 215 | + for idx in range(len(sampled_nodes)): |
| 216 | + if sampled_nodes[idx] == center_node_id: |
| 217 | + return idx |
| 218 | + return 0 |
| 219 | + |
| 220 | + def _read_value(self, obj, attr_names, method_names): |
| 221 | + if isinstance(obj, dict): |
| 222 | + for name in attr_names: |
| 223 | + if name in obj: |
| 224 | + return obj[name] |
| 225 | + for name in attr_names: |
| 226 | + if hasattr(obj, name): |
| 227 | + return getattr(obj, name) |
| 228 | + for name in method_names: |
| 229 | + if hasattr(obj, name): |
| 230 | + method = getattr(obj, name) |
| 231 | + if callable(method): |
| 232 | + return method() |
| 233 | + return None |
0 commit comments