《Scikit-Learn与TensorFlow机器学习实用指南》 第02章 一个完整的机器学习项目(下)

96
SeanCheney
0.6 2018.09.12 12:39* 字数 7799

(第一部分 机器学习基础)
第01章 机器学习概览
第02章 一个完整的机器学习项目(上)
第02章 一个完整的机器学习项目(下)
第03章 分类
第04章 训练模型
第05章 支持向量机
第06章 决策树
第07章 集成学习和随机森林
第08章 降维
(第二部分 神经网络和深度学习)
第9章 启动和运行TensorFlow
第10章 人工神经网络
第11章 训练深度神经网络(上)
第11章 训练深度神经网络(下)
第12章 设备和服务器上的分布式 TensorFlow
第13章 卷积神经网络
第14章 循环神经网络
第15章 自编码器
第16章 强化学习(上)
第16章 强化学习(下)


数据清洗

大多机器学习算法不能处理特征丢失,因此先创建一些函数来处理特征丢失的问题。前面,你应该注意到了属性total_bedrooms有一些缺失值。有三个解决选项:

  • 去掉对应的分区;

  • 去掉整个属性;

  • 进行赋值(0、平均值、中位数等等)。

用DataFrame的dropna()drop(),和 fillna()方法,可以方便地实现:

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

如果选择选项3,你需要计算训练集的中位数,用中位数填充训练集的缺失值,不要忘记保存该中位数。后面用测试集评估系统时,需要替换测试集中的缺失值,也可以用来实时替换新数据中的缺失值。

Scikit-Learn提供了一个方便的类来处理缺失值:Imputer。下面是其使用方法:首先,需要创建一个Imputer实例,指定用该属性的中位数替换它的每个缺失值:

from sklearn.preprocessing import Imputer

imputer = Imputer(strategy="median")

因为只有数值属性才能算出中位数,我们需要创建一份不包括文本属性ocean_proximity的数据副本:

housing_num = housing.drop("ocean_proximity", axis=1)

现在,就可以用fit()方法将imputer实例拟合到训练数据:

imputer.fit(housing_num)

imputer计算出了每个属性的中位数,并将结果保存在了实例变量statistics_中。只有属性total_bedrooms有缺失值,但是我们要确保一旦系统运行起来,新的数据中没有缺失值,所以安全的做法是将imputer应用到每个数值:

>>> imputer.statistics_
array([ -118.51 , 34.26 , 29. , 2119. , 433. , 1164. , 408. , 3.5414])
>>> housing_num.median().values
array([ -118.51 , 34.26 , 29. , 2119. , 433. , 1164. , 408. , 3.5414])

现在,你就可以使用这个“训练过的”imputer来对训练集进行转换,通过将缺失值替换为中位数:

X = imputer.transform(housing_num)

结果是一个普通的Numpy数组,包含有转换后的特征。如果你想将其放回到Pandas DataFrame中,也很简单:

housing_tr = pd.DataFrame(X, columns=housing_num.columns)

Scikit-Learn设计

