Download scripts/train.py from OneScience-Group/TerraMind: direct link, hf CLI and curl.
- Browser
- Download file 7.23 kB
-
https://huggingface.co/OneScience-Group/TerraMind/resolve/main/scripts/train.py
- Command line
-
hf download hf://OneScience-Group/TerraMind/scripts/train.py
-
curl -L -o train.py https://huggingface.co/OneScience-Group/TerraMind/resolve/main/scripts/train.py
7.23 kB
| """Train reduced TerraMind dual-scale masked token prediction.""" | |
| import json | |
| import os | |
| import random | |
| import sys | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| import yaml | |
| from torch.nn.parallel import DistributedDataParallel | |
| from torch.utils.data import DataLoader, Dataset, DistributedSampler | |
| ROOT = Path(__file__).resolve().parents[1] | |
| sys.path.insert(0, str(ROOT)) | |
| from model.terramind import TerraMind | |
| class TerraMindDataset(Dataset): | |
| def __init__(self, path, config, training=False): | |
| self.data = np.load(path) | |
| self.config = config | |
| self.training = training | |
| if str(self.data["format_version"]) != config["data"]["format_version"]: | |
| raise ValueError("incompatible data format") | |
| source = int(config["data"]["source_size"]) | |
| for name, channels in config["data"]["pixel_modalities"].items(): | |
| if self.data[f"pixel_{name}"].shape[1:] != (int(channels), source, source): | |
| raise ValueError(f"{name} does not preserve the TerraMesh source dimensions") | |
| def __len__(self): | |
| return len(self.data["pixel_s2l2a"]) | |
| def __getitem__(self, index): | |
| source, model_size = int(self.config["data"]["source_size"]), int(self.config["data"]["model_size"]) | |
| top = np.random.randint(0, source - model_size + 1) if self.training else (source - model_size) // 2 | |
| left = np.random.randint(0, source - model_size + 1) if self.training else (source - model_size) // 2 | |
| item = {} | |
| for name in self.config["data"]["pixel_modalities"]: | |
| values = self.data[f"pixel_{name}"][index, :, top:top + model_size, left:left + model_size] | |
| item[f"pixel_{name}"] = torch.from_numpy(values).float() | |
| patch = int(self.config["data"]["patch_size"]) | |
| vocab = int(self.config["model"]["engineering_vocab_size"]) | |
| image_tokens = { | |
| "s2l2a": item["pixel_s2l2a"].mean(dim=0), | |
| "s1grd": item["pixel_s1grd"].mean(dim=0), | |
| "s1rtc": item["pixel_s1rtc"].mean(dim=0), | |
| "dem": item["pixel_dem"].mean(dim=0), | |
| "ndvi": (item["pixel_s2l2a"][7] - item["pixel_s2l2a"][3]) / | |
| (item["pixel_s2l2a"][7] + item["pixel_s2l2a"][3]).abs().clamp_min(1e-3), | |
| "lulc": torch.from_numpy(self.data["token_map_lulc"][index, top:top + model_size, left:left + model_size]).float(), | |
| } | |
| for name, values in image_tokens.items(): | |
| pooled = torch.nn.functional.avg_pool2d(values[None, None], patch, stride=patch)[0, 0] | |
| minimum, maximum = pooled.amin(), pooled.amax() | |
| item[f"token_{name}"] = torch.round((pooled - minimum) / (maximum - minimum).clamp_min(1e-6) * (vocab - 1)).long().flatten() | |
| item["token_coords"] = torch.from_numpy(self.data["coords"][index]).long() | |
| item["token_caption"] = torch.from_numpy(self.data["caption"][index]).long() | |
| return item | |
| def device_from_config(config, rank=0): | |
| if config["runtime"]["device"] == "auto": | |
| return torch.device("cuda", rank) if torch.cuda.is_available() else torch.device("cpu") | |
| return torch.device(config["runtime"]["device"]) | |
| def unpack(batch, config, device): | |
| pixels = {name: batch[f"pixel_{name}"].to(device) for name in config["data"]["pixel_modalities"]} | |
| tokens = {name: batch[f"token_{name}"].to(device) for name in config["data"]["token_modalities"]} | |
| return pixels, tokens | |
| def sample_task(pixels, tokens, config): | |
| pixel_count = random.randint(int(config["model"]["min_pixel_modalities"]), | |
| min(int(config["model"]["max_pixel_modalities"]), len(pixels))) | |
| selected_pixels = random.sample(list(pixels), pixel_count) | |
| target_count = random.randint(1, max(1, len(tokens) // 2)) | |
| targets = random.sample(list(tokens), target_count) | |
| candidates = [name for name in tokens if name not in targets] | |
| token_count = random.randint(int(config["model"]["min_token_modalities"]), | |
| min(int(config["model"]["max_token_modalities"]), len(candidates))) | |
| inputs = random.sample(candidates, token_count) | |
| return {name: pixels[name] for name in selected_pixels}, inputs, targets | |
| def main(): | |
| config = yaml.safe_load((ROOT / "conf/config.yaml").read_text()) | |
| torch.manual_seed(int(config["seed"])) | |
| random.seed(int(config["seed"])) | |
| distributed = int(os.environ.get("WORLD_SIZE", "1")) > 1 | |
| local_rank = int(os.environ.get("LOCAL_RANK", "0")) | |
| if distributed: | |
| torch.distributed.init_process_group("nccl" if torch.cuda.is_available() else "gloo") | |
| rank = torch.distributed.get_rank() if distributed else 0 | |
| device = device_from_config(config, local_rank) | |
| dataset = TerraMindDataset(ROOT / config["data"]["root"] / "train.npz", config, training=True) | |
| sampler = DistributedSampler(dataset, shuffle=True) if distributed else None | |
| loader = DataLoader(dataset, batch_size=int(config["train"]["batch_size"]), sampler=sampler, | |
| shuffle=sampler is None, num_workers=int(config["train"]["num_workers"])) | |
| model = TerraMind(config["data"]["pixel_modalities"], config["data"]["token_modalities"], config["model"]).to(device) | |
| if distributed: | |
| model = DistributedDataParallel(model, device_ids=[local_rank] if device.type == "cuda" else None) | |
| optimizer = torch.optim.AdamW(model.parameters(), lr=float(config["train"]["learning_rate"]), | |
| weight_decay=float(config["train"]["weight_decay"])) | |
| history = [] | |
| for epoch in range(int(config["train"]["epochs"])): | |
| total, steps = 0.0, 0 | |
| model.train() | |
| for batch in loader: | |
| pixels, tokens = unpack(batch, config, device) | |
| selected_pixels, input_tokens, targets = sample_task(pixels, tokens, config) | |
| output = model(selected_pixels, tokens, targets, input_tokens, apply_input_mask=True) | |
| optimizer.zero_grad(set_to_none=True) | |
| output["loss"].backward() | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) | |
| optimizer.step() | |
| total += float(output["loss"].detach()) | |
| steps += 1 | |
| metrics = {"epoch": epoch + 1, "cross_modal_token_ce": total / max(steps, 1)} | |
| history.append(metrics) | |
| if rank == 0: | |
| print(f"epoch={epoch + 1} cross_modal_token_ce={metrics['cross_modal_token_ce']:.6f}") | |
| if rank == 0: | |
| checkpoint, metrics_path = ROOT / config["paths"]["checkpoint"], ROOT / config["paths"]["training_metrics"] | |
| checkpoint.parent.mkdir(parents=True, exist_ok=True) | |
| metrics_path.parent.mkdir(parents=True, exist_ok=True) | |
| state = model.module.state_dict() if distributed else model.state_dict() | |
| torch.save({"model": state, "model_config": config["model"], "pixel_modalities": config["data"]["pixel_modalities"], | |
| "token_modalities": config["data"]["token_modalities"], "format_version": config["data"]["format_version"]}, checkpoint) | |
| metrics_path.write_text(json.dumps({"history": history}, indent=2) + "\n") | |
| if distributed: | |
| torch.distributed.destroy_process_group() | |
| if __name__ == "__main__": | |
| main() | |