people-and-planet-ai/weather-forecasting/serving/weather-model/weather/model.py (83 lines of code) (raw):
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Defines a Fully Convolutional Network to predict precipitation."""
from __future__ import annotations
from typing import Any as AnyType
from datasets.arrow_dataset import Dataset
import numpy as np
import torch
from transformers import PretrainedConfig, PreTrainedModel
class WeatherConfig(PretrainedConfig):
"""A custom Hugging Face config for a WeatherModel.
This contains all the hyperparameters for the model, including the
mean and standard deviation used for the Normalization layer in the model.
For more information:
https://huggingface.co/docs/transformers/main/en/custom_models#writing-a-custom-configuration
"""
model_type = "weather"
def __init__(
self,
mean: list = [],
std: list = [],
num_inputs: int = 52,
num_hidden1: int = 64,
num_hidden2: int = 128,
num_outputs: int = 2,
kernel_size: tuple[int, int] = (3, 3),
**kwargs: AnyType,
) -> None:
self.mean = mean
self.std = std
self.num_inputs = num_inputs
self.num_hidden1 = num_hidden1
self.num_hidden2 = num_hidden2
self.num_outputs = num_outputs
self.kernel_size = kernel_size
super().__init__(**kwargs)
class WeatherModel(PreTrainedModel):
"""A custom Hugging Face model.
For more information:
https://huggingface.co/docs/transformers/main/en/custom_models#writing-a-custom-model
"""
config_class = WeatherConfig
def __init__(self, config: WeatherConfig) -> None:
super().__init__(config)
self.layers = torch.nn.Sequential(
Normalization(config.mean, config.std),
MoveDim(-1, 1), # convert to channels-first
torch.nn.Conv2d(config.num_inputs, config.num_hidden1, config.kernel_size),
torch.nn.ReLU(),
torch.nn.ConvTranspose2d(
config.num_hidden1, config.num_hidden2, config.kernel_size
),
torch.nn.ReLU(),
MoveDim(1, -1), # convert to channels-last
torch.nn.Linear(config.num_hidden2, config.num_outputs),
torch.nn.ReLU(), # precipitation cannot be negative
)
def forward(
self, inputs: torch.Tensor, labels: torch.Tensor | None = None
) -> dict[str, torch.Tensor]:
"""Computes predictions as expected by ModelOutputs.
If `labels` are passed, it computes the loss between the model's
predictions and the actual labels.
For more information:
https://huggingface.co/docs/transformers/main/en/main_classes/output
Args:
inputs: Input data.
labels: Ground truth data.
Returns:
{"loss": loss, "logits": predictions} if `labels` is provided.
{"logits": predictions} otherwise.
"""
predictions = self.layers(inputs)
if labels is None:
return {"logits": predictions}
loss_fn = torch.nn.SmoothL1Loss()
loss = loss_fn(predictions, labels)
return {"loss": loss, "logits": predictions}
@staticmethod
def create(inputs: Dataset, **kwargs: AnyType) -> WeatherModel:
"""Creates a new WeatherModel calculating the
mean and standard deviation from a dataset."""
data = np.array(inputs, np.float32)
mean = data.mean(axis=(0, 1, 2))[None, None, None, :]
std = data.std(axis=(0, 1, 2))[None, None, None, :]
config = WeatherConfig(mean.tolist(), std.tolist(), **kwargs)
return WeatherModel(config)
def predict(self, inputs: AnyType) -> np.ndarray:
"""Predicts a single request."""
return self.predict_batch(torch.as_tensor([inputs]))[0]
def predict_batch(self, inputs_batch: AnyType) -> np.ndarray:
"""Predicts a batch of requests."""
device = "cuda" if torch.cuda.is_available() else "cpu"
model = self.to(device)
with torch.no_grad():
outputs = model(torch.as_tensor(inputs_batch, device=device))
predictions = outputs["logits"]
return predictions.cpu().numpy()
class Normalization(torch.nn.Module):
"""Preprocessing normalization layer with z-score."""
def __init__(self, mean: AnyType, std: AnyType) -> None:
super().__init__()
self.mean = torch.nn.Parameter(torch.as_tensor(mean))
self.std = torch.nn.Parameter(torch.as_tensor(std))
def forward(self, x: torch.Tensor) -> torch.Tensor:
return (x - self.mean) / self.std
class MoveDim(torch.nn.Module):
"""Moves a dimension axis to another position."""
def __init__(self, src: int, dest: int) -> None:
super().__init__()
self.src = src
self.dest = dest
def forward(self, x: torch.Tensor) -> torch.Tensor:
return x.moveaxis(self.src, self.dest)