| |
| """ |
| Probe 10: Tissue-Specific Glycan Presence β GlycanBERT |
| ======================================================= |
| Tests whether GlycanBERT embeddings encode tissue-specific glycan identity |
| using mass-spectrometry-validated ground truth from: |
| |
| "A comprehensive N-glycoproteome atlas reveals tissue-specific glycan |
| remodeling but non-random structural microheterogeneities" |
| (Nature Communications, 2025) |
| |
| Data: 189 N-glycan structures Γ 23 mouse tissues (binary presence/absence) |
| sourced from Figure 4d + Supplement Figure 1a of the paper. |
| Glycans mapped: StrucGP β GlyTouCan β WURCS (100% resolution). |
| |
| Sub-probes: |
| 10a. Multi-label tissue prediction (23-class sigmoid probe) |
| 10b. Organ-system classification (7 systems: Cardio, Digestive, etc.) |
| 10c. Brain vs Non-brain binary classification |
| 10d. Tissue-specificity vs Ubiquity prediction (regression) |
| 10e. t-SNE visualization colored by organ system and tissue count |
| |
| Usage: |
| python probe_10_tissue_presence.py --model v5 --device cuda |
| python probe_10_tissue_presence.py --model v6 --device cuda |
| """ |
|
|
| import sys |
| import os |
| import json |
| import csv |
| import argparse |
| import numpy as np |
| from pathlib import Path |
| from collections import Counter |
|
|
| PROJECT_ROOT = Path(__file__).resolve().parents[2] |
| VOCAB_PATH = PROJECT_ROOT / 'bert_training_v4' / 'data' / 'vocabulary.json' |
| DATA_PATHS = { |
| 'v1': PROJECT_ROOT / 'bert_v6_contrastive' / 'additional_probes' / 'probe_10_tissue_presence_data.csv', |
| 'v2': PROJECT_ROOT / 'bert_v6_contrastive' / 'additional_probes' / 'probe_10_tissue_presence_data_v2.csv', |
| } |
|
|
| |
| WELL_POWERED_TISSUES = [ |
| 'Colon', 'Intestine', 'Liver', 'Pancreas', 'Stomach', |
| 'Kidney', 'Spleen', 'Cerebellum', 'Hippocampus', 'Hypothalamus', |
| 'Medulla', 'Olfactory bulb', 'White matter', 'Testis', 'Uterus', 'Lung' |
| ] |
|
|
| CHECKPOINTS = { |
| 'v5': PROJECT_ROOT / 'checkpoints_v5_bpe_topo' / 'best_v5_bpe_topo_model.pt', |
| 'v6': PROJECT_ROOT / 'bert_v5.1_contrastive' / 'checkpoints' / 'best_v51_contrastive_model.pt', |
| } |
|
|
| TISSUE_NAMES = [ |
| 'Heart', 'Colon', 'Gallbladder', 'Intestine', 'Liver', 'Pancreas', 'Stomach', |
| 'Bladder', 'Kidney', 'Spleen', 'Muscle', |
| 'Cerebellum', 'Cortex', 'Hippocampus', 'Hypothalamus', 'Medulla', 'Olfactory bulb', 'White matter', |
| 'Ovary', 'Seminal vesicle', 'Testis', 'Uterus', 'Lung' |
| ] |
|
|
| |
| ORGAN_SYSTEMS = { |
| 'Cardiovascular': ['Heart'], |
| 'Digestive': ['Colon', 'Gallbladder', 'Intestine', 'Liver', 'Pancreas', 'Stomach'], |
| 'Excretory': ['Bladder', 'Kidney'], |
| 'Immune': ['Spleen'], |
| 'Musculoskeletal': ['Muscle'], |
| 'Nervous': ['Cerebellum', 'Cortex', 'Hippocampus', 'Hypothalamus', 'Medulla', |
| 'Olfactory bulb', 'White matter'], |
| 'Reproductive': ['Ovary', 'Seminal vesicle', 'Testis', 'Uterus'], |
| 'Respiratory': ['Lung'], |
| } |
|
|
| BRAIN_TISSUES = ['Cerebellum', 'Cortex', 'Hippocampus', 'Hypothalamus', |
| 'Medulla', 'Olfactory bulb', 'White matter'] |
|
|
| |
| SYSTEM_COLORS = { |
| 'Cardiovascular': '#D55E00', |
| 'Digestive': '#E69F00', |
| 'Excretory': '#56B4E9', |
| 'Immune': '#009E73', |
| 'Musculoskeletal': '#CC79A7', |
| 'Nervous': '#0072B2', |
| 'Reproductive': '#F0E442', |
| 'Respiratory': '#882255', |
| } |
|
|
| |
| sys.path.insert(0, str(PROJECT_ROOT)) |
| sys.path.insert(0, str(PROJECT_ROOT / 'bert_training_v4')) |
| from model.multimodal_glycan_bert_v3 import MultimodalGlycanBERT, MultimodalGlycanBERTConfig |
| from downstream_tasks.utils.tokenizer import WURCSTokenizer |
|
|
|
|
| def load_model(ckpt_path, device='cuda'): |
| """Load MultimodalGlycanBERT from checkpoint.""" |
| import torch |
| print(f"Loading model from {ckpt_path}...") |
| state = torch.load(ckpt_path, map_location='cpu', weights_only=False) |
|
|
| sd = state.get('model_state_dict', state) |
| if 'proj_head_state_dict' in state: |
| sd = {k: v for k, v in sd.items() if not k.startswith('proj_head')} |
|
|
| emb_weight = sd.get('seq_embeddings.token_embeddings.weight', |
| sd.get('token_embeddings.weight')) |
| vocab_size = emb_weight.shape[0] if emb_weight is not None else 2200 |
| hidden = emb_weight.shape[1] if emb_weight is not None else 768 |
|
|
| config = MultimodalGlycanBERTConfig( |
| seq_vocab_size=vocab_size, seq_hidden_size=hidden, |
| seq_num_layers=12, seq_num_heads=12, seq_max_length=256, |
| use_cnn_frontend=True, cnn_kernel_size=3, |
| ) |
| model = MultimodalGlycanBERT(config) |
| model.load_state_dict(sd, strict=False) |
| model = model.to(device).eval() |
| n_params = sum(p.numel() for p in model.parameters()) |
| print(f" Loaded: {n_params:,} params, vocab={vocab_size}, hidden={hidden}") |
| return model |
|
|
|
|
| def batch_cls_embeddings(model, wurcs_list, tokenizer, device='cuda', |
| batch_size=64, max_len=256): |
| """Extract [CLS] embeddings for a list of WURCS strings.""" |
| import torch |
| all_embs = [] |
| errors = 0 |
|
|
| for i in range(0, len(wurcs_list), batch_size): |
| batch = wurcs_list[i:i+batch_size] |
| token_ids_list, bd_list, lt_list = [], [], [] |
|
|
| for w in batch: |
| try: |
| tok_out = tokenizer.tokenize(w) |
| ids = tok_out['token_ids'][:max_len] |
| bd = tok_out['branch_depths'][:max_len] |
| lt = tok_out['linkage_types'][:max_len] |
| token_ids_list.append(ids) |
| bd_list.append(bd) |
| lt_list.append(lt) |
| except Exception: |
| errors += 1 |
| continue |
|
|
| if not token_ids_list: |
| continue |
|
|
| max_l = max(len(x) for x in token_ids_list) |
| padded_ids = torch.zeros(len(token_ids_list), max_l, dtype=torch.long) |
| padded_bd = torch.zeros_like(padded_ids) |
| padded_lt = torch.zeros_like(padded_ids) |
| mask = torch.zeros(len(token_ids_list), max_l, dtype=torch.long) |
|
|
| for j, (ids, bd, lt) in enumerate(zip(token_ids_list, bd_list, lt_list)): |
| padded_ids[j, :len(ids)] = torch.tensor(ids, dtype=torch.long) |
| padded_bd[j, :len(bd)] = torch.tensor(bd, dtype=torch.long) |
| padded_lt[j, :len(lt)] = torch.tensor(lt, dtype=torch.long) |
| mask[j, :len(ids)] = 1 |
|
|
| padded_ids = padded_ids.to(device) |
| padded_bd = padded_bd.to(device) |
| padded_lt = padded_lt.to(device) |
|
|
| with torch.no_grad(): |
| seq_out = model.seq_embeddings(padded_ids, branch_depths=padded_bd, |
| linkage_types=padded_lt) |
| cls_emb = seq_out[:, 0, :].cpu().numpy() |
| all_embs.append(cls_emb) |
|
|
| print(f" Embedded: {sum(e.shape[0] for e in all_embs):,} ({errors} errors)") |
| return np.vstack(all_embs) if all_embs else np.zeros((0, 768)) |
|
|
|
|
| def setup_nature_style(): |
| """Configure matplotlib for Nature-style publication plots.""" |
| import matplotlib |
| matplotlib.use('Agg') |
| import matplotlib.pyplot as plt |
| plt.rcParams.update({ |
| 'font.family': 'sans-serif', |
| 'font.sans-serif': ['Arial', 'Helvetica', 'DejaVu Sans'], |
| 'font.size': 10, 'axes.titlesize': 12, 'axes.labelsize': 11, |
| 'xtick.labelsize': 9, 'ytick.labelsize': 9, 'legend.fontsize': 9, |
| 'figure.dpi': 300, 'savefig.dpi': 300, 'savefig.bbox': 'tight', |
| 'axes.linewidth': 0.8, 'axes.spines.top': False, 'axes.spines.right': False, |
| }) |
| return plt |
|
|
|
|
| |
| |
| |
|
|
| def probe_10a_multilabel(embs, tissue_matrix, output_dir, model_name): |
| """Per-tissue binary classifiers (one-vs-rest) using linear probes.""" |
| from sklearn.linear_model import LogisticRegression |
| from sklearn.model_selection import StratifiedKFold, LeaveOneOut |
| from sklearn.metrics import accuracy_score, f1_score, roc_auc_score |
|
|
| print(f"\n{'='*60}") |
| print(f"PROBE 10a: Multi-label Tissue Prediction ({model_name})") |
| print(f"{'='*60}") |
|
|
| per_tissue_results = {} |
| valid_tissues = [] |
|
|
| for t_idx, tissue in enumerate(TISSUE_NAMES): |
| y = tissue_matrix[:, t_idx] |
| n_pos = y.sum() |
| n_neg = len(y) - n_pos |
|
|
| if n_pos < 3 or n_neg < 3: |
| print(f" {tissue:<20s}: SKIP (pos={n_pos}, neg={n_neg})") |
| continue |
|
|
| valid_tissues.append(tissue) |
|
|
| |
| n_splits = min(5, n_pos, n_neg) |
| if n_splits < 2: |
| print(f" {tissue:<20s}: SKIP (can't split, pos={n_pos})") |
| continue |
|
|
| skf = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=42) |
| accs, f1s, aucs = [], [], [] |
|
|
| for train_idx, test_idx in skf.split(embs, y): |
| clf = LogisticRegression(max_iter=1000, C=1.0, solver='lbfgs') |
| clf.fit(embs[train_idx], y[train_idx]) |
| preds = clf.predict(embs[test_idx]) |
| proba = clf.predict_proba(embs[test_idx]) |
|
|
| acc = accuracy_score(y[test_idx], preds) |
| f1 = f1_score(y[test_idx], preds, average='binary', zero_division=0) |
| try: |
| auc = roc_auc_score(y[test_idx], proba[:, 1]) |
| except (ValueError, IndexError): |
| auc = 0.5 |
| accs.append(acc) |
| f1s.append(f1) |
| aucs.append(auc) |
|
|
| result = { |
| 'tissue': tissue, |
| 'n_positive': int(n_pos), |
| 'n_negative': int(n_neg), |
| 'accuracy_mean': float(np.mean(accs)), |
| 'accuracy_std': float(np.std(accs)), |
| 'f1_mean': float(np.mean(f1s)), |
| 'f1_std': float(np.std(f1s)), |
| 'auc_mean': float(np.mean(aucs)), |
| 'auc_std': float(np.std(aucs)), |
| } |
| per_tissue_results[tissue] = result |
| print(f" {tissue:<20s}: AUC={np.mean(aucs):.3f}Β±{np.std(aucs):.3f}, " |
| f"F1={np.mean(f1s):.3f}, pos={n_pos}") |
|
|
| |
| if per_tissue_results: |
| macro_auc = np.mean([r['auc_mean'] for r in per_tissue_results.values()]) |
| macro_f1 = np.mean([r['f1_mean'] for r in per_tissue_results.values()]) |
| macro_acc = np.mean([r['accuracy_mean'] for r in per_tissue_results.values()]) |
| print(f"\n MACRO AVERAGE across {len(per_tissue_results)} tissues:") |
| print(f" AUC = {macro_auc:.4f}") |
| print(f" F1 = {macro_f1:.4f}") |
| print(f" Acc = {macro_acc:.4f}") |
|
|
| results = { |
| 'task': 'multilabel_tissue_prediction', |
| 'model': model_name, |
| 'n_glycans': int(len(embs)), |
| 'n_tissues_probed': len(per_tissue_results), |
| 'macro_auc': float(macro_auc) if per_tissue_results else 0.0, |
| 'macro_f1': float(macro_f1) if per_tissue_results else 0.0, |
| 'macro_accuracy': float(macro_acc) if per_tissue_results else 0.0, |
| 'per_tissue': per_tissue_results, |
| } |
| with open(os.path.join(output_dir, f'probe10a_multilabel_{model_name}.json'), 'w') as f: |
| json.dump(results, f, indent=2) |
| return results |
|
|
|
|
| |
| |
| |
|
|
| def probe_10b_organ_system(embs, tissue_matrix, output_dir, model_name): |
| """Classify glycans by dominant organ system.""" |
| from sklearn.linear_model import LogisticRegression |
| from sklearn.model_selection import StratifiedKFold |
| from sklearn.metrics import accuracy_score, f1_score |
|
|
| print(f"\n{'='*60}") |
| print(f"PROBE 10b: Organ System Classification ({model_name})") |
| print(f"{'='*60}") |
|
|
| |
| |
| system_labels = [] |
| for i in range(len(embs)): |
| system_counts = {} |
| for sys_name, tissues in ORGAN_SYSTEMS.items(): |
| count = sum(tissue_matrix[i, TISSUE_NAMES.index(t)] |
| for t in tissues if t in TISSUE_NAMES) |
| if count > 0: |
| system_counts[sys_name] = count |
|
|
| if not system_counts: |
| system_labels.append('Unknown') |
| elif len(system_counts) == 1: |
| system_labels.append(list(system_counts.keys())[0]) |
| else: |
| |
| |
| best_sys = max(system_counts.keys(), |
| key=lambda s: system_counts[s] / len(ORGAN_SYSTEMS[s])) |
| system_labels.append(best_sys) |
|
|
| system_labels = np.array(system_labels) |
| counts = Counter(system_labels) |
| print(f" Distribution: {dict(sorted(counts.items()))}") |
|
|
| |
| valid_systems = [s for s, c in counts.items() if c >= 5 and s != 'Unknown'] |
| mask = np.array([l in valid_systems for l in system_labels]) |
|
|
| if mask.sum() < 20: |
| print(" SKIP: Too few valid samples") |
| return {'task': 'organ_system_classification', 'model': model_name, 'skipped': True} |
|
|
| embs_f = embs[mask] |
| labels_f = system_labels[mask] |
|
|
| from sklearn.preprocessing import LabelEncoder |
| le = LabelEncoder() |
| y = le.fit_transform(labels_f) |
| classes = le.classes_ |
| print(f" Classes ({len(classes)}): {list(classes)}") |
|
|
| n_splits = min(5, min(Counter(y).values())) |
| if n_splits < 2: |
| print(" SKIP: Not enough per-class for CV") |
| return {'task': 'organ_system_classification', 'model': model_name, 'skipped': True} |
|
|
| skf = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=42) |
| accs, f1s = [], [] |
| for fold, (tr, te) in enumerate(skf.split(embs_f, y)): |
| clf = LogisticRegression(max_iter=1000, C=1.0, solver='lbfgs', |
| multi_class='multinomial') |
| clf.fit(embs_f[tr], y[tr]) |
| preds = clf.predict(embs_f[te]) |
| acc = accuracy_score(y[te], preds) |
| f1 = f1_score(y[te], preds, average='macro') |
| accs.append(acc) |
| f1s.append(f1) |
| print(f" Fold {fold+1}: acc={acc:.4f}, F1={f1:.4f}") |
|
|
| print(f"\n RESULT: Acc={np.mean(accs):.4f}Β±{np.std(accs):.4f}, " |
| f"F1={np.mean(f1s):.4f}Β±{np.std(f1s):.4f}") |
|
|
| results = { |
| 'task': 'organ_system_classification', 'model': model_name, |
| 'n_samples': int(mask.sum()), |
| 'n_classes': int(len(classes)), |
| 'classes': list(classes), |
| 'distribution': {str(k): int(v) for k, v in counts.items()}, |
| 'accuracy_mean': float(np.mean(accs)), |
| 'accuracy_std': float(np.std(accs)), |
| 'f1_macro_mean': float(np.mean(f1s)), |
| 'f1_macro_std': float(np.std(f1s)), |
| } |
| with open(os.path.join(output_dir, f'probe10b_organ_system_{model_name}.json'), 'w') as f: |
| json.dump(results, f, indent=2) |
| return results |
|
|
|
|
| |
| |
| |
|
|
| def probe_10c_brain(embs, tissue_matrix, output_dir, model_name): |
| """Binary: does this glycan appear in any brain tissue?""" |
| from sklearn.linear_model import LogisticRegression |
| from sklearn.model_selection import StratifiedKFold |
| from sklearn.metrics import accuracy_score, f1_score, roc_auc_score |
|
|
| print(f"\n{'='*60}") |
| print(f"PROBE 10c: Brain vs Non-brain ({model_name})") |
| print(f"{'='*60}") |
|
|
| brain_indices = [TISSUE_NAMES.index(t) for t in BRAIN_TISSUES if t in TISSUE_NAMES] |
| y = (tissue_matrix[:, brain_indices].sum(axis=1) > 0).astype(int) |
|
|
| n_brain = y.sum() |
| n_nonbrain = len(y) - n_brain |
| print(f" Brain-present: {n_brain}, Non-brain-only: {n_nonbrain}") |
|
|
| if n_brain < 5 or n_nonbrain < 5: |
| print(" SKIP: Too few samples") |
| return {'task': 'brain_classification', 'model': model_name, 'skipped': True} |
|
|
| n_splits = min(5, n_brain, n_nonbrain) |
| skf = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=42) |
| accs, f1s, aucs = [], [], [] |
|
|
| for fold, (tr, te) in enumerate(skf.split(embs, y)): |
| clf = LogisticRegression(max_iter=1000, C=1.0) |
| clf.fit(embs[tr], y[tr]) |
| preds = clf.predict(embs[te]) |
| proba = clf.predict_proba(embs[te])[:, 1] |
| acc = accuracy_score(y[te], preds) |
| f1 = f1_score(y[te], preds, average='binary') |
| try: |
| auc = roc_auc_score(y[te], proba) |
| except ValueError: |
| auc = 0.5 |
| accs.append(acc); f1s.append(f1); aucs.append(auc) |
| print(f" Fold {fold+1}: acc={acc:.4f}, F1={f1:.4f}, AUC={auc:.4f}") |
|
|
| print(f"\n RESULT: Acc={np.mean(accs):.4f}, AUC={np.mean(aucs):.4f}") |
|
|
| results = { |
| 'task': 'brain_classification', 'model': model_name, |
| 'n_brain': int(n_brain), 'n_nonbrain': int(n_nonbrain), |
| 'accuracy_mean': float(np.mean(accs)), |
| 'accuracy_std': float(np.std(accs)), |
| 'f1_mean': float(np.mean(f1s)), |
| 'auc_mean': float(np.mean(aucs)), |
| 'auc_std': float(np.std(aucs)), |
| } |
| with open(os.path.join(output_dir, f'probe10c_brain_{model_name}.json'), 'w') as f: |
| json.dump(results, f, indent=2) |
| return results |
|
|
|
|
| |
| |
| |
|
|
| def probe_10d_breadth(embs, tissue_matrix, output_dir, model_name): |
| """Predict number of tissues a glycan appears in (1β23).""" |
| from sklearn.linear_model import Ridge |
| from sklearn.model_selection import KFold |
| from scipy.stats import spearmanr |
|
|
| print(f"\n{'='*60}") |
| print(f"PROBE 10d: Tissue Breadth Prediction ({model_name})") |
| print(f"{'='*60}") |
|
|
| y = tissue_matrix.sum(axis=1) |
| print(f" N={len(y)}, range=[{y.min()}, {y.max()}], mean={y.mean():.2f}") |
| print(f" Unique=1: {(y==1).sum()}, β€3: {(y<=3).sum()}, β₯10: {(y>=10).sum()}") |
|
|
| kf = KFold(n_splits=5, shuffle=True, random_state=42) |
| r2s, spearmans, maes = [], [], [] |
|
|
| for fold, (tr, te) in enumerate(kf.split(embs)): |
| clf = Ridge(alpha=1.0) |
| clf.fit(embs[tr], y[tr]) |
| preds = clf.predict(embs[te]) |
|
|
| r2 = clf.score(embs[te], y[te]) |
| rho, p = spearmanr(y[te], preds) |
| mae = np.mean(np.abs(y[te] - preds)) |
| r2s.append(r2); spearmans.append(rho); maes.append(mae) |
| print(f" Fold {fold+1}: RΒ²={r2:.4f}, Ο={rho:.4f}, MAE={mae:.2f}") |
|
|
| print(f"\n RESULT: RΒ²={np.mean(r2s):.4f}, Ο={np.mean(spearmans):.4f}, " |
| f"MAE={np.mean(maes):.2f}") |
|
|
| results = { |
| 'task': 'tissue_breadth_regression', 'model': model_name, |
| 'n_samples': int(len(y)), |
| 'tissue_count_range': [int(y.min()), int(y.max())], |
| 'r2_mean': float(np.mean(r2s)), |
| 'r2_std': float(np.std(r2s)), |
| 'spearman_mean': float(np.mean(spearmans)), |
| 'mae_mean': float(np.mean(maes)), |
| } |
| with open(os.path.join(output_dir, f'probe10d_breadth_{model_name}.json'), 'w') as f: |
| json.dump(results, f, indent=2) |
| return results |
|
|
|
|
| |
| |
| |
|
|
| def probe_10e_visualization(embs, tissue_matrix, compositions, output_dir, model_name): |
| """t-SNE colored by organ system and tissue breadth.""" |
| plt = setup_nature_style() |
| from sklearn.manifold import TSNE |
|
|
| print(f"\n{'='*60}") |
| print(f"PROBE 10e: t-SNE Visualization ({model_name})") |
| print(f"{'='*60}") |
|
|
| n_tissues = tissue_matrix.sum(axis=1) |
|
|
| |
| system_labels = [] |
| for i in range(len(embs)): |
| system_counts = {} |
| for sys_name, tissues in ORGAN_SYSTEMS.items(): |
| count = sum(tissue_matrix[i, TISSUE_NAMES.index(t)] |
| for t in tissues if t in TISSUE_NAMES) |
| if count > 0: |
| system_counts[sys_name] = count |
| if system_counts: |
| best = max(system_counts.keys(), |
| key=lambda s: system_counts[s] / len(ORGAN_SYSTEMS[s])) |
| system_labels.append(best) |
| else: |
| system_labels.append('Unknown') |
| system_labels = np.array(system_labels) |
|
|
| |
| print(f" Running t-SNE on {len(embs)} samples...") |
| tsne = TSNE(n_components=2, perplexity=min(30, len(embs)-1), |
| random_state=42, max_iter=1000, learning_rate='auto', init='pca') |
| coords = tsne.fit_transform(embs) |
|
|
| fig, axes = plt.subplots(1, 3, figsize=(21, 6)) |
|
|
| |
| ax = axes[0] |
| for sys_name in sorted(ORGAN_SYSTEMS.keys()): |
| mask = system_labels == sys_name |
| if mask.sum() > 0: |
| ax.scatter(coords[mask, 0], coords[mask, 1], |
| c=SYSTEM_COLORS.get(sys_name, 'gray'), s=40, alpha=0.7, |
| label=f'{sys_name} ({mask.sum()})', edgecolors='white', |
| linewidths=0.3, rasterized=True) |
| ax.set_xlabel('t-SNE 1') |
| ax.set_ylabel('t-SNE 2') |
| ax.set_title(f'Dominant Organ System β {model_name}') |
| ax.legend(frameon=False, markerscale=1.2, fontsize=7, |
| loc='center left', bbox_to_anchor=(1.0, 0.5)) |
|
|
| |
| ax = axes[1] |
| scatter = ax.scatter(coords[:, 0], coords[:, 1], |
| c=n_tissues, cmap='YlOrRd', s=40, alpha=0.7, |
| edgecolors='white', linewidths=0.3, rasterized=True, |
| vmin=1, vmax=max(n_tissues)) |
| cbar = plt.colorbar(scatter, ax=ax, shrink=0.8) |
| cbar.set_label('Number of tissues') |
| ax.set_xlabel('t-SNE 1') |
| ax.set_ylabel('t-SNE 2') |
| ax.set_title(f'Tissue Breadth β {model_name}') |
|
|
| |
| ax = axes[2] |
| brain_indices = [TISSUE_NAMES.index(t) for t in BRAIN_TISSUES if t in TISSUE_NAMES] |
| is_brain = (tissue_matrix[:, brain_indices].sum(axis=1) > 0) |
| brain_only = is_brain & (tissue_matrix[:, [i for i in range(len(TISSUE_NAMES)) |
| if TISSUE_NAMES[i] not in BRAIN_TISSUES]].sum(axis=1) == 0) |
|
|
| non_brain = ~is_brain |
| both = is_brain & ~brain_only |
|
|
| for mask, color, label, s, alpha in [ |
| (non_brain, '#E69F00', 'Non-brain only', 35, 0.7), |
| (both, '#009E73', 'Brain + peripheral', 45, 0.8), |
| (brain_only, '#0072B2', 'Brain only', 50, 0.9), |
| ]: |
| if mask.sum() > 0: |
| ax.scatter(coords[mask, 0], coords[mask, 1], |
| c=color, s=s, alpha=alpha, |
| label=f'{label} ({mask.sum()})', edgecolors='white', |
| linewidths=0.3, rasterized=True) |
| ax.set_xlabel('t-SNE 1') |
| ax.set_ylabel('t-SNE 2') |
| ax.set_title(f'Brain vs Non-brain β {model_name}') |
| ax.legend(frameon=False, markerscale=1.5, fontsize=8) |
|
|
| plt.suptitle(f'Probe 10: MS-validated Tissue-Specific Glycan Presence β ' |
| f'GlycanBERT {model_name}\n' |
| f'({len(embs)} N-glycans Γ {len(TISSUE_NAMES)} mouse tissues, ' |
| f'Nature Commun. 2025)', |
| fontsize=12, fontweight='bold', y=1.02) |
| plt.tight_layout() |
|
|
| fig_path = os.path.join(output_dir, f'probe10_tsne_{model_name.lower()}.png') |
| plt.savefig(fig_path, dpi=300, bbox_inches='tight', facecolor='white') |
| plt.savefig(fig_path.replace('.png', '.pdf'), bbox_inches='tight', facecolor='white') |
| plt.close() |
| print(f" Saved: {fig_path}") |
|
|
| np.savez(os.path.join(output_dir, f'probe10_tsne_coords_{model_name.lower()}.npz'), |
| coords=coords, systems=system_labels, n_tissues=n_tissues) |
|
|
|
|
| |
| |
| |
|
|
| def probe_10f_auc_barplot(results_10a, output_dir, model_name): |
| """Bar chart of per-tissue AUC from Probe 10a.""" |
| plt = setup_nature_style() |
|
|
| print(f"\n{'='*60}") |
| print(f"PROBE 10f: Per-tissue AUC Bar Chart ({model_name})") |
| print(f"{'='*60}") |
|
|
| per_tissue = results_10a.get('per_tissue', {}) |
| if not per_tissue: |
| print(" SKIP: No per-tissue results") |
| return |
|
|
| |
| tissues_sorted = sorted(per_tissue.keys(), |
| key=lambda t: per_tissue[t]['auc_mean'], reverse=True) |
|
|
| fig, ax = plt.subplots(figsize=(12, 6)) |
|
|
| x = np.arange(len(tissues_sorted)) |
| aucs = [per_tissue[t]['auc_mean'] for t in tissues_sorted] |
| auc_stds = [per_tissue[t]['auc_std'] for t in tissues_sorted] |
| n_pos = [per_tissue[t]['n_positive'] for t in tissues_sorted] |
|
|
| |
| colors = [] |
| for t in tissues_sorted: |
| sys_found = 'Unknown' |
| for sys_name, members in ORGAN_SYSTEMS.items(): |
| if t in members: |
| sys_found = sys_name |
| break |
| colors.append(SYSTEM_COLORS.get(sys_found, '#999999')) |
|
|
| bars = ax.bar(x, aucs, yerr=auc_stds, capsize=3, color=colors, |
| edgecolor='white', linewidth=0.5, alpha=0.85) |
|
|
| |
| for i, (bar, n) in enumerate(zip(bars, n_pos)): |
| ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + auc_stds[i] + 0.02, |
| f'n={n}', ha='center', va='bottom', fontsize=7, color='#555555') |
|
|
| ax.axhline(y=0.5, color='#CCCCCC', linestyle='--', linewidth=1, label='Chance') |
| ax.set_xticks(x) |
| ax.set_xticklabels(tissues_sorted, rotation=45, ha='right', fontsize=8) |
| ax.set_ylabel('AUC-ROC') |
| ax.set_ylim(0, 1.15) |
| ax.set_title(f'Probe 10a: Per-Tissue Presence Prediction AUC β {model_name}') |
|
|
| |
| from matplotlib.patches import Patch |
| legend_elements = [Patch(facecolor=c, label=s) for s, c in SYSTEM_COLORS.items()] |
| ax.legend(handles=legend_elements, frameon=False, fontsize=7, |
| loc='upper right', ncol=2) |
|
|
| plt.tight_layout() |
| fig_path = os.path.join(output_dir, f'probe10a_auc_barplot_{model_name.lower()}.png') |
| plt.savefig(fig_path, dpi=300, bbox_inches='tight', facecolor='white') |
| plt.savefig(fig_path.replace('.png', '.pdf'), bbox_inches='tight', facecolor='white') |
| plt.close() |
| print(f" Saved: {fig_path}") |
|
|
|
|
| |
| |
| |
|
|
| def main(): |
| parser = argparse.ArgumentParser(description='Probe 10: Tissue-Specific Glycan Presence') |
| parser.add_argument('--model', choices=['v5', 'v6'], required=True) |
| parser.add_argument('--device', default='cuda') |
| parser.add_argument('--output_dir', default=None) |
| parser.add_argument('--data-version', choices=['v1', 'v2'], default='v2', |
| help='Dataset version: v1=189 glycans, v2=243 glycans') |
| parser.add_argument('--well-powered', action='store_true', |
| help='Filter to well-powered tissues only (>=30 positives)') |
| args = parser.parse_args() |
|
|
| model_name = {'v5': 'V5-A', 'v6': 'V6'}[args.model] |
| suffix = f'_{args.data_version}' if args.data_version != 'v1' else '' |
| if args.well_powered: |
| suffix += '_wp' |
| if args.output_dir is None: |
| args.output_dir = str(PROJECT_ROOT / 'bert_v6_contrastive' / 'additional_probes' / |
| f'probe10_results_{args.model}{suffix}') |
| os.makedirs(args.output_dir, exist_ok=True) |
| DATA_PATH = DATA_PATHS[args.data_version] |
|
|
| |
| global TISSUE_NAMES |
| if args.well_powered: |
| active_tissues = [t for t in TISSUE_NAMES if t in WELL_POWERED_TISSUES] |
| print(f"\nUsing WELL-POWERED tissues only: {len(active_tissues)} tissues") |
| else: |
| active_tissues = TISSUE_NAMES[:] |
| TISSUE_NAMES = active_tissues |
|
|
| |
| print(f"\nLoading probe data from {DATA_PATH} (data={args.data_version})...") |
| wurcs_list = [] |
| compositions = [] |
| tissue_rows = [] |
|
|
| with open(DATA_PATH) as f: |
| for row in csv.DictReader(f): |
| wurcs_list.append(row['wurcs']) |
| compositions.append(row.get('composition', '')) |
| tissue_rows.append([int(row.get(t, 0)) for t in TISSUE_NAMES]) |
|
|
| tissue_matrix = np.array(tissue_rows) |
| total = len(wurcs_list) |
| print(f" Glycans: {total}") |
| print(f" Tissues: {len(TISSUE_NAMES)}") |
| print(f" Positive labels: {tissue_matrix.sum()} / {tissue_matrix.size} " |
| f"({tissue_matrix.sum()/tissue_matrix.size*100:.1f}% fill rate)") |
| print(f" Tissues per glycan: mean={tissue_matrix.sum(axis=1).mean():.1f}, " |
| f"median={np.median(tissue_matrix.sum(axis=1)):.0f}") |
|
|
| |
| print(f"\nLoading tokenizer from {VOCAB_PATH}...") |
| tokenizer = WURCSTokenizer(str(VOCAB_PATH)) |
| print(f" Vocab size: {tokenizer.vocab_size}") |
|
|
| ckpt = CHECKPOINTS[args.model] |
| if not ckpt.exists(): |
| print(f"ERROR: Checkpoint not found: {ckpt}") |
| sys.exit(1) |
| model = load_model(str(ckpt), device=args.device) |
|
|
| |
| print(f"\nEmbedding {total} glycans...") |
| embs = batch_cls_embeddings(model, wurcs_list, tokenizer, device=args.device) |
|
|
| |
| import gc, torch |
| del model |
| torch.cuda.empty_cache() |
| gc.collect() |
|
|
| print(f"\nEmbeddings shape: {embs.shape}") |
|
|
| |
| if embs.shape[0] < total: |
| print(f" WARNING: {total - embs.shape[0]} glycans failed tokenization") |
| |
| tissue_matrix = tissue_matrix[:embs.shape[0]] |
| compositions = compositions[:embs.shape[0]] |
|
|
| |
| all_results = {} |
|
|
| |
| results_10a = probe_10a_multilabel(embs, tissue_matrix, args.output_dir, model_name) |
| all_results['10a_multilabel'] = results_10a |
|
|
| |
| all_results['10b_organ_system'] = probe_10b_organ_system( |
| embs, tissue_matrix, args.output_dir, model_name) |
|
|
| |
| all_results['10c_brain'] = probe_10c_brain( |
| embs, tissue_matrix, args.output_dir, model_name) |
|
|
| |
| all_results['10d_breadth'] = probe_10d_breadth( |
| embs, tissue_matrix, args.output_dir, model_name) |
|
|
| |
| probe_10e_visualization(embs, tissue_matrix, compositions, |
| args.output_dir, model_name) |
|
|
| |
| probe_10f_auc_barplot(results_10a, args.output_dir, model_name) |
|
|
| |
| with open(os.path.join(args.output_dir, f'probe10_all_results_{model_name.lower()}.json'), 'w') as f: |
| json.dump(all_results, f, indent=2, default=str) |
|
|
| print(f"\n{'='*60}") |
| print(f"PROBE 10 COMPLETE β {model_name}") |
| print(f"Results: {args.output_dir}") |
| print(f"{'='*60}") |
|
|
|
|
| if __name__ == '__main__': |
| main() |
|
|