Coverage for /opt/hostedtoolcache/Python/3.12.15/x64/lib/python3.12/site-packages/equine/equine_gp.py: 96%

280 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-15 00:14 +0000

1# Copyright 2024, MASSACHUSETTS INSTITUTE OF TECHNOLOGY 

2# Subject to FAR 52.227-11 – Patent Rights – Ownership by the Contractor (May 2014). 

3# SPDX-License-Identifier: MIT 

4 

5import io 

6import math 

7from collections import OrderedDict 

8from collections.abc import Callable, Iterable 

9from datetime import datetime 

10from typing import Any, Optional, Union 

11 

12import icontract 

13import torch 

14from beartype import beartype 

15from torch.utils.data import DataLoader, Dataset 

16from torchmetrics.metric import Metric 

17from tqdm import tqdm 

18 

19from .equine import Equine, EquineOutput 

20from .utils import ( 

21 EQUINE_FORMAT_VERSION, 

22 generate_support, 

23 generate_train_summary, 

24 jit_archive_to_tensor, 

25 load_checkpoint, 

26 load_jit_archive, 

27 prepare_jit_module, 

28) 

29 

30BatchType = tuple[torch.Tensor, ...] 

31# ------------------------------------------------------------------------------- 

32# Note that the below code for 

33# * `_random_ortho`, 

34# * `_RandomFourierFeatures``, and 

35# * `_Laplace` 

36# is copied and modified from https://github.com/y0ast/DUE/blob/main/due/sngp.py 

37# under its original MIT license, redisplayed here: 

38# ------------------------------------------------------------------------------- 

39# MIT License 

40# 

41# Copyright (c) 2021 Joost van Amersfoort 

42# 

43# Permission is hereby granted, free of charge, to any person obtaining a copy 

44# of this software and associated documentation files (the "Software"), to deal 

45# in the Software without restriction, including without limitation the rights 

46# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 

47# copies of the Software, and to permit persons to whom the Software is 

48# furnished to do so, subject to the following conditions: 

49# 

50# The above copyright notice and this permission notice shall be included in all 

51# copies or substantial portions of the Software. 

52# 

53# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 

54# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 

55# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 

56# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 

57# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 

58# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 

59# SOFTWARE. 

60# ------------------------------------------------------------------------------ 

61# Following the recommendation of their README at https://github.com/y0ast/DUE 

62# we encourage anyone using this code in their research to cite the following papers: 

63# 

64# @article{van2021on, 

65# title={On Feature Collapse and Deep Kernel Learning for Single Forward Pass Uncertainty}, 

66# author={van Amersfoort, Joost and Smith, Lewis and Jesson, Andrew and Key, Oscar and Gal, Yarin}, 

67# journal={arXiv preprint arXiv:2102.11409}, 

68# year={2021} 

69# } 

70# 

71# @article{liu2020simple, 

72# title={Simple and principled uncertainty estimation with deterministic deep learning via distance awareness}, 

73# author={Liu, Jeremiah and Lin, Zi and Padhy, Shreyas and Tran, Dustin and Bedrax Weiss, Tania and Lakshminarayanan, Balaji}, 

74# journal={Advances in Neural Information Processing Systems}, 

75# volume={33}, 

76# pages={7498--7512}, 

77# year={2020} 

78# } 

79 

80 

81@beartype 

82def _random_ortho(n: int, m: int) -> torch.Tensor: 

83 """ 

84 Generate a random orthonormal matrix. 

85 

86 Parameters 

87 ---------- 

88 n : int 

89 The number of rows. 

90 m : int 

91 The number of columns. 

92 

93 Returns 

94 ------- 

95 torch.Tensor 

96 The random orthonormal matrix. 

97 """ 

98 q, _ = torch.linalg.qr(torch.randn(n, m)) 

99 return q 

100 

101 

102@beartype 

103class _RandomFourierFeatures(torch.nn.Module): 

104 """ 

105 A private class to generate random Fourier features for the embedding model. 

106 """ 

107 

108 def __init__( 

109 self, in_dim: int, num_random_features: int, feature_scale: Optional[float] 

110 ) -> None: 

