CoolFace
Modelpublic

LLM-course/amine-final

sourceHugging Facemitupdated 8mo agoView on Hugging Face
0likes28downloads
tokenizer.py351 linesDownload Raw Back to root
1"""2Custom Chess Tokenizer for the Chess Challenge.3 4This tokenizer uses a DECOMPOSED format compatible with the evaluator:5    "WPe2e4" -> ["WP", "e2_f", "e4_t"]6 7The decomposed format uses:8- Piece token: "WP", "BN", etc. (color + piece)9- Source square with _f suffix: "e2_f", "g1_f", etc.10- Destination square with _t suffix: "e4_t", "f3_t", etc.11- Optional suffix for annotations: "(x)", "(+)", "(+*)", "(o)", "(O)"12 13The dataset format uses:14- W/B prefix for White/Black15- Piece letter: P=Pawn, N=Knight, B=Bishop, R=Rook, Q=Queen, K=King16- Source and destination squares (e.g., e2e4)17- Special suffixes: (x)=capture, (+)=check, (+*)=checkmate, (o)/(O)=castling18"""19 20from __future__ import annotations21 22import json23import os24from pathlib import Path25from typing import Dict, List, Optional26 27from transformers import PreTrainedTokenizer28 29 30class ChessTokenizer(PreTrainedTokenizer):31    """32    A custom tokenizer for chess moves using DECOMPOSED format.33 34    This tokenizer decomposes each move into sub-tokens:35    - Piece: "WP", "BN", etc.36    - Source square with _f suffix: "e2_f", "g1_f", etc.37    - Destination square with _t suffix: "e4_t", "f3_t", etc.38    - Optional suffix: "(x)", "(+)", etc.39 40    This format is compatible with the evaluator's 'decomposed' detection.41 42    Example:43        >>> tokenizer = ChessTokenizer.build_vocab_from_dataset()44        >>> tokenizer.tokenize("WPe2e4 BPe7e5")45        ['WP', 'e2_f', 'e4_t', 'BP', 'e7_f', 'e5_t']46    """47    48    model_input_names = ["input_ids", "attention_mask"]49    vocab_files_names = {"vocab_file": "vocab.json"}50    51    # Special tokens52    PAD_TOKEN = "[PAD]"53    BOS_TOKEN = "[BOS]"54    EOS_TOKEN = "[EOS]"55    UNK_TOKEN = "[UNK]"56    57    def __init__(58        self,59        vocab_file: Optional[str] = None,60        vocab: Optional[Dict[str, int]] = None,61        **kwargs,62    ):63        """64        Initialize the chess tokenizer.65        66        Args:67            vocab_file: Path to a JSON file containing the vocabulary mapping.68            vocab: Dictionary mapping tokens to IDs (alternative to vocab_file).69            **kwargs: Additional arguments passed to PreTrainedTokenizer.70        """71        # Initialize special tokens72        self._pad_token = self.PAD_TOKEN73        self._bos_token = self.BOS_TOKEN74        self._eos_token = self.EOS_TOKEN75        self._unk_token = self.UNK_TOKEN76 77        # Remove any duplicate special-token entries passed through kwargs78        # to avoid "multiple values for keyword" errors when loading from disk.79        kwargs.pop("pad_token", None)80        kwargs.pop("bos_token", None)81        kwargs.pop("eos_token", None)82        kwargs.pop("unk_token", None)83        84        # Load or create vocabulary85        if vocab is not None:86            self._vocab = vocab87        elif vocab_file is not None and os.path.exists(vocab_file):88            with open(vocab_file, "r", encoding="utf-8") as f:89                self._vocab = json.load(f)90        else:91            # Create a minimal vocabulary with just special tokens92            # The full vocabulary should be built from the dataset93            self._vocab = self._create_default_vocab()94        95        # Create reverse mapping96        self._ids_to_tokens = {v: k for k, v in self._vocab.items()}97        98        # Call parent init AFTER setting up vocab99        super().__init__(100            pad_token=self._pad_token,101            bos_token=self._bos_token,102            eos_token=self._eos_token,103            unk_token=self._unk_token,104            **kwargs,105        )106    107    def _create_default_vocab(self) -> Dict[str, int]:108        """109        Create a minimal default vocabulary with just special tokens.110        111        For the full vocabulary, use `build_vocab_from_dataset()`.112        This minimal vocab is just a placeholder - you should build from data.113        """114        special_tokens = [self.PAD_TOKEN, self.BOS_TOKEN, self.EOS_TOKEN, self.UNK_TOKEN]115        vocab = {token: idx for idx, token in enumerate(special_tokens)}116        return vocab117    118    @classmethod119    def build_vocab_from_iterator(120        cls,121        iterator,122        min_frequency: int = 1,123    ) -> "ChessTokenizer":124        """125        Build a tokenizer vocabulary from an iterator of game strings.126 127        Decomposes each move into tokens: piece, source_f, dest_t, and optional suffix.128 129        Args:130            iterator: An iterator yielding game strings (space-separated moves).131            min_frequency: Minimum frequency for a token to be included.132 133        Returns:134            A ChessTokenizer with the built vocabulary.135        """136        from collections import Counter137 138        token_counts = Counter()139 140        for game in iterator:141            moves = game.strip().split()142            for move in moves:143                if len(move) < 6:144                    token_counts[move] += 1145                    continue146 147                # Decompose move into tokens148                piece = move[:2]           # e.g., "WP", "BN"149                source = move[2:4] + "_f"  # e.g., "e2_f"150                dest = move[4:6] + "_t"    # e.g., "e4_t"151                suffix = move[6:] if len(move) > 6 else None152 153                token_counts[piece] += 1154                token_counts[source] += 1155                token_counts[dest] += 1156                if suffix:157                    token_counts[suffix] += 1158 159        # Filter by frequency160        tokens = [161            token for token, count in token_counts.items()162            if count >= min_frequency163        ]164 165        # Sort for reproducibility166        tokens = sorted(tokens)167 168        # Build vocabulary169        special_tokens = [cls.PAD_TOKEN, cls.BOS_TOKEN, cls.EOS_TOKEN, cls.UNK_TOKEN]170        vocab = {token: idx for idx, token in enumerate(special_tokens + tokens)}171 172        return cls(vocab=vocab)173    174    @classmethod175    def build_vocab_from_dataset(176        cls,177        dataset_name: str = "dlouapre/lichess_2025-01_1M",178        split: str = "train",179        column: str = "text",180        min_frequency: int = 500,181        max_samples: Optional[int] = 100000,182    ) -> "ChessTokenizer":183        """184        Build a tokenizer vocabulary from a Hugging Face dataset.185        186        Args:187            dataset_name: Name of the dataset on Hugging Face Hub.188            split: Dataset split to use.189            column: Column containing the game strings.190            min_frequency: Minimum frequency for a token to be included (default: 500).191            max_samples: Maximum number of samples to process (default: 100k).192        193        Returns:194            A ChessTokenizer with the built vocabulary.195        """196        from datasets import load_dataset197        198        dataset = load_dataset(dataset_name, split=split)199        200        if max_samples is not None:201            dataset = dataset.select(range(min(max_samples, len(dataset))))202        203        def game_iterator():204            for example in dataset:205                yield example[column]206        207        return cls.build_vocab_from_iterator(game_iterator(), min_frequency=min_frequency)208    209    @property210    def vocab_size(self) -> int:211        """Return the size of the vocabulary."""212        return len(self._vocab)213    214    def get_vocab(self) -> Dict[str, int]:215        """Return the vocabulary as a dictionary."""216        return dict(self._vocab)217    218    def _tokenize(self, text: str) -> List[str]:219        """220        Tokenize a string of moves into decomposed tokens.221 222        Each move like "WPe2e4" becomes ["WP", "e2_f", "e4_t"].223        Moves with suffixes like "WPe2e4(x)" become ["WP", "e2_f", "e4_t", "(x)"].224 225        Args:226            text: A string of space-separated moves.227 228        Returns:229            List of decomposed tokens.230        """231        moves = text.strip().split()232        tokens = []233 234        for move in moves:235            if len(move) < 6:236                # Invalid move format, add as unknown237                tokens.append(move)238                continue239 240            # Split move into components241            piece = move[:2]           # e.g., "WP", "BN"242            source = move[2:4] + "_f"  # e.g., "e2_f", "g1_f"243            dest = move[4:6] + "_t"    # e.g., "e4_t", "f3_t"244            suffix = move[6:] if len(move) > 6 else None  # e.g., "(x)", "(+)"245 246            tokens.extend([piece, source, dest])247            if suffix:248                tokens.append(suffix)249 250        return tokens251    252    def _convert_token_to_id(self, token: str) -> int:253        """Convert a token to its ID."""254        return self._vocab.get(token, self._vocab.get(self.UNK_TOKEN, 0))255    256    def _convert_id_to_token(self, index: int) -> str:257        """Convert an ID to its token."""258        return self._ids_to_tokens.get(index, self.UNK_TOKEN)259    260    def convert_tokens_to_string(self, tokens: List[str]) -> str:261        """262        Convert decomposed tokens back to a string of moves.263 264        Reconstructs moves from [piece, source_f, dest_t, optional_suffix] format.265        E.g., ["WP", "e2_f", "e4_t"] -> "WP e2_f e4_t"266 267        For the evaluator's decomposed format, we keep the tokens space-separated.268        """269        # Filter out special tokens270        special = {self.PAD_TOKEN, self.BOS_TOKEN, self.EOS_TOKEN, self.UNK_TOKEN}271        filtered = [t for t in tokens if t not in special]272        return " ".join(filtered)273    274    def save_vocabulary(275        self,276        save_directory: str,277        filename_prefix: Optional[str] = None,278    ) -> tuple:279        """280        Save the vocabulary to a JSON file.281        282        Args:283            save_directory: Directory to save the vocabulary.284            filename_prefix: Optional prefix for the filename.285        286        Returns:287            Tuple containing the path to the saved vocabulary file.288        """289        if not os.path.isdir(save_directory):290            os.makedirs(save_directory, exist_ok=True)291        292        vocab_file = os.path.join(293            save_directory,294            (filename_prefix + "-" if filename_prefix else "") + "vocab.json",295        )296        297        with open(vocab_file, "w", encoding="utf-8") as f:298            json.dump(self._vocab, f, ensure_ascii=False, indent=2)299        300        return (vocab_file,)301 302 303def count_vocab_from_dataset(304    dataset_name: str = "dlouapre/lichess_2025-01_1M",305    split: str = "train",306    column: str = "text",307    max_samples: Optional[int] = 10000,308) -> Dict[str, int]:309    """310    Count decomposed token frequencies in a dataset (useful for vocabulary analysis).311 312    Args:313        dataset_name: Name of the dataset on Hugging Face Hub.314        split: Dataset split to use.315        column: Column containing the game strings.316        max_samples: Maximum number of samples to process.317 318    Returns:319        Dictionary mapping decomposed tokens to their frequencies.320    """321    from collections import Counter322    from datasets import load_dataset323 324    dataset = load_dataset(dataset_name, split=split)325 326    if max_samples is not None:327        dataset = dataset.select(range(min(max_samples, len(dataset))))328 329    token_counts = Counter()330 331    for example in dataset:332        moves = example[column].strip().split()333        for move in moves:334            if len(move) < 6:335                token_counts[move] += 1336                continue337 338            # Decompose move339            piece = move[:2]340            source = move[2:4] + "_f"341            dest = move[4:6] + "_t"342            suffix = move[6:] if len(move) > 6 else None343 344            token_counts[piece] += 1345            token_counts[source] += 1346            token_counts[dest] += 1347            if suffix:348                token_counts[suffix] += 1349 350    return dict(token_counts)351