1.3 基于Keras实现房价预测

前面我们使用scikit-learn机器学习库实现了房价预测。本节将使用神经网络模型来实现房价预测,使用的是TensorFlow下的Keras的API代码。我们将使用Keras创建模型、训练模型、预测和对比图预览。

Keras封装了一套高级的API用于构建深度学习模型,常用于快速实现原型设计和高级搜索。Keras对开发者的友好性、模块化、可组合性和易于扩展的关键特征,使其成为目前使用较为广泛的深度学习开源框架,它的后端之一就是TensorFlow。Keras在2015年12月份就将其以TensorFlow为后端的部分API融合到了TensorFlow框架中,所以目前我们可以通过tf.keras或者独立的Keras库来使用它。本书后面的章节会大量使用TensorFlow和Keras来构建深度学习模型。

1.3.1 数据准备

我们通过TensorFlow提供的Keras接口下的datasets模块来加载数据集。数据集由卡内基梅隆大学维护,是波士顿近郊房价数据。读者也可以通过前言中的链接去查看后续更新的数据集。在tf.keras.datasets下加载的数据集和我们在链接中看到的数据集的格式是一样的,只是年份可能不同。加载数据集的代码如下。

  import tensorflow as tf
  # 从TensorFlow导入Keras模块
  from tensorflow import keras
  import numpy as np
  # 加载波士顿房价数据集
  (train_data, train_labels), (test_data, test_labels) = \
  keras.datasets.boston_housing.load_data()
  # 清洗训练集数据
  # np.random.random()表示返回在0.0到1.0之间指定个数的随机浮点数
  # np.argsort()表示返回对数组进行排序的索引
  order = np.argsort(np.random.random(train_labels.shape))
  train_data = train_data[order]
  train_labels = train_labels[order]
  # 归一化处理数据
  # 对不同的范围和比例进行归一化处理,并且每个元素都要减去均值后再除以标准差
  # 虽然模型在没有特征归一化时也可以得到收敛,但是这会让训练更加困难,
  # 而且会导致结果模型依赖于训练数据
  mean = train_data.mean(axis=0)
  std = train_data.std(axis=0)
  train_data = (train_data - mean) / std
  test_data = (test_data - mean) / std
  print("train_data.shape: {}, train_labels.shape: {}."
      .format(train_data.shape, train_labels.shape)) 
  print("test_data.shape: {}, test_labels.shape: {}."
      .format(test_data.shape, test_labels.shape))

输出如下。

  train_data.shape:(404,13),train_labels.shape:(404,).
  test_data.shape:(102,13),test_labels.shape:(102,).

1.3.2 创建神经网络模型

在Keras中,我们创建神经网络模型,就是使用Sequential类来创建Keras模型。本次创建的模型比较简单,通过Dense类来创建神经网络层。对于输入层,层的深度是64个units,输入层必须传入input_shape参数,表示输入数据的特征维度的大小。

激活函数(activation),我们指定的是修正线性单元(ReLU),它是神经网络中最常用的激活函数。当输入的值为正数时,导数不为0,返回它本身,这就允许在训练模型时进行基于梯度的学习,也会使计算变得更快;当输入的值为负数时,学习速度可能会变得很慢,甚至会使神经元直接失效,这是因为输入的值是小于0的值,计算它的梯度也为0,从而使其权重无法得到更新,因此在传播到下一个神经网络层的时候,返回的值为0就没有什么意义了。

然后我们再添加一个隐藏层,这一层不需要input_shape参数,因为在输入层时已经指定了。我们仍然设置层的深度是64个units,激活函数是ReLu。

输出层只有一个unit,代码如下。

  # 定义创建模型函数
  def build_model():
    model = keras.Sequential([
      keras.layers.Dense(64, activation=tf.nn.relu,
                         input_shape=(train_data.shape[1],)),
      keras.layers.Dense(64, activation=tf.nn.relu),
      keras.layers.Dense(1)
    ])
    # 使用RMSProp(均方根传播)优化器,它可以加速梯度下降,其中学习速度适用于每个参数
    optimizer = tf.train.RMSPropOptimizer(0.001)
    # mse(均方差)一般用于回归问题的损失函数
    # mae(平均绝对误差)一般用于回归问题的测量/评估
    model.compile(loss='mse', optimizer=optimizer, metrics=['mae'])
    return model
  model = build_model()
  # 查看模型的架构
  model.summary()

输出的模型架构如图1.12所示。全部参数有5121个,模型中的每一层参数都可以在表格的Param列看到。每一层的输出大小在Output Shape列中显示,其中None表示是可变的batch size,它会在训练模型或者模型预测时被自动填充上具体的值。

图1.12 一个简单的Keras模型架构图

1.3.3 训练网络模型

