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.

Explore overfit and underfit

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

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

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

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

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

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

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

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


In [ ]:
import tensorflow as tf
from tensorflow import keras

import numpy as np
import matplotlib.pyplot as plt

print(tf.__version__)

Загрузите датасет IMDB

Вместо использования вложения (embedding) как в предыдущем уроке, здесь мы используем multi-hot encode предложений. Эта модель быстро переобучится на тренировочных данных. Мы посмотрим как произойдет переобучение и как его предотвратить.

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] к 1
    return results


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

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


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

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

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

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

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

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

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

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

Создайте базовую модель


In [ ]:
baseline_model = keras.Sequential([
    # Параметр `input_shape` нужен только для того, чтобы заработал `.summary`.
    keras.layers.Dense(16, activation='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dense(16, activation='relu'),
    keras.layers.Dense(1, activation='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='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dense(4, activation='relu'),
    keras.layers.Dense(1, activation='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='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dense(512, activation='relu'),
    keras.layers.Dense(1, activation='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)])

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

Стратегии предотвращения переобучения

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

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

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

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

  • L2 регуляризация, где добавляемый штраф пропорционален квадрату значений коэффициентов весов (т.е. то, что называется квадратом "L2 нормы" весов). L2 регуляризацию также называют сокращением весов в контексте нейросетей. Не дайте разным названиям запутать себя: сокращение весов математически ровно то же самое что и L2 регуляризация.

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

В tf.keras регуляризация весов добавляется передачей экземпляров регуляризатора слоям в качестве аргумента. Добавим сейчас L2 регуляризатор весов.


In [ ]:
l2_model = keras.models.Sequential([
    keras.layers.Dense(16, kernel_regularizer=keras.regularizers.l2(0.001),
                       activation='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dense(16, kernel_regularizer=keras.regularizers.l2(0.001),
                       activation='relu'),
    keras.layers.Dense(1, activation='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', baseline_history),
              ('l2', l2_model_history)])

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

Добавьте дропаут

Дропаут(исключение) одна из наиболее эффективных и часто используемых техник регуляризации нейросетей разработанная Джеффом Хинтоном и его студентами в университете Торонто. Примененный к слою Dropout состоит из некоторого количества случайно "исключенных" (т.е. приравненных к нулю) во время обучения выходных параметров слоя. Допустим что наш слой возвращает вектор [0.2, 0.5, 1.3, 0.8, 1.1] для некоторых входных данных при обучении; после применения дропаута, в этом векторе появится несколько нулевых значений распределенных случайным образом, например [0, 0.5, 1.3, 0, 1.1]. "Коэффициент дропаута(dropout rate)" это доля признаков которые будут обнулены; его обычно устанавливают между 0.2 и 0.5. Во время теста дропаут не используется, вместо этого выходные данные слоев масштабируются на коэффициент равный коэффициенту дропаута, чтобы сбалансировать тот факт, что во время проверки активно больше нейронов чем во время обучения.

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

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


In [ ]:
dpt_model = keras.models.Sequential([
    keras.layers.Dense(16, activation='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dropout(0.5),
    keras.layers.Dense(16, activation='relu'),
    keras.layers.Dropout(0.5),
    keras.layers.Dense(1, activation='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', baseline_history),
              ('dropout', dpt_model_history)])

Добавление дропаута явно улучает базовую модель.

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

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

Два важных подхода которые не были рассмотрены в данном руководстве это аугментация данных (data-augmentation) и батч-нормализация (batch normalization).