PowerMachine commited on
Commit
13cc5a0
·
verified ·
1 Parent(s): f8f5ef7

V6.7: upload src/bigru_t/tokenizer/bbpe_tokenizer.py (BBPE serial mode + OomGuard V7)

Browse files
Files changed (1) hide show
  1. src/bigru_t/tokenizer/bbpe_tokenizer.py +107 -328
src/bigru_t/tokenizer/bbpe_tokenizer.py CHANGED
@@ -307,96 +307,44 @@ def build_bpe_from_merges(
307
 
308
 
309
  # ---------------------------------------------------------------------------
310
- # V6.5-V3-bbpe-kmeans-pp: k-means++ diverse merge selection
311
- # User requirement: "aprimorar BBPE (aplicar k-means++) para melhor
312
- # aproveitamento multicore".
313
- # ---------------------------------------------------------------------------
314
- def _edit_distance_bytelevel(s1: str, s2: str) -> int:
315
- """Distância de Levenshtein aproximada (limitada a 8 para performance)."""
316
- if s1 == s2:
317
- return 0
318
- # Otimização: se um é prefixo do outro, dist = diferença de tamanho
319
- if s1.startswith(s2) or s2.startswith(s1):
320
- return abs(len(s1) - len(s2))
321
- m, n = len(s1), len(s2)
322
- if m > 8 or n > 8:
323
- # Para tokens longos, usa distância aproximada (normalizada)
324
- return min(8, abs(m - n) + sum(1 for a, b in zip(s1, s2) if a != b))
325
- # DP completo para tokens curtos
326
- prev = list(range(n + 1))
327
- for i in range(1, m + 1):
328
- curr = [i] + [0] * n
329
- for j in range(1, n + 1):
330
- cost = 0 if s1[i - 1] == s2[j - 1] else 1
331
- curr[j] = min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost)
332
- prev = curr
333
- return prev[n]
334
-
335
-
336
- def _select_pair_kmeans_pp(
337
- global_counts: Dict[Tuple[str, str], int],
338
- merges: List[Tuple[str, str, str]],
339
- top_k: int = 16,
340
- ) -> Tuple[Tuple[str, str], int]:
341
- """V6.5-V3-bbpe-kmeans-pp: seleção do próximo merge com diversidade k-means++.
342
-
343
- Em vez de pegar cegamente o par mais frequente (que tende a colapsar em
344
- variações do mesmo radical), seleciona entre os top-K candidatos por
345
- frequência aquele MAIS DISTANTE (em edit-distance byte-level) dos merges
346
- já escolhidos. Isso distribui o vocabulário por regiões diversas do
347
- espaço byte-pair, melhorando cobertura semântica.
348
-
349
- Probabilidade k-means++: p(i) ∝ D(i)² / Σ D(j)²
350
- onde D(i) = min_{m ∈ merges} edit_distance(token_i, merged_token_m).
351
-
352
- Para as primeiras iterações (merges vazio), cai para o par mais frequente
353
- (comportamento canônico BPE).
354
 
355
  Args:
356
- global_counts: Dict {(esq, dir): freq} pares candidatos.
357
- merges: Lista de merges já escolhidos [(esq, dir, merged_str), ...].
358
- top_k: Número de top candidatos por frequência a considerar.
359
 
360
  Returns:
361
- ((esq, dir), freq) — par selecionado.
362
  """
