《机器学习实战:基于Scikit-Learn、Keras和TensorFlow第2版》-学习笔记(2)

第二章 端到端的机器学习项目

· Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow, 2nd Edition, by Aurélien Géron (O’Reilly). Copyright 2019 Aurélien Géron, 978-1-492-03264-9.
· 环境:Anaconda(Python 3.8) + Pycharm
· 学习时间:2022.03.29~2022.03.31
· 预计阅读时间:1小时

本章将介绍一个端到端的项目案例。假设你是一个房地产公司最近新雇用的数据科学家,以下是你将会经历的主要步骤:

1.观察大局。

2.获得数据。

3.从数据探索和可视化中获得洞见。

4.机器学习算法的数据准备。

5.选择并训练模型。

6.微调模型。

7.展示解决方案。

8.启动、监控和维护系统。

项目案例纯属虚构,目的仅仅是为了说明机器学习项目的主要步骤,而不是为了了解房地产业务。

文章目录

  • 第二章 端到端的机器学习项目
    • 2.1 使用真实数据
    • 2.2 机器学习项目清单
    • 2.3 观察大局
      • 2.3.1 数据流水线
      • 2.3.2 框架问题
      • 2.3.3 选择性能指标
      • 2.3.4 获取数据及数据观察
      • 2.3.5 创建测试集
    • 2.4 从数据探索和可视化中获得洞见
      • 2.4.1 将地理数据可视化
      • 2.4.2 寻找相关性
      • 2.4.3 试验不同属性的组合
    • 2.5 机器学习算法的数据准备
      • 2.5.1 数据清理
      • 2.5.2 处理文本和分类属性
      • 2.5.3 自定义转换器
      • 2.5.4 特征缩放
      • 2.5.5 转换流水线
    • 2.6 选择和训练模型
      • 2.6.1 训练和评估训练集
      • 2.6.2 使用交叉验证评估
      • 2.6.3 保存模型
    • 2.7 微调模型
      • 2.7.1 网格搜索
      • 2.7.2 随机搜索
      • 2.7.3 集成方法
      • 2.7.4 分析最佳模型及其误差
      • 2.7.5 通过测试集评估系统
    • 2.8 启动、监控和维护你的系统
    • 2.9 练习题
    • 代码精简汇总(结合练习)

2.1 使用真实数据

本章我们从StatLib库中选择了加州住房价格的数据集。该数据集基于1990年加州人口普查的数据。

出于教学目的,我们还特意添加了一个分类属性,并且移除了一些特征。

原 始 数 据 集 由 R.Kelley Pace 和 Ronald Barry 提 供 , “Sparse Spatial Autoregressions”,Statistics&Probability Letters 33,no.3(1997):291–297。

2.2 机器学习项目清单

该清单可以指导你完成机器学习项目。主要有8个步骤:

1.框出问题并看整体。

2.获取数据。

3.研究数据以获得深刻见解。

4.准备数据以便更好地将潜在的数据模式提供给机器学习算法。

5.探索许多不同的模型,并列出最佳模型。

6.微调你的模型,并将它们组合成一个很好地解决方案。

7.演示你的解决方案。

8.启动、监视和维护你的系统。

2.3 观察大局

2.3.1 数据流水线

一个序列的数据处理组件称为一个数据流水线

流水线在机器学习系统中非常普遍,因为需要大量的数据操作和数据转化才能应用。组件通常是异步运行的。每个组件拉取大量的数据,然后进行处理,

再将结果传输给另一个数据仓库。一段时间之后,流水线中的下一个组件会拉取前面的数据,并给出自己的输出,以此类推。每个组件都很独立:

组件和组件之间的连接只有数据仓库。这使得整个系统非常简单易懂(在数据流图表的帮助下),不同团队可以专注于不同的组件。

如果某个组件发生故障,它的下游组件还能使用前面的最后一个输出继续正常运行(至少一段时间),所以使得整体架构鲁棒性较强。

2.3.2 框架问题

你现在可以开始设计系统了。首先,你需要回答框架问题:是有监督学习、无监督学习还是强化学习?是分类任务、回归任务还是其他任务?

应该使用批量学习还是在线学习技术?在继续阅读之前,请先暂停一会儿,尝试回答一下这些问题。

有监督、回归、批量学习

2.3.3 选择性能指标

下一步是选择性能指标。回归问题的典型性能指标是均方根误差(RMSE)。它给出了系统通常会在预测中产生多大误差,对于较大的误差,权重较高。

2.3.4 获取数据及数据观察

首先导入需要用到的库:

import os
import tarfile
import urllib
import pandas as pd

读取数据:

# 文件地址
csv_path = "D:\\Py-project\\Python Learning\\Hands-On Machine Learning\\2.End to End Project\\housing.csv"
# 读取文件
housing = pd.read_csv(csv_path)
# 查看文件基本信息
housing.head()  # 查看头五条数据
housing.tail()  # 查看末五条数据
housing.info()  # 快速获取数据集的简单描述,特别是总行数、每个属性的类型和非空值的数量
# 所有属性的字段都是数字,除了ocean_proximity。它的类型是object,因此它可以是任何类型的Python对象。不过从CSV中加载该数据,所以它是文本属性。
housing["ocean_proximity"].value_counts()  # 频次统计. 可以查看分类属性,有多少种分类存在,每种类别下分别有多少个区域
housing.describe()  # 显示数值属性的摘要

对于非数值型 Series 对象, describe() (opens new window)返回值的总数、唯一值数量、出现次数最多的值及出现的次数。

count、mean、min以及max行的意思很清楚。需要注意的是,这里的空值会被忽略(因此本例中,total_bedrooms的count是20 433而不是20 640)。

std行显示的是标准差(用来测量数值的离散程度)。25%、50%和75%行显示相应的百分位数:百分位数表示一组观测值中给定百分比的观测值都低于该值。

快速查看数据的直方图:

import matplotlib
import matplotlib.pyplot as plt

plt.style.use('seaborn-white')
matplotlib.rcParams["font.sans-serif"] = ["SimHei"]  # 指定字体为SimHei,用于显示中文,如果Ariel,中文会乱码
matplotlib.rcParams["axes.unicode_minus"] = False  # 用来正常显示负号
housing.hist(bins=50, figsize=(20, 15))  # 绘制每个数值属性的直方图
# 直方图用来显示给定值范围(横轴)的实例数量(纵轴)。你可以一次绘制一个属性,也可以在整个数据集上调用hist()方法,绘制每个属性的直方图。
plt.savefig('①直方图.png')

2.3.5 创建测试集

在进一步查看数据之前,你需要先创建一个测试集

在这个阶段主动搁置部分数据听起来可能有些奇怪。毕竟,你才只简单浏览了一下数据而已,在决定用什么算法之前,当然还需要了解更多的知识,对吧?

没错,但是大脑是个非常神奇的模式检测系统,也就是说,它很容易过拟合:如果是你本人来浏览测试集数据,很可能会跌入某个看似有趣的测试数据模式,

进而选择某个特殊的机器学习模型。然后当你再使用测试集对泛化误差率进行估算时,估计结果将会过于乐观,该系统启动后的表现将不如预期那般优秀。

这称为数据窥探偏误(data snooping bias)。

理论上,创建测试集非常简单,只需要随机选择一些实例,通常是数据集的20%(如果数据集很大,比例将更小),然后将它们放在一边:

