机器学习(二)

端到端的机器学习项目

Posted by Gavin on March 13, 2019

不离不弃

芳龄永继

概述

第二章假设我们将亲自实践一个端到端的机器学习项目,体会真正的机器学习工程项目的过程。


实验流程

实验数据

StatLib 库中选择了加州住房价格的数据集,该数据集基于1990年加州人口普查的数据。数据集下载地址:Dataset

下载并预览数据

下载并解压 housing.tgz

预览数据

每一行代表一个区域,每个区域有10个相关属性: longitude, latitude, housing_median_age, total_rooms, total_bed rooms, population, households, median_income, median_house_value, ocean_proximity

读取并初步分析数据

  1. 读取数据
  2. 查看数据结构和描述

    可知大部分属性是float64类型的,ocean_proximity则是object,该数据集中包含20640个实例,total_bedrooms只有20433个实例,即207个区域缺失了这个特征
  3. 查看数据基本情况
    由上一步可知,ocean_proximity则是object,预览数据时可知,其是文本类型数据,查看其存在多少种分类

    可知其存在5种分类
    绘制各属性的频数分布直方图观察数据分布情况

    发现房价、房龄两个属性被设置了上限,大量超过限制的实例被压缩到末尾区间内,以及部分属性重尾(右侧延伸大于其左侧延伸),之后需要注意这些问题
import pandas as pd
import matplotlib.pyplot as plt

def load_housing_data(housing_path):
	return pd.read_csv(housing_path)

if __name__ == '__main__':
	housing = load_housing_data('housing.csv')
	housing.head()
	housing.info()
	housing['ocean_proximity'].value_counts()
	housing.describe()

	housing.hist(bins=50, figsize=(20,15))
	plt.savefig('housing_distribution.png')

创建测试集

选取数据集的20%作为测试集,首先我们为排除人为影响,随机选取数据的20%作为测试集,其分布如下
思索一下,发现问题似乎没有这么简单,随机抽样固然排除了人为因素,但是容易造成抽样偏差,假设一个地区有70%的男生,30%的女生,完全随机抽样有可能全部抽出男生,那么此时测试集就不能代表整体了。因而我们为保证测试集具有代表性,会选择分层抽样。此例中我们按照地区收入中位数进行分层抽样:

  1. 首先观察收入中位数的分布,发现大部分人处于2~3w左右
  2. 收入数据为连续值,我们四舍五入对其取整,将其离散化
  3. 为减少离散值类数目,将大于5w的部分归于5w类目中
  4. 按此比例对原数据进行分层抽样
train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)

housing['income_cat'] = np.ceil(housing['median_income'] / 1.5)
housing['income_cat'].where(housing['income_cat'] < 5, 5.0, inplace=True)

spliter = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
for train_index, test_index in spliter.split(housing,housing['income_cat']):
	strat_train_set = housing.loc[train_index]
	strat_test_set = housing.loc[test_index]
for data_set in (strat_train_set, strat_test_set):
	data_set.drop(['income_cat'], axis=1 ,inplace=True)

PS:测试集的构建很重要,却往往被忽视,因此机器学习项目中需要注意

数据探索和可视化

首先将测试集放在一边,我们只能够在训练集上进行数据探索(如果数据量比较大,可以先提取出一个较小的探索集),这样才不会损坏训练集。

  1. 将地理数据可视化

    可见与加州地图轮廓类似,但也看不出什么了,因而改变alpha参数,观察实例分布密度

    可见有明显的高密度区域,就是湾区、洛杉矶等地。
  2. 将人口、房价信息可视化

    图中每个圆的半径代表人口数量,颜色代表房价高低(蓝色为低,红色为高),可见人口与房价的关系。
  3. 寻找特征之间的相关性
    查看各属性与房价之间的皮尔森相关性系数

    相关性从-1~1。越接近1,就有越强的正相关;越接近-1,就有越强的负相关。从图中可知,一个地区收入中位数越高,房价越高;而越往北,房价越低。而与房价相关性最高的特征是收入。
  4. 绘制收入、房价散点图
    二者相关性的确很强,但是50w以上的部分是一条直线,因为我们将50w以上的实例全部归类于50w这一类了。仔细观察,发现45w、35w、23w附近貌似也有这样的直线,值得注意。
  5. 尝试不同属性的组合
    比如目前知道每个地区的房间数,但并没有什么用,如果知道了每个家庭的房间数,可能就大大不同了,因此可以对属性进行组合。

    发现每个家庭的房间数、每个房间的卧室数与房价也很有相关性

     housing = strat_train_set.copy()
     housing.plot(kind='scatter', x='longitude', y='latitude')
     plt.savefig('gregrophy.png')
     housing.plot(kind='scatter', x='longitude', y='latitude', alpha=0.1)
     plt.savefig('gregrophy_more.png')
    
     fig = plt.scatter(x=housing['longitude'], y=housing['latitude'], alpha=0.4, \
         s=housing['population']/100, label='population', \
         c=housing['median_house_value'], cmap=plt.get_cmap('jet'))
     plt.colorbar(fig)
     plt.legend()
     plt.savefig('gregrophy_population_value.png')
    
     corr_matrix = housing.corr()
     corr_matrix['median_house_value'].sort_values(ascending=False)
    
     housing.plot(kind='scatter', x='median_income', y='median_house_value', alpha=0.1)
     plt.savefig('income_value.png')
    
     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)
    

数据准备

  1. 准备干净的数据集,将预测器与标签分开
  2. 数据清洗
    • 放弃这些相应的地区
    • 放弃这个属性
    • 将缺失的值设为某个值(0、平均数或中位数等)
      我选择用中位数填补缺失值

      填补之后,可见数据集已经没有缺失部分了
     housing = strat_train_set.drop('median_house_value', axis=1)
     housing_labels = strat_train_set['median_house_value'].copy()
    
     imputer = Imputer(strategy='median')
     housing_num = housing.drop('ocean_proximity', axis=1)
     imputer.fit(housing_num)
     imputer.statistics_
     X = imputer.transform(housing_num)
     housing_tr = pd.DataFrame(X, columns=housing_num.columns)
    
  3. 处理文本与分类属性
    之前ocean_proximity为文本属性,无法进行数值计算,因此我们需要将其转化为数字,但是此时算法会将其理解为纯数值进行运算,这是不对的,我们的原始数据是分类数据,是类别,不能用于加减乘除等运算,因此我们需要将其转换成独热编码。

     encoder = LabelEncoder()
     housing_cat = housing['ocean_proximity']
     housing_cat_encoded = encoder.fit_transform(housing_cat)
    
     encoder = OneHotEncoder()
     housing_cat_1hot = encoder.fit_transform(housing_cat_encoded.reshape(-1,1))
    
     encoder = LabelBinarizer()
     housing_cat_1hot = encoder.fit_transform(housing_cat)
    
  4. 自定义转换器
    之前讨论过组合属性形成新的属性,我们也希望可以引入新的属性,但是又希望这个过程可以与Scikit-Learn的流水线无缝衔接,因此可以自定义转换器。所需要的只是新创建一个类,应用fit()、transform()、fit_transform()就可以了。如果添加TransformerMixin作为基类,可以直接获得最后一个方法;添加BaseEstimator作为基类,则可以获得两个自动调整超参数的方法get_params()和set_params()。
    增加前:
    增加后:
    新增加了每间房房间数量和每户家庭人口数两个组合属性

     class CombinedAttributesAdder(BaseEstimator, TransformerMixin):
     """docstring for CombinedAttributesAdder"""
     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, 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]
    
  5. 转换流水线
    许多数据转换需要正确的步骤来运行,因此需要设置流水线工作。我仿照书上写这段代码时,发现由于版本更新,这里失效了:

    经检查是LabelBinarizer更新后只接收两个参数,因此重写了一个自己的MyLabelBinarizer

     class MyLabelBinarizer(BaseEstimator, TransformerMixin):
     """docstring for DataFrameSelector"""
     def __init__(self):
         self.encoder = LabelBinarizer()
     def fit(self, X, y=None):
         return encoder.fit(X)
     def transform(self, X, y=None):
         return encoder.transform(X)
    


    准备数据成功

选择和训练模型

  1. 培训和评估训练集
    使用线性回归模型,看看预测情况如何

     #train the model
     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: ", lin_reg.predict(some_data_prepared))
     print("Labels: ", list(some_labels))
     housing_predictions = lin_reg.predict(housing_prepared)
     lin_mse = mean_squared_error(housing_labels, housing_predictions)
     lin_rmse = np.sqrt(lin_mse)
    


    部分预测结果,效果不尽如人意,利用RMSE进行评估

    效果确实不行!

    使用决策树模型,看看情况如何

    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)
    


    情况喜人!!但是转念一想,100%正确,这不是过拟合了吧?进行模型验证评估!!

  2. 验证模型 选择交叉验证方法验证模型

    效果真的烂,保险起见,检查一下线性回归模型

    也很差劲,换模型!!

     #valid models
     tree_scores = cross_val_score(tree_reg, housing_prepared, housing_labels, scoring='neg_mean_squared_error', cv=10)
     tree_rmse_scores = np.sqrt(-tree_scores)
     display_scores(tree_rmse_scores)
    
     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)
    
  3. 使用随机森林
    情况烂到家了,理论上来说,不应该,选其他模型或者调参数
  4. 调参
    利用网格搜索使用不同参数组合,找出最好参数结果

    查看整体情况

    此时随机森林的最佳参数是max_features为6,n_estimators为30时,RMSE=50019

     #improve the models
     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_
     cvres = grid_search.cv_results_
     for mean_score, params in zip(cvres['mean_test_score'], cvres['params']):
         print(np.sqrt(-mean_score), params)
    
  5. 分析最佳模型及其错误
    查看每个特征的重要性


    可见收入是最重要的特征

     #analyse the model
     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)
    
  6. 通过测试集评估系统

    最终误差可以接受

     #final model
     final_model = grid_search.best_estimator_
    
     X_test = strat_test_set.drop('median_house_value', axis=1)
     y_teat = strat_test_set['median_house_value'].copy()
     X_test_prepared = full_pipeline.fit_transform(X_test)
     final_predictons = final_model.predict(X_test_prepared)
     final_mse = mean_squared_error(y_teat, final_predictons)
     final_rmse = np.sqrt(final_mse)
    
  7. 启动、监控和维护系统

后记

性能指标

回归问题的典型性能衡量指标是均方根误差(RMSE),它测量的是预测过程中预测错误的标准偏差。

当数据离群点很多时,可以考虑使用平均绝对误差(MAE

特征缩放

如果输入的数据有较大的比例差异,可以考虑对特征数值进行缩放,通常有两种方法:最大-最小缩放标准化
最大-最小缩放:归一化,将数值缩放到0~1之间,实现方法是将值减去最小值并除以最大值和最小值的差
标准化:首先减去平均值,再除以方差