OSCR

Probabilistic Forecasting and Information-Theoretic Analysis of Multivariate fMRI Dynamics.

Code ↔ Paper

20 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 20 matches
  1. [1] § 2. Materials and Methods › 2.4. Forecasting Models › 2.4.4. Transformer ↔ models/transformer.py, lines 19–93 · score 0.81 · transformer encoder layers, attention heads, embedded, positions, vector, stacked
  2. [2] § 2. Materials and Methods › 2.5. Training and Evaluation Protocol ↔ utils/training.py, lines 86–235 · score 0.80 · learning rate scheduling, AdamW, weight decay, optimizer, loss, training
  3. [3] § 2. Materials and Methods › 2.4. Forecasting Models › 2.4.3. Long Short-Term Memory Network ↔ models/lstm.py, lines 18–67 · score 0.80 · LSTM layers, recurrent layers, hidden state, multi layer, dropout, batch
  4. [4] § 2. Materials and Methods › 2.4. Forecasting Models › 2.4.3. Long Short-Term Memory Network ↔ models/lstm.py, lines 18–67 · score 0.74 · recurrent layer, hidden state, full forecasting, reshaped, maps, linear
  5. [5] § 2. Materials and Methods › 2.4. Forecasting Models › 2.4.4. Transformer ↔ scripts/cross_validation.py, lines 60–108 · score 0.73 · transformer encoder layers, attention heads, forecasting horizon, dropout, dimension, training
  6. [6] § 2. Materials and Methods › 2.4. Forecasting Models › 2.4.4. Transformer ↔ models/transformer.py, lines 19–93 · score 0.73 · Learned positional embeddings, ROI vector, layer, space, transformer, linear
  7. [7] § 2. Materials and Methods › 2.2. Forecasting Problem Formulation ↔ utils/measures.py, lines 741–806 · score 0.69 · Empirical residual histograms, predictive distribution, model predicted, binning, discretized, forecasting models
  8. [8] § 2. Materials and Methods › 2.1. Dataset and Region-of-Interest Definition ↔ utils/nsd_utils.py, lines 353–433 · score 0.66 · MNI space, ROI atlas, ANTs, affine, scan, warped
  9. [9] § 2. Materials and Methods › 2.1. Dataset and Region-of-Interest Definition ↔ utils/nsd_utils.py, lines 353–433 · score 0.66 · MNI space, ROI atlas, ANTs, affine, scan, warped
  10. [10] § 2. Materials and Methods › 2.5. Training and Evaluation Protocol ↔ utils/training.py, lines 430–488 · score 0.66 · LOSO folds, LOSO CV, cross validation, forecasting models, training
  11. [11] § 2. Materials and Methods › 2.6. Directed Information Post Analysis ↔ utils/measures.py, lines 741–806 · score 0.65 · empirical residual histogram, predictive distributions, model prediction, probability, horizon, ROI
  12. [12] § 2. Materials and Methods › 2.1. Dataset and Region-of-Interest Definition ↔ utils/nsd_utils.py, lines 1095–1143 · score 0.65 · LO1, VO1, LO2, PPA, V3A, VO2
  13. [13] § 2. Materials and Methods › 2.1. Dataset and Region-of-Interest Definition ↔ utils/nsd_utils.py, lines 1090–1138 · score 0.65 · LO1, VO1, LO2, PPA, V3A, VO2
  14. [14] § 2. Materials and Methods › 2.2. Forecasting Problem Formulation ↔ utils/measures.py, lines 168–172 · score 0.64 · Squared Scaled Error, step forecasting, RMSSE, Root, predictive
  15. [15] § 3. Results › Post Hoc Directed Information Analysis ↔ scripts/compute_di.py, lines 75–135 · score 0.59 · DI computation, source ROI, target ROI, likelihoods, probabilistic, training
  16. [16] § 2. Materials and Methods › 2.5. Training and Evaluation Protocol ↔ scripts/cross_validation.py, lines 111–234 · score 0.58 · cross validation, LOSO CV, fold, held, training, Model
  17. [17] § 2. Materials and Methods › 2.4. Forecasting Models › 2.4.2. Exponential Smoothing ↔ models/exponential_smoothing.py, lines 1–12 · score 0.57 · exponential smoothing model, fMRI, global, predictions, forecasting
  18. [18] § 3. Results ↔ utils/parse_data.py, lines 334–393 · score 0.56 · phase randomization, amplitude, power, spectrum, surrogate, variance
  19. [19] § 2. Materials and Methods › 2.3. Data Parsing and Sliding-Window Construction ↔ utils/parse_data.py, lines 243–331 · score 0.53 · sliding window, target windows, overlapping, Parsing
  20. [20] § 2. Materials and Methods › 2.5. Training and Evaluation Protocol ↔ utils/training.py, lines 430–488 · score 0.51 · LOSO folds, cross validated, Training, predictability, Model, forecasting

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Python · 643 lines · 21 KB · Apache-2.0 · 3 matches

  1. import copy
  2. import inspect
  3. import pandas as pd
  4. import numpy as np
  5. from tqdm.auto import tqdm
  6. from pathlib import Path
  7. import torch
  8. import torch.nn as nn
  9. from torch.utils.data import Dataset, DataLoader
  10. from .measures import compute_eta_gauss, compute_rmse, compute_rmsse
  11. from .parse_data import split_by_subject, normalize_items, build_sliding_windows
  12. try:
  13. from models.naive_models import last_value_model_generator
  14. except ImportError:
  15. from ..models.naive_models import last_value_model_generator
  16. device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
  17. class FMRIWindowDataset(Dataset):
  18. """PyTorch dataset for fMRI windowed sequences."""
  19. def __init__(self, X, Y=None):
  20. self.X = torch.tensor(X, dtype=torch.float32)
  21. self.Y = None
  22. if Y is not None:
  23. self.Y = torch.tensor(Y, dtype=torch.float32)
  24. def __len__(self):
  25. return len(self.X)
  26. def __getitem__(self, idx):
  27. if self.Y is not None:
  28. return self.X[idx], self.Y[idx]
  29. return self.X[idx]
  30. class DeltaAwareLoss(nn.Module):
  31. """
  32. Combined loss: HuberLoss + delta (change) penalty.
  33. Combines a robust HuberLoss on absolute values with a penalty on incorrect
  34. forecast dynamics (differences between consecutive timesteps). This encourages
  35. the model to not only predict accurate values but also preserve the temporal
  36. structure and rate of change in the fMRI time series.
  37. Attributes:
  38. alpha (float): Weight for the delta loss term. Controls the trade-off between
  39. prediction accuracy and dynamics preservation.
  40. base (nn.HuberLoss): Huber loss instance used for both absolute and delta terms.
  41. Input shapes:
  42. - pred: (batch_size, horizon, n_roi) predicted fMRI connectivity values
  43. - target: (batch_size, horizon, n_roi) ground truth fMRI connectivity values
  44. Output:
  45. - scalar tensor: weighted combination of base loss and delta loss
  46. Example:
  47. >>> criterion = DeltaAwareLoss(alpha=0.3, delta=0.5)
  48. >>> pred = torch.randn(32, 5, 100) # batch=32, horizon=5, n_roi=100
  49. >>> target = torch.randn(32, 5, 100)
  50. >>> loss = criterion(pred, target) # returns scalar
  51. """
  52. def __init__(self, alpha=0.3, delta=0.5):
  53. super().__init__()
  54. self.alpha = alpha
  55. self.base = nn.HuberLoss(delta=delta)
  56. def forward(self, pred, target):
  57. base_loss = self.base(pred, target)
  58. pred_delta = pred[:, 1:, :] - pred[:, :-1, :]
  59. target_delta = target[:, 1:, :] - target[:, :-1, :]
  60. delta_loss = self.base(pred_delta, target_delta)
  61. return base_loss + self.alpha * delta_loss
  62. def train_model(
  63. model,
  64. train_loader,
  65. val_loader=None,
  66. num_epochs=30,
  67. device=None,
  68. patience=5,
  69. checkpoint_dir=None,
  70. checkpoint_prefix="forecast_model",
  71. checkpoint_every=None,
  72. save_best=True,
  73. save_last=False,
  74. verbose=True
  75. ):
  76. if device is None:
  77. device = torch.device("cpu")
  78. elif isinstance(device, str):
  79. device = torch.device(device)
  80. if type(checkpoint_dir) is str:
  81. checkpoint_dir = Path(checkpoint_dir)
  82. if checkpoint_dir is not None:
  83. checkpoint_dir.mkdir(parents=True, exist_ok=True)
  84. optimizer = torch.optim.AdamW(
  85. model.parameters(), lr=5e-4, weight_decay=1e-5
  86. )
  87. scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
  88. optimizer, mode="min", factor=0.5, patience=patience
  89. )
  90. criterion = DeltaAwareLoss(alpha=0.3, delta=0.5)
  91. best_val_loss = float("inf")
  92. patience_counter = 0
  93. best_state = None
  94. def _save_checkpoint(path, epoch, train_loss, val_loss=None, is_best=False):
  95. if checkpoint_dir is None:
  96. return
  97. torch.save(
  98. {
  99. "epoch": epoch,
  100. "model_state_dict": model.state_dict(),
  101. "optimizer_state_dict": optimizer.state_dict(),
  102. "scheduler_state_dict": scheduler.state_dict(),
  103. "train_loss": train_loss,
  104. "val_loss": val_loss,
  105. "best_val_loss": best_val_loss,
  106. "is_best": is_best,
  107. },
  108. path,
  109. )
  110. for epoch in range(num_epochs):
  111. model.train()
  112. total_loss = 0.0
  113. loop = tqdm(train_loader, desc=f"Epoch {epoch+1}/{num_epochs}", leave=False)
  114. for X_batch, Y_batch in loop:
  115. X_batch = X_batch.to(device, non_blocking=True)
  116. Y_batch = Y_batch.to(device, non_blocking=True)
  117. optimizer.zero_grad()
  118. loss = criterion(model(X_batch), Y_batch)
  119. loss.backward()
  120. optimizer.step()
  121. total_loss += loss.item()
  122. loop.set_postfix(loss=f"{loss.item():.4f}")
  123. avg_loss = total_loss / len(train_loader)
  124. scheduler.step(avg_loss)
  125. if val_loader is not None:
  126. model.eval()
  127. val_loss = 0.0
  128. with torch.no_grad():
  129. for xb, yb in val_loader:
  130. val_loss += criterion(
  131. model(xb.to(device)), yb.to(device)
  132. ).item()
  133. val_loss /= len(val_loader)
  134. if verbose:
  135. print(f" Epoch {epoch+1:2d} | train: {avg_loss:.6f} | val: {val_loss:.6f}")
  136. if val_loss < best_val_loss:
  137. best_val_loss = val_loss
  138. best_state = {k: v.clone() for k, v in model.state_dict().items()}
  139. patience_counter = 0
  140. if save_best and checkpoint_dir is not None:
  141. _save_checkpoint(
  142. checkpoint_dir / f"{checkpoint_prefix}_best.pt",
  143. epoch=epoch + 1,
  144. train_loss=avg_loss,
  145. val_loss=val_loss,
  146. is_best=True,
  147. )
  148. else:
  149. patience_counter += 1
  150. if verbose:
  151. print(f" No improvement ({patience_counter}/{patience})")
  152. if patience_counter >= patience:
  153. if verbose:
  154. print(f" Early stopping at epoch {epoch+1}")
  155. break
  156. else:
  157. if verbose:
  158. print(f" Epoch {epoch+1:2d} | train loss: {avg_loss:.6f}")
  159. if avg_loss < best_val_loss:
  160. best_val_loss = avg_loss
  161. best_state = {k: v.detach().cpu().clone() for k, v in model.state_dict().items()}
  162. if save_best and checkpoint_dir is not None:
  163. _save_checkpoint(
  164. checkpoint_dir / f"{checkpoint_prefix}_best.pt",
  165. epoch=epoch + 1,
  166. train_loss=avg_loss,
  167. val_loss=None,
  168. is_best=True,
  169. )
  170. if (
  171. checkpoint_dir is not None
  172. and checkpoint_every is not None
  173. and checkpoint_every > 0
  174. and (epoch + 1) % checkpoint_every == 0
  175. ):
  176. _save_checkpoint(
  177. checkpoint_dir / f"{checkpoint_prefix}_epoch{epoch+1:03d}.pt",
  178. epoch=epoch + 1,
  179. train_loss=avg_loss,
  180. val_loss=val_loss if val_loader is not None else None,
  181. is_best=False,
  182. )
  183. if save_last and checkpoint_dir is not None:
  184. _save_checkpoint(
  185. checkpoint_dir / f"{checkpoint_prefix}_last.pt",
  186. epoch=epoch + 1,
  187. train_loss=avg_loss,
  188. val_loss=val_loss if val_loader is not None else None,
  189. is_best=False,
  190. )
  191. if best_state is not None:
  192. model.load_state_dict(best_state)
  193. if verbose:
  194. print(f" Best val loss: {best_val_loss:.6f} | weights restored")
  195. return model
  196. def get_predictions(model, loader, device=None):
  197. """Runs inference and returns concatenated predictions."""
  198. if device is None:
  199. device = next(model.parameters()).device
  200. model = model.to(device)
  201. model.eval()
  202. all_preds = []
  203. with torch.no_grad():
  204. for batch in loader:
  205. if isinstance(batch, (tuple, list)):
  206. xb = batch[0]
  207. else:
  208. xb = batch
  209. xb = xb.to(device, non_blocking=True)
  210. preds = model(xb)
  211. all_preds.append(preds.detach().cpu().numpy())
  212. return np.concatenate(all_preds, axis=0)
  213. def _is_torch_model(model):
  214. return isinstance(model, nn.Module)
  215. def _flatten_model_inputs(X):
  216. """Convert windowed inputs (N, M, ROI) into tabular features for sklearn models."""
  217. return X.reshape(X.shape[0], -1)
  218. def _flatten_model_targets(Y):
  219. """Convert forecasting targets into 2D multi-output targets for sklearn models."""
  220. return Y.reshape(Y.shape[0], -1)
  221. def _reshape_predictions(preds, target_shape):
  222. """Restore flattened sklearn predictions back to forecasting shape."""
  223. preds = np.asarray(preds, dtype=np.float32)
  224. if preds.shape == target_shape:
  225. return preds
  226. if preds.ndim == 1:
  227. preds = preds[:, None]
  228. if preds.ndim == 2:
  229. return preds.reshape(target_shape)
  230. raise ValueError(
  231. f"Could not reshape predictions from {preds.shape} to {target_shape}"
  232. )
  233. def _expects_windowed_input(model):
  234. return getattr(model, "expects_windowed_input", False)
  235. def _clone_model(model):
  236. """Best-effort clone for either PyTorch modules or sklearn estimators."""
  237. if _is_torch_model(model):
  238. return {k: v.detach().cpu().clone() for k, v in model.state_dict().items()}
  239. return copy.deepcopy(model)
  240. def train_forecasting_model(
  241. model,
  242. X_train,
  243. Y_train,
  244. X_val=None,
  245. Y_val=None,
  246. batch_size=512,
  247. num_epochs=30,
  248. device=None,
  249. patience=5,
  250. checkpoint_dir=None,
  251. checkpoint_prefix="forecast_model",
  252. checkpoint_every=None,
  253. save_best=True,
  254. save_last=False,
  255. verbose=True
  256. ):
  257. """
  258. Train either a PyTorch forecasting model or an sklearn-style estimator.
  259. """
  260. if _is_torch_model(model):
  261. train_loader = DataLoader(
  262. FMRIWindowDataset(X_train, Y_train),
  263. batch_size=batch_size,
  264. shuffle=True,
  265. pin_memory=True,
  266. )
  267. val_loader = None
  268. if X_val is not None and Y_val is not None and len(X_val) > 0:
  269. val_loader = DataLoader(
  270. FMRIWindowDataset(X_val, Y_val),
  271. batch_size=batch_size,
  272. shuffle=False,
  273. pin_memory=True,
  274. )
  275. return train_model(
  276. model,
  277. train_loader,
  278. val_loader=val_loader,
  279. num_epochs=num_epochs,
  280. device=device,
  281. patience=patience,
  282. checkpoint_dir=checkpoint_dir,
  283. checkpoint_prefix=checkpoint_prefix,
  284. checkpoint_every=checkpoint_every,
  285. save_best=save_best,
  286. save_last=save_last,
  287. verbose=verbose,
  288. )
  289. if hasattr(model, "fit") and hasattr(model, "predict"):
  290. if _expects_windowed_input(model):
  291. model.fit(X_train, Y_train)
  292. else:
  293. X_train_flat = _flatten_model_inputs(X_train)
  294. Y_train_flat = _flatten_model_targets(Y_train)
  295. model.fit(X_train_flat, Y_train_flat)
  296. return model
  297. raise TypeError(
  298. "Unsupported model type. Expected a torch.nn.Module or an estimator "
  299. "with fit/predict methods."
  300. )
  301. def predict_forecasting_model(model, X, batch_size=512, device=None):
  302. """Run inference for either a PyTorch forecasting model or an sklearn estimator."""
  303. if _is_torch_model(model):
  304. test_loader = DataLoader(
  305. FMRIWindowDataset(X),
  306. batch_size=batch_size,
  307. shuffle=False,
  308. pin_memory=True,
  309. )
  310. return get_predictions(model, test_loader, device)
  311. if hasattr(model, "predict"):
  312. if _expects_windowed_input(model):
  313. preds = model.predict(X)
  314. else:
  315. preds = model.predict(_flatten_model_inputs(X))
  316. return preds
  317. raise TypeError(
  318. "Unsupported model type. Expected a torch.nn.Module or an estimator "
  319. "with a predict method."
  320. )
  321. def _make_model(model_gen, n_roi=None, M=None, H=None):
  322. """
  323. Instantiate model_gen while preserving compatibility with zero-arg notebook
  324. lambdas and newer factories that accept fold dimensions.
  325. """
  326. try:
  327. signature = inspect.signature(model_gen)
  328. except (TypeError, ValueError):
  329. return model_gen()
  330. kwargs = {}
  331. for name in signature.parameters:
  332. if name in {"n_roi", "input_size", "input_dim"} and n_roi is not None:
  333. kwargs[name] = n_roi
  334. elif name in {"M", "window_size"} and M is not None:
  335. kwargs[name] = M
  336. elif name in {"H", "output_horizon", "horizon"} and H is not None:
  337. kwargs[name] = H
  338. if kwargs:
  339. return model_gen(**kwargs)
  340. return model_gen()
  341. def horizon_rmse(y_true, y_pred):
  342. """Compute and print RMSE separately for each forecast step."""
  343. horizon_scores = []
  344. print("\nHorizon-wise RMSE:")
  345. for h in range(y_true.shape[1]):
  346. r = compute_rmse(y_true[:, h, :], y_pred[:, h, :])
  347. horizon_scores.append(r)
  348. print(f" Step {h+1} RMSE: {r:.6f}")
  349. return horizon_scores
  350. def run_loso_cv(dataset_raw, model_gen, M=50, H=3, stride=1,
  351. num_epochs=20, batch_size=512, device=device,
  352. checkpoint_dir=None, checkpoint_prefix="forecast_model",
  353. checkpoint_every=None, save_best=True, save_last=False,
  354. results_path="loso_results.csv", patience=5,
  355. compute_naive_rmse=True):
  356. """
  357. Leave-One-Subject-Out Cross Validation (LOSO-CV).
  358. Supports both:
  359. - PyTorch forecasting models with the existing training loop
  360. - sklearn-style estimators exposing fit(X, y) and predict(X)
  361. Set ``compute_naive_rmse=False`` when a caller computes the last-value
  362. baseline once and joins those scores onto multiple model result tables.
  363. Returns:
  364. df - LOSO summary dataframe
  365. last_trained_model - model from the last fold
  366. last_X_test - test windows from the last fold
  367. last_Y_test - test targets from the last fold
  368. best_model - model with highest eta across all folds
  369. best_X_test - test windows of the best eta fold
  370. best_Y_test - test targets of the best eta fold
  371. """
  372. if checkpoint_dir is not None:
  373. checkpoint_dir = Path(checkpoint_dir)
  374. checkpoint_dir.mkdir(parents=True, exist_ok=True)
  375. subjects = sorted(set(d["subject"] for d in dataset_raw))
  376. n_subjects = len(subjects)
  377. print(f"\n{'='*60}")
  378. print(f"LOSO-CV | {n_subjects} subjects | M={M}, H={H}")
  379. print(f"{'='*60}")
  380. fold_results = []
  381. last_trained_model = None
  382. last_X_test = None
  383. last_Y_test = None
  384. best_eta_model = None
  385. best_eta_score = -float("inf")
  386. best_eta_subject = None
  387. best_X_test = None
  388. best_Y_test = None
  389. best_n_roi = None
  390. for fold_i, test_subj in enumerate(tqdm(subjects, desc="LOSO Folds")):
  391. print(f"\nFold {fold_i+1}/{n_subjects} | Test subject: {test_subj}")
  392. train_items, test_items = split_by_subject(
  393. dataset_raw,
  394. test_subjects=[test_subj]
  395. )
  396. train_norm = normalize_items(train_items)
  397. test_norm = normalize_items(test_items)
  398. X_tr, Y_tr = build_sliding_windows(train_norm, M, H, stride)
  399. X_te, Y_te = build_sliding_windows(test_norm, M, H, stride)
  400. if len(X_tr) == 0 or len(X_te) == 0:
  401. print("Skipping fold (no valid windows)")
  402. continue
  403. val_split = int(len(X_tr) * 0.9)
  404. X_val, Y_val = X_tr[val_split:], Y_tr[val_split:]
  405. X_tr, Y_tr = X_tr[:val_split], Y_tr[:val_split]
  406. n_roi = X_tr.shape[2]
  407. print(f"Train windows: {len(X_tr)} | Val windows: {len(X_val)} "
  408. f"| Test windows: {len(X_te)} | ROIs: {n_roi}")
  409. try:
  410. model = _make_model(model_gen, n_roi=n_roi, M=M, H=H)
  411. if _is_torch_model(model):
  412. model = model.to(device)
  413. except AttributeError as e:
  414. print(f"Model is not CUDA compatible: {e}\nContinuing with CPU...")
  415. model = _make_model(model_gen, n_roi=n_roi, M=M, H=H)
  416. fold_checkpoint_dir = None
  417. if checkpoint_dir is not None and _is_torch_model(model):
  418. fold_checkpoint_dir = checkpoint_dir
  419. print(f"Training (max {num_epochs} epochs, early stopping patience={patience})...")
  420. model = train_forecasting_model(
  421. model,
  422. X_tr,
  423. Y_tr,
  424. X_val=X_val,
  425. Y_val=Y_val,
  426. batch_size=batch_size,
  427. num_epochs=num_epochs,
  428. device=device,
  429. patience=patience,
  430. checkpoint_dir=fold_checkpoint_dir,
  431. checkpoint_prefix=f"{checkpoint_prefix}_fold{fold_i+1:02d}_{test_subj}",
  432. checkpoint_every=checkpoint_every,
  433. save_best=save_best,
  434. save_last=save_last,
  435. )
  436. all_preds = predict_forecasting_model(
  437. model,
  438. X_te,
  439. batch_size=batch_size,
  440. device=device,
  441. )
  442. all_preds = _reshape_predictions(all_preds, Y_te.shape)
  443. all_targets = Y_te
  444. model_r = compute_rmse(all_targets, all_preds)
  445. model_rmsse = compute_rmsse(all_targets, all_preds)
  446. eta = compute_eta_gauss(all_targets, all_preds)
  447. naive_r = np.nan
  448. beat_naive = pd.NA
  449. if compute_naive_rmse:
  450. naive_model = last_value_model_generator(H=H)
  451. naive_model = train_forecasting_model(
  452. naive_model,
  453. X_tr,
  454. Y_tr,
  455. verbose=False,
  456. )
  457. naive_preds = predict_forecasting_model(
  458. naive_model,
  459. X_te,
  460. batch_size=batch_size,
  461. device=device,
  462. )
  463. naive_preds = _reshape_predictions(naive_preds, Y_te.shape)
  464. naive_r = compute_rmse(all_targets, naive_preds)
  465. beat_naive = model_r < naive_r
  466. print(f"\nResults:")
  467. print(f" MODEL RMSE : {model_r:.6f}")
  468. if compute_naive_rmse:
  469. print(f" Naive RMSE : {naive_r:.6f}")
  470. print(f" eta : {eta:.4f}")
  471. if compute_naive_rmse:
  472. print(f" Beat naive : {'YES' if beat_naive else 'NO'}")
  473. hor_rmse = horizon_rmse(all_targets, all_preds)
  474. fold_results.append({
  475. "test_subject": test_subj,
  476. "Model_RMSE": round(model_r, 6),
  477. "Naive_RMSE": round(naive_r, 6),
  478. "Model_RMSSE": round(model_rmsse, 6),
  479. "eta": round(eta, 4),
  480. "beat_naive": beat_naive,
  481. })
  482. last_trained_model = model
  483. last_X_test = X_te
  484. last_Y_test = Y_te
  485. if eta > best_eta_score:
  486. best_eta_score = eta
  487. best_eta_subject = test_subj
  488. best_eta_model = _clone_model(model)
  489. best_X_test = X_te.copy()
  490. best_Y_test = Y_te.copy()
  491. best_n_roi = n_roi
  492. print(f" New best eta model saved: {test_subj} (eta={eta:.4f})")
  493. del X_tr, Y_tr, X_val, Y_val
  494. del train_norm, test_norm
  495. torch.cuda.empty_cache()
  496. print(f"\n{'='*60}")
  497. print("LOSO-CV SUMMARY")
  498. print(f"{'='*60}")
  499. df = pd.DataFrame(fold_results)
  500. if df.empty:
  501. raise ValueError(
  502. "LOSO-CV produced no valid folds. Check subject count and window "
  503. f"settings M={M}, H={H}, stride={stride}."
  504. )
  505. print(df.to_string(index=False))
  506. print(f"\nMean Model RMSE : {df['Model_RMSE'].mean():.6f}")
  507. if compute_naive_rmse:
  508. print(f"Mean Naive RMSE : {df['Naive_RMSE'].mean():.6f}")
  509. print(f"Mean eta : {df['eta'].mean():.4f}")
  510. print(f"Mean RMSSE : {df['Model_RMSSE'].mean():.6f}")
  511. if compute_naive_rmse:
  512. print(f"Folds beat naive : {df['beat_naive'].sum()} / {len(df)}")
  513. if results_path is not None:
  514. results_path = Path(results_path)
  515. results_path.parent.mkdir(parents=True, exist_ok=True)
  516. df.to_csv(results_path, index=False)
  517. print(f"\nResults saved to {results_path}")
  518. print(f"\nBest eta fold : {best_eta_subject} (eta={best_eta_score:.4f})")
  519. best_model = _make_model(model_gen, n_roi=best_n_roi, M=M, H=H)
  520. if _is_torch_model(best_model):
  521. best_model = best_model.to(device)
  522. if best_eta_model is not None:
  523. best_model.load_state_dict(best_eta_model)
  524. best_model.eval()
  525. elif best_eta_model is not None:
  526. best_model = best_eta_model
  527. return df, last_trained_model, last_X_test, last_Y_test, \
  528. best_model, best_X_test, best_Y_test