Scikit-Learn设计的API设计的非常好。它的主要设计原则是:

  • 一致性:所有对象的接口一致且简单:
    估计量(estimator)。任何可以基于数据集而对一些参数进行估计的对象都被设计成估计量(比如,imputer就是个估计量)。估计本身是通过fit()方法,只需要一个数据集作为参数(对于监督学习算法,需要两个数据集;第二个数据集包含标签)。任何其它用来指导估计过程的参数都被当做超参数(比如imputerstrategy),并且超参数要被设置成实例变量(通常是通过构造器参数)。
    转换量(transformer)。一些估计量(比如imputer)也可以转换数据集,这些估计量被称为转换量。API也是相当简单:转换是通过transform()方法,被转换的数据集作为参数。返回的是经过转换的数据集。转换过程依赖学习到的参数,比如imputer的例子。所有的转换都有一个便捷的方法fit_transform(),等同于调用fit()transform()(但有时fit_transform()经过优化,运行的更快)。
    预测量(predictor)。最后,一些估计量可以根据给出的数据集做预测,这些估计量称为预测量。例如,上一章的LinearRegression模型就是一个预测量:它根据一个国家的人均GDP预测生活满意度。预测量有一个predict()方法,可以用新实例的数据集做出相应的预测。预测量还有一个score()方法,可以根据测试集(和相应的标签,如果是监督学习算法的话)对预测进行衡量。

  • 可检验。所有估计量的超参数都可以通过公共实例变量直接访问(比如,imputer.strategy),并且所有估计量学习到的参数也可以通过公共实例变量添加下划线后缀访问(比如,imputer.statistics_)。

  • 类不可扩散。数据集被表示成NumPy数组或SciPy稀疏矩阵,而不是自制的类。超参数只是普通的Python字符串或数字。

  • 可组合。尽可能使用现存的模块。例如,用任意的转换量序列加上一个估计量,就可以做成一个Pipeline,后面会看到例子。

  • 合理的默认值。Scikit-Learn给大多数参数提供了合理的默认值,很容易就能创建一个系统。

处理文本和分类属性

前面,我们丢弃了分类属性ocean_proximity,因为它是一个文本属性,不能计算出中位数。大多数机器学习算法更喜欢和数字打交道,所以让我们把这些文本标签转换为数字。

Scikit-Learn为这个任务提供了一个转换量LabelEncoder

>>> from sklearn.preprocessing import LabelEncoder
>>> encoder = LabelEncoder()
>>> housing_cat = housing["ocean_proximity"]
>>> housing_cat_encoded = encoder.fit_transform(housing_cat)
>>> housing_cat_encoded
array([1, 1, 4, ..., 1, 0, 3])

好了一些,现在就可以在任何ML算法里用这个数值数据了。你可以查看映射表,编码器是通过属性classes_来学习的(“<1H OCEAN”被映射为0,“INLAND”被映射为1,等等):

>>> print(encoder.classes_)
['<1H OCEAN' 'INLAND' 'ISLAND' 'NEAR BAY' 'NEAR OCEAN']

这种做法的问题是,ML算法会认为两个临近的值比两个疏远的值要更相似。显然这样不对(比如,分类0和4比0和1更相似)。要解决这个问题,一个常见的方法是给每个分类创建一个二元属性:当分类是“<1H OCEAN”,该属性为1(否则为0),当分类是“INLAND”,另一个属性等于1(否则为0),以此类推。这称作独热编码(One-Hot Encoding),因为只有一个属性会等于1(热),其余会是0(冷)。

Scikit-Learn提供了一个编码器OneHotEncoder,用于将整书分类值转变为独热矢量。注意fit_transform()用于2D数组,而housing_cat_encoded是一个1D数组,所以需要将其变形:

>>> from sklearn.preprocessing import OneHotEncoder
>>> encoder = OneHotEncoder()
>>> housing_cat_1hot = encoder.fit_transform(housing_cat_encoded.reshape(-1,1))
>>> housing_cat_1hot
<16513x5 sparse matrix of type '<class 'numpy.float64'>'
    with 16513 stored elements in Compressed Sparse Row format>

注意输出结果是一个SciPy稀疏矩阵,而不是NumPy数组。当分类属性有数千个分类时,这样非常有用。经过独热编码,我们得到了一个有数千列的矩阵,这个矩阵每行只有一个1,其余都是0。使用大量内存来存储这些0非常浪费,所以稀疏矩阵只存储非零元素的位置。你可以像一个2D数据那样进行使用,但是如果你真的想将其转变成一个(密集的)NumPy数组,只需调用toarray()方法:

>>> housing_cat_1hot.toarray()
array([[ 0.,  1.,  0.,  0.,  0.],
       [ 0.,  1.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  1.],
       ...,
       [ 0.,  1.,  0.,  0.,  0.],
       [ 1.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  1.,  0.]])

使用类LabelBinarizer,我们可以用一步执行这两个转换(从文本分类到整数分类,再从整数分类到独热矢量):

