training.py 17 KB

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