但为了即使在更新数据集之后也有一个稳定的训练测试分割,常见地解决方案是每个实例都使用一个标识符来决定是否进入测试集

(假定每个实例都有一个唯一且不变的标识符)。例如,你可以计算每个实例标识符的哈希值,如果这个哈希值小于或等于最大哈希值的20%,

则将该实例放入测试集。这样可以确保测试集在多个运行里都是一致的,即便更新数据集也仍然一致。

新实例的20%将被放入新的测试集,而之前训练集中的实例也不会被放入新测试集。

from zlib import crc32
import numpy as np


def test_set_check(identifier, test_ratio):
 return crc32(np.int64(identifier)) & 0xffffffff < test_ratio * 2 ** 32

不幸的是,housing数据集没有标识符列。最简单的解决方法是使用行索引作为ID:

housing_with_id = housing.reset_index()  # 添加一列‘index’索引列

如果使用行索引作为唯一标识符,你需要确保在数据集的末尾添加新数据,并且不会删除任何行。

如果不能保证这一点,那么你可以尝试使用某个最稳定的特征来创建唯一标识符。

例如,一个区域的经纬度肯定几百万年都不会变,你可以将它们组合成如下的ID

housing_with_id[“id”] = housing[“longitude”] * 1000 + housing[“latitude”] # 添加一列‘index’索引列

Scikit-Learn提供了一些函数,可以通过多种方式将数据集分成多个子集。

最简单的函数是train_test_split()。首先,它也有random_state参数,让你可以像之前提到过的那样设置随机生成器种子;

random_state是为了保证程序每次运行都分割一样的训练集和测试集。其次,你可以把行数相同的多个数据集一次性发送给它,它会根据相同的索引将其拆分

from sklearn.model_selection import train_test_split

train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)  # 划分数据集,test_size是测试集的百分比大小

要预测房价中位数,收入中位数是一个非常重要的属性。于是你希望确保在收入属性上,测试集能够代表整个数据集中各种不同类型的收入。

由于收入中位数是一个连续的数值属性,所以你得先创建一个收入类别的属性。

用pd.cut()来创建5个收入类别属性的(用1~5来做标签),0~1.5是类别1,1.5~3是类别2,以此类推:

housing["income_cat"] = pd.cut(housing["median_income"], bins=[0.0, 1.5, 3.0, 4.5, 6.0, np.inf], labels=[1, 2, 3, 4, 5])  # np.inf是无穷大

重新抽样划分数据集:

现在,你可以根据收入类别进行分层抽样了。使用Scikit-Learn的StratifiedShuffleSplit类(分层随机切分交叉验证器):

from sklearn.model_selection import StratifiedShuffleSplit


split = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=42)  # 设置分组抽样切分数据集的参数
n_splits重新打乱和切分迭代的次数。test_size和train_size的大小只用设置1个,另一个默认是其补集。
for train_index, test_index in split.split(housing, housing["income_cat"]):  # 生成索引以将数据分为训练集和测试集。
  strat_train_set = housing.loc[train_index]
  strat_test_set = housing.loc[test_index]
# 查看分层结果
print(strat_test_set["income_cat"].value_counts() / len(strat_test_set))
print(strat_train_set["income_cat"].value_counts() / len(strat_train_set))
# 现在你可以删除income_cat属性,将数据恢复原样了:
for set_ in (strat_train_set, strat_test_set):
  set_.drop("income_cat", axis=1, inplace=True)

2.4 从数据探索和可视化中获得洞见

到现在为止,我们还只是在快速浏览数据,从而对手头上正在处理的数据类型形成一个大致的了解。本阶段的目标是再深入一点。

首先,把测试集放在一边,你能探索的只有训练集。此外,如果训练集非常庞大,你可以抽样一个探索集,这样后面的操作更简单快捷一些。

让我们先创建一个副本,这样可以随便尝试而不损害训练集:

housing = strat_train_set.copy()

2.4.1 将地理数据可视化

由于存在地理位置信息(经度和纬度),因此建立一个各区域的分布图以便于可视化数据是一个很好的想法

housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.1)  # kind指定图形类型,x,y定数据,alpha突出高密度数据点位置
plt.savefig('②地位位置.png')

现在加入房价信息和人口信息。每个圆的半径大小代表了每个区域的人口数量(选项s),颜色代表价格(选项c)。

我们使用一个名叫jet的预定义颜色表(选项cmap)来进行可视化,颜色范围从蓝(低)到红(高)

housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.4,
             s=housing["population"] / 100, label="population", figsize=(10, 7),
             c="median_house_value", cmap=plt.get_cmap("jet"), colorbar=True)
plt.legend()
plt.savefig('③地理位置+房价+人口.png')

s指定圆的大小与人口数量相关,label标注,c指定圆的颜色与房价高低相关,cmap指定颜色表,colorbar=True/False开启或关闭色条显示。

2.4.2 寻找相关性

corr_matrix = housing.corr()  # corr()方法计算每对属性之间的标准相关系数(默认pearson相关)
# 也可以用kendall或spearman,eg: corr_matrix = housing.corr(method='spearman/kendall')
print(corr_matrix)
print(corr_matrix["median_house_value"].sort_values(ascending=False))  # 单独现实1列的相关性,sort_values是用来排序的

还有一种方法可以检测属性之间的相关性,就是使用pandas的scatter_matrix函数,它会绘制出每个数值属性相对于其他数值属性的相关性。

现在我们有11个数值属性,可以得到121个图像,篇幅原因无法完全展示,这里仅关注那些与房价中位数属性最相关的,可算作是最有潜力的属性

from pandas.plotting import scatter_matrix


attributes = ["median_house_value", "median_income", "total_rooms", "housing_median_age"]
scatter_matrix(housing[attributes], figsize=(12, 8))  # 指定绘制部分数据,可加入“diagonal='kde'”让直方图变成密度分布图
plt.savefig('④相关性散点图.png')

最有潜力能够预测房价中位数的属性是收入中位数,所以我们单独来看看其相关性的散点图

housing.plot(kind="scatter", x="median_income", y="median_house_value", alpha=0.1)
plt.savefig('⑤房价中位数与收入中位数相关性散点图.png')

2.4.3 试验不同属性的组合

在准备给机器学习算法输入数据之前,你要做的最后一件事应该是尝试各种属性的组合。

例如,如果你不知道一个区域有多少个家庭,那么知道一个区域的“房间总数”也没什么用。你真正想要知道的是一个家庭的房间数量。

同样,单看“卧室总数”这个属性本身也没什么意义,你可能想拿它和“房间总数”来对比,或者拿来同“每个家庭的人口数”这个属性组合似乎也挺有意思。

我们来试着创建这些新属性:

housing["rooms_per_household"] = housing["total_rooms"] / housing["households"]
housing["bedrooms_per_room"] = housing["total_bedrooms"] / housing["total_rooms"]
housing["population_per_household"] = housing["population"] / housing["households"]
# 再来看看相关矩阵:
corr_matrix = housing.corr()
corr_matrix["median_house_value"].sort_values(ascending=False).to_excel(R"⑥新相关性矩阵.xlsx")

显然,卧室/房间比例更低的房屋往往价格更贵。同样,“每个家庭的房间数量”也比“房间总数”更具信息量——房屋越大,价格越贵。

这一轮的探索不一定要多么彻底,关键是迈开第一步,快速获得洞见,这将有助于你获得非常棒的第一个原型。

