FST_code / src /lmr /data /tinygsm_no_slides_bert.py
jasonfan's picture
2026-03-19
3b2d368 verified
Raw
History Blame Contribute Delete
13 kB
#!/usr/bin/env python3
"""
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
# Dataset sources
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")
# Optional whitelist
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
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"))
# Create output directory
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")
# ===== Step 1-3: Process TinyGSM train and validation =====
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"
# Transform to text format
ds_transformed = _transform_tinygsm_to_text(ds_stream)
# Generate binary file
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")
# ===== Step 4-5: Replace test with GSM8K =====
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:
# Load GSM8K test
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"
# Transform GSM8K to text format
ds_transformed = _transform_gsm8k_to_text(ds_gsm8k_test)
# Generate binary file (replaces any existing test.bin)
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))
# Try to get available splits
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 split exists, use streaming
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)
# Otherwise, load train and split locally
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([])
# Compute validation count
total = len(ds_full)
val_count = max(1, int(round(total * val_ratio)))
test_count = max(1, int(round(total * val_ratio))) # Same size as val
# Split
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"]
# Return requested split
if split_name == "train":
return ds_train.shuffle(seed=44)
elif split_name == "validation":
return ds_val.shuffle(seed=45)
else: # test (will be replaced by GSM8K later)
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
# Extract numerical answer from GSM8K format
final_answer = _extract_gsm8k_answer(answer_text)
# Convert to code format
code = _convert_gsm8k_to_code(question, answer_text, final_answer)
# Format in TinyGSM style
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
"""
# Method 1: #### pattern (primary)
match = re.search(r'####\s*([+-]?[\d,]+\.?\d*)', answer_text)
if match:
return match.group(1).replace(',', '').strip()
# Method 2: "result =" pattern
match = re.search(r'result\s*=\s*([+-]?[\d,]+\.?\d*)', answer_text, re.IGNORECASE)
if match:
return match.group(1).replace(',', '').strip()
# Method 3: Last number (fallback)
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.
"""
# Extract reasoning (text before ####)
if "####" in answer_text:
reasoning = answer_text.split("####")[0].strip()
else:
reasoning = answer_text.strip()
# Build code
code_lines = ["# Solution:"]
# Add reasoning steps as comments
for line in reasoning.split('\n'):
line = line.strip()
if line:
# Remove repeated equals signs (<<...>>)
line = re.sub(r'<<[^>]+>>', '', line)
code_lines.append(f"# {line}")
# Add final result
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
# Standalone execution
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 # 1% for validation
)
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)