Skill Tracer
Skills › Scientific Agent Skills › geomaster › machine-learning.md

machine-learning.md

391 lines · skills/geomaster/references/machine-learning.md at 5f2bc03

Open on GitHub
1# Machine Learning for Geospatial Data
2
3Machine learning for remote sensing and spatial analysis, reviewed 2026-10-01.
4RF arithmetic/masking is tested with synthetic rasters. Neural network, PyG and SHAP
5examples are illustrative, source-reviewed templates; no training run or accuracy
6claim is made. Install their packages separately. Record feature units, masks, band
7order, normalization, labels and spatial/temporal split definitions.
8

Machine Learning for Geospatial Data

Machine learning for remote sensing and spatial analysis, reviewed 2026-10-01. RF arithmetic/masking is tested with synthetic rasters. Neural network, PyG and SHAP examples are illustrative, source-reviewed templates; no training run or accuracy claim is made. Install their packages separately. Record feature units, masks, band order, normalization, labels and spatial/temporal split definitions.

Raw lines 1 to 8
9## Traditional Machine Learning

Traditional Machine Learning

Raw lines 9 to 10
11### Random Forest for land cover
13Use the bundled `classify_imagery` helper (see the main skill) for a small masked
14raster fit/predict exercise. Rasterize labels in the raster CRS using its actual
15transform. Exclude nodata from both fitting and prediction, reserve an output nodata
16code and verify integer class range before casting.
18For assessment, split at independent spatial/temporal units **before** fitting:
20```python
21from sklearn.model_selection import GroupShuffleSplit
22from sklearn.ensemble import RandomForestClassifier
23from sklearn.metrics import classification_report
24import numpy as np
26# X, y and spatial_block_id have aligned rows, one row per valid labelled sample.
27train, validation = next(GroupShuffleSplit(n_splits=1, test_size=0.2,
28 random_state=42).split(X, y, groups=spatial_block_id))
29if set(np.unique(y[validation])) - set(np.unique(y[train])):
30 raise ValueError('Holdout contains classes absent from training')
31model = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=1)
32model.fit(X[train], y[train])
33print(classification_report(y[validation], model.predict(X[validation]), zero_division=0))
34```
36Random pixels from the same labelled polygon in both partitions leak spatial
37information. Block size and buffer separation depend on autocorrelation and the
38intended transfer region; grouping alone does not prove independence. Also report
39held-out sample/area support, imbalance and uncertainty. Feature importance is model
40association, not causal evidence.

Random Forest for land cover

Use the bundled classify_imagery helper (see the main skill) for a small masked raster fit/predict exercise. Rasterize labels in the raster CRS using its actual transform. Exclude nodata from both fitting and prediction, reserve an output nodata code and verify integer class range before casting.

For assessment, split at independent spatial/temporal units before fitting:

from sklearn.model_selection import GroupShuffleSplit
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report
import numpy as np

# X, y and spatial_block_id have aligned rows, one row per valid labelled sample.
train, validation = next(GroupShuffleSplit(n_splits=1, test_size=0.2,
    random_state=42).split(X, y, groups=spatial_block_id))
if set(np.unique(y[validation])) - set(np.unique(y[train])):
    raise ValueError('Holdout contains classes absent from training')
model = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=1)
model.fit(X[train], y[train])
print(classification_report(y[validation], model.predict(X[validation]), zero_division=0))

Random pixels from the same labelled polygon in both partitions leak spatial information. Block size and buffer separation depend on autocorrelation and the intended transfer region; grouping alone does not prove independence. Also report held-out sample/area support, imbalance and uncertainty. Feature importance is model association, not causal evidence.

Raw lines 11 to 41
42### Support Vector Machine
44```python
45from sklearn.svm import SVC
46from sklearn.preprocessing import StandardScaler
48def svm_classifier(X_train, y_train):
49 """SVM classifier for remote sensing."""
51 # Scale features
52 scaler = StandardScaler()
53 X_train_scaled = scaler.fit_transform(X_train)
55 # Train SVM
56 svm = SVC(
57 kernel='rbf',
58 C=100,
59 gamma='scale',
60 class_weight='balanced',
61 probability=True
62 )
63 svm.fit(X_train_scaled, y_train)
65 return svm, scaler
67# Multi-class classification
68def multiclass_svm(X_train, y_train):
69 from sklearn.multiclass import OneVsRestClassifier
71 scaler = StandardScaler()
72 X_train_scaled = scaler.fit_transform(X_train)
74 svm_ovr = OneVsRestClassifier(
75 SVC(kernel='rbf', C=10, probability=True),
76 n_jobs=-1
77 )
78 svm_ovr.fit(X_train_scaled, y_train)
80 return svm_ovr, scaler
81```

