bertose-affinose-training-code / code /probes /probe_8_disease_tissue.py
supanthadey1's picture
Add BERTose and AFFINose training code release
1d6f391 verified
Raw
History Blame Contribute Delete
21.9 kB
#!/usr/bin/env python3
"""
Probe 8: Disease/Tissue Embedding Analysis - GlycanBERT
========================================================
Uses glycowork metadata (disease_association, tissue_sample, glycan_type)
to probe what biological knowledge GlycanBERT embeddings encode.
Sub-probes:
8a. Glycan Type Classification (N / O / free / repeat / lipid) [16,556 samples]
8b. Tissue Source Classification (top-10 tissues) [~2,000 samples]
8c. Disease Association (cancer vs non-cancer vs healthy) [~200 samples]
8d. t-SNE Visualization colored by glycan_type + tissue [all 39,873]
Data: bert_v6_contrastive/analysis/probe8_disease_tissue_data.csv
Usage:
python probe_8_disease_tissue.py --model v5 --device cuda
python probe_8_disease_tissue.py --model v6 --device cuda
"""
import sys
import os
import json
import csv
import argparse
import ast
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_PATH = PROJECT_ROOT / 'bert_v6_contrastive' / 'analysis' / 'probe8_disease_tissue_data.csv'
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',
}
GTYPE_COLORS = {
'N': '#0072B2', 'O': '#E69F00', 'free': '#009E73',
'repeat': '#D55E00', 'lipid': '#CC79A7',
}
TISSUE_COLORS = [
'#0072B2', '#E69F00', '#009E73', '#D55E00', '#CC79A7',
'#56B4E9', '#F0E442', '#999999', '#882255', '#44AA99',
]
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'):
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()
print(f" Loaded: {sum(p.numel() for p in model.parameters()):,} params, vocab={vocab_size}, hidden={hidden}")
return model
def batch_cls_embeddings(model, wurcs_list, tokenizer, device='cuda',
batch_size=128, max_len=256):
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)
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)
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)
if (i // batch_size) % 20 == 0:
print(f" Embedded {i+len(batch)}/{len(wurcs_list)} ({errors} errors)")
print(f" Total 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():
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_8a_glycan_type(embs, labels, output_dir, model_name):
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import accuracy_score, f1_score
from sklearn.preprocessing import LabelEncoder
print(f"\n{'='*60}")
print(f"PROBE 8a: Glycan Type Classification ({model_name})")
print(f"{'='*60}")
le = LabelEncoder()
y = le.fit_transform(labels)
classes = le.classes_
print(f" Classes: {list(classes)}")
print(f" Distribution: {dict(zip(classes, np.bincount(y)))}")
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
accs, f1s = [], []
for fold, (train_idx, test_idx) in enumerate(skf.split(embs, y)):
clf = LogisticRegression(max_iter=1000, C=1.0, solver='lbfgs',
multi_class='multinomial')
clf.fit(embs[train_idx], y[train_idx])
preds = clf.predict(embs[test_idx])
acc = accuracy_score(y[test_idx], preds)
f1 = f1_score(y[test_idx], preds, average='macro')
accs.append(acc)
f1s.append(f1)
print(f" Fold {fold+1}: acc={acc:.4f}, F1={f1:.4f}")
mean_acc, std_acc = np.mean(accs), np.std(accs)
mean_f1, std_f1 = np.mean(f1s), np.std(f1s)
print(f"\n RESULT: Accuracy = {mean_acc:.4f} +/- {std_acc:.4f}")
print(f" F1 Macro = {mean_f1:.4f} +/- {std_f1:.4f}")
results = {
'task': 'glycan_type_classification', 'model': model_name,
'n_samples': len(labels), 'n_classes': len(classes),
'classes': list(classes),
'distribution': {str(c): int(n) for c, n in zip(classes, np.bincount(y))},
'accuracy_mean': float(mean_acc), 'accuracy_std': float(std_acc),
'f1_macro_mean': float(mean_f1), 'f1_macro_std': float(std_f1),
'per_fold_accuracy': [float(a) for a in accs],
}
with open(os.path.join(output_dir, f'probe8a_glycan_type_{model_name}.json'), 'w') as f:
json.dump(results, f, indent=2)
return results
def probe_8b_tissue(embs, labels, output_dir, model_name):
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import accuracy_score, f1_score
from sklearn.preprocessing import LabelEncoder
print(f"\n{'='*60}")
print(f"PROBE 8b: Tissue Classification ({model_name})")
print(f"{'='*60}")
counts = Counter(labels)
top_tissues = [t for t, c in counts.most_common(10) if c >= 30]
mask = np.array([l in top_tissues for l in labels])
embs_f = embs[mask]
labels_f = np.array(labels)[mask]
if len(labels_f) < 50:
print(" SKIP: Too few samples for tissue classification")
return {'task': 'tissue_classification', 'model': model_name, 'skipped': True}
le = LabelEncoder()
y = le.fit_transform(labels_f)
classes = le.classes_
print(f" Classes ({len(classes)}): {list(classes)}")
print(f" Samples: {len(labels_f)}")
n_splits = min(5, min(Counter(y).values()))
if n_splits < 2:
print(" SKIP: Not enough samples per class for stratified CV")
return {'task': 'tissue_classification', 'model': model_name, 'skipped': True}
skf = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=42)
accs, f1s = [], []
for fold, (train_idx, test_idx) in enumerate(skf.split(embs_f, y)):
clf = LogisticRegression(max_iter=1000, C=1.0, solver='lbfgs',
multi_class='multinomial')
clf.fit(embs_f[train_idx], y[train_idx])
preds = clf.predict(embs_f[test_idx])
acc = accuracy_score(y[test_idx], preds)
f1 = f1_score(y[test_idx], preds, average='macro')
accs.append(acc)
f1s.append(f1)
print(f" Fold {fold+1}: acc={acc:.4f}, F1={f1:.4f}")
mean_acc, std_acc = np.mean(accs), np.std(accs)
mean_f1, std_f1 = np.mean(f1s), np.std(f1s)
print(f"\n RESULT: Accuracy = {mean_acc:.4f} +/- {std_acc:.4f}")
print(f" F1 Macro = {mean_f1:.4f} +/- {std_f1:.4f}")
results = {
'task': 'tissue_classification', 'model': model_name,
'n_samples': int(len(labels_f)), 'n_classes': int(len(classes)),
'classes': list(classes),
'accuracy_mean': float(mean_acc), 'accuracy_std': float(std_acc),
'f1_macro_mean': float(mean_f1), 'f1_macro_std': float(std_f1),
'per_fold_accuracy': [float(a) for a in accs],
}
with open(os.path.join(output_dir, f'probe8b_tissue_{model_name}.json'), 'w') as f:
json.dump(results, f, indent=2)
return results
def probe_8c_disease(embs_all, disease_labels, wurcs_all, output_dir, model_name):
print(f"\n{'='*60}")
print(f"PROBE 8c: Disease Association ({model_name})")
print(f"{'='*60}")
cancer_keywords = ['cancer', 'carcinoma', 'tumor', 'glioblastoma', 'melanoma',
'leukemia', 'lymphoma', 'sarcoma', 'adenoma', 'myeloma',
'neuroblastoma', 'hepatoblastoma']
labels = []
for d in disease_labels:
if not d:
labels.append('healthy')
elif any(kw in d.lower() for kw in cancer_keywords):
labels.append('cancer')
else:
labels.append('non_cancer_disease')
labels = np.array(labels)
counts = Counter(labels)
print(f" Distribution: {dict(counts)}")
has_disease = labels != 'healthy'
n_disease = has_disease.sum()
print(f" Disease glycans: {n_disease}")
if n_disease < 50:
print(" SKIP: Too few disease glycans for classification")
results = {
'task': 'disease_association', 'model': model_name,
'n_total': int(len(labels)), 'distribution': {str(k): int(v) for k, v in counts.items()},
'skipped': True, 'reason': f'Only {n_disease} disease glycans'
}
with open(os.path.join(output_dir, f'probe8c_disease_{model_name}.json'), 'w') as f:
json.dump(results, f, indent=2)
return results
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import accuracy_score, f1_score, roc_auc_score
disease_idx = np.where(has_disease)[0]
healthy_idx = np.where(~has_disease)[0]
np.random.seed(42)
healthy_sub = np.random.choice(healthy_idx, size=min(len(healthy_idx), len(disease_idx)*3), replace=False)
idx = np.concatenate([disease_idx, healthy_sub])
np.random.shuffle(idx)
X = embs_all[idx]
y = (labels[idx] != 'healthy').astype(int)
print(f" Binary probe: disease={y.sum()}, healthy={len(y)-y.sum()}")
n_splits = min(5, min(Counter(y).values()))
if n_splits < 2:
results = {'task': 'disease_association', 'model': model_name, 'skipped': True}
with open(os.path.join(output_dir, f'probe8c_disease_{model_name}.json'), 'w') as f:
json.dump(results, f, indent=2)
return results
skf = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=42)
accs, f1s, aucs = [], [], []
for fold, (tr, te) in enumerate(skf.split(X, y)):
clf = LogisticRegression(max_iter=1000, C=1.0)
clf.fit(X[tr], y[tr])
preds = clf.predict(X[te])
proba = clf.predict_proba(X[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:
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}+/-{np.std(accs):.4f}, AUC={np.mean(aucs):.4f}+/-{np.std(aucs):.4f}")
results = {
'task': 'disease_association', 'model': model_name,
'n_disease': int(n_disease), 'n_healthy_subsample': int(len(healthy_sub)),
'distribution': {str(k): int(v) for k, v in counts.items()},
'accuracy_mean': float(np.mean(accs)), 'accuracy_std': float(np.std(accs)),
'f1_mean': float(np.mean(f1s)), 'auc_mean': float(np.mean(aucs)),
}
with open(os.path.join(output_dir, f'probe8c_disease_{model_name}.json'), 'w') as f:
json.dump(results, f, indent=2)
return results
def probe_8d_visualization(embs, gtype_labels, tissue_labels, disease_labels,
output_dir, model_name):
plt = setup_nature_style()
from sklearn.manifold import TSNE
print(f"\n{'='*60}")
print(f"PROBE 8d: t-SNE Visualization ({model_name})")
print(f"{'='*60}")
n = len(embs)
if n > 10000:
np.random.seed(42)
idx = np.random.choice(n, 10000, replace=False)
embs_sub = embs[idx]
gtype_sub = np.array(gtype_labels)[idx]
tissue_sub = np.array(tissue_labels)[idx]
disease_sub = np.array(disease_labels)[idx]
else:
embs_sub = embs
gtype_sub = np.array(gtype_labels)
tissue_sub = np.array(tissue_labels)
disease_sub = np.array(disease_labels)
print(f" Running t-SNE on {len(embs_sub)} samples (perplexity=50)...")
tsne = TSNE(n_components=2, perplexity=50, random_state=42, max_iter=1000,
learning_rate='auto', init='pca')
coords = tsne.fit_transform(embs_sub)
fig, axes = plt.subplots(1, 3, figsize=(21, 6))
ax = axes[0]
for gtype in ['N', 'O', 'free', 'repeat', 'lipid']:
mask = gtype_sub == gtype
if mask.sum() > 0:
ax.scatter(coords[mask, 0], coords[mask, 1],
c=GTYPE_COLORS.get(gtype, 'gray'), s=8, alpha=0.5,
label=f'{gtype} ({mask.sum()})', edgecolors='none', rasterized=True)
no_type = gtype_sub == ''
if no_type.sum() > 0:
ax.scatter(coords[no_type, 0], coords[no_type, 1],
c='#DDDDDD', s=4, alpha=0.2, label=f'unlabeled ({no_type.sum()})',
edgecolors='none', rasterized=True)
ax.set_xlabel('t-SNE 1'); ax.set_ylabel('t-SNE 2')
ax.set_title(f'Glycan Type - {model_name}')
ax.legend(frameon=False, markerscale=2, fontsize=8)
ax = axes[1]
tissue_counts = Counter([t for t in tissue_sub if t])
top_tissues = [t for t, _ in tissue_counts.most_common(10)]
for i, tissue in enumerate(top_tissues):
mask = tissue_sub == tissue
if mask.sum() > 0:
ax.scatter(coords[mask, 0], coords[mask, 1],
c=TISSUE_COLORS[i % len(TISSUE_COLORS)], s=12, alpha=0.6,
label=f'{tissue} ({mask.sum()})', edgecolors='none', rasterized=True)
other = np.array([t not in top_tissues for t in tissue_sub])
if other.sum() > 0:
ax.scatter(coords[other, 0], coords[other, 1],
c='#EEEEEE', s=3, alpha=0.15, edgecolors='none', rasterized=True)
ax.set_xlabel('t-SNE 1'); ax.set_ylabel('t-SNE 2')
ax.set_title(f'Tissue Source (top 10) - {model_name}')
ax.legend(frameon=False, markerscale=2, fontsize=7, loc='center left',
bbox_to_anchor=(1.0, 0.5))
ax = axes[2]
cancer_kw = ['cancer', 'carcinoma', 'tumor', 'glioblastoma', 'melanoma',
'leukemia', 'lymphoma']
disease_cats = []
for d in disease_sub:
if not d:
disease_cats.append('healthy')
elif any(kw in d.lower() for kw in cancer_kw):
disease_cats.append('cancer')
else:
disease_cats.append('other_disease')
disease_cats = np.array(disease_cats)
for cat, color, s, alpha in [('healthy', '#DDDDDD', 3, 0.15),
('other_disease', '#E69F00', 25, 0.8),
('cancer', '#D55E00', 30, 0.9)]:
mask = disease_cats == cat
if mask.sum() > 0:
ax.scatter(coords[mask, 0], coords[mask, 1],
c=color, s=s, alpha=alpha,
label=f'{cat} ({mask.sum()})', edgecolors='none', rasterized=True)
ax.set_xlabel('t-SNE 1'); ax.set_ylabel('t-SNE 2')
ax.set_title(f'Disease Association - {model_name}')
ax.legend(frameon=False, markerscale=2, fontsize=8)
plt.suptitle(f'Probe 8: Glycowork Metadata Embedding Analysis - GlycanBERT {model_name}',
fontsize=13, fontweight='bold')
plt.tight_layout()
fig_path = os.path.join(output_dir, f'probe8_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'probe8_tsne_coords_{model_name.lower()}.npz'),
coords=coords, gtype=gtype_sub, tissue=tissue_sub, disease=disease_sub)
def main():
parser = argparse.ArgumentParser()
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('--max_samples', type=int, default=None)
args = parser.parse_args()
model_name = {'v5': 'V5-A', 'v6': 'V6'}[args.model]
if args.output_dir is None:
args.output_dir = str(PROJECT_ROOT / 'bert_v6_contrastive' / 'analysis' /
f'probe8_disease_tissue_{args.model}')
os.makedirs(args.output_dir, exist_ok=True)
print(f"\nLoading probe data from {DATA_PATH}...")
wurcs_list, iupac_list = [], []
gtype_labels, tissue_labels, disease_labels = [], [], []
with open(DATA_PATH) as f:
for row in csv.DictReader(f):
wurcs_list.append(row['wurcs'])
iupac_list.append(row.get('iupac', ''))
gtype_labels.append(row.get('glycan_type', ''))
tissue_labels.append(row.get('tissue', '').split(';')[0] if row.get('tissue') else '')
disease_labels.append(row.get('disease', ''))
total = len(wurcs_list)
if args.max_samples and args.max_samples < total:
np.random.seed(42)
idx = np.random.choice(total, args.max_samples, replace=False)
wurcs_list = [wurcs_list[i] for i in idx]
gtype_labels = [gtype_labels[i] for i in idx]
tissue_labels = [tissue_labels[i] for i in idx]
disease_labels = [disease_labels[i] for i in idx]
total = args.max_samples
print(f" Total: {total:,}")
print(f" glycan_type: {sum(1 for g in gtype_labels if g):,}")
print(f" tissue: {sum(1 for t in tissue_labels if t):,}")
print(f" disease: {sum(1 for d in disease_labels if d):,}")
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}")
all_results = {}
gtype_mask = np.array([bool(g) for g in gtype_labels])
if gtype_mask.sum() > 100:
all_results['8a_glycan_type'] = probe_8a_glycan_type(
embs[gtype_mask], np.array(gtype_labels)[gtype_mask],
args.output_dir, model_name)
tissue_mask = np.array([bool(t) for t in tissue_labels])
if tissue_mask.sum() > 50:
all_results['8b_tissue'] = probe_8b_tissue(
embs[tissue_mask], np.array(tissue_labels)[tissue_mask],
args.output_dir, model_name)
all_results['8c_disease'] = probe_8c_disease(
embs, disease_labels, wurcs_list, args.output_dir, model_name)
probe_8d_visualization(embs, gtype_labels, tissue_labels, disease_labels,
args.output_dir, model_name)
with open(os.path.join(args.output_dir, f'probe8_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 8 COMPLETE - {model_name}")
print(f"Results: {args.output_dir}")
print(f"{'='*60}")
if __name__ == '__main__':
main()