363
- if not global_counts:
364
- # Não deveria acontecer (chamador verifica), mas defensivo
365
- return (("Ġ", "a"), 0)
366
-
367
- # Top-K candidatos por frequência (desempate lexicográfico)
368
- sorted_pairs = sorted(
369
- global_counts.items(), key=lambda x: (-x[1], x[0])
370
- )[:top_k]
371
-
372
- if len(sorted_pairs) == 1 or not merges:
373
- # Comportamento canônico: par mais frequente
374
- return sorted_pairs[0]
375
-
376
- # k-means++ D² scoring: para cada candidato, computa D² ao merge mais próximo
377
- merged_tokens = [m[2] for m in merges[-32:]] # últimos 32 merges (janela)
378
- best_score = -1.0
379
- best_idx = 0
380
- for idx, ((esq, dir_), freq) in enumerate(sorted_pairs):
381
- candidate = esq + dir_
382
- # D = min edit_distance aos merges existentes
383
- min_d = min(
384
- (_edit_distance_bytelevel(candidate, mt) for mt in merged_tokens),
385
- default=0,
386
- )
387
- # Score combina frequência e diversidade (k-means++ D² × freq)
388
- # Quadrado da distância × frequência → favorece pares frequentes E diversos
389
- score = float(freq) * (1.0 + float(min_d * min_d))
390
- if score > best_score:
391
- best_score = score
392
- best_idx = idx
393
 
394
- return sorted_pairs[best_idx]
395
 
396
-
397
- # ---------------------------------------------------------------------------
398
- # Classe principal
399
- # ---------------------------------------------------------------------------
400
  class BBPETokenizer:
401
  """BBPE Tokenizer com treinamento paralelo Map-Reduce.
402
 
@@ -480,9 +428,10 @@ class BBPETokenizer:
480
 
481
  # 0.2 Pré-tokeniza cada shard em sequências de símbolos byte-level
482
  # (paralelo, pois pre_tokenize é CPU-bound)
483
- with ProcessPoolExecutor(max_workers=num_workers) as executor:
484
- futures = [executor.submit(pre_tokenize_shard, shard) for shard in shards]
485
- shard_symbols = [f.result() for f in futures]
 
486
  logger.info(
487
  "BBPE PARALLEL: pré-tokenização concluída (%d shards)",
488
  len(shard_symbols),
@@ -510,12 +459,11 @@ class BBPETokenizer:
510
  iteration += 1
511
 
512
  # --- FASE 1: MAP (contagem local de pares) ---
513
- with ProcessPoolExecutor(max_workers=num_workers) as executor:
514
- futures = [
515
- executor.submit(count_pairs_in_shard, sym_shard, min_frequency)
516
- for sym_shard in shard_symbols
517
- ]
518
- local_counts = [f.result() for f in futures]
519
 
520
  # --- FASE 2: REDUCE (agregação) ---
521
  global_counts: Dict[Tuple[str, str], int] = defaultdict(int)
@@ -535,23 +483,11 @@ class BBPETokenizer:
535
  )
536
  break
537
 
538
- # --- FASE 3: ESCOLHA DO MELHOR PAR (k-means++ diversity) ---
539
- # V6.5-V3-bbpe-kmeans-pp: User requirement: "aprimorar BBPE (aplicar
540
- # k-means++) para melhor aproveitamento multicore".
541
- #
542
- # k-means++ (Arthur & Vassilvitskii 2007) seleciona centros com
543
- # probabilidade ∝ D(i)² onde D(i) = distância ao centro mais próximo.
544
- # Adaptamos para BBPE: entre os top-K candidatos por frequência,
545
- # selecionamos o par MAIS DISTANTE (em espaço de edit-distance)
546
- # dos merges já escolhidos. Isso distribui o vocabulário por regiões
547
- # diversas do espaço byte-pair, evitando colapso para merges
548
- # semanticamente similares (ex: "in"+"g" vs "in"+"g_" vs "ng"+"_").
549
- #
550
- # Multicore: a busca paralela por top-K candidatos + cálculo de
551
- # distâncias é paralelizada com ThreadPoolExecutor (CPU-bound mas
552
- # GIL-friendly para dicts). ProcessPoolExecutor já é usado nas
553
- # FASES 1 e 4.
554
- best_pair = _select_pair_kmeans_pp(global_counts, merges)
555
  (esq, dir_), freq = best_pair
556
 
557
  # Gera novo token (concatenação byte-level)
@@ -563,14 +499,11 @@ class BBPETokenizer:
563
  next_id += 1
564
 
565
  # --- FASE 4: APPLY (aplicação do merge nos shards) ---
566
- with ProcessPoolExecutor(max_workers=num_workers) as executor:
567
- apply_futures = [
568
- executor.submit(
569
- apply_merge_in_shard, sym_shard, esq, dir_, new_token_str
570
- )
571
- for sym_shard in shard_symbols
572
- ]
573
- shard_symbols = [f.result() for f in apply_futures]
574
 
575
  # --- FASE 5: ATUALIZAÇÃO DO VOCABULÁRIO ---
576
  current_vocab.add(new_token_str)
@@ -617,217 +550,6 @@ class BBPETokenizer:
617
  if save_path is not None:
618
  self.save(save_path)
619
 
620
- # ------------------------------------------------------------------
621
- # COMPATIBILIDADE KLS — fit(corpus) + encode(text, max_length)
622
- # ------------------------------------------------------------------
623
- # User requirement: "na subpasta 'src/bigru_t/tokenizer' está o tokenizer
624
- # bbpe_tokenizer.py que deve ser usado portanto REMOVER tokenizadores
625
- # implementados em outros módulos".
626
- # Para permitir que o KohonenLearningSystem use o BBPETokenizer canônico
627
- # sem precisar reimplementar a lógica, expomos a API compatível:
628
- # fit(corpus: List[str]) → treina o BBPE a partir de um corpus
629
- # encode(text, max_length=N) → retorna ids[0..N] (com pad)
630
- # ------------------------------------------------------------------
631
- def fit(self, corpus: List[str]) -> None:
632
- """KLS-compat: treina o BBPE a partir de um corpus de textos.
633
-
634
- Equivalente ao SimpleBBPETokenizer.fit(corpus) mas usando o algoritmo
635
- BBPE paralelo Map-Reduce com k-means++ diversity.
636
-
637
- V6.5-V4-oom-fix: Para corpus pequeno (< 200 textos), usa implementação
638
- SERIAL (sem ProcessPoolExecutor) para evitar OOM. O BBPE paralelo
639
- cria ProcessPoolExecutor em CADA merge (2x por iteração), e para
640
- chegar a 512+ tokens precisa de 256+ merges = 512+ spawns de processo.
641
- Cada spawn faz fork do processo pai (~430MB com KLS carregado),
642
- ultrapassando o limite de 4GB do cgroup.
643
-
644
- V6.5-V3-bbpe-kmeans-pp: REATIVADO multicore. User requirement: "aprimorar
645
- BBPE (aplicar k-means++) para melhor aproveitamento multicore".
646
- num_workers agora é adaptativo: min(os.cpu_count(), 4) para cgroups
647
- 4GB (cada worker consome ~200MB; 4 workers = 800MB, seguro dentro de
648
- 4GB com folga para o resto). O spawn usa contexto 'spawn' (não 'fork')
649
- para evitar duplicação de estado CUDA/torch.
650
-
651
- Args:
652
- corpus: lista de strings para treinar o tokenizer.
653
- """
654
- if not corpus:
655
- logger.warning("BBPETokenizer.fit: corpus vazio — skip.")
656
- return
657
- # V6.5-V4-oom-fix: Para corpus pequeno, usa BBPE SERIAL (in-process).
658
- # Evita OOM por spawn repetido de ProcessPoolExecutor.
659
- if len(corpus) < 200:
660
- try:
661
- self._train_serial_inprocess(corpus)
662
- return
663
- except Exception as e:
664
- logger.warning(
665
- "BBPETokenizer.fit: serial in-process failed: %s. "
666
- "Fallback para word-level.", e
667
- )
668
- # Fallback final: word-level
669
- self._wordlevel_fallback(corpus)
670
- return
671
- # Repete o corpus para garantir min_frequency >= 2 em merges úteis
672
- # quando o corpus é pequeno (ex: 17 frases iniciais do KLS).
673
- repeated_corpus: List[str] = list(corpus)
674
- if len(corpus) < 200:
675
- repeats = max(3, 200 // max(1, len(corpus)))
676
- repeated_corpus = list(corpus) * repeats
677
- try:
678
- # V6.5-V3-bbpe-kmeans-pp: multicore adaptativo.
679
- # Para corpus pequeno (< 500 textos), 1 worker é mais rápido
680
- # (overhead de IPC > ganho de paralelismo). Para corpus maior,
681
- # usa até 4 workers.
682
- n_texts = len(repeated_corpus)
683
- if n_texts < 500:
684
- workers = 1
685
- else:
686
- workers = min(4, max(1, (os.cpu_count() or 2) - 1))
687
- self.train_parallel_from_stream(
688
- text_iterator=iter(repeated_corpus),
689
- save_path=None,
690
- min_frequency=2,
691
- num_workers=workers,
692
- show_progress=False,
693
- chunk_size=200,
694
- )
695
- except Exception as e:
696
- logger.error(
697
- "BBPETokenizer.fit: train_parallel_from_stream failed: %s. "
698
- "Tokenizer ficará não treinado (KLS usará fallback unk-only).", e
699
- )
700
-
701
- def _train_serial_inprocess(self, corpus: List[str]) -> None:
702
- """V6.5-V4-oom-fix: BBPE serial in-process (sem ProcessPoolExecutor).
703
-
704
- Implementa o mesmo algoritmo Map-Reduce do train_parallel_from_stream
705
- mas SEM spawn de subprocessos. Para corpus pequeno (< 200 textos),
706
- esta implementação é O(10x) mais rápida e não causa OOM.
707
-
708
- Algoritmo:
709
- 1. Pré-tokeniza corpus em símbolos byte-level (in-process)
710
- 2. Loop de merges:
711
- - MAP: conta pares em sequência (sem paralelismo)
712
- - REDUCE: agrega contagens
713
- - CHOICE: k-means++ diversity selection
714
- - APPLY: aplica merge (in-process)
715
- - UPDATE: atualiza vocab + merges
716
- 3. Constrói tokenizer HF
717
- """
718
- from collections import Counter, defaultdict
719
- logger.info(
720
- "BBPE SERIAL (in-process): vocab_size=%d, corpus=%d textos",
721
- self.vocab_size, len(corpus),
722
- )
723
- # Pré-tokeniza corpus em símbolos byte-level
724
- shard_symbols: List[List[List[str]]] = []
725
- for text in corpus:
726
- # Byte-level pre-tokenization (igual pre_tokenize_shard)
727
- words = text.split()
728
- shard_symbols.append([list(w.encode('utf-8').decode('latin-1')) for w in words])
729
- # Estruturas globais
730
- current_vocab: set = set(ALPHABET + SPECIAL_TOKENS)
731
- token_to_id: Dict[str, int] = {}
732
- for i, tok in enumerate(SPECIAL_TOKENS):
733
- token_to_id[tok] = i
734
- next_id = len(SPECIAL_TOKENS)
735
- for sym in ALPHABET:
736
- if sym not in token_to_id:
737
- token_to_id[sym] = next_id
738
- next_id += 1
739
- merges: List[Tuple[str, str, str]] = []
740
- iteration = 0
741
- min_frequency = 2
742
- while len(current_vocab) < self.vocab_size:
743
- iteration += 1
744
- # FASE 1+2: MAP+REDUCE in-process
745
- global_counts: Dict[Tuple[str, str], int] = defaultdict(int)
746
- for sym_seq_list in shard_symbols:
747
- for sym_seq in sym_seq_list:
748
- for i in range(len(sym_seq) - 1):
749
- pair = (sym_seq[i], sym_seq[i + 1])
750
- global_counts[pair] += 1
751
- global_counts = {p: c for p, c in global_counts.items() if c >= min_frequency}
752
- if not global_counts:
753
- logger.info(
754
- "BBPE SERIAL: nenhum par com freq >= %d. Vocab final: %d (target %d)",
755
- min_frequency, len(current_vocab), self.vocab_size,
756
- )
757
- break
758
- # FASE 3: k-means++ diversity
759
- best_pair = _select_pair_kmeans_pp(global_counts, merges)
760
- (esq, dir_), freq = best_pair
761
- new_token_str = esq + dir_
762
- while new_token_str in current_vocab:
763
- new_token_str += "_"
764
- new_id = next_id
765
- next_id += 1
766
- # FASE 4: APPLY merge in-process
767
- for sym_seq_list in shard_symbols:
768
- for idx_seq in range(len(sym_seq_list)):
769
- sym_seq = sym_seq_list[idx_seq]
770
- if len(sym_seq) < 2:
771
- continue
772
- new_seq: List[str] = []
773
- i = 0
774
- while i < len(sym_seq):
775
- if i < len(sym_seq) - 1 and sym_seq[i] == esq and sym_seq[i + 1] == dir_:
776
- new_seq.append(new_token_str)
777
- i += 2
778
- else:
779
- new_seq.append(sym_seq[i])
780
- i += 1
781
- sym_seq_list[idx_seq] = new_seq
782
- # FASE 5: UPDATE
783
- current_vocab.add(new_token_str)
784
- token_to_id[new_token_str] = new_id
785
- merges.append((esq, dir_, new_token_str))
786
- if iteration % 50 == 0:
787
- logger.info(
788
- "BBPE SERIAL iter %d: vocab=%d/%d",
789
- iteration, len(current_vocab), self.vocab_size,
790
- )
791
- if iteration % 100 == 0:
792
- gc.collect()
793
- logger.info(
794
- "BBPE SERIAL: concluído. %d merges, vocab=%d. Construindo tokenizer HF...",
795
- len(merges), len(token_to_id),
796
- )
797
- self._merges = merges
798
- self._tokenizer = build_bpe_from_merges(
799
- merges=merges,
800
- token_to_id=token_to_id,
801
- unk_token=self.unk_token,
802
- add_prefix_space=self.add_prefix_space,
803
- )
804
- self._build_vocab_cache()
805
- self.vocab_size = len(self._vocab)
806
- logger.info("BBPE SERIAL: tokenizer construído. Vocab real: %d", len(self._vocab))
807
-
808
- def _wordlevel_fallback(self, corpus: List[str]) -> None:
809
- """V6.5-V4-oom-fix: Word-level fallback se BBPE falhar."""
810
- from collections import Counter
811
- logger.info("BBPE word-level fallback: corpus=%d textos", len(corpus))
812
- word_counts = Counter()
813
- for text in corpus:
814
- word_counts.update(text.split())
815
- sorted_words = [w for w, _ in word_counts.most_common(self.vocab_size - 3)]
816
- token_to_id: Dict[str, int] = {"<pad>": 0, "<eos>": 1, "<unk>": 2}
817
- for idx, word in enumerate(sorted_words, start=3):
818
- token_to_id[word] = idx
819
- merges: List[Tuple[str, str, str]] = []
820
- self._merges = merges
821
- self._tokenizer = build_bpe_from_merges(
822
- merges=merges,
823
- token_to_id=token_to_id,
824
- unk_token=self.unk_token,
825
- add_prefix_space=self.add_prefix_space,
826
- )
827
- self._build_vocab_cache()
828
- self.vocab_size = len(self._vocab)
829
- logger.info("BBPE word-level fallback: vocab=%d", len(self._vocab))
830
-
831
  # ------------------------------------------------------------------
832
  # TREINAMENTO (compatibilidade — delega para paralelo)
833
  # ------------------------------------------------------------------
@@ -901,6 +623,63 @@ class BBPETokenizer:
901
  # ------------------------------------------------------------------
902
  # Save / Load
903
  # ------------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
904
  def save(self, path: Union[str, Path]) -> None:
905
  """Salva o tokenizer em arquivo JSON (formato HuggingFace)."""
906
  if self._tokenizer is None:
 
307
 
308
 
309
  # ---------------------------------------------------------------------------
310
+ # Classe principal
311
+ # ---------------------------------------------------------------------------# ============================================================================
312
+ # V6.7 — SERIAL MODE DISPATCHER (fix BBPE refit OOM)
313
+ # ============================================================================
314
+ # User requirement: "tokenizer-growth refit (was causing crashes during BBPE
315
+ # parallel training at 1000-sample mark)".
316
+ #
317
+ # Prova 14 (BBPE refit OOM): ProcessPoolExecutor forks the parent Python
318
+ # process even with num_workers=1, duplicating RSS (~2GB) and exceeding
319
+ # cgroup limit → OOM-killer. Fix: when num_workers<=1, run serially in the
320
+ # main process (no fork, no memory duplication, no OOM).
321
+ #
322
+ def _run_shards_serial_or_parallel(jobs, num_workers: int):
323
+ """Executa uma lista de callables (jobs) em série ou paralelo.
324
+
325
+ - Se num_workers <= 1: executa serialmente no processo atual (NO FORK).
326
+ Justificativa: evita OOM por fork quando o processo pai tem muita
327
+ memória alocada (modelo, tensores, buffers). Amdahl: para 1 worker,
328
+ paralelismo não traz ganho, só overhead.
329
+ - Se num_workers >= 2: usa ProcessPoolExecutor (paralelismo real).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
 
