CoolFace
Modelpublic

LLM-course/basic_tokenizer

sourceHugging Facemitupdated 8mo agoView on Hugging Face
0likes21downloads
tokenizer.py357 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 os18from pathlib import Path19from typing import Dict, List, Optional20 21from transformers import PreTrainedTokenizer22 23import re24 25 26 27class ChessTokenizer(PreTrainedTokenizer):28    """29    A custom tokenizer for chess moves using extended UCI notation.30    31    This tokenizer maps each possible chess move to a unique token ID.32    The vocabulary is built from the training dataset to ensure all moves33    encountered during training have a corresponding token.34    35    Example:36        >>> tokenizer = ChessTokenizer()37        >>> tokenizer.encode("WPe2e4 BPe7e5")38        [1, 42, 87, 2]  # [BOS, e2e4, e7e5, EOS]39    """40    41    model_input_names = ["input_ids", "attention_mask"]42    vocab_files_names = {"vocab_file": "vocab.json"}43    44    # Special tokens45    PAD_TOKEN = "[PAD]"46    BOS_TOKEN = "[BOS]"47    EOS_TOKEN = "[EOS]"48    UNK_TOKEN = "[UNK]"49    50    _MOVE_RE = re.compile(51        r'^(?P<color>[WB])(?P<piece>[PNBRQK])(?P<from>[a-h][1-8])(?P<to>[a-h][1-8])(?P<rest>.*)$'52    )53 54    _SUFFIX_MAP = {55        "(x)": "cap",56        "(+)": "check",57        "(+*)": "mate",58        "(o)": "castle_k",59        "(O)": "castle_q",60    }61 62    _PROMO_RE = re.compile(r'=?([QRBNqrbn])')  # accepts "=Q" or "q" style63 64 65    def __init__(66        self,67        vocab_file: Optional[str] = None,68        vocab: Optional[Dict[str, int]] = None,69        **kwargs,70    ):71        """72        Initialize the chess tokenizer.73        74        Args:75            vocab_file: Path to a JSON file containing the vocabulary mapping.76            vocab: Dictionary mapping tokens to IDs (alternative to vocab_file).77            **kwargs: Additional arguments passed to PreTrainedTokenizer.78        """79        # Initialize special tokens80        self._pad_token = self.PAD_TOKEN81        self._bos_token = self.BOS_TOKEN82        self._eos_token = self.EOS_TOKEN83        self._unk_token = self.UNK_TOKEN84 85        # Remove any duplicate special-token entries passed through kwargs86        # to avoid "multiple values for keyword" errors when loading from disk.87        kwargs.pop("pad_token", None)88        kwargs.pop("bos_token", None)89        kwargs.pop("eos_token", None)90        kwargs.pop("unk_token", None)91        92        # Load or create vocabulary93        if vocab is not None:94            self._vocab = vocab95        elif vocab_file is not None and os.path.exists(vocab_file):96            with open(vocab_file, "r", encoding="utf-8") as f:97                self._vocab = json.load(f)98        else:99            # Create a minimal vocabulary with just special tokens100            # The full vocabulary should be built from the dataset101            self._vocab = self._create_default_vocab()102        103        # Create reverse mapping104        self._ids_to_tokens = {v: k for k, v in self._vocab.items()}105        106        # Call parent init AFTER setting up vocab107        super().__init__(108            pad_token=self._pad_token,109            bos_token=self._bos_token,110            eos_token=self._eos_token,111            unk_token=self._unk_token,112            **kwargs,113        )114    115    def _decompose_move(self, tok: str) -> List[str]:116        """117        Convert e.g. 'WPe2e4(x)' -> ['WP', 'e2_f', 'e4_t', 'cap']118        """119        m = self._MOVE_RE.match(tok)120        if not m:121            return [self.UNK_TOKEN]122 123        color = m.group("color")124        piece = m.group("piece")125        from_sq = m.group("from")126        to_sq = m.group("to")127        rest = m.group("rest") or ""128 129        out = [f"{color}{piece}", f"{from_sq}_f", f"{to_sq}_t"]130 131        for raw, mapped in self._SUFFIX_MAP.items():132            if raw in rest:133                out.append(mapped)134 135        # Promotion (rare depending on dataset formatting, but safe)136        pm = self._PROMO_RE.search(rest)137        if pm:138            p = pm.group(1).lower()139            if p in ("q", "r", "b", "n"):140                out.append(f"promo_{p}")141 142        return out143    def _create_default_vocab(self) -> Dict[str, int]:144        """145        Create a minimal default vocabulary with just special tokens.146        147        For the full vocabulary, use `build_vocab_from_dataset()`.148        This minimal vocab is just a placeholder - you should build from data.149        """150        special_tokens = [self.PAD_TOKEN, self.BOS_TOKEN, self.EOS_TOKEN, self.UNK_TOKEN]151        vocab = {token: idx for idx, token in enumerate(special_tokens)}152        return vocab153    154    @classmethod155    def build_vocab_from_iterator(156        cls,157        iterator,158        min_frequency: int = 1,159    ) -> "ChessTokenizer":160        """161        Build a tokenizer vocabulary from an iterator of game strings.162        163        Args:164            iterator: An iterator yielding game strings (space-separated moves).165            min_frequency: Minimum frequency for a token to be included.166        167        Returns:168            A ChessTokenizer with the built vocabulary.169        """170        from collections import Counter171        172        token_counts = Counter()173        174        for game in iterator:175            moves = game.strip().split()176            token_counts.update(moves)177        178        # Filter by frequency179        tokens = [180            token for token, count in token_counts.items()181            if count >= min_frequency182        ]183        184        # Sort for reproducibility185        tokens = sorted(tokens)186        187        # Build vocabulary188        special_tokens = [cls.PAD_TOKEN, cls.BOS_TOKEN, cls.EOS_TOKEN, cls.UNK_TOKEN]189        vocab = {token: idx for idx, token in enumerate(special_tokens + tokens)}190        191        return cls(vocab=vocab)192    193    @classmethod194    def build_vocab_from_dataset(195        cls,196        dataset_name: str = "dlouapre/lichess_2025-01_1M",197        split: str = "train",198        column: str = "text",199        min_frequency: int = 500,200        max_samples: Optional[int] = 100000,201    ) -> "ChessTokenizer":202        """203        Build a tokenizer vocabulary from a Hugging Face dataset.204        205        Args:206            dataset_name: Name of the dataset on Hugging Face Hub.207            split: Dataset split to use.208            column: Column containing the game strings.209            min_frequency: Minimum frequency for a token to be included (default: 500).210            max_samples: Maximum number of samples to process (default: 100k).211        212        Returns:213            A ChessTokenizer with the built vocabulary.214        """215        from datasets import load_dataset216        217        dataset = load_dataset(dataset_name, split=split)218        219        if max_samples is not None:220            dataset = dataset.select(range(min(max_samples, len(dataset))))221        222        def game_iterator():223            for example in dataset:224                yield example[column]225        226        return cls.build_vocab_from_iterator(game_iterator(), min_frequency=min_frequency)227    228    @property229    def vocab_size(self) -> int:230        """Return the size of the vocabulary."""231        return len(self._vocab)232    233    def get_vocab(self) -> Dict[str, int]:234        """Return the vocabulary as a dictionary."""235        return dict(self._vocab)236    237    # def _tokenize(self, text: str) -> List[str]:238    #     """239    #     Tokenize a string of moves into a list of tokens.240        241    #     Args:242    #         text: A string of space-separated moves.243        244    #     Returns:245    #         List of move tokens.246    #     """247    #     return text.strip().split()248    def _tokenize(self, text: str) -> List[str]:249        parts = text.strip().split()250        out: List[str] = []251        special = {self.PAD_TOKEN, self.BOS_TOKEN, self.EOS_TOKEN, self.UNK_TOKEN}252 253        for p in parts:254            if p in special:255                out.append(p)256            else:257                out.extend(self._decompose_move(p))258        return out259 260 261    @classmethod262    def build_structured_vocab(cls) -> "ChessTokenizer":263        special = [cls.PAD_TOKEN, cls.BOS_TOKEN, cls.EOS_TOKEN, cls.UNK_TOKEN]264 265        # 12 color+piece tokens266        cp = [f"{c}{p}" for c in ("W", "B") for p in ("P", "N", "B", "R", "Q", "K")]267 268        files = "abcdefgh"269        ranks = "12345678"270 271        from_tokens = [f"{f}{r}_f" for f in files for r in ranks]  # 64272        to_tokens = [f"{f}{r}_t" for f in files for r in ranks]    # 64273 274        suffix = ["cap", "check", "mate", "castle_k", "castle_q"]275        promo = [f"promo_{p}" for p in ("q", "r", "b", "n")]276 277        tokens = special + cp + from_tokens + to_tokens + suffix + promo278        vocab = {t: i for i, t in enumerate(tokens)}279        return cls(vocab=vocab)280 281    def _convert_token_to_id(self, token: str) -> int:282        """Convert a token to its ID."""283        return self._vocab.get(token, self._vocab.get(self.UNK_TOKEN, 0))284    285    def _convert_id_to_token(self, index: int) -> str:286        """Convert an ID to its token."""287        return self._ids_to_tokens.get(index, self.UNK_TOKEN)288    289    def convert_tokens_to_string(self, tokens: List[str]) -> str:290        """Convert a list of tokens back to a string."""291        # Filter out special tokens for cleaner output292        special = {self.PAD_TOKEN, self.BOS_TOKEN, self.EOS_TOKEN, self.UNK_TOKEN}293        return " ".join(t for t in tokens if t not in special)294    295    def save_vocabulary(296        self,297        save_directory: str,298        filename_prefix: Optional[str] = None,299    ) -> tuple:300        """301        Save the vocabulary to a JSON file.302        303        Args:304            save_directory: Directory to save the vocabulary.305            filename_prefix: Optional prefix for the filename.306        307        Returns:308            Tuple containing the path to the saved vocabulary file.309        """310        if not os.path.isdir(save_directory):311            os.makedirs(save_directory, exist_ok=True)312        313        vocab_file = os.path.join(314            save_directory,315            (filename_prefix + "-" if filename_prefix else "") + "vocab.json",316        )317        318        with open(vocab_file, "w", encoding="utf-8") as f:319            json.dump(self._vocab, f, ensure_ascii=False, indent=2)320        321        return (vocab_file,)322 323 324def count_vocab_from_dataset(325    dataset_name: str = "dlouapre/lichess_2025-01_1M",326    split: str = "train",327    column: str = "text",328    max_samples: Optional[int] = 10000,329) -> Dict[str, int]:330    """331    Count token frequencies in a dataset (useful for vocabulary analysis).332    333    Args:334        dataset_name: Name of the dataset on Hugging Face Hub.335        split: Dataset split to use.336        column: Column containing the game strings.337        max_samples: Maximum number of samples to process.338    339    Returns:340        Dictionary mapping tokens to their frequencies.341    """342    from collections import Counter343    from datasets import load_dataset344    345    dataset = load_dataset(dataset_name, split=split)346    347    if max_samples is not None:348        dataset = dataset.select(range(min(max_samples, len(dataset))))349    350    token_counts = Counter()351    352    for example in dataset:353        moves = example[column].strip().split()354        token_counts.update(moves)355    356    return dict(token_counts)357