Skip to content

Commit 493831e

Browse files
committed
feat: add GCN transformation functionality with integration tests and required resources
1 parent e5c863f commit 493831e

7 files changed

Lines changed: 723 additions & 0 deletions

File tree

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
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
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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+
torch

0 commit comments

Comments
 (0)