OSCR

Deep Learning Predicts Hematopoietic Stem Cell Aging From 3D Chromatin Images.

Code ↔ Paper

21 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 21 matches
  1. [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. [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. [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. [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. [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. [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. [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. [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. [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. [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. [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. [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. [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. [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. [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. [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. [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. [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. [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. [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. [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

  1. # %% [markdown]
  2. # Tensorflow and Keras were installed following the official tutorial:
  3. # https://www.tensorflow.org/install/pip
  4. # %%
  5. import tensorflow as tf
  6. from tensorflow import keras
  7. from tensorflow.keras import layers
  8. import matplotlib.pyplot as plt
  9. import numpy as np
  10. import pandas as pd
  11. import seaborn as sns
  12. import sys
  13. sys.path.append("..")
  14. import methods.explainability as ex
  15. from methods import utils
  16. from methods.chromagenet import CalibratedModel, make_model
  17. from sklearn.metrics import brier_score_loss
  18. from sklearn.isotonic import IsotonicRegression
  19. from sklearn.linear_model import LogisticRegression
  20. from betacal import BetaCalibration
  21. physical_devices = tf.config.list_physical_devices("GPU")
  22. print("Num GPUs:", len(physical_devices))
  23. # %%
  24. pal = utils.get_class_palette()
  25. mic_pal = utils.get_microscopist_palette()
  26. # %%
  27. NORM = "none"
  28. RES = 0.05
  29. k_cv = 5
  30. IMGS_DIR = f"../../data/preprocessed/2D_res={RES}_norm={NORM}_{k_cv}fold_withDMSO"
  31. CNN_DENSE_FILTS = (256, 64, 16)
  32. IMAGE_SIZE = (128, 128)
  33. BATCH_SIZE = 64
  34. CHANNEL_MODE = "grayscale"
  35. N_OUTPUT_UNITS = 1
  36. LR_SCHED = None
  37. OPTIMIZER = "AdamW"
  38. COMMENT = "testing baseline"
  39. EPOCHS = 50
  40. LR = 1e-4
  41. SEED = 2023
  42. if CHANNEL_MODE == "rgb":
  43. channels = (3,)
  44. elif CHANNEL_MODE == "grayscale":
  45. channels = (1,)
  46. if N_OUTPUT_UNITS == 1:
  47. OUTPUT_FUNC = "sigmoid"
  48. LOSS_FUNC = tf.keras.losses.BinaryCrossentropy(
  49. label_smoothing=0.1,
  50. )
  51. LABEL_MODE = "binary"
  52. elif N_OUTPUT_UNITS == 2:
  53. OUTPUT_FUNC = "softmax"
  54. LOSS_FUNC = "categorical_crossentropy"
  55. LABEL_MODE = "categorical"
  56. # %%
  57. data_augmentation = keras.Sequential(
  58. [
  59. layers.RandomFlip("horizontal_and_vertical"),
  60. layers.RandomRotation(factor=0.5, fill_mode="constant"),
  61. layers.RandomContrast(factor=0.05),
  62. ]
  63. )
  64. # %%
  65. n_0 = 0
  66. n_1 = 1
  67. for k in range(1, k_cv + 1):
  68. train_ds = None
  69. print(f"Starting fold {k}")
  70. ds = tf.keras.utils.image_dataset_from_directory(
  71. f"{IMGS_DIR}/fold_{k}/",
  72. color_mode=CHANNEL_MODE,
  73. labels="inferred",
  74. label_mode=LABEL_MODE,
  75. interpolation="bilinear",
  76. seed=SEED,
  77. image_size=IMAGE_SIZE,
  78. batch_size=BATCH_SIZE,
  79. shuffle=False,
  80. )
  81. for _, y in ds:
  82. unique, counts = np.unique(y.numpy(), return_counts=True)
  83. count_dict = dict(zip(unique, counts))
  84. n_0 += count_dict.get(0, 0) # Add count of class 0
  85. n_1 += count_dict.get(1, 0) # Add count of class 1
  86. print(f"Fold {k}: Class 0 = {n_0}, Class 1 = {n_1}")
  87. # Final total counts
  88. print(f"Total images: Class 0 = {n_0}, Class 1 = {n_1}")
  89. # %%
  90. testing_ds = tf.keras.utils.image_dataset_from_directory(
  91. f"{IMGS_DIR}/fold_1/",
  92. color_mode=CHANNEL_MODE,
  93. labels="inferred",
  94. label_mode=LABEL_MODE,
  95. interpolation="bilinear",
  96. seed=SEED,
  97. image_size=IMAGE_SIZE,
  98. batch_size=BATCH_SIZE,
  99. shuffle=True,
  100. )
  101. # %%
  102. plt.figure(figsize=(10, 10))
  103. for images, labels in testing_ds.take(1):
  104. young_images = images[labels.numpy().ravel() == 1.]
  105. for i in range(9):
  106. print(np.max(young_images[i]))
  107. print(np.min(young_images[i]))
  108. ax = plt.subplot(3, 3, i + 1)
  109. plt.imshow(young_images[i].numpy().astype("uint8"))
  110. plt.axis("off")
  111. plt.tight_layout()
  112. # %%
  113. plt.figure(figsize=(10, 10))
  114. for images, labels in testing_ds.take(1):
  115. aged_images = images[labels.numpy().ravel() == 0.]
  116. for i in range(9):
  117. print(np.max(aged_images[i]))
  118. print(np.min(aged_images[i]))
  119. ax = plt.subplot(3, 3, i + 1)
  120. plt.imshow(aged_images[i].numpy().astype("uint8"))
  121. plt.axis("off")
  122. plt.tight_layout()
  123. # %%
  124. # Apply `data_augmentation` to the training images.
  125. testing_ds = testing_ds.map(
  126. lambda img, label: (data_augmentation(img), label),
  127. num_parallel_calls=tf.data.AUTOTUNE,
  128. )
  129. plt.figure(figsize=(10, 10))
  130. for images, labels in testing_ds.take(1):
  131. for i in range(9):
  132. print(np.max(images[i]))
  133. print(np.min(images[i]))
  134. ax = plt.subplot(3, 3, i + 1)
  135. plt.imshow(images[i].numpy())
  136. plt.title(int(labels[i]))
  137. plt.axis("off")
  138. # %%
  139. # Build ChromAgeNet model
  140. model = make_model(
  141. input_shape=IMAGE_SIZE + channels,
  142. dense_units=CNN_DENSE_FILTS,
  143. n_output_units=N_OUTPUT_UNITS,
  144. output_activation=OUTPUT_FUNC,
  145. )
  146. # %%
  147. model.summary()
  148. # %%
  149. # keras.utils.plot_model(model, show_shapes=True)
  150. # %%
  151. os.chdir(f"../../../notebooks")
  152. # %%
  153. import os
  154. for k in range(1, k_cv + 1):
  155. os.makedirs(f"../models/checkpoints/fold_{k}", exist_ok=True)
  156. # %%
  157. callbacks = [
  158. keras.callbacks.ModelCheckpoint(
  159. "{epoch}-{val_accuracy:.3f}.weights.h5",
  160. monitor="val_auc",
  161. save_weights_only=True,
  162. save_best_only=False,
  163. )
  164. ]
  165. lr_obj = None
  166. if LR_SCHED == "cosine_decay":
  167. lr_obj = keras.optimizers.schedules.CosineDecay(
  168. initial_learning_rate=LR, decay_steps=100, alpha=1e-1
  169. )
  170. if lr_obj is None:
  171. lr_obj = LR
  172. if OPTIMIZER == "AdamW":
  173. optim = keras.optimizers.AdamW(
  174. learning_rate=lr_obj, use_ema=True
  175. )
  176. elif OPTIMIZER == "Adam":
  177. optim = keras.optimizers.Adam(learning_rate=lr_obj, use_ema=True)
  178. elif OPTIMIZER == "SGD":
  179. optim = keras.optimizers.SGD(learning_rate=lr_obj, momentum=0.6)
  180. metrics = [
  181. "accuracy",
  182. tf.keras.metrics.AUC(),
  183. tf.keras.metrics.Precision(),
  184. tf.keras.metrics.Recall(),
  185. tf.keras.metrics.F1Score(),
  186. ]
  187. def plot_loss(hist):
  188. plt.plot(hist["loss"])
  189. plt.plot(hist["val_loss"])
  190. plt.title("Training Progress")
  191. plt.ylabel("Loss")
  192. plt.xlabel("Epochs")
  193. plt.legend(["train_loss", "val_loss"], loc="upper left")
  194. plt.show()
  195. def plot_acc(hist):
  196. plt.plot(hist["accuracy"])
  197. plt.plot(hist["val_accuracy"])
  198. plt.title("Training Progress")
  199. plt.ylabel("Accuracy")
  200. plt.xlabel("Epochs")
  201. plt.legend(["train_acc", "val_acc"], loc="upper left")
  202. plt.show()
  203. # %%
  204. sns.set_style("whitegrid")
  205. train_ds = None
  206. IMGS_DIR = f"../../../../data/preprocessed/2D_res={RES}_norm={NORM}_{k_cv}fold_withDMSO"
  207. model.compile(
  208. optimizer=optim,
  209. loss=LOSS_FUNC,
  210. metrics=metrics,
  211. jit_compile=True,
  212. )
  213. initial_weights = model.get_weights()
  214. for k in range(1, k_cv + 1):
  215. train_ds = None
  216. os.chdir(f"../models/checkpoints/fold_{k}")
  217. print("Saving model weights at: ", os.getcwd())
  218. print(f"Starting fold {k}")
  219. for i in range(1, k_cv + 1):
  220. if i == k:
  221. val_ds = tf.keras.utils.image_dataset_from_directory(
  222. f"{IMGS_DIR}/fold_{i}/",
  223. color_mode=CHANNEL_MODE,
  224. labels="inferred",
  225. label_mode=LABEL_MODE,
  226. interpolation="bilinear",
  227. seed=SEED,
  228. image_size=IMAGE_SIZE,
  229. batch_size=BATCH_SIZE,
  230. shuffle=False,
  231. )
  232. continue
  233. if train_ds:
  234. train_ds = train_ds.concatenate(
  235. tf.keras.utils.image_dataset_from_directory(
  236. f"{IMGS_DIR}/fold_{i}/",
  237. color_mode=CHANNEL_MODE,
  238. labels="inferred",
  239. label_mode=LABEL_MODE,
  240. interpolation="bilinear",
  241. seed=SEED,
  242. image_size=IMAGE_SIZE,
  243. batch_size=BATCH_SIZE,
  244. shuffle=True,
  245. )
  246. )
  247. else:
  248. train_ds = tf.keras.utils.image_dataset_from_directory(
  249. f"{IMGS_DIR}/fold_{i}/",
  250. color_mode=CHANNEL_MODE,
  251. labels="inferred",
  252. label_mode=LABEL_MODE,
  253. interpolation="bilinear",
  254. seed=SEED,
  255. image_size=IMAGE_SIZE,
  256. batch_size=BATCH_SIZE,
  257. shuffle=True,
  258. )
  259. n_train_ims = train_ds.cardinality().numpy() * BATCH_SIZE
  260. print(n_train_ims)
  261. train_ds = train_ds.unbatch().shuffle(10000).batch(BATCH_SIZE)
  262. # Prefetching samples in GPU memory helps maximize GPU utilization.
  263. train_ds = train_ds.cache().prefetch(tf.data.AUTOTUNE)
  264. val_ds = val_ds.cache().prefetch(tf.data.AUTOTUNE)
  265. train_ds = train_ds.map(
  266. lambda img, label: (data_augmentation(img), label),
  267. num_parallel_calls=tf.data.AUTOTUNE,
  268. )
  269. model.set_weights(initial_weights)
  270. history = model.fit(
  271. train_ds,
  272. epochs=EPOCHS,
  273. callbacks=callbacks,
  274. validation_data=val_ds,
  275. shuffle=True,
  276. sample_weight=None,
  277. )
  278. hist = utils.fix_repeated_metric_names(model.history.history)
  279. hist_df = ex.get_history_df(hist)
  280. ex.plot_training(hist_df)
  281. plot_loss(model.history.history)
  282. plot_acc(model.history.history)
  283. model.evaluate(val_ds)
  284. os.chdir(f"../../../notebooks")
  285. model.save(f"../models/res{RES}_norm{NORM}_fold{k}.keras")
  286. # %%
  287. best_fold_models = [
  288. "../models/checkpoints/fold_1/44-0.692.weights.h5",
  289. "../models/checkpoints/fold_2/48-0.639.weights.h5",
  290. "../models/checkpoints/fold_3/50-0.678.weights.h5",
  291. "../models/checkpoints/fold_4/43-0.658.weights.h5",
  292. "../models/checkpoints/fold_5/42-0.651.weights.h5",
  293. ]
  294. def find_optimal_t(tpr, fpr, thresholds):
  295. # Find optimal threshold using Youden’s J statistic
  296. J_scores = tpr - fpr
  297. best_idx = np.argmax(J_scores)
  298. return thresholds[best_idx]
  299. def plot_class_distplot(df, x, hue, threshold, palette):
  300. sns.displot(df, x=x, hue=hue, binwidth=0.04,
  301. palette=pal, height=3, kde=True)
  302. # Add a vertical line at the best threshold
  303. plt.axvline(threshold, color='red', linestyle='--', linewidth=2)
  304. plt.xlabel("Averaged probability (whole nucleus)")
  305. plt.ylabel("Number of nuclei")
  306. plt.xlim(0, 1)
  307. plt.show()
  308. def compute_ece(y_true, y_probs, n_bins=10):
  309. bins = np.linspace(0, 1, n_bins + 1) # Create bin edges
  310. bin_indices = np.digitize(y_probs, bins) - 1 # Assign samples to bins
  311. ece = 0.0
  312. for i in range(n_bins):
  313. mask = bin_indices == i
  314. if np.sum(mask) > 0:
  315. acc = np.mean(y_true[mask]) # Fraction of positives in bin
  316. conf = np.mean(y_probs[mask]) # Mean predicted probability in bin
  317. ece += np.abs(acc - conf) * (np.sum(mask) / len(y_probs)) # Weighted average
  318. return ece
  319. # %%
  320. import sklearn.metrics as skmetrics
  321. aucs_df = pd.DataFrame(columns=["TPR", "FPR", "fold"])
  322. pr_df = pd.DataFrame(columns=["Precision", "Recall", "fold"])
  323. conf_2D_df = pd.DataFrame(columns=["Confusion", "index", "fold"])
  324. conf_3D_df = pd.DataFrame(columns=["Confusion", "index", "fold"])
  325. cv_3D_df = pd.DataFrame()
  326. cv_2D_df = pd.DataFrame()
  327. eces, cal_eces = [], []
  328. briers, cal_briers = [], []
  329. IMGS_DIR = f"../../data/preprocessed/2D_res={RES}_norm={NORM}_{k_cv}fold_withDMSO"
  330. for fold in range(1, k_cv + 1):
  331. train_ds = None
  332. threshold_list = []
  333. print(f"Starting fold {fold}")
  334. model.load_weights(best_fold_models[fold-1])
  335. val_ids, val_ds = utils.dataset_from_partition(
  336. IMGS_DIR,
  337. f"fold_{fold}",
  338. CHANNEL_MODE,
  339. LABEL_MODE,
  340. IMAGE_SIZE,
  341. BATCH_SIZE,
  342. SEED,
  343. )
  344. val_df = ex.get_output_df_voting(val_ids, val_ds, model, LABEL_MODE)
  345. val_df["class"] = val_df["label"].map({0: 'aged', 1: 'young'})
  346. val_df["fold"] = fold
  347. y = np.concatenate([y for _, y in val_ds], axis=0)
  348. print(val_ds.cardinality().numpy() * BATCH_SIZE)
  349. y_prob = model.predict(val_ds)
  350. # Compute ROC curve
  351. fpr, tpr, thresholds = skmetrics.roc_curve(y, y_prob)
  352. aucs_df = pd.concat([aucs_df, pd.DataFrame(
  353. {"TPR":tpr, "FPR":fpr, "fold":fold})]
  354. )
  355. best_t = find_optimal_t(tpr, fpr, thresholds)
  356. threshold_list.append(best_t)
  357. # Compute PR curve
  358. precision, recall, thresholds = skmetrics.precision_recall_curve(y, y_prob)
  359. pr_df = pd.concat([pr_df, pd.DataFrame(
  360. {"Precision":precision, "Recall":recall, "fold":fold})]
  361. )
  362. # Compute confusion matrices
  363. y_pred = (y_prob > best_t).astype(int)
  364. conf_mat_2D = skmetrics.confusion_matrix(y, y_pred, normalize="true").flatten()
  365. conf_2D_df = pd.concat([conf_2D_df, pd.DataFrame(
  366. {"Confusion":conf_mat_2D, "index":[1, 2, 3, 4], "fold":fold})]
  367. )
  368. cv_2D_df = pd.concat([cv_2D_df,
  369. pd.DataFrame(
  370. {"nuc_id": np.concatenate([y for y in val_ids], axis=0),
  371. "label": y.ravel(),
  372. "pred": y_pred.ravel(),
  373. "y_prob": y_prob.ravel(),
  374. "fold": fold,
  375. })]
  376. )
  377. val_df["soft_pred"] = (val_df["mean_prob"] >= best_t).astype(int)
  378. conf_mat_3D = skmetrics.confusion_matrix(val_df["label"], val_df["soft_pred"], normalize="true").flatten()
  379. conf_3D_df = pd.concat([conf_3D_df, pd.DataFrame(
  380. {"Confusion":conf_mat_3D, "index":[1, 2, 3, 4], "fold":fold})]
  381. )
  382. cv_3D_df = pd.concat([cv_3D_df, val_df])
  383. ex.plot_auc(val_df["label"], val_df["mean_prob"], threshold=best_t)
  384. ex.plot_conf_mat(val_df["label"], val_df["mean_prob"], threshold=best_t)
  385. plot_class_distplot(val_df, x="mean_prob", hue="class", threshold=best_t, palette=pal)
  386. ir = IsotonicRegression(out_of_bounds = 'clip')
  387. lr = LogisticRegression(solver='lbfgs')
  388. bc = BetaCalibration(parameters="abm")
  389. #calibrators = [ir, lr, bc]
  390. calibrators = [bc]
  391. uncal_probs = model.predict(val_ds)
  392. test_labels = np.concatenate([y for _, y in val_ds], axis=0)
  393. cal_error = compute_ece(test_labels, uncal_probs, n_bins=10)
  394. bs = brier_score_loss(test_labels, uncal_probs)
  395. print("Pre-Calibration Error:", cal_error)
  396. print("Pre-Calibration Brier Score:", bs)
  397. eces.append(cal_error)
  398. briers.append(bs)
  399. nuc_prob_outputs = [val_df["mean_prob"]]
  400. plot_probs = [uncal_probs]
  401. for calibrator in calibrators:
  402. print(f"Calibrating with {calibrator}")
  403. calibrator.fit(uncal_probs.ravel(), test_labels.ravel())
  404. cal_probs = calibrator.predict(uncal_probs)
  405. cal_error = compute_ece(test_labels, cal_probs, n_bins=10)
  406. bs = brier_score_loss(test_labels, cal_probs)
  407. print("Post-Calibration Error:", cal_error)
  408. print("Post-Calibration Brier Score:", bs)
  409. cal_eces.append(cal_error)
  410. cal_briers.append(bs)
  411. cal_val_df = ex.get_output_df_voting(val_ids, val_ds, model,
  412. LABEL_MODE, thresh=0.5,
  413. cal_model=calibrator)
  414. cal_val_df["class"] = cal_val_df["label"].map({0: 'aged', 1: 'young'})
  415. nuc_prob_outputs.append(cal_val_df["mean_prob"])
  416. plot_probs.append(cal_probs)
  417. # Compute ROC curve
  418. cal_fpr, cal_tpr, cal_thresholds = skmetrics.roc_curve(y, cal_probs)
  419. best_cal_t = find_optimal_t(cal_tpr, cal_fpr, cal_thresholds)
  420. threshold_list.append(best_cal_t)
  421. ex.plot_conf_mat(cal_val_df["label"], cal_val_df["mean_prob"], threshold=best_cal_t)
  422. plot_class_distplot(cal_val_df, x="mean_prob",
  423. hue="class", threshold=best_cal_t, palette=pal)
  424. legends = ["Uncalibrated", "Beta Calib."] # "Isotonic Reg.", "Platt Scaling",
  425. labels = [cal_val_df["label"], cal_val_df["label"], cal_val_df["label"]]
  426. utils.plot_multiple_auc(labels, nuc_prob_outputs, legends, threshold_list)
  427. # 5. Create the integrated model
  428. calibrated_model = CalibratedModel(model, calibrator)
  429. # 6. Save the integrated model
  430. save_path = f"../models/chromagenet/calmodel_fold_{fold}"
  431. calibrated_model.save(save_path)
  432. utils.plot_calibration_curve(test_labels, plot_probs, legends)
  433. # %%
  434. print(np.mean(eces), np.std(eces))
  435. print(np.mean(cal_eces), np.std(cal_eces))
  436. print(np.mean(briers), np.std(briers))
  437. print(np.mean(cal_briers), np.std(cal_briers))
  438. # %%
  439. from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score
  440. def build_performance_table(df, pred_col="soft_pred", prob_col="mean_prob"):
  441. # Assuming your DataFrame is named df
  442. metrics_per_fold = {}
  443. for fold, data in df.groupby("fold"):
  444. y_true = data["label"] # Ground truth labels
  445. y_pred = data[pred_col] # Model predictions (binary)
  446. y_scores = data[prob_col] # Probability scores
  447. metrics_per_fold[fold] = {
  448. "accuracy": accuracy_score(y_true, y_pred),
  449. "precision": precision_score(y_true, y_pred, zero_division=0),
  450. "recall": recall_score(y_true, y_pred),
  451. "AUC": roc_auc_score(y_true, y_scores),
  452. "F1": f1_score(y_true, y_pred),
  453. }
  454. # Convert results to a DataFrame
  455. metrics_df = pd.DataFrame.from_dict(metrics_per_fold, orient="index")
  456. # Compute mean, std, and 95% CI
  457. sum_df = metrics_df.agg(["mean", "std"])
  458. z_score = 1.96
  459. ci_lower = sum_df.loc["mean"] - (z_score * sum_df.loc["std"] / np.sqrt(k_cv))
  460. ci_upper = sum_df.loc["mean"] + (z_score * sum_df.loc["std"] / np.sqrt(k_cv))
  461. sum_df.loc["CI"] = [(round(low, 3), round(up, 3)) for low, up in zip(ci_lower, ci_upper)]
  462. metrics_df = pd.concat([metrics_df, sum_df])
  463. return metrics_df
  464. # %%
  465. sum_2d_df = build_performance_table(cv_2D_df, pred_col="pred", prob_col="y_prob")
  466. sum_2d_df
  467. # %%
  468. sum_2d_df.to_latex(float_format="%.3f")
  469. # %%
  470. soft_df = build_performance_table(cv_3D_df, pred_col="soft_pred")
  471. soft_df
  472. # %%
  473. hard_df = build_performance_table(cv_3D_df, pred_col="hard_preds")
  474. hard_df
  475. # %%
  476. soft_df.to_latex(float_format="%.3f")
  477. # %%
  478. from sklearn.metrics import precision_recall_curve, average_precision_score, roc_curve, auc
  479. sns.set_theme(style="whitegrid")
  480. plt.figure(figsize=(5, 4))
  481. optimal_thresholds = {} # Store optimal thresholds per fold
  482. for fold, group in cv_df.groupby("fold"):
  483. print(f"Processing fold {fold}")
  484. y_true = group["label"] # True labels
  485. y_prob = group["mean_prob"] # Soft voting probabilities
  486. # Compute Precision-Recall curve
  487. precision, recall, thresholds = precision_recall_curve(y_true, y_prob)
  488. # Compute F1-score for each threshold
  489. f1_scores = (2 * precision * recall) / (precision + recall + 1e-6) # Avoid division by zero
  490. best_idx = np.argmax(f1_scores)
  491. best_threshold = thresholds[best_idx] if best_idx < len(thresholds) else thresholds[-1]
  492. print(f"Optimal threshold for fold {fold}: {best_threshold:.3f}")
  493. optimal_thresholds[fold] = best_threshold # Store best threshold
  494. # Compute AUC-PR for this fold
  495. auc_pr = average_precision_score(y_true, y_prob)
  496. # Plot Precision-Recall curve using Seaborn
  497. sns.lineplot(x=recall, y=precision, label=f'Fold {fold} (AUC-PR = {auc_pr:.2f})')
  498. # Mark the optimal threshold point without adding it to the legend
  499. sns.scatterplot(x=[recall[best_idx]], y=[precision[best_idx]], color='red', s=60, zorder=3)
  500. # Formatting
  501. sns.despine()
  502. plt.xlabel("Recall", fontsize=12)
  503. plt.ylabel("Precision", fontsize=12)
  504. plt.title("Precision-Recall Curves per Fold", fontsize=14)
  505. plt.legend(loc="best", fontsize=9)
  506. plt.show()
  507. # Print optimal thresholds per fold
  508. print("Optimal thresholds per fold:", optimal_thresholds)
  509. # %%
  510. sns.set_theme(style="whitegrid")
  511. plt.figure(figsize=(5, 4))
  512. for fold, group in cv_df.groupby("fold"):
  513. y_true = group["label"] # True labels
  514. y_prob = group["mean_prob"] # Soft voting probabilities
  515. # Compute ROC curve and AUC
  516. fpr, tpr, thresholds = roc_curve(y_true, y_prob)
  517. roc_auc = auc(fpr, tpr)
  518. # Find optimal threshold using Youden’s J statistic
  519. J_scores = tpr - fpr
  520. best_idx = np.argmax(J_scores)
  521. best_threshold = thresholds[best_idx]
  522. # Plot per fold
  523. sns.lineplot(x=fpr, y=tpr, label=f'Fold {fold} (AUC = {roc_auc:.2f})')
  524. sns.scatterplot(x=[fpr[best_idx]], y=[tpr[best_idx]], color='red', s=60, zorder=3, label="_nolegend_")
  525. # Plot chance line
  526. plt.plot([0, 1], [0, 1], 'k--', label="Random Guessing (AUC = 0.50)")
  527. # Formatting
  528. plt.xlabel("False Positive Rate (FPR)")
  529. plt.ylabel("True Positive Rate (TPR)")
  530. plt.title("ROC Curves per Fold")
  531. plt.legend(loc="best", fontsize=9)
  532. # Show plot
  533. plt.show()
  534. # %%
  535. def plot_conf_mat(conf_df, title="Confusion matrix"):
  536. summ_df = conf_df.groupby("index")["Confusion"].agg(["mean", "std"]).reset_index()
  537. conf_plot = summ_df["mean"].to_numpy().reshape((2, 2))
  538. annot = np.array([
  539. f"{m:.3f}% \n ± {s:.3f}" for m, s in zip(summ_df["mean"], summ_df["std"])
  540. ]).reshape((2, 2))
  541. plt.figure(figsize=(4, 3))
  542. sns.heatmap(conf_plot, annot=annot, fmt='', cmap="Blues")
  543. plt.xticks([0.5, 1.5], ["Aged", "Young"])
  544. plt.yticks([0.5, 1.5], ["Aged", "Young"])
  545. plt.title(title)
  546. plt.show()
  547. plot_conf_mat(conf_2D_df, title="Confusion matrix (2D XY slide)")
  548. # %%
  549. plot_conf_mat(conf_3D_df, title="Confusion matrix (whole 3D nucleus)")
  550. # %%
  551. plt.figure(figsize=(4, 3))
  552. sns.lineplot(data=aucs_df, x="FPR", y="TPR", hue="fold")
  553. plt.plot([0, 1], [0, 1], linestyle="--", color="gray", label="Random (AUC=0.5)")
  554. plt.show()
  555. # %%
  556. plt.figure(figsize=(4, 3))
  557. sns.lineplot(data=pr_df, x="Recall", y="Precision", hue="fold")
  558. plt.ylim((0.4, 1))
  559. baseline = sum(y) / len(y)
  560. plt.axhline(y=baseline, color="gray", linestyle="--", label="Random Baseline")
  561. plt.show()
  562. # %%
  563. train_df = ex.get_output_df_voting(train_ids, train_ds, model, LABEL_MODE)
  564. val_df = ex.get_output_df_voting(val_ids, val_ds, model, LABEL_MODE)
  565. # %%
  566. dataset_names = ["train", "val"]
  567. dataframes = [train_df, val_df]
  568. thresholds = np.arange(0.3, 0.8, 0.05)
  569. thresh_df = ex.plot_metrics(dataframes, dataset_names, thresholds)
  570. for m_name in ["acc", "precision", "recall", "f1"]:
  571. sns.lineplot(x="threshold", y=m_name, hue="dataset", data=thresh_df)
  572. plt.title(m_name)
  573. plt.show()

04_chromagenet_training_crossvalidation.ipynb at commit a07bfc0, under MIT · at the source

Overview

  1. Universitat Pompeu Fabra (UPF) Barcelona Spain
  2. ISGlobal Barcelona Spain
  3. Regenerative Medicine Program The Bellvitge Institute for Biomedical Research (IDIBELL) Barcelona Spain
  4. Program for Advancing the Clinical Translation of Regenerative Medicine of Catalonia, P‐CMR[C] Barcelona Spain
  5. Center for Networked Biomedical Research on Bioengineering, Biomaterials and Nanomedicine (CIBER‐BBN) Madrid Spain
  6. The Catalan Institution for Research and Advanced Studies (ICREA) Barcelona Spain
  7. Barcelona Supercomputing Center (BSC) Barcelona Spain
Journal: Aging cell, volume 25, issue 10, article e70656
Dates: received 22 December 2025; accepted 29 July 2026; published online 27 September 2026; in print October 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1111/acel.70656 · PMID 42802377 · PMCID PMC13617143 · OpenAlex W7214564284
Open access: gold, a free copy (OpenAlex)
Status: code verified (how the code is found and verified)
Categories: mouse (organism), cellular / molecular (subfield)
Methods: Connectivity, Statistics, Smoothing, state filtering, decompositions, Machine learning, Spectral & time-frequency, Physiology & signal measures
Keywords: age measurement, cellular aging, deep learning, hematopoietic stem cells, machine learning, nuclear architecture
MeSH: Cellular Senescence*, Chromatin*, Deep Learning*, Hematopoietic Stem Cells*, Imaging, Three-Dimensional*, Animals, Mice (* major topic)
Topic: Hematopoietic Stem Cell Transplantation (Hematology, Medicine), according to OpenAlex
Funding: European Research Council (101002453); Ministerio de Ciencia, Innovación y Universidades (RYC2018‐025979‐I, PGC2018‐102049‐B‐I00, CEX2023‐0001290‐S, CNS2023‐144908, PID2021‐123922NB‐I00); Ministerio para la Transformación Digital y de la Función Pública (C005/24‐ED CV1); “la Caixa” Foundation (LCF/BQ/DI22/11940001)
Citations: not cited yet (Europe PMC); 95 references in the paper

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

License: MIT
State: the link answers, verified on 1 October 2026
Evidence: files inventoried
Commit: a07bfc0ab1a9f84358426f15e23649c1152773cb, 25 September 2026
Languages: Jupyter (13), Python (7)
Size: 36 files, 20 scripts
Software Heritage: not archived
Found in: “Data Availability Statement”
Holds: README, license file, 13 notebooks
Not found: CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (16 files), pandas (16 files), Matplotlib (13 files), seaborn (12 files), TensorFlow (7 files), scikit-learn (6 files), Keras (5 files), scikit-image (4 files), SciPy (4 files), SHAP (3 files), XGBoost (3 files), PyRadiomics (2 files), SimpleITK (2 files), statannotations (2 files), UMAP (2 files), Pillow (1 file)
Availability: 1 check, the latest on 1 October 2026: the link answers
  • 1 October 2026: the link answers
22 files

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://doi.org/10.34810/data2372. The authors can provide additional data upon receiving a reasonable request. The code repository hosting all source code developed for this project can be found in https://github.com/biomedical‐data‐science/chromagenet (https://github.com/biomedical-data-science/chromagenet). This repository includes all Python scripts and Jupyter notebooks needed to replicate the results presented in this article.

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://doi.org/10.1111/acel.70656

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/acel.70656},
url = {https://doi.org/10.1111/acel.70656},
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/10/01
VL - 25
IS - 10
SP - e70656
SN - 1474-9718
PB - Wiley
DO - 10.1111/acel.70656
UR - https://doi.org/10.1111/acel.70656
LA - en
ER -

CSL-JSON

{
"id": "10.1111/acel.70656",
"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": "Aging Cell",
"volume": "25",
"issue": "10",
"page": "e70656",
"DOI": "10.1111/acel.70656",
"PMID": "42802377",
"PMCID": "PMC13617143",
"ISSN": "1474-9718",
"publisher": "Wiley",
"URL": "https://doi.org/10.1111/acel.70656",
"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 medicine
In 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 reports
In 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 cancer
In 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: iScience
In 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 reports
In 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 advances
In 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 medicine
In 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 communications
In 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 medicine
In 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 research
In 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.

Add the badge to the code's README

OSCR: the badge of this paper

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 -->
[![OSCR](https://openscicode.org/badge/doi/10.1111/acel.70656.svg)](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:end
HTML
<!-- 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.