Skip to content

Commit ea97c17

Browse files
committed
feat: Add ML graph classification with PyTorch model integration and separate Docker services
1 parent ac7ab22 commit ea97c17

26 files changed

Lines changed: 972 additions & 44 deletions

.dockerignore.backend

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
node_modules
2+
npm-debug.log
3+
build
4+
.git
5+
.gitignore
6+
README.md
7+
.env
8+
.nyc_output
9+
coverage
10+
.nyc_output
11+
.coverage
12+
.pytest_cache
13+
__pycache__
14+
*.pyc
15+
*.pyo
16+
*.pyd
17+
.Python
18+
env
19+
pip-log.txt
20+
pip-delete-this-directory.txt
21+
.tox
22+
.coverage
23+
.coverage.*
24+
.cache
25+
nosetests.xml
26+
coverage.xml
27+
*.cover
28+
*.log
29+
.git
30+
.mypy_cache
31+
.pytest_cache
32+
.hypothesis
33+
src/
34+
public/
35+
package.json
36+
package-lock.json

.github/workflows/ci-cd.yml

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77
branches: [ main ]
88

99
jobs:
10-
test:
10+
test-frontend:
1111
runs-on: ubuntu-latest
1212
steps:
1313
- uses: actions/checkout@v4
@@ -22,13 +22,32 @@ jobs:
2222
run: npm ci
2323

2424
- name: Run tests
25-
run: npm test -- --coverage --watchAll=false --passWithNoTests --passWithNoTests
25+
run: npm test -- --coverage --watchAll=false --passWithNoTests
2626

2727
- name: Run linting
2828
run: npm run lint || true
2929

30-
build:
31-
needs: test
30+
test-backend:
31+
runs-on: ubuntu-latest
32+
steps:
33+
- uses: actions/checkout@v4
34+
35+
- name: Setup Python
36+
uses: actions/setup-python@v4
37+
with:
38+
python-version: '3.11'
39+
40+
- name: Install Python dependencies
41+
run: |
42+
pip install --upgrade pip
43+
pip install -r requirements.txt
44+
45+
- name: Test backend
46+
run: |
47+
python -c "import simple_classifier; print('Backend imports successful')"
48+
49+
build-frontend:
50+
needs: test-frontend
3251
runs-on: ubuntu-latest
3352
steps:
3453
- uses: actions/checkout@v4
@@ -51,8 +70,34 @@ jobs:
5170
name: build-files
5271
path: build/
5372

54-
docker:
55-
needs: build
73+
docker-frontend:
74+
needs: build-frontend
75+
runs-on: ubuntu-latest
76+
if: github.ref == 'refs/heads/main'
77+
steps:
78+
- uses: actions/checkout@v4
79+
80+
- name: Set up Docker Buildx
81+
uses: docker/setup-buildx-action@v3
82+
83+
- name: Login to Docker Hub
84+
uses: docker/login-action@v3
85+
with:
86+
username: ${{ secrets.DOCKER_HUB_USERNAME }}
87+
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
88+
89+
- name: Build and push Frontend Docker image
90+
uses: docker/build-push-action@v5
91+
with:
92+
context: .
93+
file: ./Dockerfile
94+
push: true
95+
tags: |
96+
${{ secrets.DOCKER_HUB_USERNAME }}/algo-visualize-frontend:latest
97+
${{ secrets.DOCKER_HUB_USERNAME }}/algo-visualize-frontend:${{ github.sha }}
98+
99+
docker-backend:
100+
needs: test-backend
56101
runs-on: ubuntu-latest
57102
if: github.ref == 'refs/heads/main'
58103
steps:
@@ -67,21 +112,23 @@ jobs:
67112
username: ${{ secrets.DOCKER_HUB_USERNAME }}
68113
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
69114

