深度学习训练营 21天实战TensorFlow+Keras+scikit-learn
上QQ阅读APP看书,第一时间看更新

1.2 基于scikit-learn实现房价预测

本节将使用scikit-learn来实现房价预测。首先使用决定系数R2Score来做一个小测验;然后使用决策树回归器配合不同的深度值来创建和对比模型;最后使用网格搜索(grid search)来查找最佳模型,以便进行房价预测。

1.2.1 衡量R2值

通过sklearn提供的r2_score()函数来计算拟合优度(Goodness of Fit),R2值越接近1表明拟合的程度越高;反之,R2值越小,则说明回归直线的拟合程度越低。

  # 导入r2_score
  from sklearn.metrics import r2_score
  # 定义函数,计算目标值和预测值之间的分值
  def performance_metric(y_true,y_predict):
     score=r2_score(y_true,y_predict)
     return score

这里有一个小测验,假设有5个真实数值和5个预测数值,分别是[3,−0.5,2,7,4.2]和[2.5,0.0,2.1,7.8,5.3],然后通过r2_score()函数来看它的拟合优度的程度值。

  # 真实数值
  test_y_true=[3,-0.5,2,7,4.2]
  # 预测数值
  test_y_predict=[2.5,0.0,2.1,7.8,5.3]
  # 通过r2_score函数来计算
  score=performance_metric(test_y_true,test_y_predict)
  print("决定系数,R^2=:{}。".format(score))

输出如下。

  决定系数,R^2=:0.9228556485355649。

可以看到,R2值非常接近1,说明相关程度高。

1.2.2 模型性能对比

我们将通过4个不同的深度值创建4个不同的模型,然后来对比这4个模型的性能表现,最后通过图表直观显示。现在,我们把房屋特征和价格的数据进行清洗和分割。

  # 从sklearn库里导入train_test_split方法
  from sklearn.model_selection import train_test_split
  # train_test_split()方法用来清洗和分割数据
  # 参数1:特征样本,就是房屋特征数据
  # 参数2:特征样本对应的目标(房屋)价格
  # 参数3:分配给测试集的大小为0.1,也就是10%的数据用于测试、90%的数据用于训练
  # 参数4:random_state表示随机数生成器的种子,如果希望第二次调用train_test_split()方法
  #        的结果和第一次调用的结果一致,那么就可以设置一个值,多少都可以,生产环境不要设值
  X_train, X_test, y_train, y_test = train_test_split(features, prices, test_size=0.1, random_state=50)
  print("X_train.shape={}, y_train.shape={}.".format(X_train.shape, y_train.shape))
  print("X_test.shape={}, y_test.shape={}.".format(X_test.shape, y_test.shape))

输出如下。

  X_train.shape=(440,3),y_train.shape=(440,).
  X_test.shape=(49,3),y_test.shape=(49,).

