.. _sec_word2vec_data: Раздел учебника: Набор данных для предобучения словесных вложений ================================================================= Теперь, когда мы разобрали технические детали моделей word2vec и методов приближенного обучения, перейдем к их реализации. В частности, в качестве примера рассмотрим модель skip-gram из :numref:`sec_word2vec` и метод отрицательной выборки из :numref:`sec_approx_train`. В этом разделе мы начнем с набора данных для предобучения модели эмбеддингов слов: исходный формат данных будет преобразован в мини-пакеты, которые можно перебирать в процессе обучения. .. raw:: latex \diilbookstyleinputcell .. code:: python import collections import math import os import random import torch from d2l import torch as d2l Чтение набора данных -------------------- Используемый нами набор данных — `Penn Tree Bank (PTB) `__. Этот корпус сформирован из статей *The Wall Street Journal* и разделён на обучающую, валидационную и тестовую выборки. В исходном формате каждая строка текстового файла представляет собой предложение, слова в котором разделены пробелами. Здесь мы рассматриваем каждое слово как токен. .. raw:: latex \diilbookstyleinputcell .. code:: python #@save d2l.DATA_HUB['ptb'] = (d2l.DATA_URL + 'ptb.zip', '319d85e578af0cdc590547f26231e4e31cdf1e42') #@save def read_ptb(): """Load the PTB dataset into a list of text lines.""" data_dir = d2l.download_extract('ptb') # Read the training set with open(os.path.join(data_dir, 'ptb.train.txt')) as f: raw_text = f.read() return [line.split() for line in raw_text.split('\n')] sentences = read_ptb() f'# sentences: {len(sentences)}' Прочитав обучающую выборку, мы строим словарь для корпуса, в котором любое слово, встречающееся менее 10 раз, заменяется на токен . Обратите внимание, что исходный набор данных также содержит токены , обозначающие редкие (неизвестные) слова. .. raw:: latex \diilbookstyleinputcell .. code:: python vocab = d2l.Vocab(sentences, min_freq=10) f'vocab size: {len(vocab)}' Субсэмплирование ---------------- Текстовые данные обычно содержат высокочастотные слова, такие как «the», «a» и «in»: они могут встречаться даже миллиарды раз в очень больших корпусах. Однако эти слова часто встречаются вместе со многими различными словами в контекстных окнах, предоставляя мало полезной информации. Например, рассмотрим слово «chip» в контекстном окне: интуитивно его совместное появление с низкочастотным словом «intel» более полезно при обучении, чем совместное появление с высокочастотным словом «a». Кроме того, обучение на огромном количестве (высокочастотных) слов занимает много времени. Таким образом, при обучении моделей эмбеддингов слов высокочастотные слова можно *субдискретизировать* :cite:`Mikolov.Sutskever.Chen.ea.2013`. Конкретнее, каждое индексированное слово :math:`w_i` в наборе данных будет отброшено с вероятностью .. math:: P(w_i) = \max\left(1 - \sqrt{\frac{t}{f(w_i)}}, 0\right), где :math:`f(w_i)` — это отношение числа слов :math:`w_i` к общему числу слов в наборе данных, а константа :math:`t` — гиперпараметр (:math:`10^{-4}` в эксперименте). Мы видим, что слово :math:`w_i` (высокой частотности) может быть отброшено только тогда, когда его относительная частота :math:`f(w_i) > t`, и чем выше относительная частота слова, тем больше вероятность его отбрасывания. .. raw:: latex \diilbookstyleinputcell .. code:: python #@save def subsample(sentences, vocab): """Subsample high-frequency words.""" # Exclude unknown tokens ('') sentences = [[token for token in line if vocab[token] != vocab.unk] for line in sentences] counter = collections.Counter([ token for line in sentences for token in line]) num_tokens = sum(counter.values()) # Return True if `token` is kept during subsampling def keep(token): return(random.uniform(0, 1) < math.sqrt(1e-4 / counter[token] * num_tokens)) return ([[token for token in line if keep(token)] for line in sentences], counter) subsampled, counter = subsample(sentences, vocab) Приведённый ниже фрагмент кода отображает гистограмму количества токенов в предложении до и после субдискретизации. Как и ожидалось, субдискретизация значительно сокращает длину предложений за счёт удаления высокочастотных слов, что приводит к ускорению обучения. .. raw:: latex \diilbookstyleinputcell .. code:: python d2l.show_list_len_pair_hist(['origin', 'subsampled'], '# tokens per sentence', 'count', sentences, subsampled); Для отдельных токенов частота выборки высокочастотного слова «the» составляет менее 1/20. .. raw:: latex \diilbookstyleinputcell .. code:: python def compare_counts(token): return (f'# of "{token}": ' f'before={sum([l.count(token) for l in sentences])}, ' f'after={sum([l.count(token) for l in subsampled])}') compare_counts('the') В отличие от этого, слова с низкой частотностью, такие как «join», сохраняются полностью. .. raw:: latex \diilbookstyleinputcell .. code:: python compare_counts('join') После отбора подвыборки мы сопоставляем токены с их индексами в корпусе. .. raw:: latex \diilbookstyleinputcell .. code:: python corpus = [vocab[line] for line in subsampled] corpus[:3] Извлечение центральных слов и слов контекста -------------------------------------------- Следующая ``get_centers_and_contexts`` функция извлекает все центральные слова и их контекстные слова из ``corpus``. Она случайным образом выбирает целое число от 1 до ``max_window_size`` в качестве размера контекстного окна. Для любого центрального слова его контекстными словами являются те слова, расстояние до которых от него не превышает выбранного размера контекстного окна. .. raw:: latex \diilbookstyleinputcell .. code:: python #@save def get_centers_and_contexts(corpus, max_window_size): """Return center words and context words in skip-gram.""" centers, contexts = [], [] for line in corpus: # To form a "center word--context word" pair, each sentence needs to # have at least 2 words if len(line) < 2: continue centers += line for i in range(len(line)): # Context window centered at `i` window_size = random.randint(1, max_window_size) indices = list(range(max(0, i - window_size), min(len(line), i + 1 + window_size))) # Exclude the center word from the context words indices.remove(i) contexts.append([line[idx] for idx in indices]) return centers, contexts Далее мы создадим искусственный набор данных, содержащий два предложения, состоящих из 7 и 3 слов соответственно. Пусть максимальный размер контекстного окна равен 2, и выведем все центральные слова и их контекстные слова. .. raw:: latex \diilbookstyleinputcell .. code:: python tiny_dataset = [list(range(7)), list(range(7, 10))] print('dataset', tiny_dataset) for center, context in zip(*get_centers_and_contexts(tiny_dataset, 2)): print('center', center, 'has contexts', context) При обучении на наборе данных PTB мы устанавливаем максимальный размер контекстного окна равным 5. Приведённый ниже код извлекает все центральные слова и их контекстные слова из набора данных. .. raw:: latex \diilbookstyleinputcell .. code:: python all_centers, all_contexts = get_centers_and_contexts(corpus, 5) f'# center-context pairs: {sum([len(contexts) for contexts in all_contexts])}' Отрицательная выборка --------------------- Для приближённого обучения мы используем отрицательную выборку. Чтобы выбирать шумовые слова в соответствии с заданным распределением, мы определяем следующий класс ``RandomGenerator``, в котором (возможно, ненормированное) распределение выборки передаётся через аргумент ``sampling_weights``. .. raw:: latex \diilbookstyleinputcell .. code:: python #@save class RandomGenerator: """Randomly draw among {1, ..., n} according to n sampling weights.""" def __init__(self, sampling_weights): # Exclude self.population = list(range(1, len(sampling_weights) + 1)) self.sampling_weights = sampling_weights self.candidates = [] self.i = 0 def draw(self): if self.i == len(self.candidates): # Cache `k` random sampling results self.candidates = random.choices( self.population, self.sampling_weights, k=10000) self.i = 0 self.i += 1 return self.candidates[self.i - 1] Например, мы можем выбрать 10 случайных величин :math:`X` среди индексов 1, 2 и 3 с вероятностями выборки :math:`P(X=1)=2/9, P(X=2)=3/9` и :math:`P(X=3)=4/9` следующим образом. Для пары из центрального слова и слова контекста мы случайным образом выбираем ``K`` (5 в эксперименте) шумовых примеров. Согласно рекомендациям из статьи word2vec, вероятность выбора :math:`P(w)` шумового примера :math:`w` устанавливается равной его относительной частоте в словаре, возведённой в степень 0.75 :cite:`Mikolov.Sutskever.Chen.ea.2013`. .. raw:: latex \diilbookstyleinputcell .. code:: python #@save def get_negatives(all_contexts, vocab, counter, K): """Return noise words in negative sampling.""" # Sampling weights for words with indices 1, 2, ... (index 0 is the # excluded unknown token) in the vocabulary sampling_weights = [counter[vocab.to_tokens(i)]**0.75 for i in range(1, len(vocab))] all_negatives, generator = [], RandomGenerator(sampling_weights) for contexts in all_contexts: negatives = [] while len(negatives) < len(contexts) * K: neg = generator.draw() # Noise words cannot be context words if neg not in contexts: negatives.append(neg) all_negatives.append(negatives) return all_negatives all_negatives = get_negatives(all_contexts, vocab, counter, 5) .. _subsec_word2vec-minibatch-loading: Загрузка обучающих примеров мини-пакетами ----------------------------------------- После извлечения всех центральных слов вместе с их контекстными и сэмплированными шумовыми словами они преобразуются в мини-пакеты примеров, которые можно итеративно загружать во время обучения. В мини-батче :math:`i^\textrm{th}` пример включает центральное слово, его :math:`n_i` контекстные слова и :math:`m_i` шумовые слова. Из-за различных размеров контекстного окна :math:`n_i+m_i` варьируется для разных :math:`i`. Поэтому для каждого примера мы объединяем его контекстные и шумовые слова в переменной ``contexts_negatives`` и дополняем нулями до тех пор, пока длина объединения не достигнет :math:`\max_i n_i+m_i` (``max_len``). Чтобы исключить дополнения из вычисления функции потерь, мы определяем переменную маски ``masks``. Существует взаимно однозначное соответствие между элементами в ``masks`` и элементами в ``contexts_negatives``, где нули (в противном случае единицы) в ``masks`` соответствуют дополнениям в ``contexts_negatives``. Чтобы различать положительные и отрицательные примеры, мы отделяем контекстные слова от шумовых в ``contexts_negatives`` с помощью логической переменной ``labels``. Аналогично ``masks``, существует взаимно однозначное соответствие между элементами ``labels`` и элементами ``contexts_negatives``, где единицы (в противном случае нули) в ``labels`` соответствуют контекстным словам (положительным примерам) в ``contexts_negatives``. Реализация описанной выше идеи представлена в следующей ``batchify`` функции. Её входной параметр ``data`` — это список длиной, равной размеру батча, каждый элемент которого является примером, состоящим из центрального слова ``center``, его контекстных слов ``context`` и шумовых слов ``negative``. Функция возвращает мини-батч, который может быть использован для вычислений в процессе обучения, например, содержащий переменную маски. .. raw:: latex \diilbookstyleinputcell .. code:: python #@save def batchify(data): """Return a minibatch of examples for skip-gram with negative sampling.""" max_len = max(len(c) + len(n) for _, c, n in data) centers, contexts_negatives, masks, labels = [], [], [], [] for center, context, negative in data: cur_len = len(context) + len(negative) centers += [center] contexts_negatives += [context + negative + [0] * (max_len - cur_len)] masks += [[1] * cur_len + [0] * (max_len - cur_len)] labels += [[1] * len(context) + [0] * (max_len - len(context))] return (torch.tensor(centers).reshape((-1, 1)), torch.tensor( contexts_negatives), torch.tensor(masks), torch.tensor(labels)) Давайте проверим эту функцию на мини-батче из двух примеров. .. raw:: latex \diilbookstyleinputcell .. code:: python x_1 = (1, [2, 2], [3, 3, 3, 3]) x_2 = (1, [2, 2, 2], [3, 3]) batch = batchify((x_1, x_2)) names = ['centers', 'contexts_negatives', 'masks', 'labels'] for name, data in zip(names, batch): print(name, '=', data) Собираем всё вместе ------------------- Наконец, мы определяем функцию ``load_data_ptb``, которая считывает набор данных PTB и возвращает итератор данных и словарь. .. raw:: latex \diilbookstyleinputcell .. code:: python #@save def load_data_ptb(batch_size, max_window_size, num_noise_words): """Download the PTB dataset and then load it into memory.""" num_workers = d2l.get_dataloader_workers() sentences = read_ptb() vocab = d2l.Vocab(sentences, min_freq=10) subsampled, counter = subsample(sentences, vocab) corpus = [vocab[line] for line in subsampled] all_centers, all_contexts = get_centers_and_contexts( corpus, max_window_size) all_negatives = get_negatives( all_contexts, vocab, counter, num_noise_words) class PTBDataset(torch.utils.data.Dataset): def __init__(self, centers, contexts, negatives): assert len(centers) == len(contexts) == len(negatives) self.centers = centers self.contexts = contexts self.negatives = negatives def __getitem__(self, index): return (self.centers[index], self.contexts[index], self.negatives[index]) def __len__(self): return len(self.centers) dataset = PTBDataset(all_centers, all_contexts, all_negatives) data_iter = torch.utils.data.DataLoader(dataset, batch_size, shuffle=True, collate_fn=batchify, num_workers=num_workers) return data_iter, vocab Давайте выведем на экран первый мини-батч итератора данных. .. raw:: latex \diilbookstyleinputcell .. code:: python data_iter, vocab = load_data_ptb(512, 5, 5) for batch in data_iter: for name, data in zip(names, batch): print(name, 'shape:', data.shape) break Итоги ----- - Частотные слова могут быть не столь полезны при обучении. Мы можем выполнять их субдискретизацию для ускорения обучения. - Для повышения вычислительной эффективности мы загружаем примеры мини-пакетами. Мы можем определить дополнительные переменные, чтобы отличать дополнения от реальных данных, а положительные примеры от отрицательных. Упражнения ---------- 1. Как изменится время выполнения кода в этом разделе, если не использовать субдискретизацию? 2. Класс ``RandomGenerator`` кэширует результаты случайной выборки ``k``. Установите ``k`` в другие значения и посмотрите, как это повлияет на скорость загрузки данных. 3. Какие другие гиперпараметры в коде этого раздела могут влиять на скорость загрузки данных? `Обсуждения `__