LTAE & LightTAE
1. Introduction
The Lightweight Temporal Attention Encoder (L-TAE) is a compact, fast attention mechanism designed specifically for classifying per-pixel satellite image time series — the kind of long, irregularly-sampled sequence of spectral observations you get from a single pixel's history in a data cube. It was introduced in:
Garnot, V. S. F., & Landrieu, L. (2020). Satellite image time series classification with pixel-set encoders and temporal self-attention. CVPR 2020.
cdts.ai exposes two related classes:
LTAE: the reusable temporal-fusion block itself — takes an encoded(batch, seq_len, in_channels)sequence and fuses it into a single(batch, n_neurons[-1])embedding via multi-head attention. Useful if you want to plug L-TAE fusion into your own custom architecture (e.g. as the temporal-fusion stage of a spatial-temporal model).LightTAE: the full, ready-to-train pixel time-series classifier — a small per-pixel MLP spatial encoder, followed byLTAEtemporal fusion, followed by an MLP decoder to class logits. This is the model you want for standard "classify this pixel's time series into a land-cover class" tasks.
Both were ported layer-for-layer from the R package sits's sits_lighttae() (.torch_light_temporal_attention_encoder / sits_lighttae() in sits's R/api_torch_psetae.R and R/sits_lighttae.R), so trained weights are directly portable between the two implementations via state_dict() — there is no name-translation table needed. This was validated in-session by exporting a trained sits_lighttae() model's weights, loading them into cdts.ai.LightTAE via load_state_dict, and confirming the outputs match sits's own predictions within float32 tolerance on the same input.
2. How It Works
The "L" in L-TAE stands for lightweight, and the trick that makes it fast is a learned "master query": unlike standard self-attention, where the query vector is computed from the input at every forward pass, L-TAE's query is a single learned parameter per attention head, shared across every input in the batch. This collapses what would normally be an O(seq_len²) self-attention computation down to O(seq_len) — each timestep only needs to attend to the master query, not to every other timestep.
LightTAE's forward pass has three stages:
- Spatial encoder (
_PixelSpatialEncoder): a small per-timestep MLP (Linear -> BatchNorm1d -> ReLU, stacked) that independently encodes every(pixel, time)spectral observation fromn_bandsinto a higher-dimensional embedding (default(32, 64, 128)). - Temporal encoder (
LTAE): adds a sinusoidal positional encoding keyed onday_offsets(day counts from the first observation, not the raw calendar date — so it doesn't matter which year your series starts in), then fuses the sequence via the master-query multi-head attention described above, producing one embedding per pixel. - Decoder: a small MLP (
Linear -> BatchNorm1d -> ReLU, stacked) mapping the fused embedding ton_labelsclass logits (softmax is applied externally, matchingsits's convention — usetorch.nn.functional.cross_entropy, which expects raw logits, or applysoftmax/argmaxyourself at inference time).
Because the flatten/positional-encoding buffers are sized at construction time from day_offsets, a given LightTAE (or LTAE) instance is tied to one fixed sequence length and temporal sampling pattern for its lifetime — the same constraint sits_lighttae()'s timeline parameter imposes. If your pixels have varying numbers of valid observations, interpolate/gap-fill them onto a common day_offsets grid before feeding them in.
3. When to Use It
| LightTAE | TempCNN | |
|---|---|---|
| Best for | Per-pixel time series with many, well-sampled observations where temporal ordering/attention genuinely helps | Shorter or noisier series, or when you want a simpler, faster-to-train baseline |
| Compute cost | Moderate (attention has more parameters than a plain conv stack) | Lower |
| Cross-validated against | sits_lighttae() (R) |
sits_tempcnn() (R) |
See the TempCNN tutorial for the simpler 1D-CNN alternative, and the UTAE tutorial if you need spatially-aware segmentation (a class per pixel over a whole image patch, not just a single pixel's own time series).
4. Preparing Your Data
LightTAE expects a 3D tensor of shape (Batch, Time, Bands) — one time series of spectral bands per pixel — plus a fixed day_offsets timeline (a Python list of day counts from the first observation) passed at construction time.
import numpy as np
import torch
# Example: 36 time steps, 16-day composites, starting at day 0
day_offsets = list(range(0, 36 * 16, 16))
# Your training tensor: (n_samples, n_times, n_bands)
X_train = torch.tensor(np.load("pixel_time_series.npy"), dtype=torch.float32)
y_train = torch.tensor(np.load("pixel_labels.npy"), dtype=torch.long)
If you're pulling data from a cdts STAC cube rather than pre-extracted .npy arrays, reduce the cube to a table of per-pixel time series (e.g. via .stack(pixel=("y", "x")) on an xarray.DataArray) and compute day_offsets from cube.time:
import xarray as xr
cube = xr.open_zarr("s3://my-bucket/sentinel2_cube.zarr")["reflectance"]
day_offsets = ((cube.time - cube.time[0]) / np.timedelta64(1, "D")).values.tolist()
5. Instantiating the Model
from cdts.ai import LightTAE
model = LightTAE(
n_bands=6, # number of spectral bands per observation
day_offsets=day_offsets,
n_labels=10, # number of land-cover classes
layers_spatial_encoder=(32, 64, 128), # spatial-encoder MLP widths
n_heads=16, # attention heads
n_neurons=(256, 128), # LTAE internal MLP widths (n_neurons[0] must equal d_model)
dropout_rate=0.2,
dim_input_decoder=128, # must match n_neurons[-1]
dim_layers_decoder=(64, 32),
)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)
Using the LTAE block on its own
If you want L-TAE's temporal fusion inside a custom architecture (e.g. after your own spatial feature extractor), instantiate it directly:
from cdts.ai import LTAE
temporal_fusion = LTAE(
in_channels=128, # dimensionality of your incoming per-timestep features
day_offsets=day_offsets,
n_heads=16,
n_neurons=(256, 128),
dropout_rate=0.2,
).to(device)
# x: (batch, seq_len, in_channels) -> (batch, n_neurons[-1])
fused = temporal_fusion(x)
6. Training Loop
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
from cdts.ai.losses import FocalLoss
train_loader = DataLoader(TensorDataset(X_train, y_train), batch_size=64, shuffle=True)
optimizer = optim.Adam(model.parameters(), lr=1e-3)
criterion = FocalLoss(alpha=0.25, gamma=2.0) # or torch.nn.CrossEntropyLoss() for balanced classes
num_epochs = 30
for epoch in range(num_epochs):
model.train()
epoch_loss = 0.0
for values, labels in train_loader:
values, labels = values.to(device), labels.to(device)
optimizer.zero_grad()
logits = model(values) # (batch, n_labels)
loss = criterion(logits, labels)
loss.backward()
optimizer.step()
epoch_loss += loss.item()
print(f"Epoch [{epoch + 1}/{num_epochs}], Loss: {epoch_loss / len(train_loader):.4f}")
7. Inference
model.eval()
with torch.no_grad():
new_series = torch.rand(1, len(day_offsets), 6).to(device) # (1, n_times, n_bands)
logits = model(new_series)
predicted_class = torch.argmax(logits, dim=1)
print(f"Predicted class: {predicted_class.item()}")
For inference over an entire spatial extent, extract each pixel's time series (reshaped to (N_pixels, n_times, n_bands)), run them through the model in batches, then reshape the predictions back to (H, W).
8. Validation Against sits
Both LTAE and LightTAE were validated end-to-end in-session against sits_lighttae(): a model trained in R was exported (state_dict()-compatible weight names, since the port is layer-for-layer), loaded into cdts.ai.LightTAE via load_state_dict(), and run on the same input data. Outputs matched sits's predictions within float32 numerical tolerance, confirming a faithful architectural port rather than just a similar-looking reimplementation.
References
- Garnot, V. S. F., Landrieu, L., Giordano, S., & Chehata, N. (2020). Satellite image time series classification with pixel-set encoders and temporal self-attention. In Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR) (pp. 12322–12331). https://doi.org/10.1109/CVPR42600.2020.01234
- Garnot, V. S. F., & Landrieu, L. (2020). Lightweight Temporal Self-Attention for Classifying Satellite Image Time Series. arXiv:2007.00586. https://arxiv.org/abs/2007.00586
- e-sensing/sits: https://github.com/e-sensing/sits