>>> from sklearn.preprocessing import LabelBinarizer
>>> encoder = LabelBinarizer()
>>> housing_cat_1hot = encoder.fit_transform(housing_cat)
>>> housing_cat_1hot
array([[0, 1, 0, 0, 0],
       [0, 1, 0, 0, 0],
       [0, 0, 0, 0, 1],
       ...,
       [0, 1, 0, 0, 0],
       [1, 0, 0, 0, 0],
       [0, 0, 0, 1, 0]])

注意默认返回的结果是一个密集NumPy数组。向构造器LabelBinarizer传递sparse_output=True,就可以得到一个稀疏矩阵。

自定义转换量

尽管Scikit-Learn提供了许多有用的转换量,你还是需要自己动手写转换量执行任务,比如自定义的清理操作,或属性组合。你需要让自制的转换量与Scikit-Learn组件(比如pipeline)无缝衔接工作,因为Scikit-Learn是依赖鸭子类型的(而不是继承),你所需要做的是创建一个类并执行三个方法:fit()(返回self),transform(),和fit_transform()。通过添加TransformerMixin作为基类,可以很容易地得到最后一个。另外,如果你添加BaseEstimator作为基类(且构造器中避免使用*args**kargs),你就能得到两个额外的方法(get_params()set_params()),二者可以方便地进行超参数自动微调。例如,一个小转换量类添加了上面讨论的属性:

from sklearn.base import BaseEstimator, TransformerMixin
rooms_ix, bedrooms_ix, population_ix, household_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, y=None):
        rooms_per_household = X[:, rooms_ix] / X[:, household_ix]
        population_per_household = X[:, population_ix] / X[:, household_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)

在这个例子中,转换量有一个超参数add_bedrooms_per_room,默认设为True(提供一个合理的默认值很有帮助)。这个超参数可以让你方便地发现添加了这个属性是否对机器学习算法有帮助。更一般地,你可以为每个不能完全确保的数据准备步骤添加一个超参数。数据准备步骤越自动化,可以自动化的操作组合就越多,越容易发现更好用的组合(并能节省大量时间)。

特征缩放

数据要做的最重要的转换之一是特征缩放。除了个别情况,当输入的数值属性量度不同时,机器学习算法的性能都不会好。这个规律也适用于房产数据:总房间数分布范围是6到39320,而收入中位数只分布在0到15。不需要对目标值进行缩放。

有两种常见的方法可以让所有的属性有相同的量度:线性函数归一化(Min-Max scaling)和标准化(standardization)。

线性函数归一化(许多人称其为归一化(normalization))很简单:值被转变、重新缩放,直到范围变成0到1。我们通过减去最小值,然后再除以最大值与最小值的差值,来进行归一化。Scikit-Learn提供了一个转换量MinMaxScaler来实现这个功能。它有一个超参数feature_range,可以让你改变范围,如果不希望范围是0到1。

标准化就很不同:首先减去平均值(所以标准化值的平均值总是0),然后除以方差,使得到的分布具有单位方差。与归一化不同,标准化不会限定值到某个特定的范围,这对某些算法可能构成问题(比如,神经网络常需要输入值得范围是0到1)。但是,标准化受到异常值的影响很小。例如,假设一个分区的收入中位数是100。归一化会将其它范围是0到15的值变为0-0.15,但是标准化不会受什么影响。Scikit-Learn提供了一个转换量StandardScaler来进行标准化。

警告:与所有的转换一样,缩放器只能向训练集拟合,而不是向完整的数据集(包括测试集)。只有这样,才能用缩放器转换训练集和测试集(和新数据)。

转换Pipeline

你已经看到,存在许多数据转换步骤,需要按一定的顺序执行。幸运的是,Scikit-Learn提供了类Pipeline,来进行这一系列的转换。下面是一个数值属性的小pipeline:

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

num_pipeline = Pipeline([
        ('imputer', Imputer(strategy="median")),
        ('attribs_adder', CombinedAttributesAdder()),
        ('std_scaler', StandardScaler()),
        ])