这也是一个不断迭代的过程:一旦你的原型产生并且开始运行,你可以分析它的输出以获得更多洞见,然后再次回到这个探索步骤。

2.5 机器学习算法的数据准备

这里你应该编写函数来执行,而不是手动操作,原因如下:

  • 你可以在任何数据集上轻松重现这些转换(例如,获得更新的数据集之后)。

  • 你可以逐渐建立起一个转换函数的函数库,可以在以后的项目中重用。

  • 你可以在实时系统中使用这些函数来转换新数据,再输入给算法。

  • 你可以轻松尝试多种转换方式,查看哪种转换的组合效果最佳。

但是现在,让我们先回到一个干净的训练集(再次复制strat_train_set),然后将预测器和标签分开,因为这里我们不一定对它们使用相同的转换方式

(需要注意drop()会创建一个数据副本,但是不影响strat_train_set)

housing = strat_train_set.drop("median_house_value", axis=1)  # drop将预测值列和标签分开
housing_labels = strat_train_set["median_house_value"].copy()

2.5.1 数据清理

大部分的机器学习算法无法在缺失的特征上工作,所以我们要创建一些函数来辅助它。

前面我们已经注意到total_bedrooms属性有部分值缺失,所以我们要解决它。有以下三种选择:

  1. 放弃这些相应的区域。

  2. 放弃整个属性。

  3. 将缺失的值设置为某个值(0、平均数或者中位数等)。

通过DataFrame的dropna()、drop()和fillna()方法,可以轻松完成这些操作:

housing.dropna(subset=["total_bedrooms"])  # option 1
housing.drop("total_bedrooms", axis=1)  # option 2
median = housing["total_bedrooms"].median()
housing["total_bedrooms"].fillna(median, inplace=True)  # option 3

Scikit-Learn提供了一个非常容易上手的类来处理缺失值:SimpleImputer。使用方法如下:

# 首先,你需要创建一个SimpleImputer实例,指定你要用属性的中位数值替换该属性的缺失值:
from sklearn.impute import SimpleImputer


imputer = SimpleImputer(strategy="median")
# 由于中位数值只能在数值属性上计算,所以我们需要创建一个没有文本属性ocean_proximity的数据副本:
housing_num = housing.drop("ocean_proximity", axis=1)
# 使用fit()方法将imputer实例适配到训练数据,使用transform()方法将imputer应用到housing_num:
# fit_transform()是两者的联合优化方法。(适用于sklearn的大多数方法)
x = imputer.fit_transform(housing_num)

PS:这里imputer仅仅只是计算了每个属性的中位数值,并将结果存储在其实例变量statistics_中。虽然只有total_bedrooms这个属性存在缺失值,

但是我们无法确认系统启动之后新数据中是否一定不存在任何缺失值,所以稳妥起见,还是将imputer应用于所有的数值属性:

现在x是一个包含转换后特征的NumPy数组(不适用与pandas的dataframe)。如果你想将它放回pandas DataFrame,也很简单:

housing_tr = pd.DataFrame(x, columns=housing_num.columns, index=housing_num.index)

2.5.2 处理文本和分类属性

到目前为止,我们只处理数值属性,但现在让我们看一下文本属性。在此数据集中,只有一个:ocean_proximity属性。

housing_cat = housing[["ocean_proximity"]]

它不是任意文本,而是有限个可能的取值,每个值代表一个类别。因此,此属性是分类属性。

大多数机器学习算法更喜欢使用数字,因此让我们将这些类别从文本转到数字。为此,我们可以使用Scikit-Learn的OrdinalEncoder类

from sklearn.preprocessing import OrdinalEncoder


ordinal_encoder = OrdinalEncoder()
housing_cat_encoded = ordinal_encoder.fit_transform(housing_cat)
print('使用Categories_实例变量获取类别列表: ', ordinal_encoder.categories_)
print(housing_cat_encoded[:10])

这种表征方式产生的一个问题是,机器学习算法会认为两个相近的值比两个离得较远的值更为相似一些。

在某些情况下这是对的(对一些有序类别,像“坏”“平均”“好”“优秀”),但是,对ocean_proximity而言情况并非如此

(例如,类别0和类别4之间就比类别0和类别1之间的相似度更高)。

为了解决这个问题,常见的解决方案是给每个类别创建一个二进制的属性:

当类别是“<1H OCEAN”时,一个属性为1(其他为0),当类别是“INLAND”时,另一个属性为1(其他为0),以此类推。

这就是独热编码(one hot),因为只有一个属性为1(热),其他均为0(冷)。新的属性有时候称为哑(dummy)属性。

Scikit-Learn提供了一个OneHotEncoder编码器,可以将整数类别值转换为独热向量。我们用它来将类别编码为独热向量。

from sklearn.preprocessing import OneHotEncoder


cat_encoder = OneHotEncoder()
housing_cat_1hot = cat_encoder.fit_transform(housing_cat)
print('观察生成的SciPy稀疏矩阵:\n', housing_cat_1hot)

这里的输出是一个SciPy稀疏矩阵,而不是一个NumPy数组。

当你有成千上万个类别属性时,这个函数会非常有用。因为在独热编码完成之后,我们会得到一个几千列的矩阵,并且全是0,每行仅有一个1。

占用大量内存来存储0是一件非常浪费的事情,因此稀疏矩阵选择仅存储非零元素的位置。而你依旧可以像使用一个普通的二维数组那样来使用它,

当然如果你实在想把它转换成一个(密集的)NumPy数组,只需要调用toarray()方法即可:

housing_cat_1hot_array = housing_cat_1hot.toarray()
print('---begin---\n', housing_cat_1hot_array)
print('再次使用Categories_实例变量获取类别列表: ', cat_encoder.categories_)

如果类别属性具有大量可能的类别(例如,国家代码、专业、物种),那么独热编码会导致大量的输入特征,这可能会减慢训练并降低性能。

如果发生这种情况,你可能想要用相关的数字特征代替类别输入。

例如,你可以用与海洋的距离来替换ocean_proximity特征(类似地,可以用该国家的人口和人均GDP来代替国家代码)。

或者,你可以用可学习的低维向量(称为嵌入)来替换每个类别。每个类别的表征可以在训练期间学习。这是表征学习的示例

2.5.3 自定义转换器

虽然Scikit-Learn提供了许多有用的转换器(比如上面的SimpleImputer、OrdinalEncoder),

但是你仍然需要为一些诸如自定义清理操作或组合特定属性等任务编写自己的转换器。

你当然希望让自己的转换器与Scikit-Learn自身的功能(比如流水线)无缝衔接,而由于Scikit-Learn依赖于鸭子类型的编译,而不是继承,

所以你所需要的只是创建一个类,然后应用以下三种方法:fit()(返回self)、transform()、fit_transform()。

你可以通过添加TransformerMixin作为基类,直接得到最后一种方法。同时,如果添加BaseEstimator作为基类(并在构造函数中避免*args和**kargs)

你还能额外获得两种非常有用的自动调整超参数的方法(get_params()和set_params(),包含在BaseEstimator类中)。

注:大概意思就是,用sklearn的类组建自己的新类。

from sklearn.base import BaseEstimator, TransformerMixin

rooms_ix, bedrooms_ix, population_ix, households_ix = 3, 4, 5, 6
 

