从一个小实例了解机器学习全过程(《机器学习实战》笔记)

从一个小实例了解机器学习全过程

  • 准备工作
    • 明确目标
    • 选择性能指标
    • 查看数据结构
  • 创建测试集
    • 编写自定义函数(随机抽样)以完成测试集的创建
    • 使用train_test_split函数(随机抽样)完成测试集的创建
    • 使用StratifiedShuffleSplit函数(分层抽样)完成数据集的创建
  • 通过数据可视化得到信息
    • 将地理数据可视化
    • 相关性
    • 组合不同的属性
  • 数据处理
    • 数据的清理
    • 处理文本和分类属性
    • 自定义一个转换器
    • 特征缩放
    • 转换流水线
  • 选择和训练模型
    • 培训和评估训练集
    • 交叉验证
  • 微调模型——网格搜索
  • 一段完整的代码

我们通过一个机器学习的小实例来对机器学习技术进行一个初步了解。

准备工作

明确目标

该实例的目的在于通过1990年加州房子的各种特征(如经纬度、该地区收入中位数等)来预测该地区房子价格的中位数。数据集已经上传至文章的附件中(包含地区的房价中位数),很明显这是一个监督式学习任务,并且是一个回归任务。

选择性能指标

回归问题的典型性能指标是均方根误差(RMSE),它测量的是预测过程中,预测错误的标准偏差,也是下面我们使用的性能指标。下面是标准差公式:
R S E M ( X , h ) = 1 m ∑ i = 1 n ( h ( x ( i ) ) − y ( i ) ) RSEM(\bf{X},\it{h})=\sqrt{\frac{1}{m}\sum_{i=1}^{n}(h(\bf{x^{(\it{i})}})-\it{y}^{(i)})} RSEM(X,h)=m1i=1n(h(x(i))y(i))
其中 x ( i ) \bf{x^{(\it{i})}} x(i)表示特征值向量, h ( x ( i ) ) h(\bf{x^{(\it{i})}}) h(x(i))表示得到的预测值, y ( i ) y^{(i)} y(i)表示真实值。
另外我们介绍另一种性能指标:平均绝对误差(MAE),该指标适合具有较多离群数据的情况,见下面的公式:
M A E ( X , h ) = 1 m ∑ i = 1 n ∣ h ( x ( i ) ) − y ( i ) ∣ MAE(\bf{X},\it{h})=\frac{1}{m}\sum_{i=1}^{n}|h(\bf{x}^{\it(i)})-\it{y}^{(i)}| MAE(X,h)=m1i=1nh(x(i))y(i)
均方根误差和平均绝对误差都可以理解为两个向量之间的距离:预测向量和目标向量。距离或者范数的测度可能有多种:

  • 计算平方和的根(RSEM)对应欧几里得范数,成为 l 2 l_2 l2范数,记为 ∣ ∣ ⋅ ∣ ∣ 2 ||·||_{2} 2或者 ∣ ∣ ⋅ ∣ ∣ ||·||
  • 计算绝对值的总和(MAE)对应 l 1 l_1 l1范数,记为 ∣ ∣ ⋅ ∣ ∣ 1 ||·||_1 1,也成为曼哈顿距离。
  • 包含n个元素的向量 v k \bf{v}_{\it{k}} vk的范数可以定义为 ∣ ∣ v ∣ ∣ k = ( ∣ v 0 ∣ k + ∣ v 1 ∣ k + ⋯ + ∣ v n ∣ k ) 1 k ||\bf{v}||_{\it{k}}=\it(|v_0|^k+|v_1|^k+\cdots+|v_n|^k)^{\frac{1}{k}} vk=(v0k+v1k++vnk)k1
  • 范数指数越高,则越关注大的价值,忽略小的价值。这就是为什么RMSE比MAE对异常值更加敏感。但是当异常值稀少的时候(如钟形曲线),RSEM的表现更加优异,通常作为首选。

查看数据结构

所有代码运行在jupyter notebook中,我们首先使用pandas中的read_csv函数将数据集读入,之后再使用head方法查看数据的前5行,使用info方法查看数据集的简单描述:

import pandas as pd
housing = pd.read_csv("datasets/housing/housing.csv")
housing.info()
# 
# RangeIndex: 20640 entries, 0 to 20639
# Data columns (total 10 columns):
# longitude             20640 non-null float64
# latitude              20640 non-null float64
# housing_median_age    20640 non-null float64
# total_rooms           20640 non-null float64
# total_bedrooms        20433 non-null float64
# population            20640 non-null float64
# households            20640 non-null float64
# median_income         20640 non-null float64
# median_house_value    20640 non-null float64
# ocean_proximity       20640 non-null object
# dtypes: float64(9), object(1)
# memory usage: 1.6+ MB
housing.head()

从一个小实例了解机器学习全过程(《机器学习实战》笔记)_第1张图片
从表中可以看出,其一共有10个属性,依次是经度、纬度、房屋年龄中位数、房间总数、卧室总数、该地区的人口、家庭数、收入中位数、房价中位数(目标)、是否近海。另外其他属性都是数字,除了是否近海这一属性是文本属性,因此我们可以使用value_counts方法来查看该属性所包含的所有种类以及数量:

housing["ocean_proximity"].value_counts()
# <1H OCEAN     9136
# INLAND        6551
# NEAR OCEAN    2658
# NEAR BAY      2290
# ISLAND           5
# Name: ocean_proximity, dtype: int64

我们也可以通过调用hist方法使用直方图的形式来查看数据的分布:

import matplotlib.pyplot as plt
housing.hist(bins=50,figsize=(20,15))

从一个小实例了解机器学习全过程(《机器学习实战》笔记)_第2张图片
从图中可以看出,对于房价中位数(标签),超过50万美元的数据非常多,对于房屋年龄也有相同的情况发生,这是因为这两个数据被设定了一个上限,若样本的数据超过了上限,则将其取为上限值,面对这种情况通常有两种选择:

  1. 对于超过上限的地区重新进行数据的收集。
  2. 将超过上限的地区的数据删除(包括训练集和数据集)。

另外对于数据还有以下说明:

  • 各类特征都被进行过不同程度的缩放。
  • 数据表现出了重尾:图形在中位数右侧的延伸比左侧要远得多。这种形式的数据是不利于检测的,需要进行一些转化将其变为钟形分布。

创建测试集

如果使用人工来选择测试集和训练集,很可能会无意识的按某种模式选择数据,使得之后训练的时候结果过于乐观而模型在测试集上的性能较差,称之为数据窥探偏误
一般情况下,测试集占总数据量的20%,下面介绍几种创建测试集的方法。

编写自定义函数(随机抽样)以完成测试集的创建

这意味着我们需要手动编写一个函数来完成测试集的创建,代码如下:

import numpy as np
def split_train_test(data,test_ratio):
    """
    data为传入的数据集
    test_ratio为测试集的比例(0~1)
    将训练集和测试集以DataFrame的形式返回
    """
    np.random.seed(42)
    #保证每次选取的测试集不变
    shuffled_indices = np.random.permutation(len(data))
    #np.random.permutation(len(data))函数用于生成一个长度为传入数据长度的随机数列
    #即从0到len(data)的随机排列
    test_set_size = int(len(data) * test_ratio)
    test_indice = shuffled_indices[:test_set_size]
    train_indece = shuffled_indices[test_set_size:]
    return data.iloc[train_indece],data.iloc[test_indice]

train_set,test_set = split_train_test(housing,0.2)
print("tranin:",len(train_set),",test:",len(test_set))
# tranin: 16512 ,test: 4128

使用train_test_split函数(随机抽样)完成测试集的创建

代码如下:

from sklearn.model_selection import train_test_split
train_set,test_set = train_test_split(housing,test_size=0.2,random_state=42)
#random_state相当于随机数种子
print("tranin:",len(train_set),",test:",len(test_set))
# tranin: 16512 ,test: 4128

使用StratifiedShuffleSplit函数(分层抽样)完成数据集的创建

在这里我们按收入的中位数来进行分层抽样,在进行分层抽样之前,我们需要将收入中位数从较为连续的数据变为更加离散的数据(分层),代码如下:

housing["income_cat"] = np.ceil(housing["median_income"]/1.5)
#新建一列income_cat,由median_income这一列的数据除以1.5然后取整得到(分层)
housing["income_cat"].where(housing["income_cat"] < 5,5.0,inplace=True)
#where(housing["income_cat"] < 5,5.0,inplace=True)意思是若该列的数据小于5则不发生变化
#将超过5的数据幅值为5
housing["income_cat"].value_counts()
#每一层的数据
# 3.0    7236
# 2.0    6581
# 4.0    3639
# 5.0    2362
# 1.0     822
# Name: income_cat, dtype: int64
housing["income_cat"].value_counts()/len(housing)
#每层数据占比
# 3.0    0.350581
# 2.0    0.318847
# 4.0    0.176308
# 5.0    0.114438
# 1.0    0.039826
# Name: income_cat, dtype: float64
housing["income_cat"].value_counts().plot.bar()
#数据可视化

从一个小实例了解机器学习全过程(《机器学习实战》笔记)_第3张图片
之后我们就可以导入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]
print("tranin:",len(train_set),",test:",len(test_set))
# tranin: 16512 ,test: 4128

最后删除income_cat属性,将数据恢复原样:

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

之后我们使用的就是由该方法得到的测试集和训练集。

通过数据可视化得到信息

在使用可视化手段对训练集探索之前,我们需要创建一份副本,以保证之后的操作不会损坏训练集:

housing = strat_train_set.copy()

将地理数据可视化

由于我们具有经度和纬度,我们可以使用散点图来进行地理数据的可视化:

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)
# alpha代表透明度,设置透明度以得到密度信息;s代表点的半径,使用该地区的人口来表示,人口越多半径越大;
# 将房价中位数用颜色表示出来,cmap=plt.get_cmap("jet")为预定义的颜色表,房价数值越大,则越偏向红色

从一个小实例了解机器学习全过程(《机器学习实战》笔记)_第4张图片
从这张图可以看出来,地理位置和人口密度息息相关。

相关性

我们可以使用corr方法来计算每对属性之间标准相关系数(皮尔逊相关系数):

corr_matrix = housing.corr()
corr_matrix["median_house_value"].sort_values(ascending=False)
# median_house_value    1.000000
# median_income         0.688075
# income_cat            0.643892
# total_rooms           0.134153
# housing_median_age    0.105623
# households            0.065843
# total_bedrooms        0.049686
# population           -0.024650
# longitude            -0.045967
# latitude             -0.144160
# Name: median_house_value, dtype: float64

相关系数范围是-1~1,为负时代表负相关,为正则代表正相关,其绝对值越大则相关性越强。但是这里的相关性指的是线性相关性,因此即使相关系数为0,它们之间也有可能有着强烈的关联。
另外使用pandas中的scatter_matrix函数也可以查看特征之间的相关性,在此我们只查看部分特征:

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

从一个小实例了解机器学习全过程(《机器学习实战》笔记)_第5张图片
这张图片矩阵的对角线上的图片代表数据的分布直方图,其他地方的图片则代表两个属性的散点图。通过收入中位数和房价中位数之间的散点图(第一行左数第二个和第二行左数第一个)我们可以发现二者具有比较强的相关性(线性性)。另外我们发现50万元的价格上限处在图中有一条明显的水平线,在45/35和28万元处也有类似的情况出现。为了让我们训练的模型具有更好的性能,可以删除相应的数据。

组合不同的属性

我们可以将看似无关的特征进行组合,来获得与相关性更大的特征,比如地区每个家庭的房间数、每个家庭的平均人数以及卧室占房间数的比例:

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)
# median_house_value          1.000000
# median_income               0.687160
# rooms_per_household         0.146285
# total_rooms                 0.135097
# housing_median_age          0.114110
# households                  0.064506
# total_bedrooms              0.047689
# population_per_household   -0.021985
# population                 -0.026920
# longitude                  -0.047432
# latitude                   -0.142724
# bedrooms_per_room          -0.259984
# Name: median_house_value, dtype: float64

可以看出我们获得的新属性和房价中位数具有更高的相关性。

数据处理

在进行数据处理之前,我们首先要从训练集中提取出训练数据和对应的标签:

housing = strat_train_set.drop("median_house_value",axis=1)
housing_labels = strat_train_set["median_house_value"].copy()

数据的清理

之前我们就注意到,数据集中total_bedroom属性有部分值缺失,对于缺失值我们一般由三种手段进行处理:

  • 放弃含有缺失值的样本(删除行)
  • 放弃这个属性(删除列)
  • 将缺失值设置为某个数(如0、平均数或者中位数)