housing_num_tr = num_pipeline.fit_transform(housing_num)

Pipeline构造器需要一个定义步骤顺序的名字/估计量对的列表。除了最后一个估计量,其余都要是转换量(即,它们都要有fit_transform()方法)。名字可以随意起。

当你调用pipeline的fit()方法,就会对所有转换量顺序调用fit_transform()方法,将每次调用的输出作为参数传递给下一个调用,一直到最后一个评估量,它只执行fit()方法。

pipeline暴露相同的方法作为最终的评估量。在这个例子中,最后的评估量是一个StandardScaler,它是一个转换量,因此这个pipeline有一个transform()方法,可以顺序对数据做所有转换(它还有一个fit_transform方法可以使用,就不必先调用fit()再进行transform())。

你现在就有了一个对数值的pipeline,你还需要对分类值应用LabelBinarizer:如何将这些转换写成一个pipeline呢?Scikit-Learn提供了一个类FeatureUnion实现这个功能。你给它一列转换量(可以是所有的转换量),当调用它的transform()方法,每个转换量的transform()会被并行执行,等待输出,然后将输出合并起来,并返回结果(当然,调用它的fit()方法就会调用每个转换量的fit())。一个完整的处理数值和分类属性的pipeline如下所示:

from sklearn.pipeline import FeatureUnion

num_attribs = list(housing_num)
cat_attribs = ["ocean_proximity"]

num_pipeline = Pipeline([
        ('selector', DataFrameSelector(num_attribs)),
        ('imputer', Imputer(strategy="median")),
        ('attribs_adder', CombinedAttributesAdder()),
('std_scaler', StandardScaler()),
    ])

cat_pipeline = Pipeline([
        ('selector', DataFrameSelector(cat_attribs)),
        ('label_binarizer', LabelBinarizer()),
    ])

full_pipeline = FeatureUnion(transformer_list=[
        ("num_pipeline", num_pipeline),
        ("cat_pipeline", cat_pipeline),
    ])

你可以很简单地运行整个pipeline:

>>> housing_prepared = full_pipeline.fit_transform(housing)
>>> housing_prepared
array([[ 0.73225807, -0.67331551,  0.58426443, ...,  0.        ,
         0.        ,  0.        ],
       [-0.99102923,  1.63234656, -0.92655887, ...,  0.        ,
         0.        ,  0.        ],
       [...]
>>> housing_prepared.shape
(16513, 17)

每个子pipeline都以一个选择转换量开始:通过选择对应的属性(数值或分类)、丢弃其它的,来转换数据,并将输出DataFrame转变成一个NumPy数组。Scikit-Learn没有工具来处理Pandas DataFrame,因此我们需要写一个简单的自定义转换量来做这项工作:

from sklearn.base import BaseEstimator, TransformerMixin

class DataFrameSelector(BaseEstimator, TransformerMixin):
    def __init__(self, attribute_names):
        self.attribute_names = attribute_names
    def fit(self, X, y=None):
        return self
    def transform(self, X):
        return X[self.attribute_names].values

选择并训练模型

可到这一步了!你在前面限定了问题、获得了数据、探索了数据、采样了一个测试集、写了自动化的转换pipeline来清理和为算法准备数据。现在,你已经准备好选择并训练一个机器学习模型了。

在训练集上训练和评估

好消息是基于前面的工作,接下来要做的比你想的要简单许多。像前一章那样,我们先来训练一个线性回归模型:

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:\t", lin_reg.predict(some_data_prepared))
Predictions:     [ 303104.   44800.  308928.  294208.  368704.]
>>> print("Labels:\t\t", list(some_labels))
Labels:      [359400.0, 69700.0, 302100.0, 301300.0, 351900.0]

行的通,尽管预测并不怎么准确(比如,第二个预测偏离了50%!)。让我们使用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)
>>> lin_rmse
68628.413493824875

okay,有总比没有强,但显然结果并不好:大多数分区的median_housing_values位于120000美元到265000美元之间,因此预测误差$68628不能让人满意。这是一个模型欠拟合训练数据的例子。当这种情况发生时,意味着特征没有提供足够多的信息来做出一个好的预测,或者模型并不强大。就像前一章看到的,修复欠拟合的主要方法是选择一个更强大的模型,给训练算法提供更好的特征,或去掉模型上的限制。这个模型还没有正则化,所以排除了最后一个选项。你可以尝试添加更多特征(比如,人口的对数值),但是首先让我们尝试一个更为复杂的模型,看看效果。

来训练一个DecisionTreeRegressor。这是一个强大的模型,可以发现数据中复杂的非线性关系(决策树会在第6章详细讲解)。代码看起来很熟悉:

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)
>>> tree_rmse
0.0