111 """ 

112 Initialize the _RandomFourierFeatures module, which generates random Fourier features 

113 for the embedding model. 

114 

115 Parameters 

116 ---------- 

117 in_dim : int 

118 The input dimensionality. 

119 num_random_features : int 

120 The number of random Fourier features to generate. 

121 feature_scale : Optional[float] 

122 The scaling factor for the random Fourier features. If None, defaults to sqrt(num_random_features / 2). 

123 """ 

124 super().__init__() 

125 if feature_scale is None: 125 ↛ 126line 125 didn't jump to line 126 because the condition on line 125 was never true

126 feature_scale = math.sqrt(num_random_features / 2) 

127 

128 self.register_buffer("feature_scale", torch.tensor(feature_scale)) 

129 

130 if num_random_features <= in_dim: 130 ↛ 131line 130 didn't jump to line 131 because the condition on line 130 was never true

131 W: torch.Tensor = _random_ortho(in_dim, num_random_features) 

132 else: 

133 # generate blocks of orthonormal rows which are not necessarily orthonormal 

134 # to each other. 

135 dim_left = num_random_features 

136 ws = [] 

137 while dim_left > in_dim: 

138 ws.append(_random_ortho(in_dim, in_dim)) 

139 dim_left -= in_dim 

140 ws.append(_random_ortho(in_dim, dim_left)) 

141 W: torch.Tensor = torch.cat(ws, 1) 

142 

143 feature_norm = torch.randn(W.shape) ** 2 

144 

145 W = W * feature_norm.sum(0).sqrt() 

146 self.register_buffer("W", W) 

147 

148 b: torch.Tensor = torch.empty(num_random_features).uniform_(0, 2 * math.pi) 

149 self.register_buffer("b", b) 

150 

151 def forward(self, x: torch.Tensor) -> torch.Tensor: 

152 """ 

153 Compute the forward pass of the _RandomFourierFeatures module. 

154 

155 Parameters 

156 ---------- 

157 x : torch.Tensor 

158 The input tensor of shape (batch_size, in_dim). 

159 

160 Returns 

161 ------- 

162 torch.Tensor 

163 The output tensor of shape (batch_size, num_random_features). 

164 """ 

165 k = torch.cos(x @ self.W + self.b) 

166 k = k / self.feature_scale 

167 

168 return k 

169 

170 

171class _Laplace(torch.nn.Module): 

172 """ 

173 A private class to compute a Laplace approximation to a Gaussian Process (GP) 

174 """ 

175 

176 def __init__( 

177 self, 

178 feature_extractor: torch.nn.Module, 

179 num_deep_features: int, 

180 num_gp_features: int, 

181 normalize_gp_features: bool, 

182 num_random_features: int, 

183 num_outputs: int, 

184 feature_scale: Optional[float], 

185 mean_field_factor: Optional[float], # required for classification problems 

186 ridge_penalty: float = 1.0, 

187 ) -> None: 

188 """ 

189 Initialize the _Laplace module. 

190 

191 Parameters 

192 ---------- 

193 feature_extractor : torch.nn.Module 

194 The feature extractor module. 

195 num_deep_features : int 

196 The number of features output by the feature extractor. 

197 num_gp_features : int 

198 The number of features to use in the Gaussian process. 

199 normalize_gp_features : bool 

200 Whether to normalize the GP features. 

201 num_random_features : int 

202 The number of random Fourier features to use. 

203 num_outputs : int 

204 The number of outputs of the model. 

205 feature_scale : Optional[float] 

206 The scaling factor for the random Fourier features. 

207 mean_field_factor : Optional[float] 

208 The mean-field factor for the Gaussian-Softmax approximation. 

209 Required for classification problems. 

210 ridge_penalty : float, optional 

211 The ridge penalty for the Laplace approximation. 

212 """ 

213 super().__init__() 

214 self.feature_extractor = feature_extractor 

215 self.mean_field_factor = mean_field_factor 

216 self.ridge_penalty = ridge_penalty 

217 self.train_batch_size = 0 # to be set later 

218 

219 if num_gp_features > 0: 219 ↛ 229line 219 didn't jump to line 229 because the condition on line 219 was always true