得到440行的数据用于训练,49行的数据用于测试。接下来,我们定义一个函数来衡量模型的性能,深度值分别是1、3、6、10,然后输出这些模型的图,便于更直观地对比。

  # 导入绘图库
  import matplotlib.pyplot as plt
  # 使用matplotlib在Jupyter Notebook中绘图时,需要使用这个
  %matplotlib inline
  # 导入sklearn的清洗分割、学习曲线和决策树回归器的对象和函数
  from sklearn.model_selection import ShuffleSplit, learning_curve
  from sklearn.tree import DecisionTreeRegressor
  # 定义模型的性能对比函数
  # 通过不同大小的深度值来创建for循环里的模型,然后以图的形式展现
  def ModelLearningGraphMetrics(X, y):
    # 清洗和分割数据对象定义
    # 参数1:n_splits表示重新清洗和分割数据的迭代次数,默认值为10
    # 参数2:test_size=0.2表示有0.2的数据用于测试,也就是20%的数据用于测试,80%的数据用于训练
    # 参数3:random_state表示随机数生成器的种子,如果希望第二次调用ShuffleSplit()方法
    #        的结果和第一次调用的结果一致,那么就可以设置一个值,多少都可以,生产环境不要设值
    cv = ShuffleSplit(n_splits=10, test_size=0.2, random_state=0)
    # 生成训练集大小 
    # 函数np.rint()是计算数组各元素的四舍五入的值到最近的整数
    # 函数np.linspace(start_i, stop_i, num)表示从起始值到结束值之间,以均匀的间隔返回指定个数的值。
    # 那么这里就是从1开始、以X结束的总行数的80%的数据,数据间隔是9,最后将数据元素都转换成整型
    train_sizes = np.rint(np.linspace(1, X.shape[0]*0.8 - 1, 9)).astype(int)
    # 创建一个绘图窗口,大小10×7,单位是英寸(inch)
    fig = plt.figure(figsize=(10, 7))
    # 根据深度值创建不同的模型
    # 这里的深度值就是1、3、6、10这4个
    for k, depth in enumerate([1,3,6,10]):
        # 根据深度(max_depth)值来创建决策树回归器
        regressor = DecisionTreeRegressor(max_depth=depth)
        # 通过学习曲线函数计算训练集和测试集的分值
        # 参数1:评估器,这里就是决策树回归器
        # 参数2:特征样本,房屋特征
        # 参数3:目标标签,房屋价格
        # 参数4:训练样本的个数,这是用来生成学习曲线的
        # 参数5:交叉验证生成器,或可迭代对象
        # 参数6:评分器,是一个可调用对象
        sizes, train_scores, test_scores = learning_curve(
           regressor, X, y, train_sizes=train_sizes, cv=cv, scoring='r2')
        # 计算训练集分值和测试集分值的标准差
        train_std = np.std(train_scores, axis=1)
        test_std = np.std(test_scores, axis=1)
        # 计算训练集分值和测试集分值的均值
        train_mean = np.mean(train_scores, axis=1)
        test_mean = np.mean(test_scores, axis=1)
        # 根据学习曲线值来绘制图,四个图的位置通过k+1来控制
        ax = fig.add_subplot(2, 2, k+1)
        # 绘制训练得分线,plot()方法
        #  参数1:X轴方向的值
        #  参数2:y轴方向的值
        #  参数3:绘制出来的线的样式风格,比如这里的“o”表示一个圆点标记,而“-”表示实线
        #  参数4:绘制的线的颜色
        #  参数5:图例上的标题
        ax.plot(sizes, train_mean, 'o-', color='r', label='Training Score')
        # 绘制测试得分线
        ax.plot(sizes, test_mean, 'o-', color='g', label='Testing Score')
        # fill_between()方法表示为训练得分线描边
        #  参数1:X轴方向的值
        #  参数2:y轴方向的覆盖下限
        #  参数3:y轴方向的覆盖上限
        #  参数4:设置覆盖区域的透明度
        #  参数5:设置覆盖区域的颜色
        ax.fill_between(sizes, train_mean - train_std, 
            train_mean + train_std, alpha=0.15, color='r')
        # fill_between()方法表示为测试得分线描边
        ax.fill_between(sizes, test_mean - test_std, 
            test_mean + test_std, alpha=0.15, color='g')
        # 在绘图的窗口上添加标题
        ax.set_title('max_depth = {}'.format(depth))
        # 设置X轴的标题
        ax.set_xlabel('Number of Training Points')
        # 设置y轴的标题
        ax.set_ylabel('Score')
        # 设置X轴方向的最小值和最大值
        ax.set_xlim([0, X.shape[0]*0.8])
        # 设置y轴方向的最小值和最大值
        ax.set_ylim([-0.05, 1.05])
    # 添加图例
    ax.legend(bbox_to_anchor=(1.05, 2.05), loc='lower left', borderaxespad=0.)
    # 添加图形总标题
    fig.suptitle('Decision Tree Regressor Learning Performances', 
                 fontsize=16, y=1.03)
    # 自动调整subplot符合图的区域的参数的布局。生产环境中不要使用该函数,因为这是一个实验特性函数
    fig.tight_layout()
    # 显示绘图
    fig.show()
  ModelLearningGraphMetrics(features, prices)

输出的图形如图1.6所示。

图1.6 决策树回归器的模型性能表现

通过上面4个模型的图形对比,我们发现max_depth=3的模型表现不错;max_depth=1的模型有些欠拟合;max_depth=10的模型有些过拟合;而max_depth=6的模型训练集得分高,但是测试集得分有些低,略微过拟合。

