.. _sec_linear_scratch: Реализация линейной регрессии с нуля ==================================== Теперь мы готовы приступить к созданию полностью рабочей реализации линейной регрессии. В этом разделе мы реализуем весь метод с нуля, включая (i) модель; (ii) функцию потерь; (iii) оптимизатор стохастического градиентного спуска на мини-батчах; и (iv) функцию обучения, которая объединяет все эти компоненты Наконец, мы запустим наш генератор синтетических данных из :numref:`sec_synthetic-regression-data` и применим нашу модель к полученному набору данных. Хотя современные фреймворки глубокого обучения могут автоматизировать практически всю эту работу, реализация с нуля — это единственный способ убедиться, что вы действительно понимаете, что делаете. Более того, когда придет время настраивать модели, определяя собственные слои или функции потерь, понимание того, как все устроено «под капотом», окажется весьма полезным. В этом разделе мы будем опираться только на тензоры и автоматическое дифференцирование. Позже мы рассмотрим более лаконичную реализацию, используя дополнительные возможности фреймворков глубокого обучения, но сохраняя структуру изложенного ниже. .. raw:: latex \diilbookstyleinputcell .. code:: python %matplotlib inline import torch from d2l import torch as d2l Определение модели ------------------ Прежде чем мы сможем приступить к оптимизации параметров нашей модели с помощью мини-батч SGD, нам сначала нужно задать эти параметры. В дальнейшем мы инициализируем веса, выбирая случайные числа из нормального распределения со средним 0 и стандартным отклонением 0,01. Магическое число 0,01 часто хорошо работает на практике, но вы можете указать другое значение через аргумент ``sigma``. Кроме того, мы устанавливаем смещение равным 0. Обратите внимание, что в рамках объектно-ориентированного подхода мы добавляем код в метод ``__init__`` подкласса ``d2l.Module`` (представленного в :numref:`subsec_oo-design-models`). .. raw:: latex \diilbookstyleinputcell .. code:: python class LinearRegressionScratch(d2l.Module): #@save """The linear regression model implemented from scratch.""" def __init__(self, num_inputs, lr, sigma=0.01): super().__init__() self.save_hyperparameters() self.w = torch.normal(0, sigma, (num_inputs, 1), requires_grad=True) self.b = torch.zeros(1, requires_grad=True) Далее мы должны определить нашу модель, связав её входные данные и параметры с выходом. Используя те же обозначения, что и :eq:`eq_linreg-y-vec` для нашей линейной модели, мы просто берём матрично-векторное произведение входных признаков :math:`\mathbf{X}` и весов модели :math:`\mathbf{w}`, а затем прибавляем смещение :math:`b` к каждому примеру. Результат произведения :math:`\mathbf{Xw}` — это вектор, а смещение :math:`b` — скаляр. Благодаря механизму транслирования (см. :numref:`subsec_broadcasting`), при сложении вектора и скаляра скаляр добавляется к каждому компоненту вектора. Полученный метод ``forward`` регистрируется в классе ``LinearRegressionScratch`` через ``add_to_class`` (описанный в :numref:`oo-design-utilities`). .. raw:: latex \diilbookstyleinputcell .. code:: python @d2l.add_to_class(LinearRegressionScratch) #@save def forward(self, X): return torch.matmul(X, self.w) + self.b Определение функции потерь -------------------------- Поскольку обновление модели требует вычисления градиента функции потерь, мы должны сначала определить функцию потерь. Здесь мы используем квадратичную функцию потерь из :eq:`eq_mse`. При реализации нам необходимо привести форму истинных значений ``y`` к форме предсказаний ``y_hat``. Результат, возвращаемый следующим методом, также будет иметь ту же форму, что и ``y_hat``. Мы также возвращаем усреднённое значение функции потерь по всем примерам в мини-батче. .. raw:: latex \diilbookstyleinputcell .. code:: python @d2l.add_to_class(LinearRegressionScratch) #@save def loss(self, y_hat, y): l = (y_hat - y) ** 2 / 2 return l.mean() Определение алгоритма оптимизации --------------------------------- Как обсуждалось в :numref:`sec_linear_regression`, линейная регрессия имеет аналитическое решение. Однако наша цель здесь — продемонстрировать, как обучать более общие нейронные сети, а для этого необходимо научить вас использовать мини-батч стохастический градиентный спуск (SGD). Поэтому мы воспользуемся этим случаем, чтобы привести ваш первый рабочий пример использования SGD. На каждом шаге, используя мини-батч, случайно выбранный из нашего набора данных, мы оцениваем градиент функции потерь по параметрам. Затем мы обновляем параметры в направлении, способном уменьшить значение функции потерь. В приведённом ниже коде выполняется обновление параметров при заданном наборе параметров и скорости обучения ``lr``. Поскольку наша функция потерь вычисляется как среднее значение по мини-батчу, нам не нужно корректировать скорость обучения в зависимости от размера батча. В последующих главах мы рассмотрим, как следует адаптировать скорость обучения для очень больших мини-батчей, возникающих при распределенном обучении в масштабах всей системы. Пока же мы можем игнорировать эту зависимость. Мы определяем наш ``SGD`` класс, являющийся подклассом ``d2l.HyperParameters`` (описанного в :numref:`oo-design-utilities`), так, чтобы его API был схож с API встроенного оптимизатора SGD. Обновление параметров происходит в методе ``step``. Метод ``zero_grad`` обнуляет все градиенты, что необходимо выполнять перед шагом обратного распространения ошибки. .. raw:: latex \diilbookstyleinputcell .. code:: python class SGD(d2l.HyperParameters): #@save """Minibatch stochastic gradient descent.""" def __init__(self, params, lr): self.save_hyperparameters() def step(self): for param in self.params: param -= self.lr * param.grad def zero_grad(self): for param in self.params: if param.grad is not None: param.grad.zero_() Далее мы определяем метод ``configure_optimizers``, который возвращает объект класса ``SGD``. .. raw:: latex \diilbookstyleinputcell .. code:: python @d2l.add_to_class(LinearRegressionScratch) #@save def configure_optimizers(self): return SGD([self.w, self.b], self.lr) Обучение -------- Теперь, когда все компоненты на своих местах (параметры, функция потерь, модель и оптимизатор), мы готовы реализовать основной цикл обучения. Крайне важно полностью понять этот код, поскольку вы будете использовать аналогичные циклы обучения для всех остальных моделей глубокого обучения, рассмотренных в этой книге. В каждой *эпохе* мы проходим по всему обучающему набору данных, обрабатывая каждый пример (при условии, что количество примеров делится на размер батча). На каждой *итерации* мы выбираем мини-батч обучающих примеров и вычисляем его функцию потерь с помощью метода ``training_step`` модели. Затем мы вычисляем градиенты по каждому параметру. Наконец, мы вызываем алгоритм оптимизации для обновления параметров модели. Вкратце, мы выполним следующий цикл: - Инициализируйте параметры :math:`(\mathbf{w}, b)` - Повторять до завершения - Вычислить градиент :math:`\mathbf{g} \leftarrow \partial_{(\mathbf{w},b)} \frac{1}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} l(\mathbf{x}^{(i)}, y^{(i)}, \mathbf{w}, b)` - Обновление параметров :math:`(\mathbf{w}, b) \leftarrow (\mathbf{w}, b) - \eta \mathbf{g}` Вспомним синтетический набор данных для регрессии, который мы сгенерировали в :numref:`sec_synthetic-regression-data` does not provide a validation dataset. In most cases, however, we will want a validation dataset to measure our model quality. Here we pass the validation dataloader once in each epoch to measure the model performance. Following our object-oriented design, the ``prepare_batch`` and ``fit_epoch`` methods are registered in the ``d2l.Trainer`` (введенном в :numref:`oo-design-training`). .. raw:: latex \diilbookstyleinputcell .. code:: python @d2l.add_to_class(d2l.Trainer) #@save def prepare_batch(self, batch): return batch .. raw:: latex \diilbookstyleinputcell .. code:: python @d2l.add_to_class(d2l.Trainer) #@save def fit_epoch(self): self.model.train() for batch in self.train_dataloader: loss = self.model.training_step(self.prepare_batch(batch)) self.optim.zero_grad() with torch.no_grad(): loss.backward() if self.gradient_clip_val > 0: # To be discussed later self.clip_gradients(self.gradient_clip_val, self.model) self.optim.step() self.train_batch_idx += 1 if self.val_dataloader is None: return self.model.eval() for batch in self.val_dataloader: with torch.no_grad(): self.model.validation_step(self.prepare_batch(batch)) self.val_batch_idx += 1 Мы почти готовы к обучению модели, но сначала нам понадобятся обучающие данные. Здесь мы используем класс ``SyntheticRegressionData`` и передаем некоторые параметры ground truth. Затем мы обучаем нашу модель со скоростью обучения ``lr=0.03`` и устанавливаем ``max_epochs=3``. Обратите внимание, что в общем случае и количество эпох, и скорость обучения являются гиперпараметрами. Как правило, настройка гиперпараметров — задача нетривиальная, и обычно мы используем разделение данных на три части: один набор для обучения, второй для выбора гиперпараметров и третий, зарезервированный для окончательной оценки. Пока мы опустим эти детали, но вернемся к ним позже. .. raw:: latex \diilbookstyleinputcell .. code:: python model = LinearRegressionScratch(2, lr=0.03) data = d2l.SyntheticRegressionData(w=torch.tensor([2, -3.4]), b=4.2) trainer = d2l.Trainer(max_epochs=3) trainer.fit(model, data) Поскольку мы сами создали набор данных, мы точно знаем истинные параметры. Следовательно, мы можем **оценить успех обучения, сравнив истинные параметры с теми, которые мы получили в процессе обучения**. Как видно, они действительно очень близки друг к другу. .. raw:: latex \diilbookstyleinputcell .. code:: python with torch.no_grad(): print(f'error in estimating w: {data.w - model.w.reshape(data.w.shape)}') print(f'error in estimating b: {data.b - model.b}') Не стоит считать само собой разумеющимся возможность точного восстановления параметров, лежащих в основе данных (ground truth). В общем случае для глубоких моделей уникальные решения для параметров не существуют, и даже для линейных моделей точное восстановление параметров возможно только тогда, когда ни один признак линейно не зависит от остальных. Однако в машинном обучении нас часто меньше заботит восстановление истинных скрытых параметров, а скорее поиск параметров, обеспечивающих высокую точность предсказаний :cite:`Vapnik.1992`. К счастью, даже на сложных задачах оптимизации стохастический градиентный спуск часто способен найти весьма хорошие решения, отчасти благодаря тому, что для глубоких сетей существует множество конфигураций параметров, приводящих к высокой точности предсказаний. Итог ---- В этом разделе мы сделали важный шаг к проектированию систем глубокого обучения, реализовав полностью функциональную модель нейронной сети и цикл обучения. В ходе этого процесса мы создали загрузчик данных, модель, функцию потерь, процедуру оптимизации, а также инструмент визуализации и мониторинга. Мы достигли этого, объединив в один объект Python все необходимые компоненты для обучения модели. Хотя это ещё не реализация профессионального уровня, она полностью работоспособна, и подобный код уже может помочь вам быстро решать небольшие задачи. В следующих разделах мы рассмотрим, как сделать это *более лаконично* (избегая шаблонного кода) и *более эффективно* (используя возможности наших GPU на полную мощность). Упражнения ---------- 1. Что произойдет, если мы инициализируем веса нулями? Будет ли работать алгоритм? А что, если мы инициализируем параметры с дисперсией :math:`1000` вместо :math:`0.01`? 2. Предположим, что вы — `Георг Симон Ом `__ и пытаетесь вывести модель сопротивления, связывающую напряжение и ток. Можете ли вы использовать автоматическое дифференцирование для обучения параметров вашей модели? 3. Можно ли использовать `закон Планка `__ для определения температуры объекта по спектральной плотности энергии? В качестве справки напомним, что спектральная плотность :math:`B` излучения абсолютно черного тела задается формулой :math:`B(\lambda, T) = \frac{2 hc^2}{\lambda^5} \cdot \left(\exp \frac{h c}{\lambda k T} - 1\right)^{-1}`. Здесь :math:`\lambda` — длина волны, :math:`T` — температура, :math:`c` — скорость света, :math:`h` — постоянная Планка, а :math:`k` — постоянная Больцмана. Вы измеряете энергию для различных длин волн :math:`\lambda`, и теперь вам необходимо подобрать параметры так, чтобы кривая спектральной плотности соответствовала закону Планка. 4. Какие проблемы могут возникнуть при вычислении вторых производных функции потерь? Как их можно решить? 5. Зачем нужен метод ``reshape`` в функции ``loss``? 6. Экспериментируйте с различными скоростями обучения, чтобы выяснить, как быстро падает значение функции потерь. Можно ли снизить ошибку, увеличив количество эпох обучения? 7. Если количество примеров не делится на размер батча, что происходит с ``data_iter`` в конце эпохи? 8. Попробуйте реализовать другую функцию потерь, например, функцию потерь, основанную на абсолютном значении ``(y_hat - d2l.reshape(y, y_hat.shape)).abs().sum()``. 1. Проверьте, что происходит для регулярных данных. 2. Проверьте, есть ли различия в поведении, если вы активно возмущаете некоторые элементы, такие как :math:`y_5 = 10000`, в :math:`\mathbf{y}`. 3. Можете ли вы предложить дешевое решение для объединения лучших аспектов квадратичной функции потерь и функции потерь, основанной на модуле? Подсказка: как можно избежать получения действительно больших значений градиента? 9. Зачем нужно перемешивать набор данных? Можете ли вы привести пример, в котором специально сконструированный набор данных нарушил бы работу алгоритма оптимизации? `Обсуждения `__