基本回归:预测燃油效率

在 TensorFlow.org 上查看 在 Google Colab 中运行 在 GitHub 上查看源代码 下载笔记本

回归问题中,目标是预测连续值的输出,例如价格或概率。与之形成对比的是分类问题,其目标是从类别列表中选择一个类别(例如,一张图片中包含苹果或橙子,识别图片中哪种水果)。

本教程使用经典的Auto MPG数据集,并演示如何构建模型来预测 1970 年代后期和 1980 年代初期的汽车的燃油效率。为此,您将向模型提供那个时期许多汽车的描述。此描述包括气缸数、排量、马力、重量等属性。

本示例使用 Keras API。(访问 Keras 的教程指南了解更多信息。)

# Use seaborn for pairplot.
pip install -q seaborn
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns

# Make NumPy printouts easier to read.
np.set_printoptions(precision=3, suppress=True)
import tensorflow as tf

from tensorflow import keras
from tensorflow.keras import layers

print(tf.__version__)

Auto MPG 数据集

该数据集可从UCI 机器学习资源库获取。

获取数据

首先使用 pandas 下载并导入数据集

url = 'http://archive.ics.uci.edu/ml/machine-learning-databases/auto-mpg/auto-mpg.data'
column_names = ['MPG', 'Cylinders', 'Displacement', 'Horsepower', 'Weight',
                'Acceleration', 'Model Year', 'Origin']

raw_dataset = pd.read_csv(url, names=column_names,
                          na_values='?', comment='\t',
                          sep=' ', skipinitialspace=True)
dataset = raw_dataset.copy()
dataset.tail()

清理数据

该数据集包含一些未知值

dataset.isna().sum()

为了使本入门教程保持简单,请删除这些行

dataset = dataset.dropna()

"Origin" 列是分类的,而不是数值的。因此,下一步是使用pd.get_dummies对该列中的值进行独热编码。

dataset['Origin'] = dataset['Origin'].map({1: 'USA', 2: 'Europe', 3: 'Japan'})
dataset = pd.get_dummies(dataset, columns=['Origin'], prefix='', prefix_sep='')
dataset.tail()

将数据拆分为训练集和测试集

现在,将数据集拆分为训练集和测试集。您将在模型的最终评估中使用测试集。

train_dataset = dataset.sample(frac=0.8, random_state=0)
test_dataset = dataset.drop(train_dataset.index)

检查数据

查看训练集中几对列的联合分布。

第一行表明燃油效率(MPG)是所有其他参数的函数。其他行表明它们是彼此的函数。

sns.pairplot(train_dataset[['MPG', 'Cylinders', 'Displacement', 'Weight']], diag_kind='kde')

我们也检查一下总体统计数据。请注意每个特征覆盖的范围差异很大

train_dataset.describe().transpose()

将特征与标签分开

将目标值(“标签”)与特征分开。此标签是您将训练模型预测的值。

train_features = train_dataset.copy()
test_features = test_dataset.copy()

train_labels = train_features.pop('MPG')
test_labels = test_features.pop('MPG')

归一化

在统计数据表中,很容易看到每个特征的范围差异很大

train_dataset.describe().transpose()[['mean', 'std']]

将使用不同比例和范围的特征进行归一化是一种良好的做法。

这很重要的一点原因是,特征会乘以模型权重。因此,输出的比例和梯度的比例会受到输入比例的影响。

虽然模型可能在没有特征归一化的情况下收敛,但归一化会使训练更加稳定。

归一化层

tf.keras.layers.Normalization是在模型中添加特征归一化的简洁方法。

第一步是创建层

normalizer = tf.keras.layers.Normalization(axis=-1)

然后,通过调用Normalization.adapt将预处理层的状态拟合到数据

normalizer.adapt(np.array(train_features))

计算均值和方差,并将它们存储在层中

print(normalizer.mean.numpy())

当调用该层时,它将返回输入数据,其中每个特征都独立地进行了归一化

first = np.array(train_features[:1])

with np.printoptions(precision=2, suppress=True):
  print('First example:', first)
  print()
  print('Normalized:', normalizer(first).numpy())

线性回归

在构建深度神经网络模型之前,先使用一个或多个变量进行线性回归。

使用一个变量的线性回归

从使用单个变量的线性回归开始,根据"Horsepower"预测"MPG"

使用tf.keras训练模型通常从定义模型架构开始。使用tf.keras.Sequential模型,该模型表示一系列步骤