220 self.num_gp_features = num_gp_features 

221 random_matrix: torch.Tensor = torch.normal( 

222 0, 0.05, (num_gp_features, num_deep_features) 

223 ) 

224 self.register_buffer("random_matrix", random_matrix) 

225 self.jl: Callable = lambda x: torch.nn.functional.linear( 

226 x, self.random_matrix 

227 ) 

228 else: 

229 self.num_gp_features: int = num_deep_features 

230 self.jl: Callable = lambda x: x # Identity 

231 

232 self.normalize_gp_features = normalize_gp_features 

233 if normalize_gp_features: 233 ↛ 236line 233 didn't jump to line 236 because the condition on line 233 was always true

234 self.normalize: torch.nn.LayerNorm = torch.nn.LayerNorm(num_gp_features) 

235 

236 self.rff: _RandomFourierFeatures = _RandomFourierFeatures( 

237 num_gp_features, num_random_features, feature_scale 

238 ) 

239 self.beta: torch.nn.Linear = torch.nn.Linear(num_random_features, num_outputs) 

240 

241 self.num_data = 0 # to be set later 

242 self.register_buffer("seen_data", torch.tensor(0)) 

243 

244 precision = torch.eye(num_random_features) * self.ridge_penalty 

245 self.register_buffer("precision", precision) 

246 

247 self.recompute_covariance = True 

248 self.register_buffer("covariance", torch.eye(num_random_features)) 

249 self.training_parameters_set = False 

250 

251 def reset_precision_matrix(self) -> None: 

252 """ 

253 Reset the precision matrix to the identity matrix times the ridge penalty. 

254 """ 

255 identity = torch.eye(self.precision.shape[0], device=self.precision.device) 

256 self.precision: torch.Tensor = identity * self.ridge_penalty 

257 self.seen_data: torch.Tensor = torch.tensor(0) 

258 self.recompute_covariance = True 

259 

260 @icontract.require(lambda num_data: num_data > 0) 

261 @icontract.require( 

262 lambda num_data, batch_size: (0 < batch_size) & (batch_size <= num_data) 

263 ) 

264 def set_training_params(self, num_data: int, batch_size: int) -> None: 

265 """ 

266 Set the training parameters for the Laplace approximation. 

267 

268 Parameters 

269 ---------- 

270 num_data : int 

271 The total number of data points. 

272 batch_size : int 

273 The batch size to use during training. 

274 """ 

275 self.num_data: int = num_data 

276 self.train_batch_size: int = batch_size 

277 self.training_parameters_set: bool = True 

278 

279 @icontract.require(lambda mean_field_factor: mean_field_factor is not None) 

280 def mean_field_logits( 

281 self, logits: torch.Tensor, pred_cov: torch.Tensor, mean_field_factor: float 

282 ) -> torch.Tensor: 

283 """ 

284 Compute the mean-field logits for the Gaussian-Softmax approximation. 

285 

286 Parameters 

287 ---------- 

288 logits : torch.Tensor 

289 The logits tensor of shape (batch_size, num_outputs). 

290 pred_cov : torch.Tensor 

291 The predicted covariance matrix of shape (batch_size, batch_size). 

292 mean_field_factor : float 

293 Diagonal scaling factor 

294 

295 Returns 

296 ------- 

297 torch.Tensor 

298 The mean-field logits tensor of shape (batch_size, num_outputs). 

299 """ 

300 # Mean-Field approximation as alternative to MC integration of Gaussian-Softmax 

301 # Based on: https://arxiv.org/abs/2006.07584 

302 

303 logits_scale = torch.sqrt(1.0 + torch.diag(pred_cov) * mean_field_factor) 

304 if mean_field_factor > 0: 304 ↛ 307line 304 didn't jump to line 307 because the condition on line 304 was always true

305 logits = logits / logits_scale.unsqueeze(-1) 

306 

307 return logits 

308 

309 @icontract.require(lambda self: self.training_parameters_set) 

310 def forward( 

311 self, x: torch.Tensor 

312 ) -> Union[torch.Tensor, tuple[torch.Tensor, torch.Tensor]]: 

