| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| from tokenizers import Tokenizer |
| |
| |
| |
| |
| from tokenizers.models import BPE |
| |
| from tokenizers.trainers import BpeTrainer |
| |
| |
| from tokenizers.pre_tokenizers import Whitespace |
| |
| |
| from tokenizers.processors import BertProcessing |
| |
| |
| |
| from tokenizers.normalizers import NFD, Sequence |
|
|
| |
| import os |
|
|
| |
| from hindi_preprocessor import clean_hindi_text |
|
|
|
|
| |
| |
| |
| |
| |
| class HindiBPEEncoder: |
| def __init__(self, tokenizer_path="hindi_bpe_tokenizer.json"): |
| """ |
| Initialize the HindiBPEEncoder. |
| Sets up the tokenizer by either loading an existing trained model |
| or creating a new untrained tokenizer. |
| |
| Args: |
| tokenizer_path (str): Path to save/load the tokenizer file (default: "hindi_bpe_tokenizer.json") |
| """ |
| |
| self.tokenizer = None |
| |
| |
| |
| self.tokenizer_path = tokenizer_path |
| |
| |
| self.load_or_initialize_tokenizer() |
| |
| def load_or_initialize_tokenizer(self): |
| """ |
| Load existing tokenizer from file or initialize a new one. |
| |
| This method implements a persistence mechanism: |
| 1. Checks if a saved tokenizer file exists |
| 2. If exists, tries to load it (may fail if file is corrupted) |
| 3. If loading fails or file doesn't exist, creates a new tokenizer |
| 4. This allows the app to remember trained tokenizers across sessions |
| """ |
| |
| if os.path.exists(self.tokenizer_path): |
| try: |
| |
| |
| self.tokenizer = Tokenizer.from_file(self.tokenizer_path) |
| print(f"Loaded tokenizer from {self.tokenizer_path}") |
| except Exception as e: |
| |
| |
| print(f"Error loading tokenizer: {e}. Initializing new tokenizer.") |
| self.initialize_tokenizer() |
| else: |
| |
| self.initialize_tokenizer() |
| |
| def initialize_tokenizer(self): |
| """ |
| Initialize a new, untrained BPE tokenizer for Hindi with byte-level fallback. |
| |
| This creates a tokenizer with basic configuration but no vocabulary yet. |
| The tokenizer must be trained before it can encode/decode text effectively. |
| |
| Configuration choices: |
| - BPE model: Uses Byte Pair Encoding algorithm for subword tokenization |
| - unk_token: "<unk>" token for unknown/out-of-vocabulary words |
| - byte_fallback: True enables byte-level BPE with 256 base tokens |
| Starts with exactly 256 tokens (one for each byte 0-255) |
| Text is encoded as UTF-8 bytes, then BPE learns merges on bytes |
| This ensures merges are always learned regardless of vocab_size |
| - Unicode normalization (NFD): Normalizes text to Normalization Form Decomposed |
| Ensures consistent Unicode representation (composed vs decomposed forms) |
| Prevents issues where same character in different forms is treated differently |
| Example: "छोड़कर" will always be normalized consistently |
| - Whitespace pre-tokenizer: Splits on whitespace, which works well for |
| Hindi since words in Devanagari script are space-separated |
| """ |
| |
| |
| |
| |
| |
| self.tokenizer = Tokenizer(BPE(unk_token="<unk>", byte_fallback=True)) |
| |
| |
| |
| |
| |
| |
| self.tokenizer.normalizer = Sequence([NFD()]) |
| |
| |
| |
| |
| |
| self.tokenizer.pre_tokenizer = Whitespace() |
| |
| print("Initialized new BPE tokenizer with 256 base tokens (byte-level BPE + Unicode normalization)") |
| |
| def train_tokenizer(self, training_texts, vocab_size=5000, use_streaming=False, chunk_size=1024*1024): |
| """ |
| Train the BPE tokenizer on provided Hindi text corpus. |
| |
| Optimized for large datasets with streaming support and efficient preprocessing. |
| |
| BPE Training Process: |
| 1. Preprocess: Clean and normalize training text using regex (optimized) |
| 2. Starts with character-level vocabulary |
| 3. Iteratively finds most frequent character pairs |
| 4. Merges them into new subword units |
| 5. Repeats until vocabulary reaches specified size |
| |
| Args: |
| training_texts: Hindi text corpus - can be: |
| - str: Text content (for small-medium datasets) |
| - str (file path): Path to file (if use_streaming=True) |
| vocab_size (int): Desired vocabulary size (default: 5000) |
| With 256 base tokens, any vocab_size > 256 will learn merges |
| Recommended: 3000-10000 for Hindi |
| use_streaming (bool): If True, treat training_texts as file path and stream |
| chunk_size (int): Chunk size for streaming (default: 1MB) |
| |
| Returns: |
| str: Success message or error description |
| """ |
| |
| if not training_texts: |
| return "Error: Please provide training text" |
| |
| try: |
| from hindi_preprocessor import clean_hindi_text, clean_hindi_text_streaming |
| |
| |
| trainer = BpeTrainer( |
| vocab_size=vocab_size, |
| |
| |
| |
| |
| |
| |
| special_tokens=["<unk>", "<s>", "</s>", "<pad>", "<mask>"], |
| min_frequency=2 |
| |
| ) |
| |
| |
| if use_streaming: |
| |
| |
| def text_iterator(): |
| for cleaned_chunk in clean_hindi_text_streaming(training_texts, chunk_size): |
| |
| for line in cleaned_chunk.split('\n'): |
| line = line.strip() |
| if line: |
| yield line |
| |
| training_iterator = text_iterator() |
| else: |
| |
| if isinstance(training_texts, str) and len(training_texts) > 100 * 1024 * 1024: |
| |
| from io import StringIO |
| def text_iterator(): |
| buffer = StringIO(training_texts) |
| chunk_size = 10 * 1024 * 1024 |
| while True: |
| chunk = buffer.read(chunk_size) |
| if not chunk: |
| break |
| cleaned = clean_hindi_text(chunk) |
| |
| for line in cleaned.split('\n'): |
| line = line.strip() |
| if line: |
| yield line |
| training_iterator = text_iterator() |
| else: |
| |
| cleaned_text = clean_hindi_text(training_texts) |
| |
| training_iterator = (line.strip() for line in cleaned_text.split('\n') if line.strip()) |
| |
| |
| |
| print(f"\n Training BPE tokenizer (vocab_size={vocab_size})...") |
| self.tokenizer.train_from_iterator( |
| training_iterator, |
| trainer=trainer |
| ) |
| |
| |
| |
| vocab = self.tokenizer.get_vocab() |
| |
| |
| |
| base_tokens = 0 |
| merged_tokens = 0 |
| for token in vocab.keys(): |
| |
| if token in ["<unk>", "<s>", "</s>", "<pad>", "<mask>"]: |
| continue |
| |
| try: |
| token_bytes = token.encode('utf-8') |
| if len(token_bytes) == 1: |
| base_tokens += 1 |
| else: |
| merged_tokens += 1 |
| except: |
| merged_tokens += 1 |
| |
| |
| |
| num_merges = len(vocab) - base_tokens - 5 |
| |
| print(f" ✓ Training complete!") |
| print(f" Vocabulary size: {len(vocab)}") |
| print(f" Base tokens (256): ~{base_tokens}") |
| print(f" Estimated merges: ~{num_merges}") |
| print(f" Merged tokens: ~{merged_tokens}") |
| |
| if num_merges <= 0: |
| print(f"\n ⚠️ WARNING: No BPE merges learned!") |
| print(f" This should not happen with byte-level BPE (256 base tokens).") |
| print(f" Check that vocab_size ({vocab_size}) > 256 and training data is sufficient.") |
| |
| |
| |
| |
| try: |
| |
| |
| cls_token_id = self.tokenizer.token_to_id("</s>") if self.tokenizer.token_to_id("</s>") is not None else 1 |
| sep_token_id = self.tokenizer.token_to_id("<s>") if self.tokenizer.token_to_id("<s>") is not None else 0 |
| self.tokenizer.post_processor = BertProcessing( |
| ("</s>", cls_token_id), |
| ("<s>", sep_token_id), |
| ) |
| except: |
| |
| |
| pass |
| |
| |
| |
| print(f"\n 💾 Saving tokenizer to '{self.tokenizer_path}'...") |
| try: |
| self.tokenizer.save(self.tokenizer_path) |
| |
| import os |
| if os.path.exists(self.tokenizer_path): |
| file_size = os.path.getsize(self.tokenizer_path) |
| print(f" ✓ Tokenizer saved successfully ({file_size:,} bytes)") |
| else: |
| return f"Error: Tokenizer file was not created at '{self.tokenizer_path}'" |
| except Exception as save_error: |
| return f"Error saving tokenizer: {str(save_error)}" |
| |
| return f"Tokenizer trained successfully with vocab size {vocab_size}!" |
| except Exception as e: |
| |
| |
| return f"Error training tokenizer: {str(e)}" |
| |
| def encode(self, text): |
| """ |
| Encode Hindi text into token IDs and subword tokens. |
| |
| Encoding Process: |
| 1. Preprocess: Clean and normalize Hindi text using regex |
| 2. Pre-tokenize: Split text on whitespace into words |
| 3. Apply BPE: Break words into subword units using learned merges |
| 4. Convert to IDs: Map each token to its vocabulary ID |
| |
| Args: |
| text (str): Hindi text to encode |
| |
| Returns: |
| dict: Contains token_ids, tokens, attention_mask, and offsets |
| OR error dict if encoding fails |
| """ |
| |
| if not self.tokenizer: |
| return "Error: Tokenizer not initialized" |
| |
| |
| if not text or not text.strip(): |
| return {"error": "Please provide text to encode"} |
| |
| try: |
| |
| |
| cleaned_text = clean_hindi_text(text) |
| |
| |
| encoded = self.tokenizer.encode(cleaned_text) |
| |
| |
| |
| original_char_count = len(cleaned_text) |
| token_count = len(encoded.ids) |
| compression_ratio = original_char_count / token_count if token_count > 0 else 0 |
| compression_percentage = (1 - token_count / original_char_count) * 100 if original_char_count > 0 else 0 |
| |
| |
| return { |
| "token_ids": encoded.ids, |
| |
| "tokens": encoded.tokens, |
| |
| "attention_mask": encoded.attention_mask, |
| |
| "offsets": encoded.offsets, |
| |
| "compression_ratio": compression_ratio, |
| "compression_percentage": compression_percentage, |
| "original_char_count": original_char_count, |
| "token_count": token_count |
| } |
| except Exception as e: |
| |
| return {"error": f"Encoding error: {str(e)}"} |
| |
| def decode(self, token_ids): |
| """ |
| Decode token IDs back to Hindi text. |
| |
| Decoding Process: |
| 1. Takes list of token IDs |
| 2. Maps each ID to its corresponding token string |
| 3. Concatenates tokens to reconstruct original text |
| 4. Handles special tokens appropriately |
| |
| Args: |
| token_ids: Can be: |
| - str: Comma-separated token IDs (e.g., "1, 2, 3") |
| - list: List of integers (e.g., [1, 2, 3]) |
| |
| Returns: |
| str: Decoded Hindi text, or error message if decoding fails |
| """ |
| |
| if not self.tokenizer: |
| return "Error: Tokenizer not initialized" |
| |
| try: |
| |
| |
| if isinstance(token_ids, str): |
| if not token_ids.strip(): |
| return "" |
| |
| |
| ids = [int(x.strip()) for x in token_ids.split(",") if x.strip()] |
| |
| elif isinstance(token_ids, list): |
| ids = token_ids |
| else: |
| return "Error: Invalid input format" |
| |
| |
| |
| decoded = self.tokenizer.decode(ids) |
| return decoded |
| except Exception as e: |
| |
| return f"Decoding error: {str(e)}" |
| |
| def get_vocab_size(self): |
| """ |
| Get the current vocabulary size of the tokenizer. |
| |
| Returns: |
| int: Number of tokens in vocabulary, or 0 if tokenizer not initialized |
| For untrained tokenizer, this will be 0 or very small |
| For trained tokenizer, this matches the vocab_size used during training |
| """ |
| |
| if not self.tokenizer: |
| return 0 |
| try: |
| |
| return self.tokenizer.get_vocab_size() |
| except: |
| |
| return 0 |
|
|
|
|