使用单个变量的线性回归模型有两个步骤

  • 使用tf.keras.layers.Normalization预处理层对"Horsepower"输入特征进行归一化。
  • 应用线性变换(\(y = mx+b\)),使用线性层(tf.keras.layers.Dense)生成 1 个输出。

输入的数量可以通过input_shape参数设置,也可以在模型首次运行时自动设置。

首先,创建一个由"Horsepower"特征组成的 NumPy 数组。然后,实例化tf.keras.layers.Normalization,并将它的状态拟合到horsepower数据

horsepower = np.array(train_features['Horsepower'])

horsepower_normalizer = layers.Normalization(input_shape=[1,], axis=None)
horsepower_normalizer.adapt(horsepower)

构建 Keras Sequential 模型

horsepower_model = tf.keras.Sequential([
    horsepower_normalizer,
    layers.Dense(units=1)
])

horsepower_model.summary()

此模型将根据"Horsepower"预测"MPG"

在未经训练的模型上运行前 10 个"Horsepower"值。输出不会很好,但请注意它具有预期的形状(10, 1)

horsepower_model.predict(horsepower[:10])

构建模型后,使用 Keras 的Model.compile方法配置训练过程。compile 最重要的参数是lossoptimizer,因为它们定义了要优化的内容(mean_absolute_error)以及优化方式(使用tf.keras.optimizers.Adam)。

horsepower_model.compile(
    optimizer=tf.keras.optimizers.Adam(learning_rate=0.1),
    loss='mean_absolute_error')

使用 Keras 的Model.fit执行 100 个 epochs 的训练

%%time
history = horsepower_model.fit(
    train_features['Horsepower'],
    train_labels,
    epochs=100,
    # Suppress logging.
    verbose=0,
    # Calculate validation results on 20% of the training data.
    validation_split = 0.2)

使用存储在history对象中的统计信息可视化模型的训练进度

hist = pd.DataFrame(history.history)
hist['epoch'] = history.epoch
hist.tail()
def plot_loss(history):
  plt.plot(history.history['loss'], label='loss')
  plt.plot(history.history['val_loss'], label='val_loss')
  plt.ylim([0, 10])
  plt.xlabel('Epoch')
  plt.ylabel('Error [MPG]')
  plt.legend()
  plt.grid(True)
plot_loss(history)

收集测试集上的结果以备后用

test_results = {}

test_results['horsepower_model'] = horsepower_model.evaluate(
    test_features['Horsepower'],
    test_labels, verbose=0)

由于这是一个单变量回归,因此可以轻松地将模型的预测视为输入的函数

x = tf.linspace(0.0, 250, 251)
y = horsepower_model.predict(x)
def plot_horsepower(x, y):
  plt.scatter(train_features['Horsepower'], train_labels, label='Data')
  plt.plot(x, y, color='k', label='Predictions')
  plt.xlabel('Horsepower')
  plt.ylabel('MPG')
  plt.legend()
plot_horsepower(x, y)

使用多个输入的线性回归

您可以使用几乎相同的设置根据多个输入进行预测。此模型仍然执行相同的 \(y = mx+b\),只是 \(m\) 是一个矩阵,而 \(x\) 是一个向量。

再次创建一个两步 Keras Sequential 模型,第一层是您之前定义并适应整个数据集的normalizertf.keras.layers.Normalization(axis=-1)

linear_model = tf.keras.Sequential([
    normalizer,
    layers.Dense(units=1)
])

当您在输入批次上调用Model.predict时,它会为每个示例生成units=1个输出

linear_model.predict(train_features[:10])

当您调用模型时,它的权重矩阵将被构建,请检查kernel权重(\(y=mx+b\) 中的 \(m\)) 的形状是否为(9, 1)

linear_model.layers[1].kernel

使用 Keras 的Model.compile配置模型,并使用Model.fit进行 100 个 epochs 的训练

linear_model.compile(
    optimizer=tf.keras.optimizers.Adam(learning_rate=0.1),
    loss='mean_absolute_error')
%%time
history = linear_model.fit(
    train_features,
    train_labels,
    epochs=100,
    # Suppress logging.
    verbose=0,
    # Calculate validation results on 20% of the training data.
    validation_split = 0.2)

在这个回归模型中使用所有输入比只有一个输入的horsepower_model实现了更低的训练和验证误差

plot_loss(history)

收集测试集上的结果以备后用

test_results['linear_model'] = linear_model.evaluate(
    test_features, test_labels, verbose=0)

使用深度神经网络 (DNN) 进行回归

