amitpande74 commited on
Commit
d4fdb7e
·
1 Parent(s): b0dd450

Run real model: load 53MB int8 TorchScript, drop SVD fallback, slim deps

Browse files
app.py CHANGED
@@ -1,32 +1,28 @@
1
  #!/usr/bin/env python3
2
  """
3
- Flexynesis Tissue VAE - Web Application (v3, demo-safe)
4
- ======================================================
5
- Upload a gene expression matrix -> get UBERON tissue classification + embeddings.
 
6
 
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
  """
 
 
 
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 load_all():
41
- art = joblib.load(MODEL_DIR / "vae_tissue.artifacts.joblib")
42
- gene_list = list(art['feature_lists']['gex'])
43
- scaler = art['transforms']['gex']
44
-
45
- train_emb = pd.read_csv(MODEL_DIR / "embeddings_train.csv", index_col=0)
46
- test_emb = pd.read_csv(MODEL_DIR / "embeddings_test.csv", index_col=0)
47
- train_clin = pd.read_csv(MODEL_DIR / "train_clin.csv", index_col=0)
48
- test_clin = pd.read_csv(MODEL_DIR / "test_clin.csv", index_col=0)
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, knn_ref, ref_emb, ref_clin = load_all()
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(ref_emb):,} reference samples · "
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"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))
100
- if genes_in_rows > genes_in_cols:
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 = '\t' if uploaded.name.endswith(('.tsv', '.txt')) else ','
154
- df = pd.read_csv(uploaded, index_col=0, sep=sep)
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("Processing..."):
160
- df = orient_matrix(df)
161
  overlap = len(set(df.columns) & set(gene_list))
162
- st.write(f"Gene overlap: **{overlap:,}/{len(gene_list):,}** ({100*overlap/len(gene_list):.1f}%)")
 
163
  if overlap < 1000:
164
  st.warning("Low gene overlap — results may be unreliable.")
165
 
166
- aligned = pd.DataFrame(0.0, index=df.index, columns=gene_list)
167
- common = [g for g in gene_list if g in df.columns]
168
  aligned[common] = df[common].values
169
- aligned = aligned.fillna(0)
170
- X_scaled = scaler.transform(aligned.values)
171
-
172
- emb, pred_labels, max_probs = embed_and_classify(X_scaled)
173
-
174
- distances, indices = knn_ref.kneighbors(emb)
175
- breakdowns = []
176
- for i in range(len(emb)):
177
- src = Counter(ref_clin.iloc[indices[i]]['source'].values)
178
- breakdowns.append('; '.join(f"{s}:{c}" for s, c in src.most_common()))
 
 
179
 
180
  st.markdown("---")
181
  st.subheader("📊 Results")
182
- conf_col = [f"{p:.1%}" if not np.isnan(p) else "kNN" for p in max_probs]
183
- results = pd.DataFrame({
184
- 'Sample': df.index,
185
- 'Tissue': pred_labels,
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
- col1, col2 = st.columns(2)
193
- with col1:
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.markdown("---")
202
- c1, c2 = st.columns(2)
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:b8353267526ba6fe1b189650a11eb9918282fa9537515f9893a74195263ef688
3
- size 37219853
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8b7b0835aad6daab7947f21f1207a8dc05400135cf51f0ddb4b95d2edfe6a4e4
3
+ size 52895629