Deep Learning¶
PyTorch models in zeit.ai, and the functions that take them from a cube to a map: samples, train and predict. The models are ordinary nn.Modules, which can also be trained with a loop of your own. Tutorials: Deep Learning.
From a cube to a map¶
The same path as train_classifier and classify, with the deep learning models: labelled points or polygons and a cube give the samples, train fits a model with a ready-made loop, and predict classifies every pixel into a georeferenced map. What prediction has to check (bands, dates, classes, normalization, patch size) travels with the model.
from zeit import ai
samples = ai.samples(cube, "samples.gpkg", label="class") # pixels; patch=64 for the patch models
model = ai.train(ai.TempCNN, samples, epochs=50)
classes = ai.predict(model, cube) # label (y, x), georeferenced
classes.zeit.save("classes")
ai.save(model, "tempcnn.pt") # ai.load("tempcnn.pt") later
| Model | Samples | Dates when predicting |
|---|---|---|
TempCNN, LightTAE |
pixels (patch=None) |
as many as in training (e.g. the same composites in another year) |
UTAE |
patches, a multiple of 2 ** (stages - 1) pixels (8 by default) |
any: the positions come from the cube's dates |
SiameseChangeDetector |
patches of a pair of images (before, after), even size |
a pair |
GeoFoundationViT |
patches (Prithvi: 6 bands, 224 pixels) | as in training |
samples¶
zeit.ai.samples(
data, samples, label="class", patch=None, split=0.2,
split_by="block", block_size=64, seed=42, nodata="auto",
)
Labelled samples of a cube, for train.
| Parameter | Type | Default | Description |
|---|---|---|---|
data |
DataArray, path or pair |
required | A (time, y, x) or (time, band, y, x) cube (in memory or dask), anything load_raster reads, or a pair of images (before, after) for the Siamese detector. |
samples |
GeoDataFrame or path |
required | Points or polygons with their class, reprojected when in another CRS. |
label |
str |
"class" |
The column holding the class. |
patch |
int |
None |
None: one sample per pixel, (time, band), for TempCNN and LightTAE; points on the same pixel count once, a polygon gives every pixel it covers. A size: a (time, band, patch, patch) window around each point or small polygon (larger polygons are tiled by windows), with every labelled pixel in it and -1 elsewhere, for the patch models. |
split |
float |
0.2 |
Share of the samples kept for validation (0: none). |
split_by |
str |
"block" |
"block": whole blocks of block_size pixels go to validation, so that it measures how the model does away from what it was trained on (neighbouring pixels are alike: random pixels on both sides would measure that); "random": samples at random. |
block_size |
int |
64 |
Side of the validation blocks, in pixels. |
seed |
int |
42 |
Seed of the split. |
nodata |
"auto", float or None |
"auto" |
Value marking a missing observation. |
Returns a SampleSet. The bands are normalized by their 2% and 98% quantiles in the training samples, as sits does, and missing values become 0 after it (gaps can be filled first with zeit.smooth). Patch models learn best from dense labels (polygons): a patch around a point has a single labelled pixel.
SampleSet¶
The samples, as a PyTorch Dataset, made by samples. Each item is a dict with x (normalized: (time, band) or (time, band, patch, patch)), y (the class index, or a (patch, patch) mask with -1 where unlabelled) and positions (days since the first date). train and val are the two parts of the split, classes the class names, meta what train records in the model. For a loop of your own: DataLoader(samples.train, batch_size=64, shuffle=True).
| Parameter | Type | Default | Description |
|---|---|---|---|
X, y |
ndarray |
required | The raw values and the labels. |
is_val |
ndarray |
required | Which samples are for validation. |
meta |
dict |
required | Bands, dates, positions, classes, normalization, patch. |
indices |
ndarray |
None |
The samples this view holds (train, val). |
train¶
zeit.ai.train(
model, samples, epochs=50, batch_size=None, lr=0.001,
weight_decay=0.0, loss="ce", patience=10, device="auto", seed=42,
verbose=False, **model_kwargs,
)
Trains a model on a SampleSet with Adam, keeping the weights of the epoch with the lowest validation loss.
| Parameter | Type | Default | Description |
|---|---|---|---|
model |
class or nn.Module |
required | TempCNN, LightTAE, UTAE, SiameseChangeDetector or GeoFoundationViT: a class is built with the samples' bands, dates and classes (and model_kwargs); an instance is trained as it is. |
samples |
SampleSet |
required | From samples: pixels or patches, as the model takes. |
epochs |
int |
50 |
At most this many passes over the training samples. |
batch_size |
int |
None |
Samples per step: 64 pixels or 8 patches (a patch holds many labelled pixels, and smaller batches leave more steps per epoch). |
lr, weight_decay |
float |
0.001, 0.0 |
Adam's learning rate and weight decay. |
loss |
str or function |
"ce" |
"ce" (cross-entropy), "focal" (for imbalanced classes) or (logits, target) -> loss, with target -1 where unlabelled. |
patience |
int |
10 |
Stop after this many epochs without a lower validation loss, once the model has taken 50 steps (validation runs with batch normalization's running statistics, which need them to settle), and keep the best epoch (None: every epoch, the last one kept). |
device |
str |
"auto" |
"auto" (the GPU if there is one), "cpu", "cuda"... |
seed |
int |
42 |
Seed of the initial weights and the order of the samples. |
verbose |
bool |
False |
Print each epoch's losses and validation accuracy. |
**model_kwargs |
For a class: its own arguments (e.g. dropout_rate, UTAE's encoder_widths). |
Returns the trained model in evaluation mode, with zeit_meta_ (bands, dates, classes, normalization, patch, the model's class and arguments) and zeit_history_ (per epoch: train_loss, val_loss, val_accuracy).
predict¶
zeit.ai.predict(
model, data, batch_size=256, overlap=0.25, probability=False,
device="auto", nodata="auto", chunks=None,
)
Classifies every pixel of a cube with a model trained by train (or loaded by load). zeit.classify(cube, model) does the same for such a model.
| Parameter | Type | Default | Description |
|---|---|---|---|
model |
nn.Module |
required | A model with zeit_meta_. |
data |
as in samples |
required | The cube; its bands are taken by name and normalized as the training samples were. A lazy cube stays lazy, computed block by block. |
batch_size |
int |
256 |
Pixels or windows through the model at a time. |
overlap |
float |
0.25 |
Patch models: the image is covered by windows of the training patch size, overlapping by this share; each pixel's probabilities are the average of the windows over it, weighted down towards their edges, so there are no seams. The windows are laid on the whole image, so a lazy cube gives the map in memory. |
probability |
bool |
False |
Also return each class's probability. |
device |
str |
"auto" |
As in train. |
nodata |
"auto", float or None |
"auto" |
Value marking a missing observation. |
chunks |
"auto", dict |
None |
A raster path: read lazily. |
Returns an xarray.Dataset like zeit.classify's: label (y, x) (1 for the first class, 0 where the pixel has no data at any date; the names in flag_meanings) and, with probability=True, probability (class, y, x). Georeferenced as the input.
save¶
Saves a trained model: its weights, zeit_meta_ and history.
| Parameter | Type | Default | Description |
|---|---|---|---|
model |
nn.Module |
required | A model trained by train. |
path |
path | required | The file to write. |
load¶
Loads a model saved by save, ready for predict. A model train built from its class is built again; for one trained as an instance, pass one like it.
| Parameter | Type | Default | Description |
|---|---|---|---|
path |
path | required | The file save wrote. |
model |
nn.Module |
None |
An instance to put the weights in (a model trained from an instance). |
device |
str |
"cpu" |
Where to load the weights. |
Data¶
STACCubeDataset¶
A PyTorch Dataset of the windows of a (time, band, y, x) cube, lazy or not, for a loop of your own. The windows start every stride pixels and the last one of each row and column lies against the far edge, so they cover the whole cube (a cube smaller than a window gives one, padded with NaN). Only the window asked for is computed, so it works on cubes larger than memory.
| Parameter | Type | Default | Description |
|---|---|---|---|
cube |
xr.DataArray |
required | (time, band, y, x) cube, e.g. from build_time_series. |
patch_size |
int |
256 |
Window side in pixels. |
stride |
int |
256 |
Step between windows. Smaller than patch_size gives overlapping windows. |
Each item is a dict: x, a float32 tensor (time, band, patch_size, patch_size) with missing values left as NaN; valid, (time, patch_size, patch_size), True where every band has a value; positions, the days since the cube's first date (for UTAE's batch_positions; the day of the year would make dates a year apart collide); and row, col, the window's top-left pixel. dataset.positions holds the same positions.
from zeit.ai import STACCubeDataset
dataset = STACCubeDataset(cube, patch_size=64, stride=64)
item = dataset[0]
x = torch.nan_to_num(item["x"]) # or fill the gaps as your model needs
Pixel classifiers¶
TempCNN¶
class zeit.ai.TempCNN(
in_channels, n_times, num_classes=5, hidden_dims=(64, 64, 64),
kernel_sizes=(3, 3, 3), dropout_rates=(0.2, 0.2, 0.2),
dense_layer_nodes=256, dense_layer_dropout_rate=0.5,
)
Temporal convolutional network (Pelletier et al., 2019), ported layer for layer from R sits' sits_tempcnn(); weights load directly with load_state_dict. Input (batch, bands, time), output class logits (batch, num_classes). Tutorial: TempCNN.
| Parameter | Type | Default | Description |
|---|---|---|---|
in_channels |
int |
required | Bands per observation. |
n_times |
int |
required | Series length. Fixed for the life of the model (the flatten layer depends on it). |
num_classes |
int |
5 |
Output classes. |
hidden_dims |
tuple |
(64, 64, 64) |
Filters of the three convolution blocks. |
kernel_sizes |
tuple |
(3, 3, 3) |
Kernel sizes. |
dropout_rates |
tuple |
(0.2, 0.2, 0.2) |
Dropout after each convolution block. |
dense_layer_nodes |
int |
256 |
Width of the dense layer. |
dense_layer_dropout_rate |
float |
0.5 |
Dropout of the dense layer. |
from zeit.ai import TempCNN
model = TempCNN(in_channels=6, n_times=23, num_classes=4)
logits = model(torch.randn(32, 6, 23)) # (32, 4)
LightTAE¶
class zeit.ai.LightTAE(
n_bands, day_offsets, n_labels,
layers_spatial_encoder=(32, 64, 128), n_heads=16,
n_neurons=(256, 128), dropout_rate=0.2, dim_input_decoder=128,
dim_layers_decoder=(64, 32),
)
Lightweight temporal attention classifier: per-observation MLP encoder, L-TAE temporal attention, MLP decoder. Ported from sits_lighttae(). Input (batch, time, bands), output logits (batch, n_labels). Tutorial: LTAE & LightTAE.
| Parameter | Type | Default | Description |
|---|---|---|---|
n_bands |
int |
required | Bands per observation. |
day_offsets |
list[float] |
required | Day of each observation, counted from the first. Fixes the series length. |
n_labels |
int |
required | Output classes. |
layers_spatial_encoder |
tuple |
(32, 64, 128) |
Widths of the per-observation encoder. |
n_heads |
int |
16 |
Attention heads. |
n_neurons |
tuple |
(256, 128) |
L-TAE widths; n_neurons[0] is the attention dimension. |
dropout_rate |
float |
0.2 |
Dropout. |
dim_input_decoder |
int |
128 |
Decoder input width; must equal n_neurons[-1]. |
dim_layers_decoder |
tuple |
(64, 32) |
Decoder hidden widths. |
from zeit.ai import LightTAE
model = LightTAE(n_bands=6, day_offsets=list(range(0, 36 * 16, 16)), n_labels=5)
logits = model(torch.randn(8, 36, 6)) # (8, 5)
LTAE¶
class zeit.ai.LTAE(
in_channels=128, day_offsets=None, n_heads=16,
n_neurons=(256, 128), dropout_rate=0.2,
)
The L-TAE temporal attention block on its own, to build custom models. Input (batch, time, in_channels), output (batch, n_neurons[-1]).
| Parameter | Type | Default | Description |
|---|---|---|---|
in_channels |
int |
128 |
Features per time step. |
day_offsets |
list[float] |
None |
Day of each observation, counted from the first. Required in practice. |
n_heads |
int |
16 |
Attention heads. |
n_neurons |
tuple |
(256, 128) |
Internal widths. |
dropout_rate |
float |
0.2 |
Dropout. |
Patch segmentation¶
UTAE¶
class zeit.ai.UTAE(
input_dim, encoder_widths=(64, 64, 64, 128),
decoder_widths=(32, 32, 64, 128), out_conv=(32, 20), str_conv_k=4,
str_conv_s=2, str_conv_p=1, agg_mode="att_group",
encoder_norm="group", n_head=16, d_model=256, d_k=4,
encoder=False, return_maps=False, pad_value=0,
padding_mode="reflect",
)
U-Net with temporal attention (Garnot & Landrieu, 2021), ported from the official utae-paps code and bit-exact with it. Input (batch, time, bands, H, W) plus batch_positions (batch, time) (day of each observation); output class scores (batch, classes, H, W). Tutorial: UTAE.
| Parameter | Type | Default | Description |
|---|---|---|---|
input_dim |
int |
required | Bands. |
encoder_widths |
list[int] |
(64, 64, 64, 128) |
Encoder widths, finest level first. |
decoder_widths |
list[int] |
(32, 32, 64, 128) |
Decoder widths. Same length as the encoder; last value equal to the encoder's. |
out_conv |
list[int] |
(32, 20) |
Output convolution widths; the last is the number of classes. |
str_conv_k, str_conv_s, str_conv_p |
int |
4, 2, 1 |
Kernel, stride and padding of the strided convolutions. |
agg_mode |
str |
"att_group" |
Temporal aggregation of skip connections: "att_group", "att_mean" or "mean". |
encoder_norm |
str |
"group" |
"group", "batch" or "instance". |
n_head |
int |
16 |
Attention heads. |
d_model |
int |
256 |
Attention width (divisible by n_head). |
d_k |
int |
4 |
Key size per head. |
encoder |
bool |
False |
Return features instead of class scores. |
return_maps |
bool |
False |
Also return the decoder feature maps. |
pad_value |
float |
0 |
Value marking padded time steps (for variable-length batches). |
padding_mode |
str |
"reflect" |
Spatial padding of the convolutions. |
from zeit.ai import UTAE
model = UTAE(input_dim=6, out_conv=[32, 10]).eval()
x = torch.randn(2, 12, 6, 128, 128)
days = torch.arange(12.0).expand(2, -1) * 16
scores = model(x, batch_positions=days) # (2, 10, 128, 128)
Change detection¶
SiameseChangeDetector¶
Two-date change detection with a shared (Siamese) convolutional encoder: both images are encoded with the same weights, their feature difference is decoded to a per-pixel map. An independent implementation of the design of Daudt et al. (2018). Tutorial: Siamese Change Detector.
| Parameter | Type | Default | Description |
|---|---|---|---|
in_channels |
int |
required | Bands per image. |
num_classes |
int |
2 |
Output classes (2 for change / no change). |
from zeit.ai import SiameseChangeDetector
model = SiameseChangeDetector(in_channels=4)
logits = model(torch.randn(8, 4, 256, 256), torch.randn(8, 4, 256, 256)) # (8, 2, 256, 256)
Foundation models¶
GeoFoundationViT¶
Loads a geospatial foundation model from the HuggingFace Hub (Prithvi-100M by default) and adds a 1 × 1 convolution head for segmentation. If the backbone cannot be loaded, it falls back to an untrained 3-D convolution and sets model.has_hf = False. Tutorial: GeoFoundationViT.
| Parameter | Type | Default | Description |
|---|---|---|---|
model_id |
str |
"ibm-nasa-geospatial/Prithvi-100M" |
HuggingFace model ID. |
num_classes |
int |
2 |
Output classes of the head. |
Losses¶
FocalLoss¶
Cross-entropy that down-weights easy examples, for heavily imbalanced problems such as change detection. Takes logits (B, C, …) and integer targets.
| Parameter | Type | Default | Description |
|---|---|---|---|
alpha |
float |
0.25 |
Overall weight. |
gamma |
float |
2.0 |
Focusing strength. 0 gives plain cross-entropy. |
reduction |
str |
"mean" |
"mean" or "sum". |
TverskyLoss¶
Overlap-based loss for the positive (change) class, with separate weights for false positives and false negatives.
| Parameter | Type | Default | Description |
|---|---|---|---|
alpha |
float |
0.3 |
Weight of false positives. |
beta |
float |
0.7 |
Weight of false negatives. beta > alpha penalises missed changes more. |
smooth |
float |
1.0 |
Smoothing term. |
ContrastiveSiameseLoss¶
Contrastive loss on two feature maps: pulls features together where nothing changed (label 0) and pushes them at least margin apart where something changed (label 1).
| Parameter | Type | Default | Description |
|---|---|---|---|
margin |
float |
2.0 |
Minimum distance for changed pixels. |