等一下,发生了什么?没有误差?这个模型可能是绝对完美的吗?当然,更大可能性是这个模型严重过拟合数据。如何确定呢?如前所述,直到你准备运行一个具备足够信心的模型,都不要碰测试集,因此你需要使用训练集的部分数据来做训练,用一部分来做模型验证。

使用交叉验证做更佳的评估

评估决策树模型的一种方法是用函数train_test_split来分割训练集,得到一个更小的训练集和一个验证集,然后用更小的训练集来训练模型,用验证集来评估。这需要一定工作量,并不难而且也可行。

另一种更好的方法是使用Scikit-Learn的交叉验证功能。下面的代码采用了K折交叉验证(K-fold cross-validation):它随机地将训练集分成十个不同的子集,成为“折”,然后训练评估决策树模型10次,每次选一个不用的折来做评估,用其它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)
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)
Scores: [ 74678.4916885   64766.2398337   69632.86942005  69166.67693232
          71486.76507766  73321.65695983  71860.04741226  71086.32691692
          76934.2726093   69060.93319262]
Mean: 71199.4280043
Standard deviation: 3202.70522793

现在决策树就不像前面看起来那么好了。实际上,它看起来比线性回归模型还糟!注意到交叉验证不仅可以让你得到模型性能的评估,还能测量评估的准确性(即,它的标准差)。决策树的评分大约是71200,通常波动有±3200。如果只有一个验证集,就得不到这些信息。但是交叉验证的代价是训练了模型多次,不可能总是这样。

让我们计算下线性回归模型的的相同分数,以做确保:

>>> 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)
Scores: [ 70423.5893262   65804.84913139  66620.84314068  72510.11362141
          66414.74423281  71958.89083606  67624.90198297  67825.36117664
          72512.36533141  68028.11688067]
Mean: 68972.377566
Standard deviation: 2493.98819069

判断没错:决策树模型过拟合很严重,它的性能比线性回归模型还差。

现在再尝试最后一个模型:RandomForestRegressor。第7章我们会看到,随机森林是通过用特征的随机子集训练许多决策树。在其它多个模型之上建立模型成为集成学习(Ensemble Learning),它是推进ML算法的一种好方法。我们会跳过大部分的代码,因为代码本质上和其它模型一样:

>>> from sklearn.ensemble import RandomForestRegressor
>>> forest_reg = RandomForestRegressor()
>>> forest_reg.fit(housing_prepared, housing_labels)
>>> [...]
>>> forest_rmse
22542.396440343684
>>> display_scores(forest_rmse_scores)
Scores: [ 53789.2879722   50256.19806622  52521.55342602  53237.44937943
          52428.82176158  55854.61222549  52158.02291609  50093.66125649
          53240.80406125  52761.50852822]
Mean: 52634.1919593
Standard deviation: 1576.20472269

现在好多了:随机森林看起来很有希望。但是,训练集的评分仍然比验证集的评分低很多。解决过拟合可以通过简化模型,给模型加限制(即,规整化),或用更多的训练数据。在深入随机森林之前,你应该尝试下机器学习算法的其它类型模型(不同核心的支持向量机,神经网络,等等),不要在调节超参数上花费太多时间。目标是列出一个可能模型的列表(两到五个)。

