任务描述
kaggle 案例 california-housing-prices
https://www.kaggle.com/camnugent/california-housing-prices
基于给定的数据,训练模型预测某一区域的房价中位数
房价数据包括人口 . 收入中位数 . 房价中位数 等对于每个街区的描述属性
设计问题解决方案时应该了解到的信息:
- 弄清楚模型的应用目的.
- 大致弄明白当前(非机器学习模型的condition下)是如何的解答该应用问题.
接下来,你需要设计采用哪种模型解决问题.监督学习?无监督学习?增强学习?从问题类型上来分,是一个分类问题?还是一个回归问题?亦或者是其他的问题?需要采用分批次学习还是在线学习?
挑选一个性能评价指标:
- 均方根误差RMSE
RMSE是一种常用的测量数值之间差异的度量,其代表预测的值和观察到的值之差的样本标准差.
比如 :根据正态分布的结论,RMSE=50,000,这意味着模型预测的68%的结果落在预测值左右(+ -)50,000的范围之内.有95%的预测落在2倍标准差即100,000的范围内.因此较好地描述了预测的性能.
- 均值绝对误差MAE
RMSE和MAE都是一种衡量两个向量之间差值距离的方式,总的来说RMSE更优也更常用.但是在样本中存在很多离群点的时候,你就应该考虑使用MAE对其进行衡量,因为MAE相对RMSE而言对离群值不那么敏感.- RMSE对应于欧几里得范式,也称之为二范式.
- MAE对应于一范式.有时又称为曼哈顿范式.
- 更一般的情况,也存在k范式.
快速了解数据:
import pandas as pd
housing = pd.read_csv(\'housing.csv\')
housing.head() # 查看DF的前5行
可以用info()方法简要查看数据的描述.特别是数据的行数,每个特征的数据类型,以及非空值的数量. 可以了解到:
- 20000左右的数据量,对于机器学习模型来说算是一个小的数据集
可以看到total_bedrooms这一属性只有20433个非空值,即有207个缺失值,这是值得格外注意的.
In: housing.info()
Out:
<class \'pandas.core.frame.DataFrame\'>
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
通过对于前5行的浏览,发现ocean_proximity这一属性为object类型,并且值是重复的,这也意味着这可能是一个类别属性.因此,我们可以用value_counts( )方法弄清白到底有哪些类,并且每个类中到底存在多少实例.
In: type(housing[\'ocean_proximity\'])
Out: pandas.core.series.Series
In: housing[\'ocean_proximity\'].value_counts()
Out:
<1H OCEAN 9136
INLAND 6551
NEAR OCEAN 2658
NEAR BAY 2290
ISLAND 5
Name: ocean_proximity, dtype: int64
housing.describe()`方法可以展示数值特征的概要
In: housing.describe()
另外一个快速整体认知数据的方法是绘制每一个属性的直方图.
- hist()方法是依赖于matplotlib包的,因此在使用之前必须先申明基于matplotlib.这个操作可以使用Jupyter的魔法命令
%matplotlib inline.这能够告知Jupyter启用matplotlib.
%matplotlib inline
import matplotlib.pyplot as plt
housing.hist(bins=50, figsize=(20,15))
从直方图中得到的总结:
- 特征median_income数值的单位并不是dollar.已经被缩放并capped(设置阈值,这里是[0.5, 15])处理过了.在训练的时候这无关紧要,但是需要知道这些数据大概是如何计算得到.
- housing_median_age和median_house_value也是明显被capped过(注意到在上界出现了峰值).这将是一个需要异常注意的问题,因为median_house_value将是我们模型应用的标签,即ML模型训练后得出的任何预测几乎都不可能超过这个上界.
因此,在模型应用的时候需要格外地注意,目标应用和训练集上的数值差别 - 这些属性存在多个不同的规模量级,这也就需要在后面进行
特征归一化. - 很多
直方图显示出的特征数据分布都是尾部大的分布,这对于机器学习模型来说是不太好训练的.因此我们需要将这些特征的分布修正为正态分布.
创建测试集
数据窥探误差:目前为止我们只是对数据快速瞥了一眼,在挑选真正的模型之前我们还有很多信息需要学习.如果在这之前我们只是对test_set进行一个窥探,那么很容易造成认识上的错觉,倾向于选择某一个特定的模型进行训练.然鹅,大部分情况下这种都是过于乐观的操作.这就是叫做数据窥探误差
import numpy as np
自己写的一个拆分方法
def split_train_test(data,test_radio):
shuffled_indices = np.random.permutation(len(data)) # 产生一个目标长度的乱序索引
test_set_size = int(len(data) * test_radio) #指定测试集的数量
test_indices = shuffled_indices[:test_set_size]
train_indices = shuffled_indices[test_set_size:]
return data.iloc[train_indices],data.iloc[test_indices]
易混淆的iloc和loc的区分::
在pandas中loc和iloc都能实现对目标数据的准确索引.但实质上是有区别的:
- loc[\'a\',\'b\' ]中填入的是行列位置标签
- iloc[a,b] 中填入的索引
train_set,test_set = split_train_test(housing,0.2)
上述方法当然有效,但是并不完美.因为如果你重新run这一程序,你将拿到不同的测试集.慢慢的,你的算法将可能看到整个数据集,而这是你应该避免的.
# 查看拆分的效果
In: train_set.shape
Out: (16512, 10)
In: test_set.shape
Out: (4128, 10)
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个样本的数据集D进行分层抽样而获得70%样本的训练集S和含30%样本的测试集T,若D包含500个正例、500个反例,则分层采样得到的S应包含350个正例、350个反例,而T则包含150个正例、150个反例;
若S、T中样本类别比例差别很大,则误差估计将由于训练/测试数据分布的差异而产生偏差。
In: housing[\'median_income\'].hist()
Out: <matplotlib.axes._subplots.AxesSubplot at 0x2070de81dd8>
假设median_income属性非常重要:
通过直方图可以看出,大多数的income集中在2-5 unit之间.但是也是有一些income和2-5的范围差得很远,比如tail部分.
在这种情况下,保证数据集中income衡量的特征中每一层都有足够数量的实例是很有必要的,否则就会产生一些偏差.同时也这意味着不能有太多的分层,并且每个分层是需要足够大的.
后面的代码通过将收入中位数除以 1.5(以限制收入分类的层次数量),创建了一个收入类别属性,并且需要用ceil对值舍入(尽量产生离散的分类),然后将所有大于 5的分类归入到分类5.
housing[\'income_cat\'] = np.ceil(housing[\'median_income\'] / 1.5) # 添加income_cat属性,辅助分层
housing[\'income_cat\'].where(housing[\'income_cat\']<5, 5.0, inplace=True) # where方法操作
接下来准备基于income类对数据进行分层采样.这个问题需要用到sklearn中的StratifiedShuffleSplit class.
from sklearn.model_selection import StratifiedShuffleSplit
# random_state 依然是随机种子生成器
# n_split 是将训练数据分成train-test对的对数.-->我们这个地方汇总为一组数据
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数据集中income 属性类别的比例,瞥一眼看看是否成功分层采样.
In: housing[\'income_cat\'].value_counts() / len(housing)
Out:
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
完成分割后,接下来务必将数据集的特征还原,即丢掉income_cat属性:
# 完成分割,删除income_cat属性
for set in (strat_train_set,strat_test_set):
set.drop([\'income_cat\'], axis=1,inplace=True)
客观上来讲,对于本例20,000个样本的小集合来说,上述得到的训练集会有利于模型训练.
可视化数据寻找规律
接下来需要更加地了解数据.
复制一份数据,避免训练集被损坏.
housing = strat_train_set.copy()
看一下人口密度的特征:
In: import matplotlib.pyplot as plt
housing.plot(kind=\'scatter\',x=\'longitude\',y=\'latitude\')
Out: <matplotlib.axes._subplots.AxesSubplot at 0x2070b54c978>
可以看到California的轮廓.但是这个整个很难看出特点.因此设置alpha = 0.1,使得视图可以区分分布点的密度.
In: housing.plot(kind=\'scatter\',x=\'longitude\',y=\'latitude\',alpha = 0.1)
Out: <matplotlib.axes._subplots.AxesSubplot at 0x2070e36b390>
基于上述图就可以清楚地看到高人口密度的区域了.大多集中在海湾和城市.
接下来看房价特征:
In: 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,figsize=(12,7),legend=True,use_index=True)
Out: <matplotlib.axes._subplots.AxesSubplot at 0x2070e3d7358>
每个圈的半径表示街区的人口(选项s),颜色代表价格(选项c)。我们用预先定义的名为jet的颜色图(选项cmap),它的范围是从蓝色(低价)到红色(高价):
这张图说明房价和位置,还有人口密度联系密切. 这对于使用聚类算法去分析主要的簇可能是有帮助的,并且能够添加衡量是否邻近\'簇\'中心的新特征.
搜索关联性
数据集较小,可以轻易使用corr()方法计算出每对属性间的标准相关系数(也称作皮尔逊相关系数)
In: corr_matrix = housing.corr()
In: corr_matrix.shape,type(corr_matrix)
Out: ((9, 9), pandas.core.frame.DataFrame)
In: corr_matrix
相关系数的范围为-1到1. 越接近1,意味着强正相关;例如收入中位数越大,大概率上房价中位数也会很大.当相关系数越接近-1,意味着负相关越强.相关系数越接近0,越是意味着两种feature之间没有直接的线性关系.
上图表述了不同的分布图所描述的两种特征的相关程度.
需要注意的是:关联系数仅仅衡量了线性的关联,它可能完全无法刻画出非线性的关系.比如第3行,其实能看出特征之间还是有明显的关系的,只不过不是线性关系罢了.
In: corr_matrix[\'median_house_value\'].sort_values(ascending=False)
Out:
median_house_value 1.000000
median_income 0.687160
total_rooms 0.135097
housing_median_age 0.114110
households 0.064506
total_bedrooms 0.047689
population -0.026920
longitude -0.047432
latitude -0.142724
Name: median_house_value, dtype: float64
可以看到median_house_value 与median_income 的相关性是非常强的.
另一种检测属性间相关系数的方法是使用 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=(10,10))
seaborn中的pairplot有更好的相关系数图绘图效果
import seaborn as sns
attributes = ["median_house_value", "median_income", "total_rooms",
"housing_median_age"]
f1 = sns.pairplot(housing[attributes],diag_kind="kde")
其实对于预测median_house_value最优希望的特征就是median_income.
# 绘图查看median_house_value和median_income的相关程度
In: housing.plot(kind=\'scatter\',x=\'median_income\',y=\'median_house_value\',alpha=0.1)
Out: <matplotlib.axes._subplots.AxesSubplot at 0x20710c25160>
图中揭示了:
- median_income和median_house_value的关联确实非常强,可以看到明显的集中和上升变化趋势.
- 在图中可以看到不止一条value分层线,50000是预先设定,同时还能看到45000 . 35000等等. 这些对应的区域其实
可能是需要被去掉的,否则可能会造成错误的分布特点.
尝试特征组合
在为算法准备数据之前,还需要做的一个步骤就是尝试组合出各种各样的特征.将一些没有多大用处的特征利用起来,通过运算组合得到一些新的有意义的特征,这一步也是比较重要的,能够有效提升模型效果.
比如:
- 在本例中total_rooms这一属性直接利用是没有多少价值的,知道一个区域的总房间数似乎无用.
- 我们真正要用的上是每个家庭有多少间屋子.
- 同理,total_bedrooms也是没有意义的,可以构造出bedrooms_per_room属性.
- 同时population_per_household似乎也是一个有意思的属性.
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\']
housing.head()
完成组合,查看一下皮尔逊相关系数:
In: corr_matrix = housing.corr()
Out:
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
可以发现我们构造的bedrooms_per_room属性,还有rooms_per_household属性与目标的相关性是明显高于total_rooms和households/total_rooms的.说明构造还是比较成功的.
添加特征是一个不断迭代循环的过程,一旦模型得到提升,就可以再次分析其输出,拿到更多信息**并且返回这一探索的步骤.
为机器学习算法准备数据
数据清洗
数据清洗之前,需要再将strat_train_set复制一份.
并且需要将数据源和标签分开,因为两者进行的是不一样的操作.
housing = strat_train_set.drop(\'median_house_value\',axis=1)
housing_labels = strat_train_set[\'median_house_value\'].copy()
drop()方法创建了一个data的副本,同时还并不会影响strat_train_set
大多机器学习算法不能处理特征丢失,因此先创建一些函数来处理特征丢失的问题。前面,注意到属性total_bedrooms有一些缺失值。
针对这个问题有三个解决选项:
- 去掉对应的街区;
- 去掉整个属性;
- 进行赋值(0、平均值、中位数等等)
用DataFrame的dropna(),drop(),和fillna()方法,可以方便地实现:
housing.dropna(subset=[\'total_bedrooms\']) #dropna丢弃该行数据,指定到 total_bedrooms列去寻找缺失数据,axis默认为0-->行丢弃
housing.drop(\'total_bedrooms\',axis=1) # 去掉整个属性
median = housing[\'total_bedrooms\'].median()
housing[\'total_bedrooms\'].fillna(median) # 用中位数进行填充
scikit-learn 也提供了一个方便的类来处理缺失值:Imputer.用法如下:
- 首先需要创建一个Imputer实例,指定用中位数替换它的每个缺失值
In: from sklearn.preprocessing import Imputer
In: imputer = Imputer(strategy=\'median\')
In: imputer # imputer实例
Out: Imputer(axis=0, copy=True, missing_values=\'NaN\', strategy=\'median\', verbose=0)
只有数值属性才能计算出中位数,因此也需要创建一份不包括文本属性ocean_proximity的数据副本:
housing_num = housing.drop(\'ocean_proximity\',axis=1).copy() # 只有这样的样本才不会计算报错
现在,就可以用fit()方法将imputer实例拟合到训练数据:
In: imputer.fit(housing_num)
Out: Imputer(axis=0, copy=True, missing_values=\'NaN\', strategy=\'median\', verbose=0)
imputer实例在这个过程中简单计算了每一个属性的median value,并且存在了statistics_属性当中.
In: imputer.statistics_
Out: array([-118.51 , 34.26 , 29. , 2119.5 , 433. , 1164. ,408. , 3.5409])
In: housing_num.median().values
Out: array([-118.51 , 34.26 , 29. , 2119.5 , 433. , 1164. , 408. , 3.5409])
使用训练过的imputer对训练集进行转换变形
In: X = imputer.transform(housing_num)
In: X.shape,type(X)
Out: ((16512, 8), numpy.ndarray)
housing_num转换后变成了ndarray的格式,当然也可以放回DF中
housing_tr = pd.DataFrame(X,columns=housing_num.columns)
housing_tr.head()
处理文本和类别属性
在前面,为了处理缺失的数据值,我们去掉了属性ocean_proximity,因为文本属性并没有中位数.
为了让机器学习算法能利用文本标签,我们需要把文本转换为数字.
scikit-learn提供了LabelEncoder:
In: from sklearn.preprocessing import LabelEncoder
In: encoder = LabelEncoder()
In: housing_cat = housing[\'ocean_proximity\']
In: housing_cat_encoded= encoder.fit_transform(housing_cat)
In: housing_cat_encoded
Out: array([0, 0, 4, ..., 1, 0, 3])
转换后的数字量就可以用于任何ML模型的学习.同时还可以通过encoder.classes_查看数值和类别的mapping映射关系
In: encoder.classes_ # 查看分出的类别
Out: array([\'<1H OCEAN\', \'INLAND\', \'ISLAND\', \'NEAR BAY\', \'NEAR OCEAN\'],dtype=object)
LabelEncoder还不够好 ! 为什么?
对于LabelEncoder来说,其生成的标签数值并非是二值,本例中出现了0,1,2,3,4.将这样的数据喂给ML模型,模型会误认为类别的相似度由数值的大小影响.为了解决这样的问题,引入了one-hot解决方案.
One-Hot:
要更优地处理离散特征这还不够,Scikit-Learn 提供了一个编码器OneHotEncoder,用于将整数分类值转变为独热向量。结果是为每一个类别都创建了一个二值属性,值为\'1\'时,代表属于该类(hot),否则均为\'0\'(cold).
利用encoder.fit_transform()之前需要注意该方法用于处理2d数组,因此需要先将housing_cat_encoded变形.
In: from sklearn.preprocessing import OneHotEncoder
In: encoder = OneHotEncoder()
In: housing_cat_1hot = encoder.fit_transform(housing_cat_encoded.reshape(-1,1)) # 变形操作
In: housing_cat_1hot
Out: <16512x5 sparse matrix of type \'<class \'numpy.float64\'>\'with 16512 stored elements in Compressed Sparse Row format>
注意到上述的housing_cat_1hot结果是一个SciPy系数矩阵,而非一个Nunmpy array.在有成千上万的类别属性的时候这是非常有用的,可以节省很多空间,因为不用存储0.
以上的步骤也都可以用LabelBinarizer一步搞定,即从文本到整数分类,再转换成one-hot类
In: from sklearn.preprocessing import LabelBinarizer
In: encoder = LabelBinarizer(sparse_output = False)
In: housing_cat_1hot = encoder.fit_transform(housing_cat)
In: housing_cat_1hot
Out:
array([[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]])
自定义转换器
尽管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
# 这里的示例没有定义fit_transform(), 因为可以直接用fit 和 transform步骤达到同样的效果
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]
In: attr_adder = CombinedAttributesAdder(add_bedrooms_per_room=False)
In: housing_extra_attribs = attr_adder.transform(housing.values) # 返回一个加入新特征的数据集合
In: housing.shape
Out: (16512, 9)
In: housing_extra_attribs.shape
Out: (16512, 11)
在这个例子中,转换器只有一个超参数,即add_bedrooms_per_room这个布尔量,默认设置为True(提供一个明智的默认设置通常是很有帮助的).这个超参数会让你轻易地发现添加这个特征是否对ML算法的有好处.更一般的,你可以往gate中添加一个你在数据准备阶段不能完全确定其作用的超参数.数据准备步骤越是自动化,可以自动化解析出的特征组合就越多,这也使得你更可能找到一个非常好的特征组合(这将节省你大量时间)
这里需要区分一下fit() . transform(). 和fit_transform():
- fit()是很好理解的,就是训练拟合,学习到相关参数.
- transform()和fit_transform()看起来是非常相似,也是很容易混淆的,实质上两种有重大区别:
- fit_transform()用在训练集当中.是fit()和transform()的组合,可以直接完成两个步骤.
- transform()如上所述,只有转化的功能,用于测试集当中.但是这时不需要fit的原因是转换相关的参数都已经拿到了.
- fit()是后续所有api的先决条件
- fit_transform和transform的效果其实是没有区别的(正确使用的前提下).
此外,scikit-learn无法直接处理DataFrame,因此需要自定义一个方法能够实现向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 # .values实现取出所有的值, 为数组的形式
数据归一化
你需要对你数据进行的最重要的转化之一就是特征归一化.在输入特征有不同的量级的时候ML模型的表现通常不好.对于本数据集来说,同样存在这一的情况.
需要注意的是目标值value通常是不需要进行归一化的
有两种常见的方法可以让所有的属性有相同的量度:线性函数归一化(Min-Max scaling)和标准化(standardization)。
Min-Max scaling:
Min-Max scaling是一种非常简单的概念,可以将所有的值重新缩放至0,1的范围内.Scikit-Learn 提供了一个转换器MinMaxScaler来实现这个功能。它有一个超参数feature_range,可以让你改变范围,如果不希望范围是 0 到 1;Scikit-Learn 提供了一个转换器StandardScaler来进行标准化
Standardization:
Standardization就是另外一种完全不同的方法.
- 标准化不会将数值约束到一个特定的范围内,这对于一些特定的算法(比如神经网络)是不太合适的
- 标准化受到离群值的影响较小.相对而言,Min-Max scaling会受到更大的影响.
- 标准化后的结果分布不会有单位偏差
和所有的变换一样,scaler只能用训练数据进行训练,而不能整个数据集拟合,这是需要格外注意的地方.只有这样我们才能用它来对训练集和测试集进行变形.
from sklearn.preprocessing import MinMaxScaler,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()) # 数据归一化
])
In: housing_num_tr = num_pipeline.fit_transform(housing_num)
Pipeline构造器需要传入定义一系列步骤的名字/预测器对的列表.除开最后一个预测器,所有的预测器都需要是一个转换器(必须要有fit_transform()方法).管道的名字可以随意取.
当对pipeline使用fit()方法时,他会自动顺序调用所有转换器的fit_transform()方法,将每一个调用方法的输出结果作为参数传给下一个方法调用,直到run到了最后一个预测器,这个预测器只有fit()方法.
pipeline公开了和最终预测器相同的方法.在本例中最后一个预测器是StandardScaler,这是一个转换器,因此pipeline有一个顺序地将所有转换器运用到data上的transform()方法.(它还有一个fit_transform方法,这个方法可以代替fit()和transform()的顺序执行)
注意,pipeline最后一步如果有predict()方法我们才可以对pipeline使用fit_predict(),同理,最后一步如果有transform()方法我们才可以对pipeline使用fit_transform()方法。
为了将处理类别值的LabelBinary方法和先前处理numerical的pipeline进行组合,将这些变换组合到单个的管道中:Scikit-Learn提供了FeatureUnion类解决这个问题.
- 你可以传入一个转换器的列表(还可以是一整个转换器管道)
- 运行的时候管道会将每一个转换器的输出融合并返回.
from sklearn.pipeline import FeatureUnion
实际编码中,笔者遇到pipeline传参个数出错的问题,查阅资料后得出以下解决方案:
- The pipeline is assuming LabelBinarizer\'s fit_transform method is defined to take three positional arguments,This can be solved by making a custom transformer that can handle 3 positional arguments.
- Since LabelBinarizer doesn\'t allow more than 2 positional arguments you should create your custom binarizer like,solution2 is following
solution_1:
from sklearn.base import TransformerMixin # gives fit_transform method for free
class MyLabelBinarizer(TransformerMixin):
def __init__(self, *args, **kwargs):
self.encoder = LabelBinarizer(*args, **kwargs)
def fit(self, x, y=0):
self.encoder.fit(x)
return self
def transform(self, x, y=0):
return self.encoder.transform(x)
solution2:
class CustomLabelBinarizer(BaseEstimator, TransformerMixin):
def __init__(self, sparse_output=False):
self.sparse_output = sparse_output
def fit(self, X, y=None):
return self
def transform(self, X, y=None):
enc = LabelBinarizer(sparse_output=self.sparse_output)
return enc.fit_transform(X)
管道组合
In: num_attribs = list(housing_num)
In: cat_attribs = [\'ocean_proximity\']
In: num_pipeline = Pipeline([
(\'selector\',DataFrameSelector(num_attribs)),
(\'imputer\',Imputer(strategy = \'median\')),
(\'attribs_adder\',CombinedAttributesAdder()),
(\'std_scaler\',StandardScaler()),
])
In: cat_pipeline = Pipeline([
(\'selector\',DataFrameSelector(cat_attribs)),
(\'label_binarizer\',CustomLabelBinarizer()),
])
In: full_pipeline = FeatureUnion(transformer_list=[
(\'num_pipeline\',num_pipeline),
(\'cat_pipeline\',cat_pipeline),
])
In: housing.shape
Out: (16512, 9)
In: housing_prepared = full_pipeline.fit_transform(housing)
In: housing_prepared.shape
Out: (16512, 16) # 已经在管道中完成转换
In: type(housing_prepared) #housing_prepared是一个ndarray的形式
Out: numpy.ndarray
每一个子管道都从选择转换器开始:完成的任务是挑选需要的目标属性,抛弃其他的属性,将DataFrame转换成Numpy array.在scikit-learn中没有任何处理DataFrame的函数,因此我们需要自己写一个简单的转换器.
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 # 保证返回的是一个array
注释以下概念:
每一个scikit-learn中的扩展都需要继承特定的sklearn.base下的包的类:
- BaseEstimator 估计器的基类
- ClassifierMixin 分类器的混合类
- ClusterMixin 聚类器的混合类
- RegressorMixin 回归器的混合类
- TransformerMixin 转换器的混合类
创建了DataFrameSelector类继承了BaseEstimator,TransformerMixin意味着它的基本功能就是单一估计器和转换器的组合
模型选择与训练
设计问题 , 拿到数据+瞥一眼 , 取出测试集与训练集 , 运用转换管道清洗数据 . 接下来就该挑选并训练ML模型
首先训练一个线性回归的模型.
In: from sklearn.linear_model import LinearRegression
In: lin_reg = LinearRegression()
In: lin_reg.fit(housing_prepared, housing_labels)
Out: LinearRegression(copy_X=True, fit_intercept=True, n_jobs=None,normalize=False)
In: housing_prepared.shape
Out: (16512, 16)
现在已经拿到一个有效的线性回归模型了.
为了瞄一眼效果,可以从训练集中取一些数据拿来尝试
In: some_data = housing_prepared[:5]
In: some_data.shape
Out: (5, 16)
In: some_labels = housing_labels.iloc[:5]
In: \'Predictions:\t\',lin_reg.predict(some_data)
Out: (\'Predictions:\t\', array([210644.60459286, 317768.80697211, 210956.43331178, 59218.98886849,189747.55849879]))
In: \'Predictions:\t\',list(some_labels)
Out: (\'Predictions:\t\', [286600.0, 340600.0, 196900.0, 46300.0, 254500.0])
iloc和loc的区分::
在pandas中loc和iloc都能实现对目标数据的准确索引.但实质上是有区别的:
- loc[\'a\',\'b\' ]中填入的是行列位置标签
- iloc[a,b] 中填入的索引
我们拿到了我们的预测值,尽管偏差还是蛮大的.
我们可以用RMSE指标来对整个训练集的效果进行一个评价
In: from sklearn.metrics import mean_squared_error
In: housing_predictions = lin_reg.predict(housing_prepared)
In: lin_mse = mean_squared_error(housing_labels,housing_predictions)
In: lin_rmse = np.sqrt(lin_mse)
In: lin_rmse
Out: 68628.19819848922
由统计直方图看出,median_housing_values大多数都分布在12000 - 265000之间,因此目前这个lin_reg模型的RMSE达到了68628,这是很不理想的.
这是欠拟合的情况发生了(在训练集上查看出来拟合的效果):
- 发生这种情况告诉我们特征并没有提供足够多的信息.
- 模型并不足够好
面对这种情况时,我们解决欠拟合的主要方法是:
- 挑选一个更加强大的模型,并喂更好的特征
- 减少对于模型的约束(比如正则化).
但是我们这个模型并没有正则化,因此只能采取第一种方法进行优化.
我们可以尝试添加更多的特征,比如取人口的对数.
下面尝试训练一种更加强大的回归模型,决策树回归模型DecisionTreeRegressor.这种模型能够发现复杂的非线性关系.
In: from sklearn.tree import DecisionTreeRegressor
In: tree_reg = DecisionTreeRegressor()
In: tree_reg.fit(housing_prepared,housing_labels)
Out: DecisionTreeRegressor(criterion=\'mse\', max_depth=None, max_features=None,
max_leaf_nodes=None, min_impurity_decrease=0.0,
min_impurity_split=None, min_samples_leaf=1,
min_samples_split=2, min_weight_fraction_leaf=0.0,
presort=False, random_state=None, splitter=\'best\')
In: housing_predicions = tree_reg.predict(housing_prepared)
In: housing_predicions[:5]
Out: array([286600., 340600., 196900., 46300., 254500.])
In: tree_mse = mean_squared_error(housing_labels,housing_predicions)
In: tree_rmse = np.sqrt(tree_mse)
In: tree_rmse
Out: 0.0
可以发现运用DT模型的时候,这个模型竟然没有error,模型几乎是完美的.但是在训练集上有这样的表现,这往往是严重过拟合的表现.
因此就如前述,对于一个模型而言,千万不能让它去碰测试集.你需要用训练集的一部分去做训练,另一部分做模型验证.因为有些强大的模型能够完美地直接拟合已经训练的数据.
使用交叉验证做出更好的评估
一种评估DT模型的方法就是使用train_test_split方法将训练集分为更小的训练集和验证集,然后用小的训练集和验证集去训练并验证模型.
一种很好的替代方法就是使用scikit-learn的交叉验证功能,这样的验证方法更加可靠.下面的代码演示了K折交叉验证:
- 它随机将训练集拆分为10个独立的子集,称之为\'折\'
- 然后利用10个子集训练并评估10个不同的DT模型
- 每次都选用一个不同折用于评估,利用剩下的9个折进行训练.
- 结果是包含10个评估结果的array
In: from sklearn.model_selection import cross_val_score
In: scores =cross_val_score(tree_reg,housing_prepared,housing_labels,scoring=\'neg_mean_squared_error\'
,cv=10)
In: rmse_scores = np.sqrt(-scores)
In: rmse_scores
Out: array([69166.55059074, 66543.54599875, 71015.98771317, 69322.34433533,
70674.92662341, 75050.13558149, 70625.34940601, 70823.47513762,
76224.34231131, 70124.08527976])
注意:scikit-learn交叉验证期望的是一个效用函数(越大越好)而不是损失函数(越小越好),因此得分函数通常是MSE值的负值.这也是为什么在开根之前要先取-scores.
def display_scores(scores):
print(\'Scores:\',scores)
print(\'Mean:\',scores.mean())
print(\'Standard deviation:\',scores.std())
In: display_scores(rmse_scores)
Out:
Scores: [69166.55059074 66543.54599875 71015.98771317 69322.34433533,
70674.92662341 75050.13558149 70625.34940601 70823.47513762,76224.34231131 70124.08527976]
Mean: 70957.07429775785
Standard deviation: 2660.0686304080364
利用了更加科学的方法:\'交叉验证\'(能真正看出效果),看上去似乎DT的表现并不如先前那么好.事实上,看起来比线性回归模型还要糟糕!
注意到交叉验证不仅让你得到模型性能的评估,并且还能看到模型有多么准确(标准差). 决策树的平均分大概为71400,波动通常在3200上下.如果你只有一个验证集你将得不到这些信息.
但是交叉验证的消耗在于多次训练模型,这样的代价并不是在任何情况下都能接受.
下面计算一下线性回归模型的分数:
In: lin_scores = cross_val_score(lin_reg,housing_prepared,housing_labels,scoring=
\'neg_mean_squared_error\',cv=10)
In: lin_rmse_scores = np.sqrt(-lin_scores)
In: display_scores(lin_rmse_scores)
Out: Scores: [66782.73843989 66960.118071 70347.95244419 74739.57052552
68031.13388938 71193.84183426 64969.63056405 68281.61137997 71552.91566558 67665.10082067]
Mean: 69052.46136345083
Standard deviation: 2731.674001798348
可以看到DT模型过拟合验证集,表现甚至是要比线性回归模型更差的
接下来尝试最后一个模型:随机森林.
随机森林的原理是用随机的特征子集训练大量的DT模型.然后基于大量的DT模型做出决策.这样的思路是提升ML算法性能的一个重要方法.
In: from sklearn.ensemble import RandomForestRegressor
In: forest_reg = RandomForestRegressor()
In: forest_reg.fit(housing_prepared,housing_labels)
In: forest_score = cross_val_score(forest_reg,housing_prepared,housing_labels,scoring=
\'neg_mean_squared_error\',cv=10)
In: forest_rmse_scores = np.sqrt(-forest_score)
In: display_scores(forest_rmse_scores)
Out: Scores: [52139.11827562 49499.22947406 52429.83310745 55613.43640446
52186.19472032 56245.76235768 51450.03759208 51103.18141115
56223.7137723 53716.20236911]
Mean: 53060.67094842296
Standard deviation: 2195.8523393951145
计算随机森林的rmse得分:
In: housing_predicions = forest_reg.predict(housing_prepared)
In: forest_rmse_scores = np.sqrt(mean_squared_error(housing_labels,housing_predicions))
In: forest_rmse_scores
Out: 22723.911950422345
我们可以看到随机森林的效果提升明显,是一个很有希望的方法.但是训练集上的结果依然比验证集上的得分小非常多,这说明模型依然是过拟合的.
为了解决过拟合,可以采取以下的措施:
- 简化模型,采用正则化的方式限制模型
- 使用更多的训练数据(防止过拟合,被部分数据带偏)
在深入随机森林之前,你应该尝试下机器学习算法的其它类型模型(不同核心的支持向量机,神经网络,等等),不要在调节超参数上花费太多时间。目标是列出一个可能模型的列表(两到五个)。
模型微调
假定你已经有了好几个有希望的模型,那么你需要如何去调整优化它们呢?
<简书不支持html这种标记方式>网格搜索(调参神器) <我能怎么办,我只能手动标注重点了