notRaphael commited on
Commit
a1c4356
·
verified ·
1 Parent(s): bb14e48

Upload kaggle_inference_notebook.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. kaggle_inference_notebook.py +391 -0
kaggle_inference_notebook.py ADDED
@@ -0,0 +1,391 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ CSIRO Image2Biomass Prediction - Kaggle Inference Notebook
4
+ ============================================================
5
+ This notebook loads trained models and generates submission.csv.
6
+
7
+ Requirements:
8
+ - Trained model weights saved as a Kaggle dataset
9
+ - No internet access (all models pre-downloaded)
10
+
11
+ Expected model dataset structure:
12
+ /kaggle/input/biomass-models/
13
+ fold_0/best_model.pth
14
+ fold_1/best_model.pth
15
+ ...
16
+ training_info.json
17
+ """
18
+
19
+ import os
20
+ import sys
21
+ import json
22
+ import time
23
+ import warnings
24
+ from pathlib import Path
25
+ from typing import Dict, List, Optional, Tuple
26
+
27
+ import numpy as np
28
+ import pandas as pd
29
+ import torch
30
+ import torch.nn as nn
31
+ import torch.nn.functional as F
32
+ from torch.utils.data import Dataset, DataLoader
33
+ from torch.cuda.amp import autocast
34
+ from PIL import Image
35
+
36
+ warnings.filterwarnings('ignore')
37
+
38
+ os.system('pip install -q timm albumentations')
39
+ import timm
40
+ import albumentations as A
41
+ from albumentations.pytorch import ToTensorV2
42
+
43
+ # ============================================================
44
+ # Configuration
45
+ # ============================================================
46
+ class CFG:
47
+ COMPETITION = 'csiro-biomass'
48
+ DATA_DIR = Path(f'/kaggle/input/{COMPETITION}')
49
+ MODEL_DIR = Path('/kaggle/input/biomass-models') # Your uploaded model weights
50
+ OUTPUT_DIR = Path('/kaggle/working')
51
+
52
+ BATCH_SIZE = 32
53
+ NUM_WORKERS = 2
54
+ N_TTA = 4 # Number of TTA augmentations
55
+ DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
56
+
57
+ TARGET_COLS = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']
58
+ IMAGENET_MEAN = (0.485, 0.456, 0.406)
59
+ IMAGENET_STD = (0.229, 0.224, 0.225)
60
+
61
+ BACKBONE_CONFIGS = {
62
+ 'dinov2_small': {'name': 'vit_small_patch14_dinov2.lvd142m', 'feat_dim': 384},
63
+ 'dinov2_base': {'name': 'vit_base_patch14_dinov2.lvd142m', 'feat_dim': 768},
64
+ 'dinov2_large': {'name': 'vit_large_patch14_dinov2.lvd142m', 'feat_dim': 1024},
65
+ 'dinov2_base_reg': {'name': 'vit_base_patch14_reg4_dinov2.lvd142m', 'feat_dim': 768},
66
+ 'convnext_large': {'name': 'convnext_large.fb_in22k_ft_in1k', 'feat_dim': 1536},
67
+ 'convnextv2_large': {'name': 'convnextv2_large.fcmae_ft_in22k_in1k', 'feat_dim': 1536},
68
+ 'efficientnet_b4': {'name': 'efficientnet_b4.ra2_in1k', 'feat_dim': 1792},
69
+ 'swin_large': {'name': 'swin_large_patch4_window7_224.ms_in22k_ft_in1k', 'feat_dim': 1536},
70
+ 'eva02_large': {'name': 'eva02_large_patch14_448.mim_m38m_ft_in22k_in1k', 'feat_dim': 1024},
71
+ }
72
+
73
+
74
+ # ============================================================
75
+ # Model Definition (must match training)
76
+ # ============================================================
77
+ class BiomassModel(nn.Module):
78
+ def __init__(self, backbone_name, num_targets=5, hidden_dim=512,
79
+ dropout=0.3, pretrained=False, img_size=224,
80
+ use_ndvi=False, separate_heads=False):
81
+ super().__init__()
82
+ self.use_ndvi = use_ndvi
83
+ self.separate_heads = separate_heads
84
+
85
+ kwargs = {'pretrained': pretrained, 'num_classes': 0}
86
+ if 'vit' in backbone_name or 'dinov2' in backbone_name:
87
+ kwargs['img_size'] = img_size
88
+
89
+ self.backbone = timm.create_model(backbone_name, **kwargs)
90
+ feat_dim = self.backbone.num_features
91
+
92
+ if use_ndvi:
93
+ self.ndvi_embed = nn.Sequential(nn.Linear(1, 32), nn.GELU(), nn.Linear(32, 64))
94
+ feat_dim += 64
95
+
96
+ if separate_heads:
97
+ self.heads = nn.ModuleList([
98
+ nn.Sequential(
99
+ nn.LayerNorm(feat_dim), nn.Dropout(dropout),
100
+ nn.Linear(feat_dim, hidden_dim), nn.GELU(),
101
+ nn.Dropout(dropout * 0.5), nn.Linear(hidden_dim, 1),
102
+ ) for _ in range(num_targets)
103
+ ])
104
+ else:
105
+ self.head = nn.Sequential(
106
+ nn.LayerNorm(feat_dim), nn.Dropout(dropout),
107
+ nn.Linear(feat_dim, hidden_dim), nn.GELU(),
108
+ nn.Dropout(dropout * 0.5),
109
+ nn.Linear(hidden_dim, hidden_dim // 2), nn.GELU(),
110
+ nn.Dropout(dropout * 0.3),
111
+ nn.Linear(hidden_dim // 2, num_targets),
112
+ )
113
+
114
+ def forward(self, x, ndvi=None):
115
+ features = self.backbone(x)
116
+ if self.use_ndvi and ndvi is not None:
117
+ features = torch.cat([features, self.ndvi_embed(ndvi.unsqueeze(-1))], dim=-1)
118
+ if self.separate_heads:
119
+ return torch.cat([h(features) for h in self.heads], dim=-1)
120
+ return self.head(features)
121
+
122
+
123
+ # ============================================================
124
+ # Dataset
125
+ # ============================================================
126
+ class TestDataset(Dataset):
127
+ def __init__(self, image_dir, df, transform, use_ndvi=False):
128
+ self.image_dir = Path(image_dir)
129
+ self.df = df.reset_index(drop=True)
130
+ self.transform = transform
131
+ self.use_ndvi = use_ndvi
132
+
133
+ def __len__(self):
134
+ return len(self.df)
135
+
136
+ def __getitem__(self, idx):
137
+ row = self.df.iloc[idx]
138
+ img_id = row['image_id'] if 'image_id' in row.index else row.name
139
+
140
+ img_path = None
141
+ for ext in ['.jpg', '.jpeg', '.png', '.JPG']:
142
+ p = self.image_dir / f"{img_id}{ext}"
143
+ if p.exists():
144
+ img_path = p
145
+ break
146
+ if img_path is None:
147
+ candidates = list(self.image_dir.glob(f"{img_id}*"))
148
+ img_path = candidates[0] if candidates else self.image_dir / f"{img_id}.jpg"
149
+
150
+ img = np.array(Image.open(img_path).convert('RGB'))
151
+ img_tensor = self.transform(image=img)['image']
152
+
153
+ result = {'image': img_tensor, 'image_id': str(img_id)}
154
+ if self.use_ndvi and 'NDVI' in self.df.columns:
155
+ result['ndvi'] = torch.tensor(float(row['NDVI']), dtype=torch.float32)
156
+ return result
157
+
158
+
159
+ # ============================================================
160
+ # TTA Transforms
161
+ # ============================================================
162
+ def get_tta_transforms(img_size=224, n_tta=4):
163
+ tfms = []
164
+
165
+ # 0: Standard center crop
166
+ tfms.append(A.Compose([
167
+ A.Resize(height=int(img_size * 1.14), width=int(img_size * 1.14)),
168
+ A.CenterCrop(height=img_size, width=img_size),
169
+ A.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),
170
+ ToTensorV2(),
171
+ ]))
172
+
173
+ # 1: HFlip
174
+ tfms.append(A.Compose([
175
+ A.Resize(height=int(img_size * 1.14), width=int(img_size * 1.14)),
176
+ A.CenterCrop(height=img_size, width=img_size),
177
+ A.HorizontalFlip(p=1.0),
178
+ A.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),
179
+ ToTensorV2(),
180
+ ]))
181
+
182
+ # 2: VFlip
183
+ tfms.append(A.Compose([
184
+ A.Resize(height=int(img_size * 1.14), width=int(img_size * 1.14)),
185
+ A.CenterCrop(height=img_size, width=img_size),
186
+ A.VerticalFlip(p=1.0),
187
+ A.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),
188
+ ToTensorV2(),
189
+ ]))
190
+
191
+ # 3: Both flips
192
+ tfms.append(A.Compose([
193
+ A.Resize(height=int(img_size * 1.14), width=int(img_size * 1.14)),
194
+ A.CenterCrop(height=img_size, width=img_size),
195
+ A.HorizontalFlip(p=1.0),
196
+ A.VerticalFlip(p=1.0),
197
+ A.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),
198
+ ToTensorV2(),
199
+ ]))
200
+
201
+ return tfms[:n_tta]
202
+
203
+
204
+ # ============================================================
205
+ # Inference Functions
206
+ # ============================================================
207
+ def load_model(ckpt_path, device):
208
+ ckpt = torch.load(ckpt_path, map_location=device, weights_only=False)
209
+ args = ckpt.get('args', {})
210
+
211
+ # Resolve backbone name
212
+ backbone_key = args.get('backbone', 'vit_base_patch14_dinov2.lvd142m')
213
+ if backbone_key in BACKBONE_CONFIGS:
214
+ backbone_name = BACKBONE_CONFIGS[backbone_key]['name']
215
+ else:
216
+ backbone_name = backbone_key
217
+
218
+ img_size = args.get('img_size', 224)
219
+
220
+ model = BiomassModel(
221
+ backbone_name=backbone_name,
222
+ num_targets=5,
223
+ hidden_dim=args.get('hidden_dim', 512),
224
+ dropout=args.get('dropout', 0.3),
225
+ pretrained=False,
226
+ img_size=img_size,
227
+ use_ndvi=args.get('use_ndvi', False),
228
+ separate_heads=args.get('separate_heads', False),
229
+ )
230
+
231
+ model.load_state_dict(ckpt['model_state_dict'])
232
+ model = model.to(device).eval()
233
+
234
+ return model, args
235
+
236
+
237
+ @torch.no_grad()
238
+ def predict(model, loader, device, log_transform=True):
239
+ model.eval()
240
+ preds_list, ids_list = [], []
241
+
242
+ for batch in loader:
243
+ images = batch['image'].to(device)
244
+ ndvi = batch.get('ndvi', None)
245
+ if ndvi is not None:
246
+ ndvi = ndvi.to(device)
247
+
248
+ with autocast(dtype=torch.float16):
249
+ preds = model(images, ndvi)
250
+
251
+ preds_list.append(preds.cpu().numpy())
252
+ ids_list.extend(batch['image_id'])
253
+
254
+ preds = np.concatenate(preds_list)
255
+ if log_transform:
256
+ preds = np.expm1(preds)
257
+ return preds, ids_list
258
+
259
+
260
+ def predict_tta(model, test_df, img_dir, device, img_size, log_transform,
261
+ use_ndvi, batch_size, num_workers, n_tta):
262
+ tta_tfms = get_tta_transforms(img_size, n_tta)
263
+ all_preds = []
264
+ ids = None
265
+
266
+ for i, tfm in enumerate(tta_tfms):
267
+ ds = TestDataset(img_dir, test_df, tfm, use_ndvi)
268
+ loader = DataLoader(ds, batch_size=batch_size, shuffle=False,
269
+ num_workers=num_workers, pin_memory=True)
270
+ p, image_ids = predict(model, loader, device, log_transform)
271
+ all_preds.append(p)
272
+ if ids is None:
273
+ ids = image_ids
274
+
275
+ return np.mean(all_preds, axis=0), ids
276
+
277
+
278
+ # ============================================================
279
+ # Main Inference
280
+ # ============================================================
281
+ device = torch.device(CFG.DEVICE)
282
+ print(f"Device: {device}")
283
+
284
+ # Find data
285
+ for alt in ['/kaggle/input/csiro-biomass', '/kaggle/input/csiro-image2biomass-prediction',
286
+ '/kaggle/input/csiro-image2biomass']:
287
+ if Path(alt).exists():
288
+ CFG.DATA_DIR = Path(alt)
289
+ break
290
+
291
+ # Find model weights
292
+ for alt in ['/kaggle/input/biomass-models', '/kaggle/input/biomass-weights',
293
+ '/kaggle/working']:
294
+ if Path(alt).exists() and list(Path(alt).glob('fold_*')):
295
+ CFG.MODEL_DIR = Path(alt)
296
+ break
297
+
298
+ print(f"Data: {CFG.DATA_DIR}")
299
+ print(f"Models: {CFG.MODEL_DIR}")
300
+
301
+ # Load test data
302
+ test_csv = None
303
+ for fname in ['test.csv', 'Test.csv']:
304
+ if (CFG.DATA_DIR / fname).exists():
305
+ test_csv = CFG.DATA_DIR / fname
306
+ break
307
+
308
+ test_df = pd.read_csv(test_csv)
309
+ print(f"Test samples: {len(test_df)}")
310
+
311
+ # Find test images
312
+ test_img_dir = None
313
+ for d in ['test_images', 'test', 'images/test']:
314
+ if (CFG.DATA_DIR / d).exists():
315
+ test_img_dir = CFG.DATA_DIR / d
316
+ break
317
+
318
+ print(f"Test images: {test_img_dir}")
319
+
320
+ # Find fold models
321
+ fold_dirs = sorted(CFG.MODEL_DIR.glob('fold_*'))
322
+ print(f"Found {len(fold_dirs)} fold models")
323
+
324
+ # Ensemble prediction
325
+ all_fold_preds = []
326
+ image_ids = None
327
+
328
+ for fold_dir in fold_dirs:
329
+ ckpt_path = fold_dir / 'best_model.pth'
330
+ if not ckpt_path.exists():
331
+ continue
332
+
333
+ print(f"\nLoading {ckpt_path}...")
334
+ model, args = load_model(str(ckpt_path), device)
335
+
336
+ img_size = args.get('img_size', 224)
337
+ log_transform = args.get('log_transform', True)
338
+ use_ndvi = args.get('use_ndvi', False)
339
+
340
+ preds, ids = predict_tta(
341
+ model, test_df, str(test_img_dir), device,
342
+ img_size=img_size,
343
+ log_transform=log_transform,
344
+ use_ndvi=use_ndvi,
345
+ batch_size=CFG.BATCH_SIZE,
346
+ num_workers=CFG.NUM_WORKERS,
347
+ n_tta=CFG.N_TTA,
348
+ )
349
+
350
+ all_fold_preds.append(preds)
351
+ if image_ids is None:
352
+ image_ids = ids
353
+
354
+ print(f" Mean predictions: {preds.mean(axis=0)}")
355
+
356
+ del model
357
+ torch.cuda.empty_cache()
358
+
359
+ # Average across folds
360
+ ensemble_preds = np.mean(all_fold_preds, axis=0)
361
+ ensemble_preds = np.clip(ensemble_preds, 0, None)
362
+
363
+ # Post-process: ensure total >= component sum
364
+ comp_sum = ensemble_preds[:, 0] + ensemble_preds[:, 1] + ensemble_preds[:, 2]
365
+ mask = ensemble_preds[:, 4] < comp_sum
366
+ ensemble_preds[mask, 4] = comp_sum[mask]
367
+
368
+ print(f"\nEnsemble predictions summary:")
369
+ for i, name in enumerate(TARGET_COLS):
370
+ col = ensemble_preds[:, i]
371
+ print(f" {name}: mean={col.mean():.2f}, std={col.std():.2f}, "
372
+ f"min={col.min():.2f}, max={col.max():.2f}")
373
+
374
+ # Create submission
375
+ rows = []
376
+ for i, img_id in enumerate(image_ids):
377
+ for j, target_name in enumerate(TARGET_COLS):
378
+ rows.append({
379
+ 'sample_id': f"{img_id}__{target_name}",
380
+ 'target': float(max(0, ensemble_preds[i, j])),
381
+ })
382
+
383
+ submission = pd.DataFrame(rows)
384
+ submission.to_csv('submission.csv', index=False)
385
+ print(f"\nSubmission saved: submission.csv ({len(submission)} rows)")
386
+ print(submission.head(10))
387
+
388
+ # Verify format
389
+ assert submission.columns.tolist() == ['sample_id', 'target']
390
+ assert len(submission) == len(test_df) * 5
391
+ print("\n✅ Submission format verified!")