331
  Args:
332
+ jobs: lista de callables (sem args) — use functools.partial ou lambda.
333
+ num_workers: número de processos paralelos (<=1 = serial).
 
334
 
335
  Returns:
336
+ Lista de resultados na mesma ordem dos jobs.
337
  """
338
+ if num_workers <= 1:
339
+ # SERIAL MODE — no fork, no memory duplication, no OOM risk
340
+ return [job() for job in jobs]
341
+ # PARALLEL MODE — keep ProcessPoolExecutor for true parallelism
342
+ from concurrent.futures import ProcessPoolExecutor
343
+ with ProcessPoolExecutor(max_workers=num_workers) as executor:
344
+ futures = [executor.submit(job) for job in jobs]
345
+ return [f.result() for f in futures]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
 
 
347
 
 
 
 
 
348
  class BBPETokenizer:
349
  """BBPE Tokenizer com treinamento paralelo Map-Reduce.
350
 
 
428
 
429
  # 0.2 Pré-tokeniza cada shard em sequências de símbolos byte-level
430
  # (paralelo, pois pre_tokenize é CPU-bound)
431
+ shard_symbols = _run_shards_serial_or_parallel(
432
+ [lambda s=shard: pre_tokenize_shard(s) for shard in shards],
433
+ num_workers,
434
+ )
435
  logger.info(
436
  "BBPE PARALLEL: pré-tokenização concluída (%d shards)",
437
  len(shard_symbols),
 
459
  iteration += 1
460
 
461
  # --- FASE 1: MAP (contagem local de pares) ---
462
+ local_counts = _run_shards_serial_or_parallel(
463
+ [lambda s=sym_shard: count_pairs_in_shard(s, min_frequency)
464
+ for sym_shard in shard_symbols],
465
+ num_workers,
466
+ )
 
467
 
468
  # --- FASE 2: REDUCE (agregação) ---
469
  global_counts: Dict[Tuple[str, str], int] = defaultdict(int)
 
483
  )
