Tokenizers
Os Tokenizers são um dos componentes centrais do pipeline da PNL. Eles têm um propósito: traduzir texto em dados que podem ser processados pelo modelo. Os modelos só podem processar números, portanto os tokenizers precisam converter nossas entradas de texto em dados numéricos. Nesta seção, vamos explorar exatamente o que acontece no pipeline de tokenização.
Nas tarefas de PNL, os dados que são geralmente processados são texto bruto. Aqui está um exemplo de tal texto:
Jim Henson was a puppeteer
Entretanto, os modelos só podem processar números, portanto, precisamos encontrar uma maneira de converter o texto bruto em números. Isto é o que os tokenizers fazem, e há muitas maneiras de se fazer isso. O objetivo é encontrar a representação mais significativa - ou seja, a que faz mais sentido para o modelo - e, se possível, a menor representação.
Vamos dar uma olhada em alguns exemplos de algoritmos de tokenização, e tentar responder algumas das perguntas que você possa ter sobre tokenização.
Baseado em palavras (word-based)
O primeiro tipo de tokenizer que me vem à mente é baseado em palavras. É geralmente muito fácil de instalar e usar com apenas algumas regras, e muitas vezes produz resultados decentes. Por exemplo, na imagem abaixo, o objetivo é dividir o texto bruto em palavras e encontrar uma representação numérica para cada uma delas:
Há diferentes maneiras de dividir o texto. Por exemplo, poderíamos utilizar o espaço em branco para simbolizar o texto em palavras, usando a função split()
do Python:
tokenized_text = "Jim Henson was a puppeteer".split()
print(tokenized_text)
['Jim', 'Henson', 'was', 'a', 'puppeteer']
Há também variações de tokenizers de palavras que têm regras extras para pontuação. Com este tipo de tokenizer, podemos terminar com alguns “vocabulários” bem grandes, onde um vocabulário é definido pelo número total de tokens independentes que tem no texto de exemplo.
A cada palavra é atribuída uma identificação, começando em 0 e indo até o tamanho do vocabulário. O modelo utiliza estas identificações para identificar cada palavra.
Se quisermos cobrir completamente um idioma com um tokenizer baseado em palavras, precisaremos ter um identificador para cada palavra no idioma, o que gerará uma enorme quantidade de tokens. Por exemplo, existem mais de 500.000 palavras no idioma inglês, portanto, para construir um mapa a partir de cada palavra para um ID de entrada, precisaríamos manter um registro desse grande número de IDs. Além disso, palavras como “dog” são representadas de forma diferente de palavras como “dogs”, e o modelo inicialmente não terá como saber que “dog” e “dogs” são semelhantes: ele identificará as duas palavras como não relacionadas. O mesmo se aplica a outras palavras semelhantes, como “run” e “running”, que o modelo não verá inicialmente como sendo semelhantes.
Finalmente, precisamos de token personalizada para representar palavras que não estão em nosso vocabulário. Isto é conhecido como o símbolo “unknown” (desconhecido), frequentemente representado como ”[UNK]” ou ”<unk>”. Geralmente é um mau sinal se você vê que o tokenizer está produzindo muitos desses tokens, pois não foi capaz de recuperar uma representação sensata de uma palavra e você está perdendo informações ao longo do caminho. O objetivo ao elaborar o vocabulário é fazê-lo de tal forma que o tokenizer transforme o menor número possível de palavras no token desconhecido.
Uma maneira de reduzir a quantidade de tokens desconhecidas é ir um nível mais fundo, usando um tokenizer baseado em caracteres.
Baseado em caracteres (Character-based)
Os tokenizers baseados em caracteres dividem o texto em caracteres, ao invés de palavras. Isto tem dois benefícios principais:
- O vocabulário será muito menor;
- Há muito menos tokes fora de vocabulário (desconhecidas), uma vez que cada palavra pode ser construída a partir de personagens.
Mas também aqui surgem algumas questões sobre os espaços e à pontuação:
Esta abordagem também não é perfeita. Como a representação agora é baseada em caracteres e não em palavras, pode-se argumentar que, intuitivamente, ela é menos significativa: cada caractere não significa muito por si só, ao contrario do caso das palavras. No entanto, isto novamente difere de acordo com o idioma; em chinês, por exemplo, cada caractere traz mais informações do que um caractere em um idioma latino.
Outra coisa a considerar é que acabaremos com uma quantidade muito grande de tokens a serem processadas por nosso modelo: enquanto uma palavra seria apenas um único token com um tokenizer baseado em palavras, ela pode facilmente se transformar em 10 ou mais tokens quando convertida em caracteres.
Para obter o melhor dos dois mundos, podemos usar uma terceira técnica que combina as duas abordagens: Tokenização por sub-palavras.
Tokenização por sub-palavras (Subword tokenization)
Algoritmos de tokenização de sub-palavras baseiam-se no princípio de que palavras frequentemente usadas não devem ser divididas em sub-palavras menores, mas palavras raras devem ser decompostas em sub-palavras significativas.
Por exemplo, “irritantemente” poderia ser considerado uma palavra rara e poderia ser decomposto em “irritante” e “mente”. É provável que ambas apareçam mais frequentemente como sub-palavras isoladas, enquanto ao mesmo tempo o significado de “irritantemente” é mantido pelo significado composto de “irritante” e “mente”.
Aqui está um exemplo que mostra como um algoritmo de tokenização de uma sub-palavra indicaria a sequência “Let’s do tokenization!
Estas sub-palavras acabam fornecendo muito significado semântico: por exemplo, no exemplo acima “tokenization” foi dividido em “token” e “ization”, dois tokens que têm um significado semântico enquanto são eficientes em termos de espaço (apenas dois tokens são necessários para representar uma palavra longa). Isto nos permite ter uma cobertura relativamente boa com pequenos vocabulários, e perto de nenhum token desconhecido.
Esta abordagem é especialmente útil em idiomas aglutinativos como o turco, onde é possível formar palavras (quase) arbitrariamente longas e complexas, encadeando sub-palavras.
E outros!
Sem surpresas, há muito mais técnicas por aí. Para citar algumas:
- Byte-level BPE, utilizada no GPT-2
- WordPiece, utilizada em BERT
- SentencePiece ou Unigram, como as utilizadas em vários modelos multilíngue
Agora você deve ter conhecimento suficiente de como funcionam os tokenizers para começar a utilizar a API.
Carregando e salvando
Carregando e salvando tokenizers é tão simples quanto com os modelos. Na verdade, ele se baseia nos mesmos dois métodos: from_pretrained()
e save_pretrained()
. Estes métodos irão carregar ou salvar o algoritmo utilizado pelo tokenizer (um pouco como a arquitetura do modelo), bem como seu vocabulário (um pouco como os pesos do modelo).
O carregamento do tokenizer BERT treinado com o mesmo checkpoint do BERT é feito da mesma forma que o carregamento do modelo, exceto que utilizamos a classe BertTokenizer
:
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained("bert-base-cased")
Similar ao AutoModel
, a classe AutoTokenizer
ira carregar a classe tokenizer apropriada na biblioteca com base no nome do checkpoint, e pode ser utilizada diretamente com qualquer checkpoint:
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
Agora podemos usar o tokenizer, como mostrado na seção anterior:
tokenizer("Using a Transformer network is simple")
{'input_ids': [101, 7993, 170, 11303, 1200, 2443, 1110, 3014, 102],
'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0],
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1]}
Salvar um tokenizer é idêntico a salvar um modelo:
tokenizer.save_pretrained("directory_on_my_computer")
Falaremos mais sobre token_type_ids' no [Capítulo 3](/course/pt/chapter3), e explicaremos a
attention_mask’ um pouco mais tarde. Primeiro, vamos ver como os input_ids
são gerados. Para fazer isso, precisaremos olhar os métodos intermediários do tokenizer.
Encoding
Traduzir texto para números é conhecido como encoding. O encoding é feito em um processo de duas etapas: a tokenização, seguida pela conversão para IDs de entrada.
Como vimos, o primeiro passo é dividir o texto em palavras (ou partes de palavras, símbolos de pontuação, etc.), normalmente chamadas de tokens. Há várias regras que podem guiar esse processo, e é por isso que precisamos instanciar o tokenizer usando o nome do modelo, para nos certificarmos de usar as mesmas regras que foram usadas quando o modelo foi pré-treinado.
O segundo passo é converter esses tokens em números, para que possamos construir um tensor a partir deles e alimentá-los com o modelo. Para isso, o tokenizer tem um vocabulário (vocabulary), que é a parte que realizamos o download quando o instanciamos com o método from_pretrained()
. Mais uma vez, precisamos utilizar o mesmo vocabulário utilizado quando o modelo foi pré-treinado.
Para entender melhor os dois passos, vamos explorá-los separadamente. Note que usaremos alguns métodos que executam partes da pipeline de tokenização separadamente para mostrar os resultados intermediários dessas etapas, mas na prática, você deve chamar o tokenizer diretamente em suas entradas (como mostrado na seção 2).
Tokenização
O processo de tokenization é feito através do método tokenize()
do tokenizer:
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
sequence = "Using a Transformer network is simple"
tokens = tokenizer.tokenize(sequence)
print(tokens)
A saída deste método é uma lista de strings, ou tokens:
['Using', 'a', 'transform', '##er', 'network', 'is', 'simple']
Este tokenizer é um tokenizer de sub-palavras: ele divide as palavras até obter tokens que podem ser representadas por seu vocabulário. É o caso aqui do “transformer”, que é dividido em dois tokens: “transform” e “##er”.
Desde os tokens até IDs de entrada
A conversão para IDs de entrada é feita pelo método de tokenização convert_tokens_to_ids()
:
ids = tokenizer.convert_tokens_to_ids(tokens)
print(ids)
[7993, 170, 11303, 1200, 2443, 1110, 3014]
Estas saídas, uma vez convertidas no tensor com a estrutura apropriada, podem então ser usadas como entradas para um modelo como visto anteriormente neste capítulo.
✏️ Experimente realizar isso! Replicar os dois últimos passos (tokenização e conversão para IDs de entrada) nas frases de entrada que usamos na seção 2 (“I’ve been waiting for a HuggingFace course my whole life.” e “I hate this so much!”). Verifique se você recebe os mesmos IDs de entrada que recebemos antes!
Decoding
Decoding vai pela direção ao contrário: a partir de índices de vocabulário, queremos obter uma string. Isto pode ser feito com o método decode()
da seguinte forma:
decoded_string = tokenizer.decode([7993, 170, 11303, 1200, 2443, 1110, 3014])
print(decoded_string)
'Using a Transformer network is simple'
Observe que o método decode
não apenas converte os índices em tokens, mas também agrupa os tokens que fizeram parte das mesmas palavras para produzir uma frase legível. Este comportamento será extremamente útil quando utilizamos modelos que preveem um novo texto (seja texto gerado a partir de um prompt, ou para problemas de sequence-to-sequence como tradução ou sumarização).
Até agora você já deve entender as operações atômicas que um tokenizer pode lidar: tokenização, conversão para IDs, e conversão de IDs de volta para uma string. Entretanto, acabamos de começar a ver a ponta do iceberg. Na seção seguinte, vamos nos aproximar de seus limites e dar uma olhada em como superá-los.