相关的代码如下:

housing.dropna(subset=["total_bedrooms"])
#删除行
housing.drop("total_bedrooms",axis=1)
#删除列
median = housing["total_bedrooms"].median()
housing["total_bedrooms"].fillna(median)
#使用中位数代替

当然我们还可以使用sklearn中的SimpleImputer(书中为Imputer)来轻松的完成缺失值的处理,但是由于其只可以处理数值,所以在使用前需要先去掉它的文本属性:

from sklearn.impute import SimpleImputer
imputer = SimpleImputer(strategy="median")
#将缺失值替换为中位数
housing_num = housing.drop("ocean_proximity",axis=1)
imputer.fit(housing_num)
#适配至训练集
X = imputer.transform(housing_num)
#以Numpy数组的形式返回
housing_tr = pd.DataFrame(X,columns=housing_num.columns)
#转化为DataFrame
housing_tr.head()

处理文本和分类属性

对于文本属性ocean_proximity,我们需要将其转化为数字。我们可以使用sklearn中的转化器LabelEncoder将其转化为数字数列,并查看映射:

from sklearn.preprocessing import LabelEncoder
encoder = LabelEncoder()
housing_cat = housing["ocean_proximity"]
housing_cat_encoded = encoder.fit_transform(housing_cat)
# array([0, 0, 4, ..., 1, 0, 3])
encoder.classes_
# array(['<1H OCEAN', 'INLAND', 'ISLAND', 'NEAR BAY', 'NEAR OCEAN'],
#       dtype=object)

这种编码方式产生的问题是,机器学习算法会认为两个相近的数字比两个离得较远的数字更为相近,然而事实并非如此。为了避免这种情况,我们可以对其使用**one hot(独热)**编码,我们可以使用sklearn提供的OneHotEncoder编码器来将上面的编码转化为one hot编码,这里需要注意的是,其输入应该是一个二维数组,因此在进行转化之前应该将其进行重塑:

from sklearn.preprocessing import OneHotEncoder
encoder = OneHotEncoder()
#实例化转换器对象
housing_cat_1hot = encoder.fit_transform(housing_cat_encoded.reshape(-1,1))
#首先将数组从(1,?)变为(?,1),输出为(?,5)大小的稀疏矩阵

我们也可以使用LabelBinarizer来一次性完成两个转换:

from sklearn.preprocessing import LabelBinarizer
encoder = LabelBinarizer()
one_hot_code = encoder.fit_transform(housing_cat)
#先适配后转换
print(one_hot_code)
# [[1 0 0 0 0]
#  [1 0 0 0 0]
#  [0 0 0 0 1]
#  ...
#  [0 1 0 0 0]
#  [1 0 0 0 0]
#  [0 0 0 1 0]]

自定义一个转换器

之前我们介绍了多个转换器,比如LabelEncoder、OneHotEncoder,我们也可以自定义一个转换器,来完成我们想要的数据的转换,另外所有自定义的转换器都必须有三个方法:fit()(用于返回自身)、transform()(转换数据)
#fit_transform()(相当于先执行fit后执行transform)。比如我们可以自定义一个CombineAttributesAdder,用于将我们之前地区每个家庭的房间数、每个家庭的平均人数以及卧室占房间数的比例加入到输入的数据集中,并以Numpy数组的形式返回:

housing = strat_train_set.drop("median_house_value",axis = 1)
housing_labels = strat_train_set["median_house_value"].copy()
housing_num = housing.drop("ocean_proximity",axis=1)
#重写一遍
from sklearn.base import BaseEstimator,TransformerMixin