class CombinedAttributesAdder(BaseEstimator, TransformerMixin):
  	def __init__(self, add_bedrooms_per_room=True):  # no *args or **kargs
   	 	self.add_bedrooms_per_room = add_bedrooms_per_room

  	def fit(self, x, y=None):
    	return self  # nothing else to do
	def transform(self, x):
        rooms_per_household = x[:, rooms_ix] / x[:, households_ix]
        population_per_household = x[:, population_ix] / x[:, households_ix]
        if self.add_bedrooms_per_room:
            bedrooms_per_room = x[:, bedrooms_ix] / x[:, rooms_ix]
            return np.c_[x, rooms_per_household, population_per_household, bedrooms_per_room]
        else:
            return np.c_[x, rooms_per_household, population_per_household]


attr_adder = CombinedAttributesAdder(add_bedrooms_per_room=False)
housing_extra_attribs = attr_adder.transform(housing.values)

2.5.4 特征缩放

特征缩放即把数据都放在同一个尺度下。

最重要也最需要应用到数据上的转换就是特征缩放。如果输入的数值属性具有非常大的比例差异,往往会导致机器学习算法的性能表现不佳,当然也有极少数特例

案例中的房屋数据就是这样:房间总数的范围从6~39 320,而收入中位数的范围是0~15。

注意,目标值通常不需要缩放。

同比例缩放所有属性的两种常用方法是最小-最大缩放和标准化。

最小-最大缩放(又叫作归一化):

将值重新缩放使其最终范围归于0~1之间。实现方法是将值减去最小值并除以最大值和最小值的差。

对此,Scikit-Learn提供了一个名为MinMaxScaler的转换器。如果出于某种原因,你希望范围不是0~1,那么可以通过调整超参数feature_range进行更改。

housing_try_by_myself = strat_train_set.drop('ocean_proximity', axis=1)  # 创建一个自己用的训练数据集

from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler(feature_range=(0, 2))  # feature_range可用来设置归一化的范围区域,不设置默认是(0,1)
housing_try_by_myself = scaler.fit_transform(housing_try_by_myself)
print('\nhello myself: \n', housing_try_by_myself)

标准化:

首先减去平均值(所以标准化值的均值总是零),然后除以方差,从而使得结果的分布具备单位方差。不同于最小-最大缩放的是,标准化不将值绑定到特定范围,

对某些算法而言,这可能是个问题(例如,神经网络期望的输入值范围通常是0~1)。但是标准化的方法受异常值的影响更小。

例如,假设某个地区的平均收入为100(错误数据),最小-最大缩放会将所有其他值从0~15降到0~0.15,而标准化则不会受到很大影响。

Scikit-Learn提供了一个标准化的转换器StandardScaler。

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
housing_try_by_myself = scaler.fit_transform(housing_try_by_myself)
print('\nhello myself 2 : \n', housing_try_by_myself)

重要的是,跟所有转换一样,缩放器仅用来拟合训练集,而不是完整的数据集(包括测试集)。只有这样,才能使用它们来转换训练集和测试集(和新数据)。

2.5.5 转换流水线

转换流水线: 带有最终估计器的转换管道 → 按流程处理数据。

正如你所见,许多数据转换的步骤需要以正确的顺序来执行。而Scikit-Learn正好提供了Pipeline类来支持这样的转换。下面是一个数值属性的流水线示例:

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

num_pipeline = Pipeline([
  ('imputer', SimpleImputer(strategy="median")),
  ('attribs_adder', CombinedAttributesAdder()),
  ('std_scaler', StandardScaler())])
housing_num_tr = num_pipeline.fit_transform(housing_num)

Pipeline构造函数会通过一系列名称/估算器的配对来定义步骤序列。除了最后一个是估算器之外,前面都必须是转换器(也就是说,必须有fit_transform()方法)。至于命名可以随意,你喜欢就好(只要它们是独一无二的,不含双下划线),它们稍后在超参数调整中会有用

到目前为止,我们分别处理了类别列和数值列。拥有一个能够处理所有列的转换器会更方便,将适当的转换应用于每个列。

在0.20版中,Scikit-Learn为此引入了ColumnTransformer,好消息是它与pandas DataFrames一起使用时效果很好。

让我们用它来将所有转换应用到房屋数据:

from sklearn.compose import ColumnTransformer

# ColumnTransformer将转换器应用于数组或pandas的DataFrame的列
# 该估计器允许独立地转换输入的不同列或列子集,并将每个转换器生成的特征连接起来形成一个单一的特征空间。
# 这对于异构或柱状数据非常有用,可以将多个特征提取机制或转换组合成单个转换器。
num_attribs = list(housing_num)
cat_attribs = ["ocean_proximity"]
full_pipeline = ColumnTransformer([("num", num_pipeline, num_attribs), ("cat", OneHotEncoder(), cat_attribs)])
housing_prepared = full_pipeline.fit_transform(housing)

构造函数需要一个元组列表,其中每个元组都包含一个名字、一个转换器,以及一个该转换器能够应用的列名字(或索引)的列表。

在此示例中,我们指定数值列使用之前定义的num_pipeline进行转换,类别列使用OneHotEncoder进行转换。

最后,我们将ColumnTransformer应用到房屋数据:它将每个转换器应用于适当的列,并沿第二个轴合并输出(转换器必须返回相同数量的行)。

请注意,OneHotEncoder返回一个稀疏矩阵,而num_pipeline返回一个密集矩阵。

当稀疏矩阵和密集矩阵混合在一起时,ColumnTransformer会估算最终矩阵的密度(即单元格的非零比率),

如果密度低于给定的阈值,则返回一个稀疏矩阵(通过默认值为sparse_threshold=0.3)。

在此示例中,它返回一个密集矩阵。我们有一个预处理流水线,该流水线可以获取全部房屋数据并对每一列进行适当的转换。

2.6 选择和训练模型

2.6.1 训练和评估训练集

首先,如同我们在第1章所做的,先训练一个线性回归模型:

from sklearn.linear_model import LinearRegression

lin_reg = LinearRegression()
lin_reg.fit(housing_prepared, housing_labels)
# 现在你有一个可以工作的线性回归模型了。让我们用几个训练集的实例试试:
some_data = housing.iloc[:5]
some_labels = housing_labels.iloc[:5]
some_data_prepared = full_pipeline.transform(some_data)
print("Predictions:", lin_reg.predict(some_data_prepared))
print("Labels:", list(some_labels))

可以工作了,虽然预测还不是很准确(例如,第一次的预测失效接近40%!)。

我们可以使用Scikit-Learn的mean_squared_error()函数来测量整个训练集上回归模型的RMSE:

from sklearn.metrics import mean_squared_error 

housing_predictions = lin_reg.predict(housing_prepared)
lin_mse = mean_squared_error(housing_labels, housing_predictions)
lin_rmse = np.sqrt(lin_mse)
print((lin_rmse))

好吧,这虽然比什么都没有要好,但显然也不是一个好看的成绩:

大多数区域的median_housing_values分布在120 000~265 000美元之间,所以典型的预测误差达到68 628美元只能算是差强人意。

这就是一个典型的模型对训练数据欠拟合的案例。这种情况发生时,通常意味着这些特征可能无法提供足够的信息来做出更好的预测,或者是模型本身不够强大。

我们在第1章已经提到,想要修正欠拟合,可以通过选择更强大的模型,或为算法训练提供更好的特征,又或者减少对模型的限制等方法。

