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.

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

Мы будем использовать классический датасет Auto MPG и построим модель, которая будет предсказывать эффективность расхода топлива автомобилей конца 70-х и начала 80-х. Для этого мы загрузим описания множества различных автомобилей того времени. Эти описания будут содержать такие параметры как количество цилиндров, лошадиных сил, объем двигателя и вес.

В этом примере используется tf.keras API, подробнее смотри здесь.


In [ ]:
# Установим библиотеку seaborn для построения парных графиков
!pip install seaborn

In [ ]:
import pathlib

import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns

import tensorflow.compat.v1 as tf

from tensorflow import keras
from tensorflow.keras import layers

print(tf.__version__)

Датасет Auto MPG

Датасет доступен в репозитории машинного обучения UCI.

Загружаем данные

Сперва загрузим наш набор данных:


In [ ]:
dataset_path = keras.utils.get_file("auto-mpg.data", "http://archive.ics.uci.edu/ml/machine-learning-databases/auto-mpg/auto-mpg.data")
dataset_path

Импортируем его при помощи библиотеки Pandas:


In [ ]:
column_names = ['Расход топлива','Кол-во цилиндров','Объем двигателя','Л.с.','Вес',
                'Разгон до 100 км/ч', 'Год выпуска', 'Страна выпуска']
raw_dataset = pd.read_csv(dataset_path, names=column_names,
                      na_values = "?", comment='\t',
                      sep=" ", skipinitialspace=True)

dataset = raw_dataset.copy()
dataset.tail()

Подготовим данные

Нам нужно почистить наши данные, так как датасет содержит несколько неизвестных значений:


In [ ]:
dataset.isna().sum()

Чтобы было проще давай просто удалим ряды, где отсутствуют значения:


In [ ]:
dataset = dataset.dropna()

"Страна выпуска" - это колонка с указанием категории, а не значений. Давай переведем ее в двоичный код:


In [ ]:
origin = dataset.pop('Страна выпуска')

In [ ]:
dataset['США'] = (origin == 1)*1.0
dataset['Европа'] = (origin == 2)*1.0
dataset['Япония'] = (origin == 3)*1.0
dataset.tail()

Разделим на тренировочные и проверочные данные

Теперь нам необходимо разделить данные на две части: для обучения и для проверки.

Точность получившейся модели мы сможем проверить на втором наборе данных.


In [ ]:
train_dataset = dataset.sample(frac=0.8,random_state=0)
test_dataset = dataset.drop(train_dataset.index)

Проверим данные

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


In [ ]:
sns.pairplot(train_dataset[["Расход топлива", "Кол-во цилиндров", "Объем двигателя", "Вес"]], diag_kind="kde")

А также посмотрим как выглядит общая статистика:


In [ ]:
train_stats = train_dataset.describe()
train_stats.pop("Расход топлива")
train_stats = train_stats.transpose()
train_stats

Выделим значения

Теперь давай отделим целевые значения label от характеристик.

Мы будем использовать эти значения для обучения нашей модели:


In [ ]:
train_labels = train_dataset.pop('Расход топлива')
test_labels = test_dataset.pop('Расход топлива')

Нормализуем данные

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

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

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


In [ ]:
def norm(x):
  return (x - train_stats['mean']) / train_stats['std']
normed_train_data = norm(train_dataset)
normed_test_data = norm(test_dataset)

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

Внимание: статистика, используемая для нормализации входных данных так же важна, как и сами веса модели.

Модель

Построим модель

Теперь давай построим нашу модель. Мы будем использовать Sequential (последовательную) модель с двумя полносвязными Dense слоями, а выходящий слой будет возвращать одно постоянно изменяющееся значение. Все этапы построения модели мы опишем в функции build_model, так как позже мы создадим еще одну модель.


In [ ]:
def build_model():
  model = keras.Sequential([
    layers.Dense(64, activation=tf.nn.relu, input_shape=[len(train_dataset.keys())]),
    layers.Dense(64, activation=tf.nn.relu),
    layers.Dense(1)
  ])

  optimizer = tf.train.RMSPropOptimizer(0.001)

  model.compile(loss='mse',
                optimizer=optimizer,
                metrics=['mae', 'mse'])
  return model

In [ ]:
model = build_model()

Проверим модель

Давай воспользуемся методом .summary чтобы посмотреть как выглядит наша модель:


In [ ]:
model.summary()

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


In [ ]:
example_batch = normed_train_data[:10]
example_result = model.predict(example_batch)
example_result

