training.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. import pathlib as pl
  2. import time
  3. from threading import Event
  4. from typing import Callable, Tuple, cast
  5. import numpy as np
  6. import pandas as pd
  7. import torch
  8. import torch.nn as nn
  9. from torch.utils.data import DataLoader
  10. from model.dataset import ADNIDataset
  11. from util.progress import ProgressTracker
  12. from util.ui_logger import PipelineLogger
  13. type TrainMetrics = Tuple[
  14. float, float, float, float
  15. ] # (train_loss, val_loss, train_acc, val_acc)
  16. type TestMetrics = Tuple[float, float] # (test_loss, test_acc)
  17. type KLLossFn = Callable[[nn.Module], torch.Tensor | None]
  18. def _model_device(model: nn.Module) -> torch.device:
  19. first_param = next(model.parameters(), None)
  20. if first_param is None:
  21. return torch.device("cpu")
  22. return first_param.device
  23. def _move_batch_to_model_device(
  24. model: nn.Module,
  25. mri: torch.Tensor,
  26. xls: torch.Tensor,
  27. targets: torch.Tensor,
  28. ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
  29. device = _model_device(model)
  30. mri = mri.to(device, non_blocking=True)
  31. xls = xls.to(device, non_blocking=True)
  32. targets = targets.to(device, non_blocking=True)
  33. return mri, xls, targets
  34. def _batch_correct_and_total(
  35. outputs: torch.Tensor,
  36. targets: torch.Tensor,
  37. ) -> Tuple[int, int]:
  38. if outputs.ndim > 1 and outputs.size(-1) > 1:
  39. predicted_classes = outputs.argmax(dim=1)
  40. if targets.ndim > 1 and targets.size(-1) > 1:
  41. target_classes = targets.argmax(dim=1)
  42. else:
  43. target_classes = targets.long().view(-1)
  44. correct = int((predicted_classes == target_classes).sum().item())
  45. total = int(target_classes.numel())
  46. return correct, total
  47. predicted = (outputs > 0.5).to(dtype=targets.dtype)
  48. correct = int((predicted == targets).sum().item())
  49. total = int(targets.numel())
  50. return correct, total
  51. def _check_control_events(
  52. stop_event: Event | None,
  53. pause_event: Event | None,
  54. ) -> None:
  55. if stop_event is not None and stop_event.is_set():
  56. raise InterruptedError("Pipeline execution stopped by user.")
  57. while pause_event is not None and pause_event.is_set():
  58. time.sleep(0.5)
  59. if stop_event is not None and stop_event.is_set():
  60. raise InterruptedError("Pipeline execution stopped by user while paused.")
  61. def test_model(
  62. model: nn.Module,
  63. test_loader: DataLoader[ADNIDataset],
  64. criterion: nn.Module,
  65. progress: ProgressTracker,
  66. log: PipelineLogger,
  67. stop_event: Event | None = None,
  68. pause_event: Event | None = None,
  69. ) -> TestMetrics:
  70. """
  71. Tests the model on the test dataset.
  72. Args:
  73. model (nn.Module): The model to test.
  74. test_loader (DataLoader[ADNIDataset]): DataLoader for the test dataset.
  75. criterion (nn.Module): Loss function to compute the loss.
  76. Returns:
  77. TrainMetrics: A tuple containing the test loss and test accuracy.
  78. """
  79. model.eval()
  80. test_loss = 0.0
  81. correct = 0
  82. total = 0
  83. total_samples = 0
  84. test_progress = progress.get_sub_tracker("Testing Batches")
  85. test_progress.update(total=len(test_loader), advance=0)
  86. for _, (mri, xls, targets, _) in enumerate(test_loader):
  87. _check_control_events(stop_event=stop_event, pause_event=pause_event)
  88. mri, xls, targets = _move_batch_to_model_device(model, mri, xls, targets)
  89. outputs = model((mri, xls))
  90. loss = criterion(outputs, targets)
  91. batch_size = mri.size(0)
  92. test_loss += loss.item() * batch_size
  93. total_samples += batch_size
  94. # Calculate accuracy
  95. batch_correct, batch_total = _batch_correct_and_total(outputs, targets)
  96. correct += batch_correct
  97. total += batch_total
  98. test_progress.update(total=len(test_loader), advance=1)
  99. test_loss = test_loss / total_samples if total_samples > 0 else 0.0
  100. test_acc = correct / total if total > 0 else 0.0
  101. return test_loss, test_acc
  102. def train_epoch(
  103. model: nn.Module,
  104. train_loader: DataLoader[ADNIDataset],
  105. val_loader: DataLoader[ADNIDataset],
  106. optimizer: torch.optim.Optimizer,
  107. criterion: nn.Module,
  108. log: PipelineLogger,
  109. progress: ProgressTracker,
  110. stop_event: Event | None = None,
  111. pause_event: Event | None = None,
  112. ) -> Tuple[float, float, float, float]:
  113. """
  114. Trains the model for one epoch and evaluates it on the validation set.
  115. Args:
  116. model (nn.Module): The model to train.
  117. train_loader (DataLoader[ADNIDataset]): DataLoader for the training dataset.
  118. val_loader (DataLoader[ADNIDataset]): DataLoader for the validation dataset.
  119. optimizer (torch.optim.Optimizer): Optimizer for updating model parameters.
  120. criterion (nn.Module): Loss function to compute the loss.
  121. Returns:
  122. Tuple[float, float, float, float]: A tuple containing the training loss, validation loss, training accuracy, and validation accuracy.
  123. """
  124. model.train()
  125. train_loss = 0.0
  126. train_correct = 0
  127. train_total = 0
  128. train_total_samples = 0
  129. # Training loop - with progress bar for the training batches
  130. train_progress = progress.get_sub_tracker("Training Batches")
  131. train_progress.reset()
  132. train_progress.set_title("Training Batches")
  133. train_progress.update(total=len(train_loader), advance=0)
  134. for _, (mri, xls, targets, _) in enumerate(train_loader):
  135. _check_control_events(stop_event=stop_event, pause_event=pause_event)
  136. mri, xls, targets = _move_batch_to_model_device(model, mri, xls, targets)
  137. optimizer.zero_grad()
  138. outputs = model((mri, xls))
  139. loss = criterion(outputs, targets)
  140. loss.backward() # type: ignore[reportUnknownMemberType]
  141. optimizer.step()
  142. batch_size = mri.size(0)
  143. train_loss += loss.item() * batch_size
  144. train_total_samples += batch_size
  145. batch_correct, batch_total = _batch_correct_and_total(outputs, targets)
  146. train_correct += batch_correct
  147. train_total += batch_total
  148. train_progress.update(total=len(train_loader), advance=1)
  149. train_loss = train_loss / train_total_samples if train_total_samples > 0 else 0.0
  150. model.eval()
  151. val_loss = 0.0
  152. correct = 0
  153. total = 0
  154. val_total_samples = 0
  155. test_progress = progress.get_sub_tracker("Validation Batches")
  156. test_progress.reset()
  157. test_progress.set_title("Validation Batches")
  158. test_progress.update(total=len(val_loader), advance=0)
  159. with torch.no_grad():
  160. for _, (mri, xls, targets, _) in enumerate(val_loader):
  161. _check_control_events(stop_event=stop_event, pause_event=pause_event)
  162. mri, xls, targets = _move_batch_to_model_device(model, mri, xls, targets)
  163. outputs = model((mri, xls))
  164. loss = criterion(outputs, targets)
  165. batch_size = mri.size(0)
  166. val_loss += loss.item() * batch_size
  167. val_total_samples += batch_size
  168. # Calculate accuracy
  169. batch_correct, batch_total = _batch_correct_and_total(outputs, targets)
  170. correct += batch_correct
  171. total += batch_total
  172. test_progress.update(total=len(val_loader), advance=1)
  173. val_loss = val_loss / val_total_samples if val_total_samples > 0 else 0.0
  174. val_acc = correct / total if total > 0 else 0.0
  175. train_acc = train_correct / train_total if train_total > 0 else 0.0
  176. return train_loss, val_loss, train_acc, val_acc
  177. def train_model(
  178. model: nn.Module,
  179. train_loader: DataLoader[ADNIDataset],
  180. val_loader: DataLoader[ADNIDataset],
  181. optimizer: torch.optim.Optimizer,
  182. criterion: nn.Module,
  183. num_epochs: int,
  184. output_path: pl.Path,
  185. progress: ProgressTracker,
  186. log: PipelineLogger,
  187. stop_event: Event | None = None,
  188. pause_event: Event | None = None,
  189. ) -> Tuple[nn.Module, pd.DataFrame]:
  190. """
  191. Trains the model using the provided training and validation data loaders.
  192. Args:
  193. model (nn.Module): The model to train.
  194. train_loader (DataLoader[ADNIDataset]): DataLoader for the training dataset.
  195. val_loader (DataLoader[ADNIDataset]): DataLoader for the validation dataset.
  196. num_epochs (int): Number of epochs to train the model.
  197. learning_rate (float): Learning rate for the optimizer.
  198. Returns:
  199. Result[nn.Module, str]: A Result object containing the trained model or an error message.
  200. """
  201. # Record the training history
  202. # We record the Epoch, Training Loss, Validation Loss, Training Accuracy, and Validation Accuracy
  203. # use a (num_epochs, 4) shape ndarray to store the history before creating the DataArray
  204. nhist = np.zeros((num_epochs, 4), dtype=np.float32)
  205. # Get new progress bar for epochs
  206. epoch_progress = progress.get_sub_tracker("Epochs")
  207. epoch_progress.update(total=num_epochs, advance=0)
  208. for epoch in range(num_epochs):
  209. _check_control_events(stop_event=stop_event, pause_event=pause_event)
  210. train_loss, val_loss, train_acc, val_acc = train_epoch(
  211. model,
  212. train_loader,
  213. val_loader,
  214. optimizer,
  215. criterion,
  216. log=log,
  217. progress=epoch_progress,
  218. stop_event=stop_event,
  219. pause_event=pause_event,
  220. )
  221. # Update the history
  222. nhist[epoch, 0] = train_loss
  223. nhist[epoch, 1] = val_loss
  224. nhist[epoch, 2] = train_acc
  225. nhist[epoch, 3] = val_acc
  226. log.info(
  227. f"Epoch [{epoch + 1}/{num_epochs}], "
  228. f"Train Loss: {train_loss:.4f}, Val Loss: {val_loss:.4f}, "
  229. f"Train Acc: {train_acc:.4f}, Val Acc: {val_acc:.4f}"
  230. )
  231. # If we are at 25, 50, or 75% of the epochs, save the model
  232. if num_epochs > 4:
  233. if (epoch + 1) % (num_epochs // 4) == 0:
  234. model_save_path = (
  235. output_path / "intermediate_models" / f"model_epoch_{epoch + 1}.pt"
  236. )
  237. torch.save(model.state_dict(), model_save_path)
  238. log.info(f"Model saved at epoch {epoch + 1}")
  239. epoch_progress.update(total=num_epochs, advance=1)
  240. # return the trained model and the traning history
  241. history = pd.DataFrame(
  242. data=nhist.astype(np.float32),
  243. columns=["train_loss", "val_loss", "train_acc", "val_acc"],
  244. index=np.arange(1, num_epochs + 1),
  245. )
  246. return model, history
  247. def test_model_bayesian(
  248. model: nn.Module,
  249. test_loader: DataLoader[ADNIDataset],
  250. criterion: nn.Module,
  251. get_kl_loss: KLLossFn,
  252. progress: ProgressTracker,
  253. log: PipelineLogger,
  254. stop_event: Event | None = None,
  255. pause_event: Event | None = None,
  256. ) -> TestMetrics:
  257. """
  258. Tests a Bayesian model on the test dataset with KL-augmented loss.
  259. """
  260. model.eval()
  261. test_loss = 0.0
  262. correct = 0
  263. total = 0
  264. total_samples = 0
  265. test_progress = progress.get_sub_tracker("Testing Batches")
  266. test_progress.update(total=len(test_loader), advance=0)
  267. with torch.no_grad():
  268. for _, (mri, xls, targets, _) in enumerate(test_loader):
  269. _check_control_events(stop_event=stop_event, pause_event=pause_event)
  270. mri, xls, targets = _move_batch_to_model_device(model, mri, xls, targets)
  271. outputs = model((mri, xls))
  272. data_loss = cast(torch.Tensor, criterion(outputs, targets))
  273. batch_size = mri.size(0)
  274. kl_term = get_kl_loss(model)
  275. kl_loss = (
  276. kl_term / batch_size
  277. if kl_term is not None
  278. else torch.tensor(0.0, device=outputs.device)
  279. )
  280. loss: torch.Tensor = data_loss + kl_loss
  281. test_loss += loss.item() * batch_size
  282. total_samples += batch_size
  283. batch_correct, batch_total = _batch_correct_and_total(outputs, targets)
  284. correct += batch_correct
  285. total += batch_total
  286. test_progress.update(total=len(test_loader), advance=1)
  287. test_loss = test_loss / total_samples if total_samples > 0 else 0.0
  288. test_acc = correct / total if total > 0 else 0.0
  289. return test_loss, test_acc
  290. def train_epoch_bayesian(
  291. model: nn.Module,
  292. train_loader: DataLoader[ADNIDataset],
  293. val_loader: DataLoader[ADNIDataset],
  294. optimizer: torch.optim.Optimizer,
  295. criterion: nn.Module,
  296. get_kl_loss: KLLossFn,
  297. log: PipelineLogger,
  298. progress: ProgressTracker,
  299. stop_event: Event | None = None,
  300. pause_event: Event | None = None,
  301. ) -> TrainMetrics:
  302. """
  303. Trains a Bayesian model for one epoch and evaluates on validation data.
  304. """
  305. model.train()
  306. train_loss = 0.0
  307. train_correct = 0
  308. train_total = 0
  309. train_total_samples = 0
  310. train_progress = progress.get_sub_tracker("Training Batches")
  311. train_progress.reset()
  312. train_progress.set_title("Training Batches")
  313. train_progress.update(total=len(train_loader), advance=0)
  314. for _, (mri, xls, targets, _) in enumerate(train_loader):
  315. _check_control_events(stop_event=stop_event, pause_event=pause_event)
  316. mri, xls, targets = _move_batch_to_model_device(model, mri, xls, targets)
  317. optimizer.zero_grad()
  318. outputs = model((mri, xls))
  319. data_loss = cast(torch.Tensor, criterion(outputs, targets))
  320. batch_size = mri.size(0)
  321. kl_term = get_kl_loss(model)
  322. kl_loss = (
  323. kl_term / batch_size
  324. if kl_term is not None
  325. else torch.tensor(0.0, device=outputs.device)
  326. )
  327. loss: torch.Tensor = data_loss + kl_loss
  328. loss.backward() # type: ignore[reportUnknownMemberType]
  329. optimizer.step()
  330. train_loss += loss.item() * batch_size
  331. train_total_samples += batch_size
  332. batch_correct, batch_total = _batch_correct_and_total(outputs, targets)
  333. train_correct += batch_correct
  334. train_total += batch_total
  335. train_progress.update(total=len(train_loader), advance=1)
  336. train_loss = train_loss / train_total_samples if train_total_samples > 0 else 0.0
  337. model.eval()
  338. val_loss = 0.0
  339. correct = 0
  340. total = 0
  341. val_total_samples = 0
  342. test_progress = progress.get_sub_tracker("Validation Batches")
  343. test_progress.reset()
  344. test_progress.set_title("Validation Batches")
  345. test_progress.update(total=len(val_loader), advance=0)
  346. with torch.no_grad():
  347. for _, (mri, xls, targets, _) in enumerate(val_loader):
  348. _check_control_events(stop_event=stop_event, pause_event=pause_event)
  349. mri, xls, targets = _move_batch_to_model_device(model, mri, xls, targets)
  350. outputs = model((mri, xls))
  351. data_loss = cast(torch.Tensor, criterion(outputs, targets))
  352. batch_size = mri.size(0)
  353. kl_term = get_kl_loss(model)
  354. kl_loss = (
  355. kl_term / batch_size
  356. if kl_term is not None
  357. else torch.tensor(0.0, device=outputs.device)
  358. )
  359. vloss: torch.Tensor = data_loss + kl_loss
  360. val_loss += vloss.item() * batch_size
  361. val_total_samples += batch_size
  362. batch_correct, batch_total = _batch_correct_and_total(outputs, targets)
  363. correct += batch_correct
  364. total += batch_total
  365. test_progress.update(total=len(val_loader), advance=1)
  366. val_loss = val_loss / val_total_samples if val_total_samples > 0 else 0.0
  367. val_acc = correct / total if total > 0 else 0.0
  368. train_acc = train_correct / train_total if train_total > 0 else 0.0
  369. return train_loss, val_loss, train_acc, val_acc
  370. def train_model_bayesian(
  371. model: nn.Module,
  372. train_loader: DataLoader[ADNIDataset],
  373. val_loader: DataLoader[ADNIDataset],
  374. optimizer: torch.optim.Optimizer,
  375. criterion: nn.Module,
  376. num_epochs: int,
  377. output_path: pl.Path,
  378. get_kl_loss: KLLossFn,
  379. progress: ProgressTracker,
  380. log: PipelineLogger,
  381. stop_event: Event | None = None,
  382. pause_event: Event | None = None,
  383. ) -> Tuple[nn.Module, pd.DataFrame]:
  384. """
  385. Trains a Bayesian model with KL-augmented objective.
  386. """
  387. nhist = np.zeros((num_epochs, 4), dtype=np.float32)
  388. epoch_progress = progress.get_sub_tracker("Epochs")
  389. epoch_progress.update(total=num_epochs, advance=0)
  390. for epoch in range(num_epochs):
  391. _check_control_events(stop_event=stop_event, pause_event=pause_event)
  392. train_loss, val_loss, train_acc, val_acc = train_epoch_bayesian(
  393. model,
  394. train_loader,
  395. val_loader,
  396. optimizer,
  397. criterion,
  398. get_kl_loss,
  399. log=log,
  400. progress=epoch_progress,
  401. stop_event=stop_event,
  402. pause_event=pause_event,
  403. )
  404. nhist[epoch, 0] = train_loss
  405. nhist[epoch, 1] = val_loss
  406. nhist[epoch, 2] = train_acc
  407. nhist[epoch, 3] = val_acc
  408. log.info(
  409. f"Epoch [{epoch + 1}/{num_epochs}], "
  410. f"Train Loss: {train_loss:.4f}, Val Loss: {val_loss:.4f}, "
  411. f"Train Acc: {train_acc:.4f}, Val Acc: {val_acc:.4f}"
  412. )
  413. if num_epochs > 4:
  414. if (epoch + 1) % (num_epochs // 4) == 0:
  415. model_save_path = (
  416. output_path / "intermediate_models" / f"model_epoch_{epoch + 1}.pt"
  417. )
  418. torch.save(model.state_dict(), model_save_path)
  419. log.info(f"Model saved at epoch {epoch + 1}")
  420. epoch_progress.update(total=num_epochs, advance=1)
  421. history = pd.DataFrame(
  422. data=nhist.astype(np.float32),
  423. columns=["train_loss", "val_loss", "train_acc", "val_acc"],
  424. index=np.arange(1, num_epochs + 1),
  425. )
  426. return model, history