RMSE ( X , h ) = 1 m ∑ i = 1 m ( h ( x ( i ) ) − y ( i ) ) 2 \operatorname{RMSE}(\boldsymbol{X}, h)=\sqrt{\frac{1}{m} \sum_{i=1}^{m}\left(h\left(\boldsymbol{x}^{(i)}\right)-y^{(i)}\right)^{2}} RMSE(X,h)=m1i=1∑m(h(x(i))−y(i))2
MAE ( X , h ) = 1 m ∑ i = 1 m ∣ h ( x ( i ) ) − y ( i ) ∣ \operatorname{MAE}(\boldsymbol{X}, h)=\frac{1}{m} \sum_{i=1}^{m} \mid h\left(\boldsymbol{x}^{(i)}\right)-y^{(i)}\mid MAE(X,h)=m1i=1∑m∣h(x(i))−y(i)∣
虽然大多数时候 RMSE 是回归任务可靠的性能指标,在有些情况下,你可能需要另外的函数。例如,假设存在许多异常的街区。此时,你可能需要使用平均绝对误差(Mean Absolute Error,也称作平均绝对偏差).
范数的指数越高,就越关注大的值而忽略小的值。这就是为什么 RMSE 比 MAE 对异常值更敏感。但是当异常值是指数分布的(类似正态曲线),RMSE 就会表现很好。
下面是获取数据的函数:
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()
然后使用 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)
这个函数会返回一个包含所有数据的 Pandas DataFrame 对象。
head()
方法info()
方法可以快速查看数据的描述,特别是总行数、每个属性的类型和非空值的数量。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
value_counts()
是一种查看表格某列中有多少个不同值的快捷方法,并计算每个不同值有在该列中有多少重复值。
value_counts()
是Series拥有的方法,一般在DataFrame中使用时,需要指定对哪一列或行使用。
describe()
方法展示了数值属性的概括另一种快速了解数据类型的方法是画出每个数值属性的柱状图。柱状图(的纵轴)展示了特定范围的实例的个数。你还可以一次给一个属性画图,或对完整数据集调用hist()
方法,后者会画出每个数值属性的柱状图。
%matplotlib inline # only in a Jupyter notebook
import matplotlib.pyplot as plt
housing.hist(bins=50, figsize=(20,15))
plt.show()
注:hist()方法依赖于 Matplotlib,后者依赖于用户指定的图形后端以打印到屏幕上。因此在画图之前,你要指定 Matplotlib 要使用的后端。最简单的方法是使用 Jupyter 的魔术命令%matplotlib inline。它会告诉 Jupyter 设定好 Matplotlib,以使用 Jupyter 自己的后端。绘图就会在笔记本中渲染了。注意在 Jupyter 中调用show()不是必要的,因为代码框执行后 Jupyter 会自动展示图像。
train_test_split
,首先,它有一个random_state参数,可以设定前面讲过的随机生成器种子;第二,你可以将种子传递给多个行数相同的数据集,可以在相同的索引上分割数据集(这个功能非常有用,比如你的标签值是放在另一个DataFrame里的):
from sklearn.model_selection import train_test_split
train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)
分层采样:使用 Scikit-Learn 的StratifiedShuffleSplit
类
https://www.jb51.net/article/152564.htm
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]
housing.plot(kind="scatter", x="longitude", y="latitude")
将alpha设为 0.1,可以更容易看出数据点的密度
housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.1)
每个圈的半径表示街区的人口(选项s),颜色代表价格(选项c)。我们用预先定义的名为jet的颜色图(选项cmap),它的范围是从蓝色(低价)到红色(高价):
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()
因为数据集并不是非常大,你可以很容易地使用corr()
方法计算出每对属性间的标准相关系数(standard correlation coefficient,也称作皮尔逊相关系数):
corr_matrix = housing.corr()
现在来看下每个属性和房价中位数的关联度:
>>> corr_matrix["median_house_value"].sort_values(ascending=False)
median_house_value 1.000000
median_income 0.687170
total_rooms 0.135231
housing_median_age 0.114220
households 0.064702
total_bedrooms 0.047865
population -0.026699
longitude -0.047279
latitude -0.142826
Name: median_house_value, dtype: float64
相关系数的范围是 -1 到 1。当接近 1 时,意味强正相关;例如,当收入中位数增加时,房价中位数也会增加。当相关系数接近 -1 时,意味强负相关.最后,相关系数接近 0,意味没有线性相关性.
另一种检测属性间相关系数的方法是使用 Pandas 的scatter_matrix
函数,它能画出每个数值属性对每个其它数值属性的图。因为现在共有 11 个数值属性,你可以得到11 ∗ * ∗ 2 = 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))
属性组合试验
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.687170
rooms_per_household 0.199343
total_rooms 0.135231
housing_median_age 0.114220
households 0.064702
total_bedrooms 0.047865
population_per_household -0.021984
population -0.026699
longitude -0.047279
latitude -0.142826
bedrooms_per_room -0.260070
Name: median_house_value, dtype: float64
看起来不错,与总房间数或卧室数相比,新的bedrooms_per_room属性与房价中位数的关联更强。显然,卧室数/总房间数的比例越低,房价就越高。每户的房间数也比街区的总房间数的更有信息,很明显,房屋越大,房价就越高。
先回到干净的训练集(通过再次复制strat_train_set),将预测量和标签分开,因为我们不想对预测量和目标值应用相同的转换(注意drop()创建了一份数据的备份,而不影响strat_train_set):
housing = strat_train_set.drop("median_house_value", axis=1)
housing_labels = strat_train_set["median_house_value"].copy()
大多机器学习算法不能处理缺失的特征,因此先创建一些函数来处理特征缺失的问题。前面,你应该注意到了属性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
该函数主要用于滤除缺失数据。
如果是Series,则返回一个仅含非空数据和索引值的Series,默认丢弃含有缺失值的行。
data.dropna(how = 'all') # 传入这个参数后将只丢弃全为缺失值的那些行
data.dropna(axis = 1) # 丢弃有缺失值的列(一般不会这么做,这样会删掉一个特征)
data.dropna(axis=1,how="all") # 丢弃全为缺失值的那些列
data.dropna(axis=0,subset = ["Age", "Sex"]) # 丢弃‘Age’和‘Sex’这两列中有缺失值的行
当你要删除某一行或者某一列时,用drop函数,它不改变原有的df中的数据,而是返回另一个dataframe来存放删除后的数据。
1.命令: df.drop()
删除行:df.drop(‘apps’)
删除列:df.dorp(‘col’, axis=1),删除列要加axis=1,默认是删除行的
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 数组。如果你想将其放回到 PandasDataFrame中,也很简单:
housing_tr = pd.DataFrame(X, columns=housing_num.columns)
sklearn数据预处理中fit()
,transform()
与fit_transform()
的区别
https://zhuanlan.zhihu.com/p/42297868
Fit(): Method calculates the parameters μ and σ and saves them as internal objects.
解释:简单来说,就是求得训练集X的均值,方差,最大值,最小值,这些训练集X固有的属性。
Transform(): Method using these calculated parameters apply the transformation to a particular dataset.
解释:在fit的基础上,进行标准化,降维,归一化等操作(看具体用的是哪个工具,如PCA,StandardScaler等)
Fit_transform(): joins the fit() and transform() method for transformation of dataset.
transform()
和fit_transform()
二者的功能都是对数据进行某种统一处理(比如标准化~N(0,1),将数据缩放(映射)到某个固定区间,归一化,正则化等)
fit_transform(trainData)
对部分数据先拟合fit,找到该part的整体指标,如均值、方差、最大值最小值等等(根据具体转换的目的),然后对该trainData进行转换transform,从而实现数据的标准化、归一化等等。
根据对之前部分trainData进行fit的整体指标,对剩余的数据(testData)使用同样的均值、方差、最大最小值等指标进行转换transform(testData),从而保证train、test处理方式相同。所以,一般都是这么用:
from sklearn.preprocessing import StandardScaler
sc = StandardScaler()
sc.fit_transform(X_train)
sc.transform(X_test)
Note:
fit_transform(trainData)
,之后再transform(testData)
transform(testData)
,程序会报错fit_transfrom(trainData)
后,使用fit_transform(testData)
而不transform(testData)
,虽然也能归一化,但是两个结果不是在同一个“标准”下的,具有明显差异。(一定要避免这种情况)Scikit-Learn 设计的 API 设计的非常好。它的主要设计原则是:
可检验。所有估计器的超参数都可以通过实例的公共变量直接访问(比如,imputer.strategy),并且所有估计器学习到的参数也可以通过在实例变量名后加下划线来访问(比如,imputer.statistics_)。
类不可扩散。数据集被表示成 NumPy 数组或 SciPy 稀疏矩阵,而不是自制的类。超参数只是普通的 Python 字符串或数字。
可组合。尽可能使用现存的模块。例如,用任意的转换器序列加上一个估计器,就可以做成一个流水线,后面会看到例子。
合理的默认值。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])
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 ''
with 16513 stored elements in Compressed Sparse Row format>
Python的reshape的用法:reshape(1,-1)
https://blog.csdn.net/qq_29831163/article/details/90112000
numpy中reshape函数的三种常见相关用法
reshape(1,-1)转化成1行:
reshape(2,-1)转换成两行:
reshape(-1,1)转换成1列:
reshape(-1,2)转化成两列
https://www.cnblogs.com/tillnight1996/p/10663259.html
注意输出结果是一个 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 组件(比如流水线)无缝衔接工作,因为 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(提供一个合理的默认值很有帮助)。这个超参数可以让你方便地发现添加了这个属性是否对机器学习算法有帮助。更一般地,你可以为每个不能完全确保的数据准备步骤添加一个超参数。数据准备步骤越自动化,可以自动化的操作组合就越多,越容易发现更好用的组合(并能节省大量时间)。
有两种常见的方法可以让所有的属性有相同的量度:线性函数归一化(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
来进行标准化。
存在许多数据转换步骤,需要按一定的顺序执行。Scikit-Learn 提供了类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()
方法)。名字可以随意起。
当你调用流水线的fit()
方法,就会对所有转换器顺序调用fit_transform()
方法,将每次调用的输出作为参数传递给下一个调用,一直到最后一个估计器,它只执行fit()
方法。
流水线暴露相同的方法作为最终的估计器。在这个例子中,最后的估计器是一个StandardScaler
,它是一个转换器,因此这个流水线有一个transform()
方法,可以顺序对数据做所有转换(它还有一个fit_transform
方法可以使用,就不必先调用fit()
再进行 transform()
)。
如果不需要手动将 PandasDataFrame中的数值列转成 Numpy 数组的格式,而可以直接将DataFrame输入 pipeline 中进行处理就好了。Scikit-Learn 没有工具来处理 PandasDataFrame,因此我们需要写一个简单的自定义转换器来做这项工作:
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
每个子流水线都以一个选择转换器开始:通过选择对应的属性(数值或分类)、丢弃其它的,来转换数据,并将输出DataFrame转变成一个 NumPy 数组。这样,你就可以很简单的写出一个以 Pandas DataFrame为输入并且可以处理数值的流水线: 该流水线从DataFrameSelector开始获取数值属性,前面讨论过的其他数据处理步骤紧随其后。 并且你也可以通过使用DataFrameSelector选择类别属性并为其写另一个流水线然后应用LabelBinarizer.
你现在就有了一个对数值的流水线,你还需要对分类值应用LabelBinarizer:如何将这些转换写成一个流水线呢?Scikit-Learn 提供了一个类FeatureUnion实现这个功能。你给它一列转换器(可以是所有的转换器),当调用它的transform()方法,每个转换器的transform()会被并行执行,等待输出,然后将输出合并起来,并返回结果(当然,调用它的fit()方法就会调用每个转换器的fit())。一个完整的处理数值和类别属性的流水线如下所示:
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
array([[ 0.73225807, -0.67331551, 0.58426443, ..., 0. ,
0. , 0. ],
[-0.99102923, 1.63234656, -0.92655887, ..., 0. ,
0. , 0. ],
[...]
>>> housing_prepared.shape
(16513, 17)
在训练集上训练和评估
我们先来训练一个线性回归模型:
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]
让我们使用 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
训练一个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)
>>> tree_rmse
0.0
等一下, 没有误差?这个模型可能是绝对完美的吗?当然,更大可能性是这个模型严重过拟合数据。如何确定呢?如前所述,直到你准备运行一个具备足够信心的模型,都不要碰测试集,因此你需要使用训练集的部分数据来做训练,用一部分来做模型验证。
使用交叉验证做更佳的评估
下面的代码采用了 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)
警告: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
让我们计算下线性回归模型的的相同分数,以做确保:
>>> 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
我们会跳过大部分的代码,因为代码本质上和其它模型一样:
>>> 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
现在好多了:随机森林看起来很有希望。但是,训练集的评分仍然比验证集的评分低很多。解决过拟合可以通过简化模型,给模型加限制(即,规整化),或用更多的训练数据。在深入随机森林之前,你应该尝试下机器学习算法的其它类型模型(不同核心的支持向量机,神经网络,等等),不要在调节超参数上花费太多时间。目标是列出一个可能模型的列表(两到五个)。
使用 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_estimators和max_features的3 × 4 = 12种组合(不用担心这些超参数的含义,会在第 7 章中解释)。然后尝试第二个dict中超参数的2 × 3 = 6种组合,这次会将超参数bootstrap设为False而不是True(后者是该超参数的默认值)。
总之,网格搜索会探索 12 + 6 = 18种RandomForestRegressor的超参数组合,会训练每个模型五次(因为用的是五折交叉验证)。换句话说,训练总共有18 × 5 = 90轮!K 折将要花费大量时间,完成后,你就能获得参数的最佳组合,如下所示:
>>> grid_search.best_params_
{'max_features': 6, 'n_estimators': 30}
你还能直接得到最佳的估计器:
>>> 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}