Commit ·
d4fdb7e
1
Parent(s): b0dd450
Run real model: load 53MB int8 TorchScript, drop SVD fallback, slim deps
Browse files- app.py +63 -145
- embeddings_train.csv +0 -3
- label_mapping.json +44 -0
- requirements.txt +0 -2
- test_clin.csv +0 -0
- train_clin.csv +0 -0
- embeddings_test.csv → vae_tissue_int8.torchscript.pt +2 -2
app.py
CHANGED
|
@@ -1,32 +1,28 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
-
Flexynesis Tissue VAE
|
| 4 |
-
======================================================
|
| 5 |
-
Upload a
|
|
|
|
| 6 |
|
| 7 |
-
|
| 8 |
-
(
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
(Hugging Face free tier), the app runs in demo mode: uploaded samples are
|
| 12 |
-
projected with TruncatedSVD and classified by kNN against the pre-computed
|
| 13 |
-
118K reference embeddings. When the .pth IS present (local), the trained VAE
|
| 14 |
-
encoder is used for exact inference.
|
| 15 |
|
| 16 |
Author: Amit Pande, MDC Berlin/BIMSB
|
| 17 |
"""
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
-
import streamlit as st
|
| 20 |
-
import pandas as pd
|
| 21 |
import numpy as np
|
|
|
|
|
|
|
|
|
|
| 22 |
import joblib
|
| 23 |
-
from pathlib import Path
|
| 24 |
-
from sklearn.neighbors import KNeighborsClassifier
|
| 25 |
-
from collections import Counter
|
| 26 |
|
| 27 |
MODEL_DIR = Path(".")
|
| 28 |
-
K = 5
|
| 29 |
-
LATENT_DIM = 121
|
| 30 |
|
| 31 |
st.set_page_config(page_title="Flexynesis Tissue VAE", page_icon="🧬", layout="wide")
|
| 32 |
st.title("🧬 Flexynesis Tissue VAE")
|
|
@@ -36,103 +32,39 @@ st.markdown(
|
|
| 36 |
"across **42 UBERON tissue categories** (94.9% balanced accuracy, 121-dim latent space)."
|
| 37 |
)
|
| 38 |
|
|
|
|
| 39 |
@st.cache_resource
|
| 40 |
-
def
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
all_emb = pd.concat([train_emb, test_emb])
|
| 51 |
-
all_clin = pd.concat([train_clin, test_clin])
|
| 52 |
-
idx = all_emb.index.intersection(all_clin.index)
|
| 53 |
-
ref_emb = all_emb.loc[idx]
|
| 54 |
-
ref_clin = all_clin.loc[idx]
|
| 55 |
-
mask = (ref_clin['uberon_tissue'].notna() &
|
| 56 |
-
~ref_clin['uberon_tissue'].isin(['unknown', 'other', 'unmapped', 'nan', '']))
|
| 57 |
-
ref_emb = ref_emb[mask]
|
| 58 |
-
ref_clin = ref_clin[mask]
|
| 59 |
-
|
| 60 |
-
knn = KNeighborsClassifier(n_neighbors=K, metric='cosine', n_jobs=-1)
|
| 61 |
-
knn.fit(ref_emb.values, ref_clin['uberon_tissue'].values)
|
| 62 |
-
|
| 63 |
-
model = None
|
| 64 |
-
pth = MODEL_DIR / "vae_tissue.final_model.pth"
|
| 65 |
-
if pth.exists():
|
| 66 |
-
try:
|
| 67 |
-
import torch
|
| 68 |
-
model = torch.load(pth, map_location='cpu', weights_only=False)
|
| 69 |
-
model.eval()
|
| 70 |
-
except Exception as ex:
|
| 71 |
-
st.sidebar.warning(f"Weights present but not loaded ({ex}); using kNN demo mode.")
|
| 72 |
-
model = None
|
| 73 |
-
|
| 74 |
-
return model, gene_list, scaler, knn, ref_emb, ref_clin
|
| 75 |
|
| 76 |
try:
|
| 77 |
-
model, gene_list, scaler,
|
| 78 |
-
mode_str = "Full VAE encoder + kNN" if model is not None else "kNN on pre-computed embeddings"
|
| 79 |
st.success(
|
| 80 |
-
f"✅ {len(gene_list):,} genes · "
|
| 81 |
-
f"{len(
|
| 82 |
-
f"{ref_clin['uberon_tissue'].nunique()} tissues · "
|
| 83 |
-
f"Mode: **{mode_str}**"
|
| 84 |
)
|
| 85 |
-
if model is None:
|
| 86 |
-
st.info(
|
| 87 |
-
"ℹ️ Running in **demo mode**. The 118K reference embeddings are used as-is; "
|
| 88 |
-
"uploaded samples are projected with TruncatedSVD and classified by kNN. "
|
| 89 |
-
"Full VAE encoding of new samples requires the model weights "
|
| 90 |
-
"(vae_tissue.final_model.pth), deposited at Zenodo and too large for the free tier."
|
| 91 |
-
)
|
| 92 |
except Exception as e:
|
| 93 |
-
st.error(f"
|
| 94 |
-
st.info("
|
|
|
|
| 95 |
st.stop()
|
| 96 |
|
|
|
|
| 97 |
def orient_matrix(df):
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
if
|
| 101 |
df = df.T
|
| 102 |
return df
|
| 103 |
|
| 104 |
-
def embed_and_classify(X_scaled):
|
| 105 |
-
if model is not None:
|
| 106 |
-
import torch
|
| 107 |
-
X_t = torch.tensor(X_scaled, dtype=torch.float32)
|
| 108 |
-
with torch.no_grad():
|
| 109 |
-
h = model.encoders[0](X_t)
|
| 110 |
-
mu = model.FC_mean(h[0])
|
| 111 |
-
logits = model.MLPs['uberon_tissue'](mu)
|
| 112 |
-
lmap = model.dataset.label_mappings['uberon_tissue']
|
| 113 |
-
n2i = {v: k for k, v in lmap.items()}
|
| 114 |
-
ni = n2i.get('nan', None)
|
| 115 |
-
if ni is not None:
|
| 116 |
-
logits[:, ni] = -1e9
|
| 117 |
-
probs = torch.softmax(logits, dim=1)
|
| 118 |
-
pred_idx = logits.argmax(dim=1).numpy()
|
| 119 |
-
emb = mu.numpy()
|
| 120 |
-
pred_labels = [lmap[int(i)] for i in pred_idx]
|
| 121 |
-
max_probs = probs.numpy().max(axis=1)
|
| 122 |
-
else:
|
| 123 |
-
from sklearn.decomposition import TruncatedSVD
|
| 124 |
-
n_comp = min(LATENT_DIM, X_scaled.shape[0] - 1, X_scaled.shape[1])
|
| 125 |
-
n_comp = max(n_comp, 1)
|
| 126 |
-
svd = TruncatedSVD(n_components=n_comp, random_state=42)
|
| 127 |
-
emb_reduced = svd.fit_transform(X_scaled)
|
| 128 |
-
if emb_reduced.shape[1] < LATENT_DIM:
|
| 129 |
-
pad = np.zeros((emb_reduced.shape[0], LATENT_DIM - emb_reduced.shape[1]))
|
| 130 |
-
emb = np.concatenate([emb_reduced, pad], axis=1)
|
| 131 |
-
else:
|
| 132 |
-
emb = emb_reduced[:, :LATENT_DIM]
|
| 133 |
-
pred_labels = knn_ref.predict(emb).tolist()
|
| 134 |
-
max_probs = np.full(len(pred_labels), np.nan)
|
| 135 |
-
return emb, pred_labels, max_probs
|
| 136 |
|
| 137 |
st.markdown("---")
|
| 138 |
col1, col2 = st.columns([2, 1])
|
|
@@ -150,64 +82,50 @@ with col2:
|
|
| 150 |
""")
|
| 151 |
|
| 152 |
if uploaded:
|
| 153 |
-
sep =
|
| 154 |
-
df
|
| 155 |
st.write(f"**Uploaded:** {df.shape[0]:,} × {df.shape[1]:,}")
|
| 156 |
st.dataframe(df.iloc[:5, :5], use_container_width=True)
|
| 157 |
|
| 158 |
if st.button("🚀 Classify Tissues", type="primary"):
|
| 159 |
-
with st.spinner("
|
| 160 |
-
df
|
| 161 |
overlap = len(set(df.columns) & set(gene_list))
|
| 162 |
-
st.write(f"Gene overlap: **{overlap:,}/{len(gene_list):,}**
|
|
|
|
| 163 |
if overlap < 1000:
|
| 164 |
st.warning("Low gene overlap — results may be unreliable.")
|
| 165 |
|
| 166 |
-
aligned
|
| 167 |
-
common
|
| 168 |
aligned[common] = df[common].values
|
| 169 |
-
aligned
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
|
|
|
|
|
|
| 179 |
|
| 180 |
st.markdown("---")
|
| 181 |
st.subheader("📊 Results")
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
'Confidence': conf_col,
|
| 187 |
-
'kNN Dist': distances.mean(axis=1).round(4),
|
| 188 |
-
'Sources': breakdowns,
|
| 189 |
})
|
| 190 |
st.dataframe(results, use_container_width=True, height=400)
|
| 191 |
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
st.subheader("Tissue Distribution")
|
| 195 |
-
st.bar_chart(pd.Series(pred_labels).value_counts())
|
| 196 |
-
with col2:
|
| 197 |
-
st.subheader("Confidence Distribution")
|
| 198 |
-
conf_vals = [p if not np.isnan(p) else 0 for p in max_probs]
|
| 199 |
-
st.bar_chart(pd.DataFrame({'Confidence': conf_vals}, index=df.index))
|
| 200 |
|
| 201 |
-
st.
|
| 202 |
-
|
| 203 |
-
with c1:
|
| 204 |
-
emb_df = pd.DataFrame(emb, index=df.index,
|
| 205 |
-
columns=[f"z{i}" for i in range(emb.shape[1])])
|
| 206 |
-
st.download_button("📥 Embeddings (CSV)", emb_df.to_csv(),
|
| 207 |
-
"flexynesis_embeddings.csv", "text/csv")
|
| 208 |
-
with c2:
|
| 209 |
-
st.download_button("📥 Classifications (CSV)", results.to_csv(index=False),
|
| 210 |
-
"flexynesis_classifications.csv", "text/csv")
|
| 211 |
|
| 212 |
st.markdown("---")
|
| 213 |
st.caption(
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
+
Flexynesis Tissue VAE — Web Application (compressed, real-model inference)
|
| 4 |
+
=========================================================================
|
| 5 |
+
Upload a bulk RNA-seq gene-expression matrix → UBERON tissue-of-origin
|
| 6 |
+
predictions + 121-dim latent embeddings.
|
| 7 |
|
| 8 |
+
Runs the trained supervised VAE via a 53 MB int8 TorchScript bundle
|
| 9 |
+
(vae_tissue_int8.torchscript.pt) — exact inference, no flexynesis dependency,
|
| 10 |
+
fits the Hugging Face free tier. Trained on 118,263 tissue-curated samples
|
| 11 |
+
(TCGA, GTEx, ARCHS4), 42 UBERON tissues, 94.9% balanced accuracy.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
Author: Amit Pande, MDC Berlin/BIMSB
|
| 14 |
"""
|
| 15 |
+
import json
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
from collections import Counter
|
| 18 |
|
|
|
|
|
|
|
| 19 |
import numpy as np
|
| 20 |
+
import pandas as pd
|
| 21 |
+
import streamlit as st
|
| 22 |
+
import torch
|
| 23 |
import joblib
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
MODEL_DIR = Path(".")
|
|
|
|
|
|
|
| 26 |
|
| 27 |
st.set_page_config(page_title="Flexynesis Tissue VAE", page_icon="🧬", layout="wide")
|
| 28 |
st.title("🧬 Flexynesis Tissue VAE")
|
|
|
|
| 32 |
"across **42 UBERON tissue categories** (94.9% balanced accuracy, 121-dim latent space)."
|
| 33 |
)
|
| 34 |
|
| 35 |
+
|
| 36 |
@st.cache_resource
|
| 37 |
+
def load_model():
|
| 38 |
+
model = torch.jit.load(str(MODEL_DIR / "vae_tissue_int8.torchscript.pt"))
|
| 39 |
+
model.eval()
|
| 40 |
+
art = joblib.load(MODEL_DIR / "vae_tissue.artifacts.joblib")
|
| 41 |
+
gene_list = list(art["feature_lists"]["gex"])
|
| 42 |
+
scaler = art["transforms"]["gex"]
|
| 43 |
+
label_mapping = {int(k): v for k, v in
|
| 44 |
+
json.loads((MODEL_DIR / "label_mapping.json").read_text()).items()}
|
| 45 |
+
return model, gene_list, scaler, label_mapping
|
| 46 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
try:
|
| 49 |
+
model, gene_list, scaler, label_mapping = load_model()
|
|
|
|
| 50 |
st.success(
|
| 51 |
+
f"✅ Model loaded · {len(gene_list):,} genes · "
|
| 52 |
+
f"{len(label_mapping)} tissue classes · exact VAE inference (int8 TorchScript)"
|
|
|
|
|
|
|
| 53 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
except Exception as e:
|
| 55 |
+
st.error(f"Model load error: {e}")
|
| 56 |
+
st.info("Ensure vae_tissue_int8.torchscript.pt, vae_tissue.artifacts.joblib, "
|
| 57 |
+
"and label_mapping.json are in the repository root.")
|
| 58 |
st.stop()
|
| 59 |
|
| 60 |
+
|
| 61 |
def orient_matrix(df):
|
| 62 |
+
in_cols = len(set(df.columns) & set(gene_list))
|
| 63 |
+
in_rows = len(set(df.index) & set(gene_list))
|
| 64 |
+
if in_rows > in_cols:
|
| 65 |
df = df.T
|
| 66 |
return df
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
st.markdown("---")
|
| 70 |
col1, col2 = st.columns([2, 1])
|
|
|
|
| 82 |
""")
|
| 83 |
|
| 84 |
if uploaded:
|
| 85 |
+
sep = "\t" if uploaded.name.endswith((".tsv", ".txt")) else ","
|
| 86 |
+
df = pd.read_csv(uploaded, index_col=0, sep=sep)
|
| 87 |
st.write(f"**Uploaded:** {df.shape[0]:,} × {df.shape[1]:,}")
|
| 88 |
st.dataframe(df.iloc[:5, :5], use_container_width=True)
|
| 89 |
|
| 90 |
if st.button("🚀 Classify Tissues", type="primary"):
|
| 91 |
+
with st.spinner("Running the VAE..."):
|
| 92 |
+
df = orient_matrix(df)
|
| 93 |
overlap = len(set(df.columns) & set(gene_list))
|
| 94 |
+
st.write(f"Gene overlap: **{overlap:,}/{len(gene_list):,}** "
|
| 95 |
+
f"({100*overlap/len(gene_list):.1f}%)")
|
| 96 |
if overlap < 1000:
|
| 97 |
st.warning("Low gene overlap — results may be unreliable.")
|
| 98 |
|
| 99 |
+
aligned = pd.DataFrame(0.0, index=df.index, columns=gene_list)
|
| 100 |
+
common = [g for g in gene_list if g in df.columns]
|
| 101 |
aligned[common] = df[common].values
|
| 102 |
+
aligned = aligned.fillna(0)
|
| 103 |
+
X = torch.tensor(scaler.transform(aligned.values), dtype=torch.float32)
|
| 104 |
+
|
| 105 |
+
with torch.no_grad():
|
| 106 |
+
logits = model(X)
|
| 107 |
+
nan_idx = {v: k for k, v in label_mapping.items()}.get("nan")
|
| 108 |
+
if nan_idx is not None:
|
| 109 |
+
logits[:, nan_idx] = -1e9
|
| 110 |
+
probs = torch.softmax(logits, dim=1)
|
| 111 |
+
pred_idx = logits.argmax(dim=1)
|
| 112 |
+
pred_labels = [label_mapping[int(i)] for i in pred_idx]
|
| 113 |
+
conf = probs.max(dim=1).values.numpy()
|
| 114 |
|
| 115 |
st.markdown("---")
|
| 116 |
st.subheader("📊 Results")
|
| 117 |
+
results = pd.DataFrame({
|
| 118 |
+
"Sample": df.index,
|
| 119 |
+
"Tissue": pred_labels,
|
| 120 |
+
"Confidence": [f"{c:.1%}" for c in conf],
|
|
|
|
|
|
|
|
|
|
| 121 |
})
|
| 122 |
st.dataframe(results, use_container_width=True, height=400)
|
| 123 |
|
| 124 |
+
st.subheader("Tissue Distribution")
|
| 125 |
+
st.bar_chart(pd.Series(pred_labels).value_counts())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
|
| 127 |
+
st.download_button("📥 Predictions (CSV)", results.to_csv(index=False),
|
| 128 |
+
"flexynesis_predictions.csv", "text/csv")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
st.markdown("---")
|
| 131 |
st.caption(
|
embeddings_train.csv
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:917d5c6677790a8725edaf78a589fa0c8cc675019548b2add49778f6edaeb2cc
|
| 3 |
-
size 155547606
|
|
|
|
|
|
|
|
|
|
|
|
label_mapping.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"0": "adipose",
|
| 3 |
+
"1": "adrenal_gland",
|
| 4 |
+
"2": "biliary_tract",
|
| 5 |
+
"3": "bladder",
|
| 6 |
+
"4": "blood",
|
| 7 |
+
"5": "blood_vessel",
|
| 8 |
+
"6": "bone_marrow",
|
| 9 |
+
"7": "brain",
|
| 10 |
+
"8": "breast",
|
| 11 |
+
"9": "cervix",
|
| 12 |
+
"10": "colon",
|
| 13 |
+
"11": "esophagus",
|
| 14 |
+
"12": "eye",
|
| 15 |
+
"13": "fibroblast",
|
| 16 |
+
"14": "head_and_neck",
|
| 17 |
+
"15": "heart",
|
| 18 |
+
"16": "kidney",
|
| 19 |
+
"17": "liver",
|
| 20 |
+
"18": "lung",
|
| 21 |
+
"19": "lymphoid",
|
| 22 |
+
"20": "muscle",
|
| 23 |
+
"21": "nerve",
|
| 24 |
+
"22": "other",
|
| 25 |
+
"23": "ovary",
|
| 26 |
+
"24": "pancreas",
|
| 27 |
+
"25": "pituitary",
|
| 28 |
+
"26": "placenta",
|
| 29 |
+
"27": "pleura",
|
| 30 |
+
"28": "prostate",
|
| 31 |
+
"29": "salivary_gland",
|
| 32 |
+
"30": "skin",
|
| 33 |
+
"31": "small_intestine",
|
| 34 |
+
"32": "soft_tissue",
|
| 35 |
+
"33": "spinal_cord",
|
| 36 |
+
"34": "spleen",
|
| 37 |
+
"35": "stem_cell",
|
| 38 |
+
"36": "stomach",
|
| 39 |
+
"37": "testis",
|
| 40 |
+
"38": "thymus",
|
| 41 |
+
"39": "thyroid",
|
| 42 |
+
"40": "uterus",
|
| 43 |
+
"41": "vagina"
|
| 44 |
+
}
|
requirements.txt
CHANGED
|
@@ -4,5 +4,3 @@ pandas>=1.5.0
|
|
| 4 |
numpy>=1.24.0
|
| 5 |
scikit-learn>=1.3.0
|
| 6 |
joblib>=1.3.0
|
| 7 |
-
h5py>=3.10.0
|
| 8 |
-
flexynesis>=0.3.0
|
|
|
|
| 4 |
numpy>=1.24.0
|
| 5 |
scikit-learn>=1.3.0
|
| 6 |
joblib>=1.3.0
|
|
|
|
|
|
test_clin.csv
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
train_clin.csv
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
embeddings_test.csv → vae_tissue_int8.torchscript.pt
RENAMED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8b7b0835aad6daab7947f21f1207a8dc05400135cf51f0ddb4b95d2edfe6a4e4
|
| 3 |
+
size 52895629
|