forge-tiny-drift-multiruntime / export_models.py
sankalpsthakur's picture
Fix standalone reproduction paths
8f34820 verified
Raw
History Blame Contribute Delete
6.28 kB
from __future__ import annotations
import json
import os
import random
import shutil
import sys
from importlib.metadata import version
from pathlib import Path
os.environ.setdefault("TF_CPP_MIN_LOG_LEVEL", "2")
os.environ.setdefault("TF_ENABLE_ONEDNN_OPTS", "0")
import numpy as np
import torch
from torch import nn
ROOT = Path(__file__).resolve().parent
sys.path.insert(0, str(ROOT / "src"))
from forge_lab.models.numpy_runtime import ( # noqa: E402
DEFAULT_CENTER,
DEFAULT_SCALE,
WINDOW_SIZE,
)
from forge_lab.models.pytorch_model import TinyDriftNet # noqa: E402
ARTIFACTS = ROOT
MODEL_VERSION = "forge-tiny-drift-v0.1"
def synthetic_windows(count: int = 2048, seed: int = 17) -> tuple[np.ndarray, np.ndarray]:
rng = np.random.default_rng(seed)
windows = np.empty((count, WINDOW_SIZE, 2), dtype=np.float32)
labels = np.empty(count, dtype=np.float32)
x = np.arange(WINDOW_SIZE, dtype=np.float32)
for index in range(count):
slope = rng.uniform(-0.01, 0.13)
force = (
rng.uniform(96.0, 104.0)
+ slope * x
+ rng.normal(0.0, rng.uniform(0.03, 0.18), WINDOW_SIZE)
)
deviation = (
0.07 + np.maximum(0.0, force - 101.0) * 0.011 + rng.normal(0.0, 0.002, WINDOW_SIZE)
)
windows[index, :, 0] = force
windows[index, :, 1] = deviation
shift = force[-10:].mean() - force[:10].mean()
labels[index] = float(slope >= 0.07 and shift >= 2.0)
return windows, labels
def train_model() -> tuple[TinyDriftNet, dict]:
random.seed(17)
np.random.seed(17)
torch.manual_seed(17)
x, y = synthetic_windows()
model = TinyDriftNet()
optimizer = torch.optim.AdamW(model.classifier.parameters(), lr=0.04, weight_decay=0.001)
loss_fn = nn.BCELoss()
x_tensor = torch.from_numpy(x)
y_tensor = torch.from_numpy(y)
model.train()
for _ in range(240):
optimizer.zero_grad(set_to_none=True)
loss = loss_fn(model(x_tensor), y_tensor)
loss.backward()
optimizer.step()
model.eval()
with torch.no_grad():
probabilities = model(x_tensor)
predictions = probabilities >= 0.5
labels = y_tensor.bool()
accuracy = float((predictions == labels).float().mean())
false_positives = ((predictions == 1) & (labels == 0)).sum()
false_negatives = ((predictions == 0) & (labels == 1)).sum()
false_positive_rate = float(false_positives / (labels == 0).sum())
false_negative_rate = float(false_negatives / (labels == 1).sum())
metrics = {
"synthetic_examples": len(x),
"accuracy": round(accuracy, 6),
"false_positive_rate": round(false_positive_rate, 6),
"false_negative_rate": round(false_negative_rate, 6),
"training_seed": 17,
}
return model, metrics
def export_pytorch_and_onnx(model: TinyDriftNet) -> None:
torch.save(
{
"model_version": MODEL_VERSION,
"state_dict": model.state_dict(),
},
ARTIFACTS / "tiny_drift_pytorch.pt",
)
example = torch.zeros(2, WINDOW_SIZE, 2, dtype=torch.float32)
batch = torch.export.Dim("batch", min=1, max=512)
onnx_program = torch.onnx.export(
model,
(example,),
input_names=["telemetry"],
output_names=["probability"],
dynamic_shapes=({0: batch},),
dynamo=True,
verify=True,
external_data=False,
)
onnx_program.save(ARTIFACTS / "tiny_drift.onnx")
def export_tensorflow_and_litert(weight: np.ndarray, bias: float) -> None:
import tensorflow as tf
from forge_lab.models.tensorflow_model import TensorFlowDriftModel
model = TensorFlowDriftModel(weight, bias)
saved_model = ARTIFACTS / "tensorflow_saved_model"
if saved_model.exists():
shutil.rmtree(saved_model)
concrete = model.__call__.get_concrete_function()
tf.saved_model.save(model, saved_model, signatures={"serving_default": concrete})
converter = tf.lite.TFLiteConverter.from_concrete_functions([concrete], model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
(ARTIFACTS / "tiny_drift.tflite").write_bytes(converter.convert())
def write_metadata(model: TinyDriftNet, metrics: dict) -> None:
weight = model.classifier.weight.detach().cpu().numpy().reshape(-1)
bias = float(model.classifier.bias.detach().cpu().item())
weights = {
"model_version": MODEL_VERSION,
"feature_center": DEFAULT_CENTER.tolist(),
"feature_scale": DEFAULT_SCALE.tolist(),
"linear_weight": weight.tolist(),
"linear_bias": bias,
}
(ARTIFACTS / "weights.json").write_text(json.dumps(weights, indent=2) + "\n")
manifest = {
"model_version": MODEL_VERSION,
"input": {"name": "telemetry", "shape": ["batch", WINDOW_SIZE, 2], "dtype": "float32"},
"channels": ["peak_force_kn", "part_deviation_mm"],
"outputs": ["probability"],
"runtimes": ["numpy", "pytorch", "onnxruntime", "tensorflow", "litert"],
"framework_versions": {
"python": sys.version.split()[0],
"numpy": np.__version__,
"pytorch": torch.__version__,
"pytorch_cuda_build": torch.version.cuda,
"onnx": version("onnx"),
"onnxruntime": version("onnxruntime"),
"tensorflow": version("tensorflow-cpu"),
"litert": version("ai-edge-litert"),
},
"metrics": metrics,
"data": "deterministic synthetic telemetry; no plant data",
"safety": "L0 decision support only; output cannot actuate equipment",
"cuda_validation": "not executed; no GPU or nvcc in the build workspace",
}
(ARTIFACTS / "model_manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
def main() -> None:
ARTIFACTS.mkdir(parents=True, exist_ok=True)
model, metrics = train_model()
write_metadata(model, metrics)
weight = model.classifier.weight.detach().cpu().numpy().reshape(-1)
bias = float(model.classifier.bias.detach().cpu().item())
export_pytorch_and_onnx(model)
export_tensorflow_and_litert(weight, bias)
print(json.dumps(metrics, indent=2))
if __name__ == "__main__":
main()