我们这个模型不是一个正则化的模型,所以就排除了最后那个选项。你可以试试添加更多的特征(例如,人口数量的日志),但首先,

让我们尝试一个更复杂的模型,看看它到底是怎么工作的。

我们来训练一个DecisionTreeRegressor。这是一个非常强大的模型,它能够从数据中找到复杂的非线性关系:

from sklearn.tree import DecisionTreeRegressor

tree_reg = DecisionTreeRegressor()
tree_reg.fit(housing_prepared, housing_labels)
# 既然这个模型已经训练有素,我们可以用训练集来评估一下:
housing_predictions = tree_reg.predict(housing_prepared)
tree_mse = mean_squared_error(housing_labels, housing_predictions)
tree_rmse = np.sqrt(tree_mse)
print(tree_rmse)

PS: 和转换器的区别好像在:转换器的使用是fit和transform,而算法估算器是fit和predict。(好像还有SVM是用fit和score?)

等等,什么!完全没有错误?这个模型真的可以做到绝对完美吗?当然,更有可能的是这个模型对数据严重过拟合了。我们怎么确认呢?

前面提到过,在你有信心启动模型之前,都不要触碰测试集,所以这里,你需要拿训练集中的一部分用于训练,另一部分用于模型验证。

2.6.2 使用交叉验证评估

评估决策树模型的一种方法是使用train_test_split函数将训练集分为较小的训练集和验证集,然后根据这些较小的训练集来训练模型,并对其进行评估。

另一个不错的选择是使用Scikit-Learn的K-折交叉验证功能。以下是执行K-折交叉验证的代码:

它将训练集随机分割成10个不同的子集,每个子集称为一个折叠,然后对决策树模型进行10次训练和评估——每次挑选1个折叠进行评估,

使用另外的9个折叠进行训练。产生的结果是一个包含10次评估分数的数组:

from sklearn.model_selection import cross_val_score

scores = cross_val_score(tree_reg, housing_prepared, housing_labels, scoring="neg_mean_squared_error", cv=10)
tree_rmse_scores = np.sqrt(-scores)

Scikit-Learn的交叉验证功能更倾向于使用效用函数(越大越好)而不是成本函数(越小越好),所以计算分数的函数实际上是负的MSE(一个负值)函数,

这就是为什么上面的代码在计算平方根之前会先计算出-scores。

让我们看看结果:

def display_scores(scores):
  print("Scores:", scores)
  print("Mean:", scores.mean())
  print("Standard deviation:", scores.std())


display_scores(tree_rmse_scores)

这次的决策树模型好像不如之前表现得好。事实上,它看起来简直比线性回归模型还要糟糕!

请注意,交叉验证不仅可以得到一个模型性能的评估值,还可以衡量该评估的精确度(即其标准差)。这里该决策树得出的评分约为71 407,上下浮动±2439。

如果你只使用了一个验证集,就收不到这样的结果信息。交叉验证的代价就是要多次训练模型,因此也不是永远都行得通。

保险起见,让我们也计算一下线性回归模型的评分:

lin_scores = cross_val_score(lin_reg, housing_prepared, housing_labels, scoring="neg_mean_squared_error", cv=10)
lin_rmse_scores = np.sqrt(-lin_scores)
display_scores(lin_rmse_scores)

没错,决策树模型确实是严重过拟合了,以至于表现得比线性回归模型还要糟糕。

我们再来试试最后一个模型:RandomForestRegressor随机森林:

通过对特征的随机子集进行许多个决策树的训练,然后对其预测取平均。在多个模型的基础之上建立模型,称为集成学习,

这是进一步推动机器学习算法的好方法。这里我们将跳过大部分代码,因为与其他模型基本相同:

from sklearn.ensemble import RandomForestRegressor  # 导入模块

forest_reg = RandomForestRegressor()  # 定义1个随机森林算法,因为后续可以对算法进行改进和微调,所以需要定义1个算法
forest_reg.fit(housing_prepared, housing_labels)  # 训练该模型(就是用数据去fit这个模型)
housing_predictions = forest_reg.predict(housing_prepared)  # 使用模型,用模型去预测要预测的数据
forest_mse = mean_squared_error(housing_labels, housing_predictions)
forest_rmse = np.sqrt(forest_mse)
scores = cross_val_score(forest_reg, housing_prepared, housing_labels, scoring="neg_mean_squared_error", cv=10)
forest_rmse_scores = np.sqrt(-scores)
print(forest_rmse)
display_scores(forest_rmse_scores)

哇,这个就好多了:随机森林看起来很有戏。但是,请注意,训练集上的分数仍然远低于验证集,这意味着该模型仍然对训练集过拟合。

过拟合的可能解决方案包括简化模型、约束模型(即使其正规化),或获得更多的训练数据。不过在深入探索随机森林之前,

你应该先尝试一遍各种机器学习算法的其他模型(几种具有不同内核的支持向量机,比如神经网络模型等),但是记住,别花太多时间去调整超参数。

我们的目的是筛选出几个(2~5个)有效的模型。

2.6.3 保存模型

每一个尝试过的模型你都应该妥善保存,以便将来可以轻松回到你想要的模型当中。

记得还要同时保存超参数和训练过的参数,以及交叉验证的评分和实际预测的结果。这样你就可以轻松地对比不同模型类型的评分,以及不同模型造成的错误类型

通过Python的pickle模块或joblib库,你可以轻松保存Scikit-Learn模型,这样可以更有效地将大型NumPy数组(可以用pip安装)序列化:

import joblib

joblib.dump(forest_reg, filename='forest_reg_example_from_book.m')  # 将模型forest_reg存入forest_reg_example_from_book文件
# and later...
my_model_loaded = joblib.load('forest_reg_example_from_book.m')  # 调用forest_reg_example_from_book 文件里的forest_reg模型

2.7 微调模型

假设你现在有了一个有效模型的候选列表。现在你需要对它们进行微调。我们来看几个可行的方法。

2.7.1 网格搜索

一种微调的方法是手动调整超参数,直到找到一组很好的超参数值组合。这个过程非常枯燥乏味,你可能坚持不到足够的时间来探索出各种组合。

相反,你可以用Scikit-Learn的GridSearchCV来替你进行探索。你所要做的只是告诉它你要进行实验的超参数是什么,以及需要尝试的值,

它将会使用交叉验证来评估超参数值的所有可能组合。例如,下面这段代码搜索RandomForestRegressor的超参数值的最佳组合:

from sklearn.model_selection import GridSearchCV
param_grid = [{'n_estimators': [3, 10, 30], 'max_features': [2, 4, 6, 8]}, {'bootstrap': [False], 'n_estimators': [3, 10], 'max_features': [2, 3, 4]}]
forest_reg = RandomForestRegressor()
grid_search = GridSearchCV(forest_reg, param_grid, cv=5, scoring='neg_mean_squared_error', return_train_score=True)
grid_search.fit(housing_prepared, housing_labels)

当你不知道超参数应该赋什么值时,一个简单的方法是尝试10的连续幂次方

(如果你想要得到更细粒度的搜索,可以使用更小的数,参考这个示例中所示的n_estimators超参数)。

这个param_grid告诉Scikit-Learn:

首先评估第一个dict中的n_estimator和max_features的所有3×4=12种超参数值组合(先不要担心这些超参数现在意味着什么,在第7章中会解释)

