ML-MODIS / scripts /train.py
zhangrenchao's picture
Publish ML-MODIS engineering reproduction
73ddb67 verified
Raw
History Blame Contribute Delete
4.61 kB
#!/usr/bin/env python3
"""Train month-by-target ML-MODIS forests, optionally task-parallel under torchrun."""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
import numpy as np
import torch
import yaml
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "model"))
from ml_modis import BootstrapRandomForestRegressor, feature_names, regression_metrics, validate_multimodal_keys
def args_parser() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--config", default=str(ROOT / "conf/config.yaml"))
parser.add_argument("--data", default=None)
parser.add_argument("--checkpoint", default=None)
parser.add_argument("--paper-model", action="store_true")
parser.add_argument("--trees", type=int, default=None)
return parser.parse_args()
def distributed_context() -> tuple[int, int]:
world = int(os.environ.get("WORLD_SIZE", "1"))
rank = int(os.environ.get("RANK", "0"))
if world > 1:
torch.distributed.init_process_group(backend="gloo")
return rank, world
def main() -> None:
args = args_parser()
config = yaml.safe_load(Path(args.config).read_text())
settings = dict(config["model"])
if args.paper_model:
settings.update(config["paper_model"])
if args.trees is not None:
settings["trees"] = args.trees
data_path = ROOT / (args.data or config["data"]["path"])
with np.load(data_path) as archive:
data = {key: archive[key] for key in archive.files}
validate_multimodal_keys(data)
rank, world = distributed_context()
months = config["data"]["months"]
targets = config["data"]["variables"]["targets"]["names"]
tasks = [(int(month), target_index, target) for month in months
for target_index, target in enumerate(targets)]
local_models = {}
for task_index, (month, target_index, target) in enumerate(tasks):
if task_index % world != rank:
continue
mask = (data["month"] == month) & (data["year"] != config["train"]["excluded_year"])
x, y = data["X"][mask], data["Y"][mask, target_index]
model = BootstrapRandomForestRegressor(
n_trees=int(settings["trees"]), min_leaf=int(settings["min_leaf"]),
max_features=int(settings["max_features"]), bootstrap_fraction=float(settings["bootstrap_fraction"]),
max_depth=settings["max_depth"], split_candidates=int(settings["split_candidates"]),
seed=int(config["runtime"]["seed"] + task_index * 1009),
).fit(x, y)
oob, counts = model.oob_predict(x)
local_models[f"{month}:{target}"] = {
"state": model.state_dict(), "oob_metrics": regression_metrics(y[counts > 0], oob[counts > 0]),
"train_samples": int(mask.sum()), "excluded_year": int(config["train"]["excluded_year"]),
}
print(f"rank={rank} trained month={month} target={target} samples={mask.sum()}", flush=True)
if world > 1:
gathered = [None] * world if rank == 0 else None
torch.distributed.gather_object(local_models, gathered, dst=0)
if rank == 0:
local_models = {key: value for shard in gathered for key, value in shard.items()}
if rank == 0:
checkpoint = ROOT / (args.checkpoint or config["paths"]["checkpoint"])
checkpoint.parent.mkdir(parents=True, exist_ok=True)
model_config = {
"architecture": "BootstrapRandomForestRegressor", "settings": settings,
"targets": targets, "months": months, "input_features": 114,
"feature_names": feature_names(), "excluded_year": int(config["train"]["excluded_year"]),
}
torch.save({"model": local_models, "model_config": model_config,
"format_version": config["format_version"],
"training": {"paper_model": args.paper_model, "distributed_world_size": world}}, checkpoint)
summary = {key: value["oob_metrics"] for key, value in sorted(local_models.items())}
metrics_path = ROOT / config["paths"]["training_metrics"]
metrics_path.parent.mkdir(parents=True, exist_ok=True)
metrics_path.write_text(json.dumps({"format_version": config["format_version"],
"models": summary}, indent=2, allow_nan=False) + "\n")
print(json.dumps({"checkpoint": str(checkpoint), "models": len(local_models), "oob": summary}, indent=2))
if world > 1:
torch.distributed.destroy_process_group()
if __name__ == "__main__":
main()