实验目标:选择加州房价数据集,基于1990年加州人口普查的数据,添加了一个分类属性,并且移除了一些特征。模型需要从这个数据中学习,从而能够根据其他指标,预测任意区域的房价中位数。
工作环境选择的Jupyter notebook,使用python3.0版本,安装Python模块:Jupyter、Numpy、Pandas、Matplotlib和Scikit-Learn。
import pandas as pd
housing = pd.read_csv("C:/Users/10489/Desktop/datasets/housing/housing.csv")#这里大家就写自己下载好的数据集地址
housing.head()#默认是5行
每一行都表示一个分区,每一列代表一个属性:经度、维度、房屋年龄中位数、总房间数、卧室数量、人口数、家庭数、收入中位数、房屋价值中位数、大海距离。
housing.info()
info()方法可以快速查看数据的描述,包括总行数、每个属性的类型和非空值的数量。可以看出来,所有的属性都是数值的,只有第十项大海距离这项不是数值,它是文本。通过上面前五行的表格数据,可以看出其中有的列是相同的、重复的,意味着它可能是一个分类属性,它可以使用value_counts()的方法查看都有什么类型,每个类型有几个分区:
housing["ocean_proximity"].value_counts()
可以看出有五个类别,每个类别有9139、6551、2658、2290、5个分区。
housing.describe()
describe()方法展示了数值属性的概括。std表示标准差。25%、50%、75%展示了对应的分位数:每个分位数指明小于这个值,且指定分组的百分比。例如,25%的分区的房屋年龄中位数小于18,而50%的小于29,75%的小于37。
import matplotlib.pyplot as plt
housing.hist(bins=55, figsize=(20, 15))
plt.show()
这是快速了解数据类型的一种方法,画出每个数值属性的柱状图。纵轴表示了特定范围的实例个数。
from sklearn.model_selection import train_test_split
train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)
做一个分层处理
import numpy as np
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])
分层采样:将人群分成均匀的子分组,称为分层,从每个分层取出合适数量的实例,以保证测试集对总人数有代表性。分层不宜过多,这里分了5层。
housing.head()
housing["income_cat"].hist()
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]
按照收入类别划分训练集和测试集。
strat_train_set["income_cat"].value_counts() / len(strat_train_set)
strat_test_set["income_cat"].value_counts()/len(strat_test_set)
strat_train_set.drop('income_cat',axis=1,inplace=True)
strat_test_set.drop('income_cat',axis=1,inplace=True)
将income_cat这个属性删除,使得数据回到最初的状态,这一部分我们只是熟悉一下这个数据集,观察一下数据集,并没有开始搭建模型。
housing = strat_train_set.copy()
我们要保证我们将测试集放在了一旁,只研究了训练集。这一步呢是做了一个复制,是因为这个数据集很小,所以我们可以在这个数据集上直接进行操作,复制下来,以免损伤训练集。
housing.plot(kind='scatter',x='longitude',y='latitude',alpha=0.1)
因为数据中存在着地理信息,比如经度纬度,所以创建一个横纵坐标为经纬度的散点图。可以清楚地看到高密度区域,湾区、洛杉矶和圣迭戈,以及中央谷,特别是从萨克拉门托到弗雷斯诺。
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()
通过颜色的变化来观察房价,每个圈的半径s表示分区的人口数目,颜色c表示价格。用预先定义的颜色图(cmap)jet,右侧从蓝色到红色是从低价到高价的过程。这张图说明了房价和位置(比如:靠海)和人口密集联系密切。可以使用聚类算法来检测主要的聚集,用一个新的特征值测量聚类中心的距离。
(1)简单的查找关联的方法是使用corr()的方法来计算出没对属性间的标准相关系数(皮尔逊相关系数),适用于数据集不是很大的情况下。
corr_matrix = housing.corr()
corr_matrix["median_house_value"].sort_values(ascending=False)
相关系数的范围是(-1,1),当接近1时,意味强正相关;当相关系数接近-1的时候,意味着强负相关;相关系数接近0,意味着没有线性相关性。
(2)但数据集较大的情况下,利用Pandas的scatter_matrix函数来寻找相关关系,它能画出每个数值属性对每个其他数值属性的图。
from pandas.plotting import scatter_matrix
attributes = ["median_house_value", "median_income", "total_rooms",
"housing_median_age"]
scatter_matrix(housing[attributes], figsize=(12, 8))
因为现在有11个数值属性,图的数量太多,所以只关注几个和房价中尉最有可能相关的属性。如果pandas将每个变量对自己作图,主对角线(左上到右下)都会是直线图,所以pandas展示的是每个属性的柱状图。
housing.plot(kind="scatter", x="median_income", y="median_house_value", alpha=0.1)
通过观察,发现最有希望预测房价中位数的属性是收入中位数,因此将这张图放大。这张图说明了几点问题,首先,相关性非常高;可以清晰地看到向上的趋势,丙炔数据点不是非常分散;第二,这张图上不仅清晰的呈现了一条位于500000美金的水平线,还呈现了别的不是那么明显的直线,450000、350000、280000和更靠下的线。我们应该去除对应的分区,以防止这种过拟合的现象。
给算法准备数据之前,我们需要做的最后一件事,就是尝试多种属性组合。比如我们不知道某个分区有多少户,该分区的总房间数就没什么用,我们真正需要的是每户有几个房间,或者是将人数与房间数做一个组合等等。
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)
通过这个结果可以看出,与总房间数相比,新的bedrooms_per_room属性与房价中位数的关联更强。显然,卧室数/总房间数的比例越低,房价就越高。每户的房间数也比分区的总房间信息更明显,房屋越大,房价就越高。
将训练集和测试集分开。
housing_train = strat_train_set.drop('median_house_value',axis=1)
housing_label = strat_train_set['median_house_value'].copy()
这是一个准备工作,为后续模型做准备,前面都是分析数据集的过程。
大多数机器学习算法不能处理特征丢失,因此我们应该先创建一些函数来处理特征丢失的问题。这里我们选择sklearn来解决问题。
from sklearn.impute import SimpleImputer
housing_num = housing_train.drop('ocean_proximity',axis=1)
imputer = SimpleImputer(strategy='median')
X = imputer.fit_transform(housing_num)
housing_tr = pd.DataFrame(X, columns=housing_num.columns, index=housing_num.index)
sklearn中的Imputer是处理缺失值的类,我们要取的值是中位数,所以一定是数值属性才可以取到中位数,所以这里我们要刨除ocean_proximity的数据副本。我们要确保所有的属性都没有缺失值,所以要fit一下。使用这个训练过后的imputer来对训练集进行转换,通过将缺失值替换为中位数。上面的过程都是numpy数组,我们要是想把它显示出来要变成dataframe的形式。
housing_cat = housing_train[['ocean_proximity']]
from sklearn.preprocessing import OrdinalEncoder
ordinal_encoder = OrdinalEncoder()
housing_cat_encoded = ordinal_encoder.fit_transform(housing_cat)
print(housing_cat_encoded[:10])
类别转化需要二维数据,所以housing_cat.value是2维的,有两个[]。后面是将文本属性进行数字化编码。
from sklearn.preprocessing import OneHotEncoder
cat_encoder = OneHotEncoder()
housing_cat_one_hot = cat_encoder.fit_transform(housing_cat)
print(cat_encoder.categories_)
print(housing_cat_one_hot.toarray())
将文本属性转换成稀疏矩阵。稀疏矩阵:one_hot编码后只记住1的位置,提高了运算效率
虽然sklearn中提供了很多的转换量,但是我们还是需要自己手写一个,比如自定义清理操作或者属性组合。我们需要让自制的转换量与sklearn中组件(比如Pipeline)无缝衔接,因为sklearn是依赖鸭子类型的,不是继承的,所以我们需要做的就是创建一个类并执行三个方法:fit()、transform()、fit_transform()。通过添加TransformerMixin作为基类,可以很容易得到最后一个。另外,如果我们添加BaseEstimator作为基类,我们就能够得到两个额外的方法(get_params()和set_params()),二者可以方便地进行超参数自动微调。
from sklearn.base import BaseEstimator,TransformerMixin
rooms_id,bedrooms_id,population_id,households_id = 3,4,5,6
class CombineAttributesAdder(BaseEstimator,TransformerMixin):
def __init__(self,add_bedrooms_per_room=True):
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_id]/X[:,households_id]
population_per_household = X[:,population_id]/X[:,households_id]
if self.add_bedrooms_per_room:
bedrooms_per_room = X[:,bedrooms_id]/X[:,rooms_id]
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 = CombineAttributesAdder(add_bedrooms_per_room=False)
housing_extra_attr = attr_adder.transform(housing_train.values)
如果想要添加bedrooms_per_room这个属性,就把False改为True。
除了个别情况,当输入的数值属性量度不同的时候,机器学习算法的性能都不会好。这个规律也适用于房产数据。有两种常见的方法可以让所有的属性有相同的量度:线性函数归一化(Min-Max scaling)和标准化(standardization)。
线性函数归一化:值被转变、重新缩放,直到范围变成0到1。我们通过减去最小值,然后除以最大值与最小值的差值,来进行归一化。sklearn中提供了一个转换量MinMaxScaler来实现这个功能,它有一个超参数feature_range,可以让我们改变范围,如果不希望这个范围是0到1的话。
标准化:首先减去平均值,然后除以方差,是得到的分布具有单位方差。与归一化不同,标准化不会限定值到某个特定的范围,这对某些算法可能会有些问题(比如神经网络常常需要输入值为(0,1))。但是标准化受到异常值的影响很小,sklearn中提供了一个转换量StandardScaler来进行标准化。
将替换缺失值、添加属性、替换文本值的操作结合到一起。
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
num_pipline = Pipeline([
('imputer',SimpleImputer(strategy='median')), # 使用中位数补充缺失值
('attr_adder',CombineAttributesAdder()), # 添加额外的数值属性
('std_scaler',StandardScaler()) # 将数值属性标准化
])
housing_num_str = num_pipline.fit_transform(housing_num)
这一部分是将数值那些写进去,下面是将文本的写进去
# ColumnTransformer 返回一个密集矩阵或者是稀疏矩阵
# 默认情况下,未指定的列将会被删除
from sklearn.compose import ColumnTransformer
# list(housing_num)返回的是list类型的列名
num_attribs = list(housing_num)
cat_attribs = ['ocean_proximity']
full_pipeline = ColumnTransformer([
("num",num_pipline,num_attribs), # 密集矩阵 num_pipline 是上面提到的代码
("cat",OneHotEncoder(),cat_attribs) # 稀疏矩阵 文本类型的one_hot处理
])
查看结果。
housing_prepare = full_pipeline.fit_transform(housing_train)
print(pd.DataFrame(housing_prepare))
结果中的8、9、10都是添加的属性,就是上面说的组合的属性。可以带入模型进行预测分析。
(1)线性回归模型
from sklearn.linear_model import LinearRegression
lin_reg = LinearRegression() #线性回归
lin_reg.fit(housing_prepare, housing_label) #拟合
from sklearn.metrics import mean_squared_error
housing_predictions = lin_reg.predict(housing_prepare)
lin_mse = mean_squared_error(housing_label, housing_predictions)
lin_rmse = np.sqrt(lin_mse)
lin_rmse
从数据中看,大多数分区的median_housing_values位于120000美元到265000美元之间,因此这个结果68628.2不能让人满意。这个模型是欠拟合训练数据的例子。当这种情况发生的时候,意味着特征没有提供足够多的信息来做出一个好的预测,或者说是模型不够强大,学习不到位。之前说过,修复欠拟合的主要方法是选择一个更强大的模型,给训练算法提供更好的特征,或者去除模型上的限制。对于模型限制这项,由于这个模型还没有正则化,所以不存在这个选项。为了解决这个欠拟合的问题,我们可以选择尝试添加更多特征,或者尝试一个复杂的模型。
(2)决策树回归
from sklearn.tree import DecisionTreeRegressor
tree_reg = DecisionTreeRegressor(random_state=42)
tree_reg.fit(housing_prepare, housing_label)
housing_predictions = tree_reg.predict(housing_prepare)
tree_mse = mean_squared_error(housing_label, housing_predictions)
tree_rmse = np.sqrt(tree_mse)
tree_rmse
评估决策树模型的一种方法是用函数train_test_split来分割训练集,得到一个更小的训练集和一个验证集,然后用更小的训练集来训练模型,用验证集来评估。这个方法很简单,但是任务量很大。
另一种方法就是用sklearn中的交叉验证功能。采用k折交叉验证(K-foldcross-validation):它随机地将训练集分成十个不同的子集,称为“折”,然后训练评估决策树模型10次,每次选一个不用的折来做评估,其他的9个来做训练。结果是一个包含10个评分的数组:
from sklearn.model_selection import cross_val_score
scores = cross_val_score(tree_reg, housing_prepare, housing_label,
scoring="neg_mean_squared_error",cv=10)
tree_rmse_scores = np.sqrt(-scores)
决策树的结果:
def display_scores(scores):
print("Scores:", scores)
print("Mean:", scores.mean())
print("Standard deviation:", scores.std())
display_scores(tree_rmse_scores)
lin_scores = cross_val_score(lin_reg, housing_prepare, housing_label,
scoring = 'neg_mean_squared_error", cv=10)
lin_rmse_scores = np.sqrt(-lin_scores)
display_scores(lin_rmse_scores)
通过结果,可以确定之前的判断没有错误,决策树模型过拟合很严重,它的性能比线性回归模型还差。
from sklearn.ensemble import RandomForestRegressor
forest_reg = RandomForestRegressor()
forest_reg.fit(housing_prepare, housing_label)
housing_predictions = forest_reg.predict(housing_prepare)
forest_mse = mean_squared_error(housing_predictions, housing_label)
forest_rmse = np.sqrt(forest_mse)
print(forest_rmse)
forest_scores = cross_val_score(forest_reg, housing_prepare, housing_label, scoring='neg_mean_squared_error',cv=10)
forest_rmse_scores = np.sqrt(-forest_scores)
display_scores(forest_rmse_scores)
随机森林看起来很有希望。但是训练集的评分仍然比验证集的评分低很多。解决过拟合可以通过简化模型,给模型加限制(规整化),或用更多的数据集进行训练。
import joblib
joblib.dump(lin_reg,"line_reg.pkl")#保存模型
my_model_loaded = joblib.load("line_reg.pkl")#加载模型
微调的一种方法是手工调整超参数,直到找到一个好的超参数组合。这么做的话会花费很长的时间,还会因为自己的疏忽漏失某种合适的组合。
这里我们使用sklearn中的GridSearchCV来做这项搜索工作,我们这样做是告诉GridSearchCV要试验有哪些超参数,要试验什么值,GridSearchCV能用交叉验证试验所有可能的超参数值得组合。
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, scoring='neg_mean_squared_error', return_train_score=True)
grid_search.fit(housing_prepare, housing_label)
这段代码的含义是,param_grid告诉sklearn首先评估所有的列的第一个dict中的n_estimators和max_features的34=12种组合,然后尝试第二个dict中超参数的23=6种组合,这次会将超参数bootstrap设为False而不是True。总得来说,网格搜索会探索12+6=18种RandomForestRegressor的超参数组合,会训练每个模型五次(用的是五折交叉验证)。训练总共要花费18*5=90轮。获得下面的最佳组合。
grid_search.best_params_
grid_search.best_estimator_
也可以得到估计值:
cvres = grid_search.cv_results_
cvres.keys()
for mean_score, params in zip(cvrs["mean_test_score"], cvrs["params"]):
print(np.sqrt(-mean_score), params
当搜索相对较少的组合的时候,网格搜索其实还可以。但是当超参数的搜索空间很大的时候,最好使用RandomizedSearchCV。这个类的使用方法和GridSearchCV很相似,但它不是尝试所有可能的组合,而是通过选择每个超参数的一个随机值的特定数量的随机组合。
他有两个优点:(1)如果你让随机搜索运行,比如1000次,它会搜索每个超参数的1000个不同的值(而不是像网格搜索那样,只搜索每个超参数的几个值)。(2)我们可以方便地通过设定搜索次数,控制超参数搜索的计算量。
分析最佳模型及其误差
另一种微调系统的方法是将表现好的模型组合起来。组合之后的性能通常要比单独的模型要好(就像随机森林要比单独的决策树要好),特别是当单独模型的误差类别不同的时候。
通过分析最佳模型,常常可以获得对问题更深的了解。比如,RandomForestRegressor可以指出每个属性对于做出准确预测的相对重要性:
feature_importances = grid_search.best_estimator_.feature_importances_
将重要性分数和属性名放在一起:(有了这个信息,你就可以丢弃一些不那么重要的参数,比如显然只要一个分类ocean_proximity就够了,所以可以丢弃掉其它的)。
extra_attribs = ["room_per_hhold", "pop_per_hhold", "bedrooms_per_room"]
cat_encoder = full_pipeline.named_transformers_["cat"]
cat_one_hot_attribs = list(cat_encoder.categories_[0])
print(num_attribs)
print(extra_attribs)
print(cat_one_hot_attribs)
attributes = num_attribs + extra_attribs + cat_one_hot_attribs
sorted(zip(feature_importances, attributes), reverse=True)
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_prepare = full_pipeline.transform(X_test)
final_predictions = final_model.predict(X_test_prepare)
final_mse = mean_squared_error(Y_test, final_predictions)
final_rmse = np.sqrt(final_mse)
print(final_mse)
print(final_rmse)
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)))
ps:需要数据集可以私聊我!!!