训练这个模型500次,并且将训练精确度和验证精确度记录在history对象中,以便绘图预览。我们自定义一个回调对象类,重写on_epoch_end()函数,在每次epoch结束时会调用该函数。

  # 自定义一个回调对象类,在每次epoch(代)结束时都会调用该函数
  class PrintDot(keras.callbacks.Callback):
    def on_epoch_end(self, epoch, logs):
        if epoch % 100 == 0: print('')
        print('.', end='')
  EPOCHS = 500
  # 训练模型
  # 参数1:房屋特征数据
  # 参数2:房屋价格数据
  # 参数3:迭代次数
  # 参数4:验证集分割比例,0.2表示20%的数据用于验证,80%的数据用于训练
  # 参数5:输出打印日志信息,0表示不输出打印日志信息
  # 参数6:回调对象,这里我们使用自定义的回调类PrintDot
  history = model.fit(train_data, train_labels, epochs=EPOCHS,
                    validation_split=0.2, verbose=0,
                    callbacks=[PrintDot()])

1.3.4 可视化模型的结果

通过history对象,我们可以读取该模型训练时的误差数值,以便于观察何时是最佳模型,何时应该停止训练。

  import matplotlib.pyplot as plt
  # 绘制图来显示训练时的accuracy和loss
  def plot_history(history):
    plt.figure()
    plt.xlabel('Epoch')
    plt.ylabel('Mean Abs Error [1000$]')
    plt.plot(history.epoch, np.array(history.history['mean_absolute_error']),
             label='Train Loss')
    plt.plot(history.epoch, np.array(history.history['val_mean_absolute_error']),
             label='Val loss')
    plt.legend()
    plt.ylim([0, 5])
    plt.show()
  plot_history(history)

输出如图1.13所示。

图1.13 模型训练时的平均绝对误差表现图

可以发现,大约在150到200次迭代时,训练损失值就没怎么降低了。所以,这里我们要用到一个降低过拟合技术:早期停止(Early Stopping)。它是指在指定的迭代次数内,如果依旧没有损失降低、模型性能提升的话,就自动终止训练。

我们重新构建和训练该模型,在重新构建模型前,请先清除Keras的内存状态。最简单的办法就是重新运行Jupyter Notebook;如果读者使用的是终端,就重新启动该脚本程序。

  # 重新构建模型
  model = build_model()
  # 设置早期停止,如果20次的迭代依旧没有降低验证损失,则自动停止训练
  early_stop = keras.callbacks.EarlyStopping(monitor='val_loss', patience=20)
  # 重新训练模型,此时的callbacks有两个回调函数,所以我们使用数组的形式传入它们
  history = model.fit(train_data, train_labels, epochs=EPOCHS,
                         validation_split=0.2, verbose=0,
                         callbacks=[early_stop, PrintDot()])
  # 打印输出历史记录的曲线图
  plot_history(history)

输出如图1.14所示。

图1.14 训练和验证模型时的平均绝对误差图(使用早期停止技术)

1.3.5 评估和预测模型

接下来,我们来测试模型在测试集下的表现,这就需要对模型进行评估了。通过evaluate()函数,传入测试房屋特征数据集,测试房屋价格数据,计算测试集的平均绝对误差。

  [loss, mae] = model.evaluate(test_data, test_labels, verbose=0)
  print("Testing set Mean Abs Error: ${:7.2f}".format(mae * 1000))

输出日志如下。

  Testing set Mean Abs Error:$2930.86

然后预测模型,通过predict()函数,传入测试房屋特征数据集,返回预测房价;最后将返回的数据通过flatten()函数进行扁平化处理,以便于绘制散点图。

  # 使用测试数据集预测模型
  test_predictions = model.predict(test_data).flatten()
  plt.scatter(test_labels, test_predictions)
  plt.xlabel('True Values [1000$]')
  plt.ylabel('Predictions [1000$]')
  plt.axis('equal')
  plt.xlim(plt.xlim())
  plt.ylim(plt.ylim())
  plt.plot([-100, 100], [-100, 100])
  plt.show()

输出结果如图1.15所示。

图1.15 预测房价的回归模型图

1.3.6 预测可视化显示

接下来,我们来看下将真实房价和预测房价进行对比,从而形成的价格差直方图。我们先计算预测房价和真实房价的差价,代码如下。

  error = test_predictions - test_labels
  plt.hist(error, bins=50)
  plt.xlabel("Prediction Error [1000$]")
  plt.ylabel("Count")
  plt.show()

输出如图1.16所示。

图1.16 预测房价和真实房价的价格差直方图

最后,我们通过真实房价和预测房价生成一张更直观的图。函数plotVersusFigure()在上面的代码中已经定义过,这里就不再介绍。

  plotVersusFigure(test_labels,test_predictions)

输出如图1.17所示。

图1.17 真实房价与预测房价的对比图

通过以上分析可知,不管是用scikit-learn的机器学习库来预测房价,还是使用Keras的神经网络模型来预测房价,真实房价和预测房价总是有些误差。所以我们能控制的就是在训练神经网络模型时,调整训练的超参数、迭代次数、网络层数和优化器等参数,以得到更好的、适用于该房屋数据的预测模型。这里的数据量比较小,如果数据量更大一些,模型效果会更好。