| """v9 YAML-BERT dataset: BPE-expand each linearizer node into subword |
| positions, mask whole logical KEYs, emit per-logical-node atomic labels.""" |
| from __future__ import annotations |
|
|
| import random |
| import re |
|
|
| import torch |
| from torch.utils.data import Dataset |
|
|
| from yaml_bert.config import YamlBertConfig |
| from yaml_bert.subtree_masking import descendants_of |
| from yaml_bert.types import NodeType, YamlNode |
| from yaml_bert.vocab import Vocabulary |
|
|
|
|
| _LIST_INDEX_RE = re.compile(r"\.\d+$") |
|
|
|
|
| def _strip_trailing_list_index(path: str) -> str: |
| return _LIST_INDEX_RE.sub("", path) |
|
|
|
|
| def compute_children_info(nodes: list[YamlNode]) -> dict: |
| """Same as v8 — operates on LOGICAL nodes (not subwords).""" |
| n = len(nodes) |
| full_path_of: list[str] = [] |
| for node in nodes: |
| if node.parent_path: |
| full_path_of.append(f"{node.parent_path}.{node.token}") |
| else: |
| full_path_of.append(node.token) |
|
|
| key_positions: list[int] = [ |
| i for i, node in enumerate(nodes) |
| if node.node_type in (NodeType.KEY, NodeType.LIST_KEY) |
| ] |
| path_to_key_pos: dict[str, int] = { |
| full_path_of[p]: p for p in key_positions |
| } |
|
|
| children_of: list[list[int]] = [[] for _ in range(n)] |
| parent_of: list[int] = [-1] * n |
| depth_of: list[int] = [node.depth for node in nodes] |
|
|
| for p in key_positions: |
| parent_path = nodes[p].parent_path |
| if not parent_path: |
| continue |
| parent_pos = path_to_key_pos.get(parent_path) |
| if parent_pos is None: |
| stripped = _strip_trailing_list_index(parent_path) |
| if stripped != parent_path: |
| parent_pos = path_to_key_pos.get(stripped) |
| if parent_pos is not None: |
| parent_of[p] = parent_pos |
| children_of[parent_pos].append(p) |
|
|
| return { |
| "children_of": children_of, |
| "parent_of": parent_of, |
| "key_positions": key_positions, |
| "depth_of": depth_of, |
| "full_path_of": full_path_of, |
| } |
|
|
|
|
| _NODE_TYPE_INDEX = { |
| NodeType.KEY: 0, |
| NodeType.VALUE: 1, |
| NodeType.LIST_KEY: 2, |
| NodeType.LIST_VALUE: 3, |
| } |
| _MASKABLE_TYPES = (NodeType.KEY, NodeType.LIST_KEY) |
|
|
|
|
| class YamlBertDataset(Dataset): |
| """v9 dataset: subword expansion + whole-key MLM masking + recon.""" |
|
|
| def __init__( |
| self, |
| documents: list[list[YamlNode]], |
| vocab: Vocabulary, |
| config: YamlBertConfig, |
| ) -> None: |
| self.documents = documents |
| self.vocab = vocab |
| self.mask_prob = config.mask_prob |
| self.max_seq_len = config.max_seq_len |
| self.recon_enabled = config.recon_enabled |
|
|
| self._cached_children_info: list[dict] = [] |
| self._cached_descendants: list[dict[int, set[int]] | None] = [] |
| for doc in documents: |
| |
| |
| ci = compute_children_info(doc) |
| self._cached_children_info.append(ci) |
| if self.recon_enabled: |
| desc_cache: dict[int, set[int]] = {} |
| for kp in ci["key_positions"]: |
| if ci["children_of"][kp]: |
| desc_cache[kp] = descendants_of(kp, ci["children_of"]) |
| self._cached_descendants.append(desc_cache) |
| else: |
| self._cached_descendants.append(None) |
|
|
| def __len__(self) -> int: |
| return len(self.documents) |
|
|
| def __getitem__(self, idx: int) -> dict: |
| nodes = self.documents[idx] |
|
|
| |
| sub_token_ids: list[int] = [] |
| sub_node_types: list[int] = [] |
| sub_depths: list[int] = [] |
| sub_sibling: list[int] = [] |
| sub_logical_ids: list[int] = [] |
| per_logical_subword_spans: list[tuple[int, int]] = [] |
| |
| |
| kept_logical: int = 0 |
| for logical_idx, node in enumerate(nodes): |
| is_value = node.node_type in (NodeType.VALUE, NodeType.LIST_VALUE) |
| ids = self.vocab.encode_token(node.token, is_value=is_value) |
| if len(sub_token_ids) + len(ids) > self.max_seq_len: |
| break |
| start = len(sub_token_ids) |
| sub_token_ids.extend(ids) |
| sub_node_types.extend([_NODE_TYPE_INDEX[node.node_type]] * len(ids)) |
| sub_depths.extend([min(node.depth, 15)] * len(ids)) |
| sub_sibling.extend([min(node.sibling_index, 31)] * len(ids)) |
| sub_logical_ids.extend([kept_logical] * len(ids)) |
| per_logical_subword_spans.append((start, start + len(ids))) |
| kept_logical += 1 |
|
|
| n_logical = kept_logical |
| |
| ci = self._cached_children_info[idx] |
| kept_set = set(range(n_logical)) |
| children_of_t = [ |
| [c for c in ci["children_of"][p] if c in kept_set] |
| for p in range(n_logical) |
| ] |
| parent_of_t = [ |
| ci["parent_of"][p] if (ci["parent_of"][p] in kept_set or ci["parent_of"][p] == -1) else -1 |
| for p in range(n_logical) |
| ] |
| key_positions_t = [p for p in ci["key_positions"] if p < n_logical] |
| depth_of_t = ci["depth_of"][:n_logical] |
| full_path_of_t = ci["full_path_of"][:n_logical] |
| ci_t = { |
| "children_of": children_of_t, |
| "parent_of": parent_of_t, |
| "key_positions": key_positions_t, |
| "depth_of": depth_of_t, |
| "full_path_of": full_path_of_t, |
| } |
|
|
| |
| atomic_labels: list[int] = [-100] * n_logical |
| mask_id = self.vocab.mask_id |
| unk_id = self.vocab.unk_id |
| subword_vocab_size = self.vocab.subword_vocab_size |
|
|
| mlm_masked_positions: set[int] = set() |
| for logical_idx in key_positions_t: |
| if random.random() >= self.mask_prob: |
| continue |
| tok = nodes[logical_idx].token |
| atomic_id = self.vocab.encode_atomic_target(tok) |
| if atomic_id == unk_id: |
| continue |
| atomic_labels[logical_idx] = atomic_id |
| mlm_masked_positions.add(logical_idx) |
| r = random.random() |
| start, end = per_logical_subword_spans[logical_idx] |
| if r < 0.8: |
| for p in range(start, end): |
| sub_token_ids[p] = mask_id |
| elif r < 0.9: |
| for p in range(start, end): |
| sub_token_ids[p] = random.randint(4, subword_vocab_size - 1) |
|
|
| result = { |
| "token_ids": torch.tensor(sub_token_ids, dtype=torch.long), |
| "node_types": torch.tensor(sub_node_types, dtype=torch.long), |
| "depths": torch.tensor(sub_depths, dtype=torch.long), |
| "sibling_indices": torch.tensor(sub_sibling, dtype=torch.long), |
| "logical_ids": torch.tensor(sub_logical_ids, dtype=torch.long), |
| "atomic_labels": torch.tensor(atomic_labels, dtype=torch.long), |
| "children_info": ci_t, |
| "n_logical": n_logical, |
| } |
|
|
| if self.recon_enabled: |
| from yaml_bert.subtree_masking import pick_subtrees, bag_of_keys_target |
| picked_roots = pick_subtrees( |
| N=n_logical, |
| key_positions=key_positions_t, |
| depth_of=depth_of_t, |
| children_of=children_of_t, |
| mlm_masked_positions=mlm_masked_positions, |
| rng=random, |
| descendants_cache={ |
| kp: descendants_of(kp, children_of_t) |
| for kp in key_positions_t |
| if children_of_t[kp] |
| }, |
| ) |
| subtree_mask = torch.zeros(n_logical, dtype=torch.bool) |
| picked_positions_all: set[int] = set() |
| bag_targets: list[torch.Tensor] = [] |
| position_to_key_str = { |
| i: nodes[i].token for i in key_positions_t |
| } |
| for root_pos in picked_roots: |
| descs = { |
| d for d in descendants_of(root_pos, children_of_t) |
| if d < n_logical |
| } |
| picked_positions_all |= descs |
| bag_targets.append(bag_of_keys_target( |
| subtree_positions=descs, |
| position_to_key_str=position_to_key_str, |
| atomic_vocab=self.vocab.atomic_target_vocab, |
| vocab_size=self.vocab.atomic_target_vocab_size, |
| )) |
| |
| for lpos in picked_positions_all: |
| subtree_mask[lpos] = True |
| start, end = per_logical_subword_spans[lpos] |
| for p in range(start, end): |
| sub_token_ids[p] = mask_id |
| result["token_ids"] = torch.tensor(sub_token_ids, dtype=torch.long) |
| result["subtree_mask"] = subtree_mask |
| result["subtree_roots"] = picked_roots |
| result["bag_of_keys_targets"] = bag_targets |
| result["_atomic_vocab_size"] = self.vocab.atomic_target_vocab_size |
|
|
| return result |
|
|
|
|
| _COLLATE_NON_TENSOR_KEYS = frozenset({ |
| "children_info", |
| "subtree_roots", |
| "bag_of_keys_targets", |
| "subtree_mask", |
| "_atomic_vocab_size", |
| "n_logical", |
| }) |
|
|
|
|
| def collate_fn(batch: list[dict]) -> dict: |
| """Pad subword-level tensors AND logical-level tensors. |
| |
| Subword-level (per-position): token_ids, node_types, depths, sibling_indices, |
| logical_ids — padded to max subword length |
| Logical-level (per-logical-node): atomic_labels — padded to max logical count |
| """ |
| max_sub_len = max(item["token_ids"].size(0) for item in batch) |
| max_logical = max(item["n_logical"] for item in batch) |
|
|
| subword_keys = ("token_ids", "node_types", "depths", "sibling_indices", |
| "logical_ids") |
| padded_sub: dict[str, list[torch.Tensor]] = {k: [] for k in subword_keys} |
| padded_labels: list[torch.Tensor] = [] |
| padding_masks: list[torch.Tensor] = [] |
| batch_info: list[dict] = [] |
| n_logical_per_doc: list[int] = [] |
|
|
| for item in batch: |
| sub_len = item["token_ids"].size(0) |
| pad_sub = max_sub_len - sub_len |
| for k in subword_keys: |
| pad_value = -1 if k == "logical_ids" else 0 |
| if pad_sub > 0: |
| padding = torch.full((pad_sub,), pad_value, dtype=torch.long) |
| padded_sub[k].append(torch.cat([item[k], padding])) |
| else: |
| padded_sub[k].append(item[k]) |
|
|
| labels = item["atomic_labels"] |
| pad_lab = max_logical - labels.size(0) |
| if pad_lab > 0: |
| padded_labels.append(torch.cat([ |
| labels, torch.full((pad_lab,), -100, dtype=torch.long), |
| ])) |
| else: |
| padded_labels.append(labels) |
|
|
| mask = torch.cat([ |
| torch.zeros(sub_len, dtype=torch.bool), |
| torch.ones(pad_sub, dtype=torch.bool), |
| ]) if pad_sub > 0 else torch.zeros(sub_len, dtype=torch.bool) |
| padding_masks.append(mask) |
| batch_info.append(item["children_info"]) |
| n_logical_per_doc.append(item["n_logical"]) |
|
|
| result = {k: torch.stack(v) for k, v in padded_sub.items()} |
| result["atomic_labels"] = torch.stack(padded_labels) |
| result["padding_mask"] = torch.stack(padding_masks) |
| result["batch_info"] = batch_info |
| result["n_logical_per_doc"] = torch.tensor(n_logical_per_doc, dtype=torch.long) |
|
|
| |
| B = len(batch) |
| L = max_logical |
| parent_of_tensor = torch.full((B, L), -1, dtype=torch.long) |
| top_level_key_mask = torch.zeros((B, L), dtype=torch.bool) |
| for b_idx, info in enumerate(batch_info): |
| parent_of = info["parent_of"] |
| n_b = len(parent_of) |
| if n_b > 0: |
| parent_of_tensor[b_idx, :n_b] = torch.tensor(parent_of, dtype=torch.long) |
| depth_of = info["depth_of"] |
| depth_zero_kps = [kp for kp in info["key_positions"] if depth_of[kp] == 0] |
| if depth_zero_kps: |
| top_level_key_mask[b_idx, depth_zero_kps] = True |
|
|
| edges_by_depth: dict[int, list[tuple[int, int, int]]] = {} |
| parents_set_by_depth: dict[int, set[tuple[int, int]]] = {} |
| for b_idx, info in enumerate(batch_info): |
| children_of = info["children_of"] |
| depth_of = info["depth_of"] |
| for parent_pos in info["key_positions"]: |
| kids = children_of[parent_pos] |
| if not kids: |
| continue |
| parent_depth = depth_of[parent_pos] |
| edges_by_depth.setdefault(parent_depth, []).extend( |
| (b_idx, child_pos, parent_pos) for child_pos in kids |
| ) |
| parents_set_by_depth.setdefault(parent_depth, set()).add( |
| (b_idx, parent_pos), |
| ) |
|
|
| result["parent_of_tensor"] = parent_of_tensor |
| result["top_level_key_mask"] = top_level_key_mask |
| result["edges_by_depth"] = { |
| d: torch.tensor(edges, dtype=torch.long) |
| for d, edges in edges_by_depth.items() |
| } |
| result["parents_by_depth"] = { |
| d: torch.tensor(sorted(parents_set), dtype=torch.long) |
| for d, parents_set in parents_set_by_depth.items() |
| } |
|
|
| if "subtree_mask" in batch[0]: |
| subtree_masks: list[torch.Tensor] = [] |
| for item in batch: |
| sm = item["subtree_mask"] |
| pad_len = max_logical - sm.size(0) |
| if pad_len > 0: |
| subtree_masks.append(torch.cat([ |
| sm, torch.zeros(pad_len, dtype=torch.bool), |
| ])) |
| else: |
| subtree_masks.append(sm) |
| result["subtree_mask"] = torch.stack(subtree_masks) |
|
|
| flat_roots: list[tuple[int, int]] = [] |
| flat_targets: list[torch.Tensor] = [] |
| for b_idx, item in enumerate(batch): |
| for root_pos, target in zip( |
| item["subtree_roots"], item["bag_of_keys_targets"] |
| ): |
| flat_roots.append((b_idx, root_pos)) |
| flat_targets.append(target) |
| if flat_roots: |
| result["subtree_roots_flat"] = torch.tensor( |
| flat_roots, dtype=torch.long, |
| ) |
| result["bag_of_keys_targets_flat"] = torch.stack(flat_targets) |
| else: |
| result["subtree_roots_flat"] = torch.zeros((0, 2), dtype=torch.long) |
| v = batch[0].get("_atomic_vocab_size", 0) |
| result["bag_of_keys_targets_flat"] = torch.zeros( |
| (0, v), dtype=torch.float, |
| ) |
|
|
| return result |
|
|