Crea un modelo de lenguaje en tus chats de WhatsApp |  por Bernhard Pfann, CFA |  noviembre de 2023

Para entrenar un modelo de lenguaje, necesitamos dividir el lenguaje en partes (los llamados tokens) e introducirlos en el modelo de forma incremental. La tokenización se puede realizar en múltiples niveles.

  • Nivel de personaje: El texto se percibe como una secuencia de caracteres individuales (incluidos espacios en blanco). Este enfoque granular permite formar cada palabra posible a partir de una secuencia de caracteres. Sin embargo, es más difícil captar las relaciones semánticas entre palabras.
  • Nivel de palabra: El texto se representa como una secuencia de palabras. Sin embargo, el vocabulario del modelo está limitado por las palabras existentes en los datos de entrenamiento.
  • Nivel de subpalabra: El texto se divide en unidades de subpalabras, que son más pequeñas que las palabras pero más grandes que los caracteres.

Si bien comencé con un tokenizador a nivel de personaje, sentí que se desperdiciaba tiempo de entrenamiento al aprender secuencias de caracteres de palabras repetitivas, en lugar de centrarme en la relación semántica entre las palabras a lo largo de la oración.

En aras de la simplicidad conceptual, decidí cambiar a un tokenizador a nivel de palabra, dejando de lado las bibliotecas disponibles para estrategias de tokenización más sofisticadas.

from nltk.tokenize import RegexpTokenizer

def custom_tokenizer(txt: str, spec_tokens: List[str], pattern: str="|\d|\\w+|[^\\s]") -> List[str]:
"""
Tokenize text into words or characters using NLTK's RegexpTokenizer, considerung
given special combinations as single tokens.

:param txt: The corpus as a single string element.
:param spec_tokens: A list of special tokens (e.g. ending, out-of-vocab).
:param pattern: By default the corpus is tokenized on a word level (split by spaces).
Numbers are considered single tokens.

>> note: The pattern for character level tokenization is '|.'
"""
pattern = "|".join(spec_tokens) + pattern
tokenizer = RegexpTokenizer(pattern)
tokens = tokenizer.tokenize(txt)
return tokens

["Alice:", "Hi", "how", "are", "you", "guys", "?", "<END>", "Tom:", ... ]

Resultó que mis datos de entrenamiento tienen un vocabulario de ~70.000 palabras únicas. Sin embargo, como aparecen muchas palabras solo una o dos veces, decidí reemplazar palabras tan raras por un token especial “unk>”. Esto tuvo el efecto de reducir el vocabulario a aproximadamente 25 000 palabras, lo que da lugar a un modelo más pequeño que debe entrenarse más adelante.

from collections import Counter

def get_infrequent_tokens(tokens: Union[List[str], str], min_count: int) -> List[str]:
"""
Identify tokens that appear less than a minimum count.

:param tokens: When it is the raw text in a string, frequencies are counted on character level.
When it is the tokenized corpus as list, frequencies are counted on token level.
:min_count: Threshold of occurence to flag a token.
:return: List of tokens that appear infrequently.
"""
counts = Counter(tokens)
infreq_tokens = set([k for k,v in counts.items() if v<=min_count])
return infreq_tokens

def mask_tokens(tokens: List[str], mask: Set[str]) -> List[str]:
"""
Iterate through all tokens. Any token that is part of the set, is replaced by the unknown token.

:param tokens: The tokenized corpus.
:param mask: Set of tokens that shall be masked in the corpus.
:return: List of tokenized corpus after the masking operation.
"""
return [t.replace(t, unknown_token) if t in mask else t for t in tokens]

infreq_tokens = get_infrequent_tokens(tokens, min_count=2)
tokens = mask_tokens(tokens, infreq_tokens)

["Alice:", "Hi", "how", "are", "you", "<UNK>", "?", "<END>", "Tom:", ... ]