Похоже все работает правильно, модель показывает результат ожидаемой формы и класса.

Обучим модель

Мы будем обучать модель в течение 1000 эпох и фиксировать точность модели на тренирвочных и проверочных данных в объекте history.


In [ ]:
# Выведем прогресс обучения в виде точек после каждой завершенной эпохи
class PrintDot(keras.callbacks.Callback):
  def on_epoch_end(self, epoch, logs):
    if epoch % 100 == 0: print('')
    print('.', end='')

EPOCHS = 1000

history = model.fit(
  normed_train_data, train_labels,
  epochs=EPOCHS, validation_split = 0.2, verbose=0,
  callbacks=[PrintDot()])

Теперь давай сделаем визуализацию процесса обучения при помощи статистики из объекта history:


In [ ]:
hist = pd.DataFrame(history.history)
hist['epoch'] = history.epoch
hist.tail()

In [ ]:
def plot_history(history):
  hist = pd.DataFrame(history.history)
  hist['epoch'] = history.epoch

  plt.figure(figsize=(8,12))

  plt.subplot(2,1,1)
  plt.xlabel('Эпоха')
  plt.ylabel('Среднее абсолютное отклонение')
  plt.plot(hist['epoch'], hist['mean_absolute_error'],
           label='Ошибка при обучении')
  plt.plot(hist['epoch'], hist['val_mean_absolute_error'],
           label = 'Ошибка при проверке')
  plt.ylim([0,5])
  plt.legend()

  plt.subplot(2,1,2)
  plt.xlabel('Эпоха')
  plt.ylabel('Среднеквадратическая ошибка')
  plt.plot(hist['epoch'], hist['mean_squared_error'],
           label='Ошибка при обучении')
  plt.plot(hist['epoch'], hist['val_mean_squared_error'],
           label = 'Ошибка при проверке')
  plt.ylim([0,20])
  plt.legend()
  plt.show()


plot_history(history)

Полученный график показывает, что после нескольких сотен эпох наша модель улучшается совсем незначительно в процессе обучения. Давай обновим метод model.fit чтобы автоматически прекращать обучение как только показатель проверки Val loss не улучшается. Для этого мы используем функцию обратного вызова callback, которая проверяет показатели обучения после каждой эпохи. Если после определенного количество эпох нет никаких улучшений, то функция автоматически остановит его.

Читай больше про функцию callback здесь.


In [ ]:
model = build_model()

# Параметр patience определяет количество эпох, которые можно пропустить без улучшений
early_stop = keras.callbacks.EarlyStopping(monitor='val_loss', patience=50)

history = model.fit(normed_train_data, train_labels, epochs=EPOCHS,
                    validation_split = 0.2, verbose=0, callbacks=[early_stop, PrintDot()])

plot_history(history)

График показывает что среднее значение ошибки на проверочных данных - около 2 галлонов на милю. Хорошо это или плохо? Решать тебе.

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


In [ ]:
loss, mae, mse = model.evaluate(normed_test_data, test_labels, verbose=2)

print("Среднее абсолютное отклонение на проверочных данных: {:5.2f} галлон на милю".format(mae))

Делаем предсказания

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


In [ ]:
test_predictions = model.predict(normed_test_data).flatten()

plt.scatter(test_labels, test_predictions)
plt.xlabel('Истинные значения')
plt.ylabel('Предсказанные значения')
plt.axis('equal')
plt.axis('square')
plt.xlim([0,plt.xlim()[1]])
plt.ylim([0,plt.ylim()[1]])
_ = plt.plot([-100, 100], [-100, 100])

In [ ]:
error = test_predictions - test_labels
plt.hist(error, bins = 25)
plt.xlabel("Prediction Error [MPG]")
_ = plt.ylabel("Count")

Заключение

Это руководство познакомило тебя с несколькими способами решения задач регрессии.

Ключевые моменты урока:

  • Mean Squared Error (MSE) (пер. "Среднеквадратическая ошибка") - это распространенная функция потерь, используемая для решения задач регрессии (отличная от задач классификации)
  • Показатели оценки модели для регрессии отличаются от используемых в классификации
  • Когда входные данные имеют параметры в разных форматах, каждый параметр должен быть нормализован
  • Если данных для обучения немного, используй небольшую сеть из нескольких скрытых слоев. Это поможет избежать переобучения
  • Используй метод ранней остановки - это очень полезная техника для избежания переобучения