Spaces:
Sleeping
Sleeping
fix: SHAP explain - torch import, reduce samples, fix visualizer scale
Browse files- explain/shap_wrapper.py +10 -14
- explain/visualizer.py +7 -1
- routes/explain.py +9 -5
explain/shap_wrapper.py
CHANGED
|
@@ -12,21 +12,17 @@ class GNNFeatureWrapper:
|
|
| 12 |
|
| 13 |
def __call__(self, binary_coalitions: np.ndarray) -> np.ndarray:
|
| 14 |
predictions = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
with torch.no_grad():
|
| 16 |
for coalition in binary_coalitions:
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
).
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
logits = self.model(
|
| 24 |
-
x_perturbed,
|
| 25 |
-
self.data_obj.edge_index.to(self.device),
|
| 26 |
-
self.data_obj.edge_attr.to(self.device),
|
| 27 |
-
torch.zeros(x_perturbed.shape[0], dtype=torch.long, device=self.device),
|
| 28 |
-
)
|
| 29 |
-
probs = torch.sigmoid(logits)
|
| 30 |
-
predictions.append(probs[0, self.target_task_idx].cpu().item())
|
| 31 |
|
| 32 |
return np.array(predictions)
|
|
|
|
| 12 |
|
| 13 |
def __call__(self, binary_coalitions: np.ndarray) -> np.ndarray:
|
| 14 |
predictions = []
|
| 15 |
+
x = self.data_obj.x.to(self.device)
|
| 16 |
+
edge_index = self.data_obj.edge_index.to(self.device)
|
| 17 |
+
edge_attr = self.data_obj.edge_attr.to(self.device)
|
| 18 |
+
batch = torch.zeros(x.shape[0], dtype=torch.long, device=self.device)
|
| 19 |
+
|
| 20 |
with torch.no_grad():
|
| 21 |
for coalition in binary_coalitions:
|
| 22 |
+
mask = torch.tensor(coalition, dtype=torch.float32, device=self.device).unsqueeze(1)
|
| 23 |
+
x_perturbed = x * mask
|
| 24 |
+
logits = self.model(x_perturbed, edge_index, edge_attr, batch)
|
| 25 |
+
prob = torch.sigmoid(logits[0, self.target_task_idx]).cpu().item()
|
| 26 |
+
predictions.append(prob)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
return np.array(predictions)
|
explain/visualizer.py
CHANGED
|
@@ -13,12 +13,18 @@ def generate_similarity_map(smiles: str, attributions: list, target_assay: str)
|
|
| 13 |
if mol is None:
|
| 14 |
raise ValueError(f"Invalid SMILES: {smiles}")
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
fig, ax = plt.subplots(figsize=(8, 6))
|
| 17 |
SimilarityMaps.GetSimilarityMapFromWeights(
|
| 18 |
mol,
|
| 19 |
attributions,
|
| 20 |
colorMap='RdYlBu_r',
|
| 21 |
-
scale=
|
| 22 |
alpha=0.45,
|
| 23 |
)
|
| 24 |
ax.set_title(f"SHAP Attributions — {target_assay}", fontsize=14, fontweight="bold")
|
|
|
|
| 13 |
if mol is None:
|
| 14 |
raise ValueError(f"Invalid SMILES: {smiles}")
|
| 15 |
|
| 16 |
+
if mol.GetNumAtoms() != len(attributions):
|
| 17 |
+
raise ValueError(
|
| 18 |
+
f"Atom count mismatch: mol has {mol.GetNumAtoms()} atoms, "
|
| 19 |
+
f"but {len(attributions)} attributions provided"
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
fig, ax = plt.subplots(figsize=(8, 6))
|
| 23 |
SimilarityMaps.GetSimilarityMapFromWeights(
|
| 24 |
mol,
|
| 25 |
attributions,
|
| 26 |
colorMap='RdYlBu_r',
|
| 27 |
+
scale=1,
|
| 28 |
alpha=0.45,
|
| 29 |
)
|
| 30 |
ax.set_title(f"SHAP Attributions — {target_assay}", fontsize=14, fontweight="bold")
|
routes/explain.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import numpy as np
|
|
|
|
| 2 |
from fastapi import APIRouter, Header, HTTPException
|
| 3 |
from pydantic import BaseModel
|
| 4 |
from firebase_auth import verify_token
|
|
@@ -43,20 +44,23 @@ async def explain(request: ExplainRequest, authorization: str = Header(...)):
|
|
| 43 |
raise HTTPException(status_code=400, detail=str(e))
|
| 44 |
|
| 45 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 46 |
-
import torch
|
| 47 |
data = data.to(device)
|
| 48 |
|
| 49 |
num_nodes = data.x.shape[0]
|
| 50 |
predict_fn = GNNFeatureWrapper(model, data, request.target_task, device)
|
| 51 |
-
|
|
|
|
|
|
|
| 52 |
active_state = np.ones((1, num_nodes))
|
| 53 |
|
| 54 |
import shap
|
| 55 |
explainer = shap.KernelExplainer(predict_fn, background_reference)
|
| 56 |
-
|
|
|
|
| 57 |
|
| 58 |
-
attributions = shap_values[0].
|
| 59 |
-
|
|
|
|
| 60 |
|
| 61 |
try:
|
| 62 |
vis_base64 = generate_similarity_map(
|
|
|
|
| 1 |
import numpy as np
|
| 2 |
+
import torch
|
| 3 |
from fastapi import APIRouter, Header, HTTPException
|
| 4 |
from pydantic import BaseModel
|
| 5 |
from firebase_auth import verify_token
|
|
|
|
| 44 |
raise HTTPException(status_code=400, detail=str(e))
|
| 45 |
|
| 46 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
|
|
| 47 |
data = data.to(device)
|
| 48 |
|
| 49 |
num_nodes = data.x.shape[0]
|
| 50 |
predict_fn = GNNFeatureWrapper(model, data, request.target_task, device)
|
| 51 |
+
|
| 52 |
+
random_states = np.random.randint(0, 2, size=(20, num_nodes))
|
| 53 |
+
background_reference = np.vstack([np.zeros((1, num_nodes)), random_states])
|
| 54 |
active_state = np.ones((1, num_nodes))
|
| 55 |
|
| 56 |
import shap
|
| 57 |
explainer = shap.KernelExplainer(predict_fn, background_reference)
|
| 58 |
+
nsamples = min(500, 2 ** num_nodes)
|
| 59 |
+
shap_values = explainer.shap_values(active_state, nsamples=nsamples)
|
| 60 |
|
| 61 |
+
attributions = np.array(shap_values[0]) if isinstance(shap_values, list) else np.array(shap_values)
|
| 62 |
+
attributions = attributions.flatten().tolist()
|
| 63 |
+
high_impact = np.where(np.abs(np.array(attributions)) > 0.05)[0].tolist()
|
| 64 |
|
| 65 |
try:
|
| 66 |
vis_base64 = generate_similarity_map(
|