机器学习项目入门篇:一个完整的机器学习项目

本部分,我们会作为被一家地产公司雇佣的数据科学家,完整地学习一个项目。下面是主要步骤:

  1. 项目概述。
  2. 获取数据。
  3. 发现并可视化数据,发现规律。
  4. 为机器学习算法准备数据。
  5. 选择模型,进行训练。
  6. 微调模型。
  7. 给出解决方案。
  8. 部署、监控、维护系统。

一、项目概述

任务是利用加州普查数据,建立一个加州房价模型。这个数据包含每个分区组的人口、收入中位数、房价中位数等指标。分区组是美国调查局发布样本数据的最小地理单位(一个分区通常有600到3000人)。我们将其简称为“分区”。你的模型要利用这个数据进行学习,然后根据其它指标,预测任何分区的的房价中位数。

1.划定问题

首先,你需要划定问题:监督或非监督,还是强化学习?这是个分类任务、回归任务,还是其它的?要使用批量学习还是线上学习?

答:这是一个典型的监督学习任务,因为你要使用的是有标签的训练样本(每个实例都有预定的产出,即分区的房价中位数)。并且,这是一个典型的回归任务,因为你要预测一个值。讲的更细些,这是一个多变量回归问题,因为系统要使用多个变量进行预测(要使用分区的人口,收入中位数等等)。最后,没有连续的数据流进入系统,没有特别需求需要对数据变动作出快速适应。数据量不大可以放到内存中,因此批量学习就够了。

2.选择性能指标

下一步是选择性能指标。回归问题的典型指标是均方根误差(RMSE)。均方根误差测量的是系统预测误差的标准差。计算RMSE的计算方法如下:

机器学习项目入门篇:一个完整的机器学习项目

虽然大多数时候RMSE是回归任务可靠的性能指标,在有些情况下,你可能需要另外的函数。例如,假设存在许多异常的分区。此时,你可能需要使用绝对平均误差(Mean Absolute Error,也称作平均绝对偏差。MAE的计算如下:

机器学习项目入门篇:一个完整的机器学习项目

二、获取数据

1.创建工作环境

我的工作环境是Jupyter notebook,使用python3.0版本,安装Python模块:Jupyter、NumPy、Pandas、Matplotlib和Scikit-Learn。

2.下载数据

import os
import tarfile
from six.moves import urllib

DOWNLOAD_ROOT = "https://raw.githubusercontent.com/ageron/handson-ml/master/"
HOUSING_PATH = "datasets/housing"
HOUSING_URL = DOWNLOAD_ROOT + HOUSING_PATH + "/housing.tgz"

def fetch_housing_data(housing_url=HOUSING_URL, housing_path=HOUSING_PATH):
    if not os.path.isdir(housing_path):
        os.makedirs(housing_path)
    tgz_path = os.path.join(housing_path, "housing.tgz")
    urllib.request.urlretrieve(housing_url, tgz_path)
    housing_tgz = tarfile.open(tgz_path)
    housing_tgz.extractall(path=housing_path)
    housing_tgz.close()

fetch_housing_data()

然后使用Pandas加载数据: 

import pandas as pd

def load_housing_data(housing_path=HOUSING_PATH):
    csv_path = os.path.join(housing_path, "housing.csv")
    return pd.read_csv(csv_path)

3.查看数据结构

使用DataFrame的head()方法查看该数据集的顶部5行。

housing = load_housing_data()
housing.head()

机器学习项目入门篇:一个完整的机器学习项目

每一行都表示一个分区。共有10个属性(截图中可以看到6个):经度、维度、房屋年龄中位数、总房间数、卧室数量、人口数、家庭数、收入中位数、房屋价值中位数、大海距离。

info()方法可以快速查看数据的描述,包括总行数、每个属性的类型和非空值的数量。

housing.info()

机器学习项目入门篇:一个完整的机器学习项目

所有的属性都是数值的,除了大海距离这项。它的类型是对象,因此可以包含任意Python对象,但是因为是从CSV文件加载的,所以必然是文本。当查看顶部的五行时,你可能注意到那一列的值是重复的,意味着它可能是一个分类属性。可以使用value_counts()方法查看都有什么类型,每个类都有多少分区:

housing["ocean_proximity"].value_counts()

 机器学习项目入门篇:一个完整的机器学习项目

describe()方法展示了数值属性的概括:

housing.describe()

机器学习项目入门篇:一个完整的机器学习项目

count、mean、min和max几行的意思很明了。注意,空值被忽略了(所以,卧室总数是20433而不是20640)。std是标准差(揭示数值的分散度)。25%、50%、75%展示了对应的分位数:每个分位数指明小于这个值,且指定分组的百分比。例如,25%的分区的房屋年龄中位数小于18,而50%的小于29,75%的小于37。

另一种快速了解数据类型的方法是画出每个数值属性的柱状图。柱状图(的纵轴)展示了特定范围的实例的个数。你还可以一次给一个属性画图,或对完整数据集调用hist()方法,后者会画出每个数值属性的柱状图。

机器学习项目入门篇:一个完整的机器学习项目


4.创建测试集

纯随机采样测试集

(1)自己编写代码,设置随机数生成器的种子(比如np.random.seed(42)),以产生总是相同的混合指数(shuffled indices)

# to make this notebook's output identical at every run
np.random.seed(42)

import numpy as np

def split_train_test(data, test_ratio):
    shuffled_indices = np.random.permutation(len(data))
    test_set_size = int(len(data) * test_ratio)
    test_indices = shuffled_indices[:test_set_size]
    train_indices = shuffled_indices[test_set_size:]
    return data.iloc[train_indices], data.iloc[test_indices]
train_set, test_set = split_train_test(housing, 0.2)
print(len(train_set), "train +", len(test_set), "test")

 (2)使用hash

import hashlib

def test_set_check(identifier, test_ratio, hash):
    return hash(np.int64(identifier)).digest()[-1] < 256 * test_ratio

def split_train_test_by_id(data, test_ratio, id_column, hash=hashlib.md5):
    ids = data[id_column]
    in_test_set = ids.apply(lambda id_: test_set_check(id_, test_ratio, hash))
    return data.loc[~in_test_set], data.loc[in_test_set]
housing_with_id = housing.reset_index()   # adds an `index` column
train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "index")

 如果使用行索引作为唯一识别码,你需要保证新数据放到现有数据的尾部,且没有行被深处。如果做不到,则可以用最稳定的特征来创建唯一识别码。例如,一个区的维度和经度在几百万年之内是不变的,所以可以将两者结合成一个ID:

housing_with_id["id"] = housing["longitude"] * 1000 + housing["latitude"]
train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "id")

(3)sklearn实现

from sklearn.model_selection import train_test_split

train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)

分层采样的测试集

当你的数据集很大时(尤其是和属性数相比),这通常可行;但如果数据集不大,就会有采样偏差的风险。当一个调查公司想要对1000个人进行调查,它们不是在电话亭里随机选1000个人出来。调查公司要保证这1000个人对人群整体有代表性。例如,美国人口的51.3%是女性,48.7%是男性。所以在美国,严谨的调查需要保证样本也是这个比例:513名女性,487名男性。这称作分层采样(stratified sampling):将人群分成均匀的子分组,称为分层,从每个分层取出合适数量的实例,以保证测试集对总人数有代表性。

假设专家告诉你,收入中位数是预测房价中位数非常重要的属性。你可能想要保证测试集可以代表整体数据集中的多种收入分类。因为收入中位数是一个连续的数值属性,你首先需要创建一个收入分类属性。大多数的收入中位数的值聚集在2-5(一万美元),但是一些收入中位数会超过6。数据集中的每个分层都要有足够的实例位于你的数据中,这点很重要。否则,对分层重要性的评估就会有偏差。这意味着,你不能有过多的分层,且每个分层都要足够大。后面的代码通过将收入中位数除以1.5(以限制收入分类的数量),创建了一个收入分类属性,用ceil对值舍入(以产生离散的分类),然后将所有大于5的分类归入到分类5:

housing["income_cat"] = np.ceil(housing["median_income"] / 1.5)
housing["income_cat"].where(housing["income_cat"] < 5, 5.0, inplace=True)

根据收入分类,使用Scikit-Learn的StratifiedShuffleSplit类进行分层采样:

from sklearn.model_selection import StratifiedShuffleSplit

split = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
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]

 在测试集和完整的房产数据集中查看收入分类比例:

strat_test_set["income_cat"].value_counts() / len(strat_test_set)
housing["income_cat"].value_counts() / len(housing)

机器学习项目入门篇:一个完整的机器学习项目 机器学习项目入门篇:一个完整的机器学习项目

对比了总数据集、分层采样的测试集、纯随机采样测试集的收入分类比例。可以看到,分层采样测试集的收入分类比例与总数据集几乎相同,而随机采样数据集偏差严重。

def income_cat_proportions(data):
    return data["income_cat"].value_counts() / len(data)

train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)

compare_props = pd.DataFrame({
    "Overall": income_cat_proportions(housing),
    "Stratified": income_cat_proportions(strat_test_set),
    "Random": income_cat_proportions(test_set),
}).sort_index()
compare_props["Rand. %error"] = 100 * compare_props["Random"] / compare_props["Overall"] - 100
compare_props["Strat. %error"] = 100 * compare_props["Stratified"] / compare_props["Overall"] - 100

compare_props

机器学习项目入门篇:一个完整的机器学习项目

删除income_cat属性,使数据回到初始状态:

for set_ in (strat_train_set, strat_test_set):
    set_.drop("income_cat", axis=1, inplace=True)

 我们用了大量时间来生成测试集的原因是:测试集通常被忽略,但实际是机器学习非常重要的一部分。还有,生成测试集过程中的许多思路对于后面的交叉验证讨论是非常有帮助的。

三、发现并可视化数据,发现规律

首先,保证你将测试集放在了一旁,只是研究训练集。另外,如果训练集非常大,你可能需要再采样一个探索集,保证操作方便快速。在我们的案例中,数据集很小,所以可以在全集上直接工作。创建一个副本,以免损伤训练集:

housing = strat_train_set.copy()

1.地理数据可视化 

因为存在地理信息(纬度和经度),创建一个所有分区的散点图来数据可视化是一个不错的主意。

housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.1)

机器学习项目入门篇:一个完整的机器学习项目

可以非常清楚地看到高密度区域,湾区、洛杉矶和圣迭戈,以及*谷,特别是从萨克拉门托到弗雷斯诺。现在来看房价,每个圈的半径表示分区的人口(选项s),颜色代表价格(选项c)。我们用预先定义的颜色图(选项cmap)jet,它的范围是从蓝色(低价)到红色(高价): 

housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.4,
    s=housing["population"]/100, label="population",
    c="median_house_value", cmap=plt.get_cmap("jet"), colorbar=True,
)
plt.legend()

机器学习项目入门篇:一个完整的机器学习项目

这张图说明房价和位置(比如,靠海)和人口密度联系密切,这点你可能早就知道。可以使用聚类算法来检测主要的聚集,用一个新的特征值测量聚集中心的距离。海洋距离属性也可能有用,尽管北加州海岸区域的房价并不高,所以这不是一个简单的规则。 

2.查找关联 

(1)因为数据集并不是非常大,你可以很容易地使用corr()方法计算出每对属性间的标准相关系数(也称作皮尔逊相关系数):

corr_matrix = housing.corr()
corr_matrix["median_house_value"].sort_values(ascending=False)

机器学习项目入门篇:一个完整的机器学习项目

相关系数的范围是-1到1。当接近1时,意味强正相关;例如,当收入中位数增加时,房价中位数也会增加。当相关系数接近-1时,意味强负相关;你可以看到,纬度和房价中位数有轻微的负相关性(即,越往北,房价越可能降低)。最后,相关系数接近0,意味没有线性相关性。 

(2)另一种检测属性间相关系数的方法是使用Pandas的scatter_matrix函数,它能画出每个数值属性对每个其它数值属性的图。因为现在共有11个数值属性,你可以得到112=121张图,在一页上画不下,所以只关注几个和房价中位数最有可能相关的属性。

from pandas.tools.plotting import scatter_matrix

attributes = ["median_house_value", "median_income", "total_rooms",
              "housing_median_age"]
scatter_matrix(housing[attributes], figsize=(12, 8))

机器学习项目入门篇:一个完整的机器学习项目

如果pandas将每个变量对自己作图,主对角线(左上到右下)都会是直线图。所以Pandas展示的是每个属性的柱状图(也可以是其它的,请参考Pandas文档)。

最有希望用来预测房价中位数的属性是收入中位数,因此将这张图放大:

housing.plot(kind="scatter", x="median_income", y="median_house_value",
             alpha=0.1)
plt.axis([0, 16, 0, 550000])
save_fig("income_vs_house_value_scatterplot")

 机器学习项目入门篇:一个完整的机器学习项目

这张图说明了几点。首先,相关性非常高;可以清晰地看到向上的趋势,并且数据点不是非常分散。第二,我们之前看到的最高价,清晰地呈现为一条位于500000美元的水平线。这张图也呈现了一些不是那么明显的直线:一条位于450000美元的直线,一条位于350000美元的直线,一条在$280000的线,和一些更靠下的线。你可能希望去除对应的分区,以防止算法重复这些巧合。

 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)

 机器学习项目入门篇:一个完整的机器学习项目

与总房间数或卧室数相比,新的bedrooms_per_room属性与房价中位数的关联更强。显然,卧室数/总房间数的比例越低,房价就越高。每户的房间数也比分区的总房间数的更有信息,很明显,房屋越大,房价就越高。 

四、为机器学习算法准备数据

回到清洗训练集(通过再次复制strat_train_set),将预测量和标签分开,因为我们不想对预测量和目标值应用相同的转换(注意copy()创建了一份数据的备份,而不影响strat_train_set):

housing = strat_train_set.drop("median_house_value", axis=1) # drop labels for training set
housing_labels = strat_train_set["median_house_value"].copy()

1.数据清洗 

大多机器学习算法不能处理特征丢失,因此先创建一些函数来处理特征丢失的问题。前面,你应该注意到了属性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来对训练集进行转换,通过将缺失值替换为中位数:

X = imputer.transform(housing_num)

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

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

2.处理文本和分类属性 

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

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

housing_cat = housing[['ocean_proximity']]
from sklearn.preprocessing import OrdinalEncoder
ordinal_encoder = OrdinalEncoder()
housing_cat_encoded = ordinal_encoder.fit_transform(housing_cat)

查看映射表,编码器是通过属性classes_来学习的(“<1H OCEAN”被映射为0,“INLAND”被映射为1,等等):

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

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

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

cat_encoder = OneHotEncoder(sparse=False)
housing_cat_1hot = cat_encoder.fit_transform(housing_cat)
housing_cat_1hot

机器学习项目入门篇:一个完整的机器学习项目

3.自定义转换量 

尽管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)

4.特征缩放

数据要做的最重要的转换之一是特征缩放。除了个别情况,当输入的数值属性量度不同时,机器学习算法的性能都不会好。这个规律也适用于房产数据:总房间数分布范围是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来进行标准化。

5.转换Pipeline

你已经看到,存在许多数据转换步骤,需要按一定的顺序执行。幸运的是,Scikit-Learn提供了类Pipeline,来进行这一系列的转换。下面是一个数值属性的小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),
    ])
housing_prepared = full_pipeline.fit_transform(housing)
housing_prepared.shape  #(16513, 17)

五、选择模型,进行训练

1.在训练集上训练和评估

(1)线性回归模型

from sklearn.linear_model import LinearRegression

lin_reg = LinearRegression()
lin_reg.fit(housing_prepared, housing_labels)

使用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.19819848922

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

(2)决策树

训练一个DecisionTreeRegressor。这是一个强大的模型,可以发现数据中复杂的非线性关系。代码:

from sklearn.tree import DecisionTreeRegressor

tree_reg = DecisionTreeRegressor(random_state=42)
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

 没有误差?这个模型可能是绝对完美的吗?当然,更大可能性是这个模型严重过拟合数据。

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

评估决策树模型的一种方法是用函数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)
tree_rmse_scores = np.sqrt(-scores)

 决策树的结果:

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

display_scores(tree_rmse_scores)

机器学习项目入门篇:一个完整的机器学习项目

计算线性回归模型的的相同分数 :

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)

机器学习项目入门篇:一个完整的机器学习项目

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

3.尝试新方法:随机森林 

from sklearn.ensemble import RandomForestRegressor

forest_reg = RandomForestRegressor(n_estimators=10, random_state=42)
forest_reg.fit(housing_prepared, housing_labels)
from sklearn.model_selection import cross_val_score

forest_scores = cross_val_score(forest_reg, housing_prepared, housing_labels,
                                scoring="neg_mean_squared_error", cv=10)
forest_rmse_scores = np.sqrt(-forest_scores)
display_scores(forest_rmse_scores)

 机器学习项目入门篇:一个完整的机器学习项目

随机森林看起来很有希望。但是,训练集的评分仍然比验证集的评分低很多。解决过拟合可以通过简化模型,给模型加限制(即,规整化),或用更多的训练数据。 

六、微调模型

1.网格搜索

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

你应该使用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)

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_

机器学习项目入门篇:一个完整的机器学习项目

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

grid_search.best_estimator_

 也可以得到评估值:

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

机器学习项目入门篇:一个完整的机器学习项目

2.随机搜索

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

  • 如果你让随机搜索运行,比如1000次,它会探索每个超参数的1000个不同的值(而不是像网格搜索那样,只搜索每个超参数的几个值)。
  • 你可以方便地通过设定搜索次数,控制超参数搜索的计算量。

3.集成方法

另一种微调系统的方法是将表现最好的模型组合起来。组合(集成)之后的性能通常要比单独的模型要好(就像随机森林要比单独的决策树要好),特别是当单独模型的误差类型不同时。

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

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

feature_importances = grid_search.best_estimator_.feature_importances_

机器学习项目入门篇:一个完整的机器学习项目  

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

extra_attribs = ["rooms_per_hhold", "pop_per_hhold", "bedrooms_per_room"]
#cat_encoder = cat_pipeline.named_steps["cat_encoder"] # old solution
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
sorted(zip(feature_importances, attributes), reverse=True)

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

机器学习项目入门篇:一个完整的机器学习项目

有了这个信息,你就可以丢弃一些不那么重要的特征(比如,显然只要一个分类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)   #47730.22690385927

八、部署、监控、维护系统

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

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

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

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

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

 

完结!

参考书籍《Scikit-Learn与TensorFlow机器学习实用指南》的第02、03章。

完整的代码https://github.com/ageron/handson-ml

上一篇:机器学习一个完整的项目过程


下一篇:刷题-力扣-剑指 Offer II 055. 二叉搜索树迭代器