Deep Learning Predicts Hematopoietic Stem Cell Aging From 3D Chromatin Images.
The 21 matches
- [1] § Methods › ChromAgeNet Architecture ↔ methods/chromagenet.py, lines 8 to 80 · score 0.93 · leaky ReLU, separable convolutions, Batch Normalization, depthwise, global, swish · Report a mismatch
- [2] § Methods › Explainability Analyses ↔ methods/explainability.py, lines 427 to 462 · score 0.82 · L2 normalized gradients, gradient ascent, convolutional filter, Activation, maximization, network · Report a mismatch
- [3] § Methods › Model Calibration ↔ notebooks/04_chromagenet_training_crossvalidation.ipynb, lines 388 to 531 · score 0.81 · Platt Scaling, isotonic regression, Beta Calibration, calibrated model, post, logistic · Report a mismatch
- [4] § Methods › ML Analyses of Chromatin‐Extracted Features ↔ notebooks/09_radiomics_classification.ipynb, lines 483 to 515 · score 0.80 · XGBoost, Logistic Regression, Random Forests, Cal, RFE, ML · Report a mismatch
- [5] § Methods › ML Analyses of Chromatin‐Extracted Features ↔ notebooks/10_radiomics_classification_binarized.ipynb, lines 464 to 497 · score 0.80 · XGBoost, Logistic Regression, Random Forests, Cal, RFE, ML · Report a mismatch
- [6] § Methods › ChromAgeNet Training and Inference ↔ methods/explainability.py, lines 313 to 361 · score 0.76 · Receiver Operating Characteristic, ROC curve, decision thresholds, FPR, TPR, AUC · Report a mismatch
- [7] § Methods › ChromAgeNet Training and Inference ↔ methods/utils.py, lines 338 to 371 · score 0.75 · Receiver Operating Characteristic, ROC curve, decision thresholds, FPR, TPR, AUC · Report a mismatch
- [8] § Methods › ML Analyses of Chromatin‐Extracted Features ↔ notebooks/09_radiomics_classification.ipynb, lines 731 to 813 · score 0.75 · precision recall curves, TreeExplainer, confusion matrices, exPlanations, SHAP, UMAP · Report a mismatch
- [9] § Results › Deep Learning Discriminates Young and Aged HSCs ↔ notebooks/04_chromagenet_training_crossvalidation.ipynb, lines 388 to 531 · score 0.74 · beta calibrated model, Precision Recall curve, ROC curve, Confusion matrices, uncalibrated, CV · Report a mismatch
- [10] § Methods › ML Analyses of Chromatin‐Extracted Features ↔ notebooks/10_radiomics_classification_binarized.ipynb, lines 652 to 731 · score 0.73 · precision recall curves, TreeExplainer, confusion matrices, exPlanations, SHAP, ML · Report a mismatch
- [11] § Methods › Image Preprocessing and Segmentation ↔ methods/czi_preprocessing.py, lines 837 to 907 · score 0.71 · intensities outside, nucleus mask, trimming, padding, zeroed, preprocessed · Report a mismatch
- [12] § Results › Comparison With Models Trained on Chromatin‐Extracted Features ↔ notebooks/10_radiomics_classification_binarized.ipynb, lines 464 to 497 · score 0.69 · XGBoost, logistic regression, Random Forest, binarizing, RF, calibrated · Report a mismatch
- [13] § Methods › ChromAgeNet Training and Inference ↔ notebooks/04_chromagenet_training_crossvalidation.ipynb, lines 246 to 342 · score 0.67 · initial weights, Model weights, JIT, epochs, loss, optimized · Report a mismatch
- [14] § Results › Comparison With Models Trained on Chromatin‐Extracted Features ↔ notebooks/09_radiomics_classification.ipynb, lines 483 to 515 · score 0.67 · XGBoost, logistic regression, Random Forest, RF, calibrated, selection · Report a mismatch
- [15] § Methods › Image Preprocessing and Segmentation ↔ methods/czi_preprocessing.py, lines 729 to 751 · score 0.67 · binary closing, Otsu, hole, hysteresis, segmentation, smoothing · Report a mismatch
- [16] § Methods › ChromAgeNet Training and Inference ↔ notebooks/04_chromagenet_training_crossvalidation.ipynb, lines 68 to 75 · score 0.64 · random rotations, flips, horizontal, vertical, augmentation, filling · Report a mismatch
- [17] § Results › Chromatin‐Extracted Features Inform on Stem Cell Aging ↔ notebooks/11_radiomics_compounds_comparison.ipynb, lines 124 to 140 · score 0.61 · axis length, sum entropy, zone, median, Wavelet, LoG · Report a mismatch
- [18] § Methods › Explainability Analyses ↔ notebooks/07_chromagenet_explainability.ipynb, lines 355 to 378 · score 0.58 · max_evals, batch_size, SHAP, predictions, young, aged · Report a mismatch
- [19] § Methods › Explainability Analyses ↔ notebooks/07_chromagenet_explainability.ipynb, lines 28 to 110 · score 0.57 · distance transform, Shannon entropy, maps, intensities · Report a mismatch
- [20] § Methods › Model Calibration ↔ methods/chromagenet.py, lines 83 to 170 · score 0.56 · calibrated probabilities, probabilistic predictions, Error, classifies, model, ChromAgeNet · Report a mismatch
- [21] § Methods › Explainability Analyses ↔ notebooks/07_chromagenet_explainability.ipynb, lines 539 to 592 · score 0.53 · HSIC, patch, clipped, space, Occlusion, attribution · Report a mismatch
Paper
Loaded from Europe PMC by your browser; OSCR does not store it: doi.org · Europe PMC
The paper is loaded when this pane is shown.
The authors' code
Jupyter notebook · 720 lines · 21 KB · MIT · 4 matches
- # %% [markdown]
- # Tensorflow and Keras were installed following the official tutorial:
- # https://www.tensorflow.org/install/pip
- # %%
- import tensorflow as tf
- from tensorflow import keras
- from tensorflow.keras import layers
- import matplotlib.pyplot as plt
- import numpy as np
- import pandas as pd
- import seaborn as sns
- import sys
- sys.path.append("..")
- import methods.explainability as ex
- from methods import utils
- from methods.chromagenet import CalibratedModel, make_model
- from sklearn.metrics import brier_score_loss
- from sklearn.isotonic import IsotonicRegression
- from sklearn.linear_model import LogisticRegression
- from betacal import BetaCalibration
- physical_devices = tf.config.list_physical_devices("GPU")
- print("Num GPUs:", len(physical_devices))
- # %%
- pal = utils.get_class_palette()
- mic_pal = utils.get_microscopist_palette()
- # %%
- NORM = "none"
- RES = 0.05
- k_cv = 5
- IMGS_DIR = f"../../data/preprocessed/2D_res={RES}_norm={NORM}_{k_cv}fold_withDMSO"
- CNN_DENSE_FILTS = (256, 64, 16)
- IMAGE_SIZE = (128, 128)
- BATCH_SIZE = 64
- CHANNEL_MODE = "grayscale"
- N_OUTPUT_UNITS = 1
- LR_SCHED = None
- OPTIMIZER = "AdamW"
- COMMENT = "testing baseline"
- EPOCHS = 50
- LR = 1e-4
- SEED = 2023
- if CHANNEL_MODE == "rgb":
- channels = (3,)
- elif CHANNEL_MODE == "grayscale":
- channels = (1,)
- if N_OUTPUT_UNITS == 1:
- OUTPUT_FUNC = "sigmoid"
- LOSS_FUNC = tf.keras.losses.BinaryCrossentropy(
- label_smoothing=0.1,
- )
- LABEL_MODE = "binary"
- elif N_OUTPUT_UNITS == 2:
- OUTPUT_FUNC = "softmax"
- LOSS_FUNC = "categorical_crossentropy"
- LABEL_MODE = "categorical"
- # %%
- data_augmentation = keras.Sequential(
- [
- layers.RandomFlip("horizontal_and_vertical"),
- layers.RandomRotation(factor=0.5, fill_mode="constant"),
- layers.RandomContrast(factor=0.05),
- ]
- )
- # %%
- n_0 = 0
- n_1 = 1
- for k in range(1, k_cv + 1):
- train_ds = None
- print(f"Starting fold {k}")
- ds = tf.keras.utils.image_dataset_from_directory(
- f"{IMGS_DIR}/fold_{k}/",
- color_mode=CHANNEL_MODE,
- labels="inferred",
- label_mode=LABEL_MODE,
- interpolation="bilinear",
- seed=SEED,
- image_size=IMAGE_SIZE,
- batch_size=BATCH_SIZE,
- shuffle=False,
- )
- for _, y in ds:
- unique, counts = np.unique(y.numpy(), return_counts=True)
- count_dict = dict(zip(unique, counts))
- n_0 += count_dict.get(0, 0) # Add count of class 0
- n_1 += count_dict.get(1, 0) # Add count of class 1
- print(f"Fold {k}: Class 0 = {n_0}, Class 1 = {n_1}")
- # Final total counts
- print(f"Total images: Class 0 = {n_0}, Class 1 = {n_1}")
- # %%
- testing_ds = tf.keras.utils.image_dataset_from_directory(
- f"{IMGS_DIR}/fold_1/",
- color_mode=CHANNEL_MODE,
- labels="inferred",
- label_mode=LABEL_MODE,
- interpolation="bilinear",
- seed=SEED,
- image_size=IMAGE_SIZE,
- batch_size=BATCH_SIZE,
- shuffle=True,
- )
- # %%
- plt.figure(figsize=(10, 10))
- for images, labels in testing_ds.take(1):
- young_images = images[labels.numpy().ravel() == 1.]
- for i in range(9):
- print(np.max(young_images[i]))
- print(np.min(young_images[i]))
- ax = plt.subplot(3, 3, i + 1)
- plt.imshow(young_images[i].numpy().astype("uint8"))
- plt.axis("off")
- plt.tight_layout()
- # %%
- plt.figure(figsize=(10, 10))
- for images, labels in testing_ds.take(1):
- aged_images = images[labels.numpy().ravel() == 0.]
- for i in range(9):
- print(np.max(aged_images[i]))
- print(np.min(aged_images[i]))
- ax = plt.subplot(3, 3, i + 1)
- plt.imshow(aged_images[i].numpy().astype("uint8"))
- plt.axis("off")
- plt.tight_layout()
- # %%
- # Apply `data_augmentation` to the training images.
- testing_ds = testing_ds.map(
- lambda img, label: (data_augmentation(img), label),
- num_parallel_calls=tf.data.AUTOTUNE,
- )
- plt.figure(figsize=(10, 10))
- for images, labels in testing_ds.take(1):
- for i in range(9):
- print(np.max(images[i]))
- print(np.min(images[i]))
- ax = plt.subplot(3, 3, i + 1)
- plt.imshow(images[i].numpy())
- plt.title(int(labels[i]))
- plt.axis("off")
- # %%
- # Build ChromAgeNet model
- model = make_model(
- input_shape=IMAGE_SIZE + channels,
- dense_units=CNN_DENSE_FILTS,
- n_output_units=N_OUTPUT_UNITS,
- output_activation=OUTPUT_FUNC,
- )
- # %%
- model.summary()
- # %%
- # keras.utils.plot_model(model, show_shapes=True)
- # %%
- os.chdir(f"../../../notebooks")
- # %%
- import os
- for k in range(1, k_cv + 1):
- os.makedirs(f"../models/checkpoints/fold_{k}", exist_ok=True)
- # %%
- callbacks = [
- keras.callbacks.ModelCheckpoint(
- "{epoch}-{val_accuracy:.3f}.weights.h5",
- monitor="val_auc",
- save_weights_only=True,
- save_best_only=False,
- )
- ]
- lr_obj = None
- if LR_SCHED == "cosine_decay":
- lr_obj = keras.optimizers.schedules.CosineDecay(
- initial_learning_rate=LR, decay_steps=100, alpha=1e-1
- )
- if lr_obj is None:
- lr_obj = LR
- if OPTIMIZER == "AdamW":
- optim = keras.optimizers.AdamW(
- learning_rate=lr_obj, use_ema=True
- )
- elif OPTIMIZER == "Adam":
- optim = keras.optimizers.Adam(learning_rate=lr_obj, use_ema=True)
- elif OPTIMIZER == "SGD":
- optim = keras.optimizers.SGD(learning_rate=lr_obj, momentum=0.6)
- metrics = [
- "accuracy",
- tf.keras.metrics.AUC(),
- tf.keras.metrics.Precision(),
- tf.keras.metrics.Recall(),
- tf.keras.metrics.F1Score(),
- ]
- def plot_loss(hist):
- plt.plot(hist["loss"])
- plt.plot(hist["val_loss"])
- plt.title("Training Progress")
- plt.ylabel("Loss")
- plt.xlabel("Epochs")
- plt.legend(["train_loss", "val_loss"], loc="upper left")
- plt.show()
- def plot_acc(hist):
- plt.plot(hist["accuracy"])
- plt.plot(hist["val_accuracy"])
- plt.title("Training Progress")
- plt.ylabel("Accuracy")
- plt.xlabel("Epochs")
- plt.legend(["train_acc", "val_acc"], loc="upper left")
- plt.show()
- # %%
- sns.set_style("whitegrid")
- train_ds = None
- IMGS_DIR = f"../../../../data/preprocessed/2D_res={RES}_norm={NORM}_{k_cv}fold_withDMSO"
- model.compile(
- optimizer=optim,
- loss=LOSS_FUNC,
- metrics=metrics,
- jit_compile=True,
- )
- initial_weights = model.get_weights()
- for k in range(1, k_cv + 1):
- train_ds = None
- os.chdir(f"../models/checkpoints/fold_{k}")
- print("Saving model weights at: ", os.getcwd())
- print(f"Starting fold {k}")
- for i in range(1, k_cv + 1):
- if i == k:
- val_ds = tf.keras.utils.image_dataset_from_directory(
- f"{IMGS_DIR}/fold_{i}/",
- color_mode=CHANNEL_MODE,
- labels="inferred",
- label_mode=LABEL_MODE,
- interpolation="bilinear",
- seed=SEED,
- image_size=IMAGE_SIZE,
- batch_size=BATCH_SIZE,
- shuffle=False,
- )
- continue
- if train_ds:
- train_ds = train_ds.concatenate(
- tf.keras.utils.image_dataset_from_directory(
- f"{IMGS_DIR}/fold_{i}/",
- color_mode=CHANNEL_MODE,
- labels="inferred",
- label_mode=LABEL_MODE,
- interpolation="bilinear",
- seed=SEED,
- image_size=IMAGE_SIZE,
- batch_size=BATCH_SIZE,
- shuffle=True,
- )
- )
- else:
- train_ds = tf.keras.utils.image_dataset_from_directory(
- f"{IMGS_DIR}/fold_{i}/",
- color_mode=CHANNEL_MODE,
- labels="inferred",
- label_mode=LABEL_MODE,
- interpolation="bilinear",
- seed=SEED,
- image_size=IMAGE_SIZE,
- batch_size=BATCH_SIZE,
- shuffle=True,
- )
- n_train_ims = train_ds.cardinality().numpy() * BATCH_SIZE
- print(n_train_ims)
- train_ds = train_ds.unbatch().shuffle(10000).batch(BATCH_SIZE)
- # Prefetching samples in GPU memory helps maximize GPU utilization.
- train_ds = train_ds.cache().prefetch(tf.data.AUTOTUNE)
- val_ds = val_ds.cache().prefetch(tf.data.AUTOTUNE)
- train_ds = train_ds.map(
- lambda img, label: (data_augmentation(img), label),
- num_parallel_calls=tf.data.AUTOTUNE,
- )
- model.set_weights(initial_weights)
- history = model.fit(
- train_ds,
- epochs=EPOCHS,
- callbacks=callbacks,
- validation_data=val_ds,
- shuffle=True,
- sample_weight=None,
- )
- hist = utils.fix_repeated_metric_names(model.history.history)
- hist_df = ex.get_history_df(hist)
- ex.plot_training(hist_df)
- plot_loss(model.history.history)
- plot_acc(model.history.history)
- model.evaluate(val_ds)
- os.chdir(f"../../../notebooks")
- model.save(f"../models/res{RES}_norm{NORM}_fold{k}.keras")
- # %%
- best_fold_models = [
- "../models/checkpoints/fold_1/44-0.692.weights.h5",
- "../models/checkpoints/fold_2/48-0.639.weights.h5",
- "../models/checkpoints/fold_3/50-0.678.weights.h5",
- "../models/checkpoints/fold_4/43-0.658.weights.h5",
- "../models/checkpoints/fold_5/42-0.651.weights.h5",
- ]
- def find_optimal_t(tpr, fpr, thresholds):
- # Find optimal threshold using Youden’s J statistic
- J_scores = tpr - fpr
- best_idx = np.argmax(J_scores)
- return thresholds[best_idx]
- def plot_class_distplot(df, x, hue, threshold, palette):
- sns.displot(df, x=x, hue=hue, binwidth=0.04,
- palette=pal, height=3, kde=True)
- # Add a vertical line at the best threshold
- plt.axvline(threshold, color='red', linestyle='--', linewidth=2)
- plt.xlabel("Averaged probability (whole nucleus)")
- plt.ylabel("Number of nuclei")
- plt.xlim(0, 1)
- plt.show()
- def compute_ece(y_true, y_probs, n_bins=10):
- bins = np.linspace(0, 1, n_bins + 1) # Create bin edges
- bin_indices = np.digitize(y_probs, bins) - 1 # Assign samples to bins
- ece = 0.0
- for i in range(n_bins):
- mask = bin_indices == i
- if np.sum(mask) > 0:
- acc = np.mean(y_true[mask]) # Fraction of positives in bin
- conf = np.mean(y_probs[mask]) # Mean predicted probability in bin
- ece += np.abs(acc - conf) * (np.sum(mask) / len(y_probs)) # Weighted average
- return ece
- # %%
- import sklearn.metrics as skmetrics
- aucs_df = pd.DataFrame(columns=["TPR", "FPR", "fold"])
- pr_df = pd.DataFrame(columns=["Precision", "Recall", "fold"])
- conf_2D_df = pd.DataFrame(columns=["Confusion", "index", "fold"])
- conf_3D_df = pd.DataFrame(columns=["Confusion", "index", "fold"])
- cv_3D_df = pd.DataFrame()
- cv_2D_df = pd.DataFrame()
- eces, cal_eces = [], []
- briers, cal_briers = [], []
- IMGS_DIR = f"../../data/preprocessed/2D_res={RES}_norm={NORM}_{k_cv}fold_withDMSO"
- for fold in range(1, k_cv + 1):
- train_ds = None
- threshold_list = []
- print(f"Starting fold {fold}")
- model.load_weights(best_fold_models[fold-1])
- val_ids, val_ds = utils.dataset_from_partition(
- IMGS_DIR,
- f"fold_{fold}",
- CHANNEL_MODE,
- LABEL_MODE,
- IMAGE_SIZE,
- BATCH_SIZE,
- SEED,
- )
- val_df = ex.get_output_df_voting(val_ids, val_ds, model, LABEL_MODE)
- val_df["class"] = val_df["label"].map({0: 'aged', 1: 'young'})
- val_df["fold"] = fold
- y = np.concatenate([y for _, y in val_ds], axis=0)
- print(val_ds.cardinality().numpy() * BATCH_SIZE)
- y_prob = model.predict(val_ds)
- # Compute ROC curve
- fpr, tpr, thresholds = skmetrics.roc_curve(y, y_prob)
- aucs_df = pd.concat([aucs_df, pd.DataFrame(
- {"TPR":tpr, "FPR":fpr, "fold":fold})]
- )
- best_t = find_optimal_t(tpr, fpr, thresholds)
- threshold_list.append(best_t)
- # Compute PR curve
- precision, recall, thresholds = skmetrics.precision_recall_curve(y, y_prob)
- pr_df = pd.concat([pr_df, pd.DataFrame(
- {"Precision":precision, "Recall":recall, "fold":fold})]
- )
- # Compute confusion matrices
- y_pred = (y_prob > best_t).astype(int)
- conf_mat_2D = skmetrics.confusion_matrix(y, y_pred, normalize="true").flatten()
- conf_2D_df = pd.concat([conf_2D_df, pd.DataFrame(
- {"Confusion":conf_mat_2D, "index":[1, 2, 3, 4], "fold":fold})]
- )
- cv_2D_df = pd.concat([cv_2D_df,
- pd.DataFrame(
- {"nuc_id": np.concatenate([y for y in val_ids], axis=0),
- "label": y.ravel(),
- "pred": y_pred.ravel(),
- "y_prob": y_prob.ravel(),
- "fold": fold,
- })]
- )
- val_df["soft_pred"] = (val_df["mean_prob"] >= best_t).astype(int)
- conf_mat_3D = skmetrics.confusion_matrix(val_df["label"], val_df["soft_pred"], normalize="true").flatten()
- conf_3D_df = pd.concat([conf_3D_df, pd.DataFrame(
- {"Confusion":conf_mat_3D, "index":[1, 2, 3, 4], "fold":fold})]
- )
- cv_3D_df = pd.concat([cv_3D_df, val_df])
- ex.plot_auc(val_df["label"], val_df["mean_prob"], threshold=best_t)
- ex.plot_conf_mat(val_df["label"], val_df["mean_prob"], threshold=best_t)
- plot_class_distplot(val_df, x="mean_prob", hue="class", threshold=best_t, palette=pal)
- ir = IsotonicRegression(out_of_bounds = 'clip')
- lr = LogisticRegression(solver='lbfgs')
- bc = BetaCalibration(parameters="abm")
- #calibrators = [ir, lr, bc]
- calibrators = [bc]
- uncal_probs = model.predict(val_ds)
- test_labels = np.concatenate([y for _, y in val_ds], axis=0)
- cal_error = compute_ece(test_labels, uncal_probs, n_bins=10)
- bs = brier_score_loss(test_labels, uncal_probs)
- print("Pre-Calibration Error:", cal_error)
- print("Pre-Calibration Brier Score:", bs)
- eces.append(cal_error)
- briers.append(bs)
- nuc_prob_outputs = [val_df["mean_prob"]]
- plot_probs = [uncal_probs]
- for calibrator in calibrators:
- print(f"Calibrating with {calibrator}")
- calibrator.fit(uncal_probs.ravel(), test_labels.ravel())
- cal_probs = calibrator.predict(uncal_probs)
- cal_error = compute_ece(test_labels, cal_probs, n_bins=10)
- bs = brier_score_loss(test_labels, cal_probs)
- print("Post-Calibration Error:", cal_error)
- print("Post-Calibration Brier Score:", bs)
- cal_eces.append(cal_error)
- cal_briers.append(bs)
- cal_val_df = ex.get_output_df_voting(val_ids, val_ds, model,
- LABEL_MODE, thresh=0.5,
- cal_model=calibrator)
- cal_val_df["class"] = cal_val_df["label"].map({0: 'aged', 1: 'young'})
- nuc_prob_outputs.append(cal_val_df["mean_prob"])
- plot_probs.append(cal_probs)
- # Compute ROC curve
- cal_fpr, cal_tpr, cal_thresholds = skmetrics.roc_curve(y, cal_probs)
- best_cal_t = find_optimal_t(cal_tpr, cal_fpr, cal_thresholds)
- threshold_list.append(best_cal_t)
- ex.plot_conf_mat(cal_val_df["label"], cal_val_df["mean_prob"], threshold=best_cal_t)
- plot_class_distplot(cal_val_df, x="mean_prob",
- hue="class", threshold=best_cal_t, palette=pal)
- legends = ["Uncalibrated", "Beta Calib."] # "Isotonic Reg.", "Platt Scaling",
- labels = [cal_val_df["label"], cal_val_df["label"], cal_val_df["label"]]
- utils.plot_multiple_auc(labels, nuc_prob_outputs, legends, threshold_list)
- # 5. Create the integrated model
- calibrated_model = CalibratedModel(model, calibrator)
- # 6. Save the integrated model
- save_path = f"../models/chromagenet/calmodel_fold_{fold}"
- calibrated_model.save(save_path)
- utils.plot_calibration_curve(test_labels, plot_probs, legends)
- # %%
- print(np.mean(eces), np.std(eces))
- print(np.mean(cal_eces), np.std(cal_eces))
- print(np.mean(briers), np.std(briers))
- print(np.mean(cal_briers), np.std(cal_briers))
- # %%
- from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score
- def build_performance_table(df, pred_col="soft_pred", prob_col="mean_prob"):
- # Assuming your DataFrame is named df
- metrics_per_fold = {}
- for fold, data in df.groupby("fold"):
- y_true = data["label"] # Ground truth labels
- y_pred = data[pred_col] # Model predictions (binary)
- y_scores = data[prob_col] # Probability scores
- metrics_per_fold[fold] = {
- "accuracy": accuracy_score(y_true, y_pred),
- "precision": precision_score(y_true, y_pred, zero_division=0),
- "recall": recall_score(y_true, y_pred),
- "AUC": roc_auc_score(y_true, y_scores),
- "F1": f1_score(y_true, y_pred),
- }
- # Convert results to a DataFrame
- metrics_df = pd.DataFrame.from_dict(metrics_per_fold, orient="index")
- # Compute mean, std, and 95% CI
- sum_df = metrics_df.agg(["mean", "std"])
- z_score = 1.96
- ci_lower = sum_df.loc["mean"] - (z_score * sum_df.loc["std"] / np.sqrt(k_cv))
- ci_upper = sum_df.loc["mean"] + (z_score * sum_df.loc["std"] / np.sqrt(k_cv))
- sum_df.loc["CI"] = [(round(low, 3), round(up, 3)) for low, up in zip(ci_lower, ci_upper)]
- metrics_df = pd.concat([metrics_df, sum_df])
- return metrics_df
- # %%
- sum_2d_df = build_performance_table(cv_2D_df, pred_col="pred", prob_col="y_prob")
- sum_2d_df
- # %%
- sum_2d_df.to_latex(float_format="%.3f")
- # %%
- soft_df = build_performance_table(cv_3D_df, pred_col="soft_pred")
- soft_df
- # %%
- hard_df = build_performance_table(cv_3D_df, pred_col="hard_preds")
- hard_df
- # %%
- soft_df.to_latex(float_format="%.3f")
- # %%
- from sklearn.metrics import precision_recall_curve, average_precision_score, roc_curve, auc
- sns.set_theme(style="whitegrid")
- plt.figure(figsize=(5, 4))
- optimal_thresholds = {} # Store optimal thresholds per fold
- for fold, group in cv_df.groupby("fold"):
- print(f"Processing fold {fold}")
- y_true = group["label"] # True labels
- y_prob = group["mean_prob"] # Soft voting probabilities
- # Compute Precision-Recall curve
- precision, recall, thresholds = precision_recall_curve(y_true, y_prob)
- # Compute F1-score for each threshold
- f1_scores = (2 * precision * recall) / (precision + recall + 1e-6) # Avoid division by zero
- best_idx = np.argmax(f1_scores)
- best_threshold = thresholds[best_idx] if best_idx < len(thresholds) else thresholds[-1]
- print(f"Optimal threshold for fold {fold}: {best_threshold:.3f}")
- optimal_thresholds[fold] = best_threshold # Store best threshold
- # Compute AUC-PR for this fold
- auc_pr = average_precision_score(y_true, y_prob)
- # Plot Precision-Recall curve using Seaborn
- sns.lineplot(x=recall, y=precision, label=f'Fold {fold} (AUC-PR = {auc_pr:.2f})')
- # Mark the optimal threshold point without adding it to the legend
- sns.scatterplot(x=[recall[best_idx]], y=[precision[best_idx]], color='red', s=60, zorder=3)
- # Formatting
- sns.despine()
- plt.xlabel("Recall", fontsize=12)
- plt.ylabel("Precision", fontsize=12)
- plt.title("Precision-Recall Curves per Fold", fontsize=14)
- plt.legend(loc="best", fontsize=9)
- plt.show()
- # Print optimal thresholds per fold
- print("Optimal thresholds per fold:", optimal_thresholds)
- # %%
- sns.set_theme(style="whitegrid")
- plt.figure(figsize=(5, 4))
- for fold, group in cv_df.groupby("fold"):
- y_true = group["label"] # True labels
- y_prob = group["mean_prob"] # Soft voting probabilities
- # Compute ROC curve and AUC
- fpr, tpr, thresholds = roc_curve(y_true, y_prob)
- roc_auc = auc(fpr, tpr)
- # Find optimal threshold using Youden’s J statistic
- J_scores = tpr - fpr
- best_idx = np.argmax(J_scores)
- best_threshold = thresholds[best_idx]
- # Plot per fold
- sns.lineplot(x=fpr, y=tpr, label=f'Fold {fold} (AUC = {roc_auc:.2f})')
- sns.scatterplot(x=[fpr[best_idx]], y=[tpr[best_idx]], color='red', s=60, zorder=3, label="_nolegend_")
- # Plot chance line
- plt.plot([0, 1], [0, 1], 'k--', label="Random Guessing (AUC = 0.50)")
- # Formatting
- plt.xlabel("False Positive Rate (FPR)")
- plt.ylabel("True Positive Rate (TPR)")
- plt.title("ROC Curves per Fold")
- plt.legend(loc="best", fontsize=9)
- # Show plot
- plt.show()
- # %%
- def plot_conf_mat(conf_df, title="Confusion matrix"):
- summ_df = conf_df.groupby("index")["Confusion"].agg(["mean", "std"]).reset_index()
- conf_plot = summ_df["mean"].to_numpy().reshape((2, 2))
- annot = np.array([
- f"{m:.3f}% \n ± {s:.3f}" for m, s in zip(summ_df["mean"], summ_df["std"])
- ]).reshape((2, 2))
- plt.figure(figsize=(4, 3))
- sns.heatmap(conf_plot, annot=annot, fmt='', cmap="Blues")
- plt.xticks([0.5, 1.5], ["Aged", "Young"])
- plt.yticks([0.5, 1.5], ["Aged", "Young"])
- plt.title(title)
- plt.show()
- plot_conf_mat(conf_2D_df, title="Confusion matrix (2D XY slide)")
- # %%
- plot_conf_mat(conf_3D_df, title="Confusion matrix (whole 3D nucleus)")
- # %%
- plt.figure(figsize=(4, 3))
- sns.lineplot(data=aucs_df, x="FPR", y="TPR", hue="fold")
- plt.plot([0, 1], [0, 1], linestyle="--", color="gray", label="Random (AUC=0.5)")
- plt.show()
- # %%
- plt.figure(figsize=(4, 3))
- sns.lineplot(data=pr_df, x="Recall", y="Precision", hue="fold")
- plt.ylim((0.4, 1))
- baseline = sum(y) / len(y)
- plt.axhline(y=baseline, color="gray", linestyle="--", label="Random Baseline")
- plt.show()
- # %%
- train_df = ex.get_output_df_voting(train_ids, train_ds, model, LABEL_MODE)
- val_df = ex.get_output_df_voting(val_ids, val_ds, model, LABEL_MODE)
- # %%
- dataset_names = ["train", "val"]
- dataframes = [train_df, val_df]
- thresholds = np.arange(0.3, 0.8, 0.05)
- thresh_df = ex.plot_metrics(dataframes, dataset_names, thresholds)
- for m_name in ["acc", "precision", "recall", "f1"]:
- sns.lineplot(x="threshold", y=m_name, hue="dataset", data=thresh_df)
- plt.title(m_name)
- plt.show()
04_chromagenet_training_crossvalidation.ipynb at commit a07bfc0, under MIT · at the source
Overview
- Universitat Pompeu Fabra (UPF) Barcelona Spain
- ISGlobal Barcelona Spain
- Regenerative Medicine Program The Bellvitge Institute for Biomedical Research (IDIBELL) Barcelona Spain
- Program for Advancing the Clinical Translation of Regenerative Medicine of Catalonia, P‐CMR[C] Barcelona Spain
- Center for Networked Biomedical Research on Bioengineering, Biomaterials and Nanomedicine (CIBER‐BBN) Madrid Spain
- The Catalan Institution for Research and Advanced Studies (ICREA) Barcelona Spain
- Barcelona Supercomputing Center (BSC) Barcelona Spain
Abstract
The functional decline of the hematopoietic system during aging affects organismal function and contributes to reduced healthspan. Quantifying hematopoietic aging holds great scientific and clinical relevance. Alterations in chromatin architecture are a well‐established hallmark of aging that encode rich and informative signatures of the aging process, yet they remain largely unexplored as quantitative markers. Here, we present an interpretable deep learning approach based on convolutional neural networks, ChromAgeNet, that learns changes in the spatial features of chromatin architecture upon aging of hematopoietic stem cells (HSCs). We trained our algorithm on 3D microscope images of DAPI‐stained HSC nuclei to discriminate between young and aged murine HSCs, achieving an AUROC of 0.77 ± 0.03. This approach outperforms classical machine learning models trained on handcrafted chromatin features from the same dataset. We then applied explainable artificial intelligence techniques, identifying chromatin entropy, peripheral heterochromatin, and chromatin condensates as predictive markers. As a proof of concept, we evaluated the potential of our model as a phenotypic screening tool for aged HSCs treated with epigenetic drugs to detect rejuvenation. Altogether, we demonstrate that changes in chromatin organization can be modeled via machine learning to predict age‐associated chromatin states in the hematopoietic compartment. Our developed framework, ChromAgeNet, serves as an interpretable algorithm to unravel the intricate relationship between chromatin changes and stem cell aging, and advance high‐throughput drug screening for rejuvenation therapies.
Reproduced under the paper's license (CC BY), from the paper cited above.
Repository
Its files are read in the Code ↔ Paper reader above, with 21 matches between paragraphs and lines of code.
biomedical-data-science/chromagenet
a07bfc0ab1a9f84358426f15e23649c1152773cb, 25 September 2026Availability: 1 check, the latest on 1 October 2026: the link answers
- 1 October 2026: the link answers
22 files
- methods/
chromagenet.py , Python, 171 lines, 2 matches - methods/
czi_preprocessing.py , Python, 961 lines, 2 matches - methods/
czi_utils.py , Python, 94 lines - methods/
explainability.py , Python, 521 lines, 2 matches - methods/
ml_training.py , Python, 272 lines - methods/
preprocessing_3D.py , Python, 14 lines - methods/
utils.py , Python, 391 lines, 1 match - notebooks/
01_czi_exploration_and_o , Jupyter, 221 linesutlier_detection.ipynb - notebooks/
02_dataset_summary_table , Jupyter, 199 liness.ipynb - notebooks/
03_czi_preprocessing.ipy , Jupyter, 198 linesnb - notebooks/
04_chromagenet_training_ , Jupyter, 720 lines, 4 matchescrossvalidation.ipynb - notebooks/
05_chromagenet_metadata_ , Jupyter, 265 linesevaluation.ipynb - notebooks/
06_chromagenet_compounds , Jupyter, 408 lines_evaluation.ipynb - notebooks/
07_chromagenet_explainab , Jupyter, 615 lines, 3 matchesility.ipynb - notebooks/
08_radiomics_feature_ext , Jupyter, 351 linesraction.ipynb - notebooks/
09_radiomics_classificat , Jupyter, 934 lines, 3 matchesion.ipynb - notebooks/
10_radiomics_classificat , Jupyter, 827 lines, 3 matchesion_binarized.ipynb - notebooks/
11_radiomics_compounds_c , Jupyter, 150 lines, 1 matchomparison.ipynb - notebooks/
12_radiomics_filters_vis , Jupyter, 187 linesual_comparison.ipynb - notebooks/
13_radiomics_supplementa , Jupyter, 166 linesry_tables.ipynb - LICENSE, License, 21 lines
- README.md, Text, 90 lines
The paper's code and data availability statement is in the Data section.
Tracing map
Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.
What the map holds:
- 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
- 20 scripts, each with its path and the digest of its content;
- 21 matches between paragraphs of the paper and lines of the code (method lexical-v1);
- neither the text of the paper nor the code itself.
Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.
Data
No dataset and no data link were found in the paper.
Data Availability Statement
The datasets generated and analyzed during the current study are available in the Catalan Open Research Area (CORA) repository https://
Reproduced under the paper's license (CC BY), from the paper cited above.
Versions
The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.
Version 1, 1 October 2026: the first record
Recorded: type, language, journal, volume, issue, pages, dates, 6 authors, 6 keywords, 7 MeSH terms, 4 funders, 90 references.
Cite
This paper
Picazo, P. I., Mejía‐Ramírez, E., Di Bari, D., Vitali, E., Florian, M. C., & Petrone, P. (2026). Deep Learning Predicts Hematopoietic Stem Cell Aging From 3D Chromatin Images. Aging cell, 25(10), e70656. https://
BibTeX
@article{picazo2026deep,
author = {Picazo, Pablo Iáñez and Mejía‐Ramírez, Eva and Di Bari, Dario and Vitali, Elena and Florian, Maria Carolina and Petrone, Paula},
title = {{Deep Learning Predicts Hematopoietic Stem Cell Aging From 3D Chromatin Images}},
journal = {Aging cell},
year = {2026},
month = oct,
volume = {25},
number = {10},
pages = {e70656},
publisher = {Wiley},
issn = {1474-9718},
doi = {10.1111/
url = {https://
pmid = {42802377},
pmcid = {PMC13617143}
}
RIS
TY - JOUR
AU - Picazo, Pablo Iáñez
AU - Mejía‐Ramírez, Eva
AU - Di Bari, Dario
AU - Vitali, Elena
AU - Florian, Maria Carolina
AU - Petrone, Paula
TI - Deep Learning Predicts Hematopoietic Stem Cell Aging From 3D Chromatin Images
T2 - Aging cell
J2 - Aging Cell
PY - 2026
DA - 2026/
VL - 25
IS - 10
SP - e70656
SN - 1474-9718
PB - Wiley
DO - 10.1111/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1111/
"type": "article-journal",
"title": "Deep Learning Predicts Hematopoietic Stem Cell Aging From 3D Chromatin Images",
"container-title": "Aging cell",
"author": [
{
"family": "Picazo",
"given": "Pablo Iáñez"
},
{
"family": "Mejía‐Ramírez",
"given": "Eva"
},
{
"family": "Di Bari",
"given": "Dario"
},
{
"family": "Vitali",
"given": "Elena"
},
{
"family": "Florian",
"given": "Maria Carolina"
},
{
"family": "Petrone",
"given": "Paula"
}
],
"container-title-short":
"volume": "25",
"issue": "10",
"page": "e70656",
"DOI": "10.1111/
"PMID": "42802377",
"PMCID": "PMC13617143",
"ISSN": "1474-9718",
"publisher": "Wiley",
"URL": "https://
"language": "en",
"issued": {
"date-parts": [
[
2026,
10,
1
]
]
}
}
The tracing map gets a citation of its own once an author has validated it and it has a DOI.
Similar papers
The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.
- [1] doi:10.1093/narmme/ugae011 [code]
- Nuclear expansion and chromatin structure remodeling in mouse aging neurons.Journal: NAR molecular medicineIn common: statannotations, seaborn, scikit-learn, 4 other tools, mouse, cellular / molecular, 7 references
- [2] doi:10.1016/j.celrep.2023.113500 [code]
- CellBiAge: Improved single-cell age classification using data binarization.Journal: Cell reportsIn common: XGBoost, TensorFlow, seaborn, 5 other tools, mouse, cellular / molecular, 5 references
- [3] doi:10.1186/s12885-025-14454-9 [code]
- Prediction of 1p/
19q state in glioma by integrated deep learning method based on MRI radiomics. Journal: BMC cancerIn common: PyRadiomics, SimpleITK, Keras, 8 other tools, 2 references - [4] doi:10.1016/j.isci.2025.114100 [code]
- Toward neuroimaging-based diagnostic support: a deep learning approach with a closed-loop system for psychiatric classification.Journal: iScienceIn common: PyRadiomics, SHAP, XGBoost, 9 other tools
- [5] doi:10.1038/s41598-026-48613-0 [code]
- An snRNA-seq aging clock for the fruit fly head sheds light on sex-biased aging.Journal: Scientific reportsIn common: SHAP, XGBoost, Keras, 7 other tools, cellular / molecular, 3 references
- [6] doi:10.1093/noajnl/vdag132 [code]
- Spherical radiomics for radiogenomic assessment of glioblastoma heterogeneity.Journal: Neuro-oncology advancesIn common: PyRadiomics, SHAP, SimpleITK, 8 other tools, 1 reference
- [7] doi:10.1038/s43856-024-00452-8 [code]
- Multi-scale V-net architecture with deep feature CRF layers for brain extraction.Journal: Communications medicineIn common: SHAP, XGBoost, Keras, 9 other tools, 1 reference
- [8] doi:10.1038/s41467-026-72057-9 [code]
- Sex-specific behavioral feedback modulates sensorimotor processing and drives flexible social behavior.Journal: Nature communicationsIn common: statannotations, UMAP, Keras, 9 other tools, 1 reference
- [9] doi:10.2147/ijgm.s408725 [code]
- Using Radiomics and Convolutional Neural Networks for the Prediction of Hematoma Expansion After Intracerebral Hemorrhage.Journal: International journal of general medicineIn common: PyRadiomics, SimpleITK, Keras, 9 other tools
- [10] doi:10.1038/s41422-022-00719-6 [code]
- Single-cell epigenome analysis reveals age-associated decay of heterochromatin domains in excitatory neurons in the mouse brain.Journal: Cell researchIn common: UMAP, scikit-learn, pandas, 3 other tools, mouse, cellular / molecular, 5 references
Contribute
The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.
Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 1 repository of the authors' code, each at its verified commit and with its license, 20 scripts, and 21 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:de6005520d8717a7…
Add the badge to the code's README
If you are an author or a maintainer of this paper's code, you can add this badge. It says what OSCR knows of the paper (its code, its validated tracing map) and links the code to this page. Paste one of these snippets under the README's title. Keep the two marks around it, so that the badge can be replaced in place and is never added twice. OSCR changes nothing in your repository.
Markdown
<!-- oscr-badge:start -->
[](https://openscicode.org/badge/doi/10.1111/acel.70656)
<!-- oscr-badge:end -->reStructuredText
.. oscr-badge:start
.. image:: https://openscicode.org/badge/doi/10.1111/acel.70656.svg
:target: https://openscicode.org/badge/doi/10.1111/acel.70656
:alt: OSCR
.. oscr-badge:endHTML
<!-- oscr-badge:start -->
<a href="https://openscicode.org/badge/doi/10.1111/acel.70656"><img src="https://openscicode.org/badge/doi/10.1111/acel.70656.svg" alt="OSCR"></a>
<!-- oscr-badge:end -->On GitHub, open the README, choose Edit (the pencil), paste the snippet under the title, then "Commit changes".
If you choose to, you can have the badge kept up to date for you, with oscr badge --write from the command line or with the GitHub Action in your repository's workflows. Every other form and option (a card, AsciiDoc, LaTeX, shields.io) is on the badge page.
Request its removal
To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).
Discussion, reproductions, activity
Discussion: code errors and mismatches between the code and the paper about this paper and its code, from readers and its authors, beside the paper and its tracing map.
Report a mismatch between the code and the paper · Report a code error (signed in)
Reproductions: reports from readers who ran the authors' code and did not get the paper's result, what they ran, with which environment, commit and data.
Report a reproduction failure (signed in)
Versions of its code: the releases of its code tied to a version of this paper (the preprint, the accepted manuscript, the version of record…), each with the tracing map versioned with it.
Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.