70-
- name: Build and push Docker image
115+
- name: Build and push Backend Docker image
71116
uses: docker/build-push-action@v5
72117
with:
73118
context: .
119+
file: ./Dockerfile.backend
74120
push: true
75121
tags: |
76-
${{ secrets.DOCKER_HUB_USERNAME }}/algo-visualize:latest
77-
${{ secrets.DOCKER_HUB_USERNAME }}/algo-visualize:${{ github.sha }}
122+
${{ secrets.DOCKER_HUB_USERNAME }}/algo-visualize-backend:latest
123+
${{ secrets.DOCKER_HUB_USERNAME }}/algo-visualize-backend:${{ github.sha }}
78124
79125
deploy:
80-
needs: docker
126+
needs: [docker-frontend, docker-backend]
81127
runs-on: ubuntu-latest
82128
if: github.ref == 'refs/heads/main'
83129
steps:
84130
- name: Deploy to production
85131
run: |
86132
echo "Deployment step - configure based on your hosting platform"
87-
echo "Docker image: ${{ secrets.DOCKER_HUB_USERNAME }}/algo-visualize:${{ github.sha }}"
133+
echo "Frontend image: ${{ secrets.DOCKER_HUB_USERNAME }}/algo-visualize-frontend:${{ github.sha }}"
134+
echo "Backend image: ${{ secrets.DOCKER_HUB_USERNAME }}/algo-visualize-backend:${{ github.sha }}"