接着,尝试第二个dict中超参数值的所有2×3=6种组合,但这次超参数bootstrap需要设置为False而不是True(True是该超参数的默认值)。

总而言之,网格搜索将探索RandomForestRegressor超参数值的12+6=18种组合,并对每个模型进行5次训练(因为我们使用的是5-折交叉验证)。

换句话说,总共会完成18×5=90次训练!这可能需要相当长的时间,但是完成后你就可以获得最佳的参数组合:

print(grid_search.best_params_)

因为被评估的最优n_estimator是最大值8和30,所以还可以试试更高的值,评分可能还会继续改善。

你可以直接得到最好的估算器:

print(grid_search.best_estimator_)

如果GridSearchCV被初始化为refit=True(默认值),那么一旦通过交叉验证找到了最佳估算器,它将在整个训练集上重新训练。

这通常是个好方法,因为提供更多的数据很可能提升其性能。

当然还有评估分数:

cvres = grid_search.cv_results_
for mean_score, params in zip(cvres["mean_test_score"], cvres["params"]):
  print(np.sqrt(-mean_score), params)

在本例中,我们得到的最佳解决方案是将超参数max_features设置为8,将超参数n_estimators设置为30。这个组合的RMSE分数为49682,略优于之前使用默认超参数值的分数50 182。

不要忘了,有些数据准备的步骤也可以当作超参数来处理。

例如,网格搜索会自动查找是否添加你不确定的特征(比如是否使用转换器CombinedAttributesAdder的超参数add_bedrooms_per_room)。

同样,还可以用它来自动寻找处理问题的最佳方法,例如处理异常值、缺失特征,以及特征选择等。

2.7.2 随机搜索

如果探索的组合数量较少(例如上一个示例),那么网格搜索是一种不错的方法。

但是当超参数的搜索范围较大时,通常会优先选择使用RandomizedSearchCV. 这个类用起来与GridSearchCV类大致相同,

但它不会尝试所有可能的组合,而是在每次迭代中为每个超参数选择一个随机值,然后对一定数量的随机组合进行评估。这种方法有两个显著好处:

如果运行随机搜索1000个迭代,那么将会探索每个超参数的1000个不同的值(而不是像网格搜索方法那样每个超参数仅探索少量几个值)。

通过简单地设置迭代次数,可以更好地控制要分配给超参数搜索的计算预算。

2.7.3 集成方法

还有一种微调系统的方法是将表现最优的模型组合起来。组合(或“集成”)方法通常比最佳的单一模型更好

(就像随机森林比其所依赖的任何单个决策树模型更好一样),特别是当单一模型会产生不同类型误差时更是如此。我们将在第7章中更详细地介绍这个主题。

2.7.4 分析最佳模型及其误差

通过检查最佳模型,你总是可以得到一些好的洞见。例如在进行准确预测时,RandomForestRegressor可以指出每个属性的相对重要程度:

feature_importances = grid_search.best_estimator_.feature_importances_
print(feature_importances)  # 此处仅有数据中各个属性/特征的重要性分数,没有属性名称
# 将这些重要性分数显示在对应的属性名称旁边:
extra_attribs = ["rooms_per_hhold", "pop_per_hhold", "bedrooms_per_room"]
cat_encoder = full_pipeline.named_transformers_["cat"]
cat_one_hot_attribs = list(cat_encoder.categories_[0])
attributes = num_attribs + extra_attribs + cat_one_hot_attribs
print(sorted(zip(feature_importances, attributes), reverse=True))

2.7.5 通过测试集评估系统

通过一段时间的训练,你终于有了一个表现足够优秀的系统。现在是用测试集评估最终模型的时候了。

这个过程没有什么特别的,只需要从测试集中获取预测器和标签,运行full_pipeline来转换数据(调用transform()而不是fit_transform())

然后在测试集上评估最终模型:

final_model = grid_search.best_estimator_
x_test = strat_test_set.drop("median_house_value", axis=1)
y_test = strat_test_set["median_house_value"].copy()
x_test_prepared = full_pipeline.transform(x_test)
final_predictions = final_model.predict(x_test_prepared)
final_mse = mean_squared_error(y_test, final_predictions)
final_rmse = np.sqrt(final_mse)  # => evaluates to 47,730.2

在某些情况下,泛化误差的这种点估计将不足以说服你启动生产环境:如果它仅比当前生产环境中的模型好0.1%?

你可能想知道这个估计的精确度。为此,你可以使用scipy.stats.t.interval()计算泛化误差的95%置信区间:

from scipy import stats
confidence = 0.95
squared_errors = (final_predictions - y_test) ** 2
np.sqrt(stats.t.interval(confidence, len(squared_errors) - 1, loc=squared_errors.mean(), scale=stats.sem(squared_errors)))

如果之前进行过大量的超参数调整,这时的评估结果通常会略逊于你之前使用交叉验证时的表现结果(因为通过不断调整,系统在验证数据上终于表现良好,在未知数据集上可能达不到这么好的效果)。

在本例中,结果虽然并非如此,但是当这种情况发生时,你一定不要继续调整超参数,不要试图再努力让测试集的结果变得好看一些,因为这些改进在泛化到新的数据集时又会变成无用功。

2.8 启动、监控和维护你的系统

即使是经过训练可以对猫和狗的图片进行分类的模型,也可能需要定期重新训练,不是因为猫和狗会一夜之间有变化,而是因为相机在不断变化,

图像格式、清晰度、亮度和尺寸也会变化。而且,人们明年可能会爱上不同的品种,或者他们可能会给宠物戴上小帽子,谁知道呢?

最后,请保留每个模型的备份,准备好流程和工具,以便在新模型出现时快速回滚到以前的模型,以防新模型由于某种情况开始出现严重故障。

进行备份还可以轻松实现将新模型与以前的模型进行比较。同样,你应该保留每个版本的数据集,以便在新数据集遭到破坏的情况下可以回滚到先前的数据集

(如果事实能证明添加到其中的新数据都是离群值)。备份数据集还可以针对任何先前的数据集来评估任何模型。

2.9 练习题

使用本章的房屋数据集完下面的练习题:

  1. 使用不同的超参数,如kernel=“linear”(具有C超参数的多种值)或kernel=“rbf”(C超参数和gamma超参数的多种值),尝试一个支持向量机回归器(sklearn.svm.SVR),不用担心现在不知道这些超参数的含义。最好的SVR预测器是如何工作的?

  2. 尝试用RandomizedSearchCV替换GridSearchCV。

  3. 尝试在准备流水线中添加一个转换器,从而只选出最重要的属性。

  4. 尝试创建一个覆盖完整的数据准备和最终预测的流水线。

  5. 使用GridSearchCV自动探索一些准备选项。

以上练习题的解决方案可以在Jupyter notebook上获得,链接地址为https://github.com/ageron/handson-ml2。

代码精简汇总(结合练习)

import pandas as pd
import numpy as np
from sklearn.model_selection import StratifiedShuffleSplit
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import OneHotEncoder
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
from sklearn.metrics import mean_absolute_error
from sklearn.metrics import r2_score
from sklearn.model_selection import cross_val_score
from sklearn.tree import DecisionTreeRegressor
from sklearn.ensemble import RandomForestRegressor
import joblib
from sklearn.model_selection import GridSearchCV
from scipy import stats

from sklearn.svm import SVC
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import uniform


# 〇 读取文件
# 文件地址
csv_path = "housing.csv"
# 读取文件
housing = pd.read_csv(csv_path)

