In [ ]:
#@title Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

In [ ]:
#@title MIT License
#
# Copyright (c) 2017 François Chollet
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.

Знакомимся с переобучением и недообучением

Note: Вся информация в этом разделе переведена с помощью русскоговорящего Tensorflow сообщества на общественных началах. Поскольку этот перевод не является официальным, мы не гарантируем что он на 100% аккуратен и соответствует официальной документации на английском языке. Если у вас есть предложение как исправить этот перевод, мы будем очень рады увидеть pull request в tensorflow/docs репозиторий GitHub. Если вы хотите помочь сделать документацию по Tensorflow лучше (сделать сам перевод или проверить перевод подготовленный кем-то другим), напишите нам на docs-ru@tensorflow.org list.

Как и в предыдущий раз мы будем использовать tf.keras API, подробнее о котором ты можешь прочитать в нашем руководстве по Keras.

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

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

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

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

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

В этом уроке мы познакомимся с двумя распространенными методами регуляризации: регуляризация весов и исключение (dropout). Мы используем их чтобы улучшить показатели нашей модели из урока по классификации обзоров фильмов из IMDB.


In [ ]:
import tensorflow.compat.v1 as tf

from tensorflow import keras

import numpy as np
import matplotlib.pyplot as plt

print(tf.__version__)

Загружаем датасет IMDB

Вместо того, чтобы использовать embedding слой, как мы делали это в предыдущем уроке, здесь мы попробуем multi-hot-encoding. Наша модель быстро начнет переобучаться на тренировочных данных. Мы посмотрим как это произойдет и рассмотрим способы предотвращения этого.

Использование multi-hot-encoding на нашем массиве конвертирует его в векторы 0 и 1. Говоря конкретнее, это означает что например последовательность [3, 5] будет конвертирована в 10,000-размерный вектор, который будет состоять полностью из нулей за исключением 3 и 5, которые будут представлены в виде единиц.


In [ ]:
NUM_WORDS = 10000

(train_data, train_labels), (test_data, test_labels) = keras.datasets.imdb.load_data(num_words=NUM_WORDS)

def multi_hot_sequences(sequences, dimension):
    # Создаем матрицу формы (len(sequences), dimension), состоящую из нулей
    results = np.zeros((len(sequences), dimension))
    for i, word_indices in enumerate(sequences):
        results[i, word_indices] = 1.0  # назначаем единицу на конкретные показатели results[i]
    return results


train_data = multi_hot_sequences(train_data, dimension=NUM_WORDS)
test_data = multi_hot_sequences(test_data, dimension=NUM_WORDS)

Давай посмотрим на один из получившихся multi-hot векторов. Номера слов были отсортированы по частоте, и вполне ожидаемо, что многие значения единицы будут около нуля. Проверим это на графике:


In [ ]:
plt.plot(train_data[0])

Продемонстрируем переобучение

Самый простой способ предотвратить переобучение, это уменьшить размер модели, или количество обучаемых параметров, которые определяются количеством слоев и блоков на каждый слой. В глубоком обучении количество обучаемых параметров часто называют емкостью модели. Понятно, что модель с большим количество параметров будет иметь больший запас для обучения, и следовательно легче сможет выучить взаимосвязи между тренировочными образцами данных и целевыми проверочными. Обучение же без возможности обобщения окажется бесполезным, особенно если мы попытаемся получить предсказания на новых, ранее не виденных данных.

Всегда помни об этом: модели глубокого обучения всегда хорошо справляются с подстраиванием под тренировочные данные, но наша конечная цель - обучение обощению.

С другой стороны, если нейросеть имеет ограниченные ресурсы для запоминания шаблонов, то тогда она не сможет так же легко находить паттерны в данных. Чтобы сократить потери, такая модель будет вынуждена обучаться сжатым представлениям, которые имеют больше предсказательной силы. В то же самое время, если мы сделаем нашу модель слишком маленькой, тогда ей будет трудно подстроиться под тренировочный сет данных. Всегда нужно искать баланс между слишком большой емкостью и недостаточной емкостью.

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

Чтобы найди подходящий размер модели лучше начать с относительно небольших слоев и параметров, затем начать увеличивать размер слоев или добавлять новые до тех пор, пока ты показатели не начнут ухудшаться на проверочных данных. Давай попробуем разобраться на примере нашей сети для классификации обзоров.