提示:你要保存每个试验过的模型,以便后续可以再用。要确保有超参数和训练参数,以及交叉验证评分,和实际的预测值。这可以让你比较不同类型模型的评分,还可以比较误差种类。你可以用Python的模块pickle,非常方便地保存Scikit-Learn模型,或使用sklearn.externals.joblib,后者序列化大NumPy数组更有效率:

from sklearn.externals import joblib

joblib.dump(my_model, "my_model.pkl")
# 然后
my_model_loaded = joblib.load("my_model.pkl")

模型微调

假设你现在有了一个列表,列表里有几个有希望的模型。你现在需要对它们进行微调。让我们来看几种微调的方法。

网格搜索

微调的一种方法是手工调整超参数,直到找到一个好的超参数组合。这么做的话会非常冗长,你也可能没有时间探索多种组合。

你应该使用Scikit-Learn的GridSearchCV来做这项搜索工作。你所需要做的是告诉GridSearchCV要试验有哪些超参数,要试验什么值,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')

grid_search.fit(housing_prepared, housing_labels)

当你不能确定超参数该有什么值,一个简单的方法是尝试连续的10的次方(如果想要一个粒度更小的搜寻,可以用更小的数,就像在这个例子中对超参数n_estimators做的)。

param_grid告诉Scikit-Learn首先评估所有的列在第一个dict中的n_estimatorsmax_features的3 × 4 = 12种组合(不用担心这些超参数的含义,会在第7章中解释)。然后尝试第二个dict中超参数的2 × 3 = 6种组合,这次会将超参数bootstrap设为False而不是True(后者是该超参数的默认值)。

总之,网格搜索会探索12 + 6 = 18种RandomForestRegressor的超参数组合,会训练每个模型五次(因为用的是五折交叉验证)。换句话说,训练总共有18 × 5 = 90轮!折将要花费大量时间,完成后,你就能获得参数的最佳组合,如下所示:

>>> grid_search.best_params_
{'max_features': 6, 'n_estimators': 30}

提示:因为30是n_estimators的最大值,你也应该估计更高的值,因为这个值会持续提升。

你还能直接得到最佳的估计量:

>>> grid_search.best_estimator_
RandomForestRegressor(bootstrap=True, criterion='mse', max_depth=None,
           max_features=6, max_leaf_nodes=None, min_samples_leaf=1,
           min_samples_split=2, min_weight_fraction_leaf=0.0,
           n_estimators=30, n_jobs=1, oob_score=False, random_state=None,
           verbose=0, warm_start=False)

注意:如果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)
...
64912.0351358 {'max_features': 2, 'n_estimators': 3}
55535.2786524 {'max_features': 2, 'n_estimators': 10}
52940.2696165 {'max_features': 2, 'n_estimators': 30}
60384.0908354 {'max_features': 4, 'n_estimators': 3}
52709.9199934 {'max_features': 4, 'n_estimators': 10}
50503.5985321 {'max_features': 4, 'n_estimators': 30}
59058.1153485 {'max_features': 6, 'n_estimators': 3}
52172.0292957 {'max_features': 6, 'n_estimators': 10}
49958.9555932 {'max_features': 6, 'n_estimators': 30}
59122.260006 {'max_features': 8, 'n_estimators': 3}
52441.5896087 {'max_features': 8, 'n_estimators': 10}
50041.4899416 {'max_features': 8, 'n_estimators': 30}
62371.1221202 {'bootstrap': False, 'max_features': 2, 'n_estimators': 3}
54572.2557534 {'bootstrap': False, 'max_features': 2, 'n_estimators': 10}
59634.0533132 {'bootstrap': False, 'max_features': 3, 'n_estimators': 3}
52456.0883904 {'bootstrap': False, 'max_features': 3, 'n_estimators': 10}
58825.665239 {'bootstrap': False, 'max_features': 4, 'n_estimators': 3}
52012.9945396 {'bootstrap': False, 'max_features': 4, 'n_estimators': 10}