room_ix,bedrooms_ix,population_ix,household_ix = 3, 4, 5, 6
#全局变量,代表了所需要的数据的列索引
class CombineAttributesAdder(BaseEstimator,TransformerMixin):
    """
    继承TransformerMixin可以直接获得fit_transform方法,
    继承BaseEstimator则可以获得set_params()方法和get_params()方法(用于调参)
    但是继承BaseEstimator则必须在构造函数中避免*args和**kargs
    """
    def __init__(self,add_bedrooms_per_room = True):
        #add_bedrooms_per_room用于判断是否添加人均卧室数
        self.add_bedrooms_per_room = add_bedrooms_per_room
    def fit(self,X,y=None):
        return self
        #返回自己,这个转换器不需要适配
    def transform(self,X,y=None):
        rooms_per_household = X[:,room_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[:,room_ix]
            return np.c_[X,rooms_per_household,population_per_household,
                        bedrooms_per_room]
            #np.c_[a,b]代表将两个Numpy数组进行横向拼接
        else:
            return np.c_[X,rooms_per_household,population_per_household]

attrs_adder = CombineAttributesAdder(add_bedrooms_per_room=False)
housing_extra_attribs = attrs_adder.transform(housing.values)
print(housing_extra_attribs)
# [[-121.89 37.29 38.0 ... '<1H OCEAN' 4.625368731563422 2.094395280235988]
#  [-121.93 37.05 14.0 ... '<1H OCEAN' 6.008849557522124 2.7079646017699117]
#  [-117.2 32.77 31.0 ... 'NEAR OCEAN' 4.225108225108225 2.0259740259740258]
#  ...
#  [-116.4 34.09 9.0 ... 'INLAND' 6.34640522875817 2.742483660130719]
#  [-118.01 33.82 31.0 ... '<1H OCEAN' 5.50561797752809 3.808988764044944]
#  [-122.45 37.77 52.0 ... 'NEAR BAY' 4.843505477308295 1.9859154929577465]]

特征缩放

如果输入的数值属性具有较大比例的差异,往往会导致性能不佳,因此我们需要对所有属性进行一个缩放,常用的方法有最大最小缩放以及标准化
最大最小缩放也称为归一化,即将每一个值除以最大值与最小值的差,可以使用sklearn提供的MinMaxScaler缩放器实现。
标准化则是将所有值减去平均值,之后除以方差,该方法的确定是数值的输出不在0~1之间,优点是不容易受到异常值的影响。可以使用sklearn提供的StrandadScaler缩放器实现。
另外缩放器只用在训练集上适配,在测试集上直接使用transform转换即可。

转换流水线

一般情况下数据需要经过多个数据转换步骤,需要用到转换器、缩放器等,我们可以将他们组装成一个流水线,这样就可以实现一步完成数据的转换。sklearn提供了Pipeline来实现这样的转换,比如我们之前提到的对数值属性的处理:

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
#以列表的形式输入,元素以元组的形式给出,左边是自定义的名字,右边是转换器的实例化对象
num_pipeline = Pipeline([
    ("imputer",SimpleImputer(strategy="median")),
    #用于处理缺失值
    ("attribs_adder",CombineAttributesAdder()),
    #用于添加新的特征
    ("std_scaler",StandardScaler())
    #用于标准化
])

housing_num_tr = num_pipeline.fit_transform(housing_num)

流水线的前端都必须是转换器,而末端则可以是转换器或者估算器。当调用流水线的fit()方法时,会依次调用流水线上的fit_transform()方法,将其输出作为下一个转化器的输入,传递到末端若是一个估算器则只会执行fit()方法。当然其也有transform和fit_transform方法,与之前介绍的类似。

现在我们有了一个处理数值属性的流水线,同样的我们需要一个处理文本属性的流水线,最后我们可以使用sklearn提供的Feature将两个流水线组合,它会自动将两个流水线的结果进行合并,其也有transform(),fit(),fit_transform()方法,在此不再介绍。一个完整的处理数值和文本属性的流水线如下:

class DataFrameSelector(BaseEstimator,TransformerMixin):
    """
    自定义的转换器,用于选取指定的特征
    """
    def __init__(self,list):
        self.list = list
    def fit(self,X,y=None):
        return self
    def transform(self,X,y=None):
        return X[self.list].values
        

class MyLabelBinarizer(BaseEstimator,TransformerMixin):
    """
    避免版本的原因导致报错,重写独热编码
    """
    def __init__(self):
        self.encoder = LabelBinarizer()
    def fit(self,x,y=None):
        return self.encoder.fit(x)
    def transform(self,X):
        return self.encoder.transform(X)
    

from sklearn.pipeline import FeatureUnion


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

num_pipeline = Pipeline([
    ("selector",DataFrameSelector(num_attribs)),
    ("imputer",SimpleImputer(strategy="median")),
    ("attribs_adder",CombineAttributesAdder(True)),
    ("std_scaler",StandardScaler())
])

cat_pipeline = Pipeline([
    ("selector",DataFrameSelector(cat_attribs)),
    ("label_binarizer",MyLabelBinarizer())
])

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

housing_prepared = full_pipeline.fit_transform(housing)
#以Numpy数组的形式返回

选择和训练模型

现在一切准备就绪,接下来可以选择模型来对其进行训练了!

培训和评估训练集

首先我们选择线性模型,对其进行训练并测试其在训练集上的预测精度:

from sklearn.linear_model import LinearRegression
housing_prepared = pd.DataFrame(housing_prepared)
lin_reg =LinearRegression()
lin_reg.fit(housing_prepared,housing_labels)
some_labels = housing_labels.iloc[:5]
some_data_prepared = housing_prepared.iloc[:5]
print(lin_reg.predict(some_data_prepared))
#展示在训练集上的预测
# [210644.60459286 317768.80697211 210956.43331178  59218.98886849
#  189747.55849879]
print(some_labels.values)
#实际结果
# [286600. 340600. 196900.  46300. 254500.]

我们还可以查看其标准差:

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)
# 68628.19819848923

