尽管 Scikit-Learn 提供了许多有用的转换器,你仍是须要本身动手写转换器执行任务,好比自定义的清理操做,或属性组合。你须要让自制的转换器与 Scikit-Learn 组件(好比流水线)无缝衔接工做,由于 Scikit-Learn 是依赖鸭子类型的(而不是继承),你所须要作的是建立一个类并执行三个方法:fit()(返回self),transform(),和fit_transform()。经过添加TransformerMixin做为基类,能够很容易地获得最后一个。另外,若是你添加BaseEstimator做为基类(且构造器中避免使用args和kargs),你就能获得两个额外的方法(get_params()和set_params()),两者能够方便地进行超参数自动微调*。例如,一个小转换器类添加了上面讨论的属性:node
# 添加一个特征组合的装换器 from sklearn.base import BaseEstimator, TransformerMixin rooms_ix, bedrooms_ix, population_ix, household_ix = 3, 4, 5, 6 # 这里的示例没有定义fit_transform(),多是由于fit()没有作任何动做(我猜的 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] # X[:,3]表示的是第4列全部数据 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, # np.c_表示的是拼接数组。 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(提供一个合理的默认值颇有帮助)。这个超参数可让你方便地发现添加了这个属性是否对机器学习算法有帮助。更通常地,你能够为每一个不能彻底确保的数据准备步骤添加一个超参数。数据准备步骤越自动化,能够自动化的操做组合就越多,越容易发现更好用的组合(并能节省大量时间)。git
另外sklearn是不能直接处理DataFrames的,那么咱们须要自定义一个处理的方法将之转化为numpy类型算法
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 #返回的为numpy array
有两种常见的方法可让全部的属性有相同的量度:线性函数归一化(Min-Max scaling)和标准化(standardization)。Scikit-Learn 提供了一个转换器MinMaxScaler来实现这个功能。它有一个超参数feature_range,可让你改变范围,若是不但愿范围是 0 到 1;Scikit-Learn 提供了一个转换器StandardScaler来进行标准化bootstrap
min-max方式,对应的方法为数组
MinMaxScaler(self, feature_range=(0, 1), copy=True)
standardization 标准化数据,对应的方法为网络
StandardScaler(self, copy=True, with_mean=True, with_std=True)
目前在数据预处理阶段,咱们须要对缺失值进行处理、特征组合和特征缩放。每一步的执行都有着前后顺序,存在许多数据转换步骤,须要按必定的顺序执行。sklearn提供了Pipeline帮助顺序完成转换幸运的是,Scikit-Learn 提供了类Pipeline,来进行这一系列的转换。下面是一个数值属性的小流水线:dom
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()方法。函数
估计器(Estimator):不少时候能够直接理解成分类器,主要包含两个函数:fit()和predict()
装换器(Transformer):转换器用于数据预处理和数据转换,主要是三个方法:fit(),transform()和fit_transform()工具
最后的估计器是一个StandardScaler,它是一个转换器,所以这个流水线有一个transform()方法,能够顺序对数据作全部转换(它还有一个fit_transform方法可使用,就没必要先调用fit()再进行transform())。
num_attribs = list(housing_num) # 返回的为列名[col1,col2,....] 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)), ('cat_encoder', CategoricalEncoder(encoding="onehot-dense")), ])
上面定义的为分别处理数值类型和标签类型的转换流程,housing_num为DataFrame类型,list(DataFrame)的结果返回的为列名字。上面着两个流程还能够再整合一块儿。
from sklearn.pipeline import FeatureUnion 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)
每一个子流水线都以一个选择转换器开始:经过选择对应的属性(数值或分类)、丢弃其它的,来转换数据,并将输出DataFrame转变成一个 NumPy 数组。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
咱们先来训练一个线性回归模型:
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
OK,有总比没有强,但显然结果并很差,这是一个模型欠拟合训练数据的例子。当这种状况发生时,意味着特征没有提供足够多的信息来作出一个好的预测,或者模型并不强大,修复欠拟合的主要方法是选择一个更强大的模型,给训练算法提供更好的特征,或去掉模型上的限制,你能够尝试添加更多特征(好比,人口的对数值),可是首先让咱们尝试一个更为复杂的模型,看看效果。训练一个决策树模型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
等一下,发生了什么?没有偏差?这个模型多是绝对完美的吗?固然,更大可能性是这个模型严重过拟合数据。如何肯定呢?如前所述,直到你准备运行一个具有足够信心的模型,都不要碰测试集,所以你须要使用训练集的部分数据来作训练,用一部分来作模型验证。
使用 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(随机森林),随机森林是经过用特征的随机子集训练许多决策树。在其它多个模型之上创建模型成为集成学习(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_estimators和max_features的3 × 4 = 12种组合(不用担忧这些超参数的含义,会在第 7 章中解释)。而后尝试第二个dict中超参数的2 × 3 = 6种组合,此次会将超参数bootstrap设为False而不是True(后者是该超参数的默认值)。完成后,你就能得到参数的最佳组合,以下所示:
>>> 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)
固然,也能够获得评估得分:
>>> 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)要稍微好一些。祝贺你,你成功地微调了最佳模型!
随机搜索:当探索相对较少的组合时,就像前面的例子,网格搜索还能够。可是当超参数的搜索空间很大时,最好使用RandomizedSearchCV。这个类的使用方法和类GridSearchCV很类似,但它不是尝试全部可能的组合,而是经过选择每一个超参数的一个随机值的特定数量的随机组合。这个方法有两个优势:
经过分析最佳模型,经常能够得到对问题更深的了解。好比,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
评估结果一般要比交叉验证的效果差一点,若是你以前作过不少超参数微调(由于你的系统在验证集上微调,获得了不错的性能,一般不会在未知的数据集上有一样好的效果)。这个例子不属于这种状况,可是当发生这种状况时,你必定要忍住不要调节超参数,使测试集的效果变好;这样的提高不能推广到新数据上。