CoolFace
Modelpublic

LLM-course/chess-learning-v2

sourceHugging Facemitupdated 8mo agoView on Hugging Face
0likes19downloads
tokenizer.py359 linesDownload Raw Back to root
1"""2Custom Chess Tokenizer for the Chess Challenge.3 4This tokenizer treats each move as a single token using the extended UCI notation5from the Lichess dataset (e.g., WPe2e4, BNg8f6).6 7The dataset format uses:8- W/B prefix for White/Black9- Piece letter: P=Pawn, N=Knight, B=Bishop, R=Rook, Q=Queen, K=King10- Source and destination squares (e.g., e2e4)11- Special suffixes: (x)=capture, (+)=check, (+*)=checkmate, (o)/(O)=castling12"""13 14from __future__ import annotations15 16import json17import os18import re19from pathlib import Path20from typing import Dict, List, Optional, Tuple21 22from transformers import PreTrainedTokenizer23 24 25 26# Parse "WPe2e4(x+*)" etc.27_MOVE_RE = re.compile(28    r"^(?P<side>[WB])"29    r"(?P<piece>[PNBRQK])"30    r"(?P<src>[a-h][1-8])"31    r"(?P<dst>[a-h][1-8])"32    r"(?P<suffix>.*)$"33)34 35 36 37# Promotions like "=Q" or "=q"38_PROMO_RE = re.compile(r"=([QRBNqrbn])")39 40 41def _parse_suffix(suffix: str) -> Tuple[bool, bool, bool, Optional[str], Optional[str]]:42    """43    Returns:44      is_capture, is_check, is_mate, castle_kind, promo_piece45 46    castle_kind: "k" (kingside) or "q" (queenside) or None47    promo_piece: one of "q","r","b","n" or None48    """49    if not suffix:50        return False, False, False, None, None51 52    # Normalize 53    suf = suffix.strip()54 55    is_capture = "x" in suf56    is_check = "+" in suf57 58    # Mate indicator59    # We'll treat any "*" as mate.60    is_mate = "*" in suf61 62    # Castling: dataset uses (o)/(O) in the move string for king moves63    castle_kind = None64    if "(O)" in suf:65        castle_kind = "q"66    elif "(o)" in suf:67        castle_kind = "k"68 69    promo_piece = None70    m = _PROMO_RE.search(suf)71    if m:72        promo_piece = m.group(1).lower()73 74    return is_capture, is_check, is_mate, castle_kind, promo_piece75 76 77def _reindex_vocab(vocab: Dict[str, int]) -> Dict[str, int]:78    # sort by old id for stability79    items = sorted(vocab.items(), key=lambda kv: kv[1])80    return {tok: new_id for new_id, (tok, _) in enumerate(items)}81 82 83 84class ChessTokenizer(PreTrainedTokenizer):85    """86    A custom tokenizer for chess moves using extended UCI notation.87    88    This tokenizer maps each possible chess move to a unique token ID.89    The vocabulary is built from the training dataset to ensure all moves90    encountered during training have a corresponding token.91    92    Example:93        >>> tokenizer = ChessTokenizer()94        >>> tokenizer.encode("WPe2e4 BPe7e5")95        [1, 42, 87, 2]  # [BOS, e2e4, e7e5, EOS]96    """97    98    model_input_names = ["input_ids", "attention_mask"]99    vocab_files_names = {"vocab_file": "vocab.json"}100    101    # Special tokens102    PAD_TOKEN = "[PAD]"103    BOS_TOKEN = "[BOS]"104    EOS_TOKEN = "[EOS]"105    UNK_TOKEN = "[UNK]"106 107 108     # Component tokens109    SIDE_TOKENS = ("[W]", "[B]")110    PIECE_TOKENS = ("[P]", "[N]", "[B]", "[R]", "[Q]", "[K]")111    # flags112    FLAG_TOKENS = (113        "[x]",       # capture114        "[+]",       # check115        "[#]",       # mate116        "[O-O]",     # kingside castle marker (not required by evaluator)117        "[O-O-O]",   # queenside castle marker118        # promotions119        "[=q]", "[=r]", "[=b]", "[=n]",120    )   121    def __init__(122        self,123        vocab_file: Optional[str] = None,124        vocab: Optional[Dict[str, int]] = None,125        **kwargs,126    ):127        """128        Initialize the chess tokenizer.129        130        Args:131            vocab_file: Path to a JSON file containing the vocabulary mapping.132            vocab: Dictionary mapping tokens to IDs (alternative to vocab_file).133            **kwargs: Additional arguments passed to PreTrainedTokenizer.134        """135        # Initialize special tokens136        self._pad_token = self.PAD_TOKEN137        self._bos_token = self.BOS_TOKEN138        self._eos_token = self.EOS_TOKEN139        self._unk_token = self.UNK_TOKEN140 141        # Remove any duplicate special-token entries passed through kwargs142        # to avoid "multiple values for keyword" errors when loading from disk.143        kwargs.pop("pad_token", None)144        kwargs.pop("bos_token", None)145        kwargs.pop("eos_token", None)146        kwargs.pop("unk_token", None)147        148        # Load or create vocabulary149        if vocab is not None:150            self._vocab = vocab151        elif vocab_file is not None and os.path.exists(vocab_file):152            with open(vocab_file, "r", encoding="utf-8") as f:153                self._vocab = json.load(f)154        else:155            # Create a minimal vocabulary with just special tokens156            # The full vocabulary should be built from the dataset157            self._vocab = self._create_default_vocab()158        159        self._vocab = _reindex_vocab(self._vocab)160        161        # Create reverse mapping162        self._ids_to_tokens = {v: k for k, v in self._vocab.items()}163        164        # Call parent init AFTER setting up vocab165        super().__init__(166            pad_token=self._pad_token,167            bos_token=self._bos_token,168            eos_token=self._eos_token,169            unk_token=self._unk_token,170            **kwargs,171        )172    173    def _create_default_vocab(self) -> Dict[str, int]:174        """175        Create a minimal default vocabulary with just special tokens.176        177        For the full vocabulary, use `build_vocab_from_dataset()`.178        This minimal vocab is just a placeholder - you should build from data.179        """180        tokens: List[str] = [self.PAD_TOKEN, self.BOS_TOKEN, self.EOS_TOKEN, self.UNK_TOKEN]181        tokens += list(self.SIDE_TOKENS)182        tokens += list(self.PIECE_TOKENS)183 184        # Squares (64)185        for file in "abcdefgh":186            for rank in "12345678":187                tokens.append(f"[{file}{rank}]")188 189        tokens += list(self.FLAG_TOKENS)190 191        return {tok: idx for idx, tok in enumerate(tokens)}192    193    @classmethod194    def build_vocab_from_iterator(195        cls,196        iterator,197        min_frequency: int = 1,198    ) -> "ChessTokenizer":199        return cls()200    201    @classmethod202    def build_vocab_from_dataset(203        cls,204        dataset_name: str = "dlouapre/lichess_2025-01_1M",205        split: str = "train",206        column: str = "text",207        min_frequency: int = 500,208        max_samples: Optional[int] = 100000,209    ) -> "ChessTokenizer":210        return cls()211    212    @property213    def vocab_size(self) -> int:214        """Return the size of the vocabulary."""215        return len(self._vocab)216    217    def get_vocab(self) -> Dict[str, int]:218        """Return the vocabulary as a dictionary."""219        return dict(self._vocab)220    221    def _tokenize(self, text: str) -> List[str]:222        """223        Tokenize a string of moves into a list of tokens.224        225        Args:226            text: A string of space-separated moves.227        228        Returns:229            List of move tokens.230        """231        text = (text or "").strip()232        if not text:233            return []234 235        chunks = text.split()236        out: List[str] = []237 238        for chunk in chunks:239            # If chunk is pure uci like "e2e4" or "e7e8q"240            if re.fullmatch(r"[a-h][1-8][a-h][1-8][qrbn]?", chunk):241                src = chunk[0:2]242                dst = chunk[2:4]243                out.append(f"[{src}]")244                out.append(f"[{dst}]")245                if len(chunk) == 5 and chunk[4] in "qrbn":246                    out.append(f"[={chunk[4]}]")247                continue248 249            m = _MOVE_RE.match(chunk)250            if not m:251                out.append(self.UNK_TOKEN)252                continue253 254            side = "[W]" if m.group("side") == "W" else "[BL]"255            piece = m.group("piece")256            src = m.group("src")257            dst = m.group("dst")258            suffix = m.group("suffix") or ""259 260            out.append(side)261            out.append(f"[{piece}]")262            out.append(f"[{src}]")263            out.append(f"[{dst}]")264 265            is_cap, is_chk, is_mate, castle_kind, promo = _parse_suffix(suffix)266 267            # Castling markers (optional; evaluator doesn't need them)268            if castle_kind == "k":269                out.append("[O-O]")270            elif castle_kind == "q":271                out.append("[O-O-O]")272 273            if is_cap:274                out.append("[x]")275            if is_mate:276                out.append("[#]")277            elif is_chk:278                out.append("[+]")279 280            if promo in ("q", "r", "b", "n"):281                out.append(f"[={promo}]")282 283        return out284 285    286    def _convert_token_to_id(self, token: str) -> int:287        """Convert a token to its ID."""288        return self._vocab.get(token, self._vocab.get(self.UNK_TOKEN, 0))289    290    def _convert_id_to_token(self, index: int) -> str:291        """Convert an ID to its token."""292        return self._ids_to_tokens.get(index, self.UNK_TOKEN)293    294    def convert_tokens_to_string(self, tokens: List[str]) -> str:295        """Convert a list of tokens back to a string."""296        # Filter out special tokens for cleaner output297        special = {self.PAD_TOKEN, self.BOS_TOKEN, self.EOS_TOKEN, self.UNK_TOKEN}298        return " ".join(t for t in tokens if t not in special)299    300    def save_vocabulary(301        self,302        save_directory: str,303        filename_prefix: Optional[str] = None,304    ) -> tuple:305        """306        Save the vocabulary to a JSON file.307        308        Args:309            save_directory: Directory to save the vocabulary.310            filename_prefix: Optional prefix for the filename.311        312        Returns:313            Tuple containing the path to the saved vocabulary file.314        """315        if not os.path.isdir(save_directory):316            os.makedirs(save_directory, exist_ok=True)317        318        vocab_file = os.path.join(319            save_directory,320            (filename_prefix + "-" if filename_prefix else "") + "vocab.json",321        )322        323        with open(vocab_file, "w", encoding="utf-8") as f:324            json.dump(self._vocab, f, ensure_ascii=False, indent=2)325        326        return (vocab_file,)327 328 329def count_vocab_from_dataset(330    dataset_name: str = "dlouapre/lichess_2025-01_1M",331    split: str = "train",332    column: str = "text",333    max_samples: Optional[int] = 10000,334) -> Dict[str, int]:335    """336    Count token frequencies in a dataset (useful for vocabulary analysis).337    338    Args:339        dataset_name: Name of the dataset on Hugging Face Hub.340        split: Dataset split to use.341        column: Column containing the game strings.342        max_samples: Maximum number of samples to process.343    344    Returns:345        Dictionary mapping tokens to their frequencies.346    """347    from collections import Counter348    from datasets import load_dataset349 350    ds = load_dataset(dataset_name, split=split)351    if max_samples is not None:352        ds = ds.select(range(min(max_samples, len(ds))))353 354    tok = ChessTokenizer()355    counts = Counter()356    for ex in ds:357        counts.update(tok._tokenize(ex[column]))358    return dict(counts)359