.. _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``, сможете ли
вы объяснить почему?
`Обсуждения `__