training.py 16 KB

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