Для начала мы построим простую модель используя только слои Dense в качестве основы, а затем сделаем маленькую и большую версию этой модели для сравнения.

Строим основу для модели


In [ ]:
baseline_model = keras.Sequential([
    # Параметр `input_shape` нужен только для того, чтобы заработал `.summary`
    keras.layers.Dense(16, activation=tf.nn.relu, input_shape=(NUM_WORDS,)),
    keras.layers.Dense(16, activation=tf.nn.relu),
    keras.layers.Dense(1, activation=tf.nn.sigmoid)
])

baseline_model.compile(optimizer='adam',
                       loss='binary_crossentropy',
                       metrics=['accuracy', 'binary_crossentropy'])

baseline_model.summary()

In [ ]:
baseline_history = baseline_model.fit(train_data,
                                      train_labels,
                                      epochs=20,
                                      batch_size=512,
                                      validation_data=(test_data, test_labels),
                                      verbose=2)

Создаем малый вариант

Давай построим модель с меньшим количесвом скрытых блоков и сравним ее с первой моделью:


In [ ]:
smaller_model = keras.Sequential([
    keras.layers.Dense(4, activation=tf.nn.relu, input_shape=(NUM_WORDS,)),
    keras.layers.Dense(4, activation=tf.nn.relu),
    keras.layers.Dense(1, activation=tf.nn.sigmoid)
])

smaller_model.compile(optimizer='adam',
                loss='binary_crossentropy',
                metrics=['accuracy', 'binary_crossentropy'])

smaller_model.summary()

И обучим модель используя те же данные:


In [ ]:
smaller_history = smaller_model.fit(train_data,
                                    train_labels,
                                    epochs=20,
                                    batch_size=512,
                                    validation_data=(test_data, test_labels),
                                    verbose=2)

Создаем большую модель

В качестве упражнения ты можешь создать модель даже еще больше, и посмотреть как быстро она начнет переобучаться. Затем протестируем эту модель, которая будет иметь гораздо бóльшую емкость, чем требуется для решения нашей задачи:


In [ ]:
bigger_model = keras.models.Sequential([
    keras.layers.Dense(512, activation=tf.nn.relu, input_shape=(NUM_WORDS,)),
    keras.layers.Dense(512, activation=tf.nn.relu),
    keras.layers.Dense(1, activation=tf.nn.sigmoid)
])

bigger_model.compile(optimizer='adam',
                     loss='binary_crossentropy',
                     metrics=['accuracy','binary_crossentropy'])

bigger_model.summary()

И опять потренируем уже новую модель используя те же данные:


In [ ]:
bigger_history = bigger_model.fit(train_data, train_labels,
                                  epochs=20,
                                  batch_size=512,
                                  validation_data=(test_data, test_labels),
                                  verbose=2)

Построим графики потерь

Непрерывные линии показывают потери во время обучения, а прерывистые - во время проверки (помни - чем меньше потери на проверочных данных, тем точнее модель). В нашем случае самая маленькая модель начинает переобучаться позже, чем основная (после 6 эпох вместо 4) и ее показатели ухудшаются гораздо медленее после переобучения.


In [ ]:
def plot_history(histories, key='binary_crossentropy'):
  plt.figure(figsize=(16,10))

  for name, history in histories:
    val = plt.plot(history.epoch, history.history['val_'+key],
                   '--', label=name.title()+' Val')
    plt.plot(history.epoch, history.history[key], color=val[0].get_color(),
             label=name.title()+' Train')

  plt.xlabel('Epochs')
  plt.ylabel(key.replace('_',' ').title())
  plt.legend()

  plt.xlim([0,max(history.epoch)])


plot_history([('baseline', baseline_history),
              ('smaller', smaller_history),
              ('bigger', bigger_history)])

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

Как решить проблему переобучения?

Добавить регуляризацию весов

Тебе может быть знаком принцип бритвы Оккама: если есть 2 толкования явления, то правильным является самое "простое" - то, которое содержит меньше всего предположений. Этот принцип также применим к моделям, обучемым при помощи нейронных сетей: для одних и той же сети и данных существует несколько весовых значений, или моделей, которые могут быть обучены. Простые модели переобучиваются гораздо реже, чем сложные.

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