Dockerfile.backend

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
FROM python:3.11-slim
2+
3+
WORKDIR /app
4+
5+
# Install system dependencies
6+
RUN apt-get update && apt-get install -y \
7+
gcc \
8+
g++ \
9+
&& rm -rf /var/lib/apt/lists/*
10+
11+
# Copy requirements and install Python dependencies
12+
COPY requirements.txt .
13+
RUN pip install --no-cache-dir -r requirements.txt
14+
15+
# Copy backend files
16+
COPY graph_generator.py .
17+
COPY simple_classifier.py .
18+
COPY graph_classifier_model.pth .
19+
20+
# Expose port
21+
EXPOSE 5000
22+
23+
# Health check
24+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
25+
CMD python -c "import requests; requests.get('http://localhost:5000/health')" || exit 1
26+
27+
# Run the application
28+
CMD ["python", "graph_generator.py"]

ML_INTEGRATION.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# ML Model Integration Guide
2+
3+
## Overview
4+
Your PyTorch graph classification model (`graph_classifier_model.pth`) has been integrated into the React application.
5+
6+
## Quick Start
7+
8+
### Option 1: Use the startup script
9+
```bash
10+
start_app.bat
11+
```
12+
13+
### Option 2: Manual startup
14+
1. **Start Backend:**
15+
```bash
16+
python graph_generator.py
17+
```
18+
19+
2. **Start Frontend:**
20+
```bash
21+
npm start
22+
```
23+
24+
## How to Use
25+
26+
1. **Enter Graph Data:** Input edges in the format `u v` (one per line)
27+
2. **Load Graph:** Click "Load Graph" to visualize
28+
3. **Classify Graph:** Click "Classify Graph" to get ML prediction
29+
30+
## Model Integration Details
31+
32+
- **Model File:** `graph_classifier_model.pth` (your PyTorch model)
33+
- **Backend:** Flask server on port 5000
34+
- **Frontend:** React app on port 3000
35+
- **Classification Types:** Tree, Cycle, DAG
36+
37+
## API Endpoints
38+
39+
- `POST /classify` - Classify graph structure
40+
- `POST /generate_graph` - Generate graph visualization
41+
- `GET /health` - Health check
42+
43+
## Testing
44+
45+
Test the backend:
46+
```bash
47+
python test_backend.py
48+
```
49+
50+
## Files Added/Modified
51+
52+
### New Files:
53+
- `simple_classifier.py` - ML model wrapper
54+
- `ModelPrediction.jsx` - Prediction display component
55+
- `start_app.bat` - Startup script
56+
- `test_backend.py` - Backend testing
57+
58+
### Modified Files:
59+
- `GraphInput.jsx` - Added classification button
60+
- `graph_generator.py` - Added classification endpoint
61+
- `App.jsx` - Updated title
62+
63+
## Troubleshooting
64+
65+
1. **Backend not starting:** Check if port 5000 is available
66+
2. **Model not loading:** Ensure `graph_classifier_model.pth` exists
67+
3. **Frontend errors:** Check if backend is running first
68+
69+
## Next Steps
70+
71+
To improve accuracy, you can:
72+
1. Enhance feature extraction in `simple_classifier.py`
73+
2. Add proper PyTorch model inference
74+
3. Train the model with more graph features
3.68 KB
Binary file not shown.

build-backend.bat

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
@echo off
2+
echo Building Backend Docker Image
3+
echo ============================
4+
5+
docker build -f Dockerfile.backend -t algo-visualize-backend:latest .
6+
7+
echo Backend image built successfully!
8+
echo Run with: docker run -p 5000:5000 algo-visualize-backend:latest

convert_model.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import torch
2+
import torch.nn as nn
3+
import tensorflow as tf
4+
import numpy as np
5+
import networkx as nx
6+
7+
# Simple feature extraction for graph classification
8+
def extract_graph_features(edges):
9+
"""Extract basic graph features from edge list"""
10+
if not edges:
11+
return np.zeros(64) # Return zero vector if no edges
12+
13+
# Create graph
14+
G = nx.Graph()
15+
for edge in edges:
16+
if len(edge) >= 2:
17+
G.add_edge(str(edge[0]), str(edge[1]))
18+
19+
if G.number_of_nodes() == 0:
20+
return np.zeros(64)
21+
22+
# Basic graph features
23+
features = []
24+
25+
# Node count
26+
features.append(G.number_of_nodes())
27+
# Edge count
28+
features.append(G.number_of_edges())
29+
# Density
30+
features.append(nx.density(G))
31+
# Average clustering
32+
features.append(nx.average_clustering(G))
33+
# Number of connected components
34+
features.append(nx.number_connected_components(G))
35+
36+
# Degree statistics
37+
degrees = [d for n, d in G.degree()]
38+
if degrees:
39+
features.extend([
40+
np.mean(degrees),
41+
np.std(degrees),
42+
np.max(degrees),
43+
np.min(degrees)
44+
])
45+
else:
46+
features.extend([0, 0, 0, 0])
47+
48+
# Pad or truncate to 64 features
49+
features = features[:64]
50+
while len(features) < 64:
51+
features.append(0.0)
52+
53+
return np.array(features, dtype=np.float32)
54+
55+
def convert_pth_to_h5(pth_path, h5_path):
56+
# Load PyTorch model state dict
57+
state_dict = torch.load(pth_path, map_location='cpu')
58+
59+
# Create TensorFlow model that mimics the GCN structure
60+
# Input: 64-dimensional graph features
61+
tf_model = tf.keras.Sequential([
62+
tf.keras.layers.Dense(64, activation='relu', input_shape=(64,), name='conv1'),
63+
tf.keras.layers.Dense(64, activation='relu', name='conv2'),
64+
tf.keras.layers.Dense(64, activation='relu', name='conv3'),
65+
tf.keras.layers.Dense(3, activation='softmax', name='classifier')
66+
])
67+
68+
# Build the model
69+
tf_model.build((None, 64))
70+
71+
# Extract and set weights from PyTorch model
72+
try:
73+
# Set conv1 weights (assuming it's the first GCN layer)
74+
conv1_weight = state_dict['conv1.lin.weight'].numpy().T # Transpose for TF
75+
conv1_bias = state_dict['conv1.bias'].numpy()
76+
tf_model.layers[0].set_weights([conv1_weight, conv1_bias])
77+
78+
# Set conv2 weights
79+
conv2_weight = state_dict['conv2.lin.weight'].numpy().T
80+
conv2_bias = state_dict['conv2.bias'].numpy()
81+
tf_model.layers[1].set_weights([conv2_weight, conv2_bias])
82+
83+
# Set conv3 weights
84+
conv3_weight = state_dict['conv3.lin.weight'].numpy().T
85+
conv3_bias = state_dict['conv3.bias'].numpy()
86+
tf_model.layers[2].set_weights([conv3_weight, conv3_bias])
87+
88+
# Set final linear layer weights
89+
lin_weight = state_dict['lin.weight'].numpy().T
90+
lin_bias = state_dict['lin.bias'].numpy()
91+
tf_model.layers[3].set_weights([lin_weight, lin_bias])
92+
93+
print("Successfully transferred weights from PyTorch to TensorFlow")
94+
95+
except Exception as e:
96+
print(f"Warning: Could not transfer all weights: {e}")
97+
print("Using randomly initialized weights")
98+
99+
# Save TensorFlow model
100+
tf_model.save(h5_path)
101+
print(f"Model converted and saved to {h5_path}")
102+
103+
if __name__ == "__main__":
104+
pth_file = 'graph_classifier_model.pth'
105+
print(f"Converting {pth_file} to TensorFlow format...")
106+
convert_pth_to_h5(pth_file, 'model.h5')
107+
print("Conversion complete! You can now start the backend.")

0 commit comments

Comments
 (0)