我们还可以通过观察max_depth等于1到10的值来进行模型的性能对比,最后通过输出图形来看它们的表现。比如以后在其他场景中,读者也可以使用下述策略,会更好地找到最佳值模型。

  
  # 导入sklearn库的数据清洗与分割函数,验证曲线函数
  from sklearn.model_selection import ShuffleSplit, validation_curve
  # 定义模型在复杂度高的情况下性能表现的函数
  # 随着模型复杂度的增加计算它的性能表现
  def ModelComplexityPerformanceMetrics(X, y):
    # 清洗和分割数据对象定义
    # 参数1:n_splits表示重新清洗和分割数据的迭代次数,默认值为10
    # 参数2:test_size=0.2表示有0.2的数据用于测试,也就是20%的数据用于测试,80%的数据用于训练
    # 参数3:random_state表示随机数生成器的种子,如果希望第二次调用ShuffleSplit()方法
    #        的结果和第一次调用的结果一致,那么就可以设置一个值,多少都可以,生产环境不要设值
    cv = ShuffleSplit(n_splits=10, test_size=0.2, random_state=0)
    # 定义从1到10为深度(max_depth)的参数值
    max_depth = np.arange(1,11)
    # 通过不同的max_depth的参数值来计算训练集和测试集的分值
    # 参数1:评估器,这里是决策树回归器
    # 参数2:特征样本,房屋特征
    # 参数3:目标标签,房屋价格
    # 参数4:传入的深度参数名称
    # 参数5:传入的深度参数范围值
    # 参数6:交叉验证生成器,或可迭代对象
    # 参数7:评分器,是一个可调用对象
    train_scores, test_scores = \
    validation_curve(DecisionTreeRegressor(), X, y, param_name="max_depth", 
                      param_range=max_depth, cv=cv, scoring='r2')
    # 计算训练集分值和测试集分值的均值
    train_mean = np.mean(train_scores, axis=1)
    test_mean = np.mean(test_scores, axis=1)
    
    # 计算训练集分值和测试集分值的标准差
    train_std = np.std(train_scores, axis=1)
    test_std = np.std(test_scores, axis=1)
    # 绘制验证分值的曲线图
    # figsize表示要绘制的图形窗口大小,单位是英寸
    plt.figure(figsize=(7, 5))
    # 在绘制的图形窗口上添加一个标题
    plt.title('Decision Tree Regressor Complexity Performance')
    # 绘制训练得分线,plot()方法
    #  参数1:X轴方向的值
    #  参数2:y轴方向的值
    #  参数3:绘制出来的线的样式风格,比如这里的“o”表示一个圆点标记,而“-”表示实线
    #  参数4:绘制的线的颜色
    #  参数5:图例上的标题
    plt.plot(max_depth, train_mean, 'o-', color='r', label='Training Score')
    # 绘制测试得分线
    plt.plot(max_depth, test_mean, 'o-', color='g', label='Validation Score')
    # fill_between()方法表示为两条曲线描边,第一条是训练得分线,第二条是测试得分线
    #  参数1:X轴方向的值
    #  参数2:y轴方向的覆盖下限
    #  参数3:y轴方向的覆盖上限
    #  参数4:设置覆盖区域的透明度
    #  参数5:设置覆盖区域的颜色
    plt.fill_between(max_depth, train_mean - train_std, \
        train_mean + train_std, alpha=0.15, color='r')
    plt.fill_between(max_depth, test_mean - test_std, \
        test_mean + test_std, alpha=0.15, color='g')
    # 图上加标题注解
    # 添加图例
    plt.legend(loc='lower right')
    # 添加X轴的标题
    plt.xlabel('Maximum Depth')
    # 添加y轴的标题
    plt.ylabel('Score')
    # 设置y轴方向的最小值和最大值
    plt.ylim([-0.05,1.05])
    # 显示绘图
    plt.show()
  ModelComplexityPerformanceMetrics(features, prices)

输出的图形如图1.7所示。

图1.7 决策树回归器的模型性能表现

从结果图上看,max_depth的参数值在1到10中时,max_depth=4是最佳位置,当max_depth>4时模型表现就有些过拟合了。

1.2.3 网格搜索模型