Штраф имеет 2 вида:

  • Регуляризация L1 - штраф прямо пропорционален абсолютному значению коэффицентов весов (сокращенно мы называем его "норма L1")

  • Регуляризация L2 - штраф добавляется пропорционально квадрату значения коэффицента весов. Норму L2 также называют угасанием весов. Это два одинаковых названия для одной и той же математической формулы

Чтобы осуществить регуляризацию в tf.keras мы добавим новый регулятор в блок со слоями как аргумент. Давай попробуем добавить L2 и посмотреть что получится:


In [ ]:
l2_model = keras.models.Sequential([
    keras.layers.Dense(16, kernel_regularizer=keras.regularizers.l2(0.001),
                       activation=tf.nn.relu, input_shape=(NUM_WORDS,)),
    keras.layers.Dense(16, kernel_regularizer=keras.regularizers.l2(0.001),
                       activation=tf.nn.relu),
    keras.layers.Dense(1, activation=tf.nn.sigmoid)
])

l2_model.compile(optimizer='adam',
                 loss='binary_crossentropy',
                 metrics=['accuracy', 'binary_crossentropy'])

l2_model_history = l2_model.fit(train_data, train_labels,
                                epochs=20,
                                batch_size=512,
                                validation_data=(test_data, test_labels),
                                verbose=2)

Значение l2(0.001) означает, что каждый коэффицент матрицы весов слоя будет добавлять 0.001 * weight_coefficient_value**2 к общей потери сети. Обрати внимание, что штраф добавляется только во время обучения, потери во время этой стадии будут гораздо выше, чем во время проверки.

Вот так выглядит влияние регуляризации L2:


In [ ]:
plot_history([('Базовая модель', baseline_history),
              ('Регуляризация L2', l2_model_history)])

Как видишь, прошедшая L2 регуляризцию модель стала более устойчива к переобучению, чем наша изначальная, несмотря на то, что обе модели имели равное количество параметров.

Добавить исключение Dropout

Метод исключения (или выпадения) Dropout - один из самых эффективных и часто используемых приемов регуляризации нейронных сетей. Он был разработан Джеффом Хинтоном совместно с его студентами в Университете Торонто. Применяемый к слою Dropout состоит из случайно выпадающих (или равных нулю) признаков этого слоя.

Допустим, что наш слой обычно возвращает вектор [0.2, 0.5, 1.3, 0.8, 1.1] на входной образец данных. После применения Dropout этот вектор будет случайным образом приравнивать к нулю какие-то его значения, например так - [0, 0.5, 1.3, 0, 1.1].

Ту часть признаков, которые "выпадут" или обнуляться называют коэффицентом исключения dropout. Обычно его устанавливают между 0.2 и 0.5. Во время проверки dropout не используется, и вместо этого все выходные значения уменьшаются на соотвествующий коэффиент (скажем, 0.5). Это поможет нам сбалансировать тот факт, что во время проверки было активировано больше блоков, чем во время обучения.

В tf.keras ты можешь использовать метод исключения в своей сети при помощи слоя Dropout, который применяется к выводу данных из предшествующего слоя.

Давай добавим два слоя Dropout в нашу сеть на данных IMDB и посмотрим насколько хорошо она справится с переобучением:


In [ ]:
dpt_model = keras.models.Sequential([
    keras.layers.Dense(16, activation=tf.nn.relu, input_shape=(NUM_WORDS,)),
    keras.layers.Dropout(0.5),
    keras.layers.Dense(16, activation=tf.nn.relu),
    keras.layers.Dropout(0.5),
    keras.layers.Dense(1, activation=tf.nn.sigmoid)
])

dpt_model.compile(optimizer='adam',
                  loss='binary_crossentropy',
                  metrics=['accuracy','binary_crossentropy'])

dpt_model_history = dpt_model.fit(train_data, train_labels,
                                  epochs=20,
                                  batch_size=512,
                                  validation_data=(test_data, test_labels),
                                  verbose=2)

In [ ]:
plot_history([('Базовая модель', baseline_history),
              ('Метод Dropout', dpt_model_history)])

Метод Dropout имеет явные преимущества по сравнению с нашей изначальной, базовой моделью.

Подведем итоги - вот самые основные способы предотвращения переобучения нейросетей:

  • Использовать больше данных для обучения
  • Уменьшить емкость сети
  • Использовать регуляризацию весов
  • Или dropout

Также существуют еще два важных подхода, которые не были продемонстрированы в этом уроке: увеличение или аугментация данных и нормализация батча.