Commit 路
eaa8a05
1
Parent(s): 55ffc43
Update to 118K tissue-curated model (kNN demo mode)
Browse files- app.py +15 -28
- embeddings_test.csv +2 -2
- embeddings_train.csv +2 -2
- test_clin.csv +0 -0
- train_clin.csv +0 -0
- vae_tissue.artifacts.joblib +2 -2
app.py
CHANGED
|
@@ -7,17 +7,11 @@ Upload a gene expression matrix -> get UBERON tissue classification + embeddings
|
|
| 7 |
v3 (May 2026): Trained on 118,263 tissue-curated samples from TCGA, GTEx, ARCHS4
|
| 8 |
(cell lines excluded, classes balanced). 42 UBERON tissues, 94.9% balanced accuracy.
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
118K reference embeddings. When the .pth IS present (e.g. running locally),
|
| 16 |
-
the app uses the trained VAE encoder for exact inference. Both modes use the
|
| 17 |
-
same 118K reference embeddings; only the encoding of new uploads differs.
|
| 18 |
-
|
| 19 |
-
Run locally (full VAE inference, needs the .pth in model/):
|
| 20 |
-
streamlit run app.py
|
| 21 |
|
| 22 |
Author: Amit Pande, MDC Berlin/BIMSB
|
| 23 |
"""
|
|
@@ -30,8 +24,7 @@ from pathlib import Path
|
|
| 30 |
from sklearn.neighbors import KNeighborsClassifier
|
| 31 |
from collections import Counter
|
| 32 |
|
| 33 |
-
|
| 34 |
-
MODEL_DIR = Path("model")
|
| 35 |
K = 5
|
| 36 |
LATENT_DIM = 121
|
| 37 |
|
|
@@ -43,7 +36,6 @@ st.markdown(
|
|
| 43 |
"across **42 UBERON tissue categories** (94.9% balanced accuracy, 121-dim latent space)."
|
| 44 |
)
|
| 45 |
|
| 46 |
-
# -- Load model (optional) + artifacts + reference embeddings --
|
| 47 |
@st.cache_resource
|
| 48 |
def load_all():
|
| 49 |
art = joblib.load(MODEL_DIR / "vae_tissue.artifacts.joblib")
|
|
@@ -68,7 +60,6 @@ def load_all():
|
|
| 68 |
knn = KNeighborsClassifier(n_neighbors=K, metric='cosine', n_jobs=-1)
|
| 69 |
knn.fit(ref_emb.values, ref_clin['uberon_tissue'].values)
|
| 70 |
|
| 71 |
-
# Load the full VAE only if the weights are present (local / paid Space).
|
| 72 |
model = None
|
| 73 |
pth = MODEL_DIR / "vae_tissue.final_model.pth"
|
| 74 |
if pth.exists():
|
|
@@ -77,7 +68,7 @@ def load_all():
|
|
| 77 |
model = torch.load(pth, map_location='cpu', weights_only=False)
|
| 78 |
model.eval()
|
| 79 |
except Exception as ex:
|
| 80 |
-
st.sidebar.warning(f"
|
| 81 |
model = None
|
| 82 |
|
| 83 |
return model, gene_list, scaler, knn, ref_emb, ref_clin
|
|
@@ -96,15 +87,13 @@ try:
|
|
| 96 |
"鈩癸笍 Running in **demo mode**. The 118K reference embeddings are used as-is; "
|
| 97 |
"uploaded samples are projected with TruncatedSVD and classified by kNN. "
|
| 98 |
"Full VAE encoding of new samples requires the model weights "
|
| 99 |
-
"(vae_tissue.final_model.pth),
|
| 100 |
-
"for the free Hugging Face tier."
|
| 101 |
)
|
| 102 |
except Exception as e:
|
| 103 |
st.error(f"Load error: {e}")
|
| 104 |
-
st.info("Place reference files in
|
| 105 |
st.stop()
|
| 106 |
|
| 107 |
-
# -- Helpers --
|
| 108 |
def orient_matrix(df):
|
| 109 |
genes_in_cols = len(set(df.columns) & set(gene_list))
|
| 110 |
genes_in_rows = len(set(df.index) & set(gene_list))
|
|
@@ -112,11 +101,10 @@ def orient_matrix(df):
|
|
| 112 |
df = df.T
|
| 113 |
return df
|
| 114 |
|
| 115 |
-
def embed_and_classify(
|
| 116 |
-
"""Return (embeddings, pred_labels, max_probs). Uses VAE if available, else SVD+kNN."""
|
| 117 |
if model is not None:
|
| 118 |
import torch
|
| 119 |
-
X_t = torch.tensor(
|
| 120 |
with torch.no_grad():
|
| 121 |
h = model.encoders[0](X_t)
|
| 122 |
mu = model.FC_mean(h[0])
|
|
@@ -133,10 +121,10 @@ def embed_and_classify(df_aligned_scaled):
|
|
| 133 |
max_probs = probs.numpy().max(axis=1)
|
| 134 |
else:
|
| 135 |
from sklearn.decomposition import TruncatedSVD
|
| 136 |
-
n_comp = min(LATENT_DIM,
|
| 137 |
n_comp = max(n_comp, 1)
|
| 138 |
svd = TruncatedSVD(n_components=n_comp, random_state=42)
|
| 139 |
-
emb_reduced = svd.fit_transform(
|
| 140 |
if emb_reduced.shape[1] < LATENT_DIM:
|
| 141 |
pad = np.zeros((emb_reduced.shape[0], LATENT_DIM - emb_reduced.shape[1]))
|
| 142 |
emb = np.concatenate([emb_reduced, pad], axis=1)
|
|
@@ -146,14 +134,13 @@ def embed_and_classify(df_aligned_scaled):
|
|
| 146 |
max_probs = np.full(len(pred_labels), np.nan)
|
| 147 |
return emb, pred_labels, max_probs
|
| 148 |
|
| 149 |
-
# -- File upload --
|
| 150 |
st.markdown("---")
|
| 151 |
col1, col2 = st.columns([2, 1])
|
| 152 |
with col1:
|
| 153 |
uploaded = st.file_uploader(
|
| 154 |
"Upload CSV/TSV (genes 脳 samples or samples 脳 genes)",
|
| 155 |
type=["csv", "tsv", "txt"],
|
| 156 |
-
help="HGNC gene symbols. Log2-transformed expression values
|
| 157 |
with col2:
|
| 158 |
st.markdown(f"""
|
| 159 |
**Expected input:**
|
|
@@ -227,4 +214,4 @@ st.caption(
|
|
| 227 |
"Flexynesis Tissue VAE (v3) 路 Akalin Lab, MDC Berlin/BIMSB 路 "
|
| 228 |
"118,263 samples 路 42 UBERON tissues 路 94.9% balanced accuracy 路 "
|
| 229 |
"github.com/BIMSBbioinfo/flexynesis"
|
| 230 |
-
)
|
|
|
|
| 7 |
v3 (May 2026): Trained on 118,263 tissue-curated samples from TCGA, GTEx, ARCHS4
|
| 8 |
(cell lines excluded, classes balanced). 42 UBERON tissues, 94.9% balanced accuracy.
|
| 9 |
|
| 10 |
+
When the full model weights (vae_tissue.final_model.pth, ~8 GB) are absent
|
| 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 |
"""
|
|
|
|
| 24 |
from sklearn.neighbors import KNeighborsClassifier
|
| 25 |
from collections import Counter
|
| 26 |
|
| 27 |
+
MODEL_DIR = Path(".")
|
|
|
|
| 28 |
K = 5
|
| 29 |
LATENT_DIM = 121
|
| 30 |
|
|
|
|
| 36 |
"across **42 UBERON tissue categories** (94.9% balanced accuracy, 121-dim latent space)."
|
| 37 |
)
|
| 38 |
|
|
|
|
| 39 |
@st.cache_resource
|
| 40 |
def load_all():
|
| 41 |
art = joblib.load(MODEL_DIR / "vae_tissue.artifacts.joblib")
|
|
|
|
| 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():
|
|
|
|
| 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
|
|
|
|
| 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"Load error: {e}")
|
| 94 |
+
st.info("Place reference files in repo root (embeddings_*.csv, *_clin.csv, vae_tissue.artifacts.joblib).")
|
| 95 |
st.stop()
|
| 96 |
|
|
|
|
| 97 |
def orient_matrix(df):
|
| 98 |
genes_in_cols = len(set(df.columns) & set(gene_list))
|
| 99 |
genes_in_rows = len(set(df.index) & set(gene_list))
|
|
|
|
| 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])
|
|
|
|
| 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)
|
|
|
|
| 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])
|
| 139 |
with col1:
|
| 140 |
uploaded = st.file_uploader(
|
| 141 |
"Upload CSV/TSV (genes 脳 samples or samples 脳 genes)",
|
| 142 |
type=["csv", "tsv", "txt"],
|
| 143 |
+
help="HGNC gene symbols. Log2-transformed expression values.")
|
| 144 |
with col2:
|
| 145 |
st.markdown(f"""
|
| 146 |
**Expected input:**
|
|
|
|
| 214 |
"Flexynesis Tissue VAE (v3) 路 Akalin Lab, MDC Berlin/BIMSB 路 "
|
| 215 |
"118,263 samples 路 42 UBERON tissues 路 94.9% balanced accuracy 路 "
|
| 216 |
"github.com/BIMSBbioinfo/flexynesis"
|
| 217 |
+
)
|
embeddings_test.csv
CHANGED
|
@@ -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:b8353267526ba6fe1b189650a11eb9918282fa9537515f9893a74195263ef688
|
| 3 |
+
size 37219853
|
embeddings_train.csv
CHANGED
|
@@ -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:917d5c6677790a8725edaf78a589fa0c8cc675019548b2add49778f6edaeb2cc
|
| 3 |
+
size 155547606
|
test_clin.csv
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
train_clin.csv
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
vae_tissue.artifacts.joblib
CHANGED
|
@@ -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:2bafdb85f7da1e7976055e42329dcaf8d228cb15217692b6be9a36de5de0c29c
|
| 3 |
+
size 658142
|