网格搜索最重要的功能就是能够自动调参,只要用户把参数传进去,它就能返回最优的结果模型和参数。它只适用于小数据集,同时还有一个缺点,那就是可能调参到局部最优,而不是全局最优。

  # 从sklearn库导入网格搜索VC、数据清洗与分割、决策树和分值计算对象的函数
  from sklearn.model_selection import GridSearchCV, ShuffleSplit
  from sklearn.tree import DecisionTreeRegressor
  from sklearn.metrics import make_scorer
  # 定义网格搜索最佳模型函数
  def gridSearchVC_fit_model(X, y):
    # 清洗和分割数据对象定义,
    # 参数1:n_splits表示重新清洗和分割数据的迭代次数,默认值为10
    # 参数2:test_size=0.2表示有0.2的数据用于测试,也就是20%的数据用于测试,80%的数据用于训练
    # 参数3:random_state表示随机数生成器的种子,如果希望第二次调用ShuffleSplit()方法
    #        的结果和第一次调用的结果一致,那么就可以设置一个值,多少都可以,生产环境不要设值
    cv = ShuffleSplit(n_splits=10, test_size=0.2, random_state=0)
    # 创建决策树回归器对象
    regressor = DecisionTreeRegressor(random_state=0)
    # 创建一个字典,表示max_depth的参数值是从1到10
    # 注意:如果代码运行的环境是Python 2,去掉这个list()函数调用
    params = { "max_depth" : list(range(1, 10)) }
    # 通过make_scorer()函数将上面定义的performance_metric()函数转换成计算分值函数
    scoring_fnc = make_scorer(score_func=performance_metric)
    # 创建网格搜索对象
    # 参数1:评估器,就是回归器,这里表示的是决策树回归器
    # 参数2:网格搜索参数
    # 参数3:计算分值函数
    # 参数4:cv(Cross-Validation)交叉验证,传入交叉验证生成器,或者可迭代对象
    grid = GridSearchCV(estimator=regressor, param_grid=params, 
                         scoring=scoring_fnc, cv=cv)
    # 根据数据计算/训练适合网格搜索对象的最佳模型
    grid = grid.fit(X, y)
    # 返回计算得到的最佳模型
    return grid.best_estimator_
  # 网格搜索函数得到最佳模型
  reg = gridSearchVC_fit_model(X_train, y_train)
  print("参数max_depth={}是最佳模型。".format(reg.get_params()['max_depth']))

输出如下。

  参数max_depth=4是最佳模型。

1.2.4 波士顿房价预测

在预测波士顿房价前,我们先做一个模拟客户数据来预测,数据如表1.1所示。

表1.1 模拟房屋数据客户1

  # 小测验
  # 假设有以下3个客户的数据,分别是
client_data = [[3, 30, 10],  # 客户1
               [7, 20, 19],  # 客户2
               [9, 2, 9]]    # 客户3
  # 尝试预测
  for i, price in enumerate(reg.predict(client_data)):
    print("预测客户{}的销售价格是${:,.2f}".format(i+1, price))

输出如下。

  预测客户1的销售价格是$320,425.00
  预测客户2的销售价格是$410,658.62
  预测客户3的销售价格是$938,053.85

定义预测波士顿房价的函数,经过10次迭代,然后看下对比图。

  # 预测房价函数
  def PredictHousingPrice(X, y, fitter):
    # 迭代10次
    epochs = 10
    # 存储预测的价格
    y_predict_test_price = None
    # 分割训练集和测试集数据,20%的数据用于测试,80%的数据用于训练
    X_train, X_test, y_train, y_test = train_test_split(X, y,
            test_size=0.2, random_state=0)
    # 迭代训练
    for epoch_i in range(epochs):
        # 根据数据训练模型,并返回最佳模型
        reg = fitter(X_train, y_train)
        # 预测测试集数据
        predicted_price = reg.predict(X_test)
       # 将预测到的结果存起来
        y_predict_test_price = predicted_price
        print("迭代第{}次。".format(epoch_i+1))
    return y_test, y_predict_test_price
  y_true_price, y_predict_price = \
  PredictHousingPrice(features, prices, gridSearchVC_fit_model)

输出如下。

  迭代第1次。
  迭代第2次。
  迭代第3次。
  ……
  迭代第9次。
  迭代第10次。

查看真实房价的前5行数据。我们先将y_true_price转换成Series对象,然后在调用reset_index()函数时就会把数据创建成一个新的Series,旧的索引就会被作为数据的一列,再通过drop()函数删除旧的索引那一列。

  pd.Series(y_true_price).reset_index().drop('index', axis=1).head()

输出如表1.2所示。

表1.2 查看真实房价的前5行数据

我们再来看预测房价的前5行数据。

  pd.Series(y_predict_price).head()

输出如表1.3所示。

表1.3 查看预测房价的前5行数据

