Курсовая работа. Глубокое обучение
Скачать 4.97 Mb.
|
132 Глава 4 . Расширения Вскоре я покажу пару экспериментов с набором данных MNIST, которые покажут преимущества этой функции потерь перед MSE. Но сначала да- вайте обсудим компромиссы, связанные с выбором функции активации, и посмотрим, есть ли что-то получше сигмоиды. Примечание о функциях активации В главе 2 мы утверждали, что сигмоида — хорошая функция активации, потому что: y она нелинейная и монотонная; y вносит в модель некоторую нормализацию, приводя промежуточные элементы в диапазон от 0 до 1. Однако у сигмоиды есть и недостаток, как у MSE: она производит от- носительно плоские градиенты во время обратного прохода. Градиент, который передается в сигмовидную функцию (или любую другую функ- цию) на обратном проходе, показывает, насколько выходные данные функции в конечном итоге влияют на потери. Поскольку максимальный наклон сигмовидной функции равен 0,25, градиенты в лучшем случае будут разделены на 4 при передаче к предыдущей операции в модели. Хуже того, когда вход сигмоиды меньше –2 или больше 2: градиент, который получают эти входы, будет почти равен 0, так как функция sigmoid(x) почти плоская при x = –2 или x = 2. Это означает, что лю- бые параметры, влияющие на эти входные данные, получат небольшие градиенты и сеть будет учиться медленно 1 . Кроме того, если в слоях нейронной сети используется несколько сигмоид одна за другой, эта проблема будет усугубляться, что приведет к дальнейшему уменьше- нию градиентов. А какой будет функция активации, у которой будут противоположные преимущества и недостатки? 1 Для понимания: представьте, что вес w вносит вклад в функцию f (так что f = w × x 1 + ...) и во время прямого прохода нашей нейронной сети f = –10 для некоторых на- блюдений. Поскольку функция sigmoid(x) является такой плоской при x = –10 , изменение значения w почти не повлияет на прогноз модели и, следовательно, на потери. Многопеременная логистическая функция активации с перекрестно-энтропийными потерями 133 Другая крайность: Rectified Linear Unit Функция активации Recified Linear Unit (блок линейной ректифика- ции), или ReLU, — это популярная функция, в своих плюсах и минусах противоположная сигмоиде. ReLU равна 0, если x меньше 0, и x в про- тивном случае. Схема показана на рис. 4.5. Функция активации ReLU Рис. 4.5. Функция активации ReLU Эта функция активации монотонная и нелинейная. Она создает более крутые градиенты, чем сигмоида: 1, если входные данные для функции больше 0, и 0 в противном случае, то есть среднее значение равно 0.5, тогда как максимальный градиент сигмоиды равен 0.25. Функция активации ReLU популярна в нейронных сетях глубокого обучения, потому что ее недостаток (а именно тот факт, что она генерирует резкое и несколько произвольное различие между значениями, меньшими или большими, чем 0) компенсируется другими методами, а вот ее преимущество (генерация больших градиентов) имеет решающее значение для настройки весов. Но есть некоторая золотая середина между этими двумя функциями ак- тивации, которую мы будем использовать в этой главе: гиперболический тангенс. Золотая середина: гиперболический тангенс Функция гиперболического тангенса (tanh) по форме аналогична сиг- моиде, но отображает входные данные в значения от –1 до 1 (рис. 4.6). 134 Глава 4 . Расширения Функция активации tanh tanh(x) Рис. 4.6. Функция активации tanh Эта функция дает значительно более крутые градиенты, чем сигмои- да, — максимальный градиент tanh равен 1 в отличие от 0.25 у сигмоиды. На рис. 4.7 показаны градиенты этих двух функций. Производная sigmoid(х) Производная tanh(x) Рис. 4.7. Сигмоидная производная против производной tanh Кроме того, точно так же, как f(x) = sigmoid(x) имеет простую производ ную f ′(x) = sigmoid(x) × (1 – sigmoid(x)), так и f(x) = tanh (x) имеет простую производную f ′(x) = 1 – tanh(x) 2 Эксперименты 135 Выбор функции активации всегда влечет за собой компромиссы незави- симо от архитектуры: нам нужна функция активации, которая позволит нашей сети изучать нелинейные отношения между входом и выходом, не добавляя при этом лишней сложности. Например, функция активации «Leaky ReLU» допускает небольшой отрицательный наклон, когда вход для функции ReLU меньше 0, что позволяет функции ReLU отправлять градиенты назад, а функция активации «ReLU6» перекрывает положи- тельный конец ReLU на уровне 6, добавляя сети нелинейности. Тем не менее обе эти функции активации сложнее, чем ReLU, и для простой за- дачи чересчур сложные функции активации могут усложнить обучение сети. Таким образом, в моделях, которые мы будем показывать далее в этой книге, мы будем использовать функцию активации tanh, которая является разумным компромиссом. Теперь, когда мы выбрали функцию активации, опробуем ее в деле. Эксперименты Вернемся к теме из начала главы и покажем, почему перекрестно-энтро- пийная функция потерь softmax настолько распространена в глубоком обучении 1 . Мы будем использовать набор данных MNIST, представляю- щий собой черно-белые изображения написанных от руки цифр размером 28 × 28 пикселей, каждый из которых окрашен в диапазоне от 0 (белый) до 255 (черный). Кроме того, этот набор данных разделен на обучающий набор из 60 000 изображений и тестовый набор из 10 000 дополнительных изображений. В репозитории этой книги на GitHub ( https://oreil.ly/2H7rJvf ) есть дополнительная функция для считывания изображений и их меток в обучающие и тестовые наборы: X_train, y_train, X_test, y_test = mnist.load() Наша цель — обучить нейронную сеть распознавать цифру на изобра- жении. 1 Например, в учебнике по классификации MNIST TensorFlow используется функция softmax_cross_entropy_with_logits , а nn.CrossEntropyLoss в PyTorch фактически вычисляет внутри нее функцию softmax 136 Глава 4 . Расширения Предварительная обработка данных Для выполнения классификации нужно будет преобразовать наши век- торы меток изображений в ndarray той же формы, что и прогнозы. Метка «0» будет сопоставляться с вектором с единичным значением первого элемента и 0 на остальных позициях, метка «1» превращается в вектор 1 на второй позиции (с индексом 1) и т. д. ( https://oreil.ly/2KTRm3z ): Наконец, всегда полезно масштабировать данные так, чтобы среднее значение равнялось 0 и дисперсия равнялась 1, как мы это делали в пре- дыдущих главах. Однако в данном случае каждая точка данных явля- ется изображением, и мы не будем выполнять такое масштабирование, поскольку это приведет к изменению значений соседних пикселей, что, в свою очередь, может привести к искажению изображения! Вместо этого мы выполним глобальное масштабирование нашего набора данных путем вычитания общего среднего и деления на общую дисперсию (обратите внимание, что мы используем статистику из обучающего набора для масштабирования тестового набора): X_train, X_test = X_train - np.mean(X_train), X_test - np.mean(X_train) X_train, X_test = X_train / np.std(X_train), X_test / np.std(X_train) Модель Наша модель должна выдавать 10 выходов для каждого входа. Каждый выход представляет собой вероятность принадлежности изображения к одному из 10 классов. Поскольку на выходе мы получаем вероятность, на последнем слое будем использовать сигмоидную функцию активации. В этой главе мы хотим проиллюстрировать, действительно ли всякие трюки по улучшению обучения помогают, поэтому мы будем использовать двухслойную нейронную сеть с количеством нейронов в скрытом слое, близким к среднему геометрическому числу наших входов (784) и коли- честву выходов (10): Теперь давайте сравним сеть, обученную с помощью функции MSE, с се- тью, в которой используется перекрестно-энтропийная функция потерь Эксперименты 137 softmax . Значения потерь приведены для каждого наблюдения (напомним, что в среднем перекрестно-энтропийные значения потерь будут втрое больше, чем MSE). Если мы запустим: model = NeuralNetwork( layers=[Dense(neurons=89, activation=Tanh()), Dense(neurons=10, activation=Sigmoid())], loss = MeanSquaredError(), seed=20190119) optimizer = SGD(0.1) trainer = Trainer(model, optimizer) trainer.fit(X_train, train_labels, X_test, test_labels, epochs = 50, eval_every = 10, seed=20190119, batch_size=60); calc_accuracy_model(model, X_test) это даст нам: Validation loss after 10 epochs is 0.611 Validation loss after 20 epochs is 0.428 Validation loss after 30 epochs is 0.389 Validation loss after 40 epochs is 0.374 Validation loss after 50 epochs is 0.366 The model validation accuracy is: 72.58% Теперь давайте проверим наш собственный тезис: перекрестно-энтро- пийная функция потери softmax поможет нашей модели учиться быстрее. Эксперимент: перекрестно-энтропийная функция потери softmax Сначала давайте изменим предыдущую модель: model = NeuralNetwork( layers=[Dense(neurons=89, activation=Tanh()), 138 Глава 4 . Расширения Dense(neurons=10, activation=Linear())], loss = SoftmaxCrossEntropy(), seed=20190119) Поскольку теперь мы пропускаем выходные данные модели через функцию softmax, сигмоида уже не нужна. Запустим обучение на 50 эпох, что дает нам следующие результаты: Validation loss after 10 epochs is 0.630 Validation loss after 20 epochs is 0.574 Validation loss after 30 epochs is 0.549 Validation loss after 40 epochs is 0.546 Loss increased after epoch 50, final loss was 0.546, using the model from epoch 40 The model validation accuracy is: 91.01% Действительно, замена нашей функции потерь на функцию с более кру- тыми градиентами дает огромное повышение точности нашей модели 1 Мы можем добиться большего, даже не меняя нашу архитектуру. В сле- дующем разделе мы рассмотрим импульс — самое важное и простое улучшение для стохастического градиентного спуска, который мы ис- пользовали до сих пор. Импульс До сих пор мы использовали только одно правило обновления весовых коэффициентов — мы просто брали производную потери по весам и сме- щали значения весов в направлении градиента. Это означает, что наша функция _update_rule в оптимизаторе выглядела так: 1 Вы могли бы сказать, что перекрестно-энтропийная функция потери softmax побеж- дает нечестно, так как функция softmax нормализует полученные значения, тогда как СКО просто получает 10 входов, пропущенных через сигмоиду без нормализации. Однако на сайте https://oreil.ly/2H7rJvf я показал, что MSE работает хуже, чем пере- крестно-энтропийная функция потери, даже после нормализации входных данных. Импульс 139 update = self.lr*kwargs['grad'] kwargs['param'] -= update Для начала объясним, зачем нам нужно что-то здесь менять и вводить понятие инертности. Об инертности наглядно Вспомните рис. 4.3, на котором построено значение отдельного параметра в зависимости от значения потерь в сети. Представьте, что значение пара- метра постоянно обновляется в одном и том же направлении и значение потери продолжает уменьшаться с каждой итерацией. Рабочая точка движется вниз по склону, и величина обновления на каждом временном шаге будет аналогична «скорости» параметра. Однако в реальном мире объекты останавливаются и меняют направление не мгновенно, потому что у них есть инерция. То есть скорость объекта в данный момент яв- ляется не только функцией сил, действующих на тело в данный момент, но и накопленных ранее скоростей. Пользуясь этой аналогией, мы далее введем в нашу модель инертность. Реализация инертности в классе оптимизатора Внедрение импульса или инерции означает, что величина обновления на каждом шаге будет вычисляться как средневзвешенное значение от пре- дыдущих обновлений, причем веса будут уменьшаться в геометрической прогрессии от недавних к давним. Тогда нам надо будет задать степень затухания, которая определяет, насколько величина обновления зависит от накопленной скорости или от текущего значения. Математическое представление Математически, если наш параметр инертности равен μ, а градиент на каждом временном шаге равен ∇ t , наше обновление веса будет иметь вид: обновление = ∇ t + μ ×∇ t – 1 + μ 2 × ∇ t – 2 + ... Например, если бы наш параметр инерции был равен 0.9, мы бы умножили градиент предыдущего значения на 0.9 градиент двух шагов назад — на 0.9 2 = 0.81, трех шагов назад — на 0.9 3 = 0.729 и т. д. После этого мы бы 140 Глава 4 . Расширения прибавили все это к градиенту от текущего временного шага, чтобы полу- чить обновленное значение общего веса для текущего временного шага. Код Как это реализовать? Вычислять бесконечную сумму всякий раз, когда мы хотим обновить значения весов? Нет, все проще. Наш Optimizer будет отслеживать лишь некоторое количество предыдущих шагов и прибавлять их к текущему. Затем на каждом временном шаге мы будем использовать текущий градиент, изменяя вес и смещая накопленные значения. С физи- кой тут мало связи, поэтому мы будем называть эту величину «скоростью». Как обновлять скорость? В два этапа: 1. Умножаем скорость на параметр инерции. 2. Добавляем градиент. Тогда начиная с t = 1 скорость будет иметь значения: 1. ∇ 1 2. ∇ 2 + μ ×∇ 1 3. ∇ 3 + μ ×(∇ 2 + μ ×∇ 1 ) = μ ×(∇ 2 + μ 2 × ∇ 1 ) Саму скорость можно использовать для обновления параметров. Добавим это в новый подкласс Optimizer , который мы назовем SGDMomentum . В этом классе будут функции step и _update_rule : def step(self) -> None: ''' если итерация первая: инициализируем «скорости» для всех параметров. иначе вызываем функцию _update_rule. ''' if self.first: # задаем скорости для первой итерации self.velocities = [np.zeros_like(param) for param in self.net.params()] self.first = False for (param, param_grad, velocity) in zip(self.net.params(), self.net.param_grads(), self.velocities): Импульс 141 # передаем скорости в функцию "_update_rule" self._update_rule(param=param, grad=param_grad, velocity=velocity) def _update_rule(self, **kwargs) -> None: ''' обновление по скорости и инерции. ''' # обновление скорости kwargs['velocity'] *= self.momentum kwargs['velocity'] += self.lr * kwargs['grad'] # Обновляем параметры kwargs['param'] -= kwargs['velocity'] Посмотрим, поможет ли новый оптимизатор процессу обучения. Эксперимент: стохастический градиентный спуск с инерцией Выполним обучение той же нейронной сети с одним скрытым слоем на наборе данных MNIST, используя оптимизатор SGDMomentum(lr = 0.1, momentum = 0.9) вместо SGD(lr = 0.1) : Validation loss after 10 epochs is 0.441 Validation loss after 20 epochs is 0.351 Validation loss after 30 epochs is 0.345 Validation loss after 40 epochs is 0.338 Loss increased after epoch 50, final loss was 0.338, using the model from epoch 40 The model validation accuracy is: 95.51% Видно, что потери стали значительно ниже, а точность стала значительно выше, а значит, инерция сделала свое дело 1 ! 1 Более того, инерция не единственный способ использовать информацию за преде- лами градиента текущей партии данных для обновления параметров. Мы кратко рассмотрим другие правила обновления в приложении A, а реализованы они в би- блиотеке Lincoln, которую мы добавили на репозиторий GitHub книги ( https://oreil. ly/2MhdQ1B ). 142 Глава 4 . Расширения В качестве альтернативы мы могли бы изменять скорость обучения, причем не только вручную, но и автоматически, по некоторому правилу. Наиболее распространенные из таких правил рассмотрим далее. Скорость обучения [Скорость обучения] часто является самым важным гипер- параметром, и ее всегда нужно настраивать правильно. Иешуа Бенжио, «Practical recommendations for gradient-based training of deep architectures», 2012 Причина снижения скорости обучения была показана на рис. 4.3 в пре- дыдущем разделе: в начале обучения удобнее делать «большие» шаги, но по мере обучения мы рано или поздно достигнем точки, в которой начнем «пропускать» минимум. Но такая проблема вообще может не возникнуть, так как, если соотношение между нашими весами и потерями «плавно снижается» по мере приближения к минимуму, как на рис. 4.3, величина градиентов будет автоматически уменьшаться при уменьшении наклона. Но такая ситуация может и не произойти, и в любом случае снижение скорости обучения может дать нам более тонкий контроль над процессом. Типы снижения скорости обучения Существуют разные способы снижения скорости обучения. Самый про- стой: линейное затухание, при котором скорость обучения линейно умень- шается от первоначального значения до некоторого конечного значения в конце каждой эпохи. То есть на шаге t, если скорость обучения, с которой мы хотим начать, равна α start , а конечная скорость обучения равна α end , то скорость обучения на каждом временном шаге равна: , где N — общее число эпох. Скорость обучения 143 Еще один рабочий метод — экспоненциальное затухание, при котором скорость обучения снижается на некоторую долю. Формула простая: , где Реализовать это просто: у класса Optimizer будет атрибут конечной ско- рости обучения final_lr , до которой начальная скорость обучения будет постепенно снижаться: def __init__(self, lr: float = 0.01, final_lr: float = 0, decay_type: str = 'exponential') self.lr = lr self.final_lr = final_lr self.decay_type = decay_type В начале обучения мы можем вызвать функцию _setup_decay , которая вы- числяет, как сильно скорость обучения будет снижаться в каждую эпоху: self.optim._setup_decay () Эти вычисления реализуют линейное и экспоненциальное затухание скорости обучения, которые мы только что видели: def _setup_decay(self) -> None: if not self.decay_type: return elif self.decay_type == 'exponential': self.decay_per_epoch = np.power(self.final_lr / self.lr, 1.0 / (self.max_epochs-1)) elif self.decay_type == 'linear': self.decay_per_epoch = (self.lr — self.final_lr) / (self.max_epochs-1) В конце каждой эпохи уменьшаем скорость обучения: |