在这个例子中,我们通过设定超参数max_features为6,n_estimators为30,得到了最佳方案。对这个组合,RMSE的值是49959,这比之前使用默认的超参数的值(52634)要稍微好一些。祝贺你,你成功地微调了最佳模型!

提示:不要忘记,你可以像超参数一样处理数据准备的步骤。例如,网格搜索可以自动判断是否添加一个你不确定的特征(比如,使用转换量CombinedAttributesAdder的超参数add_bedrooms_per_room)。它还能用相似的方法来自动找到处理异常值、缺失特征、特征选择等任务的最佳方法。

随机搜索

当探索相对较少的组合时,就像前面的例子,网格搜索还可以。但是当超参数的搜索空间很大时,最好使用RandomizedSearchCV。这个类的使用方法和类GridSearchCV很相似,但它不是尝试所有可能的组合,而是通过选择每个超参数的一个随机值的特定数量的随机组合。这个方法有两个优点:

  • 如果你让随机搜索运行,比如1000次,它会探索每个超参数的1000个不同的值(而不是像网格搜索那样,只搜索每个超参数的几个值)。

  • 你可以方便地通过设定搜索次数,控制超参数搜索的计算量。

集成方法

另一种微调系统的方法是将表现最好的模型组合起来。组合(集成)之后的性能通常要比单独的模型要好(就像随机森林要比单独的决策树要好),特别是当单独模型的误差类型不同时。我们会在第7章更深入地讲解这点。

分析最佳模型和它们的误差

通过分析最佳模型,常常可以获得对问题更深的了解。比如,RandomForestRegressor可以指出每个属性对于做出准确预测的相对重要性:

>>> feature_importances = grid_search.best_estimator_.feature_importances_
>>> feature_importances
array([  7.14156423e-02,   6.76139189e-02,   4.44260894e-02,
         1.66308583e-02,   1.66076861e-02,   1.82402545e-02,
         1.63458761e-02,   3.26497987e-01,   6.04365775e-02,
         1.13055290e-01,   7.79324766e-02,   1.12166442e-02,
         1.53344918e-01,   8.41308969e-05,   2.68483884e-03,
         3.46681181e-03])

将重要性分数和属性名放到一起:

>>> extra_attribs = ["rooms_per_hhold", "pop_per_hhold", "bedrooms_per_room"]
>>> cat_one_hot_attribs = list(encoder.classes_)
>>> attributes = num_attribs + extra_attribs + cat_one_hot_attribs
>>> sorted(zip(feature_importances,attributes), reverse=True)
[(0.32649798665134971, 'median_income'),
 (0.15334491760305854, 'INLAND'),
 (0.11305529021187399, 'pop_per_hhold'),
 (0.07793247662544775, 'bedrooms_per_room'),
 (0.071415642259275158, 'longitude'),
 (0.067613918945568688, 'latitude'),
 (0.060436577499703222, 'rooms_per_hhold'),
 (0.04442608939578685, 'housing_median_age'),
 (0.018240254462909437, 'population'),
 (0.01663085833886218, 'total_rooms'),
 (0.016607686091288865, 'total_bedrooms'),
 (0.016345876147580776, 'households'),
 (0.011216644219017424, '<1H OCEAN'),
 (0.0034668118081117387, 'NEAR OCEAN'),
 (0.0026848388432755429, 'NEAR BAY'),
 (8.4130896890070617e-05, 'ISLAND')]

有了这个信息,你就可以丢弃一些不那么重要的特征(比如,显然只要一个分类ocean_proximity就够了,所以可以丢弃掉其它的)。

你还应该看一下系统犯的误差,搞清为什么会有些误差,以及如何改正问题(添加更多的特征,或相反,去掉没有什么信息的特征,清洗异常值等等)。

用测试集评估系统

调节完系统之后,你终于有了一个性能足够好的系统。现在就可以用测试集评估最后的模型了。这个过程没有什么特殊的:从测试集得到预测值和标签,运行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 48,209.6