构建真实房价和预测房价的对比图函数。

  # 显示真实房价和预测房价对比图
  def plotVersusFigure(y_true_price, y_predict_price):
    # 创建一个10×7的窗口
    plt.figure(figsize=(10, 7))
    # 绘制的图1是真实房价
    X_show = np.rint(np.linspace(1, 
                                   np.max(y_true_price), 
                                   len(y_true_price))
                    ).astype(int)
    # 绘制图1线,plot()方法:
    #  参数1:X轴方向的值,真实房价从最低价到最高价
    #  参数2:y轴方向的值,真实房价的值
    #  参数3:绘制出来的线的样式风格,比如这里的“o”表示一个圆点标记,而“-”表示实线
    #  参数4:绘制的线的颜色,这里是青色
    plt.plot(X_show, y_true_price, 'o-', color='c')
    # 绘制的图2是预测房价,叠加在图1上
    X_show_predicted = np.rint(np.linspace(1, 
                                             np.max(y_predict_price), 
                                             len(y_predict_price))
                              ).astype(int)
    # 绘制图2线,plot()方法:
    #  参数1:X轴方向的值,预测房价从最低价到最高价
    #  参数2:y轴方向的值,预测房价的值
    #  参数3:绘制出来的线的样式风格,比如这里的“o”表示一个圆点标记,而“-”表示实线
    #  参数4:绘制的线的颜色,这里是洋红色
    plt.plot(X_show_predicted, y_predict_price, 'o-', color='m')
    # 添加标题
    plt.title('Housing Prices Prediction')
    # 添加图例
    plt.legend(loc='lower right', labels=["True Prices", "Predicted Prices"])
    # 添加X轴的标题
    plt.xlabel("House's Price Tendency By Array")
    # 添加y轴的标题
    plt.ylabel("House's Price")
    # 显示绘制
    plt.show()

输出打印波士顿真实房价和预测房价的对比图。

  # 波士顿房屋价格对比图
  plotVersusFigure(y_true_price,y_predict_price)

输出如图1.8所示。

图1.8 波士顿真实房价与预测房价对比图

通过结果可知,预测房价与真实房价之间是有一定误差的。这需要我们不断地调整参数并重新训练、预测等。

1.2.5 北京房价预测

读取bj_housing.csv文件进行北京房价预测,其中一共有9999条数据。训练和预测使用的代码都在波士顿房价预测小节中详细介绍过,这里不再重复。所以本小节的代码就是读取数据,训练模型,预测和绘图显示。这里的北京房屋数据是真实的,但它的预测结果肯定受宏观环境等多种因素的影响,可能会造成预测结果不精准。读取数据代码如下。

  df=pd.read_csv('bj_housing.csv')
  df.describe()

输出如图1.9所示。

图1.9 北京房价基本信息统计

数据的各列名称解释如下。

◇ Area:房屋面积,单位是平方米。

◇ Value:房屋售价,单位是万元。

◇ Room:房间数,单位是间。

◇ Living:厅数,单位是间。

◇ School:是否为学区房,0(表“否”)或1(表“是”)。

◇ Year:房屋建造年份。

◇ Floor:房屋所处楼层。

然后,将房屋特征数据集和真实价格数据集分开,代码如下。

  bj_prices=df['Value']
  bj_features=df.drop('Value',axis=1)
  bj_features.head()

房屋特征数据集前5行数据输出如图1.10所示。

图1.10 北京房屋特征数据集前5行数据

根据数据训练和预测模型。

  y_true_bj_price,y_predict_bj_price=\
  PredictHousingPrice(bj_features,bj_prices,gridSearchVC_fit_model)

输出如下。

  迭代第1次。
  迭代第2次。
  迭代第3次。
  ……
  迭代第9次。
  迭代第10次。

查看北京真实房价的前5行数据,先将y_true_bj_price转换成Series对象,然后在调用reset_index()时函数就会把数据创建成一个新的Series,旧的索引就会被作为该数据对象的一列,再通过drop()函数删除旧的索引那一列。

  y_true_bj_price.reset_index().drop('index',axis=1).head()

输出如表1.4所示。

表1.4 查看真实房价的前5行数据

我们再来看预测房价的前5行数据。

  pd.Series(y_predict_bj_price).head()

输出如表1.5所示。

表1.5 查看预测房价的前5行数据

构建真实房价和预测房价的对比图。

  plotVersusFigure(y_true_bj_price,y_predict_bj_price)

绘制的结果如图1.11所示。

图1.11 北京真实房价与预测房价对比图

通过对比图我们可以看到,预测房价与真实房价之间还是有一定误差的,这需要我们不断地去调整参数重新训练模型和预测等。