Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

14.6 Вложения подслов

В английском языке слова «helps», «helped» и «helping» — словоизменительные формы одного слова «help». Отношение между «dog» и «dogs» такое же, как между «cat» и «cats», а отношение между «boy» и «boyfriend» — такое же, как между «girl» и «girlfriend». В других языках, например во французском и испанском, у многих глаголов более 40 словоизменительных форм, а в финском у существительного может быть до 15 падежных форм. В лингвистике морфология изучает образование слов и отношения между словами. Однако ни word2vec, ни GloVe не исследуют внутреннюю структуру слов.

14.6.1 Модель fastText

Вспомним, как слова представляются в word2vec. И в модели skip-gram, и в модели непрерывного мешка слов разные словоизменительные формы одного и того же слова представляются разными векторами напрямую, без общих параметров. Чтобы использовать морфологическую информацию, модель fastText предложила подход вложения подслов, где подслово — это символьная -грамма (Bojanowski et al., 2017). fastText можно рассматривать как модель skip-gram на уровне подслов: вместо обучения векторных представлений на уровне слов каждое центральное слово представляется суммой векторов его подслов.

Покажем, как получить подслова для каждого центрального слова в fastText, на примере слова «where». Сначала добавим в начало и конец слова специальные символы «<» и «>», чтобы отличать префиксы и суффиксы от других подслов. Затем извлечём из слова символьные -граммы. Например, при мы получим все подслова длины 3: «<wh», «whe», «her», «ere», «re>» и специальное подслово «<where>».

В fastText для любого слова через обозначают объединение всех его подслов длины от 3 до 6 и его специального подслова. Словарь — это объединение подслов всех слов. Пусть — вектор подслова в словаре; тогда вектор слова как центрального слова в модели skip-gram — это сумма векторов его подслов:

Остальное в fastText совпадает с моделью skip-gram. По сравнению с skip-gram словарь fastText больше, а параметров модели больше. Кроме того, чтобы вычислить представление слова, нужно просуммировать векторы всех его подслов, что приводит к более высокой вычислительной сложности. Однако благодаря общим параметрам подслов у слов со сходной структурой редкие слова и даже слова вне словаря могут получать в fastText лучшие векторные представления.

14.6.2 Кодирование пар байтов (Byte Pair Encoding)

В fastText все извлекаемые подслова должны иметь заданную длину, например от до , поэтому размер словаря заранее определить нельзя. Чтобы получить подслова переменной длины в словаре фиксированного размера, можно применить алгоритм сжатия, называемый кодированием пар байтов (byte pair encoding, BPE), для извлечения подслов (Sennrich et al., 2015).

Кодирование пар байтов проводит статистический анализ обучающего набора данных, чтобы найти общие символы внутри слов — последовательности подряд идущих знаков произвольной длины. Начиная с символов длины 1, BPE итеративно объединяет самые частые пары подряд идущих символов, порождая новые, более длинные символы. Для эффективности пары, пересекающие границы слов, не рассматриваются. В итоге слова можно разбивать на подслова, составленные из таких символов. BPE и его варианты использовались для входных представлений в предобученных моделях обработки естественного языка, таких как GPT-2 (Radford et al., 2019) и RoBERTa (Liu et al., 2019). Ниже мы покажем, как работает кодирование пар байтов.

Сначала инициализируем словарь символов всеми строчными буквами английского алфавита, специальным символом конца слова '_' и специальным символом неизвестного токена '[UNK]'.

MXNet, PyTorch:

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]']
           

PaddlePaddle:

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]']
           

Поскольку пары символов, пересекающие границы слов, не рассматриваются, нам нужен только словарь raw_token_freqs, отображающий слова в их частоты (число вхождений) в наборе данных. Обратите внимание: специальный символ '_' добавляется в конец каждого слова, чтобы по выходной последовательности символов (например, «a_ tall er_ man») легко восстановить последовательность слов (например, «a taller man»). Поскольку мы начинаем процесс слияния только с одиночных символов и специальных символов, между каждой парой подряд идущих символов внутри каждого слова (ключа словаря token_freqs) вставляется пробел. Иными словами, пробел — разделитель между символами слова.

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

