v2: distance-to-transmitter channel — fixes far-field square cutoff
This commit is contained in:
BIN
outputs_v2/prediction_00.png
Normal file
BIN
outputs_v2/prediction_00.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 299 KiB |
BIN
outputs_v2/prediction_01.png
Normal file
BIN
outputs_v2/prediction_01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 185 KiB |
BIN
outputs_v2/prediction_02.png
Normal file
BIN
outputs_v2/prediction_02.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 270 KiB |
BIN
outputs_v2/prediction_03.png
Normal file
BIN
outputs_v2/prediction_03.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 194 KiB |
BIN
outputs_v2/prediction_04.png
Normal file
BIN
outputs_v2/prediction_04.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 343 KiB |
BIN
outputs_v2/prediction_05.png
Normal file
BIN
outputs_v2/prediction_05.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 267 KiB |
@@ -41,9 +41,11 @@ class RadioMapSeerDataset(Dataset):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, data_root=DEFAULT_DATA_ROOT, split="train",
|
def __init__(self, data_root=DEFAULT_DATA_ROOT, split="train",
|
||||||
sim="DPM", img_size=256, max_samples=None):
|
sim="DPM", img_size=256, max_samples=None, add_distance=False):
|
||||||
self.data_root = Path(data_root)
|
self.data_root = Path(data_root)
|
||||||
self.img_size = img_size
|
self.img_size = img_size
|
||||||
|
self.add_distance = add_distance # v2: distance-to-Tx channel
|
||||||
|
self._build_cache = {} # cache shared building maps
|
||||||
|
|
||||||
self.gain_dir = self.data_root / "gain" / sim
|
self.gain_dir = self.data_root / "gain" / sim
|
||||||
self.build_dir = self.data_root / "png" / "buildings_complete"
|
self.build_dir = self.data_root / "png" / "buildings_complete"
|
||||||
@@ -79,12 +81,26 @@ class RadioMapSeerDataset(Dataset):
|
|||||||
def __getitem__(self, idx):
|
def __getitem__(self, idx):
|
||||||
map_id, tx_id = self.samples[idx]
|
map_id, tx_id = self.samples[idx]
|
||||||
|
|
||||||
buildings = _load_gray(self.build_dir / f"{map_id}.png", self.img_size)
|
# buildings are shared across a map's 80 transmitters -> cache to cut I/O
|
||||||
antenna = _load_gray(self.ant_dir / f"{map_id}_{tx_id}.png", self.img_size)
|
buildings = self._build_cache.get(map_id)
|
||||||
gain = _load_gray(self.gain_dir / f"{map_id}_{tx_id}.png", self.img_size)
|
if buildings is None:
|
||||||
|
buildings = _load_gray(self.build_dir / f"{map_id}.png", self.img_size)
|
||||||
|
self._build_cache[map_id] = buildings
|
||||||
|
|
||||||
x = np.stack([buildings, antenna], axis=0) # [2, H, W]
|
antenna = _load_gray(self.ant_dir / f"{map_id}_{tx_id}.png", self.img_size)
|
||||||
y = gain[None, :, :] # [1, H, W]
|
gain = _load_gray(self.gain_dir / f"{map_id}_{tx_id}.png", self.img_size)
|
||||||
|
|
||||||
|
channels = [buildings, antenna]
|
||||||
|
if self.add_distance: # v2 third channel
|
||||||
|
H = W = self.img_size
|
||||||
|
r, c = np.unravel_index(int(np.argmax(antenna)), antenna.shape)
|
||||||
|
yy, xx = np.mgrid[0:H, 0:W]
|
||||||
|
dist = np.sqrt((yy - r) ** 2 + (xx - c) ** 2).astype(np.float32)
|
||||||
|
dist /= (2 ** 0.5) * H # normalize to ~[0, 1]
|
||||||
|
channels.append(dist)
|
||||||
|
|
||||||
|
x = np.stack(channels, axis=0)
|
||||||
|
y = gain[None, :, :]
|
||||||
return torch.from_numpy(x), torch.from_numpy(y)
|
return torch.from_numpy(x), torch.from_numpy(y)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
47
src/train.py
47
src/train.py
@@ -1,15 +1,18 @@
|
|||||||
"""
|
"""
|
||||||
train.py — train the U-Net on RadioMapSeer coverage maps.
|
train.py — train the U-Net on RadioMapSeer coverage maps.
|
||||||
|
|
||||||
Examples:
|
v1: 2-channel input (buildings, transmitter).
|
||||||
# smoke test: tiny subset, 1 epoch — verify the loop and measure VRAM/speed
|
v2: add --distance for a 3rd channel (distance-to-Tx) that fixes the square cutoff.
|
||||||
python src\\train.py --max-samples 200 --epochs 1 --batch-size 16
|
|
||||||
|
|
||||||
# full run (size it after the smoke test)
|
Examples:
|
||||||
python src\\train.py --epochs 30 --batch-size 16
|
# v1
|
||||||
|
python src\\train.py --epochs 30 --batch-size 32 --num-workers 8
|
||||||
|
# v2 (separate checkpoint dir so v1 stays intact)
|
||||||
|
python src\\train.py --distance --epochs 30 --batch-size 32 --num-workers 8 --ckpt-dir checkpoints_v2
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import csv
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -20,15 +23,17 @@ from torch.utils.data import DataLoader
|
|||||||
from dataset import RadioMapSeerDataset
|
from dataset import RadioMapSeerDataset
|
||||||
from model import UNet
|
from model import UNet
|
||||||
|
|
||||||
|
torch.backends.cudnn.benchmark = True # autotune convs (input size is fixed)
|
||||||
|
|
||||||
def evaluate(model, loader, criterion, device, amp):
|
|
||||||
model.eval()
|
def evaluate(net, loader, criterion, device, amp):
|
||||||
|
net.eval()
|
||||||
total, n = 0.0, 0
|
total, n = 0.0, 0
|
||||||
with torch.no_grad():
|
with torch.no_grad():
|
||||||
for x, y in loader:
|
for x, y in loader:
|
||||||
x, y = x.to(device), y.to(device)
|
x, y = x.to(device), y.to(device)
|
||||||
with torch.autocast(device_type=device, dtype=torch.float16, enabled=amp):
|
with torch.autocast(device_type=device, dtype=torch.float16, enabled=amp):
|
||||||
loss = criterion(model(x), y)
|
loss = criterion(net(x), y)
|
||||||
total += loss.item() * x.size(0)
|
total += loss.item() * x.size(0)
|
||||||
n += x.size(0)
|
n += x.size(0)
|
||||||
return total / n
|
return total / n
|
||||||
@@ -37,25 +42,28 @@ def evaluate(model, loader, criterion, device, amp):
|
|||||||
def main():
|
def main():
|
||||||
p = argparse.ArgumentParser()
|
p = argparse.ArgumentParser()
|
||||||
p.add_argument("--epochs", type=int, default=30)
|
p.add_argument("--epochs", type=int, default=30)
|
||||||
p.add_argument("--batch-size", type=int, default=16)
|
p.add_argument("--batch-size", type=int, default=32)
|
||||||
p.add_argument("--lr", type=float, default=1e-3)
|
p.add_argument("--lr", type=float, default=1e-3)
|
||||||
p.add_argument("--img-size", type=int, default=256)
|
p.add_argument("--img-size", type=int, default=256)
|
||||||
p.add_argument("--base", type=int, default=64)
|
p.add_argument("--base", type=int, default=64)
|
||||||
p.add_argument("--max-samples", type=int, default=None,
|
p.add_argument("--max-samples", type=int, default=None)
|
||||||
help="Limit samples for quick smoke tests")
|
|
||||||
p.add_argument("--num-workers", type=int, default=4)
|
p.add_argument("--num-workers", type=int, default=4)
|
||||||
p.add_argument("--ckpt-dir", default="checkpoints")
|
p.add_argument("--ckpt-dir", default="checkpoints")
|
||||||
|
p.add_argument("--distance", action="store_true",
|
||||||
|
help="v2: add a distance-to-transmitter input channel")
|
||||||
|
p.add_argument("--compile", action="store_true",
|
||||||
|
help="wrap model in torch.compile (optional; flaky on Windows)")
|
||||||
args = p.parse_args()
|
args = p.parse_args()
|
||||||
|
|
||||||
device = "cuda" if torch.cuda.is_available() else "cpu"
|
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||||
amp = (device == "cuda")
|
amp = (device == "cuda")
|
||||||
print(f"Device: {device} | AMP: {amp}")
|
print(f"Device: {device} | AMP: {amp} | distance: {args.distance} | compile: {args.compile}")
|
||||||
|
|
||||||
train_ds = RadioMapSeerDataset(split="train", img_size=args.img_size,
|
train_ds = RadioMapSeerDataset(split="train", img_size=args.img_size,
|
||||||
max_samples=args.max_samples)
|
max_samples=args.max_samples, add_distance=args.distance)
|
||||||
val_max = None if args.max_samples is None else max(20, args.max_samples // 5)
|
val_max = None if args.max_samples is None else max(20, args.max_samples // 5)
|
||||||
val_ds = RadioMapSeerDataset(split="val", img_size=args.img_size,
|
val_ds = RadioMapSeerDataset(split="val", img_size=args.img_size,
|
||||||
max_samples=val_max)
|
max_samples=val_max, add_distance=args.distance)
|
||||||
print(f"Train: {len(train_ds)} | Val: {len(val_ds)}")
|
print(f"Train: {len(train_ds)} | Val: {len(val_ds)}")
|
||||||
|
|
||||||
dl_kwargs = dict(batch_size=args.batch_size, num_workers=args.num_workers,
|
dl_kwargs = dict(batch_size=args.batch_size, num_workers=args.num_workers,
|
||||||
@@ -63,7 +71,9 @@ def main():
|
|||||||
train_loader = DataLoader(train_ds, shuffle=True, **dl_kwargs)
|
train_loader = DataLoader(train_ds, shuffle=True, **dl_kwargs)
|
||||||
val_loader = DataLoader(val_ds, shuffle=False, **dl_kwargs)
|
val_loader = DataLoader(val_ds, shuffle=False, **dl_kwargs)
|
||||||
|
|
||||||
model = UNet(in_channels=2, out_channels=1, base=args.base).to(device)
|
in_ch = 3 if args.distance else 2
|
||||||
|
model = UNet(in_channels=in_ch, out_channels=1, base=args.base).to(device)
|
||||||
|
net = torch.compile(model) if args.compile else model
|
||||||
criterion = nn.MSELoss()
|
criterion = nn.MSELoss()
|
||||||
optimizer = torch.optim.Adam(model.parameters(), lr=args.lr)
|
optimizer = torch.optim.Adam(model.parameters(), lr=args.lr)
|
||||||
scaler = torch.amp.GradScaler(enabled=amp)
|
scaler = torch.amp.GradScaler(enabled=amp)
|
||||||
@@ -72,20 +82,19 @@ def main():
|
|||||||
ckpt_dir.mkdir(exist_ok=True)
|
ckpt_dir.mkdir(exist_ok=True)
|
||||||
best_val = float("inf")
|
best_val = float("inf")
|
||||||
|
|
||||||
import csv
|
|
||||||
history_path = ckpt_dir / "history.csv"
|
history_path = ckpt_dir / "history.csv"
|
||||||
with open(history_path, "w", newline="") as f:
|
with open(history_path, "w", newline="") as f:
|
||||||
csv.writer(f).writerow(["epoch", "train_mse", "val_mse"])
|
csv.writer(f).writerow(["epoch", "train_mse", "val_mse"])
|
||||||
|
|
||||||
for epoch in range(1, args.epochs + 1):
|
for epoch in range(1, args.epochs + 1):
|
||||||
model.train()
|
net.train()
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
running, n = 0.0, 0
|
running, n = 0.0, 0
|
||||||
for i, (x, y) in enumerate(train_loader, 1):
|
for i, (x, y) in enumerate(train_loader, 1):
|
||||||
x, y = x.to(device), y.to(device)
|
x, y = x.to(device), y.to(device)
|
||||||
optimizer.zero_grad(set_to_none=True)
|
optimizer.zero_grad(set_to_none=True)
|
||||||
with torch.autocast(device_type=device, dtype=torch.float16, enabled=amp):
|
with torch.autocast(device_type=device, dtype=torch.float16, enabled=amp):
|
||||||
loss = criterion(model(x), y)
|
loss = criterion(net(x), y)
|
||||||
scaler.scale(loss).backward()
|
scaler.scale(loss).backward()
|
||||||
scaler.step(optimizer)
|
scaler.step(optimizer)
|
||||||
scaler.update()
|
scaler.update()
|
||||||
@@ -95,7 +104,7 @@ def main():
|
|||||||
print(f" epoch {epoch} step {i}/{len(train_loader)} | loss {running/n:.5f}")
|
print(f" epoch {epoch} step {i}/{len(train_loader)} | loss {running/n:.5f}")
|
||||||
|
|
||||||
train_loss = running / n
|
train_loss = running / n
|
||||||
val_loss = evaluate(model, val_loader, criterion, device, amp)
|
val_loss = evaluate(net, val_loader, criterion, device, amp)
|
||||||
dt = time.time() - t0
|
dt = time.time() - t0
|
||||||
print(f"[epoch {epoch}/{args.epochs}] train MSE {train_loss:.5f} | "
|
print(f"[epoch {epoch}/{args.epochs}] train MSE {train_loss:.5f} | "
|
||||||
f"val MSE {val_loss:.5f} | val RMSE {val_loss**0.5:.5f} | {dt:.0f}s")
|
f"val MSE {val_loss:.5f} | val RMSE {val_loss**0.5:.5f} | {dt:.0f}s")
|
||||||
|
|||||||
@@ -31,14 +31,17 @@ def main():
|
|||||||
device = "cuda" if torch.cuda.is_available() else "cpu"
|
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||||
|
|
||||||
ckpt = torch.load(args.ckpt, map_location=device, weights_only=False)
|
ckpt = torch.load(args.ckpt, map_location=device, weights_only=False)
|
||||||
base = ckpt.get("args", {}).get("base", 64)
|
cargs = ckpt.get("args", {})
|
||||||
model = UNet(in_channels=2, out_channels=1, base=base).to(device)
|
base = cargs.get("base", 64)
|
||||||
|
add_dist = bool(cargs.get("distance", False))
|
||||||
|
in_ch = 3 if add_dist else 2
|
||||||
|
model = UNet(in_channels=in_ch, out_channels=1, base=base).to(device)
|
||||||
model.load_state_dict(ckpt["model_state"])
|
model.load_state_dict(ckpt["model_state"])
|
||||||
model.eval()
|
model.eval()
|
||||||
print(f"Loaded {args.ckpt} (epoch {ckpt.get('epoch', '?')}, "
|
print(f"Loaded {args.ckpt} | in_channels={in_ch} | distance={add_dist} | "
|
||||||
f"val MSE {ckpt.get('val_loss', float('nan')):.5f})")
|
f"val MSE {ckpt.get('val_loss', float('nan')):.5f}")
|
||||||
|
|
||||||
ds = RadioMapSeerDataset(split="test", img_size=args.img_size) # unseen cities
|
ds = RadioMapSeerDataset(split="test", img_size=args.img_size, add_distance=add_dist)
|
||||||
rng = np.random.default_rng(args.seed)
|
rng = np.random.default_rng(args.seed)
|
||||||
idxs = rng.choice(len(ds), size=args.n, replace=False)
|
idxs = rng.choice(len(ds), size=args.n, replace=False)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user