| import torch |
| import logging |
| import numpy as np |
| import torch.nn as nn |
| from typing import Callable, List |
| from accelerate import Accelerator |
| from sklearn.linear_model import LinearRegression |
|
|
|
|
| class eval_mode: |
| def __init__(self, *models, no_grad=False): |
| self.models = models |
| self.no_grad = no_grad |
| self.no_grad_context = torch.no_grad() |
|
|
| def __enter__(self): |
| self.prev_states = [] |
| for model in self.models: |
| self.prev_states.append(model.training) |
| model.train(False) |
| if self.no_grad: |
| self.no_grad_context.__enter__() |
|
|
| def __exit__(self, *args): |
| if self.no_grad: |
| self.no_grad_context.__exit__(*args) |
| for model, state in zip(self.models, self.prev_states): |
| model.train(state) |
| return False |
|
|
|
|
| def embed_trajectory_dataset( |
| model, |
| dataset, |
| obs_only=True, |
| device=None, |
| embed_goal=False, |
| ): |
| if type(model) is nn.parallel.DistributedDataParallel: |
| return embed_trajectory_dataset_ddp( |
| model, |
| dataset, |
| obs_only=obs_only, |
| device=device, |
| embed_goal=embed_goal, |
| ) |
| else: |
| result = [] |
| accelerator = Accelerator() |
| device = device or accelerator.device |
| with eval_mode(model, no_grad=True): |
| for i in range(len(dataset)): |
| obs, *rest = dataset[i] |
| obs = obs.to(accelerator.device) |
| obs_enc = model(obs).to(device) |
| if obs_only: |
| result.append(obs_enc) |
| else: |
| if embed_goal: |
| |
| goal = rest[-1] |
| rest = rest[:-1] |
| goal = goal.to(accelerator.device) |
| goal_enc = model(goal).to(device) |
| rest.append(goal_enc) |
| rest = [x.to(device) for x in rest] |
| result.append((obs_enc, *rest)) |
| return result |
|
|
|
|
| def embed_trajectory_dataset_ddp( |
| model: nn.Module, |
| dataset, |
| obs_only=True, |
| device=None, |
| embed_goal=False, |
| ): |
| assert type(model) is nn.parallel.DistributedDataParallel, "Model must be DDP" |
| embeddings = [] |
| accelerator = Accelerator() |
| dataloader = torch.utils.data.DataLoader( |
| dataset, |
| batch_size=1, |
| num_workers=1, |
| shuffle=False, |
| pin_memory=True, |
| ) |
| dataloader = accelerator.prepare(dataloader) |
| |
| max_T = max(dataset.get_seq_length(i) for i in range(len(dataset))) |
| with eval_mode(model, no_grad=True): |
| for obs, *rest in dataloader: |
| obs = obs.to(accelerator.device) |
| obs_enc = model(obs) |
| obs_enc = pad_to_length(obs_enc, max_T, dim=1) |
| obs_enc = accelerator.gather_for_metrics(obs_enc) |
| if obs_only: |
| embeddings.append(obs_enc) |
| else: |
| if embed_goal: |
| |
| goal = rest[-1] |
| rest = rest[:-1] |
| goal = goal.to(accelerator.device) |
| goal_enc = model(goal) |
| rest.append(goal_enc) |
| rest = [x.to(accelerator.device) for x in rest] |
| rest = [pad_to_length(x, max_T, dim=1) for x in rest] |
| rest = [accelerator.gather_for_metrics(x) for x in rest] |
| embeddings.append((obs_enc, *rest)) |
|
|
| device = device or accelerator.device |
| |
| result = [] |
| if obs_only: |
| embeddings = torch.cat(embeddings, dim=0) |
| assert len(embeddings) == len(dataset) |
| else: |
| embeddings = [torch.cat(x, dim=0) for x in zip(*embeddings)] |
| assert len(embeddings[0]) == len(dataset) |
| for i in range(len(dataset)): |
| T = dataset.get_seq_length(i) |
| if obs_only: |
| result.append(embeddings[i, :T].to(device)) |
| else: |
| result.append([x[i, :T].to(device) for x in embeddings]) |
| return result |
|
|
|
|
| def pad_to_length(x: torch.Tensor, length: int, dim: int = 0): |
| """ |
| Pad tensor x to length along dim, adding zeros at the end. |
| """ |
| pad_size = length - x.shape[dim] |
| if pad_size <= 0: |
| return x |
| pad = torch.zeros( |
| *x.shape[:dim], |
| pad_size, |
| *x.shape[dim + 1 :], |
| device=x.device, |
| dtype=x.dtype, |
| ) |
| return torch.cat([x, pad], dim=dim) |
|
|
|
|
| def repeat_start_to_length(x: torch.Tensor, length: int, dim: int = 0): |
| """ |
| Pad tensor x to length along dim, repeating the first value at the start. |
| """ |
| pad_size = length - x.shape[dim] |
| if pad_size <= 0: |
| return x |
| first_frame = x.index_select(dim, torch.tensor(0, device=x.device)) |
| repeat_shape = [1] * len(x.shape) |
| repeat_shape[dim] = pad_size |
| pad = first_frame.repeat(*repeat_shape) |
| return torch.cat([pad, x], dim=dim) |
|
|
|
|
| def nn_lookup( |
| query: torch.Tensor, |
| pool: torch.Tensor, |
| metric: Callable[[torch.Tensor, torch.Tensor], torch.Tensor], |
| ): |
| pairwise_query = query.repeat_interleave(len(pool), dim=0) |
| pairwise_pool = pool.repeat((len(query), 1)) |
| dist = metric(pairwise_query, pairwise_pool) |
| nn_dist, nn_idx = dist.view(len(query), len(pool)).sort(dim=1) |
| return nn_dist, nn_idx |
|
|
|
|
| def batch_knn( |
| query: torch.Tensor, |
| pool: torch.Tensor, |
| metric: Callable[[torch.Tensor, torch.Tensor], torch.Tensor], |
| k: int, |
| batch_size: int, |
| ): |
| """ |
| Return the k nearest neighbors of query in pool using metric. |
| Input: |
| query: Tensor[N, D] of query points |
| pool: Tensor[M, D] of pool points |
| metric: Callable[[Tensor[N, D], Tensor[M, D]], Tensor[N, M]] distance function |
| k: int number of neighbors to return |
| batch_size: int batch size for computation. Batched over query. |
| Output: (distances, indices) |
| distances: Tensor[N, k] of distances to the k nearest neighbors |
| indices: Tensor[N, k] of indices of the k nearest neighbors |
| """ |
| nn_dists = [] |
| nn_idxs = [] |
| for i in range(0, len(query), batch_size): |
| batch = query[i : i + batch_size].to(pool.device) |
| nn_dist, nn_idx = nn_lookup(batch, pool, metric) |
| nn_dists.append(nn_dist[:, :k]) |
| nn_idxs.append(nn_idx[:, :k]) |
| return torch.cat(nn_dists), torch.cat(nn_idxs) |
|
|
|
|
| def linear_probe_with_trajectory_split( |
| X: torch.Tensor, |
| y: torch.Tensor, |
| train_idx: List[int], |
| val_idx: List[int], |
| ): |
| X_train = torch.cat([X[i] for i in train_idx]).cpu().numpy() |
| y_train = torch.cat([y[i] for i in train_idx]).cpu().numpy() |
| X_val = torch.cat([X[i] for i in val_idx]).cpu().numpy() |
| y_val = torch.cat([y[i] for i in val_idx]).cpu().numpy() |
|
|
| X_all = torch.cat(X).cpu().numpy() |
| y_all = torch.cat(y).cpu().numpy() |
|
|
| m = LinearRegression() |
| |
| m.fit(X_all, y_all) |
| linear_probe_mse_train_all = np.mean((m.predict(X_train) - y_train) ** 2).item() |
| |
| linear_probe_mse_val_all = np.mean((m.predict(X_val) - y_val) ** 2).item() |
| return { |
| "linear_probe_mse_train_all": linear_probe_mse_train_all, |
| "linear_probe_mse_val_all": linear_probe_mse_val_all, |
| } |
|
|
|
|
| def mse(a: torch.Tensor, b: torch.Tensor): |
| return ((a - b) ** 2).mean(dim=1) |
|
|
|
|
| def mahalanobis(a, b, VI): |
| u = a - b |
| v = u @ VI |
| return (u * v).sum(dim=-1).sqrt() |
|
|
|
|
| class OLS: |
| """ |
| OLS in torch |
| NOTE: discrepancy with sklearn's LinearRegression when ill-conditioned; reverting to sklearn for now |
| """ |
|
|
| def __init__(self, bias=True, fallback_to_cpu=True): |
| self.bias = bias |
| self.w = None |
| self.fallback_to_cpu = fallback_to_cpu |
|
|
| def fit(self, X: torch.Tensor, y: torch.Tensor): |
| """ |
| Fit the model |
| """ |
| if self.bias: |
| X = torch.cat([X, torch.ones(X.shape[0], 1, device=X.device)], dim=1) |
| self.w = torch.linalg.lstsq(X, y).solution |
| if torch.isnan(self.w).any(): |
| cond = torch.linalg.cond(X) |
| rank = torch.linalg.matrix_rank(X) |
| msg = f"NaNs in OLS solution. Input shape: {X.shape}, cond: {cond}, rank: {rank}" |
| if not self.fallback_to_cpu: |
| raise ValueError(msg) |
| logging.warn(f"{msg}; Falling back to CPU with gelss driver.") |
| self.w = torch.linalg.lstsq(X.cpu(), y.cpu(), driver="gelss").solution |
| self.w = self.w.to(X.device) |
| return self |
|
|
| def predict(self, X: torch.Tensor): |
| """ |
| Predict the output |
| """ |
| if self.w is None: |
| raise ValueError("Model not fitted") |
| if self.bias: |
| X = torch.cat([X, torch.ones(X.shape[0], 1, device=X.device)], dim=1) |
| return X @ self.w |
|
|
|
|
| class SGDClassifier: |
| def __init__(self, lr=1e-4, max_iter=1000, tol=1e-3, batch_size=2048): |
| self.lr = lr |
| self.max_iter = max_iter |
| self.tol = tol |
| self.batch_size = batch_size |
|
|
| def fit(self, X: torch.Tensor, y: torch.Tensor): |
| n_samples, input_dim = X.shape |
| n_classes = y.max().item() + 1 |
| self.linear = nn.Linear(input_dim, n_classes).to(X.device) |
| optimizer = torch.optim.AdamW( |
| self.linear.parameters(), lr=self.lr, weight_decay=0.0 |
| ) |
| criterion = nn.CrossEntropyLoss() |
| for j in range(self.max_iter): |
| total_loss = 0 |
| n_batches = 0 |
| indices = torch.randperm(n_samples).to(X.device) |
| for i in range(0, n_samples, self.batch_size): |
| batch_indices = indices[i : i + self.batch_size] |
| batch_X, batch_y = X[batch_indices], y[batch_indices] |
| optimizer.zero_grad() |
| logits = self.linear(batch_X) |
| loss = criterion(logits, batch_y) |
| loss.backward() |
| optimizer.step() |
| total_loss += loss.item() |
| n_batches += 1 |
| avg_loss = total_loss / n_batches |
| if avg_loss < self.tol: |
| break |
| if j + 1 < self.max_iter: |
| logging.info(f"Converged at epoch {j+1}.") |
| else: |
| logging.info(f"Max iter reached. Final loss {avg_loss}") |
| return self |
|
|
| def predict(self, X: torch.Tensor): |
| with torch.no_grad(): |
| return torch.argmax(self.linear(X), dim=1) |
|
|
| def score(self, X: torch.Tensor, y: torch.Tensor): |
| return (self.predict(X) == y).float().mean().item() |
|
|