yum/FastTextPager
Compressed text paging over OSC.
git clone https://git.yummers.dev/yum/FastTextPager
0c54e1f
master
1# !!! AI ARTIFACT !!! 2# This file was primarily written with AI. 3 4import os 5import argparse 6from datasets import load_dataset ,interleave_datasets 7import sentencepiece as spm 8import itertools 9import random 10from unidecode import unidecode 11 12# --- Dataset 1: Wikipedia --- 13DATASET_NAME_WIKI = "wikipedia" 14DATASET_CONFIG_WIKI = "20220301.en" 15 16DATASET_SPLIT_WIKI = "train" 17TEXT_COLUMN_WIKI = "text" 18 19# --- Dataset 2: DailyDialog --- 20DATASET_NAME_DD = "daily_dialog" 21DATASET_SPLIT_DD = "train" 22UTTERANCE_COLUMN_DD = "dialog" 23 24# --- Dataset 3: BlendedSkillTalk (includes Persona-Chat) --- 25DATASET_NAME_BST = "blended_skill_talk" 26DATASET_SPLIT_BST = "train" 27 28# --- Dataset 4: OpenSubtitles --- 29DATASET_NAME_OS = "Helsinki-NLP/open_subtitles" 30DATASET_LANG_PAIR_OS = ("en" ,"fr" )# Load en-fr and use the 'en' part 31DATASET_SPLIT_OS = "train" 32TEXT_COLUMN_OS = "en" # Access via item['translation']['en'] 33 34# Reserve one space for special "empty" token. 35VOCAB_SIZE = 65535 36OUTPUT_DIR = "./custom_unigram_tokenizer_65k" 37MODEL_PREFIX = os .path .join (OUTPUT_DIR ,"unigram" ) 38MODEL_FILE = MODEL_PREFIX + ".model" 39VOCAB_FILE = MODEL_PREFIX + ".vocab" 40 41UNK_TOKEN = "[UNK]" 42PAD_TOKEN = "[PAD]" 43CONTROL_SYMBOLS = ["[CLS]" ,"[SEP]" ,"[MASK]" ] 44 45BATCH_SIZE = 1000 46 47def wiki_iterator (dataset_wiki ,batch_size = BATCH_SIZE ): 48if dataset_wiki : 49for i in range (0 ,len (dataset_wiki ),batch_size ): 50yield [unidecode (text )for text in dataset_wiki [i :i + batch_size ][TEXT_COLUMN_WIKI ]if text ] 51 52def dd_iterator (dataset_dd ,batch_size = BATCH_SIZE ): 53if dataset_dd : 54current_batch = [] 55for dialogue in dataset_dd : 56utterances = dialogue [UTTERANCE_COLUMN_DD ] 57for utterance in utterances : 58if utterance : 59normalized_utterance = unidecode (utterance ) 60current_batch .append (normalized_utterance ) 61if len (current_batch )== batch_size : 62yield current_batch 63current_batch = [] 64if current_batch : 65yield current_batch 66 67def bst_iterator (dataset_bst ,batch_size = BATCH_SIZE ): 68if dataset_bst : 69current_batch = [] 70for session in dataset_bst : 71texts_to_add = [] 72if session .get ("previous_utterance" ): 73texts_to_add .append (session ["previous_utterance" ]) 74if session .get ("free_messages" ): 75texts_to_add .extend (session ["free_messages" ]) 76if session .get ("guided_messages" ): 77texts_to_add .extend (session ["guided_messages" ]) 78 79for text in texts_to_add : 80if text and isinstance (text ,str ): 81normalized_text = unidecode (text ) 82current_batch .append (normalized_text ) 83if len (current_batch )== batch_size : 84yield current_batch 85current_batch = [] 86if current_batch : 87yield current_batch 88 89def os_iterator (dataset_os ,batch_size = BATCH_SIZE ,lang_code = TEXT_COLUMN_OS ): 90if dataset_os : 91current_batch = [] 92for item in dataset_os : 93text = item ['translation' ][lang_code ] 94if text : 95normalized_text = unidecode (text ) 96current_batch .append (normalized_text ) 97if len (current_batch )== batch_size : 98yield current_batch 99current_batch = [] 100if current_batch : 101yield current_batch 102 103def count_wiki_items_chars (dataset_wiki ): 104item_count = 0 105char_count = 0 106if dataset_wiki : 107item_count = len (dataset_wiki ) 108try : 109char_count = sum (len (unidecode (text ))for text in dataset_wiki [TEXT_COLUMN_WIKI ]if text and isinstance (text ,str )) 110except Exception : 111f"Warning: Direct column access for char count failed for { DATASET_NAME_WIKI } . Iterating row by row (slower)." ) 112char_count = sum (len (unidecode (row [TEXT_COLUMN_WIKI ]))for row in dataset_wiki if row .get (TEXT_COLUMN_WIKI )and isinstance (row [TEXT_COLUMN_WIKI ],str )) 113 114return item_count ,char_count 115 116def count_dd_items_chars (dataset_dd ): 117item_count = 0 118char_count = 0 119if dataset_dd : 120for dialogue in dataset_dd : 121utterances = dialogue [UTTERANCE_COLUMN_DD ] 122for utterance in utterances : 123if utterance and isinstance (utterance ,str ): 124item_count += 1 125char_count += len (unidecode (utterance )) 126return item_count ,char_count 127 128def count_bst_items_chars (dataset_bst ): 129item_count = 0 130char_count = 0 131if dataset_bst : 132for session in dataset_bst : 133texts_to_process = [] 134# Gather all potential text strings first 135prev_utt = session .get ("previous_utterance" ) 136if prev_utt and isinstance (prev_utt ,list ): 137if len (prev_utt )> 0 and isinstance (prev_utt [0 ],str ): 138texts_to_process .append (prev_utt [0 ]) 139elif prev_utt and isinstance (prev_utt ,str ): 140texts_to_process .append (prev_utt ) 141 142free_msgs = session .get ("free_messages" ) 143if free_msgs and isinstance (free_msgs ,list ): 144for item in free_msgs : 145if isinstance (item ,list ): 146texts_to_process .extend (msg for msg in item if msg and isinstance (msg ,str )) 147elif isinstance (item ,str ): 148texts_to_process .append (item ) 149 150guided_msgs = session .get ("guided_messages" ) 151if guided_msgs and isinstance (guided_msgs ,list ): 152for item in guided_msgs : 153if isinstance (item ,list ): 154texts_to_process .extend (msg for msg in item if msg and isinstance (msg ,str )) 155elif isinstance (item ,str ): 156texts_to_process .append (item ) 157 158# Count items and chars from the gathered list 159for text in texts_to_process : 160if text and isinstance (text ,str ): 161normalized_text = unidecode (text ) 162if normalized_text : 163item_count += 1 164char_count += len (normalized_text ) 165 166return item_count ,char_count 167 168def count_os_items_chars (dataset_os ,lang_code = TEXT_COLUMN_OS ): 169item_count = 0 170char_count = 0 171if dataset_os : 172for item in dataset_os : 173text = item ['translation' ][lang_code ] 174if text and isinstance (text ,str ): 175normalized_text = unidecode (text ) 176if normalized_text :# Ensure not empty after unidecode 177item_count += 1 178char_count += len (normalized_text ) 179return item_count ,char_count 180 181def load_and_count_datasets (wiki_fraction ,subtitles_fraction ): 182"""Loads, potentially shrinks, and counts items/chars in datasets.""" 183datasets = {} 184counts = {} 185total_items = 0 186total_chars = 0 187 188# --- Wikipedia --- 189f"Loading dataset 1: { DATASET_NAME_WIKI } ( { DATASET_CONFIG_WIKI } ), split: { DATASET_SPLIT_WIKI } " ) 190dataset_wiki_full = load_dataset (DATASET_NAME_WIKI ,DATASET_CONFIG_WIKI ,split = DATASET_SPLIT_WIKI ,trust_remote_code = True ) 191f"Original Wikipedia dataset size: { len ( dataset_wiki_full ):, } " ) 192 193# Shrink the Wikipedia dataset 194split_test_size = 1.0 - wiki_fraction 195shrunk_dataset_split = dataset_wiki_full .train_test_split (test_size = split_test_size ,seed = random .randint (0 ,1000000 )) 196dataset_wiki = shrunk_dataset_split ['train' ] 197f"Using { wiki_fraction * 100 :.3f } % of Wikipedia dataset: { len ( dataset_wiki ):, } items" ) 198 199count_wiki ,chars_wiki = count_wiki_items_chars (dataset_wiki ) 200f"Wikipedia dataset loaded (shrunk). Precise text items: { count_wiki :, } , Characters: { chars_wiki :, } " ) 201datasets ['wiki' ]= dataset_wiki 202counts ['wiki' ]= (count_wiki ,chars_wiki ) 203total_items += count_wiki 204total_chars += chars_wiki 205 206# --- DailyDialog --- 207f"\nLoading dataset 2: { DATASET_NAME_DD } , split: { DATASET_SPLIT_DD } " ) 208dataset_dd = load_dataset (DATASET_NAME_DD ,split = DATASET_SPLIT_DD ,trust_remote_code = True ) 209count_dd ,chars_dd = count_dd_items_chars (dataset_dd ) 210f"DailyDialog dataset loaded. Precise text items (utterances): { count_dd :, } , Characters: { chars_dd :, } " ) 211datasets ['dd' ]= dataset_dd 212counts ['dd' ]= (count_dd ,chars_dd ) 213total_items += count_dd 214total_chars += chars_dd 215 216# --- BlendedSkillTalk --- 217f"\nLoading dataset 3: { DATASET_NAME_BST } , split: { DATASET_SPLIT_BST } " ) 218dataset_bst = load_dataset (DATASET_NAME_BST ,split = DATASET_SPLIT_BST ,trust_remote_code = True ) 219count_bst ,chars_bst = count_bst_items_chars (dataset_bst ) 220f"BlendedSkillTalk dataset loaded. Precise text items (extracted): { count_bst :, } , Characters: { chars_bst :, } " ) 221datasets ['bst' ]= dataset_bst 222counts ['bst' ]= (count_bst ,chars_bst ) 223total_items += count_bst 224total_chars += chars_bst 225 226# --- OpenSubtitles --- 227f"\nLoading dataset 4: { DATASET_NAME_OS } , lang_pair: { DATASET_LANG_PAIR_OS } , split: { DATASET_SPLIT_OS } " ) 228# Note: OpenSubtitles can be very large. Consider streaming or specific configurations if memory is an issue. 229# For now, loading a standard configuration. 230dataset_os = load_dataset (DATASET_NAME_OS ,lang1 = DATASET_LANG_PAIR_OS [0 ],lang2 = DATASET_LANG_PAIR_OS [1 ],split = DATASET_SPLIT_OS ,trust_remote_code = True ) 231split_test_size = 1.0 - subtitles_fraction 232dataset_os = dataset_os .train_test_split (test_size = split_test_size ,seed = random .randint (0 ,1000000 ))['train' ] 233count_os ,chars_os = count_os_items_chars (dataset_os ,lang_code = TEXT_COLUMN_OS ) 234f"OpenSubtitles dataset loaded. Precise text items: { count_os :, } , Characters: { chars_os :, } " ) 235datasets ['os' ]= dataset_os 236counts ['os' ]= (count_os ,chars_os ) 237total_items += count_os 238total_chars += chars_os 239 240f"\nTotal precise text items from loaded datasets: { total_items :, } " ) 241f"Total characters from loaded datasets: { total_chars :, } " ) 242 243return datasets ,counts 244 245def train_tokenizer (model_prefix ,datasets ,counts ): 246"""Trains a Unigram tokenizer using SentencePiece and saves it.""" 247total_items = sum (c [0 ]for c in counts .values ()) 248total_chars = sum (c [1 ]for c in counts .values ()) 249if total_items == 0 : 250"Error: No items found in the datasets to train on. Exiting training." ) 251return 252 253f"\nTotal precise text items for training: { total_items :, } " ) 254f"Total characters for training: { total_chars :, } " ) 255 256output_dir = os .path .dirname (model_prefix ) 257os .makedirs (output_dir ,exist_ok = True ) 258 259f"\nStarting SentencePiece Unigram tokenizer training with vocab size: { VOCAB_SIZE } " ) 260f"Using combined dataset iterator." ) 261f"Output model prefix: { model_prefix } " ) 262 263iterators_to_chain = [] 264def flatten_iterator (iterator ): 265for batch in iterator : 266for item in batch : 267yield item 268 269if datasets .get ('wiki' )and counts ['wiki' ][0 ]> 0 : 270iterators_to_chain .append (flatten_iterator (wiki_iterator (datasets ['wiki' ]))) 271if datasets .get ('dd' )and counts ['dd' ][0 ]> 0 : 272iterators_to_chain .append (flatten_iterator (dd_iterator (datasets ['dd' ]))) 273if datasets .get ('bst' )and counts ['bst' ][0 ]> 0 : 274iterators_to_chain .append (flatten_iterator (bst_iterator (datasets ['bst' ]))) 275if datasets .get ('os' )and counts ['os' ][0 ]> 0 : 276iterators_to_chain .append (flatten_iterator (os_iterator (datasets ['os' ]))) 277 278if not iterators_to_chain : 279"Error: No valid dataset iterators available for training. Exiting." ) 280return 281 282combined_iterator = itertools .chain (* iterators_to_chain ) 283 284# Include whitespace symbols so we can efficiently break lines. 285# If we include the single space, it prevents the tokenizer from merging 286# spaces with regular words, and tanks the average chars/token. 287# This many tokens is kinda overkill, but it gives us a way to efficiently 288# clear even fairly large boards, so I think it's worth. 289whitespace_symbols = [] 290for i in range (2 ,40 ): 291whitespace_symbols .append ('▁' * i ) 292 293spm .SentencePieceTrainer .train ( 294sentence_iterator = combined_iterator , 295model_prefix = model_prefix , 296vocab_size = VOCAB_SIZE , 297model_type = 'unigram' , 298character_coverage = 1.0 , 299unk_piece = UNK_TOKEN , 300pad_piece = PAD_TOKEN , 301control_symbols = CONTROL_SYMBOLS , 302user_defined_symbols = whitespace_symbols , 303# These whitespace options must be false, or else whitespace won't 304# be respected when encoding. 305add_dummy_prefix = False , 306remove_extra_whitespaces = False , 307split_by_whitespace = False , 308num_threads = os .cpu_count (), 309input_sentence_size = total_items , 310 ) 311"\nTraining finished." ) 312f"SentencePiece model saved to: { model_prefix } .model" ) 313f"SentencePiece vocabulary saved to: { model_prefix } .vocab" ) 314 315def extract_text_samples (dataset ,count ,num_samples ,text_extractor_func ): 316"""Extracts a specified number of text samples from a dataset.""" 317samples = [] 318if dataset is None or count == 0 or num_samples == 0 : 319return samples 320# Take samples from the beginning, ensure we don't exceed dataset size 321actual_samples_to_take = min (num_samples ,count ) 322# Use the provided function to extract text correctly for this dataset type 323samples = text_extractor_func (dataset .select (range (actual_samples_to_take ))) 324return [unidecode (s )for s in samples if s and isinstance (s ,str )] 325 326def wiki_text_extractor (dataset_slice ): 327"""Extracts text from a slice of the Wikipedia dataset.""" 328return [text for text in dataset_slice [TEXT_COLUMN_WIKI ]if text ] 329 330def dd_text_extractor (dataset_slice ): 331"""Extracts text from a slice of the DailyDialog dataset.""" 332texts = [] 333for dialogue in dataset_slice : 334texts .extend (utterance for utterance in dialogue [UTTERANCE_COLUMN_DD ]if utterance ) 335return texts 336 337def bst_text_extractor (dataset_slice ): 338"""Extracts text from a slice of the BlendedSkillTalk dataset.""" 339texts = [] 340for session in dataset_slice : 341if session .get ("previous_utterance" ): 342texts .append (session ["previous_utterance" ]) 343if session .get ("free_messages" ): 344texts .extend (session ["free_messages" ]) 345if session .get ("guided_messages" ): 346texts .extend (session ["guided_messages" ]) 347return [t for t in texts if t and isinstance (t ,str )]# Ensure only valid strings 348 349def os_text_extractor (dataset_slice ,lang_code = TEXT_COLUMN_OS ): 350"""Extracts text from a slice of the OpenSubtitles dataset.""" 351texts = [] 352for item in dataset_slice : 353text = item ['translation' ][lang_code ] 354if text and isinstance (text ,str ): 355texts .append (text ) 356return texts 357 358def test_tokenizer (model_path ,datasets ,counts ,sample_size = 1000 ): 359"\n--- Testing the trained Unigram (SentencePiece) tokenizer on data sample ---" ) 360if sample_size <= 0 : 361"Error: Sample size for testing must be positive." ) 362return 363 364sp = spm .SentencePieceProcessor () 365sp .load (model_path ) 366f"Successfully loaded SentencePiece model from: { model_path } " ) 367f"Vocabulary size: { sp . get_piece_size () } " ) 368 369# Identify available datasets with content 370available_datasets = { 371name :data 372for name ,data in datasets .items () 373if counts [name ][0 ]> 0 374 } 375num_available_datasets = len (available_datasets ) 376 377if num_available_datasets == 0 : 378"Warning: No data available in loaded datasets to sample for testing." ) 379return 380 381f"Found { num_available_datasets } non-empty dataset(s) for testing." ) 382f"Attempting to sample up to { sample_size } total items equally from these datasets..." ) 383 384# Calculate equal number of samples per available dataset 385samples_per_dataset = sample_size // num_available_datasets 386f"Targeting approximately { samples_per_dataset } samples per dataset." ) 387 388test_samples = [] 389dataset_extractors = { 390'wiki' :wiki_text_extractor , 391'dd' :dd_text_extractor , 392'bst' :bst_text_extractor , 393'os' :os_text_extractor , 394 } 395 396actual_samples_collected = {} 397 398# Sample equally from each available dataset 399for name ,dataset in available_datasets .items (): 400count = counts [name ][0 ] 401extractor = dataset_extractors [name ] 402 403# Determine the target number of samples for *this* dataset 404num_samples_to_target = min (samples_per_dataset ,count ) 405if num_samples_to_target <= 0 : 406f" Skipping ' { name } ' (no items requested or available)." ) 407actual_samples_collected [name ]= 0 408continue 409 410f" Sampling from ' { name } ' (target: { num_samples_to_target } items)..." ) 411 412items_before = len (test_samples ) 413# For OpenSubtitles, pass the lang_code if the extractor needs it 414if name == 'os' : 415extracted_items = extract_text_samples (dataset ,count ,num_samples_to_target ,lambda ds_slice :extractor (ds_slice ,lang_code = TEXT_COLUMN_OS )) 416else : 417extracted_items = extract_text_samples (dataset ,count ,num_samples_to_target ,extractor ) 418 419# Limit the extracted items to the target number 420final_samples_for_dataset = extracted_items [:num_samples_to_target ] 421 422test_samples .extend (final_samples_for_dataset ) 423items_added = len (final_samples_for_dataset ) 424actual_samples_collected [name ]= items_added 425f" Added { items_added } items from ' { name } '." ) 426 427actual_sample_size = len (test_samples ) 428if actual_sample_size == 0 : 429"\nCould not gather any samples for testing." ) 430"--- Test finished ---" ) 431return 432 433f"\n--- Starting Test Run ---" ) 434f"Testing on { actual_sample_size } sampled text items (final count)." ) 435f"Samples breakdown: { actual_samples_collected } " ) 436 437total_chars = 0 438total_tokens = 0 439examples_to_show = 5 440examples_shown = 0 441 442random .shuffle (test_samples ) 443 444for i ,text_sample in enumerate (test_samples ): 445if not text_sample :# Should already be filtered by extractor, but double-check 446continue 447try : 448tokens = sp .encode_as_pieces (text_sample ) 449num_tokens = len (tokens ) 450num_chars = len (text_sample ) 451total_tokens += num_tokens 452total_chars += num_chars 453 454if examples_shown < examples_to_show : 455f"\nSample { examples_shown + 1 } (Overall index { i } ):" )# Clarify sample index 456f" Text ( { num_chars } chars): { text_sample [: 100 ] } { '...' if len ( text_sample ) > 100 else '' } " ) 457f" Tokens ( { num_tokens } ): { tokens } " ) 458examples_shown += 1 459except Exception as e : 460f"\nWarning: Error encoding sample (Overall index { i } ) with SentencePiece. Skipping sample. Error: { e } " ) 461f"Problematic sample text: { text_sample [: 100 ] } ..." )# Show problematic text 462 463# --- Test Summary --- 464if total_tokens > 0 and total_chars > 0 :# Avoid division by zero 465avg_chars_per_token = total_chars / total_tokens 466f"\n--- Test Summary ---" ) 467f"Tested on { actual_sample_size } text items." ) 468f"Final samples breakdown: { actual_samples_collected } " ) 469f"Total characters in final sample: { total_chars :, } " ) 470f"Total tokens in final sample: { total_tokens :, } " ) 471f"Average characters per token: { avg_chars_per_token :.4f } " ) 472elif actual_sample_size > 0 : 473"\n--- Test Summary ---" ) 474f"Tested on { actual_sample_size } text items." ) 475f"Final samples breakdown: { actual_samples_collected } " ) 476"No valid tokens were generated from the sample data (or samples were empty after unidecode)." ) 477else : 478# This case should be caught earlier, but included for completeness 479"\n--- Test Summary ---" ) 480"No samples were collected or processed." ) 481 482"--- Test finished ---" ) 483 484def parse_args (): 485parser = argparse .ArgumentParser (description = "Train and test a Unigram tokenizer on combined datasets." ) 486parser .add_argument ("--train" ,action = "store_true" ,help = "Train the tokenizer on datasets" ) 487parser .add_argument ("--test" ,action = "store_true" ,help = "Test the trained tokenizer" ) 488parser .add_argument ("--sample_size" ,type = int ,default = 1000 ,help = "Number of items to sample for testing" ) 489parser .add_argument ("--wiki_fraction" ,type = float ,default = 0.05 ,help = "Fraction of Wikipedia dataset to use (e.g., 0.05 for 5%)" ) 490parser .add_argument ("--subtitles_fraction" ,type = float , 491default = 0.05 ,help = "Fraction of opensubtitles dataset to use (e.g., 0.05 for 5%)" ) 492return parser .parse_args () 493 494if __name__ == "__main__" : 495args = parse_args () 496 497# Load datasets and calculate counts once 498"--- Loading and Counting Datasets ---" ) 499datasets ,counts = load_and_count_datasets (wiki_fraction = args .wiki_fraction ,subtitles_fraction = args .subtitles_fraction ) 500"--- Dataset Loading Finished ---" ) 501 502run_train = args .train 503run_test = args .test 504 505# If no arguments provided, run both by default 506if not args .train and not args .test : 507run_train = True 508run_test = True 509 510if run_train : 511"\n--- Starting Tokenizer Training ---" ) 512train_tokenizer (MODEL_PREFIX ,datasets ,counts ) 513"--- Training Finished ---" ) 514 515if run_test : 516# Ensure tokenizer file exists if training didn't just run 517if not os .path .exists (MODEL_FILE ): 518f"\nError: SentencePiece model file { MODEL_FILE } not found. Cannot run test." ) 519"Please run with --train first or ensure the file exists." ) 520else : 521"\n--- Starting Tokenizer Testing ---" ) 522test_tokenizer (MODEL_FILE ,datasets ,counts ,sample_size = args .sample_size ) 523"--- Testing Finished ---" ) 524 525"\nScript finished." )