Multi-temporal Sentinel-2 crop classification and rice phenology staging. Predicts crop type (rice / corn / soybean / background) and phenological stage (Greenup → Dormancy) for every input point × date combination.
PhenoSync-S2/
├── features.py # Spectral indices, temporal stats, phase-gated features
├── model.py # BiLSTM + multi-head attention architecture
├── train.py # Training script (XGBoost + LSTM modes)
├── inference.py # Inference + result.json export
├── run.sh # Container entry point (platform use only)
├── Dockerfile # Docker image — bundles code + trained weights
├── requirements.txt # Python dependencies
├── setup_data.py # Zip extraction helper (if data arrives zipped)
│
├── DATA/ # Place training data here before training
│ ├── points_train_label.csv
│ ├── region_train_1/
│ ├── region_train_2/
│ ├── region_train_3/
│ └── region_train_4/
│
├── models/ # Trained weights saved here (commit before pushing)
│ ├── lstm_best.pt
│ ├── lstm_final.pt
│ ├── lstm_config.pkl
│ ├── xgb_crop.pkl
│ ├── xgb_pheno.pkl
│ ├── xgb_remapper.pkl
│ └── label_encoders.pkl
│
└── test_input_sample/ # Local test data (mirrors /input on platform)
├── test_point.csv
├── test_data_label_sample.csv
└── region_test/
Python 3.10+. CUDA GPU strongly recommended for LSTM training.
pip install -r requirements.txtOr manually:
pip install numpy pandas scikit-learn torch xgboost joblib rasterioFor CUDA support, install PyTorch with the appropriate CUDA version from https://pytorch.org/get-started/locally/
Place extracted training data under DATA/:
DATA/
├── points_train_label.csv # 5,446 labeled rows
│ # Columns: point_id, Longitude, Latitude,
│ # phenophase_date, crop_type, phenophase_name
│ # Crop distribution: rice=2569 corn=1603 soybean=1274
│
├── region_train_1/ # Flat directory of TIFF files
│ ├── region00_2018-06-07-00-00_..._Sentinel-2_L2A_B01_(Raw).tiff
│ └── ... (12 bands × all dates × all sub-regions, ~2500 files per dir)
├── region_train_2/
├── region_train_3/
└── region_train_4/
TIFF filename format (parsed automatically):
regionXX_YYYY-MM-DD-00-00_YYYY-MM-DD-23-59_Sentinel-2_L2A_BXX_(Raw).tiff
If data arrived as
.zipfiles, runpython setup_data.pyto extract everything and print ready-to-run training commands.
Training is a two-stage pipeline. Run XGBoost first (fast, validates data pipeline), then LSTM (higher accuracy, GPU recommended).
XGBoost operates on 156-dimensional flat feature vectors (temporal statistics across all timesteps). Good baseline, fast iteration.
python train.py --mode xgboost `
--label_csv DATA\points_train_label.csv `
--tiff_dirs DATA\region_train_1 DATA\region_train_2 DATA\region_train_3 DATA\region_train_4 `
--output_dir modelsExpected terminal output:
[index] 7698 TIFFs | 3 regions | N dates
[features] 5446 samples | XXXX in-bounds
[features] flat shape=(5446, 156) | max_seq_len=N | oob=X
[xgb] Train acc — crop=0.9XX pheno=0.9XX
[log] Run appended → models/training_log.csv
BiLSTM processes full temporal sequences (T × 24-dim feature vectors). Learns phenological trajectories that flat statistics miss.
python train.py --mode lstm `
--label_csv DATA\points_train_label.csv `
--tiff_dirs DATA\region_train_1 DATA\region_train_2 DATA\region_train_3 DATA\region_train_4 `
--output_dir models `
--epochs 80 `
--batch_size 64 `
--hidden_dim 128Expected output per epoch:
[lstm] Epoch 001/080 | train_loss=1.2341 | val_loss=1.1890 | crop_acc=0.612 | val_crop=0.634 | val_pheno=0.521 | trm=0.02
[lstm] Epoch 010/080 | train_loss=0.8123 | val_loss=0.7654 | crop_acc=0.781 | val_crop=0.803 | val_pheno=0.742 | trm=0.20
...
[lstm] Best val loss=0.4123 | Models saved to models
Early stopping triggers after 15 epochs without validation loss improvement.
| Argument | Default | Description |
|---|---|---|
--mode |
required | xgboost or lstm |
--label_csv |
required | Path to labeled training CSV |
--tiff_dirs |
required | One or more TIFF directories (space-separated) |
--output_dir |
models |
Where to save model weights |
--epochs |
50 |
LSTM training epochs |
--batch_size |
64 |
LSTM batch size (reduce to 32/16 if OOM) |
--hidden_dim |
128 |
LSTM hidden units per direction (BiLSTM = ×2) |
--lr |
1e-3 |
AdamW learning rate |
--weight_crop |
0.4 |
Crop loss weight (phenology gets remaining 0.6) |
| File | Description |
|---|---|
lstm_best.pt |
Best LSTM weights by validation loss |
lstm_final.pt |
Final epoch weights |
lstm_config.pkl |
Architecture config — required for inference |
xgb_crop.pkl |
XGBoost crop classifier |
xgb_pheno.pkl |
XGBoost phenology classifier |
xgb_remapper.pkl |
Label index remapper |
label_encoders.pkl |
Class name encoders — required for inference |
training_log.csv |
Accuracy history across all runs |
python inference.py `
--input_csv test_input_sample\test_point.csv `
--tiff_dir test_input_sample\region_test `
--output_dir output `
--model_dir modelsRequires a label CSV with ground truth. Prints Crop MacroF1, Rice Phenology MacroF1, and AlgoScore locally so you can verify quality before submitting.
python inference.py `
--input_csv test_input_sample\test_point.csv `
--tiff_dir test_input_sample\region_test `
--output_dir output `
--model_dir models `
--label_csv DATA\points_train_label.csvOutput:
[score] Crop MacroF1=0.XXXX (n=XXX)
[score] Rice Phenology MacroF1=0.XXXX (n=XXX)
[score] AlgoScore=XX.XX
{
"124.703696_48.543523_2018/9/1": ["rice", "Maturity"],
"125.331726_48.768485_2018/6/21": ["corn", "Greenup"],
"125.331726_48.768485_2018/7/31": ["soybean", "Peak"],
...
}Key format: {Longitude}_{Latitude}_{phenophase_date}
Critical: The date string must be preserved exactly as it appears in the input CSV (e.g. 2018/9/1 — not 2018-09-01). Every row in the input CSV must have a corresponding key. Missing keys are penalized.
12 Sentinel-2 raw bands: B01 B02 B03 B04 B05 B06 B07 B08 B8A B09 B11 B12
10 spectral indices:
| Index | Formula | Why it matters |
|---|---|---|
| NDVI | (B08−B04)/(B08+B04) | Vegetation density, primary growth signal |
| LSWI | (B08−B11)/(B08+B11) | Leaf water content, flooding detection |
| NDWI | (B03−B08)/(B03+B08) | Open water surface |
| EVI | 2.5×(B08−B04)/(B08+6B04−7.5B02+1) | Canopy LAI, less saturated than NDVI |
| RGVI | (B03−B04)/(B03+B04) | Early-season rice growth signal |
| NDRE | (B8A−B05)/(B8A+B05) | Nitrogen status — strong corn/soy separator |
| CIre | B07/B05 − 1 | Chlorophyll red-edge — C4 corn vs. soybean |
| CWC | (B8A−B12)/(B8A+B12) | Canopy water via SWIR-2 — key soybean indicator |
| NBR | (B08−B12)/(B08+B12) | Water stress, SWIR-2 based |
| LSWI−EVI | LSWI − EVI | >0 signals flooded/transplanting (rice transplant indicator) |
2 positional encodings: sin(2π·doy/365), cos(2π·doy/365) — encodes calendar position of each timestep
- 6 temporal statistics × 24 features = 144 dims:
mean, std, max, min, median, temporal diff_std - Phenological timing: DOY of NDVI peak, DOY of greenup, NDVI amplitude, CWC amplitude, flooding fraction
- Phase-gated means: NDVI/LSWI/EVI during transplanting phase; NDVI/EVI/CWC during growing phase
- 1 observation DOY (query date)
Input (B, T, 24)
│
BiLSTM — 128 hidden × 2 directions → (B, T, 256)
│
MultiHeadAttentionPool — 2 heads → (B, 256)
│
DOY sinusoidal encoding — 4-dim [sin, cos, sin2x, cos2x] → concat → (B, 260)
│
├── Crop Head: Linear(260→128) → LayerNorm → ReLU → Linear(128→64) → Linear(64→4)
└── Pheno Head: Linear(260→128) → LayerNorm → ReLU → Linear(128→64) → Linear(64→7)
Outputs: crop logits (4) + phenology logits (7)
The attention pooling mechanism allows the model to selectively weight timesteps — e.g., it learns to upweight transplanting-phase observations for rice phenology, and NDRE-peak windows for corn.
| Technique | Detail | Purpose |
|---|---|---|
| Temporal Random Masking | 0→20% curriculum over 10 epochs | Cloud-gap robustness — simulates missing observations |
| Stratified 80/20 val split | Stratified by crop class | Honest generalization estimate across all classes |
| Early stopping | Patience=15 on val loss | Prevent overfitting |
| Class-weighted CrossEntropyLoss | sklearn compute_class_weight |
Balances minority class (soybean) learning |
| WeightedRandomSampler | Per-sample weight from class frequency | Balanced mini-batches during training |
| CosineAnnealingLR | T_max = total epochs | Smooth LR decay toward zero |
| AdamW optimizer | Default lr=1e-3, weight_decay | Better generalization than Adam |
Crop classes (4): rice corn soybean background
Phenology stages (7):
| Stage | Description | Typical NDVI range |
|---|---|---|
| Greenup | Vegetation emergence, NDVI rising from baseline | 0.2–0.4 |
| MidGreenup | Rapid canopy development | 0.4–0.6 |
| Peak | Maximum NDVI / canopy closure | 0.7–0.9 |
| Maturity | Canopy stable, grain-filling underway | 0.6–0.8 |
| Senescence | NDVI declining, crop aging | 0.4–0.6 |
| MidSenescence | Continued NDVI decline | 0.2–0.4 |
| Dormancy | Post-harvest / bare soil | <0.2 |
Important: Rice phenology is only scored when the crop prediction is also correct. Improving crop F1 — especially rice recall — directly improves the phenology component of AlgoScore.