写在前面
参考书籍
:Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow
❤本文为机器学习实战学习笔记,主要内容为第二章房价预测项目,文中除了书中主要内容,还包含部分博主少量自己修改的部分,如果有什么需要改进的地方,可以在评论区留言❤。
下载数据可以直接通过浏览器下载压缩包,也可以通过函数来进行。
数据集下载地址
import os
import tarfile
import urllib.request
DOWNLOAD_ROOT = "https://raw.githubusercontent.com/ageron/handson-ml2/master/"
HOUSING_PATH = os.path.join("datasets", "housing") # datasets\test
HOUSING_URL = DOWNLOAD_ROOT + "datasets/housing/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") # datasets\test\housing.tgz
# 将housing_url下载的文件保存到tgz_path路径
urllib.request.urlretrieve(housing_url, tgz_path)
housing_tgz = tarfile.open(tgz_path)
# 解压到housing_path路径
housing_tgz.extractall(path=housing_path)
housing_tgz.close()
fetch_housing_data()
每次调用 fetch_housing_data()
,就会自动在工作区创建一个 datasets/housing
目录,然后下载 housing.tgz
文件,并解压到当前文件夹。
再创建一个函数来加载数据,返回值为 DataFrame
对象。
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)
housing = load_housing_data()
显示数据集前五行, head()
中也可以指定显示的行数。
housing.head()
使用 info()
获取数据集的简单描述。包括总行数、每个属性的类型和非空值的数量等。
housing.info()
使用 describe()
获取数值属性的描述,注意统计时的空值会被忽略。
housing.describe()
绘制每个数值的直方图。直方图横轴表示数值范围,纵轴表示实例数量。
import matplotlib.pyplot as plt
# 绘制直方图
housing.hist(bins=50, figsize=(20,15))
# 保存图片
plt.savefig("./images/end_to_end_project/attribute_histogram_plots.png", dpi=300)
housing_median_age
与 median_house_value
右侧的数据可以看出,房龄中位数和房价中位数被设置了上限,如果直接用这些数据进行模型训练,那么模型学习到的价格很可能永远不会超过这个限制。如果需要精确的预测房价,甚至可以超过 50
万美元,那么有以下两个选择:
50
万。经调研可知,收入中位数对于预测房价中位数十分重要。所以在收入这一属性上,我们希望测试集能够代表各种不同类型的收入,即分层抽样。首先要根据上面图中的 median_income
直方图中的数值,大多数收入中位数聚集在 [1.5, 6]
之间,划分层次时,每层要有足够多的实例,因此,使用 pd.cut()
方法创建 5
个收入类别, 0-1.5
、 1.5-3
、 3-4.5
、 4.5-6
、 6- +∞
。
housing["income_cat"] = pd.cut(housing["median_income"],
bins=[0., 1.5, 3.0, 4.5, 6., np.inf],
labels=[1, 2, 3, 4, 5])
# 根据income_cat数据绘制直方图
housing["income_cat"].hist()
根据 income_cat
列进行分层抽样,使用 Scikit-learn
的 StratfiedShuffleSplit
类来实现。划分完测试集就可以将 income_cat
列删除。
from sklearn.model_selection import StratifiedShuffleSplit
s = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
for train_index, test_index in s.split(housing, housing["income_cat"]):
# train_index: [17606 18632 14650 ... 13908 11159 15775]
strat_train_set = housing.loc[train_index]
# test_index: [5241 10970 20351 ... 4019 12107 2398]
strat_test_set = housing.loc[test_index]
for dataset in (strat_train_set, strat_test_set):
dataset.drop("income_cat", axis=1, inplace=True)
housing_train = start_train_set.copy()
根据数据集中的经纬度,可以绘制一个各区域的分布图。
# 显示中文
plt.rcParams['font.family'] = 'SimHei'
# 显示负号
plt.rcParams['axes.unicode_minus'] = False
housing_train.plot(kind="scatter", x="longitude", y="latitude")
plt.xlabel('经度')
plt.ylabel('纬度')
plt.savefig("./images/end_to_end_project/bad_visualization_plot.png", dpi=300)
将 alpha
设置为1,显示高密度点的位置。
housing_train.plot(kind="scatter", x="longitude", y="latitude", alpha=0.1)
plt.xlabel('经度')
plt.ylabel('纬度')
plt.savefig("./images/end_to_end_project/better_visualization_plot.png", dpi=300)
为了更加图像能够凸显更多的信息,将每个区域的人口数量作为图中每个圆的半径大小,房价中位数表示圆的颜色,使用预定义颜色表 "jet"
,颜色由(低房价)到红(高房价)。
housing_train.plot(kind="scatter", x="longitude", y="latitude", alpha=0.3,
s=housing_train["population"]/50, label="population", figsize=(10,7),
c="median_house_value", cmap=plt.get_cmap("jet"), colorbar=True,
sharex=False)
plt.legend()
plt.xlabel('经度')
plt.ylabel('纬度')
plt.savefig("./images/end_to_end_project/housing_prices_scatterplot.png", dpi=300)
使用 corr()
方法计算每对属性之间的皮尔逊相关系数。相关系数范围 [-1, 1]
,越接近 1
表示有越强的正相关,越接近 -1
表示有越强的负相关。
corr_matrix = housing_train.corr()
corr_matrix
具体看每个属性与房价中位数的相关性。
corr_matrix["median_house_value"].sort_values(ascending=False)
也可以使用 pandas
的 scatter_matrix
函数,他会绘制出每个数值属性相对于其他数值属性的相关性。比如现在有 9
个数值属性,能够得到 81
个图像,根据 corr()
计算的相关性,我们选取前 4
个属性来绘制图像,这样只得到 16
个图像。
from pandas.plotting import scatter_matrix
attributes = ["median_house_value", "median_income", "total_rooms",
"housing_median_age"]
scatter_matrix(housing_train[attributes], figsize=(12, 8))
plt.savefig("./images/end_to_end_project/scatter_matrix_plot.png", dpi=300)
与房价中位数最相关的属性是收入中位数,我们将上图中的收入中位数散点图单独拿出来。
housing_train.plot(kind="scatter", x="median_income", y="median_house_value",
alpha=0.2)
plt.axis([0, 16, 0, 550000])
plt.savefig("./images/end_to_end_project/income_vs_house_value_scatterplot.png", dpi=300)
根据图中的点,可以看出两个属性确实具有较强的相关性,除此之外,图中还有几条比较明显的直线, 50
万、 45
万、 35
万时,甚至还有更多,这些比较“怪”的数据,再投入模型之前,要尝试删除这些相关区域。
在把训练集投入模型之前,我们应该尝试各种属性的组合,例如相比于一个区域的房间总数 (total_rooms)
与家庭数量 (households)
,我们可能更需要的是单个家庭的房间数量 (rooms_per_household)
。下面尝试创建一些新的组合属性。
housing_train["rooms_per_household"] = housing_train["total_rooms"]/housing_train["households"]
housing_train["bedrooms_per_room"] = housing_train["total_bedrooms"]/housing_train["total_rooms"]
housing_train["population_per_household"]=housing_train["population"]/housing_train["households"]
# 计算相关矩阵
corr_matrix = housing_train.corr()
corr_matrix["median_house_value"].sort_values(ascending=False)
通过计算得到的相关性可以看到,某些组合属性与房价中位数的相关性比单个属性的相关性都要高。
填补 total_bedrooms
属性中的缺失值。
# 获取删除标签列后的数据集
housing = strat_train_set.drop("median_house_value", axis=1)
# 标签列
housing_labels = strat_train_set["median_house_value"].copy()
from sklearn.impute import SimpleImputer
# 创建SimpleImputer实例,设置中位数替换
imputer = SimpleImputer(strategy="median")
# 创建一个没有文本属性"ocean_proximity"的数据集
housing_num = housing.drop("ocean_proximity", axis=1)
# 将imputer实例适配到housing_num
imputer.fit(housing_num)
# 执行中位数替换缺失值的转化。
X = imputer.transform(housing_num)
# 将numpy数组转换成DataFrame
housing_tr = pd.DataFrame(X, columns=housing_num.columns,
index=housing.index)
对于文本属性 ocean_proximity
,我们先看一下它的大致内容。
# 注意:[["ocean_proximity"]]获得DataFrame类型的数据,因为后面需要对二维数据进行转化
housing_cat = housing[["ocean_proximity"]]
housing_cat.value_counts()
可以看出它的值不是任意文本,而是每个值代表一个类别,可以使用 Scikit-Learn
中的 OrdinalEncoder
类将这些类别从文本转成数字。
from sklearn.preprocessing import OrdinalEncoder
ordinal_encoder = OrdinalEncoder()
# 拟合并转换
housing_cat_encoded = ordinal_encoder.fit_transform(housing_cat)
housing_cat_encoded[:10]
使用 Categories_
实例变量获取类别列表。
ordinal_encoder.categories_
这样表征方式会产生一个问题,模型训练时会认为两个相近的值比两个相远的值更相似一些。在某些情况下是对的( ['坏',‘平均’,‘好’,‘优秀 ’]
),但在目前的序列 categories
中,情况可能就不是这样( 1H OCEAN
与 NEAR OCEAN
的相似度比相邻情况下的 1H OCEAN
与 INLAND
的相似度高)。常见的解决方案是给每个类别创建一个二进制属性:当类别为 1H OCEAN
时,它的某个属性为 1
(其他属性为 0
),当类别是 INLAND
时,另一个属性为 1
(其他属性为 0
),以此类推,这就是独热编码可以使用 sklearn
中的 OneHotEncoder
编码器,将整数类别值转换为独热向量。
from sklearn.preprocessing import OneHotEncoder
cat_encoder = OneHotEncoder()
housing_cat_1hot = cat_encoder.fit_transform(housing_cat)
housing_cat_1hot
从结果可以知道,这是一个 Scipy
稀疏矩阵。稀疏矩阵选择仅存储非零元素的位置。如果需要也可以使用 toarray()
方法得到一个(密集的) NumPy
数组。
housing_cat_1hot.toarray()
注意: 如果类别中的种类有很多,那么独热编码会导致过多的输入特征,可能会减慢训练速度并降低性能。如果出现这种情况,可以使用相关的数字特征代替类别。如上面的 ocean_proximity
特征,可以使用海洋的距离作为特征进行替换。
Scikit-Learn
中有许多有用的转换器,但对于一些自定义清理操作或者组合特定属性等任务需要编写自己的转换器。由于 Scikit-Learn
的编译特性,编写转换器也十分方便,我们只需创建一个类,然后实现 fit()
、 transformm()
、 fit_transform()
。该类可以通过添加 TransformMixin
作为基类,直接得到 fit_transform()
方法,同时可以添加 BaseEstimator
作为基类(并在构造函数中避免 *arg
和 **kargs
),还可以获得自动调整参数的方法( get_params()
和 set_params()
)。下面实现一个简单的转换器类,用来添加之前的组合属性。
from sklearn.base import BaseEstimator, TransformerMixin
# 选取列名
col_names = ["total_rooms", "total_bedrooms", "population", "households"]
rooms_ix, bedrooms_ix, population_ix, households_ix = [housing.columns.get_loc(c) for c in col_names]
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
def transform(self, X):
rooms_per_household = X[:, rooms_ix] / X[:, households_ix]
population_per_household = X[:, population_ix] / X[:, households_ix]
# 根据超参数add_bedrooms_per_room判断是否需要添加该组合属性
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)
# 将housing_extra_attribs从array转为DataFrame
housing_extra_attribs = pd.DataFrame(
housing_extra_attribs,
columns=list(housing.columns)+["rooms_per_household", "population_per_household"],
index=housing.index)
housing_extra_attribs.head()
我们可以设置转换器中的超参数 add_bedrooms_per_room
的值,来轻松的知晓添加这个属性是否有助于机器学习的算法。
大多数据转换的步骤需要以正确的顺序来执行, Sickit-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)
housing_num_tr
Pipeline
构造函数会通过一系列的名称/估算器的配对来定义步骤序列。除了最后一个是估算器之外,前面都必须是转换器(必须有 fit_transform()
方法)。当调用流水线的 fit()
方法时,会在所有转换器上按照顺序依次调用 fit_transform()
,每次将一个调用的输出作为输出传递给下一个调用方法,知道传递到最终的估算器,则只会调用 fit()
方法。
现在我们已经分别处理了类别列和数值列。然后可以构造一个能够处理所有列的转换器。将是当的转换应用到每个列,对此我们可以使用 ColumnTransformer
。下面用它来将所有的转换应用到房屋数据。
from sklearn.compose import ColumnTransformer
# 获得数值列名称列表
num_attribs = list(housing_num)
# 获得类别列名称列表
cat_attribs = ["ocean_proximity"]
# 元组中的三个参数分别代表:名称(自定),转换器,以及一个该转换器能够应用的列名字(或索引)的列表
full_pipeline = ColumnTransformer([
# 数值属性列转换器
("num", num_pipeline, num_attribs),
# 文本属性列转换器
("cat", OneHotEncoder(), cat_attribs),
])
# 将ColumnTranformer应用到房屋数据
housing_prepared = full_pipeline.fit_transform(housing)
housing_prepared
我们观察上面的结果可以看出,它是一个密集矩阵,然而转换器 OneHotEncoder
返回一个稀疏矩阵。当一个稀疏矩阵和密集矩阵混合在一起时, ColumnTransformer
会估算最终矩阵的密度(即单元格的非零比率),如果密度低于给定的阈值(默认值 sparse_threshold=0.3
),则返回一个稀疏矩阵。
首先,先训练一个线性回归模型。
from sklearn.linear_model import LinearRegression
lin_reg = LinearRegression()
# 模型训练
lin_reg.fit(housing_prepared, housing_labels)
使用 5
个训练集的实例进行测试。
# 在几个训练实例上应用完整的预处理
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))
预测的结果还不太准确。可以使用 Scikit-Learn
的 mean_squared_errod()
函数来测量整个训练集上回归模型的 RMSE
( Root Mean Square Error
均方根误差)。
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
根据结果可以看出,这个模型的效果并不是很好,根据之前的统计,大多数区域的 median_housing_values
分布在 120000~265000
美元之间,所以预测的误差达到 68628
美元难以让人接受。这是典型的模型对训练数据欠拟合的情况。欠拟合的情况下,往往说明这些特征无法提供足够的信息来做出更好的预测,或者是模型的本身有所欠缺。想要修正的话,可以通过选择更强大的模型,或为算法训练提供更好的特征,又或者减少对模型的限制。 LinearRegression
并不是一个正则化模型,所以只能通过选择更强大的模型,或是对特征进行修改。接下来尝试一个更强大的模型 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
根据 tree_rmse
的值,貌似结果非常完美,但事实却可能是这个模型对数据严重过拟合了。如何确认?可以使用下节的方式。
评估模型的一种方法是使用 train_test_split
函数将训练集划分为较小的训练集和测试集。还有一种方法是使用 Scikit-Learn
的 K
折交叉验证的功能。比如:它可以将训练集分割成 10
个不同的子集,每个子集称为一个折叠,然后对模型进行 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(temp_scores):
print("Scores:", temp_scores)
print("Mean:", temp_scores.mean())
print("Standard deviation:", temp_scores.std())
display_scores(tree_rmse_scores)
注: Scikit-Learn
的交叉验证功能更倾向于使用效用函数(越大越好)而不是成本函数(越小越好),所以上述的 scores
中的分数实际是负的 MSE
函数,所以要是用 np.sqrt(-scores)
。
以这个结果来看决策树模型表现的也不太好,误差 71407
,上下浮动 ±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(n_estimators=100, random_state=42)
forest_reg.fit(housing_prepared, housing_labels)
housing_predictions = forest_reg.predict(housing_prepared)
forest_mse = mean_squared_error(housing_labels, housing_predictions)
forest_rmse = np.sqrt(forest_mse)
forest_rmse
根据 RMSE
的结果来看,比较容易接受,接下来看一下交叉验证的评分。
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)
交叉验证的结果 50182
,虽然比之前的模型都要好,但还是远远高于训练集上的 18603
,这就意味着该模型仍然对训练集过拟合。如此以来,我们可以着手对模型进行微调了,但在微调之前可以将训练好的模型进行保存,同时也要保存超参数和训练过的参数以及交叉验证的评分(忘记了就只有重新再跑一边模型了)和实际预测的结果。
使用 Scikit-Learn
的 GridSearchCV
来寻找最佳的超参数组合,你只需要传入超参数是什么,以及它需要尝试的值, GridSearchCV
会使用交叉验证来评估超参数值的所有组合,下面通过 GridSearchCV
来搜索 RandomForestRegressor
的超参数值的最佳组合。
from sklearn.model_selection import GridSearchCV
param_grid = [
# 尝试3×4=12种超参数组合
{
'n_estimators': [3, 10, 30], 'max_features': [2, 4, 6, 8]},
# 之后设置bootstrap=False,再尝试2×3=6种超参数组合
{
'bootstrap': [False], 'n_estimators': [3, 10], 'max_features': [2, 3, 4]},
]
forest_reg = RandomForestRegressor(random_state=42)
# 训练5次,总共(12+6)×5=90次
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)
上面代码中的 param_grid
参数中值的含义是,首先评估第一个字典中的 n_estimator
和 max_features
的所有 3×4
共 12
种组合;接着在尝试第二个字典中的 1×2×3
共 6
种组合。所以 GridSearchCV
将探索 RandomForestRegressor
超参数值的 18
种组合,并对每个模型进行 5
次训练( cv=5
),总 5×18
共 90
次训练,可能需要很长时间,但可能会得到最佳的超参数组合(可能得到的超参数值处于边界处,根据情况还需要扩大或缩小范围,再调参)。
查看评分最高的超参数组合。
grid_search.best_params_
由于超参数的最佳值都处于取值范围的右边界,可能需要再扩大取值范围,继续寻找。
param_grid = [
{
'n_estimators': [30, 50, 70, 90], 'max_features': [7, 8, 9]},
]
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)
grid_search.best_params_
接下来看看当前的最佳估算器(输出只显示非默认的参数)。
grid_search.best_estimator_
GridSearchCV
计算的各种超参数组合的评分。
cvres = grid_search.cv_results_
for mean_score, params in zip(cvres["mean_test_score"], cvres["params"]):
print(np.sqrt(-mean_score), params)
根据图中的两条红线的得分可以看出,相较于第一次调参结果,第二次的超参数组合确实要高一些。
如果探索的组合数量较少时,网格搜索是一种不错的方法,但当超参数的搜索范围较大时,通常会优先选择使用 RandomizedSearchCV
。它与 GridSearchCV
用法相似,但它不会尝试所有可能的组合,而是在每次迭代中为每个超参数选择一个随机值,然后对一定数量的随机组合进行评估,这种方法有两个显著的好处。
1000
个迭代,那么将会探索每个超参数的 1000
个不同的值(而不像网格搜索方法那样每个超参数仅探索少量几个值)。from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint
param_distribs = {
# 均匀离散随机变量
'n_estimators': randint(low=1, high=200),
'max_features': randint(low=7, high=9),
}
forest_reg = RandomForestRegressor(random_state=42)
rnd_search = RandomizedSearchCV(forest_reg, param_distributions=param_distribs,
n_iter=10, cv=5, scoring='neg_mean_squared_error', random_state=42)
rnd_search.fit(housing_prepared, housing_labels)
看一下 RandomizedSearchCV
计算的各种超参数组合的评分。
cvres = rnd_search.cv_results_
for mean_score, params in zip(cvres["mean_test_score"], cvres["params"]):
print(np.sqrt(-mean_score), params)
结果显而易见,它相比于网格搜索有着更好的随机性,能够在相同时间内探索更多可能的区域,但带来的就是不确定性,就如同上面的结果, max_features
的取值绝大多数为 7
.
通过检查最佳模型,你总是可以得到一些好的洞见,如在进行准确预测时, RandomForestRegressor
可以指出每个属性的相对重要程度。
feature_importances = grid_search.best_estimator_.feature_importances_
feature_importances
将这些重要性分数显示在对应的属性名称旁边。
extra_attribs = ["rooms_per_hhold", "pop_per_hhold", "bedrooms_per_room"]
# cat_encoder: OneHotEncoder()
cat_encoder = full_pipeline.named_transformers_["cat"]
# cat_one_hot_attribs: ['<1H OCEAN', 'INLAND', 'ISLAND', 'NEAR BAY', 'NEAR OCEAN']
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
中只有 INLAND
类别是有用的,我们可以试着删除其他所有的类别( <1H OCEAN>
、 NEAR OCEAN
、 NEAR BAY
、 ISLAND
)。
经过前面的训练,现在有了一个表现足够优秀的系统了,是时候用测试集评估最终模型的时候了,我们只需要从测试集中获取预测器和标签,运行 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)
final_rmse
在某些情况下,泛化误差的这种点估计将不足以说服你启动生产环境,我们可以使用 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)))
如果之前进行过大量的超参数调整,这时的评估结果往往会略逊于你之前使用交叉验证是的表现结果(通过不断调整,系统在测试集上面终于表现良好,但在未知数据集上可能达不到这么好的效果)。在上面的结果中,结果尽管并非如此,但你也一定不要去继续调整参数,不要试图再努力让测试集的结果变得好看,因为这些改进在泛化到新的数据集时又会变成无用功。
将模型部署到生产环境中。比较简单的方法是保存训练好的 Scikit-Learn
模型(使用 joblib
),包括完整的预处理和预测流水线,然后在生产环境中加载经过训练的模型并通过调用 prepare()
方法来进行预测。