313 """ 

314 Compute the forward pass of the Laplace approximation to the Gaussian Process. 

315 

316 Parameters 

317 ---------- 

318 x : torch.Tensor 

319 The input tensor of shape (batch_size, num_features). 

320 

321 Returns 

322 ------- 

323 Union[torch.Tensor, tuple[torch.Tensor, torch.Tensor]] 

324 If the model is in training mode, returns the predicted mean of shape (batch_size, 1). 

325 If the model is in evaluation mode, returns a tuple containing the predicted mean of shape (batch_size, 1) 

326 and the predicted covariance matrix of shape (batch_size, batch_size). 

327 """ 

328 f = self.feature_extractor(x) 

329 f_reduc = self.jl(f) 

330 if self.normalize_gp_features: 330 ↛ 333line 330 didn't jump to line 333 because the condition on line 330 was always true

331 f_reduc = self.normalize(f_reduc) 

332 

333 k = self.rff(f_reduc) 

334 

335 pred = self.beta(k) 

336 

337 if self.training: 

338 precision_minibatch = k.t() @ k 

339 self.precision += precision_minibatch 

340 self.seen_data += x.shape[0] 

341 

342 assert self.seen_data <= self.num_data, ( 

343 "Did not reset precision matrix at start of epoch" 

344 ) 

345 else: 

346 assert self.seen_data > (self.num_data - self.train_batch_size), ( 

347 "Not seen sufficient data for precision matrix" 

348 ) 

349 

350 if self.recompute_covariance: 

351 with torch.no_grad(): 

352 eps = 1e-7 

353 jitter = eps * torch.eye( 

354 self.precision.shape[1], 

355 device=self.precision.device, 

356 ) 

357 u, info = torch.linalg.cholesky_ex(self.precision + jitter) 

358 assert (info == 0).all(), "Precision matrix inversion failed!" 

359 torch.cholesky_inverse(u, out=self.covariance) 

360 

361 self.recompute_covariance: bool = False 

362 

363 with torch.no_grad(): 

364 pred_cov = k @ ((self.covariance @ k.t()) * self.ridge_penalty) 

365 

366 if self.mean_field_factor is None: 366 ↛ 367line 366 didn't jump to line 367 because the condition on line 366 was never true

367 return pred, pred_cov 

368 else: 

369 pred = self.mean_field_logits(pred, pred_cov, self.mean_field_factor) 

370 

371 return pred 

372 

373 

374# ------------------------------------------------------------------------------- 

375# EquineGP, below, demonstrates how to adapt that approach in EQUINE 

376@beartype 

377class EquineGP(Equine): 

378 """ 

379 An example of an EQUINE model that builds upon the approach in "Spectral Norm 

380 Gaussian Processes" (SNGP). This wraps any pytorch embedding neural network and provides 

381 the `forward`, `predict`, `save`, and `load` methods required by Equine. 

382 

383 Notes 

384 ----- 

385 Although this model build upon the approach in SNGP, it does not enforce the spectral normalization 

386 and ResNet architecture required for SNGP. Instead, it is a simple wrapper around 

387 any pytorch embedding neural network. Your mileage may vary. 

388 """ 

389 

390 def __init__( 

391 self, 

392 embedding_model: torch.nn.Module, 

393 emb_out_dim: int, 

394 num_classes: int, 

395 num_random_features: int = 1024, 

396 init_temperature: float = 1.0, 

397 device: str = "cpu", 

398 feature_names: Optional[list[str]] = None, 

399 label_names: Optional[list[str]] = None, 

400 ) -> None: 

401 """ 

402 Initialize the EquineGP model. 

403 

404 Parameters 

405 ---------- 

406 embedding_model : torch.nn.Module 

407 Neural Network feature embedding. 

408 emb_out_dim : int 

409 The number of deep features from the feature embedding. 

410 num_classes : int 

411 The number of output classes this model predicts. 

412 num_random_features : int 

413 The dimension of the output of the RandomFourierFeatures operation 

414 init_temperature : float, optional 

415 What to use as the initial temperature (1.0 has no effect). 

416 device : str, optional 

417 Either 'cuda' or 'cpu'. 

418 feature_names : list[str], optional 

419 List of strings of the names of the tabular features (ex ["duration", "fiat_mean", ...]) 

420 label_names : list[str], optional 

421 List of strings of the names of the labels (ex ["streaming", "voip", ...]) 

422 """ 

