14.6. Incorporação de subpalavra¶ Open the notebook in SageMaker Studio Lab
As palavras em inglês geralmente têm estruturas internas e métodos de formação. Por exemplo, podemos deduzir a relação entre “dog”, “dogs” e “dogcatcher” por sua grafia. Todas essas palavras têm a mesma raiz, “cachorro”, mas usam sufixos diferentes para mudar o significado da palavra. Além disso, essa associação pode ser estendida a outras palavras. Por exemplo, a relação entre “cachorro” e “cachorros” é exatamente como a relação entre “cat” e “cats”. A relação entre “boy” e “boyfriend” é igual à relação entre “girl” e “girlfriend”. Essa característica não é exclusiva do inglês. Em francês e espanhol, muitos verbos podem ter mais de 40 formas diferentes, dependendo do contexto. Em finlandês, um substantivo pode ter mais de 15 formas. Na verdade, a morfologia, que é um importante ramo da linguística, estuda a estrutura interna e a formação das palavras.
14.6.1. fastText¶
No word2vec, não usamos informações morfológicas diretamente. Em ambos os modelo skip-gram e modelo de saco de palavras contínuo, usamos diferentes vetores para representam palavras com diferentes formas. Por exemplo, “cachorro” e “cachorros” são representado por dois vetores diferentes, enquanto a relação entre esses dois vetores não é representado diretamente no modelo. Em vista disso, fastText [Bojanowski et al., 2017] propõe o método de incorporação de subpalavra, tentando assim introduzir informação morfológica no modelo skip-gram em word2vec.
Em fastText, cada palavra central é representada como uma coleção de subpalavras. A seguir, usamos a palavra “onde” como exemplo para entender como as subpalavras são formadas. Primeiro, adicionamos os caracteres especiais “<” e “>” no início e no final da palavra para distinguir as subpalavras usadas como prefixos e sufixos. Em seguida, tratamos a palavra como uma sequência de caracteres para extrair os \(n\)-gramas. Por exemplo, quando \(n=3\), podemos obter todas as subpalavras com um comprimento de \(3\):
e a subpalavra especial \(\textrm{"<where>"}\).
Em fastText, para uma palavra \(w\), registramos a união de todas as suas subpalavras com comprimento de \(3\) a \(6\) e as subpalavras especiais como \(\mathcal{G}_w\). Assim, o dicionário é a união da coleção de subpalavras de todas as palavras. Suponha que o vetor da subpalavra \(g\) no dicionário seja \(\mathbf{z}_g\). Então, o vetor de palavra central \(\mathbf{u}_w\) para a palavra \(w\) no modelo de grama de salto pode ser expresso como
O resto do processo fastText é consistente com o modelo skip-gram, portanto, não é repetido aqui. Como podemos ver, em comparação com o modelo skip-gram, o dicionário em fastText é maior, resultando em mais parâmetros do modelo. Além disso, o vetor de uma palavra requer a soma de todos os vetores de subpalavra, o que resulta em maior complexidade de computação. No entanto, podemos obter vetores melhores para palavras complexas mais incomuns, mesmo palavras que não existem no dicionário, olhando para outras palavras com estruturas semelhantes.
14.6.2. Codificação de par de bytes¶
Em fastText, todas as subpalavras extraídas devem ter os comprimentos especificados, como \(3\) a \(6\), portanto, o tamanho do vocabulário não pode ser predefinido. Para permitir subpalavras de comprimento variável em um vocabulário de tamanho fixo, podemos aplicar um algoritmo de compressão chamado de codificação de par de bytes (BPE) para extrair subpalavras [Sennrich et al., 2015].
A codificação de pares de bytes realiza uma análise estatística do conjunto de dados de treinamento para descobrir símbolos comuns em uma palavra, como caracteres consecutivos de comprimento arbitrário. Começando com símbolos de comprimento \(1\), a codificação de pares de bytes mescla iterativamente o par mais frequente de símbolos consecutivos para produzir novos símbolos mais longos. Observe que, para eficiência, os pares que cruzam os limites das palavras não são considerados. No final, podemos usar esses símbolos como subpalavras para segmentar palavras. A codificação de pares de bytes e suas variantes foram usadas para representações de entrada em modelos populares de pré-treinamento de processamento de linguagem natural, como GPT-2 [Radford et al., 2019] e RoBERTa [Liu et al., 2019]. A seguir, ilustraremos como funciona a codificação de pares de bytes.
Primeiro, inicializamos o vocabulário de símbolos como todos os
caracteres minúsculos do inglês, um símbolo especial de fim de palavra
'_'
e um símbolo especial desconhecido' [UNK] '
.
import collections
symbols = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'_', '[UNK]']
Uma vez que não consideramos pares de símbolos que cruzam os limites das
palavras, precisamos apenas de um dicionário raw_token_freqs
que
mapeia as palavras para suas frequências (número de ocorrências) em um
conjunto de dados. Observe que o símbolo especial '_'
é anexado a
cada palavra para que podemos facilmente recuperar uma sequência de
palavras (por exemplo, “um homem mais alto”) a partir de uma sequência
de símbolos de saída (por exemplo, “a_ tall er_ man”). Uma vez que
iniciamos o processo de fusão a partir de um vocabulário de apenas
caracteres únicos e símbolos especiais, o espaço é inserido entre cada
par de caracteres consecutivos dentro de cada palavra (chaves do
dicionário token_freqs
). Em outras palavras, o espaço é o
delimitador entre os símbolos de uma palavra.
raw_token_freqs = {'fast_': 4, 'faster_': 3, 'tall_': 5, 'taller_': 4}
token_freqs = {}
for token, freq in raw_token_freqs.items():
token_freqs[' '.join(list(token))] = raw_token_freqs[token]
token_freqs
{'f a s t _': 4, 'f a s t e r _': 3, 't a l l _': 5, 't a l l e r _': 4}
Definimos a seguinte função get_max_freq_pair
que retorna o par mais
frequente de símbolos consecutivos em uma palavra, onde as palavras vêm
de chaves do dicionário de entrada token_freqs
.
def get_max_freq_pair(token_freqs):
pairs = collections.defaultdict(int)
for token, freq in token_freqs.items():
symbols = token.split()
for i in range(len(symbols) - 1):
# Key of `pairs` is a tuple of two consecutive symbols
pairs[symbols[i], symbols[i + 1]] += freq
return max(pairs, key=pairs.get) # Key of `pairs` with the max value
Como uma abordagem gananciosa com base na frequência de símbolos
consecutivos, a codificação de pares de bytes usará a seguinte função
merge_symbols
para mesclar o par mais frequente de símbolos
consecutivos para produzir novos símbolos.
def merge_symbols(max_freq_pair, token_freqs, symbols):
symbols.append(''.join(max_freq_pair))
new_token_freqs = dict()
for token, freq in token_freqs.items():
new_token = token.replace(' '.join(max_freq_pair),
''.join(max_freq_pair))
new_token_freqs[new_token] = token_freqs[token]
return new_token_freqs
Agora realizamos iterativamente o algoritmo de codificação de par de
bytes sobre as chaves do dicionário token_freqs
. Na primeira
iteração, o par mais frequente de símbolos consecutivos são 't'
e' a'
, portanto, a codificação de pares de bytes os mescla para
produzir um novo símbolo 'ta'
. Na segunda iteração, a codificação do
par de bytes continua a mesclar 'ta'
e' l'
para resultar em
outro novo símbolo 'tal'
.
num_merges = 10
for i in range(num_merges):
max_freq_pair = get_max_freq_pair(token_freqs)
token_freqs = merge_symbols(max_freq_pair, token_freqs, symbols)
print(f'merge #{i + 1}:', max_freq_pair)
merge #1: ('t', 'a')
merge #2: ('ta', 'l')
merge #3: ('tal', 'l')
merge #4: ('f', 'a')
merge #5: ('fa', 's')
merge #6: ('fas', 't')
merge #7: ('e', 'r')
merge #8: ('er', '_')
merge #9: ('tall', '_')
merge #10: ('fast', '_')
Após 10 iterações de codificação de par de bytes, podemos ver que a lista de “símbolos” agora contém mais 10 símbolos que são mesclados iterativamente de outros símbolos.
print(symbols)
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '_', '[UNK]', 'ta', 'tal', 'tall', 'fa', 'fas', 'fast', 'er', 'er_', 'tall_', 'fast_']
Para o mesmo conjunto de dados especificado nas chaves do dicionário
raw_token_freqs
, cada palavra no conjunto de dados agora é
segmentada por subpalavras “fast_”, “fast”, “er_”, “tall_” e “tall”
como resultado do algoritmo de codificação de par de bytes. Por exemplo,
as palavras “faster_” e “taller_” são segmentadas como “fast er_” e
“tall er_”, respectivamente.
print(list(token_freqs.keys()))
['fast_', 'fast er_', 'tall_', 'tall er_']
Observe que o resultado da codificação do par de bytes depende do
conjunto de dados que está sendo usado. Também podemos usar as
sub-palavras aprendidas com um conjunto de dados para segmentar palavras
de outro conjunto de dados. Como uma abordagem gananciosa, a seguinte
função segment_BPE
tenta dividir as palavras nas subpalavras mais
longas possíveis a partir dos symbols
do argumento de entrada.
def segment_BPE(tokens, symbols):
outputs = []
for token in tokens:
start, end = 0, len(token)
cur_output = []
# Segment token with the longest possible subwords from symbols
while start < len(token) and start < end:
if token[start: end] in symbols:
cur_output.append(token[start: end])
start = end
end = len(token)
else:
end -= 1
if start < len(token):
cur_output.append('[UNK]')
outputs.append(' '.join(cur_output))
return outputs
A seguir, usamos as sub-palavras em ‘symbols’ da lista, que é aprendido
com o conjunto de dados acima mencionado, para segmentar tokens
que
representam outro conjunto de dados.
tokens = ['tallest_', 'fatter_']
print(segment_BPE(tokens, symbols))
['tall e s t _', 'fa t t er_']
14.6.3. Sumário¶
FastText propõe um método de incorporação de subpalavra. Com base no modelo skip-gram em word2vec, ele representa o vetor de palavra central como a soma dos vetores de subpalavra da palavra.
A incorporação de subpalavra utiliza os princípios da morfologia, o que geralmente melhora a qualidade das representações de palavras incomuns.
A codificação de pares de bytes realiza uma análise estatística do conjunto de dados de treinamento para descobrir símbolos comuns em uma palavra. Como uma abordagem gananciosa, a codificação de pares de bytes mescla iterativamente o par mais frequente de símbolos consecutivos.
14.6.4. Exercícios¶
Quando há muitas subpalavras (por exemplo, 6 palavras em inglês resultam em cerca de \(3\times 10^8\) combinações), quais são os problemas? Você consegue pensar em algum método para resolvê-los? Dica: consulte o final da seção 3.2 do artigo fastText [Bojanowski et al., 2017].
Como você pode projetar um modelo de incorporação de subpalavra com base no modelo de saco de palavras contínuo?
Para obter um vocabulário de tamanho \(m\), quantas operações de fusão são necessárias quando o tamanho inicial do vocabulário de símbolos é \(n\)?
Como podemos estender a ideia de codificação de par de bytes para extrair frases?