Download src/experiments/deep.py from NeerajCodz/aiBatteryLifeCycle: direct link, hf CLI and curl.
- Browser
- Download file 16.2 kB
-
https://huggingface.co/spaces/NeerajCodz/aiBatteryLifeCycle/resolve/main/src/experiments/deep.py
- Command line
-
hf download hf://spaces/NeerajCodz/aiBatteryLifeCycle/src/experiments/deep.py
-
curl -L -o deep.py https://huggingface.co/spaces/NeerajCodz/aiBatteryLifeCycle/resolve/main/src/experiments/deep.py
16.2 kB
| """Battery-grouped sequence-model evaluation with train-only preprocessing.""" | |
| from __future__ import annotations | |
| import random | |
| from collections.abc import Callable | |
| import numpy as np | |
| import pandas as pd | |
| from sklearn.model_selection import GroupShuffleSplit | |
| from src.evaluation.metrics import regression_metrics | |
| from src.evaluation.protocol import grouped_train_val_test_folds | |
| TORCH_MODEL_IDS = ( | |
| "vanilla_lstm", "bidirectional_lstm", "gru", "attention_lstm", | |
| "battery_gpt", "temporal_fusion_transformer", "vae_lstm", | |
| ) | |
| TF_MODEL_IDS = ("itransformer", "physics_itransformer", "dynamic_graph_itransformer") | |
| class SequenceStandardizer: | |
| """Median-impute and standardize sequence channels using training rows only.""" | |
| def fit(self, X: np.ndarray) -> "SequenceStandardizer": | |
| flat = np.asarray(X, dtype=float).reshape(-1, X.shape[-1]) | |
| self.median_ = np.nanmedian(flat, axis=0) | |
| filled = np.where(np.isnan(flat), self.median_, flat) | |
| self.mean_ = filled.mean(axis=0) | |
| self.scale_ = filled.std(axis=0) | |
| self.scale_[self.scale_ == 0] = 1.0 | |
| return self | |
| def transform(self, X: np.ndarray) -> np.ndarray: | |
| values = np.asarray(X, dtype=float) | |
| filled = np.where(np.isnan(values), self.median_, values) | |
| return ((filled - self.mean_) / self.scale_).astype(np.float32) | |
| def _set_seed(seed: int) -> None: | |
| random.seed(seed) | |
| np.random.seed(seed) | |
| try: | |
| import torch | |
| torch.manual_seed(seed) | |
| if torch.cuda.is_available(): | |
| torch.cuda.manual_seed_all(seed) | |
| except ImportError: | |
| pass | |
| def _torch_factories(n_features: int) -> dict[str, Callable[[], object]]: | |
| from src.models.deep.lstm import AttentionLSTM, BidirectionalLSTM, GRUModel, VanillaLSTM | |
| from src.models.deep.transformer import BatteryGPT, TemporalFusionTransformer | |
| from src.models.deep.vae_lstm import VAE_LSTM | |
| return { | |
| "vanilla_lstm": lambda: VanillaLSTM(n_features, hidden_dim=64, n_layers=2), | |
| "bidirectional_lstm": lambda: BidirectionalLSTM(n_features, hidden_dim=64, n_layers=2), | |
| "gru": lambda: GRUModel(n_features, hidden_dim=64, n_layers=2), | |
| "attention_lstm": lambda: AttentionLSTM(n_features, hidden_dim=64, n_layers=2), | |
| "battery_gpt": lambda: BatteryGPT(n_features, d_model=64, n_heads=4, n_layers=2), | |
| "temporal_fusion_transformer": lambda: TemporalFusionTransformer( | |
| n_features, d_model=64, n_heads=4, n_layers=2 | |
| ), | |
| "vae_lstm": lambda: VAE_LSTM( | |
| n_features, seq_len=64, hidden_dim=64, latent_dim=16, n_layers=2 | |
| ), | |
| } | |
| def _train_torch_model( | |
| model_id: str, | |
| model: object, | |
| X_train: np.ndarray, | |
| y_train: np.ndarray, | |
| X_val: np.ndarray, | |
| y_val: np.ndarray, | |
| X_test: np.ndarray, | |
| *, | |
| max_epochs: int, | |
| patience: int, | |
| batch_size: int, | |
| device: str, | |
| ) -> tuple[np.ndarray, np.ndarray, int]: | |
| import torch | |
| from torch.utils.data import DataLoader, TensorDataset | |
| from src.models.deep.lstm import train_loop | |
| from src.models.deep.vae_lstm import train_vae | |
| y_mean = float(y_train.mean()) | |
| y_scale = float(y_train.std()) or 1.0 | |
| normalize = lambda values: ((values - y_mean) / y_scale).astype(np.float32) | |
| train_loader = DataLoader(TensorDataset( | |
| torch.from_numpy(X_train), torch.from_numpy(normalize(y_train)) | |
| ), batch_size=batch_size, shuffle=True) | |
| val_loader = DataLoader(TensorDataset( | |
| torch.from_numpy(X_val), torch.from_numpy(normalize(y_val)) | |
| ), batch_size=batch_size, shuffle=False) | |
| if model_id == "vae_lstm": | |
| history = train_vae( | |
| model, train_loader, val_loader, max_epochs=max_epochs, | |
| patience=patience, device=device, warmup_epochs=min(30, max_epochs // 3), | |
| ) | |
| else: | |
| history = train_loop( | |
| model, train_loader, val_loader, max_epochs=max_epochs, | |
| patience=patience, device=device, | |
| ) | |
| model.eval() | |
| def predict(values: np.ndarray) -> np.ndarray: | |
| outputs = [] | |
| loader = DataLoader(TensorDataset(torch.from_numpy(values)), batch_size=batch_size) | |
| with torch.no_grad(): | |
| for (xb,) in loader: | |
| raw = model(xb.to(device)) | |
| if isinstance(raw, dict): | |
| raw = raw["health_pred"] | |
| outputs.append(raw.detach().cpu().numpy().reshape(-1)) | |
| return np.concatenate(outputs) * y_scale + y_mean | |
| epochs = len(history.get("train_losses", [])) | |
| return predict(X_train), predict(X_test), epochs | |
| def _train_tensorflow_model( | |
| model_id: str, | |
| X_train: np.ndarray, | |
| y_train: np.ndarray, | |
| X_val: np.ndarray, | |
| y_val: np.ndarray, | |
| X_test: np.ndarray, | |
| *, | |
| max_epochs: int, | |
| patience: int, | |
| batch_size: int, | |
| ) -> tuple[np.ndarray, np.ndarray, int]: | |
| import tensorflow as tf | |
| from src.models.deep.itransformer import ( | |
| build_dynamic_graph_itransformer, | |
| build_itransformer, | |
| build_physics_itransformer, | |
| ) | |
| builders = { | |
| "itransformer": build_itransformer, | |
| "physics_itransformer": build_physics_itransformer, | |
| "dynamic_graph_itransformer": build_dynamic_graph_itransformer, | |
| } | |
| y_mean = float(y_train.mean()) | |
| y_scale = float(y_train.std()) or 1.0 | |
| train_target = ((y_train - y_mean) / y_scale).astype(np.float32) | |
| val_target = ((y_val - y_mean) / y_scale).astype(np.float32) | |
| model = builders[model_id](X_train.shape[1], X_train.shape[2], d_model=32, n_heads=4, n_blocks=2) | |
| if model_id == "physics_itransformer": | |
| model.compile( | |
| optimizer=tf.keras.optimizers.Adam(1e-3), | |
| loss={"soh_ml": "mae", "soh_phy": "mae"}, | |
| loss_weights={"soh_ml": 1.0, "soh_phy": model.physics_loss_weight}, | |
| ) | |
| fit_y = {"soh_ml": train_target, "soh_phy": train_target} | |
| val_y = {"soh_ml": val_target, "soh_phy": val_target} | |
| else: | |
| model.compile(optimizer=tf.keras.optimizers.Adam(1e-3), loss="mae") | |
| fit_y, val_y = train_target, val_target | |
| history = model.fit( | |
| X_train, fit_y, validation_data=(X_val, val_y), epochs=max_epochs, | |
| batch_size=batch_size, verbose=0, | |
| callbacks=[tf.keras.callbacks.EarlyStopping( | |
| monitor="val_loss", patience=patience, restore_best_weights=True | |
| )], | |
| ) | |
| def predict(values: np.ndarray) -> np.ndarray: | |
| raw = model.predict(values, batch_size=batch_size, verbose=0) | |
| if isinstance(raw, list): | |
| raw = raw[0] | |
| return np.asarray(raw).reshape(-1) * y_scale + y_mean | |
| result = (predict(X_train), predict(X_test), len(history.history["loss"])) | |
| tf.keras.backend.clear_session() | |
| return result | |
| def run_grouped_sequence_benchmark( | |
| X: np.ndarray, | |
| index: pd.DataFrame, | |
| *, | |
| dataset_name: str, | |
| n_splits: int = 5, | |
| seeds: tuple[int, ...] = (17, 42, 2026), | |
| max_epochs: int = 200, | |
| patience: int = 20, | |
| batch_size: int = 64, | |
| model_ids: tuple[str, ...] | None = None, | |
| ) -> tuple[pd.DataFrame, pd.DataFrame]: | |
| """Evaluate all ten sequence models without using test data for stopping.""" | |
| if len(index) != len(X): | |
| raise ValueError("Sequence tensor and index have different row counts") | |
| requested = set(model_ids or (TORCH_MODEL_IDS + TF_MODEL_IDS)) | |
| unknown = requested.difference(TORCH_MODEL_IDS + TF_MODEL_IDS) | |
| if unknown: | |
| raise KeyError(f"Unknown sequence model IDs: {sorted(unknown)}") | |
| y = index["SoH"].to_numpy(dtype=float) | |
| groups = index["battery_id"].astype(str).to_numpy() | |
| available_splits = min(n_splits, np.unique(groups).size) | |
| if available_splits < 2: | |
| raise ValueError("At least two batteries are required") | |
| metric_rows: list[dict[str, object]] = [] | |
| prediction_rows: list[dict[str, object]] = [] | |
| for seed in seeds: | |
| _set_seed(seed) | |
| folds = grouped_train_val_test_folds( | |
| groups, n_splits=available_splits, validation_fraction=0.2, | |
| random_state=seed, | |
| ) | |
| for fold, (train_idx, val_idx, test_idx) in enumerate(folds, start=1): | |
| scaler = SequenceStandardizer().fit(X[train_idx]) | |
| X_train, X_val, X_test = ( | |
| scaler.transform(X[subset]) for subset in (train_idx, val_idx, test_idx) | |
| ) | |
| device = "cuda" if __import__("torch").cuda.is_available() else "cpu" | |
| for model_id, factory in _torch_factories(X.shape[-1]).items(): | |
| if model_id not in requested: | |
| continue | |
| print( | |
| f"[{dataset_name}] seed={seed} fold={fold}/{available_splits} " | |
| f"model={model_id} start", | |
| flush=True, | |
| ) | |
| _set_seed(seed) | |
| train_pred, test_pred, epochs = _train_torch_model( | |
| model_id, factory(), X_train, y[train_idx], X_val, y[val_idx], X_test, | |
| max_epochs=max_epochs, patience=patience, batch_size=batch_size, | |
| device=device, | |
| ) | |
| _append_results( | |
| metric_rows, prediction_rows, dataset_name, seed, fold, model_id, | |
| train_idx, test_idx, groups, index, y, train_pred, test_pred, epochs, | |
| ) | |
| print( | |
| f"[{dataset_name}] seed={seed} fold={fold}/{available_splits} " | |
| f"model={model_id} done epochs={epochs} mae={metric_rows[-1]['mae']:.4f}", | |
| flush=True, | |
| ) | |
| for model_id in TF_MODEL_IDS: | |
| if model_id not in requested: | |
| continue | |
| print( | |
| f"[{dataset_name}] seed={seed} fold={fold}/{available_splits} " | |
| f"model={model_id} start", | |
| flush=True, | |
| ) | |
| _set_seed(seed) | |
| train_pred, test_pred, epochs = _train_tensorflow_model( | |
| model_id, X_train, y[train_idx], X_val, y[val_idx], X_test, | |
| max_epochs=max_epochs, patience=patience, batch_size=batch_size, | |
| ) | |
| _append_results( | |
| metric_rows, prediction_rows, dataset_name, seed, fold, model_id, | |
| train_idx, test_idx, groups, index, y, train_pred, test_pred, epochs, | |
| ) | |
| print( | |
| f"[{dataset_name}] seed={seed} fold={fold}/{available_splits} " | |
| f"model={model_id} done epochs={epochs} mae={metric_rows[-1]['mae']:.4f}", | |
| flush=True, | |
| ) | |
| return pd.DataFrame(metric_rows), pd.DataFrame(prediction_rows) | |
| def _append_results( | |
| metric_rows: list[dict[str, object]], | |
| prediction_rows: list[dict[str, object]], | |
| dataset_name: str, | |
| seed: int, | |
| fold: int, | |
| model_id: str, | |
| train_idx: np.ndarray, | |
| test_idx: np.ndarray, | |
| groups: np.ndarray, | |
| index: pd.DataFrame, | |
| y: np.ndarray, | |
| train_pred: np.ndarray, | |
| test_pred: np.ndarray, | |
| epochs: int, | |
| ) -> None: | |
| metrics = regression_metrics(y[test_idx], test_pred) | |
| train_metrics = regression_metrics(y[train_idx], train_pred) | |
| metric_rows.append({ | |
| "dataset": dataset_name, "seed": seed, "fold": fold, "model": model_id, | |
| **metrics, "train_mae": train_metrics["mae"], | |
| "generalization_gap_mae": metrics["mae"] - train_metrics["mae"], | |
| "epochs": epochs, | |
| }) | |
| for local, row_idx in enumerate(test_idx): | |
| prediction_rows.append({ | |
| "dataset": dataset_name, "seed": seed, "fold": fold, "model": model_id, | |
| "row_index": int(row_idx), "battery_id": groups[row_idx], | |
| "cycle_number": int(index.iloc[row_idx]["cycle_number"]), | |
| "y_true": y[row_idx], "y_pred": float(test_pred[local]), | |
| "residual": float(y[row_idx] - test_pred[local]), | |
| }) | |
| def run_zero_shot_sequence( | |
| source_X: np.ndarray, | |
| source_index: pd.DataFrame, | |
| targets: dict[str, tuple[np.ndarray, pd.DataFrame]], | |
| *, | |
| source_name: str = "NASA", | |
| seeds: tuple[int, ...] = (17, 42, 2026), | |
| max_epochs: int = 200, | |
| patience: int = 20, | |
| batch_size: int = 64, | |
| model_ids: tuple[str, ...] | None = None, | |
| ) -> tuple[pd.DataFrame, pd.DataFrame]: | |
| """Train each sequence model on NASA train/validation batteries once per seed. | |
| All target-domain labels remain untouched until scoring, and the same frozen | |
| source model is used for every supplied external target. | |
| """ | |
| y_source = source_index["SoH"].to_numpy(dtype=float) | |
| source_groups = source_index["battery_id"].astype(str).to_numpy() | |
| lengths = {name: len(values[0]) for name, values in targets.items()} | |
| target_concat = np.concatenate([values[0] for values in targets.values()], axis=0) | |
| metric_rows: list[dict[str, object]] = [] | |
| prediction_rows: list[dict[str, object]] = [] | |
| requested = set(model_ids or (TORCH_MODEL_IDS + TF_MODEL_IDS)) | |
| unknown = requested.difference(TORCH_MODEL_IDS + TF_MODEL_IDS) | |
| if unknown: | |
| raise KeyError(f"Unknown sequence model IDs: {sorted(unknown)}") | |
| for seed in seeds: | |
| splitter = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=seed) | |
| train_idx, val_idx = next(splitter.split(source_X, y_source, source_groups)) | |
| scaler = SequenceStandardizer().fit(source_X[train_idx]) | |
| X_train = scaler.transform(source_X[train_idx]) | |
| X_val = scaler.transform(source_X[val_idx]) | |
| X_target = scaler.transform(target_concat) | |
| device = "cuda" if __import__("torch").cuda.is_available() else "cpu" | |
| factories = _torch_factories(source_X.shape[-1]) | |
| for model_id in TORCH_MODEL_IDS + TF_MODEL_IDS: | |
| if model_id not in requested: | |
| continue | |
| print( | |
| f"[{source_name}->external] seed={seed} model={model_id} start", | |
| flush=True, | |
| ) | |
| _set_seed(seed) | |
| if model_id in factories: | |
| _, combined_pred, epochs = _train_torch_model( | |
| model_id, factories[model_id](), X_train, y_source[train_idx], | |
| X_val, y_source[val_idx], X_target, max_epochs=max_epochs, | |
| patience=patience, batch_size=batch_size, device=device, | |
| ) | |
| else: | |
| _, combined_pred, epochs = _train_tensorflow_model( | |
| model_id, X_train, y_source[train_idx], X_val, y_source[val_idx], | |
| X_target, max_epochs=max_epochs, patience=patience, | |
| batch_size=batch_size, | |
| ) | |
| offset = 0 | |
| for target_name, (_, target_index) in targets.items(): | |
| count = lengths[target_name] | |
| pred = combined_pred[offset : offset + count] | |
| offset += count | |
| y_target = target_index["SoH"].to_numpy(dtype=float) | |
| metric_rows.append({ | |
| "source_dataset": source_name, | |
| "target_dataset": target_name, | |
| "seed": seed, | |
| "model": model_id, | |
| "epochs": epochs, | |
| **regression_metrics(y_target, pred), | |
| }) | |
| for row, value in enumerate(pred): | |
| prediction_rows.append({ | |
| "source_dataset": source_name, | |
| "target_dataset": target_name, | |
| "seed": seed, | |
| "model": model_id, | |
| "battery_id": str(target_index.iloc[row]["battery_id"]), | |
| "cycle_number": int(target_index.iloc[row]["cycle_number"]), | |
| "y_true": y_target[row], | |
| "y_pred": float(value), | |
| "residual": float(y_target[row] - value), | |
| }) | |
| print( | |
| f"[{source_name}->external] seed={seed} model={model_id} " | |
| f"done epochs={epochs}", | |
| flush=True, | |
| ) | |
| return pd.DataFrame(metric_rows), pd.DataFrame(prediction_rows) | |