评估结果通常要比交叉验证的效果差一点,如果你之前做过很多超参数微调(因为你的系统在验证集上微调,得到了不错的性能,通常不会在未知的数据集上有同样好的效果)。这个例子不属于这种情况,但是当发生这种情况时,你一定要忍住不要调节超参数,使测试集的效果变好;这样的提升不能推广到新数据上。

然后就是项目的预上线阶段:你需要展示你的方案(重点说明学到了什么、做了什么、没做什么、做过什么假设、系统的限制是什么,等等),记录下所有事情,用漂亮的图表和容易记住的表达(比如,“收入中位数是房价最重要的预测量”)做一次精彩的展示。

启动、监控、维护系统

很好,你被允许启动系统了!你需要为实际生产做好准备,特别是接入输入数据源,并编写测试。

你还需要编写监控代码,以固定间隔检测系统的实时表现,当发生下降时触发报警。这对于捕获突然的系统崩溃和性能下降十分重要。做监控很常见,是因为模型会随着数据的演化而性能下降,除非模型用新数据定期训练。

评估系统的表现需要对预测值采样并进行评估。这通常需要人来分析。分析者可能是领域专家,或者是众包平台(比如Amazon Mechanical Turk 或 CrowdFlower)的工人。不管采用哪种方法,你都需要将人工评估的pipeline植入系统。

你还要评估系统输入数据的质量。有时因为低质量的信号(比如失灵的传感器发送随机值,或另一个团队的输出停滞),系统的表现会逐渐变差,但可能需要一段时间,系统的表现才能下降到一定程度,触发警报。如果监测了系统的输入,你就可能尽量早的发现问题。对于线上学习系统,监测输入数据是非常重要的。

最后,你可能想定期用新数据训练模型。你应该尽可能自动化这个过程。如果不这么做,非常有可能你需要每隔至少六个月更新模型,系统的表现就会产生严重波动。如果你的系统是一个线上学习系统,你需要定期保存系统状态快照,好能方便地回滚到之前的工作状态。

实践!

希望这一章能告诉你机器学习项目是什么样的,你能用学到的工具训练一个好系统。你已经看到,大部分的工作是数据准备步骤、搭建监测工具、建立人为评估pipeline和自动化定期模型训练,当然,最好能了解整个过程、熟悉三或四种算法,而不是在探索高级算法上浪费全部时间,导致在全局上的时间不够。

因此,如果你还没这样做,现在最好拿起台电脑,选择一个感兴趣的数据集,将整个流程从头到尾完成一遍。一个不错的着手开始的地点是竞赛网站,比如http://kaggle.com/:你会得到一个数据集,一个目标,以及分享经验的人。

练习

使用本章的房产数据集:

  1. 尝试一个支持向量机回归器(sklearn.svm.SVR),使用多个超参数,比如kernel="linear"(多个超参数C值)。现在不用担心这些超参数是什么含义。最佳的SVR预测表现如何?
  2. 尝试用RandomizedSearchCV替换GridSearchCV
  3. 尝试在准备pipeline中添加一个只选择最重要属性的转换器。
  4. 尝试创建一个单独的可以完成数据准备和最终预测的pipeline。
  5. 使用GridSearchCV自动探索一些准备过程中的候选项。
    练习题答案可以在线上的Jupyter notebooks(https://github.com/ageron/handson-ml)找到。

(第一部分 机器学习基础)
第01章 机器学习概览
第02章 一个完整的机器学习项目(上)
第02章 一个完整的机器学习项目(下)
第03章 分类
第04章 训练模型
第05章 支持向量机
第06章 决策树
第07章 集成学习和随机森林
第08章 降维
(第二部分 神经网络和深度学习)
第9章 启动和运行TensorFlow
第10章 人工神经网络
第11章 训练深度神经网络(上)
第11章 训练深度神经网络(下)
第12章 设备和服务器上的分布式 TensorFlow
第13章 卷积神经网络
第14章 循环神经网络
第15章 自编码器
第16章 强化学习(上)
第16章 强化学习(下)


Gupao