423 super().__init__( 

424 embedding_model, feature_names=feature_names, label_names=label_names 

425 ) 

426 self.num_deep_features = emb_out_dim 

427 self.num_gp_features = emb_out_dim 

428 self.normalize_gp_features = True 

429 self.num_random_features = num_random_features 

430 self.num_outputs = num_classes 

431 self.mean_field_factor = 25 

432 self.ridge_penalty = 1 

433 self.feature_scale: float = 2.0 

434 self.init_temperature = init_temperature 

435 self.register_buffer( 

436 "temperature", torch.Tensor(self.init_temperature * torch.ones(1)) 

437 ) 

438 self.model: _Laplace = _Laplace( 

439 self.embedding_model, 

440 self.num_deep_features, 

441 self.num_gp_features, 

442 self.normalize_gp_features, 

443 self.num_random_features, 

444 self.num_outputs, 

445 self.feature_scale, 

446 self.mean_field_factor, 

447 self.ridge_penalty, 

448 ) 

449 self.device_type = device 

450 self.device: torch.device = torch.device(self.device_type) 

451 self.model.to(self.device) 

452 

453 def train_model( 

454 self, 

455 dataset: Dataset, 

456 loss_fn: Callable, 

457 opt: torch.optim.Optimizer, 

458 num_epochs: int, 

459 scheduler: Optional[torch.optim.lr_scheduler.LRScheduler] = None, 

460 batch_size: int = 64, 

461 validation_dataset: Optional[Dataset] = None, 

462 val_metrics: Optional[Iterable[Metric]] = None, 

463 vis_support: bool = False, 

464 support_size: int = 25, 

465 ) -> dict[str, Any]: 

466 """ 

467 Train or fine-tune an EquineGP model. 

468 

469 Parameters 

470 ---------- 

471 dataset : TensorDataset 

472 An iterable, pytorch TensorDataset. 

473 loss_fn : Callable 

474 A pytorch loss function, e.g., torch.nn.CrossEntropyLoss(). 

475 opt : torch.optim.Optimizer 

476 A pytorch optimizer, e.g., torch.optim.Adam(). 

477 num_epochs : int 

478 The desired number of epochs to use for training. 

479 scheduler : torch.optim.LRScheduler 

480 A pytorch scheduler, if one is desired 

481 validation_dataset: Dataset 

482 If provided, will compute validation metrics on this dataset after each epoch of training 

483 batch_size : int, optional 

484 The number of samples to use per batch. 

485 

486 Returns 

487 ------- 

488 dict[str, Any] 

489 A dict containing a dict of summary stats and a dataloader for the calibration data. 

490 

491 """ 

492 

493 self.validate_feature_label_names(dataset[0][0].shape[-1], self.num_outputs) 

494 

495 train_loader = DataLoader( 

496 dataset, batch_size=batch_size, shuffle=True, drop_last=False 

497 ) 

498 

499 val_loader: Optional[DataLoader] = None 

500 if validation_dataset is not None: 

501 val_loader = DataLoader( 

502 validation_dataset, 

503 batch_size=batch_size, 

504 shuffle=False, 

505 drop_last=False, 

506 ) 

507 

508 self.model.set_training_params(len(dataset), batch_size) 

509 val_metrics_outputs: Optional[list[list[float]]] = None 

510 

511 if validation_dataset is not None and val_metrics is not None: 

512 val_metrics_outputs = [[] for i in range(len(list(val_metrics)))] 

513 

514 for _ in tqdm(range(num_epochs)): 

515 self.model.train() 

516 self.model.reset_precision_matrix() 

517 epoch_loss = 0.0 

518 for i, (xs, labels) in enumerate(train_loader): 

519 opt.zero_grad() 

520 xs = xs.to(self.device) 

521 labels = labels.to(self.device) 

522 yhats = self.model(xs) 

523 loss = loss_fn(yhats, labels.to(torch.long)) 

