axay28's picture
Add model source
c407ed8 verified
Raw
History Blame Contribute Delete
4.47 kB
from __future__ import annotations
from pathlib import Path
import torch
from tqdm import trange
from tiny_transformer.config import ModelConfig, TrainConfig
from tiny_transformer.data import TextDataset, split_tokens
from tiny_transformer.model import TinyTransformer
from tiny_transformer.tokenizer import BytePairTokenizer, CharTokenizer, Tokenizer, tokenizer_from_dict
@torch.no_grad()
def estimate_loss(
model: TinyTransformer,
train_data: TextDataset,
val_data: TextDataset,
batch_size: int,
eval_batches: int,
) -> dict[str, float]:
model.eval()
losses: dict[str, float] = {}
for split, dataset in {"train": train_data, "val": val_data}.items():
split_losses = []
for _ in range(eval_batches):
x, y = dataset.get_batch(batch_size)
_, loss = model(x, y)
if loss is None:
raise RuntimeError("Expected a loss during evaluation")
split_losses.append(loss.item())
losses[split] = sum(split_losses) / len(split_losses)
model.train()
return losses
def train_from_text(
text: str,
model_config: ModelConfig | None = None,
train_config: TrainConfig | None = None,
device: str = "cpu",
tokenizer_name: str = "char",
bpe_vocab_size: int = 256,
) -> TinyTransformer:
train_config = train_config or TrainConfig()
torch.manual_seed(train_config.seed)
tokenizer = train_tokenizer(text, tokenizer_name, bpe_vocab_size)
token_ids = tokenizer.encode(text)
train_ids, val_ids = split_tokens(token_ids)
if model_config is None:
model_config = ModelConfig(vocab_size=tokenizer.vocab_size)
else:
model_config = ModelConfig(**{**model_config.to_dict(), "vocab_size": tokenizer.vocab_size})
train_data = TextDataset(train_ids, model_config.block_size, device)
val_data = TextDataset(val_ids, model_config.block_size, device)
model = TinyTransformer(model_config).to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=train_config.learning_rate)
device_type = "cuda" if device.startswith("cuda") else "mps" if device == "mps" else "cpu"
amp_enabled = train_config.use_amp and device_type in {"cuda", "mps"}
scaler = torch.amp.GradScaler("cuda", enabled=amp_enabled and device_type == "cuda")
progress = trange(train_config.max_steps, desc="training", leave=True)
for step in progress:
if step % train_config.eval_interval == 0 or step == train_config.max_steps - 1:
losses = estimate_loss(
model, train_data, val_data, train_config.batch_size, train_config.eval_batches
)
progress.set_postfix(train=f"{losses['train']:.3f}", val=f"{losses['val']:.3f}")
optimizer.zero_grad(set_to_none=True)
for _ in range(train_config.grad_accum_steps):
x, y = train_data.get_batch(train_config.batch_size)
with torch.autocast(device_type=device_type, enabled=amp_enabled):
_, loss = model(x, y)
if loss is None:
raise RuntimeError("Expected a loss during training")
loss = loss / train_config.grad_accum_steps
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
save_checkpoint(model, tokenizer, train_config.output_path)
return model
def train_tokenizer(text: str, tokenizer_name: str, bpe_vocab_size: int) -> Tokenizer:
if tokenizer_name == "char":
return CharTokenizer.train(text)
if tokenizer_name == "bpe":
return BytePairTokenizer.train(text, vocab_size=bpe_vocab_size)
raise ValueError("tokenizer_name must be 'char' or 'bpe'")
def save_checkpoint(model: TinyTransformer, tokenizer: Tokenizer, path: str) -> None:
output = Path(path)
output.parent.mkdir(parents=True, exist_ok=True)
torch.save(
{
"model_config": model.config.to_dict(),
"model_state": model.state_dict(),
"tokenizer": tokenizer.to_dict(),
},
output,
)
def load_checkpoint(path: str, device: str = "cpu") -> tuple[TinyTransformer, Tokenizer]:
payload = torch.load(path, map_location=device)
tokenizer = tokenizer_from_dict(payload["tokenizer"])
config = ModelConfig(**payload["model_config"])
model = TinyTransformer(config).to(device)
model.load_state_dict(payload["model_state"])
model.eval()
return model, tokenizer