training.py 17 KB

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