Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| import matplotlib | |
| matplotlib.use('Agg') | |
| import matplotlib.pyplot as plt | |
| from matplotlib.patches import Circle | |
| class SechGraphConv(nn.Module): | |
| def __init__(self, in_ch, out_ch): | |
| super().__init__() | |
| self.linear = nn.Linear(in_ch, out_ch) | |
| self.inv_e = 1.0 / np.e | |
| def forward(self, x, edge_index, edge_weight=None): | |
| N = x.size(0) | |
| if edge_weight is None: | |
| edge_weight = torch.ones(edge_index.size(1)) | |
| sech_w = 1.0 / torch.cosh(edge_weight) | |
| row, col = edge_index | |
| out = torch.zeros_like(x) | |
| for i in range(N): | |
| mask = row == i | |
| if mask.sum() > 0: | |
| out[i] = (x[col[mask]] * sech_w[mask].view(-1,1)).sum(0) | |
| deg = torch.zeros(N) | |
| for i in range(N): | |
| deg[i] = (row == i).sum().float() | |
| deg_inv_sqrt = deg.pow(-0.5) | |
| deg_inv_sqrt[deg_inv_sqrt == float('inf')] = 0 | |
| out = deg_inv_sqrt.view(-1,1) * out | |
| out = self.linear(out) | |
| return out * (1.0 / torch.cosh(self.inv_e * out)) | |
| class SentinelGNN(nn.Module): | |
| def __init__(self, in_ch, hidden, out_ch, layers=2): | |
| super().__init__() | |
| self.convs = nn.ModuleList() | |
| self.convs.append(SechGraphConv(in_ch, hidden)) | |
| for _ in range(layers-2): | |
| self.convs.append(SechGraphConv(hidden, hidden)) | |
| self.convs.append(SechGraphConv(hidden, out_ch)) | |
| def forward(self, x, edge_index, ew=None): | |
| for i, conv in enumerate(self.convs): | |
| x = conv(x, edge_index, ew) | |
| return x | |
| def generate_graph(n_nodes, p_edge, n_features, n_classes): | |
| """Generate random graph.""" | |
| x = torch.randn(n_nodes, n_features) | |
| edge_list = [] | |
| for i in range(n_nodes): | |
| for j in range(i+1, n_nodes): | |
| if np.random.rand() < p_edge: | |
| edge_list.append([i, j]) | |
| edge_list.append([j, i]) | |
| edge_index = torch.tensor(edge_list, dtype=torch.long).t() if edge_list else torch.zeros(2,0, dtype=torch.long) | |
| y = torch.randint(0, n_classes, (n_nodes,)) | |
| return x, edge_index, y | |
| def run_gnn(n_nodes, p_edge, n_features, n_classes, n_layers, hidden): | |
| """Run Sentinel GNN.""" | |
| x, edge_index, y = generate_graph(n_nodes, p_edge, n_features, n_classes) | |
| model = SentinelGNN(n_features, hidden, n_classes, n_layers) | |
| out = model(x, edge_index) | |
| pred = out.argmax(dim=1) | |
| # Visualize | |
| fig, axes = plt.subplots(1, 2, figsize=(14, 5)) | |
| # Graph layout (simple spring) | |
| pos = np.random.randn(n_nodes, 2) * 2 | |
| for _ in range(50): | |
| forces = np.zeros_like(pos) | |
| for i in range(n_nodes): | |
| for j in range(n_nodes): | |
| if i != j: | |
| diff = pos[i] - pos[j] | |
| dist = np.linalg.norm(diff) + 1e-8 | |
| if dist < 3: | |
| forces[i] += diff / dist * (3 - dist) * 0.1 | |
| pos += forces | |
| # Draw graph | |
| ax = axes[0] | |
| for i in range(edge_index.size(1)): | |
| u, v = edge_index[0, i].item(), edge_index[1, i].item() | |
| if u < v: | |
| ax.plot([pos[u,0], pos[v,0]], [pos[u,1], pos[v,1]], 'gray', alpha=0.3, linewidth=0.5) | |
| colors = plt.cm.tab10(np.linspace(0, 1, n_classes)) | |
| for c in range(n_classes): | |
| mask = y.numpy() == c | |
| ax.scatter(pos[mask,0], pos[mask,1], c=[colors[c]], s=100, label=f'Class {c}', edgecolors='black') | |
| ax.set_title(f'Graph (N={n_nodes}, E={edge_index.size(1)//2})') | |
| ax.legend() | |
| ax.set_aspect('equal') | |
| ax.axis('off') | |
| # Embedding visualization | |
| ax = axes[1] | |
| emb = out.detach().numpy() | |
| for c in range(n_classes): | |
| mask = y.numpy() == c | |
| if mask.sum() > 0: | |
| ax.scatter(emb[mask, 0], emb[mask, 1] if emb.shape[1] > 1 else emb[mask, 0] * 0, | |
| c=[colors[c]], s=100, label=f'Class {c}', edgecolors='black', alpha=0.7) | |
| ax.set_title('Sentinel GNN Embeddings') | |
| ax.legend() | |
| ax.grid(True, alpha=0.3) | |
| plt.tight_layout() | |
| plt.savefig('/tmp/gnn_viz.png', dpi=150) | |
| plt.close() | |
| stats = f""" | |
| ## Sentinel GNN Results | |
| | Property | Value | | |
| |----------|-------| | |
| | Nodes | {n_nodes} | | |
| | Edges | {edge_index.size(1)//2} | | |
| | Features | {n_features} | | |
| | Classes | {n_classes} | | |
| | Layers | {n_layers} | | |
| | Hidden | {hidden} | | |
| | Parameters | {sum(p.numel() for p in model.parameters()):,} | | |
| ### Key Innovation | |
| **Hyperbolic message passing**: sech(‖x−y‖) is the natural distance kernel | |
| for hyperbolic geometry — matching brain connectome structure. | |
| """ | |
| return '/tmp/gnn_viz.png', stats | |
| with gr.Blocks(title="Sentinel Graph Neural Network") as demo: | |
| gr.Markdown(""" | |
| # 🌐 Sentinel Graph Neural Network | |
| **Hyperbolic message passing with sech kernel.** | |
| Brain connectomes and social networks have hyperbolic structure. | |
| The sech kernel is the natural distance function in hyperbolic space. | |
| """) | |
| with gr.Row(): | |
| with gr.Column(): | |
| n_nodes = gr.Slider(10, 500, value=100, step=10, label="Nodes") | |
| p_edge = gr.Slider(0.01, 0.5, value=0.05, label="Edge Probability") | |
| n_features = gr.Slider(2, 64, value=8, step=2, label="Node Features") | |
| n_classes = gr.Slider(2, 10, value=3, step=1, label="Classes") | |
| n_layers = gr.Slider(1, 5, value=2, step=1, label="GNN Layers") | |
| hidden = gr.Slider(8, 128, value=32, step=8, label="Hidden Dim") | |
| with gr.Column(): | |
| btn = gr.Button("Generate & Run", variant="primary") | |
| output_img = gr.Image() | |
| output_stats = gr.Markdown() | |
| btn.click(run_gnn, [n_nodes, p_edge, n_features, n_classes, n_layers, hidden], [output_img, output_stats]) | |
| gr.Markdown(""" | |
| ## About Sentinel GNN | |
| - **Message kernel**: sech(‖x−y‖) (hyperbolic geometry) | |
| - **Activation**: σ(x) = x·sech(x/e) (theorem-backed gradient) | |
| - **Applications**: Brain connectomes, social networks, molecules, knowledge graphs | |
| [Model Repo](https://huggingface.co/5dimension/sentinel-graph-neural-network) | |
| """) | |
| if __name__ == "__main__": | |
| demo.launch() | |