当然我们可以使用决策时作为模型:

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)
# 0

可以看出其出现了过拟合,我们无法再使用训练集本身来对其精度进行检测,为了避免这样的情况我们可以使用下面讲的交叉验证。

交叉验证

使用交叉验证我们可以更好地对模型进行一个评估,sklearn中的cross_val_score可以实现K-折(K-fold)交叉验证。它可以将训练集分为若干个子集,每个子集成为一个折叠,每次使用9个折叠进行训练,一个折叠进行评估,然后返回10次评估分数的数组:

from sklearn.model_selection import cross_val_score
tree_reg = DecisionTreeRegressor()
#重新定义一个决策树
scores = cross_val_score(tree_reg,housing_prepared,housing_labels,
                        scoring="neg_mean_squared_error",cv=10)
#分别代表训练的
rmse_scores = np.sqrt(-scores)
#输出的scores是负的MSE,因此开方之前要加上负号
def display_scores(scores):
    print("Scores:",scores)
    print("Mean:",scores.mean())
    print("Std:",scores.std())
display_scores(rmse_scores)
# Scores: [68208.23482526 67301.92441039 70025.00684877 68169.35934599
#  70672.37635862 75701.81736785 72127.09981248 70889.7643626
#  76965.39542913 68993.49537748]
# Mean: 70905.44741385692
# Std: 3056.99828621384

经过验证可以看出这个模型甚至比线性模型更差,这也验证了决策树确实发生了过度拟合。
最后我们试试随机森林的工作效果:

from sklearn.ensemble import RandomForestRegressor
forest_reg = RandomForestRegressor()
scores = cross_val_score(forest_reg,housing_prepared,housing_labels,
                        scoring="neg_mean_squared_error",cv=10)
rmse_scores = np.sqrt(-scores)
display_scores(rmse_scores)
# Scores: [49450.10757967 47533.72637867 49623.96297177 52320.85986332
#  49496.07056891 53397.26384609 48467.34041513 48013.9060821
#  52857.82678771 49995.90266225]
# Mean: 50115.69671556102
# Std: 1950.2557781752575

该模型具有一个更低的平均标准差,因此我们选择此模型来解决我们的问题。

微调模型——网格搜索

现在选择好了模型,最后一步就是调整参数,使得其性能最佳。
使用sklearn的GridSearchCV可以帮助你尝试所有想要的超参数组合,将其使用交叉验证来评估性能。比如:

from sklearn.model_selection import GridSearchCV
param_grid = [
    {"n_estimators":[3,10,30],"max_features":[2,4,6,8]},

    #第一组参数组合,n_estimators共有3种,max_features共有4种,共有12种组合

    {"bootstrap":[False],"n_estimators":[3,10],"max_features":[2,3,4]}

    #第二组参数组合,bootstrap为假,n_estimators2种值,max_features3中值,共有6种组合
    
]
new_forest_reg = RandomForestRegressor()
grid_search = GridSearchCV(new_forest_reg,param_grid,cv=5,
                          scoring="neg_mean_squared_error")
grid_search.fit(housing_prepared,housing_labels)