Определим функцию get_max_freq_pair, которая возвращает самую частую пару подряд идущих символов внутри слов; слова берутся из ключей входного словаря 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):
            # Ключ «pairs» — кортеж из двух подряд идущих символов
            pairs[symbols[i], symbols[i + 1]] += freq
    return max(pairs, key=pairs.get)  # Ключ «pairs» с наибольшим значением

Как жадный метод, основанный на частотах подряд идущих символов, кодирование пар байтов использует следующую функцию merge_symbols, чтобы объединить самую частую пару подряд идущих символов и получить новый символ.

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

Теперь итеративно выполним алгоритм кодирования пар байтов над ключами словаря token_freqs. На первой итерации самая частая пара подряд идущих символов — 't' и 'a', поэтому BPE объединяет их в новый символ 'ta'. На второй итерации BPE продолжает объединять 'ta' и 'l' и получает ещё один новый символ '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'слияние № {i+1}:',max_freq_pair)

После 10 итераций кодирования пар байтов видно, что список symbols теперь содержит ещё 10 символов, итеративно полученных слиянием из других символов.

print(symbols)

Для того же набора данных, заданного ключами словаря raw_token_freqs, в результате алгоритма кодирования пар байтов каждое слово набора теперь разбито на подслова «fast_», «fast», «er_», «tall_» и «tall». Например, слова «faster_» и «taller_» разбиты на «fast er_» и «tall er_» соответственно.

print(list(token_freqs.keys()))

Заметим, что результат кодирования пар байтов зависит от используемого набора данных. Подслова, выученные на одном наборе данных, можно использовать и для разбиения слов другого набора. Как жадный метод, следующая функция segment_BPE пытается разбить слова на самые длинные подслова из входного параметра symbols.

def segment_BPE(tokens, symbols):
    outputs = []
    for token in tokens:
        start, end = 0, len(token)
        cur_output = []
        # Сегментируем токен самыми длинными подсловами из 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

Мы используем подслова из списка symbols (выученные на описанном выше наборе данных), чтобы представить tokens из другого набора данных.

tokens = ['tallest_', 'fatter_']
print(segment_BPE(tokens, symbols))

Резюме

  • Модель fastText предлагает метод вложения подслов: на основе модели skip-gram из word2vec она представляет центральное слово суммой векторов его подслов.
  • Кодирование пар байтов проводит статистический анализ обучающего набора данных, чтобы найти общие символы внутри слов. Как жадный метод, оно итеративно объединяет самые частые пары подряд идущих символов.
  • Вложения подслов могут улучшить качество представлений редких слов и слов вне словаря.

Упражнения

  1. Например, в английском языке около возможных -грамм. Какая проблема возникает при слишком большом числе подслов? Как её решить? Подсказка: см. конец раздела 3.2 статьи о fastText (Bojanowski et al., 2017).
  2. Как спроектировать модель вложений подслов на основе модели непрерывного мешка слов?
  3. Сколько операций слияния нужно, чтобы получить словарь размера , если размер начального словаря символов равен ?
  4. Как расширить идею кодирования пар байтов, чтобы извлекать фразы?

MXNet: Обсуждение

PyTorch: Обсуждение

PaddlePaddle: Обсуждение

Литература

  • Bojanowski, P., Grave, E., Joulin, A., & Mikolov, T. (2017). Enriching word vectors with subword information. Transactions of the Association for Computational Linguistics, 5, 135–146.
  • Sennrich, R., Haddow, B., & Birch, A. (2015). Neural machine translation of rare words with subword units. arXiv preprint arXiv:1508.07909.
  • Radford, A., Wu, J., Child, R., Luan, D., Amodei, D., & Sutskever, I. (2019). Language models are unsupervised multitask learners. OpenAI Blog, 1(8), 9.
  • Liu, Y., Ott, M., Goyal, N., Du, J., Joshi, M., Chen, D., Levy, O., Lewis, M., Zettlemoyer, L., & Stoyanov, V. (2019). Roberta: A robustly optimized bert pretraining approach. arXiv preprint arXiv:1907.11692.