gabrielkasmi commited on
Commit
a2aee5d
Β·
verified Β·
1 Parent(s): cdc4997

Upload 9 files

Browse files

add source training code. Weights landing in `weights` are in the root of the folder.

train/dataset.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ dataset.py β€” PyTorch datasets wrapping the BDAPPV HuggingFace dataset.
3
+
4
+ Two dataset classes:
5
+ - SegmentationDataset : positives only (has_mask=True), returns (image, mask)
6
+ - ClassificationDataset : all examples, returns (image, label)
7
+ """
8
+
9
+ import torch
10
+ from torch.utils.data import Dataset
11
+ from torchvision import transforms as T
12
+
13
+ IMG_MEAN = [0.485, 0.456, 0.406]
14
+ IMG_STD = [0.229, 0.224, 0.225]
15
+
16
+
17
+ class SegmentationDataset(Dataset):
18
+ """
19
+ Wraps a HF split pre-filtered to positives (has_mask=True).
20
+ Returns (image_tensor [3, H, W], mask_tensor [1, H, W]) as float32.
21
+ """
22
+
23
+ def __init__(self, hf_split, img_size: int = 400, augment: bool = False):
24
+ self.data = hf_split.filter(lambda x: x["has_mask"])
25
+ self.augment = augment
26
+ self.img_size = img_size
27
+
28
+ self.img_tf = T.Compose([
29
+ T.Resize((img_size, img_size)),
30
+ T.ToTensor(),
31
+ T.Normalize(mean=IMG_MEAN, std=IMG_STD),
32
+ ])
33
+ self.mask_tf = T.Compose([
34
+ T.Resize((img_size, img_size), interpolation=T.InterpolationMode.NEAREST),
35
+ T.ToTensor(),
36
+ ])
37
+
38
+ def __len__(self):
39
+ return len(self.data)
40
+
41
+ def __getitem__(self, idx):
42
+ ex = self.data[idx]
43
+ image = ex["image"].convert("RGB")
44
+ mask = ex["mask"].convert("L")
45
+
46
+ if self.augment:
47
+ if torch.rand(1) > 0.5:
48
+ image = T.functional.hflip(image)
49
+ mask = T.functional.hflip(mask)
50
+ if torch.rand(1) > 0.5:
51
+ image = T.functional.vflip(image)
52
+ mask = T.functional.vflip(mask)
53
+
54
+ image = self.img_tf(image)
55
+ mask = self.mask_tf(mask)
56
+ mask = (mask > 0.5).float() # binarize β†’ {0, 1}
57
+
58
+ return image, mask
59
+
60
+
61
+ class ClassificationDataset(Dataset):
62
+ """
63
+ All examples (positives + negatives).
64
+ Returns (image_tensor [3, H, W], label [float scalar]).
65
+ label = 1.0 if has_mask else 0.0
66
+ """
67
+
68
+ def __init__(self, hf_split, img_size: int = 299, augment: bool = False):
69
+ self.data = hf_split
70
+ self.augment = augment
71
+
72
+ self.img_tf = T.Compose([
73
+ T.Resize((img_size, img_size)),
74
+ T.ToTensor(),
75
+ T.Normalize(mean=IMG_MEAN, std=IMG_STD),
76
+ ])
77
+
78
+ def __len__(self):
79
+ return len(self.data)
80
+
81
+ def __getitem__(self, idx):
82
+ ex = self.data[idx]
83
+ image = ex["image"].convert("RGB")
84
+ label = float(ex["has_mask"])
85
+
86
+ if self.augment:
87
+ if torch.rand(1) > 0.5:
88
+ image = T.functional.hflip(image)
89
+ if torch.rand(1) > 0.5:
90
+ image = T.functional.vflip(image)
91
+
92
+ image = self.img_tf(image)
93
+ return image, torch.tensor(label)
train/evaluate.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ evaluate.py β€” Evaluate trained models on the BDAPPV test split.
3
+
4
+ Usage:
5
+ # Segmentation
6
+ python evaluate.py seg --provider google --checkpoint weights/deeplab_google_best.pth
7
+ python evaluate.py seg --provider ign --checkpoint weights/deeplab_ign_best.pth
8
+
9
+ # Classification
10
+ python evaluate.py clf --provider google --checkpoint weights/inception_google_best.pth
11
+
12
+ # Cross-provider (train Google, eval IGN) β€” the distribution shift benchmark
13
+ python evaluate.py seg --provider ign --checkpoint weights/deeplab_google_best.pth
14
+ """
15
+
16
+ import argparse
17
+
18
+ import torch
19
+ import torch.nn as nn
20
+ from torch.utils.data import DataLoader
21
+ from torchvision.models.segmentation import deeplabv3_resnet101
22
+ from torchvision.models import inception_v3
23
+
24
+ from datasets import load_dataset
25
+
26
+ from dataset import SegmentationDataset, ClassificationDataset
27
+
28
+
29
+ # ── Metrics ───────────────────────────────────────────────────────────────────
30
+
31
+ def compute_seg_metrics(pred_logits, target, threshold=0.5):
32
+ pred = (torch.sigmoid(pred_logits) > threshold).float()
33
+ tp = (pred * target).sum().item()
34
+ fp = (pred * (1 - target)).sum().item()
35
+ fn = ((1 - pred) * target).sum().item()
36
+ iou = tp / max(tp + fp + fn, 1e-6)
37
+ prec = tp / max(tp + fp, 1e-6)
38
+ rec = tp / max(tp + fn, 1e-6)
39
+ f1 = 2 * prec * rec / max(prec + rec, 1e-6)
40
+ return iou, f1
41
+
42
+
43
+ def compute_clf_metrics(logits, labels, threshold=0.0):
44
+ preds = (logits > threshold).float()
45
+ acc = (preds == labels).float().mean().item()
46
+ tp = (preds * labels).sum().item()
47
+ fp = (preds * (1 - labels)).sum().item()
48
+ fn = ((1 - preds) * labels).sum().item()
49
+ prec = tp / max(tp + fp, 1e-6)
50
+ rec = tp / max(tp + fn, 1e-6)
51
+ f1 = 2 * prec * rec / max(prec + rec, 1e-6)
52
+ return acc, prec, rec, f1
53
+
54
+
55
+ # ── Segmentation eval ─────────────────────────────────────────────────────────
56
+
57
+ def eval_seg(args):
58
+ device = (
59
+ torch.device("mps") if torch.backends.mps.is_available() else
60
+ torch.device("cuda") if torch.cuda.is_available() else
61
+ torch.device("cpu")
62
+ )
63
+
64
+ ds = load_dataset("gabrielkasmi/bdappv", args.provider)
65
+ test_ds = SegmentationDataset(ds["test"], img_size=args.img_size, augment=False)
66
+ loader = DataLoader(test_ds, batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers)
67
+
68
+ model = deeplabv3_resnet101(weights=None, aux_loss=False)
69
+ model.classifier[-1] = nn.Conv2d(256, 1, kernel_size=1)
70
+ state = torch.load(args.checkpoint, map_location="cpu", weights_only=False)
71
+ model_dict = model.state_dict()
72
+ compatible = {k: v for k, v in state.items() if k in model_dict and v.shape == model_dict[k].shape}
73
+ model_dict.update(compatible)
74
+ model.load_state_dict(model_dict)
75
+ model = model.to(device)
76
+ model.eval()
77
+
78
+ total_iou, total_f1 = 0.0, 0.0
79
+ with torch.no_grad():
80
+ for images, masks in loader:
81
+ images, masks = images.to(device), masks.to(device)
82
+ logits = model(images)["out"]
83
+ if logits.shape[-2:] != masks.shape[-2:]:
84
+ logits = nn.functional.interpolate(logits, size=masks.shape[-2:], mode="bilinear", align_corners=False)
85
+ iou, f1 = compute_seg_metrics(logits, masks)
86
+ total_iou += iou
87
+ total_f1 += f1
88
+
89
+ n = len(loader)
90
+ print(f"\n── Segmentation results ──────────────────────")
91
+ print(f" Provider : {args.provider}")
92
+ print(f" Checkpoint : {args.checkpoint}")
93
+ print(f" Test images: {len(test_ds):,}")
94
+ print(f" IoU : {total_iou / n:.4f}")
95
+ print(f" F1 : {total_f1 / n:.4f}")
96
+
97
+
98
+ # ── Classification eval ───────────────────────────────────────────────────────
99
+
100
+ def eval_clf(args):
101
+ device = (
102
+ torch.device("mps") if torch.backends.mps.is_available() else
103
+ torch.device("cuda") if torch.cuda.is_available() else
104
+ torch.device("cpu")
105
+ )
106
+
107
+ ds = load_dataset("gabrielkasmi/bdappv", args.provider)
108
+ test_ds = ClassificationDataset(ds["test"], img_size=299, augment=False)
109
+ loader = DataLoader(test_ds, batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers)
110
+
111
+ model = inception_v3(weights=None, aux_logits=True)
112
+ model.fc = nn.Linear(model.fc.in_features, 1)
113
+ model.AuxLogits.fc = nn.Linear(model.AuxLogits.fc.in_features, 1)
114
+ state = torch.load(args.checkpoint, map_location="cpu", weights_only=False)
115
+ model.load_state_dict(state)
116
+ model.aux_logits = False
117
+ model = model.to(device)
118
+ model.eval()
119
+
120
+ all_logits, all_labels = [], []
121
+ with torch.no_grad():
122
+ for images, labels in loader:
123
+ images = images.to(device)
124
+ logits = model(images)
125
+ if hasattr(logits, "logits"):
126
+ logits = logits.logits
127
+ all_logits.append(logits.cpu())
128
+ all_labels.append(labels.unsqueeze(1))
129
+
130
+ all_logits = torch.cat(all_logits)
131
+ all_labels = torch.cat(all_labels)
132
+ acc, prec, rec, f1 = compute_clf_metrics(all_logits, all_labels)
133
+
134
+ print(f"\n── Classification results ────────────────────")
135
+ print(f" Provider : {args.provider}")
136
+ print(f" Checkpoint : {args.checkpoint}")
137
+ print(f" Test images: {len(test_ds):,}")
138
+ print(f" Accuracy : {acc:.4f}")
139
+ print(f" Precision : {prec:.4f}")
140
+ print(f" Recall : {rec:.4f}")
141
+ print(f" F1 : {f1:.4f}")
142
+
143
+
144
+ # ── Entry point ───────────────────────────────────────────────────────────────
145
+
146
+ if __name__ == "__main__":
147
+ parser = argparse.ArgumentParser()
148
+ sub = parser.add_subparsers(dest="task", required=True)
149
+
150
+ for name in ["seg", "clf"]:
151
+ p = sub.add_parser(name)
152
+ p.add_argument("--provider", required=True, choices=["google", "ign"])
153
+ p.add_argument("--checkpoint", required=True)
154
+ p.add_argument("--batch_size", type=int, default=8)
155
+ p.add_argument("--img_size", type=int, default=400)
156
+ p.add_argument("--num_workers", type=int, default=0)
157
+
158
+ args = parser.parse_args()
159
+ if args.task == "seg":
160
+ eval_seg(args)
161
+ else:
162
+ eval_clf(args)
train/model.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ model.py β€” helpers to load BDAPPV baseline models from HuggingFace Hub.
3
+
4
+ Usage:
5
+ from huggingface_hub import hf_hub_download
6
+ import importlib.util, sys
7
+
8
+ path = hf_hub_download("gabrielkasmi/bdappv-models", "model.py")
9
+ spec = importlib.util.spec_from_file_location("bdappv_model", path)
10
+ mod = importlib.util.module_from_spec(spec); spec.loader.exec_module(mod)
11
+
12
+ seg = mod.load_segmentation_model("google") # or "ign"
13
+ clf = mod.load_classification_model("google") # or "ign"
14
+ """
15
+
16
+ import torch
17
+ import torch.nn as nn
18
+ from huggingface_hub import hf_hub_download
19
+ from torchvision.models.segmentation import deeplabv3_resnet101
20
+ from torchvision.models import inception_v3
21
+
22
+ REPO = "gabrielkasmi/bdappv-models"
23
+
24
+
25
+ def load_segmentation_model(provider: str = "google", device: str = "cpu"):
26
+ """
27
+ Load the DeepLabV3-ResNet101 segmentation model.
28
+
29
+ Args:
30
+ provider : "google" or "ign"
31
+ device : "cpu", "cuda", or "mps"
32
+
33
+ Returns:
34
+ model in eval mode
35
+ """
36
+ assert provider in ("google", "ign"), "provider must be 'google' or 'ign'"
37
+
38
+ path = hf_hub_download(REPO, f"deeplab_{provider}_best.pth")
39
+ model = deeplabv3_resnet101(weights=None, aux_loss=False)
40
+ model.classifier[-1] = nn.Conv2d(256, 1, kernel_size=1)
41
+
42
+ state = torch.load(path, map_location=device, weights_only=False)
43
+ model_dict = model.state_dict()
44
+ compatible = {k: v for k, v in state.items()
45
+ if k in model_dict and v.shape == model_dict[k].shape}
46
+ model_dict.update(compatible)
47
+ model.load_state_dict(model_dict)
48
+
49
+ return model.eval().to(device)
50
+
51
+
52
+ def load_classification_model(provider: str = "google", device: str = "cpu"):
53
+ """
54
+ Load the InceptionV3 classification model (panel / no panel).
55
+
56
+ Args:
57
+ provider : "google" or "ign"
58
+ device : "cpu", "cuda", or "mps"
59
+
60
+ Returns:
61
+ model in eval mode
62
+ """
63
+ assert provider in ("google", "ign"), "provider must be 'google' or 'ign'"
64
+
65
+ path = hf_hub_download(REPO, f"inception_{provider}_best.pth")
66
+ model = inception_v3(weights=None, aux_logits=True)
67
+ model.fc = nn.Linear(model.fc.in_features, 1)
68
+ model.AuxLogits.fc = nn.Linear(model.AuxLogits.fc.in_features, 1)
69
+
70
+ state = torch.load(path, map_location=device, weights_only=False)
71
+ model.load_state_dict(state)
72
+ model.aux_logits = False # dΓ©sactive pour l'infΓ©rence
73
+
74
+ return model.eval().to(device)
train/pretrained/deeplabv3_weights.tar ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bbedde9b8e74934fd117c6fc73984a5521ba51863d28d4c980135b162240df8d
3
+ size 244640087
train/pretrained/inceptionv3_weights.tar ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d8d8b4a8d5ac646605012f3aaa6ee75420ef9201bb16e853efd7edbbc9ff4bd5
3
+ size 390201979
train/requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ torch>=2.2.0
2
+ torchvision>=0.17.0
3
+ datasets>=2.18.0
4
+ Pillow>=10.0.0
train/train_clf.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ train_clf.py β€” InceptionV3 binary classification on BDAPPV (panel / no panel).
3
+
4
+ Usage:
5
+ python train_clf.py --provider google
6
+ python train_clf.py --provider ign
7
+ python train_clf.py --provider google --checkpoint weights/inception_google_best.pth
8
+ """
9
+
10
+ import argparse
11
+ import os
12
+
13
+ import torch
14
+ from tqdm import tqdm
15
+ import torch.nn as nn
16
+ from torch.utils.data import DataLoader
17
+ from torchvision.models import inception_v3, Inception_V3_Weights
18
+
19
+ from datasets import load_dataset
20
+
21
+ from dataset import ClassificationDataset
22
+
23
+
24
+ # ── Metrics ───────────────────────────────────────────────────────────────────
25
+
26
+ def accuracy_f1(logits, labels, threshold=0.0):
27
+ preds = (logits > threshold).float()
28
+ correct = (preds == labels).float().mean().item()
29
+ tp = (preds * labels).sum().item()
30
+ fp = (preds * (1 - labels)).sum().item()
31
+ fn = ((1 - preds) * labels).sum().item()
32
+ prec = tp / max(tp + fp, 1e-6)
33
+ rec = tp / max(tp + fn, 1e-6)
34
+ f1 = 2 * prec * rec / max(prec + rec, 1e-6)
35
+ return correct, f1
36
+
37
+
38
+ # ── Main ──────────────────────────────────────────────────────────────────────
39
+
40
+ def train(args):
41
+ device = (
42
+ torch.device("mps") if torch.backends.mps.is_available() else
43
+ torch.device("cuda") if torch.cuda.is_available() else
44
+ torch.device("cpu")
45
+ )
46
+ print(f"Device : {device}")
47
+ print(f"Provider: {args.provider} | effective batch: {args.batch_size * args.grad_accum}")
48
+
49
+ # ── Data ──────────────────────────────────────────────────────────────────
50
+ print("Loading dataset from HuggingFace Hub...")
51
+ ds = load_dataset("gabrielkasmi/bdappv", args.provider)
52
+
53
+ train_ds = ClassificationDataset(ds["train"], img_size=299, augment=True)
54
+ val_ds = ClassificationDataset(ds["validation"], img_size=299, augment=False)
55
+
56
+ print(f" train : {len(train_ds):,} | val : {len(val_ds):,}")
57
+
58
+ train_loader = DataLoader(
59
+ train_ds, batch_size=args.batch_size, shuffle=True,
60
+ num_workers=args.num_workers, pin_memory=(device.type != "mps"),
61
+ )
62
+ val_loader = DataLoader(
63
+ val_ds, batch_size=args.batch_size, shuffle=False,
64
+ num_workers=args.num_workers, pin_memory=(device.type != "mps"),
65
+ )
66
+
67
+ # ── Model ─────────────────────────────────────────────────────────────────
68
+ model = inception_v3(weights=Inception_V3_Weights.DEFAULT)
69
+
70
+ if args.checkpoint:
71
+ # Poids issus d'un modΓ¨le Γ  2 classes β€” on charge avec la tΓͺte originale
72
+ # puis on la remplace par une tΓͺte binaire (1 sortie, BCEWithLogitsLoss)
73
+ model.fc = nn.Linear(model.fc.in_features, 2)
74
+ model.AuxLogits.fc = nn.Linear(model.AuxLogits.fc.in_features, 2)
75
+ ckpt = torch.load(args.checkpoint, map_location="cpu", weights_only=False)
76
+ state = ckpt["model_state_dict"] if isinstance(ckpt, dict) else ckpt
77
+ model.load_state_dict(state, strict=True)
78
+ print(f"Loaded checkpoint: {args.checkpoint}")
79
+
80
+ # Remplace la tΓͺte par 1 sortie pour BCEWithLogitsLoss
81
+ model.fc = nn.Linear(model.fc.in_features, 1)
82
+ model.AuxLogits.fc = nn.Linear(model.AuxLogits.fc.in_features, 1)
83
+
84
+ model = model.to(device)
85
+
86
+ # ── Optimizer + scheduler ─────────────────────────────────────────────────
87
+ optimizer = torch.optim.AdamW(model.parameters(), lr=args.lr, weight_decay=1e-4)
88
+ scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=args.epochs)
89
+ criterion = nn.BCEWithLogitsLoss()
90
+
91
+ # ── Training loop ─────────────────────────────────────────────────────────
92
+ os.makedirs(args.out_dir, exist_ok=True)
93
+ best_f1 = 0.0
94
+ patience_ctr = 0
95
+
96
+ for epoch in range(1, args.epochs + 1):
97
+ model.train()
98
+ total_loss = 0.0
99
+ optimizer.zero_grad()
100
+
101
+ pbar = tqdm(train_loader, desc=f"Epoch {epoch}/{args.epochs}", leave=False)
102
+ for step, (images, labels) in enumerate(pbar):
103
+ images = images.to(device)
104
+ labels = labels.to(device).unsqueeze(1)
105
+
106
+ out = model(images)
107
+ logits = out.logits if hasattr(out, "logits") else out[0]
108
+ aux = out.aux_logits if hasattr(out, "aux_logits") else out[1]
109
+
110
+ loss = (criterion(logits, labels) + 0.4 * criterion(aux, labels)) / args.grad_accum
111
+ loss.backward()
112
+ total_loss += loss.item() * args.grad_accum
113
+ pbar.set_postfix(loss=f"{loss.item() * args.grad_accum:.4f}")
114
+
115
+ if (step + 1) % args.grad_accum == 0 or (step + 1) == len(train_loader):
116
+ optimizer.step()
117
+ optimizer.zero_grad()
118
+
119
+ # ── Validation ────────────────────────────────────────────────────────
120
+ model.eval()
121
+ all_logits, all_labels = [], []
122
+ with torch.no_grad():
123
+ for images, labels in val_loader:
124
+ images = images.to(device)
125
+ logits = model(images)
126
+ if hasattr(logits, "logits"):
127
+ logits = logits.logits
128
+ all_logits.append(logits.cpu())
129
+ all_labels.append(labels.unsqueeze(1))
130
+
131
+ all_logits = torch.cat(all_logits)
132
+ all_labels = torch.cat(all_labels)
133
+ acc, f1 = accuracy_f1(all_logits, all_labels)
134
+ avg_loss = total_loss / len(train_loader)
135
+
136
+ print(f"Epoch {epoch:3d}/{args.epochs} loss={avg_loss:.4f} val_acc={acc:.4f} val_f1={f1:.4f}")
137
+
138
+ scheduler.step()
139
+
140
+ if f1 > best_f1:
141
+ best_f1 = f1
142
+ patience_ctr = 0
143
+ ckpt_path = os.path.join(args.out_dir, f"inception_{args.provider}_best.pth")
144
+ torch.save(model.state_dict(), ckpt_path)
145
+ print(f" β†’ checkpoint saved (F1={best_f1:.4f})")
146
+ else:
147
+ patience_ctr += 1
148
+ if patience_ctr >= args.patience:
149
+ print(f"\nEarly stopping β€” no improvement for {args.patience} epochs.")
150
+ break
151
+
152
+ print(f"\nDone. Best val F1: {best_f1:.4f}")
153
+ print(f"Weights: {os.path.join(args.out_dir, f'inception_{args.provider}_best.pth')}")
154
+
155
+
156
+ if __name__ == "__main__":
157
+ parser = argparse.ArgumentParser()
158
+ parser.add_argument("--provider", default="google", choices=["google", "ign"])
159
+ parser.add_argument("--epochs", type=int, default=50)
160
+ parser.add_argument("--batch_size", type=int, default=8)
161
+ parser.add_argument("--grad_accum", type=int, default=4, help="Gradient accumulation steps")
162
+ parser.add_argument("--lr", type=float, default=1e-4)
163
+ parser.add_argument("--num_workers", type=int, default=2, help="Workers pour le dataloader")
164
+ parser.add_argument("--checkpoint", default=None)
165
+ parser.add_argument("--out_dir", default="weights")
166
+ parser.add_argument("--patience", type=int, default=7, help="Early stopping patience (epochs)")
167
+ args = parser.parse_args()
168
+ train(args)
train/train_seg.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ train_seg.py β€” DeepLabV3-ResNet50 segmentation on BDAPPV.
3
+
4
+ Usage:
5
+ python train_seg.py --provider google
6
+ python train_seg.py --provider ign
7
+ python train_seg.py --provider google --checkpoint weights/deeplab_google_best.pth # resume
8
+ """
9
+
10
+ import argparse
11
+ import os
12
+
13
+ import torch
14
+ from tqdm import tqdm
15
+ import torch.nn as nn
16
+ from torch.utils.data import DataLoader
17
+ from torchvision.models.segmentation import deeplabv3_resnet101, DeepLabV3_ResNet101_Weights
18
+
19
+ from datasets import load_dataset
20
+
21
+ from dataset import SegmentationDataset
22
+
23
+
24
+ # ── Metrics ───────────────────────────────────────────────────────────────────
25
+
26
+ def iou_score(pred_logits, target, threshold=0.5):
27
+ pred = (torch.sigmoid(pred_logits) > threshold).float()
28
+ inter = (pred * target).sum()
29
+ union = pred.sum() + target.sum() - inter
30
+ return (inter / union.clamp(min=1e-6)).item()
31
+
32
+
33
+ # ── Main ──────────────────────────────────────────────────────────────────────
34
+
35
+ def train(args):
36
+ device = (
37
+ torch.device("mps") if torch.backends.mps.is_available() else
38
+ torch.device("cuda") if torch.cuda.is_available() else
39
+ torch.device("cpu")
40
+ )
41
+ print(f"Device : {device}")
42
+ print(f"Provider: {args.provider} | effective batch: {args.batch_size * args.grad_accum}")
43
+
44
+ # ── Data ──────────────────────────────────────────────────────────────────
45
+ print("Loading dataset from HuggingFace Hub...")
46
+ ds = load_dataset("gabrielkasmi/bdappv", args.provider)
47
+
48
+ train_ds = SegmentationDataset(ds["train"], img_size=args.img_size, augment=True)
49
+ val_ds = SegmentationDataset(ds["validation"], img_size=args.img_size, augment=False)
50
+
51
+ print(f" train positives : {len(train_ds):,}")
52
+ print(f" val positives : {len(val_ds):,}")
53
+
54
+ train_loader = DataLoader(
55
+ train_ds, batch_size=args.batch_size, shuffle=True,
56
+ num_workers=args.num_workers, pin_memory=(device.type != "mps"),
57
+ drop_last=True,
58
+ )
59
+ val_loader = DataLoader(
60
+ val_ds, batch_size=args.batch_size, shuffle=False,
61
+ num_workers=args.num_workers, pin_memory=(device.type != "mps"),
62
+ )
63
+
64
+ # ── Model ─────────────────────────────────────────────────────────────────
65
+ model = deeplabv3_resnet101(weights=DeepLabV3_ResNet101_Weights.DEFAULT)
66
+ model.classifier[-1] = nn.Conv2d(256, 1, kernel_size=1)
67
+ model.aux_classifier[-1] = nn.Conv2d(256, 1, kernel_size=1)
68
+
69
+ if args.checkpoint:
70
+ # Le checkpoint mΓ©lange une tΓͺte principale binaire et une tΓͺte aux Γ  21 classes
71
+ # (poids de base incohΓ©rents) β€” on ne charge que les tenseurs dont la shape colle.
72
+ ckpt = torch.load(args.checkpoint, map_location="cpu", weights_only=False)
73
+ state = ckpt["model_state_dict"] if isinstance(ckpt, dict) else ckpt
74
+ model_dict = model.state_dict()
75
+ compatible = {k: v for k, v in state.items() if k in model_dict and v.shape == model_dict[k].shape}
76
+ skipped = set(state.keys()) - set(compatible.keys())
77
+ model_dict.update(compatible)
78
+ model.load_state_dict(model_dict)
79
+ print(f"Loaded checkpoint: {args.checkpoint} ({len(compatible)}/{len(state)} tensors matched, {len(skipped)} skipped)")
80
+
81
+ model = model.to(device)
82
+
83
+ # ── Optimizer + scheduler ─────────────────────────────────────────────────
84
+ optimizer = torch.optim.AdamW(model.parameters(), lr=args.lr, weight_decay=1e-4)
85
+ scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=args.epochs)
86
+ criterion = nn.BCEWithLogitsLoss()
87
+
88
+ # ── Training loop ─────────────────────────────────────────────────────────
89
+ os.makedirs(args.out_dir, exist_ok=True)
90
+ best_iou = 0.0
91
+ patience_ctr = 0
92
+
93
+ for epoch in range(1, args.epochs + 1):
94
+ model.train()
95
+ total_loss = 0.0
96
+ optimizer.zero_grad()
97
+
98
+ pbar = tqdm(train_loader, desc=f"Epoch {epoch}/{args.epochs}", leave=False)
99
+ for step, (images, masks) in enumerate(pbar):
100
+ if step == 3:
101
+ break
102
+ images, masks = images.to(device), masks.to(device)
103
+
104
+ out = model(images)
105
+ logits = out["out"]
106
+ aux = out["aux"]
107
+
108
+ if logits.shape[-2:] != masks.shape[-2:]:
109
+ logits = nn.functional.interpolate(logits, size=masks.shape[-2:], mode="bilinear", align_corners=False)
110
+ aux = nn.functional.interpolate(aux, size=masks.shape[-2:], mode="bilinear", align_corners=False)
111
+
112
+ loss = (criterion(logits, masks) + 0.4 * criterion(aux, masks)) / args.grad_accum
113
+ loss.backward()
114
+ total_loss += loss.item() * args.grad_accum
115
+ pbar.set_postfix(loss=f"{loss.item() * args.grad_accum:.4f}")
116
+
117
+ if (step + 1) % args.grad_accum == 0 or (step + 1) == len(train_loader):
118
+ optimizer.step()
119
+ optimizer.zero_grad()
120
+
121
+ # ── Validation ────────────────────────────────────────────────────────
122
+ model.eval()
123
+ val_iou = 0.0
124
+ with torch.no_grad():
125
+ for images, masks in val_loader:
126
+ images, masks = images.to(device), masks.to(device)
127
+ logits = model(images)["out"]
128
+ if logits.shape[-2:] != masks.shape[-2:]:
129
+ logits = nn.functional.interpolate(logits, size=masks.shape[-2:], mode="bilinear", align_corners=False)
130
+ val_iou += iou_score(logits, masks)
131
+
132
+ val_iou /= len(val_loader)
133
+ avg_loss = total_loss / len(train_loader)
134
+
135
+ print(f"Epoch {epoch:3d}/{args.epochs} loss={avg_loss:.4f} val_iou={val_iou:.4f}")
136
+
137
+ scheduler.step()
138
+
139
+ if val_iou > best_iou:
140
+ best_iou = val_iou
141
+ patience_ctr = 0
142
+ ckpt_path = os.path.join(args.out_dir, f"deeplab_{args.provider}_best.pth")
143
+ torch.save(model.state_dict(), ckpt_path)
144
+ print(f" β†’ checkpoint saved (IoU={best_iou:.4f})")
145
+ else:
146
+ patience_ctr += 1
147
+ if patience_ctr >= args.patience:
148
+ print(f"\nEarly stopping β€” no improvement for {args.patience} epochs.")
149
+ break
150
+
151
+ print(f"\nDone. Best val IoU: {best_iou:.4f}")
152
+ print(f"Weights: {os.path.join(args.out_dir, f'deeplab_{args.provider}_best.pth')}")
153
+
154
+
155
+ if __name__ == "__main__":
156
+ parser = argparse.ArgumentParser()
157
+ parser.add_argument("--provider", default="google", choices=["google", "ign"])
158
+ parser.add_argument("--epochs", type=int, default=50)
159
+ parser.add_argument("--batch_size", type=int, default=4)
160
+ parser.add_argument("--grad_accum", type=int, default=8, help="Gradient accumulation steps (eff. batch = batch_size Γ— grad_accum)")
161
+ parser.add_argument("--lr", type=float, default=1e-4)
162
+ parser.add_argument("--img_size", type=int, default=400)
163
+ parser.add_argument("--num_workers", type=int, default=2, help="Workers pour le dataloader")
164
+ parser.add_argument("--checkpoint", default=None)
165
+ parser.add_argument("--out_dir", default="weights")
166
+ parser.add_argument("--patience", type=int, default=7, help="Early stopping patience (epochs)")
167
+ args = parser.parse_args()
168
+ train(args)
train/weights/.gitkeep ADDED
File without changes