training.py at commit a064b79, under Apache-2.0 · at the source

Overview

  1. Department of Electrical & Computer Engineering, Rice University, Houston, TX 77005, USA; (Z.Z.); (B.A.)
  2. Department of Electrical & Electronics Engineering, Özyeğin University, 34794 Istanbul, Türkiye
  3. Department of Urology, Houston Methodist, Houston, TX 77030, USA
Institutions: Rice University (United States); Özyeğin University (Türkiye); Houston Methodist (United States)
Journal: Entropy (Basel, Switzerland), volume 28, issue 7, article 738
Dates: received 22 May 2026; accepted 22 June 2026; published online 1 July 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.3390/e28070738 · PMID 42511348 · PMCID PMC13409728 · OpenAlex W7166854284
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: fMRI (modality), computational (subfield)
Methods: Statistics, Machine learning, Preprocessing, Connectivity, fMRI & imaging
Keywords: functional magnetic resonance imaging, BOLD signal, probabilistic forecasting, information theory, entropy, directed information, brain dynamics, stochastic processes, transformer models, recurrent neural networks
Topic: Functional Brain Connectivity Studies (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Funding: Rice University (Provost's TMC Collaborator Seed Fund)
Citations: not cited yet (Europe PMC); 32 references in the paper

Abstract

Functional magnetic resonance imaging (fMRI) signals exhibit complex temporal structure arising from multivariate neural dynamics, physiological variability, and measurement uncertainty. In this work, we formulate region-of-interest-level fMRI analysis as a probabilistic multi-step forecasting problem and investigate the predictability of blood-oxygen-level-dependent (BOLD) activity from an information-theoretic perspective. Using the Natural Scenes Dataset, we model multiregional BOLD activity as a stochastic process with finite memory and train multiple forecasting architectures, including linear regression, exponential smoothing, recurrent neural networks, and transformer-based models, to predict future BOLD samples from preceding temporal observations. Forecasting performance is analyzed together with entropy-based quantities, including marginal entropy, conditional entropy, and normalized predictive information measures estimated directly from model-derived predictive distributions without imposing restrictive Gaussian assumptions on the underlying BOLD dynamics. The transformer model achieved significant improvement over a naive persistence baseline (p=0.001) while yielding a high predictive information fraction (η=75.49%). Post hoc directed information analysis revealed that short-horizon prediction was dominated primarily by autoregressive, within-ROI, temporal structure. Overall, the proposed framework demonstrates how probabilistic forecasting and information-theoretic analysis can be integrated to characterize the predictability, uncertainty structure, and directional organization of large-scale fMRI dynamics and may support future downstream neuroengineering and neural-state inference applications.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repositories

Its files are read in the Code ↔ Paper reader above, with 20 matches between paragraphs and lines of code.

ab126/fmri_forecasting

License: Apache-2.0
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: a064b79113462d62a1c3808f3e3008fe8cf8df31, 15 September 2026
Languages: Python (22), Jupyter (6)
Size: 31 files, 28 scripts
Software Heritage: not archived
Found in: “Data Availability Statement”
Holds: README, license file, tests, 5 notebooks
Not found: CITATION.cff, environment file, continuous integration, documentation
Tools: NumPy (20 files), PyTorch (13 files), Matplotlib (6 files), scikit-learn (4 files), NiBabel (3 files), pandas (3 files), seaborn (3 files), Nilearn (2 files), ANTs (1 file), statsmodels (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
30 files

Zenodo 20341604

License: Apache-2.0
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Size: 1 file
Software Heritage: not checked
Found in: “Data Availability Statement”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (19 files), PyTorch (13 files), Matplotlib (6 files), scikit-learn (4 files), pandas (3 files), seaborn (3 files), NiBabel (2 files), Nilearn (2 files), ANTs (1 file), statsmodels (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers (HTTP 200)
  • 27 September 2026: the link answers (HTTP 200)
29 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:

  • 2 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 55 scripts, each with its path and the digest of its content;
  • 20 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 Natural Scenes Dataset (NSD) analyzed in this study is publicly available from the original NSD release [17]. The code used for forecasting, entropy estimation, and directed information analysis is publicly available at https://github.com/ab126/fmri_forecasting (accessed on 21 June 2026). An archived release of the code associated with this manuscript is available through Zenodo: https://doi.org/10.5281/zenodo.20341604.

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, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 5 authors, 10 keywords, 1 funder, 21 references.

Cite

This paper

Bayer, A., Zhang, Z., Ipek, A. E., Khavari, R., & Aazhang, B. (2026). Probabilistic Forecasting and Information-Theoretic Analysis of Multivariate fMRI Dynamics. Entropy (Basel, Switzerland), 28(7), 738. https://doi.org/10.3390/e28070738

BibTeX

@article{bayer2026probabilistic,
author = {Bayer, Arda and Zhang, Zhiyao and Ipek, Ahmet Emre and Khavari, Rose and Aazhang, Behnaam},
title = {{Probabilistic Forecasting and Information-Theoretic Analysis of Multivariate fMRI Dynamics}},
journal = {Entropy (Basel, Switzerland)},
year = {2026},
month = jul,
volume = {28},
number = {7},
pages = {738},
publisher = {Multidisciplinary Digital Publishing Institute (MDPI)},
issn = {1099-4300},
doi = {10.3390/e28070738},
url = {https://doi.org/10.3390/e28070738},
pmid = {42511348},
pmcid = {PMC13409728}
}

RIS

TY - JOUR
AU - Bayer, Arda
AU - Zhang, Zhiyao
AU - Ipek, Ahmet Emre
AU - Khavari, Rose
AU - Aazhang, Behnaam
TI - Probabilistic Forecasting and Information-Theoretic Analysis of Multivariate fMRI Dynamics
T2 - Entropy (Basel, Switzerland)
J2 - Entropy (Basel)
PY - 2026
DA - 2026/07/01
VL - 28
IS - 7
SP - 738
SN - 1099-4300
PB - Multidisciplinary Digital Publishing Institute (MDPI)
DO - 10.3390/e28070738
UR - https://doi.org/10.3390/e28070738
LA - en
ER -

CSL-JSON

{
"id": "10.3390/e28070738",
"type": "article-journal",
"title": "Probabilistic Forecasting and Information-Theoretic Analysis of Multivariate fMRI Dynamics",
"container-title": "Entropy (Basel, Switzerland)",
"author": [
{
"family": "Bayer",
"given": "Arda"
},
{
"family": "Zhang",
"given": "Zhiyao"
},
{
"family": "Ipek",
"given": "Ahmet Emre"
},
{
"family": "Khavari",
"given": "Rose"
},
{
"family": "Aazhang",
"given": "Behnaam"
}
],
"container-title-short": "Entropy (Basel)",
"volume": "28",
"issue": "7",
"page": "738",
"DOI": "10.3390/e28070738",
"PMID": "42511348",
"PMCID": "PMC13409728",
"ISSN": "1099-4300",
"publisher": "Multidisciplinary Digital Publishing Institute (MDPI)",
"URL": "https://doi.org/10.3390/e28070738",
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
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.7554/elife.107933 [code]
Modality-agnostic decoding of vision and language from fMRI.
Journal: eLife
In common: Nilearn, NiBabel, statsmodels, 6 other tools, fMRI, 3 references
[2] doi:10.1038/s41597-026-07248-6 [code]
A large-scale fMRI dataset for vision-language semantic association.
Journal: Scientific data
In common: ANTs, Nilearn, NiBabel, 5 other tools, fMRI, 2 references
[3] doi:10.1162/netn.a.547 [code]
An evaluation of the efficacy of single-echo and multi-echo fMRI denoising strategies.
Journal: Network neuroscience (Cambridge, Mass.)
In common: ANTs, Nilearn, NiBabel, 6 other tools, fMRI, 1 reference
[4] doi:10.1016/j.neuroimage.2026.122171 [code]
A conserved node degree-based backbone and flexible hub organization of brain connectome during naturalistic movie watching.
Journal: NeuroImage
In common: ANTs, Nilearn, NiBabel, 4 other tools, fMRI, 2 references
[5] doi:10.1162/imag.a.1286 [code]
Behavioral imitation with artificial neural networks leads to personalized models of brain dynamics during videogame play.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: Nilearn, NiBabel, statsmodels, 6 other tools, fMRI, 1 reference
[6] doi:10.1162/imag.a.1256 [code]
Gamer in the scanner: Event-related analysis of fMRI activity during retro videogame play guided by automated annotations of game content.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: Nilearn, NiBabel, statsmodels, 6 other tools, fMRI, 1 reference
[7] doi:10.1038/s41467-026-76452-0 [code]
Music evokes shared neural representations of imagined narratives across sensory modalities.
Journal: Nature communications
In common: ANTs, Nilearn, NiBabel, 7 other tools
[8] doi:10.1371/journal.pbio.3003856 [code]
Aging and metabolism contribute separately to brain-body health.
Journal: PLoS biology
In common: ANTs, Nilearn, NiBabel, 7 other tools
[9] doi:10.1162/imag.a.1164 [code]
Bias and generalizability of brain age prediction models: A multi-cohort evaluation with anatomical and interpretability insights.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: ANTs, Nilearn, NiBabel, 7 other tools
[10] doi:10.1038/s41467-026-71151-2 [code]
Common and distinct neural correlates of social interaction processing and theory of mind in narratives.
Journal: Nature communications
In common: ANTs, Nilearn, NiBabel, 6 other tools, 1 reference

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.

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: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.