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 RegexpTokenizerdef 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 Counterdef 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:", ... ]