bertose-affinose-training-code / code /probes /probe_10_tissue_presence.py
supanthadey1's picture
Add BERTose and AFFINose training code release
1d6f391 verified
Raw
History Blame Contribute Delete
33.5 kB
#!/usr/bin/env python3
"""
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',
}
# Tissues with >=30 positives in v2 dataset (well-powered for classification)
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 system groupings
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']
# Nature-style colors
SYSTEM_COLORS = {
'Cardiovascular': '#D55E00',
'Digestive': '#E69F00',
'Excretory': '#56B4E9',
'Immune': '#009E73',
'Musculoskeletal': '#CC79A7',
'Nervous': '#0072B2',
'Reproductive': '#F0E442',
'Respiratory': '#882255',
}
# ─── Model loading (matches probe_8) ────────────────────────────────
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
# ═══════════════════════════════════════════════════════════════════════
# PROBE 10a: Multi-label Tissue Prediction
# ═══════════════════════════════════════════════════════════════════════
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)
# Use stratified 5-fold or LOO for very small classes
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}")
# Macro averages
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
# ═══════════════════════════════════════════════════════════════════════
# PROBE 10b: Organ System Classification
# ═══════════════════════════════════════════════════════════════════════
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}")
# Assign each glycan to the organ system where it appears most
# (for glycans in multiple systems, assign to MOST SPECIFIC one)
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:
# Assign to the system with highest relative presence
# (count / number of tissues in that system)
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()))}")
# Filter to systems with β‰₯5 samples
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
# ═══════════════════════════════════════════════════════════════════════
# PROBE 10c: Brain vs Non-brain
# ═══════════════════════════════════════════════════════════════════════
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
# ═══════════════════════════════════════════════════════════════════════
# PROBE 10d: Tissue Breadth Prediction (regression)
# ═══════════════════════════════════════════════════════════════════════
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
# ═══════════════════════════════════════════════════════════════════════
# PROBE 10e: t-SNE Visualization
# ═══════════════════════════════════════════════════════════════════════
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)
# Assign primary organ system for coloring
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)
# Run t-SNE
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))
# ─── Panel 1: Organ System ───
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))
# ─── Panel 2: Tissue Breadth (colormap) ───
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}')
# ─── Panel 3: Brain vs Non-brain ───
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)
# ═══════════════════════════════════════════════════════════════════════
# PROBE 10f: Per-tissue AUC bar chart
# ═══════════════════════════════════════════════════════════════════════
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
# Sort by AUC descending
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]
# Color by organ system
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)
# Add sample count annotations
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}')
# Legend for organ systems
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}")
# ═══════════════════════════════════════════════════════════════════════
# Main
# ═══════════════════════════════════════════════════════════════════════
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]
# -- Determine active tissues --
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
# ── Load data ──
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}")
# ── Load model + tokenizer ──
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)
# ── Embed ──
print(f"\nEmbedding {total} glycans...")
embs = batch_cls_embeddings(model, wurcs_list, tokenizer, device=args.device)
# Free GPU
import gc, torch
del model
torch.cuda.empty_cache()
gc.collect()
print(f"\nEmbeddings shape: {embs.shape}")
# Check for tokenization failures (rows that didn't get embedded)
if embs.shape[0] < total:
print(f" WARNING: {total - embs.shape[0]} glycans failed tokenization")
# Truncate data to match
tissue_matrix = tissue_matrix[:embs.shape[0]]
compositions = compositions[:embs.shape[0]]
# ── Run all sub-probes ──
all_results = {}
# 10a: Multi-label
results_10a = probe_10a_multilabel(embs, tissue_matrix, args.output_dir, model_name)
all_results['10a_multilabel'] = results_10a
# 10b: Organ system
all_results['10b_organ_system'] = probe_10b_organ_system(
embs, tissue_matrix, args.output_dir, model_name)
# 10c: Brain vs non-brain
all_results['10c_brain'] = probe_10c_brain(
embs, tissue_matrix, args.output_dir, model_name)
# 10d: Tissue breadth regression
all_results['10d_breadth'] = probe_10d_breadth(
embs, tissue_matrix, args.output_dir, model_name)
# 10e: t-SNE
probe_10e_visualization(embs, tissue_matrix, compositions,
args.output_dir, model_name)
# 10f: AUC bar plot
probe_10f_auc_barplot(results_10a, args.output_dir, model_name)
# Save combined results
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()