524 loss.backward() 

525 opt.step() 

526 epoch_loss += loss.item() 

527 if scheduler is not None: 

528 scheduler.step() 

529 self.model.eval() 

530 # compute the validation metrics 

531 if ( 

532 validation_dataset is not None 

533 and val_loader is not None 

534 and val_metrics is not None 

535 and val_metrics_outputs is not None 

536 ): 

537 for _, (xs_val, labels_val) in enumerate(val_loader): 

538 xs_val = xs_val.to(self.device) 

539 labels_val = labels_val.to(self.device) 

540 yhats_val = self.model(xs_val) 

541 for metric in val_metrics: 

542 metric.update(yhats_val, labels_val) 

543 for i, metric in enumerate(val_metrics): 

544 val_metrics_outputs[i].append(metric.compute()) 

545 if vis_support: 

546 self.update_support(dataset.tensors[0], dataset.tensors[1], support_size) 

547 

548 _, train_y = dataset[:] 

549 date_trained = datetime.now().strftime("%m/%d/%Y, %H:%M:%S") 

550 self.train_summary: dict[str, Any] = generate_train_summary( 

551 self, train_y, date_trained 

552 ) 

553 

554 return_dict: dict[str, Any] = dict() 

555 return_dict["train_summary"] = self.train_summary 

556 if validation_dataset is not None: 

557 return_dict["val_metrics"] = val_metrics_outputs 

558 

559 return return_dict 

560 

561 def update_support( 

562 self, support_x: torch.Tensor, support_y: torch.Tensor, support_size: int 

563 ) -> None: 

564 """Function to update protonet support examples with given examples. 

565 

566 Parameters 

567 ---------- 

568 support_x : torch.Tensor 

569 Tensor containing support examples for protonet. 

570 support_y : torch.Tensor 

571 Tensor containing labels for given support examples. 

572 

573 Returns 

574 ------- 

575 None 

576 """ 

577 

578 labels, counts = torch.unique(support_y, return_counts=True) 

579 support = OrderedDict() 

580 for label, count in list(zip(labels.tolist(), counts.tolist())): 

581 class_support = generate_support( 

582 support_x, 

583 support_y, 

584 support_size=min(count, support_size), 

585 selected_labels=[label], 

586 ) 

587 support.update(class_support) 

588 

589 self.support = support 

590 

591 support_embeddings = OrderedDict().fromkeys(self.support.keys(), torch.Tensor()) 

592 for label in support: 

593 support_embeddings[label] = self.compute_embeddings(support[label]) 

594 

595 self.support_embeddings = support_embeddings 

596 self.prototypes: torch.Tensor = self.compute_prototypes() 

597 

598 def compute_embeddings(self, x: torch.Tensor) -> torch.Tensor: 

599 """ 

600 Method for computing deep embeddings for given input tensor. 

601 

602 Parameters 

603 ---------- 

604 x : torch.Tensor 

605 Input tensor for generating embeddings. 

606 

607 Returns 

608 ------- 

609 torch.Tensor 

610 Output embeddings . 

611 """ 

612 f = self.model.feature_extractor(x) 

613 f_reduc = self.model.jl(f) 

614 if self.model.normalize_gp_features: 614 ↛ 617line 614 didn't jump to line 617 because the condition on line 614 was always true

615 f_reduc = self.model.normalize(f_reduc) 

616 

617 return self.model.rff(f_reduc) 

618 

619 @icontract.require(lambda self: len(self.support) > 0) 

620 def compute_prototypes(self) -> torch.Tensor: 

621 """ 

622 Method for computing class prototypes based on given support examples. 

623 ``Prototypes'' in this context are the means of the support embeddings for each class. 

624 

625 Returns 

626 ------- 

627 torch.Tensor 

628 Tensors of prototypes for each of the given classes in the support. 

629 """ 

630 # Compute support embeddings 

631 support_embeddings = OrderedDict().fromkeys(self.support.keys()) 

632 for label in self.support: 

633 support_embeddings[label] = self.compute_embeddings(self.support[label]) 

634 

635 # Compute prototype for each class 

636 proto_list = [] 

637 for label in self.support: # look at doing functorch 

