OReilly.Hands-On Machine Learning with Scikit-Learn and TensorFlow读书笔记
Chapter 2 End-to-End Machine Learning Project
需要掌握
1、获取数据
a)下载tgz文件,在本地解压为csv格式文件
import os
import tarfile
from six.moves import urllib
#远程网站根目录
DOWNLOAD_ROOT = "https://raw.githubusercontent.com/ageron/handson-ml/master/"
#存放csv文件的目录,同时作为本地存放csv文件的目录
HOUSING_PATH = "datasets/housing"
#远程压缩文件全路径=网站根目录+存放csv文件的目录+压缩文件名
HOUSING_URL = DOWNLOAD_ROOT + HOUSING_PATH + "/housing.tgz"
def fetch_housing_data(housing_url=HOUSING_URL, housing_path=HOUSING_PATH):
#将压缩文件全路径 和 本地存放csv文件目录 作为参数传入函数
#本地存放csv文件目录不存在,则创建
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)
#打开文件,并提取(解压)到 本地存放csv文件目录,然后关闭文件
housing_tgz = tarfile.open(tgz_path)
housing_tgz.extractall(path=housing_path)
housing_tgz.close()
fetch_housing_data()
b) 用pandas.read_csv()读取csv数据,返回一个pandas.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()
c) 查看pandas.DataFrame对象信息:
housing.head()#查看前5条记录
housing.info()#查看每列信息,包括column name,number of non-null values,datatype
housing.describe()#每列数据的详细统计信息,包括count, mean, std, min, 25%, 50%, 75%, max of each column
housing["ocean_proximity"].value_counts()#查看每列不同值的统计信息
d)可视化,以对数据有个初步的感性认识
%matplotlib inline
import matplotlib.pyplot as plt
#为每个数值属性绘制直方图,50个数据刻度,每张图的大小是(20,15)
#housing数据集有9个数值属性
housing.hist(bins=50,figsize=(20,15));
e) 分割数据,得到训练集和测试集
Scikit-Learn 提供了一些函数,可以用多种方式将数据集分割成多个子集。最简单的函数
是sklearn.model_selection.train_test_split()函数,将原始数据集分割为训练集和测试集。其中,
test_size指示测试集占原始数据集的比例,
random_state=42指示随机参数(指定相同的种子参数,则每次分割得到相同结果)
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 类:
#先为数据集增加一列income_cat,值为median_income/1.5取整,值超过5.0的取5.0
housing["income_cat"]=np.ceil(housing["median_income"]/1.5)
housing["income_cat"].where(housing["income_cat"]<5, 5.0, inplace=True)
#然后根据income_cat列的不同值所占比例采样,避免采样偏差
from sklearn.model_selection import StratifiedShuffleSplit
#n_splits=1表示shuffle的次数
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]
#最后,把income_cat列从训练集和测试集中删掉,inplace=True表示在原始数据集上直接操作
for set in (strat_train_set,strat_test_set):
set.drop(["income_cat"],axis=1, inplace=True)
2、通过可视化数据发现规律
a) 绘制散点图,从直观上猜测数据属性之间的关联
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();
b) 查找关联
使用 corr() 方法计算出每对属性间的标准相关系数(standard correlation coefficient,也称作皮尔逊相关系数) :
corr_matrix = housing.corr()
corr_matrix["median_house_value"].sort_values(ascending=False)
另一种检测属性间相关系数的方法是使用 Pandas 的 scatter_matrix 函数,它能画出一个数
值属性集合中每对数值属性的关系图。
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))
c) 属性组合实验
尝试衍生属性,即从现存属性的组合中得到新的相关属性,再计算这些属性与原来属性之间的关联。
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)
3、为机器学习算法准备数据
a) 将训练集分割为预测量和标签,即X和y
housing = strat_train_set.drop("median_house_value", axis=1)
housing_labels = strat_train_set["median_house_value"].copy()
b) 处理缺失值
属性total_bedroom存在缺失值,三种解决方式:
- 去掉对应的街区,即去掉包含缺失值的行
- 去掉整个属性,即去掉包含缺失值的列
- 填充,用0、均值、中位数填充缺失值
housing.dropna(subset=["total_bedrooms"]) # 选项1,如果在原始数据集上修改,加上inplace=True选项
housing.drop("total_bedrooms", axis=1) # 选项2
median = housing["total_bedrooms"].median()#先计算中位数
housing["total_bedrooms"].fillna(median) # 选项3
更简便的方式是Scikit-Learn的类Imputer,可以以多种策略strategy来填充缺失值
from sklearn.preprocessing import Imputer
imputer = Imputer(strategy="median")
麻烦的地方是,需要把非数值属性ocean_proximity剔除,填充之后,再合并回来。
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
事实上,imputer.statistics_中存放了所有(数值)属性的中位数
>>>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])
c) 将非数值(文本和分类)属性值转化为数值
利用Scikit-Learn的LabelEncoder类可以实现,但缺点是只能处理一个非数值属性,多个非数值属性需要分别处理,再合并。顾名思义,LabelEncoder设计目的也是处理数据集中的标签label。
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])
可以通过LabelEncoder的classes_属性查看学习到的映射(<1H OCEAN 被映射为 0, INLAND 被映射为 1,等等),将n个不同的非数值属性值转化为0到n-1之间的数字。
>>> print(encoder.classes_)
['<1H OCEAN' 'INLAND' 'ISLAND' 'NEAR BAY' 'NEAR OCEAN']
缺点是映射为数字之后,数字之间的距离不一定能反映出非数值属性值之间的近似度。例如,0和4所代表的分类'<1H OCEAN'
和'NEAR OCEAN'
之间的近似度远大于0和1所代表的的分类'<1H OCEAN'
和'INLAND'
。
One-Hot编码可以部分解决这个问题:将不同的非数值属性值对应的数字映射为向量,向量之间彼此正交,不存在哪两个向量更近似(近似度都为0),但没有解决相似非数值属性值之间语义近似的问题(可以通过训练embedding解决)。
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 '<class 'numpy.float64'>'
with 16513 stored elements in Compressed Sparse Row format>
>>> 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 ,就可以得到一个稀疏矩阵。
d) 自定义转换器
可以自定义转换器(transformer)来执行前面提到的一些任务,例如缺失值填充、组合属性得到新属性,将非数值属性转换为数值属性。
如果希望自定义的转换器可以与现有的Scikit-Learn组件(例如pipeline)无缝连接,就需要创建一个类,并实现fit(),transfrom()和fit_transform()方法。实现fit()用于与前一个组件连接,实现transfrom()用于与下一个组件相连。将TransformerMixin 作为基类,自动得到fit_transform()方法。添加 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)
#得到的housing_extra_attribs是numpy.ndarray类型。下面代码将其转换为DataFrame类型
housing_extra_dataframe= pd.DataFrame(housing_extra_attribs,
columns=housing.columns.append(Index( ["rooms_per_household","population_per_household"])))
#查看转换是否成功
housing_extra_dataframe.head()
e) 特征缩放
目的是让每个特征具有相同的量纲(**问题是,one-hot向量如何缩放才能与其他属性值量纲相同?**答案是one-hot向量占据多列,每一列或是1或是0。也就是说,把向量的每个分量看作一个单独的属性)。两种方式:线性函数归一化(Min-Max scaling)和
标准化(standardization)。
线性函数归一化(或归一化normalization):通过减去最小值,然后再除以最大值与最小值的差值,来进行归一化。Scikit-Learn 提供了一个转换器 MinMaxScaler来实现这个功能。如果不希望范围是 0 到 1,可以利用超参数 feature_range指定范围。
from sklearn.preprocessing import MinMaxScaler
scaler=MinMaxScaler(feature_range=(0,2))#可以指定特征的值的范围
scaler.fit_transform(np.array(housing["total_rooms"]).reshape(-1,1))
#只接受二维ndarry,而housing["total_rooms"]是Series类型
标准化:减去平均值,除以方差。标准化不会限定值到某个特定的范围,这对某些算法可能构成问题(比如,神经网络常需要输入值得范围是 0 到 1) 。但是,标准化受到异常值的影响很小。例如,假设一个街区的收入中位数由于某种错误变成了100,归一化会将其它范围是 0 到 15 的值变为 0-0.15,但是标准化不会受什么影响。Scikit-Learn 提供了一个转换器 StandardScaler 来进行标准化。
f) 转换流水线
许多数据转换由一系列子转换构成。Scikit-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())
])
#type(housing_num)是numpy.ndarray
housing_num_tr=num_pipeline.fit_transform(housing_num)
Pipeline 构造器的list类型参数定义了名字/估计器对序列。除了最后一个估计器,其余还必须是转换器(如前所述,转换器都是估计器)。调用流水线的 fit() 方法,就会对所有转换器顺序调用 fit_transform() 方法,将每次调用的输出作为参数传递给下一个调用,一直到最后一个估计器,它只执行 fit() 方法。上面例子中最后的估计器是一个 StandardScaler ,它也是一个转换器,因此这个流水线有一个 transform() 方法,可以顺序对数据做所有转换。
这个流水线用于处理数值型属性,还需要一个Pipeline处理分类型属性。
然后,使用Scikit-Learn中FeatureUnion类将两个Pipeline的输出合并起来。一个完整的处理数值和类别属性的流水线如下所示:
from sklearn.pipeline import FeatureUnion
from sklearn.preprocessing import OneHotEncoder
num_attribs = list(housing_num)
cat_attribs = ["ocean_proximity"]
num_pipeline = Pipeline([
('selector', DataFrameSelector(num_attribs)),
('imputer', SimpleImputer(strategy="median")),
('attribs_adder', CombinedAttributesAdder()),
('std_scaler', StandardScaler()),
])
cat_pipeline = Pipeline([
('selector', DataFrameSelector(cat_attribs)),
('1hot_encoder', OneHotEncoder()),
])
full_pipeline = FeatureUnion(transformer_list=[
("num_pipeline", num_pipeline),
("cat_pipeline", cat_pipeline),
])
其中,
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
>>> 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)
与书上结果不同,我的结果是(16512,16)
4、选择和训练模型
a) 在训练集上训练和评估
#导入线性回归模型
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]
#用pipeline预处理测试数据
some_data_prepared=full_pipeline.transform(some_data)
#打印出测试结果,与标签对比
print("Predictions:\t", lin_reg.predict(some_data_prepared))
print("Labels:\t\t", list(some_labels))
#计算模型的RMSE(Root Mean Squared 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
#导入决策树回归模型
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_mse
b) 交叉验证
K 折交叉验证(K-fold cross-validation):
Scikit-Learn 交叉验证功能期望的是效用函数(越大越好) 而不是损失函数(越低越好) ,因此得分函数实际上与 MSE 相反(即负值) ,这就是为什么前面的代码在计算平方根之前先计算 -scores 。
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)
#定义一个显示结果的函数
def display_scores(scores):
print("Scores:",scores)
print("Mean:",scores.mean())
print("Standard deviation:",scores.std())
display_scores(rmse_scores)
#导入随机森林回归模型
from sklearn.ensemble import RandomForestRegressor
forest_reg=RandomForestRegressor()
forest_reg.fit(housing_prepared,housing_labels)
forest_predictions=forest_reg.predict(housing_prepared)
forest_mse=mean_squared_error(housing_labels,forest_predictions)
forest_rmse=np.sqrt(forest_mse)
print(forest_rmse)
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)
c) 保存和载入模型
from sklearn.externals import joblib
joblib.dump(forest_reg,"forest_reg.pkl")
forest_reg=joblib.load("forest_reg.pkl")
d) 模型微调
#网格搜索
from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import RandomForestRegressor
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)
#结论
grid_search.best_params_
grid_search.best_params_
cvres=grid_search.cv_results_
for mean_score, params in zip(cvres["mean_test_score"],cvres["params"]):
print(np.sqrt(-mean_score),params)
#分析最佳模型和它们的误差
feature_importances=grid_search.best_estimator_.feature_importances_
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)
#随机搜索
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import RandomizedSearchCV
param_rand = {
'n_estimators':range(30,200,4),
'max_features':range(2,16,2),
'bootstrap':[False,True]
}
ran_search=RandomizedSearchCV(forest_reg, param_rand,
scoring='neg_mean_squared_error',
cv=10,n_iter=300)
ran_search.fit(housing_prepared,housing_labels)
5、用测试集评估系统
#集成方法
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)