在上一节中,您实现了用于单个和多个输入的两个线性模型。

在这里,您将实现单个输入和多个输入的 DNN 模型。

代码基本相同,只是模型扩展为包含一些“隐藏”的非线性层。“隐藏”一词在这里只是表示不直接连接到输入或输出。

这些模型将包含比线性模型更多的层

  • 归一化层,如前所述(对于单个输入模型使用horsepower_normalizer,对于多个输入模型使用normalizer)。
  • 两个隐藏的、非线性的、具有 ReLU(relu)激活函数非线性的Dense层。
  • 一个线性的Dense单输出层。

这两个模型都将使用相同的训练过程,因此compile方法包含在下面的build_and_compile_model函数中。

def build_and_compile_model(norm):
  model = keras.Sequential([
      norm,
      layers.Dense(64, activation='relu'),
      layers.Dense(64, activation='relu'),
      layers.Dense(1)
  ])

  model.compile(loss='mean_absolute_error',
                optimizer=tf.keras.optimizers.Adam(0.001))
  return model

使用 DNN 和单个输入进行回归

创建一个 DNN 模型,该模型仅使用"Horsepower"作为输入,并使用horsepower_normalizer(之前定义)作为归一化层

dnn_horsepower_model = build_and_compile_model(horsepower_normalizer)

此模型的可训练参数比线性模型多很多

dnn_horsepower_model.summary()

使用 Keras 的Model.fit训练模型

%%time
history = dnn_horsepower_model.fit(
    train_features['Horsepower'],
    train_labels,
    validation_split=0.2,
    verbose=0, epochs=100)

此模型比线性单输入horsepower_model略好

plot_loss(history)

如果您将预测绘制为"Horsepower"的函数,您应该注意到此模型如何利用隐藏层提供的非线性

x = tf.linspace(0.0, 250, 251)
y = dnn_horsepower_model.predict(x)
plot_horsepower(x, y)

收集测试集上的结果以备后用

test_results['dnn_horsepower_model'] = dnn_horsepower_model.evaluate(
    test_features['Horsepower'], test_labels,
    verbose=0)

使用 DNN 和多个输入进行回归

重复上一个过程,使用所有输入。该模型在验证数据集上的性能略有提高。

dnn_model = build_and_compile_model(normalizer)
dnn_model.summary()
%%time
history = dnn_model.fit(
    train_features,
    train_labels,
    validation_split=0.2,
    verbose=0, epochs=100)
plot_loss(history)

收集测试集上的结果

test_results['dnn_model'] = dnn_model.evaluate(test_features, test_labels, verbose=0)

性能

由于所有模型都已训练完毕,因此您可以查看它们的测试集性能

pd.DataFrame(test_results, index=['Mean absolute error [MPG]']).T

这些结果与训练期间观察到的验证误差相符。

进行预测

您现在可以使用 Keras 的Model.predict在测试集上使用dnn_model进行预测,并查看损失

test_predictions = dnn_model.predict(test_features).flatten()

a = plt.axes(aspect='equal')
plt.scatter(test_labels, test_predictions)
plt.xlabel('True Values [MPG]')
plt.ylabel('Predictions [MPG]')
lims = [0, 50]
plt.xlim(lims)
plt.ylim(lims)
_ = plt.plot(lims, lims)

看来该模型预测得相当好。

现在,检查误差分布

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

如果您对模型感到满意,请使用Model.save保存它以备后用

dnn_model.save('dnn_model.keras')

如果您重新加载模型,它将给出相同的输出

reloaded = tf.keras.models.load_model('dnn_model.keras')

test_results['reloaded'] = reloaded.evaluate(
    test_features, test_labels, verbose=0)
pd.DataFrame(test_results, index=['Mean absolute error [MPG]']).T

结论

本笔记本介绍了一些处理回归问题的技术。以下是一些可能会有所帮助的额外提示

  • 均方误差 (MSE)(tf.keras.losses.MeanSquaredError)和平均绝对误差 (MAE)(tf.keras.losses.MeanAbsoluteError)是回归问题中常用的损失函数。MAE 对异常值不太敏感。分类问题使用不同的损失函数。
  • 类似地,用于回归的评估指标与分类不同。
  • 当数值输入数据特征的值具有不同的范围时,应将每个特征独立地缩放到相同的范围。
  • 过拟合是 DNN 模型的常见问题,虽然在本教程中没有出现。访问 过拟合和欠拟合 教程以获取更多帮助。
# 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.