638 class_prototype = torch.mean(support_embeddings[label], dim=0) # type: ignore 

639 proto_list.append(class_prototype) 

640 

641 prototypes = torch.stack(proto_list) 

642 

643 return prototypes 

644 

645 @icontract.require(lambda self: len(self.support) > 0) 

646 def get_support(self) -> OrderedDict[int, torch.Tensor]: 

647 """ 

648 Method for returning support examples used in training. 

649 

650 Returns 

651 ------- 

652 OrderedDict[int, torch.Tensor] 

653 Dictionary containing support examples for each class. 

654 """ 

655 return self.support 

656 

657 @icontract.require(lambda self: len(self.prototypes) > 0) 

658 def get_prototypes(self) -> torch.Tensor: 

659 """ 

660 Method for returning class prototypes. 

661 

662 Returns 

663 ------- 

664 torch.Tensor 

665 Tensors of prototypes for each of the given classes in the support. 

666 """ 

667 return self.prototypes 

668 

669 @icontract.require(lambda num_calibration_epochs: 0 < num_calibration_epochs) 

670 @icontract.require(lambda calibration_lr: calibration_lr > 0.0) 

671 def calibrate_model( 

672 self, 

673 dataset: torch.utils.data.Dataset, 

674 num_calibration_epochs: int = 1, 

675 calibration_lr: float = 0.01, 

676 calibration_batch_size: int = 256, 

677 ) -> None: 

678 """ 

679 Fine-tune the temperature after training. Note this function is also run at the conclusion of train_model. 

680 

681 Parameters 

682 ---------- 

683 dataset : TensorDataset 

684 An iterable, pytorch TensorDataset. 

685 num_calibration_epochs : int, optional 

686 Number of epochs to tune temperature. 

687 calibration_lr : float, optional 

688 Learning rate for temperature optimization. 

689 """ 

690 

691 calibration_loader = DataLoader( 

692 dataset, 

693 batch_size=calibration_batch_size, 

694 shuffle=True, 

695 drop_last=False, 

696 ) 

697 

698 self.temperature.requires_grad = True 

699 loss_fn = torch.nn.functional.cross_entropy 

700 optimizer = torch.optim.Adam([self.temperature], lr=calibration_lr) 

701 for _ in range(num_calibration_epochs): 

702 for xs, labels in calibration_loader: 

703 optimizer.zero_grad() 

704 xs = xs.to(self.device) 

705 labels = labels.to(self.device) 

706 with torch.no_grad(): 

707 logits = self.model(xs) 

708 logits = logits / self.temperature 

709 loss = loss_fn(logits, labels.to(torch.long)) 

710 loss.backward() 

711 optimizer.step() 

712 self.temperature.requires_grad = False 

713 

714 def forward(self, X: torch.Tensor) -> torch.Tensor: 

715 """ 

716 EquineGP forward function, generates logits for classification. 

717 

718 Parameters 

719 ---------- 

720 X : torch.Tensor 

721 Input tensor for generating predictions. 

722 

723 Returns 

724 ------- 

725 torch.Tensor 

726 Output probabilities computed. 

727 """ 

728 X = X.to(self.device) 

729 preds = self.model(X) 

730 return preds / self.temperature.to(self.device) 

731 

732 @icontract.ensure( 

733 lambda result: all((0 <= result.ood_scores) & (result.ood_scores <= 1.0)) 

734 ) 

735 def predict(self, X: torch.Tensor) -> EquineOutput: 

736 """ 

737 Predict function for EquineGP, inherited and implemented from Equine. 

738 

739 Parameters 

740 ---------- 

741 X : torch.Tensor 

742 Input tensor. 

743 

744 Returns 

745 ------- 

746 EquineOutput 

747 Output object containing prediction probabilities and OOD scores. 

748 """ 

749 logits = self(X) 

750 preds = torch.softmax(logits, dim=1) 

751 equiprobable = torch.ones(self.num_outputs) / self.num_outputs 

752 max_entropy = torch.sum(torch.special.entr(equiprobable)) 

753 ood_score = torch.sum(torch.special.entr(preds), dim=1) / max_entropy 

