| |
| """ |
| TinyGSM Dataset Initialization - Replace Test with GSM8K |
| 按正常流程生成 train/val/test,最后把 test.bin 替换为 GSM8K 测试集 |
| """ |
|
|
| import os |
| import re |
| import json |
| from pathlib import Path |
| from datasets import load_dataset |
| from tqdm import tqdm |
|
|
| from lmr.tokenizer import Tokenizer |
| from lmr.data.disk_dataset import DiskDataset |
|
|
| |
| TINYGSM_HF = "TinyGSM/TinyGSM" |
| GSM8K_HF = "openai/gsm8k" |
|
|
|
|
| def initialize_dataset(dataset_config, dataset_dir): |
| """ |
| Initialize TinyGSM dataset, then replace test with GSM8K. |
| |
| Steps: |
| 1. Load TinyGSM train split |
| 2. Split into train/val/test (normal process) |
| 3. Generate train.bin and validation.bin |
| 4. Load GSM8K test set |
| 5. Replace test.bin with GSM8K data |
| """ |
| from pathlib import Path |
| from lmr.tokenizer import Tokenizer |
| from lmr.data.disk_dataset import DiskDataset |
|
|
| dataset_dir = Path(dataset_dir) |
| print(f"\n{'='*60}") |
| print(f"Initializing TinyGSM dataset: {dataset_config.dataset_name}") |
| print(f"Process: TinyGSM train/val + GSM8K test") |
| print(f"{'='*60}\n") |
|
|
| tokenizer = Tokenizer.get_instance() |
| component_name = getattr(dataset_config, "component_name", "tinygsm") |
| |
| |
| component_whitelist = getattr(dataset_config, "component_whitelist", None) |
| if component_whitelist is not None and component_name not in component_whitelist: |
| print(f"Component {component_name} not on whitelist, skipping...") |
| return |
|
|
| |
| token_limits = { |
| "train": _parse_token_limit(getattr(dataset_config, "max_tokens_train", None)), |
| "validation": _parse_token_limit(getattr(dataset_config, "max_tokens_validation", None)), |
| "test": _parse_token_limit(getattr(dataset_config, "max_tokens_test", None)), |
| } |
| tokens_buffer = _parse_token_limit(getattr(dataset_config, "tokens_buffer", "10k")) |
|
|
| |
| component_dir = dataset_dir / dataset_config.dataset_name |
| component_dir.mkdir(parents=True, exist_ok=True) |
|
|
| print(f"Output directory: {component_dir}") |
| print(f"Token limits: {token_limits}") |
| print(f"Tokens buffer: {tokens_buffer}\n") |
|
|
| |
| for split_name in ("train", "validation"): |
| |
| token_limit = token_limits[split_name] |
| if token_limit is not None and tokens_buffer is not None: |
| token_limit += tokens_buffer |
|
|
| print(f"{'='*60}") |
| print(f"Processing {split_name} split from TinyGSM...") |
| print(f"Token limit (with buffer): {token_limit}") |
| print(f"{'='*60}") |
|
|
| try: |
| ds_stream = _load_tinygsm_split(split_name, dataset_config) |
| except Exception as e: |
| print(f"Warning: Could not load {split_name} split: {e}") |
| continue |
|
|
| output_path = component_dir / f"{split_name}.bin" |
| metadata_path = component_dir / f"metadata_{split_name}.json" |
|
|
| |
| ds_transformed = _transform_tinygsm_to_text(ds_stream) |
|
|
| |
| print(f"Generating binary file: {output_path}") |
| |
| DiskDataset.generate_bin( |
| ds_transformed, |
| tokenizer, |
| output_path, |
| token_limit=token_limit, |
| metadata_path=metadata_path, |
| pad_to_length = 1024 |
| ) |
|
|
| print(f"✓ Completed {split_name} split") |
| print(f" Binary file: {output_path}") |
| print(f" Metadata: {metadata_path}\n") |
|
|
| |
| split_name = "test" |
| token_limit = token_limits[split_name] |
| if token_limit is not None and tokens_buffer is not None: |
| token_limit += tokens_buffer |
|
|
| print(f"{'='*60}") |
| print(f"Replacing {split_name} split with GSM8K test set...") |
| print(f"Token limit (with buffer): {token_limit}") |
| print(f"{'='*60}") |
|
|
| try: |
| |
| print("Loading GSM8K test set from HuggingFace...") |
| ds_gsm8k_test = load_dataset(GSM8K_HF, "main", split="test", streaming=False) |
| print(f"✓ Loaded GSM8K test set: {len(ds_gsm8k_test)} samples\n") |
| except Exception as e: |
| print(f"❌ Error loading GSM8K test set: {e}") |
| print("Keeping TinyGSM test split...") |
| return |
|
|
| output_path = component_dir / "test.bin" |
| metadata_path = component_dir / "metadata_test.json" |
|
|
| |
| ds_transformed = _transform_gsm8k_to_text(ds_gsm8k_test) |
|
|
| |
| print(f"Generating binary file: {output_path}") |
| DiskDataset.generate_bin( |
| ds_transformed, |
| tokenizer, |
| output_path, |
| token_limit=token_limit, |
| metadata_path=metadata_path |
| ) |
|
|
| print(f"✓ Completed {split_name} split (GSM8K)") |
| print(f" Binary file: {output_path}") |
| print(f" Metadata: {metadata_path}\n") |
|
|
| print(f"{'='*60}") |
| print("✓ Dataset initialization complete!") |
| print(f" Train: TinyGSM") |
| print(f" Validation: TinyGSM") |
| print(f" Test: GSM8K (1319 samples)") |
| print(f"{'='*60}\n") |
|
|
|
|
| def _load_tinygsm_split(split_name, dataset_config): |
| """Load TinyGSM split, creating val from train if needed.""" |
| val_ratio = float(getattr(dataset_config, "val_ratio", 0.01)) |
|
|
| |
| available_splits = [] |
| try: |
| hf_all = load_dataset(TINYGSM_HF, split=None) |
| if isinstance(hf_all, dict): |
| available_splits = list(hf_all.keys()) |
| except Exception: |
| available_splits = [] |
|
|
| hf_name = "train" if split_name == "train" else split_name |
|
|
| |
| if hf_name in available_splits: |
| print(f"Using existing '{hf_name}' split (streaming mode)") |
| return load_dataset(TINYGSM_HF, split=hf_name, streaming=True) |
|
|
| |
| print(f"Split '{hf_name}' not found, loading 'train' and splitting locally") |
| ds_full = load_dataset(TINYGSM_HF, split="train", streaming=False) |
|
|
| if val_ratio <= 0: |
| if split_name == "train": |
| return ds_full.shuffle(seed=42) |
| else: |
| return iter([]) |
|
|
| |
| total = len(ds_full) |
| val_count = max(1, int(round(total * val_ratio))) |
| test_count = max(1, int(round(total * val_ratio))) |
|
|
| |
| split1 = ds_full.train_test_split(test_size=test_count, seed=42) |
| ds_remain = split1["train"] |
| ds_test = split1["test"] |
|
|
| split2 = ds_remain.train_test_split(test_size=val_count, seed=43) |
| ds_train = split2["train"] |
| ds_val = split2["test"] |
|
|
| |
| if split_name == "train": |
| return ds_train.shuffle(seed=44) |
| elif split_name == "validation": |
| return ds_val.shuffle(seed=45) |
| else: |
| return ds_test.shuffle(seed=46) |
|
|
|
|
| def _transform_tinygsm_to_text(dataset_stream): |
| """Transform TinyGSM samples to training format.""" |
| current = 0 |
| for sample in dataset_stream: |
| try: |
| current += 1 |
|
|
| if isinstance(sample, dict): |
| getf = sample.get |
| else: |
| getf = lambda k, default=None: getattr(sample, k, default) |
|
|
| question = (getf("question", None) or "").strip() |
| code = (getf("code", None) or "").strip() |
| answer = (getf("answer", None) or getf("final_answer", None) or "").strip() |
|
|
| if not question: |
| continue |
|
|
| text = _format_training_text(question, code, answer) |
|
|
| if current % 100000 == 0: |
| print(text[:200]) |
|
|
| yield {"text": text} |
|
|
| except Exception as e: |
| print(f"Warning: Error processing sample #{current}: {e}") |
| continue |
|
|
|
|
| def _transform_gsm8k_to_text(dataset): |
| """ |
| Transform GSM8K test set to TinyGSM training format. |
| |
| GSM8K format: |
| question: "Natalia sold clips..." |
| answer: "Step 1...\n#### 42" |
| |
| Convert to: |
| <|bos|>Question: ... |
| Solution: |
| # Step 1... |
| result = 42 |
| Answer: 42<|eos|> |
| """ |
| current = 0 |
| for sample in dataset: |
| try: |
| current += 1 |
|
|
| question = (sample.get("question", "") or "").strip() |
| answer_text = (sample.get("answer", "") or "").strip() |
|
|
| if not question or not answer_text: |
| continue |
|
|
| |
| final_answer = _extract_gsm8k_answer(answer_text) |
|
|
| |
| code = _convert_gsm8k_to_code(question, answer_text, final_answer) |
|
|
| |
| text = _format_training_text(question, code, final_answer) |
|
|
| if current % 100 == 0: |
| print(f"GSM8K sample {current}:") |
| print(text[:300]) |
| print("---") |
|
|
| yield {"text": text} |
|
|
| except Exception as e: |
| print(f"Warning: Error processing GSM8K sample #{current}: {e}") |
| continue |
|
|
|
|
| def _extract_gsm8k_answer(answer_text: str) -> str: |
| """ |
| Extract numerical answer from GSM8K format. |
| GSM8K uses "#### number" to mark the final answer. |
| |
| 支持多种格式: |
| - #### 42 |
| - #### 3.14 |
| - #### -5 |
| - #### 1,234 |
| """ |
| |
| match = re.search(r'####\s*([+-]?[\d,]+\.?\d*)', answer_text) |
| if match: |
| return match.group(1).replace(',', '').strip() |
| |
| |
| match = re.search(r'result\s*=\s*([+-]?[\d,]+\.?\d*)', answer_text, re.IGNORECASE) |
| if match: |
| return match.group(1).replace(',', '').strip() |
| |
| |
| numbers = re.findall(r'([+-]?[\d,]+\.?\d*)', answer_text) |
| if numbers: |
| return numbers[-1].replace(',', '').strip() |
| |
| return "0" |
|
|
|
|
| def _convert_gsm8k_to_code(question: str, answer_text: str, final_answer: str) -> str: |
| """ |
| Convert GSM8K natural language solution to Python-like code. |
| |
| Strategy: Extract reasoning steps as comments, add final calculation. |
| """ |
| |
| if "####" in answer_text: |
| reasoning = answer_text.split("####")[0].strip() |
| else: |
| reasoning = answer_text.strip() |
| |
| |
| code_lines = ["# Solution:"] |
| |
| |
| for line in reasoning.split('\n'): |
| line = line.strip() |
| if line: |
| |
| line = re.sub(r'<<[^>]+>>', '', line) |
| code_lines.append(f"# {line}") |
| |
| |
| if final_answer: |
| code_lines.append(f"\nresult = {final_answer}") |
| |
| return "\n".join(code_lines) |
|
|
|
|
| def _format_training_text(question, code, answer): |
| """ |
| Format training text in TinyGSM style: |
| |
| <|bos|>Question: ... |
| |
| Solution: |
| ```python |
| ... |
| ``` |
| |
| Answer: ...<|eos|> |
| """ |
| q = (question or "").strip() |
| c = (code or "").rstrip() |
| a = (answer or "").strip() |
|
|
| parts = [] |
| parts.append("<|bos|>Question: " + q) |
| parts.append("") |
|
|
| if c: |
| parts.append("Solution:") |
| parts.append("```python") |
| parts.append(c) |
| parts.append("```") |
| parts.append("") |
| else: |
| parts.append("Solution: ") |
| parts.append("") |
|
|
| if a: |
| parts.append("Answer: " + a) |
| else: |
| parts.append("Answer: ") |
|
|
| parts.append("<|eos|>") |
|
|
| return "\n".join(parts) |
|
|
|
|
| def _parse_token_limit(limit_str): |
| """Parse token limit string to integer.""" |
| if limit_str is None: |
| return None |
|
|
| s = str(limit_str).lower().strip() |
|
|
| if s in ("none", "null", "na", ""): |
| return None |
|
|
| try: |
| if s.endswith("b"): |
| return int(float(s[:-1]) * 1_000_000_000) |
| elif s.endswith("m"): |
| return int(float(s[:-1]) * 1_000_000) |
| elif s.endswith("k"): |
| return int(float(s[:-1]) * 1_000) |
| else: |
| return int(s) |
| except Exception: |
| print(f"Warning: Could not parse token limit '{limit_str}' -> treating as None") |
| return None |
|
|
|
|
| |
| if __name__ == "__main__": |
| from types import SimpleNamespace |
|
|
| config = SimpleNamespace( |
| dataset_name="tinygsm", |
| component_name="tinygsm", |
| max_tokens_train="50m", |
| max_tokens_validation=None, |
| max_tokens_test=None, |
| tokens_buffer="10k", |
| component_whitelist=None, |
| val_ratio=0.01 |
| ) |
|
|
| dataset_dir = Path("./data/processed") |
|
|
| print("=" * 60) |
| print("TinyGSM Dataset Initialization") |
| print("=" * 60) |
|
|
| initialize_dataset(config, dataset_dir) |
|
|
| print("\n" + "=" * 60) |
| print("✓ Initialization Complete!") |
| print("=" * 60) |