3.3. Синтетические данные для регрессии

Машинное обучение заключается в извлечении информации из данных. Поэтому может возникнуть вопрос: что же мы можем узнать из синтетических данных? Хотя нас могут не интересовать сами по себе закономерности, которые мы заложили в модель генерации искусственных данных, такие наборы данных всё же полезны в учебных целях. Они помогают нам оценивать свойства алгоритмов машинного обучения и подтверждать, что наши реализации работают так, как ожидается. Например, если мы создаём данные, для которых истинные параметры известны априори, то мы можем проверить, способна ли наша модель действительно их восстановить.

%matplotlib inline
import random
import torch
from d2l import torch as d2l

3.3.1. Генерация набора данных

Для этого примера мы будем работать в низкоразмерном пространстве для краткости. Приведённый ниже фрагмент кода генерирует 1000 примеров с признаками размерности 2, взятыми из стандартного нормального распределения. Полученная матрица признаков \(\mathbf{X}\) принадлежит пространству \(\mathbb{R}^{1000 \times 2}\). Мы генерируем каждую метку, применяя истинную линейную функцию, и добавляем к ней аддитивный шум \(\boldsymbol{\epsilon}\), который для каждого примера выбирается независимо и из одного и того же распределения:

(3.3.1)\[\mathbf{y}= \mathbf{X} \mathbf{w} + b + \boldsymbol{\epsilon}.\]

Для удобства мы предполагаем, что \(\boldsymbol{\epsilon}\) берётся из нормального распределения со средним \(\mu= 0\) и стандартным отклонением \(\sigma = 0.01\). Обратите внимание, что в рамках объектно-ориентированного подхода мы добавляем код в метод __init__ подкласса d2l.DataModule (описанного в Section 3.2.3). Хорошей практикой является возможность задавать любые дополнительные гиперпараметры. Мы реализуем это с помощью save_hyperparameters(). Значение batch_size будет определено позже.

class SyntheticRegressionData(d2l.DataModule):  #@save
    """Synthetic data for linear regression."""
    def __init__(self, w, b, noise=0.01, num_train=1000, num_val=1000,
                 batch_size=32):
        super().__init__()
        self.save_hyperparameters()
        n = num_train + num_val
        self.X = torch.randn(n, len(w))
        noise = torch.randn(n, 1) * noise
        self.y = torch.matmul(self.X, w.reshape((-1, 1))) + b + noise

Ниже мы зададим истинные параметры равными \(\mathbf{w} = [2, -3.4]^\top\) и \(b = 4.2\). Позже мы сможем сравнить наши оценённые параметры с этими значениями истинного положения (ground truth).

data = SyntheticRegressionData(w=torch.tensor([2, -3.4]), b=4.2)

Каждая строка в features представляет собой вектор из \(\mathbb{R}^2\), а каждая строка в labels — скаляр. Давайте рассмотрим первый элемент.

print('features:', data.X[0],'\nlabel:', data.y[0])

3.3.2. Чтение набора данных

Обучение моделей машинного обучения часто требует многократного прохода по набору данных, при котором каждый раз выбирается один мини-батч примеров. Эти данные затем используются для обновления модели. Чтобы проиллюстрировать, как это работает, мы реализуем метод get_dataloader, зарегистрировав его в классе SyntheticRegressionData через add_to_class (описанный в Section 3.2.1). Он принимает размер батча, матрицу признаков и вектор меток, а затем генерирует мини-батчи размера batch_size. Таким образом, каждый мини-батч состоит из кортежа признаков и меток. Обратите внимание, что нам нужно учитывать, находимся ли мы в режиме обучения или валидации: в первом случае данные следует читать в случайном порядке, тогда как во втором возможность чтения данных в заданном порядке может быть важна для целей отладки.

@d2l.add_to_class(SyntheticRegressionData)
def get_dataloader(self, train):
    if train:
        indices = list(range(0, self.num_train))
        # The examples are read in random order
        random.shuffle(indices)
    else:
        indices = list(range(self.num_train, self.num_train+self.num_val))
    for i in range(0, len(indices), self.batch_size):
        batch_indices = torch.tensor(indices[i: i+self.batch_size])
        yield self.X[batch_indices], self.y[batch_indices]