754 embeddings = self.compute_embeddings(X) 

755 eq_out = EquineOutput( 

756 classes=preds, ood_scores=ood_score, embeddings=embeddings 

757 ) # TODO return embeddings 

758 

759 self.validate_feature_label_names(X.shape[-1], self.num_outputs) 

760 

761 return eq_out 

762 

763 def save(self, path: str) -> None: 

764 """ 

765 Function to save all model parameters to a file. 

766 

767 Parameters 

768 ---------- 

769 path : str 

770 Filename to write the model. 

771 """ 

772 model_settings = { 

773 "emb_out_dim": self.num_deep_features, 

774 "num_classes": self.num_outputs, 

775 "num_random_features": self.num_random_features, 

776 "init_temperature": self.temperature.item(), 

777 "device": self.device_type, 

778 } 

779 

780 jit_model = torch.jit.script(prepare_jit_module(self.model.feature_extractor)) 

781 buffer = io.BytesIO() 

782 torch.jit.save(jit_model, buffer) 

783 

784 laplace_sd = self.model.state_dict() 

785 keys_to_delete = [] 

786 for key in laplace_sd: 

787 if "feature_extractor" in key: 

788 keys_to_delete.append(key) 

789 for key in keys_to_delete: 

790 del laplace_sd[key] 

791 

792 # Plain values only, so load_checkpoint can read it with weights_only=True. 

793 save_data = { 

794 "equine_format_version": EQUINE_FORMAT_VERSION, 

795 "embed_jit_save": jit_archive_to_tensor(buffer), 

796 "feature_names": self.feature_names, 

797 "label_names": self.label_names, 

798 "laplace_model_save": laplace_sd, 

799 "num_data": self.model.num_data, 

800 "settings": model_settings, 

801 "support": {int(label): x for label, x in self.support.items()}, 

802 "train_batch_size": self.model.train_batch_size, 

803 "train_summary": self.train_summary, 

804 } 

805 

806 torch.save(save_data, path) # TODO allow model checkpointing 

807 

808 @classmethod 

809 def load(cls, path: str) -> Equine: 

810 """ 

811 Load a previously saved EquineGP model. 

812 

813 Files from earlier EQUINE releases must first be rewritten with 

814 ``python -m equine.upgrade``. 

815 

816 Parameters 

817 ---------- 

818 path : str 

819 Input filename. 

820 

821 Returns 

822 ------- 

823 EquineGP 

824 The reconstituted EquineGP object. 

825 

826 Raises 

827 ------ 

828 ValueError 

829 If the file is not in the current EQUINE format. 

830 """ 

831 return cls._from_checkpoint(load_checkpoint(path)) 

832 

833 @classmethod 

834 def _from_checkpoint(cls, model_save: dict[str, Any]) -> Equine: 

835 """ 

836 Rebuild an EquineGP from an already-loaded checkpoint dictionary. 

837 

838 Parameters 

839 ---------- 

840 model_save : dict[str, Any] 

841 The dictionary returned by ``utils.load_checkpoint``. 

842 

843 Returns 

844 ------- 

845 EquineGP 

846 The reconstituted EquineGP object. 

847 """ 

848 jit_model = load_jit_archive(model_save.get("embed_jit_save")) 

849 eq_model = cls(jit_model, **model_save.get("settings")) 

850 

851 eq_model.feature_names = model_save.get("feature_names") 

852 eq_model.label_names = model_save.get("label_names") 

853 eq_model.train_summary = model_save.get("train_summary") 

854 

855 eq_model.model.load_state_dict( 

856 model_save.get("laplace_model_save"), strict=False 

857 ) 

858 eq_model.model.seen_data = model_save.get("laplace_model_save").get("seen_data") 

859 

860 eq_model.model.set_training_params( 

861 model_save.get("num_data"), model_save.get("train_batch_size") 

862 ) 

863 eq_model.eval() 

864 

865 support = OrderedDict( 

866 (int(label), x) for label, x in model_save.get("support").items() 

867 ) 

868 if len(support) > 0: 

869 eq_model.support = support 

870 eq_model.prototypes = eq_model.compute_prototypes() 

871 

872 return eq_model