Support Vector Machine

from sklearn.svm import SVC
from sklearn.preprocessing import StandardScaler

def svm_classifier(X_train, y_train):
    """SVM classifier for remote sensing."""

    # Scale features
    scaler = StandardScaler()
    X_train_scaled = scaler.fit_transform(X_train)

    # Train SVM
    svm = SVC(
        kernel='rbf',
        C=100,
        gamma='scale',
        class_weight='balanced',
        probability=True
    )
    svm.fit(X_train_scaled, y_train)

    return svm, scaler

# Multi-class classification
def multiclass_svm(X_train, y_train):
    from sklearn.multiclass import OneVsRestClassifier

    scaler = StandardScaler()
    X_train_scaled = scaler.fit_transform(X_train)

    svm_ovr = OneVsRestClassifier(
        SVC(kernel='rbf', C=10, probability=True),
        n_jobs=-1
    )
    svm_ovr.fit(X_train_scaled, y_train)

    return svm_ovr, scaler
Raw lines 42 to 82
83## Deep Learning

Deep Learning

Raw lines 83 to 84
85### CNN tensor example (PyTorch)
87```python
88import torch
89import torch.nn as nn
90import torch.nn.functional as F
91from torch.utils.data import DataLoader
93# Define CNN
94class LandCoverCNN(nn.Module):
95 def __init__(self, in_channels=12, num_classes=10):
96 super().__init__()
97 self.encoder = nn.Sequential(
98 nn.Conv2d(in_channels, 64, 3, padding=1),
99 nn.BatchNorm2d(64),
100 nn.ReLU(),
101 nn.MaxPool2d(2),
103 nn.Conv2d(64, 128, 3, padding=1),
104 nn.BatchNorm2d(128),
105 nn.ReLU(),
106 nn.MaxPool2d(2),
108 nn.Conv2d(128, 256, 3, padding=1),
109 nn.BatchNorm2d(256),
110 nn.ReLU(),
111 nn.MaxPool2d(2),
112 )
114 self.decoder = nn.Sequential(
115 nn.ConvTranspose2d(256, 128, 2, stride=2),
116 nn.BatchNorm2d(128),
117 nn.ReLU(),
119 nn.ConvTranspose2d(128, 64, 2, stride=2),
120 nn.BatchNorm2d(64),
121 nn.ReLU(),
123 nn.ConvTranspose2d(64, num_classes, 2, stride=2),
124 )
126 def forward(self, x):
127 x = self.encoder(x)
128 x = self.decoder(x)
129 return x
131# Training
132def train_model(train_loader, val_loader, num_epochs=50):
133 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
134 model = LandCoverCNN().to(device)
136 criterion = nn.CrossEntropyLoss(ignore_index=-1) # explicit nodata label
137 optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
139 for epoch in range(num_epochs):
140 model.train()
141 train_loss = 0
143 for images, labels in train_loader:
144 images, labels = images.to(device), labels.to(device)
146 optimizer.zero_grad()
147 outputs = model(images)
148 loss = criterion(outputs, labels)
149 loss.backward()
150 optimizer.step()
152 train_loss += loss.item()
154 # Validation
155 model.eval()
156 val_loss = 0
157 with torch.no_grad():
158 for images, labels in val_loader:
159 images, labels = images.to(device), labels.to(device)
160 outputs = model(images)
161 loss = criterion(outputs, labels)
162 val_loss += loss.item()
164 print(f'Epoch {epoch+1}/{num_epochs}, Train Loss: {train_loss:.4f}, Val Loss: {val_loss:.4f}')
166 return model
167```

CNN tensor example (PyTorch)

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader

# Define CNN
class LandCoverCNN(nn.Module):
    def __init__(self, in_channels=12, num_classes=10):
        super().__init__()
        self.encoder = nn.Sequential(
            nn.Conv2d(in_channels, 64, 3, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(),
            nn.MaxPool2d(2),

            nn.Conv2d(64, 128, 3, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(),
            nn.MaxPool2d(2),

            nn.Conv2d(128, 256, 3, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(),
            nn.MaxPool2d(2),
        )

        self.decoder = nn.Sequential(
            nn.ConvTranspose2d(256, 128, 2, stride=2),
            nn.BatchNorm2d(128),
            nn.ReLU(),

            nn.ConvTranspose2d(128, 64, 2, stride=2),
            nn.BatchNorm2d(64),
            nn.ReLU(),

            nn.ConvTranspose2d(64, num_classes, 2, stride=2),
        )

    def forward(self, x):
        x = self.encoder(x)
        x = self.decoder(x)
        return x

# Training
def train_model(train_loader, val_loader, num_epochs=50):
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model = LandCoverCNN().to(device)

    criterion = nn.CrossEntropyLoss(ignore_index=-1)  # explicit nodata label
    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

    for epoch in range(num_epochs):
        model.train()
        train_loss = 0

        for images, labels in train_loader:
            images, labels = images.to(device), labels.to(device)

            optimizer.zero_grad()
            outputs = model(images)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()

            train_loss += loss.item()

        # Validation
        model.eval()
        val_loss = 0
        with torch.no_grad():
            for images, labels in val_loader:
                images, labels = images.to(device), labels.to(device)
                outputs = model(images)
                loss = criterion(outputs, labels)
                val_loss += loss.item()

        print(f'Epoch {epoch+1}/{num_epochs}, Train Loss: {train_loss:.4f}, Val Loss: {val_loss:.4f}')

    return model
Raw lines 85 to 168
169### U-Net for Semantic Segmentation
171Inputs are `(N,C,H,W)` float tensors; labels are `(N,H,W)` integer class IDs.
172This architecture requires H/W divisible by 16; pad/crop with a recorded policy.
173The simpler CNN above requires divisibility by 8. Neither model is pretrained.
175```python
176class UNet(nn.Module):
177 def __init__(self, in_channels=4, num_classes=5):
178 super().__init__()
180 # Encoder
181 self.enc1 = self.conv_block(in_channels, 64)
182 self.enc2 = self.conv_block(64, 128)
183 self.enc3 = self.conv_block(128, 256)
184 self.enc4 = self.conv_block(256, 512)
186 # Bottleneck
187 self.bottleneck = self.conv_block(512, 1024)
189 # Decoder
190 self.up1 = nn.ConvTranspose2d(1024, 512, 2, stride=2)
191 self.dec1 = self.conv_block(1024, 512)
193 self.up2 = nn.ConvTranspose2d(512, 256, 2, stride=2)
194 self.dec2 = self.conv_block(512, 256)
196 self.up3 = nn.ConvTranspose2d(256, 128, 2, stride=2)
197 self.dec3 = self.conv_block(256, 128)
199 self.up4 = nn.ConvTranspose2d(128, 64, 2, stride=2)
200 self.dec4 = self.conv_block(128, 64)
202 # Final layer
203 self.final = nn.Conv2d(64, num_classes, 1)
205 def conv_block(self, in_ch, out_ch):
206 return nn.Sequential(
207 nn.Conv2d(in_ch, out_ch, 3, padding=1),
208 nn.BatchNorm2d(out_ch),
209 nn.ReLU(inplace=True),
210 nn.Conv2d(out_ch, out_ch, 3, padding=1),
211 nn.BatchNorm2d(out_ch),
212 nn.ReLU(inplace=True)
213 )
215 def forward(self, x):
216 # Encoder
217 e1 = self.enc1(x)
218 e2 = self.enc2(F.max_pool2d(e1, 2))
219 e3 = self.enc3(F.max_pool2d(e2, 2))
220 e4 = self.enc4(F.max_pool2d(e3, 2))
222 # Bottleneck
223 b = self.bottleneck(F.max_pool2d(e4, 2))
225 # Decoder with skip connections
226 d1 = self.dec1(torch.cat([self.up1(b), e4], dim=1))
227 d2 = self.dec2(torch.cat([self.up2(d1), e3], dim=1))
228 d3 = self.dec3(torch.cat([self.up3(d2), e2], dim=1))
229 d4 = self.dec4(torch.cat([self.up4(d3), e1], dim=1))
231 return self.final(d4)
232```

U-Net for Semantic Segmentation

Inputs are (N,C,H,W) float tensors; labels are (N,H,W) integer class IDs. This architecture requires H/W divisible by 16; pad/crop with a recorded policy. The simpler CNN above requires divisibility by 8. Neither model is pretrained.

class UNet(nn.Module):
    def __init__(self, in_channels=4, num_classes=5):
        super().__init__()

        # Encoder
        self.enc1 = self.conv_block(in_channels, 64)
        self.enc2 = self.conv_block(64, 128)
        self.enc3 = self.conv_block(128, 256)
        self.enc4 = self.conv_block(256, 512)

        # Bottleneck
        self.bottleneck = self.conv_block(512, 1024)

        # Decoder
        self.up1 = nn.ConvTranspose2d(1024, 512, 2, stride=2)
        self.dec1 = self.conv_block(1024, 512)

        self.up2 = nn.ConvTranspose2d(512, 256, 2, stride=2)
        self.dec2 = self.conv_block(512, 256)

        self.up3 = nn.ConvTranspose2d(256, 128, 2, stride=2)
        self.dec3 = self.conv_block(256, 128)

        self.up4 = nn.ConvTranspose2d(128, 64, 2, stride=2)
        self.dec4 = self.conv_block(128, 64)

        # Final layer
        self.final = nn.Conv2d(64, num_classes, 1)

    def conv_block(self, in_ch, out_ch):
        return nn.Sequential(
            nn.Conv2d(in_ch, out_ch, 3, padding=1),
            nn.BatchNorm2d(out_ch),
            nn.ReLU(inplace=True),
            nn.Conv2d(out_ch, out_ch, 3, padding=1),
            nn.BatchNorm2d(out_ch),
            nn.ReLU(inplace=True)
        )

    def forward(self, x):
        # Encoder
        e1 = self.enc1(x)
        e2 = self.enc2(F.max_pool2d(e1, 2))
        e3 = self.enc3(F.max_pool2d(e2, 2))
        e4 = self.enc4(F.max_pool2d(e3, 2))

        # Bottleneck
        b = self.bottleneck(F.max_pool2d(e4, 2))

        # Decoder with skip connections
        d1 = self.dec1(torch.cat([self.up1(b), e4], dim=1))
        d2 = self.dec2(torch.cat([self.up2(d1), e3], dim=1))
        d3 = self.dec3(torch.cat([self.up3(d2), e2], dim=1))
        d4 = self.dec4(torch.cat([self.up4(d3), e1], dim=1))

        return self.final(d4)
Raw lines 169 to 233
234### Change Detection with Siamese Network
236```python
237class SiameseNetwork(nn.Module):
238 """Siamese network for change detection."""
240 def __init__(self):
241 super().__init__()
242 self.feature_extractor = nn.Sequential(
243 nn.Conv2d(3, 32, 3, padding=1),
244 nn.BatchNorm2d(32),
245 nn.ReLU(),
246 nn.MaxPool2d(2),
248 nn.Conv2d(32, 64, 3, padding=1),
249 nn.BatchNorm2d(64),
250 nn.ReLU(),
251 nn.MaxPool2d(2),
253 nn.Conv2d(64, 128, 3, padding=1),
254 nn.BatchNorm2d(128),
255 nn.ReLU(),
256 )
258 self.classifier = nn.Sequential(
259 nn.Conv2d(384, 128, 3, padding=1),
260 nn.ReLU(),
261 nn.Conv2d(128, 64, 3, padding=1),
262 nn.ReLU(),
263 nn.Conv2d(64, 2, 1), # Binary: change / no change
264 )
266 def forward(self, x1, x2):
267 f1 = self.feature_extractor(x1)
268 f2 = self.feature_extractor(x2)
270 # Concatenate features
271 diff = torch.abs(f1 - f2)
272 combined = torch.cat([f1, f2, diff], dim=1)
274 logits = self.classifier(combined)
275 return F.interpolate(logits, size=x1.shape[-2:], mode='bilinear', align_corners=False)
276```

Change Detection with Siamese Network

class SiameseNetwork(nn.Module):
    """Siamese network for change detection."""

    def __init__(self):
        super().__init__()
        self.feature_extractor = nn.Sequential(
            nn.Conv2d(3, 32, 3, padding=1),
            nn.BatchNorm2d(32),
            nn.ReLU(),
            nn.MaxPool2d(2),

            nn.Conv2d(32, 64, 3, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(),
            nn.MaxPool2d(2),

            nn.Conv2d(64, 128, 3, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(),
        )

        self.classifier = nn.Sequential(
            nn.Conv2d(384, 128, 3, padding=1),
            nn.ReLU(),
            nn.Conv2d(128, 64, 3, padding=1),
            nn.ReLU(),
            nn.Conv2d(64, 2, 1),  # Binary: change / no change
        )

    def forward(self, x1, x2):
        f1 = self.feature_extractor(x1)
        f2 = self.feature_extractor(x2)

        # Concatenate features
        diff = torch.abs(f1 - f2)
        combined = torch.cat([f1, f2, diff], dim=1)

        logits = self.classifier(combined)
        return F.interpolate(logits, size=x1.shape[-2:], mode='bilinear', align_corners=False)
Raw lines 234 to 277
278## Graph Neural Networks

Graph Neural Networks

Raw lines 278 to 279
280### PyTorch Geometric for Spatial Data
282```python
283import torch
284import numpy as np
285import torch.nn.functional as F
286from torch_geometric.data import Data
287from torch_geometric.nn import GCNConv
289# Create spatial graph
290def create_spatial_graph(points_gdf, feature_columns, k_neighbors=5):
291 """Create graph from point data using k-NN."""
293 from sklearn.neighbors import NearestNeighbors
295 if points_gdf.crs is None or not points_gdf.crs.is_projected:
296 raise ValueError('Use a justified metric CRS for Euclidean neighbours')
297 if not 1 <= k_neighbors < len(points_gdf):
298 raise ValueError('k must be between 1 and n-1')
299 coords = np.array([[p.x, p.y] for p in points_gdf.geometry])
301 # Find k-nearest neighbors
302 nbrs = NearestNeighbors(n_neighbors=k_neighbors + 1).fit(coords)
303 distances, indices = nbrs.kneighbors(coords)
305 # Create edge index
306 edge_index = []
307 for i, neighbors in enumerate(indices):
308 for j in neighbors:
309 if i != j:
310 edge_index.append([j, i]) # neighbour sends message to the query node
312 edge_index = torch.tensor(edge_index, dtype=torch.long).t().contiguous()
314 # Node features
315 features = points_gdf[feature_columns].to_numpy(dtype=float)
316 if not np.isfinite(features).all():
317 raise ValueError('Features must be finite numeric predictors; exclude target and IDs')
318 x = torch.tensor(features, dtype=torch.float)
320 return Data(x=x, edge_index=edge_index)
322# GCN for spatial prediction
323class SpatialGCN(torch.nn.Module):
324 def __init__(self, num_features, hidden_channels=64):
325 super().__init__()
326 self.conv1 = GCNConv(num_features, hidden_channels)
327 self.conv2 = GCNConv(hidden_channels, hidden_channels)
328 self.conv3 = GCNConv(hidden_channels, 1)
330 def forward(self, data):
331 x, edge_index = data.x, data.edge_index
332 x = self.conv1(x, edge_index).relu()
333 x = F.dropout(x, p=0.5, training=self.training)
334 x = self.conv2(x, edge_index).relu()
335 x = self.conv3(x, edge_index)
336 return x
337```

PyTorch Geometric for Spatial Data

import torch
import numpy as np
import torch.nn.functional as F
from torch_geometric.data import Data
from torch_geometric.nn import GCNConv

# Create spatial graph
def create_spatial_graph(points_gdf, feature_columns, k_neighbors=5):
    """Create graph from point data using k-NN."""

    from sklearn.neighbors import NearestNeighbors

    if points_gdf.crs is None or not points_gdf.crs.is_projected:
        raise ValueError('Use a justified metric CRS for Euclidean neighbours')
    if not 1 <= k_neighbors < len(points_gdf):
        raise ValueError('k must be between 1 and n-1')
    coords = np.array([[p.x, p.y] for p in points_gdf.geometry])

    # Find k-nearest neighbors
    nbrs = NearestNeighbors(n_neighbors=k_neighbors + 1).fit(coords)
    distances, indices = nbrs.kneighbors(coords)

    # Create edge index
    edge_index = []
    for i, neighbors in enumerate(indices):
        for j in neighbors:
            if i != j:
                edge_index.append([j, i])  # neighbour sends message to the query node

    edge_index = torch.tensor(edge_index, dtype=torch.long).t().contiguous()

    # Node features
    features = points_gdf[feature_columns].to_numpy(dtype=float)
    if not np.isfinite(features).all():
        raise ValueError('Features must be finite numeric predictors; exclude target and IDs')
    x = torch.tensor(features, dtype=torch.float)

    return Data(x=x, edge_index=edge_index)

# GCN for spatial prediction
class SpatialGCN(torch.nn.Module):
    def __init__(self, num_features, hidden_channels=64):
        super().__init__()
        self.conv1 = GCNConv(num_features, hidden_channels)
        self.conv2 = GCNConv(hidden_channels, hidden_channels)
        self.conv3 = GCNConv(hidden_channels, 1)

    def forward(self, data):
        x, edge_index = data.x, data.edge_index
        x = self.conv1(x, edge_index).relu()
        x = F.dropout(x, p=0.5, training=self.training)
        x = self.conv2(x, edge_index).relu()
        x = self.conv3(x, edge_index)
        return x
Raw lines 280 to 338
339## Explainable AI (XAI) for Geospatial

Explainable AI (XAI) for Geospatial

Raw lines 339 to 340
341### SHAP for Model Interpretation
343```python
344import shap
345import numpy as np
347def explain_model(model, X, feature_names):
348 """Explain model predictions using SHAP."""
350 # Create explainer
351 explainer = shap.Explainer(model, X)
353 # Calculate SHAP values
354 shap_values = explainer(X)
356 # For multiclass outputs, select one class Explanation first.
357 if shap_values.values.ndim != 2:
358 raise ValueError('Select the intended model output/class before plotting')
359 shap.plots.beeswarm(shap_values)
360 for i in range(X.shape[1]):
361 shap.plots.scatter(shap_values[:, i])
363 return shap_values
365```
367Averaging SHAP values over neighbours is a descriptive spatial summary, not a
368correction for autocorrelation or causal explanation. Define a compatible background
369sample, model output scale and held-out interpretation set. Explanations can shift
370when correlated features or background support change.

SHAP for Model Interpretation

import shap
import numpy as np

def explain_model(model, X, feature_names):
    """Explain model predictions using SHAP."""

    # Create explainer
    explainer = shap.Explainer(model, X)

    # Calculate SHAP values
    shap_values = explainer(X)

    # For multiclass outputs, select one class Explanation first.
    if shap_values.values.ndim != 2:
        raise ValueError('Select the intended model output/class before plotting')
    shap.plots.beeswarm(shap_values)
    for i in range(X.shape[1]):
        shap.plots.scatter(shap_values[:, i])

    return shap_values

Averaging SHAP values over neighbours is a descriptive spatial summary, not a correction for autocorrelation or causal explanation. Define a compatible background sample, model output scale and held-out interpretation set. Explanations can shift when correlated features or background support change.

Raw lines 341 to 371
372### Attention Maps for CNNs
374Grad-CAM requires activations and gradients for a chosen layer and a defined scalar
375target. Generic `nn.Module` has no `get_gradient` or `get_activation` methods. Register
376forward/backward hooks (and remove them), retain the intended target gradient, pool
377spatial gradients and combine with the captured activation. For segmentation choose
378class and pixel/region explicitly; `argmax` over every output axis is not a class index.
379Check shape, zero normalization range and detach before converting to NumPy.
381TorchGeo adds geospatial datasets/samplers, not an automatic scientific training
382pipeline. Follow its versioned dataset documentation, labels and preprocessing; keep
383region/scene separation when defining samplers. No model weights were downloaded.
385Sources: [GroupShuffleSplit](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.GroupShuffleSplit.html),
386[SHAP plots](https://shap.readthedocs.io/en/latest/generated/shap.plots.beeswarm.html),
387[PyTorch Conv2d](https://docs.pytorch.org/docs/stable/generated/torch.nn.Conv2d.html),
388[TorchGeo docs](https://torchgeo.readthedocs.io/en/stable/),
389[PyG GCNConv](https://pytorch-geometric.readthedocs.io/en/latest/generated/torch_geometric.nn.conv.GCNConv.html).
391For more ML examples, see [code-examples.md](code-examples.md).

Attention Maps for CNNs

Grad-CAM requires activations and gradients for a chosen layer and a defined scalar target. Generic nn.Module has no get_gradient or get_activation methods. Register forward/backward hooks (and remove them), retain the intended target gradient, pool spatial gradients and combine with the captured activation. For segmentation choose class and pixel/region explicitly; argmax over every output axis is not a class index. Check shape, zero normalization range and detach before converting to NumPy.

TorchGeo adds geospatial datasets/samplers, not an automatic scientific training pipeline. Follow its versioned dataset documentation, labels and preprocessing; keep region/scene separation when defining samplers. No model weights were downloaded.

Sources: GroupShuffleSplit, SHAP plots, PyTorch Conv2d, TorchGeo docs, PyG GCNConv.

For more ML examples, see code-examples.md.

Raw lines 372 to 391