.. _sec_oo-design: Объектно-ориентированное проектирование для реализации ====================================================== В нашем введении в линейную регрессию мы рассмотрели различные компоненты, включая данные, модель, функцию потерь и алгоритм оптимизации. Действительно, линейная регрессия — одна из самых простых моделей машинного обучения. Однако для её обучения используются многие те же компоненты, что и для других моделей в этой книге. Поэтому, прежде чем погружаться в детали реализации, стоит спроектировать некоторые интерфейсы (API), которые мы будем использовать на протяжении всего текста. Рассматривая компоненты глубокого обучения как объекты, мы можем начать с определения классов для этих объектов и их взаимодействий. Такой объектно-ориентированный подход к реализации значительно упростит изложение материала, и вы, возможно, захотите использовать его в своих проектах. Вдохновляясь такими открытыми библиотеками, как `PyTorch Lightning `__, на высоком уровне мы хотим иметь три класса: (i) ``Module`` содержит модели, функции потерь и методы оптимизации; (ii) ``DataModule`` предоставляет загрузчики данных для обучения и валидации; (iii) оба класса объединяются с помощью класса ``Trainer``, который позволяет обучать модели на различных аппаратных платформах. Большая часть кода в этой книге использует ``Module`` и ``DataModule``. Мы будем обращаться к классу ``Trainer`` только при обсуждении GPU, CPU, параллельного обучения и алгоритмов оптимизации. .. raw:: latex \diilbookstyleinputcell .. code:: python import time import numpy as np import torch from torch import nn from d2l import torch as d2l .. _oo-design-utilities: Утилиты ------- Для упрощения объектно-ориентированного программирования в Jupyter-блокнотах нам понадобится несколько вспомогательных функций. Одна из проблем заключается в том, что определения классов обычно представляют собой довольно длинные блоки кода. Читаемость блокнотов требует коротких фрагментов кода, перемежающихся пояснениями, что несовместимо со стилем программирования, принятым в библиотеках Python. Первая вспомогательная функция позволяет регистрировать функции как методы класса *после* того, как класс уже создан. Более того, мы можем сделать это *даже после* создания экземпляров этого класса! Это позволяет разбить реализацию класса на несколько блоков кода. .. raw:: latex \diilbookstyleinputcell .. code:: python def add_to_class(Class): #@save """Register functions as methods in created class.""" def wrapper(obj): setattr(Class, obj.__name__, obj) return wrapper Давайте кратко рассмотрим, как это использовать. Мы планируем реализовать класс ``A`` с методом ``do``. Вместо того чтобы помещать код для обоих ``A`` и ``do`` в один блок, мы можем сначала объявить класс ``A`` и создать объект ``a``. .. raw:: latex \diilbookstyleinputcell .. code:: python class A: def __init__(self): self.b = 1 a = A() Далее мы определяем метод ``do`` обычным образом, но не в области видимости класса ``A``. Вместо этого мы декорируем этот метод с помощью ``add_to_class``, передавая класс ``A`` в качестве аргумента. Благодаря этому метод получает доступ к переменным-членам класса ``A`` так же, как мы и ожидали, если бы он был включен в определение класса ``A``. Давайте посмотрим, что произойдет, когда мы вызовем его для объекта ``a``. .. raw:: latex \diilbookstyleinputcell .. code:: python @add_to_class(A) def do(self): print('Class attribute "b" is', self.b) a.do() Второй из них — это вспомогательный класс, который сохраняет все аргументы метода ``__init__`` класса в виде атрибутов класса. Это позволяет неявно расширять сигнатуры вызова конструктора без написания дополнительного кода. .. raw:: latex \diilbookstyleinputcell .. code:: python class HyperParameters: #@save """The base class of hyperparameters.""" def save_hyperparameters(self, ignore=[]): raise NotImplemented Мы отложим его реализацию до раздела :numref:`sec_utils`. Чтобы использовать его, определим наш класс, который наследуется от ``HyperParameters`` и вызывает ``save_hyperparameters`` в методе ``__init__``. .. raw:: latex \diilbookstyleinputcell .. code:: python # Call the fully implemented HyperParameters class saved in d2l class B(d2l.HyperParameters): def __init__(self, a, b, c): self.save_hyperparameters(ignore=['c']) print('self.a =', self.a, 'self.b =', self.b) print('There is no self.c =', not hasattr(self, 'c')) b = B(a=1, b=2, c=3) Последняя утилита позволяет интерактивно отслеживать ход эксперимента по мере его выполнения. В честь гораздо более мощного (и сложного) `TensorBoard `__ мы назвали её ``ProgressBoard``. Её реализация отложена до раздела :numref:`sec_utils`. А пока просто посмотрим, как она работает. Метод ``draw`` отображает точку ``(x, y)`` на графике, с меткой ``label``, указанной в легенде. Необязательный параметр ``every_n`` сглаживает линию, отображая только :math:`1/n` точек на графике. Их значения усредняются по :math:`n` соседним точкам из исходного графика. .. raw:: latex \diilbookstyleinputcell .. code:: python class ProgressBoard(d2l.HyperParameters): #@save """The board that plots data points in animation.""" def __init__(self, xlabel=None, ylabel=None, xlim=None, ylim=None, xscale='linear', yscale='linear', ls=['-', '--', '-.', ':'], colors=['C0', 'C1', 'C2', 'C3'], fig=None, axes=None, figsize=(3.5, 2.5), display=True): self.save_hyperparameters() def draw(self, x, y, label, every_n=1): raise NotImplemented В следующем примере мы нарисуем ``sin`` и ``cos`` с разной степенью сглаживания. Если вы запустите этот блок кода, вы увидите, как линии анимированно вытягиваются. .. raw:: latex \diilbookstyleinputcell .. code:: python board = d2l.ProgressBoard('x') for x in np.arange(0, 10, 0.1): board.draw(x, np.sin(x), 'sin', every_n=2) board.draw(x, np.cos(x), 'cos', every_n=10) .. _subsec_oo-design-models: Модели ------ Класс ``Module`` является базовым классом для всех моделей, которые мы будем реализовывать. Как минимум нам нужны три метода. Первый, ``__init__``, хранит обучаемые параметры, метод ``training_step`` принимает пакет данных и возвращает значение функции потерь, а метод ``configure_optimizers`` возвращает метод оптимизации или список таких методов, используемых для обновления обучаемых параметров. Опционально мы можем определить ``validation_step`` для отчёта о показателях оценки. Иногда мы помещаем код для вычисления вывода в отдельный метод ``forward``, чтобы сделать его более переиспользуемым. .. raw:: latex \diilbookstyleinputcell .. code:: python class Module(nn.Module, d2l.HyperParameters): #@save """The base class of models.""" def __init__(self, plot_train_per_epoch=2, plot_valid_per_epoch=1): super().__init__() self.save_hyperparameters() self.board = ProgressBoard() def loss(self, y_hat, y): raise NotImplementedError def forward(self, X): assert hasattr(self, 'net'), 'Neural network is defined' return self.net(X) def plot(self, key, value, train): """Plot a point in animation.""" assert hasattr(self, 'trainer'), 'Trainer is not inited' self.board.xlabel = 'epoch' if train: x = self.trainer.train_batch_idx / \ self.trainer.num_train_batches n = self.trainer.num_train_batches / \ self.plot_train_per_epoch else: x = self.trainer.epoch + 1 n = self.trainer.num_val_batches / \ self.plot_valid_per_epoch self.board.draw(x, value.to(d2l.cpu()).detach().numpy(), ('train_' if train else 'val_') + key, every_n=int(n)) def training_step(self, batch): l = self.loss(self(*batch[:-1]), batch[-1]) self.plot('loss', l, train=True) return l def validation_step(self, batch): l = self.loss(self(*batch[:-1]), batch[-1]) self.plot('loss', l, train=False) def configure_optimizers(self): raise NotImplementedError Вы можете заметить, что ``Module`` является подклассом ``nn.Module``, базового класса нейронных сетей в PyTorch. Он предоставляет удобные возможности для работы с нейронными сетями. Например, если мы определим метод ``forward``, такой как ``forward(self, X)``, то для объекта ``a`` мы можем вызвать этот метод с помощью ``a(X)``. Это работает, поскольку вызывается метод ``forward`` из встроенного метода ``__call__``. Более подробные сведения и примеры о ``nn.Module`` можно найти в :numref:`sec_model_construction`. .. _oo-design-data: Данные ------ Класс ``DataModule`` является базовым классом для работы с данными. Метод ``__init__`` часто используется для подготовки данных, что может включать их скачивание и предварительную обработку. Метод ``train_dataloader`` возвращает загрузчик данных для обучающего набора данных. Загрузчик данных — это (Python) генератор, который выдает пакет данных при каждом обращении. Этот пакет затем подается в метод ``training_step`` класса ``Module`` для вычисления функции потерь. Существует необязательный метод ``val_dataloader``, возвращающий загрузчик данных для валидационного набора данных. Он работает аналогичным образом, за исключением того, что выдает пакеты данных для метода ``validation_step`` в классе ``Module``. .. raw:: latex \diilbookstyleinputcell .. code:: python class DataModule(d2l.HyperParameters): #@save """The base class of data.""" def __init__(self, root='../data', num_workers=4): self.save_hyperparameters() def get_dataloader(self, train): raise NotImplementedError def train_dataloader(self): return self.get_dataloader(train=True) def val_dataloader(self): return self.get_dataloader(train=False) .. _oo-design-training: Обучение -------- Класс ``Trainer`` обучает обучаемые параметры класса ``Module`` на данных, указанных в ``DataModule``. Ключевой метод — ``fit``, который принимает два аргумента: ``model``, объект класса ``Module``, и ``data``, объект класса ``DataModule``. Затем он проходит по всему набору данных ``max_epochs`` раз для обучения модели. Как и ранее, мы отложим реализацию этого метода до следующих глав. .. raw:: latex \diilbookstyleinputcell .. code:: python class Trainer(d2l.HyperParameters): #@save """The base class for training models with data.""" def __init__(self, max_epochs, num_gpus=0, gradient_clip_val=0): self.save_hyperparameters() assert num_gpus == 0, 'No GPU support yet' def prepare_data(self, data): self.train_dataloader = data.train_dataloader() self.val_dataloader = data.val_dataloader() self.num_train_batches = len(self.train_dataloader) self.num_val_batches = (len(self.val_dataloader) if self.val_dataloader is not None else 0) def prepare_model(self, model): model.trainer = self model.board.xlim = [0, self.max_epochs] self.model = model def fit(self, model, data): self.prepare_data(data) self.prepare_model(model) self.optim = model.configure_optimizers() self.epoch = 0 self.train_batch_idx = 0 self.val_batch_idx = 0 for self.epoch in range(self.max_epochs): self.fit_epoch() def fit_epoch(self): raise NotImplementedError Итоги ----- Чтобы подчеркнуть объектно-ориентированный дизайн нашей будущей реализации глубокого обучения, приведённые выше классы лишь демонстрируют, как их объекты хранят данные и взаимодействуют друг с другом. В остальной части книги мы будем продолжать расширять реализации этих классов, например, с помощью ``@add_to_class``. Кроме того, полностью реализованные классы сохранены в `библиотеке D2L `__ — *легковесном инструментарии*, который упрощает структурированное моделирование в глубоком обучении. В частности, он позволяет повторно использовать многие компоненты в разных проектах без существенных изменений. Например, мы можем заменить только оптимизатор, только модель, только набор данных и т. д.; такая степень модульности окупается на протяжении всей книги за счет лаконичности и простоты (именно поэтому мы её добавили) и может принести ту же пользу вашим собственным проектам. Упражнения ---------- 1. Полные реализации указанных выше классов сохранены в `библиотеке D2L `__. Мы настоятельно рекомендуем вам внимательно изучить эти реализации, как только вы приобретете некоторый опыт в моделировании с помощью глубокого обучения. 2. Удалите оператор ``save_hyperparameters`` из класса ``B``. Сможете ли вы по-прежнему выводить ``self.a`` и ``self.b``? Дополнительно: если вы изучили полную реализацию класса ``HyperParameters``, сможете ли вы объяснить почему? `Обсуждения `__