# ① 数据集处理部分(划分训练集和测试集)
# 添加一列‘index’索引列,使用行索引作为ID
housing_with_id = housing.reset_index()  # 需要确保在数据集的末尾添加新数据,并且不会删除任何行。
# 为收入中位数划分类别。用pd.cut()来创建5个收入类别属性的(用1~5来做标签),0~1.5是类别1,1.5~3是类别2,以此类推:
housing["income_cat"] = pd.cut(housing["median_income"],
                               bins=[0.0, 1.5, 3.0, 4.5, 6.0, np.inf], labels=[1, 2, 3, 4, 5])  # np.inf是无穷大
# 根据收入类别进行分层抽样了,划分数据集。使用Scikit-Learn的StratifiedShuffleSplit类(分层随机切分交叉验证器):
split = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=42)  # 设置分组抽样切分数据集的参数
# n_splits重新打乱和切分迭代的次数。test_size和train_size的大小只用设置1个,另一个默认是其补集。
for train_index, test_index in split.split(housing, housing["income_cat"]):  # 生成索引以将数据分为训练集和测试集。
    strat_train_set = housing.loc[train_index]
    strat_test_set = housing.loc[test_index]
# 删除income_cat属性,将数据恢复原样:
for set_ in (strat_train_set, strat_test_set):
    set_.drop("income_cat", axis=1, inplace=True)
# 存入数据值(特征值)
housing = strat_train_set.drop("median_house_value", axis=1)  # drop将预测值列和标签分开
# 存入预测值(即要预测的值)
housing_labels = strat_train_set["median_house_value"].copy()


# ② 数据预处理部分(此部分无用,全集合在第③部分里了)
# 划分数据属性和文本属性:
housing_num = housing.drop("ocean_proximity", axis=1)  # 仅有数据属性的数据集
housing_cat = housing[["ocean_proximity"]]  # 仅有文本属性的数据集
# 1.对于数据属性缺失值:
imputer = SimpleImputer(strategy="median")  # 创建一个SimpleImputer估算器,将数据型缺失值转换为该属性的中位数
housing_num_fill = imputer.fit_transform(housing_num)  # fit适配该数据集的估算器,transform应用该估算器
# 2.对于文本属性:
# 首先判断它是分类属性,可以转化为数字标签。同时,为了避免数字之间相似性影响到原本标签之间的相似性,可以采用Onehot二进制编码.
cat_encoder = OneHotEncoder()  # 创建一个OneHotEncoder编码器,将分类属性转换为onehot向量
housing_cat_1hot = cat_encoder.fit_transform(housing_cat)  # fit适配该数据集的编码器,transform应用该编码器
# 3.特征值缩放(标准化):
scaler = StandardScaler()
housing_try_by_myself = scaler.fit_transform(housing_num_fill)
# 4.自定义一个CombinedAttributesAdder转换器(用sklearn的类组建自己的新类)


class CombinedAttributesAdder(BaseEstimator, TransformerMixin):  # 添加TransformerMixin和BaseEstimator作为基类
    def __init__(self, add_bedrooms_per_room=True):  # no *args or **kargs
        self.add_bedrooms_per_room = add_bedrooms_per_room

    def fit(self, x, y=None):
        return self  # nothing else to do

    def transform(self, x):
        rooms_per_household = x[:, rooms_ix] / x[:, households_ix]
        population_per_household = x[:, population_ix] / x[:, households_ix]
        if self.add_bedrooms_per_room:
            bedrooms_per_room = x[:, bedrooms_ix] / x[:, rooms_ix]
            return np.c_[x, rooms_per_household, population_per_household, bedrooms_per_room]
        else:
            return np.c_[x, rooms_per_household, population_per_household]


rooms_ix, bedrooms_ix, population_ix, households_ix = 3, 4, 5, 6  # 定义类的初始值


# ③ 将上述步骤转化成一个可以随时调用的流水线:
# 1. 设置 数值属性的pipeline (# 命名,转换器/估算器/编码器):
num_pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy="median")),  # 将数据型缺失值转换为该属性的中位数
    ('attribs_adder', CombinedAttributesAdder()),  # 调用自己创建的转换器
    ('std_scaler', StandardScaler())])  # 标准化缩放
# 2.引入ColumnTransformer独立地转换输入的不同列或列子集,将多个转换组合成单个转换器(应用于数组或pandas的DataFrame的列)
# 提供列名数据
num_attribs = list(housing_num)  # 数值属性列名的列表
cat_attribs = ["ocean_proximity"]  # 文本属性列名的列表
# 构造1个ColumnTransformer需要一个元组列表,包含:1个名字、1个转换器,以及1个该转换器能够应用的列名字(或索引)的列表。
full_pipeline = ColumnTransformer([
    ("num", num_pipeline, num_attribs),
    ("cat", OneHotEncoder(), cat_attribs),
])  # 定义1个集合转换器

housing_prepared = full_pipeline.fit_transform(housing)  # 对训练数据应用数据处理流程


# ④ 选择和训练模型
# PS:定义一个查看评分的函数
def display_scores(scores):
    print("Scores:", scores)  # 看交叉验证的一组的值
    print("Mean:", scores.mean())  # 取交叉验证的评分的均值
    print("Standard deviation:", scores.std())  # 看标准差
    scores_write = str(scores)
    scores_mean_write = str(scores.mean())
    scores_std_write = str(scores.std())
    with open('2.2预测结果.txt', 'a') as file:
        file.write('\n' + '本部分结果如下:')
        file.write('\n' + "Scores:" + scores_write)  # 写入交叉验证的一组的值
        file.write('\n' + "Mean:" + scores_mean_write)  # 写入交叉验证的评分的均值
        file.write('\n' + "Standard deviation:" + scores_std_write)  # 写入标准差
        file.close()


# 1.使用LinearRegression训练一个线性回归模型:
# first part 训练模型:
lin_reg = LinearRegression()  # 定义1个(含有特定参数的)线性回归模型(本例中没有设置参数)
lin_reg.fit(housing_prepared, housing_labels)  # 用训练集训练这个模型
# second part 模型应用:
housing_predictions = lin_reg.predict(housing_prepared)  # 带入数据进行预测
# third part 结果评估:
# 回归模型的评价指标有:MSE(均方误差),RMSE(均方根误差),MAE(平均绝对误差)、R-Squared(拟合度,三种误差的综合度量)
lin_mse = mean_squared_error(housing_labels, housing_predictions)  # 用mean_squared_error测量回归模型的MSE
lin_rmse = np.sqrt(lin_mse)  # 开根号,得到RMSE值
lin_mae = mean_absolute_error(housing_labels, housing_predictions)  # 用mean_absolute_error测量回归模型的MAE
lin_r_2 = r2_score(housing_labels, housing_predictions)  # 用r2_score测量回归模型的R-Squared
# 也可交叉验证(KFold-折交叉验证功能)后,再来评估
lin_cross_mse = cross_val_score(lin_reg, housing_prepared, housing_labels, scoring="neg_mean_squared_error", cv=10)
# 模型,数据,预测值,scoring选择评分的指标(此处是‘-MSE’),cv用来确定交叉验证的切分策略,数值默认是指定KFold的折数
# 此处得到一组-MSE值
lin_cross_rmse = np.sqrt(-lin_cross_mse)  # 得到一组RMSE值
print('\n\n LinearRegression预测结果如下:\n')
display_scores(lin_cross_rmse)  # 调用自定义函数,观察评分的均值和标准差


