dataset.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. """Dataset loading, stratified splitting and per-channel normalisation.
  2. Every cycle sample is an n x 4 matrix [signal_value, second_value, 1, 1].
  3. Samples are zero-padded to a fixed sequence length (divisible by patch size)
  4. and normalised per channel using statistics computed on the training split
  5. only. The train / validation / evaluation split is stratified per part
  6. (8 : 1.5 : 0.5) so every part contributes samples to all three groups.
  7. """
  8. import csv
  9. import random
  10. from pathlib import Path
  11. import numpy as np
  12. import torch
  13. from torch.utils.data import Dataset
  14. from tqdm import tqdm
  15. ROOT = Path(__file__).resolve().parents[1]
  16. INPUT_DIR = ROOT / "inputdatas"
  17. SPLIT_PATH = ROOT / "split.csv"
  18. PATCH_SIZE = 8
  19. N_CHANNELS = 4
  20. # 1552 = 194 patches of 8, covers the observed 1540..1546 cycle lengths.
  21. SEQ_LEN = 1552
  22. SPLIT_SEED = 42
  23. TRAIN_RATIO = 0.80
  24. VAL_RATIO = 0.15
  25. EVAL_RATIO = 0.05
  26. def discover_samples() -> list[dict]:
  27. samples: list[dict] = []
  28. for path in sorted(INPUT_DIR.glob("**/*.npy")):
  29. samples.append(
  30. {
  31. "path": str(path),
  32. "part": path.parent.name,
  33. "name": path.name,
  34. }
  35. )
  36. return samples
  37. def ensure_split(samples: list[dict]) -> list[dict]:
  38. """Attach a split label to every sample, writing split.csv on first run."""
  39. if SPLIT_PATH.exists():
  40. split_map: dict[str, str] = {}
  41. with SPLIT_PATH.open(encoding="utf-8") as handle:
  42. for row in csv.DictReader(handle):
  43. split_map[row["path"]] = row["split"]
  44. for item in samples:
  45. item["split"] = split_map.get(item["path"], "train")
  46. return samples
  47. rng = random.Random(SPLIT_SEED)
  48. parts: dict[str, list[dict]] = {}
  49. for item in samples:
  50. parts.setdefault(item["part"], []).append(item)
  51. for part, items in parts.items():
  52. order = sorted(items, key=lambda item: item["name"])
  53. rng.shuffle(order)
  54. n = len(order)
  55. n_eval = max(1, round(n * EVAL_RATIO))
  56. n_val = max(1, round(n * VAL_RATIO))
  57. n_train = n - n_val - n_eval
  58. for index, item in enumerate(order):
  59. if index < n_train:
  60. item["split"] = "train"
  61. elif index < n_train + n_val:
  62. item["split"] = "val"
  63. else:
  64. item["split"] = "eval"
  65. with SPLIT_PATH.open("w", encoding="utf-8", newline="") as handle:
  66. writer = csv.DictWriter(handle, fieldnames=["path", "part", "name", "split"])
  67. writer.writeheader()
  68. for item in sorted(samples, key=lambda item: item["path"]):
  69. writer.writerow(
  70. {"path": item["path"], "part": item["part"], "name": item["name"], "split": item["split"]}
  71. )
  72. return samples
  73. def _load_raw(path: str) -> np.ndarray:
  74. return np.load(path).astype(np.float32)
  75. def compute_stats(samples: list[dict]) -> tuple[np.ndarray, np.ndarray]:
  76. """Per-channel mean/std from the training split only (std==0 -> scale 1)."""
  77. train = [item for item in samples if item["split"] == "train"]
  78. sums = np.zeros(N_CHANNELS, dtype=np.float64)
  79. squares = np.zeros(N_CHANNELS, dtype=np.float64)
  80. total = 0
  81. for item in tqdm(train, desc="统计归一化参数"):
  82. values = _load_raw(item["path"])
  83. sums += values.sum(axis=0)
  84. squares += (values.astype(np.float64) ** 2).sum(axis=0)
  85. total += len(values)
  86. mean = (sums / total).astype(np.float32)
  87. variance = squares / total - (mean.astype(np.float64) ** 2)
  88. std = np.sqrt(np.clip(variance, 0, None)).astype(np.float32)
  89. std[std == 0] = 1.0
  90. return mean, std
  91. class CycleDataset(Dataset):
  92. def __init__(self, samples: list[dict], mean: np.ndarray, std: np.ndarray):
  93. self.mean = mean
  94. self.std = std
  95. self.items: list[tuple[dict, torch.Tensor, int]] = []
  96. for item in tqdm(samples, desc="加载样本到内存"):
  97. raw = _load_raw(item["path"])
  98. length = raw.shape[0]
  99. normalised = (raw - mean) / std
  100. padded = np.zeros((SEQ_LEN, N_CHANNELS), dtype=np.float32)
  101. padded[:length] = normalised
  102. self.items.append((item, torch.from_numpy(padded), length))
  103. def __len__(self) -> int:
  104. return len(self.items)
  105. def __getitem__(self, index: int) -> tuple[torch.Tensor, int]:
  106. _, tensor, length = self.items[index]
  107. return tensor, length
  108. def build_split_datasets() -> tuple[list[dict], np.ndarray, np.ndarray]:
  109. samples = ensure_split(discover_samples())
  110. mean, std = compute_stats(samples)
  111. return samples, mean, std