Чтобы развить интуицию, давайте рассмотрим первый мини-батч данных. Каждый мини-батч признаков содержит информацию о его размере и размерности входных признаков. Аналогично, наш мини-батч меток будет иметь согласованную форму, определяемую batch_size.

X, y = next(iter(data.train_dataloader()))
print('X shape:', X.shape, '\ny shape:', y.shape)

Хотя это может показаться незначительным, вызов iter(data.train_dataloader()) демонстрирует мощь объектно-ориентированного дизайна Python. Обратите внимание, что мы добавили метод в класс SyntheticRegressionData после создания объекта data. Тем не менее, объект получает выгоду от добавления функциональности в класс ex post facto.

В течение каждой итерации мы получаем различные мини-пакеты до тех пор, пока весь набор данных не будет исчерпан (попробуйте это сделать). Хотя реализованная выше итерация хороша для учебных целей, она неэффективна, что может создать проблемы при решении реальных задач. Например, она требует загрузки всех данных в память и выполнения множества случайных обращений к памяти. Встроенные итераторы, реализованные во фреймворке глубокого обучения, значительно эффективнее и могут работать с источниками данных, такими как данные, хранящиеся в файлах, данные, получаемые через поток, а также данные, генерируемые или обрабатываемые на лету. Далее давайте попробуем реализовать тот же метод с использованием встроенных итераторов.

3.3.3. Краткая реализация загрузчика данных

Вместо написания собственного итератора мы можем воспользоваться существующим API фреймворка для загрузки данных. Как и прежде, нам нужен набор данных с признаками X и метками y. Кроме того, мы задаём параметры batch_size во встроенном загрузчике данных и позволяем ему эффективно перемешивать примеры.

@d2l.add_to_class(d2l.DataModule)  #@save
def get_tensorloader(self, tensors, train, indices=slice(0, None)):
    tensors = tuple(a[indices] for a in tensors)
    dataset = torch.utils.data.TensorDataset(*tensors)
    return torch.utils.data.DataLoader(dataset, self.batch_size,
                                       shuffle=train)
@d2l.add_to_class(SyntheticRegressionData)  #@save
def get_dataloader(self, train):
    i = slice(0, self.num_train) if train else slice(self.num_train, None)
    return self.get_tensorloader((self.X, self.y), train, i)

Новый загрузчик данных работает так же, как и предыдущий, но при этом он более эффективен и обладает дополнительными возможностями.

X, y = next(iter(data.train_dataloader()))
print('X shape:', X.shape, '\ny shape:', y.shape)

Например, загрузчик данных, предоставляемый API фреймворка, поддерживает встроенный метод __len__, поэтому мы можем выполнить запрос к его длине, то есть количеству батчей.

len(data.train_dataloader())

3.3.4. Итоги

Загрузчики данных — это удобный способ абстрагировать процесс загрузки и обработки данных. Благодаря этому один и тот же алгоритм машинного обучения способен обрабатывать множество различных типов и источников данных без необходимости внесения изменений. Одним из преимуществ загрузчиков данных является их возможность композиции. Например, мы можем загружать изображения, а затем применять к ним фильтр постобработки, который обрезает их или иным образом изменяет. Таким образом, загрузчики данных могут использоваться для описания всего конвейера обработки данных.

Что касается самой модели, то двумерная линейная модель — одна из самых простых, с которыми мы можем столкнуться. Она позволяет нам проверять точность моделей регрессии, не беспокоясь о недостаточном объёме данных или недоопределенной системе уравнений. Мы с успехом воспользуемся этим в следующем разделе.

3.3.5. Упражнения

  1. Что произойдет, если количество примеров не делится на размер батча? Как изменить это поведение, указав другой аргумент с помощью API фреймворка?

  2. Предположим, что мы хотим сгенерировать большой набор данных, в котором размер вектора параметров w и количество примеров num_examples велики.

    1. Что произойдет, если мы не можем уместить все данные в памяти?

    2. Как бы вы перемешали данные, если они хранятся на диске? Ваша задача — разработать эффективный алгоритм, который не требует большого количества случайных чтений или записей. Подсказка: генераторы псевдослучайных перестановок позволяют организовать перемешивание без необходимости явно хранить таблицу перестановок (Naor and Reingold, 1999).

  3. Реализуйте генератор данных, который создает новые данные на лету при каждом вызове итератора.

  4. Как спроектировать генератор случайных данных, который при каждом вызове генерирует одни и те же данные?

Обсуждения