我们可以查看最佳的组合,也可以得到其中最好的估算器还有评估分数:

print(grid_search.best_params_)
# {'max_features': 6, 'n_estimators': 30}
print(grid_search.best_estimator_)
# RandomForestRegressor(bootstrap=True, ccp_alpha=0.0, criterion='mse',
#                       max_depth=None, max_features=6, max_leaf_nodes=None,
#                       max_samples=None, min_impurity_decrease=0.0,
#                       min_impurity_split=None, min_samples_leaf=1,
#                       min_samples_split=2, min_weight_fraction_leaf=0.0,
#                       n_estimators=30, n_jobs=None, oob_score=False,
#                       random_state=None, verbose=0, warm_start=False)
cvres = grid_search.cv_results_
for mean_score,params in zip(cvres["mean_test_score"],cvres["params"]):
    print(np.sqrt(-mean_score),params)
# 63937.89227277836 {'max_features': 2, 'n_estimators': 3}
# 55116.59507601997 {'max_features': 2, 'n_estimators': 10}
# 52989.34015374805 {'max_features': 2, 'n_estimators': 30}
# 60152.96447010605 {'max_features': 4, 'n_estimators': 3}
# 52777.040456991184 {'max_features': 4, 'n_estimators': 10}
# 50490.81020927522 {'max_features': 4, 'n_estimators': 30}
# 59813.49472570128 {'max_features': 6, 'n_estimators': 3}
# 51773.74480116615 {'max_features': 6, 'n_estimators': 10}
# 50079.06184466367 {'max_features': 6, 'n_estimators': 30}
# 59276.94681942987 {'max_features': 8, 'n_estimators': 3}
# 51465.06643740047 {'max_features': 8, 'n_estimators': 10}
# 49853.330162163606 {'max_features': 8, 'n_estimators': 30}
# 63345.65014079787 {'bootstrap': False, 'max_features': 2, 'n_estimators': 3}
# 54048.395802778825 {'bootstrap': False, 'max_features': 2, 'n_estimators': 10}
# 59988.171618591914 {'bootstrap': False, 'max_features': 3, 'n_estimators': 3}
# 52538.79645679762 {'bootstrap': False, 'max_features': 3, 'n_estimators': 10}
# 58597.08236751539 {'bootstrap': False, 'max_features': 4, 'n_estimators': 3}
# 51516.91358105093 {'bootstrap': False, 'max_features': 4, 'n_estimators': 10}

可以看出max_features为6,n_estimators为30的时候具有一个最好的性能。最后我们查看各个特征的重要程度:

feature_importances = grid_search.best_estimator_.feature_importances_
extra_attribs = ["rooms_per_hhold","pop_per_hhold","bedrooms_per_room"]
cat_ont_hot_attribs = list(encoder.classes_)
attributes = num_attribs + extra_attribs + cat_ont_hot_attribs
sorted(zip(feature_importances,attributes),reverse=True)
# [(0.30684961643167274, 'median_income'),
#  (0.15842808826772728, 'INLAND'),
#  (0.10806079656618511, 'pop_per_hhold'),
#  (0.0857846818443435, 'bedrooms_per_room'),
#  (0.07984197666267197, 'longitude'),
#  (0.0682767104147938, 'latitude'),
#  (0.06235922339438398, 'rooms_per_hhold'),
#  (0.042694898354819724, 'housing_median_age'),
#  (0.018039618577725656, 'total_rooms'),
#  (0.017381655368930767, 'population'),
#  (0.016858884221298987, 'total_bedrooms'),
#  (0.0160694274402511, 'households'),
#  (0.009193680857258888, '<1H OCEAN'),
#  (0.005092120983978223, 'NEAR OCEAN'),
#  (0.0050270640313138226, 'NEAR BAY'),
#  (4.1556582644446e-05, 'ISLAND')]

最后我们将其应用到验证集上,查看其预测的标准差:

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)
#注意此处是transform()而不是fit_transform()
final_predictions = final_model.predict(X_test_prepared)

final_mse = mean_squared_error(Y_test,final_predictions)
final_rmse = np.sqrt(final_mse)
print(final_rmse)
# 47832.516665361305

一段完整的代码

根据以上的探究,我们可以写出该机器学习小实例的完整代码,下载链接。

你可能感兴趣的:(机器学习)