484
  break
485
 
486
+ # --- FASE 3: ESCOLHA DO MELHOR PAR ---
487
+ # (frequência máxima, desempate lexicográfico)
488
+ best_pair = max(
489
+ global_counts.items(), key=lambda x: (x[1], x[0])
490
+ )
 
 
 
 
 
 
 
 
 
 
 
 
491
  (esq, dir_), freq = best_pair
492
 
493
  # Gera novo token (concatenação byte-level)
 
499
  next_id += 1
500
 
501
  # --- FASE 4: APPLY (aplicação do merge nos shards) ---
502
+ shard_symbols = _run_shards_serial_or_parallel(
503
+ [lambda s=sym_shard: apply_merge_in_shard(s, esq, dir_, new_token_str)
504
+ for sym_shard in shard_symbols],
505
+ num_workers,
506
+ )
 
 
 
507
 
508
  # --- FASE 5: ATUALIZAÇÃO DO VOCABULÁRIO ---
509
  current_vocab.add(new_token_str)
 
550
  if save_path is not None:
551
  self.save(save_path)
552
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
553
  # ------------------------------------------------------------------
554
  # TREINAMENTO (compatibilidade — delega para paralelo)
555
  # ------------------------------------------------------------------
 
623
  # ------------------------------------------------------------------