# 2.使用DecisionTreeRegressor训练一个决策树模型
# first part 训练模型:
tree_reg = DecisionTreeRegressor()  # 定义1个(含有特定参数的)决策树模型(本例中没有设置参数)
tree_reg.fit(housing_prepared, housing_labels)  # 用训练集训练这个模型
# second part 模型应用:
housing_predictions = tree_reg.predict(housing_prepared)  # 带入数据进行预测
# third part 结果评估:
tree_mse = mean_squared_error(housing_labels, housing_predictions)  # 用mean_squared_error测量回归模型的MSE
tree_rmse = np.sqrt(tree_mse)  # 开根号,得到RMSE值
# 同样可以用交叉验证后的RMSE均值和标准差 进行评估
tree_cross_mse = cross_val_score(tree_reg, housing_prepared, housing_labels, scoring="neg_mean_squared_error", cv=10)
# 得到一组-MSE值
tree_cross_rmse = np.sqrt(-tree_cross_mse)  # 得到一组RMSE值
print('\n\n DecisionTreeRegressor预测结果如下:\n')
display_scores(tree_cross_rmse)  # 调用自定义函数,观察评分的均值和标准差


# 3.使用RandomForestRegressor训练一个随机森林模型(进行许多个决策树的训练,然后对其预测取平均):
# first part 训练模型:
forest_reg = RandomForestRegressor()  # 定义1个(含有特定参数的)随机森林模型(本例中没有设置参数)
forest_reg.fit(housing_prepared, housing_labels)  # 训练该模型(就是用数据去fit这个模型)
# second part 模型应用:
housing_predictions = forest_reg.predict(housing_prepared)  # 带入数据进行预测
# third part 结果评估:
forest_mse = mean_squared_error(housing_labels, housing_predictions)  # 算MSE
forest_rmse = np.sqrt(forest_mse)  # 算RMSE
forest_cross_mse = cross_val_score(forest_reg, housing_prepared, housing_labels, scoring="neg_mean_squared_error", cv=10)
# 交叉验证,得到一组-MSE值
forest_rmse_scores = np.sqrt(-forest_cross_mse)  # 得到一组RMSE值
print('\n\n RandomForestRegressor预测结果如下:\n')
display_scores(forest_rmse_scores)  # 调用自定义函数,观察评分的均值和标准差


# 4.保存每一个尝试过的模型
joblib.dump(lin_reg, filename='book_lin_reg_example.m')  # 将模型lin_reg存入lin_reg_example_from_book文件
joblib.dump(tree_reg, filename='book_tree_reg_example.m')  # 将模型tree_reg存入tree_reg_example_from_book文件
joblib.dump(forest_reg, filename='book_forest_reg_example.m')  # 将模型forest_reg存入forest_reg_example_from_book文件
# 调用保存的模型(需要的时候可以调用):my_model_loaded = joblib.load('lin_reg_example_from_book.m')
# 调用保存的模型(需要的时候可以调用):my_model_loaded = joblib.load('tree_reg_example_from_book.m')
# 调用保存的模型(需要的时候可以调用):my_model_loaded = joblib.load('forest_reg_example_from_book.m')


# ⑤ 微调模型(现在有了一些有效模型的候选列表。现在你需要对它们进行微调)
# 1.方法一:网格搜索GridSearchCV(提供要实验的参数及其值,使用交叉验证来评估所有组合)
# first 设置微调
param_grid = [{'n_estimators': [3, 10, 30], 'max_features': [2, 4, 6, 8]},
              {'bootstrap': [False], 'n_estimators': [3, 10], 'max_features': [2, 3, 4]}]  # 提供参数及其值
forest_reg = RandomForestRegressor()  # 定义1个随机森林模型
grid_search = GridSearchCV(forest_reg, param_grid, cv=5, scoring='neg_mean_squared_error', return_train_score=True)  # 定义1个GridSearchCV微调模型
# second 应用微调
grid_search.fit(housing_prepared, housing_labels)  # 把数据带入微调模型
# third 结果展示
print('\n\n GridSearchCV随机森林调优结果如下:\n')
print(grid_search.best_params_)  # 显示最优的参数组合
print(grid_search.best_estimator_)  # 显示最好的估算器(随机森林模型在这个训练集上最优的参数)
cvres = grid_search.cv_results_  # 将所有训练的结果存进cvres
for mean_score, params in zip(cvres["mean_test_score"], cvres["params"]):  # 循环读取每一个组合的模型得分
    print(np.sqrt(-mean_score), params)  # 显示每个参数组合下的RMSE得分
feature_importances = grid_search.best_estimator_.feature_importances_  # 显示每个属性的相对重要程度:
print(feature_importances)  # 此处仅有数据中各个属性/特征的重要性分数,没有属性名称
extra_attribs = ["rooms_per_hhold", "pop_per_hhold", "bedrooms_per_room"]  # 将这些重要性分数显示在对应的属性名称旁边:
cat_encoder = full_pipeline.named_transformers_["cat"]
cat_one_hot_attribs = list(cat_encoder.categories_[0])
attributes = num_attribs + extra_attribs + cat_one_hot_attribs
print(sorted(zip(feature_importances, attributes), reverse=True))

# 2.方法二:随机搜索RandomizedSearchCV(在每次迭代中为每个超参数选择一个随机值,然后对一定数量的随机组合进行评估)
#   通过简单地设置迭代次数,可以更好地控制要分配给超参数搜索的计算预算。
# svm_reg = SVC()
# param_random = {'C': np.linspace(0.1, 10, 10), 'gamma': np.linspace(1, 0.01, 10)}
# randomized_search = RandomizedSearchCV(svm_reg, param_distributions=param_random, cv=4)
# randomized_search.fit(housing_prepared, housing_labels)
# random_cvres = randomized_search.cv_results_
# for mean_score, params in zip(random_cvres["mean_test_score"], random_cvres["params"]):
#     print(np.sqrt(-mean_score), params)
# PS: 以上内容输出有问题:“The least populated class in y has only 1 members, which is less than n_splits=4”,建议之后回头看


# ⑥ 用测试集评估模型(从测试集中获取预测器和标签,运行full_pipeline来转换数据,然后在测试集上评估最终模型)
final_model = grid_search.best_estimator_  # 调用微调后的最佳模型
x_test = strat_test_set.drop("median_house_value", axis=1)  # 提取测试集的特征列
y_test = strat_test_set["median_house_value"].copy()  # 提取测试集的预测列
x_test_prepared = full_pipeline.transform(x_test)  # 用full_pipeline转换测试集的数据
final_predictions = final_model.predict(x_test_prepared)  # 把测试集数据带入微调模型进行预测
final_mse = mean_squared_error(y_test, final_predictions)  # 评估MSE
final_rmse = np.sqrt(final_mse)  # 评估RMSE
# 使用scipy.stats.t.interval()计算泛化误差的95%置信区间
confidence = 0.95
squared_errors = (final_predictions - y_test) ** 2
intern = np.sqrt(stats.t.interval(confidence, len(squared_errors)-1, loc=squared_errors.mean(), scale=stats.sem(squared_errors)))
print('\n\n 随机森林预测测试集置信区间如下:\n', intern)

你可能感兴趣的:(机器学习,机器学习,sklearn,数据分析,回归,python)