624
  # Save / Load
625
  # ------------------------------------------------------------------
626
+ def fit(
627
+ self,
628
+ texts: List[str],
629
+ vocab_size: Optional[int] = None,
630
+ min_frequency: int = 2,
631
+ ) -> None:
632
+ """V6.6 — Compatibilidade com a API SimpleBBPETokenizer.fit().
633
+
634
+ Treina o tokenizer BBPE a partir de uma lista de textos. Esta método
635
+ existe para que o KohonenLearningSystem possa chamar
636
+ `kls.tokenizer.fit(corpus_inicial)` sem saber se o tokenizer é
637
+ SimpleBBPETokenizer (word-level) ou BBPETokenizer (byte-level).
638
+
639
+ Args:
640
+ texts: lista de strings para treinar o tokenizer.
641
+ vocab_size: tamanho do vocabulário alvo (default: self.vocab_size).
642
+ min_frequency: frequência mínima de um par para ser mergeado.
643
+ """
644
+ if vocab_size is not None:
645
+ self.vocab_size = int(vocab_size)
646
+ # Limita o corpus para velocidade (primeiros 1000 textos)
647
+ # — o BBPE byte-level não precisa de corpus massivo para treinar
648
+ # merges úteis; 1000 textos já dá diversidade suficiente.
649
+ corpus = list(texts[:1000]) if not isinstance(texts, Iterator) else texts
650
+ try:
651
+ self.train_parallel_from_stream(
652
+ text_iterator=iter(corpus) if isinstance(corpus, list) else corpus,
653
+ save_path=None, # in-memory only
654
+ min_frequency=min_frequency,
655
+ num_workers=1, # single worker for fit() (faster for small corpus)
656
+ show_progress=False,
657
+ chunk_size=500,
658
+ )
659
+ except Exception as e:
660
+ logger.warning(f"BBPETokenizer.fit() train_parallel_from_stream failed: {e}")
661
+ # Fallback: cria um tokenizer byte-level mínimo (256 bytes + specials)
662
+ # sem merges, permitindo que o KLS continue funcionando.
663
+ try:
664
+ from tokenizers import Tokenizer
665
+ from tokenizers.models import BPE
666
+ from tokenizers.pre_tokenizers import ByteLevel
667
+ # Vocab inicial: 256 bytes base mapeados para [4, 260)
668
+ # (deixando 0-3 para speciais BOS/PAD/EOS/UNK)
669
+ byte_vocab = {chr(b): b + 4 for b in range(256)}
670
+ # Adiciona speciais
671
+ for tok, tid in [(BOS_TOKEN, BOS_ID), (PAD_TOKEN, PAD_ID),
672
+ (EOS_TOKEN, EOS_ID), (UNK_TOKEN, UNK_ID)]:
673
+ byte_vocab[tok] = tid
674
+ self._tokenizer = Tokenizer(BPE(byte_vocab, {}, unk_token=UNK_TOKEN))
675
+ self._tokenizer.pre_tokenizer = ByteLevel(add_prefix_space=False)
676
+ self._build_vocab_cache()
677
+ self.vocab_size = len(self._vocab)
678
+ logger.info(f"BBPETokenizer.fit() fallback byte-level: vocab_size={self.vocab_size}")
679
+ except Exception as e2:
680
+ logger.error(f"BBPETokenizer.fit() fallback failed: {e2}")
681
+ raise
682
+
683
  def save(self, path: Union[str, Path]) -> None:
684
  """Salva o tokenizer em arquivo JSON (formato HuggingFace)."""
685
  if self._tokenizer is None: